moduleActiveRecord# = 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+ (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/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)# 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.classSessionStore<ActionDispatch::Session::AbstractStoremoduleClassMethods# :nodoc:defmarshal(data)ActiveSupport::Base64.encode64(Marshal.dump(data))ifdataenddefunmarshal(data)Marshal.load(ActiveSupport::Base64.decode64(data))ifdataenddefdrop_table!connection.drop_tabletable_nameenddefcreate_table!connection.create_table(table_name)do|t|t.stringsession_id_column,:limit=>255t.textdata_column_nameendconnection.add_indextable_name,session_id_column,:unique=>trueendend# The default Active Record class.classSession<ActiveRecord::BaseextendClassMethods### :singleton-method:# Customizable data column name. Defaults to 'data'.cattr_accessor:data_column_nameself.data_column_name='data'before_save:marshal_data!before_save:raise_on_session_data_overflow!class<<selfdefdata_column_size_limit@data_column_size_limit||=columns_hash[data_column_name].limitend# Hook to set up sessid compatibility.deffind_by_session_id(session_id)setup_sessid_compatibility!find_by_session_id(session_id)endprivatedefsession_id_column'session_id'end# Compatibility with tables using sessid instead of session_id.defsetup_sessid_compatibility!# Reset column info since it may be stale.reset_column_informationifcolumns_hash['sessid']defself.find_by_session_id(*args)find_by_sessid(*args)enddefine_method(:session_id){sessid}define_method(:session_id=){|session_id|self.sessid=session_id}elseclass<<self;remove_method:find_by_session_id;enddefself.find_by_session_id(session_id)find:first,:conditions=>{:session_id=>session_id}endendendenddefinitialize(attributes=nil)@data=nilsuperend# Lazy-unmarshal session state.defdata@data||=self.class.unmarshal(read_attribute(@@data_column_name))||{}endattr_writer:data# Has the session been loaded yet?defloaded?@dataendprivatedefmarshal_data!returnfalseunlessloaded?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.defraise_on_session_data_overflow!returnfalseunlessloaded?limit=self.class.data_column_size_limitiflimitandread_attribute(@@data_column_name).size>limitraiseActionController::SessionOverflowErrorendendend# 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.classSqlBypassextendClassMethods### :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<<selfalias:data_column_name:data_columnremove_method:connectiondefconnection@@connection||=ActiveRecord::Base.connectionend# Look up a session by id and unmarshal its data if found.deffind_by_session_id(session_id)ifrecord=connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{connection.quote(session_id)}")new(:session_id=>session_id,:marshaled_data=>record['data'])endendendattr_reader:session_id,:new_recordalias:new_record?:new_recordattr_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.definitialize(attributes)@session_id=attributes[:session_id]@data=attributes[:data]@marshaled_data=attributes[:marshaled_data]@new_record=@marshaled_data.nil?end# Lazy-unmarshal session state.defdataunless@dataif@marshaled_data@data,@marshaled_data=self.class.unmarshal(@marshaled_data)||{},nilelse@data={}endend@dataenddefloaded?@dataenddefsavereturnfalseunlessloaded?marshaled_data=self.class.marshal(data)connect=connectionif@new_record@new_record=falseconnect.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_sqlelseconnect.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_sqlendenddefdestroyreturnif@new_recordconnect=connectionconnect.delete<<-end_sql,'Destroy session'
DELETE FROM #{table_name}
WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)} end_sqlendend# The class used for session storage. Defaults to# ActiveRecord::SessionStore::Sessioncattr_accessor:session_classself.session_class=SessionSESSION_RECORD_KEY='rack.session.record'privatedefget_session(env,sid)Base.silencedosid||=generate_sidsession=find_session(sid)env[SESSION_RECORD_KEY]=session[sid,session.data]endenddefset_session(env,sid,session_data)Base.silencedorecord=get_session_model(env,sid)record.data=session_datareturnfalseunlessrecord.savesession_data=record.dataifsession_data&&session_data.respond_to?(:each_value)session_data.each_valuedo|obj|obj.clear_association_cacheifobj.respond_to?(:clear_association_cache)endendendsidenddefdestroy(env)ifsid=current_session_id(env)Base.silencedoget_session_model(env,sid).destroyendendenddefget_session_model(env,sid)ifenv[ENV_SESSION_OPTIONS_KEY][:id].nil?env[SESSION_RECORD_KEY]=find_session(sid)elseenv[SESSION_RECORD_KEY]||=find_session(sid)endenddeffind_session(id)@@session_class.find_by_session_id(id)||@@session_class.new(:session_id=>id,:data=>{})endendend