actionpack 6.0.0.beta2 → 6.0.2.rc1

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +116 -3
  3. data/README.rdoc +2 -1
  4. data/lib/action_controller.rb +4 -1
  5. data/lib/action_controller/metal.rb +3 -3
  6. data/lib/action_controller/metal/exceptions.rb +1 -1
  7. data/lib/action_controller/metal/helpers.rb +1 -1
  8. data/lib/action_controller/metal/live.rb +1 -1
  9. data/lib/action_controller/metal/mime_responds.rb +1 -1
  10. data/lib/action_controller/metal/params_wrapper.rb +2 -2
  11. data/lib/action_controller/metal/renderers.rb +4 -4
  12. data/lib/action_controller/metal/rendering.rb +1 -1
  13. data/lib/action_controller/metal/request_forgery_protection.rb +2 -2
  14. data/lib/action_controller/metal/strong_parameters.rb +5 -11
  15. data/lib/action_controller/renderer.rb +2 -2
  16. data/lib/action_controller/template_assertions.rb +1 -1
  17. data/lib/action_controller/test_case.rb +3 -2
  18. data/lib/action_dispatch.rb +1 -1
  19. data/lib/action_dispatch/http/content_security_policy.rb +20 -9
  20. data/lib/action_dispatch/http/mime_negotiation.rb +5 -0
  21. data/lib/action_dispatch/http/mime_type.rb +13 -1
  22. data/lib/action_dispatch/http/request.rb +2 -1
  23. data/lib/action_dispatch/http/response.rb +27 -7
  24. data/lib/action_dispatch/journey/formatter.rb +2 -2
  25. data/lib/action_dispatch/journey/path/pattern.rb +6 -1
  26. data/lib/action_dispatch/journey/route.rb +5 -4
  27. data/lib/action_dispatch/journey/routes.rb +0 -1
  28. data/lib/action_dispatch/middleware/actionable_exceptions.rb +39 -0
  29. data/lib/action_dispatch/middleware/cookies.rb +7 -3
  30. data/lib/action_dispatch/middleware/debug_exceptions.rb +8 -2
  31. data/lib/action_dispatch/middleware/debug_view.rb +12 -0
  32. data/lib/action_dispatch/middleware/exception_wrapper.rb +1 -0
  33. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -2
  34. data/lib/action_dispatch/middleware/remote_ip.rb +3 -3
  35. data/lib/action_dispatch/middleware/session/cookie_store.rb +4 -3
  36. data/lib/action_dispatch/middleware/stack.rb +34 -2
  37. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  38. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  39. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +3 -1
  40. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  41. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +6 -2
  42. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
  43. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +3 -0
  44. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +2 -0
  45. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +4 -0
  46. data/lib/action_dispatch/railtie.rb +6 -2
  47. data/lib/action_dispatch/routing.rb +4 -4
  48. data/lib/action_dispatch/routing/mapper.rb +28 -13
  49. data/lib/action_dispatch/routing/route_set.rb +13 -15
  50. data/lib/action_dispatch/system_test_case.rb +22 -3
  51. data/lib/action_dispatch/system_testing/browser.rb +23 -0
  52. data/lib/action_dispatch/system_testing/driver.rb +2 -0
  53. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +2 -1
  54. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +7 -6
  55. data/lib/action_dispatch/testing/assertions.rb +1 -1
  56. data/lib/action_dispatch/testing/request_encoder.rb +2 -2
  57. data/lib/action_dispatch/testing/test_response.rb +1 -1
  58. data/lib/action_pack/gem_version.rb +2 -2
  59. metadata +20 -15
  60. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +0 -26
@@ -79,6 +79,11 @@ module ActionDispatch
79
79
  else
80
80
  [Mime[:html]]
81
81
  end
82
+
83
+ v = v.select do |format|
84
+ format.symbol || format.ref == "*/*"
85
+ end
86
+
82
87
  set_header k, v
83
88
  end
84
89
  end
@@ -170,6 +170,7 @@ module Mime
170
170
  def parse(accept_header)
171
171
  if !accept_header.include?(",")
172
172
  accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first
173
+ return [] unless accept_header
173
174
  parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact
174
175
  else
175
176
  list, index = [], 0
@@ -221,7 +222,18 @@ module Mime
221
222
 
222
223
  attr_reader :hash
223
224
 
