actionpack 4.0.0.beta1 → 4.0.0.rc1

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

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +195 -11
  3. data/lib/abstract_controller/base.rb +1 -1
  4. data/lib/abstract_controller/helpers.rb +2 -2
  5. data/lib/abstract_controller/layouts.rb +10 -5
  6. data/lib/abstract_controller/rendering.rb +11 -3
  7. data/lib/abstract_controller/translation.rb +1 -1
  8. data/lib/action_controller/log_subscriber.rb +5 -0
  9. data/lib/action_controller/metal.rb +2 -3
  10. data/lib/action_controller/metal/force_ssl.rb +52 -17
  11. data/lib/action_controller/metal/helpers.rb +0 -1
  12. data/lib/action_controller/metal/hide_actions.rb +1 -1
  13. data/lib/action_controller/metal/http_authentication.rb +3 -2
  14. data/lib/action_controller/metal/live.rb +34 -0
  15. data/lib/action_controller/metal/rendering.rb +1 -1
  16. data/lib/action_controller/metal/strong_parameters.rb +7 -3
  17. data/lib/action_controller/test_case.rb +45 -11
  18. data/lib/action_dispatch.rb +4 -6
  19. data/lib/action_dispatch/http/cache.rb +2 -2
  20. data/lib/action_dispatch/http/headers.rb +39 -15
  21. data/lib/action_dispatch/http/mime_negotiation.rb +1 -1
  22. data/lib/action_dispatch/http/mime_type.rb +11 -3
  23. data/lib/action_dispatch/http/parameters.rb +17 -24
  24. data/lib/action_dispatch/http/request.rb +17 -2
  25. data/lib/action_dispatch/http/response.rb +2 -1
  26. data/lib/action_dispatch/http/upload.rb +5 -5
  27. data/lib/action_dispatch/http/url.rb +53 -12
  28. data/lib/action_dispatch/journey/formatter.rb +1 -1
  29. data/lib/action_dispatch/journey/path/pattern.rb +1 -1
  30. data/lib/action_dispatch/journey/route.rb +8 -0
  31. data/lib/action_dispatch/journey/router.rb +3 -1
  32. data/lib/action_dispatch/journey/visitors.rb +8 -0
  33. data/lib/action_dispatch/middleware/cookies.rb +169 -135
  34. data/lib/action_dispatch/middleware/exception_wrapper.rb +1 -0
  35. data/lib/action_dispatch/middleware/remote_ip.rb +2 -2
  36. data/lib/action_dispatch/middleware/request_id.rb +1 -1
  37. data/lib/action_dispatch/middleware/session/cookie_store.rb +38 -58
  38. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb +1 -1
  39. data/lib/action_dispatch/middleware/templates/rescues/_trace.erb +4 -6
  40. data/lib/action_dispatch/middleware/templates/rescues/routing_error.erb +1 -1
  41. data/lib/action_dispatch/middleware/templates/rescues/template_error.erb +1 -1
  42. data/lib/action_dispatch/routing.rb +28 -64
  43. data/lib/action_dispatch/routing/mapper.rb +61 -48
  44. data/lib/action_dispatch/routing/route_set.rb +17 -14
  45. data/lib/action_dispatch/testing/assertions/routing.rb +2 -2
  46. data/lib/action_dispatch/testing/assertions/selector.rb +2 -2
  47. data/lib/action_dispatch/testing/integration.rb +36 -35
  48. data/lib/action_dispatch/testing/test_process.rb +1 -1
  49. data/lib/action_pack/version.rb +7 -6
  50. data/lib/action_view/buffers.rb +6 -0
  51. data/lib/action_view/dependency_tracker.rb +3 -1
  52. data/lib/action_view/helpers/asset_tag_helper.rb +13 -8
  53. data/lib/action_view/helpers/capture_helper.rb +2 -2
  54. data/lib/action_view/helpers/date_helper.rb +1 -1
  55. data/lib/action_view/helpers/form_helper.rb +56 -19
  56. data/lib/action_view/helpers/form_options_helper.rb +3 -3
  57. data/lib/action_view/helpers/form_tag_helper.rb +1 -1
  58. data/lib/action_view/helpers/javascript_helper.rb +2 -2
  59. data/lib/action_view/helpers/number_helper.rb +25 -0
  60. data/lib/action_view/helpers/tags/base.rb +9 -10
  61. data/lib/action_view/helpers/tags/check_box.rb +1 -1
  62. data/lib/action_view/helpers/tags/checkable.rb +2 -2
  63. data/lib/action_view/helpers/tags/collection_check_boxes.rb +3 -3
  64. data/lib/action_view/helpers/tags/collection_helpers.rb +3 -3
  65. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +3 -3
  66. data/lib/action_view/helpers/tags/collection_select.rb +1 -1
  67. data/lib/action_view/helpers/tags/color_field.rb +2 -2
  68. data/lib/action_view/helpers/tags/date_field.rb +2 -2
  69. data/lib/action_view/helpers/tags/date_select.rb +2 -2
  70. data/lib/action_view/helpers/tags/datetime_field.rb +2 -2
  71. data/lib/action_view/helpers/tags/datetime_local_field.rb +2 -2
  72. data/lib/action_view/helpers/tags/datetime_select.rb +2 -2
  73. data/lib/action_view/helpers/tags/email_field.rb +2 -2
  74. data/lib/action_view/helpers/tags/file_field.rb +2 -2
  75. data/lib/action_view/helpers/tags/grouped_collection_select.rb +2 -2
  76. data/lib/action_view/helpers/tags/hidden_field.rb +2 -2
  77. data/lib/action_view/helpers/tags/label.rb +2 -2
  78. data/lib/action_view/helpers/tags/month_field.rb +2 -2
  79. data/lib/action_view/helpers/tags/number_field.rb +2 -2
  80. data/lib/action_view/helpers/tags/password_field.rb +2 -2
  81. data/lib/action_view/helpers/tags/radio_button.rb +2 -2
  82. data/lib/action_view/helpers/tags/range_field.rb +2 -2
  83. data/lib/action_view/helpers/tags/search_field.rb +2 -2
  84. data/lib/action_view/helpers/tags/select.rb +2 -3
  85. data/lib/action_view/helpers/tags/tel_field.rb +2 -2
  86. data/lib/action_view/helpers/tags/text_area.rb +2 -2
  87. data/lib/action_view/helpers/tags/text_field.rb +2 -2
  88. data/lib/action_view/helpers/tags/time_field.rb +2 -2
  89. data/lib/action_view/helpers/tags/time_select.rb +2 -2
  90. data/lib/action_view/helpers/tags/time_zone_select.rb +2 -2
  91. data/lib/action_view/helpers/tags/url_field.rb +2 -2
  92. data/lib/action_view/helpers/tags/week_field.rb +2 -2
  93. data/lib/action_view/helpers/text_helper.rb +8 -5
  94. data/lib/action_view/helpers/url_helper.rb +18 -6
  95. data/lib/action_view/lookup_context.rb +7 -1
  96. data/lib/action_view/path_set.rb +6 -0
  97. data/lib/action_view/renderer/abstract_renderer.rb +15 -0
  98. data/lib/action_view/renderer/partial_renderer.rb +14 -0
  99. data/lib/action_view/renderer/renderer.rb +6 -0
  100. data/lib/action_view/template.rb +3 -2
  101. data/lib/action_view/template/handlers/erb.rb +29 -3
  102. data/lib/action_view/template/resolver.rb +3 -3
  103. data/lib/action_view/test_case.rb +1 -0
  104. data/lib/action_view/vendor/html-scanner/html/sanitizer.rb +5 -5
  105. data/lib/action_view/vendor/html-scanner/html/selector.rb +8 -8
  106. metadata +8 -8
