actionpack 5.2.1 → 7.0.2.4

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

Potentially problematic release.


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

Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -220
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -6
  5. data/lib/abstract_controller/asset_paths.rb +1 -1
  6. data/lib/abstract_controller/base.rb +24 -4
  7. data/lib/abstract_controller/caching/fragments.rb +8 -24
  8. data/lib/abstract_controller/caching.rb +2 -2
  9. data/lib/abstract_controller/callbacks.rb +34 -8
  10. data/lib/abstract_controller/collector.rb +5 -4
  11. data/lib/abstract_controller/error.rb +1 -1
  12. data/lib/abstract_controller/helpers.rb +107 -90
  13. data/lib/abstract_controller/logger.rb +1 -1
  14. data/lib/abstract_controller/railties/routes_helpers.rb +19 -1
  15. data/lib/abstract_controller/rendering.rb +9 -9
  16. data/lib/abstract_controller/translation.rb +12 -5
  17. data/lib/abstract_controller/url_for.rb +4 -6
  18. data/lib/abstract_controller.rb +2 -0
  19. data/lib/action_controller/api.rb +5 -4
  20. data/lib/action_controller/base.rb +6 -9
  21. data/lib/action_controller/caching.rb +1 -3
  22. data/lib/action_controller/log_subscriber.rb +13 -9
  23. data/lib/action_controller/metal/basic_implicit_render.rb +1 -1
  24. data/lib/action_controller/metal/conditional_get.rb +57 -6
  25. data/lib/action_controller/metal/content_security_policy.rb +2 -3
  26. data/lib/action_controller/metal/cookies.rb +4 -2
  27. data/lib/action_controller/metal/data_streaming.rb +9 -18
  28. data/lib/action_controller/metal/default_headers.rb +17 -0
  29. data/lib/action_controller/metal/etag_with_template_digest.rb +4 -6
  30. data/lib/action_controller/metal/exceptions.rb +55 -12
  31. data/lib/action_controller/metal/flash.rb +10 -6
  32. data/lib/action_controller/metal/head.rb +7 -4
  33. data/lib/action_controller/metal/helpers.rb +15 -6
  34. data/lib/action_controller/metal/http_authentication.rb +41 -39
  35. data/lib/action_controller/metal/implicit_render.rb +5 -15
  36. data/lib/action_controller/metal/instrumentation.rb +59 -55
  37. data/lib/action_controller/metal/live.rb +80 -33
  38. data/lib/action_controller/metal/logging.rb +20 -0
  39. data/lib/action_controller/metal/mime_responds.rb +22 -7
  40. data/lib/action_controller/metal/parameter_encoding.rb +35 -4
  41. data/lib/action_controller/metal/params_wrapper.rb +50 -31
  42. data/lib/action_controller/metal/permissions_policy.rb +46 -0
  43. data/lib/action_controller/metal/redirecting.rb +93 -23
  44. data/lib/action_controller/metal/renderers.rb +4 -4
  45. data/lib/action_controller/metal/rendering.rb +14 -9
  46. data/lib/action_controller/metal/request_forgery_protection.rb +160 -58
  47. data/lib/action_controller/metal/rescue.rb +2 -2
  48. data/lib/action_controller/metal/streaming.rb +1 -4
  49. data/lib/action_controller/metal/strong_parameters.rb +236 -88
  50. data/lib/action_controller/metal/testing.rb +9 -2
  51. data/lib/action_controller/metal/url_for.rb +1 -1
  52. data/lib/action_controller/metal.rb +16 -17
  53. data/lib/action_controller/railtie.rb +49 -6
  54. data/lib/action_controller/railties/helpers.rb +1 -1
  55. data/lib/action_controller/renderer.rb +37 -13
  56. data/lib/action_controller/template_assertions.rb +1 -1
  57. data/lib/action_controller/test_case.rb +98 -68
  58. data/lib/action_controller.rb +4 -5
  59. data/lib/action_dispatch/http/cache.rb +45 -32
  60. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  61. data/lib/action_dispatch/http/content_security_policy.rb +69 -56
  62. data/lib/action_dispatch/http/filter_parameters.rb +14 -8
  63. data/lib/action_dispatch/http/filter_redirect.rb +2 -3
  64. data/lib/action_dispatch/http/headers.rb +4 -4
  65. data/lib/action_dispatch/http/mime_negotiation.rb +44 -16
  66. data/lib/action_dispatch/http/mime_type.rb +47 -30
  67. data/lib/action_dispatch/http/parameters.rb +18 -27
  68. data/lib/action_dispatch/http/permissions_policy.rb +173 -0
  69. data/lib/action_dispatch/http/request.rb +49 -35
  70. data/lib/action_dispatch/http/response.rb +34 -26
  71. data/lib/action_dispatch/http/upload.rb +9 -1
  72. data/lib/action_dispatch/http/url.rb +86 -94
  73. data/lib/action_dispatch/journey/formatter.rb +55 -31
  74. data/lib/action_dispatch/journey/gtg/builder.rb +30 -46
  75. data/lib/action_dispatch/journey/gtg/simulator.rb +15 -8
  76. data/lib/action_dispatch/journey/gtg/transition_table.rb +78 -21
  77. data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
  78. data/lib/action_dispatch/journey/nodes/node.rb +83 -16
  79. data/lib/action_dispatch/journey/parser.rb +13 -13
  80. data/lib/action_dispatch/journey/parser.y +1 -1
  81. data/lib/action_dispatch/journey/path/pattern.rb +42 -34
  82. data/lib/action_dispatch/journey/route.rb +14 -31
  83. data/lib/action_dispatch/journey/router/utils.rb +16 -14
  84. data/lib/action_dispatch/journey/router.rb +27 -35
  85. data/lib/action_dispatch/journey/routes.rb +3 -5
  86. data/lib/action_dispatch/journey/scanner.rb +10 -4
  87. data/lib/action_dispatch/journey/visitors.rb +1 -4
  88. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  89. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  90. data/lib/action_dispatch/journey.rb +0 -2
  91. data/lib/action_dispatch/middleware/actionable_exceptions.rb +45 -0
  92. data/lib/action_dispatch/middleware/callbacks.rb +2 -4
  93. data/lib/action_dispatch/middleware/cookies.rb +136 -113
  94. data/lib/action_dispatch/middleware/debug_exceptions.rb +47 -68
  95. data/lib/action_dispatch/middleware/debug_locks.rb +8 -8
  96. data/lib/action_dispatch/middleware/debug_view.rb +66 -0
  97. data/lib/action_dispatch/middleware/exception_wrapper.rb +79 -30
  98. data/lib/action_dispatch/middleware/executor.rb +4 -1
  99. data/lib/action_dispatch/middleware/flash.rb +10 -12
  100. data/lib/action_dispatch/middleware/host_authorization.rb +159 -0
  101. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -3
  102. data/lib/action_dispatch/middleware/remote_ip.rb +30 -20
  103. data/lib/action_dispatch/middleware/request_id.rb +5 -6
  104. data/lib/action_dispatch/middleware/server_timing.rb +33 -0
  105. data/lib/action_dispatch/middleware/session/abstract_store.rb +16 -3
  106. data/lib/action_dispatch/middleware/session/cache_store.rb +11 -6
  107. data/lib/action_dispatch/middleware/session/cookie_store.rb +24 -19
  108. data/lib/action_dispatch/middleware/show_exceptions.rb +20 -11
  109. data/lib/action_dispatch/middleware/ssl.rb +20 -15
  110. data/lib/action_dispatch/middleware/stack.rb +79 -7
  111. data/lib/action_dispatch/middleware/static.rb +150 -94
  112. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  113. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  114. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  115. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +6 -11
  116. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  117. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +4 -2
  118. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +46 -36
  119. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +8 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +7 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +25 -6
  122. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
  123. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +9 -6
  124. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +4 -1
  125. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +121 -15
  126. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +5 -5
  129. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +4 -4
  130. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +5 -5
  131. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
  132. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +16 -2
  133. data/lib/action_dispatch/railtie.rb +16 -4
  134. data/lib/action_dispatch/request/session.rb +59 -22
  135. data/lib/action_dispatch/request/utils.rb +28 -2
  136. data/lib/action_dispatch/routing/inspector.rb +102 -54
  137. data/lib/action_dispatch/routing/mapper.rb +184 -156
  138. data/lib/action_dispatch/routing/polymorphic_routes.rb +21 -19
  139. data/lib/action_dispatch/routing/redirection.rb +4 -6
  140. data/lib/action_dispatch/routing/route_set.rb +83 -73
  141. data/lib/action_dispatch/routing/routes_proxy.rb +1 -1
  142. data/lib/action_dispatch/routing/url_for.rb +2 -3
  143. data/lib/action_dispatch/routing.rb +23 -22
  144. data/lib/action_dispatch/system_test_case.rb +65 -16
  145. data/lib/action_dispatch/system_testing/browser.rb +43 -16
  146. data/lib/action_dispatch/system_testing/driver.rb +42 -10
  147. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +58 -12
  148. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +3 -10
  149. data/lib/action_dispatch/testing/assertion_response.rb +0 -1
  150. data/lib/action_dispatch/testing/assertions/response.rb +4 -7
  151. data/lib/action_dispatch/testing/assertions/routing.rb +20 -8
  152. data/lib/action_dispatch/testing/assertions.rb +3 -6
  153. data/lib/action_dispatch/testing/integration.rb +61 -30
  154. data/lib/action_dispatch/testing/request_encoder.rb +2 -2
  155. data/lib/action_dispatch/testing/test_process.rb +8 -6
  156. data/lib/action_dispatch/testing/test_request.rb +3 -3
  157. data/lib/action_dispatch/testing/test_response.rb +4 -32
  158. data/lib/action_dispatch.rb +15 -7
  159. data/lib/action_pack/gem_version.rb +4 -4
  160. data/lib/action_pack.rb +1 -1
  161. metadata +44 -25
  162. data/lib/action_controller/metal/force_ssl.rb +0 -99
  163. data/lib/action_dispatch/http/parameter_filter.rb +0 -86
  164. data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
  165. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -49
  166. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -120
  167. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +0 -26
