activerecord-session_store 0.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord-session_store might be problematic. Click here for more details.

data/README.md ADDED
@@ -0,0 +1,69 @@
1
+ Active Record Session Store
2
+ ===========================
3
+
4
+ A session store backed by an Active Record class. A default class is
5
+ provided, but any object duck-typing to an Active Record Session class
6
+ with text `session_id` and `data` attributes is sufficient.
7
+
8
+ Installation
9
+ ------------
10
+
11
+ Include this gem into your Gemfile:
12
+
13
+ gem 'activerecord-session_store', github: 'rails/activerecord-session_store'
14
+
15
+ Run the migration generator:
16
+
17
+ rails generate active_record:session_migration
18
+
19
+ Then, set your session store in `config/initializers/session_store.rb`:
20
+
21
+ Foo::Application.config.session_store :active_record_store
22
+
23
+ Configuration
24
+ --------------
25
+
26
+ The default assumes a `sessions` tables with columns:
27
+
28
+ * `id` (numeric primary key),
29
+ * `session_id` (string, usually varchar; maximum length is 255), and
30
+ * `data` (text or longtext; careful if your session data exceeds 65KB).
31
+
32
+ The `session_id` column should always be indexed for speedy lookups.
33
+ Session data is marshaled to the `data` column in Base64 format.
34
+ If the data you write is larger than the column's size limit,
35
+ ActionController::SessionOverflowError will be raised.
36
+
37
+ You may configure the table name, primary key, and data column.
38
+ For example, at the end of `config/application.rb`:
39
+
40
+ ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table'
41
+ ActiveRecord::SessionStore::Session.primary_key = 'session_id'
42
+ ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data'
43
+
44
+ Note that setting the primary key to the `session_id` frees you from
45
+ having a separate `id` column if you don't want it. However, you must
46
+ set `session.model.id = session.session_id` by hand! A before filter
47
+ on ApplicationController is a good place.
48
+
49
+ Since the default class is a simple Active Record, you get timestamps
50
+ for free if you add `created_at` and `updated_at` datetime columns to
51
+ the `sessions` table, making periodic session expiration a snap.
52
+
53
+ You may provide your own session class implementation, whether a
54
+ feature-packed Active Record or a bare-metal high-performance SQL
55
+ store, by setting
56
+
57
+ ActiveRecord::SessionStore.session_class = MySessionClass
58
+
59
+ You must implement these methods:
60
+
61
+ * `self.find_by_session_id(session_id)`
62
+ * `initialize(hash_of_session_id_and_data, options_hash = {})`
63
+ * `attr_reader :session_id`
64
+ * `attr_accessor :data`
65
+ * `save`
66
+ * `destroy`
67
+
68
+ The example SqlBypass class is a generic SQL session store. You may
69
+ use it as a basis for high-performance database-specific stores.
@@ -0,0 +1,118 @@
1
+ require 'action_dispatch/middleware/session/abstract_store'
2
+
3
+ module ActionDispatch
4
+ module Session
5
+ # = Active Record Session Store
6
+ #
7
+ # A session store backed by an Active Record class. A default class is
8
+ # provided, but any object duck-typing to an Active Record Session class
9
+ # with text +session_id+ and +data+ attributes is sufficient.
10
+ #
11
+ # The default assumes a +sessions+ tables with columns:
12
+ # +id+ (numeric primary key),
13
+ # +session_id+ (string, usually varchar; maximum length is 255), and
14
+ # +data+ (text or longtext; careful if your session data exceeds 65KB).
15
+ #
16
+ # The +session_id+ column should always be indexed for speedy lookups.
17
+ # Session data is marshaled to the +data+ column in Base64 format.
18
+ # If the data you write is larger than the column's size limit,
19
+ # ActionController::SessionOverflowError will be raised.
20
+ #
21
+ # You may configure the table name, primary key, and data column.
22
+ # For example, at the end of <tt>config/application.rb</tt>:
23
+ #
24
+ # ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table'
25
+ # ActiveRecord::SessionStore::Session.primary_key = 'session_id'
26
+ # ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data'
27
+ #
28
+ # Note that setting the primary key to the +session_id+ frees you from
29
+ # having a separate +id+ column if you don't want it. However, you must
30
+ # set <tt>session.model.id = session.session_id</tt> by hand! A before filter
31
+ # on ApplicationController is a good place.
32
+ #
33
+ # Since the default class is a simple Active Record, you get timestamps
34
+ # for free if you add +created_at+ and +updated_at+ datetime columns to
35
+ # the +sessions+ table, making periodic session expiration a snap.
36
+ #
37
+ # You may provide your own session class implementation, whether a
38
+ # feature-packed Active Record or a bare-metal high-performance SQL
39
+ # store, by setting
40
+ #
41
+ # ActionDispatch::Session::ActiveRecordStore.session_class = MySessionClass
42
+ #
43
+ # You must implement these methods:
44
+ #
45
+ # self.find_by_session_id(session_id)
46
+ # initialize(hash_of_session_id_and_data, options_hash = {})
47
+ # attr_reader :session_id
48
+ # attr_accessor :data
49
+ # save
50
+ # destroy
51
+ #
52
+ # The example SqlBypass class is a generic SQL session store. You may
53
+ # use it as a basis for high-performance database-specific stores.
54
+ class ActiveRecordStore < ActionDispatch::Session::AbstractStore
55
+ # The class used for session storage. Defaults to
56
+ # ActiveRecord::SessionStore::Session
57
+ cattr_accessor :session_class
58
+
59
+ SESSION_RECORD_KEY = 'rack.session.record'
60
+ ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY
61
+
62
+ private
63
+ def get_session(env, sid)
64
+ ActiveRecord::Base.logger.quietly do
65
+ unless sid and session = @@session_class.find_by_session_id(sid)
66
+ # If the sid was nil or if there is no pre-existing session under the sid,
67
+ # force the generation of a new sid and associate a new session associated with the new sid
68
+ sid = generate_sid
69
+ session = @@session_class.new(:session_id => sid, :data => {})
70
+ end
71
+ env[SESSION_RECORD_KEY] = session
72
+ [sid, session.data]
73
+ end
74
+ end
75
+
76
+ def set_session(env, sid, session_data, options)
77
+ ActiveRecord::Base.logger.quietly do
78
+ record = get_session_model(env, sid)
79
+ record.data = session_data
80
+ return false unless record.save
81
+
82
+ session_data = record.data
83
+ if session_data && session_data.respond_to?(:each_value)
84
+ session_data.each_value do |obj|
85
+ obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
86
+ end
87
+ end
88
+ end
89
+
90
+ sid
91
+ end
92
+
93
+ def destroy_session(env, session_id, options)
94
+ if sid = current_session_id(env)
95
+ ActiveRecord::Base.logger.quietly do
96
+ get_session_model(env, sid).destroy
97
+ env[SESSION_RECORD_KEY] = nil
98
+ end
99
+ end
100
+
101
+ generate_sid unless options[:drop]
102
+ end
103
+
104
+ def get_session_model(env, sid)
105
+ if env[ENV_SESSION_OPTIONS_KEY][:id].nil?
106
+ env[SESSION_RECORD_KEY] = find_session(sid)
107
+ else
108
+ env[SESSION_RECORD_KEY] ||= find_session(sid)
109
+ end
110
+ end
111
+
112
+ def find_session(id)
113
+ @@session_class.find_by_session_id(id) ||
114
+ @@session_class.new(:session_id => id, :data => {})
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,36 @@
1
+ require 'action_dispatch/session/active_record_store'
2
+
3
+ module ActiveRecord
4
+ module SessionStore
5
+ module ClassMethods # :nodoc:
6
+ def marshal(data)
7
+ ::Base64.encode64(Marshal.dump(data)) if data
8
+ end
9
+
10
+ def unmarshal(data)
11
+ Marshal.load(::Base64.decode64(data)) if data
12
+ end
13
+
14
+ def drop_table!
15
+ connection.schema_cache.clear_table_cache!(table_name)
16
+ connection.drop_table table_name
17
+ end
18
+
19
+ def create_table!
20
+ connection.schema_cache.clear_table_cache!(table_name)
21
+ connection.create_table(table_name) do |t|
22
+ t.string session_id_column, :limit => 255
23
+ t.text data_column_name
24
+ end
25
+ connection.add_index table_name, session_id_column, :unique => true
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ require 'active_record/session_store/session'
32
+ require 'active_record/session_store/sql_bypass'
33
+ require 'active_record/session_store/railtie' if defined?(Rails)
34
+
35
+
36
+ ActionDispatch::Session::ActiveRecordStore.session_class = ActiveRecord::SessionStore::Session
@@ -0,0 +1,9 @@
1
+ require 'rails/railtie'
2
+
3
+ module ActiveRecord
4
+ module SessionStore
5
+ class Railtie < Rails::Railtie
6
+ rake_tasks { load "tasks/database.rake" }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,88 @@
1
+ module ActiveRecord
2
+ module SessionStore
3
+ # The default Active Record class.
4
+ class Session < ActiveRecord::Base
5
+ extend ClassMethods
6
+
7
+ ##
8
+ # :singleton-method:
9
+ # Customizable data column name. Defaults to 'data'.
10
+ cattr_accessor :data_column_name
11
+ self.data_column_name = 'data'
12
+
13
+ before_save :marshal_data!
14
+ before_save :raise_on_session_data_overflow!
15
+
16
+ class << self
17
+ def data_column_size_limit
18
+ @data_column_size_limit ||= columns_hash[data_column_name].limit
19
+ end
20
+
21
+ # Hook to set up sessid compatibility.
22
+ def find_by_session_id(session_id)
23
+ setup_sessid_compatibility!
24
+ find_by_session_id(session_id)
25
+ end
26
+
27
+ private
28
+ def session_id_column
29
+ 'session_id'
30
+ end
31
+
32
+ # Compatibility with tables using sessid instead of session_id.
33
+ def setup_sessid_compatibility!
34
+ # Reset column info since it may be stale.
35
+ reset_column_information
36
+ if columns_hash['sessid']
37
+ def self.find_by_session_id(*args)
38
+ find_by_sessid(*args)
39
+ end
40
+
41
+ define_method(:session_id) { sessid }
42
+ define_method(:session_id=) { |session_id| self.sessid = session_id }
43
+ else
44
+ class << self; remove_possible_method :find_by_session_id; end
45
+
46
+ def self.find_by_session_id(session_id)
47
+ where(session_id: session_id).first
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ def initialize(attributes = nil)
54
+ @data = nil
55
+ super
56
+ end
57
+
58
+ # Lazy-unmarshal session state.
59
+ def data
60
+ @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
61
+ end
62
+
63
+ attr_writer :data
64
+
65
+ # Has the session been loaded yet?
66
+ def loaded?
67
+ @data
68
+ end
69
+
70
+ private
71
+ def marshal_data!
72
+ return false unless loaded?
73
+ write_attribute(@@data_column_name, self.class.marshal(data))
74
+ end
75
+
76
+ # Ensures that the data about to be stored in the database is not
77
+ # larger than the data storage column. Raises
78
+ # ActionController::SessionOverflowError.
79
+ def raise_on_session_data_overflow!
80
+ return false unless loaded?
81
+ limit = self.class.data_column_size_limit
82
+ if limit and read_attribute(@@data_column_name).size > limit
83
+ raise ActionController::SessionOverflowError
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,140 @@
1
+ module ActiveRecord
2
+ module SessionStore
3
+ # A barebones session store which duck-types with the default session
4
+ # store but bypasses Active Record and issues SQL directly. This is
5
+ # an example session model class meant as a basis for your own classes.
6
+ #
7
+ # The database connection, table name, and session id and data columns
8
+ # are configurable class attributes. Marshaling and unmarshaling
9
+ # are implemented as class methods that you may override. By default,
10
+ # marshaling data is
11
+ #
12
+ # ::Base64.encode64(Marshal.dump(data))
13
+ #
14
+ # and unmarshaling data is
15
+ #
16
+ # Marshal.load(::Base64.decode64(data))
17
+ #
18
+ # This marshaling behavior is intended to store the widest range of
19
+ # binary session data in a +text+ column. For higher performance,
20
+ # store in a +blob+ column instead and forgo the Base64 encoding.
21
+ class SqlBypass
22
+ extend ClassMethods
23
+
24
+ ##
25
+ # :singleton-method:
26
+ # The table name defaults to 'sessions'.
27
+ cattr_accessor :table_name
28
+ @@table_name = 'sessions'
29
+
30
+ ##
31
+ # :singleton-method:
32
+ # The session id field defaults to 'session_id'.
33
+ cattr_accessor :session_id_column
34
+ @@session_id_column = 'session_id'
35
+
36
+ ##
37
+ # :singleton-method:
38
+ # The data field defaults to 'data'.
39
+ cattr_accessor :data_column
40
+ @@data_column = 'data'
41
+
42
+ class << self
43
+ alias :data_column_name :data_column
44
+
45
+ # Use the ActiveRecord::Base.connection by default.
46
+ attr_writer :connection
47
+
48
+ # Use the ActiveRecord::Base.connection_pool by default.
49
+ attr_writer :connection_pool
50
+
51
+ def connection
52
+ @connection ||= ActiveRecord::Base.connection
53
+ end
54
+
55
+ def connection_pool
56
+ @connection_pool ||= ActiveRecord::Base.connection_pool
57
+ end
58
+
59
+ # Look up a session by id and unmarshal its data if found.
60
+ def find_by_session_id(session_id)
61
+ if record = connection.select_one("SELECT #{connection.quote_column_name(data_column)} AS data FROM #{@@table_name} WHERE #{connection.quote_column_name(@@session_id_column)}=#{connection.quote(session_id.to_s)}")
62
+ new(:session_id => session_id, :marshaled_data => record['data'])
63
+ end
64
+ end
65
+ end
66
+
67
+ delegate :connection, :connection=, :connection_pool, :connection_pool=, :to => self
68
+
69
+ attr_reader :session_id, :new_record
70
+ alias :new_record? :new_record
71
+
72
+ attr_writer :data
73
+
74
+ # Look for normal and marshaled data, self.find_by_session_id's way of
75
+ # telling us to postpone unmarshaling until the data is requested.
76
+ # We need to handle a normal data attribute in case of a new record.
77
+ def initialize(attributes)
78
+ @session_id = attributes[:session_id]
79
+ @data = attributes[:data]
80
+ @marshaled_data = attributes[:marshaled_data]
81
+ @new_record = @marshaled_data.nil?
82
+ end
83
+
84
+ # Returns true if the record is persisted, i.e. it's not a new record
85
+ def persisted?
86
+ !@new_record
87
+ end
88
+
89
+ # Lazy-unmarshal session state.
90
+ def data
91
+ unless @data
92
+ if @marshaled_data
93
+ @data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
94
+ else
95
+ @data = {}
96
+ end
97
+ end
98
+ @data
99
+ end
100
+
101
+ def loaded?
102
+ @data
103
+ end
104
+
105
+ def save
106
+ return false unless loaded?
107
+ marshaled_data = self.class.marshal(data)
108
+ connect = connection
109
+
110
+ if @new_record
111
+ @new_record = false
112
+ connect.update <<-end_sql, 'Create session'
113
+ INSERT INTO #{table_name} (
114
+ #{connect.quote_column_name(session_id_column)},
115
+ #{connect.quote_column_name(data_column)} )
116
+ VALUES (
117
+ #{connect.quote(session_id)},
118
+ #{connect.quote(marshaled_data)} )
119
+ end_sql
120
+ else
121
+ connect.update <<-end_sql, 'Update session'
122
+ UPDATE #{table_name}
123
+ SET #{connect.quote_column_name(data_column)}=#{connect.quote(marshaled_data)}
124
+ WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)}
125
+ end_sql
126
+ end
127
+ end
128
+
129
+ def destroy
130
+ return if @new_record
131
+
132
+ connect = connection
133
+ connect.delete <<-end_sql, 'Destroy session'
134
+ DELETE FROM #{table_name}
135
+ WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id.to_s)}
136
+ end_sql
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1 @@
1
+ require 'active_record/session_store'
@@ -0,0 +1,24 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module ActiveRecord
4
+ module Generators
5
+ class SessionMigrationGenerator < Base
6
+ source_root File.expand_path("../templates", __FILE__)
7
+ argument :name, :type => :string, :default => "add_sessions_table"
8
+
9
+ def create_migration_file
10
+ migration_template "migration.rb", "db/migrate/#{file_name}.rb"
11
+ end
12
+
13
+ protected
14
+
15
+ def session_table_name
16
+ current_table_name = ActiveRecord::SessionStore::Session.table_name
17
+ if current_table_name == 'session' || current_table_name == 'sessions'
18
+ current_table_name = ActiveRecord::Base.pluralize_table_names ? 'sessions' : 'session'
19
+ end
20
+ current_table_name
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,12 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration
2
+ def change
3
+ create_table :<%= session_table_name %> do |t|
4
+ t.string :session_id, :null => false
5
+ t.text :data
6
+ t.timestamps
7
+ end
8
+
9
+ add_index :<%= session_table_name %>, :session_id
10
+ add_index :<%= session_table_name %>, :updated_at
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ namespace 'db:sessions' do
2
+ desc "Creates a sessions migration for use with ActiveRecord::SessionStore"
3
+ task :create => [:environment, 'db:load_config'] do
4
+ raise 'Task unavailable to this database (no migration support)' unless ActiveRecord::Base.connection.supports_migrations?
5
+ Rails.application.load_generators
6
+ require 'rails/generators/rails/session_migration/session_migration_generator'
7
+ Rails::Generators::SessionMigrationGenerator.start [ ENV['MIGRATION'] || 'add_sessions_table' ]
8
+ end
9
+
10
+ desc "Clear the sessions table"
11
+ task :clear => [:environment, 'db:load_config'] do
12
+ ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::SessionStore::Session.table_name}"
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-session_store
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - David Heinemeier Hansson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-22 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 4.0.0.beta
22
+ - - <
23
+ - !ruby/object:Gem::Version
24
+ version: '5'
25
+ type: :runtime
26
+ prerelease: false
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 4.0.0.beta
33
+ - - <
34
+ - !ruby/object:Gem::Version
35
+ version: '5'
36
+ - !ruby/object:Gem::Dependency
37
+ name: actionpack
38
+ requirement: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: 4.0.0.beta
44
+ - - <
45
+ - !ruby/object:Gem::Version
46
+ version: '5'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: 4.0.0.beta
55
+ - - <
56
+ - !ruby/object:Gem::Version
57
+ version: '5'
58
+ - !ruby/object:Gem::Dependency
59
+ name: railties
60
+ requirement: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: 4.0.0.beta
66
+ - - <
67
+ - !ruby/object:Gem::Version
68
+ version: '5'
69
+ type: :runtime
70
+ prerelease: false
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: 4.0.0.beta
77
+ - - <
78
+ - !ruby/object:Gem::Version
79
+ version: '5'
80
+ - !ruby/object:Gem::Dependency
81
+ name: sqlite3
82
+ requirement: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ description:
97
+ email: david@loudthinking.com
98
+ executables: []
99
+ extensions: []
100
+ extra_rdoc_files:
101
+ - README.md
102
+ files:
103
+ - README.md
104
+ - lib/action_dispatch/session/active_record_store.rb
105
+ - lib/active_record/session_store/railtie.rb
106
+ - lib/active_record/session_store/session.rb
107
+ - lib/active_record/session_store/sql_bypass.rb
108
+ - lib/active_record/session_store.rb
109
+ - lib/activerecord/session_store.rb
110
+ - lib/generators/active_record/session_migration_generator.rb
111
+ - lib/generators/active_record/templates/migration.rb
112
+ - lib/tasks/database.rake
113
+ homepage: http://www.rubyonrails.org
114
+ licenses:
115
+ - MIT
116
+ post_install_message:
117
+ rdoc_options:
118
+ - --main
119
+ - README.md
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ none: false
124
+ requirements:
125
+ - - ! '>='
126
+ - !ruby/object:Gem::Version
127
+ version: 1.9.3
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubyforge_project:
136
+ rubygems_version: 1.8.23
137
+ signing_key:
138
+ specification_version: 3
139
+ summary: An Action Dispatch session store backed by an Active Record class.
140
+ test_files: []