@@ -18,7 +18,7 @@ module ActionDispatch
18
18
  query_parameters.dup
19
19
  end
20
20
  params.merge!(path_parameters)
21
- encode_params(params).with_indifferent_access
21
+ params.with_indifferent_access
22
22
  end
23
23
  end
24
24
  alias :params :parameters
@@ -50,40 +50,33 @@ module ActionDispatch
50
50
 
51
51
  private
52
52
 
53
+ # Convert nested Hash to HashWithIndifferentAccess
54
+ # and UTF-8 encode both keys and values in nested Hash.
55
+ #
53
56
  # TODO: Validate that the characters are UTF-8. If they aren't,
54
57
  # you'll get a weird error down the road, but our form handling
55
58
  # should really prevent that from happening
56
- def encode_params(params)
59
+ def normalize_encode_params(params)
57
60
  if params.is_a?(String)
58
61
  return params.force_encoding(Encoding::UTF_8).encode!
59
62
  elsif !params.is_a?(Hash)
60
63
  return params
61
64
  end
62
65
 
66
+ new_hash = {}
63
67
  params.each do |k, v|
64
- case v
65
- when Hash
66
- encode_params(v)
67
- when Array
68
- v.map! {|el| encode_params(el) }
69
- else
70
- encode_params(v)
71
- end
72
- end
73
- end
74
-
75
- # Convert nested Hash to ActiveSupport::HashWithIndifferentAccess
76
- def normalize_parameters(value)
77
- case value
78
- when Hash
79
- h = {}
80
- value.each { |k, v| h[k] = normalize_parameters(v) }
81
- h.with_indifferent_access
82
- when Array
83
- value.map { |e| normalize_parameters(e) }
84
- else
85
- value
68
+ new_key = k.is_a?(String) ? k.dup.force_encoding("UTF-8").encode! : k
69
+ new_hash[new_key] =
70
+ case v
71
+ when Hash
72
+ normalize_encode_params(v)
73
+ when Array
74
+ v.map! {|el| normalize_encode_params(el) }
75
+ else
76
+ normalize_encode_params(v)
77
+ end
86
78
  end
