actionpack 8.0.2.1 → 8.1.0.beta1

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +251 -141
  3. data/README.rdoc +1 -1
  4. data/lib/abstract_controller/asset_paths.rb +4 -2
  5. data/lib/abstract_controller/base.rb +11 -14
  6. data/lib/abstract_controller/caching.rb +6 -3
  7. data/lib/abstract_controller/collector.rb +1 -1
  8. data/lib/abstract_controller/logger.rb +2 -1
  9. data/lib/action_controller/base.rb +1 -1
  10. data/lib/action_controller/caching.rb +1 -2
  11. data/lib/action_controller/form_builder.rb +1 -1
  12. data/lib/action_controller/log_subscriber.rb +7 -0
  13. data/lib/action_controller/metal/allow_browser.rb +1 -1
  14. data/lib/action_controller/metal/conditional_get.rb +25 -0
  15. data/lib/action_controller/metal/data_streaming.rb +1 -3
  16. data/lib/action_controller/metal/exceptions.rb +5 -0
  17. data/lib/action_controller/metal/flash.rb +1 -4
  18. data/lib/action_controller/metal/head.rb +3 -1
  19. data/lib/action_controller/metal/live.rb +0 -6
  20. data/lib/action_controller/metal/permissions_policy.rb +9 -0
  21. data/lib/action_controller/metal/rate_limiting.rb +22 -7
  22. data/lib/action_controller/metal/redirecting.rb +63 -7
  23. data/lib/action_controller/metal/renderers.rb +27 -6
  24. data/lib/action_controller/metal/rendering.rb +8 -2
  25. data/lib/action_controller/metal/request_forgery_protection.rb +18 -10
  26. data/lib/action_controller/metal/rescue.rb +9 -0
  27. data/lib/action_controller/railtie.rb +2 -6
  28. data/lib/action_controller/renderer.rb +0 -1
  29. data/lib/action_dispatch/constants.rb +6 -0
  30. data/lib/action_dispatch/http/cache.rb +111 -1
  31. data/lib/action_dispatch/http/content_security_policy.rb +13 -1
  32. data/lib/action_dispatch/http/filter_parameters.rb +5 -3
  33. data/lib/action_dispatch/http/mime_negotiation.rb +8 -3
  34. data/lib/action_dispatch/http/mime_types.rb +1 -0
  35. data/lib/action_dispatch/http/param_builder.rb +28 -27
  36. data/lib/action_dispatch/http/parameters.rb +3 -3
  37. data/lib/action_dispatch/http/permissions_policy.rb +4 -0
  38. data/lib/action_dispatch/http/query_parser.rb +12 -10
  39. data/lib/action_dispatch/http/request.rb +10 -5
  40. data/lib/action_dispatch/http/response.rb +65 -17
  41. data/lib/action_dispatch/http/url.rb +101 -5
  42. data/lib/action_dispatch/journey/gtg/simulator.rb +33 -12
  43. data/lib/action_dispatch/journey/gtg/transition_table.rb +29 -39
  44. data/lib/action_dispatch/journey/nodes/node.rb +2 -1
  45. data/lib/action_dispatch/journey/route.rb +45 -31
  46. data/lib/action_dispatch/journey/router/utils.rb +8 -14
  47. data/lib/action_dispatch/journey/router.rb +59 -81
  48. data/lib/action_dispatch/journey/routes.rb +7 -0
  49. data/lib/action_dispatch/journey/visitors.rb +55 -23
  50. data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
  51. data/lib/action_dispatch/middleware/cookies.rb +4 -2
  52. data/lib/action_dispatch/middleware/debug_exceptions.rb +10 -2
  53. data/lib/action_dispatch/middleware/debug_view.rb +11 -0
  54. data/lib/action_dispatch/middleware/exception_wrapper.rb +14 -8
  55. data/lib/action_dispatch/middleware/executor.rb +12 -2
  56. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -6
  57. data/lib/action_dispatch/middleware/session/cache_store.rb +17 -0
  58. data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
  59. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -2
  60. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
  61. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  62. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
  63. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +1 -0
  64. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
  65. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
  66. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
  67. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
  68. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
  69. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
  70. data/lib/action_dispatch/railtie.rb +10 -2
  71. data/lib/action_dispatch/routing/inspector.rb +4 -1
  72. data/lib/action_dispatch/routing/mapper.rb +323 -173
  73. data/lib/action_dispatch/routing/route_set.rb +3 -6
  74. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +2 -2
  75. data/lib/action_dispatch/testing/assertion_response.rb +1 -1
  76. data/lib/action_dispatch/testing/assertions/response.rb +14 -0
  77. data/lib/action_dispatch/testing/assertions/routing.rb +11 -3
  78. data/lib/action_pack/gem_version.rb +3 -3
  79. metadata +11 -10
