Froze rails gems
[depot.git] / vendor / rails / actionpack / lib / action_controller / session / active_record_store.rb
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 <tt>config/environment.rb</tt>:
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 <tt>session.model.id = session.session_id</tt> 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
158 #
159 # ActiveSupport::Base64.encode64(Marshal.dump(data))
160 #
161 # and unmarshaling data is
162 #
163 # Marshal.load(ActiveSupport::Base64.decode64(data))
164 #
165 # This marshaling behavior is intended to store the widest range of
166 # binary session data in a +text+ column. For higher performance,
167 # store in a +blob+ column instead and forgo the Base64 encoding.
168 class SqlBypass
169 # Use the ActiveRecord::Base.connection by default.
170 cattr_accessor :connection
171
172 # The table name defaults to 'sessions'.
173 cattr_accessor :table_name
174 @@table_name = 'sessions'
175
176 # The session id field defaults to 'session_id'.
177 cattr_accessor :session_id_column
178 @@session_id_column = 'session_id'
179
180 # The data field defaults to 'data'.
181 cattr_accessor :data_column
182 @@data_column = 'data'
183
184 class << self
185
186 def connection
187 @@connection ||= ActiveRecord::Base.connection
188 end
189
190 # Look up a session by id and unmarshal its data if found.
191 def find_by_session_id(session_id)
192 if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
193 new(:session_id => session_id, :marshaled_data => record['data'])
194 end
195 end
196
197 def marshal(data) ActiveSupport::Base64.encode64(Marshal.dump(data)) if data end
198 def unmarshal(data) Marshal.load(ActiveSupport::Base64.decode64(data)) if data end
199
200 def create_table!
201 @@connection.execute <<-end_sql
202 CREATE TABLE #{table_name} (
203 id INTEGER PRIMARY KEY,
204 #{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE,
205 #{@@connection.quote_column_name(data_column)} TEXT
206 )
207 end_sql
208 end
209
210 def drop_table!
211 @@connection.execute "DROP TABLE #{table_name}"
212 end
213 end
214
215 attr_reader :session_id
216 attr_writer :data
217
218 # Look for normal and marshaled data, self.find_by_session_id's way of
219 # telling us to postpone unmarshaling until the data is requested.
220 # We need to handle a normal data attribute in case of a new record.
221 def initialize(attributes)
222 @session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data]
223 @new_record = @marshaled_data.nil?
224 end
225
226 def new_record?
227 @new_record
228 end
229
230 # Lazy-unmarshal session state.
231 def data
232 unless @data
233 if @marshaled_data
234 @data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
235 else
236 @data = {}
237 end
238 end
239 @data
240 end
241
242 def loaded?
243 !! @data
244 end
245
246 def save
247 return false if !loaded?
248 marshaled_data = self.class.marshal(data)
249
250 if @new_record
251 @new_record = false
252 @@connection.update <<-end_sql, 'Create session'
253 INSERT INTO #{@@table_name} (
254 #{@@connection.quote_column_name(@@session_id_column)},
255 #{@@connection.quote_column_name(@@data_column)} )
256 VALUES (
257 #{@@connection.quote(session_id)},
258 #{@@connection.quote(marshaled_data)} )
259 end_sql
260 else
261 @@connection.update <<-end_sql, 'Update session'
262 UPDATE #{@@table_name}
263 SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)}
264 WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
265 end_sql
266 end
267 end
268
269 def destroy
270 unless @new_record
271 @@connection.delete <<-end_sql, 'Destroy session'
272 DELETE FROM #{@@table_name}
273 WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
274 end_sql
275 end
276 end
277 end
278
279
280 # The class used for session storage. Defaults to
281 # CGI::Session::ActiveRecordStore::Session.
282 cattr_accessor :session_class
283 self.session_class = Session
284
285 # Find or instantiate a session given a CGI::Session.
286 def initialize(session, option = nil)
287 session_id = session.session_id
288 unless @session = ActiveRecord::Base.silence { @@session_class.find_by_session_id(session_id) }
289 unless session.new_session
290 raise CGI::Session::NoSession, 'uninitialized session'
291 end
292 @session = @@session_class.new(:session_id => session_id, :data => {})
293 # session saving can be lazy again, because of improved component implementation
294 # therefore next line gets commented out:
295 # @session.save
296 end
297 end
298
299 # Access the underlying session model.
300 def model
301 @session
302 end
303
304 # Restore session state. The session model handles unmarshaling.
305 def restore
306 if @session
307 @session.data
308 end
309 end
310
311 # Save session store.
312 def update
313 if @session
314 ActiveRecord::Base.silence { @session.save }
315 end
316 end
317
318 # Save and close the session store.
319 def close
320 if @session
321 update
322 @session = nil
323 end
324 end
325
326 # Delete and close the session store.
327 def delete
328 if @session
329 ActiveRecord::Base.silence { @session.destroy }
330 @session = nil
331 end
332 end
333
334 protected
335 def logger
336 ActionController::Base.logger rescue nil
337 end
338 end
339 end
340 end