2009-02-04 21:26:08 +01:00
require 'rack/utils'
module ActionController
module Session
2010-09-05 22:24:15 +02:00
class AbstractStore
2009-02-04 21:26:08 +01:00
ENV_SESSION_KEY = 'rack.session' . freeze
ENV_SESSION_OPTIONS_KEY = 'rack.session.options' . freeze
HTTP_COOKIE = 'HTTP_COOKIE' . freeze
SET_COOKIE = 'Set-Cookie' . freeze
2010-09-05 22:24:15 +02:00
# thin wrapper around Hash that allows us to lazily
# load session id into session_options
class OptionsHash < Hash
def initialize ( by , env , default_options )
@by = by
@env = env
@session_id_loaded = false
merge! ( default_options )
end
def [] ( key )
if key == :id
load_session_id! unless super ( :id ) || has_session_id?
end
super ( key )
end
private
def has_session_id?
@session_id_loaded
end
def load_session_id!
self [ :id ] = @by . send ( :extract_session_id , @env )
@session_id_loaded = true
end
end
2009-02-04 21:26:08 +01:00
class SessionHash < Hash
def initialize ( by , env )
super ( )
@by = by
@env = env
@loaded = false
end
def session_id
ActiveSupport :: Deprecation . warn (
2009-02-28 02:23:00 +01:00
" ActionController::Session::AbstractStore::SessionHash # session_id " +
" has been deprecated. Please use request.session_options[:id] instead. " , caller )
@env [ ENV_SESSION_OPTIONS_KEY ] [ :id ]
2009-02-04 21:26:08 +01:00
end
def [] ( key )
2010-09-05 22:24:15 +02:00
load_for_read!
super
end
def has_key? ( key )
load_for_read!
2009-02-04 21:26:08 +01:00
super
end
def []= ( key , value )
2010-09-05 22:24:15 +02:00
load_for_write!
super
end
def clear
load_for_write!
2009-02-04 21:26:08 +01:00
super
end
def to_hash
2010-09-05 22:24:15 +02:00
load_for_read!
2009-02-04 21:26:08 +01:00
h = { } . replace ( self )
h . delete_if { | k , v | v . nil? }
h
end
2010-09-05 22:24:15 +02:00
def update ( hash )
load_for_write!
super
end
def delete ( key )
load_for_write!
super
end
2009-02-04 21:26:08 +01:00
def data
ActiveSupport :: Deprecation . warn (
2009-02-28 02:23:00 +01:00
" ActionController::Session::AbstractStore::SessionHash # data " +
" has been deprecated. Please use # to_hash instead. " , caller )
2009-02-04 21:26:08 +01:00
to_hash
end
2009-02-28 02:23:00 +01:00
def inspect
2010-09-05 22:24:15 +02:00
load_for_read!
2009-02-28 02:23:00 +01:00
super
end
2010-09-05 22:24:15 +02:00
def exists?
return @exists if instance_variable_defined? ( :@exists )
@exists = @by . send ( :exists? , @env )
end
def loaded?
@loaded
end
def destroy
clear
@by . send ( :destroy , @env ) if @by
@env [ ENV_SESSION_OPTIONS_KEY ] [ :id ] = nil if @env && @env [ ENV_SESSION_OPTIONS_KEY ]
@loaded = false
end
2009-02-04 21:26:08 +01:00
private
2010-09-05 22:24:15 +02:00
def load_for_read!
load ! if ! loaded? && exists?
2009-02-04 21:26:08 +01:00
end
2010-09-05 22:24:15 +02:00
def load_for_write!
load ! unless loaded?
2009-02-28 02:23:00 +01:00
end
2010-09-05 22:24:15 +02:00
def load!
id , session = @by . send ( :load_session , @env )
@env [ ENV_SESSION_OPTIONS_KEY ] [ :id ] = id
replace ( session )
@loaded = true
2009-02-04 21:26:08 +01:00
end
2010-09-05 22:24:15 +02:00
2009-02-04 21:26:08 +01:00
end
DEFAULT_OPTIONS = {
:key = > '_session_id' ,
:path = > '/' ,
:domain = > nil ,
:expire_after = > nil ,
:secure = > false ,
:httponly = > true ,
:cookie_only = > true
}
def initialize ( app , options = { } )
# Process legacy CGI options
options = options . symbolize_keys
if options . has_key? ( :session_path )
2010-05-25 19:45:45 +02:00
ActiveSupport :: Deprecation . warn " Giving :session_path to SessionStore is deprecated, " <<
" please use :path instead " , caller
2009-02-04 21:26:08 +01:00
options [ :path ] = options . delete ( :session_path )
end
if options . has_key? ( :session_key )
2010-05-25 19:45:45 +02:00
ActiveSupport :: Deprecation . warn " Giving :session_key to SessionStore is deprecated, " <<
" please use :key instead " , caller
2009-02-04 21:26:08 +01:00
options [ :key ] = options . delete ( :session_key )
end
if options . has_key? ( :session_http_only )
2010-05-25 19:45:45 +02:00
ActiveSupport :: Deprecation . warn " Giving :session_http_only to SessionStore is deprecated, " <<
" please use :httponly instead " , caller
2009-02-04 21:26:08 +01:00
options [ :httponly ] = options . delete ( :session_http_only )
end
@app = app
@default_options = DEFAULT_OPTIONS . merge ( options )
@key = @default_options [ :key ]
@cookie_only = @default_options [ :cookie_only ]
end
def call ( env )
2010-09-05 22:24:15 +02:00
prepare! ( env )
2009-02-04 21:26:08 +01:00
response = @app . call ( env )
session_data = env [ ENV_SESSION_KEY ]
options = env [ ENV_SESSION_OPTIONS_KEY ]
2010-09-05 22:24:15 +02:00
if ! session_data . is_a? ( AbstractStore :: SessionHash ) || session_data . loaded? || options [ :expire_after ]
session_data . send ( :load! ) if session_data . is_a? ( AbstractStore :: SessionHash ) && ! session_data . loaded?
2009-02-04 21:26:08 +01:00
2009-02-28 02:23:00 +01:00
sid = options [ :id ] || generate_sid
2009-02-04 21:26:08 +01:00
unless set_session ( env , sid , session_data . to_hash )
return response
end
2010-09-05 22:24:15 +02:00
if ( env [ " rack.request.cookie_hash " ] && env [ " rack.request.cookie_hash " ] [ @key ] != sid ) || options [ :expire_after ]
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 ]
unless headers [ SET_COOKIE ] . blank?
headers [ SET_COOKIE ] << " \n #{ cookie } "
else
headers [ SET_COOKIE ] = cookie
end
2009-02-04 21:26:08 +01:00
end
end
response
end
private
2010-09-05 22:24:15 +02:00
def prepare! ( env )
env [ ENV_SESSION_KEY ] = SessionHash . new ( self , env )
env [ ENV_SESSION_OPTIONS_KEY ] = OptionsHash . new ( self , env , @default_options )
end
2009-02-04 21:26:08 +01:00
def generate_sid
ActiveSupport :: SecureRandom . hex ( 16 )
end
def load_session ( env )
2010-09-05 22:24:15 +02:00
stale_session_check! do
sid = current_session_id ( env )
sid , session = get_session ( env , sid )
[ sid , session ]
end
end
def extract_session_id ( env )
stale_session_check! do
request = Rack :: Request . new ( env )
sid = request . cookies [ @key ]
sid || = request . params [ @key ] unless @cookie_only
sid
2009-02-04 21:26:08 +01:00
end
2010-09-05 22:24:15 +02:00
end
def current_session_id ( env )
env [ ENV_SESSION_OPTIONS_KEY ] [ :id ]
end
def exists? ( env )
current_session_id ( env ) . present?
2009-02-04 21:26:08 +01:00
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
2010-09-05 22:24:15 +02:00
def destroy ( env )
raise '#destroy needs to be implemented.'
end
module SessionUtils
private
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 , " Session contains objects whose class definition isn \\ 't available. \n Remember to require the classes for all objects kept in the session. \n (Original exception: \# {const_error.message} [ \# {const_error.class}]) \n "
end
retry
else
raise
end
end
end
include SessionUtils
2009-02-04 21:26:08 +01:00
end
end
end