actionpack 6.0.5.1 → 6.1.0

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +248 -344
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/abstract_controller/base.rb +35 -2
  6. data/lib/abstract_controller/callbacks.rb +2 -2
  7. data/lib/abstract_controller/helpers.rb +105 -90
  8. data/lib/abstract_controller/rendering.rb +9 -9
  9. data/lib/abstract_controller/translation.rb +8 -2
  10. data/lib/abstract_controller.rb +1 -0
  11. data/lib/action_controller/api.rb +2 -2
  12. data/lib/action_controller/base.rb +4 -2
  13. data/lib/action_controller/caching.rb +0 -1
  14. data/lib/action_controller/log_subscriber.rb +3 -3
  15. data/lib/action_controller/metal/conditional_get.rb +10 -2
  16. data/lib/action_controller/metal/content_security_policy.rb +1 -1
  17. data/lib/action_controller/metal/cookies.rb +3 -1
  18. data/lib/action_controller/metal/data_streaming.rb +1 -1
  19. data/lib/action_controller/metal/etag_with_template_digest.rb +2 -4
  20. data/lib/action_controller/metal/exceptions.rb +33 -0
  21. data/lib/action_controller/metal/head.rb +7 -4
  22. data/lib/action_controller/metal/helpers.rb +11 -1
  23. data/lib/action_controller/metal/http_authentication.rb +5 -3
  24. data/lib/action_controller/metal/implicit_render.rb +1 -1
  25. data/lib/action_controller/metal/instrumentation.rb +11 -9
  26. data/lib/action_controller/metal/live.rb +1 -1
  27. data/lib/action_controller/metal/logging.rb +20 -0
  28. data/lib/action_controller/metal/mime_responds.rb +6 -2
  29. data/lib/action_controller/metal/parameter_encoding.rb +35 -4
  30. data/lib/action_controller/metal/params_wrapper.rb +16 -11
  31. data/lib/action_controller/metal/permissions_policy.rb +46 -0
  32. data/lib/action_controller/metal/redirecting.rb +1 -1
  33. data/lib/action_controller/metal/rendering.rb +6 -0
  34. data/lib/action_controller/metal/request_forgery_protection.rb +1 -1
  35. data/lib/action_controller/metal/rescue.rb +1 -1
  36. data/lib/action_controller/metal/strong_parameters.rb +103 -15
  37. data/lib/action_controller/metal.rb +2 -2
  38. data/lib/action_controller/renderer.rb +23 -13
  39. data/lib/action_controller/test_case.rb +62 -56
  40. data/lib/action_controller.rb +2 -3
  41. data/lib/action_dispatch/http/cache.rb +12 -10
  42. data/lib/action_dispatch/http/content_security_policy.rb +11 -0
  43. data/lib/action_dispatch/http/filter_parameters.rb +1 -1
  44. data/lib/action_dispatch/http/filter_redirect.rb +1 -1
  45. data/lib/action_dispatch/http/headers.rb +3 -2
  46. data/lib/action_dispatch/http/mime_negotiation.rb +14 -8
  47. data/lib/action_dispatch/http/mime_type.rb +29 -16
  48. data/lib/action_dispatch/http/parameters.rb +1 -19
  49. data/lib/action_dispatch/http/permissions_policy.rb +173 -0
  50. data/lib/action_dispatch/http/request.rb +24 -8
  51. data/lib/action_dispatch/http/response.rb +17 -16
  52. data/lib/action_dispatch/http/url.rb +3 -2
  53. data/lib/action_dispatch/journey/formatter.rb +53 -28
  54. data/lib/action_dispatch/journey/gtg/builder.rb +22 -36
  55. data/lib/action_dispatch/journey/gtg/simulator.rb +8 -7
  56. data/lib/action_dispatch/journey/gtg/transition_table.rb +6 -4
  57. data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
  58. data/lib/action_dispatch/journey/nodes/node.rb +4 -3
  59. data/lib/action_dispatch/journey/parser.rb +13 -13
  60. data/lib/action_dispatch/journey/parser.y +1 -1
  61. data/lib/action_dispatch/journey/path/pattern.rb +13 -18
  62. data/lib/action_dispatch/journey/route.rb +7 -18
  63. data/lib/action_dispatch/journey/router/utils.rb +6 -4
  64. data/lib/action_dispatch/journey/router.rb +26 -30
  65. data/lib/action_dispatch/journey.rb +0 -2
  66. data/lib/action_dispatch/middleware/actionable_exceptions.rb +1 -1
  67. data/lib/action_dispatch/middleware/cookies.rb +67 -32
  68. data/lib/action_dispatch/middleware/debug_exceptions.rb +8 -15
  69. data/lib/action_dispatch/middleware/debug_view.rb +1 -1
  70. data/lib/action_dispatch/middleware/exception_wrapper.rb +28 -16
  71. data/lib/action_dispatch/middleware/executor.rb +1 -1
  72. data/lib/action_dispatch/middleware/host_authorization.rb +35 -35
  73. data/lib/action_dispatch/middleware/remote_ip.rb +5 -4
  74. data/lib/action_dispatch/middleware/request_id.rb +4 -5
  75. data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -2
  76. data/lib/action_dispatch/middleware/session/cookie_store.rb +2 -2
  77. data/lib/action_dispatch/middleware/ssl.rb +9 -6
  78. data/lib/action_dispatch/middleware/stack.rb +18 -0
  79. data/lib/action_dispatch/middleware/static.rb +154 -93
  80. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  81. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -1
  82. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +1 -1
  83. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +2 -5
  84. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +2 -2
  85. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +2 -3
  86. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +100 -8
  87. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  88. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +12 -1
  89. data/lib/action_dispatch/railtie.rb +3 -2
  90. data/lib/action_dispatch/request/session.rb +2 -8
  91. data/lib/action_dispatch/request/utils.rb +26 -2
  92. data/lib/action_dispatch/routing/inspector.rb +8 -7
  93. data/lib/action_dispatch/routing/mapper.rb +102 -71
  94. data/lib/action_dispatch/routing/polymorphic_routes.rb +16 -19
  95. data/lib/action_dispatch/routing/redirection.rb +3 -3
  96. data/lib/action_dispatch/routing/route_set.rb +49 -41
  97. data/lib/action_dispatch/system_test_case.rb +29 -24
  98. data/lib/action_dispatch/system_testing/browser.rb +33 -27
  99. data/lib/action_dispatch/system_testing/driver.rb +6 -7
  100. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +47 -6
  101. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +4 -7
  102. data/lib/action_dispatch/testing/assertions/response.rb +2 -4
  103. data/lib/action_dispatch/testing/assertions/routing.rb +5 -5
  104. data/lib/action_dispatch/testing/assertions.rb +1 -1
  105. data/lib/action_dispatch/testing/integration.rb +38 -27
  106. data/lib/action_dispatch/testing/test_process.rb +29 -4
  107. data/lib/action_dispatch/testing/test_request.rb +3 -3
  108. data/lib/action_dispatch.rb +3 -2
  109. data/lib/action_pack/gem_version.rb +3 -3
  110. data/lib/action_pack.rb +1 -1
  111. metadata +21 -23
  112. data/lib/action_controller/metal/force_ssl.rb +0 -58
  113. data/lib/action_dispatch/http/parameter_filter.rb +0 -12
  114. data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
  115. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  116. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -119
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/deep_dup"
4
+
5
+ module ActionDispatch #:nodoc:
6
+ class PermissionsPolicy
7
+ class Middleware
8
+ CONTENT_TYPE = "Content-Type"
9
+ # The Feature-Policy header has been renamed to Permissions-Policy.
10
+ # The Permissions-Policy requires a different implementation and isn't
11
+ # yet supported by all browsers. To avoid having to rename this
12
+ # middleware in the future we use the new name for the middleware but
13
+ # keep the old header name and implementation for now.
14
+ POLICY = "Feature-Policy"
15
+
16
+ def initialize(app)
17
+ @app = app
18
+ end
19
+
20
+ def call(env)
21
+ request = ActionDispatch::Request.new(env)
22
+ _, headers, _ = response = @app.call(env)
23
+
24
+ return response unless html_response?(headers)
25
+ return response if policy_present?(headers)
26
+
27
+ if policy = request.permissions_policy
28
+ headers[POLICY] = policy.build(request.controller_instance)
29
+ end
30
+
31
+ if policy_empty?(policy)
32
+ headers.delete(POLICY)
33
+ end
34
+
35
+ response
36
+ end
37
+
38
+ private
39
+ def html_response?(headers)
40
+ if content_type = headers[CONTENT_TYPE]
41
+ /html/.match?(content_type)
42
+ end
43
+ end
44
+
45
+ def policy_present?(headers)
46
+ headers[POLICY]
47
+ end
48
+
49
+ def policy_empty?(policy)
50
+ policy&.directives&.empty?
51
+ end
52
+ end
53
+
54
+ module Request
55
+ POLICY = "action_dispatch.permissions_policy"
56
+
57
+ def permissions_policy
58
+ get_header(POLICY)
59
+ end
60
+
61
+ def permissions_policy=(policy)
62
+ set_header(POLICY, policy)
63
+ end
64
+ end
65
+
66
+ MAPPINGS = {
67
+ self: "'self'",
68
+ none: "'none'",
69
+ }.freeze
70
+
71
+ # List of available permissions can be found at
72
+ # https://github.com/w3c/webappsec-permissions-policy/blob/master/features.md#policy-controlled-features
73
+ DIRECTIVES = {
74
+ accelerometer: "accelerometer",
75
+ ambient_light_sensor: "ambient-light-sensor",
76
+ autoplay: "autoplay",
77
+ camera: "camera",
78
+ encrypted_media: "encrypted-media",
79
+ fullscreen: "fullscreen",
80
+ geolocation: "geolocation",
81
+ gyroscope: "gyroscope",
82
+ magnetometer: "magnetometer",
83
+ microphone: "microphone",
84
+ midi: "midi",
85
+ payment: "payment",
86
+ picture_in_picture: "picture-in-picture",
87
+ speaker: "speaker",
88
+ usb: "usb",
89
+ vibrate: "vibrate",
90
+ vr: "vr",
91
+ }.freeze
92
+
93
+ private_constant :MAPPINGS, :DIRECTIVES
94
+
95
+ attr_reader :directives
96
+
97
+ def initialize
98
+ @directives = {}
99
+ yield self if block_given?
100
+ end
101
+
102
+ def initialize_copy(other)
103
+ @directives = other.directives.deep_dup
104
+ end
105
+
106
+ DIRECTIVES.each do |name, directive|
107
+ define_method(name) do |*sources|
108
+ if sources.first
109
+ @directives[directive] = apply_mappings(sources)
110
+ else
111
+ @directives.delete(directive)
112
+ end
113
+ end
114
+ end
115
+
116
+ def build(context = nil)
117
+ build_directives(context).compact.join("; ")
118
+ end
119
+
120
+ private
121
+ def apply_mappings(sources)
122
+ sources.map do |source|
123
+ case source
124
+ when Symbol
125
+ apply_mapping(source)
126
+ when String, Proc
127
+ source
128
+ else
129
+ raise ArgumentError, "Invalid HTTP permissions policy source: #{source.inspect}"
130
+ end
131
+ end
132
+ end
133
+
134
+ def apply_mapping(source)
135
+ MAPPINGS.fetch(source) do
136
+ raise ArgumentError, "Unknown HTTP permissions policy source mapping: #{source.inspect}"
137
+ end
138
+ end
139
+
140
+ def build_directives(context)
141
+ @directives.map do |directive, sources|
142
+ if sources.is_a?(Array)
143
+ "#{directive} #{build_directive(sources, context).join(' ')}"
144
+ elsif sources
145
+ directive
146
+ else
147
+ nil
148
+ end
149
+ end
150
+ end
151
+
152
+ def build_directive(sources, context)
153
+ sources.map { |source| resolve_source(source, context) }
154
+ end
155
+
156
+ def resolve_source(source, context)
157
+ case source
158
+ when String
159
+ source
160
+ when Symbol
161
+ source.to_s
162
+ when Proc
163
+ if context.nil?
164
+ raise RuntimeError, "Missing context for the dynamic permissions policy source: #{source.inspect}"
165
+ else
166
+ context.instance_exec(&source)
167
+ end
168
+ else
169
+ raise RuntimeError, "Unexpected permissions policy source: #{source.inspect}"
170
+ end
171
+ end
172
+ end
173
+ end
@@ -23,6 +23,7 @@ module ActionDispatch
23
23
  include ActionDispatch::Http::FilterParameters
