| 11 | | # Active Record database-based session storage class. |
|---|
| 12 | | # |
|---|
| 13 | | # Implements session storage in a database using the ActiveRecord ORM library. Assumes that the database |
|---|
| 14 | | # has a table called +sessions+ with columns +id+ (numeric, primary key), +sessid+ and +data+ (text). |
|---|
| 15 | | # The session data is stored in the +data+ column in the binary Marshal format; the user is responsible for ensuring that |
|---|
| 16 | | # only data that can be Marshaled is stored in the session. |
|---|
| 17 | | # |
|---|
| 18 | | # Adding +created_at+ or +updated_at+ datetime columns to the sessions table will enable stamping of the data, which can |
|---|
| 19 | | # be used to clear out old sessions. |
|---|
| 20 | | # |
|---|
| 21 | | # It's highly recommended to have an index on the sessid column to improve performance. |
|---|
| | 8 | # A session store backed by an Active Record class. |
|---|
| | 9 | # |
|---|
| | 10 | # A default class is provided, but any object duck-typing to an Active |
|---|
| | 11 | # Record +Session+ class with text +session_id+ and +data+ attributes |
|---|
| | 12 | # may be used as the backing store. |
|---|
| | 13 | # |
|---|
| | 14 | # The default assumes a +sessions+ tables with columns +id+ (numeric |
|---|
| | 15 | # primary key), +session_id+ (text), and +data+ (text). Session data is |
|---|
| | 16 | # marshaled to +data+. +session_id+ should be indexed for speedy lookups. |
|---|
| | 17 | # |
|---|
| | 18 | # Since the default class is a simple Active Record, you get timestamps |
|---|
| | 19 | # for free if you add +created_at+ and +updated_at+ datetime columns to |
|---|
| | 20 | # the +sessions+ table, making periodic session expiration a snap. |
|---|
| | 21 | # |
|---|
| | 22 | # You may provide your own session class, whether a feature-packed |
|---|
| | 23 | # Active Record or a bare-metal high-performance SQL store, by setting |
|---|
| | 24 | # +CGI::Session::ActiveRecordStore.session_class = MySessionClass+ |
|---|
| | 25 | # You must implement these methods: |
|---|
| | 26 | # self.find_by_session_id(session_id) |
|---|
| | 27 | # initialize(hash_of_session_id_and_data) |
|---|
| | 28 | # attr_reader :session_id |
|---|
| | 29 | # attr_accessor :data |
|---|
| | 30 | # save! |
|---|
| | 31 | # destroy |
|---|
| | 32 | # |
|---|
| | 33 | # The fast SqlBypass class is a generic SQL session store. You may |
|---|
| | 34 | # use it as a basis for high-performance database-specific stores. |
|---|
| 25 | | end |
|---|
| 26 | | |
|---|
| 27 | | # Create a new ActiveRecordStore instance. This constructor is used internally by CGI::Session. |
|---|
| 28 | | # The user does not generally need to call it directly. |
|---|
| | 38 | self.table_name = 'sessions' |
|---|
| | 39 | before_create :marshal_data! |
|---|
| | 40 | before_update :marshal_data_if_changed! |
|---|
| | 41 | after_save :clear_data_cache! |
|---|
| | 42 | |
|---|
| | 43 | class << self |
|---|
| | 44 | # Hook to set up sessid compatibility. |
|---|
| | 45 | def find_by_session_id(session_id) |
|---|
| | 46 | setup_sessid_compatibility! |
|---|
| | 47 | find_by_session_id(session_id) |
|---|
| | 48 | end |
|---|
| | 49 | |
|---|
| | 50 | # Compatibility with tables using sessid instead of session_id. |
|---|
| | 51 | def setup_sessid_compatibility! |
|---|
| | 52 | if !@sessid_compatibility_checked |
|---|
| | 53 | if columns_hash['sessid'] |
|---|
| | 54 | def self.find_by_session_id(*args) |
|---|
| | 55 | find_by_sessid(*args) |
|---|
| | 56 | end |
|---|
| | 57 | |
|---|
| | 58 | alias_method :session_id, :sessid |
|---|
| | 59 | define_method(:session_id) { sessid } |
|---|
| | 60 | define_method(:session_id=) { |session_id| self.sessid = session_id } |
|---|
| | 61 | else |
|---|
| | 62 | def self.find_by_session_id(session_id) |
|---|
| | 63 | find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id] |
|---|
| | 64 | end |
|---|
| | 65 | end |
|---|
| | 66 | @sessid_compatibility_checked = true |
|---|
| | 67 | end |
|---|
| | 68 | end |
|---|
| | 69 | |
|---|
| | 70 | def marshal(data) Base64.encode64(Marshal.dump(data)) end |
|---|
| | 71 | def unmarshal(data) Marshal.load(Base64.decode64(data)) end |
|---|
| | 72 | def fingerprint(data) Digest::MD5.hexdigest(data) end |
|---|
| | 73 | |
|---|
| | 74 | def create_table! |
|---|
| | 75 | connection.execute <<-end_sql |
|---|
| | 76 | CREATE TABLE #{table_name} ( |
|---|
| | 77 | id INTEGER PRIMARY KEY, |
|---|
| | 78 | #{connection.quote_column_name('session_id')} TEXT UNIQUE, |
|---|
| | 79 | #{connection.quote_column_name('data')} TEXT |
|---|
| | 80 | ) |
|---|
| | 81 | end_sql |
|---|
| | 82 | end |
|---|
| | 83 | |
|---|
| | 84 | def drop_table! |
|---|
| | 85 | connection.execute "DROP TABLE #{table_name}" |
|---|
| | 86 | end |
|---|
| | 87 | end |
|---|
| | 88 | |
|---|
| | 89 | # Lazy-unmarshal session state. |
|---|
| | 90 | def data |
|---|
| | 91 | unless @data |
|---|
| | 92 | @data = self.class.unmarshal(read_attribute('data')) |
|---|
| | 93 | @fingerprint = self.class.fingerprint(@data) |
|---|
| | 94 | end |
|---|
| | 95 | @data |
|---|
| | 96 | end |
|---|
| | 97 | |
|---|
| | 98 | private |
|---|
| | 99 | def marshal_data! |
|---|
| | 100 | write_attribute('data', self.class.marshal(@data || {})) |
|---|
| | 101 | end |
|---|
| | 102 | |
|---|
| | 103 | def marshal_data_if_changed! |
|---|
| | 104 | if @data and @fingerprint != self.class.fingerprint(@data) |
|---|
| | 105 | marshal_data! |
|---|
| | 106 | end |
|---|
| | 107 | end |
|---|
| | 108 | |
|---|
| | 109 | def clear_data_cache! |
|---|
| | 110 | @data = @fingerprint = nil |
|---|
| | 111 | end |
|---|
| | 112 | end |
|---|
| | 113 | |
|---|
| | 114 | # A barebones session store which duck-types with the default session |
|---|
| | 115 | # store but bypasses Active Record and issues SQL directly. |
|---|
| 32 | | # +option+ is currently ignored as no options are recognized. |
|---|
| 33 | | # |
|---|
| 34 | | # This session's ActiveRecord database row will be created if it does not exist, or opened if it does. |
|---|
| 35 | | def initialize(session, option=nil) |
|---|
| 36 | | ActiveRecord::Base.silence do |
|---|
| 37 | | @session = Session.find_by_sessid(session.session_id) || Session.new("sessid" => session.session_id, "data" => marshalize({})) |
|---|
| 38 | | @data = unmarshalize(@session.data) |
|---|
| 39 | | end |
|---|
| 40 | | end |
|---|
| 41 | | |
|---|
| 42 | | # Update and close the session's ActiveRecord object. |
|---|
| | 123 | # This marshaling behavior is intended to store the widest range of |
|---|
| | 124 | # binary session data in a +text+ column. For higher performance, |
|---|
| | 125 | # store in a +blob+ column instead and forgo the Base64 encoding. |
|---|
| | 126 | class SqlBypass |
|---|
| | 127 | # Use the ActiveRecord::Base.connection by default. |
|---|
| | 128 | cattr_accessor :connection |
|---|
| | 129 | def self.connection |
|---|
| | 130 | @@connection ||= ActiveRecord::Base.connection |
|---|
| | 131 | end |
|---|
| | 132 | |
|---|
| | 133 | # The table name defaults to 'sessions'. |
|---|
| | 134 | cattr_accessor :table_name |
|---|
| | 135 | @@table_name = 'sessions' |
|---|
| | 136 | |
|---|
| | 137 | # The session id field defaults to 'session_id'. |
|---|
| | 138 | cattr_accessor :session_id_column |
|---|
| | 139 | @@session_id_column = 'session_id' |
|---|
| | 140 | |
|---|
| | 141 | # The data field defaults to 'data'. |
|---|
| | 142 | cattr_accessor :data_column |
|---|
| | 143 | @@data_column = 'data' |
|---|
| | 144 | |
|---|
| | 145 | class << self |
|---|
| | 146 | # Look up a session by id and unmarshal its data if found. |
|---|
| | 147 | def find_by_session_id(session_id) |
|---|
| | 148 | if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}") |
|---|
| | 149 | new(:session_id => session_id, :marshaled_data => record['data']) |
|---|
| | 150 | end |
|---|
| | 151 | end |
|---|
| | 152 | |
|---|
| | 153 | def marshal(data) Base64.encode64(Marshal.dump(data)) end |
|---|
| | 154 | def unmarshal(data) Marshal.load(Base64.decode64(data)) end |
|---|
| | 155 | def fingerprint(data) Digest::MD5.hexdigest(data) end |
|---|
| | 156 | |
|---|
| | 157 | def create_table! |
|---|
| | 158 | @@connection.execute <<-end_sql |
|---|
| | 159 | CREATE TABLE #{table_name} ( |
|---|
| | 160 | #{@@connection.quote_column_name(session_id_column)} TEXT PRIMARY KEY, |
|---|
| | 161 | #{@@connection.quote_column_name(data_column)} TEXT |
|---|
| | 162 | ) |
|---|
| | 163 | end_sql |
|---|
| | 164 | end |
|---|
| | 165 | |
|---|
| | 166 | def drop_table! |
|---|
| | 167 | @@connection.execute "DROP TABLE #{table_name}" |
|---|
| | 168 | end |
|---|
| | 169 | end |
|---|
| | 170 | |
|---|
| | 171 | attr_reader :session_id |
|---|
| | 172 | attr_writer :data |
|---|
| | 173 | |
|---|
| | 174 | # Look for normal and marshaled data, self.find_by_session_id's way of |
|---|
| | 175 | # telling us to postpone unmarshaling until the data is requested. |
|---|
| | 176 | # We need to handle a normal data attribute in case of a new record. |
|---|
| | 177 | def initialize(attributes) |
|---|
| | 178 | @session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data] |
|---|
| | 179 | @new_record = !@marshaled_data.nil? |
|---|
| | 180 | end |
|---|
| | 181 | |
|---|
| | 182 | # Lazy-unmarshal session state. Take a fingerprint so we can detect |
|---|
| | 183 | # whether to save changes later. |
|---|
| | 184 | def data |
|---|
| | 185 | if @marshaled_data |
|---|
| | 186 | @data, @marshaled_data = self.class.unmarshal(@marshaled_data), nil |
|---|
| | 187 | @fingerprint = self.class.fingerprint(@data) |
|---|
| | 188 | end |
|---|
| | 189 | @data |
|---|
| | 190 | end |
|---|
| | 191 | |
|---|
| | 192 | def save! |
|---|
| | 193 | if @new_record |
|---|
| | 194 | @new_record = false |
|---|
| | 195 | @@connection.update <<-end_sql, 'Create session' |
|---|
| | 196 | INSERT INTO #{@@table_name} ( |
|---|
| | 197 | #{@@connection.quote_column_name(@@session_id_column)}, |
|---|
| | 198 | #{@@connection.quote_column_name(@@data_column)} ) |
|---|
| | 199 | VALUES ( |
|---|
| | 200 | #{@@connection.quote(session_id)}, |
|---|
| | 201 | #{@@connection.quote(self.class.marshal(data))} ) |
|---|
| | 202 | end_sql |
|---|
| | 203 | elsif self.class.fingerprint(data) != @fingerprint |
|---|
| | 204 | @@connection.update <<-end_sql, 'Update session' |
|---|
| | 205 | UPDATE #{@@table_name} |
|---|
| | 206 | SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(self.class.marshal(data))} |
|---|
| | 207 | WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)} |
|---|
| | 208 | end_sql |
|---|
| | 209 | end |
|---|
| | 210 | end |
|---|
| | 211 | |
|---|
| | 212 | def destroy |
|---|
| | 213 | unless @new_record |
|---|
| | 214 | @@connection.delete <<-end_sql, 'Destroy session' |
|---|
| | 215 | DELETE FROM #{@@table_name} |
|---|
| | 216 | WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)} |
|---|
| | 217 | end_sql |
|---|
| | 218 | end |
|---|
| | 219 | end |
|---|
| | 220 | end |
|---|
| | 221 | |
|---|
| | 222 | # The class used for session storage. Defaults to |
|---|
| | 223 | # CGI::Session::ActiveRecordStore::Session. |
|---|
| | 224 | cattr_accessor :session_class |
|---|
| | 225 | @@session_class = Session |
|---|
| | 226 | |
|---|
| | 227 | # Find or instantiate a session given a CGI::Session. |
|---|
| | 228 | def initialize(session, option = nil) |
|---|
| | 229 | session_id = session.session_id |
|---|
| | 230 | unless @session = @@session_class.find_by_session_id(session_id) |
|---|
| | 231 | unless session.new_session |
|---|
| | 232 | raise CGI::Session::NoSession, 'uninitialized session' |
|---|
| | 233 | end |
|---|
| | 234 | @session = @@session_class.new(:session_id => session_id, :data => {}) |
|---|
| | 235 | end |
|---|
| | 236 | end |
|---|
| | 237 | |
|---|
| | 238 | # Restore session state. The session model handles unmarshaling. |
|---|
| | 239 | def restore |
|---|
| | 240 | @session.data |
|---|
| | 241 | end |
|---|
| | 242 | |
|---|
| | 243 | # Save session store. |
|---|
| | 244 | def update |
|---|
| | 245 | @session.save! |
|---|
| | 246 | end |
|---|
| | 247 | |
|---|
| | 248 | # Save and close the session store. |
|---|