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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +195 -11
- data/lib/abstract_controller/base.rb +1 -1
- data/lib/abstract_controller/helpers.rb +2 -2
- data/lib/abstract_controller/layouts.rb +10 -5
- data/lib/abstract_controller/rendering.rb +11 -3
- data/lib/abstract_controller/translation.rb +1 -1
- data/lib/action_controller/log_subscriber.rb +5 -0
- data/lib/action_controller/metal.rb +2 -3
- data/lib/action_controller/metal/force_ssl.rb +52 -17
- data/lib/action_controller/metal/helpers.rb +0 -1
- data/lib/action_controller/metal/hide_actions.rb +1 -1
- data/lib/action_controller/metal/http_authentication.rb +3 -2
- data/lib/action_controller/metal/live.rb +34 -0
- data/lib/action_controller/metal/rendering.rb +1 -1
- data/lib/action_controller/metal/strong_parameters.rb +7 -3
- data/lib/action_controller/test_case.rb +45 -11
- data/lib/action_dispatch.rb +4 -6
- data/lib/action_dispatch/http/cache.rb +2 -2
- data/lib/action_dispatch/http/headers.rb +39 -15
- data/lib/action_dispatch/http/mime_negotiation.rb +1 -1
- data/lib/action_dispatch/http/mime_type.rb +11 -3
- data/lib/action_dispatch/http/parameters.rb +17 -24
- data/lib/action_dispatch/http/request.rb +17 -2
- data/lib/action_dispatch/http/response.rb +2 -1
- data/lib/action_dispatch/http/upload.rb +5 -5
- data/lib/action_dispatch/http/url.rb +53 -12
- data/lib/action_dispatch/journey/formatter.rb +1 -1
- data/lib/action_dispatch/journey/path/pattern.rb +1 -1
- data/lib/action_dispatch/journey/route.rb +8 -0
- data/lib/action_dispatch/journey/router.rb +3 -1
- data/lib/action_dispatch/journey/visitors.rb +8 -0
- data/lib/action_dispatch/middleware/cookies.rb +169 -135
- data/lib/action_dispatch/middleware/exception_wrapper.rb +1 -0
- data/lib/action_dispatch/middleware/remote_ip.rb +2 -2
- data/lib/action_dispatch/middleware/request_id.rb +1 -1
- data/lib/action_dispatch/middleware/session/cookie_store.rb +38 -58
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/_trace.erb +4 -6
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/template_error.erb +1 -1
- data/lib/action_dispatch/routing.rb +28 -64
- data/lib/action_dispatch/routing/mapper.rb +61 -48
- data/lib/action_dispatch/routing/route_set.rb +17 -14
- data/lib/action_dispatch/testing/assertions/routing.rb +2 -2
- data/lib/action_dispatch/testing/assertions/selector.rb +2 -2
- data/lib/action_dispatch/testing/integration.rb +36 -35
- data/lib/action_dispatch/testing/test_process.rb +1 -1
- data/lib/action_pack/version.rb +7 -6
- data/lib/action_view/buffers.rb +6 -0
- data/lib/action_view/dependency_tracker.rb +3 -1
- data/lib/action_view/helpers/asset_tag_helper.rb +13 -8
- data/lib/action_view/helpers/capture_helper.rb +2 -2
- data/lib/action_view/helpers/date_helper.rb +1 -1
- data/lib/action_view/helpers/form_helper.rb +56 -19
- data/lib/action_view/helpers/form_options_helper.rb +3 -3
- data/lib/action_view/helpers/form_tag_helper.rb +1 -1
- data/lib/action_view/helpers/javascript_helper.rb +2 -2
- data/lib/action_view/helpers/number_helper.rb +25 -0
- data/lib/action_view/helpers/tags/base.rb +9 -10
- data/lib/action_view/helpers/tags/check_box.rb +1 -1
- data/lib/action_view/helpers/tags/checkable.rb +2 -2
- data/lib/action_view/helpers/tags/collection_check_boxes.rb +3 -3
- data/lib/action_view/helpers/tags/collection_helpers.rb +3 -3
- data/lib/action_view/helpers/tags/collection_radio_buttons.rb +3 -3
- data/lib/action_view/helpers/tags/collection_select.rb +1 -1
- data/lib/action_view/helpers/tags/color_field.rb +2 -2
- data/lib/action_view/helpers/tags/date_field.rb +2 -2
- data/lib/action_view/helpers/tags/date_select.rb +2 -2
- data/lib/action_view/helpers/tags/datetime_field.rb +2 -2
- data/lib/action_view/helpers/tags/datetime_local_field.rb +2 -2
- data/lib/action_view/helpers/tags/datetime_select.rb +2 -2
- data/lib/action_view/helpers/tags/email_field.rb +2 -2
- data/lib/action_view/helpers/tags/file_field.rb +2 -2
- data/lib/action_view/helpers/tags/grouped_collection_select.rb +2 -2
- data/lib/action_view/helpers/tags/hidden_field.rb +2 -2
- data/lib/action_view/helpers/tags/label.rb +2 -2
- data/lib/action_view/helpers/tags/month_field.rb +2 -2
- data/lib/action_view/helpers/tags/number_field.rb +2 -2
- data/lib/action_view/helpers/tags/password_field.rb +2 -2
- data/lib/action_view/helpers/tags/radio_button.rb +2 -2
- data/lib/action_view/helpers/tags/range_field.rb +2 -2
- data/lib/action_view/helpers/tags/search_field.rb +2 -2
- data/lib/action_view/helpers/tags/select.rb +2 -3
- data/lib/action_view/helpers/tags/tel_field.rb +2 -2
- data/lib/action_view/helpers/tags/text_area.rb +2 -2
- data/lib/action_view/helpers/tags/text_field.rb +2 -2
- data/lib/action_view/helpers/tags/time_field.rb +2 -2
- data/lib/action_view/helpers/tags/time_select.rb +2 -2
- data/lib/action_view/helpers/tags/time_zone_select.rb +2 -2
- data/lib/action_view/helpers/tags/url_field.rb +2 -2
- data/lib/action_view/helpers/tags/week_field.rb +2 -2
- data/lib/action_view/helpers/text_helper.rb +8 -5
- data/lib/action_view/helpers/url_helper.rb +18 -6
- data/lib/action_view/lookup_context.rb +7 -1
- data/lib/action_view/path_set.rb +6 -0
- data/lib/action_view/renderer/abstract_renderer.rb +15 -0
- data/lib/action_view/renderer/partial_renderer.rb +14 -0
- data/lib/action_view/renderer/renderer.rb +6 -0
- data/lib/action_view/template.rb +3 -2
- data/lib/action_view/template/handlers/erb.rb +29 -3
- data/lib/action_view/template/resolver.rb +3 -3
- data/lib/action_view/test_case.rb +1 -0
- data/lib/action_view/vendor/html-scanner/html/sanitizer.rb +5 -5
- data/lib/action_view/vendor/html-scanner/html/selector.rb +8 -8
- metadata +8 -8
@@ -18,7 +18,7 @@ module ActionDispatch
|
|
18
18
|
query_parameters.dup
|
19
19
|
end
|
20
20
|
params.merge!(path_parameters)
|
21
|
-
|
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
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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"] ||= (
|
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"] ||= (
|
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
|
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
|
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
|
-
#
|
79
|
-
#
|
80
|
-
def
|
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 :
|
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
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
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 <<
|
69
|
-
result << ":#{options
|
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
|
87
|
-
|
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
|
-
|
93
|
-
host <<
|
94
|
-
|
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)
|
@@ -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
|
@@ -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[:
|
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[:
|
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(:
|
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
|
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
|
-
|
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
|
-
|
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 []=(
|
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[
|
199
|
-
@cookies[
|
200
|
-
@set_cookies[
|
201
|
-
@delete_cookies.delete(
|
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
|
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(
|
211
|
-
return unless @cookies.has_key?
|
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(
|
217
|
-
@delete_cookies[
|
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?(
|
320
|
+
def deleted?(name, options = {})
|
225
321
|
options.symbolize_keys!
|
226
322
|
handle_options(options)
|
227
|
-
@delete_cookies[
|
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 [](
|
359
|
+
def [](name)
|
316
360
|
@parent_jar[name.to_s]
|
317
361
|
end
|
318
362
|
|
319
|
-
def []=(
|
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[
|
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
|
-
|
387
|
+
verify(signed_message)
|
359
388
|
end
|
360
|
-
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
361
|
-
nil
|
362
389
|
end
|
363
390
|
|
364
|
-
def []=(
|
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[
|
400
|
+
@parent_jar[name] = options
|
374
401
|
end
|
375
402
|
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
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
|
-
|
385
|
-
|
386
|
-
|
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
|
389
|
-
|
390
|
-
|
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::
|
397
|
-
raise "
|
398
|
-
|
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 [](
|
409
|
-
if encrypted_message = @parent_jar[
|
410
|
-
|
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 []=(
|
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[
|
456
|
+
@parent_jar[name] = options
|
427
457
|
end
|
428
458
|
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
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
|
-
|
438
|
-
|
439
|
-
|
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
|
442
|
-
|
443
|
-
|
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
|
|