actionpack 7.1.5.1 → 8.1.2

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.
Files changed (177) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +308 -523
  3. data/README.rdoc +1 -1
  4. data/lib/abstract_controller/asset_paths.rb +6 -2
  5. data/lib/abstract_controller/base.rb +104 -105
  6. data/lib/abstract_controller/caching/fragments.rb +50 -53
  7. data/lib/abstract_controller/caching.rb +8 -3
  8. data/lib/abstract_controller/callbacks.rb +70 -62
  9. data/lib/abstract_controller/collector.rb +7 -7
  10. data/lib/abstract_controller/deprecator.rb +2 -0
  11. data/lib/abstract_controller/error.rb +2 -0
  12. data/lib/abstract_controller/helpers.rb +71 -84
  13. data/lib/abstract_controller/logger.rb +4 -1
  14. data/lib/abstract_controller/railties/routes_helpers.rb +2 -0
  15. data/lib/abstract_controller/rendering.rb +13 -13
  16. data/lib/abstract_controller/translation.rb +12 -13
  17. data/lib/abstract_controller/url_for.rb +8 -6
  18. data/lib/abstract_controller.rb +2 -0
  19. data/lib/action_controller/api/api_rendering.rb +2 -0
  20. data/lib/action_controller/api.rb +76 -72
  21. data/lib/action_controller/base.rb +199 -126
  22. data/lib/action_controller/caching.rb +16 -14
  23. data/lib/action_controller/deprecator.rb +2 -0
  24. data/lib/action_controller/form_builder.rb +21 -18
  25. data/lib/action_controller/log_subscriber.rb +23 -2
  26. data/lib/action_controller/metal/allow_browser.rb +133 -0
  27. data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
  28. data/lib/action_controller/metal/conditional_get.rb +217 -175
  29. data/lib/action_controller/metal/content_security_policy.rb +25 -24
  30. data/lib/action_controller/metal/cookies.rb +4 -2
  31. data/lib/action_controller/metal/data_streaming.rb +72 -63
  32. data/lib/action_controller/metal/default_headers.rb +5 -3
  33. data/lib/action_controller/metal/etag_with_flash.rb +3 -1
  34. data/lib/action_controller/metal/etag_with_template_digest.rb +17 -15
  35. data/lib/action_controller/metal/exceptions.rb +16 -9
  36. data/lib/action_controller/metal/flash.rb +13 -14
  37. data/lib/action_controller/metal/head.rb +15 -11
  38. data/lib/action_controller/metal/helpers.rb +63 -55
  39. data/lib/action_controller/metal/http_authentication.rb +209 -201
  40. data/lib/action_controller/metal/implicit_render.rb +17 -15
  41. data/lib/action_controller/metal/instrumentation.rb +16 -14
  42. data/lib/action_controller/metal/live.rb +177 -128
  43. data/lib/action_controller/metal/logging.rb +6 -4
  44. data/lib/action_controller/metal/mime_responds.rb +151 -142
  45. data/lib/action_controller/metal/parameter_encoding.rb +34 -32
  46. data/lib/action_controller/metal/params_wrapper.rb +57 -59
  47. data/lib/action_controller/metal/permissions_policy.rb +22 -12
  48. data/lib/action_controller/metal/rate_limiting.rb +92 -0
  49. data/lib/action_controller/metal/redirecting.rb +213 -94
  50. data/lib/action_controller/metal/renderers.rb +78 -57
  51. data/lib/action_controller/metal/rendering.rb +111 -77
  52. data/lib/action_controller/metal/request_forgery_protection.rb +182 -143
  53. data/lib/action_controller/metal/rescue.rb +20 -9
  54. data/lib/action_controller/metal/streaming.rb +118 -195
  55. data/lib/action_controller/metal/strong_parameters.rb +720 -530
  56. data/lib/action_controller/metal/testing.rb +2 -0
  57. data/lib/action_controller/metal/url_for.rb +17 -15
  58. data/lib/action_controller/metal.rb +86 -60
  59. data/lib/action_controller/railtie.rb +36 -15
  60. data/lib/action_controller/railties/helpers.rb +2 -0
  61. data/lib/action_controller/renderer.rb +41 -36
  62. data/lib/action_controller/structured_event_subscriber.rb +116 -0
  63. data/lib/action_controller/template_assertions.rb +4 -2
  64. data/lib/action_controller/test_case.rb +160 -131
  65. data/lib/action_controller.rb +5 -1
  66. data/lib/action_dispatch/constants.rb +8 -0
  67. data/lib/action_dispatch/deprecator.rb +2 -0
  68. data/lib/action_dispatch/http/cache.rb +163 -35
  69. data/lib/action_dispatch/http/content_disposition.rb +2 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +54 -39
  71. data/lib/action_dispatch/http/filter_parameters.rb +14 -8
  72. data/lib/action_dispatch/http/filter_redirect.rb +22 -1
  73. data/lib/action_dispatch/http/headers.rb +22 -22
  74. data/lib/action_dispatch/http/mime_negotiation.rb +89 -41
  75. data/lib/action_dispatch/http/mime_type.rb +25 -21
  76. data/lib/action_dispatch/http/mime_types.rb +3 -0
  77. data/lib/action_dispatch/http/param_builder.rb +187 -0
  78. data/lib/action_dispatch/http/param_error.rb +26 -0
  79. data/lib/action_dispatch/http/parameters.rb +14 -12
  80. data/lib/action_dispatch/http/permissions_policy.rb +25 -36
  81. data/lib/action_dispatch/http/query_parser.rb +55 -0
  82. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  83. data/lib/action_dispatch/http/request.rb +141 -92
  84. data/lib/action_dispatch/http/response.rb +137 -77
  85. data/lib/action_dispatch/http/upload.rb +18 -16
  86. data/lib/action_dispatch/http/url.rb +187 -89
  87. data/lib/action_dispatch/journey/formatter.rb +21 -9
  88. data/lib/action_dispatch/journey/gtg/builder.rb +4 -3
  89. data/lib/action_dispatch/journey/gtg/simulator.rb +34 -11
  90. data/lib/action_dispatch/journey/gtg/transition_table.rb +47 -53
  91. data/lib/action_dispatch/journey/nfa/dot.rb +2 -0
  92. data/lib/action_dispatch/journey/nodes/node.rb +8 -6
  93. data/lib/action_dispatch/journey/parser.rb +99 -195
  94. data/lib/action_dispatch/journey/path/pattern.rb +4 -1
  95. data/lib/action_dispatch/journey/route.rb +54 -38
  96. data/lib/action_dispatch/journey/router/utils.rb +22 -27
  97. data/lib/action_dispatch/journey/router.rb +63 -83
  98. data/lib/action_dispatch/journey/routes.rb +11 -2
  99. data/lib/action_dispatch/journey/scanner.rb +46 -42
  100. data/lib/action_dispatch/journey/visitors.rb +57 -23
  101. data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
  102. data/lib/action_dispatch/journey.rb +2 -0
  103. data/lib/action_dispatch/log_subscriber.rb +7 -1
  104. data/lib/action_dispatch/middleware/actionable_exceptions.rb +2 -0
  105. data/lib/action_dispatch/middleware/assume_ssl.rb +8 -5
  106. data/lib/action_dispatch/middleware/callbacks.rb +3 -1
  107. data/lib/action_dispatch/middleware/cookies.rb +125 -106
  108. data/lib/action_dispatch/middleware/debug_exceptions.rb +37 -8
  109. data/lib/action_dispatch/middleware/debug_locks.rb +15 -13
  110. data/lib/action_dispatch/middleware/debug_view.rb +13 -5
  111. data/lib/action_dispatch/middleware/exception_wrapper.rb +18 -23
  112. data/lib/action_dispatch/middleware/executor.rb +19 -4
  113. data/lib/action_dispatch/middleware/flash.rb +63 -51
  114. data/lib/action_dispatch/middleware/host_authorization.rb +17 -15
  115. data/lib/action_dispatch/middleware/public_exceptions.rb +14 -12
  116. data/lib/action_dispatch/middleware/reloader.rb +5 -3
  117. data/lib/action_dispatch/middleware/remote_ip.rb +87 -77
  118. data/lib/action_dispatch/middleware/request_id.rb +16 -10
  119. data/lib/action_dispatch/middleware/server_timing.rb +4 -2
  120. data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -0
  121. data/lib/action_dispatch/middleware/session/cache_store.rb +30 -8
  122. data/lib/action_dispatch/middleware/session/cookie_store.rb +27 -26
  123. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +7 -3
  124. data/lib/action_dispatch/middleware/show_exceptions.rb +16 -16
  125. data/lib/action_dispatch/middleware/ssl.rb +53 -40
  126. data/lib/action_dispatch/middleware/stack.rb +11 -10
  127. data/lib/action_dispatch/middleware/static.rb +33 -31
  128. data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -5
  130. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
  131. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
  139. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
  140. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
  141. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +1 -1
  142. data/lib/action_dispatch/railtie.rb +23 -3
  143. data/lib/action_dispatch/request/session.rb +24 -21
  144. data/lib/action_dispatch/request/utils.rb +11 -3
  145. data/lib/action_dispatch/routing/endpoint.rb +2 -0
  146. data/lib/action_dispatch/routing/inspector.rb +85 -60
  147. data/lib/action_dispatch/routing/mapper.rb +1031 -851
  148. data/lib/action_dispatch/routing/polymorphic_routes.rb +69 -62
  149. data/lib/action_dispatch/routing/redirection.rb +47 -39
  150. data/lib/action_dispatch/routing/route_set.rb +79 -56
  151. data/lib/action_dispatch/routing/routes_proxy.rb +7 -4
  152. data/lib/action_dispatch/routing/url_for.rb +130 -125
  153. data/lib/action_dispatch/routing.rb +150 -148
  154. data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
  155. data/lib/action_dispatch/system_test_case.rb +91 -81
  156. data/lib/action_dispatch/system_testing/browser.rb +16 -23
  157. data/lib/action_dispatch/system_testing/driver.rb +2 -0
  158. data/lib/action_dispatch/system_testing/server.rb +2 -0
  159. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +34 -23
  160. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +2 -0
  161. data/lib/action_dispatch/testing/assertion_response.rb +9 -7
  162. data/lib/action_dispatch/testing/assertions/response.rb +52 -25
  163. data/lib/action_dispatch/testing/assertions/routing.rb +168 -87
  164. data/lib/action_dispatch/testing/assertions.rb +2 -0
  165. data/lib/action_dispatch/testing/integration.rb +233 -223
  166. data/lib/action_dispatch/testing/request_encoder.rb +11 -9
  167. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  168. data/lib/action_dispatch/testing/test_process.rb +11 -8
  169. data/lib/action_dispatch/testing/test_request.rb +3 -1
  170. data/lib/action_dispatch/testing/test_response.rb +27 -26
  171. data/lib/action_dispatch.rb +36 -32
  172. data/lib/action_pack/gem_version.rb +6 -4
  173. data/lib/action_pack/version.rb +3 -1
  174. data/lib/action_pack.rb +17 -16
  175. metadata +36 -32
  176. data/lib/action_dispatch/journey/parser.y +0 -50
  177. data/lib/action_dispatch/journey/parser_extras.rb +0 -31
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionDispatch
4
6
  # :stopdoc:
