require 'active_record' module Merb module SessionMixin def setup_session MERB_LOGGER.info("Setting up session") @session = Merb::Session.persist(cookies) @fingerprint_before = Marshal.dump(@session).hash end def finalize_session MERB_LOGGER.info("Finalize session") unless Marshal.dump(@session).hash == @fingerprint_before @session.save end @headers['Set-Cookie'] = @cookies.map { |k,v| "#{k}=#{escape(v)}; path=/" if v != @cookies[k] } - [nil] end end class Session < ::ActiveRecord::Base # 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! RAND_CHARS = [*'A'..'Z'] + [*'0'..'9'] + [*'a'..'z'] class << self # Generates a new session ID and creates a row for the new session in the database. def generate(cookies) rand_max = RAND_CHARS.size sid = (0...32).inject("") { |ret,_| ret << RAND_CHARS[rand(rand_max)] } sess = create(:session_id => sid, :data => {}) cookies[:session_id] = sess.session_id sess end # Gets the existing session based on the session_id available in cookies. # If none is found, generates a new session. def persist(cookies) if cookies[:session_id] session = find_by_session_id(cookies[:session_id]) end unless session session = generate(cookies) end session end # 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 def marshal(data) Base64.encode64(Marshal.dump(data)) if data end def unmarshal(data) Marshal.load(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 end def [](key) data[key] end def []=(key, val) data[key] = val end # Lazy-unmarshal session state. def data @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {} end # Has the session been loaded yet? def loaded? !! @data end private attr_writer :data 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 MerbController::SessionOverflowError end end end end