actionpack 4.1.0.beta2 → 4.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +241 -10
- data/MIT-LICENSE +1 -1
- data/lib/abstract_controller/rendering.rb +4 -3
- data/lib/action_controller/log_subscriber.rb +9 -0
- data/lib/action_controller/metal/head.rb +1 -1
- data/lib/action_controller/metal/live.rb +3 -1
- data/lib/action_controller/metal/mime_responds.rb +66 -27
- data/lib/action_controller/metal/params_wrapper.rb +11 -4
- data/lib/action_controller/metal/rack_delegation.rb +2 -2
- data/lib/action_controller/metal/renderers.rb +1 -1
- data/lib/action_controller/metal/rendering.rb +38 -8
- data/lib/action_controller/metal/strong_parameters.rb +26 -14
- data/lib/action_controller/railtie.rb +1 -0
- data/lib/action_controller/test_case.rb +3 -0
- data/lib/action_dispatch.rb +5 -7
- data/lib/action_dispatch/http/filter_redirect.rb +5 -4
- data/lib/action_dispatch/http/mime_negotiation.rb +5 -3
- data/lib/action_dispatch/http/mime_type.rb +1 -1
- data/lib/action_dispatch/http/response.rb +14 -2
- data/lib/action_dispatch/journey/formatter.rb +2 -2
- data/lib/action_dispatch/journey/router.rb +1 -7
- data/lib/action_dispatch/journey/visitors.rb +24 -4
- data/lib/action_dispatch/middleware/cookies.rb +85 -20
- data/lib/action_dispatch/middleware/flash.rb +20 -7
- data/lib/action_dispatch/middleware/reloader.rb +11 -2
- data/lib/action_dispatch/middleware/remote_ip.rb +2 -2
- data/lib/action_dispatch/middleware/static.rb +3 -3
- data/lib/action_dispatch/railtie.rb +2 -0
- data/lib/action_dispatch/request/utils.rb +15 -4
- data/lib/action_dispatch/routing/inspector.rb +4 -4
- data/lib/action_dispatch/routing/mapper.rb +27 -9
- data/lib/action_dispatch/routing/redirection.rb +18 -8
- data/lib/action_dispatch/routing/route_set.rb +24 -30
- data/lib/action_dispatch/testing/assertions/routing.rb +1 -1
- data/lib/action_dispatch/testing/integration.rb +2 -2
- data/lib/action_pack.rb +1 -1
- data/lib/action_pack/version.rb +1 -1
- metadata +7 -7
@@ -231,7 +231,12 @@ module ActionController
|
|
231
231
|
# by the metal call stack.
|
232
232
|
def process_action(*args)
|
233
233
|
if _wrapper_enabled?
|
234
|
-
|
234
|
+
if request.parameters[_wrapper_key].present?
|
235
|
+
wrapped_hash = _extract_parameters(request.parameters)
|
236
|
+
else
|
237
|
+
wrapped_hash = _wrap_parameters request.request_parameters
|
238
|
+
end
|
239
|
+
|
235
240
|
wrapped_keys = request.request_parameters.keys
|
236
241
|
wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)
|
237
242
|
|
@@ -259,14 +264,16 @@ module ActionController
|
|
259
264
|
|
260
265
|
# Returns the list of parameters which will be selected for wrapped.
|
261
266
|
def _wrap_parameters(parameters)
|
262
|
-
|
267
|
+
{ _wrapper_key => _extract_parameters(parameters) }
|
268
|
+
end
|
269
|
+
|
270
|
+
def _extract_parameters(parameters)
|
271
|
+
if include_only = _wrapper_options.include
|
263
272
|
parameters.slice(*include_only)
|
264
273
|
else
|
265
274
|
exclude = _wrapper_options.exclude || []
|
266
275
|
parameters.except(*(exclude + EXCLUDE_PARAMETERS))
|
267
276
|
end
|
268
|
-
|
269
|
-
{ _wrapper_key => value }
|
270
277
|
end
|
271
278
|
|
272
279
|
# Checks if we should perform parameters wrapping.
|
@@ -5,8 +5,8 @@ module ActionController
|
|
5
5
|
module RackDelegation
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
|
-
delegate :headers, :status=, :location=, :content_type=,
|
9
|
-
:status, :location, :content_type, :to => "@_response"
|
8
|
+
delegate :headers, :status=, :location=, :content_type=, :no_content_type=,
|
9
|
+
:status, :location, :content_type, :no_content_type, :to => "@_response"
|
10
10
|
|
11
11
|
def dispatch(action, request)
|
12
12
|
set_response!(request)
|
@@ -43,7 +43,7 @@ module ActionController
|
|
43
43
|
end
|
44
44
|
|
45
45
|
# Hash of available renderers, mapping a renderer name to its proc.
|
46
|
-
# Default keys are
|
46
|
+
# Default keys are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>.
|
47
47
|
RENDERERS = Set.new
|
48
48
|
|
49
49
|
# Adds a new renderer to call within controller actions.
|
@@ -2,6 +2,8 @@ module ActionController
|
|
2
2
|
module Rendering
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
|
+
RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html]
|
6
|
+
|
5
7
|
# Before processing, set the request formats in current controller formats.
|
6
8
|
def process_action(*) #:nodoc:
|
7
9
|
self.formats = request.formats.map(&:ref).compact
|
@@ -27,15 +29,29 @@ module ActionController
|
|
27
29
|
end
|
28
30
|
|
29
31
|
def render_to_body(options = {})
|
30
|
-
super || options
|
32
|
+
super || _render_in_priorities(options) || ' '
|
31
33
|
end
|
32
34
|
|
33
35
|
private
|
34
36
|
|
35
|
-
def
|
37
|
+
def _render_in_priorities(options)
|
38
|
+
RENDER_FORMATS_IN_PRIORITY.each do |format|
|
39
|
+
return options[format] if options.key?(format)
|
40
|
+
end
|
41
|
+
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def _process_format(format, options = {})
|
36
46
|
super
|
37
|
-
|
38
|
-
|
47
|
+
|
48
|
+
if options[:body]
|
49
|
+
self.headers.delete "Content-Type"
|
50
|
+
elsif options[:plain]
|
51
|
+
self.content_type = Mime::TEXT
|
52
|
+
else
|
53
|
+
self.content_type ||= format.to_s
|
54
|
+
end
|
39
55
|
end
|
40
56
|
|
41
57
|
# Normalize arguments by catching blocks and setting them on :update.
|
@@ -47,12 +63,14 @@ module ActionController
|
|
47
63
|
|
48
64
|
# Normalize both text and status options.
|
49
65
|
def _normalize_options(options) #:nodoc:
|
50
|
-
|
51
|
-
|
66
|
+
_normalize_text(options)
|
67
|
+
|
68
|
+
if options[:html]
|
69
|
+
options[:html] = ERB::Util.html_escape(options[:html])
|
52
70
|
end
|
53
71
|
|
54
|
-
if options.delete(:nothing) ||
|
55
|
-
options[:
|
72
|
+
if options.delete(:nothing) || _any_render_format_is_nil?(options)
|
73
|
+
options[:body] = " "
|
56
74
|
end
|
57
75
|
|
58
76
|
if options[:status]
|
@@ -62,6 +80,18 @@ module ActionController
|
|
62
80
|
super
|
63
81
|
end
|
64
82
|
|
83
|
+
def _normalize_text(options)
|
84
|
+
RENDER_FORMATS_IN_PRIORITY.each do |format|
|
85
|
+
if options.key?(format) && options[format].respond_to?(:to_text)
|
86
|
+
options[format] = options[format].to_text
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def _any_render_format_is_nil?(options)
|
92
|
+
RENDER_FORMATS_IN_PRIORITY.any? { |format| options.key?(format) && options[format].nil? }
|
93
|
+
end
|
94
|
+
|
65
95
|
# Process controller specific options, as status, content-type and location.
|
66
96
|
def _process_options(options) #:nodoc:
|
67
97
|
status, content_type, location = options.values_at(:status, :content_type, :location)
|
@@ -3,6 +3,7 @@ require 'active_support/core_ext/array/wrap'
|
|
3
3
|
require 'active_support/rescuable'
|
4
4
|
require 'action_dispatch/http/upload'
|
5
5
|
require 'stringio'
|
6
|
+
require 'set'
|
6
7
|
|
7
8
|
module ActionController
|
8
9
|
# Raised when a required parameter is missing.
|
@@ -125,6 +126,13 @@ module ActionController
|
|
125
126
|
@permitted = self.class.permit_all_parameters
|
126
127
|
end
|
127
128
|
|
129
|
+
# Attribute that keeps track of converted arrays, if any, to avoid double
|
130
|
+
# looping in the common use case permit + mass-assignment. Defined in a
|
131
|
+
# method to instantiate it only if needed.
|
132
|
+
def converted_arrays
|
133
|
+
@converted_arrays ||= Set.new
|
134
|
+
end
|
135
|
+
|
128
136
|
# Returns +true+ if the parameter is permitted, +false+ otherwise.
|
129
137
|
#
|
130
138
|
# params = ActionController::Parameters.new
|
@@ -149,8 +157,10 @@ module ActionController
|
|
149
157
|
# Person.new(params) # => #<Person id: nil, name: "Francesco">
|
150
158
|
def permit!
|
151
159
|
each_pair do |key, value|
|
152
|
-
convert_hashes_to_parameters(key, value)
|
153
|
-
|
160
|
+
value = convert_hashes_to_parameters(key, value)
|
161
|
+
Array.wrap(value).each do |_|
|
162
|
+
_.permit! if _.respond_to? :permit!
|
163
|
+
end
|
154
164
|
end
|
155
165
|
|
156
166
|
@permitted = true
|
@@ -284,14 +294,7 @@ module ActionController
|
|
284
294
|
# params.fetch(:none, 'Francesco') # => "Francesco"
|
285
295
|
# params.fetch(:none) { 'Francesco' } # => "Francesco"
|
286
296
|
def fetch(key, *args)
|
287
|
-
|
288
|
-
# Don't rely on +convert_hashes_to_parameters+
|
289
|
-
# so as to not mutate via a +fetch+
|
290
|
-
if value.is_a?(Hash)
|
291
|
-
value = self.class.new(value)
|
292
|
-
value.permit! if permitted?
|
293
|
-
end
|
294
|
-
value
|
297
|
+
convert_hashes_to_parameters(key, super, false)
|
295
298
|
rescue KeyError
|
296
299
|
raise ActionController::ParameterMissing.new(key)
|
297
300
|
end
|
@@ -329,12 +332,21 @@ module ActionController
|
|
329
332
|
end
|
330
333
|
|
331
334
|
private
|
332
|
-
def convert_hashes_to_parameters(key, value)
|
333
|
-
|
335
|
+
def convert_hashes_to_parameters(key, value, assign_if_converted=true)
|
336
|
+
converted = convert_value_to_parameters(value)
|
337
|
+
self[key] = converted if assign_if_converted && !converted.equal?(value)
|
338
|
+
converted
|
339
|
+
end
|
340
|
+
|
341
|
+
def convert_value_to_parameters(value)
|
342
|
+
if value.is_a?(Array) && !converted_arrays.member?(value)
|
343
|
+
converted = value.map { |_| convert_value_to_parameters(_) }
|
344
|
+
converted_arrays << converted
|
345
|
+
converted
|
346
|
+
elsif value.is_a?(Parameters) || !value.is_a?(Hash)
|
334
347
|
value
|
335
348
|
else
|
336
|
-
|
337
|
-
self[key] = self.class.new(value)
|
349
|
+
self.class.new(value)
|
338
350
|
end
|
339
351
|
end
|
340
352
|
|
@@ -213,6 +213,9 @@ module ActionController
|
|
213
213
|
# Clear the combined params hash in case it was already referenced.
|
214
214
|
@env.delete("action_dispatch.request.parameters")
|
215
215
|
|
216
|
+
# Clear the filter cache variables so they're not stale
|
217
|
+
@filtered_parameters = @filtered_env = @filtered_path = nil
|
218
|
+
|
216
219
|
params = self.request_parameters.dup
|
217
220
|
%w(controller action only_path).each do |k|
|
218
221
|
params.delete(k)
|
data/lib/action_dispatch.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
#--
|
2
|
-
# Copyright (c) 2004-
|
2
|
+
# Copyright (c) 2004-2014 David Heinemeier Hansson
|
3
3
|
#
|
4
4
|
# Permission is hereby granted, free of charge, to any person obtaining
|
5
5
|
# a copy of this software and associated documentation files (the
|
@@ -74,18 +74,16 @@ module ActionDispatch
|
|
74
74
|
autoload :MimeNegotiation
|
75
75
|
autoload :Parameters
|
76
76
|
autoload :ParameterFilter
|
77
|
-
autoload :FilterParameters
|
78
|
-
autoload :FilterRedirect
|
79
77
|
autoload :Upload
|
80
78
|
autoload :UploadedFile, 'action_dispatch/http/upload'
|
81
79
|
autoload :URL
|
82
80
|
end
|
83
81
|
|
84
82
|
module Session
|
85
|
-
autoload :AbstractStore,
|
86
|
-
autoload :CookieStore,
|
87
|
-
autoload :MemCacheStore,
|
88
|
-
autoload :CacheStore,
|
83
|
+
autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store'
|
84
|
+
autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store'
|
85
|
+
autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store'
|
86
|
+
autoload :CacheStore, 'action_dispatch/middleware/session/cache_store'
|
89
87
|
end
|
90
88
|
|
91
89
|
mattr_accessor :test_app
|
@@ -5,7 +5,8 @@ module ActionDispatch
|
|
5
5
|
FILTERED = '[FILTERED]'.freeze # :nodoc:
|
6
6
|
|
7
7
|
def filtered_location
|
8
|
-
|
8
|
+
filters = location_filter
|
9
|
+
if !filters.empty? && location_filter_match?(filters)
|
9
10
|
FILTERED
|
10
11
|
else
|
11
12
|
location
|
@@ -15,15 +16,15 @@ module ActionDispatch
|
|
15
16
|
private
|
16
17
|
|
17
18
|
def location_filter
|
18
|
-
if request
|
19
|
+
if request
|
19
20
|
request.env['action_dispatch.redirect_filter'] || []
|
20
21
|
else
|
21
22
|
[]
|
22
23
|
end
|
23
24
|
end
|
24
25
|
|
25
|
-
def location_filter_match?
|
26
|
-
|
26
|
+
def location_filter_match?(filters)
|
27
|
+
filters.any? do |filter|
|
27
28
|
if String === filter
|
28
29
|
location.include?(filter)
|
29
30
|
elsif Regexp === filter
|
@@ -50,7 +50,7 @@ module ActionDispatch
|
|
50
50
|
# GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first
|
51
51
|
#
|
52
52
|
def format(view_path = [])
|
53
|
-
formats.first
|
53
|
+
formats.first || Mime::NullType.instance
|
54
54
|
end
|
55
55
|
|
56
56
|
def formats
|
@@ -68,10 +68,12 @@ module ActionDispatch
|
|
68
68
|
|
69
69
|
# Sets the \variant for template.
|
70
70
|
def variant=(variant)
|
71
|
-
if variant.is_a?
|
71
|
+
if variant.is_a?(Symbol)
|
72
|
+
@variant = [variant]
|
73
|
+
elsif variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) }
|
72
74
|
@variant = variant
|
73
75
|
else
|
74
|
-
raise ArgumentError, "request.variant must be set to a Symbol, not a #{variant.class}. " \
|
76
|
+
raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols, not a #{variant.class}. " \
|
75
77
|
"For security reasons, never directly set the variant to a user-provided value, " \
|
76
78
|
"like params[:variant].to_sym. Check user-provided value against a whitelist first, " \
|
77
79
|
"then set the variant: request.variant = :tablet if params[:variant] == 'tablet'"
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'active_support/core_ext/module/attribute_accessors'
|
2
|
+
require 'action_dispatch/http/filter_redirect'
|
2
3
|
require 'monitor'
|
3
4
|
|
4
5
|
module ActionDispatch # :nodoc:
|
@@ -62,6 +63,8 @@ module ActionDispatch # :nodoc:
|
|
62
63
|
# content you're giving them, so we need to send that along.
|
63
64
|
attr_accessor :charset
|
64
65
|
|
66
|
+
attr_accessor :no_content_type # :nodoc:
|
67
|
+
|
65
68
|
CONTENT_TYPE = "Content-Type".freeze
|
66
69
|
SET_COOKIE = "Set-Cookie".freeze
|
67
70
|
LOCATION = "Location".freeze
|
@@ -302,8 +305,17 @@ module ActionDispatch # :nodoc:
|
|
302
305
|
!@sending_file && @charset != false
|
303
306
|
end
|
304
307
|
|
308
|
+
def remove_content_type!
|
309
|
+
headers.delete CONTENT_TYPE
|
310
|
+
end
|
311
|
+
|
305
312
|
def rack_response(status, header)
|
306
|
-
|
313
|
+
if no_content_type
|
314
|
+
remove_content_type!
|
315
|
+
else
|
316
|
+
assign_default_content_type_and_charset!(header)
|
317
|
+
end
|
318
|
+
|
307
319
|
handle_conditional_get!
|
308
320
|
|
309
321
|
header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join)
|
@@ -312,7 +324,7 @@ module ActionDispatch # :nodoc:
|
|
312
324
|
header.delete CONTENT_TYPE
|
313
325
|
[status, header, []]
|
314
326
|
else
|
315
|
-
[status, header, self]
|
327
|
+
[status, header, Rack::BodyProxy.new(self){}]
|
316
328
|
end
|
317
329
|
end
|
318
330
|
end
|
@@ -33,8 +33,8 @@ module ActionDispatch
|
|
33
33
|
return [route.format(parameterized_parts), params]
|
34
34
|
end
|
35
35
|
|
36
|
-
message = "No route matches #{constraints.inspect}"
|
37
|
-
message << " missing required keys: #{missing_keys.inspect}" if name
|
36
|
+
message = "No route matches #{Hash[constraints.sort].inspect}"
|
37
|
+
message << " missing required keys: #{missing_keys.sort.inspect}" if name
|
38
38
|
|
39
39
|
raise ActionController::UrlGenerationError, message
|
40
40
|
end
|
@@ -54,7 +54,7 @@ module ActionDispatch
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def call(env)
|
57
|
-
env['PATH_INFO'] = normalize_path(env['PATH_INFO'])
|
57
|
+
env['PATH_INFO'] = Utils.normalize_path(env['PATH_INFO'])
|
58
58
|
|
59
59
|
find_routes(env).each do |match, parameters, route|
|
60
60
|
script_name, path_info, set_params = env.values_at('SCRIPT_NAME',
|
@@ -103,12 +103,6 @@ module ActionDispatch
|
|
103
103
|
|
104
104
|
private
|
105
105
|
|
106
|
-
def normalize_path(path)
|
107
|
-
path = "/#{path}"
|
108
|
-
path.squeeze!('/')
|
109
|
-
path
|
110
|
-
end
|
111
|
-
|
112
106
|
def partitioned_routes
|
113
107
|
routes.partitioned_routes
|
114
108
|
end
|
@@ -77,12 +77,32 @@ module ActionDispatch
|
|
77
77
|
end
|
78
78
|
end
|
79
79
|
|
80
|
-
class OptimizedPath <
|
80
|
+
class OptimizedPath < Visitor # :nodoc:
|
81
|
+
def accept(node)
|
82
|
+
Array(visit(node))
|
83
|
+
end
|
84
|
+
|
81
85
|
private
|
82
86
|
|
83
|
-
|
84
|
-
|
85
|
-
|
87
|
+
def visit_CAT(node)
|
88
|
+
[visit(node.left), visit(node.right)].flatten
|
89
|
+
end
|
90
|
+
|
91
|
+
def visit_SYMBOL(node)
|
92
|
+
node.left[1..-1].to_sym
|
93
|
+
end
|
94
|
+
|
95
|
+
def visit_STAR(node)
|
96
|
+
visit(node.left)
|
97
|
+
end
|
98
|
+
|
99
|
+
def visit_GROUP(node)
|
100
|
+
[]
|
101
|
+
end
|
102
|
+
|
103
|
+
%w{ LITERAL SLASH DOT }.each do |t|
|
104
|
+
class_eval %{ def visit_#{t}(n); n.left; end }, __FILE__, __LINE__
|
105
|
+
end
|
86
106
|
end
|
87
107
|
|
88
108
|
# Used for formatting urls (url_for)
|
@@ -23,15 +23,15 @@ module ActionDispatch
|
|
23
23
|
# # This cookie will be deleted when the user's browser is closed.
|
24
24
|
# cookies[:user_name] = "david"
|
25
25
|
#
|
26
|
-
# #
|
27
|
-
# cookies[:lat_lon] = [47.68, -122.37]
|
26
|
+
# # Cookie values are String based. Other data types need to be serialized.
|
27
|
+
# cookies[:lat_lon] = JSON.generate([47.68, -122.37])
|
28
28
|
#
|
29
29
|
# # Sets a cookie that expires in 1 hour.
|
30
30
|
# cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now }
|
31
31
|
#
|
32
32
|
# # Sets a signed cookie, which prevents users from tampering with its value.
|
33
|
-
# # The cookie is signed by your app's
|
34
|
-
# # It can be read using the signed method
|
33
|
+
# # The cookie is signed by your app's `secrets.secret_key_base` value.
|
34
|
+
# # It can be read using the signed method `cookies.signed[:name]`
|
35
35
|
# cookies.signed[:user_id] = current_user.id
|
36
36
|
#
|
37
37
|
# # Sets a "permanent" cookie (which expires in 20 years from now).
|
@@ -42,10 +42,10 @@ module ActionDispatch
|
|
42
42
|
#
|
43
43
|
# Examples of reading:
|
44
44
|
#
|
45
|
-
# cookies[:user_name]
|
46
|
-
# cookies.size
|
47
|
-
# cookies[:lat_lon]
|
48
|
-
# cookies.signed[:login]
|
45
|
+
# cookies[:user_name] # => "david"
|
46
|
+
# cookies.size # => 2
|
47
|
+
# JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37]
|
48
|
+
# cookies.signed[:login] # => "XJ-122"
|
49
49
|
#
|
50
50
|
# Example for deleting:
|
51
51
|
#
|
@@ -63,7 +63,7 @@ module ActionDispatch
|
|
63
63
|
#
|
64
64
|
# The option symbols for setting cookies are:
|
65
65
|
#
|
66
|
-
# * <tt>:value</tt> - The cookie's value
|
66
|
+
# * <tt>:value</tt> - The cookie's value.
|
67
67
|
# * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root
|
68
68
|
# of the application.
|
69
69
|
# * <tt>:domain</tt> - The domain for which this cookie applies so you can
|
@@ -89,6 +89,7 @@ module ActionDispatch
|
|
89
89
|
ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
|
90
90
|
SECRET_TOKEN = "action_dispatch.secret_token".freeze
|
91
91
|
SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
|
92
|
+
COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
|
92
93
|
|
93
94
|
# Cookies can typically store 4096 bytes.
|
94
95
|
MAX_COOKIE_SIZE = 4096
|
@@ -180,7 +181,7 @@ module ActionDispatch
|
|
180
181
|
|
181
182
|
def verify_and_upgrade_legacy_signed_message(name, signed_message)
|
182
183
|
@legacy_verifier.verify(signed_message).tap do |value|
|
183
|
-
self[name] = value
|
184
|
+
self[name] = { value: value }
|
184
185
|
end
|
185
186
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
186
187
|
nil
|
@@ -210,7 +211,8 @@ module ActionDispatch
|
|
210
211
|
encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '',
|
211
212
|
secret_token: env[SECRET_TOKEN],
|
212
213
|
secret_key_base: env[SECRET_KEY_BASE],
|
213
|
-
upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present
|
214
|
+
upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?,
|
215
|
+
serializer: env[COOKIES_SERIALIZER]
|
214
216
|
}
|
215
217
|
end
|
216
218
|
|
@@ -372,28 +374,89 @@ module ActionDispatch
|
|
372
374
|
end
|
373
375
|
end
|
374
376
|
|
377
|
+
class JsonSerializer
|
378
|
+
def self.load(value)
|
379
|
+
JSON.parse(value, quirks_mode: true)
|
380
|
+
end
|
381
|
+
|
382
|
+
def self.dump(value)
|
383
|
+
JSON.generate(value, quirks_mode: true)
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
# Passing the NullSerializer downstream to the Message{Encryptor,Verifier}
|
388
|
+
# allows us to handle the (de)serialization step within the cookie jar,
|
389
|
+
# which gives us the opportunity to detect and migrate legacy cookies.
|
390
|
+
class NullSerializer
|
391
|
+
def self.load(value)
|
392
|
+
value
|
393
|
+
end
|
394
|
+
|
395
|
+
def self.dump(value)
|
396
|
+
value
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
module SerializedCookieJars
|
401
|
+
MARSHAL_SIGNATURE = "\x04\x08".freeze
|
402
|
+
|
403
|
+
protected
|
404
|
+
def needs_migration?(value)
|
405
|
+
@options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
|
406
|
+
end
|
407
|
+
|
408
|
+
def serialize(name, value)
|
409
|
+
serializer.dump(value)
|
410
|
+
end
|
411
|
+
|
412
|
+
def deserialize(name, value)
|
413
|
+
if value
|
414
|
+
if needs_migration?(value)
|
415
|
+
Marshal.load(value).tap do |v|
|
416
|
+
self[name] = { value: v }
|
417
|
+
end
|
418
|
+
else
|
419
|
+
serializer.load(value)
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
def serializer
|
425
|
+
serializer = @options[:serializer] || :marshal
|
426
|
+
case serializer
|
427
|
+
when :marshal
|
428
|
+
Marshal
|
429
|
+
when :json, :hybrid
|
430
|
+
JsonSerializer
|
431
|
+
else
|
432
|
+
serializer
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
375
437
|
class SignedCookieJar #:nodoc:
|
376
438
|
include ChainedCookieJars
|
439
|
+
include SerializedCookieJars
|
377
440
|
|
378
441
|
def initialize(parent_jar, key_generator, options = {})
|
379
442
|
@parent_jar = parent_jar
|
380
443
|
@options = options
|
381
444
|
secret = key_generator.generate_key(@options[:signed_cookie_salt])
|
382
|
-
@verifier
|
445
|
+
@verifier = ActiveSupport::MessageVerifier.new(secret, serializer: NullSerializer)
|
383
446
|
end
|
384
447
|
|
385
448
|
def [](name)
|
386
449
|
if signed_message = @parent_jar[name]
|
387
|
-
verify(signed_message)
|
450
|
+
deserialize name, verify(signed_message)
|
388
451
|
end
|
389
452
|
end
|
390
453
|
|
391
454
|
def []=(name, options)
|
392
455
|
if options.is_a?(Hash)
|
393
456
|
options.symbolize_keys!
|
394
|
-
options[:value] = @verifier.generate(options[:value])
|
457
|
+
options[:value] = @verifier.generate(serialize(name, options[:value]))
|
395
458
|
else
|
396
|
-
options = { :value => @verifier.generate(options) }
|
459
|
+
options = { :value => @verifier.generate(serialize(name, options)) }
|
397
460
|
end
|
398
461
|
|
399
462
|
raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
|
@@ -417,13 +480,14 @@ module ActionDispatch
|
|
417
480
|
|
418
481
|
def [](name)
|
419
482
|
if signed_message = @parent_jar[name]
|
420
|
-
verify(signed_message) || verify_and_upgrade_legacy_signed_message(name, signed_message)
|
483
|
+
deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message)
|
421
484
|
end
|
422
485
|
end
|
423
486
|
end
|
424
487
|
|
425
488
|
class EncryptedCookieJar #:nodoc:
|
426
489
|
include ChainedCookieJars
|
490
|
+
include SerializedCookieJars
|
427
491
|
|
428
492
|
def initialize(parent_jar, key_generator, options = {})
|
429
493
|
if ActiveSupport::LegacyKeyGenerator === key_generator
|
@@ -435,12 +499,12 @@ module ActionDispatch
|
|
435
499
|
@options = options
|
436
500
|
secret = key_generator.generate_key(@options[:encrypted_cookie_salt])
|
437
501
|
sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt])
|
438
|
-
@encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
|
502
|
+
@encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: NullSerializer)
|
439
503
|
end
|
440
504
|
|
441
505
|
def [](name)
|
442
506
|
if encrypted_message = @parent_jar[name]
|
443
|
-
decrypt_and_verify(encrypted_message)
|
507
|
+
deserialize name, decrypt_and_verify(encrypted_message)
|
444
508
|
end
|
445
509
|
end
|
446
510
|
|
@@ -450,7 +514,8 @@ module ActionDispatch
|
|
450
514
|
else
|
451
515
|
options = { :value => options }
|
452
516
|
end
|
453
|
-
|
517
|
+
|
518
|
+
options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value]))
|
454
519
|
|
455
520
|
raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
|
456
521
|
@parent_jar[name] = options
|
@@ -473,7 +538,7 @@ module ActionDispatch
|
|
473
538
|
|
474
539
|
def [](name)
|
475
540
|
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)
|
541
|
+
deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message)
|
477
542
|
end
|
478
543
|
end
|
479
544
|
end
|