79
+ new_hash.with_indifferent_access
87
80
  end
88
81
  end
89
82
  end
@@ -156,14 +156,29 @@ module ActionDispatch
156
156
  @original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath)
157
157
  end
158
158
 
159
+ # Returns the +String+ full path including params of the last URL requested.
160
+ #
161
+ # # get "/articles"
162
+ # request.fullpath # => "/articles"
163
+ #
164
+ # # get "/articles?page=2"
165
+ # request.fullpath # => "/articles?page=2"
159
166
  def fullpath
160
167
  @fullpath ||= super
161
168
  end
162
169
 
170
+ # Returns the original request URL as a +String+.
171
+ #
172
+ # # get "/articles?page=2"
173
+ # request.original_url # => "http://www.example.com/articles?page=2"
163
174
  def original_url
164
175
  base_url + original_fullpath
165
176
  end
166
177
 
178
+ # The +String+ MIME type of the request.
179
+ #
180
+ # # get "/articles"
181
+ # request.media_type # => "application/x-www-form-urlencoded"
167
182
  def media_type
168
183
  content_mime_type.to_s
169
184
  end
@@ -256,7 +271,7 @@ module ActionDispatch
256
271
 
257
272
  # Override Rack's GET method to support indifferent access
258
273
  def GET
259
- @env["action_dispatch.request.query_parameters"] ||= (normalize_parameters(super) || {})
274
+ @env["action_dispatch.request.query_parameters"] ||= (normalize_encode_params(super) || {})
260
275
  rescue TypeError => e
261
276
  raise ActionController::BadRequest.new(:query, e)
262
277
  end
@@ -264,7 +279,7 @@ module ActionDispatch
264
279
 
265
280
  # Override Rack's POST method to support indifferent access
266
281
  def POST
267
- @env["action_dispatch.request.request_parameters"] ||= (normalize_parameters(super) || {})
282
+ @env["action_dispatch.request.request_parameters"] ||= (normalize_encode_params(super) || {})
268
283
  rescue TypeError => e
269
284
  raise ActionController::BadRequest.new(:request, e)
270
285
  end
@@ -55,6 +55,7 @@ module ActionDispatch # :nodoc:
55
55
  CONTENT_TYPE = "Content-Type".freeze
56
56
  SET_COOKIE = "Set-Cookie".freeze
57
57
  LOCATION = "Location".freeze
58
+ NO_CONTENT_CODES = [204, 304]
58
59
 
59
60
  cattr_accessor(:default_charset) { "utf-8" }
60
61
  cattr_accessor(:default_headers)
@@ -289,7 +290,7 @@ module ActionDispatch # :nodoc:
289
290
 
290
291
  header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join)
291
292
 
292
- if [204, 304].include?(@status)
293
+ if NO_CONTENT_CODES.include?(@status)
293
294
  header.delete CONTENT_TYPE
294
295
  [status, header, []]
295
296
  else
@@ -6,7 +6,7 @@ module ActionDispatch
6
6
  # of its interface is available directly for convenience.
7
7
  #
8
8
  # Uploaded files are temporary files whose lifespan is one request. When
9
- # the object is finalized Ruby unlinks the file, so there is not need to
9
+ # the object is finalized Ruby unlinks the file, so there is no need to
10
10
  # clean them with a separate maintenance task.
11
11
  class UploadedFile
12
12
  # The basename of the file in the client.
@@ -75,16 +75,16 @@ module ActionDispatch
75
75
  end
76
76
 
77
77
  module Upload # :nodoc:
78
- # Convert nested Hash to ActiveSupport::HashWithIndifferentAccess and replace
79
- # file upload hash with UploadedFile objects
80
- def normalize_parameters(value)
78
+ # Replace file upload hash with UploadedFile objects
79
+ # when normalize and encode parameters.
80
+ def normalize_encode_params(value)
81
81
  if Hash === value && value.has_key?(:tempfile)
82
82
  UploadedFile.new(value)
83
83
  else
84
84
  super
85
85
  end
86
86
  end
87
- private :normalize_parameters
87
+ private :normalize_encode_params
88
88
  end
89
89
  end
90
90
  end
@@ -4,7 +4,9 @@ require 'active_support/core_ext/hash/slice'
4
4
  module ActionDispatch
5
5
  module Http
