actionpack 5.2.0 → 6.1.4

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 (156) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +408 -190
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -3
  5. data/lib/abstract_controller/base.rb +38 -4
  6. data/lib/abstract_controller/caching/fragments.rb +6 -22
  7. data/lib/abstract_controller/caching.rb +1 -1
  8. data/lib/abstract_controller/callbacks.rb +14 -2
  9. data/lib/abstract_controller/collector.rb +1 -2
  10. data/lib/abstract_controller/helpers.rb +106 -90
  11. data/lib/abstract_controller/railties/routes_helpers.rb +17 -1
  12. data/lib/abstract_controller/rendering.rb +9 -9
  13. data/lib/abstract_controller/translation.rb +11 -5
  14. data/lib/abstract_controller.rb +2 -0
  15. data/lib/action_controller/api.rb +4 -3
  16. data/lib/action_controller/base.rb +6 -9
  17. data/lib/action_controller/caching.rb +1 -3
  18. data/lib/action_controller/log_subscriber.rb +10 -7
  19. data/lib/action_controller/metal/basic_implicit_render.rb +1 -1
  20. data/lib/action_controller/metal/conditional_get.rb +19 -5
  21. data/lib/action_controller/metal/content_security_policy.rb +1 -2
  22. data/lib/action_controller/metal/cookies.rb +3 -1
  23. data/lib/action_controller/metal/data_streaming.rb +6 -7
  24. data/lib/action_controller/metal/default_headers.rb +17 -0
  25. data/lib/action_controller/metal/etag_with_template_digest.rb +4 -6
  26. data/lib/action_controller/metal/exceptions.rb +56 -2
  27. data/lib/action_controller/metal/flash.rb +5 -5
  28. data/lib/action_controller/metal/head.rb +7 -4
  29. data/lib/action_controller/metal/helpers.rb +14 -5
  30. data/lib/action_controller/metal/http_authentication.rb +25 -24
  31. data/lib/action_controller/metal/implicit_render.rb +5 -15
  32. data/lib/action_controller/metal/instrumentation.rb +13 -14
  33. data/lib/action_controller/metal/live.rb +39 -32
  34. data/lib/action_controller/metal/logging.rb +20 -0
  35. data/lib/action_controller/metal/mime_responds.rb +19 -4
  36. data/lib/action_controller/metal/parameter_encoding.rb +35 -4
  37. data/lib/action_controller/metal/params_wrapper.rb +33 -23
  38. data/lib/action_controller/metal/permissions_policy.rb +46 -0
  39. data/lib/action_controller/metal/redirecting.rb +7 -7
  40. data/lib/action_controller/metal/renderers.rb +4 -4
  41. data/lib/action_controller/metal/rendering.rb +8 -3
  42. data/lib/action_controller/metal/request_forgery_protection.rb +89 -36
  43. data/lib/action_controller/metal/rescue.rb +1 -1
  44. data/lib/action_controller/metal/streaming.rb +0 -1
  45. data/lib/action_controller/metal/strong_parameters.rb +181 -69
  46. data/lib/action_controller/metal/url_for.rb +1 -1
  47. data/lib/action_controller/metal.rb +12 -10
  48. data/lib/action_controller/railties/helpers.rb +1 -1
  49. data/lib/action_controller/renderer.rb +37 -13
  50. data/lib/action_controller/template_assertions.rb +1 -1
  51. data/lib/action_controller/test_case.rb +81 -70
  52. data/lib/action_controller.rb +7 -4
  53. data/lib/action_dispatch/http/cache.rb +34 -28
  54. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  55. data/lib/action_dispatch/http/content_security_policy.rb +47 -24
  56. data/lib/action_dispatch/http/filter_parameters.rb +9 -8
  57. data/lib/action_dispatch/http/filter_redirect.rb +2 -3
  58. data/lib/action_dispatch/http/headers.rb +4 -4
  59. data/lib/action_dispatch/http/mime_negotiation.rb +31 -13
  60. data/lib/action_dispatch/http/mime_type.rb +43 -24
  61. data/lib/action_dispatch/http/parameters.rb +14 -23
  62. data/lib/action_dispatch/http/permissions_policy.rb +173 -0
  63. data/lib/action_dispatch/http/request.rb +45 -22
  64. data/lib/action_dispatch/http/response.rb +45 -25
  65. data/lib/action_dispatch/http/upload.rb +9 -1
  66. data/lib/action_dispatch/http/url.rb +82 -82
  67. data/lib/action_dispatch/journey/formatter.rb +55 -31
  68. data/lib/action_dispatch/journey/gtg/builder.rb +22 -37
  69. data/lib/action_dispatch/journey/gtg/simulator.rb +8 -7
  70. data/lib/action_dispatch/journey/gtg/transition_table.rb +6 -5
  71. data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
  72. data/lib/action_dispatch/journey/nodes/node.rb +13 -11
  73. data/lib/action_dispatch/journey/parser.rb +13 -13
  74. data/lib/action_dispatch/journey/parser.y +1 -1
  75. data/lib/action_dispatch/journey/path/pattern.rb +21 -22
  76. data/lib/action_dispatch/journey/route.rb +10 -20
  77. data/lib/action_dispatch/journey/router/utils.rb +14 -12
  78. data/lib/action_dispatch/journey/router.rb +26 -34
  79. data/lib/action_dispatch/journey/routes.rb +1 -2
  80. data/lib/action_dispatch/journey/scanner.rb +10 -4
  81. data/lib/action_dispatch/journey/visitors.rb +1 -4
  82. data/lib/action_dispatch/journey.rb +0 -2
  83. data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
  84. data/lib/action_dispatch/middleware/callbacks.rb +2 -4
  85. data/lib/action_dispatch/middleware/cookies.rb +128 -109
  86. data/lib/action_dispatch/middleware/debug_exceptions.rb +43 -66
  87. data/lib/action_dispatch/middleware/debug_locks.rb +5 -5
  88. data/lib/action_dispatch/middleware/debug_view.rb +66 -0
  89. data/lib/action_dispatch/middleware/exception_wrapper.rb +75 -30
  90. data/lib/action_dispatch/middleware/flash.rb +2 -2
  91. data/lib/action_dispatch/middleware/host_authorization.rb +130 -0
  92. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -3
  93. data/lib/action_dispatch/middleware/remote_ip.rb +14 -16
  94. data/lib/action_dispatch/middleware/request_id.rb +5 -6
  95. data/lib/action_dispatch/middleware/session/abstract_store.rb +15 -2
  96. data/lib/action_dispatch/middleware/session/cache_store.rb +11 -6
  97. data/lib/action_dispatch/middleware/session/cookie_store.rb +24 -19
  98. data/lib/action_dispatch/middleware/show_exceptions.rb +3 -2
  99. data/lib/action_dispatch/middleware/ssl.rb +20 -15
  100. data/lib/action_dispatch/middleware/stack.rb +57 -3
  101. data/lib/action_dispatch/middleware/static.rb +153 -93
  102. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  103. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  104. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  105. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +3 -1
  106. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  107. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +4 -2
  108. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +45 -35
  109. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
  110. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
  111. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +23 -4
  112. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
  113. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +6 -3
  114. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +4 -1
  115. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +104 -8
  116. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  117. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +2 -2
  119. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -1
  120. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +2 -2
  121. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  122. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +24 -1
  123. data/lib/action_dispatch/railtie.rb +8 -2
  124. data/lib/action_dispatch/request/session.rb +17 -10
  125. data/lib/action_dispatch/request/utils.rb +28 -2
  126. data/lib/action_dispatch/routing/inspector.rb +101 -53
  127. data/lib/action_dispatch/routing/mapper.rb +156 -103
  128. data/lib/action_dispatch/routing/polymorphic_routes.rb +21 -19
  129. data/lib/action_dispatch/routing/redirection.rb +4 -4
  130. data/lib/action_dispatch/routing/route_set.rb +71 -69
  131. data/lib/action_dispatch/routing/url_for.rb +3 -3
  132. data/lib/action_dispatch/routing.rb +21 -20
  133. data/lib/action_dispatch/system_test_case.rb +54 -11
  134. data/lib/action_dispatch/system_testing/browser.rb +53 -16
  135. data/lib/action_dispatch/system_testing/driver.rb +11 -3
  136. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +49 -7
  137. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +8 -6
  138. data/lib/action_dispatch/testing/assertion_response.rb +0 -1
  139. data/lib/action_dispatch/testing/assertions/response.rb +4 -7
  140. data/lib/action_dispatch/testing/assertions/routing.rb +20 -8
  141. data/lib/action_dispatch/testing/assertions.rb +1 -1
  142. data/lib/action_dispatch/testing/integration.rb +61 -28
  143. data/lib/action_dispatch/testing/request_encoder.rb +3 -3
  144. data/lib/action_dispatch/testing/test_process.rb +29 -4
  145. data/lib/action_dispatch/testing/test_request.rb +3 -3
  146. data/lib/action_dispatch/testing/test_response.rb +4 -32
  147. data/lib/action_dispatch.rb +14 -7
  148. data/lib/action_pack/gem_version.rb +3 -3
  149. data/lib/action_pack.rb +1 -1
  150. metadata +39 -22
  151. data/lib/action_controller/metal/force_ssl.rb +0 -99
  152. data/lib/action_dispatch/http/parameter_filter.rb +0 -86
  153. data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
  154. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -49
  155. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -120
  156. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +0 -26
