Skip to content

Commit

Permalink
Rely on Rack::Session stores API for more compatibility across the Ru…
Browse files Browse the repository at this point in the history
…by world.
  • Loading branch information
josevalim committed Oct 3, 2010
1 parent 5836af8 commit 50215f9
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 346 deletions.
1 change: 1 addition & 0 deletions Gemfile
Expand Up @@ -6,6 +6,7 @@ else
gem "arel", :git => "git://github.com/rails/arel.git"
end

gem "rack", :git => "git://github.com/rack/rack.git"
gem "rails", :path => File.dirname(__FILE__)

gem "rake", ">= 0.8.7"
Expand Down
6 changes: 3 additions & 3 deletions actionpack/CHANGELOG
@@ -1,12 +1,12 @@
*Rails 3.1.0 (unreleased)*

* Rely on Rack::Session stores API for more compatibility across the Ruby world. This is backwards incompatible since Rack::Session expects #get_session to accept 4 arguments and requires #destroy_session instead of simply #destroy. [José Valim]

* file_field automatically adds :multipart => true to the enclosing form. [Santiago Pastorino]

* Renames csrf_meta_tag -> csrf_meta_tags, and aliases csrf_meta_tag for backwards compatibility. [fxn]

* Add Rack::Cache to the default stack. Create a Rails store that delegates to the Rails cache, so by default, whatever caching layer you are using will be used
for HTTP caching. Note that Rack::Cache will be used if you use #expires_in, #fresh_when or #stale with :public => true. Otherwise, the caching rules will apply
to the browser only.
* Add Rack::Cache to the default stack. Create a Rails store that delegates to the Rails cache, so by default, whatever caching layer you are using will be used for HTTP caching. Note that Rack::Cache will be used if you use #expires_in, #fresh_when or #stale with :public => true. Otherwise, the caching rules will apply to the browser only. [Yehuda Katz, Carl Lerche]

*Rails 3.0.0 (August 29, 2010)*

Expand Down
8 changes: 5 additions & 3 deletions actionpack/lib/action_controller/test_case.rb
Expand Up @@ -187,15 +187,17 @@ def recycle!
end
end

class TestSession < ActionDispatch::Session::AbstractStore::SessionHash #:nodoc:
DEFAULT_OPTIONS = ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS
class TestSession < Rack::Session::Abstract::SessionHash #:nodoc:
DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS

def initialize(session = {})
replace(session.stringify_keys)
@loaded = true
end

def exists?; true; end
def exists?
true
end
end

# Superclass for ActionController functional tests. Functional tests allow you to
Expand Down
5 changes: 0 additions & 5 deletions actionpack/lib/action_dispatch/http/url.rb
Expand Up @@ -18,11 +18,6 @@ def protocol
@protocol ||= ssl? ? 'https://' : 'http://'
end

# Is this an SSL request?
def ssl?
@ssl ||= @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
end

# Returns the \host for this request, such as "example.com".
def raw_host_with_port
if forwarded = env["HTTP_X_FORWARDED_HOST"]
Expand Down
280 changes: 49 additions & 231 deletions actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
@@ -1,5 +1,6 @@
require 'rack/utils'
require 'rack/request'
require 'rack/session/abstract/id'
require 'action_dispatch/middleware/cookies'
require 'active_support/core_ext/object/blank'

Expand All @@ -8,252 +9,69 @@ module Session
class SessionRestoreError < StandardError #:nodoc:
end

class AbstractStore
ENV_SESSION_KEY = 'rack.session'.freeze
ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze

# thin wrapper around Hash that allows us to lazily
# load session id into session_options
class OptionsHash < Hash
def initialize(by, env, default_options)
@by = by
@env = env
@session_id_loaded = false
merge!(default_options)
end

def [](key)
if key == :id
load_session_id! unless key?(:id) || has_session_id?
end
super
end

private

def has_session_id?
@session_id_loaded
end

def load_session_id!
self[:id] = @by.send(:extract_session_id, @env)
@session_id_loaded = true
end
end

class SessionHash < Hash
def initialize(by, env)
super()
@by = by
@env = env
@loaded = false
end

def [](key)
load_for_read!
super(key.to_s)
end

def has_key?(key)
load_for_read!
super(key.to_s)
end

def []=(key, value)
load_for_write!
super(key.to_s, value)
end

def clear
load_for_write!
super
end

def to_hash
load_for_read!
h = {}.replace(self)
h.delete_if { |k,v| v.nil? }
h
end

def update(hash)
load_for_write!
super(hash.stringify_keys)
end

def delete(key)
load_for_write!
super(key.to_s)
end

def inspect
load_for_read!
super
end

def exists?
return @exists if instance_variable_defined?(:@exists)
@exists = @by.send(:exists?, @env)
end

def loaded?
@loaded
end

def destroy
clear
@by.send(:destroy, @env) if defined?(@by) && @by
@env[ENV_SESSION_OPTIONS_KEY][:id] = nil if defined?(@env) && @env && @env[ENV_SESSION_OPTIONS_KEY]
@loaded = false
end

private

def load_for_read!
load! if !loaded? && exists?
end

def load_for_write!
load! unless loaded?
end

def load!
id, session = @by.send(:load_session, @env)
@env[ENV_SESSION_OPTIONS_KEY][:id] = id
replace(session.stringify_keys)
@loaded = true
end