24
24
  include ActionDispatch::Http::URL
25
25
  include ActionDispatch::ContentSecurityPolicy::Request
26
+ include ActionDispatch::PermissionsPolicy::Request
26
27
  include Rack::Request::Env
27
28
 
28
29
  autoload :Session, "action_dispatch/request/session"
@@ -44,11 +45,14 @@ module ActionDispatch
44
45
  SERVER_ADDR
45
46
  ].freeze
46
47
 
48
+ # TODO: Remove SERVER_ADDR when we remove support to Rack 2.1.
49
+ # See https://github.com/rack/rack/commit/c173b188d81ee437b588c1e046a1c9f031dea550
47
50
  ENV_METHODS.each do |env|
48
51
  class_eval <<-METHOD, __FILE__, __LINE__ + 1
49
- def #{env.sub(/^HTTP_/n, '').downcase} # def accept_charset
50
- get_header "#{env}".freeze # get_header "HTTP_ACCEPT_CHARSET".freeze
51
- end # end
52
+ # frozen_string_literal: true
53
+ def #{env.delete_prefix("HTTP_").downcase} # def accept_charset
54
+ get_header "#{env}" # get_header "HTTP_ACCEPT_CHARSET"
55
+ end # end
52
56
  METHOD
53
57
  end
54
58
 
@@ -72,7 +76,7 @@ module ActionDispatch
72
76
  PASS_NOT_FOUND = Class.new { # :nodoc:
73
77
  def self.action(_); self; end
74
78
  def self.call(_); [404, { "X-Cascade" => "pass" }, []]; end
75
- def self.binary_params_for?(action); false; end
79
+ def self.action_encoding_template(action); false; end
76
80
  }
77
81
 
78
82
  def controller_class
@@ -84,7 +88,7 @@ module ActionDispatch
84
88
  def controller_class_for(name)
85
89
  if name
86
90
  controller_param = name.underscore
87
- const_name = "#{controller_param.camelize}Controller"
91
+ const_name = controller_param.camelize << "Controller"
88
92
  begin
89
93
  ActiveSupport::Dependencies.constantize(const_name)
90
94
  rescue NameError => error
@@ -274,7 +278,7 @@ module ActionDispatch
274
278
  # (case-insensitive), which may need to be manually added depending on the
275
279
  # choice of JavaScript libraries and frameworks.
276
280
  def xml_http_request?
277
- get_header("HTTP_X_REQUESTED_WITH") =~ /XMLHttpRequest/i
281
+ /XMLHttpRequest/i.match?(get_header("HTTP_X_REQUESTED_WITH"))
278
282
  end