@@ -24,9 +24,12 @@ module ActionController
24
24
  def new_controller_thread # :nodoc:
25
25
  yield
26
26
  end
27
+
28
+ # Avoid a deadlock from the queue filling up
29
+ Buffer.queue_size = nil
27
30
  end
28
31
 
29
- # ActionController::TestCase will be deprecated and moved to a gem in Rails 5.1.
32
+ # ActionController::TestCase will be deprecated and moved to a gem in the future.
30
33
  # Please use ActionDispatch::IntegrationTest going forward.
31
34
  class TestRequest < ActionDispatch::TestRequest #:nodoc:
32
35
  DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup
@@ -84,7 +87,7 @@ module ActionController
84
87
  value = value.to_param
85
88
  end
86
89
 
87
- path_parameters[key] = value
90
+ path_parameters[key.to_sym] = value
88
91
  end
89
92
  end
90
93
 
@@ -158,7 +161,6 @@ module ActionController
158
161
  end.new
159
162
 
160
163
  private
161
-
162
164
  def params_parsers
163
165
  super.merge @custom_param_parsers
164
166
  end
@@ -177,12 +179,12 @@ module ActionController
177
179
 
178
180
  # Methods #destroy and #load! are overridden to avoid calling methods on the
179
181
  # @store object, which does not exist for the TestSession class.
