Updated README.rdoc again
[feedcatcher.git] / vendor / rails / activerecord / lib / active_record / session_store.rb
1 module ActiveRecord
2 # A session store backed by an Active Record class. A default class is
3 # provided, but any object duck-typing to an Active Record Session class
4 # with text +session_id+ and +data+ attributes is sufficient.
5 #
6 # The default assumes a +sessions+ tables with columns:
7 # +id+ (numeric primary key),
8 # +session_id+ (text, or longtext if your session data exceeds 65K), and
9 # +data+ (text or longtext; careful if your session data exceeds 65KB).
10 # The +session_id+ column should always be indexed for speedy lookups.
11 # Session data is marshaled to the +data+ column in Base64 format.
12 # If the data you write is larger than the column's size limit,
13 # ActionController::SessionOverflowError will be raised.
14 #
15 # You may configure the table name, primary key, and data column.
16 # For example, at the end of <tt>config/environment.rb</tt>:
17 # ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table'
18 # ActiveRecord::SessionStore::Session.primary_key = 'session_id'
19 # ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data'
20 # Note that setting the primary key to the +session_id+ frees you from
21 # having a separate +id+ column if you don't want it. However, you must
22 # set <tt>session.model.id = session.session_id</tt> by hand! A before filter
23 # on ApplicationController is a good place.
24 #
25 # Since the default class is a simple Active Record, you get timestamps
26 # for free if you add +created_at+ and +updated_at+ datetime columns to
27 # the +sessions+ table, making periodic session expiration a snap.
28 #
29 # You may provide your own session class implementation, whether a
30 # feature-packed Active Record or a bare-metal high-performance SQL
31 # store, by setting
32 # ActiveRecord::SessionStore.session_class = MySessionClass
33 # You must implement these methods:
34 # self.find_by_session_id(session_id)
35 # initialize(hash_of_session_id_and_data)
36 # attr_reader :session_id
37 # attr_accessor :data
38 # save
39 # destroy
40 #
41 # The example SqlBypass class is a generic SQL session store. You may
42 # use it as a basis for high-performance database-specific stores.
43 class SessionStore < ActionController::Session::AbstractStore
44 # The default Active Record class.
45 class Session < ActiveRecord::Base
46 ##
47 # :singleton-method:
48 # Customizable data column name. Defaults to 'data'.
49 cattr_accessor :data_column_name
50 self.data_column_name = 'data'
51
52 before_save :marshal_data!
53 before_save :raise_on_session_data_overflow!
54
55 class << self
56 def data_column_size_limit
57 @data_column_size_limit ||= columns_hash[@@data_column_name].limit
58 end
59
60 # Hook to set up sessid compatibility.
61 def find_by_session_id(session_id)
62 setup_sessid_compatibility!
63 find_by_session_id(session_id)
64 end
65
66 def marshal(data)
67 ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
68 end
69
70 def unmarshal(data)
71 Marshal.load(ActiveSupport::Base64.decode64(data)) if data
72 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_column_name)} TEXT(255)
80 )
81 end_sql
82 end
83
84 def drop_table!
85 connection.execute "DROP TABLE #{table_name}"
86 end
87
88 private
89 # Compatibility with tables using sessid instead of session_id.
90 def setup_sessid_compatibility!
91 # Reset column info since it may be stale.
92 reset_column_information
93 if columns_hash['sessid']
94 def self.find_by_session_id(*args)
95 find_by_sessid(*args)
96 end
97
98 define_method(:session_id) { sessid }
99 define_method(:session_id=) { |session_id| self.sessid = session_id }
100 else
101 def self.find_by_session_id(session_id)
102 find :first, :conditions => {:session_id=>session_id}
103 end
104 end
105 end
106 end
107
108 # Lazy-unmarshal session state.
109 def data
110 @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
111 end
112
113 attr_writer :data
114
115 # Has the session been loaded yet?
116 def loaded?
117 !!@data
118 end
119
120 private
121 def marshal_data!
122 return false if !loaded?
123 write_attribute(@@data_column_name, self.class.marshal(self.data))
124 end
125
126 # Ensures that the data about to be stored in the database is not
127 # larger than the data storage column. Raises
128 # ActionController::SessionOverflowError.
129 def raise_on_session_data_overflow!
130 return false if !loaded?
131 limit = self.class.data_column_size_limit
132 if loaded? and limit and read_attribute(@@data_column_name).size > limit
133 raise ActionController::SessionOverflowError
134 end
135 end
136 end
137
138 # A barebones session store which duck-types with the default session
139 # store but bypasses Active Record and issues SQL directly. This is
140 # an example session model class meant as a basis for your own classes.
141 #
142 # The database connection, table name, and session id and data columns
143 # are configurable class attributes. Marshaling and unmarshaling
144 # are implemented as class methods that you may override. By default,
145 # marshaling data is
146 #
147 # ActiveSupport::Base64.encode64(Marshal.dump(data))
148 #
149 # and unmarshaling data is
150 #
151 # Marshal.load(ActiveSupport::Base64.decode64(data))
152 #
153 # This marshaling behavior is intended to store the widest range of
154 # binary session data in a +text+ column. For higher performance,
155 # store in a +blob+ column instead and forgo the Base64 encoding.
156 class SqlBypass
157 ##
158 # :singleton-method:
159 # Use the ActiveRecord::Base.connection by default.
160 cattr_accessor :connection
161
162 ##
163 # :singleton-method:
164 # The table name defaults to 'sessions'.
165 cattr_accessor :table_name
166 @@table_name = 'sessions'
167
168 ##
169 # :singleton-method:
170 # The session id field defaults to 'session_id'.
171 cattr_accessor :session_id_column
172 @@session_id_column = 'session_id'
173
174 ##
175 # :singleton-method:
176 # The data field defaults to 'data'.
177 cattr_accessor :data_column
178 @@data_column = 'data'
179
180 class << self
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)
193 ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
194 end
195
196 def unmarshal(data)
197 Marshal.load(ActiveSupport::Base64.decode64(data)) if data
198 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 # The class used for session storage. Defaults to
280 # ActiveRecord::SessionStore::Session
281 cattr_accessor :session_class
282 self.session_class = Session
283
284 SESSION_RECORD_KEY = 'rack.session.record'.freeze
285
286 private
287 def get_session(env, sid)
288 Base.silence do
289 sid ||= generate_sid
290 session = find_session(sid)
291 env[SESSION_RECORD_KEY] = session
292 [sid, session.data]
293 end
294 end
295
296 def set_session(env, sid, session_data)
297 Base.silence do
298 record = env[SESSION_RECORD_KEY] ||= find_session(sid)
299 record.data = session_data
300 return false unless record.save
301
302 session_data = record.data
303 if session_data && session_data.respond_to?(:each_value)
304 session_data.each_value do |obj|
305 obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
306 end
307 end
308 end
309
310 return true
311 end
312
313 def find_session(id)
314 @@session_class.find_by_session_id(id) ||
315 @@session_class.new(:session_id => id, :data => {})
316 end
317 end
318 end