merb-core 0.9.5 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. data/CHANGELOG +925 -0
  2. data/CONTRIBUTORS +93 -0
  3. data/PUBLIC_CHANGELOG +85 -0
  4. data/Rakefile +18 -28
  5. data/bin/merb +34 -5
  6. data/lib/merb-core/autoload.rb +2 -3
  7. data/lib/merb-core/bootloader.rb +60 -66
  8. data/lib/merb-core/config.rb +7 -1
  9. data/lib/merb-core/controller/abstract_controller.rb +35 -21
  10. data/lib/merb-core/controller/merb_controller.rb +15 -42
  11. data/lib/merb-core/controller/mixins/authentication.rb +42 -6
  12. data/lib/merb-core/controller/mixins/conditional_get.rb +83 -0
  13. data/lib/merb-core/controller/mixins/render.rb +3 -3
  14. data/lib/merb-core/core_ext/kernel.rb +6 -19
  15. data/lib/merb-core/dispatch/cookies.rb +96 -80
  16. data/lib/merb-core/dispatch/default_exception/views/index.html.erb +2 -0
  17. data/lib/merb-core/dispatch/request.rb +18 -16
  18. data/lib/merb-core/dispatch/router/route.rb +6 -0
  19. data/lib/merb-core/dispatch/router.rb +4 -1
  20. data/lib/merb-core/dispatch/session/container.rb +64 -0
  21. data/lib/merb-core/dispatch/session/cookie.rb +91 -101
  22. data/lib/merb-core/dispatch/session/memcached.rb +38 -174
  23. data/lib/merb-core/dispatch/session/memory.rb +62 -208
  24. data/lib/merb-core/dispatch/session/store_container.rb +145 -0
  25. data/lib/merb-core/dispatch/session.rb +174 -48
  26. data/lib/merb-core/rack/middleware/conditional_get.rb +14 -8
  27. data/lib/merb-core/rack/middleware/csrf.rb +73 -0
  28. data/lib/merb-core/rack.rb +1 -0
  29. data/lib/merb-core/script.rb +112 -0
  30. data/lib/merb-core/server.rb +2 -0
  31. data/lib/merb-core/tasks/merb_rake_helper.rb +25 -0
  32. data/lib/merb-core/test/helpers/request_helper.rb +40 -3
  33. data/lib/merb-core/test/run_specs.rb +4 -3
  34. data/lib/merb-core/vendor/facets/inflect.rb +7 -10
  35. data/lib/merb-core/version.rb +1 -1
  36. data/lib/merb-core.rb +11 -40
  37. data/spec/private/core_ext/kernel_spec.rb +0 -11
  38. data/spec/private/dispatch/fixture/log/merb_test.log +893 -0
  39. data/spec/private/router/fixture/log/merb_test.log +12 -1728
  40. data/spec/private/router/route_spec.rb +4 -0
  41. data/spec/private/router/router_spec.rb +8 -0
  42. data/spec/private/vendor/facets/plural_spec.rb +1 -1
  43. data/spec/private/vendor/facets/singular_spec.rb +1 -1
  44. data/spec/public/abstract_controller/controllers/display.rb +8 -2
  45. data/spec/public/abstract_controller/controllers/filters.rb +18 -0
  46. data/spec/public/abstract_controller/display_spec.rb +6 -2
  47. data/spec/public/abstract_controller/filter_spec.rb +4 -0
  48. data/spec/public/controller/authentication_spec.rb +114 -43
  49. data/spec/public/controller/base_spec.rb +8 -0
  50. data/spec/public/controller/conditional_get_spec.rb +100 -0
  51. data/spec/public/controller/config/init.rb +1 -1
  52. data/spec/public/controller/controllers/authentication.rb +29 -0
  53. data/spec/public/controller/controllers/base.rb +13 -0
  54. data/spec/public/controller/controllers/conditional_get.rb +35 -0
  55. data/spec/public/controller/controllers/cookies.rb +10 -1
  56. data/spec/public/controller/cookies_spec.rb +38 -9
  57. data/spec/public/controller/spec_helper.rb +1 -0
  58. data/spec/public/controller/url_spec.rb +70 -1
  59. data/spec/public/directory_structure/directory/log/merb_test.log +461 -0
  60. data/spec/public/rack/conditinal_get_middleware_spec.rb +77 -89
  61. data/spec/public/rack/csrf_middleware_spec.rb +70 -0
  62. data/spec/public/reloading/directory/log/merb_test.log +52 -0
  63. data/spec/public/request/request_spec.rb +19 -1
  64. data/spec/public/router/fixation_spec.rb +26 -4
  65. data/spec/public/router/fixture/log/merb_test.log +234 -30332
  66. data/spec/public/session/controllers/sessions.rb +52 -0
  67. data/spec/public/session/cookie_session_spec.rb +73 -0
  68. data/spec/public/session/memcached_session_spec.rb +31 -0
  69. data/spec/public/session/memory_session_spec.rb +28 -0
  70. data/spec/public/session/multiple_sessions_spec.rb +74 -0
  71. data/spec/public/session/no_session_spec.rb +12 -0
  72. data/spec/public/session/session_spec.rb +91 -0
  73. data/spec/public/test/controllers/spec_helper_controller.rb +2 -1
  74. data/spec/public/test/request_helper_spec.rb +15 -0
  75. data/spec/spec_helper.rb +2 -2
  76. metadata +23 -5
  77. data/spec/private/dispatch/cookies_spec.rb +0 -219
  78. data/spec/private/dispatch/session_mixin_spec.rb +0 -47