@@ -12,7 +12,7 @@ module ActionDispatch
12
12
  class Mapper
13
13
  URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
14
14
 
15
- class Constraints < Routing::Endpoint #:nodoc:
15
+ class Constraints < Routing::Endpoint # :nodoc:
16
16
  attr_reader :app, :constraints
17
17
 
18
18
  SERVE = ->(app, req) { app.serve req }
@@ -50,25 +50,41 @@ module ActionDispatch
50
50
 
51
51
  private
52
52
  def constraint_args(constraint, request)
53
- constraint.arity == 1 ? [request] : [request.path_parameters, request]
53
+ arity = if constraint.respond_to?(:arity)
54
+ constraint.arity
55
+ else
56
+ constraint.method(:call).arity
57
+ end
58
+
59
+ if arity < 1
60
+ []
61
+ elsif arity == 1
62
+ [request]
63
+ else
64
+ [request.path_parameters, request]
65
+ end
54
66
  end
55
67
  end
56
68
 
57
- class Mapping #:nodoc:
69
+ class Mapping # :nodoc:
58
70
  ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
59
71
  OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z}
60
72
 
61
- attr_reader :requirements, :defaults
62
- attr_reader :to, :default_controller, :default_action
63
- attr_reader :required_defaults, :ast
73
+ attr_reader :path, :requirements, :defaults, :to, :default_controller,
74
+ :default_action, :required_defaults, :ast, :scope_options
64
75
 