6
6
  module URL
7
- IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
7
+ IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
8
+ HOST_REGEXP = /(^.*:\/\/)?([^:]+)(?::(\d+$))?/
9
+ PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/
8
10
 
9
11
  mattr_accessor :tld_length
10
12
  self.tld_length = 1
@@ -28,6 +30,7 @@ module ActionDispatch
28
30
  end
29
31
 
30
32
  def url_for(options = {})
33
+ options = options.dup
31
34
  path = options.delete(:script_name).to_s.chomp("/")
32
35
  path << options.delete(:path).to_s
33
36
 
@@ -59,14 +62,20 @@ module ActionDispatch
59
62
  result = ""
60
63
 
61
64
  unless options[:only_path]
62
- unless options[:protocol] == false
63
- result << (options[:protocol] || "http")
64
- result << ":" unless result.match(%r{:|//})
65
+ if match = options[:host].match(HOST_REGEXP)
66
+ options[:protocol] ||= match[1] unless options[:protocol] == false
67
+ options[:host] = match[2]
68
+ options[:port] = match[3] unless options.key?(:port)
65
69
  end
66
- result << "//" unless result.match("//")
70
+
71
+ options[:protocol] = normalize_protocol(options)
72
+ options[:host] = normalize_host(options)
73
+ options[:port] = normalize_port(options)
74
+
75
+ result << options[:protocol]
67
76
  result << rewrite_authentication(options)
68
- result << host_or_subdomain_and_domain(options)
69
- result << ":#{options.delete(:port)}" if options[:port]
77
+ result << options[:host]
78
+ result << ":#{options[:port]}" if options[:port]
70
79
  end
71
80
  result
72
81
  end
@@ -75,6 +84,10 @@ module ActionDispatch
75
84
  host && IP_HOST_REGEXP !~ host
76
85
  end
77
86
 
87
+ def same_host?(options)
88
+ (options[:subdomain] == true || !options.key?(:subdomain)) && options[:domain].nil?
89
+ end
90
+
78
91
  def rewrite_authentication(options)
79
92
  if options[:user] && options[:password]
80
93
  "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
@@ -83,19 +96,47 @@ module ActionDispatch
83
96
  end
84
97
  end
85
98
 
86
- def host_or_subdomain_and_domain(options)
87
- return options[:host] if !named_host?(options[:host]) || (options[:subdomain].nil? && options[:domain].nil?)
99
+ def normalize_protocol(options)
100
+ case options[:protocol]
101
+ when nil
102
+ "http://"
103
+ when false, "//"
104
+ "//"
105
+ when PROTOCOL_REGEXP
106
+ "#{$1}://"
107
+ else
108
+ raise ArgumentError, "Invalid :protocol option: #{options[:protocol].inspect}"
109
+ end
110
+ end
111
+
112
+ def normalize_host(options)
113
+ return options[:host] if !named_host?(options[:host]) || same_host?(options)
88
114
 
89
115
  tld_length = options[:tld_length] || @@tld_length
90
116
 
91
117
  host = ""
92
- unless options[:subdomain] == false
93
- host << (options[:subdomain] || extract_subdomain(options[:host], tld_length)).to_param
94
- host << "."
118
+ if options[:subdomain] == true || !options.key?(:subdomain)
119
+ host << extract_subdomain(options[:host], tld_length).to_param
120
+ elsif options[:subdomain].present?
121
+ host << options[:subdomain].to_param
95
122
  end
123
+ host << "." unless host.empty?
96
124
  host << (options[:domain] || extract_domain(options[:host], tld_length))
97
125
  host
98
126
  end
127
+
128
+ def normalize_port(options)
129
+ return nil if options[:port].nil? || options[:port] == false
130
+
131
+ case options[:protocol]
132
+ when "//"
133
+ nil
134
+ when "https://"
135
+ options[:port].to_i == 443 ? nil : options[:port]
136
+ else
137
+ options[:port].to_i == 80 ? nil : options[:port]
138
+ end
139
+ end
99
140
  end
100
141
 
101
142
  def initialize(env)
@@ -58,7 +58,7 @@ module ActionDispatch
58
58
  end
59
59
  end
60
60
 
61
- parameterized_parts.keep_if { |_, v| v }
61
+ parameterized_parts.keep_if { |_, v| v }
62
62
  parameterized_parts
63
63
  end
64
64
 
@@ -20,7 +20,7 @@ module ActionDispatch
20
20
  @separators = strexp.separators.join
21
21
  @anchored = strexp.anchor
22
22
  else
23
- raise "wtf bro: #{strexp}"
23
+ raise ArgumentError, "Bad expression: #{strexp}"
24
24
  end
25
25
 
26
26
  @names = nil
@@ -71,6 +71,10 @@ module ActionDispatch
71
71
  Visitors::Formatter.new(path_options).accept(path.spec)
72
72
  end
73
73
 
74
+ def optimized_path
75
+ Visitors::OptimizedPath.new.accept(path.spec)
76
+ end
77
+
74
78
  def optional_parts
75
79
  path.optional_names.map { |n| n.to_sym }
76
80
  end
@@ -98,6 +102,10 @@ module ActionDispatch
98
102
  value === request.send(method).to_s
99
103
  when Array
100
104
  value.include?(request.send(method))
105
+ when TrueClass
106
+ request.send(method).present?
107
+ when FalseClass
108
+ request.send(method).blank?
101
109
  else
102
110
  value === request.send(method)
103
111
  end
@@ -38,7 +38,9 @@ module ActionDispatch
38
38
  env['REMOTE_ADDR']
39
39
  end
40
40
 
41
- def [](k); env[k]; end
41
+ def [](k)
42
+ env[k]
43
+ end
42
44
  end
43
45
 
44
46
  attr_reader :request_class, :formatter
@@ -74,6 +74,14 @@ module ActionDispatch
74
74
  end
75
75
  end
76
76
 
77
+ class OptimizedPath < String # :nodoc:
78
+ private
79
+
80
+ def visit_GROUP(node)
81
+ ""
82
+ end
83
+ end
84
+
77
85
  # Used for formatting urls (url_for)
78
86
  class Formatter < Visitor # :nodoc:
79
87
  attr_reader :options, :consumed
@@ -1,5 +1,6 @@
1
1
  require 'active_support/core_ext/hash/keys'
2
2
  require 'active_support/core_ext/module/attribute_accessors'
3
+ require 'active_support/core_ext/object/blank'
3
4
  require 'active_support/key_generator'
4
5
  require 'active_support/message_verifier'
5
6
 
@@ -30,7 +31,7 @@ module ActionDispatch
30
31
  #
31
32
  # # Sets a signed cookie, which prevents users from tampering with its value.
32
33
  # # The cookie is signed by your app's <tt>config.secret_key_base</tt> value.
33
- # # It can be read using the signed method <tt>cookies.signed[:key]</tt>
34
+ # # It can be read using the signed method <tt>cookies.signed[:name]</tt>
34
35
  # cookies.signed[:user_id] = current_user.id
35
36
  #
36
37
  # # Sets a "permanent" cookie (which expires in 20 years from now).
@@ -52,13 +53,13 @@ module ActionDispatch
52
53
  #
53
54
  # Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie:
54
55
  #
55
- # cookies[:key] = {
56
+ # cookies[:name] = {
56
57
  # value: 'a yummy cookie',
57
58
  # expires: 1.year.from_now,
58
59
  # domain: 'domain.com'
59
60
  # }
60
61
  #
61
- # cookies.delete(:key, domain: 'domain.com')
62
+ # cookies.delete(:name, domain: 'domain.com')
62
63
  #
63
64
  # The option symbols for setting cookies are:
64
65
  #
@@ -69,7 +70,7 @@ module ActionDispatch
69
70
  # restrict to the domain level. If you use a schema like www.example.com
70
71
  # and want to share session with user.example.com set <tt>:domain</tt>
71
72
  # to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with
72
- # <tt>:all</tt> again when deleting keys.
73
+ # <tt>:all</tt> again when deleting cookies.
73
74
  #
74
75
  # domain: nil # Does not sets cookie domain. (default)
75
76
  # domain: :all # Allow the cookie for the top most level
@@ -86,7 +87,8 @@ module ActionDispatch
86
87
  SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze
87
88
  ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
88
89
  ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
89
- TOKEN_KEY = "action_dispatch.secret_token".freeze
90
+ SECRET_TOKEN = "action_dispatch.secret_token".freeze
91
+ SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
90
92
 
91
93
  # Cookies can typically store 4096 bytes.
92
94
  MAX_COOKIE_SIZE = 4096
@@ -94,8 +96,99 @@ module ActionDispatch
94
96
  # Raised when storing more than 4K of session data.
95
97
  CookieOverflow = Class.new StandardError
96
98
 
99
+ # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed
100
+ module ChainedCookieJars
101
+ # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
102
+ #
103
+ # cookies.permanent[:prefers_open_id] = true
104
+ # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
105
+ #
106
+ # This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
107
+ #
108
+ # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
109
+ #
110
+ # cookies.permanent.signed[:remember_me] = current_user.id
111
+ # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
112
+ def permanent
113
+ @permanent ||= PermanentCookieJar.new(self, @key_generator, @options)
114
+ end
115
+
116
+ # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
117
+ # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
118
+ # cookie was tampered with by the user (or a 3rd party), nil will be returned.
119
+ #
120
+ # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set,
121
+ # legacy cookies signed with the old key generator will be transparently upgraded.
122
+ #
123
+ # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+.
124
+ #
125
+ # Example:
126
+ #
127
+ # cookies.signed[:discount] = 45
128
+ # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
129
+ #
130
+ # cookies.signed[:discount] # => 45
131
+ def signed
132
+ @signed ||=
133
+ if @options[:upgrade_legacy_signed_cookies]
134
+ UpgradeLegacySignedCookieJar.new(self, @key_generator, @options)
135
+ else
136
+ SignedCookieJar.new(self, @key_generator, @options)
137
+ end
138
+ end
139
+
140
+ # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
141
+ # If the cookie was tampered with by the user (or a 3rd party), nil will be returned.
142
+ #
143
+ # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set,
144
+ # legacy cookies signed with the old key generator will be transparently upgraded.
145
+ #
146
+ # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+.
147
+ #
148
+ # Example:
149
+ #
150
+ # cookies.encrypted[:discount] = 45
151
+ # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/
152
+ #
153
+ # cookies.encrypted[:discount] # => 45
154
+ def encrypted
155
+ @encrypted ||=
156
+ if @options[:upgrade_legacy_signed_cookies]
157
+ UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options)
158
+ else
159
+ EncryptedCookieJar.new(self, @key_generator, @options)
160
+ end
161
+ end
162
+
163
+ # Returns the +signed+ or +encrypted jar, preferring +encrypted+ if +secret_key_base+ is set.
164
+ # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
165
+ def signed_or_encrypted
166
+ @signed_or_encrypted ||=
167
+ if @options[:secret_key_base].present?
168
+ encrypted
169
+ else
170
+ signed
171
+ end
172
+ end
173
+ end
174
+
175
+ module VerifyAndUpgradeLegacySignedMessage
176
+ def initialize(*args)
177
+ super
178
+ @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token])
179
+ end
180
+
181
+ def verify_and_upgrade_legacy_signed_message(name, signed_message)
182
+ @legacy_verifier.verify(signed_message).tap do |value|
183
+ self[name] = value
184
+ end
185
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
186
+ nil
187
+ end
188
+ end
189
+
97
190
  class CookieJar #:nodoc:
