/
active_record_store.rb
168 lines (151 loc) · 5.91 KB
/
active_record_store.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
require "active_support/core_ext/module/attribute_accessors"
require 'action_dispatch/middleware/session/abstract_store'
module ActionDispatch
module Session
# = Active Record Session Store
#
# A session store backed by an Active Record class. A default class is
# provided, but any object duck-typing to an Active Record Session class
# with text +session_id+ and +data+ attributes is sufficient.
#
# The default assumes a +sessions+ tables with columns:
# +id+ (numeric primary key),
# +session_id+ (string, usually varchar; maximum length is 255), and
# +data+ (text or longtext; careful if your session data exceeds 65KB).
#
# The +session_id+ column should always be indexed for speedy lookups.
# Session data is marshaled to the +data+ column in Base64 format.
# If the data you write is larger than the column's size limit,
# ActionController::SessionOverflowError will be raised.
#
# You may configure the table name, primary key, and data column.
# For example, at the end of <tt>config/application.rb</tt>:
#
# ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table'
# ActiveRecord::SessionStore::Session.primary_key = 'session_id'
# ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data'
#
# Note that setting the primary key to the +session_id+ frees you from
# having a separate +id+ column if you don't want it. However, you must
# set <tt>session.model.id = session.session_id</tt> by hand! A before filter
# on ApplicationController is a good place.
#
# Since the default class is a simple Active Record, you get timestamps
# for free if you add +created_at+ and +updated_at+ datetime columns to
# the +sessions+ table, making periodic session expiration a snap.
#
# You may provide your own session class implementation, whether a
# feature-packed Active Record or a bare-metal high-performance SQL
# store, by setting
#
# ActionDispatch::Session::ActiveRecordStore.session_class = MySessionClass
#
# You must implement these methods:
#
# self.find_by_session_id(session_id)
# initialize(hash_of_session_id_and_data, options_hash = {})
# attr_reader :session_id
# attr_accessor :data
# save
# destroy
#
# The example SqlBypass class is a generic SQL session store. You may
# use it as a basis for high-performance database-specific stores.
class ActiveRecordStore < ActionDispatch::Session::AbstractSecureStore
# The class used for session storage. Defaults to
# ActiveRecord::SessionStore::Session
class_attribute :session_class
SESSION_RECORD_KEY = 'rack.session.record'
ENV_SESSION_OPTIONS_KEY = Rack::RACK_SESSION_OPTIONS
private
def get_session(request, sid)
logger.silence do
unless sid and session = get_session_with_fallback(sid)
# If the sid was nil or if there is no pre-existing session under the sid,
# force the generation of a new sid and associate a new session associated with the new sid
sid = generate_sid
session = session_class.new(:session_id => sid.private_id, :data => {})
end
request.env[SESSION_RECORD_KEY] = session
[sid, session.data]
end
end
def write_session(request, sid, session_data, options)
logger.silence do
record, sid = get_session_model(request, sid)
record.data = session_data
return false unless record.save
session_data = record.data
if session_data && session_data.respond_to?(:each_value)
session_data.each_value do |obj|
obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
end
end
sid
end
end
def delete_session(request, session_id, options)
logger.silence do
if sid = current_session_id(request)
if model = get_session_with_fallback(sid)
data = model.data
model.destroy
end
end
request.env[SESSION_RECORD_KEY] = nil
unless options[:drop]
new_sid = generate_sid
if options[:renew]
new_model = session_class.new(:session_id => new_sid.private_id, :data => data)
new_model.save
request.env[SESSION_RECORD_KEY] = new_model
end
new_sid
end
end
end
def get_session_model(request, id)
logger.silence do
model = get_session_with_fallback(id)
unless model
id = generate_sid
model = session_class.new(:session_id => id.private_id, :data => {})
model.save
end
if request.env[ENV_SESSION_OPTIONS_KEY][:id].nil?
request.env[SESSION_RECORD_KEY] = model
else
request.env[SESSION_RECORD_KEY] ||= model
end
[model, id]
end
end
def get_session_with_fallback(sid)
if sid && !self.class.private_session_id?(sid.public_id)
if (secure_session = session_class.find_by_session_id(sid.private_id))
secure_session
elsif (insecure_session = session_class.find_by_session_id(sid.public_id))
insecure_session.session_id = sid.private_id # this causes the session to be secured
insecure_session
end
end
end
def find_session(request, id)
model, id = get_session_model(request, id)
[id, model.data]
end
module NilLogger
def self.silence
yield
end
end
def logger
ActiveRecord::Base.logger || NilLogger
end
def self.private_session_id?(session_id)
# user tried to retrieve a session by a private key?
session_id =~ /\A\d+::/
end
end
end
end