65
76
  def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
66
- options = scope[:options].merge(options) if scope[:options]
67
-
68
- defaults = (scope[:defaults] || {}).dup
69
- scope_constraints = scope[:constraints] || {}
77
+ scope_params = {
78
+ blocks: scope[:blocks] || [],
79
+ constraints: scope[:constraints] || {},
80
+ defaults: (scope[:defaults] || {}).dup,
81
+ module: scope[:module],
82
+ options: scope[:options] || {}
83
+ }
70
84
 
71
- new set, ast, defaults, controller, default_action, scope[:module], to, formatted, scope_constraints, scope[:blocks] || [], via, options_constraints, anchor, options
85
+ new set: set, ast: ast, controller: controller, default_action: default_action,
86
+ to: to, formatted: formatted, via: via, options_constraints: options_constraints,
87
+ anchor: anchor, scope_params: scope_params, options: scope_params[:options].merge(options)
72
88
  end
73
89
 
74
90
  def self.check_via(via)
@@ -96,43 +112,41 @@ module ActionDispatch
96
112
  end
97
113
 
98
114
  def self.optional_format?(path, format)
99
- format != false && path !~ OPTIONAL_FORMAT_REGEX
115
+ format != false && !path.match?(OPTIONAL_FORMAT_REGEX)
100
116
  end
101
117
 
102
- def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options)
103
- @defaults = defaults
104
- @set = set
105
-
106
- @to = to
107
- @default_controller = controller
108
- @default_action = default_action
109
- @ast = ast
118
+ def initialize(set:, ast:, controller:, default_action:, to:, formatted:, via:, options_constraints:, anchor:, scope_params:, options:)
119
+ @defaults = scope_params[:defaults]
120
+ @set = set
121
+ @to = intern(to)
122
+ @default_controller = intern(controller)
123
+ @default_action = intern(default_action)
110
124
  @anchor = anchor
111
125
  @via = via
112
126
  @internal = options.delete(:internal)
127
+ @scope_options = scope_params[:options]
128
+ ast = Journey::Ast.new(ast, formatted)
113
129
 
114
- path_params = ast.find_all(&:symbol?).map(&:to_sym)
115
-
116
- options = add_wildcard_options(options, formatted, ast)
130
+ options = ast.wildcard_options.merge!(options)
117
131
 
118
- options = normalize_options!(options, path_params, modyoule)
132
+ options = normalize_options!(options, ast.path_params, scope_params[:module])
119
133
 
120
- split_options = constraints(options, path_params)
134
+ split_options = constraints(options, ast.path_params)
121
135
 
122
- constraints = scope_constraints.merge Hash[split_options[:constraints] || []]
136
+ constraints = scope_params[:constraints].merge Hash[split_options[:constraints] || []]
123
137
 
124
138
  if options_constraints.is_a?(Hash)
125
139
  @defaults = Hash[options_constraints.find_all { |key, default|
126
140
  URL_OPTIONS.include?(key) && (String === default || Integer === default)
127
141
  }].merge @defaults
128
- @blocks = blocks
142
+ @blocks = scope_params[:blocks]
129
143
  constraints.merge! options_constraints
130
144
  else
131
145
  @blocks = blocks(options_constraints)
132
146
  end
133
147
 
134
- requirements, conditions = split_constraints path_params, constraints
135
- verify_regexp_requirements requirements.map(&:last).grep(Regexp)
148
+ requirements, conditions = split_constraints ast.path_params, constraints
149
+ verify_regexp_requirements requirements, ast.wildcard_options
136
150
 
137
151
  formats = normalize_format(formatted)
138
152
 
@@ -140,35 +154,29 @@ module ActionDispatch
140
154
  @conditions = Hash[conditions]
141
155
  @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))
142
156
 
143
- if path_params.include?(:action) && !@requirements.key?(:action)
157
+ if ast.path_params.include?(:action) && !@requirements.key?(:action)
144
158
  @defaults[:action] ||= "index"
145
159
  end
146
160
 
147
161
  @required_defaults = (split_options[:required_defaults] || []).map(&:first)
162
+
163
+ ast.requirements = @requirements
164
+ @path = Journey::Path::Pattern.new(ast, @requirements, JOINED_SEPARATORS, @anchor)
148
165
  end