225
+ MIME_NAME = "[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}"
226
+ MIME_PARAMETER_KEY = "[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}"
227
+ MIME_PARAMETER_VALUE = "#{Regexp.escape('"')}?[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}#{Regexp.escape('"')}?"
228
+ MIME_PARAMETER = "\s*\;\s+#{MIME_PARAMETER_KEY}(?:\=#{MIME_PARAMETER_VALUE})?"
229
+ MIME_REGEXP = /\A(?:\*\/\*|#{MIME_NAME}\/(?:\*|#{MIME_NAME})(?:\s*#{MIME_PARAMETER}\s*)*)\z/
230
+
231
+ class InvalidMimeType < StandardError; end
232
+
224
233
  def initialize(string, symbol = nil, synonyms = [])
234
+ if string.nil? || ! MIME_REGEXP.match?(string)
235
+ raise InvalidMimeType, "#{string.inspect} is not a valid MIME type"
236
+ end
225
237
  @symbol, @synonyms = symbol, synonyms
226
238
  @string = string
227
239
  @hash = [@string, @synonyms, @symbol].hash
@@ -303,7 +315,7 @@ module Mime
303
315
  include Singleton
304
316
 
305
317
  def initialize
306
- super "*/*", :all
318
+ super "*/*", nil
307
319
  end
308
320
 
309
321
  def all?; true; end
@@ -264,7 +264,8 @@ module ActionDispatch
264
264
  # (case-insensitive), which may need to be manually added depending on the
265
265
  # choice of JavaScript libraries and frameworks.
266
266
  def xml_http_request?
267
- get_header("HTTP_X_REQUESTED_WITH") =~ /XMLHttpRequest/i
267
+ header = get_header("HTTP_X_REQUESTED_WITH")
268
+ header && /XMLHttpRequest/i.match?(header)
268
269
  end
269
270
  alias :xhr? :xml_http_request?
270
271
 
@@ -85,6 +85,7 @@ module ActionDispatch # :nodoc:
85
85
 
86
86
  cattr_accessor :default_charset, default: "utf-8"
87
87
  cattr_accessor :default_headers
88
+ cattr_accessor :return_only_media_type_on_content_type, default: false
88
89
 
89
90
  include Rack::Response::Helpers
90
91
  # Aliasing these off because AD::Http::Cache::Response defines them.
@@ -242,8 +243,22 @@ module ActionDispatch # :nodoc:
242
243
  end
243
244
 
244
245
  # Content type of response.
245
- # It returns just MIME type and does NOT contain charset part.
246
246
  def content_type
247
+ if self.class.return_only_media_type_on_content_type
248
+ ActiveSupport::Deprecation.warn(
249
+ "Rails 6.1 will return Content-Type header without modification." \
250
+ " If you want just the MIME type, please use `#media_type` instead."
251
+ )
252
+
253
+ content_type = super
254
+ content_type ? content_type.split(/;\s*charset=/)[0].presence : content_type
255
+ else
256
+ super.presence
257
+ end
258
+ end
259
+
260
+ # Media type of response.
261
+ def media_type
247
262
  parsed_content_type_header.mime_type
248
263
  end
249
264
 
@@ -404,15 +419,18 @@ module ActionDispatch # :nodoc:
404
419
  end
405
420
 
406
421
  private
407
-
408
422
  ContentTypeHeader = Struct.new :mime_type, :charset
409
423
  NullContentTypeHeader = ContentTypeHeader.new nil, nil
410
424
 
425
+ CONTENT_TYPE_PARSER = /
426
+ \A
427
+ (?<mime_type>[^;\s]+\s*(?:;\s*(?:(?!charset)[^;\s])+)*)?
428
+ (?:;\s*charset=(?<quote>"?)(?<charset>[^;\s]+)\k<quote>)?
429
+ /x # :nodoc:
430
+
411
431
  def parse_content_type(content_type)
412
- if content_type
413
- type, charset = content_type.split(/;\s*charset=/)
414
- type = nil if type && type.empty?
415
- ContentTypeHeader.new(type, charset)
432
+ if content_type && match = CONTENT_TYPE_PARSER.match(content_type)
433
+ ContentTypeHeader.new(match[:mime_type], match[:charset])
416
434
  else
417
435
  NullContentTypeHeader
418
436
  end
@@ -459,7 +477,7 @@ module ActionDispatch # :nodoc:
459
477
  end
460
478
 
461
479
  def assign_default_content_type_and_charset!
462
- return if content_type
480
+ return if media_type
463
481
 
464
482
  ct = parsed_content_type_header
465
483
  set_content_type(ct.mime_type || Mime[:html].to_s,
@@ -517,4 +535,6 @@ module ActionDispatch # :nodoc:
517
535
  end
518
536
  end
519
537
  end
538
+
539
+ ActiveSupport.run_load_hooks(:action_dispatch_response, Response)
520
540
  end
@@ -67,7 +67,7 @@ module ActionDispatch
67
67
  parameterized_parts = recall.merge(options)
68
68
 
69
69
  keys_to_keep = route.parts.reverse_each.drop_while { |part|
70
- !options.key?(part) || (options[part] || recall[part]).nil?
70
+ !(options.key?(part) || route.scope_options.key?(part)) || (options[part] || recall[part]).nil?
71
71
  } | route.required_parts
72
72
 
73
73
  parameterized_parts.delete_if do |bad_key, _|
@@ -152,7 +152,7 @@ module ActionDispatch
152
152
  missing_keys << key
153
153
  end
154
154
  else
155
- unless /\A#{tests[key]}\Z/ === parts[key]
155
+ if parts[key].nil? || !/\A#{tests[key]}\Z/.match?(parts[key])
156
156
  missing_keys ||= []
157
157
  missing_keys << key
158
158
  end
@@ -119,7 +119,8 @@ module ActionDispatch
119
119
 
120
120
  class UnanchoredRegexp < AnchoredRegexp # :nodoc:
121
121
  def accept(node)
122
- %r{\A#{visit node}(?:\b|\Z)}
122
+ path = visit node
123
+ path == "/" ? %r{\A/} : %r{\A#{path}(?:\b|\Z|/)}
123
124
  end
124
125
  end
125
126
 
@@ -136,6 +137,10 @@ module ActionDispatch
136
137
  Array.new(length - 1) { |i| self[i + 1] }
137
138
  end
138
139
 
140
+ def named_captures
141
+ @names.zip(captures).to_h
142
+ end
143
+
139
144
  def [](x)
140
145
  idx = @offsets[x - 1] + x
141
146
  @match[idx]
@@ -4,9 +4,9 @@ module ActionDispatch
4
4
  # :stopdoc:
5
5
  module Journey
6
6
  class Route
7
- attr_reader :app, :path, :defaults, :name, :precedence
7
+ attr_reader :app, :path, :defaults, :name, :precedence, :constraints,
8
+ :internal, :scope_options
8
9
 
9
- attr_reader :constraints, :internal
10
10
  alias :conditions :constraints
11
11
 
12
12
  module VerbMatchers
@@ -51,13 +51,13 @@ module ActionDispatch
51
51
 
52
52
  def self.build(name, app, path, constraints, required_defaults, defaults)
53
53
  request_method_match = verb_matcher(constraints.delete(:request_method))
54
- new name, app, path, constraints, required_defaults, defaults, request_method_match, 0
54
+ new name, app, path, constraints, required_defaults, defaults, request_method_match, 0, {}
55
55
  end
56
56
 
57
57
  ##
58
58
  # +path+ is a path constraint.
59
59
  # +constraints+ is a hash of constraints to be applied to this route.
60
- def initialize(name, app, path, constraints, required_defaults, defaults, request_method_match, precedence, internal = false)
60
+ def initialize(name, app, path, constraints, required_defaults, defaults, request_method_match, precedence, scope_options, internal = false)
61
61
  @name = name
62
62
  @app = app
63
63
  @path = path
@@ -72,6 +72,7 @@ module ActionDispatch
72
72
  @decorated_ast = nil
73
73
  @precedence = precedence
74
74
  @path_formatter = @path.build_formatter
75
+ @scope_options = scope_options
75
76
  @internal = internal
76
77
  end
77
78
 
@@ -56,7 +56,6 @@ module ActionDispatch
56
56
  end
57
57
 
58
58
  def simulator
59
- return if ast.nil?
60
59
  @simulator ||= begin
61
60
  gtg = GTG::Builder.new(ast).transition_table
62
61
  GTG::Simulator.new(gtg)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "action_dispatch/http/request"
5
+ require "active_support/actionable_error"
6
+
7
+ module ActionDispatch
8
+ class ActionableExceptions # :nodoc:
9
+ cattr_accessor :endpoint, default: "/rails/actions"
10
+
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env)
16
+ request = ActionDispatch::Request.new(env)
17
+ return @app.call(env) unless actionable_request?(request)
18
+
19
+ ActiveSupport::ActionableError.dispatch(request.params[:error].to_s.safe_constantize, request.params[:action])
20
+
21
+ redirect_to request.params[:location]
22
+ end
23
+
24
+ private
25
+ def actionable_request?(request)
26
+ request.show_exceptions? && request.post? && request.path == endpoint
27
+ end
28
+
29
+ def redirect_to(location)
30
+ body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>"
31
+
32
+ [302, {
33
+ "Content-Type" => "text/html; charset=#{Response.default_charset}",
34
+ "Content-Length" => body.bytesize.to_s,
35
+ "Location" => location,
36
+ }, [body]]
37
+ end
38
+ end
39
+ end
@@ -338,7 +338,7 @@ module ActionDispatch
338
338
 
339
339
  def update_cookies_from_jar
340
340
  request_jar = @request.cookie_jar.instance_variable_get(:@cookies)
341
- set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) }
341
+ set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) || @set_cookies.key?(k) }
342
342
 
343
343
  @cookies.update set_cookies if set_cookies
344
344
  end
@@ -534,9 +534,13 @@ module ActionDispatch
534
534
  if value
535
535
  case
536
536
  when needs_migration?(value)
537
- self[name] = Marshal.load(value)
537
+ Marshal.load(value).tap do |v|
538
+ self[name] = { value: v }
539
+ end
538
540
  when rotate
539
- self[name] = serializer.load(value)
541
+ serializer.load(value).tap do |v|
542
+ self[name] = { value: v }
543
+ end
540
544
  else
541
545
  serializer.load(value)
542
546
  end
@@ -4,6 +4,8 @@ require "action_dispatch/http/request"
4
4
  require "action_dispatch/middleware/exception_wrapper"
5
5
  require "action_dispatch/routing/inspector"
6
6
 
7
+ require "active_support/actionable_error"
8
+
7
9
  require "action_view"
8
10
  require "action_view/base"
9
11
 
@@ -60,7 +62,11 @@ module ActionDispatch
60
62
  log_error(request, wrapper)
61
63
 
62
64
  if request.get_header("action_dispatch.show_detailed_exceptions")
63
- content_type = request.formats.first
65
+ begin
66
+ content_type = request.formats.first
67
+ rescue Mime::Type::InvalidMimeType
68
+ render_for_api_request(Mime[:text], wrapper)
69
+ end
64
70
 
65
71
  if api_request?(content_type)
66
72
  render_for_api_request(content_type, wrapper)
@@ -142,7 +148,7 @@ module ActionDispatch
142
148
  message = []
143
149
  message << " "
144
150
  message << "#{exception.class} (#{exception.message}):"
145
- message.concat(exception.annoted_source_code) if exception.respond_to?(:annoted_source_code)
151
+ message.concat(exception.annotated_source_code) if exception.respond_to?(:annotated_source_code)
146
152
  message << " "
147
153
  message.concat(trace)
148
154
 
@@ -52,5 +52,17 @@ module ActionDispatch
52
52
  super
53
53
  end
54
54
  end
55
+
56
+ def protect_against_forgery?
57
+ false
58
+ end
59
+
60
+ def params_valid?
61
+ begin
62
+ @request.parameters
63
+ rescue ActionController::BadRequest
64
+ false
65
+ end
66
+ end
55
67
  end
56
68
  end
@@ -12,6 +12,7 @@ module ActionDispatch
12
12
  "ActionController::UnknownHttpMethod" => :method_not_allowed,
13
13
  "ActionController::NotImplemented" => :not_implemented,
14
14
  "ActionController::UnknownFormat" => :not_acceptable,
15
+ "Mime::Type::InvalidMimeType" => :not_acceptable,
15
16
  "ActionController::MissingExactTemplate" => :not_acceptable,
16
17
  "ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
17
18
  "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
@@ -21,8 +21,12 @@ module ActionDispatch
21
21
  def call(env)
22
22
  request = ActionDispatch::Request.new(env)
23
23
  status = request.path_info[1..-1].to_i
24
- content_type = request.formats.first
25
- body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
24
+ begin
25
+ content_type = request.formats.first
26
+ rescue Mime::Type::InvalidMimeType
27
+ content_type = Mime[:text]
28
+ end
29
+ body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
26
30
 
27
31
  render(status, content_type, body)
28
32
  end
@@ -8,13 +8,13 @@ module ActionDispatch
8
8
  # contain the address, and then picking the last-set address that is not
9
9
  # on the list of trusted IPs. This follows the precedent set by e.g.
10
10
  # {the Tomcat server}[https://issues.apache.org/bugzilla/show_bug.cgi?id=50453],
11
- # with {reasoning explained at length}[http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection]
11
+ # with {reasoning explained at length}[https://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection]
12
12
  # by @gingerlime. A more detailed explanation of the algorithm is given
13
13
  # at GetIp#calculate_ip.
14
14
  #
15
15
  # Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2]
16
16
  # requires. Some Rack servers simply drop preceding headers, and only report
17
- # the value that was {given in the last header}[http://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
17
+ # the value that was {given in the last header}[https://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
18
18
  # If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn)
19
19
  # then you should test your Rack server to make sure your data is good.
20
20
  #
@@ -102,7 +102,7 @@ module ActionDispatch
102
102
  # proxies, that header may contain a list of IPs. Other proxy services
103
103
  # set the Client-Ip header instead, so we check that too.
104
104
  #
105
- # As discussed in {this post about Rails IP Spoofing}[http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/],
105
+ # As discussed in {this post about Rails IP Spoofing}[https://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/],
106
106
  # while the first IP in the list is likely to be the "originating" IP,
107
107
  # it could also have been set by the client maliciously.
108
108
  #
@@ -24,9 +24,10 @@ module ActionDispatch
24
24
  #
25
25
  # Rails.application.config.session_store :cookie_store, key: '_your_app_session'
26
26
  #
27
- # By default, your secret key base is derived from your application name in
28
- # the test and development environments. In all other environments, it is stored
29
- # encrypted in the <tt>config/credentials.yml.enc</tt> file.
27
+ # In the development and test environments your application's secret key base is
28
+ # generated by Rails and stored in a temporary file in <tt>tmp/development_secret.txt</tt>.
29
+ # In all other environments, it is stored encrypted in the
30
+ # <tt>config/credentials.yml.enc</tt> file.
30
31
  #
31
32
  # If your application was not updated to Rails 5.2 defaults, the secret_key_base
32
33
  # will be found in the old <tt>config/secrets.yml</tt> file.
@@ -36,6 +36,31 @@ module ActionDispatch
36
36
  def build(app)
37
37
  klass.new(app, *args, &block)
38
38
  end
39
+
40
+ def build_instrumented(app)
41
+ InstrumentationProxy.new(build(app), inspect)
42
+ end
43
+ end
44
+
45
+ # This class is used to instrument the execution of a single middleware.
46
+ # It proxies the `call` method transparently and instruments the method
47
+ # call.
48
+ class InstrumentationProxy
49
+ EVENT_NAME = "process_middleware.action_dispatch"
50
+
51
+ def initialize(middleware, class_name)
52
+ @middleware = middleware
53
+
54
+ @payload = {
55
+ middleware: class_name,
56
+ }
57
+ end
58
+
59
+ def call(env)
60
+ ActiveSupport::Notifications.instrument(EVENT_NAME, @payload) do
61
+ @middleware.call(env)
62
+ end
63
+ end
39
64
  end
40
65
 
41
66
  include Enumerable
@@ -97,8 +122,15 @@ module ActionDispatch
97
122
  middlewares.push(build_middleware(klass, args, block))
98
123
  end
99
124
 
100
- def build(app = Proc.new)
101
- middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }
125
+ def build(app = nil, &block)
126
+ instrumenting = ActiveSupport::Notifications.notifier.listening?(InstrumentationProxy::EVENT_NAME)
127
+ middlewares.freeze.reverse.inject(app || block) do |a, e|
128
+ if instrumenting
129
+ e.build_instrumented(a)
130
+ else
131
+ e.build(a)
132
+ end
133
+ end
102
134
  end
103
135
 
104
136
  private
@@ -0,0 +1,13 @@
1
+ <% actions = ActiveSupport::ActionableError.actions(exception) %>
2
+
3
+ <% if actions.any? %>
4
+ <div class="actions">
5
+ <% actions.each do |action, _| %>
6
+ <%= button_to action, ActionDispatch::ActionableExceptions.endpoint, params: {
7
+ error: exception.class.name,
8
+ action: action,
9
+ location: request.path
10
+ } %>
11
+ <% end %>
12
+ </div>
13
+ <% end %>