@@ -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,12 +193,11 @@ 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)
@@ -272,7 +368,7 @@ module ActionDispatch
272
368
  end
273
369
  end
274
370
 
275
- # Returns whether this request is using the standard port
371
+ # Returns whether this request is using the standard port.
276
372
  #
277
373
  # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
278
374
  # req.standard_port? # => true
@@ -307,7 +403,7 @@ module ActionDispatch
307
403
  standard_port? ? "" : ":#{port}"
308
404
  end
309
405
 
310
- # Returns the requested port, such as 8080, based on SERVER_PORT
406
+ # Returns the requested port, such as 8080, based on SERVER_PORT.
311
407
  #
312
408
  # req = ActionDispatch::Request.new 'SERVER_PORT' => '80'
313
409
  # req.server_port # => 80
@@ -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
@@ -47,25 +47,27 @@ module ActionDispatch
47
47
  Array(t)
48
48
  end
49
49
 
50
- def move(t, full_string, start_index, end_index)
50
+ def move(t, full_string, token, start_index, token_matches_default)
51
51
  return [] if t.empty?
52
52
 
53
53
  next_states = []
54
54
 
55
- tok = full_string.slice(start_index, end_index - start_index)
56
- token_matches_default_component = DEFAULT_EXP_ANCHORED.match?(tok)
57
-
58
- t.each { |s, previous_start|
55
+ transitions_count = t.size
56
+ i = 0
57
+ while i < transitions_count
58
+ s = t[i]
59
+ previous_start = t[i + 1]
59
60
  if previous_start.nil?
60
61
  # In the simple case of a "default" param regex do this fast-path and add all
61
62
  # next states.
62
- if token_matches_default_component && states = @stdparam_states[s]
63
- states.each { |re, v| next_states << [v, nil].freeze if !v.nil? }
63
+ if token_matches_default && std_state = @stdparam_states[s]
64
+ next_states << std_state << nil
64
65
  end
65
66
 
66
67
  # When we have a literal string, we can just pull the next state
67
68
  if states = @string_states[s]
68
- next_states << [states[tok], nil].freeze unless states[tok].nil?
69
+ state = states[token]
70
+ next_states << state << nil unless state.nil?
69
71
  end
70
72
  end
71
73
 
@@ -80,19 +82,21 @@ module ActionDispatch
80
82
  previous_start
81
83
  end
82
84
 
83
- slice_length = end_index - slice_start
85
+ slice_length = start_index + token.length - slice_start
84
86
  curr_slice = full_string.slice(slice_start, slice_length)
85
87
 
86
88
  states.each { |re, v|
87
89
  # if we match, we can try moving past this
88
- next_states << [v, nil].freeze if !v.nil? && re.match?(curr_slice)
90
+ next_states << v << nil if !v.nil? && re.match?(curr_slice)
89
91
  }
90
92
 
91
93
  # and regardless, we must continue accepting tokens and retrying this regexp. we
92
94
  # need to remember where we started as well so we can take bigger slices.
93
- next_states << [s, slice_start].freeze
95
+ next_states << s << slice_start
94
96
  end
95
- }
97
+
98
+ i += 2
99
+ end
96
100
 