149
166
 
167
+ JOINED_SEPARATORS = SEPARATORS.join # :nodoc:
168
+
150
169
  def make_route(name, precedence)
151
- route = Journey::Route.new(name,
152
- application,
153
- path,
154
- conditions,
155
- required_defaults,
156
- defaults,
157
- request_method,
158
- precedence,
159
- @internal)
160
-
161
- route
170
+ Journey::Route.new(name: name, app: application, path: path, constraints: conditions,
171
+ required_defaults: required_defaults, defaults: defaults,
172
+ request_method_match: request_method, precedence: precedence,
173
+ scope_options: scope_options, internal: @internal)
162
174
  end
163
175
 
164
176
  def application
165
177
  app(@blocks)
166
178
  end
167
179
 
168
- def path
169
- build_path @ast, requirements, @anchor
170
- end
171
-
172
180
  def conditions
173
181
  build_conditions @conditions, @set.request_class
174
182
  end
@@ -187,48 +195,9 @@ module ActionDispatch
187
195
  end
188
196
  private :request_method
189
197
 
190
- JOINED_SEPARATORS = SEPARATORS.join # :nodoc:
191
-
192
- def build_path(ast, requirements, anchor)
193
- pattern = Journey::Path::Pattern.new(ast, requirements, JOINED_SEPARATORS, anchor)
194
-
195
- # Find all the symbol nodes that are adjacent to literal nodes and alter
196
- # the regexp so that Journey will partition them into custom routes.
197
- ast.find_all { |node|
198
- next unless node.cat?
199
-
200
- if node.left.literal? && node.right.symbol?
201
- symbol = node.right
202
- elsif node.left.literal? && node.right.cat? && node.right.left.symbol?
203
- symbol = node.right.left
204
- elsif node.left.symbol? && node.right.literal?
205
- symbol = node.left
206
- elsif node.left.symbol? && node.right.cat? && node.right.left.literal?
207
- symbol = node.left
208
- else
209
- next
210
- end
211
-
212
- if symbol
213
- symbol.regexp = /(?:#{Regexp.union(symbol.regexp, '-')})+/
214
- end
215
- }
216
-
217
- pattern
218
- end
219
- private :build_path
220
-
221
198
  private
222
- def add_wildcard_options(options, formatted, path_ast)
223
- # Add a constraint for wildcard route to make it non-greedy and match the
224
- # optional format part of the route by default.
225
- if formatted != false
226
- path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash|
227
- hash[node.name.to_sym] ||= /.+?/
228
- }.merge options
229
- else
230
- options
231
- end
199
+ def intern(object)
200
+ object.is_a?(String) ? -object : object
232
201
  end
233
202
 
234
203
  def normalize_options!(options, path_params, modyoule)
@@ -277,14 +246,18 @@ module ActionDispatch
277
246
  end
278
247
  end
279
248
 
280
- def verify_regexp_requirements(requirements)
281
- requirements.each do |requirement|
282
- if requirement.source =~ ANCHOR_CHARACTERS_REGEX
249
+ def verify_regexp_requirements(requirements, wildcard_options)
250
+ requirements.each do |requirement, regex|
251
+ next unless regex.is_a? Regexp
252
+
253
+ if ANCHOR_CHARACTERS_REGEX.match?(regex.source)
283
254
  raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
284
255
  end
285
256
 
286
- if requirement.multiline?
287
- raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
257
+ if regex.multiline?
258
+ next if wildcard_options.key?(requirement)
259
+
260
+ raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{regex.inspect}"
288
261
  end
289
262
  end
290
263
  end
@@ -308,8 +281,8 @@ module ActionDispatch
308
281
  def check_controller_and_action(path_params, controller, action)
309
282
  hash = check_part(:controller, controller, path_params, {}) do |part|
310
283
  translate_controller(part) {
311
- message = "'#{part}' is not a supported controller name. This can lead to potential routing problems.".dup
312
- message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
284
+ message = +"'#{part}' is not a supported controller name. This can lead to potential routing problems."
285
+ message << " See https://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
313
286
 
314
287
  raise ArgumentError, message
315
288
  }
@@ -333,8 +306,8 @@ module ActionDispatch
333
306
  end
334
307
 
335
308
  def split_to(to)
336
- if to =~ /#/
337
- to.split("#")
309
+ if /#/.match?(to)
310
+ to.split("#").map!(&:-@)
338
311
  else
339
312
  []
340
313
  end
@@ -342,10 +315,10 @@ module ActionDispatch
342
315
 
343
316
  def add_controller_module(controller, modyoule)
344
317
  if modyoule && !controller.is_a?(Regexp)
345
- if controller =~ %r{\A/}
346
- controller[1..-1]
318
+ if controller&.start_with?("/")
319
+ -controller[1..-1]
347
320
  else
348
- [modyoule, controller].compact.join("/")
321
+ -[modyoule, controller].compact.join("/")
349
322
  end
350
323
  else
351
324
  controller
@@ -354,7 +327,7 @@ module ActionDispatch
354
327
 
355
328
  def translate_controller(controller)
356
329
  return controller if Regexp === controller
357
- return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/
330
+ return controller.to_s if /\A[a-z_0-9][a-z_0-9\/]*\z/.match?(controller)
358
331
 