279
283
  alias :xhr? :xml_http_request?
280
284
 
@@ -290,6 +294,7 @@ module ActionDispatch
290
294
  end
291
295
 
292
296
  def remote_ip=(remote_ip)
297
+ @remote_ip = nil
293
298
  set_header "action_dispatch.remote_ip", remote_ip
294
299
  end
295
300
 
@@ -331,7 +336,7 @@ module ActionDispatch
331
336
  # variable is already set, wrap it in a StringIO.
332
337
  def body
333
338
  if raw_post = get_header("RAW_POST_DATA")
334
- raw_post = raw_post.dup.force_encoding(Encoding::BINARY)
339
+ raw_post = (+raw_post).force_encoding(Encoding::BINARY)
335
340
  StringIO.new(raw_post)
336
341
  else
337
342
  body_stream
@@ -376,6 +381,9 @@ module ActionDispatch
376
381
  def GET
377
382
  fetch_header("action_dispatch.request.query_parameters") do |k|
378
383
  rack_query_params = super || {}
384
+ controller = path_parameters[:controller]
385
+ action = path_parameters[:action]
386
+ rack_query_params = Request::Utils.set_binary_encoding(self, rack_query_params, controller, action)
379
387
  # Check for non UTF-8 parameter values, which would cause errors later
380
388
  Request::Utils.check_param_encoding(rack_query_params)