5
7
  module Journey
@@ -36,29 +38,50 @@ module ActionDispatch
36
38
  def self.verb; ""; end
37
39
  end
38
40
 
41
+ class Or
42
+ attr_reader :verb
43
+
44
+ def initialize(verbs)
45
+ @verbs = verbs
46
+ @verb = @verbs.map(&:verb).join("|")
47
+ end
48
+
49
+ def call(req)
50
+ @verbs.any? { |v| v.call req }
51
+ end
52
+ end
53
+
39
54
  VERB_TO_CLASS = VERBS.each_with_object(all: All) do |verb, hash|
40
55
  klass = const_get verb
41
56
  hash[verb] = klass
42
57
  hash[verb.downcase] = klass
43
58
  hash[verb.downcase.to_sym] = klass
44
59
  end
45
- end
46
60
 
47
- def self.verb_matcher(verb)
48
- VerbMatchers::VERB_TO_CLASS.fetch(verb) do
61
+ VERB_TO_CLASS.default_proc = proc do |_, verb|
49
62
  VerbMatchers::Unknown.new verb.to_s.dasherize.upcase
50
63
  end
64
+
65
+ def self.for(verbs)
66
+ if verbs.any? { |v| VERB_TO_CLASS[v] == All }
67
+ All
68
+ elsif verbs.one?
69
+ VERB_TO_CLASS[verbs.first]
70
+ else
71
+ Or.new(verbs.map { |v| VERB_TO_CLASS[v] })
72
+ end
73
+ end
51
74
  end
52
75
 
53
76
  ##
54
77
  # +path+ is a path constraint.
55
- # +constraints+ is a hash of constraints to be applied to this route.
56
- def initialize(name:, app: nil, path:, constraints: {}, required_defaults: [], defaults: {}, request_method_match: nil, precedence: 0, scope_options: {}, internal: false, source_location: nil)
78
+ # `constraints` is a hash of constraints to be applied to this route.
79
+ def initialize(name:, app: nil, path:, constraints: {}, required_defaults: [], defaults: {}, via: nil, precedence: 0, scope_options: {}, internal: false, source_location: nil)
57
80
  @name = name
58
81
  @app = app
59
82
  @path = path
60
83
 
61
- @request_method_match = request_method_match
84
+ @request_method_match = via && VerbMatchers.for(via)
62
85
  @constraints = constraints
63
86
  @defaults = defaults
64
87
  @required_defaults = nil
@@ -82,14 +105,14 @@ module ActionDispatch
82
105
  nil
83
106
  end
84
107
 
85
- # Needed for `bin/rails routes`. Picks up succinctly defined requirements
86
- # for a route, for example route
108
+ # Needed for `bin/rails routes`. Picks up succinctly defined requirements for a
109
+ # route, for example route
87
110
  #
88
- # get 'photo/:id', :controller => 'photos', :action => 'show',
89
- # :id => /[A-Z]\d{5}/
111
+ # get 'photo/:id', :controller => 'photos', :action => 'show',
112
+ # :id => /[A-Z]\d{5}/
90
113
  #
91
- # will have {:controller=>"photos", :action=>"show", :id=>/[A-Z]\d{5}/}
92
- # as requirements.
114
+ # will have {:controller=>"photos", :action=>"show", :[id=>/](A-Z){5}/} as
115
+ # requirements.
93
116
  def requirements
94
117
  @defaults.merge(path.requirements).delete_if { |_, v|
95
118
  /.+?/m == v
@@ -144,21 +167,23 @@ module ActionDispatch
144
167
  end
145
168
 
146
169
  def matches?(request)
147
- match_verb(request) &&
148
- constraints.all? { |method, value|
149
- case value
150
- when Regexp, String
151
- value === request.send(method).to_s
152
- when Array
153
- value.include?(request.send(method))
154
- when TrueClass
155
- request.send(method).present?
156
- when FalseClass
157
- request.send(method).blank?
158
- else
159
- value === request.send(method)
160
- end
161
- }
170
+ @request_method_match.call(request) && (
171
+ constraints.empty? ||
172
+ constraints.all? { |method, value|
173
+ case value
174
+ when Regexp, String
175
+ value === request.send(method).to_s
176
+ when Array
177
+ value.include?(request.send(method))
178
+ when TrueClass
179
+ request.send(method).present?
180
+ when FalseClass
181
+ request.send(method).blank?
182
+ else
183
+ value === request.send(method)
184
+ end
185
+ }
186
+ )
162
187
  end
163
188
 
164
189
  def ip
@@ -166,21 +191,12 @@ module ActionDispatch
166
191
  end
167
192
 
168
193
  def requires_matching_verb?
169
- !@request_method_match.all? { |x| x == VerbMatchers::All }
194
+ @request_method_match != VerbMatchers::All
170
195
  end
171
196
 
172
197
  def verb
173
- verbs.join("|")
198
+ @request_method_match.verb
174
199
  end
175
-
176
- private
177
- def verbs
178
- @request_method_match.map(&:verb)
179
- end
180
-
181
- def match_verb(request)
182
- @request_method_match.any? { |m| m.call request }
183
- end
184
200
  end
185
201
  end
186
202
  # :startdoc:
@@ -1,21 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionDispatch
4
6
  module Journey # :nodoc:
5
7
  class Router # :nodoc:
6
8
  class Utils # :nodoc:
7
9
  # Normalizes URI path.
8
10
  #
9
- # Strips off trailing slash and ensures there is a leading slash.
10
- # Also converts downcase URL encoded string to uppercase.
11
+ # Strips off trailing slash and ensures there is a leading slash. Also converts
12
+ # downcase URL encoded string to uppercase.
11
13
  #
12
- # normalize_path("/foo") # => "/foo"
13
- # normalize_path("/foo/") # => "/foo"
14
- # normalize_path("foo") # => "/foo"
15
- # normalize_path("") # => "/"
16
- # normalize_path("/%ab") # => "/%AB"
14
+ # normalize_path("/foo") # => "/foo"
15
+ # normalize_path("/foo/") # => "/foo"
16
+ # normalize_path("foo") # => "/foo"
17
+ # normalize_path("") # => "/"
18
+ # normalize_path("/%ab") # => "/%AB"
17
19
  def self.normalize_path(path)
18
- path ||= ""
20
+ return "/".dup unless path
21
+
22
+ # Fast path for the overwhelming majority of paths that don't need to be normalized
23
+ if path == "/" || (path.start_with?("/") && !path.end_with?("/") && !path.match?(%r{%|//}))
24
+ return path.dup
25
+ end
26
+
27
+ # Slow path
19
28
  encoding = path.encoding
20
29
  path = +"/#{path}"
21
30
  path.squeeze!("/")
@@ -28,8 +37,7 @@ module ActionDispatch
28
37
  path.force_encoding(encoding)
29
38
  end
30
39
 
31
- # URI path and fragment escaping
32
- # https://tools.ietf.org/html/rfc3986
40
+ # URI path and fragment escaping https://tools.ietf.org/html/rfc3986
33
41
  class UriEncoder # :nodoc:
34
42
  ENCODE = "%%%02X"
35
43
  US_ASCII = Encoding::US_ASCII
@@ -42,11 +50,11 @@ module ActionDispatch
42
50
  UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~"
43
51
  SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;="
44
52
 
45
- ESCAPED = /%[a-zA-Z0-9]{2}/.freeze
53
+ ESCAPED = /%[a-zA-Z0-9]{2}/
46
54
 
47
- FRAGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/?]/.freeze
48
- SEGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@]/.freeze
49
- PATH = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/]/.freeze
55
+ FRAGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/?]/
56
+ SEGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@]/
57
+ PATH = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/]/
50
58
 
51
59
  def escape_fragment(fragment)
52
60
  escape(fragment, FRAGMENT)
@@ -60,11 +68,6 @@ module ActionDispatch
60
68
  escape(segment, SEGMENT)
61
69
  end
62
70
 
63
- def unescape_uri(uri)
64
- encoding = uri.encoding == US_ASCII ? UTF_8 : uri.encoding
65
- uri.gsub(ESCAPED) { |match| [match[1, 2].hex].pack("C") }.force_encoding(encoding)
66
- end
67
-
68
71
  private
69
72
  def escape(component, pattern)
70
73
  component.gsub(pattern) { |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII)
@@ -90,14 +93,6 @@ module ActionDispatch
90
93
  def self.escape_fragment(fragment)
91
94
  ENCODER.escape_fragment(fragment.to_s)
92
95
  end
93
-
94
- # Replaces any escaped sequences with their unescaped representations.
95
- #
96
- # uri = "/topics?title=Ruby%20on%20Rails"
97
- # unescape_uri(uri) #=> "/topics?title=Ruby on Rails"
98
- def self.unescape_uri(uri)
99
- ENCODER.unescape_uri(uri)
100
- end
101
96
  end
102
97
  end
103
98
  end
@@ -1,14 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
5
+ require "cgi/escape"
6
+ require "cgi/util" if RUBY_VERSION < "3.5"
3
7
  require "action_dispatch/journey/router/utils"
4
8
  require "action_dispatch/journey/routes"
5
9
  require "action_dispatch/journey/formatter"
6
-
7
- before = $-w
8
- $-w = false
9
10
  require "action_dispatch/journey/parser"
10
- $-w = before
11
-
12
11
  require "action_dispatch/journey/route"
13
12
  require "action_dispatch/journey/path/pattern"
14
13
 
@@ -22,78 +21,85 @@ module ActionDispatch
22
21
  end
23
22
 
24
23
  def eager_load!
25
- # Eagerly trigger the simulator's initialization so
26
- # it doesn't happen during a request cycle.
24
+ # Eagerly trigger the simulator's initialization so it doesn't happen during a
25
+ # request cycle.
27
26
  simulator
28
27
  nil
29
28
  end
30
29
 
31
30
  def serve(req)
32
- find_routes(req) do |match, parameters, route|
33
- set_params = req.path_parameters
34
- path_info = req.path_info
35
- script_name = req.script_name
36
-
37
- unless route.path.anchored
38
- req.script_name = (script_name.to_s + match.to_s).chomp("/")
39
- req.path_info = match.post_match
40
- req.path_info = "/" + req.path_info unless req.path_info.start_with? "/"
41
- end
42
-
43
- tmp_params = set_params.merge route.defaults
44
- parameters.each_pair { |key, val|
45
- tmp_params[key] = val.force_encoding(::Encoding::UTF_8)
46
- }
47
-
48
- req.path_parameters = tmp_params
49
- req.route_uri_pattern = route.path.spec.to_s
31
+ recognize(req) do |route, parameters|
32
+ req.path_parameters = parameters
33
+ req.route = route
50
34
 