359
332
  yield
360
333
  end
@@ -385,12 +358,23 @@ module ActionDispatch
385
358
  end
386
359
  end
387
360
 
388
- # Invokes Journey::Router::Utils.normalize_path and ensure that
389
- # (:locale) becomes (/:locale) instead of /(:locale). Except
390
- # for root cases, where the latter is the correct one.
361
+ # Invokes Journey::Router::Utils.normalize_path, then ensures that
362
+ # /(:locale) becomes (/:locale). Except for root cases, where the
363
+ # former is the correct one.
391
364
  def self.normalize_path(path)
392
365
  path = Journey::Router::Utils.normalize_path(path)
393
- path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
366
+
367
+ # the path for a root URL at this point can be something like
368
+ # "/(/:locale)(/:platform)/(:browser)", and we would want
369
+ # "/(:locale)(/:platform)(/:browser)"
370
+
371
+ # reverse "/(", "/((" etc to "(/", "((/" etc
372
+ path.gsub!(%r{/(\(+)/?}, '\1/')
373
+ # if a path is all optional segments, change the leading "(/" back to
374
+ # "/(" so it evaluates to "/" when interpreted with no options.
375
+ # Unless, however, at least one secondary segment consists of a static
376
+ # part, ex. "(/:locale)(/pages/:page)"
377
+ path.sub!(%r{^(\(+)/}, '/\1') if %r{^(\(+[^)]+\))(\(+/:[^)]+\))*$}.match?(path)
394
378
  path
395
379
  end
396
380
 
@@ -547,16 +531,16 @@ module ActionDispatch
547
531
  # Constrains parameters with a hash of regular expressions
548
532
  # or an object that responds to <tt>matches?</tt>. In addition, constraints
549
533
  # other than path can also be specified with any object
550
- # that responds to <tt>===</tt> (eg. String, Array, Range, etc.).
534
+ # that responds to <tt>===</tt> (e.g. String, Array, Range, etc.).
551
535
  #
552
536
  # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get
553
537
  #
554
538
  # match 'json_only', constraints: { format: 'json' }, via: :get
555
539
  #
556
- # class Whitelist
540
+ # class PermitList
557
541
  # def matches?(request) request.remote_ip == '1.2.3.4' end
558
542
  # end
559
- # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get
543
+ # match 'path', to: 'c#a', constraints: PermitList.new, via: :get
560
544
  #
561
545
  # See <tt>Scoping#constraints</tt> for more examples with its scope
562
546
  # equivalent.
@@ -611,7 +595,7 @@ module ActionDispatch
611
595
  end
612
596
 
613
597
  raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
614
- raise ArgumentError, <<-MSG.strip_heredoc unless path
598
+ raise ArgumentError, <<~MSG unless path
615
599
  Must be called with mount point
616
600
 
617
601
  mount SomeRackApp, at: "some_route"
@@ -644,7 +628,7 @@ module ActionDispatch
644
628
 
645
629
  # Query if the following named route was already defined.
646
630
  def has_named_route?(name)
647
- @set.named_routes.key? name
631
+ @set.named_routes.key?(name)
648
632
  end
649
633
 
650
634
  private
@@ -668,7 +652,7 @@ module ActionDispatch
668
652
 
669
653
  script_namer = ->(options) do
670
654
  prefix_options = options.slice(*_route.segment_keys)
671
- prefix_options[:relative_url_root] = "".freeze
655
+ prefix_options[:relative_url_root] = ""
672
656
 
673
657
  if options[:_recall]
674
658
  prefix_options.reverse_merge!(options[:_recall].slice(*_route.segment_keys))
@@ -676,7 +660,7 @@ module ActionDispatch
676
660
 
677
661
  # We must actually delete prefix segment keys to avoid passing them to next url_for.
678
662
  _route.segment_keys.each { |k| options.delete(k) }
679
- _url_helpers.send("#{name}_path", prefix_options)
663
+ _url_helpers.public_send("#{name}_path", prefix_options)
680
664
  end
681
665
 
682
666
  app.routes.define_mounted_helper(name, script_namer)
@@ -736,6 +720,14 @@ module ActionDispatch
736
720
  map_method(:delete, args, &block)
737
721
  end
738
722
 
723
+ # Define a route that only recognizes HTTP OPTIONS.
724
+ # For supported arguments, see match[rdoc-ref:Base#match]
725
+ #
726
+ # options 'carrots', to: 'food#carrots'
727
+ def options(*args, &block)
728
+ map_method(:options, args, &block)
729
+ end
730
+
739
731
  private
740
732
  def map_method(method, args, &block)
741
733
  options = args.extract_options!
@@ -934,7 +926,7 @@ module ActionDispatch
934
926
  # namespace :admin, as: "sekret" do
935
927
  # resources :posts
936
928
  # end
937
- def namespace(path, options = {})
929
+ def namespace(path, options = {}, &block)
938
930
  path = path.to_s
939
931
 
940
932
  defaults = {
@@ -945,7 +937,7 @@ module ActionDispatch
945
937
  }
946
938
 
947
939
  path_scope(options.delete(:path) { path }) do
948
- scope(defaults.merge!(options)) { yield }
940
+ scope(defaults.merge!(options), &block)
949
941
  end
950
942
  end
951
943
 