180
- class TestSession < Rack::Session::Abstract::SessionHash #:nodoc:
182
+ class TestSession < Rack::Session::Abstract::PersistedSecure::SecureSessionHash #:nodoc:
181
183
  DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS
182
184
 
183
185
  def initialize(session = {})
184
186
  super(nil, nil)
185
- @id = SecureRandom.hex(16)
187
+ @id = Rack::Session::SessionId.new(SecureRandom.hex(16))
186
188
  @data = stringify_keys(session)
187
189
  @loaded = true
188
190
  end
@@ -203,12 +205,16 @@ module ActionController
203
205
  clear
204
206
  end
205
207
 
208
+ def dig(*keys)
209
+ keys = keys.map.with_index { |key, i| i.zero? ? key.to_s : key }
210
+ @data.dig(*keys)
211
+ end
212
+
206
213
  def fetch(key, *args, &block)
207
214
  @data.fetch(key.to_s, *args, &block)
208
215
  end
209
216
 
210
217
  private
211
-
212
218
  def load!
213
219
  @id
214
220
  end
@@ -276,9 +282,6 @@ module ActionController
276
282
  # after calling +post+. If the various assert methods are not sufficient, then you
277
283
  # may use this object to inspect the HTTP response in detail.
278
284
  #
279
- # (Earlier versions of \Rails required each functional test to subclass
280
- # Test::Unit::TestCase and define @controller, @request, @response in +setup+.)
281
- #
282
285
  # == Controller is automatically inferred
283
286
  #
284
287
  # ActionController::TestCase will automatically infer the controller under test
@@ -457,13 +460,10 @@ module ActionController
457
460
  # respectively which will make tests more expressive.
458
461
  #
459
462
  # Note that the request method is not verified.
460
- def process(action, method: "GET", params: {}, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil)
463
+ def process(action, method: "GET", params: nil, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil)
461
464
  check_required_ivars
462
465
 
463
- if body
464
- @request.set_header "RAW_POST_DATA", body
465
- end
466
-
466
+ action = +action.to_s
467
467
  http_method = method.to_s.upcase
468
468
 
469
469
  @html_document = nil