381
389
  set_header k, Request::Utils.normalize_encode_params(rack_query_params)
@@ -391,6 +399,8 @@ module ActionDispatch
391
399
  pr = parse_formatted_parameters(params_parsers) do |params|
392
400
  super || {}
393
401
  end
402
+ pr = Request::Utils.set_binary_encoding(self, pr, path_parameters[:controller], path_parameters[:action])
403
+ Request::Utils.check_param_encoding(pr)
394
404
  self.request_parameters = Request::Utils.normalize_encode_params(pr)
395
405
  end
396
406
  rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
@@ -409,7 +419,7 @@ module ActionDispatch
409
419
 
410
420
  # True if the request came from localhost, 127.0.0.1, or ::1.
411
421
  def local?
412
- LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip
422
+ LOCALHOST.match?(remote_addr) && LOCALHOST.match?(remote_ip)
413
423
  end
414
424
 
415
425
  def request_parameters=(params)
@@ -428,6 +438,10 @@ module ActionDispatch
428
438
  super || scheme == "wss"
429
439
  end
430
440
 
441
+ def inspect # :nodoc:
442
+ "#<#{self.class.name} #{method} #{original_url.dump} for #{remote_ip}>"
443
+ end
444
+
431
445
  private
432
446
  def check_method(name)
433
447
  HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}")
@@ -435,3 +449,5 @@ module ActionDispatch
435
449
  end
436
450
  end
437
451
  end
452
+
453
+ ActiveSupport.run_load_hooks :action_dispatch_request, ActionDispatch::Request
@@ -81,11 +81,22 @@ module ActionDispatch # :nodoc:
81
81
  CONTENT_TYPE = "Content-Type"
82
82
  SET_COOKIE = "Set-Cookie"
83
83
  LOCATION = "Location"
84
- NO_CONTENT_CODES = [100, 101, 102, 204, 205, 304]
84
+ NO_CONTENT_CODES = [100, 101, 102, 103, 204, 205, 304]
85
85
 
86
86
  cattr_accessor :default_charset, default: "utf-8"
87
87
  cattr_accessor :default_headers
88
- cattr_accessor :return_only_media_type_on_content_type, default: false
88
+
89
+ def self.return_only_media_type_on_content_type=(*)
90
+ ActiveSupport::Deprecation.warn(
91
+ ".return_only_media_type_on_content_type= is dreprecated with no replacement and will be removed in 6.2."
92
+ )
93
+ end
94
+
95
+ def self.return_only_media_type_on_content_type
96
+ ActiveSupport::Deprecation.warn(
97
+ ".return_only_media_type_on_content_type is dreprecated with no replacement and will be removed in 6.2."
98
+ )
99
+ end
89
100
 
90
101
  include Rack::Response::Helpers
91
102
  # Aliasing these off because AD::Http::Cache::Response defines them.
@@ -243,17 +254,7 @@ module ActionDispatch # :nodoc:
243
254
 
244
255
  # Content type of response.
245
256
  def content_type
246
- if self.class.return_only_media_type_on_content_type
247
- ActiveSupport::Deprecation.warn(
248
- "Rails 6.1 will return Content-Type header without modification." \
249
- " If you want just the MIME type, please use `#media_type` instead."
250
- )
251
-
252
- content_type = super
253
- content_type ? content_type.split(/;\s*charset=/)[0].presence : content_type
254
- else
255
- super.presence
256
- end
257
+ super.presence
257
258
  end
258
259
 
259
260
  # Media type of response.
@@ -442,8 +443,8 @@ module ActionDispatch # :nodoc:
442
443
  end
443
444
 
444
445
  def set_content_type(content_type, charset)
445
- type = (content_type || "").dup
446
- type << "; charset=#{charset.to_s.downcase}" if charset
446
+ type = content_type || ""
447
+ type = "#{type}; charset=#{charset.to_s.downcase}" if charset
447
448
  set_header CONTENT_TYPE, type
448
449
  end
449
450
 
@@ -503,7 +504,7 @@ module ActionDispatch # :nodoc:
503
504
  end
504
505
 
505
506
  def respond_to?(method, include_private = false)
506
- if method.to_s == "to_path"
507
+ if method.to_sym == :to_path
507
508
  @response.stream.respond_to?(method)