@@ -983,7 +975,7 @@ module ActionDispatch
983
975
  #
984
976
  # Requests to routes can be constrained based on specific criteria:
985
977
  #
986
- # constraints(-> (req) { req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do
978
+ # constraints(-> (req) { /iPhone/.match?(req.env["HTTP_USER_AGENT"]) }) do
987
979
  # resources :iphones
988
980
  # end
989
981
  #
@@ -993,7 +985,7 @@ module ActionDispatch
993
985
  #
994
986
  # class Iphone
995
987
  # def self.matches?(request)
996
- # request.env["HTTP_USER_AGENT"] =~ /iPhone/
988
+ # /iPhone/.match?(request.env["HTTP_USER_AGENT"])
997
989
  # end
998
990
  # end
999
991
  #
@@ -1004,8 +996,8 @@ module ActionDispatch
1004
996
  # constraints(Iphone) do
1005
997
  # resources :iphones
1006
998
  # end
1007
- def constraints(constraints = {})
1008
- scope(constraints: constraints) { yield }
999
+ def constraints(constraints = {}, &block)
1000
+ scope(constraints: constraints, &block)
1009
1001
  end
1010
1002
 
1011
1003
  # Allows you to set default parameters for a route, such as this:
@@ -1134,10 +1126,14 @@ module ActionDispatch
1134
1126
  RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
1135
1127
  CANONICAL_ACTIONS = %w(index create new show update destroy)
1136
1128
 
1137
- class Resource #:nodoc:
1129
+ class Resource # :nodoc:
1138
1130
  attr_reader :controller, :path, :param
1139
1131
 
1140
1132
  def initialize(entities, api_only, shallow, options = {})
1133
+ if options[:param].to_s.include?(":")
1134
+ raise ArgumentError, ":param option can't contain colons"
1135
+ end
1136
+
1141
1137
  @name = entities.to_s
1142
1138
  @path = (options[:path] || @name).to_s
1143
1139
  @controller = (options[:controller] || @name).to_s
@@ -1159,10 +1155,16 @@ module ActionDispatch
1159
1155
  end
1160
1156
 
1161
1157
  def actions
1158
+ if @except
1159
+ available_actions - Array(@except).map(&:to_sym)
1160
+ else
1161
+ available_actions
1162
+ end
1163
+ end
1164
+
1165
+ def available_actions
1162
1166
  if @only
1163
1167
  Array(@only).map(&:to_sym)
1164
- elsif @except
1165
- default_actions - Array(@except).map(&:to_sym)
1166
1168
  else
1167
1169
  default_actions
1168
1170
  end
@@ -1219,7 +1221,7 @@ module ActionDispatch
1219
1221
  def singleton?; false; end
1220
1222
  end
1221
1223
 
1222
- class SingletonResource < Resource #:nodoc:
1224
+ class SingletonResource < Resource # :nodoc:
1223
1225
  def initialize(entities, api_only, shallow, options)
1224
1226
  super
1225
1227
  @as = nil
@@ -1275,6 +1277,16 @@ module ActionDispatch
1275
1277
  # DELETE /profile
1276
1278
  # POST /profile
1277
1279
  #
1280
+ # If you want instances of a model to work with this resource via
1281
+ # record identification (e.g. in +form_with+ or +redirect_to+), you
1282
+ # will need to call resolve[rdoc-ref:CustomUrls#resolve]:
1283
+ #
1284
+ # resource :profile
1285
+ # resolve('Profile') { [:profile] }
1286
+ #
1287
+ # # Enables this to work with singular routes:
1288
+ # form_with(model: @profile) {}
1289
+ #
1278
1290
  # === Options
1279
1291
  # Takes same options as resources[rdoc-ref:#resources]
1280
1292
  def resource(*resources, &block)
@@ -1389,6 +1401,8 @@ module ActionDispatch
1389
1401
  # as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt>
1390
1402
  # to be shortened to just <tt>/comments/1234</tt>.
1391
1403
  #
1404
+ # Set <tt>shallow: false</tt> on a child resource to ignore a parent's shallow parameter.
1405
+ #
1392
1406
  # [:shallow_path]
1393
1407
  # Prefixes nested shallow routes with the specified path.
1394
1408
  #
@@ -1431,6 +1445,9 @@ module ActionDispatch
1431
1445
  # Allows you to specify the default value for optional +format+
1432
1446
  # segment or disable it by supplying +false+.
1433
1447
  #
1448
+ # [:param]
1449
+ # Allows you to override the default param name of +:id+ in the URL.
1450
+ #
1434
1451
  # === Examples
1435
1452
  #
1436
1453
  # # routes call <tt>Admin::PostsController</tt>
@@ -1480,15 +1497,13 @@ module ActionDispatch
1480
1497
  # with GET, and route to the search action of +PhotosController+. It will also
1481
1498
  # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
1482
1499
  # route helpers.
1483
- def collection
1500
+ def collection(&block)
1484
1501
  unless resource_scope?
1485
1502
  raise ArgumentError, "can't use collection outside resource(s) scope"
1486
1503
  end
1487
1504
 
1488
1505
  with_scope_level(:collection) do
1489
- path_scope(parent_resource.collection_scope) do
1490
- yield
1491
- end
1506
+ path_scope(parent_resource.collection_scope, &block)
1492
1507
  end
1493
1508
  end