98
- include Enumerable
191
+ include Enumerable, ChainedCookieJars
99
192
 
100
193
  # This regular expression is used to split the levels of a domain.
101
194
  # The top level domain can be any string without a period or
@@ -115,7 +208,10 @@ module ActionDispatch
115
208
  { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '',
116
209
  encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '',
117
210
  encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '',
118
- token_key: env[TOKEN_KEY] }
211
+ secret_token: env[SECRET_TOKEN],
212
+ secret_key_base: env[SECRET_KEY_BASE],
213
+ upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?
214
+ }
119
215
  end
120
216
 
121
217
  def self.build(request)
@@ -184,7 +280,7 @@ module ActionDispatch
184
280
 
185
281
  # Sets the cookie named +name+. The second argument may be the very cookie
186
282
  # value, or a hash of options as documented above.
187
- def []=(key, options)
283
+ def []=(name, options)
188
284
  if options.is_a?(Hash)
189
285
  options.symbolize_keys!
190
286
  value = options[:value]
@@ -195,36 +291,36 @@ module ActionDispatch
195
291
 
196
292
  handle_options(options)
197
293
 
198
- if @cookies[key.to_s] != value or options[:expires]
199
- @cookies[key.to_s] = value
200
- @set_cookies[key.to_s] = options
201
- @delete_cookies.delete(key.to_s)
294
+ if @cookies[name.to_s] != value or options[:expires]
295
+ @cookies[name.to_s] = value
296
+ @set_cookies[name.to_s] = options
297
+ @delete_cookies.delete(name.to_s)
202
298
  end