@@ -478,6 +478,10 @@ module ActionController
478
478
  @response.request = @request
479
479
  @controller.recycle!
480
480
 
481
+ if body
482
+ @request.set_header "RAW_POST_DATA", body
483
+ end
484
+
481
485
  @request.set_header "REQUEST_METHOD", http_method
482
486
 
483
487
  if as
@@ -485,63 +489,14 @@ module ActionController
485
489
  format ||= as
486
490
  end
487
491
 
488
- parameters = params.symbolize_keys
492
+ parameters = (params || {}).symbolize_keys
489
493
 
490
494
  if format
491
495
  parameters[:format] = format
492
496
  end
493
497
 
494
- generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action.to_s))
495
- generated_path = generated_path(generated_extras)
496
- query_string_keys = query_parameter_names(generated_extras)
497
-
498
- @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters, generated_path, query_string_keys)
499
-
500
- @request.session.update(session) if session
501
- @request.flash.update(flash || {})
502
-
503
- if xhr
504
- @request.set_header "HTTP_X_REQUESTED_WITH", "XMLHttpRequest"
505
- @request.fetch_header("HTTP_ACCEPT") do |k|
506
- @request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
507
- end
508
- end
509
-
510
- @request.fetch_header("SCRIPT_NAME") do |k|
511
- @request.set_header k, @controller.config.relative_url_root
512
- end
513
-
514
- begin
515
- @controller.recycle!
516
- @controller.dispatch(action, @request, @response)
517
- ensure
518
- @request = @controller.request
519
- @response = @controller.response
520
-
521
- if @request.have_cookie_jar?
522
- unless @request.cookie_jar.committed?
523
- @request.cookie_jar.write(@response)
524
- cookies.update(@request.cookie_jar.instance_variable_get(:@cookies))
525
- end
526
- end
527
- @response.prepare!
528
-
529
- if flash_value = @request.flash.to_session_value
530
- @request.session["flash"] = flash_value
531
- else
532
- @request.session.delete("flash")
533
- end
534
-
535
- if xhr
536
- @request.delete_header "HTTP_X_REQUESTED_WITH"
537
- @request.delete_header "HTTP_ACCEPT"
538
- end
539
- @request.query_string = ""
540
-
541
- @response.sent!
542
- end
543
-
544
- @response
498
+ setup_request(controller_class_name, action, parameters, session, flash, xhr)
499
+ process_controller_response(action, cookies, xhr)
545
500
  end
546
501
 
547
502
  def controller_class_name
@@ -597,13 +552,69 @@ module ActionController
597
552
  end
598
553
 
599
554
  private
555
+ def setup_request(controller_class_name, action, parameters, session, flash, xhr)
556
+ generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action))
557
+ generated_path = generated_path(generated_extras)
558
+ query_string_keys = query_parameter_names(generated_extras)
559
+
560
+ @request.assign_parameters(@routes, controller_class_name, action, parameters, generated_path, query_string_keys)
561
+
562
+ @request.session.update(session) if session
563
+ @request.flash.update(flash || {})
564
+
565
+ if xhr
566
+ @request.set_header "HTTP_X_REQUESTED_WITH", "XMLHttpRequest"
567
+ @request.fetch_header("HTTP_ACCEPT") do |k|
568
+ @request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
569
+ end
570
+ end
571
+
572
+ @request.fetch_header("SCRIPT_NAME") do |k|
573
+ @request.set_header k, @controller.config.relative_url_root
574
+ end
575
+ end
576
+
577
+ def process_controller_response(action, cookies, xhr)
578
+ begin
579
+ @controller.recycle!
580
+ @controller.dispatch(action, @request, @response)
581
+ ensure
582
+ @request = @controller.request
583
+ @response = @controller.response
584
+
585
+ if @request.have_cookie_jar?
586
+ unless @request.cookie_jar.committed?
587
+ @request.cookie_jar.write(@response)
588
+ cookies.update(@request.cookie_jar.instance_variable_get(:@cookies))
589
+ end
590
+ end
591
+ @response.prepare!
592
+
593
+ if flash_value = @request.flash.to_session_value
594
+ @request.session["flash"] = flash_value
595
+ else
596
+ @request.session.delete("flash")
597
+ end
598
+
599
+ if xhr
600
+ @request.delete_header "HTTP_X_REQUESTED_WITH"
601
+ @request.delete_header "HTTP_ACCEPT"
602
+ end
603
+ @request.query_string = ""
604
+
605
+ @response.sent!
606
+ end
607
+
608
+ @response
609
+ end
600
610
 