1494
1509
 
@@ -1503,7 +1518,7 @@ module ActionDispatch
1503
1518
  # This will recognize <tt>/photos/1/preview</tt> with GET, and route to the
1504
1519
  # preview action of +PhotosController+. It will also create the
1505
1520
  # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
1506
- def member
1521
+ def member(&block)
1507
1522
  unless resource_scope?
1508
1523
  raise ArgumentError, "can't use member outside resource(s) scope"
1509
1524
  end
@@ -1511,27 +1526,25 @@ module ActionDispatch
1511
1526
  with_scope_level(:member) do
1512
1527
  if shallow?
1513
1528
  shallow_scope {
1514
- path_scope(parent_resource.member_scope) { yield }
1529
+ path_scope(parent_resource.member_scope, &block)
1515
1530
  }
1516
1531
  else
1517
- path_scope(parent_resource.member_scope) { yield }
1532
+ path_scope(parent_resource.member_scope, &block)
1518
1533
  end
1519
1534
  end
1520
1535
  end
1521
1536
 
1522
- def new
1537
+ def new(&block)
1523
1538
  unless resource_scope?
1524
1539
  raise ArgumentError, "can't use new outside resource(s) scope"
1525
1540
  end
1526
1541
 
1527
1542
  with_scope_level(:new) do
1528
- path_scope(parent_resource.new_scope(action_path(:new))) do
1529
- yield
1530
- end
1543
+ path_scope(parent_resource.new_scope(action_path(:new)), &block)
1531
1544
  end
1532
1545
  end
1533
1546
 
1534
- def nested
1547
+ def nested(&block)
1535
1548
  unless resource_scope?
1536
1549
  raise ArgumentError, "can't use nested outside resource(s) scope"
1537
1550
  end
@@ -1540,12 +1553,12 @@ module ActionDispatch
1540
1553
  if shallow? && shallow_nesting_depth >= 1
1541
1554
  shallow_scope do
1542
1555
  path_scope(parent_resource.nested_scope) do
1543
- scope(nested_options) { yield }
1556
+ scope(nested_options, &block)
1544
1557
  end
1545
1558
  end
1546
1559
  else
1547
1560
  path_scope(parent_resource.nested_scope) do
1548
- scope(nested_options) { yield }
1561
+ scope(nested_options, &block)
1549
1562
  end
1550
1563
  end
1551
1564
  end
@@ -1571,6 +1584,22 @@ module ActionDispatch
1571
1584
  !parent_resource.singleton? && @scope[:shallow]
1572
1585
  end
1573
1586
 
1587
+ def draw(name)
1588
+ path = @draw_paths.find do |_path|
1589
+ File.exist? "#{_path}/#{name}.rb"
1590
+ end
1591
+
1592
+ unless path
1593
+ msg = "Your router tried to #draw the external file #{name}.rb,\n" \
1594
+ "but the file was not found in:\n\n"
1595
+ msg += @draw_paths.map { |_path| " * #{_path}" }.join("\n")
1596
+ raise ArgumentError, msg
1597
+ end
1598
+
1599
+ route_path = "#{path}/#{name}.rb"
1600
+ instance_eval(File.read(route_path), route_path.to_s)
1601
+ end
1602
+
1574
1603
  # Matches a URL pattern to one or more routes.
1575
1604
  # For more information, see match[rdoc-ref:Base#match].
1576
1605
  #
@@ -1588,7 +1617,7 @@ module ActionDispatch
1588
1617
  when Symbol
1589
1618
  options[:action] = to
1590
1619
  when String
1591
- if to =~ /#/
1620
+ if /#/.match?(to)
1592
1621
  options[:to] = to
1593
1622
  else
1594
1623
  options[:controller] = to
@@ -1645,26 +1674,26 @@ module ActionDispatch
1645
1674
  end
1646
1675
 
1647
1676
  private
1648
-
1649
1677
  def parent_resource
1650
1678
  @scope[:scope_level_resource]
1651
1679
  end
1652
1680
 
1653
1681
  def apply_common_behavior_for(method, resources, options, &block)
1654
1682
  if resources.length > 1
1655
- resources.each { |r| send(method, r, options, &block) }
1683
+ resources.each { |r| public_send(method, r, options, &block) }
1656
1684
  return true
1657
1685
  end
1658
1686
 
1659
- if options.delete(:shallow)
1687
+ if options[:shallow]
1688
+ options.delete(:shallow)
1660
1689
  shallow do
1661
- send(method, resources.pop, options, &block)
1690
+ public_send(method, resources.pop, options, &block)
1662
1691
  end
1663
1692
  return true
1664
1693
  end
1665
1694
 
1666
1695
  if resource_scope?
1667
- nested { send(method, resources.pop, options, &block) }
1696
+ nested { public_send(method, resources.pop, options, &block) }
1668
1697
  return true
1669
1698
  end
1670
1699
 
@@ -1675,7 +1704,7 @@ module ActionDispatch
1675
1704
  scope_options = options.slice!(*RESOURCE_OPTIONS)
1676
1705
  unless scope_options.empty?
1677
1706
  scope(scope_options) do
1678
- send(method, resources.pop, options, &block)
1707
+ public_send(method, resources.pop, options, &block)
1679
1708
  end
1680
1709
  return true
1681
1710
  end
