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:
parent
5836af8f8b
commit
50215f9525
9 changed files with 90 additions and 346 deletions
1
Gemfile
1
Gemfile
|
@ -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"
|
||||
|
|
|
@ -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)*
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue