actionpack 6.1.5 → 7.0.0.alpha1

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -521
  3. data/MIT-LICENSE +2 -1
  4. data/README.rdoc +2 -3
  5. data/lib/abstract_controller/asset_paths.rb +1 -1
  6. data/lib/abstract_controller/base.rb +7 -21
  7. data/lib/abstract_controller/caching/fragments.rb +2 -2
  8. data/lib/abstract_controller/caching.rb +1 -1
  9. data/lib/abstract_controller/callbacks.rb +9 -8
  10. data/lib/abstract_controller/collector.rb +2 -2
  11. data/lib/abstract_controller/error.rb +1 -1
  12. data/lib/abstract_controller/helpers.rb +3 -2
  13. data/lib/abstract_controller/logger.rb +1 -1
  14. data/lib/abstract_controller/railties/routes_helpers.rb +2 -0
  15. data/lib/abstract_controller/translation.rb +0 -2
  16. data/lib/abstract_controller/url_for.rb +4 -6
  17. data/lib/action_controller/api.rb +1 -1
  18. data/lib/action_controller/log_subscriber.rb +3 -1
  19. data/lib/action_controller/metal/conditional_get.rb +38 -1
  20. data/lib/action_controller/metal/content_security_policy.rb +1 -1
  21. data/lib/action_controller/metal/cookies.rb +1 -1
  22. data/lib/action_controller/metal/data_streaming.rb +5 -13
  23. data/lib/action_controller/metal/exceptions.rb +19 -30
  24. data/lib/action_controller/metal/flash.rb +6 -2
  25. data/lib/action_controller/metal/http_authentication.rb +15 -16
  26. data/lib/action_controller/metal/instrumentation.rb +55 -52
  27. data/lib/action_controller/metal/live.rb +42 -2
  28. data/lib/action_controller/metal/mime_responds.rb +3 -3
  29. data/lib/action_controller/metal/params_wrapper.rb +7 -7
  30. data/lib/action_controller/metal/permissions_policy.rb +1 -1
  31. data/lib/action_controller/metal/query_tags.rb +16 -0
  32. data/lib/action_controller/metal/redirecting.rb +50 -16
  33. data/lib/action_controller/metal/rendering.rb +7 -7
  34. data/lib/action_controller/metal/request_forgery_protection.rb +64 -20
  35. data/lib/action_controller/metal/rescue.rb +1 -1
  36. data/lib/action_controller/metal/streaming.rb +1 -3
  37. data/lib/action_controller/metal/strong_parameters.rb +25 -29
  38. data/lib/action_controller/metal/testing.rb +0 -2
  39. data/lib/action_controller/metal.rb +7 -10
  40. data/lib/action_controller/railtie.rb +42 -5
  41. data/lib/action_controller/test_case.rb +6 -2
  42. data/lib/action_controller.rb +2 -5
  43. data/lib/action_dispatch/http/cache.rb +13 -6
  44. data/lib/action_dispatch/http/content_security_policy.rb +40 -37
  45. data/lib/action_dispatch/http/filter_parameters.rb +5 -0
  46. data/lib/action_dispatch/http/mime_negotiation.rb +13 -3
  47. data/lib/action_dispatch/http/mime_type.rb +9 -11
  48. data/lib/action_dispatch/http/parameters.rb +4 -4
  49. data/lib/action_dispatch/http/permissions_policy.rb +1 -1
  50. data/lib/action_dispatch/http/request.rb +10 -19
  51. data/lib/action_dispatch/http/response.rb +3 -3
  52. data/lib/action_dispatch/http/url.rb +9 -10
  53. data/lib/action_dispatch/journey/gtg/builder.rb +11 -12
  54. data/lib/action_dispatch/journey/gtg/simulator.rb +10 -4
  55. data/lib/action_dispatch/journey/gtg/transition_table.rb +77 -21
  56. data/lib/action_dispatch/journey/nodes/node.rb +70 -5
  57. data/lib/action_dispatch/journey/path/pattern.rb +22 -13
  58. data/lib/action_dispatch/journey/route.rb +5 -12
  59. data/lib/action_dispatch/journey/router/utils.rb +2 -2
  60. data/lib/action_dispatch/journey/router.rb +1 -1
  61. data/lib/action_dispatch/journey/routes.rb +3 -3
  62. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  63. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  64. data/lib/action_dispatch/middleware/actionable_exceptions.rb +0 -1
  65. data/lib/action_dispatch/middleware/cookies.rb +7 -3
  66. data/lib/action_dispatch/middleware/debug_exceptions.rb +6 -4
  67. data/lib/action_dispatch/middleware/debug_locks.rb +3 -3
  68. data/lib/action_dispatch/middleware/exception_wrapper.rb +4 -0
  69. data/lib/action_dispatch/middleware/executor.rb +1 -1
  70. data/lib/action_dispatch/middleware/flash.rb +9 -11
  71. data/lib/action_dispatch/middleware/host_authorization.rb +25 -73
  72. data/lib/action_dispatch/middleware/remote_ip.rb +16 -4
  73. data/lib/action_dispatch/middleware/session/abstract_store.rb +1 -1
  74. data/lib/action_dispatch/middleware/show_exceptions.rb +6 -18
  75. data/lib/action_dispatch/middleware/stack.rb +50 -9
  76. data/lib/action_dispatch/middleware/static.rb +2 -5
  77. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +1 -1
  78. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +4 -11
  79. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +2 -2
  80. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +2 -2
  81. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +4 -4
  82. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +3 -3
  83. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +28 -18
  84. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +3 -3
  85. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +3 -3
  86. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +3 -3
  87. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +3 -3
  88. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +3 -3
  89. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +5 -14
  90. data/lib/action_dispatch/railtie.rb +8 -2
  91. data/lib/action_dispatch/request/session.rb +43 -13
  92. data/lib/action_dispatch/routing/mapper.rb +44 -72
  93. data/lib/action_dispatch/routing/redirection.rb +0 -2
  94. data/lib/action_dispatch/routing/route_set.rb +7 -4
  95. data/lib/action_dispatch/routing/routes_proxy.rb +1 -1
  96. data/lib/action_dispatch/routing/url_for.rb +1 -2
  97. data/lib/action_dispatch/routing.rb +2 -2
  98. data/lib/action_dispatch/system_test_case.rb +6 -12
  99. data/lib/action_dispatch/system_testing/driver.rb +24 -4
  100. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +10 -6
  101. data/lib/action_dispatch/testing/assertions.rb +2 -5
  102. data/lib/action_dispatch/testing/integration.rb +6 -8
  103. data/lib/action_dispatch/testing/test_process.rb +2 -2
  104. data/lib/action_dispatch.rb +1 -1
  105. data/lib/action_pack/gem_version.rb +4 -4
  106. data/lib/action_pack.rb +1 -1
  107. metadata +18 -18
