mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Extract ActiveRecord::SessionStore from Rails
This functionality will be available from gem `active_record-session_store` instead.
This commit is contained in:
parent
2c571b3f05
commit
0ffe19056c
18 changed files with 10 additions and 947 deletions
|
@ -1,5 +1,9 @@
|
||||||
## Rails 4.0.0 (unreleased) ##
|
## Rails 4.0.0 (unreleased) ##
|
||||||
|
|
||||||
|
* `ActiveRecord::SessionStore` is extracted out of Rails into a gem `activerecord-session_store`.
|
||||||
|
Setting `config.session_store` to `:active_record_store` will no longer work and will break
|
||||||
|
if the `activerecord-session_store` gem isn't available. *Prem Sichanugrist*
|
||||||
|
|
||||||
* Fix select_tag when option_tags is nil.
|
* Fix select_tag when option_tags is nil.
|
||||||
Fixes #7404.
|
Fixes #7404.
|
||||||
|
|
||||||
|
|
|
@ -88,15 +88,6 @@ module ActionController
|
||||||
#
|
#
|
||||||
# Do not put secret information in cookie-based sessions!
|
# Do not put secret information in cookie-based sessions!
|
||||||
#
|
#
|
||||||
# Other options for session storage:
|
|
||||||
#
|
|
||||||
# * 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
|
|
||||||
#
|
|
||||||
# MyApplication::Application.config.session_store :active_record_store
|
|
||||||
#
|
|
||||||
# in your <tt>config/initializers/session_store.rb</tt> and run <tt>script/rails g session_migration</tt>.
|
|
||||||
#
|
|
||||||
# == Responses
|
# == Responses
|
||||||
#
|
#
|
||||||
# Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response
|
# Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response
|
||||||
|
|
|
@ -1,288 +0,0 @@
|
||||||
require 'active_record_unit'
|
|
||||||
|
|
||||||
class ActiveRecordStoreTest < ActionDispatch::IntegrationTest
|
|
||||||
class TestController < ActionController::Base
|
|
||||||
def no_session_access
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_session_value
|
|
||||||
raise "missing session!" unless session
|
|
||||||
session[:foo] = params[:foo] || "bar"
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_session_value
|
|
||||||
render :text => "foo: #{session[:foo].inspect}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_session_id
|
|
||||||
render :text => "#{request.session_options[:id]}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def call_reset_session
|
|
||||||
session[:foo]
|
|
||||||
reset_session
|
|
||||||
reset_session if params[:twice]
|
|
||||||
session[:foo] = "baz"
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
def renew
|
|
||||||
env["rack.session.options"][:renew] = true
|
|
||||||
session[:foo] = "baz"
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def setup
|
|
||||||
ActiveRecord::SessionStore.session_class.create_table!
|
|
||||||
end
|
|
||||||
|
|
||||||
def teardown
|
|
||||||
ActiveRecord::SessionStore.session_class.drop_table!
|
|
||||||
end
|
|
||||||
|
|
||||||
%w{ session sql_bypass }.each do |class_name|
|
|
||||||
define_method("test_setting_and_getting_session_value_with_#{class_name}_store") do
|
|
||||||
with_store class_name do
|
|
||||||
with_test_route_set do
|
|
||||||
get '/set_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert cookies['_session_id']
|
|
||||||
|
|
||||||
get '/get_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert_equal 'foo: "bar"', response.body
|
|
||||||
|
|
||||||
get '/set_session_value', :foo => "baz"
|
|
||||||
assert_response :success
|
|
||||||
assert cookies['_session_id']
|
|
||||||
|
|
||||||
get '/get_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert_equal 'foo: "baz"', response.body
|
|
||||||
|
|
||||||
get '/call_reset_session'
|
|
||||||
assert_response :success
|
|
||||||
assert_not_equal [], headers['Set-Cookie']
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
define_method("test_renewing_with_#{class_name}_store") do
|
|
||||||
with_store class_name do
|
|
||||||
with_test_route_set do
|
|
||||||
get '/set_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert cookies['_session_id']
|
|
||||||
|
|
||||||
get '/renew'
|
|
||||||
assert_response :success
|
|
||||||
assert_not_equal [], headers['Set-Cookie']
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
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 test_calling_reset_session_twice_does_not_raise_errors
|
|
||||||
with_test_route_set do
|
|
||||||
get '/call_reset_session', :twice => "true"
|
|
||||||
assert_response :success
|
|
||||||
|
|
||||||
get '/get_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert_equal 'foo: "baz"', response.body
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_setting_session_value_after_session_reset
|
|
||||||
with_test_route_set do
|
|
||||||
get '/set_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert cookies['_session_id']
|
|
||||||
session_id = cookies['_session_id']
|
|
||||||
|
|
||||||
get '/call_reset_session'
|
|
||||||
assert_response :success
|
|
||||||
assert_not_equal [], headers['Set-Cookie']
|
|
||||||
|
|
||||||
get '/get_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert_equal 'foo: "baz"', response.body
|
|
||||||
|
|
||||||
get '/get_session_id'
|
|
||||||
assert_response :success
|
|
||||||
assert_not_equal session_id, response.body
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_getting_session_value_after_session_reset
|
|
||||||
with_test_route_set do
|
|
||||||
get '/set_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert cookies['_session_id']
|
|
||||||
session_cookie = cookies.send(:hash_for)['_session_id']
|
|
||||||
|
|
||||||
get '/call_reset_session'
|
|
||||||
assert_response :success
|
|
||||||
assert_not_equal [], headers['Set-Cookie']
|
|
||||||
|
|
||||||
cookies << session_cookie # replace our new session_id with our old, pre-reset session_id
|
|
||||||
|
|
||||||
get '/get_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert_equal 'foo: nil', response.body, "data for this session should have been obliterated from the database"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_getting_from_nonexistent_session
|
|
||||||
with_test_route_set do
|
|
||||||
get '/get_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert_equal 'foo: nil', response.body
|
|
||||||
assert_nil cookies['_session_id'], "should only create session on write, not read"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_getting_session_id
|
|
||||||
with_test_route_set do
|
|
||||||
get '/set_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert cookies['_session_id']
|
|
||||||
session_id = cookies['_session_id']
|
|
||||||
|
|
||||||
get '/get_session_id'
|
|
||||||
assert_response :success
|
|
||||||
assert_equal session_id, response.body, "should be able to read session id without accessing the session hash"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_doesnt_write_session_cookie_if_session_id_is_already_exists
|
|
||||||
with_test_route_set do
|
|
||||||
get '/set_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert cookies['_session_id']
|
|
||||||
|
|
||||||
get '/get_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert_equal nil, headers['Set-Cookie'], "should not resend the cookie again if session_id cookie is already exists"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_prevents_session_fixation
|
|
||||||
with_test_route_set do
|
|
||||||
get '/set_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert cookies['_session_id']
|
|
||||||
|
|
||||||
get '/get_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert_equal 'foo: "bar"', response.body
|
|
||||||
session_id = cookies['_session_id']
|
|
||||||
assert session_id
|
|
||||||
|
|
||||||
reset!
|
|
||||||
|
|
||||||
get '/get_session_value', :_session_id => session_id
|
|
||||||
assert_response :success
|
|
||||||
assert_equal 'foo: nil', response.body
|
|
||||||
assert_not_equal session_id, cookies['_session_id']
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_allows_session_fixation
|
|
||||||
with_test_route_set(:cookie_only => false) do
|
|
||||||
get '/set_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert cookies['_session_id']
|
|
||||||
|
|
||||||
get '/get_session_value'
|
|
||||||
assert_response :success
|
|
||||||
assert_equal 'foo: "bar"', response.body
|
|
||||||
session_id = cookies['_session_id']
|
|
||||||
assert session_id
|
|
||||||
|
|
||||||
reset!
|
|
||||||
|
|
||||||
get '/set_session_value', :_session_id => session_id, :foo => "baz"
|
|
||||||
assert_response :success
|
|
||||||
assert_equal session_id, cookies['_session_id']
|
|
||||||
|
|
||||||
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
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_incoming_invalid_session_id_via_cookie_should_be_ignored
|
|
||||||
with_test_route_set do
|
|
||||||
open_session do |sess|
|
|
||||||
sess.cookies['_session_id'] = 'INVALID'
|
|
||||||
|
|
||||||
sess.get '/set_session_value'
|
|
||||||
new_session_id = sess.cookies['_session_id']
|
|
||||||
assert_not_equal 'INVALID', new_session_id
|
|
||||||
|
|
||||||
sess.get '/get_session_value'
|
|
||||||
new_session_id_2 = sess.cookies['_session_id']
|
|
||||||
assert_equal new_session_id, new_session_id_2
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_incoming_invalid_session_id_via_parameter_should_be_ignored
|
|
||||||
with_test_route_set(:cookie_only => false) do
|
|
||||||
open_session do |sess|
|
|
||||||
sess.get '/set_session_value', :_session_id => 'INVALID'
|
|
||||||
new_session_id = sess.cookies['_session_id']
|
|
||||||
assert_not_equal 'INVALID', new_session_id
|
|
||||||
|
|
||||||
sess.get '/get_session_value'
|
|
||||||
new_session_id_2 = sess.cookies['_session_id']
|
|
||||||
assert_equal new_session_id, new_session_id_2
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_session_store_with_all_domains
|
|
||||||
with_test_route_set(:domain => :all) do
|
|
||||||
get '/set_session_value'
|
|
||||||
assert_response :success
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def with_test_route_set(options = {})
|
|
||||||
with_routing do |set|
|
|
||||||
set.draw do
|
|
||||||
get ':action', :to => 'active_record_store_test/test'
|
|
||||||
end
|
|
||||||
|
|
||||||
@app = self.class.build_app(set) do |middleware|
|
|
||||||
middleware.use ActiveRecord::SessionStore, options.reverse_merge(:key => '_session_id')
|
|
||||||
middleware.delete "ActionDispatch::ShowExceptions"
|
|
||||||
end
|
|
||||||
|
|
||||||
yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def with_store(class_name)
|
|
||||||
session_class, ActiveRecord::SessionStore.session_class =
|
|
||||||
ActiveRecord::SessionStore.session_class, "ActiveRecord::SessionStore::#{class_name.camelize}".constantize
|
|
||||||
yield
|
|
||||||
ensure
|
|
||||||
ActiveRecord::SessionStore.session_class = session_class
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,5 +1,8 @@
|
||||||
## Rails 4.0.0 (unreleased) ##
|
## Rails 4.0.0 (unreleased) ##
|
||||||
|
|
||||||
|
* ActiveRecord::SessionStore has been extracted from Active Record as `activerecord-session_store`
|
||||||
|
gem. Please read the `README.md` file on the gem for the usage. *Prem Sichanugrist*
|
||||||
|
|
||||||
* Fix `reset_counters` when there are multiple `belongs_to` association with the
|
* Fix `reset_counters` when there are multiple `belongs_to` association with the
|
||||||
same foreign key and one of them have a counter cache.
|
same foreign key and one of them have a counter cache.
|
||||||
Fixes #5200.
|
Fixes #5200.
|
||||||
|
|
|
@ -53,17 +53,6 @@ module ActiveRecord
|
||||||
autoload :ReadonlyAttributes
|
autoload :ReadonlyAttributes
|
||||||
autoload :Reflection
|
autoload :Reflection
|
||||||
autoload :Sanitization
|
autoload :Sanitization
|
||||||
|
|
||||||
# ActiveRecord::SessionStore depends on the abstract store in Action Pack.
|
|
||||||
# Eager loading this class would break client code that eager loads Active
|
|
||||||
# Record standalone.
|
|
||||||
#
|
|
||||||
# Note that the Rails application generator creates an initializer specific
|
|
||||||
# for setting the session store. Thus, albeit in theory this autoload would
|
|
||||||
# not be thread-safe, in practice it is because if the application uses this
|
|
||||||
# session store its autoload happens at boot time.
|
|
||||||
autoload :SessionStore
|
|
||||||
|
|
||||||
autoload :Schema
|
autoload :Schema
|
||||||
autoload :SchemaDumper
|
autoload :SchemaDumper
|
||||||
autoload :SchemaMigration
|
autoload :SchemaMigration
|
||||||
|
|
|
@ -408,21 +408,6 @@ db_namespace = namespace :db do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :sessions do
|
|
||||||
# desc "Creates a sessions migration for use with ActiveRecord::SessionStore"
|
|
||||||
task :create => [:environment, :load_config] do
|
|
||||||
raise 'Task unavailable to this database (no migration support)' unless ActiveRecord::Base.connection.supports_migrations?
|
|
||||||
Rails.application.load_generators
|
|
||||||
require 'rails/generators/rails/session_migration/session_migration_generator'
|
|
||||||
Rails::Generators::SessionMigrationGenerator.start [ ENV['MIGRATION'] || 'add_sessions_table' ]
|
|
||||||
end
|
|
||||||
|
|
||||||
# desc "Clear the sessions table"
|
|
||||||
task :clear => [:environment, :load_config] do
|
|
||||||
ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::SessionStore::Session.table_name}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :railties do
|
namespace :railties do
|
||||||
|
|
|
@ -1,365 +0,0 @@
|
||||||
require 'action_dispatch/middleware/session/abstract_store'
|
|
||||||
|
|
||||||
module ActiveRecord
|
|
||||||
# = Active Record Session Store
|
|
||||||
#
|
|
||||||
# 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+ (string, usually varchar; maximum length is 255), 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/application.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, options_hash = {})
|
|
||||||
# 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 < ActionDispatch::Session::AbstractStore
|
|
||||||
module ClassMethods # :nodoc:
|
|
||||||
def marshal(data)
|
|
||||||
::Base64.encode64(Marshal.dump(data)) if data
|
|
||||||
end
|
|
||||||
|
|
||||||
def unmarshal(data)
|
|
||||||
Marshal.load(::Base64.decode64(data)) if data
|
|
||||||
end
|
|
||||||
|
|
||||||
def drop_table!
|
|
||||||
connection.schema_cache.clear_table_cache!(table_name)
|
|
||||||
connection.drop_table table_name
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_table!
|
|
||||||
connection.schema_cache.clear_table_cache!(table_name)
|
|
||||||
connection.create_table(table_name) do |t|
|
|
||||||
t.string session_id_column, :limit => 255
|
|
||||||
t.text data_column_name
|
|
||||||
end
|
|
||||||
connection.add_index table_name, session_id_column, :unique => true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# The default Active Record class.
|
|
||||||
class Session < ActiveRecord::Base
|
|
||||||
extend ClassMethods
|
|
||||||
|
|
||||||
##
|
|
||||||
# :singleton-method:
|
|
||||||
# Customizable data column name. Defaults to 'data'.
|
|
||||||
cattr_accessor :data_column_name
|
|
||||||
self.data_column_name = 'data'
|
|
||||||
|
|
||||||
attr_accessible :session_id, :data, :marshaled_data
|
|
||||||
|
|
||||||
before_save :marshal_data!
|
|
||||||
before_save :raise_on_session_data_overflow!
|
|
||||||
|
|
||||||
class << self
|
|
||||||
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
|
|
||||||
|
|
||||||
private
|
|
||||||
def session_id_column
|
|
||||||
'session_id'
|
|
||||||
end
|
|
||||||
|
|
||||||
# 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
|
|
||||||
class << self; remove_possible_method :find_by_session_id; end
|
|
||||||
|
|
||||||
def self.find_by_session_id(session_id)
|
|
||||||
where(session_id: session_id).first
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(attributes = nil, options = {})
|
|
||||||
@data = nil
|
|
||||||
super
|
|
||||||
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 unless loaded?
|
|
||||||
write_attribute(@@data_column_name, self.class.marshal(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 unless loaded?
|
|
||||||
limit = self.class.data_column_size_limit
|
|
||||||
if 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
|
|
||||||
#
|
|
||||||
# ::Base64.encode64(Marshal.dump(data))
|
|
||||||
#
|
|
||||||
# and unmarshaling data is
|
|
||||||
#
|
|
||||||
# Marshal.load(::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
|
|
||||||
extend ClassMethods
|
|
||||||
|
|
||||||
##
|
|
||||||
# :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
|
|
||||||
alias :data_column_name :data_column
|
|
||||||
|
|
||||||
# Use the ActiveRecord::Base.connection by default.
|
|
||||||
attr_writer :connection
|
|
||||||
|
|
||||||
# Use the ActiveRecord::Base.connection_pool by default.
|
|
||||||
attr_writer :connection_pool
|
|
||||||
|
|
||||||
def connection
|
|
||||||
@connection ||= ActiveRecord::Base.connection
|
|
||||||
end
|
|
||||||
|
|
||||||
def connection_pool
|
|
||||||
@connection_pool ||= ActiveRecord::Base.connection_pool
|
|
||||||
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 #{connection.quote_column_name(data_column)} AS data FROM #{@@table_name} WHERE #{connection.quote_column_name(@@session_id_column)}=#{connection.quote(session_id.to_s)}")
|
|
||||||
new(:session_id => session_id, :marshaled_data => record['data'])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
delegate :connection, :connection=, :connection_pool, :connection_pool=, :to => self
|
|
||||||
|
|
||||||
attr_reader :session_id, :new_record
|
|
||||||
alias :new_record? :new_record
|
|
||||||
|
|
||||||
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 = attributes[:session_id]
|
|
||||||
@data = attributes[:data]
|
|
||||||
@marshaled_data = attributes[:marshaled_data]
|
|
||||||
@new_record = @marshaled_data.nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns true if the record is persisted, i.e. it's not a new record
|
|
||||||
def persisted?
|
|
||||||
!@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 unless loaded?
|
|
||||||
marshaled_data = self.class.marshal(data)
|
|
||||||
connect = connection
|
|
||||||
|
|
||||||
if @new_record
|
|
||||||
@new_record = false
|
|
||||||
connect.update <<-end_sql, 'Create session'
|
|
||||||
INSERT INTO #{table_name} (
|
|
||||||
#{connect.quote_column_name(session_id_column)},
|
|
||||||
#{connect.quote_column_name(data_column)} )
|
|
||||||
VALUES (
|
|
||||||
#{connect.quote(session_id)},
|
|
||||||
#{connect.quote(marshaled_data)} )
|
|
||||||
end_sql
|
|
||||||
else
|
|
||||||
connect.update <<-end_sql, 'Update session'
|
|
||||||
UPDATE #{table_name}
|
|
||||||
SET #{connect.quote_column_name(data_column)}=#{connect.quote(marshaled_data)}
|
|
||||||
WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)}
|
|
||||||
end_sql
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
return if @new_record
|
|
||||||
|
|
||||||
connect = connection
|
|
||||||
connect.delete <<-end_sql, 'Destroy session'
|
|
||||||
DELETE FROM #{table_name}
|
|
||||||
WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id.to_s)}
|
|
||||||
end_sql
|
|
||||||
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'
|
|
||||||
ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY
|
|
||||||
|
|
||||||
private
|
|
||||||
def get_session(env, sid)
|
|
||||||
Base.silence do
|
|
||||||
unless sid and session = @@session_class.find_by_session_id(sid)
|
|
||||||
# If the sid was nil or if there is no pre-existing session under the sid,
|
|
||||||
# force the generation of a new sid and associate a new session associated with the new sid
|
|
||||||
sid = generate_sid
|
|
||||||
session = @@session_class.new(:session_id => sid, :data => {})
|
|
||||||
end
|
|
||||||
env[SESSION_RECORD_KEY] = session
|
|
||||||
[sid, session.data]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_session(env, sid, session_data, options)
|
|
||||||
Base.silence do
|
|
||||||
record = get_session_model(env, sid)
|
|
||||||
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
|
|
||||||
|
|
||||||
sid
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy_session(env, session_id, options)
|
|
||||||
if sid = current_session_id(env)
|
|
||||||
Base.silence do
|
|
||||||
get_session_model(env, sid).destroy
|
|
||||||
env[SESSION_RECORD_KEY] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
generate_sid unless options[:drop]
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_session_model(env, sid)
|
|
||||||
if env[ENV_SESSION_OPTIONS_KEY][:id].nil?
|
|
||||||
env[SESSION_RECORD_KEY] = find_session(sid)
|
|
||||||
else
|
|
||||||
env[SESSION_RECORD_KEY] ||= find_session(sid)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_session(id)
|
|
||||||
@@session_class.find_by_session_id(id) ||
|
|
||||||
@@session_class.new(:session_id => id, :data => {})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,24 +0,0 @@
|
||||||
require 'rails/generators/active_record'
|
|
||||||
|
|
||||||
module ActiveRecord
|
|
||||||
module Generators
|
|
||||||
class SessionMigrationGenerator < Base
|
|
||||||
argument :name, :type => :string, :default => "add_sessions_table"
|
|
||||||
|
|
||||||
def create_migration_file
|
|
||||||
migration_template "migration.rb", "db/migrate/#{file_name}.rb"
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
def session_table_name
|
|
||||||
current_table_name = ActiveRecord::SessionStore::Session.table_name
|
|
||||||
if current_table_name == 'session' || current_table_name == 'sessions'
|
|
||||||
current_table_name = ActiveRecord::Base.pluralize_table_names ? 'sessions' : 'session'
|
|
||||||
end
|
|
||||||
current_table_name
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,12 +0,0 @@
|
||||||
class <%= migration_class_name %> < ActiveRecord::Migration
|
|
||||||
def change
|
|
||||||
create_table :<%= session_table_name %> do |t|
|
|
||||||
t.string :session_id, :null => false
|
|
||||||
t.text :data
|
|
||||||
t.timestamps
|
|
||||||
end
|
|
||||||
|
|
||||||
add_index :<%= session_table_name %>, :session_id
|
|
||||||
add_index :<%= session_table_name %>, :updated_at
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,81 +0,0 @@
|
||||||
require 'cases/helper'
|
|
||||||
require 'action_dispatch'
|
|
||||||
require 'active_record/session_store'
|
|
||||||
|
|
||||||
module ActiveRecord
|
|
||||||
class SessionStore
|
|
||||||
class SessionTest < ActiveRecord::TestCase
|
|
||||||
self.use_transactional_fixtures = false
|
|
||||||
|
|
||||||
attr_reader :session_klass
|
|
||||||
|
|
||||||
def setup
|
|
||||||
super
|
|
||||||
ActiveRecord::Base.connection.schema_cache.clear!
|
|
||||||
Session.drop_table! if Session.table_exists?
|
|
||||||
@session_klass = Class.new(Session)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_data_column_name
|
|
||||||
# default column name is 'data'
|
|
||||||
assert_equal 'data', Session.data_column_name
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_table_name
|
|
||||||
assert_equal 'sessions', Session.table_name
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_accessible_attributes
|
|
||||||
assert Session.accessible_attributes.include?(:session_id)
|
|
||||||
assert Session.accessible_attributes.include?(:data)
|
|
||||||
assert Session.accessible_attributes.include?(:marshaled_data)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_create_table!
|
|
||||||
assert !Session.table_exists?
|
|
||||||
Session.create_table!
|
|
||||||
assert Session.table_exists?
|
|
||||||
Session.drop_table!
|
|
||||||
assert !Session.table_exists?
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_find_by_sess_id_compat
|
|
||||||
Session.reset_column_information
|
|
||||||
klass = Class.new(Session) do
|
|
||||||
def self.session_id_column
|
|
||||||
'sessid'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
klass.create_table!
|
|
||||||
|
|
||||||
assert klass.columns_hash['sessid'], 'sessid column exists'
|
|
||||||
session = klass.new(:data => 'hello')
|
|
||||||
session.sessid = "100"
|
|
||||||
session.save!
|
|
||||||
|
|
||||||
found = klass.find_by_session_id("100")
|
|
||||||
assert_equal session, found
|
|
||||||
assert_equal session.sessid, found.session_id
|
|
||||||
ensure
|
|
||||||
klass.drop_table!
|
|
||||||
Session.reset_column_information
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_find_by_session_id
|
|
||||||
Session.create_table!
|
|
||||||
session_id = "10"
|
|
||||||
s = session_klass.create!(:data => 'world', :session_id => session_id)
|
|
||||||
t = session_klass.find_by_session_id(session_id)
|
|
||||||
assert_equal s, t
|
|
||||||
assert_equal s.data, t.data
|
|
||||||
Session.drop_table!
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_loaded?
|
|
||||||
Session.create_table!
|
|
||||||
s = Session.new
|
|
||||||
assert !s.loaded?, 'session is not loaded'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,75 +0,0 @@
|
||||||
require 'cases/helper'
|
|
||||||
require 'action_dispatch'
|
|
||||||
require 'active_record/session_store'
|
|
||||||
|
|
||||||
module ActiveRecord
|
|
||||||
class SessionStore
|
|
||||||
class SqlBypassTest < ActiveRecord::TestCase
|
|
||||||
def setup
|
|
||||||
super
|
|
||||||
Session.drop_table! if Session.table_exists?
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_create_table
|
|
||||||
assert !Session.table_exists?
|
|
||||||
SqlBypass.create_table!
|
|
||||||
assert Session.table_exists?
|
|
||||||
SqlBypass.drop_table!
|
|
||||||
assert !Session.table_exists?
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_new_record?
|
|
||||||
s = SqlBypass.new :data => 'foo', :session_id => 10
|
|
||||||
assert s.new_record?, 'this is a new record!'
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_persisted?
|
|
||||||
s = SqlBypass.new :data => 'foo', :session_id => 10
|
|
||||||
assert !s.persisted?, 'this is a new record!'
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_not_loaded?
|
|
||||||
s = SqlBypass.new({})
|
|
||||||
assert !s.loaded?, 'it is not loaded'
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_loaded?
|
|
||||||
s = SqlBypass.new :data => 'hello'
|
|
||||||
assert s.loaded?, 'it is loaded'
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_save
|
|
||||||
SqlBypass.create_table! unless Session.table_exists?
|
|
||||||
session_id = 20
|
|
||||||
s = SqlBypass.new :data => 'hello', :session_id => session_id
|
|
||||||
s.save
|
|
||||||
t = SqlBypass.find_by_session_id session_id
|
|
||||||
assert_equal s.session_id, t.session_id
|
|
||||||
assert_equal s.data, t.data
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_destroy
|
|
||||||
SqlBypass.create_table! unless Session.table_exists?
|
|
||||||
session_id = 20
|
|
||||||
s = SqlBypass.new :data => 'hello', :session_id => session_id
|
|
||||||
s.save
|
|
||||||
s.destroy
|
|
||||||
assert_nil SqlBypass.find_by_session_id session_id
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_data_column
|
|
||||||
SqlBypass.drop_table! if exists = Session.table_exists?
|
|
||||||
old, SqlBypass.data_column = SqlBypass.data_column, 'foo'
|
|
||||||
SqlBypass.create_table!
|
|
||||||
|
|
||||||
session_id = 20
|
|
||||||
SqlBypass.new(:data => 'hello', :session_id => session_id).save
|
|
||||||
assert_equal 'hello', SqlBypass.find_by_session_id(session_id).data
|
|
||||||
ensure
|
|
||||||
SqlBypass.drop_table!
|
|
||||||
SqlBypass.data_column = old
|
|
||||||
SqlBypass.create_table! if exists
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,8 +1,3 @@
|
||||||
# Be sure to restart your server when you modify this file.
|
# Be sure to restart your server when you modify this file.
|
||||||
|
|
||||||
Blog::Application.config.session_store :cookie_store, key: '_blog_session'
|
Blog::Application.config.session_store :cookie_store, key: '_blog_session'
|
||||||
|
|
||||||
# Use the database for sessions instead of the cookie-based default,
|
|
||||||
# which shouldn't be used to store highly confidential information
|
|
||||||
# (create the session table with "rails generate session_migration")
|
|
||||||
# Blog::Application.config.session_store :active_record_store
|
|
||||||
|
|
|
@ -1491,13 +1491,9 @@ For example, Action Pack uses this method to load the class that provides a cert
|
||||||
<ruby>
|
<ruby>
|
||||||
# action_controller/metal/session_management.rb
|
# action_controller/metal/session_management.rb
|
||||||
def session_store=(store)
|
def session_store=(store)
|
||||||
if store == :active_record_store
|
@@session_store = store.is_a?(Symbol) ?
|
||||||
self.session_store = ActiveRecord::SessionStore
|
ActionDispatch::Session.const_get(store.to_s.camelize) :
|
||||||
else
|
store
|
||||||
@@session_store = store.is_a?(Symbol) ?
|
|
||||||
ActionDispatch::Session.const_get(store.to_s.camelize) :
|
|
||||||
store
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
</ruby>
|
</ruby>
|
||||||
|
|
||||||
|
|
|
@ -125,8 +125,6 @@ module Rails
|
||||||
case @session_store
|
case @session_store
|
||||||
when :disabled
|
when :disabled
|
||||||
nil
|
nil
|
||||||
when :active_record_store
|
|
||||||
ActiveRecord::SessionStore
|
|
||||||
when Symbol
|
when Symbol
|
||||||
ActionDispatch::Session.const_get(@session_store.to_s.camelize)
|
ActionDispatch::Session.const_get(@session_store.to_s.camelize)
|
||||||
else
|
else
|
||||||
|
|
|
@ -173,7 +173,6 @@ module Rails
|
||||||
"#{orm}:migration",
|
"#{orm}:migration",
|
||||||
"#{orm}:model",
|
"#{orm}:model",
|
||||||
"#{orm}:observer",
|
"#{orm}:observer",
|
||||||
"#{orm}:session_migration",
|
|
||||||
"#{test}:controller",
|
"#{test}:controller",
|
||||||
"#{test}:helper",
|
"#{test}:helper",
|
||||||
"#{test}:integration",
|
"#{test}:integration",
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
# Be sure to restart your server when you modify this file.
|
# Be sure to restart your server when you modify this file.
|
||||||
|
|
||||||
<%= app_const %>.config.session_store :cookie_store, key: <%= "'_#{app_name}_session'" %>
|
<%= app_const %>.config.session_store :cookie_store, key: <%= "'_#{app_name}_session'" %>
|
||||||
|
|
||||||
# Use the database for sessions instead of the cookie-based default,
|
|
||||||
# which shouldn't be used to store highly confidential information
|
|
||||||
# (create the session table with "rails generate session_migration").
|
|
||||||
# <%= app_const %>.config.session_store :active_record_store
|
|
||||||
|
|
|
@ -163,26 +163,6 @@ module ApplicationTests
|
||||||
end
|
end
|
||||||
|
|
||||||
# AR
|
# AR
|
||||||
test "database middleware doesn't initialize when session store is not active_record" do
|
|
||||||
add_to_config <<-RUBY
|
|
||||||
config.root = "#{app_path}"
|
|
||||||
config.session_store :cookie_store, { :key => "blahblahblah" }
|
|
||||||
RUBY
|
|
||||||
require "#{app_path}/config/environment"
|
|
||||||
|
|
||||||
assert !Rails.application.config.middleware.include?(ActiveRecord::SessionStore)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "database middleware initializes when session store is active record" do
|
|
||||||
add_to_config "config.session_store :active_record_store"
|
|
||||||
|
|
||||||
require "#{app_path}/config/environment"
|
|
||||||
|
|
||||||
expects = [ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActiveRecord::SessionStore]
|
|
||||||
middleware = Rails.application.config.middleware.map { |m| m.klass }
|
|
||||||
assert_equal expects, middleware & expects
|
|
||||||
end
|
|
||||||
|
|
||||||
test "active_record extensions are applied to ActiveRecord" do
|
test "active_record extensions are applied to ActiveRecord" do
|
||||||
add_to_config "config.active_record.table_name_prefix = 'tbl_'"
|
add_to_config "config.active_record.table_name_prefix = 'tbl_'"
|
||||||
require "#{app_path}/config/environment"
|
require "#{app_path}/config/environment"
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
require 'generators/generators_test_helper'
|
|
||||||
require 'rails/generators/rails/session_migration/session_migration_generator'
|
|
||||||
|
|
||||||
class SessionMigrationGeneratorTest < Rails::Generators::TestCase
|
|
||||||
include GeneratorsTestHelper
|
|
||||||
|
|
||||||
def test_session_migration_with_default_name
|
|
||||||
run_generator
|
|
||||||
assert_migration "db/migrate/add_sessions_table.rb", /class AddSessionsTable < ActiveRecord::Migration/
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_session_migration_with_given_name
|
|
||||||
run_generator ["create_session_table"]
|
|
||||||
assert_migration "db/migrate/create_session_table.rb", /class CreateSessionTable < ActiveRecord::Migration/
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_session_migration_with_custom_table_name
|
|
||||||
ActiveRecord::SessionStore::Session.table_name = "custom_table_name"
|
|
||||||
run_generator
|
|
||||||
assert_migration "db/migrate/add_sessions_table.rb" do |migration|
|
|
||||||
assert_match(/class AddSessionsTable < ActiveRecord::Migration/, migration)
|
|
||||||
assert_match(/create_table :custom_table_name/, migration)
|
|
||||||
end
|
|
||||||
ensure
|
|
||||||
ActiveRecord::SessionStore::Session.table_name = "sessions"
|
|
||||||
end
|
|
||||||
end
|
|
Loading…
Reference in a new issue