203
299
 
204
300
  value
205
301
  end
206
302
 
207
303
  # Removes the cookie on the client machine by setting the value to an empty string
208
- # and setting its expiration date into the past. Like <tt>[]=</tt>, you can pass in
304
+ # and the expiration date in the past. Like <tt>[]=</tt>, you can pass in
209
305
  # an options hash to delete cookies with extra data such as a <tt>:path</tt>.
210
- def delete(key, options = {})
211
- return unless @cookies.has_key? key.to_s
306
+ def delete(name, options = {})
307
+ return unless @cookies.has_key? name.to_s
212
308
 
213
309
  options.symbolize_keys!
214
310
  handle_options(options)
215
311
 
216
- value = @cookies.delete(key.to_s)
217
- @delete_cookies[key.to_s] = options
312
+ value = @cookies.delete(name.to_s)
313
+ @delete_cookies[name.to_s] = options
218
314
  value
219
315
  end
220
316
 
221
317
  # Whether the given cookie is to be deleted by this CookieJar.
222
318
  # Like <tt>[]=</tt>, you can pass in an options hash to test if a
223
319
  # deletion applies to a specific <tt>:path</tt>, <tt>:domain</tt> etc.
224
- def deleted?(key, options = {})
320
+ def deleted?(name, options = {})
225
321
  options.symbolize_keys!
