actionpack 7.2.3 → 8.1.3

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +394 -119
  3. data/lib/abstract_controller/asset_paths.rb +4 -2
  4. data/lib/abstract_controller/base.rb +11 -5
  5. data/lib/abstract_controller/caching.rb +6 -3
  6. data/lib/abstract_controller/callbacks.rb +6 -0
  7. data/lib/abstract_controller/logger.rb +2 -1
  8. data/lib/abstract_controller/rendering.rb +0 -1
  9. data/lib/action_controller/api.rb +1 -0
  10. data/lib/action_controller/base.rb +3 -2
  11. data/lib/action_controller/caching.rb +1 -2
  12. data/lib/action_controller/form_builder.rb +4 -4
  13. data/lib/action_controller/log_subscriber.rb +22 -3
  14. data/lib/action_controller/metal/allow_browser.rb +12 -2
  15. data/lib/action_controller/metal/conditional_get.rb +30 -1
  16. data/lib/action_controller/metal/data_streaming.rb +5 -5
  17. data/lib/action_controller/metal/exceptions.rb +5 -0
  18. data/lib/action_controller/metal/flash.rb +1 -4
  19. data/lib/action_controller/metal/head.rb +3 -1
  20. data/lib/action_controller/metal/instrumentation.rb +1 -2
  21. data/lib/action_controller/metal/live.rb +65 -25
  22. data/lib/action_controller/metal/permissions_policy.rb +9 -0
  23. data/lib/action_controller/metal/rate_limiting.rb +39 -9
  24. data/lib/action_controller/metal/redirecting.rb +105 -13
  25. data/lib/action_controller/metal/renderers.rb +29 -9
  26. data/lib/action_controller/metal/rendering.rb +7 -1
  27. data/lib/action_controller/metal/request_forgery_protection.rb +18 -10
  28. data/lib/action_controller/metal/rescue.rb +9 -0
  29. data/lib/action_controller/metal/streaming.rb +5 -84
  30. data/lib/action_controller/metal/strong_parameters.rb +277 -89
  31. data/lib/action_controller/railtie.rb +33 -15
  32. data/lib/action_controller/structured_event_subscriber.rb +116 -0
  33. data/lib/action_controller/test_case.rb +12 -2
  34. data/lib/action_dispatch/http/cache.rb +138 -11
  35. data/lib/action_dispatch/http/content_security_policy.rb +14 -1
  36. data/lib/action_dispatch/http/filter_parameters.rb +5 -3
  37. data/lib/action_dispatch/http/mime_negotiation.rb +55 -1
  38. data/lib/action_dispatch/http/mime_types.rb +1 -0
  39. data/lib/action_dispatch/http/param_builder.rb +187 -0
  40. data/lib/action_dispatch/http/param_error.rb +26 -0
  41. data/lib/action_dispatch/http/parameters.rb +3 -3
  42. data/lib/action_dispatch/http/permissions_policy.rb +6 -0
  43. data/lib/action_dispatch/http/query_parser.rb +55 -0
  44. data/lib/action_dispatch/http/request.rb +70 -21
  45. data/lib/action_dispatch/http/response.rb +50 -16
  46. data/lib/action_dispatch/http/url.rb +110 -14
  47. data/lib/action_dispatch/journey/gtg/simulator.rb +33 -12
  48. data/lib/action_dispatch/journey/gtg/transition_table.rb +33 -41
  49. data/lib/action_dispatch/journey/nodes/node.rb +2 -1
  50. data/lib/action_dispatch/journey/parser.rb +99 -196
  51. data/lib/action_dispatch/journey/route.rb +45 -31
  52. data/lib/action_dispatch/journey/router/utils.rb +8 -14
  53. data/lib/action_dispatch/journey/router.rb +59 -81
  54. data/lib/action_dispatch/journey/routes.rb +7 -0
  55. data/lib/action_dispatch/journey/scanner.rb +44 -42
  56. data/lib/action_dispatch/journey/visitors.rb +55 -23
  57. data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
  58. data/lib/action_dispatch/log_subscriber.rb +7 -3
  59. data/lib/action_dispatch/middleware/cookies.rb +8 -4
  60. data/lib/action_dispatch/middleware/debug_exceptions.rb +24 -5
  61. data/lib/action_dispatch/middleware/debug_view.rb +11 -5
  62. data/lib/action_dispatch/middleware/exception_wrapper.rb +11 -11
  63. data/lib/action_dispatch/middleware/executor.rb +12 -2
  64. data/lib/action_dispatch/middleware/public_exceptions.rb +1 -5
  65. data/lib/action_dispatch/middleware/remote_ip.rb +11 -5
  66. data/lib/action_dispatch/middleware/request_id.rb +2 -1
  67. data/lib/action_dispatch/middleware/session/cache_store.rb +17 -0
  68. data/lib/action_dispatch/middleware/ssl.rb +13 -3
  69. data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
  70. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -5
  71. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
  72. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  73. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
  74. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -0
  75. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
  76. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
  77. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
  78. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
  79. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
  80. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
  81. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
  82. data/lib/action_dispatch/railtie.rb +21 -0
  83. data/lib/action_dispatch/request/session.rb +1 -0
  84. data/lib/action_dispatch/request/utils.rb +9 -3
  85. data/lib/action_dispatch/routing/inspector.rb +80 -57
  86. data/lib/action_dispatch/routing/mapper.rb +404 -223
  87. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -2
  88. data/lib/action_dispatch/routing/redirection.rb +10 -7
  89. data/lib/action_dispatch/routing/route_set.rb +21 -12
  90. data/lib/action_dispatch/routing/routes_proxy.rb +1 -0
  91. data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
  92. data/lib/action_dispatch/system_test_case.rb +3 -3
  93. data/lib/action_dispatch/system_testing/browser.rb +12 -21
  94. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +2 -2
  95. data/lib/action_dispatch/testing/assertions/response.rb +26 -2
  96. data/lib/action_dispatch/testing/assertions/routing.rb +27 -15
  97. data/lib/action_dispatch/testing/integration.rb +18 -7
  98. data/lib/action_dispatch.rb +14 -4
  99. data/lib/action_pack/gem_version.rb +2 -2
  100. metadata +18 -48
  101. data/lib/action_dispatch/journey/parser.y +0 -50
  102. data/lib/action_dispatch/journey/parser_extras.rb +0 -33
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ class ParamError < ActionDispatch::Http::Parameters::ParseError
5
+ def initialize(message = nil)
6
+ super
7
+ end
8
+
9
+ def self.===(other)
10
+ super || (
11
+ defined?(Rack::Utils::ParameterTypeError) && Rack::Utils::ParameterTypeError === other ||
12
+ defined?(Rack::Utils::InvalidParameterError) && Rack::Utils::InvalidParameterError === other ||
13
+ defined?(Rack::QueryParser::ParamsTooDeepError) && Rack::QueryParser::ParamsTooDeepError === other
14
+ )
15
+ end
16
+ end
17
+
18
+ class ParameterTypeError < ParamError
19
+ end
20
+
21
+ class InvalidParameterError < ParamError
22
+ end
23
+
24
+ class ParamsTooDeepError < ParamError
25
+ end
26
+ end
@@ -65,14 +65,14 @@ module ActionDispatch
65
65
  alias :params :parameters
66
66
 
67
67
  def path_parameters=(parameters) # :nodoc:
68
- delete_header("action_dispatch.request.parameters")
68
+ @env.delete("action_dispatch.request.parameters")
69
69
 
70
70
  parameters = Request::Utils.set_binary_encoding(self, parameters, parameters[:controller], parameters[:action])
71
71
  # If any of the path parameters has an invalid encoding then raise since it's
72
72
  # likely to trigger errors further on.
73
73
  Request::Utils.check_param_encoding(parameters)
74
74
 
75
- set_header PARAMETERS_KEY, parameters
75
+ @env[PARAMETERS_KEY] = parameters
76
76
  rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
77
77
  raise ActionController::BadRequest.new("Invalid path parameters: #{e.message}")
78
78
  end
@@ -82,7 +82,7 @@ module ActionDispatch
82
82
  #
83
83
  # { action: "my_action", controller: "my_controller" }
84
84
  def path_parameters
85
- get_header(PARAMETERS_KEY) || set_header(PARAMETERS_KEY, {})
85
+ @env[PARAMETERS_KEY] ||= {}
86
86
  end
87
87
 
88
88
  private
@@ -86,12 +86,14 @@ module ActionDispatch # :nodoc:
86
86
  ambient_light_sensor: "ambient-light-sensor",
87
87
  autoplay: "autoplay",
88
88
  camera: "camera",
89
+ display_capture: "display-capture",
89
90
  encrypted_media: "encrypted-media",
90
91
  fullscreen: "fullscreen",
91
92
  geolocation: "geolocation",
92
93
  gyroscope: "gyroscope",
93
94
  hid: "hid",
94
95
  idle_detection: "idle-detection",
96
+ keyboard_map: "keyboard-map",
95
97
  magnetometer: "magnetometer",
96
98
  microphone: "microphone",
97
99
  midi: "midi",
@@ -184,4 +186,8 @@ module ActionDispatch # :nodoc:
184
186
  end
185
187
  end
186
188
  end
189
+
190
+ ActiveSupport.on_load(:action_dispatch_request) do
191
+ include ActionDispatch::PermissionsPolicy::Request
192
+ end
187
193
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "rack"
5
+
6
+ module ActionDispatch
7
+ class QueryParser
8
+ DEFAULT_SEP = /& */n
9
+ COMMON_SEP = { ";" => /; */n, ";," => /[;,] */n, "&" => /& */n, "&;" => /[&;] */n }
10
+
11
+ def self.strict_query_string_separator
12
+ ActionDispatch.deprecator.warn <<~MSG
13
+ The `strict_query_string_separator` configuration is deprecated have no effect and will be removed in Rails 8.2.
14
+ MSG
15
+ @strict_query_string_separator
16
+ end
17
+
18
+ def self.strict_query_string_separator=(value)
19
+ ActionDispatch.deprecator.warn <<~MSG
20
+ The `strict_query_string_separator` configuration is deprecated have no effect and will be removed in Rails 8.2.
21
+ MSG
22
+ @strict_query_string_separator = value
23
+ end
24
+
25
+ #--
26
+ # Note this departs from WHATWG's specified parsing algorithm by
27
+ # giving a nil value for keys that do not use '='. Callers that need
28
+ # the standard's interpretation can use `v.to_s`.
29
+ def self.each_pair(s, separator = nil)
30
+ return enum_for(:each_pair, s, separator) unless block_given?
31
+
32
+ s ||= ""
33
+
34
+ splitter =
35
+ if separator
36
+ COMMON_SEP[separator] || /[#{separator}] */n
37
+ else
38
+ DEFAULT_SEP
39
+ end
40
+
41
+ s.split(splitter).each do |part|
42
+ next if part.empty?
43
+
44
+ k, v = part.split("=", 2)
45
+
46
+ k = URI.decode_www_form_component(k)
47
+ v &&= URI.decode_www_form_component(v)
48
+
49
+ yield k, v
50
+ end
51
+
52
+ nil
53
+ end
54
+ end
55
+ end
@@ -25,7 +25,6 @@ module ActionDispatch
25
25
  include ActionDispatch::Http::FilterParameters
26
26
  include ActionDispatch::Http::URL
27
27
  include ActionDispatch::ContentSecurityPolicy::Request
28
- include ActionDispatch::PermissionsPolicy::Request
29
28
  include Rack::Request::Env
30
29
 
31
30
  autoload :Session, "action_dispatch/request/session"
@@ -55,12 +54,17 @@ module ActionDispatch
55
54
  METHOD
56
55
  end
57
56
 
57
+ TRANSFER_ENCODING = "HTTP_TRANSFER_ENCODING" # :nodoc:
58
+
58
59
  def self.empty
59
60
  new({})
60
61
  end
61
62
 
62
63
  def initialize(env)
63
64
  super
65
+
66
+ @rack_request = Rack::Request.new(env)
67
+
64
68
  @method = nil
65
69
  @request_method = nil
66
70
  @remote_ip = nil
@@ -69,6 +73,8 @@ module ActionDispatch
69
73
  @ip = nil
70
74
  end
71
75
 
76
+ attr_reader :rack_request
77
+
72
78
  def commit_cookie_jar! # :nodoc:
73
79
  end
74
80
 
@@ -132,7 +138,7 @@ module ActionDispatch
132
138
 
133
139
  # Populate the HTTP method lookup cache.
134
140
  HTTP_METHODS.each { |method|
135
- HTTP_METHOD_LOOKUP[method] = method.downcase.underscore.to_sym
141
+ HTTP_METHOD_LOOKUP[method] = method.downcase.tap { |m| m.tr!("-", "_") }.to_sym
136
142
  }
137
143
 
138
144
  alias raw_request_method request_method # :nodoc:
@@ -151,11 +157,17 @@ module ActionDispatch
151
157
  #
152
158
  # request.route_uri_pattern # => "/:controller(/:action(/:id))(.:format)"
153
159
  def route_uri_pattern
154
- get_header("action_dispatch.route_uri_pattern")
160
+ unless pattern = get_header("action_dispatch.route_uri_pattern")
161
+ route = get_header("action_dispatch.route")
162
+ return if route.nil?
163
+ pattern = route.path.spec.to_s
164
+ set_header("action_dispatch.route_uri_pattern", pattern)
165
+ end
166
+ pattern
155
167
  end
156
168
 
157
- def route_uri_pattern=(pattern) # :nodoc:
158
- set_header("action_dispatch.route_uri_pattern", pattern)
169
+ def route=(route) # :nodoc:
170
+ @env["action_dispatch.route"] = route
159
171
  end
160
172
 
161
173
  def routes # :nodoc:
@@ -283,7 +295,7 @@ module ActionDispatch
283
295
 
284
296
  # Returns the content length of the request as an integer.
285
297
  def content_length
286
- return raw_post.bytesize if headers.key?("Transfer-Encoding")
298
+ return raw_post.bytesize if has_header?(TRANSFER_ENCODING)
287
299
  super.to_i
288
300
  end
289
301
 
@@ -387,15 +399,12 @@ module ActionDispatch
387
399
  # Override Rack's GET method to support indifferent access.
388
400
  def GET
389
401
  fetch_header("action_dispatch.request.query_parameters") do |k|
390
- rack_query_params = super || {}
391
- controller = path_parameters[:controller]
392
- action = path_parameters[:action]
393
- rack_query_params = Request::Utils.set_binary_encoding(self, rack_query_params, controller, action)
394
- # Check for non UTF-8 parameter values, which would cause errors later
395
- Request::Utils.check_param_encoding(rack_query_params)
396
- set_header k, Request::Utils.normalize_encode_params(rack_query_params)
402
+ encoding_template = Request::Utils::CustomParamEncoder.action_encoding_template(self, path_parameters[:controller], path_parameters[:action])
403
+ rack_query_params = ActionDispatch::ParamBuilder.from_query_string(rack_request.query_string, encoding_template: encoding_template)
404
+
405
+ set_header k, rack_query_params
397
406
  end
398
- rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError, Rack::QueryParser::ParamsTooDeepError => e
407
+ rescue ActionDispatch::ParamError => e
399
408
  raise ActionController::BadRequest.new("Invalid query parameters: #{e.message}")
400
409
  end
401
410
  alias :query_parameters :GET
@@ -403,18 +412,54 @@ module ActionDispatch
403
412
  # Override Rack's POST method to support indifferent access.
404
413
  def POST
405
414
  fetch_header("action_dispatch.request.request_parameters") do
406
- pr = parse_formatted_parameters(params_parsers) do |params|
407
- super || {}
415
+ encoding_template = Request::Utils::CustomParamEncoder.action_encoding_template(self, path_parameters[:controller], path_parameters[:action])
416
+
417
+ param_list = nil
418
+ pr = parse_formatted_parameters(params_parsers) do
419
+ if param_list = request_parameters_list
420
+ ActionDispatch::ParamBuilder.from_pairs(param_list, encoding_template: encoding_template)
421
+ else
422
+ # We're not using a version of Rack that provides raw form
423
+ # pairs; we must use its hash (and thus post-process it below).
424
+ fallback_request_parameters
425
+ end
408
426
  end
409
- pr = Request::Utils.set_binary_encoding(self, pr, path_parameters[:controller], path_parameters[:action])
410
- Request::Utils.check_param_encoding(pr)
411
- self.request_parameters = Request::Utils.normalize_encode_params(pr)
427
+
428
+ # If the request body was parsed by a custom parser like JSON
429
+ # (and thus the above block was not run), we need to
430
+ # post-process the result hash.
431
+ if param_list.nil?
432
+ pr = ActionDispatch::ParamBuilder.from_hash(pr, encoding_template: encoding_template)
433
+ end
434
+
435
+ self.request_parameters = pr
412
436
  end
413
- rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError, Rack::QueryParser::ParamsTooDeepError, EOFError => e
437
+ rescue ActionDispatch::ParamError, EOFError => e
414
438
  raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}")
415
439
  end
416
440
  alias :request_parameters :POST
417
441
 
442
+ def request_parameters_list
443
+ # We don't use Rack's parse result, but we must call it so Rack
444
+ # can populate the rack.request.* keys we need.
445
+ rack_post = rack_request.POST
446
+
447
+ if form_pairs = get_header("rack.request.form_pairs")
448
+ # Multipart
449
+ form_pairs
450
+ elsif form_vars = get_header("rack.request.form_vars")
451
+ # URL-encoded
452
+ ActionDispatch::QueryParser.each_pair(form_vars)
453
+ elsif rack_post && !rack_post.empty?
454
+ # It was multipart, but Rack did not preserve a pair list
455
+ # (probably too old). Flat parameter list is not available.
456
+ nil
457
+ else
458
+ # No request body, or not a format Rack knows
459
+ []
460
+ end
461
+ end
462
+
418
463
  # Returns the authorization header regardless of whether it was specified
419
464
  # directly or through one of the proxy alternatives.
420
465
  def authorization
@@ -469,7 +514,7 @@ module ActionDispatch
469
514
  def read_body_stream
470
515
  if body_stream
471
516
  reset_stream(body_stream) do
472
- if headers.key?("Transfer-Encoding")
517
+ if has_header?(TRANSFER_ENCODING)
473
518
  body_stream.read # Read body stream until EOF if "Transfer-Encoding" is present
474
519
  else
475
520
  body_stream.read(content_length)
@@ -491,6 +536,10 @@ module ActionDispatch
491
536
  yield
492
537
  end
493
538
  end
539
+
540
+ def fallback_request_parameters
541
+ rack_request.POST
542
+ end
494
543
  end
495
544
  end
496
545
 
@@ -119,10 +119,22 @@ module ActionDispatch # :nodoc:
119
119
  @str_body = nil
120
120
  end
121
121
 
122
+ BODY_METHODS = { to_ary: true }
123
+
124
+ def respond_to?(method, include_private = false)
125
+ if BODY_METHODS.key?(method)
126
+ @buf.respond_to?(method)
127
+ else
128
+ super
129
+ end
130
+ end
131
+
122
132
  def to_ary
123
- @buf.respond_to?(:to_ary) ?
124
- @buf.to_ary :
125
- @buf.each
133
+ if @str_body
134
+ [body]
135
+ else
136
+ @buf = @buf.to_ary
137
+ end
126
138
  end
127
139
 
128
140
  def body
@@ -265,14 +277,27 @@ module ActionDispatch # :nodoc:
265
277
  # Sets the HTTP response's content MIME type. For example, in the controller you
266
278
  # could write this:
267
279
  #
268
- # response.content_type = "text/plain"
280
+ # response.content_type = "text/html"
281
+ #
282
+ # This method also accepts a symbol with the extension of the MIME type:
283
+ #
284
+ # response.content_type = :html
269
285
  #
270
286
  # If a character set has been defined for this response (see #charset=) then the
271
287
  # character set information will also be included in the content type
272
288
  # information.
273
289
  def content_type=(content_type)
274
- return unless content_type
275
- new_header_info = parse_content_type(content_type.to_s)
290
+ case content_type
291
+ when NilClass
292
+ return
293
+ when Symbol
294
+ mime_type = Mime[content_type]
295
+ raise ArgumentError, "Unknown MIME type #{content_type}" unless mime_type
296
+ new_header_info = ContentTypeHeader.new(mime_type.to_s)
297
+ else
298
+ new_header_info = parse_content_type(content_type.to_s)
299
+ end
300
+
276
301
  prev_header_info = parsed_content_type_header
277
302
  charset = new_header_info.charset || prev_header_info.charset
278
303
  charset ||= self.class.default_charset unless prev_header_info.mime_type
@@ -342,7 +367,13 @@ module ActionDispatch # :nodoc:
342
367
  # Returns the content of the response as a string. This contains the contents of
343
368
  # any calls to `render`.
344
369
  def body
345
- @stream.body
370
+ if @stream.respond_to?(:to_ary)
371
+ @stream.to_ary.join
372
+ elsif @stream.respond_to?(:body)
373
+ @stream.body
374
+ else
375
+ @stream
376
+ end
346
377
  end
347
378
 
348
379
  def write(string)
@@ -351,11 +382,16 @@ module ActionDispatch # :nodoc:
351
382
 
352
383
  # Allows you to manually set or override the response body.
353
384
  def body=(body)
354
- if body.respond_to?(:to_path)
355
- @stream = body
356
- else
357
- synchronize do
358
- @stream = build_buffer self, munge_body_object(body)
385
+ # Prevent ActionController::Metal::Live::Response from committing the response prematurely.
386
+ synchronize do
387
+ if body.respond_to?(:to_str)
388
+ @stream = build_buffer(self, [body])
389
+ elsif body.respond_to?(:to_path)
390
+ @stream = body
391
+ elsif body.respond_to?(:to_ary)
392
+ @stream = build_buffer(self, body)
393
+ else
394
+ @stream = body
359
395
  end
360
396
  end
361
397
  end
@@ -496,10 +532,6 @@ module ActionDispatch # :nodoc:
496
532
  Buffer.new response, body
497
533
  end
498
534
 
499
- def munge_body_object(body)
500
- body.respond_to?(:each) ? body : [body]
501
- end
502
-
503
535
  def assign_default_content_type_and_charset!
504
536
  return if media_type
505
537
 
@@ -513,6 +545,8 @@ module ActionDispatch # :nodoc:
513
545
  @response = response
514
546
  end
515
547
 
548
+ attr :response
549
+
516
550
  def close
517
551
  # Rack "close" maps to Response#abort, and **not** Response#close (which is used
518
552
  # when the controller's finished writing)
@@ -11,8 +11,105 @@ module ActionDispatch
11
11
  HOST_REGEXP = /(^[^:]+:\/\/)?(\[[^\]]+\]|[^:]+)(?::(\d+$))?/
12
12
  PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/
13
13
 