601
611
  def scrub_env!(env)
602
- env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ }
603
- env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ }
604
- env.delete "action_dispatch.request.query_parameters"
605
- env.delete "action_dispatch.request.request_parameters"
612
+ env.delete_if do |k, _|
613
+ k.start_with?("rack.request", "action_dispatch.request", "action_dispatch.rescue")
614
+ end
606
615
  env["rack.input"] = StringIO.new
616
+ env.delete "CONTENT_LENGTH"
617
+ env.delete "RAW_POST_DATA"
607
618
  env
608
619
  end
609
620
 
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/rails"
4
3
  require "abstract_controller"
5
4
  require "action_dispatch"
6
- require "action_controller/metal/live"
7
5
  require "action_controller/metal/strong_parameters"
8
6
 
9
7
  module ActionController
@@ -12,7 +10,6 @@ module ActionController
12
10
  autoload :API
13
11
  autoload :Base
14
12
  autoload :Metal
15
- autoload :Middleware
16
13
  autoload :Renderer
17
14
  autoload :FormBuilder
18
15
 
@@ -21,20 +18,26 @@ module ActionController
21
18
  end
22
19
 
23
20
  autoload_under "metal" do
21
+ eager_autoload do
22
+ autoload :Live
23
+ end
24
+
24
25
  autoload :ConditionalGet
25
26
  autoload :ContentSecurityPolicy
26
27
  autoload :Cookies
27
28
  autoload :DataStreaming
29
+ autoload :DefaultHeaders
28
30
  autoload :EtagWithTemplateDigest
29
31
  autoload :EtagWithFlash
32
+ autoload :PermissionsPolicy
30
33
  autoload :Flash
31
- autoload :ForceSSL
32
34
  autoload :Head
33
35
  autoload :Helpers
34
36
  autoload :HttpAuthentication
35
37
  autoload :BasicImplicitRender
36
38
  autoload :ImplicitRender
37
39
  autoload :Instrumentation
40
+ autoload :Logging
38
41
  autoload :MimeResponds
39
42
  autoload :ParamsWrapper
40
43
  autoload :Redirecting
@@ -4,8 +4,8 @@ module ActionDispatch
4
4
  module Http
5
5
  module Cache
6
6
  module Request
7
- HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE".freeze
8
- HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH".freeze
7
+ HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE"
8
+ HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH"
9
9
 
10
10
  def if_modified_since
11
11
  if since = get_header(HTTP_IF_MODIFIED_SINCE)
@@ -114,7 +114,7 @@ module ActionDispatch
114
114
 
115
115
  # True if an ETag is set and it's a weak validator (preceded with W/)
116
116
  def weak_etag?
117
- etag? && etag.starts_with?('W/"')
117
+ etag? && etag.start_with?('W/"')
118
118
  end
119
119
 
120
120
  # True if an ETag is set and it isn't a weak validator (not preceded with W/)
@@ -123,10 +123,9 @@ module ActionDispatch
123
123
  end
124
124
 
125
125
  private
126
-
127
- DATE = "Date".freeze
128
- LAST_MODIFIED = "Last-Modified".freeze
129
- SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate])
126
+ DATE = "Date"
127
+ LAST_MODIFIED = "Last-Modified"
128
+ SPECIAL_KEYS = Set.new(%w[extras no-store no-cache max-age public private must-revalidate])
130
129
 
131
130
  def generate_weak_etag(validators)
132
131
  "W/#{generate_strong_etag(validators)}"
@@ -151,8 +150,8 @@ module ActionDispatch
151
150
  directive, argument = segment.split("=", 2)
152
151
 
153
152
  if SPECIAL_KEYS.include? directive
154
- key = directive.tr("-", "_")
155
- cache_control[key.to_sym] = argument || true
153
+ directive.tr!("-", "_")
154
+ cache_control[directive.to_sym] = argument || true
156
155
  else
157
156
  cache_control[:extras] ||= []
158
157
  cache_control[:extras] << segment
@@ -166,11 +165,12 @@ module ActionDispatch
166
165
  @cache_control = cache_control_headers
167
166
  end
168
167
 
