class CGI class Session class ActiveRecordStore class FastSessions # Use the ActiveRecord::Base.connection by default. cattr_accessor :connection @@connection = ActiveRecord::Base.connection # The table name defaults to 'fast_sessions'. cattr_accessor :table_name @@table_name = 'fast_sessions' # If you're going to use this module with MySQL 5.1.22+, then you'd # like to set this to +true+ because it will provide you with consecutive # data inserts in InnoDB. Another cases when you'd like to use it is when # your MySQL server is I/O bound now and you do not want to add random I/O # because of randomized primary key. cattr_accessor :use_auto_increment @@use_auto_increment = false # If you do not like to loose old sessions created with default AR sessions plugin, # set this to +true+ and all session reads will fall back to old sessions reads if # some session_id was not found in fast_sessions table cattr_accessor :fallback_to_old_table @@fallback_to_old_table = false # Old AR sessions table name defaults to 'sessions'. cattr_accessor :old_table_name @@old_table_name = 'sessions' # Look up a session by id and create session object from found data # If record has not been found, we'll create a fake session with empty data # to prevent AR from creation of a new session record. def self.find_by_session_id(session_id) rec = @@connection.select_one <<-end_sql, 'Load Session' SELECT data FROM #{@@table_name} WHERE session_id_crc = CRC32(#{@@connection.quote(session_id)}) AND session_id = #{@@connection.quote(session_id)} end_sql if !rec && @@fallback_to_old_table rec = @@connection.select_one <<-end_sql, 'Load Session (old)' SELECT data FROM #{@@old_table_name} WHERE session_id = #{@@connection.quote(session_id)} end_sql end session_data = rec ? rec['data'] : nil new(:session_id => session_id, :marshaled_data => session_data) end # Marshaling functions def self.marshal(data) Base64.encode64(Marshal.dump(data)) if data end def self.unmarshal(data) Marshal.load(Base64.decode64(data)) if data end # Create table for this session storage def self.create_table! # If user asked us to use auto_increment, # then we need to add this field to the table if @@use_auto_increment autoinc_id_field = "id INT(10) UNSIGNED NOT NULL auto_increment," autoinc_primary_key = "PRIMARY KEY(id)," else autoinc_primary_key = autoinc_id_field = "" end # Creating table @@connection.execute <<-end_sql CREATE TABLE #{table_name} ( #{autoinc_id_field} session_id_crc INT(10) UNSIGNED NOT NULL, session_id VARCHAR(32) NOT NULL, updated_at TIMESTAMP NOT NULL, data TEXT, #{autoinc_primary_key} UNIQUE KEY `session_id` (session_id_crc, session_id), KEY `updated_at` (`updated_at`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; end_sql end # Drop session storage table def self.drop_table! @@connection.execute "DROP TABLE #{table_name}" end # Delete old sessions from the storage table # _seconds_ parameter specifies how long you want to store your sessions. # By default, sessions stored for 1 week def self.delete_old!(seconds = 604800) @@connection.execute "DELETE FROM #{table_name} WHERE updated_at < UNIX_TIMESTAMP(NOW()) - #{seconds}" end #----------------------------------------------------------------------- attr_reader :session_id attr_writer :data # Create session object from provided data (marshaled or not) def initialize(attributes) @session_id = attributes[:session_id] @data = attributes[:data] @marshaled_data = attributes[:marshaled_data] || self.class.marshal({}) end # Lazy-unmarshal session state. def data @data ||= self.class.unmarshal(@marshaled_data) end # Create/update session if session data has been changed during a request processing def save return unless should_save_session? # Marshal data before saving marshaled_data = self.class.marshal(data) # Save data to DB @@connection.update <<-end_sql, 'Create/Update session' INSERT INTO #{@@table_name} SET data = #{@@connection.quote(marshaled_data)}, updated_at = NOW(), session_id_crc = CRC32(#{@@connection.quote(session_id)}), session_id = #{@@connection.quote(session_id)} ON DUPLICATE KEY UPDATE data = #{@@connection.quote(marshaled_data)}, updated_at = NOW() end_sql end # Destroy current session record def destroy @@connection.delete <<-end_sql, 'Destroy session' DELETE FROM #{@@table_name} WHERE session_id_crc = CRC32(#{@@connection.quote(session_id)}) AND session_id = #{@@connection.quote(session_id)} end_sql end private # Returns true if session should be saved, which is # when session data has been changed and user did not # requested skipping data saving def should_save_session? # Do not save data if user asked to skip saving or force saving it was requested force_saving = data.delete(:force_session_saving) skip_saving = data.delete(:skip_session_saving) # Forced saving has higher priority return true if force_saving return false if skip_saving # Handle a special case (original session was empty and new session has only an empty flash) if (self.class.unmarshal(@marshaled_data) == {}) return false if data.empty? || (data.keys == ["flash"] && data["flash"].empty?) end # We can update data if something changed in session hash self.class.marshal(data) != @marshaled_data end end end end end