14
+ # DomainExtractor provides utility methods for extracting domain and subdomain
15
+ # information from host strings. This module is used internally by Action Dispatch
16
+ # to parse host names and separate the domain from subdomains based on the
17
+ # top-level domain (TLD) length.
18
+ #
19
+ # The module assumes a standard domain structure where domains consist of:
20
+ # - Subdomains (optional, can be multiple levels)
21
+ # - Domain name
22
+ # - Top-level domain (TLD, can be multiple levels like .co.uk)
23
+ #
24
+ # For example, in "api.staging.example.co.uk":
25
+ # - Subdomains: ["api", "staging"]
26
+ # - Domain: "example.co.uk" (with tld_length=2)
27
+ # - TLD: "co.uk"
28
+ module DomainExtractor
29
+ extend self
30
+
31
+ # Extracts the domain part from a host string, including the specified
32
+ # number of top-level domain components.
33
+ #
34
+ # The domain includes the main domain name plus the TLD components.
35
+ # The +tld_length+ parameter specifies how many components from the right
36
+ # should be considered part of the TLD.
37
+ #
38
+ # ==== Parameters
39
+ #
40
+ # [+host+]
41
+ # The host string to extract the domain from.
42
+ #
43
+ # [+tld_length+]
44
+ # The number of domain components that make up the TLD. For example,
45
+ # use 1 for ".com" or 2 for ".co.uk".
46
+ #
47
+ # ==== Examples
48
+ #
49
+ # # Standard TLD (tld_length = 1)
50
+ # DomainExtractor.domain_from("www.example.com", 1)
51
+ # # => "example.com"
52
+ #
53
+ # # Country-code TLD (tld_length = 2)
54
+ # DomainExtractor.domain_from("www.example.co.uk", 2)
55
+ # # => "example.co.uk"
56
+ #
57
+ # # Multiple subdomains
58
+ # DomainExtractor.domain_from("api.staging.myapp.herokuapp.com", 1)
59
+ # # => "herokuapp.com"
60
+ #
61
+ # # Single component (returns the host itself)
62
+ # DomainExtractor.domain_from("localhost", 1)
63
+ # # => "localhost"
64
+ def domain_from(host, tld_length)
65
+ host.split(".").last(1 + tld_length).join(".")
66
+ end
67
+
68
+ # Extracts the subdomain components from a host string as an Array.
69
+ #
70
+ # Returns all the components that come before the domain and TLD parts.
71
+ # The +tld_length+ parameter is used to determine where the domain begins
72
+ # so that everything before it is considered a subdomain.
73
+ #
74
+ # ==== Parameters
75
+ #
76
+ # [+host+]
77
+ # The host string to extract subdomains from.
78
+ #
79
+ # [+tld_length+]
80
+ # The number of domain components that make up the TLD. This affects
81
+ # where the domain boundary is calculated.
82
+ #
83
+ # ==== Examples
84
+ #
85
+ # # Standard TLD (tld_length = 1)
86
+ # DomainExtractor.subdomains_from("www.example.com", 1)
87
+ # # => ["www"]
88
+ #
89
+ # # Country-code TLD (tld_length = 2)
90
+ # DomainExtractor.subdomains_from("api.staging.example.co.uk", 2)
91
+ # # => ["api", "staging"]
92
+ #
93
+ # # No subdomains
94
+ # DomainExtractor.subdomains_from("example.com", 1)
95
+ # # => []
96
+ #
97
+ # # Single subdomain with complex TLD
98
+ # DomainExtractor.subdomains_from("www.mysite.co.uk", 2)
99
+ # # => ["www"]
100
+ #
101
+ # # Multiple levels of subdomains
102
+ # DomainExtractor.subdomains_from("dev.api.staging.example.com", 1)
103
+ # # => ["dev", "api", "staging"]
104
+ def subdomains_from(host, tld_length)
105
+ parts = host.split(".")
106
+ parts[0..-(tld_length + 2)]
107
+ end
108
+ end
109
+
14
110
  mattr_accessor :secure_protocol, default: false
15
111
  mattr_accessor :tld_length, default: 1
112
+ mattr_accessor :domain_extractor, default: DomainExtractor
16
113
 
17
114
  class << self
18
115
  # Returns the domain part of a host given the domain level.
@@ -96,34 +193,33 @@ module ActionDispatch
96
193
  end
97
194
 
98
195
  def extract_domain_from(host, tld_length)
99
- host.split(".").last(1 + tld_length).join(".")
196
+ domain_extractor.domain_from(host, tld_length)
100
197
  end
101
198
 
102
199
  def extract_subdomains_from(host, tld_length)
103
- parts = host.split(".")
104
- parts[0..-(tld_length + 2)]
200
+ domain_extractor.subdomains_from(host, tld_length)
105
201
  end
106
202
 
107
203
  def build_host_url(host, port, protocol, options, path)
