omg-actionpack 8.0.0.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +129 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +57 -0
- data/lib/abstract_controller/asset_paths.rb +14 -0
- data/lib/abstract_controller/base.rb +299 -0
- data/lib/abstract_controller/caching/fragments.rb +149 -0
- data/lib/abstract_controller/caching.rb +68 -0
- data/lib/abstract_controller/callbacks.rb +265 -0
- data/lib/abstract_controller/collector.rb +44 -0
- data/lib/abstract_controller/deprecator.rb +9 -0
- data/lib/abstract_controller/error.rb +8 -0
- data/lib/abstract_controller/helpers.rb +243 -0
- data/lib/abstract_controller/logger.rb +16 -0
- data/lib/abstract_controller/railties/routes_helpers.rb +25 -0
- data/lib/abstract_controller/rendering.rb +126 -0
- data/lib/abstract_controller/translation.rb +42 -0
- data/lib/abstract_controller/url_for.rb +37 -0
- data/lib/abstract_controller.rb +36 -0
- data/lib/action_controller/api/api_rendering.rb +18 -0
- data/lib/action_controller/api.rb +155 -0
- data/lib/action_controller/base.rb +332 -0
- data/lib/action_controller/caching.rb +49 -0
- data/lib/action_controller/deprecator.rb +9 -0
- data/lib/action_controller/form_builder.rb +55 -0
- data/lib/action_controller/log_subscriber.rb +96 -0
- data/lib/action_controller/metal/allow_browser.rb +123 -0
- data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
- data/lib/action_controller/metal/conditional_get.rb +341 -0
- data/lib/action_controller/metal/content_security_policy.rb +86 -0
- data/lib/action_controller/metal/cookies.rb +20 -0
- data/lib/action_controller/metal/data_streaming.rb +154 -0
- data/lib/action_controller/metal/default_headers.rb +21 -0
- data/lib/action_controller/metal/etag_with_flash.rb +22 -0
- data/lib/action_controller/metal/etag_with_template_digest.rb +59 -0
- data/lib/action_controller/metal/exceptions.rb +106 -0
- data/lib/action_controller/metal/flash.rb +67 -0
- data/lib/action_controller/metal/head.rb +67 -0
- data/lib/action_controller/metal/helpers.rb +129 -0
- data/lib/action_controller/metal/http_authentication.rb +565 -0
- data/lib/action_controller/metal/implicit_render.rb +67 -0
- data/lib/action_controller/metal/instrumentation.rb +120 -0
- data/lib/action_controller/metal/live.rb +398 -0
- data/lib/action_controller/metal/logging.rb +22 -0
- data/lib/action_controller/metal/mime_responds.rb +337 -0
- data/lib/action_controller/metal/parameter_encoding.rb +84 -0
- data/lib/action_controller/metal/params_wrapper.rb +312 -0
- data/lib/action_controller/metal/permissions_policy.rb +38 -0
- data/lib/action_controller/metal/rate_limiting.rb +62 -0
- data/lib/action_controller/metal/redirecting.rb +251 -0
- data/lib/action_controller/metal/renderers.rb +181 -0
- data/lib/action_controller/metal/rendering.rb +260 -0
- data/lib/action_controller/metal/request_forgery_protection.rb +667 -0
- data/lib/action_controller/metal/rescue.rb +33 -0
- data/lib/action_controller/metal/streaming.rb +183 -0
- data/lib/action_controller/metal/strong_parameters.rb +1546 -0
- data/lib/action_controller/metal/testing.rb +25 -0
- data/lib/action_controller/metal/url_for.rb +65 -0
- data/lib/action_controller/metal.rb +339 -0
- data/lib/action_controller/railtie.rb +149 -0
- data/lib/action_controller/railties/helpers.rb +26 -0
- data/lib/action_controller/renderer.rb +161 -0
- data/lib/action_controller/template_assertions.rb +13 -0
- data/lib/action_controller/test_case.rb +691 -0
- data/lib/action_controller.rb +80 -0
- data/lib/action_dispatch/constants.rb +34 -0
- data/lib/action_dispatch/deprecator.rb +9 -0
- data/lib/action_dispatch/http/cache.rb +249 -0
- data/lib/action_dispatch/http/content_disposition.rb +47 -0
- data/lib/action_dispatch/http/content_security_policy.rb +365 -0
- data/lib/action_dispatch/http/filter_parameters.rb +80 -0
- data/lib/action_dispatch/http/filter_redirect.rb +50 -0
- data/lib/action_dispatch/http/headers.rb +134 -0
- data/lib/action_dispatch/http/mime_negotiation.rb +187 -0
- data/lib/action_dispatch/http/mime_type.rb +389 -0
- data/lib/action_dispatch/http/mime_types.rb +54 -0
- data/lib/action_dispatch/http/parameters.rb +119 -0
- data/lib/action_dispatch/http/permissions_policy.rb +189 -0
- data/lib/action_dispatch/http/rack_cache.rb +67 -0
- data/lib/action_dispatch/http/request.rb +498 -0
- data/lib/action_dispatch/http/response.rb +556 -0
- data/lib/action_dispatch/http/upload.rb +107 -0
- data/lib/action_dispatch/http/url.rb +344 -0
- data/lib/action_dispatch/journey/formatter.rb +226 -0
- data/lib/action_dispatch/journey/gtg/builder.rb +149 -0
- data/lib/action_dispatch/journey/gtg/simulator.rb +50 -0
- data/lib/action_dispatch/journey/gtg/transition_table.rb +217 -0
- data/lib/action_dispatch/journey/nfa/dot.rb +27 -0
- data/lib/action_dispatch/journey/nodes/node.rb +208 -0
- data/lib/action_dispatch/journey/parser.rb +103 -0
- data/lib/action_dispatch/journey/path/pattern.rb +209 -0
- data/lib/action_dispatch/journey/route.rb +189 -0
- data/lib/action_dispatch/journey/router/utils.rb +105 -0
- data/lib/action_dispatch/journey/router.rb +151 -0
- data/lib/action_dispatch/journey/routes.rb +82 -0
- data/lib/action_dispatch/journey/scanner.rb +70 -0
- data/lib/action_dispatch/journey/visitors.rb +267 -0
- data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
- data/lib/action_dispatch/journey/visualizer/fsm.js +159 -0
- data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
- data/lib/action_dispatch/journey.rb +7 -0
- data/lib/action_dispatch/log_subscriber.rb +25 -0
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
- data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
- data/lib/action_dispatch/middleware/callbacks.rb +38 -0
- data/lib/action_dispatch/middleware/cookies.rb +719 -0
- data/lib/action_dispatch/middleware/debug_exceptions.rb +206 -0
- data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
- data/lib/action_dispatch/middleware/debug_view.rb +73 -0
- data/lib/action_dispatch/middleware/exception_wrapper.rb +350 -0
- data/lib/action_dispatch/middleware/executor.rb +32 -0
- data/lib/action_dispatch/middleware/flash.rb +318 -0
- data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
- data/lib/action_dispatch/middleware/public_exceptions.rb +64 -0
- data/lib/action_dispatch/middleware/reloader.rb +16 -0
- data/lib/action_dispatch/middleware/remote_ip.rb +199 -0
- data/lib/action_dispatch/middleware/request_id.rb +50 -0
- data/lib/action_dispatch/middleware/server_timing.rb +78 -0
- data/lib/action_dispatch/middleware/session/abstract_store.rb +112 -0
- data/lib/action_dispatch/middleware/session/cache_store.rb +66 -0
- data/lib/action_dispatch/middleware/session/cookie_store.rb +129 -0
- data/lib/action_dispatch/middleware/session/mem_cache_store.rb +34 -0
- data/lib/action_dispatch/middleware/show_exceptions.rb +88 -0
- data/lib/action_dispatch/middleware/ssl.rb +180 -0
- data/lib/action_dispatch/middleware/stack.rb +194 -0
- data/lib/action_dispatch/middleware/static.rb +192 -0
- data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
- data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
- data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +17 -0
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
- data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
- data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
- data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +62 -0
- data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +35 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +284 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
- data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
- data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +19 -0
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +232 -0
- data/lib/action_dispatch/railtie.rb +77 -0
- data/lib/action_dispatch/request/session.rb +283 -0
- data/lib/action_dispatch/request/utils.rb +109 -0
- data/lib/action_dispatch/routing/endpoint.rb +19 -0
- data/lib/action_dispatch/routing/inspector.rb +323 -0
- data/lib/action_dispatch/routing/mapper.rb +2372 -0
- data/lib/action_dispatch/routing/polymorphic_routes.rb +363 -0
- data/lib/action_dispatch/routing/redirection.rb +218 -0
- data/lib/action_dispatch/routing/route_set.rb +958 -0
- data/lib/action_dispatch/routing/routes_proxy.rb +66 -0
- data/lib/action_dispatch/routing/url_for.rb +244 -0
- data/lib/action_dispatch/routing.rb +262 -0
- data/lib/action_dispatch/system_test_case.rb +206 -0
- data/lib/action_dispatch/system_testing/browser.rb +75 -0
- data/lib/action_dispatch/system_testing/driver.rb +85 -0
- data/lib/action_dispatch/system_testing/server.rb +33 -0
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
- data/lib/action_dispatch/testing/assertion_response.rb +48 -0
- data/lib/action_dispatch/testing/assertions/response.rb +114 -0
- data/lib/action_dispatch/testing/assertions/routing.rb +343 -0
- data/lib/action_dispatch/testing/assertions.rb +25 -0
- data/lib/action_dispatch/testing/integration.rb +694 -0
- data/lib/action_dispatch/testing/request_encoder.rb +60 -0
- data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
- data/lib/action_dispatch/testing/test_process.rb +57 -0
- data/lib/action_dispatch/testing/test_request.rb +73 -0
- data/lib/action_dispatch/testing/test_response.rb +58 -0
- data/lib/action_dispatch.rb +147 -0
- data/lib/action_pack/gem_version.rb +19 -0
- data/lib/action_pack/version.rb +12 -0
- data/lib/action_pack.rb +27 -0
- metadata +375 -0
@@ -0,0 +1,312 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "active_support/core_ext/hash/slice"
|
6
|
+
require "active_support/core_ext/hash/except"
|
7
|
+
require "active_support/core_ext/module/anonymous"
|
8
|
+
require "action_dispatch/http/mime_type"
|
9
|
+
|
10
|
+
module ActionController
|
11
|
+
# # Action Controller Params Wrapper
|
12
|
+
#
|
13
|
+
# Wraps the parameters hash into a nested hash. This will allow clients to
|
14
|
+
# submit requests without having to specify any root elements.
|
15
|
+
#
|
16
|
+
# This functionality is enabled by default for JSON, and can be customized by
|
17
|
+
# setting the format array:
|
18
|
+
#
|
19
|
+
# class ApplicationController < ActionController::Base
|
20
|
+
# wrap_parameters format: [:json, :xml]
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# You could also turn it on per controller:
|
24
|
+
#
|
25
|
+
# class UsersController < ApplicationController
|
26
|
+
# wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# If you enable `ParamsWrapper` for `:json` format, instead of having to send
|
30
|
+
# JSON parameters like this:
|
31
|
+
#
|
32
|
+
# {"user": {"name": "Konata"}}
|
33
|
+
#
|
34
|
+
# You can send parameters like this:
|
35
|
+
#
|
36
|
+
# {"name": "Konata"}
|
37
|
+
#
|
38
|
+
# And it will be wrapped into a nested hash with the key name matching the
|
39
|
+
# controller's name. For example, if you're posting to `UsersController`, your
|
40
|
+
# new `params` hash will look like this:
|
41
|
+
#
|
42
|
+
# {"name" => "Konata", "user" => {"name" => "Konata"}}
|
43
|
+
#
|
44
|
+
# You can also specify the key in which the parameters should be wrapped to, and
|
45
|
+
# also the list of attributes it should wrap by using either `:include` or
|
46
|
+
# `:exclude` options like this:
|
47
|
+
#
|
48
|
+
# class UsersController < ApplicationController
|
49
|
+
# wrap_parameters :person, include: [:username, :password]
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# On Active Record models with no `:include` or `:exclude` option set, it will
|
53
|
+
# only wrap the parameters returned by the class method `attribute_names`.
|
54
|
+
#
|
55
|
+
# If you're going to pass the parameters to an `ActiveModel` object (such as
|
56
|
+
# `User.new(params[:user])`), you might consider passing the model class to the
|
57
|
+
# method instead. The `ParamsWrapper` will actually try to determine the list of
|
58
|
+
# attribute names from the model and only wrap those attributes:
|
59
|
+
#
|
60
|
+
# class UsersController < ApplicationController
|
61
|
+
# wrap_parameters Person
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# You still could pass `:include` and `:exclude` to set the list of attributes
|
65
|
+
# you want to wrap.
|
66
|
+
#
|
67
|
+
# By default, if you don't specify the key in which the parameters would be
|
68
|
+
# wrapped to, `ParamsWrapper` will actually try to determine if there's a model
|
69
|
+
# related to it or not. This controller, for example:
|
70
|
+
#
|
71
|
+
# class Admin::UsersController < ApplicationController
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# will try to check if `Admin::User` or `User` model exists, and use it to
|
75
|
+
# determine the wrapper key respectively. If both models don't exist, it will
|
76
|
+
# then fall back to use `user` as the key.
|
77
|
+
#
|
78
|
+
# To disable this functionality for a controller:
|
79
|
+
#
|
80
|
+
# class UsersController < ApplicationController
|
81
|
+
# wrap_parameters false
|
82
|
+
# end
|
83
|
+
module ParamsWrapper
|
84
|
+
extend ActiveSupport::Concern
|
85
|
+
|
86
|
+
EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8)
|
87
|
+
|
88
|
+
class Options < Struct.new(:name, :format, :include, :exclude, :klass, :model) # :nodoc:
|
89
|
+
def self.from_hash(hash)
|
90
|
+
name = hash[:name]
|
91
|
+
format = Array(hash[:format])
|
92
|
+
include = hash[:include] && Array(hash[:include]).collect(&:to_s)
|
93
|
+
exclude = hash[:exclude] && Array(hash[:exclude]).collect(&:to_s)
|
94
|
+
new name, format, include, exclude, nil, nil
|
95
|
+
end
|
96
|
+
|
97
|
+
def initialize(name, format, include, exclude, klass, model) # :nodoc:
|
98
|
+
super
|
99
|
+
@mutex = Mutex.new
|
100
|
+
@include_set = include
|
101
|
+
@name_set = name
|
102
|
+
end
|
103
|
+
|
104
|
+
def model
|
105
|
+
super || self.model = _default_wrap_model
|
106
|
+
end
|
107
|
+
|
108
|
+
def include
|
109
|
+
return super if @include_set
|
110
|
+
|
111
|
+
m = model
|
112
|
+
@mutex.synchronize do
|
113
|
+
return super if @include_set
|
114
|
+
|
115
|
+
@include_set = true
|
116
|
+
|
117
|
+
unless super || exclude
|
118
|
+
if m.respond_to?(:attribute_names) && m.attribute_names.any?
|
119
|
+
self.include = m.attribute_names
|
120
|
+
|
121
|
+
if m.respond_to?(:stored_attributes) && !m.stored_attributes.empty?
|
122
|
+
self.include += m.stored_attributes.values.flatten.map(&:to_s)
|
123
|
+
end
|
124
|
+
|
125
|
+
if m.respond_to?(:attribute_aliases) && m.attribute_aliases.any?
|
126
|
+
self.include += m.attribute_aliases.keys
|
127
|
+
end
|
128
|
+
|
129
|
+
if m.respond_to?(:nested_attributes_options) && m.nested_attributes_options.keys.any?
|
130
|
+
self.include += m.nested_attributes_options.keys.map do |key|
|
131
|
+
(+key.to_s).concat("_attributes")
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
self.include
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def name
|
142
|
+
return super if @name_set
|
143
|
+
|
144
|
+
m = model
|
145
|
+
@mutex.synchronize do
|
146
|
+
return super if @name_set
|
147
|
+
|
148
|
+
@name_set = true
|
149
|
+
|
150
|
+
unless super || klass.anonymous?
|
151
|
+
self.name = m ? m.to_s.demodulize.underscore :
|
152
|
+
klass.controller_name.singularize
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
# Determine the wrapper model from the controller's name. By convention, this
|
159
|
+
# could be done by trying to find the defined model that has the same singular
|
160
|
+
# name as the controller. For example, `UsersController` will try to find if the
|
161
|
+
# `User` model exists.
|
162
|
+
#
|
163
|
+
# This method also does namespace lookup. Foo::Bar::UsersController will try to
|
164
|
+
# find Foo::Bar::User, Foo::User and finally User.
|
165
|
+
def _default_wrap_model
|
166
|
+
return nil if klass.anonymous?
|
167
|
+
model_name = klass.name.delete_suffix("Controller").classify
|
168
|
+
|
169
|
+
begin
|
170
|
+
if model_klass = model_name.safe_constantize
|
171
|
+
model_klass
|
172
|
+
else
|
173
|
+
namespaces = model_name.split("::")
|
174
|
+
namespaces.delete_at(-2)
|
175
|
+
break if namespaces.last == model_name
|
176
|
+
model_name = namespaces.join("::")
|
177
|
+
end
|
178
|
+
end until model_klass
|
179
|
+
|
180
|
+
model_klass
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
included do
|
185
|
+
class_attribute :_wrapper_options, default: Options.from_hash(format: [])
|
186
|
+
end
|
187
|
+
|
188
|
+
module ClassMethods
|
189
|
+
def _set_wrapper_options(options)
|
190
|
+
self._wrapper_options = Options.from_hash(options)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Sets the name of the wrapper key, or the model which `ParamsWrapper` would use
|
194
|
+
# to determine the attribute names from.
|
195
|
+
#
|
196
|
+
# #### Examples
|
197
|
+
# wrap_parameters format: :xml
|
198
|
+
# # enables the parameter wrapper for XML format
|
199
|
+
#
|
200
|
+
# wrap_parameters :person
|
201
|
+
# # wraps parameters into +params[:person]+ hash
|
202
|
+
#
|
203
|
+
# wrap_parameters Person
|
204
|
+
# # wraps parameters by determining the wrapper key from Person class
|
205
|
+
# # (+person+, in this case) and the list of attribute names
|
206
|
+
#
|
207
|
+
# wrap_parameters include: [:username, :title]
|
208
|
+
# # wraps only +:username+ and +:title+ attributes from parameters.
|
209
|
+
#
|
210
|
+
# wrap_parameters false
|
211
|
+
# # disables parameters wrapping for this controller altogether.
|
212
|
+
#
|
213
|
+
# #### Options
|
214
|
+
# * `:format` - The list of formats in which the parameters wrapper will be
|
215
|
+
# enabled.
|
216
|
+
# * `:include` - The list of attribute names which parameters wrapper will
|
217
|
+
# wrap into a nested hash.
|
218
|
+
# * `:exclude` - The list of attribute names which parameters wrapper will
|
219
|
+
# exclude from a nested hash.
|
220
|
+
#
|
221
|
+
def wrap_parameters(name_or_model_or_options, options = {})
|
222
|
+
model = nil
|
223
|
+
|
224
|
+
case name_or_model_or_options
|
225
|
+
when Hash
|
226
|
+
options = name_or_model_or_options
|
227
|
+
when false
|
228
|
+
options = options.merge(format: [])
|
229
|
+
when Symbol, String
|
230
|
+
options = options.merge(name: name_or_model_or_options)
|
231
|
+
else
|
232
|
+
model = name_or_model_or_options
|
233
|
+
end
|
234
|
+
|
235
|
+
opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options)
|
236
|
+
opts.model = model
|
237
|
+
opts.klass = self
|
238
|
+
|
239
|
+
self._wrapper_options = opts
|
240
|
+
end
|
241
|
+
|
242
|
+
# Sets the default wrapper key or model which will be used to determine wrapper
|
243
|
+
# key and attribute names. Called automatically when the module is inherited.
|
244
|
+
def inherited(klass)
|
245
|
+
if klass._wrapper_options.format.any?
|
246
|
+
params = klass._wrapper_options.dup
|
247
|
+
params.klass = klass
|
248
|
+
klass._wrapper_options = params
|
249
|
+
end
|
250
|
+
super
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
private
|
255
|
+
# Performs parameters wrapping upon the request. Called automatically by the
|
256
|
+
# metal call stack.
|
257
|
+
def process_action(*)
|
258
|
+
_perform_parameter_wrapping if _wrapper_enabled?
|
259
|
+
super
|
260
|
+
end
|
261
|
+
|
262
|
+
# Returns the wrapper key which will be used to store wrapped parameters.
|
263
|
+
def _wrapper_key
|
264
|
+
_wrapper_options.name
|
265
|
+
end
|
266
|
+
|
267
|
+
# Returns the list of enabled formats.
|
268
|
+
def _wrapper_formats
|
269
|
+
_wrapper_options.format
|
270
|
+
end
|
271
|
+
|
272
|
+
# Returns the list of parameters which will be selected for wrapped.
|
273
|
+
def _wrap_parameters(parameters)
|
274
|
+
{ _wrapper_key => _extract_parameters(parameters) }
|
275
|
+
end
|
276
|
+
|
277
|
+
def _extract_parameters(parameters)
|
278
|
+
if include_only = _wrapper_options.include
|
279
|
+
parameters.slice(*include_only)
|
280
|
+
elsif _wrapper_options.exclude
|
281
|
+
exclude = _wrapper_options.exclude + EXCLUDE_PARAMETERS
|
282
|
+
parameters.except(*exclude)
|
283
|
+
else
|
284
|
+
parameters.except(*EXCLUDE_PARAMETERS)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# Checks if we should perform parameters wrapping.
|
289
|
+
def _wrapper_enabled?
|
290
|
+
return false unless request.has_content_type?
|
291
|
+
|
292
|
+
ref = request.content_mime_type.ref
|
293
|
+
|
294
|
+
_wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key)
|
295
|
+
rescue ActionDispatch::Http::Parameters::ParseError
|
296
|
+
false
|
297
|
+
end
|
298
|
+
|
299
|
+
def _perform_parameter_wrapping
|
300
|
+
wrapped_hash = _wrap_parameters request.request_parameters
|
301
|
+
wrapped_keys = request.request_parameters.keys
|
302
|
+
wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)
|
303
|
+
|
304
|
+
# This will make the wrapped hash accessible from controller and view.
|
305
|
+
request.parameters.merge! wrapped_hash
|
306
|
+
request.request_parameters.merge! wrapped_hash
|
307
|
+
|
308
|
+
# This will display the wrapped hash in the log file.
|
309
|
+
request.filtered_parameters.merge! wrapped_filtered_hash
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionController # :nodoc:
|
6
|
+
module PermissionsPolicy
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# Overrides parts of the globally configured `Feature-Policy` header:
|
11
|
+
#
|
12
|
+
# class PagesController < ApplicationController
|
13
|
+
# permissions_policy do |policy|
|
14
|
+
# policy.geolocation "https://example.com"
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Options can be passed similar to `before_action`. For example, pass `only:
|
19
|
+
# :index` to override the header on the index action only:
|
20
|
+
#
|
21
|
+
# class PagesController < ApplicationController
|
22
|
+
# permissions_policy(only: :index) do |policy|
|
23
|
+
# policy.camera :self
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
def permissions_policy(**options, &block)
|
28
|
+
before_action(options) do
|
29
|
+
if block_given?
|
30
|
+
policy = request.permissions_policy.clone
|
31
|
+
instance_exec(policy, &block)
|
32
|
+
request.permissions_policy = policy
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionController # :nodoc:
|
6
|
+
module RateLimiting
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# Applies a rate limit to all actions or those specified by the normal
|
11
|
+
# `before_action` filters with `only:` and `except:`.
|
12
|
+
#
|
13
|
+
# The maximum number of requests allowed is specified `to:` and constrained to
|
14
|
+
# the window of time given by `within:`.
|
15
|
+
#
|
16
|
+
# Rate limits are by default unique to the ip address making the request, but
|
17
|
+
# you can provide your own identity function by passing a callable in the `by:`
|
18
|
+
# parameter. It's evaluated within the context of the controller processing the
|
19
|
+
# request.
|
20
|
+
#
|
21
|
+
# Requests that exceed the rate limit are refused with a `429 Too Many Requests`
|
22
|
+
# response. You can specialize this by passing a callable in the `with:`
|
23
|
+
# parameter. It's evaluated within the context of the controller processing the
|
24
|
+
# request.
|
25
|
+
#
|
26
|
+
# Rate limiting relies on a backing `ActiveSupport::Cache` store and defaults to
|
27
|
+
# `config.action_controller.cache_store`, which itself defaults to the global
|
28
|
+
# `config.cache_store`. If you don't want to store rate limits in the same
|
29
|
+
# datastore as your general caches, you can pass a custom store in the `store`
|
30
|
+
# parameter.
|
31
|
+
#
|
32
|
+
# Examples:
|
33
|
+
#
|
34
|
+
# class SessionsController < ApplicationController
|
35
|
+
# rate_limit to: 10, within: 3.minutes, only: :create
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# class SignupsController < ApplicationController
|
39
|
+
# rate_limit to: 1000, within: 10.seconds,
|
40
|
+
# by: -> { request.domain }, with: -> { redirect_to busy_controller_url, alert: "Too many signups on domain!" }, only: :new
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# class APIController < ApplicationController
|
44
|
+
# RATE_LIMIT_STORE = ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"])
|
45
|
+
# rate_limit to: 10, within: 3.minutes, store: RATE_LIMIT_STORE
|
46
|
+
# end
|
47
|
+
def rate_limit(to:, within:, by: -> { request.remote_ip }, with: -> { head :too_many_requests }, store: cache_store, **options)
|
48
|
+
before_action -> { rate_limiting(to: to, within: within, by: by, with: with, store: store) }, **options
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def rate_limiting(to:, within:, by:, with:, store:)
|
54
|
+
count = store.increment("rate-limit:#{controller_path}:#{instance_exec(&by)}", 1, expires_in: within)
|
55
|
+
if count && count > to
|
56
|
+
ActiveSupport::Notifications.instrument("rate_limit.action_controller", request: request) do
|
57
|
+
instance_exec(&with)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,251 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionController
|
6
|
+
module Redirecting
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
include AbstractController::Logger
|
10
|
+
include ActionController::UrlFor
|
11
|
+
|
12
|
+
class UnsafeRedirectError < StandardError; end
|
13
|
+
|
14
|
+
ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/
|
15
|
+
|
16
|
+
included do
|
17
|
+
mattr_accessor :raise_on_open_redirects, default: false
|
18
|
+
end
|
19
|
+
|
20
|
+
# Redirects the browser to the target specified in `options`. This parameter can
|
21
|
+
# be any one of:
|
22
|
+
#
|
23
|
+
# * `Hash` - The URL will be generated by calling url_for with the `options`.
|
24
|
+
# * `Record` - The URL will be generated by calling url_for with the
|
25
|
+
# `options`, which will reference a named URL for that record.
|
26
|
+
# * `String` starting with `protocol://` (like `http://`) or a protocol
|
27
|
+
# relative reference (like `//`) - Is passed straight through as the target
|
28
|
+
# for redirection.
|
29
|
+
# * `String` not containing a protocol - The current protocol and host is
|
30
|
+
# prepended to the string.
|
31
|
+
# * `Proc` - A block that will be executed in the controller's context. Should
|
32
|
+
# return any option accepted by `redirect_to`.
|
33
|
+
#
|
34
|
+
#
|
35
|
+
# ### Examples
|
36
|
+
#
|
37
|
+
# redirect_to action: "show", id: 5
|
38
|
+
# redirect_to @post
|
39
|
+
# redirect_to "http://www.rubyonrails.org"
|
40
|
+
# redirect_to "/images/screenshot.jpg"
|
41
|
+
# redirect_to posts_url
|
42
|
+
# redirect_to proc { edit_post_url(@post) }
|
43
|
+
#
|
44
|
+
# The redirection happens as a `302 Found` header unless otherwise specified
|
45
|
+
# using the `:status` option:
|
46
|
+
#
|
47
|
+
# redirect_to post_url(@post), status: :found
|
48
|
+
# redirect_to action: 'atom', status: :moved_permanently
|
49
|
+
# redirect_to post_url(@post), status: 301
|
50
|
+
# redirect_to action: 'atom', status: 302
|
51
|
+
#
|
52
|
+
# The status code can either be a standard [HTTP Status
|
53
|
+
# code](https://www.iana.org/assignments/http-status-codes) as an integer, or a
|
54
|
+
# symbol representing the downcased, underscored and symbolized description.
|
55
|
+
# Note that the status code must be a 3xx HTTP code, or redirection will not
|
56
|
+
# occur.
|
57
|
+
#
|
58
|
+
# If you are using XHR requests other than GET or POST and redirecting after the
|
59
|
+
# request then some browsers will follow the redirect using the original request
|
60
|
+
# method. This may lead to undesirable behavior such as a double DELETE. To work
|
61
|
+
# around this you can return a `303 See Other` status code which will be
|
62
|
+
# followed using a GET request.
|
63
|
+
#
|
64
|
+
# redirect_to posts_url, status: :see_other
|
65
|
+
# redirect_to action: 'index', status: 303
|
66
|
+
#
|
67
|
+
# It is also possible to assign a flash message as part of the redirection.
|
68
|
+
# There are two special accessors for the commonly used flash names `alert` and
|
69
|
+
# `notice` as well as a general purpose `flash` bucket.
|
70
|
+
#
|
71
|
+
# redirect_to post_url(@post), alert: "Watch it, mister!"
|
72
|
+
# redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
|
73
|
+
# redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
|
74
|
+
# redirect_to({ action: 'atom' }, alert: "Something serious happened")
|
75
|
+
#
|
76
|
+
# Statements after `redirect_to` in our controller get executed, so
|
77
|
+
# `redirect_to` doesn't stop the execution of the function. To terminate the
|
78
|
+
# execution of the function immediately after the `redirect_to`, use return.
|
79
|
+
#
|
80
|
+
# redirect_to post_url(@post) and return
|
81
|
+
#
|
82
|
+
# ### Open Redirect protection
|
83
|
+
#
|
84
|
+
# By default, Rails protects against redirecting to external hosts for your
|
85
|
+
# app's safety, so called open redirects. Note: this was a new default in Rails
|
86
|
+
# 7.0, after upgrading opt-in by uncommenting the line with
|
87
|
+
# `raise_on_open_redirects` in
|
88
|
+
# `config/initializers/new_framework_defaults_7_0.rb`
|
89
|
+
#
|
90
|
+
# Here #redirect_to automatically validates the potentially-unsafe URL:
|
91
|
+
#
|
92
|
+
# redirect_to params[:redirect_url]
|
93
|
+
#
|
94
|
+
# Raises UnsafeRedirectError in the case of an unsafe redirect.
|
95
|
+
#
|
96
|
+
# To allow any external redirects pass `allow_other_host: true`, though using a
|
97
|
+
# user-provided param in that case is unsafe.
|
98
|
+
#
|
99
|
+
# redirect_to "https://rubyonrails.org", allow_other_host: true
|
100
|
+
#
|
101
|
+
# See #url_from for more information on what an internal and safe URL is, or how
|
102
|
+
# to fall back to an alternate redirect URL in the unsafe case.
|
103
|
+
def redirect_to(options = {}, response_options = {})
|
104
|
+
raise ActionControllerError.new("Cannot redirect to nil!") unless options
|
105
|
+
raise AbstractController::DoubleRenderError if response_body
|
106
|
+
|
107
|
+
allow_other_host = response_options.delete(:allow_other_host) { _allow_other_host }
|
108
|
+
|
109
|
+
self.status = _extract_redirect_to_status(options, response_options)
|
110
|
+
|
111
|
+
redirect_to_location = _compute_redirect_to_location(request, options)
|
112
|
+
_ensure_url_is_http_header_safe(redirect_to_location)
|
113
|
+
|
114
|
+
self.location = _enforce_open_redirect_protection(redirect_to_location, allow_other_host: allow_other_host)
|
115
|
+
self.response_body = ""
|
116
|
+
end
|
117
|
+
|
118
|
+
# Soft deprecated alias for #redirect_back_or_to where the `fallback_location`
|
119
|
+
# location is supplied as a keyword argument instead of the first positional
|
120
|
+
# argument.
|
121
|
+
def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **args)
|
122
|
+
redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args
|
123
|
+
end
|
124
|
+
|
125
|
+
# Redirects the browser to the page that issued the request (the referrer) if
|
126
|
+
# possible, otherwise redirects to the provided default fallback location.
|
127
|
+
#
|
128
|
+
# The referrer information is pulled from the HTTP `Referer` (sic) header on the
|
129
|
+
# request. This is an optional header and its presence on the request is subject
|
130
|
+
# to browser security settings and user preferences. If the request is missing
|
131
|
+
# this header, the `fallback_location` will be used.
|
132
|
+
#
|
133
|
+
# redirect_back_or_to({ action: "show", id: 5 })
|
134
|
+
# redirect_back_or_to @post
|
135
|
+
# redirect_back_or_to "http://www.rubyonrails.org"
|
136
|
+
# redirect_back_or_to "/images/screenshot.jpg"
|
137
|
+
# redirect_back_or_to posts_url
|
138
|
+
# redirect_back_or_to proc { edit_post_url(@post) }
|
139
|
+
# redirect_back_or_to '/', allow_other_host: false
|
140
|
+
#
|
141
|
+
# #### Options
|
142
|
+
# * `:allow_other_host` - Allow or disallow redirection to the host that is
|
143
|
+
# different to the current host, defaults to true.
|
144
|
+
#
|
145
|
+
#
|
146
|
+
# All other options that can be passed to #redirect_to are accepted as options,
|
147
|
+
# and the behavior is identical.
|
148
|
+
def redirect_back_or_to(fallback_location, allow_other_host: _allow_other_host, **options)
|
149
|
+
if request.referer && (allow_other_host || _url_host_allowed?(request.referer))
|
150
|
+
redirect_to request.referer, allow_other_host: allow_other_host, **options
|
151
|
+
else
|
152
|
+
# The method level `allow_other_host` doesn't apply in the fallback case, omit
|
153
|
+
# and let the `redirect_to` handling take over.
|
154
|
+
redirect_to fallback_location, **options
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def _compute_redirect_to_location(request, options) # :nodoc:
|
159
|
+
case options
|
160
|
+
# The scheme name consist of a letter followed by any combination of letters,
|
161
|
+
# digits, and the plus ("+"), period ("."), or hyphen ("-") characters; and is
|
162
|
+
# terminated by a colon (":"). See
|
163
|
+
# https://tools.ietf.org/html/rfc3986#section-3.1 The protocol relative scheme
|
164
|
+
# starts with a double slash "//".
|
165
|
+
when /\A([a-z][a-z\d\-+.]*:|\/\/).*/i
|
166
|
+
options.to_str
|
167
|
+
when String
|
168
|
+
request.protocol + request.host_with_port + options
|
169
|
+
when Proc
|
170
|
+
_compute_redirect_to_location request, instance_eval(&options)
|
171
|
+
else
|
172
|
+
url_for(options)
|
173
|
+
end.delete("\0\r\n")
|
174
|
+
end
|
175
|
+
module_function :_compute_redirect_to_location
|
176
|
+
public :_compute_redirect_to_location
|
177
|
+
|
178
|
+
# Verifies the passed `location` is an internal URL that's safe to redirect to
|
179
|
+
# and returns it, or nil if not. Useful to wrap a params provided redirect URL
|
180
|
+
# and fall back to an alternate URL to redirect to:
|
181
|
+
#
|
182
|
+
# redirect_to url_from(params[:redirect_url]) || root_url
|
183
|
+
#
|
184
|
+
# The `location` is considered internal, and safe, if it's on the same host as
|
185
|
+
# `request.host`:
|
186
|
+
#
|
187
|
+
# # If request.host is example.com:
|
188
|
+
# url_from("https://example.com/profile") # => "https://example.com/profile"
|
189
|
+
# url_from("http://example.com/profile") # => "http://example.com/profile"
|
190
|
+
# url_from("http://evil.com/profile") # => nil
|
191
|
+
#
|
192
|
+
# Subdomains are considered part of the host:
|
193
|
+
#
|
194
|
+
# # If request.host is on https://example.com or https://app.example.com, you'd get:
|
195
|
+
# url_from("https://dev.example.com/profile") # => nil
|
196
|
+
#
|
197
|
+
# NOTE: there's a similarity with
|
198
|
+
# [url_for](rdoc-ref:ActionDispatch::Routing::UrlFor#url_for), which generates
|
199
|
+
# an internal URL from various options from within the app, e.g.
|
200
|
+
# `url_for(@post)`. However, #url_from is meant to take an external parameter to
|
201
|
+
# verify as in `url_from(params[:redirect_url])`.
|
202
|
+
def url_from(location)
|
203
|
+
location = location.presence
|
204
|
+
location if location && _url_host_allowed?(location)
|
205
|
+
end
|
206
|
+
|
207
|
+
private
|
208
|
+
def _allow_other_host
|
209
|
+
!raise_on_open_redirects
|
210
|
+
end
|
211
|
+
|
212
|
+
def _extract_redirect_to_status(options, response_options)
|
213
|
+
if options.is_a?(Hash) && options.key?(:status)
|
214
|
+
Rack::Utils.status_code(options.delete(:status))
|
215
|
+
elsif response_options.key?(:status)
|
216
|
+
Rack::Utils.status_code(response_options[:status])
|
217
|
+
else
|
218
|
+
302
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def _enforce_open_redirect_protection(location, allow_other_host:)
|
223
|
+
if allow_other_host || _url_host_allowed?(location)
|
224
|
+
location
|
225
|
+
else
|
226
|
+
raise UnsafeRedirectError, "Unsafe redirect to #{location.truncate(100).inspect}, pass allow_other_host: true to redirect anyway."
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def _url_host_allowed?(url)
|
231
|
+
host = URI(url.to_s).host
|
232
|
+
|
233
|
+
return true if host == request.host
|
234
|
+
return false unless host.nil?
|
235
|
+
return false unless url.to_s.start_with?("/")
|
236
|
+
!url.to_s.start_with?("//")
|
237
|
+
rescue ArgumentError, URI::Error
|
238
|
+
false
|
239
|
+
end
|
240
|
+
|
241
|
+
def _ensure_url_is_http_header_safe(url)
|
242
|
+
# Attempt to comply with the set of valid token characters defined for an HTTP
|
243
|
+
# header value in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
|
244
|
+
if url.match?(ILLEGAL_HEADER_VALUE_REGEX)
|
245
|
+
msg = "The redirect URL #{url} contains one or more illegal HTTP header field character. " \
|
246
|
+
"Set of legal characters defined in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6"
|
247
|
+
raise UnsafeRedirectError, msg
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|