169
- DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
170
- NO_CACHE = "no-cache".freeze
171
- PUBLIC = "public".freeze
172
- PRIVATE = "private".freeze
173
- MUST_REVALIDATE = "must-revalidate".freeze
168
+ DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
169
+ NO_STORE = "no-store"
170
+ NO_CACHE = "no-cache"
171
+ PUBLIC = "public"
172
+ PRIVATE = "private"
173
+ MUST_REVALIDATE = "must-revalidate"
174
174
 
175
175
  def handle_conditional_get!
176
176
  # Normally default cache control setting is handled by ETag
@@ -183,36 +183,42 @@ module ActionDispatch
183
183
  end
184
184
 
185
185
  def merge_and_normalize_cache_control!(cache_control)
186
- control = {}
187
- cc_headers = cache_control_headers
188
- if extras = cc_headers.delete(:extras)
186
+ control = cache_control_headers
187
+
188
+ return if control.empty? && cache_control.empty? # Let middleware handle default behavior
189
+
190
+ if extras = control.delete(:extras)
189
191
  cache_control[:extras] ||= []
190
192
  cache_control[:extras] += extras
191
193
  cache_control[:extras].uniq!
192
194
  end
193
195
 
194
- control.merge! cc_headers
195
196
  control.merge! cache_control
196
197
 
197
- if control.empty?
198
- # Let middleware handle default behavior
198
+ options = []
199
+
200
+ if control[:no_store]
201
+ options << PRIVATE if control[:private]
202
+ options << NO_STORE
199
203
  elsif control[:no_cache]
200
- self._cache_control = NO_CACHE
201
- if control[:extras]
202
- self._cache_control = _cache_control + ", #{control[:extras].join(', ')}"
203
- end
204
+ options << PUBLIC if control[:public]
205
+ options << NO_CACHE
206
+ options.concat(control[:extras]) if control[:extras]
204
207
  else
205
- extras = control[:extras]
208
+ extras = control[:extras]
206
209
  max_age = control[:max_age]
210
+ stale_while_revalidate = control[:stale_while_revalidate]
211
+ stale_if_error = control[:stale_if_error]
207
212
 
208
- options = []
209
213
  options << "max-age=#{max_age.to_i}" if max_age
210
214
  options << (control[:public] ? PUBLIC : PRIVATE)
211
215
  options << MUST_REVALIDATE if control[:must_revalidate]
216
+ options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
217
+ options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
212
218
  options.concat(extras) if extras
213
-
214
- self._cache_control = options.join(", ")
215
219
  end
220
+
221
+ self._cache_control = options.join(", ")
216
222
  end
217
223
  end