module DestroyableSession
def destroy
clear
options = @env[Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY] if @env
options ||= {}
@by.send(:destroy_session, @env, options[:id], options) if @by
options[:id] = nil
@loaded = false
end
end

DEFAULT_OPTIONS = {
:key => '_session_id',
:path => '/',
:domain => nil,
:expire_after => nil,
:secure => false,
:httponly => true,
:cookie_only => true
}
::Rack::Session::Abstract::SessionHash.send :include, DestroyableSession

module Compatibility
def initialize(app, options = {})
@app = app
@default_options = DEFAULT_OPTIONS.merge(options)
@key = @default_options.delete(:key).freeze
@cookie_only = @default_options.delete(:cookie_only)
ensure_session_key!
options[:key] ||= '_session_id'
super
end

def call(env)
prepare!(env)
response = @app.call(env)

session_data = env[ENV_SESSION_KEY]
options = env[ENV_SESSION_OPTIONS_KEY]

if !session_data.is_a?(AbstractStore::SessionHash) || session_data.loaded? || options[:expire_after]
request = ActionDispatch::Request.new(env)

return response if (options[:secure] && !request.ssl?)

session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.loaded?

sid = options[:id] || generate_sid
session_data = session_data.to_hash

value = set_session(env, sid, session_data)
return response unless value

cookie = { :value => value }
if options[:expire_after]
cookie[:expires] = Time.now + options.delete(:expire_after)
end

set_cookie(request, cookie.merge!(options))
end

response
def generate_sid
ActiveSupport::SecureRandom.hex(16)
end
end

private

def prepare!(env)
env[ENV_SESSION_KEY] = SessionHash.new(self, env)
env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options)
end

def generate_sid
ActiveSupport::SecureRandom.hex(16)
end

def set_cookie(request, options)
if request.cookie_jar[@key] != options[:value] || !options[:expires].nil?
request.cookie_jar[@key] = options
end
end

def load_session(env)
stale_session_check! do
sid = current_session_id(env)
sid, session = get_session(env, sid)
[sid, session]
end
end

def extract_session_id(env)
stale_session_check! do
request = ActionDispatch::Request.new(env)
sid = request.cookies[@key]
sid ||= request.params[@key] unless @cookie_only
sid
end
end

def current_session_id(env)
env[ENV_SESSION_OPTIONS_KEY][:id]
end
module StaleSessionCheck
def load_session(env)
stale_session_check! { super }
end

def ensure_session_key!
if @key.blank?
raise ArgumentError, 'A key is required to write a ' +
'cookie containing the session data. Use ' +
'config.session_store SESSION_STORE, { :key => ' +
'"_myapp_session" } in config/application.rb'
end
end
def extract_session_id(env)
stale_session_check! { super }
end

def stale_session_check!
yield
rescue ArgumentError => argument_error
if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
begin
# Note that the regexp does not allow $1 to end with a ':'
$1.constantize
rescue LoadError, NameError => const_error
raise ActionDispatch::Session::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n"
end
retry
else
raise
def stale_session_check!
yield
rescue ArgumentError => argument_error
if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
begin
# Note that the regexp does not allow $1 to end with a ':'
$1.constantize
rescue LoadError, NameError => const_error
raise ActionDispatch::Session::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n"
end
retry
else
raise
end
end
end

def exists?(env)
current_session_id(env).present?
end

def get_session(env, sid)
raise '#get_session needs to be implemented.'
end
class AbstractStore < Rack::Session::Abstract::ID
include Compatibility
include StaleSessionCheck

def set_session(env, sid, session_data)
raise '#set_session needs to be implemented and should return ' <<
'the value to be stored in the cookie (usually the sid)'
end
def destroy_session(env, sid, options)
ActiveSupport::Deprecation.warn "Implementing #destroy in session stores is deprecated. " <<
"Please implement destroy_session(env, session_id, options) instead."
destroy(env)
end

def destroy(env)
raise '#destroy needs to be implemented.'
end
def destroy(env)
raise '#destroy needs to be implemented.'
end
end
end
end

6 comments on commit 50215f9

@rolfb
Copy link
Contributor

@rolfb rolfb commented on 50215f9 Oct 4, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This breaks.

Uninitialized constant Rack::Session::Abstract::SessionHash (NameError)

@josevalim
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are using Rails master, you need to link to Rack git repo form your Gemfile.

@rolfb
Copy link
Contributor

@rolfb rolfb commented on 50215f9 Oct 4, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I was too quick to comment.
About to update my Gemfile to use previous commit, and noticed I hadn't specified 3-0-stable as the branch.

Sorry about that.

@dtrasbo
Copy link
Contributor

@dtrasbo dtrasbo commented on 50215f9 Oct 8, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this commit remove the ssl? method from ActionDispatch::Http::Url? Other methods in there seems to depend on it.

@josevalim
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't fear! ssl? is now in Rack!

@dtrasbo
Copy link
Contributor

@dtrasbo dtrasbo commented on 50215f9 Oct 8, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok. I'm asking because of this ticket: https://rails.lighthouseapp.com/projects/8994/tickets/5750-requestssl-should-reflect-rackurl_scheme

I've already marked it as invalid, because as I said there might be a perfectly good reason. :)

Please sign in to comment.