aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoshua Peek <josh@joshpeek.com>2008-12-15 16:33:31 -0600
committerJoshua Peek <josh@joshpeek.com>2008-12-15 16:33:31 -0600
commited708307137c811d14e5fd2cb4ea550add381a82 (patch)
tree31cb7df0a489bb4bbb0a9bc9edb24a70a869a0d1
parente8c1915416579a3840573ca2c80822d96cb31823 (diff)
downloadrails-ed708307137c811d14e5fd2cb4ea550add381a82.tar.gz
rails-ed708307137c811d14e5fd2cb4ea550add381a82.tar.bz2
rails-ed708307137c811d14e5fd2cb4ea550add381a82.zip
Switch to Rack based session stores.
-rw-r--r--actionpack/lib/action_controller.rb15
-rw-r--r--actionpack/lib/action_controller/base.rb12
-rw-r--r--actionpack/lib/action_controller/cgi_ext.rb1
-rw-r--r--actionpack/lib/action_controller/cgi_ext/session.rb53
-rw-r--r--actionpack/lib/action_controller/cgi_process.rb2
-rw-r--r--actionpack/lib/action_controller/dispatcher.rb8
-rw-r--r--actionpack/lib/action_controller/integration.rb4
-rw-r--r--actionpack/lib/action_controller/middleware_stack.rb25
-rw-r--r--actionpack/lib/action_controller/rack_process.rb139
-rw-r--r--actionpack/lib/action_controller/session/abstract_store.rb129
-rw-r--r--actionpack/lib/action_controller/session/active_record_store.rb350
-rw-r--r--actionpack/lib/action_controller/session/cookie_store.rb356
-rwxr-xr-xactionpack/lib/action_controller/session/drb_server.rb32
-rw-r--r--actionpack/lib/action_controller/session/drb_store.rb35
-rw-r--r--actionpack/lib/action_controller/session/mem_cache_store.rb119
-rw-r--r--actionpack/lib/action_controller/session_management.rb174
-rw-r--r--actionpack/test/abstract_unit.rb2
-rw-r--r--actionpack/test/activerecord/active_record_store_test.rb202
-rw-r--r--actionpack/test/controller/integration_test.rb2
-rw-r--r--actionpack/test/controller/integration_upload_test.rb2
-rw-r--r--actionpack/test/controller/rack_test.rb26
-rw-r--r--actionpack/test/controller/session/cookie_store_test.rb348
-rw-r--r--actionpack/test/controller/session/mem_cache_store_test.rb207
-rw-r--r--actionpack/test/controller/session_fixation_test.rb168
-rw-r--r--actionpack/test/controller/session_management_test.rb178
-rw-r--r--actionpack/test/controller/webservice_test.rb2
-rw-r--r--activerecord/lib/active_record.rb1
-rw-r--r--activerecord/lib/active_record/session_store.rb319
-rw-r--r--railties/lib/initializer.rb12
-rw-r--r--railties/lib/tasks/databases.rake2
-rw-r--r--railties/test/console_app_test.rb1
31 files changed, 1127 insertions, 1799 deletions
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index abc404afe7..c170e4dd2a 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -89,18 +89,15 @@ module ActionController
autoload :Headers, 'action_controller/headers'
end
- # DEPRECATE: Remove CGI support
- autoload :CgiRequest, 'action_controller/cgi_process'
- autoload :CGIHandler, 'action_controller/cgi_process'
-end
-
-class CGI
- class Session
- autoload :ActiveRecordStore, 'action_controller/session/active_record_store'
+ module Session
+ autoload :AbstractStore, 'action_controller/session/abstract_store'
autoload :CookieStore, 'action_controller/session/cookie_store'
- autoload :DRbStore, 'action_controller/session/drb_store'
autoload :MemCacheStore, 'action_controller/session/mem_cache_store'
end
+
+ # DEPRECATE: Remove CGI support
+ autoload :CgiRequest, 'action_controller/cgi_process'
+ autoload :CGIHandler, 'action_controller/cgi_process'
end
autoload :Mime, 'action_controller/mime_type'
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index 13f2e9072e..0b32da55d5 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -164,8 +164,8 @@ module ActionController #:nodoc:
#
# Other options for session storage are:
#
- # * ActiveRecordStore - Sessions are stored in your database, which works better than PStore with multiple app servers and,
- # unlike CookieStore, hides your session contents from the user. To use ActiveRecordStore, set
+ # * ActiveRecord::SessionStore - Sessions are stored in your database, which works better than PStore with multiple app servers and,
+ # unlike CookieStore, hides your session contents from the user. To use ActiveRecord::SessionStore, set
#
# config.action_controller.session_store = :active_record_store
#
@@ -1216,7 +1216,6 @@ module ActionController #:nodoc:
def log_processing
if logger && logger.info?
log_processing_for_request_id
- log_processing_for_session_id
log_processing_for_parameters
end
end
@@ -1229,13 +1228,6 @@ module ActionController #:nodoc:
logger.info(request_id)
end
- def log_processing_for_session_id
- if @_session && @_session.respond_to?(:session_id) && @_session.respond_to?(:dbman) &&
- !@_session.dbman.is_a?(CGI::Session::CookieStore)
- logger.info " Session ID: #{@_session.session_id}"
- end
- end
-
def log_processing_for_parameters
parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup
parameters = parameters.except!(:controller, :action, :format, :_method)
diff --git a/actionpack/lib/action_controller/cgi_ext.rb b/actionpack/lib/action_controller/cgi_ext.rb
index f3b8c08d8f..406b6f06d6 100644
--- a/actionpack/lib/action_controller/cgi_ext.rb
+++ b/actionpack/lib/action_controller/cgi_ext.rb
@@ -1,7 +1,6 @@
require 'action_controller/cgi_ext/stdinput'
require 'action_controller/cgi_ext/query_extension'
require 'action_controller/cgi_ext/cookie'
-require 'action_controller/cgi_ext/session'
class CGI #:nodoc:
include ActionController::CgiExt::Stdinput
diff --git a/actionpack/lib/action_controller/cgi_ext/session.rb b/actionpack/lib/action_controller/cgi_ext/session.rb
deleted file mode 100644
index d3f85e3705..0000000000
--- a/actionpack/lib/action_controller/cgi_ext/session.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-require 'digest/md5'
-require 'cgi/session'
-require 'cgi/session/pstore'
-
-class CGI #:nodoc:
- # * Expose the CGI instance to session stores.
- # * Don't require 'digest/md5' whenever a new session id is generated.
- class Session #:nodoc:
- def self.generate_unique_id(constant = nil)
- ActiveSupport::SecureRandom.hex(16)
- end
-
- # Make the CGI instance available to session stores.
- attr_reader :cgi
- attr_reader :dbman
- alias_method :initialize_without_cgi_reader, :initialize
- def initialize(cgi, options = {})
- @cgi = cgi
- initialize_without_cgi_reader(cgi, options)
- end
-
- private
- # Create a new session id.
- def create_new_id
- @new_session = true
- self.class.generate_unique_id
- end
-
- # * Don't require 'digest/md5' whenever a new session is started.
- class PStore #:nodoc:
- def initialize(session, option={})
- dir = option['tmpdir'] || Dir::tmpdir
- prefix = option['prefix'] || ''
- id = session.session_id
- md5 = Digest::MD5.hexdigest(id)[0,16]
- path = dir+"/"+prefix+md5
- path.untaint
- if File::exist?(path)
- @hash = nil
- else
- unless session.new_session
- raise CGI::Session::NoSession, "uninitialized session"
- end
- @hash = {}
- end
- @p = ::PStore.new(path)
- @p.transaction do |p|
- File.chmod(0600, p.path)
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_controller/cgi_process.rb b/actionpack/lib/action_controller/cgi_process.rb
index 5d6988e1b1..7e5e95e135 100644
--- a/actionpack/lib/action_controller/cgi_process.rb
+++ b/actionpack/lib/action_controller/cgi_process.rb
@@ -61,7 +61,7 @@ module ActionController #:nodoc:
class CgiRequest #:nodoc:
DEFAULT_SESSION_OPTIONS = {
- :database_manager => CGI::Session::CookieStore,
+ :database_manager => nil,
:prefix => "ruby_sess.",
:session_path => "/",
:session_key => "_session_id",
diff --git a/actionpack/lib/action_controller/dispatcher.rb b/actionpack/lib/action_controller/dispatcher.rb
index 203f6b1683..c9a9264b6d 100644
--- a/actionpack/lib/action_controller/dispatcher.rb
+++ b/actionpack/lib/action_controller/dispatcher.rb
@@ -45,8 +45,10 @@ module ActionController
end
cattr_accessor :middleware
- self.middleware = MiddlewareStack.new
- self.middleware.use "ActionController::Failsafe"
+ self.middleware = MiddlewareStack.new do |middleware|
+ middleware.use "ActionController::Failsafe"
+ middleware.use "ActionController::SessionManagement::Middleware"
+ end
include ActiveSupport::Callbacks
define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch
@@ -89,7 +91,7 @@ module ActionController
def _call(env)
@request = RackRequest.new(env)
- @response = RackResponse.new(@request)
+ @response = RackResponse.new
dispatch
end
diff --git a/actionpack/lib/action_controller/integration.rb b/actionpack/lib/action_controller/integration.rb
index 212a293da0..1b0543033b 100644
--- a/actionpack/lib/action_controller/integration.rb
+++ b/actionpack/lib/action_controller/integration.rb
@@ -489,8 +489,8 @@ EOF
# By default, a single session is automatically created for you, but you
# can use this method to open multiple sessions that ought to be tested
# simultaneously.
- def open_session
- application = ActionController::Dispatcher.new
+ def open_session(application = nil)
+ application ||= ActionController::Dispatcher.new
session = Integration::Session.new(application)
# delegate the fixture accessors back to the test instance
diff --git a/actionpack/lib/action_controller/middleware_stack.rb b/actionpack/lib/action_controller/middleware_stack.rb
index 1864bed23a..a6597a6fec 100644
--- a/actionpack/lib/action_controller/middleware_stack.rb
+++ b/actionpack/lib/action_controller/middleware_stack.rb
@@ -4,7 +4,12 @@ module ActionController
attr_reader :klass, :args, :block
def initialize(klass, *args, &block)
- @klass = klass.is_a?(Class) ? klass : klass.to_s.constantize
+ if klass.is_a?(Class)
+ @klass = klass
+ else
+ @klass = klass.to_s.constantize
+ end
+
@args = args
@block = block
end
@@ -21,18 +26,28 @@ module ActionController
end
def inspect
- str = @klass.to_s
- @args.each { |arg| str += ", #{arg.inspect}" }
+ str = klass.to_s
+ args.each { |arg| str += ", #{arg.inspect}" }
str
end
def build(app)
- klass.new(app, *args, &block)
+ if block
+ klass.new(app, *args, &block)
+ else
+ klass.new(app, *args)
+ end
end
end
+ def initialize(*args, &block)
+ super(*args)
+ block.call(self) if block_given?
+ end
+
def use(*args, &block)
- push(Middleware.new(*args, &block))
+ middleware = Middleware.new(*args, &block)
+ push(middleware)
end
def build(app)
diff --git a/actionpack/lib/action_controller/rack_process.rb b/actionpack/lib/action_controller/rack_process.rb
index 568f893c6c..e783839f34 100644
--- a/actionpack/lib/action_controller/rack_process.rb
+++ b/actionpack/lib/action_controller/rack_process.rb
@@ -3,24 +3,12 @@ require 'action_controller/cgi_ext'
module ActionController #:nodoc:
class RackRequest < AbstractRequest #:nodoc:
attr_accessor :session_options
- attr_reader :cgi
class SessionFixationAttempt < StandardError #:nodoc:
end
- DEFAULT_SESSION_OPTIONS = {
- :database_manager => CGI::Session::CookieStore, # store data in cookie
- :prefix => "ruby_sess.", # prefix session file names
- :session_path => "/", # available to all paths in app
- :session_key => "_session_id",
- :cookie_only => true,
- :session_http_only=> true
- }
-
- def initialize(env, session_options = DEFAULT_SESSION_OPTIONS)
- @session_options = session_options
+ def initialize(env)
@env = env
- @cgi = CGIWrapper.new(self)
super()
end
@@ -66,87 +54,25 @@ module ActionController #:nodoc:
@env['SERVER_SOFTWARE'].split("/").first
end
- def session
- unless defined?(@session)
- if @session_options == false
- @session = Hash.new
- else
- stale_session_check! do
- if cookie_only? && query_parameters[session_options_with_string_keys['session_key']]
- raise SessionFixationAttempt
- end
- case value = session_options_with_string_keys['new_session']
- when true
- @session = new_session
- when false
- begin
- @session = CGI::Session.new(@cgi, session_options_with_string_keys)
- # CGI::Session raises ArgumentError if 'new_session' == false
- # and no session cookie or query param is present.
- rescue ArgumentError
- @session = Hash.new
- end
- when nil
- @session = CGI::Session.new(@cgi, session_options_with_string_keys)
- else
- raise ArgumentError, "Invalid new_session option: #{value}"
- end
- @session['__valid_session']
- end
- end
- end
- @session
+ def session_options
+ @env['rack.session.options'] ||= {}
end
- def reset_session
- @session.delete if defined?(@session) && @session.is_a?(CGI::Session)
- @session = new_session
+ def session_options=(options)
+ @env['rack.session.options'] = options
end
- private
- # Delete an old session if it exists then create a new one.
- def new_session
- if @session_options == false
- Hash.new
- else
- CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => false)).delete rescue nil
- CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => true))
- end
- end
-
- def cookie_only?
- session_options_with_string_keys['cookie_only']
- 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 ActionController::SessionRestoreError, <<-end_msg
-Session contains objects whose class definition isn\'t available.
-Remember to require the classes for all objects kept in the session.
-(Original exception: #{const_error.message} [#{const_error.class}])
-end_msg
- end
-
- retry
- else
- raise
- end
- end
+ def session
+ @env['rack.session'] ||= {}
+ end
- def session_options_with_string_keys
- @session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).stringify_keys
- end
+ def reset_session
+ @env['rack.session'] = {}
+ end
end
class RackResponse < AbstractResponse #:nodoc:
- def initialize(request)
- @cgi = request.cgi
+ def initialize
@writer = lambda { |x| @body << x }
@block = nil
super()
@@ -247,49 +173,8 @@ end_msg
else cookies << cookie.to_s
end
- @cgi.output_cookies.each { |c| cookies << c.to_s } if @cgi.output_cookies
-
headers['Set-Cookie'] = [headers['Set-Cookie'], cookies].flatten.compact
end
end
end
-
- class CGIWrapper < ::CGI
- attr_reader :output_cookies
-
- def initialize(request, *args)
- @request = request
- @args = *args
- @input = request.body
-
- super *args
- end
-
- def params
- @params ||= @request.params
- end
-
- def cookies
- @request.cookies
- end
-
- def query_string
- @request.query_string
- end
-
- # Used to wrap the normal args variable used inside CGI.
- def args
- @args
- end
-
- # Used to wrap the normal env_table variable used inside CGI.
- def env_table
- @request.env
- end
-
- # Used to wrap the normal stdinput variable used inside CGI.
- def stdinput
- @input
- end
- end
end
diff --git a/actionpack/lib/action_controller/session/abstract_store.rb b/actionpack/lib/action_controller/session/abstract_store.rb
new file mode 100644
index 0000000000..b23bf062c4
--- /dev/null
+++ b/actionpack/lib/action_controller/session/abstract_store.rb
@@ -0,0 +1,129 @@
+require 'rack/utils'
+
+module ActionController
+ module Session
+ class AbstractStore
+ ENV_SESSION_KEY = 'rack.session'.freeze
+ ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze
+
+ HTTP_COOKIE = 'HTTP_COOKIE'.freeze
+ SET_COOKIE = 'Set-Cookie'.freeze
+
+ class SessionHash < Hash
+ def initialize(by, env)
+ @by = by
+ @env = env
+ @loaded = false
+ end
+
+ def id
+ load! unless @loaded
+ @id
+ end
+
+ def [](key)
+ load! unless @loaded
+ super
+ end
+
+ def []=(key, value)
+ load! unless @loaded
+ super
+ end
+
+ def to_hash
+ {}.replace(self)
+ end
+
+ private
+ def load!
+ @id, session = @by.send(:load_session, @env)
+ replace(session)
+ @loaded = true
+ end
+ end
+
+ DEFAULT_OPTIONS = {
+ :key => 'rack.session',
+ :path => '/',
+ :domain => nil,
+ :expire_after => nil,
+ :secure => false,
+ :httponly => true,
+ :cookie_only => true
+ }
+
+ def initialize(app, options = {})
+ @app = app
+ @default_options = DEFAULT_OPTIONS.merge(options)
+ @key = @default_options[:key]
+ @cookie_only = @default_options[:cookie_only]
+ end
+
+ def call(env)
+ session = SessionHash.new(self, env)
+ original_session = session.dup
+
+ env[ENV_SESSION_KEY] = session
+ env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
+
+ response = @app.call(env)
+
+ session = env[ENV_SESSION_KEY]
+ unless session == original_session
+ options = env[ENV_SESSION_OPTIONS_KEY]
+ sid = session.id
+
+ unless set_session(env, sid, session.to_hash)
+ return response
+ end
+
+ cookie = Rack::Utils.escape(@key) + '=' + Rack::Utils.escape(sid)
+ cookie << "; domain=#{options[:domain]}" if options[:domain]
+ cookie << "; path=#{options[:path]}" if options[:path]
+ if options[:expire_after]
+ expiry = Time.now + options[:expire_after]
+ cookie << "; expires=#{expiry.httpdate}"
+ end
+ cookie << "; Secure" if options[:secure]
+ cookie << "; HttpOnly" if options[:httponly]
+
+ headers = response[1]
+ case a = headers[SET_COOKIE]
+ when Array
+ a << cookie
+ when String
+ headers[SET_COOKIE] = [a, cookie]
+ when nil
+ headers[SET_COOKIE] = cookie
+ end
+ end
+
+ response
+ end
+
+ private
+ def generate_sid
+ ActiveSupport::SecureRandom.hex(16)
+ end
+
+ def load_session(env)
+ request = Rack::Request.new(env)
+ sid = request.cookies[@key]
+ unless @cookie_only
+ sid ||= request.params[@key]
+ end
+ sid, session = get_session(env, sid)
+ [sid, session]
+ 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.'
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/session/active_record_store.rb b/actionpack/lib/action_controller/session/active_record_store.rb
deleted file mode 100644
index fadf2a6b32..0000000000
--- a/actionpack/lib/action_controller/session/active_record_store.rb
+++ /dev/null
@@ -1,350 +0,0 @@
-require 'cgi'
-require 'cgi/session'
-require 'digest/md5'
-
-class CGI
- class Session
- attr_reader :data
-
- # Return this session's underlying Session instance. Useful for the DB-backed session stores.
- def model
- @dbman.model if @dbman
- end
-
-
- # A session store backed by an Active Record class. A default class is
- # provided, but any object duck-typing to an Active Record Session class
- # with text +session_id+ and +data+ attributes is sufficient.
- #
- # The default assumes a +sessions+ tables with columns:
- # +id+ (numeric primary key),
- # +session_id+ (text, or longtext if your session data exceeds 65K), and
- # +data+ (text or longtext; careful if your session data exceeds 65KB).
- # The +session_id+ column should always be indexed for speedy lookups.
- # Session data is marshaled to the +data+ column in Base64 format.
- # If the data you write is larger than the column's size limit,
- # ActionController::SessionOverflowError will be raised.
- #
- # You may configure the table name, primary key, and data column.
- # For example, at the end of <tt>config/environment.rb</tt>:
- # CGI::Session::ActiveRecordStore::Session.table_name = 'legacy_session_table'
- # CGI::Session::ActiveRecordStore::Session.primary_key = 'session_id'
- # CGI::Session::ActiveRecordStore::Session.data_column_name = 'legacy_session_data'
- # Note that setting the primary key to the +session_id+ frees you from
- # having a separate +id+ column if you don't want it. However, you must
- # set <tt>session.model.id = session.session_id</tt> by hand! A before filter
- # on ApplicationController is a good place.
- #
- # Since the default class is a simple Active Record, you get timestamps
- # for free if you add +created_at+ and +updated_at+ datetime columns to
- # the +sessions+ table, making periodic session expiration a snap.
- #
- # You may provide your own session class implementation, whether a
- # feature-packed Active Record or a bare-metal high-performance SQL
- # store, by setting
- # CGI::Session::ActiveRecordStore.session_class = MySessionClass
- # You must implement these methods:
- # self.find_by_session_id(session_id)
- # initialize(hash_of_session_id_and_data)
- # attr_reader :session_id
- # attr_accessor :data
- # save
- # destroy
- #
- # The example SqlBypass class is a generic SQL session store. You may
- # use it as a basis for high-performance database-specific stores.
- class ActiveRecordStore
- # The default Active Record class.
- class Session < ActiveRecord::Base
- ##
- # :singleton-method:
- # Customizable data column name. Defaults to 'data'.
- cattr_accessor :data_column_name
- self.data_column_name = 'data'
-
- before_save :marshal_data!
- before_save :raise_on_session_data_overflow!
-
- class << self
- # Don't try to reload ARStore::Session in dev mode.
- def reloadable? #:nodoc:
- false
- end
-
- def data_column_size_limit
- @data_column_size_limit ||= columns_hash[@@data_column_name].limit
- end
-
- # Hook to set up sessid compatibility.
- def find_by_session_id(session_id)
- setup_sessid_compatibility!
- find_by_session_id(session_id)
- end
-
- def marshal(data) ActiveSupport::Base64.encode64(Marshal.dump(data)) if data end
- def unmarshal(data) Marshal.load(ActiveSupport::Base64.decode64(data)) if data end
-
- def create_table!
- connection.execute <<-end_sql
- CREATE TABLE #{table_name} (
- id INTEGER PRIMARY KEY,
- #{connection.quote_column_name('session_id')} TEXT UNIQUE,
- #{connection.quote_column_name(@@data_column_name)} TEXT(255)
- )
- end_sql
- end
-
- def drop_table!
- connection.execute "DROP TABLE #{table_name}"
- end
-
- private
- # Compatibility with tables using sessid instead of session_id.
- def setup_sessid_compatibility!
- # Reset column info since it may be stale.
- reset_column_information
- if columns_hash['sessid']
- def self.find_by_session_id(*args)
- find_by_sessid(*args)
- end
-
- define_method(:session_id) { sessid }
- define_method(:session_id=) { |session_id| self.sessid = session_id }
- else
- def self.find_by_session_id(session_id)
- find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id]
- end
- end
- end
- end
-
- # Lazy-unmarshal session state.
- def data
- @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
- end
-
- attr_writer :data
-
- # Has the session been loaded yet?
- def loaded?
- !! @data
- end
-
- private
-
- def marshal_data!
- return false if !loaded?
- write_attribute(@@data_column_name, self.class.marshal(self.data))
- end
-
- # Ensures that the data about to be stored in the database is not
- # larger than the data storage column. Raises
- # ActionController::SessionOverflowError.
- def raise_on_session_data_overflow!
- return false if !loaded?
- limit = self.class.data_column_size_limit
- if loaded? and limit and read_attribute(@@data_column_name).size > limit
- raise ActionController::SessionOverflowError
- end
- end
- end
-
- # A barebones session store which duck-types with the default session
- # store but bypasses Active Record and issues SQL directly. This is
- # an example session model class meant as a basis for your own classes.
- #
- # The database connection, table name, and session id and data columns
- # are configurable class attributes. Marshaling and unmarshaling
- # are implemented as class methods that you may override. By default,
- # marshaling data is
- #
- # ActiveSupport::Base64.encode64(Marshal.dump(data))
- #
- # and unmarshaling data is
- #
- # Marshal.load(ActiveSupport::Base64.decode64(data))
- #
- # This marshaling behavior is intended to store the widest range of
- # binary session data in a +text+ column. For higher performance,
- # store in a +blob+ column instead and forgo the Base64 encoding.
- class SqlBypass
- ##
- # :singleton-method:
- # Use the ActiveRecord::Base.connection by default.
- cattr_accessor :connection
-
- ##
- # :singleton-method:
- # The table name defaults to 'sessions'.
- cattr_accessor :table_name
- @@table_name = 'sessions'
-
- ##
- # :singleton-method:
- # The session id field defaults to 'session_id'.
- cattr_accessor :session_id_column
- @@session_id_column = 'session_id'
-
- ##
- # :singleton-method:
- # The data field defaults to 'data'.
- cattr_accessor :data_column
- @@data_column = 'data'
-
- class << self
-
- def connection
- @@connection ||= ActiveRecord::Base.connection
- end
-
- # Look up a session by id and unmarshal its data if found.
- def find_by_session_id(session_id)
- if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
- new(:session_id => session_id, :marshaled_data => record['data'])
- end
- end
-
- def marshal(data) ActiveSupport::Base64.encode64(Marshal.dump(data)) if data end
- def unmarshal(data) Marshal.load(ActiveSupport::Base64.decode64(data)) if data end
-
- def create_table!
- @@connection.execute <<-end_sql
- CREATE TABLE #{table_name} (
- id INTEGER PRIMARY KEY,
- #{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE,
- #{@@connection.quote_column_name(data_column)} TEXT
- )
- end_sql
- end
-
- def drop_table!
- @@connection.execute "DROP TABLE #{table_name}"
- end
- end
-
- attr_reader :session_id
- attr_writer :data
-
- # Look for normal and marshaled data, self.find_by_session_id's way of
- # telling us to postpone unmarshaling until the data is requested.
- # We need to handle a normal data attribute in case of a new record.
- def initialize(attributes)
- @session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data]
- @new_record = @marshaled_data.nil?
- end
-
- def new_record?
- @new_record
- end
-
- # Lazy-unmarshal session state.
- def data
- unless @data
- if @marshaled_data
- @data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
- else
- @data = {}
- end
- end
- @data
- end
-
- def loaded?
- !! @data
- end
-
- def save
- return false if !loaded?
- marshaled_data = self.class.marshal(data)
-
- if @new_record
- @new_record = false
- @@connection.update <<-end_sql, 'Create session'
- INSERT INTO #{@@table_name} (
- #{@@connection.quote_column_name(@@session_id_column)},
- #{@@connection.quote_column_name(@@data_column)} )
- VALUES (
- #{@@connection.quote(session_id)},
- #{@@connection.quote(marshaled_data)} )
- end_sql
- else
- @@connection.update <<-end_sql, 'Update session'
- UPDATE #{@@table_name}
- SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)}
- WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
- end_sql
- end
- end
-
- def destroy
- unless @new_record
- @@connection.delete <<-end_sql, 'Destroy session'
- DELETE FROM #{@@table_name}
- WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
- end_sql
- end
- end
- end
-
-
- # The class used for session storage. Defaults to
- # CGI::Session::ActiveRecordStore::Session.
- cattr_accessor :session_class
- self.session_class = Session
-
- # Find or instantiate a session given a CGI::Session.
- def initialize(session, option = nil)
- session_id = session.session_id
- unless @session = ActiveRecord::Base.silence { @@session_class.find_by_session_id(session_id) }
- unless session.new_session
- raise CGI::Session::NoSession, 'uninitialized session'
- end
- @session = @@session_class.new(:session_id => session_id, :data => {})
- # session saving can be lazy again, because of improved component implementation
- # therefore next line gets commented out:
- # @session.save
- end
- end
-
- # Access the underlying session model.
- def model
- @session
- end
-
- # Restore session state. The session model handles unmarshaling.
- def restore
- if @session
- @session.data
- end
- end
-
- # Save session store.
- def update
- if @session
- ActiveRecord::Base.silence { @session.save }
- end
- end
-
- # Save and close the session store.
- def close
- if @session
- update
- @session = nil
- end
- end
-
- # Delete and close the session store.
- def delete
- if @session
- ActiveRecord::Base.silence { @session.destroy }
- @session = nil
- end
- end
-
- protected
- def logger
- ActionController::Base.logger rescue nil
- end
- end
- end
-end
diff --git a/actionpack/lib/action_controller/session/cookie_store.rb b/actionpack/lib/action_controller/session/cookie_store.rb
index ea0ea4f841..f13c9290c0 100644
--- a/actionpack/lib/action_controller/session/cookie_store.rb
+++ b/actionpack/lib/action_controller/session/cookie_store.rb
@@ -1,163 +1,219 @@
-require 'cgi'
-require 'cgi/session'
-
-# This cookie-based session store is the Rails default. Sessions typically
-# contain at most a user_id and flash message; both fit within the 4K cookie
-# size limit. Cookie-based sessions are dramatically faster than the
-# alternatives.
-#
-# If you have more than 4K of session data or don't want your data to be
-# visible to the user, pick another session store.
-#
-# CookieOverflow is raised if you attempt to store more than 4K of data.
-# TamperedWithCookie is raised if the data integrity check fails.
-#
-# A message digest is included with the cookie to ensure data integrity:
-# a user cannot alter his +user_id+ without knowing the secret key included in
-# the hash. New apps are generated with a pregenerated secret in
-# config/environment.rb. Set your own for old apps you're upgrading.
-#
-# Session options:
-#
-# * <tt>:secret</tt>: An application-wide key string or block returning a string
-# called per generated digest. The block is called with the CGI::Session
-# instance as an argument. It's important that the secret is not vulnerable to
-# a dictionary attack. Therefore, you should choose a secret consisting of
-# random numbers and letters and more than 30 characters. Examples:
-#
-# :secret => '449fe2e7daee471bffae2fd8dc02313d'
-# :secret => Proc.new { User.current_user.secret_key }
-#
-# * <tt>:digest</tt>: The message digest algorithm used to verify session
-# integrity defaults to 'SHA1' but may be any digest provided by OpenSSL,
-# such as 'MD5', 'RIPEMD160', 'SHA256', etc.
-#
-# To generate a secret key for an existing application, run
-# "rake secret" and set the key in config/environment.rb.
-#
-# Note that changing digest or secret invalidates all existing sessions!
-class CGI::Session::CookieStore
- # Cookies can typically store 4096 bytes.
- MAX = 4096
- SECRET_MIN_LENGTH = 30 # characters
-
- # Raised when storing more than 4K of session data.
- class CookieOverflow < StandardError; end
-
- # Raised when the cookie fails its integrity check.
- class TamperedWithCookie < StandardError; end
-
- # Called from CGI::Session only.
- def initialize(session, options = {})
- # The session_key option is required.
- if options['session_key'].blank?
- raise ArgumentError, 'A session_key is required to write a cookie containing the session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase" } in config/environment.rb'
- end
+module ActionController
+ module Session
+ # This cookie-based session store is the Rails default. Sessions typically
+ # contain at most a user_id and flash message; both fit within the 4K cookie
+ # size limit. Cookie-based sessions are dramatically faster than the
+ # alternatives.
+ #
+ # If you have more than 4K of session data or don't want your data to be
+ # visible to the user, pick another session store.
+ #
+ # CookieOverflow is raised if you attempt to store more than 4K of data.
+ #
+ # A message digest is included with the cookie to ensure data integrity:
+ # a user cannot alter his +user_id+ without knowing the secret key
+ # included in the hash. New apps are generated with a pregenerated secret
+ # in config/environment.rb. Set your own for old apps you're upgrading.
+ #
+ # Session options:
+ #
+ # * <tt>:secret</tt>: An application-wide key string or block returning a
+ # string called per generated digest. The block is called with the
+ # CGI::Session instance as an argument. It's important that the secret
+ # is not vulnerable to a dictionary attack. Therefore, you should choose
+ # a secret consisting of random numbers and letters and more than 30
+ # characters. Examples:
+ #
+ # :secret => '449fe2e7daee471bffae2fd8dc02313d'
+ # :secret => Proc.new { User.current_user.secret_key }
+ #
+ # * <tt>:digest</tt>: The message digest algorithm used to verify session
+ # integrity defaults to 'SHA1' but may be any digest provided by OpenSSL,
+ # such as 'MD5', 'RIPEMD160', 'SHA256', etc.
+ #
+ # To generate a secret key for an existing application, run
+ # "rake secret" and set the key in config/environment.rb.
+ #
+ # Note that changing digest or secret invalidates all existing sessions!
+ class CookieStore
+ # Cookies can typically store 4096 bytes.
+ MAX = 4096
+ SECRET_MIN_LENGTH = 30 # characters
+
+ DEFAULT_OPTIONS = {
+ :domain => nil,
+ :path => "/",
+ :expire_after => nil
+ }.freeze
+
+ ENV_SESSION_KEY = "rack.session".freeze
+ ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze
+ HTTP_SET_COOKIE = "Set-Cookie".freeze
+
+ # Raised when storing more than 4K of session data.
+ class CookieOverflow < StandardError; end
+
+ def initialize(app, options = {})
+ options = options.dup
+
+ @app = app
+
+ # The session_key option is required.
+ ensure_session_key(options[:key])
+ @key = options.delete(:key).freeze
+
+ # The secret option is required.
+ ensure_secret_secure(options[:secret])
+ @secret = options.delete(:secret).freeze
+
+ @digest = options.delete(:digest) || 'SHA1'
+ @verifier = verifier_for(@secret, @digest)
+
+ @default_options = DEFAULT_OPTIONS.merge(options).freeze
+
+ freeze
+ end
- # The secret option is required.
- ensure_secret_secure(options['secret'])
-
- # Keep the session and its secret on hand so we can read and write cookies.
- @session, @secret = session, options['secret']
-
- # Message digest defaults to SHA1.
- @digest = options['digest'] || 'SHA1'
-
- # Default cookie options derived from session settings.
- @cookie_options = {
- 'name' => options['session_key'],
- 'path' => options['session_path'],
- 'domain' => options['session_domain'],
- 'expires' => options['session_expires'],
- 'secure' => options['session_secure'],
- 'http_only' => options['session_http_only']
- }
-
- # Set no_hidden and no_cookies since the session id is unused and we
- # set our own data cookie.
- options['no_hidden'] = true
- options['no_cookies'] = true
- end
+ class SessionHash < Hash
+ def initialize(middleware, env)
+ @middleware = middleware
+ @env = env
+ @loaded = false
+ end
+
+ def [](key)
+ load! unless @loaded
+ super
+ end
+
+ def []=(key, value)
+ load! unless @loaded
+ super
+ end
+
+ def to_hash
+ {}.replace(self)
+ end
+
+ private
+ def load!
+ replace(@middleware.send(:load_session, @env))
+ @loaded = true
+ end
+ end
- # To prevent users from using something insecure like "Password" we make sure that the
- # secret they've provided is at least 30 characters in length.
- def ensure_secret_secure(secret)
- # There's no way we can do this check if they've provided a proc for the
- # secret.
- return true if secret.is_a?(Proc)
+ def call(env)
+ session_data = SessionHash.new(self, env)
+ original_value = session_data.dup
- if secret.blank?
- raise ArgumentError, %Q{A secret is required to generate an integrity hash for cookie session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase of at least #{SECRET_MIN_LENGTH} characters" } in config/environment.rb}
- end
+ env[ENV_SESSION_KEY] = session_data
+ env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
- if secret.length < SECRET_MIN_LENGTH
- raise ArgumentError, %Q{Secret should be something secure, like "#{CGI::Session.generate_unique_id}". The value you provided, "#{secret}", is shorter than the minimum length of #{SECRET_MIN_LENGTH} characters}
- end
- end
+ status, headers, body = @app.call(env)
- # Restore session data from the cookie.
- def restore
- @original = read_cookie
- @data = unmarshal(@original) || {}
- end
+ unless env[ENV_SESSION_KEY] == original_value
+ session_data = marshal(env[ENV_SESSION_KEY].to_hash)
- # Wait until close to write the session data cookie.
- def update; end
+ raise CookieOverflow if session_data.size > MAX
- # Write the session data cookie if it was loaded and has changed.
- def close
- if defined?(@data) && !@data.blank?
- updated = marshal(@data)
- raise CookieOverflow if updated.size > MAX
- write_cookie('value' => updated) unless updated == @original
- end
- end
+ options = env[ENV_SESSION_OPTIONS_KEY]
+ cookie = Hash.new
+ cookie[:value] = session_data
+ unless options[:expire_after].nil?
+ cookie[:expires] = Time.now + options[:expire_after]
+ end
- # Delete the session data by setting an expired cookie with no data.
- def delete
- @data = nil
- clear_old_cookie_value
- write_cookie('value' => nil, 'expires' => 1.year.ago)
- end
+ cookie = build_cookie(@key, cookie.merge(options))
+ case headers[HTTP_SET_COOKIE]
+ when Array
+ headers[HTTP_SET_COOKIE] << cookie
+ when String
+ headers[HTTP_SET_COOKIE] = [headers[HTTP_SET_COOKIE], cookie]
+ when nil
+ headers[HTTP_SET_COOKIE] = cookie
+ end
+ end
- private
- # Marshal a session hash into safe cookie data. Include an integrity hash.
- def marshal(session)
- verifier.generate(session)
- end
-
- # Unmarshal cookie data to a hash and verify its integrity.
- def unmarshal(cookie)
- if cookie
- verifier.verify(cookie)
+ [status, headers, body]
end
- rescue ActiveSupport::MessageVerifier::InvalidSignature
- delete
- raise TamperedWithCookie
- end
-
- # Read the session data cookie.
- def read_cookie
- @session.cgi.cookies[@cookie_options['name']].first
- end
- # CGI likes to make you hack.
- def write_cookie(options)
- cookie = CGI::Cookie.new(@cookie_options.merge(options))
- @session.cgi.send :instance_variable_set, '@output_cookies', [cookie]
- end
-
- # Clear cookie value so subsequent new_session doesn't reload old data.
- def clear_old_cookie_value
- @session.cgi.cookies[@cookie_options['name']].clear
- end
-
- def verifier
- if @secret.respond_to?(:call)
- key = @secret.call
- else
- key = @secret
- end
- ActiveSupport::MessageVerifier.new(key, @digest)
+ private
+ # Should be in Rack::Utils soon
+ def build_cookie(key, value)
+ case value
+ when Hash
+ domain = "; domain=" + value[:domain] if value[:domain]
+ path = "; path=" + value[:path] if value[:path]
+ # According to RFC 2109, we need dashes here.
+ # N.B.: cgi.rb uses spaces...
+ expires = "; expires=" + value[:expires].clone.gmtime.
+ strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
+ secure = "; secure" if value[:secure]
+ httponly = "; httponly" if value[:httponly]
+ value = value[:value]
+ end
+ value = [value] unless Array === value
+ cookie = Rack::Utils.escape(key) + "=" +
+ value.map { |v| Rack::Utils.escape(v) }.join("&") +
+ "#{domain}#{path}#{expires}#{secure}#{httponly}"
+ end
+
+ def load_session(env)
+ request = Rack::Request.new(env)
+ session_data = request.cookies[@key]
+ unmarshal(session_data) || {}
+ end
+
+ # Marshal a session hash into safe cookie data. Include an integrity hash.
+ def marshal(session)
+ @verifier.generate(session)
+ end
+
+ # Unmarshal cookie data to a hash and verify its integrity.
+ def unmarshal(cookie)
+ @verifier.verify(cookie) if cookie
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
+ nil
+ end
+
+ def ensure_session_key(key)
+ if key.blank?
+ raise ArgumentError, 'A session_key is required to write a ' +
+ 'cookie containing the session data. Use ' +
+ 'config.action_controller.session = { :session_key => ' +
+ '"_myapp_session", :secret => "some secret phrase" } in ' +
+ 'config/environment.rb'
+ end
+ end
+
+ # To prevent users from using something insecure like "Password" we make sure that the
+ # secret they've provided is at least 30 characters in length.
+ def ensure_secret_secure(secret)
+ # There's no way we can do this check if they've provided a proc for the
+ # secret.
+ return true if secret.is_a?(Proc)
+
+ if secret.blank?
+ raise ArgumentError, "A secret is required to generate an " +
+ "integrity hash for cookie session data. Use " +
+ "config.action_controller.session = { :session_key => " +
+ "\"_myapp_session\", :secret => \"some secret phrase of at " +
+ "least #{SECRET_MIN_LENGTH} characters\" } " +
+ "in config/environment.rb"
+ end
+
+ if secret.length < SECRET_MIN_LENGTH
+ raise ArgumentError, "Secret should be something secure, " +
+ "like \"#{ActiveSupport::SecureRandom.hex(16)}\". The value you " +
+ "provided, \"#{secret}\", is shorter than the minimum length " +
+ "of #{SECRET_MIN_LENGTH} characters"
+ end
+ end
+
+ def verifier_for(secret, digest)
+ key = secret.respond_to?(:call) ? secret.call : secret
+ ActiveSupport::MessageVerifier.new(key, digest)
+ end
end
+ end
end
diff --git a/actionpack/lib/action_controller/session/drb_server.rb b/actionpack/lib/action_controller/session/drb_server.rb
deleted file mode 100755
index 2caa27f62a..0000000000
--- a/actionpack/lib/action_controller/session/drb_server.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env ruby
-
-# This is a really simple session storage daemon, basically just a hash,
-# which is enabled for DRb access.
-
-require 'drb'
-
-session_hash = Hash.new
-session_hash.instance_eval { @mutex = Mutex.new }
-
-class <<session_hash
- def []=(key, value)
- @mutex.synchronize do
- super(key, value)
- end
- end
-
- def [](key)
- @mutex.synchronize do
- super(key)
- end
- end
-
- def delete(key)
- @mutex.synchronize do
- super(key)
- end
- end
-end
-
-DRb.start_service('druby://127.0.0.1:9192', session_hash)
-DRb.thread.join
diff --git a/actionpack/lib/action_controller/session/drb_store.rb b/actionpack/lib/action_controller/session/drb_store.rb
deleted file mode 100644
index 4feb2636e7..0000000000
--- a/actionpack/lib/action_controller/session/drb_store.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'cgi'
-require 'cgi/session'
-require 'drb'
-
-class CGI #:nodoc:all
- class Session
- class DRbStore
- @@session_data = DRbObject.new(nil, 'druby://localhost:9192')
-
- def initialize(session, option=nil)
- @session_id = session.session_id
- end
-
- def restore
- @h = @@session_data[@session_id] || {}
- end
-
- def update
- @@session_data[@session_id] = @h
- end
-
- def close
- update
- end
-
- def delete
- @@session_data.delete(@session_id)
- end
-
- def data
- @@session_data[@session_id]
- end
- end
- end
-end
diff --git a/actionpack/lib/action_controller/session/mem_cache_store.rb b/actionpack/lib/action_controller/session/mem_cache_store.rb
index 2f08af663d..f745715a97 100644
--- a/actionpack/lib/action_controller/session/mem_cache_store.rb
+++ b/actionpack/lib/action_controller/session/mem_cache_store.rb
@@ -1,95 +1,48 @@
-# cgi/session/memcached.rb - persistent storage of marshalled session data
-#
-# == Overview
-#
-# This file provides the CGI::Session::MemCache class, which builds
-# persistence of storage data on top of the MemCache library. See
-# cgi/session.rb for more details on session storage managers.
-#
-
begin
- require 'cgi/session'
require_library_or_gem 'memcache'
- class CGI
- class Session
- # MemCache-based session storage class.
- #
- # This builds upon the top-level MemCache class provided by the
- # library file memcache.rb. Session data is marshalled and stored
- # in a memcached cache.
- class MemCacheStore
- def check_id(id) #:nodoc:#
- /[^0-9a-zA-Z]+/ =~ id.to_s ? false : true
- end
+ module ActionController
+ module Session
+ class MemCacheStore < AbstractStore
+ def initialize(app, options = {})
+ # Support old :expires option
+ options[:expire_after] ||= options[:expires]
- # Create a new CGI::Session::MemCache instance
- #
- # This constructor is used internally by CGI::Session. The
- # user does not generally need to call it directly.
- #
- # +session+ is the session for which this instance is being
- # created. The session id must only contain alphanumeric
- # characters; automatically generated session ids observe
- # this requirement.
- #
- # +options+ is a hash of options for the initializer. The
- # following options are recognized:
- #
- # cache:: an instance of a MemCache client to use as the
- # session cache.
- #
- # expires:: an expiry time value to use for session entries in
- # the session cache. +expires+ is interpreted in seconds
- # relative to the current time if it’s less than 60*60*24*30
- # (30 days), or as an absolute Unix time (e.g., Time#to_i) if
- # greater. If +expires+ is +0+, or not passed on +options+,
- # the entry will never expire.
- #
- # This session's memcache entry will be created if it does
- # not exist, or retrieved if it does.
- def initialize(session, options = {})
- id = session.session_id
- unless check_id(id)
- raise ArgumentError, "session_id '%s' is invalid" % id
- end
- @cache = options['cache'] || MemCache.new('localhost')
- @expires = options['expires'] || 0
- @session_key = "session:#{id}"
- @session_data = {}
- # Add this key to the store if haven't done so yet
- unless @cache.get(@session_key)
- @cache.add(@session_key, @session_data, @expires)
- end
- end
+ super
- # Restore session state from the session's memcache entry.
- #
- # Returns the session state as a hash.
- def restore
- @session_data = @cache[@session_key] || {}
- end
+ @default_options = {
+ :namespace => 'rack:session',
+ :memcache_server => 'localhost:11211'
+ }.merge(@default_options)
- # Save session state to the session's memcache entry.
- def update
- @cache.set(@session_key, @session_data, @expires)
- end
-
- # Update and close the session's memcache entry.
- def close
- update
- end
+ @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
- # Delete the session's memcache entry.
- def delete
- @cache.delete(@session_key)
- @session_data = {}
- end
-
- def data
- @session_data
+ 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)
+ return true
+ rescue MemCache::MemCacheError, Errno::ECONNREFUSED
+ return false
+ end
end
end
end
diff --git a/actionpack/lib/action_controller/session_management.rb b/actionpack/lib/action_controller/session_management.rb
index 60a9aec39c..dd5001d328 100644
--- a/actionpack/lib/action_controller/session_management.rb
+++ b/actionpack/lib/action_controller/session_management.rb
@@ -3,8 +3,29 @@ module ActionController #:nodoc:
def self.included(base)
base.class_eval do
extend ClassMethods
- alias_method_chain :process, :session_management_support
- alias_method_chain :process_cleanup, :session_management_support
+ end
+ end
+
+ class Middleware
+ DEFAULT_OPTIONS = {
+ :path => "/",
+ :key => "_session_id",
+ :httponly => true,
+ }.freeze
+
+ def self.new(app)
+ cgi_options = ActionController::Base.session_options
+ options = cgi_options.symbolize_keys
+ options = DEFAULT_OPTIONS.merge(options)
+ options[:path] = options.delete(:session_path)
+ options[:key] = options.delete(:session_key)
+ options[:httponly] = options.delete(:session_http_only)
+
+ if store = ActionController::Base.session_store
+ store.new(app, options)
+ else # Sessions disabled
+ lambda { |env| app.call(env) }
+ end
end
end
@@ -12,144 +33,45 @@ module ActionController #:nodoc:
# Set the session store to be used for keeping the session data between requests.
# By default, sessions are stored in browser cookies (<tt>:cookie_store</tt>),
# but you can also specify one of the other included stores (<tt>:active_record_store</tt>,
- # <tt>:p_store</tt>, <tt>:drb_store</tt>, <tt>:mem_cache_store</tt>, or
- # <tt>:memory_store</tt>) or your own custom class.
+ # <tt>:mem_cache_store</tt>, or your own custom class.
def session_store=(store)
- ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager] =
- store.is_a?(Symbol) ? CGI::Session.const_get(store == :drb_store ? "DRbStore" : store.to_s.camelize) : store
+ if store == :active_record_store
+ self.session_store = ActiveRecord::SessionStore
+ else
+ @@session_store = store.is_a?(Symbol) ?
+ Session.const_get(store.to_s.camelize) :
+ store
+ end
end
# Returns the session store class currently used.
def session_store
- ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager]
+ if defined? @@session_store
+ @@session_store
+ else
+ Session::CookieStore
+ end
+ end
+
+ def session=(options = {})
+ self.session_store = nil if options.delete(:disabled)
+ session_options.merge!(options)
end
# Returns the hash used to configure the session. Example use:
#
# ActionController::Base.session_options[:session_secure] = true # session only available over HTTPS
def session_options
- ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS
- end
-
- # Specify how sessions ought to be managed for a subset of the actions on
- # the controller. Like filters, you can specify <tt>:only</tt> and
- # <tt>:except</tt> clauses to restrict the subset, otherwise options
- # apply to all actions on this controller.
- #
- # The session options are inheritable, as well, so if you specify them in
- # a parent controller, they apply to controllers that extend the parent.
- #
- # Usage:
- #
- # # turn off session management for all actions.
- # session :off
- #
- # # turn off session management for all actions _except_ foo and bar.
- # session :off, :except => %w(foo bar)
- #
- # # turn off session management for only the foo and bar actions.
- # session :off, :only => %w(foo bar)
- #
- # # the session will only work over HTTPS, but only for the foo action
- # session :only => :foo, :session_secure => true
- #
- # # the session by default uses HttpOnly sessions for security reasons.
- # # this can be switched off.
- # session :only => :foo, :session_http_only => false
- #
- # # the session will only be disabled for 'foo', and only if it is
- # # requested as a web service
- # session :off, :only => :foo,
- # :if => Proc.new { |req| req.parameters[:ws] }
- #
- # # the session will be disabled for non html/ajax requests
- # session :off,
- # :if => Proc.new { |req| !(req.format.html? || req.format.js?) }
- #
- # # turn the session back on, useful when it was turned off in the
- # # application controller, and you need it on in another controller
- # session :on
- #
- # All session options described for ActionController::Base.process_cgi
- # are valid arguments.
- def session(*args)
- options = args.extract_options!
-
- options[:disabled] = false if args.delete(:on)
- options[:disabled] = true if !args.empty?
- options[:only] = [*options[:only]].map { |o| o.to_s } if options[:only]
- options[:except] = [*options[:except]].map { |o| o.to_s } if options[:except]
- if options[:only] && options[:except]
- raise ArgumentError, "only one of either :only or :except are allowed"
- end
-
- write_inheritable_array(:session_options, [options])
+ @session_options ||= {}
end
- # So we can declare session options in the Rails initializer.
- alias_method :session=, :session
-
- def cached_session_options #:nodoc:
- @session_options ||= read_inheritable_attribute(:session_options) || []
- end
-
- def session_options_for(request, action) #:nodoc:
- if (session_options = cached_session_options).empty?
- {}
- else
- options = {}
-
- action = action.to_s
- session_options.each do |opts|
- next if opts[:if] && !opts[:if].call(request)
- if opts[:only] && opts[:only].include?(action)
- options.merge!(opts)
- elsif opts[:except] && !opts[:except].include?(action)
- options.merge!(opts)
- elsif !opts[:only] && !opts[:except]
- options.merge!(opts)
- end
- end
-
- if options.empty? then options
- else
- options.delete :only
- options.delete :except
- options.delete :if
- options[:disabled] ? false : options
- end
- end
+ def session(*args)
+ ActiveSupport::Deprecation.warn(
+ "Disabling sessions for a single controller has been deprecated. " +
+ "Sessions are now lazy loaded. So if you don't access them, " +
+ "consider them off. You can still modify the session cookie " +
+ "options with request.session_options.", caller)
end
end
-
- def process_with_session_management_support(request, response, method = :perform_action, *arguments) #:nodoc:
- set_session_options(request)
- process_without_session_management_support(request, response, method, *arguments)
- end
-
- private
- def set_session_options(request)
- request.session_options = self.class.session_options_for(request, request.parameters["action"] || "index")
- end
-
- def process_cleanup_with_session_management_support
- clear_persistent_model_associations
- process_cleanup_without_session_management_support
- end
-
- # Clear cached associations in session data so they don't overflow
- # the database field. Only applies to ActiveRecordStore since there
- # is not a standard way to iterate over session data.
- def clear_persistent_model_associations #:doc:
- if defined?(@_session) && @_session.respond_to?(:data)
- session_data = @_session.data
-
- if session_data && session_data.respond_to?(:each_value)
- session_data.each_value do |obj|
- obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
- end
- end
- end
- end
end
end
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index 51697fda2f..2ba056e60f 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -30,6 +30,8 @@ ActiveSupport::Deprecation.debug = true
ActionController::Base.logger = nil
ActionController::Routing::Routes.reload rescue nil
+ActionController::Base.session_store = nil
+
FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
ActionController::Base.view_paths = FIXTURE_LOAD_PATH
ActionController::Base.view_paths.load
diff --git a/actionpack/test/activerecord/active_record_store_test.rb b/actionpack/test/activerecord/active_record_store_test.rb
index 677d434f9c..6a75e6050d 100644
--- a/actionpack/test/activerecord/active_record_store_test.rb
+++ b/actionpack/test/activerecord/active_record_store_test.rb
@@ -1,140 +1,128 @@
-# These tests exercise CGI::Session::ActiveRecordStore, so you're going to
-# need AR in a sibling directory to AP and have SQLite installed.
require 'active_record_unit'
-module CommonActiveRecordStoreTests
- def test_basics
- s = session_class.new(:session_id => '1234', :data => { 'foo' => 'bar' })
- assert_equal 'bar', s.data['foo']
- assert s.save
- assert_equal 'bar', s.data['foo']
+class ActiveRecordStoreTest < ActionController::IntegrationTest
+ DispatcherApp = ActionController::Dispatcher.new
+ SessionApp = ActiveRecord::SessionStore.new(DispatcherApp,
+ :key => '_session_id')
+ SessionAppWithFixation = ActiveRecord::SessionStore.new(DispatcherApp,
+ :key => '_session_id', :cookie_only => false)
- assert_not_nil t = session_class.find_by_session_id('1234')
- assert_not_nil t.data
- assert_equal 'bar', t.data['foo']
- end
-
- def test_reload_same_session
- @new_session.update
- reloaded = CGI::Session.new(CGI.new, 'session_id' => @new_session.session_id, 'database_manager' => CGI::Session::ActiveRecordStore)
- assert_equal 'bar', reloaded['foo']
- end
-
- def test_tolerates_close_close
- assert_nothing_raised do
- @new_session.close
- @new_session.close
+ class TestController < ActionController::Base
+ def no_session_access
+ head :ok
end
- end
-end
-class ActiveRecordStoreTest < ActiveRecordTestCase
- include CommonActiveRecordStoreTests
+ def set_session_value
+ session[:foo] = params[:foo] || "bar"
+ head :ok
+ end
- def session_class
- CGI::Session::ActiveRecordStore::Session
- end
+ def get_session_value
+ render :text => "foo: #{session[:foo].inspect}"
+ end
- def session_id_column
- "session_id"
+ def rescue_action(e) raise end
end
def setup
- session_class.create_table!
-
- ENV['REQUEST_METHOD'] = 'GET'
- ENV['REQUEST_URI'] = '/'
- CGI::Session::ActiveRecordStore.session_class = session_class
-
- @cgi = CGI.new
- @new_session = CGI::Session.new(@cgi, 'database_manager' => CGI::Session::ActiveRecordStore, 'new_session' => true)
- @new_session['foo'] = 'bar'
+ ActiveRecord::SessionStore.session_class.create_table!
+ @integration_session = open_session(SessionApp)
end
-# this test only applies for eager session saving
-# def test_another_instance
-# @another = CGI::Session.new(@cgi, 'session_id' => @new_session.session_id, 'database_manager' => CGI::Session::ActiveRecordStore)
-# assert_equal @new_session.session_id, @another.session_id
-# end
-
- def test_model_attribute
- assert_kind_of CGI::Session::ActiveRecordStore::Session, @new_session.model
- assert_equal({ 'foo' => 'bar' }, @new_session.model.data)
+ def teardown
+ ActiveRecord::SessionStore.session_class.drop_table!
end
- def test_save_unloaded_session
- c = session_class.connection
- bogus_class = c.quote(ActiveSupport::Base64.encode64("\004\010o:\vBlammo\000"))
- c.insert("INSERT INTO #{session_class.table_name} ('#{session_id_column}', 'data') VALUES ('abcdefghijklmnop', #{bogus_class})")
+ def test_setting_and_getting_session_value
+ with_test_route_set do
+ get '/set_session_value'
+ assert_response :success
+ assert cookies['_session_id']
- sess = session_class.find_by_session_id('abcdefghijklmnop')
- assert_not_nil sess
- assert !sess.loaded?
+ get '/get_session_value'
+ assert_response :success
+ assert_equal 'foo: "bar"', response.body
- # because the session is not loaded, the save should be a no-op. If it
- # isn't, this'll try and unmarshall the bogus class, and should get an error.
- assert_nothing_raised { sess.save }
- end
+ get '/set_session_value', :foo => "baz"
+ assert_response :success
+ assert cookies['_session_id']
- def teardown
- session_class.drop_table!
+ get '/get_session_value'
+ assert_response :success
+ assert_equal 'foo: "baz"', response.body
+ end
end
-end
-class ColumnLimitTest < ActiveRecordTestCase
- def setup
- @session_class = CGI::Session::ActiveRecordStore::Session
- @session_class.create_table!
+ def test_getting_nil_session_value
+ with_test_route_set do
+ get '/get_session_value'
+ assert_response :success
+ assert_equal 'foo: nil', response.body
+ end
end
- def teardown
- @session_class.drop_table!
- end
+ def test_prevents_session_fixation
+ with_test_route_set do
+ get '/set_session_value'
+ assert_response :success
+ assert cookies['_session_id']
- def test_protection_from_data_larger_than_column
- # Can't test this unless there is a limit
- return unless limit = @session_class.data_column_size_limit
- too_big = ':(' * limit
- s = @session_class.new(:session_id => '666', :data => {'foo' => too_big})
- s.data
- assert_raise(ActionController::SessionOverflowError) { s.save }
- end
-end
+ get '/get_session_value'
+ assert_response :success
+ assert_equal 'foo: "bar"', response.body
+ session_id = cookies['_session_id']
+ assert session_id
+
+ reset!
-class DeprecatedActiveRecordStoreTest < ActiveRecordStoreTest
- def session_id_column
- "sessid"
+ get '/set_session_value', :_session_id => session_id, :foo => "baz"
+ assert_response :success
+ assert_equal nil, cookies['_session_id']
+
+ get '/get_session_value', :_session_id => session_id
+ assert_response :success
+ assert_equal 'foo: nil', response.body
+ assert_equal nil, cookies['_session_id']
+ end
end
- def setup
- session_class.connection.execute 'create table old_sessions (id integer primary key, sessid text unique, data text)'
- session_class.table_name = 'old_sessions'
- session_class.send :setup_sessid_compatibility!
+ def test_allows_session_fixation
+ @integration_session = open_session(SessionAppWithFixation)
- ENV['REQUEST_METHOD'] = 'GET'
- CGI::Session::ActiveRecordStore.session_class = session_class
+ with_test_route_set do
+ get '/set_session_value'
+ assert_response :success
+ assert cookies['_session_id']
- @new_session = CGI::Session.new(CGI.new, 'database_manager' => CGI::Session::ActiveRecordStore, 'new_session' => true)
- @new_session['foo'] = 'bar'
- end
+ get '/get_session_value'
+ assert_response :success
+ assert_equal 'foo: "bar"', response.body
+ session_id = cookies['_session_id']
+ assert session_id
- def teardown
- session_class.connection.execute 'drop table old_sessions'
- session_class.table_name = 'sessions'
- end
-end
+ reset!
+ @integration_session = open_session(SessionAppWithFixation)
+
+ get '/set_session_value', :_session_id => session_id, :foo => "baz"
+ assert_response :success
+ assert_equal session_id, cookies['_session_id']
-class SqlBypassActiveRecordStoreTest < ActiveRecordStoreTest
- def session_class
- unless defined? @session_class
- @session_class = CGI::Session::ActiveRecordStore::SqlBypass
- @session_class.connection = CGI::Session::ActiveRecordStore::Session.connection
+ get '/get_session_value', :_session_id => session_id
+ assert_response :success
+ assert_equal 'foo: "baz"', response.body
+ assert_equal session_id, cookies['_session_id']
end
- @session_class
end
- def test_model_attribute
- assert_kind_of CGI::Session::ActiveRecordStore::SqlBypass, @new_session.model
- assert_equal({ 'foo' => 'bar' }, @new_session.model.data)
- end
+ private
+ def with_test_route_set
+ with_routing do |set|
+ set.draw do |map|
+ map.with_options :controller => "active_record_store_test/test" do |c|
+ c.connect "/:action"
+ end
+ end
+ yield
+ end
+ end
end
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index 6a793c8bb6..fd985a9a46 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -231,8 +231,6 @@ end
class IntegrationProcessTest < ActionController::IntegrationTest
class IntegrationController < ActionController::Base
- session :off
-
def get
respond_to do |format|
format.html { render :text => "OK", :status => 200 }
diff --git a/actionpack/test/controller/integration_upload_test.rb b/actionpack/test/controller/integration_upload_test.rb
index b1dd6a6341..39d2e164e4 100644
--- a/actionpack/test/controller/integration_upload_test.rb
+++ b/actionpack/test/controller/integration_upload_test.rb
@@ -6,8 +6,6 @@ unless defined? ApplicationController
end
class UploadTestController < ActionController::Base
- session :off
-
def update
SessionUploadTest.last_request_type = ActionController::Base.param_parsers[request.content_type]
render :text => "got here"
diff --git a/actionpack/test/controller/rack_test.rb b/actionpack/test/controller/rack_test.rb
index 641ef9626e..e2ec686c41 100644
--- a/actionpack/test/controller/rack_test.rb
+++ b/actionpack/test/controller/rack_test.rb
@@ -229,7 +229,7 @@ end
class RackResponseTest < BaseRackTest
def setup
super
- @response = ActionController::RackResponse.new(@request)
+ @response = ActionController::RackResponse.new
end
def test_simple_output
@@ -265,34 +265,12 @@ class RackResponseTest < BaseRackTest
body.each { |part| parts << part }
assert_equal ["0", "1", "2", "3", "4"], parts
end
-
- def test_set_session_cookie
- cookie = CGI::Cookie.new({"name" => "name", "value" => "Josh"})
- @request.cgi.send :instance_variable_set, '@output_cookies', [cookie]
-
- @response.body = "Hello, World!"
- @response.prepare!
-
- status, headers, body = @response.out
- assert_equal "200 OK", status
- assert_equal({
- "Content-Type" => "text/html; charset=utf-8",
- "Cache-Control" => "private, max-age=0, must-revalidate",
- "ETag" => '"65a8e27d8879283831b664bd8b7f0ad4"',
- "Set-Cookie" => ["name=Josh; path="],
- "Content-Length" => "13"
- }, headers)
-
- parts = []
- body.each { |part| parts << part }
- assert_equal ["Hello, World!"], parts
- end
end
class RackResponseHeadersTest < BaseRackTest
def setup
super
- @response = ActionController::RackResponse.new(@request)
+ @response = ActionController::RackResponse.new
@response.headers['Status'] = "200 OK"
end
diff --git a/actionpack/test/controller/session/cookie_store_test.rb b/actionpack/test/controller/session/cookie_store_test.rb
index b5f14acc1f..8098059d46 100644
--- a/actionpack/test/controller/session/cookie_store_test.rb
+++ b/actionpack/test/controller/session/cookie_store_test.rb
@@ -1,298 +1,146 @@
require 'abstract_unit'
require 'stringio'
+class CookieStoreTest < ActionController::IntegrationTest
+ SessionKey = '_myapp_session'
+ SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33'
-class CGI::Session::CookieStore
- def ensure_secret_secure_with_test_hax(secret)
- if secret == CookieStoreTest.default_session_options['secret']
- return true
- else
- ensure_secret_secure_without_test_hax(secret)
- end
- end
- alias_method_chain :ensure_secret_secure, :test_hax
-end
+ DispatcherApp = ActionController::Dispatcher.new
+ CookieStoreApp = ActionController::Session::CookieStore.new(DispatcherApp,
+ :key => SessionKey, :secret => SessionSecret)
+ SignedBar = "BAh7BjoIZm9vIghiYXI%3D--" +
+ "fef868465920f415f2c0652d6910d3af288a0367"
-# Expose for tests.
-class CGI
- attr_reader :output_cookies, :output_hidden
-
- class Session
- attr_reader :dbman
+ class TestController < ActionController::Base
+ def no_session_access
+ head :ok
+ end
- class CookieStore
- attr_reader :data, :original, :cookie_options
+ def set_session_value
+ session[:foo] = "bar"
+ head :ok
end
- end
-end
-class CookieStoreTest < Test::Unit::TestCase
- def self.default_session_options
- { 'database_manager' => CGI::Session::CookieStore,
- 'session_key' => '_myapp_session',
- 'secret' => 'Keep it secret; keep it safe.',
- 'no_cookies' => true,
- 'no_hidden' => true,
- 'session_http_only' => true
- }
- end
+ def get_session_value
+ render :text => "foo: #{session[:foo].inspect}"
+ end
- def self.cookies
- { :empty => ['BAgw--0686dcaccc01040f4bd4f35fe160afe9bc04c330', {}],
- :a_one => ['BAh7BiIGYWkG--5689059497d7f122a7119f171aef81dcfd807fec', { 'a' => 1 }],
- :typical => ['BAh7ByIMdXNlcl9pZGkBeyIKZmxhc2h7BiILbm90aWNlIgxIZXkgbm93--9d20154623b9eeea05c62ab819be0e2483238759', { 'user_id' => 123, 'flash' => { 'notice' => 'Hey now' }}],
- :flashed => ['BAh7ByIMdXNlcl9pZGkBeyIKZmxhc2h7AA==--bf9785a666d3c4ac09f7fe3353496b437546cfbf', { 'user_id' => 123, 'flash' => {} }]
- }
+ def raise_data_overflow
+ session[:foo] = 'bye!' * 1024
+ head :ok
+ end
+ def rescue_action(e) raise end
end
def setup
- ENV.delete('HTTP_COOKIE')
+ @integration_session = open_session(CookieStoreApp)
end
def test_raises_argument_error_if_missing_session_key
- [nil, ''].each do |blank|
- assert_raise(ArgumentError, blank.inspect) { new_session 'session_key' => blank }
- end
+ assert_raise(ArgumentError, nil.inspect) {
+ ActionController::Session::CookieStore.new(nil,
+ :key => nil, :secret => SessionSecret)
+ }
+
+ assert_raise(ArgumentError, ''.inspect) {
+ ActionController::Session::CookieStore.new(nil,
+ :key => '', :secret => SessionSecret)
+ }
end
def test_raises_argument_error_if_missing_secret
- [nil, ''].each do |blank|
- assert_raise(ArgumentError, blank.inspect) { new_session 'secret' => blank }
- end
- end
+ assert_raise(ArgumentError, nil.inspect) {
+ ActionController::Session::CookieStore.new(nil,
+ :key => SessionKey, :secret => nil)
+ }
- def test_raises_argument_error_if_secret_is_probably_insecure
- ["password", "secret", "12345678901234567890123456789"].each do |blank|
- assert_raise(ArgumentError, blank.inspect) { new_session 'secret' => blank }
- end
+ assert_raise(ArgumentError, ''.inspect) {
+ ActionController::Session::CookieStore.new(nil,
+ :key => SessionKey, :secret => '')
+ }
end
- def test_reconfigures_session_to_omit_id_cookie_and_hidden_field
- new_session do |session|
- assert_equal true, @options['no_hidden']
- assert_equal true, @options['no_cookies']
- end
- end
+ def test_raises_argument_error_if_secret_is_probably_insecure
+ assert_raise(ArgumentError, "password".inspect) {
+ ActionController::Session::CookieStore.new(nil,
+ :key => SessionKey, :secret => "password")
+ }
- def test_restore_unmarshals_missing_cookie_as_empty_hash
- new_session do |session|
- assert_nil session.dbman.data
- assert_nil session['test']
- assert_equal Hash.new, session.dbman.data
- end
- end
+ assert_raise(ArgumentError, "secret".inspect) {
+ ActionController::Session::CookieStore.new(nil,
+ :key => SessionKey, :secret => "secret")
+ }
- def test_restore_unmarshals_good_cookies
- cookies(:empty, :a_one, :typical).each do |value, expected|
- set_cookie! value
- new_session do |session|
- assert_nil session['lazy loads the data hash']
- assert_equal expected, session.dbman.data
- end
- end
+ assert_raise(ArgumentError, "12345678901234567890123456789".inspect) {
+ ActionController::Session::CookieStore.new(nil,
+ :key => SessionKey, :secret => "12345678901234567890123456789")
+ }
end
- def test_restore_deletes_tampered_cookies
- set_cookie! 'a--b'
- new_session do |session|
- assert_raise(CGI::Session::CookieStore::TamperedWithCookie) { session['fail'] }
- assert_cookie_deleted session
- end
+ def test_setting_session_value
+ with_test_route_set do
+ get '/set_session_value'
+ assert_response :success
+ assert_equal ["_myapp_session=#{SignedBar}; path=/"],
+ headers['Set-Cookie']
+ end
end
- def test_close_doesnt_write_cookie_if_data_is_blank
- new_session do |session|
- assert_no_cookies session
- session.close
- assert_no_cookies session
- end
+ def test_getting_session_value
+ with_test_route_set do
+ cookies[SessionKey] = SignedBar
+ get '/get_session_value'
+ assert_response :success
+ assert_equal 'foo: "bar"', response.body
+ end
end
- def test_close_doesnt_write_cookie_if_data_is_unchanged
- set_cookie! cookie_value(:typical)
- new_session do |session|
- assert_no_cookies session
- session['user_id'] = session['user_id']
- session.close
- assert_no_cookies session
+ def test_disregards_tampered_sessions
+ with_test_route_set do
+ cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--123456780"
+ get '/get_session_value'
+ assert_response :success
+ assert_equal 'foo: nil', response.body
end
end
def test_close_raises_when_data_overflows
- set_cookie! cookie_value(:empty)
- new_session do |session|
- session['overflow'] = 'bye!' * 1024
- assert_raise(CGI::Session::CookieStore::CookieOverflow) { session.close }
- assert_no_cookies session
- end
- end
-
- def test_close_marshals_and_writes_cookie
- set_cookie! cookie_value(:typical)
- new_session do |session|
- assert_no_cookies session
- session['flash'] = {}
- assert_no_cookies session
- session.close
- assert_equal 1, session.cgi.output_cookies.size
- cookie = session.cgi.output_cookies.first
- assert_cookie cookie, cookie_value(:flashed)
- assert_http_only_cookie cookie
- assert_secure_cookie cookie, false
- end
- end
-
- def test_writes_non_secure_cookie_by_default
- set_cookie! cookie_value(:typical)
- new_session do |session|
- session['flash'] = {}
- session.close
- cookie = session.cgi.output_cookies.first
- assert_secure_cookie cookie,false
- end
- end
-
- def test_writes_secure_cookie
- set_cookie! cookie_value(:typical)
- new_session('session_secure'=>true) do |session|
- session['flash'] = {}
- session.close
- cookie = session.cgi.output_cookies.first
- assert_secure_cookie cookie
+ with_test_route_set do
+ assert_raise(ActionController::Session::CookieStore::CookieOverflow) {
+ get '/raise_data_overflow'
+ }
end
end
- def test_http_only_cookie_by_default
- set_cookie! cookie_value(:typical)
- new_session do |session|
- session['flash'] = {}
- session.close
- cookie = session.cgi.output_cookies.first
- assert_http_only_cookie cookie
+ def test_doesnt_write_session_cookie_if_session_is_not_accessed
+ with_test_route_set do
+ get '/no_session_access'
+ assert_response :success
+ assert_equal [], headers['Set-Cookie']
end
end
- def test_overides_http_only_cookie
- set_cookie! cookie_value(:typical)
- new_session('session_http_only'=>false) do |session|
- session['flash'] = {}
- session.close
- cookie = session.cgi.output_cookies.first
- assert_http_only_cookie cookie, false
- end
- end
-
- def test_delete_writes_expired_empty_cookie_and_sets_data_to_nil
- set_cookie! cookie_value(:typical)
- new_session do |session|
- assert_no_cookies session
- session.delete
- assert_cookie_deleted session
-
- # @data is set to nil so #close doesn't send another cookie.
- session.close
- assert_cookie_deleted session
- end
- end
-
- def test_new_session_doesnt_reuse_deleted_cookie_data
- set_cookie! cookie_value(:typical)
-
- new_session do |session|
- assert_not_nil session['user_id']
- session.delete
-
- # Start a new session using the same CGI instance.
- post_delete_session = CGI::Session.new(session.cgi, self.class.default_session_options)
- assert_nil post_delete_session['user_id']
+ def test_doesnt_write_session_cookie_if_session_is_unchanged
+ with_test_route_set do
+ cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--" +
+ "fef868465920f415f2c0652d6910d3af288a0367"
+ get '/no_session_access'
+ assert_response :success
+ assert_equal [], headers['Set-Cookie']
end
end
private
- def assert_no_cookies(session)
- assert_nil session.cgi.output_cookies, session.cgi.output_cookies.inspect
- end
-
- def assert_cookie_deleted(session, message = 'Expected session deletion cookie to be set')
- assert_equal 1, session.cgi.output_cookies.size
- cookie = session.cgi.output_cookies.first
- assert_cookie cookie, nil, 1.year.ago.to_date, "#{message}: #{cookie.name} => #{cookie.value}"
- end
-
- def assert_cookie(cookie, value = nil, expires = nil, message = nil)
- assert_equal '_myapp_session', cookie.name, message
- assert_equal [value].compact, cookie.value, message
- assert_equal expires, cookie.expires ? cookie.expires.to_date : cookie.expires, message
- end
-
- def assert_secure_cookie(cookie,value=true)
- assert cookie.secure==value
- end
-
- def assert_http_only_cookie(cookie,value=true)
- assert cookie.http_only==value
- end
-
- def cookies(*which)
- self.class.cookies.values_at(*which)
- end
-
- def cookie_value(which)
- self.class.cookies[which].first
- end
-
- def set_cookie!(value)
- ENV['HTTP_COOKIE'] = "_myapp_session=#{value}"
- end
-
- def new_session(options = {})
- with_cgi do |cgi|
- assert_nil cgi.output_hidden, "Output hidden params should be empty: #{cgi.output_hidden.inspect}"
- assert_nil cgi.output_cookies, "Output cookies should be empty: #{cgi.output_cookies.inspect}"
-
- @options = self.class.default_session_options.merge(options)
- session = CGI::Session.new(cgi, @options)
- ObjectSpace.undefine_finalizer(session)
-
- assert_nil cgi.output_hidden, "Output hidden params should be empty: #{cgi.output_hidden.inspect}"
- assert_nil cgi.output_cookies, "Output cookies should be empty: #{cgi.output_cookies.inspect}"
-
- yield session if block_given?
- session
+ def with_test_route_set
+ with_routing do |set|
+ set.draw do |map|
+ map.with_options :controller => "cookie_store_test/test" do |c|
+ c.connect "/:action"
+ end
+ end
+ yield
end
end
-
- def with_cgi
- ENV['REQUEST_METHOD'] = 'GET'
- ENV['HTTP_HOST'] = 'example.com'
- ENV['QUERY_STRING'] = ''
-
- cgi = CGI.new('query', StringIO.new(''))
- yield cgi if block_given?
- cgi
- end
-end
-
-
-class CookieStoreWithBlockAsSecretTest < CookieStoreTest
- def self.default_session_options
- CookieStoreTest.default_session_options.merge 'secret' => Proc.new { 'Keep it secret; keep it safe.' }
- end
-end
-
-
-class CookieStoreWithMD5DigestTest < CookieStoreTest
- def self.default_session_options
- CookieStoreTest.default_session_options.merge 'digest' => 'MD5'
- end
-
- def self.cookies
- { :empty => ['BAgw--0415cc0be9579b14afc22ee2d341aa21', {}],
- :a_one => ['BAh7BiIGYWkG--5a0ed962089cc6600ff44168a5d59bc8', { 'a' => 1 }],
- :typical => ['BAh7ByIMdXNlcl9pZGkBeyIKZmxhc2h7BiILbm90aWNlIgxIZXkgbm93--f426763f6ef435b3738b493600db8d64', { 'user_id' => 123, 'flash' => { 'notice' => 'Hey now' }}],
- :flashed => ['BAh7ByIMdXNlcl9pZGkBeyIKZmxhc2h7AA==--0af9156650dab044a53a91a4ddec2c51', { 'user_id' => 123, 'flash' => {} }],
- :double_escaped => [CGI.escape('BAh7ByIMdXNlcl9pZGkBeyIKZmxhc2h7AA%3D%3D--0af9156650dab044a53a91a4ddec2c51'), { 'user_id' => 123, 'flash' => {} }] }
- end
end
diff --git a/actionpack/test/controller/session/mem_cache_store_test.rb b/actionpack/test/controller/session/mem_cache_store_test.rb
index 9ab927a01f..52e31b78da 100644
--- a/actionpack/test/controller/session/mem_cache_store_test.rb
+++ b/actionpack/test/controller/session/mem_cache_store_test.rb
@@ -1,178 +1,81 @@
require 'abstract_unit'
-class CGI::Session
- def cache
- dbman.instance_variable_get(:@cache)
- end
-end
-
-
-uses_mocha 'MemCacheStore tests' do
-if defined? MemCache::MemCacheError
+# You need to start a memcached server inorder to run these tests
+class MemCacheStoreTest < ActionController::IntegrationTest
+ class TestController < ActionController::Base
+ def no_session_access
+ head :ok
+ end
-class MemCacheStoreTest < Test::Unit::TestCase
- SESSION_KEY_RE = /^session:[0-9a-z]+/
- CONN_TEST_KEY = 'connection_test'
- MULTI_TEST_KEY = '0123456789'
- TEST_DATA = 'Hello test'
+ def set_session_value
+ session[:foo] = "bar"
+ head :ok
+ end
- def self.get_mem_cache_if_available
- begin
- require 'memcache'
- cache = MemCache.new('127.0.0.1')
- # Test availability of the connection
- cache.set(CONN_TEST_KEY, 1)
- unless cache.get(CONN_TEST_KEY) == 1
- puts 'Warning: memcache server available but corrupted.'
- return nil
- end
- rescue LoadError, MemCache::MemCacheError
- return nil
+ def get_session_value
+ render :text => "foo: #{session[:foo].inspect}"
end
- return cache
+
+ def rescue_action(e) raise end
end
- CACHE = get_mem_cache_if_available
+ begin
+ DispatcherApp = ActionController::Dispatcher.new
+ MemCacheStoreApp = ActionController::Session::MemCacheStore.new(
+ DispatcherApp, :key => '_session_id')
- def test_initialization
- assert_raise(ArgumentError) { new_session('session_id' => '!invalid_id') }
- new_session do |s|
- assert_equal Hash.new, s.cache.get('session:' + s.session_id)
+ def setup
+ @integration_session = open_session(MemCacheStoreApp)
end
- end
+ def test_setting_and_getting_session_value
+ with_test_route_set do
+ get '/set_session_value'
+ assert_response :success
+ assert cookies['_session_id']
- def test_storage
- d = rand(0xffff)
- new_session do |s|
- session_key = 'session:' + s.session_id
- unless CACHE
- s.cache.expects(:get).with(session_key) \
- .returns(:test => d)
- s.cache.expects(:set).with(session_key,
- has_entry(:test, d),
- 0)
- end
- s[:test] = d
- s.close
- assert_equal d, s.cache.get(session_key)[:test]
- assert_equal d, s[:test]
- end
- end
-
- def test_deletion
- new_session do |s|
- session_key = 'session:' + s.session_id
- unless CACHE
- s.cache.expects(:delete)
- s.cache.expects(:get).with(session_key) \
- .returns(nil)
+ get '/get_session_value'
+ assert_response :success
+ assert_equal 'foo: "bar"', response.body
end
- s[:test] = rand(0xffff)
- s.delete
- assert_nil s.cache.get(session_key)
end
- end
-
- def test_other_session_retrieval
- new_session do |sa|
- unless CACHE
- sa.cache.expects(:set).with('session:' + sa.session_id,
- has_entry(:test, TEST_DATA),
- 0)
- end
- sa[:test] = TEST_DATA
- sa.close
- new_session('session_id' => sa.session_id) do |sb|
- unless CACHE
- sb.cache.expects(:[]).with('session:' + sb.session_id) \
- .returns(:test => TEST_DATA)
- end
- assert_equal(TEST_DATA, sb[:test])
+ def test_getting_nil_session_value
+ with_test_route_set do
+ get '/get_session_value'
+ assert_response :success
+ assert_equal 'foo: nil', response.body
end
end
- end
+ def test_prevents_session_fixation
+ with_test_route_set do
+ get '/get_session_value'
+ assert_response :success
+ assert_equal 'foo: nil', response.body
+ session_id = cookies['_session_id']
- def test_multiple_sessions
- s_slots = Array.new(10)
- operation = :write
- last_data = nil
- reads = writes = 0
- 50.times do
- current = rand(10)
- s_slots[current] ||= new_session('session_id' => MULTI_TEST_KEY,
- 'new_session' => true)
- s = s_slots[current]
- case operation
- when :write
- last_data = rand(0xffff)
- unless CACHE
- s.cache.expects(:set).with('session:' + MULTI_TEST_KEY,
- { :test => last_data },
- 0)
- end
- s[:test] = last_data
- s.close
- writes += 1
- when :read
- # Make CGI::Session#[] think there was no data retrieval yet.
- # Normally, the session caches the data during its lifetime.
- s.instance_variable_set(:@data, nil)
- unless CACHE
- s.cache.expects(:[]).with('session:' + MULTI_TEST_KEY) \
- .returns(:test => last_data)
- end
- d = s[:test]
- assert_equal(last_data, d, "OK reads: #{reads}, OK writes: #{writes}")
- reads += 1
+ reset!
+
+ get '/set_session_value', :_session_id => session_id
+ assert_response :success
+ assert_equal nil, cookies['_session_id']
end
- operation = rand(5) == 0 ? :write : :read
end
+ rescue LoadError, RuntimeError
+ $stderr.puts "Skipping MemCacheStoreTest tests. Start memcached and try again."
end
-
-
private
- def obtain_session_options
- options = { 'database_manager' => CGI::Session::MemCacheStore,
- 'session_key' => '_test_app_session'
- }
- # if don't have running memcache server we use mock instead
- unless CACHE
- options['cache'] = c = mock
- c.stubs(:[]).with(regexp_matches(SESSION_KEY_RE))
- c.stubs(:get).with(regexp_matches(SESSION_KEY_RE)) \
- .returns(Hash.new)
- c.stubs(:add).with(regexp_matches(SESSION_KEY_RE),
- instance_of(Hash),
- 0)
- end
- options
- end
-
-
- def new_session(options = {})
- with_cgi do |cgi|
- @options = obtain_session_options.merge(options)
- session = CGI::Session.new(cgi, @options)
- yield session if block_given?
- return session
+ def with_test_route_set
+ with_routing do |set|
+ set.draw do |map|
+ map.with_options :controller => "mem_cache_store_test/test" do |c|
+ c.connect "/:action"
+ end
+ end
+ yield
+ end
end
- end
-
- def with_cgi
- ENV['REQUEST_METHOD'] = 'GET'
- ENV['HTTP_HOST'] = 'example.com'
- ENV['QUERY_STRING'] = ''
-
- cgi = CGI.new('query', StringIO.new(''))
- yield cgi if block_given?
- cgi
- end
end
-
-end # defined? MemCache
-end # uses_mocha
diff --git a/actionpack/test/controller/session_fixation_test.rb b/actionpack/test/controller/session_fixation_test.rb
index e8dc8bd295..9e5b45dc3d 100644
--- a/actionpack/test/controller/session_fixation_test.rb
+++ b/actionpack/test/controller/session_fixation_test.rb
@@ -1,84 +1,84 @@
-require 'abstract_unit'
-
-class SessionFixationTest < ActionController::IntegrationTest
- class TestController < ActionController::Base
- session :session_key => '_myapp_session_id',
- :secret => CGI::Session.generate_unique_id,
- :except => :default_session_key
-
- session :cookie_only => false,
- :only => :allow_session_fixation
-
- def default_session_key
- render :text => "default_session_key"
- end
-
- def custom_session_key
- render :text => "custom_session_key: #{params[:id]}"
- end
-
- def allow_session_fixation
- render :text => "allow_session_fixation"
- end
-
- def rescue_action(e) raise end
- end
-
- def setup
- @controller = TestController.new
- end
-
- def test_should_be_able_to_make_a_successful_request
- with_test_route_set do
- assert_nothing_raised do
- get '/custom_session_key', :id => "1"
- end
- assert_equal 'custom_session_key: 1', @controller.response.body
- assert_not_nil @controller.session
- end
- end
-
- def test_should_catch_session_fixation_attempt
- with_test_route_set do
- assert_raises(ActionController::RackRequest::SessionFixationAttempt) do
- get '/custom_session_key', :_myapp_session_id => "42"
- end
- assert_nil @controller.session
- end
- end
-
- def test_should_not_catch_session_fixation_attempt_when_cookie_only_setting_is_disabled
- with_test_route_set do
- assert_nothing_raised do
- get '/allow_session_fixation', :_myapp_session_id => "42"
- end
- assert !@controller.response.body.blank?
- assert_not_nil @controller.session
- end
- end
-
- def test_should_catch_session_fixation_attempt_with_default_session_key
- # using the default session_key is not possible with cookie store
- ActionController::Base.session_store = :p_store
-
- with_test_route_set do
- assert_raises ActionController::RackRequest::SessionFixationAttempt do
- get '/default_session_key', :_session_id => "42"
- end
- assert_nil @controller.response
- assert_nil @controller.session
- end
- end
-
- private
- def with_test_route_set
- with_routing do |set|
- set.draw do |map|
- map.with_options :controller => "session_fixation_test/test" do |c|
- c.connect "/:action"
- end
- end
- yield
- end
- end
-end
+# require 'abstract_unit'
+#
+# class SessionFixationTest < ActionController::IntegrationTest
+# class TestController < ActionController::Base
+# session :session_key => '_myapp_session_id',
+# :secret => CGI::Session.generate_unique_id,
+# :except => :default_session_key
+#
+# session :cookie_only => false,
+# :only => :allow_session_fixation
+#
+# def default_session_key
+# render :text => "default_session_key"
+# end
+#
+# def custom_session_key
+# render :text => "custom_session_key: #{params[:id]}"
+# end
+#
+# def allow_session_fixation
+# render :text => "allow_session_fixation"
+# end
+#
+# def rescue_action(e) raise end
+# end
+#
+# def setup
+# @controller = TestController.new
+# end
+#
+# def test_should_be_able_to_make_a_successful_request
+# with_test_route_set do
+# assert_nothing_raised do
+# get '/custom_session_key', :id => "1"
+# end
+# assert_equal 'custom_session_key: 1', @controller.response.body
+# assert_not_nil @controller.session
+# end
+# end
+#
+# def test_should_catch_session_fixation_attempt
+# with_test_route_set do
+# assert_raises(ActionController::RackRequest::SessionFixationAttempt) do
+# get '/custom_session_key', :_myapp_session_id => "42"
+# end
+# assert_nil @controller.session
+# end
+# end
+#
+# def test_should_not_catch_session_fixation_attempt_when_cookie_only_setting_is_disabled
+# with_test_route_set do
+# assert_nothing_raised do
+# get '/allow_session_fixation', :_myapp_session_id => "42"
+# end
+# assert !@controller.response.body.blank?
+# assert_not_nil @controller.session
+# end
+# end
+#
+# def test_should_catch_session_fixation_attempt_with_default_session_key
+# # using the default session_key is not possible with cookie store
+# ActionController::Base.session_store = :p_store
+#
+# with_test_route_set do
+# assert_raises ActionController::RackRequest::SessionFixationAttempt do
+# get '/default_session_key', :_session_id => "42"
+# end
+# assert_nil @controller.response
+# assert_nil @controller.session
+# end
+# end
+#
+# private
+# def with_test_route_set
+# with_routing do |set|
+# set.draw do |map|
+# map.with_options :controller => "session_fixation_test/test" do |c|
+# c.connect "/:action"
+# end
+# end
+# yield
+# end
+# end
+# end
diff --git a/actionpack/test/controller/session_management_test.rb b/actionpack/test/controller/session_management_test.rb
deleted file mode 100644
index 592b0b549d..0000000000
--- a/actionpack/test/controller/session_management_test.rb
+++ /dev/null
@@ -1,178 +0,0 @@
-require 'abstract_unit'
-
-class SessionManagementTest < Test::Unit::TestCase
- class SessionOffController < ActionController::Base
- session :off
-
- def show
- render :text => "done"
- end
-
- def tell
- render :text => "done"
- end
- end
-
- class SessionOffOnController < ActionController::Base
- session :off
- session :on, :only => :tell
-
- def show
- render :text => "done"
- end
-
- def tell
- render :text => "done"
- end
- end
-
- class TestController < ActionController::Base
- session :off, :only => :show
- session :session_secure => true, :except => :show
- session :off, :only => :conditional,
- :if => Proc.new { |r| r.parameters[:ws] }
-
- def show
- render :text => "done"
- end
-
- def tell
- render :text => "done"
- end
-
- def conditional
- render :text => ">>>#{params[:ws]}<<<"
- end
- end
-
- class SpecializedController < SessionOffController
- session :disabled => false, :only => :something
-
- def something
- render :text => "done"
- end
-
- def another
- render :text => "done"
- end
- end
-
- class AssociationCachingTestController < ActionController::Base
- class ObjectWithAssociationCache
- def initialize
- @cached_associations = false
- end
-
- def fetch_associations
- @cached_associations = true
- end
-
- def clear_association_cache
- @cached_associations = false
- end
-
- def has_cached_associations?
- @cached_associations
- end
- end
-
- def show
- session[:object] = ObjectWithAssociationCache.new
- session[:object].fetch_associations
- if session[:object].has_cached_associations?
- render :text => "has cached associations"
- else
- render :text => "does not have cached associations"
- end
- end
-
- def tell
- if session[:object]
- if session[:object].has_cached_associations?
- render :text => "has cached associations"
- else
- render :text => "does not have cached associations"
- end
- else
- render :text => "there is no object"
- end
- end
- end
-
-
- def setup
- @request, @response = ActionController::TestRequest.new,
- ActionController::TestResponse.new
- end
-
- def test_session_off_globally
- @controller = SessionOffController.new
- get :show
- assert_equal false, @request.session_options
- get :tell
- assert_equal false, @request.session_options
- end
-
- def test_session_off_then_on_globally
- @controller = SessionOffOnController.new
- get :show
- assert_equal false, @request.session_options
- get :tell
- assert_instance_of Hash, @request.session_options
- assert_equal false, @request.session_options[:disabled]
- end
-
- def test_session_off_conditionally
- @controller = TestController.new
- get :show
- assert_equal false, @request.session_options
- get :tell
- assert_instance_of Hash, @request.session_options
- assert @request.session_options[:session_secure]
- end
-
- def test_controller_specialization_overrides_settings
- @controller = SpecializedController.new
- get :something
- assert_instance_of Hash, @request.session_options
- get :another
- assert_equal false, @request.session_options
- end
-
- def test_session_off_with_if
- @controller = TestController.new
- get :conditional
- assert_instance_of Hash, @request.session_options
- get :conditional, :ws => "ws"
- assert_equal false, @request.session_options
- end
-
- def test_session_store_setting
- ActionController::Base.session_store = :drb_store
- assert_equal CGI::Session::DRbStore, ActionController::Base.session_store
-
- if Object.const_defined?(:ActiveRecord)
- ActionController::Base.session_store = :active_record_store
- assert_equal CGI::Session::ActiveRecordStore, ActionController::Base.session_store
- end
- end
-
- def test_process_cleanup_with_session_management_support
- @controller = AssociationCachingTestController.new
- get :show
- assert_equal "has cached associations", @response.body
- get :tell
- assert_equal "does not have cached associations", @response.body
- end
-
- def test_session_is_enabled
- @controller = TestController.new
- get :show
- assert_nothing_raised do
- assert_equal false, @controller.session_enabled?
- end
-
- get :tell
- assert @controller.session_enabled?
- end
-end
diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb
index 4c44ea4205..e89d6bb960 100644
--- a/actionpack/test/controller/webservice_test.rb
+++ b/actionpack/test/controller/webservice_test.rb
@@ -2,8 +2,6 @@ require 'abstract_unit'
class WebServiceTest < ActionController::IntegrationTest
class TestController < ActionController::Base
- session :off
-
def assign_parameters
if params[:full]
render :text => dump_params_keys
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 1aaf456c0f..c428366a04 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -60,6 +60,7 @@ module ActiveRecord
autoload :Schema, 'active_record/schema'
autoload :SchemaDumper, 'active_record/schema_dumper'
autoload :Serialization, 'active_record/serialization'
+ autoload :SessionStore, 'active_record/session_store'
autoload :TestCase, 'active_record/test_case'
autoload :Timestamp, 'active_record/timestamp'
autoload :Transactions, 'active_record/transactions'
diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb
new file mode 100644
index 0000000000..bd198c03b2
--- /dev/null
+++ b/activerecord/lib/active_record/session_store.rb
@@ -0,0 +1,319 @@
+module ActiveRecord
+ # A session store backed by an Active Record class. A default class is
+ # provided, but any object duck-typing to an Active Record Session class
+ # with text +session_id+ and +data+ attributes is sufficient.
+ #
+ # The default assumes a +sessions+ tables with columns:
+ # +id+ (numeric primary key),
+ # +session_id+ (text, or longtext if your session data exceeds 65K), and
+ # +data+ (text or longtext; careful if your session data exceeds 65KB).
+ # The +session_id+ column should always be indexed for speedy lookups.
+ # Session data is marshaled to the +data+ column in Base64 format.
+ # If the data you write is larger than the column's size limit,
+ # ActionController::SessionOverflowError will be raised.
+ #
+ # You may configure the table name, primary key, and data column.
+ # For example, at the end of <tt>config/environment.rb</tt>:
+ # ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table'
+ # ActiveRecord::SessionStore::Session.primary_key = 'session_id'
+ # ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data'
+ # Note that setting the primary key to the +session_id+ frees you from
+ # having a separate +id+ column if you don't want it. However, you must
+ # set <tt>session.model.id = session.session_id</tt> by hand! A before filter
+ # on ApplicationController is a good place.
+ #
+ # Since the default class is a simple Active Record, you get timestamps
+ # for free if you add +created_at+ and +updated_at+ datetime columns to
+ # the +sessions+ table, making periodic session expiration a snap.
+ #
+ # You may provide your own session class implementation, whether a
+ # feature-packed Active Record or a bare-metal high-performance SQL
+ # store, by setting
+ # ActiveRecord::SessionStore.session_class = MySessionClass
+ # You must implement these methods:
+ # self.find_by_session_id(session_id)
+ # initialize(hash_of_session_id_and_data)
+ # attr_reader :session_id
+ # attr_accessor :data
+ # save
+ # destroy
+ #
+ # The example SqlBypass class is a generic SQL session store. You may
+ # use it as a basis for high-performance database-specific stores.
+ class SessionStore < ActionController::Session::AbstractStore
+ # The default Active Record class.
+ class Session < ActiveRecord::Base
+ ##
+ # :singleton-method:
+ # Customizable data column name. Defaults to 'data'.
+ cattr_accessor :data_column_name
+ self.data_column_name = 'data'
+
+ before_save :marshal_data!
+ before_save :raise_on_session_data_overflow!
+
+ class << self
+ # Don't try to reload ARStore::Session in dev mode.
+ def reloadable? #:nodoc:
+ false
+ end
+
+ def data_column_size_limit
+ @data_column_size_limit ||= columns_hash[@@data_column_name].limit
+ end
+
+ # Hook to set up sessid compatibility.
+ def find_by_session_id(session_id)
+ setup_sessid_compatibility!
+ find_by_session_id(session_id)
+ end
+
+ def marshal(data)
+ ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
+ end
+
+ def unmarshal(data)
+ Marshal.load(ActiveSupport::Base64.decode64(data)) if data
+ end
+
+ def create_table!
+ connection.execute <<-end_sql
+ CREATE TABLE #{table_name} (
+ id INTEGER PRIMARY KEY,
+ #{connection.quote_column_name('session_id')} TEXT UNIQUE,
+ #{connection.quote_column_name(@@data_column_name)} TEXT(255)
+ )
+ end_sql
+ end
+
+ def drop_table!
+ connection.execute "DROP TABLE #{table_name}"
+ end
+
+ private
+ # Compatibility with tables using sessid instead of session_id.
+ def setup_sessid_compatibility!
+ # Reset column info since it may be stale.
+ reset_column_information
+ if columns_hash['sessid']
+ def self.find_by_session_id(*args)
+ find_by_sessid(*args)
+ end
+
+ define_method(:session_id) { sessid }
+ define_method(:session_id=) { |session_id| self.sessid = session_id }
+ else
+ def self.find_by_session_id(session_id)
+ find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id]
+ end
+ end
+ end
+ end
+
+ # Lazy-unmarshal session state.
+ def data
+ @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
+ end
+
+ attr_writer :data
+
+ # Has the session been loaded yet?
+ def loaded?
+ !!@data
+ end
+
+ private
+ def marshal_data!
+ return false if !loaded?
+ write_attribute(@@data_column_name, self.class.marshal(self.data))
+ end
+
+ # Ensures that the data about to be stored in the database is not
+ # larger than the data storage column. Raises
+ # ActionController::SessionOverflowError.
+ def raise_on_session_data_overflow!
+ return false if !loaded?
+ limit = self.class.data_column_size_limit
+ if loaded? and limit and read_attribute(@@data_column_name).size > limit
+ raise ActionController::SessionOverflowError
+ end
+ end
+ end
+
+ # A barebones session store which duck-types with the default session
+ # store but bypasses Active Record and issues SQL directly. This is
+ # an example session model class meant as a basis for your own classes.
+ #
+ # The database connection, table name, and session id and data columns
+ # are configurable class attributes. Marshaling and unmarshaling
+ # are implemented as class methods that you may override. By default,
+ # marshaling data is
+ #
+ # ActiveSupport::Base64.encode64(Marshal.dump(data))
+ #
+ # and unmarshaling data is
+ #
+ # Marshal.load(ActiveSupport::Base64.decode64(data))
+ #
+ # This marshaling behavior is intended to store the widest range of
+ # binary session data in a +text+ column. For higher performance,
+ # store in a +blob+ column instead and forgo the Base64 encoding.
+ class SqlBypass
+ ##
+ # :singleton-method:
+ # Use the ActiveRecord::Base.connection by default.
+ cattr_accessor :connection
+
+ ##
+ # :singleton-method:
+ # The table name defaults to 'sessions'.
+ cattr_accessor :table_name
+ @@table_name = 'sessions'
+
+ ##
+ # :singleton-method:
+ # The session id field defaults to 'session_id'.
+ cattr_accessor :session_id_column
+ @@session_id_column = 'session_id'
+
+ ##
+ # :singleton-method:
+ # The data field defaults to 'data'.
+ cattr_accessor :data_column
+ @@data_column = 'data'
+
+ class << self
+ def connection
+ @@connection ||= ActiveRecord::Base.connection
+ end
+
+ # Look up a session by id and unmarshal its data if found.
+ def find_by_session_id(session_id)
+ if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
+ new(:session_id => session_id, :marshaled_data => record['data'])
+ end
+ end
+
+ def marshal(data)
+ ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
+ end
+
+ def unmarshal(data)
+ Marshal.load(ActiveSupport::Base64.decode64(data)) if data
+ end
+
+ def create_table!
+ @@connection.execute <<-end_sql
+ CREATE TABLE #{table_name} (
+ id INTEGER PRIMARY KEY,
+ #{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE,
+ #{@@connection.quote_column_name(data_column)} TEXT
+ )
+ end_sql
+ end
+
+ def drop_table!
+ @@connection.execute "DROP TABLE #{table_name}"
+ end
+ end
+
+ attr_reader :session_id
+ attr_writer :data
+
+ # Look for normal and marshaled data, self.find_by_session_id's way of
+ # telling us to postpone unmarshaling until the data is requested.
+ # We need to handle a normal data attribute in case of a new record.
+ def initialize(attributes)
+ @session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data]
+ @new_record = @marshaled_data.nil?
+ end
+
+ def new_record?
+ @new_record
+ end
+
+ # Lazy-unmarshal session state.
+ def data
+ unless @data
+ if @marshaled_data
+ @data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
+ else
+ @data = {}
+ end
+ end
+ @data
+ end
+
+ def loaded?
+ !!@data
+ end
+
+ def save
+ return false if !loaded?
+ marshaled_data = self.class.marshal(data)
+
+ if @new_record
+ @new_record = false
+ @@connection.update <<-end_sql, 'Create session'
+ INSERT INTO #{@@table_name} (
+ #{@@connection.quote_column_name(@@session_id_column)},
+ #{@@connection.quote_column_name(@@data_column)} )
+ VALUES (
+ #{@@connection.quote(session_id)},
+ #{@@connection.quote(marshaled_data)} )
+ end_sql
+ else
+ @@connection.update <<-end_sql, 'Update session'
+ UPDATE #{@@table_name}
+ SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)}
+ WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
+ end_sql
+ end
+ end
+
+ def destroy
+ unless @new_record
+ @@connection.delete <<-end_sql, 'Destroy session'
+ DELETE FROM #{@@table_name}
+ WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
+ end_sql
+ end
+ end
+ end
+
+ # The class used for session storage. Defaults to
+ # ActiveRecord::SessionStore::Session
+ cattr_accessor :session_class
+ self.session_class = Session
+
+ SESSION_RECORD_KEY = 'rack.session.record'.freeze
+
+ private
+ def get_session(env, sid)
+ Base.silence do
+ sid ||= generate_sid
+ session = @@session_class.find_by_session_id(sid)
+ session ||= @@session_class.new(:session_id => sid, :data => {})
+ env[SESSION_RECORD_KEY] = session
+ [sid, session.data]
+ end
+ end
+
+ def set_session(env, sid, session_data)
+ Base.silence do
+ record = env[SESSION_RECORD_KEY]
+ record.data = session_data
+ return false unless record.save
+
+ session_data = record.data
+ if session_data && session_data.respond_to?(:each_value)
+ session_data.each_value do |obj|
+ obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
+ end
+ end
+ end
+
+ return true
+ end
+ end
+end
diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb
index 06a3332c42..56e8ce95ab 100644
--- a/railties/lib/initializer.rb
+++ b/railties/lib/initializer.rb
@@ -39,7 +39,7 @@ module Rails
nil
end
end
-
+
def backtrace_cleaner
@@backtrace_cleaner ||= begin
# Relies on ActiveSupport, so we have to lazy load to postpone definition until AS has been loaded
@@ -148,7 +148,6 @@ module Rails
initialize_dependency_mechanism
initialize_whiny_nils
- initialize_temporary_session_directory
initialize_time_zone
initialize_i18n
@@ -501,13 +500,6 @@ Run `rake gems:install` to install the missing gems.
require('active_support/whiny_nil') if configuration.whiny_nils
end
- def initialize_temporary_session_directory
- if configuration.frameworks.include?(:action_controller)
- session_path = "#{configuration.root_path}/tmp/sessions/"
- ActionController::Base.session_options[:tmpdir] = File.exist?(session_path) ? session_path : Dir::tmpdir
- end
- end
-
# Sets the default value for Time.zone, and turns on ActiveRecord::Base#time_zone_aware_attributes.
# If assigned value cannot be matched to a TimeZone, an exception will be raised.
def initialize_time_zone
@@ -529,7 +521,7 @@ Run `rake gems:install` to install the missing gems.
end
end
- # Set the i18n configuration from config.i18n but special-case for the load_path which should be
+ # Set the i18n configuration from config.i18n but special-case for the load_path which should be
# appended to what's already set instead of overwritten.
def initialize_i18n
configuration.i18n.each do |setting, value|
diff --git a/railties/lib/tasks/databases.rake b/railties/lib/tasks/databases.rake
index 3a576063fa..68ffefae0b 100644
--- a/railties/lib/tasks/databases.rake
+++ b/railties/lib/tasks/databases.rake
@@ -380,7 +380,7 @@ namespace :db do
end
namespace :sessions do
- desc "Creates a sessions migration for use with CGI::Session::ActiveRecordStore"
+ desc "Creates a sessions migration for use with ActiveRecord::SessionStore"
task :create => :environment do
raise "Task unavailable to this database (no migration support)" unless ActiveRecord::Base.connection.supports_migrations?
require 'rails_generator'
diff --git a/railties/test/console_app_test.rb b/railties/test/console_app_test.rb
index 6cfc907b80..cbaf230594 100644
--- a/railties/test/console_app_test.rb
+++ b/railties/test/console_app_test.rb
@@ -4,6 +4,7 @@ require 'action_controller' # console_app uses 'action_controller/integration'
unless defined? ApplicationController
class ApplicationController < ActionController::Base; end
+ ActionController::Base.session_store = nil
end
require 'dispatcher'