218
224
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ module Http
5
+ class ContentDisposition # :nodoc:
6
+ def self.format(disposition:, filename:)
7
+ new(disposition: disposition, filename: filename).to_s
8
+ end
9
+
10
+ attr_reader :disposition, :filename
11
+
12
+ def initialize(disposition:, filename:)
13
+ @disposition = disposition
14
+ @filename = filename
15
+ end
16
+
17
+ TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!\#$+.^_`|~-]/
18
+
19
+ def ascii_filename
20
+ 'filename="' + percent_escape(I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"'
21
+ end
22
+
23
+ RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!\#$&+.^_`|~-]/
24
+
25
+ def utf8_filename
26
+ "filename*=UTF-8''" + percent_escape(filename, RFC_5987_ESCAPED_CHAR)
27
+ end
28
+
29
+ def to_s
30
+ if filename
31
+ "#{disposition}; #{ascii_filename}; #{utf8_filename}"
32
+ else
33
+ "#{disposition}"
34
+ end
35
+ end
36
+
37
+ private
38
+ def percent_escape(string, pattern)
39
+ string.gsub(pattern) do |char|
40
+ char.bytes.map { |byte| "%%%02X" % byte }.join
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -5,9 +5,9 @@ require "active_support/core_ext/object/deep_dup"
5
5
  module ActionDispatch #:nodoc:
6
6
  class ContentSecurityPolicy
7
7
  class Middleware
8
- CONTENT_TYPE = "Content-Type".freeze
9
- POLICY = "Content-Security-Policy".freeze
10
- POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
8
+ CONTENT_TYPE = "Content-Type"
9
+ POLICY = "Content-Security-Policy"
10
+ POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only"
11
11
 
12
12
  def initialize(app)
13
13
  @app = app
@@ -21,23 +21,19 @@ module ActionDispatch #:nodoc:
21
21
  return response if policy_present?(headers)
22
22
 
23
23
  if policy = request.content_security_policy
24
- if policy.directives["script-src"]
25
- if nonce = request.content_security_policy_nonce
26
- policy.directives["script-src"] << "'nonce-#{nonce}'"
27
- end
28
- end
29
-
30
- headers[header_name(request)] = policy.build(request.controller_instance)
24
+ nonce = request.content_security_policy_nonce
25
+ nonce_directives = request.content_security_policy_nonce_directives
26
+ context = request.controller_instance || request
27
+ headers[header_name(request)] = policy.build(context, nonce, nonce_directives)
31
28
  end
32
29
 
33
30
  response
34
31
  end
35
32
 
36
33
  private
37
-
38
34
  def html_response?(headers)
39
35
  if content_type = headers[CONTENT_TYPE]
40
- content_type =~ /html/
36
+ /html/.match?(content_type)
41
37
  end
42
38
  end
43
39
 
@@ -55,10 +51,11 @@ module ActionDispatch #:nodoc:
55
51
  end
56
52
 
57
53
  module Request
58
- POLICY = "action_dispatch.content_security_policy".freeze
59
- POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only".freeze
60
- NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator".freeze
61
- NONCE = "action_dispatch.content_security_policy_nonce".freeze
54
+ POLICY = "action_dispatch.content_security_policy"
55
+ POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only"
56
+ NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator"
57
+ NONCE = "action_dispatch.content_security_policy_nonce"
58
+ NONCE_DIRECTIVES = "action_dispatch.content_security_policy_nonce_directives"
62
59
 
63
60
  def content_security_policy
64
61
  get_header(POLICY)
@@ -84,6 +81,14 @@ module ActionDispatch #:nodoc:
84
81
  set_header(NONCE_GENERATOR, generator)
85
82
  end
86
83
 
84
+ def content_security_policy_nonce_directives
85
+ get_header(NONCE_DIRECTIVES)
86
+ end
87
+
88
+ def content_security_policy_nonce_directives=(generator)
89
+ set_header(NONCE_DIRECTIVES, generator)
90
+ end
91
+
87
92
  def content_security_policy_nonce
88
93
  if content_security_policy_nonce_generator
89
94
  if nonce = get_header(NONCE)
@@ -95,7 +100,6 @@ module ActionDispatch #:nodoc:
95
100
  end
96
101
 
97
102
  private
98
-
99
103
  def generate_content_security_policy_nonce
100
104
  content_security_policy_nonce_generator.call(self)
101
105
  end
@@ -113,7 +117,9 @@ module ActionDispatch #:nodoc:
113
117
  blob: "blob:",
114
118
  filesystem: "filesystem:",
115
119
  report_sample: "'report-sample'",
116
- strict_dynamic: "'strict-dynamic'"
120
+ strict_dynamic: "'strict-dynamic'",
121
+ ws: "ws:",
122
+ wss: "wss:"
117
123
  }.freeze
118
124
 
119
125
  DIRECTIVES = {
@@ -129,12 +135,19 @@ module ActionDispatch #:nodoc:
129
135
  manifest_src: "manifest-src",
130
136
  media_src: "media-src",
131
137
  object_src: "object-src",
138
+ prefetch_src: "prefetch-src",
132
139
  script_src: "script-src",
140
+ script_src_attr: "script-src-attr",
141
+ script_src_elem: "script-src-elem",
133
142
  style_src: "style-src",
143
+ style_src_attr: "style-src-attr",
144
+ style_src_elem: "style-src-elem",
134
145
  worker_src: "worker-src"
135
146
  }.freeze
136
147
 
137
- private_constant :MAPPINGS, :DIRECTIVES
148
+ DEFAULT_NONCE_DIRECTIVES = %w[script-src style-src].freeze
149
+
150
+ private_constant :MAPPINGS, :DIRECTIVES, :DEFAULT_NONCE_DIRECTIVES
138
151
 
139
152
  attr_reader :directives
140
153
 
@@ -203,8 +216,9 @@ module ActionDispatch #:nodoc:
203
216
  end
204
217
  end
205
218
 