@@ -42,11 +42,8 @@ module ActionDispatch
42
42
  HTTP_NEGOTIATE HTTP_PRAGMA HTTP_CLIENT_IP
43
43
  HTTP_X_FORWARDED_FOR HTTP_ORIGIN HTTP_VERSION
44
44
  HTTP_X_CSRF_TOKEN HTTP_X_REQUEST_ID HTTP_X_FORWARDED_HOST
45
- SERVER_ADDR
46
45
  ].freeze
47
46
 
48
- # TODO: Remove SERVER_ADDR when we remove support to Rack 2.1.
49
- # See https://github.com/rack/rack/commit/c173b188d81ee437b588c1e046a1c9f031dea550
50
47
  ENV_METHODS.each do |env|
51
48
  class_eval <<-METHOD, __FILE__, __LINE__ + 1
52
49
  # frozen_string_literal: true
@@ -90,7 +87,7 @@ module ActionDispatch
90
87
  controller_param = name.underscore
91
88
  const_name = controller_param.camelize << "Controller"
92
89
  begin
93
- ActiveSupport::Dependencies.constantize(const_name)
90
+ const_name.constantize
94
91
  rescue NameError => error
95
92
  if error.missing_name == const_name || const_name.start_with?("#{error.missing_name}::")
96
93
  raise MissingController.new(error.message, error.name)