@@ -1,62 +1,48 @@
1
1
  module Merb
2
2
 
3
- # Cookies are read and written through Merb::Controller#cookies. The cookies
4
- # you read are those received in request along with those that have been set
5
- # during the current request. The cookies you write will be sent out with the
6
- # response. Cookies are read by value (so you won't get the cookie object
7
- # itself back -- just the value it holds).
8
- class Cookies
9
-
10
- # ==== Parameters
11
- # request_cookies<Hash>:: Initial cookie store.
12
- # headers<Hash>:: The response headers.
13
- def initialize(request_cookies, headers)
14
- @_cookies = request_cookies
15
- @_headers = headers
3
+ class Cookies < Mash
4
+
5
+ def initialize(constructor = {}, cookie_defaults = {})
6
+ @_options_lookup = Mash.new
7
+ @_cookie_defaults = cookie_defaults
8
+ super constructor
16
9
  end
17
-
10
+
11
+ # Implicit assignment of cookie key and value.
12
+ #
18
13
  # ==== Parameters
19
14
  # name<~to_s>:: Name of the cookie.
15
+ # value<~to_s>:: Value of the cookie.
20
16
  #
21
- # ==== Returns
22
- # String:: Value of the cookie.
23
- def [](name)
24
- @_cookies[name]
17
+ # ==== Notes
18
+ # By using this method, a cookie key is marked for being
19
+ # included in the Set-Cookie response header.
20
+ def []=(key, value)
21
+ @_options_lookup[key] ||= {}
22
+ super
25
23
  end
26
-
24
+
25
+ # Explicit assignment of cookie key, value and options
26
+ #
27
27
  # ==== Parameters
28
28
  # name<~to_s>:: Name of the cookie.
29
- # options<Hash, ~to_s>:: Options for the cookie being set (see below).
29
+ # value<~to_s>:: Value of the cookie.
30
+ # options<Hash>:: Additional options for the cookie (see below).
30
31
  #
31
32
  # ==== Options (options)
32
- # :value<~to_s>:: Value of the cookie
33
33
  # :path<String>:: The path for which this cookie applies. Defaults to "/".
34
34
  # :expires<Time>:: Cookie expiry date.
35
35
  # :domain<String>:: The domain for which this cookie applies.
36
36
  # :secure<Boolean>:: Security flag.
37
37
  #
