omg-actionpack 8.0.0.alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|