226
322
  handle_options(options)
227
- @delete_cookies[key.to_s] == options
323
+ @delete_cookies[name.to_s] == options
228
324
  end
229
325
 
230
326
  # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie
@@ -232,59 +328,6 @@ module ActionDispatch
232
328
  @cookies.each_key{ |k| delete(k, options) }
233
329
  end
234
330
 
235
- # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
236
- #
237
- # cookies.permanent[:prefers_open_id] = true
238
- # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
239
- #
240
- # This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
241
- #
242
- # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
243
- #
244
- # cookies.permanent.signed[:remember_me] = current_user.id
245
- # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
246
- def permanent
247
- @permanent ||= PermanentCookieJar.new(self, @key_generator, @options)
248
- end
249
-
250
- # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
251
- # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
252
- # cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will
253
- # be raised.
254
- #
255
- # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+.
256
- #
257
- # Example:
258
- #
259
- # cookies.signed[:discount] = 45
260
- # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
261
- #
262
- # cookies.signed[:discount] # => 45
263
- def signed
264
- @signed ||= SignedCookieJar.new(self, @key_generator, @options)
265
- end
266
-
267
- # Only needed for supporting the +UpgradeSignatureToEncryptionCookieStore+, users and plugin authors should not use this
268
- def signed_using_old_secret #:nodoc:
269
- @signed_using_old_secret ||= SignedCookieJar.new(self, ActiveSupport::DummyKeyGenerator.new(@options[:token_key]), @options)
270
- end
271
-
272
- # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
273
- # If the cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception
274
- # will be raised.
275
- #
276
- # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+.
277
- #
278
- # Example:
279
- #
280
- # cookies.encrypted[:discount] = 45
281
- # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/
282
- #
283
- # cookies.encrypted[:discount] # => 45
284
- def encrypted
285
- @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options)
286
- end
287
-
288
331
  def write(headers)
289
332
  @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) }
290
333
  @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) }
@@ -299,24 +342,25 @@ module ActionDispatch
299
342
  self.always_write_cookie = false
300
343
 
301
344
  private
302
-
303
345
  def write_cookie?(cookie)
304
346
  @secure || !cookie[:secure] || always_write_cookie
305
347
  end
306
348
  end
307
349
 
308
350
  class PermanentCookieJar #:nodoc:
351
+ include ChainedCookieJars
352
+
309
353
  def initialize(parent_jar, key_generator, options = {})
310
354
  @parent_jar = parent_jar
311
355
  @key_generator = key_generator
312
356
  @options = options
313
357
  end
314
358
 
315
- def [](key)
359
+ def [](name)
316
360
  @parent_jar[name.to_s]
317
361
  end
318
362
 
319
- def []=(key, options)
363
+ def []=(name, options)
320
364
  if options.is_a?(Hash)
321
365
  options.symbolize_keys!
322
366
  else
@@ -324,28 +368,13 @@ module ActionDispatch
324
368
  end
325
369
 
326
370
  options[:expires] = 20.years.from_now
327
- @parent_jar[key] = options
328
- end
329
-
330
- def permanent
331
- @permanent ||= PermanentCookieJar.new(self, @key_generator, @options)
332
- end
333
-
334
- def signed
335
- @signed ||= SignedCookieJar.new(self, @key_generator, @options)
336
- end
337
-
338
- def encrypted
339
- @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options)
340
- end
341
-
342
- def method_missing(method, *arguments, &block)
343
- ActiveSupport::Deprecation.warn "#{method} is deprecated with no replacement. " +
344
- "You probably want to try this method over the parent CookieJar."
371
+ @parent_jar[name] = options
345
372
  end
346
373
  end
347
374
 
348
375
  class SignedCookieJar #:nodoc:
376
+ include ChainedCookieJars
377
+
349
378
  def initialize(parent_jar, key_generator, options = {})
350
379
  @parent_jar = parent_jar
351
380
  @options = options
@@ -355,13 +384,11 @@ module ActionDispatch
355
384
 
356
385
  def [](name)
357
386
  if signed_message = @parent_jar[name]
358
- @verifier.verify(signed_message)
387
+ verify(signed_message)
359
388
  end
360
- rescue ActiveSupport::MessageVerifier::InvalidSignature
361
- nil
362
389
  end
363
390
 