38
- # ==== Alternatives
39
- # If options is not a hash, it will be used as the cookie value directly.
40
- #
41
- # ==== Examples
42
- # cookies[:user] = "dave" # => Sets a simple session cookie
43
- # cookies[:token] = { :value => user.token, :expires => Time.now + 2.weeks }
44
- # # => Will set a cookie that expires in 2 weeks
45
- def []=(name, options)
46
- value = ''
47
- if options.is_a?(Hash)
48
- options = Mash.new(options)
49
- value = options.delete(:value)
50
- else
51
- value = options
52
- options = Mash.new
53
- end
54
- @_cookies[name] = value
55
- set_cookie(name, Merb::Request.escape(value), options)
56
- Merb.logger.info("Cookie set: #{name} => #{value} -- #{options.inspect}")
57
- options
38
+ # ==== Notes
39
+ # By using this method, a cookie key is marked for being
40
+ # included in the Set-Cookie response header.
41
+ def set_cookie(name, value, options = {})
42
+ @_options_lookup[name] = options
43
+ self[name] = value
58
44
  end
59
-
45
+
60
46
  # Removes the cookie on the client machine by setting the value to an empty
61
47
  # string and setting its expiration date into the past.
62
48
  #
@@ -64,50 +50,80 @@ module Merb
64
50
  # name<~to_s>:: Name of the cookie to delete.
65
51
  # options<Hash>:: Additional options to pass to +set_cookie+.
66
52
  def delete(name, options = {})
67
- cookie = @_cookies.delete(name)
68
- options = Mash.new(options)
69
- options[:expires] = Time.at(0)
70
-
71
- if domain = options[:domain] || Merb::Controller._session_cookie_domain
72
- options[:domain] = domain
73
- end
74
-
75
- set_cookie(name, "", options)
76
- Merb.logger.info("Cookie deleted: #{name} => #{cookie.inspect}")
77
- cookie
53
+ set_cookie(name, "", options.merge(:expires => Time.at(0)))
78
54
  end
79
55
 
80
- # ==== Parameters
81
- # name<~to_s>:: Name of the cookie.
82
- # value<~to_s>:: Value of the cookie.
83
- # options<Hash>:: Additional options for the cookie (see below).
56
+ # Generate any necessary headers.
84
57
  #
85
- # ==== Options (options)
86
- # :path<String>:: The path for which this cookie applies. Defaults to "/".
87
- # :expires<Time>:: Cookie expiry date.
88
- # :domain<String>:: The domain for which this cookie applies.
89
- # :secure<Boolean>:: Security flag.
90
- def set_cookie(name, value, options)
91
- options[:path] = '/' unless options[:path]
92
- if expiry = options[:expires]
93
- options[:expires] = expiry.gmtime.strftime(Merb::Const::COOKIE_EXPIRATION_FORMAT)
58
+ # ==== Returns
59
+ # Hash:: The headers to set, or an empty array if no cookies are set.
60
+ def extract_headers(controller_defaults = {})
61
+ defaults = @_cookie_defaults.merge(controller_defaults)
62
+ cookies = []
63
+ self.each do |name, value|
64
+ # Only set cookies that marked for inclusion in the response header.
65
+ next unless @_options_lookup[name]
66
+ options = defaults.merge(@_options_lookup[name])
67
+ if (expiry = options[:expires]).respond_to?(:gmtime)
68
+ options[:expires] = expiry.gmtime.strftime(Merb::Const::COOKIE_EXPIRATION_FORMAT)
69
+ end
70
+ secure = options.delete(:secure)
71
+ kookie = "#{name}=#{Merb::Request.escape(value)}; "
72
+ options.each { |k, v| kookie << "#{k}=#{v}; " }
73
+ kookie << 'secure' if secure
74
+ cookies << kookie.rstrip
94
75
  end
76
+ cookies.empty? ? {} : { 'Set-Cookie' => cookies }
77
+ end
78
+
79
+ end
80
+
81
+ module CookiesMixin
82
+
83
+ def self.included(base)
84
+ # Allow per-controller default cookie domains (see callback below)
85
+ base.class_inheritable_accessor :_default_cookie_domain
86
+ base._default_cookie_domain = Merb::Config[:default_cookie_domain]
95
87
 
96
- if domain = options[:domain] || Merb::Controller._session_cookie_domain
97
- options[:domain] = domain
88
+ # Add a callback to enable Set-Cookie headers
89
+ base._after_dispatch_callbacks << lambda do |c|
90
+ headers = c.request.cookies.extract_headers(:domain => c._default_cookie_domain)
91
+ c.headers.update(headers)
98
92
  end