206
- def build(context = nil)
207
- build_directives(context).compact.join("; ")
219
+ def build(context = nil, nonce = nil, nonce_directives = nil)
220
+ nonce_directives = DEFAULT_NONCE_DIRECTIVES if nonce_directives.nil?
221
+ build_directives(context, nonce, nonce_directives).compact.join("; ")
208
222
  end
209
223
 
210
224
  private
@@ -227,10 +241,14 @@ module ActionDispatch #:nodoc:
227
241
  end
228
242
  end
229
243
 
230
- def build_directives(context)
244
+ def build_directives(context, nonce, nonce_directives)
231
245
  @directives.map do |directive, sources|
232
246
  if sources.is_a?(Array)
233
- "#{directive} #{build_directive(sources, context).join(' ')}"
247
+ if nonce && nonce_directive?(directive, nonce_directives)
248
+ "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
249
+ else
250
+ "#{directive} #{build_directive(sources, context).join(' ')}"
251
+ end
234
252
  elsif sources
235
253
  directive
236
254
  else
@@ -253,11 +271,16 @@ module ActionDispatch #:nodoc:
253
271
  if context.nil?
254
272
  raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}"
255
273
  else
256
- context.instance_exec(&source)
274
+ resolved = context.instance_exec(&source)
275
+ resolved.is_a?(Symbol) ? apply_mapping(resolved) : resolved
257
276
  end
258
277
  else
259
278
  raise RuntimeError, "Unexpected content security policy source: #{source.inspect}"
260
279
  end
261
280
  end
281
+
282
+ def nonce_directive?(directive, nonce_directives)
283
+ nonce_directives.include?(directive)
284
+ end
262
285
  end
263
286
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_dispatch/http/parameter_filter"
3
+ require "active_support/parameter_filter"
4
4
 
5
5
  module ActionDispatch
6
6
  module Http
@@ -9,8 +9,8 @@ module ActionDispatch
9
9
  # sub-hashes of the params hash to filter. Filtering only certain sub-keys
10
10
  # from a hash is possible by using the dot notation: 'credit_card.number'.
11
11
  # If a block is given, each key and value of the params hash and all
12
- # sub-hashes is passed to it, where the value or the key can be replaced using
13
- # String#replace or similar method.
12
+ # sub-hashes are passed to it, where the value or the key can be replaced using
13
+ # String#replace or similar methods.
14
14
  #
15
15
  # env["action_dispatch.parameter_filter"] = [:password]
16
16
  # => replaces the value to all keys matching /password/i with "[FILTERED]"
@@ -23,13 +23,13 @@ module ActionDispatch
23
23
  # change { file: { code: "xxxx"} }
24
24
  #
25
25
  # env["action_dispatch.parameter_filter"] = -> (k, v) do
26
- # v.reverse! if k =~ /secret/i
26
+ # v.reverse! if k.match?(/secret/i)
27
27
  # end
28
28
  # => reverses the value to all keys matching /secret/i
29
29
  module FilterParameters
30
30
  ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc:
31
- NULL_PARAM_FILTER = ParameterFilter.new # :nodoc:
32
- NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc:
31
+ NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new # :nodoc:
32
+ NULL_ENV_FILTER = ActiveSupport::ParameterFilter.new ENV_MATCH # :nodoc:
33
33
 
34
34
  def initialize
35
35
  super
@@ -41,6 +41,8 @@ module ActionDispatch
41
41
  # Returns a hash of parameters with all sensitive data replaced.
42
42
  def filtered_parameters
43
43
  @filtered_parameters ||= parameter_filter.filter(parameters)
44
+ rescue ActionDispatch::Http::Parameters::ParseError
45
+ @filtered_parameters = {}
44
46
  end
45
47
 
46
48
  # Returns a hash of request.env with all sensitive data replaced.
@@ -54,7 +56,6 @@ module ActionDispatch
54
56
  end
55
57
 
56
58
  private
57
-
58
59
  def parameter_filter # :doc:
59
60
  parameter_filter_for fetch_header("action_dispatch.parameter_filter") {
60
61
  return NULL_PARAM_FILTER
@@ -69,7 +70,7 @@ module ActionDispatch
69
70
  end
70
71
 
71
72
  def parameter_filter_for(filters) # :doc:
72
- ParameterFilter.new(filters)
73
+ ActiveSupport::ParameterFilter.new(filters)
73
74
  end
74
75
 
75
76
  KV_RE = "[^&;=]+"