508
509
  else
509
510
  super
@@ -9,6 +9,7 @@ module ActionDispatch
9
9
  HOST_REGEXP = /(^[^:]+:\/\/)?(\[[^\]]+\]|[^:]+)(?::(\d+$))?/
10
10
  PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/
11
11
 
12
+ mattr_accessor :secure_protocol, default: false
12
13
  mattr_accessor :tld_length, default: 1
13
14
 
14
15
  class << self
@@ -133,13 +134,13 @@ module ActionDispatch
133
134
  end
134
135
 
135
136
  def named_host?(host)
136
- IP_HOST_REGEXP !~ host
137
+ !IP_HOST_REGEXP.match?(host)
137
138
  end
138
139
 
139
140
  def normalize_protocol(protocol)
140
141
  case protocol
141
142
  when nil
142
- "http://"
143
+ secure_protocol ? "https://" : "http://"
143
144
  when false, "//"
144
145
  "//"
145
146
  when PROTOCOL_REGEXP
@@ -15,12 +15,53 @@ module ActionDispatch
15
15
  @cache = nil
16
16
  end
17
17
 
18
- def generate(name, options, path_parameters, parameterize = nil)
18
+ class RouteWithParams
19
+ attr_reader :params
20
+
21
+ def initialize(route, parameterized_parts, params)
22
+ @route = route
23
+ @parameterized_parts = parameterized_parts
24
+ @params = params
25
+ end
26
+
27
+ def path(_)
28
+ @route.format(@parameterized_parts)
29
+ end
30
+ end
31
+
32
+ class MissingRoute
33
+ attr_reader :routes, :name, :constraints, :missing_keys, :unmatched_keys
34
+
35
+ def initialize(constraints, missing_keys, unmatched_keys, routes, name)
36
+ @constraints = constraints
37
+ @missing_keys = missing_keys
38
+ @unmatched_keys = unmatched_keys
39
+ @routes = routes
40
+ @name = name
41
+ end
42
+
43
+ def path(method_name)
44
+ raise ActionController::UrlGenerationError.new(message, routes, name, method_name)
45
+ end
46
+
47
+ def params
48
+ path("unknown")
49
+ end
50
+
51
+ def message
52
+ message = +"No route matches #{Hash[constraints.sort_by { |k, v| k.to_s }].inspect}"
53
+ message << ", missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty?
54
+ message << ", possible unmatched constraints: #{unmatched_keys.sort.inspect}" if unmatched_keys && !unmatched_keys.empty?
55
+ message
56
+ end
57
+ end
58
+
59
+ def generate(name, options, path_parameters)
19
60
  constraints = path_parameters.merge(options)
20
61
  missing_keys = nil
21
62
 
22
63
  match_route(name, constraints) do |route|
23
- parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)
64
+ parameterized_parts = extract_parameterized_parts(route, options, path_parameters)
24
65
 
25
66
  # Skip this route unless a name has been provided or it is a
26
67
  # standard Rails route since we can't determine whether an options
@@ -44,17 +85,13 @@ module ActionDispatch
44
85
  parameterized_parts.delete(key)
45
86
  end
46
87
 
47
- return [route.format(parameterized_parts), params]
88
+ return RouteWithParams.new(route, parameterized_parts, params)
48
89
  end
49
90
 
50
91
  unmatched_keys = (missing_keys || []) & constraints.keys
51
92
  missing_keys = (missing_keys || []) - unmatched_keys
52
93
 
53
- message = +"No route matches #{Hash[constraints.sort_by { |k, v| k.to_s }].inspect}"
54
- message << ", missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty?
55
- message << ", possible unmatched constraints: #{unmatched_keys.sort.inspect}" if unmatched_keys && !unmatched_keys.empty?
56
-
57
- raise ActionController::UrlGenerationError, message
94
+ MissingRoute.new(constraints, missing_keys, unmatched_keys, routes, name)
58
95
  end
59
96
 
60
97
  def clear
@@ -62,7 +99,7 @@ module ActionDispatch
62
99
  end
63
100
 
64
101
  private