99
-
100
- secure = options.delete(:secure)
101
-
102
- @_headers['Set-Cookie'] ||=[]
103
-
104
- kookie = "#{name}=#{value}; "
105
- # options are sorted for testing purposes:
106
- # Hash is unsorted so string is spec is random every run
107
- kookie << options.map{|k, v| "#{k}=#{v};"}.join(' ')
108
- kookie << ' secure' if secure
109
-
110
- @_headers['Set-Cookie'] << kookie
111
93
  end
94
+
95
+ # ==== Returns
96
+ # Merb::Cookies::
97
+ # A new Merb::Cookies instance representing the cookies that came in
98
+ # from the request object
99
+ #
100
+ # ==== Notes
101
+ # Headers are passed into the cookie object so that you can do:
102
+ # cookies[:foo] = "bar"
103
+ def cookies
104
+ request.cookies
105
+ end
106
+
107
+ module RequestMixin
108
+
109
+ # ==== Returns
110
+ # Hash:: The cookies for this request.
111
+ #
112
+ # ==== Notes
113
+ # If a method #default_cookies is defined it will be called. This can
114
+ # be used for session fixation purposes for example. The method returns
115
+ # a Hash of key => value pairs.
116
+ def cookies
117
+ @cookies ||= begin
118
+ values = self.class.query_parse(@env[Merb::Const::HTTP_COOKIE], ';,')
119
+ cookies = Merb::Cookies.new(values, :domain => Merb::Controller._default_cookie_domain, :path => '/')
120
+ cookies.update(default_cookies) if respond_to?(:default_cookies)
121
+ cookies
122
+ end
123
+ end
124
+
125
+ end
126
+
112
127
  end
128
+
113
129
  end
@@ -28,8 +28,10 @@
28
28
  <h3>Parameters</h3>
29
29
  <%= listing("Parameter", "Value", request.params) %>
30
30
 
31
+ <% if request.session? %>
31
32
  <h3>Session</h3>
32
33
  <%= listing("Key", "Value", request.session) %>
34
+ <% end %>
33
35
 
34
36
  <h3>Cookies</h3>
35
37
  <%= listing("Cookie", "Value", request.cookies) %>
@@ -3,8 +3,8 @@ require 'tempfile'
3
3
  module Merb
4
4
 
5
5
  class Request
6
- # def env def session def route_params
7
- attr_accessor :env, :session, :exceptions, :route
6
+ # def env def exceptions def route_params
7
+ attr_accessor :env, :exceptions, :route
8
8
  attr_reader :route_params
9
9
 
10
10
  # by setting these to false, auto-parsing is disabled; this way you can
@@ -207,6 +207,7 @@ module Merb
207
207
  end
208
208
 
209
209
  public
210
+
210
211
  # ==== Returns
211
212
  # Mash:: All request parameters.
212
213
  #
@@ -236,20 +237,7 @@ module Merb
236
237
  def reset_params!
237
238
  @params = nil
238
239
  end
239
-
240
- # ==== Returns
241
- # Hash:: The cookies for this request.
242
- def cookies
243
- @cookies ||= begin
244
- cookies = self.class.query_parse(@env[Merb::Const::HTTP_COOKIE], ';,')
245
- if route && route.allow_fixation? && params.key?(Merb::Controller._session_id_key)
246
- Merb.logger.info("Fixated session id: #{Merb::Controller._session_id_key}")
247
- cookies[Merb::Controller._session_id_key] = params[Merb::Controller._session_id_key]
248
- end
249
- cookies
250
- end
251
- end
252
-
240
+
253
241
  # ==== Returns
254
242
  # String:: The raw post.
255
243
  def raw_post
@@ -459,6 +447,20 @@ module Merb
459
447
  def domain(tld_length = 1)
460
448
  host.split('.').last(1 + tld_length).join('.').sub(/:\d+$/,'')
461
449
  end
450
+
451
+ # ==== Returns
452
+ # Value of If-None-Match request header.
453
+ def if_none_match
454
+ @env[Merb::Const::HTTP_IF_NONE_MATCH]
455
+ end
456
+
457
+ # ==== Returns
458
+ # Value of If-Modified-Since request header.
459
+ def if_modified_since
460
+ if time = @env[Merb::Const::HTTP_IF_MODIFIED_SINCE]
461
+ Time.rfc2822(time)
462
+ end
463
+ end
462
464
 
