1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Rely on Rack::Session stores API for more compatibility across the Ruby world.

This commit is contained in:
José Valim 2010-09-20 10:18:44 +02:00
parent 5836af8f8b
commit 50215f9525
9 changed files with 90 additions and 346 deletions

View file

@ -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"

View file

@ -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)*

View file

@ -187,15 +187,17 @@ module ActionController
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

View file

@ -18,11 +18,6 @@ module ActionDispatch
@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"]

View file

@ -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'
@ -8,252 +9,69 @@ module ActionDispatch
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
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
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
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)
def generate_sid
ActiveSupport::SecureRandom.hex(16)
end
end
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
module StaleSessionCheck
def load_session(env)
stale_session_check! { super }
end
private
def extract_session_id(env)
stale_session_check! { super }
end
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
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 load_session(env)
stale_session_check! do
sid = current_session_id(env)
sid, session = get_session(env, sid)
[sid, session]
end
end
class AbstractStore < Rack::Session::Abstract::ID
include Compatibility
include StaleSessionCheck
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 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 current_session_id(env)
env[ENV_SESSION_OPTIONS_KEY][:id]
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 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
def exists?(env)
current_session_id(env).present?
end
def get_session(env, sid)
raise '#get_session needs to be implemented.'
end
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(env)
raise '#destroy needs to be implemented.'
end
def destroy(env)
raise '#destroy needs to be implemented.'
end
end
end
end

View file

@ -1,5 +1,7 @@
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/object/blank'
require 'action_dispatch/middleware/session/abstract_store'
require 'rack/session/cookie'
module ActionDispatch
module Session
@ -38,58 +40,32 @@ module ActionDispatch
# "rake secret" and set the key in config/initializers/secret_token.rb.
#
# Note that changing digest or secret invalidates all existing sessions!
class CookieStore < AbstractStore
def initialize(app, options = {})
super(app, options.merge!(:cookie_only => true))
freeze
end
class CookieStore < Rack::Session::Cookie
include Compatibility
include StaleSessionCheck
private
def load_session(env)
data = unpacked_cookie_data(env)
data = persistent_session_id!(data)
[data["session_id"], data]
end
def extract_session_id(env)
if data = unpacked_cookie_data(env)
data["session_id"]
else
nil
end
end
def unpacked_cookie_data(env)
env["action_dispatch.request.unsigned_session_cookie"] ||= begin
stale_session_check! do
request = ActionDispatch::Request.new(env)
if data = request.cookie_jar.signed[@key]
data.stringify_keys!
end
data || {}
def unpacked_cookie_data(env)
env["action_dispatch.request.unsigned_session_cookie"] ||= begin
stale_session_check! do
request = ActionDispatch::Request.new(env)
if data = request.cookie_jar.signed[@key]
data.stringify_keys!
end
data || {}
end
end
end
def set_cookie(request, options)
request.cookie_jar.signed[@key] = options
end
def set_session(env, sid, session_data, options)
persistent_session_id!(session_data, sid)
end
def set_session(env, sid, session_data)
persistent_session_id!(session_data, sid)
end
def destroy(env)
# session data is stored on client; nothing to do here
end
def persistent_session_id!(data, sid=nil)
data ||= {}
data["session_id"] ||= sid || generate_sid
data
end
def set_cookie(env, session_id, cookie)
request = ActionDispatch::Request.new(env)
request.cookie_jar.signed[@key] = cookie
end
end
end
end

View file

@ -1,56 +1,17 @@
require 'action_dispatch/middleware/session/abstract_store'
require 'rack/session/memcache'
module ActionDispatch
module Session
class MemCacheStore < AbstractStore
class MemCacheStore < Rack::Session::Memcache
include Compatibility
include StaleSessionCheck
def initialize(app, options = {})
require 'memcache'
# Support old :expires option
options[:expire_after] ||= options[:expires]
super
@default_options = {
:namespace => 'rack:session',
:memcache_server => 'localhost:11211'
}.merge(@default_options)
@pool = options[:cache] || MemCache.new(@default_options[:memcache_server], @default_options)
unless @pool.servers.any? { |s| s.alive? }
raise "#{self} unable to find server during initialization."
end
@mutex = Mutex.new
super
end
private
def get_session(env, sid)
sid ||= generate_sid
begin
session = @pool.get(sid) || {}
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
session = {}
end
[sid, session]
end
def set_session(env, sid, session_data)
options = env['rack.session.options']
expiry = options[:expire_after] || 0
@pool.set(sid, session_data, expiry)
sid
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
false
end
def destroy(env)
if sid = current_session_id(env)
@pool.delete(sid)
end
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
false
end
end
end
end

View file

@ -53,18 +53,6 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def rescue_action(e) raise end
end
def test_raises_argument_error_if_missing_session_key
assert_raise(ArgumentError, nil.inspect) {
ActionDispatch::Session::CookieStore.new(nil,
:key => nil, :secret => SessionSecret)
}
assert_raise(ArgumentError, ''.inspect) {
ActionDispatch::Session::CookieStore.new(nil,
:key => '', :secret => SessionSecret)
}
end
def test_setting_session_value
with_test_route_set do
get '/set_session_value'

View file

@ -288,6 +288,7 @@ module ActiveRecord
self.session_class = Session
SESSION_RECORD_KEY = 'rack.session.record'
ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY
private
def get_session(env, sid)
@ -299,7 +300,7 @@ module ActiveRecord
end
end
def set_session(env, sid, session_data)
def set_session(env, sid, session_data, options)
Base.silence do
record = get_session_model(env, sid)
record.data = session_data
@ -316,12 +317,14 @@ module ActiveRecord
sid
end
def destroy(env)
def destroy_session(env, session_id, options)
if sid = current_session_id(env)
Base.silence do
get_session_model(env, sid).destroy
end
end
generate_sid unless options[:drop]
end
def get_session_model(env, sid)