108
204
  if match = host.match(HOST_REGEXP)
109
- protocol ||= match[1] unless protocol == false
110
- host = match[2]
111
- port = match[3] unless options.key? :port
205
+ protocol_from_host = match[1] if protocol.nil?
206
+ host = match[2]
207
+ port = match[3] unless options.key? :port
112
208
  end
113
209
 
114
- protocol = normalize_protocol protocol
210
+ protocol = protocol_from_host || normalize_protocol(protocol).dup
115
211
  host = normalize_host(host, options)
212
+ port = normalize_port(port, protocol)
116
213
 
117
- result = protocol.dup
214
+ result = protocol
118
215
 
119
216
  if options[:user] && options[:password]
120
217
  result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
121
218
  end
122
219
 
123
220
  result << host
124
- normalize_port(port, protocol) { |normalized_port|
125
- result << ":#{normalized_port}"
126
- }
221
+
222
+ result << ":" << port.to_s if port
127
223
 
128
224
  result.concat path
129
225
  end
@@ -169,11 +265,11 @@ module ActionDispatch
169
265
  return unless port
170
266
 
171
267
  case protocol
172
- when "//" then yield port
268
+ when "//" then port
173
269
  when "https://"
174
- yield port unless port.to_i == 443
270
+ port unless port.to_i == 443
175
271
  else
176
- yield port unless port.to_i == 80
272
+ port unless port.to_i == 80
177
273
  end
178
274
  end
179
275
  end
@@ -2,8 +2,6 @@
2
2
 
3
3
  # :markup: markdown
4
4
 
5
- require "strscan"
6
-
7
5
  module ActionDispatch
8
6
  module Journey # :nodoc:
9
7
  module GTG # :nodoc:
@@ -16,7 +14,13 @@ module ActionDispatch
16
14
  end
17
15
 
18
16
  class Simulator # :nodoc:
19
- INITIAL_STATE = [ [0, nil] ].freeze
17
+ STATIC_TOKENS = Array.new(64)
18
+ STATIC_TOKENS[".".ord] = "."
19
+ STATIC_TOKENS["/".ord] = "/"
20
+ STATIC_TOKENS["?".ord] = "?"
21
+ STATIC_TOKENS.freeze
22
+
23
+ INITIAL_STATE = [0, nil].freeze
20
24
 
21
25
  attr_reader :tt
22
26
 
@@ -25,21 +29,38 @@ module ActionDispatch
25
29
  end
26
30
 
27
31
  def memos(string)
28
- input = StringScanner.new(string)
29
32
  state = INITIAL_STATE
30
- start_index = 0
31
33
 
32
- while sym = input.scan(%r([/.?]|[^/.?]+))
33
- end_index = start_index + sym.length
34
+ pos = 0
35
+ eos = string.bytesize
36
+
37
+ while pos < eos
38
+ start_index = pos
39
+ pos += 1
34
40
 
35
- state = tt.move(state, string, start_index, end_index)
41
+ if (token = STATIC_TOKENS[string.getbyte(start_index)])
42
+ state = tt.move(state, string, token, start_index, false)
43
+ else
44
+ while pos < eos && STATIC_TOKENS[string.getbyte(pos)].nil?
45
+ pos += 1
46
+ end
36
47
 
37
- start_index = end_index
48
+ token = string.byteslice(start_index, pos - start_index)
49
+ state = tt.move(state, string, token, start_index, true)
50
+ end
38
51
  end
39
52
 
40
- acceptance_states = state.each_with_object([]) do |s_d, memos|
41
- s, idx = s_d
42
- memos.concat(tt.memo(s)) if idx.nil? && tt.accepting?(s)
53
+ acceptance_states = []
54
+ states_count = state.size
55
+ i = 0
56
+ while i < states_count
57
+ if state[i + 1].nil?
58
+ s = state[i]
59
+ if tt.accepting?(s)
60
+ acceptance_states.concat(tt.memo(s))
61
+ end
62
+ end
63
+ i += 2
43
64
  end
44
65
 
45
66
  acceptance_states.empty? ? yield : acceptance_states