463
465
  class << self
464
466
 
@@ -258,9 +258,15 @@ module Merb
258
258
  format = fallback[:format] if format == :current
259
259
  url += ".#{format}"
260
260
  end
261
+ if query_params
262
+ fragment = query_params.delete(:fragment)
263
+ end
261
264
  if query_params && !query_params.empty?
262
265
  url += "?" + Merb::Request.params_to_query_string(query_params)
263
266
  end
267
+ if fragment
268
+ url += "##{fragment}"
269
+ end
264
270
  url
265
271
  end
266
272
 
@@ -169,7 +169,7 @@ module Merb
169
169
  # String:: The generated URL.
170
170
  def generate_for_default_route(params, fallback)
171
171
  query_params = params.reject do |k,v|
172
- [:controller, :action, :id, :format].include?(k.to_sym)
172
+ [:controller, :action, :id, :format, :fragment].include?(k.to_sym)
173
173
  end
174
174
 
175
175
  controller = params[:controller] || fallback[:controller]
@@ -191,6 +191,9 @@ module Merb
191
191
  unless query_params.empty?
192
192
  url += "?" + Merb::Request.params_to_query_string(query_params)
193
193
  end
194
+ if params[:fragment]
195
+ url += "##{params[:fragment]}"
196
+ end
194
197
  url
195
198
  end
196
199
  end # self
@@ -0,0 +1,64 @@
1
+ module Merb
2
+ class SessionContainer < Mash
3
+
4
+ class_inheritable_accessor :session_store_type
5
+ cattr_accessor :subclasses
6
+ self.subclasses = []
7
+
8
+ attr_reader :session_id
9
+ attr_accessor :needs_new_cookie
10
+
11
+ class << self
12
+
13
+ # Register the subclass as an available session store type.
14
+ def inherited(klass)
15
+ self.subclasses << klass.to_s
16
+ super
17
+ end
18
+
19
+ # Generates a new session ID and creates a new session.
20
+ #
21
+ # ==== Returns
22
+ # SessionContainer:: The new session.
23
+ def generate
24
+ end
25
+
26
+ # ==== Parameters
27
+ # request<Merb::Request>:: The Merb::Request that came in from Rack.
28
+ #
29
+ # ==== Returns
30
+ # SessionContainer:: a SessionContainer. If no sessions were found,
31
+ # a new SessionContainer will be generated.
32
+ def setup(request)
33
+ end
34
+
35
+ end
36
+
37
+ # ==== Parameters
38
+ # session_id<String>:: A unique identifier for this session.
39
+ def initialize(session_id)
40
+ self.session_id = session_id
41
+ end
42
+
43
+ # Teardown and/or persist the current session.
44
+ #
45
+ # ==== Parameters
46
+ # request<Merb::Request>:: The Merb::Request that came in from Rack.
47
+ def finalize(request)
48
+ end
49
+
50
+ # Assign a new session_id.
51
+ #
52
+ # Recreates the cookie with the default expiration time. Useful during log
53
+ # in for pushing back the expiration date.
54
+ def session_id=(sid)
55
+ self.needs_new_cookie = (@session_id && @session_id != sid)
56
+ @session_id = sid
57
+ end
58
+
59
+ # Regenerate the Session ID
60
+ def regenerate
61
+ end
62
+
63
+ end
64
+ end
@@ -3,36 +3,6 @@ require 'openssl' # to generate the HMAC message digest
3
3
  # Most of this code is taken from bitsweat's implementation in rails
4
4
  module Merb
5
5
 
6
- module SessionMixin
7
-
8
- # Adds a before and after dispatch hook for setting up the cookie session
9
- # store.
10
- #
11
- # ==== Parameters
12
- # base<Class>:: The class to which the SessionMixin is mixed into.
13
- def setup_session
14
- request.session = Merb::CookieSession.new(cookies[_session_id_key], _session_secret_key)
15
- @original_session = request.session.read_cookie
16
- end
17
-
18
- # Finalizes the session by storing the session in a cookie, if the session
19
- # has changed.
20
- def finalize_session
21
- new_session = request.session.read_cookie
22
- if @original_session != new_session
23
- options = {:expires => (Time.now + _session_expiry)}
24
- options[:domain] = _session_cookie_domain if _session_cookie_domain
25
- cookies.set_cookie(_session_id_key, new_session, options)
26
- end
27
- end
28
-
29
- # ==== Returns
30
- # String:: The session store type, i.e. "cookie".
31
- def session_store_type
32
- "cookie"
33
- end
34
- end
35
-
36
6
  # If you have more than 4K of session data or don't want your data to be