97
101
  next_states
98
102
  end
@@ -163,25 +167,27 @@ module ActionDispatch
163
167
  end
164
168
 
165
169
  def []=(from, to, sym)
166
- to_mappings = states_hash_for(sym)[from] ||= {}
167
170
  case sym
171
+ when String, Symbol
172
+ to_mapping = @string_states[from] ||= {}
173
+ # account for symbols in the constraints the same as strings
174
+ to_mapping[sym.to_s] = to
168
175
  when Regexp
169
- # we must match the whole string to a token boundary
170
176
  if sym == DEFAULT_EXP
171
- sym = DEFAULT_EXP_ANCHORED
177
+ @stdparam_states[from] = to
172
178
  else
173
- sym = /\A#{sym}\Z/
179
+ to_mapping = @regexp_states[from] ||= {}
180
+ # we must match the whole string to a token boundary
181
+ to_mapping[/\A#{sym}\Z/] = to
174
182
  end
175
- when Symbol
176
- # account for symbols in the constraints the same as strings
177
- sym = sym.to_s
183
+ else
184
+ raise ArgumentError, "unknown symbol: %s" % sym.class
178
185
  end
179
- to_mappings[sym] = to
180
186
  end
181
187
 
182
188
  def states
183
189
  ss = @string_states.keys + @string_states.values.flat_map(&:values)
184
- ps = @stdparam_states.keys + @stdparam_states.values.flat_map(&:values)
190
+ ps = @stdparam_states.keys + @stdparam_states.values
185
191
  rs = @regexp_states.keys + @regexp_states.values.flat_map(&:values)
186
192
  (ss + ps + rs).uniq
187
193
  end
@@ -189,28 +195,12 @@ module ActionDispatch
189
195
  def transitions
190
196
  @string_states.flat_map { |from, hash|
191
197
  hash.map { |s, to| [from, s, to] }
192
- } + @stdparam_states.flat_map { |from, hash|
193
- hash.map { |s, to| [from, s, to] }
198
+ } + @stdparam_states.map { |from, to|
199
+ [from, DEFAULT_EXP_ANCHORED, to]
194
200
  } + @regexp_states.flat_map { |from, hash|
195
201
  hash.map { |s, to| [from, s, to] }
196
202
  }
197
203
  end
198
-
199
- private
200
- def states_hash_for(sym)
201
- case sym
202
- when String, Symbol
203
- @string_states
204
- when Regexp
205
- if sym == DEFAULT_EXP
206
- @stdparam_states
207
- else
208
- @regexp_states
209
- end
210
- else
211
- raise ArgumentError, "unknown symbol: %s" % sym.class
212
- end
213
- end
214
204
  end
215
205
  end
216
206
  end
@@ -74,6 +74,7 @@ module ActionDispatch
74
74
  def initialize(left)
75
75
  @left = left
76
76
  @memo = nil
77
+ @to_s = nil
77
78
  end
78
79
 
79
80
  def each(&block)
@@ -81,7 +82,7 @@ module ActionDispatch
81
82
  end
82
83
 
83
84
  def to_s
84
- Visitors::String::INSTANCE.accept(self, "")
85
+ @to_s ||= Visitors::String::INSTANCE.accept(self, "".dup).freeze
85
86
  end
86
87
 
87
88
  def to_dot
@@ -38,29 +38,50 @@ module ActionDispatch
38
38
  def self.verb; ""; end
39
39
  end
40
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
+
41
54
  VERB_TO_CLASS = VERBS.each_with_object(all: All) do |verb, hash|
42
55
  klass = const_get verb
43
56
  hash[verb] = klass
44
57
  hash[verb.downcase] = klass
45
58
  hash[verb.downcase.to_sym] = klass
46
59
  end
47
- end
48
60
 
49
- def self.verb_matcher(verb)
50
- VerbMatchers::VERB_TO_CLASS.fetch(verb) do
61
+ VERB_TO_CLASS.default_proc = proc do |_, verb|
51
62
  VerbMatchers::Unknown.new verb.to_s.dasherize.upcase
52
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
53
74
  end
54
75
 
55
76
  ##
56
77
  # +path+ is a path constraint.
57
78
  # `constraints` is a hash of constraints to be applied to this route.
58
- def initialize(name:, app: nil, path:, constraints: {}, required_defaults: [], defaults: {}, request_method_match: nil, precedence: 0, scope_options: {}, internal: false, source_location: nil)
79
+ def initialize(name:, app: nil, path:, constraints: {}, required_defaults: [], defaults: {}, via: nil, precedence: 0, scope_options: {}, internal: false, source_location: nil)
59
80
  @name = name
60
81
  @app = app
61
82
  @path = path
62
83
 
63
- @request_method_match = request_method_match
84
+ @request_method_match = via && VerbMatchers.for(via)
64
85
  @constraints = constraints
65
86
  @defaults = defaults
66
87
  @required_defaults = nil
@@ -146,21 +167,23 @@ module ActionDispatch
146
167
  end
147
168
 
148
169
  def matches?(request)
149
- match_verb(request) &&
150
- constraints.all? { |method, value|
151
- case value
152
- when Regexp, String
153
- value === request.send(method).to_s
154
- when Array
155
- value.include?(request.send(method))
156
- when TrueClass
157
- request.send(method).present?
158
- when FalseClass
159
- request.send(method).blank?
160
- else
161
- value === request.send(method)
162
- end
163
- }
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
+ )
164
187
  end