51
35
  _, headers, _ = response = route.app.serve(req)
52
36
 
53
- if "pass" == headers[Constants::X_CASCADE]
54
- req.script_name = script_name
55
- req.path_info = path_info
56
- req.path_parameters = set_params
57
- next
58
- end
59
-
60
- return response
37
+ return response unless headers[Constants::X_CASCADE] == "pass"
61
38
  end
62
39
 
63
40
  [404, { Constants::X_CASCADE => "pass" }, ["Not Found"]]
64
41
  end
65
42
 
66
- def recognize(rails_req)
67
- find_routes(rails_req) do |match, parameters, route|
68
- unless route.path.anchored
69
- rails_req.script_name = match.to_s
70
- rails_req.path_info = match.post_match
71
- rails_req.path_info = "/" + rails_req.path_info unless rails_req.path_info.start_with? "/"
72
- end
43
+ def recognize(req, &block)
44
+ req_params = req.path_parameters
45
+ path_info = req.path_info
46
+ script_name = req.script_name
73
47
 
74
- parameters = route.defaults.merge parameters
75
- yield(route, parameters)
76
- end
77
- end
48
+ routes = filter_routes(path_info)
78
49
 
79
- def visualizer
80
- tt = GTG::Builder.new(ast).transition_table
81
- groups = partitioned_routes.first.map(&:ast).group_by(&:to_s)
82
- asts = groups.values.map(&:first)
83
- tt.visualizer(asts)
84
- end
50
+ custom_routes.each { |r|
51
+ routes << r if r.path.match?(path_info)
52
+ }
85
53
 
86
- private
87
- def partitioned_routes
88
- routes.partition { |r|
89
- r.path.anchored && r.path.requirements_anchored?
90
- }
54
+ if req.head?
55
+ routes = match_head_routes(routes, req)
56
+ else
57
+ routes.select! { |r| r.matches?(req) }
58
+ end
59
+
60
+ if routes.size > 1
61
+ routes.sort! do |a, b|
62
+ a.precedence <=> b.precedence
63
+ end
91
64
  end
92
65
 
93
- def ast
94
- routes.ast
66
+ routes.each do |r|
67
+ match_data = r.path.match(path_info)
68
+
69
+ path_parameters = req_params.merge r.defaults
70
+
71
+ index = 1
72
+ match_data.names.each do |name|
73
+ if val = match_data[index]
74
+ val = if val.include?("%")
75
+ CGI.unescapeURIComponent(val)
76
+ else
77
+ val
78
+ end
79
+ val.force_encoding(::Encoding::UTF_8)
80
+ path_parameters[name.to_sym] = val
81
+ end
82
+ index += 1
83
+ end
84
+
85
+ if r.path.anchored
86
+ yield(r, path_parameters)
87
+ else
88
+ req.script_name = (script_name.to_s + match_data.to_s).chomp("/")
89
+ req.path_info = match_data.post_match
90
+ req.path_info = "/" + req.path_info unless req.path_info.start_with? "/"
91
+
92
+ yield(r, path_parameters)
93
+
94
+ req.script_name = script_name
95
+ req.path_info = path_info
96
+ end
97
+
98
+ req.path_parameters = req_params
95
99
  end
100
+ end
96
101
 
102
+ private
97
103
  def simulator
98
104
  routes.simulator
99
105
  end
@@ -103,35 +109,9 @@ module ActionDispatch
103
109
  end
104
110
 
105
111
  def filter_routes(path)
106
- return [] unless ast
107
112
  simulator.memos(path) { [] }
108
113
  end
109
114
 