@@ -165,7 +162,7 @@ module ActionDispatch
165
162
  set_header(routes.env_key, name.dup)
166
163
  end
167
164
 
168
- def request_method=(request_method) #:nodoc:
165
+ def request_method=(request_method) # :nodoc:
169
166
  if check_method(request_method)
170
167
  @request_method = set_header("REQUEST_METHOD", request_method)
171
168
  end
@@ -266,7 +263,7 @@ module ActionDispatch
266
263
  # # get "/articles"
267
264
  # request.media_type # => "application/x-www-form-urlencoded"
268
265
  def media_type
269
- content_mime_type.to_s
266
+ content_mime_type&.to_s
270
267
  end
271
268
 
272
269
  # Returns the content length of the request as an integer.
@@ -355,21 +352,15 @@ module ActionDispatch
355
352
  FORM_DATA_MEDIA_TYPES.include?(media_type)
356
353
  end
357
354
 
358
- def body_stream #:nodoc:
355
+ def body_stream # :nodoc:
359
356
  get_header("rack.input")
360
357
  end
361
358
 
362
- # TODO This should be broken apart into AD::Request::Session and probably
363
- # be included by the session middleware.
364
359
  def reset_session
365
- if session && session.respond_to?(:destroy)
366
- session.destroy
367
- else
368
- self.session = {}
369
- end
360
+ session.destroy
370
361
  end
371
362
 
372
- def session=(session) #:nodoc:
363
+ def session=(session) # :nodoc:
373
364
  Session.set self, session
374
365
  end
375
366
 
@@ -434,10 +425,6 @@ module ActionDispatch
434
425
  def commit_flash
435
426
  end
436
427
 
437
- def ssl?
438
- super || scheme == "wss"
439
- end
440
-
441
428
  def inspect # :nodoc:
442
429
  "#<#{self.class.name} #{method} #{original_url.dump} for #{remote_ip}>"
443
430
  end
@@ -447,6 +434,10 @@ module ActionDispatch
447
434
  HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}")
448
435
  name
449
436
  end
437
+
438
+ def default_session
439
+ Session.disabled(self)
440
+ end
450
441
  end
451
442
  end
452
443
 
@@ -88,13 +88,13 @@ module ActionDispatch # :nodoc:
88
88
 
89
89
  def self.return_only_media_type_on_content_type=(*)
90
90
  ActiveSupport::Deprecation.warn(
91
- ".return_only_media_type_on_content_type= is dreprecated with no replacement and will be removed in 7.0."
91
+ ".return_only_media_type_on_content_type= is deprecated with no replacement and will be removed in 7.0."
92
92
  )
93
93
  end
94
94
 
95
95
  def self.return_only_media_type_on_content_type
96
96
  ActiveSupport::Deprecation.warn(
97
- ".return_only_media_type_on_content_type is dreprecated with no replacement and will be removed in 7.0."
97
+ ".return_only_media_type_on_content_type is deprecated with no replacement and will be removed in 7.0."
98
98
  )
99
99
  end
100
100
 
@@ -336,7 +336,7 @@ module ActionDispatch # :nodoc:
336
336
  # Avoid having to pass an open file handle as the response body.
337
337
  # Rack::Sendfile will usually intercept the response and uses
338
338
  # the path directly, so there is no reason to open the file.
339
- class FileBody #:nodoc:
339
+ class FileBody # :nodoc:
340
340
  attr_reader :to_path
341
341
 
342
342
  def initialize(path)
@@ -222,7 +222,7 @@ module ActionDispatch
222
222
  if forwarded = x_forwarded_host.presence
223
223
  forwarded.split(/,\s?/).last
224
224
  else
225
- get_header("HTTP_HOST") || "#{server_name || server_addr}:#{get_header('SERVER_PORT')}"
225
+ get_header("HTTP_HOST") || "#{server_name}:#{get_header('SERVER_PORT')}"
226
226
  end
227
227
  end
228
228
 
@@ -258,12 +258,10 @@ module ActionDispatch
258
258
  # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
259
259
  # req.port # => 8080
260
260
  def port
261
- @port ||= begin
262
- if raw_host_with_port =~ /:(\d+)$/
263
- $1.to_i
264
- else
265
- standard_port
266
- end
261
+ @port ||= if raw_host_with_port =~ /:(\d+)$/
262
+ $1.to_i
263
+ else
264
+ standard_port
267
265
  end
268
266
  end
269
267
 
@@ -272,9 +270,10 @@ module ActionDispatch
272
270
  # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
273
271
  # req.standard_port # => 80
274
272
  def standard_port
275
- case protocol
276
- when "https://" then 443
277
- else 80
273
+ if "https://" == protocol
274
+ 443
275
+ else
276
+ 80
278
277
  end
279
278
  end
280
279
 
@@ -6,13 +6,13 @@ module ActionDispatch
6
6
  module Journey # :nodoc:
7
7
  module GTG # :nodoc:
8
8
  class Builder # :nodoc:
9
- DUMMY = Nodes::Dummy.new
9
+ DUMMY_END_NODE = Nodes::Dummy.new
10
10
 
11
11
  attr_reader :root, :ast, :endpoints
12
12
 
13
13
  def initialize(root)
14
14
  @root = root
15
- @ast = Nodes::Cat.new root, DUMMY
15
+ @ast = Nodes::Cat.new root, DUMMY_END_NODE
16
16
  @followpos = build_followpos
17
17
  end
18
18
 
@@ -28,12 +28,12 @@ module ActionDispatch
28
28
  marked[s] = true # mark s
29
29
 
30
30
  s.group_by { |state| symbol(state) }.each do |sym, ps|
31
- u = ps.flat_map { |l| @followpos[l] }
31
+ u = ps.flat_map { |l| @followpos[l] }.uniq
32
32
  next if u.empty?
33
33
 
34
34
  from = state_id[s]
35
35
 
36
- if u.all? { |pos| pos == DUMMY }
36
+ if u.all? { |pos| pos == DUMMY_END_NODE }
37
37
  to = state_id[Object.new]
38
38
  dtrans[from, to] = sym
39
39
  dtrans.add_accepting(to)
@@ -43,9 +43,9 @@ module ActionDispatch
43
43
  to = state_id[u]
44
44
  dtrans[from, to] = sym
45
45
 
46
- if u.include?(DUMMY)
46
+ if u.include?(DUMMY_END_NODE)
47
47
  ps.each do |state|
48
- if @followpos[state].include?(DUMMY)
48
+ if @followpos[state].include?(DUMMY_END_NODE)
49
49
  dtrans.add_memo(to, state.memo)
50
50
  end
51
51
  end
@@ -66,7 +66,10 @@ module ActionDispatch
66
66
  when Nodes::Group
67
67
  true
68
68
  when Nodes::Star
69
- true
69
+ # the default star regex is /(.+)/ which is NOT nullable
70
+ # but since different constraints can be provided we must
71
+ # actually check if this is the case or not.
72
+ node.regexp.match?("")
70
73
  when Nodes::Or
71
74
  node.children.any? { |c| nullable?(c) }
72
75
  when Nodes::Cat
@@ -104,7 +107,7 @@ module ActionDispatch
104
107
  def lastpos(node)
105
108
  case node
106
109
  when Nodes::Star
107
- firstpos(node.left)
110
+ lastpos(node.left)
108
111
  when Nodes::Or
109
112
  node.children.flat_map { |c| lastpos(c) }.tap(&:uniq!)
110
113
  when Nodes::Cat
@@ -131,10 +134,6 @@ module ActionDispatch
131
134
  lastpos(n.left).each do |i|
132
135
  table[i] += firstpos(n.right)
133
136
  end
134
- when Nodes::Star
135
- lastpos(n).each do |i|
136
- table[i] += firstpos(n)
137
- end
138
137
  end
139
138
  end
140
139
  table
@@ -14,7 +14,7 @@ module ActionDispatch
14
14
  end
15
15
 
16
16
  class Simulator # :nodoc:
17
- INITIAL_STATE = [0].freeze
17
+ INITIAL_STATE = [ [0, nil] ].freeze
18
18
 
19
19
  attr_reader :tt
20
20
 
@@ -25,13 +25,19 @@ module ActionDispatch
25
25
  def memos(string)
26
26
  input = StringScanner.new(string)
27
27
  state = INITIAL_STATE
28
+ start_index = 0
28
29
 
29
30
  while sym = input.scan(%r([/.?]|[^/.?]+))
30
- state = tt.move(state, sym)
31
+ end_index = start_index + sym.length
32
+
33
+ state = tt.move(state, string, start_index, end_index)
34
+
35
+ start_index = end_index
31
36
  end
32
37
 
33
- acceptance_states = state.each_with_object([]) do |s, memos|
34
- memos.concat(tt.memo(s)) if tt.accepting?(s)
38
+ acceptance_states = state.each_with_object([]) do |s_d, memos|
39
+ s, idx = s_d
40
+ memos.concat(tt.memo(s)) if idx.nil? && tt.accepting?(s)
35
41
  end
36
42
 
37
43
  acceptance_states.empty? ? yield : acceptance_states
@@ -10,11 +10,15 @@ module ActionDispatch
10
10
 
11
11
  attr_reader :memos
12
12
 
13
+ DEFAULT_EXP = /[^.\/?]+/
14
+ DEFAULT_EXP_ANCHORED = /\A#{DEFAULT_EXP}\Z/
15
+
13
16
  def initialize
14
- @regexp_states = {}
15
- @string_states = {}
16
- @accepting = {}
17
- @memos = Hash.new { |h, k| h[k] = [] }
17
+ @stdparam_states = {}
18
+ @regexp_states = {}
19
+ @string_states = {}
20
+ @accepting = {}
21
+ @memos = Hash.new { |h, k| h[k] = [] }
18
22
  end
19
23
 
20
24
  def add_accepting(state)
@@ -41,22 +45,54 @@ module ActionDispatch
41
45
  Array(t)
42
46
  end
43
47
 
44
- def move(t, a)
48
+ def move(t, full_string, start_index, end_index)
45
49
  return [] if t.empty?
46
50
 
47
- regexps = []
48
- strings = []
51
+ next_states = []
49
52
 
50
- t.each { |s|
51
- if states = @regexp_states[s]
52
- states.each { |re, v| regexps << v if re.match?(a) && !v.nil? }
53
+ tok = full_string.slice(start_index, end_index - start_index)
54
+ token_matches_default_component = DEFAULT_EXP_ANCHORED.match?(tok)
55
+
56
+ t.each { |s, previous_start|
57
+ if previous_start.nil?
58
+ # In the simple case of a "default" param regex do this fast-path
59
+ # and add all next states.
60
+ if token_matches_default_component && states = @stdparam_states[s]
61
+ states.each { |re, v| next_states << [v, nil].freeze if !v.nil? }
62
+ end
63
+
64
+ # When we have a literal string, we can just pull the next state
65
+ if states = @string_states[s]
66
+ next_states << [states[tok], nil].freeze unless states[tok].nil?
67
+ end
53
68
  end
54
69
 
55
- if states = @string_states[s]
56
- strings << states[a] unless states[a].nil?
70
+ # For regexes that aren't the "default" style, they may potentially
71
+ # not be terminated by the first "token" [./?], so we need to continue
72
+ # to attempt to match this regexp as well as any successful paths that
73
+ # continue out of it. both paths could be valid.
74
+ if states = @regexp_states[s]
75
+ slice_start = if previous_start.nil?
76
+ start_index
77
+ else
78
+ previous_start
79
+ end
80
+
81
+ slice_length = end_index - slice_start
82
+ curr_slice = full_string.slice(slice_start, slice_length)
83
+
84
+ states.each { |re, v|
85
+ # if we match, we can try moving past this
86
+ next_states << [v, nil].freeze if !v.nil? && re.match?(curr_slice)
87
+ }
88
+
89
+ # and regardless, we must continue accepting tokens and retrying this regexp.
90
+ # we need to remember where we started as well so we can take bigger slices.
91
+ next_states << [s, slice_start].freeze
57
92
  end
58
93
  }
59
- strings.concat regexps
94
+
95
+ next_states
60
96
  end
61
97
 
62
98
  def as_json(options = nil)
@@ -69,9 +105,10 @@ module ActionDispatch
69
105
  end
70
106
 
71
107
  {
72
- regexp_states: simple_regexp,
73
- string_states: @string_states,
74
- accepting: @accepting
108
+ regexp_states: simple_regexp,
109
+ string_states: @string_states,
110
+ stdparam_states: @stdparam_states,
111
+ accepting: @accepting
75
112
  }
76
113
  end
77
114
 
@@ -93,7 +130,7 @@ module ActionDispatch
93
130
  states = "function tt() { return #{to_json}; }"
94
131
 
95
132
  fun_routes = paths.sample(3).map do |ast|
96
- ast.map { |n|
133
+ ast.filter_map { |n|
97
134
  case n
98
135
  when Nodes::Symbol
99
136
  case n.left
@@ -106,7 +143,7 @@ module ActionDispatch
106
143
  else
107
144
  nil
108
145
  end
109
- }.compact.join
146
+ }.join
110
147
  end
111
148
 
112
149
  stylesheets = [fsm_css]
@@ -125,18 +162,33 @@ module ActionDispatch
125
162
 
126
163
  def []=(from, to, sym)
127
164
  to_mappings = states_hash_for(sym)[from] ||= {}
165
+ case sym
166
+ when Regexp
167
+ # we must match the whole string to a token boundary
168
+ if sym == DEFAULT_EXP
169
+ sym = DEFAULT_EXP_ANCHORED
170
+ else
171
+ sym = /\A#{sym}\Z/
172
+ end
173
+ when Symbol
174
+ # account for symbols in the constraints the same as strings
175
+ sym = sym.to_s
176
+ end
128
177
  to_mappings[sym] = to
129
178
  end
130
179
 
131
180
  def states
132
181
  ss = @string_states.keys + @string_states.values.flat_map(&:values)
182
+ ps = @stdparam_states.keys + @stdparam_states.values.flat_map(&:values)
133
183
  rs = @regexp_states.keys + @regexp_states.values.flat_map(&:values)
134
- (ss + rs).uniq
184
+ (ss + ps + rs).uniq
135
185
  end
136
186
 
137
187
  def transitions
138
188
  @string_states.flat_map { |from, hash|
139
189
  hash.map { |s, to| [from, s, to] }
190
+ } + @stdparam_states.flat_map { |from, hash|
191
+ hash.map { |s, to| [from, s, to] }
140
192
  } + @regexp_states.flat_map { |from, hash|
141
193
  hash.map { |s, to| [from, s, to] }
142
194
  }
@@ -145,10 +197,14 @@ module ActionDispatch
145
197
  private
146
198
  def states_hash_for(sym)
147
199
  case sym
148
- when String
200
+ when String, Symbol
149
201
  @string_states
150
202
  when Regexp
151
- @regexp_states
203
+ if sym == DEFAULT_EXP
204
+ @stdparam_states
205
+ else
206
+ @regexp_states
207
+ end
152
208
  else
153
209
  raise ArgumentError, "unknown symbol: %s" % sym.class
154
210
  end
@@ -4,6 +4,66 @@ require "action_dispatch/journey/visitors"
4
4
 
5
5
  module ActionDispatch
6
6
  module Journey # :nodoc:
7
+ class Ast # :nodoc:
8
+ attr_reader :names, :path_params, :tree, :wildcard_options, :terminals
9
+ alias :root :tree
10
+
11
+ def initialize(tree, formatted)
12
+ @tree = tree
13
+ @path_params = []
14
+ @names = []
15
+ @symbols = []
16
+ @stars = []
17
+ @terminals = []
18
+ @wildcard_options = {}
19
+
20
+ visit_tree(formatted)
21
+ end
22
+
23
+ def requirements=(requirements)
24
+ # inject any regexp requirements for `star` nodes so they can be
25
+ # determined nullable, which requires knowing if the regex accepts an
26
+ # empty string.
27
+ (symbols + stars).each do |node|
28
+ re = requirements[node.to_sym]
29
+ node.regexp = re if re
30
+ end
31
+ end
32
+
33
+ def route=(route)
34
+ terminals.each { |n| n.memo = route }
35
+ end
36
+
37
+ def glob?
38
+ stars.any?
39
+ end
40
+
41
+ private
42
+ attr_reader :symbols, :stars
43
+
44
+ def visit_tree(formatted)
45
+ tree.each do |node|
46
+ if node.symbol?
47
+ path_params << node.to_sym
48
+ names << node.name
49
+ symbols << node
50
+ elsif node.star?
51
+ stars << node
52
+
53
+ if formatted != false
54
+ # Add a constraint for wildcard route to make it non-greedy and
55
+ # match the optional format part of the route by default.
56
+ wildcard_options[node.name.to_sym] ||= /.+?/
57
+ end
58
+ end
59
+
60
+ if node.terminal?
61
+ terminals << node
62
+ end
63
+ end
64
+ end
65
+ end
66
+
7
67
  module Nodes # :nodoc:
8
68
  class Node # :nodoc:
9
69
  include Enumerable
@@ -78,7 +138,7 @@ module ActionDispatch
78
138
  alias :symbol :regexp
79
139
  attr_reader :name
80
140
 
81
- DEFAULT_EXP = /[^\.\/\?]+/
141
+ DEFAULT_EXP = /[^.\/?]+/
82
142
  GREEDY_EXP = /(.+)/
83
143
  def initialize(left, regexp = DEFAULT_EXP)
84
144
  super(left)
@@ -86,10 +146,6 @@ module ActionDispatch
86
146
  @name = -left.tr("*:", "")
87
147
  end
88
148
 
89
- def default_regexp?
90
- regexp == DEFAULT_EXP
91
- end
92
-
93
149
  def type; :SYMBOL; end
94
150
  def symbol?; true; end
95
151
  end
@@ -104,6 +160,15 @@ module ActionDispatch
104
160
  end
105
161
 
106
162
  class Star < Unary # :nodoc:
163
+ attr_accessor :regexp
164
+
165
+ def initialize(left)
166
+ super(left)
167
+
168
+ # By default wildcard routes are non-greedy and must match something.
169
+ @regexp = /.+?/
170
+ end
171
+
107
172
  def star?; true; end
108
173
  def type; :STAR; end
109
174
 
@@ -4,15 +4,16 @@ module ActionDispatch
4
4
  module Journey # :nodoc:
5
5
  module Path # :nodoc:
6
6
  class Pattern # :nodoc:
7
- attr_reader :spec, :requirements, :anchored
7
+ attr_reader :ast, :names, :requirements, :anchored, :spec
8
8
 
9
9
  def initialize(ast, requirements, separators, anchored)
10
- @spec = ast
10
+ @ast = ast
11
+ @spec = ast.root
11
12
  @requirements = requirements
12
13
  @separators = separators
13
14
  @anchored = anchored
14
15
 
15
- @names = nil
16
+ @names = ast.names
16
17
  @optional_names = nil
17
18
  @required_names = nil
18
19
  @re = nil
@@ -27,20 +28,28 @@ module ActionDispatch
27
28
  required_names
28
29
  offsets
29
30
  to_regexp
30
- nil
31
+ @ast = nil
31
32
  end
32
33
 
33
- def ast
34
- @spec.find_all(&:symbol?).each do |node|
35
- re = @requirements[node.to_sym]
36
- node.regexp = re if re
37
- end
34
+ def requirements_anchored?
35
+ # each required param must not be surrounded by a literal, otherwise it isn't simple to chunk-match the url piecemeal
36
+ terminals = ast.terminals
38
37
 
39
- @spec
40
- end
38
+ terminals.each_with_index { |s, index|
39
+ next if index < 1
40
+ next if s.type == :DOT || s.type == :SLASH
41
+
42
+ back = terminals[index - 1]
43
+ fwd = terminals[index + 1]
44
+
45
+ # we also don't support this yet, constraints must be regexps
46
+ return false if s.symbol? && s.regexp.is_a?(Array)
47
+
48
+ return false if back.literal?
49
+ return false if !fwd.nil? && fwd.literal?
50
+ }
41
51
 
42
- def names
43
- @names ||= spec.find_all(&:symbol?).map(&:name)
52
+ true
44
53
  end
45
54
 
46
55
  def required_names
@@ -5,7 +5,7 @@ module ActionDispatch
5
5
  module Journey
6
6
  class Route
7
7
  attr_reader :app, :path, :defaults, :name, :precedence, :constraints,
8
- :internal, :scope_options
8
+ :internal, :scope_options, :ast
9
9
 
10
10
  alias :conditions :constraints
11
11
 
@@ -65,29 +65,22 @@ module ActionDispatch
65
65
  @_required_defaults = required_defaults
66
66
  @required_parts = nil
67
67
  @parts = nil
68
- @decorated_ast = nil
69
68
  @precedence = precedence
70
69
  @path_formatter = @path.build_formatter
71
70
  @scope_options = scope_options
72
71
  @internal = internal
72
+
73
+ @ast = @path.ast.root
74
+ @path.ast.route = self
73
75
  end
74
76
 
75
77
  def eager_load!
76
78
  path.eager_load!
77
- ast
78
79
  parts
79
80
  required_defaults
80
81
  nil
81
82
  end
82
83
 
83
- def ast
84
- @decorated_ast ||= begin
85
- decorated_ast = path.ast
86
- decorated_ast.find_all(&:terminal?).each { |n| n.memo = self }
87
- decorated_ast
88
- end
89
- end
90
-
91
84
  # Needed for `bin/rails routes`. Picks up succinctly defined requirements
92
85
  # for a route, for example route
93
86
  #
@@ -142,7 +135,7 @@ module ActionDispatch
142
135
  end
143
136
 
144
137
  def glob?
145
- path.spec.any?(Nodes::Star)
138
+ path.ast.glob?
146
139
  end
147
140
 
148
141
  def dispatcher?
@@ -35,7 +35,7 @@ module ActionDispatch
35
35
  US_ASCII = Encoding::US_ASCII
36
36
  UTF_8 = Encoding::UTF_8
37
37
  EMPTY = (+"").force_encoding(US_ASCII).freeze
38
- DEC2HEX = (0..255).to_a.map { |i| ENCODE % i }.map { |s| s.force_encoding(US_ASCII) }
38
+ DEC2HEX = (0..255).map { |i| (ENCODE % i).force_encoding(US_ASCII) }
39
39
 
40
40
  ALPHA = "a-zA-Z"
41
41
  DIGIT = "0-9"
@@ -44,7 +44,7 @@ module ActionDispatch
44
44
 
45
45
  ESCAPED = /%[a-zA-Z0-9]{2}/.freeze
46
46
 
47
- FRAGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/\?]/.freeze
47
+ FRAGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/?]/.freeze
48
48
  SEGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@]/.freeze
49
49
  PATH = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/]/.freeze
50
50
 
@@ -85,7 +85,7 @@ module ActionDispatch
85
85
  private
86
86
  def partitioned_routes
87
87
  routes.partition { |r|
88
- r.path.anchored && r.ast.grep(Nodes::Symbol).all? { |n| n.default_regexp? }
88
+ r.path.anchored && r.path.requirements_anchored?
89
89
  }
90
90
  end
91
91
 
@@ -41,7 +41,7 @@ module ActionDispatch
41
41
  end
42
42
 
43
43
  def partition_route(route)
44
- if route.path.anchored && route.ast.grep(Nodes::Symbol).all?(&:default_regexp?)
44
+ if route.path.anchored && route.path.requirements_anchored?
45
45
  anchored_routes << route
46
46
  else
47
47
  custom_routes << route
@@ -50,8 +50,8 @@ module ActionDispatch
50
50
 
51
51
  def ast
52
52
  @ast ||= begin
53
- asts = anchored_routes.map(&:ast)
54
- Nodes::Or.new(asts)
53
+ nodes = anchored_routes.map(&:ast)
54
+ Nodes::Or.new(nodes)
55
55
  end
56
56
  end
57
57