37
7
  # visible to the user, pick another session store.
38
8
  #
@@ -41,108 +11,112 @@ module Merb
41
11
  #
42
12
  # A message digest is included with the cookie to ensure data integrity:
43
13
  # a user cannot alter session data without knowing the secret key included
44
- # in the hash.
45
- #
14
+ # in the hash.
15
+ #
46
16
  # To use Cookie Sessions, set in config/merb.yml
47
17
  # :session_secret_key - your secret digest key
48
18
  # :session_store: cookie
49
- class CookieSession
19
+ class CookieSession < SessionContainer
50
20
  # TODO (maybe):
51
21
  # include request ip address
52
22
  # AES encrypt marshaled data
53
-
23
+
54
24
  # Raised when storing more than 4K of session data.
55
25
  class CookieOverflow < StandardError; end
56
-
26
+
57
27
  # Raised when the cookie fails its integrity check.
58
28
  class TamperedWithCookie < StandardError; end
59
-
29
+
60
30
  # Cookies can typically store 4096 bytes.
61
31
  MAX = 4096
62
32
  DIGEST = OpenSSL::Digest::Digest.new('SHA1') # or MD5, RIPEMD160, SHA256?
63
-
64
- attr_reader :data
33
+
34
+ attr_accessor :_original_session_data
35
+
36
+ # The session store type
37
+ self.session_store_type = :cookie
38
+
39
+ class << self
40
+ # Generates a new session ID and creates a new session.
41
+ #
42
+ # ==== Returns
43
+ # SessionContainer:: The new session.
44
+ def generate
45
+ self.new(Merb::SessionMixin.rand_uuid, "", Merb::Request._session_secret_key)
46
+ end
47
+
48
+ # Setup a new session.
49
+ #
50
+ # ==== Parameters
51
+ # request<Merb::Request>:: The Merb::Request that came in from Rack.
52
+ #
53
+ # ==== Returns
54
+ # SessionContainer:: a SessionContainer. If no sessions were found,
55
+ # a new SessionContainer will be generated.
56
+ def setup(request)
57
+ session = self.new(Merb::SessionMixin.rand_uuid,
58
+ request.session_cookie_value, request._session_secret_key)
59
+ session._original_session_data = session.to_cookie
60
+ request.session = session
61
+ end
62
+
63
+ end
65
64
 
66
65
  # ==== Parameters
67
- # cookie<String>:: The cookie.
66
+ # session_id<String>:: A unique identifier for this session.
67
+ # cookie<String>:: The raw cookie data.
68
68
  # secret<String>:: A session secret.
69
69
  #
70
70
  # ==== Raises
71
71
  # ArgumentError:: Nil or blank secret.
72
- def initialize(cookie, secret)
73
- if secret.nil? or secret.blank?
72
+ def initialize(session_id, cookie, secret)
73
+ super session_id
74
+ if secret.blank? || secret.length < 16
75
+ Merb.logger.warn("You must specify a session_secret_key in your init file, and it must be at least 16 characters")
74
76
  raise ArgumentError, 'A secret is required to generate an integrity hash for cookie session data.'
75
77
  end
76
78
  @secret = secret
77
- @data = unmarshal(cookie) || Hash.new
79
+ self.update(unmarshal(cookie))
78
80
  end
79
-
80
- # ==== Returns
81
- # String:: Cookie value.
81
+
82
+ # Teardown and/or persist the current session.
82
83
  #
83
- # ==== Raises
84
- # CookieOverflow:: Session contains too much information.
85
- def read_cookie
86
- unless @data.nil?
87
- updated = marshal(@data)
88
- raise CookieOverflow if updated.size > MAX
89
- updated
90
- end
91
- end
92
-
93
84
  # ==== Parameters