110
- def find_routes(req)
111
- path_info = req.path_info
112
- routes = filter_routes(path_info).concat custom_routes.find_all { |r|
113
- r.path.match?(path_info)
114
- }
115
-
116
- if req.head?
117
- routes = match_head_routes(routes, req)
118
- else
119
- routes.select! { |r| r.matches?(req) }
120
- end
121
-
122
- routes.sort_by!(&:precedence)
123
-
124
- routes.each { |r|
125
- match_data = r.path.match(path_info)
126
- path_parameters = {}
127
- match_data.names.each_with_index { |name, i|
128
- val = match_data[i + 1]
129
- path_parameters[name.to_sym] = Utils.unescape_uri(val) if val
130
- }
131
- yield [match_data, path_parameters, r]
132
- }
133
- end
134
-
135
115
  def match_head_routes(routes, req)
136
116
  head_routes = routes.select { |r| r.requires_matching_verb? && r.matches?(req) }
137
117
  return head_routes unless head_routes.empty?
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionDispatch
4
6
  module Journey # :nodoc:
5
- # The Routing table. Contains all routes for a system. Routes can be
6
- # added to the table by calling Routes#add_route.
7
+ # The Routing table. Contains all routes for a system. Routes can be added to
8
+ # the table by calling Routes#add_route.
7
9
  class Routes # :nodoc:
8
10
  include Enumerable
9
11
 
@@ -70,6 +72,13 @@ module ActionDispatch
70
72
  route
71
73
  end
72
74
 
75
+ def visualizer
76
+ tt = GTG::Builder.new(ast).transition_table
77
+ groups = anchored_routes.map(&:ast).group_by(&:to_s)
78
+ asts = groups.values.map(&:first)
79
+ tt.visualizer(asts)
80
+ end
81
+
73
82
  private
74
83
  def clear_cache!
75
84
  @ast = nil
@@ -1,70 +1,74 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  require "strscan"
4
6
 
5
7
  module ActionDispatch
6
8
  module Journey # :nodoc:
7
9
  class Scanner # :nodoc:
10
+ STATIC_TOKENS = Array.new(150)
11
+ STATIC_TOKENS[".".ord] = :DOT
12
+ STATIC_TOKENS["/".ord] = :SLASH
13
+ STATIC_TOKENS["(".ord] = :LPAREN
14
+ STATIC_TOKENS[")".ord] = :RPAREN
15
+ STATIC_TOKENS["|".ord] = :OR
16
+ STATIC_TOKENS[":".ord] = :SYMBOL
17
+ STATIC_TOKENS["*".ord] = :STAR
18
+ STATIC_TOKENS.freeze
19
+
20
+ class Scanner < StringScanner
21
+ unless method_defined?(:peek_byte) # https://github.com/ruby/strscan/pull/89
22
+ def peek_byte
23
+ string.getbyte(pos)
24
+ end
25
+ end
26
+ end
27
+
8
28
  def initialize
9
- @ss = nil
29
+ @scanner = nil
30
+ @length = nil
10
31
  end
11
32
 
12
33
  def scan_setup(str)
13
- @ss = StringScanner.new(str)
34
+ @scanner = Scanner.new(str)
14
35
  end
15
36
 
16
- def eos?
17
- @ss.eos?
18
- end
37
+ def next_token
38
+ return if @scanner.eos?
19
39
 
20
- def pos
21
- @ss.pos
40
+ until token = scan || @scanner.eos?; end
41
+ token
22
42
  end
23
43
 
24
- def pre_match
25
- @ss.pre_match
44
+ def last_string
45
+ -@scanner.string.byteslice(@scanner.pos - @length, @length)
26
46
  end
27
47
 
28
- def next_token
29
- return if @ss.eos?
30
-
31
- until token = scan || @ss.eos?; end
32
- token
48
+ def last_literal
49
+ last_str = @scanner.string.byteslice(@scanner.pos - @length, @length)
50
+ last_str.tr! "\\", ""
51
+ -last_str
33
52
  end
34
53
 
35
54
  private
36
- # takes advantage of String @- deduping capabilities in Ruby 2.5 upwards
37
- # see: https://bugs.ruby-lang.org/issues/13077
38
- def dedup_scan(regex)
39
- r = @ss.scan(regex)
40
- r ? -r : nil
41
- end
42
-
43
55
  def scan
56
+ next_byte = @scanner.peek_byte
44
57
  case
