Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/actionpack/lib/action_controller/session/active_record_store.rb

Revision 8433, 11.7 kB (checked in by bitsweat, 5 months ago)

Ruby 1.9 compat: move from the deprecated Base64 module to ActiveSupport::Base64. Closes #10554.

Line 
1 require 'cgi'
2 require 'cgi/session'
3 require 'digest/md5'
4
5 class CGI
6   class Session
7     attr_reader :data
8
9     # Return this session's underlying Session instance. Useful for the DB-backed session stores.
10     def model
11       @dbman.model if @dbman
12     end
13
14
15     # A session store backed by an Active Record class.  A default class is
16     # provided, but any object duck-typing to an Active Record +Session+ class
17     # with text +session_id+ and +data+ attributes is sufficient.
18     #
19     # The default assumes a +sessions+ tables with columns:
20     #   +id+ (numeric primary key),
21     #   +session_id+ (text, or longtext if your session data exceeds 65K), and
22     #   +data+ (text or longtext; careful if your session data exceeds 65KB).
23     # The +session_id+ column should always be indexed for speedy lookups.
24     # Session data is marshaled to the +data+ column in Base64 format.
25     # If the data you write is larger than the column's size limit,
26     # ActionController::SessionOverflowError will be raised.
27     #
28     # You may configure the table name, primary key, and data column.
29     # For example, at the end of config/environment.rb:
30     #   CGI::Session::ActiveRecordStore::Session.table_name = 'legacy_session_table'
31     #   CGI::Session::ActiveRecordStore::Session.primary_key = 'session_id'
32     #   CGI::Session::ActiveRecordStore::Session.data_column_name = 'legacy_session_data'
33     # Note that setting the primary key to the session_id frees you from
34     # having a separate id column if you don't want it.  However, you must
35     # set session.model.id = session.session_id by hand!  A before_filter
36     # on ApplicationController is a good place.
37     #
38     # Since the default class is a simple Active Record, you get timestamps
39     # for free if you add +created_at+ and +updated_at+ datetime columns to
40     # the +sessions+ table, making periodic session expiration a snap.
41     #
42     # You may provide your own session class implementation, whether a
43     # feature-packed Active Record or a bare-metal high-performance SQL
44     # store, by setting
45     #   +CGI::Session::ActiveRecordStore.session_class = MySessionClass+
46     # You must implement these methods:
47     #   self.find_by_session_id(session_id)
48     #   initialize(hash_of_session_id_and_data)
49     #   attr_reader :session_id
50     #   attr_accessor :data
51     #   save
52     #   destroy
53     #
54     # The example SqlBypass class is a generic SQL session store.  You may
55     # use it as a basis for high-performance database-specific stores.
56     class ActiveRecordStore
57       # The default Active Record class.
58       class Session < ActiveRecord::Base
59         # Customizable data column name.  Defaults to 'data'.
60         cattr_accessor :data_column_name
61         self.data_column_name = 'data'
62
63         before_save :marshal_data!
64         before_save :raise_on_session_data_overflow!
65
66         class << self
67           # Don't try to reload ARStore::Session in dev mode.
68           def reloadable? #:nodoc:
69             false
70           end
71
72           def data_column_size_limit
73             @data_column_size_limit ||= columns_hash[@@data_column_name].limit
74           end
75
76           # Hook to set up sessid compatibility.
77           def find_by_session_id(session_id)
78             setup_sessid_compatibility!
79             find_by_session_id(session_id)
80           end
81
82           def marshal(data)   ActiveSupport::Base64.encode64(Marshal.dump(data)) if data end
83           def unmarshal(data) Marshal.load(ActiveSupport::Base64.decode64(data)) if data end
84
85           def create_table!
86             connection.execute <<-end_sql
87               CREATE TABLE #{table_name} (
88                 id INTEGER PRIMARY KEY,
89                 #{connection.quote_column_name('session_id')} TEXT UNIQUE,
90                 #{connection.quote_column_name(@@data_column_name)} TEXT(255)
91               )
92             end_sql
93           end
94
95           def drop_table!
96             connection.execute "DROP TABLE #{table_name}"
97           end
98
99           private
100             # Compatibility with tables using sessid instead of session_id.
101             def setup_sessid_compatibility!
102               # Reset column info since it may be stale.
103               reset_column_information
104               if columns_hash['sessid']
105                 def self.find_by_session_id(*args)
106                   find_by_sessid(*args)
107                 end
108
109                 define_method(:session_id)  { sessid }
110                 define_method(:session_id=) { |session_id| self.sessid = session_id }
111               else
112                 def self.find_by_session_id(session_id)
113                   find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id]
114                 end
115               end
116             end
117         end
118
119         # Lazy-unmarshal session state.
120         def data
121           @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
122         end
123
124         attr_writer :data
125
126         # Has the session been loaded yet?
127         def loaded?
128           !! @data
129         end
130
131         private
132
133           def marshal_data!
134             return false if !loaded?
135             write_attribute(@@data_column_name, self.class.marshal(self.data))
136           end
137
138           # Ensures that the data about to be stored in the database is not
139           # larger than the data storage column. Raises
140           # ActionController::SessionOverflowError.
141           def raise_on_session_data_overflow!
142             return false if !loaded?
143             limit = self.class.data_column_size_limit
144             if loaded? and limit and read_attribute(@@data_column_name).size > limit
145               raise ActionController::SessionOverflowError
146             end
147           end
148       end
149
150       # A barebones session store which duck-types with the default session
151       # store but bypasses Active Record and issues SQL directly.  This is
152       # an example session model class meant as a basis for your own classes.
153       #
154       # The database connection, table name, and session id and data columns
155       # are configurable class attributes.  Marshaling and unmarshaling
156       # are implemented as class methods that you may override.  By default,
157       # marshaling data is +ActiveSupport::Base64.encode64(Marshal.dump(data))+ and
158       # unmarshaling data is +Marshal.load(ActiveSupport::Base64.decode64(data))+.
159       #
160       # This marshaling behavior is intended to store the widest range of
161       # binary session data in a +text+ column.  For higher performance,
162       # store in a +blob+ column instead and forgo the Base64 encoding.
163       class SqlBypass
164         # Use the ActiveRecord::Base.connection by default.
165         cattr_accessor :connection
166
167         # The table name defaults to 'sessions'.
168         cattr_accessor :table_name
169         @@table_name = 'sessions'
170
171         # The session id field defaults to 'session_id'.
172         cattr_accessor :session_id_column
173         @@session_id_column = 'session_id'
174
175         # The data field defaults to 'data'.
176         cattr_accessor :data_column
177         @@data_column = 'data'
178
179         class << self
180
181           def connection
182             @@connection ||= ActiveRecord::Base.connection
183           end
184
185           # Look up a session by id and unmarshal its data if found.
186           def find_by_session_id(session_id)
187             if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
188               new(:session_id => session_id, :marshaled_data => record['data'])
189             end
190           end
191
192           def marshal(data)   ActiveSupport::Base64.encode64(Marshal.dump(data)) if data end
193           def unmarshal(data) Marshal.load(ActiveSupport::Base64.decode64(data)) if data end
194
195           def create_table!
196             @@connection.execute <<-end_sql
197               CREATE TABLE #{table_name} (
198                 id INTEGER PRIMARY KEY,
199                 #{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE,
200                 #{@@connection.quote_column_name(data_column)} TEXT
201               )
202             end_sql
203           end
204
205           def drop_table!
206             @@connection.execute "DROP TABLE #{table_name}"
207           end
208         end
209
210         attr_reader :session_id
211         attr_writer :data
212
213         # Look for normal and marshaled data, self.find_by_session_id's way of
214         # telling us to postpone unmarshaling until the data is requested.
215         # We need to handle a normal data attribute in case of a new record.
216         def initialize(attributes)
217           @session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data]
218           @new_record = @marshaled_data.nil?
219         end
220
221         def new_record?
222           @new_record
223         end
224
225         # Lazy-unmarshal session state.
226         def data
227           unless @data
228             if @marshaled_data
229               @data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
230             else
231               @data = {}
232             end
233           end
234           @data
235         end
236
237         def loaded?
238           !! @data
239         end
240
241         def save
242           return false if !loaded?
243           marshaled_data = self.class.marshal(data)
244
245           if @new_record
246             @new_record = false
247             @@connection.update <<-end_sql, 'Create session'
248               INSERT INTO #{@@table_name} (
249                 #{@@connection.quote_column_name(@@session_id_column)},
250                 #{@@connection.quote_column_name(@@data_column)} )
251               VALUES (
252                 #{@@connection.quote(session_id)},
253                 #{@@connection.quote(marshaled_data)} )
254             end_sql
255           else
256             @@connection.update <<-end_sql, 'Update session'
257               UPDATE #{@@table_name}
258               SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)}
259               WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
260             end_sql
261           end
262         end
263
264         def destroy
265           unless @new_record
266             @@connection.delete <<-end_sql, 'Destroy session'
267               DELETE FROM #{@@table_name}
268               WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
269             end_sql
270           end
271         end
272       end
273
274
275       # The class used for session storage.  Defaults to
276       # CGI::Session::ActiveRecordStore::Session.
277       cattr_accessor :session_class
278       self.session_class = Session
279
280       # Find or instantiate a session given a CGI::Session.
281       def initialize(session, option = nil)
282         session_id = session.session_id
283         unless @session = ActiveRecord::Base.silence { @@session_class.find_by_session_id(session_id) }
284           unless session.new_session
285             raise CGI::Session::NoSession, 'uninitialized session'
286           end
287           @session = @@session_class.new(:session_id => session_id, :data => {})
288           # session saving can be lazy again, because of improved component implementation
289           # therefore next line gets commented out:
290           # @session.save
291         end
292       end
293
294       # Access the underlying session model.
295       def model
296         @session
297       end
298
299       # Restore session state.  The session model handles unmarshaling.
300       def restore
301         if @session
302           @session.data
303         end
304       end
305
306       # Save session store.
307       def update
308         if @session
309           ActiveRecord::Base.silence { @session.save }
310         end
311       end
312
313       # Save and close the session store.
314       def close
315         if @session
316           update
317           @session = nil
318         end
319       end
320
321       # Delete and close the session store.
322       def delete
323         if @session
324           ActiveRecord::Base.silence { @session.destroy }
325           @session = nil
326         end
327       end
328
329       protected
330         def logger
331           ActionController::Base.logger rescue nil
332         end
333     end
334   end
335 end
Note: See TracBrowser for help on using the browser.