94
- # k<~to_s>:: The key of the session parameter to set.
95
- # v<~to_s>:: The value of the session parameter to set.
96
- def []=(k, v)
97
- @data[k] = v
85
+ # request<Merb::Request>:: The Merb::Request that came in from Rack.
86
+ def finalize(request)
87
+ if _original_session_data != (new_session_data = self.to_cookie)
88
+ request.set_session_cookie_value(new_session_data)
89
+ end
98
90
  end
99
91
 
100
- # ==== Parameters
101
- # k<~to_s>:: The key of the session parameter to retrieve.
92
+ # Create the raw cookie string; includes an HMAC keyed message digest.
102
93
  #
103
94
  # ==== Returns
104
- # String:: The value of the session parameter.
105
- def [](k)
106
- @data[k]
107
- end
108
-
109
- # Yields the session data to an each block.
95
+ # String:: Cookie value.
96
+ #
97
+ # ==== Raises
98
+ # CookieOverflow:: Session contains too much information.
110
99
  #
111
- # ==== Parameter
112
- # &b:: The block to pass to each.
113
- def each(&b)
114
- @data.each(&b)
100
+ # ==== Notes
101
+ # The data (self) is converted to a Hash first, since a container might
102
+ # choose to do a full Marshal on the data, which would make it persist
103
+ # attributes like 'needs_new_cookie', which it shouldn't.
104
+ def to_cookie
105
+ unless self.empty?
106
+ data = self.serialize
107
+ value = Merb::Request.escape "#{data}--#{generate_digest(data)}"
108
+ raise CookieOverflow if value.size > MAX
109
+ value
110
+ end
115
111
  end
116
112
 
117
- # Deletes the session by emptying stored data.
118
- def delete
119
- @data = {}
120
- end
121
-
122
113
  private
123
114
 
124
- # Attempts to redirect any messages to the data object.
125
- def method_missing(name, *args, &block)
126
- @data.send(name, *args, &block)
127
- end
128
-
129
115
  # Generate the HMAC keyed message digest. Uses SHA1.
130
116
  def generate_digest(data)
131
117
  OpenSSL::HMAC.hexdigest(DIGEST, @secret, data)
132
118
  end
133
-
134
- # Marshal a session hash into safe cookie data. Include an integrity hash.
135
- #
136
- # ==== Parameters
137
- # session<Hash>:: The session to store in the cookie.
138
- #
139
- # ==== Returns
140
- # String:: The cookie to be stored.
141
- def marshal(session)
142
- data = Base64.encode64(Marshal.dump(session)).chop
143
- Merb::Request.escape "#{data}--#{generate_digest(data)}"
144
- end
145
-
119
+
146
120
  # Unmarshal cookie data to a hash and verify its integrity.
147
121
  #
148
122
  # ==== Parameters
@@ -154,15 +128,31 @@ module Merb
154
128
  # ==== Returns
155
129
  # Hash:: The stored session data.
156
130
  def unmarshal(cookie)
157
- if cookie
158
- data, digest = cookie.split('--')
159
- return {} if data.blank?
131
+ if cookie.blank?
132
+ {}
133
+ else
134
+ data, digest = Merb::Request.unescape(cookie).split('--')
135
+ return {} if data.blank? || digest.blank?
160
136
  unless digest == generate_digest(data)
161
- delete
162
- raise TamperedWithCookie, "Maybe the site's session_secret_key has changed?"
137
+ clear
138
+ unless Merb::Config[:ignore_tampered_cookies]
139
+ raise TamperedWithCookie, "Maybe the site's session_secret_key has changed?"
140
+ end
163
141
  end
164
- Marshal.load(Base64.decode64(data))
142
+ unserialize(data)
165
143
  end
166
144
  end
145
+
146
+ protected
147
+
148
+ # Serialize current session data - as a Hash
149
+ def serialize
150
+ Base64.encode64(Marshal.dump(self.to_hash)).chop
151
+ end
152
+
153
+ # Unserialize the raw cookie data - to a Hash
154
+ def unserialize(data)
155
+ Marshal.load(Base64.decode64(data)) rescue {}
156
+ end
167
157
  end
168
- end
158
+ end