165
188
 
166
189
  def ip
@@ -168,21 +191,12 @@ module ActionDispatch
168
191
  end
169
192
 
170
193
  def requires_matching_verb?
171
- !@request_method_match.all? { |x| x == VerbMatchers::All }
194
+ @request_method_match != VerbMatchers::All
172
195
  end
173
196
 
174
197
  def verb
175
- verbs.join("|")
198
+ @request_method_match.verb
176
199
  end
177
-
178
- private
179
- def verbs
180
- @request_method_match.map(&:verb)
181
- end
182
-
183
- def match_verb(request)
184
- @request_method_match.any? { |m| m.call request }
185
- end
186
200
  end
187
201
  end
188
202
  # :startdoc:
@@ -17,7 +17,14 @@ module ActionDispatch
17
17
  # normalize_path("") # => "/"
18
18
  # normalize_path("/%ab") # => "/%AB"
19
19
  def self.normalize_path(path)
20
- 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
21
28
  encoding = path.encoding
22
29
  path = +"/#{path}"
23
30
  path.squeeze!("/")
@@ -61,11 +68,6 @@ module ActionDispatch
61
68
  escape(segment, SEGMENT)
62
69
  end
63
70
 
64
- def unescape_uri(uri)
65
- encoding = uri.encoding == US_ASCII ? UTF_8 : uri.encoding
66
- uri.gsub(ESCAPED) { |match| [match[1, 2].hex].pack("C") }.force_encoding(encoding)
67
- end
68
-
69
71
  private
70
72
  def escape(component, pattern)
71
73
  component.gsub(pattern) { |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII)
@@ -91,14 +93,6 @@ module ActionDispatch
91
93
  def self.escape_fragment(fragment)
92
94
  ENCODER.escape_fragment(fragment.to_s)
93
95
  end
94
-
95
- # Replaces any escaped sequences with their unescaped representations.
96
- #
97
- # uri = "/topics?title=Ruby%20on%20Rails"
98
- # unescape_uri(uri) #=> "/topics?title=Ruby on Rails"
99
- def self.unescape_uri(uri)
100
- ENCODER.unescape_uri(uri)
101
- end
102
96
  end
103
97
  end
104
98
  end