364
- def []=(key, options)
391
+ def []=(name, options)
365
392
  if options.is_a?(Hash)
366
393
  options.symbolize_keys!
367
394
  options[:value] = @verifier.generate(options[:value])
@@ -370,32 +397,38 @@ module ActionDispatch
370
397
  end
371
398
 
372
399
  raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
373
- @parent_jar[key] = options
400
+ @parent_jar[name] = options
374
401
  end
375
402
 
376
- def permanent
377
- @permanent ||= PermanentCookieJar.new(self, @key_generator, @options)
378
- end
379
-
380
- def signed
381
- @signed ||= SignedCookieJar.new(self, @key_generator, @options)
382
- end
403
+ private
404
+ def verify(signed_message)
405
+ @verifier.verify(signed_message)
406
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
407
+ nil
408
+ end
409
+ end
383
410
 
384
- def encrypted
385
- @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options)
386
- end
411
+ # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if
412
+ # config.secret_token and config.secret_key_base are both set. It reads
413
+ # legacy cookies signed with the old dummy key generator and re-saves
414
+ # them using the new key generator to provide a smooth upgrade path.
415
+ class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
416
+ include VerifyAndUpgradeLegacySignedMessage
387
417
 
388
- def method_missing(method, *arguments, &block)
389
- ActiveSupport::Deprecation.warn "#{method} is deprecated with no replacement. " +
390
- "You probably want to try this method over the parent CookieJar."
418
+ def [](name)
419
+ if signed_message = @parent_jar[name]
420
+ verify(signed_message) || verify_and_upgrade_legacy_signed_message(name, signed_message)
421
+ end
391
422
  end
392
423
  end
393
424
 
394
425
  class EncryptedCookieJar #:nodoc:
426
+ include ChainedCookieJars
427
+
395
428
  def initialize(parent_jar, key_generator, options = {})
396
- if ActiveSupport::DummyKeyGenerator === key_generator
397
- raise "Encrypted Cookies must be used in conjunction with config.secret_key_base." +
398
- "Set config.secret_key_base in config/initializers/secret_token.rb"
429
+ if ActiveSupport::LegacyKeyGenerator === key_generator
430
+ raise "You didn't set config.secret_key_base, which is required for this cookie jar. " +
431
+ "Read the upgrade documentation to learn more about this new config option."
399
432
  end
400
433
 
401
434
  @parent_jar = parent_jar
@@ -405,16 +438,13 @@ module ActionDispatch
405
438
  @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
406
439
  end
407
440
 
408
- def [](key)
409
- if encrypted_message = @parent_jar[key]
410
- @encryptor.decrypt_and_verify(encrypted_message)
441
+ def [](name)
442
+ if encrypted_message = @parent_jar[name]
443
+ decrypt_and_verify(encrypted_message)
411
444
  end
412
- rescue ActiveSupport::MessageVerifier::InvalidSignature,
413
- ActiveSupport::MessageEncryptor::InvalidMessage
414
- nil
415
445
  end
416
446
 
417
- def []=(key, options)
447
+ def []=(name, options)
418
448
  if options.is_a?(Hash)
419
449
  options.symbolize_keys!
420
450
  else
@@ -423,24 +453,28 @@ module ActionDispatch
423
453
  options[:value] = @encryptor.encrypt_and_sign(options[:value])
424
454
 
425
455
  raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
426
- @parent_jar[key] = options
456
+ @parent_jar[name] = options
427
457
  end
428
458
 
429
- def permanent
430
- @permanent ||= PermanentCookieJar.new(self, @key_generator, @options)
431
- end
432
-
433
- def signed
434
- @signed ||= SignedCookieJar.new(self, @key_generator, @options)
435
- end
459
+ private
460
+ def decrypt_and_verify(encrypted_message)
461
+ @encryptor.decrypt_and_verify(encrypted_message)
462
+ rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
463
+ nil
464
+ end
465
+ end
436
466
 
437
- def encrypted
438
- @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options)
439
- end
467
+ # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
468
+ # instead of EncryptedCookieJar if config.secret_token and config.secret_key_base
469
+ # are both set. It reads legacy cookies signed with the old dummy key generator and
470
+ # encrypts and re-saves them using the new key generator to provide a smooth upgrade path.
471
+ class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
472
+ include VerifyAndUpgradeLegacySignedMessage
440
473
 
441
- def method_missing(method, *arguments, &block)
442
- ActiveSupport::Deprecation.warn "#{method} is deprecated with no replacement. " +
443
- "You probably want to try this method over the parent CookieJar."
474
+ def [](name)
475
+ if encrypted_or_signed_message = @parent_jar[name]
476
+ decrypt_and_verify(encrypted_or_signed_message) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message)
477
+ end
444
478
  end
445
479
  end
446
480