@@ -1715,10 +1744,10 @@ module ActionDispatch
1715
1744
  @scope = @scope.parent
1716
1745
  end
1717
1746
 
1718
- def resource_scope(resource)
1747
+ def resource_scope(resource, &block)
1719
1748
  @scope = @scope.new(scope_level_resource: resource)
1720
1749
 
1721
- controller(resource.resource_scope) { yield }
1750
+ controller(resource.resource_scope, &block)
1722
1751
  ensure
1723
1752
  @scope = @scope.parent
1724
1753
  end
@@ -1805,7 +1834,7 @@ module ActionDispatch
1805
1834
  # and return nil in case it isn't. Otherwise, we pass the invalid name
1806
1835
  # forward so the underlying router engine treats it and raises an exception.
1807
1836
  if as.nil?
1808
- candidate unless candidate !~ /\A[_a-z]/i || has_named_route?(candidate)
1837
+ candidate unless !candidate.match?(/\A[_a-z]/i) || has_named_route?(candidate)
1809
1838
  else
1810
1839
  candidate
1811
1840
  end
@@ -1836,7 +1865,7 @@ module ActionDispatch
1836
1865
  end
1837
1866
 
1838
1867
  def map_match(paths, options)
1839
- if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
1868
+ if (on = options[:on]) && !VALID_ON_OPTIONS.include?(on)
1840
1869
  raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
1841
1870
  end
1842
1871
 
@@ -1859,7 +1888,7 @@ module ActionDispatch
1859
1888
  options_constraints = options.delete(:constraints) || {}
1860
1889
 
1861
1890
  path_types = paths.group_by(&:class)
1862
- path_types.fetch(String, []).each do |_path|
1891
+ (path_types[String] || []).each do |_path|
1863
1892
  route_options = options.dup
1864
1893
  if _path && option_path
1865
1894
  raise ArgumentError, "Ambiguous route definition. Both :path and the route path were specified as strings."
@@ -1868,7 +1897,7 @@ module ActionDispatch
1868
1897
  decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
1869
1898
  end
1870
1899
 
1871
- path_types.fetch(Symbol, []).each do |action|
1900
+ (path_types[Symbol] || []).each do |action|
1872
1901
  route_options = options.dup
1873
1902
  decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints)
1874
1903
  end
@@ -1881,14 +1910,14 @@ module ActionDispatch
1881
1910
 
1882
1911
  path_without_format = path.sub(/\(\.:format\)$/, "")
1883
1912
  if using_match_shorthand?(path_without_format)
1884
- path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1').tr("-", "_")
1913
+ path_without_format.delete_prefix("/").sub(%r{/([^/]*)$}, '#\1').tr("-", "_")
1885
1914
  else
1886
1915
  nil
1887
1916
  end
1888
1917
  end
1889
1918
 
1890
1919
  def using_match_shorthand?(path)
1891
- path =~ %r{^/?[-\w]+/[-\w/]+$}
1920
+ %r{^/?[-\w]+/[-\w/]+$}.match?(path)
1892
1921
  end
1893
1922
 
1894
1923
  def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
@@ -1914,7 +1943,7 @@ module ActionDispatch
1914
1943
 
1915
1944
  default_action = options.delete(:action) || @scope[:action]
1916
1945
 
1917
- if action =~ /^[\w\-\/]+$/
1946
+ if /^[\w\-\/]+$/.match?(action)
1918
1947
  default_action ||= action.tr("-", "_") unless action.include?("/")
1919
1948
  else
1920
1949
  action = nil
@@ -1926,7 +1955,7 @@ module ActionDispatch
1926
1955
  name_for_action(options.delete(:as), action)
1927
1956
  end
1928
1957
 
1929
- path = Mapping.normalize_path URI.parser.escape(path), formatted
1958
+ path = Mapping.normalize_path URI::DEFAULT_PARSER.escape(path), formatted
1930
1959
  ast = Journey::Parser.parse path
1931
1960
 
1932
1961
  mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
@@ -1934,9 +1963,7 @@ module ActionDispatch
1934
1963
  end
1935
1964
 
1936
1965
  def match_root_route(options)
1937
- name = has_named_route?(name_for_action(:root, nil)) ? nil : :root
1938
- args = ["/", { as: name, via: :get }.merge!(options)]
1939
-
1966
+ args = ["/", { as: :root, via: :get }.merge(options)]
1940
1967
  match(*args)
1941
1968
  end
1942
1969
  end
@@ -2052,7 +2079,7 @@ module ActionDispatch
2052
2079
  # of routing helpers, e.g:
2053
2080
  #
2054
2081
  # direct :homepage do
2055
- # "http://www.rubyonrails.org"
2082
+ # "https://rubyonrails.org"
2056
2083
  # end
2057
2084
  #
2058
2085
  # direct :commentable do |model|
@@ -2249,8 +2276,9 @@ module ActionDispatch
2249
2276
  NULL = Scope.new(nil, nil)
2250
2277
  end
2251
2278
 
2252
- def initialize(set) #:nodoc:
2279
+ def initialize(set) # :nodoc:
2253
2280
  @set = set
2281
+ @draw_paths = set.draw_paths
2254
2282
  @scope = Scope.new(path_names: @set.resources_path_names)
2255
2283
  @concerns = {}
2256
2284
  end