65
- def extract_parameterized_parts(route, options, recall, parameterize = nil)
102
+ def extract_parameterized_parts(route, options, recall)
66
103
  parameterized_parts = recall.merge(options)
67
104
 
68
105
  keys_to_keep = route.parts.reverse_each.drop_while { |part|
@@ -73,9 +110,11 @@ module ActionDispatch
73
110
  !keys_to_keep.include?(bad_key)
74
111
  end
75
112
 
76
- if parameterize
77
- parameterized_parts.each do |k, v|
78
- parameterized_parts[k] = parameterize.call(k, v)
113
+ parameterized_parts.each do |k, v|
114
+ if k == :controller
115
+ parameterized_parts[k] = v
116
+ else
117
+ parameterized_parts[k] = v.to_param
79
118
  end
80
119
  end
81
120
 
@@ -125,19 +164,10 @@ module ActionDispatch
125
164
  routes
126
165
  end
127
166
 
128
- module RegexCaseComparator
129
- DEFAULT_INPUT = /[-_.a-zA-Z0-9]+\/[-_.a-zA-Z0-9]+/
130
- DEFAULT_REGEX = /\A#{DEFAULT_INPUT}\Z/
131
-
132
- def self.===(regex)
133
- DEFAULT_INPUT == regex
134
- end
135
- end
136
-
137
167
  # Returns an array populated with missing keys if any are present.
138
168
  def missing_keys(route, parts)
139
169
  missing_keys = nil
140
- tests = route.path.requirements
170
+ tests = route.path.requirements_for_missing_keys_check
141
171
  route.required_parts.each { |key|
142
172
  case tests[key]
143
173
  when nil
@@ -145,13 +175,8 @@ module ActionDispatch
145
175
  missing_keys ||= []
146
176
  missing_keys << key
147
177
  end
148
- when RegexCaseComparator
149
- unless RegexCaseComparator::DEFAULT_REGEX === parts[key]
150
- missing_keys ||= []
151
- missing_keys << key
152
- end
153
178
  else
154
- unless /\A#{tests[key]}\Z/ === parts[key]
179
+ unless tests[key].match?(parts[key])
155
180
  missing_keys ||= []
156
181
  missing_keys << key
157
182
  end
@@ -13,45 +13,44 @@ module ActionDispatch
13
13
  def initialize(root)
14
14
  @root = root
15
15
  @ast = Nodes::Cat.new root, DUMMY
16
- @followpos = nil
16
+ @followpos = build_followpos
17
17
  end
18
18
 
19
19
  def transition_table
20
20
  dtrans = TransitionTable.new
21
- marked = {}
22
- state_id = Hash.new { |h, k| h[k] = h.length }
21
+ marked = {}.compare_by_identity
22
+ state_id = Hash.new { |h, k| h[k] = h.length }.compare_by_identity
23
+ dstates = [firstpos(root)]
23
24
 
24
- start = firstpos(root)
25
- dstates = [start]
26
25
  until dstates.empty?
27
26
  s = dstates.shift
28
27
  next if marked[s]
29
28
  marked[s] = true # mark s
30
29
 
31
30
  s.group_by { |state| symbol(state) }.each do |sym, ps|
32
- u = ps.flat_map { |l| followpos(l) }
31
+ u = ps.flat_map { |l| @followpos[l] }
33
32
  next if u.empty?
34
33
 
35
- if u.uniq == [DUMMY]
36
- from = state_id[s]
37
- to = state_id[Object.new]
38
- dtrans[from, to] = sym
34
+ from = state_id[s]
39
35
 
36
+ if u.all? { |pos| pos == DUMMY }
37
+ to = state_id[Object.new]
38
+ dtrans[from, to] = sym
40
39
  dtrans.add_accepting(to)
40
+
41
41
  ps.each { |state| dtrans.add_memo(to, state.memo) }
42
42
  else
43
- dtrans[state_id[s], state_id[u]] = sym
43
+ to = state_id[u]
44
+ dtrans[from, to] = sym
44
45
 
45
46
  if u.include?(DUMMY)
46
- to = state_id[u]
47
-
48
- accepting = ps.find_all { |l| followpos(l).include?(DUMMY) }
49
-
50
- accepting.each { |accepting_state|
51
- dtrans.add_memo(to, accepting_state.memo)
52
- }
47
+ ps.each do |state|
48
+ if @followpos[state].include?(DUMMY)
49
+ dtrans.add_memo(to, state.memo)
50
+ end
51
+ end
53
52
 
54
- dtrans.add_accepting(state_id[u])
53
+ dtrans.add_accepting(to)
55
54
  end
56
55
  end
57
56
 
@@ -92,7 +91,7 @@ module ActionDispatch
92
91
  firstpos(node.left)
93
92
  end
94
93
  when Nodes::Or
95
- node.children.flat_map { |c| firstpos(c) }.uniq
94
+ node.children.flat_map { |c| firstpos(c) }.tap(&:uniq!)
96
95
  when Nodes::Unary
97
96
  firstpos(node.left)
98
97
  when Nodes::Terminal
@@ -107,7 +106,7 @@ module ActionDispatch
107
106
  when Nodes::Star
108
107
  firstpos(node.left)
109
108
  when Nodes::Or
110
- node.children.flat_map { |c| lastpos(c) }.uniq
109
+ node.children.flat_map { |c| lastpos(c) }.tap(&:uniq!)
111
110
  when Nodes::Cat
112
111
  if nullable?(node.right)
113
112
  lastpos(node.left) | lastpos(node.right)
@@ -123,17 +122,9 @@ module ActionDispatch
123
122
  end
124
123
  end
125
124
 
126
- def followpos(node)
127
- followpos_table[node]
128
- end
129
-
130
125
  private
131
- def followpos_table
132
- @followpos ||= build_followpos
133
- end
134
-
135
126
  def build_followpos
136
- table = Hash.new { |h, k| h[k] = [] }
127
+ table = Hash.new { |h, k| h[k] = [] }.compare_by_identity
137
128
  @ast.each do |n|
138
129
  case n
139
130
  when Nodes::Cat
@@ -150,12 +141,7 @@ module ActionDispatch
150
141
  end
151
142
 
152
143
  def symbol(edge)
153
- case edge
154
- when Journey::Nodes::Symbol
155
- edge.regexp
156
- else
157
- edge.left
158
- end
144
+ edge.symbol? ? edge.regexp : edge.left
159
145
  end
160
146
  end
161
147
  end
@@ -14,6 +14,8 @@ module ActionDispatch
14
14
  end
15
15
 
16
16
  class Simulator # :nodoc:
17
+ INITIAL_STATE = [0].freeze
18
+
17
19
  attr_reader :tt
18
20
 
19
21
  def initialize(transition_table)
@@ -22,18 +24,17 @@ module ActionDispatch
22
24
 
23
25
  def memos(string)
24
26
  input = StringScanner.new(string)
25
- state = [0]
27
+ state = INITIAL_STATE
28
+
26
29
  while sym = input.scan(%r([/.?]|[^/.?]+))
27
30
  state = tt.move(state, sym)
28
31
  end
29
32
 
30
- acceptance_states = state.find_all { |s|
31
- tt.accepting? s
32
- }
33
-
34
- return yield if acceptance_states.empty?
33
+ acceptance_states = state.each_with_object([]) do |s, memos|
34
+ memos.concat(tt.memo(s)) if tt.accepting?(s)
35
+ end
35
36
 
36
- acceptance_states.flat_map { |x| tt.memo(x) }.compact
37
+ acceptance_states.empty? ? yield : acceptance_states
37
38
  end
38
39
  end
39
40
  end
@@ -45,16 +45,18 @@ module ActionDispatch
45
45
  return [] if t.empty?
46
46
 
47
47
  regexps = []
48
+ strings = []
48
49
 
49
- t.map { |s|
50
+ t.each { |s|
50
51
  if states = @regexp_states[s]
51
- regexps.concat states.map { |re, v| re === a ? v : nil }
52
+ states.each { |re, v| regexps << v if re.match?(a) && !v.nil? }
52
53
  end
53
54
 
54
55
  if states = @string_states[s]
55
- states[a]
56
+ strings << states[a] unless states[a].nil?
56
57
  end
57
- }.compact.concat regexps
58
+ }
59
+ strings.concat regexps
58
60
  end
59
61
 
60
62
  def as_json(options = nil)