45
- # /
46
- when @ss.skip(/\//)
47
- [:SLASH, "/"]
48
- when @ss.skip(/\(/)
49
- [:LPAREN, "("]
50
- when @ss.skip(/\)/)
51
- [:RPAREN, ")"]
52
- when @ss.skip(/\|/)
53
- [:OR, "|"]
54
- when @ss.skip(/\./)
55
- [:DOT, "."]
56
- when text = dedup_scan(/:\w+/)
57
- [:SYMBOL, text]
58
- when text = dedup_scan(/\*\w+/)
59
- [:STAR, text]
60
- when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\[:()])+/)
61
- text.tr! "\\", ""
62
- [:LITERAL, -text]
63
- # any char
64
- when text = dedup_scan(/./)
65
- [:LITERAL, text]
58
+ when (token = STATIC_TOKENS[next_byte]) && (token != :SYMBOL || next_byte_is_not_a_token?)
59
+ @scanner.pos += 1
60
+ @length = @scanner.skip(/\w+/).to_i + 1 if token == :SYMBOL || token == :STAR
61
+ token
62
+ when @length = @scanner.skip(/(?:[\w%\-~!$&'*+,;=@]|\\[:()])+/)
63
+ :LITERAL
64
+ when @length = @scanner.skip(/./)
65
+ :LITERAL
66
66
  end
67
67
  end
68
+
69
+ def next_byte_is_not_a_token?
70
+ !STATIC_TOKENS[@scanner.string.getbyte(@scanner.pos + 1)]
71
+ end
68
72
  end
69
73
  end
70
74
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionDispatch
4
6
  # :stopdoc:
5
7
  module Journey
@@ -126,8 +128,8 @@ module ActionDispatch
126
128
  def visit_DOT(n, seed); terminal(n, seed); end
127
129
 
128
130
  instance_methods(false).each do |pim|
129
- next unless pim =~ /^visit_(.*)$/
130
- DISPATCH_CACHE[$1.to_sym] = pim
131
+ next unless pim.start_with?("visit_")
132
+ DISPATCH_CACHE[pim.name.delete_prefix("visit_").to_sym] = pim
131
133
  end
132
134
  end
133
135
 
@@ -165,32 +167,64 @@ module ActionDispatch
165
167
  INSTANCE = new
166
168
  end
167
169
 
168
- class String < FunctionalVisitor # :nodoc:
169
- private
170
- def binary(node, seed)
171
- visit(node.right, visit(node.left, seed))
172
- end
173
-
174
- def nary(node, seed)
170
+ class String # :nodoc:
171
+ def accept(node, seed)
172
+ case node.type
173
+ when :DOT
174
+ seed << node.left
175
+ when :LITERAL
176
+ seed << node.left
177
+ when :SYMBOL
178
+ seed << node.left
179
+ when :SLASH
180
+ seed << node.left
181
+ when :CAT
182
+ accept(node.right, accept(node.left, seed))
183
+ when :STAR
184
+ accept(node.left, seed)
185
+ when :OR
175
186
  last_child = node.children.last
176
- node.children.inject(seed) { |s, c|
177
- string = visit(c, s)
178
- string << "|" unless last_child == c
179
- string
180
- }
181
- end
182
-
183
- def terminal(node, seed)
184
- seed + node.left
185
- end
186
-
187
- def visit_GROUP(node, seed)
188
- visit(node.left, seed.dup << "(") << ")"
187
+ node.children.each do |c|
188
+ accept(c, seed)
189
+ seed << "|" unless last_child == c
190
+ end
191
+ seed
192
+ when :GROUP
193
+ accept(node.left, seed << "(") << ")"
194
+ else
195
+ raise "Unknown node type: #{node.type}"
189
196
  end
197
+ end
190
198
 
191
- INSTANCE = new
199
+ INSTANCE = new
192
200
  end
193
201
 
202
+ # class String < FunctionalVisitor # :nodoc:
203
+ # private
204
+ # def binary(node, seed)
205
+ # visit(node.right, visit(node.left, seed))
206
+ # end
207
+ #
208
+ # def nary(node, seed)
209
+ # last_child = node.children.last
210
+ # node.children.inject(seed) { |s, c|
211
+ # string = visit(c, s)
212
+ # string << "|" unless last_child == c
213
+ # string
214
+ # }
215
+ # end
216
+ #
217
+ # def terminal(node, seed)
218
+ # seed + node.left
219
+ # end
220
+ #
221
+ # def visit_GROUP(node, seed)
222
+ # visit(node.left, seed.dup << "(") << ")"
223
+ # end
224
+ #
225
+ # INSTANCE = new
226
+ # end
227
+
194
228
  class Dot < FunctionalVisitor # :nodoc:
195
229
  def initialize
196
230
  @nodes = []