actionpack 5.1.7 → 5.2.4.3

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 (148) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +282 -362
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +5 -5
  5. data/lib/abstract_controller.rb +3 -0
  6. data/lib/abstract_controller/asset_paths.rb +2 -0
  7. data/lib/abstract_controller/base.rb +10 -2
  8. data/lib/abstract_controller/caching.rb +3 -2
  9. data/lib/abstract_controller/caching/fragments.rb +30 -7
  10. data/lib/abstract_controller/callbacks.rb +25 -3
  11. data/lib/abstract_controller/collector.rb +2 -0
  12. data/lib/abstract_controller/error.rb +2 -0
  13. data/lib/abstract_controller/helpers.rb +4 -5
  14. data/lib/abstract_controller/logger.rb +2 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +2 -0
  16. data/lib/abstract_controller/rendering.rb +9 -16
  17. data/lib/abstract_controller/translation.rb +2 -0
  18. data/lib/abstract_controller/url_for.rb +2 -0
  19. data/lib/action_controller.rb +3 -0
  20. data/lib/action_controller/api.rb +2 -0
  21. data/lib/action_controller/api/api_rendering.rb +2 -0
  22. data/lib/action_controller/base.rb +3 -0
  23. data/lib/action_controller/caching.rb +2 -0
  24. data/lib/action_controller/form_builder.rb +2 -0
  25. data/lib/action_controller/log_subscriber.rb +5 -3
  26. data/lib/action_controller/metal.rb +13 -14
  27. data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
  28. data/lib/action_controller/metal/conditional_get.rb +4 -3
  29. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  30. data/lib/action_controller/metal/cookies.rb +2 -0
  31. data/lib/action_controller/metal/data_streaming.rb +7 -5
  32. data/lib/action_controller/metal/etag_with_flash.rb +2 -0
  33. data/lib/action_controller/metal/etag_with_template_digest.rb +3 -2
  34. data/lib/action_controller/metal/exceptions.rb +2 -3
  35. data/lib/action_controller/metal/flash.rb +3 -2
  36. data/lib/action_controller/metal/force_ssl.rb +4 -2
  37. data/lib/action_controller/metal/head.rb +2 -0
  38. data/lib/action_controller/metal/helpers.rb +4 -3
  39. data/lib/action_controller/metal/http_authentication.rb +8 -9
  40. data/lib/action_controller/metal/implicit_render.rb +2 -0
  41. data/lib/action_controller/metal/instrumentation.rb +4 -6
  42. data/lib/action_controller/metal/live.rb +3 -1
  43. data/lib/action_controller/metal/mime_responds.rb +3 -1
  44. data/lib/action_controller/metal/parameter_encoding.rb +2 -0
  45. data/lib/action_controller/metal/params_wrapper.rb +14 -10
  46. data/lib/action_controller/metal/redirecting.rb +22 -11
  47. data/lib/action_controller/metal/renderers.rb +4 -3
  48. data/lib/action_controller/metal/rendering.rb +2 -2
  49. data/lib/action_controller/metal/request_forgery_protection.rb +62 -10
  50. data/lib/action_controller/metal/rescue.rb +5 -3
  51. data/lib/action_controller/metal/streaming.rb +3 -1
  52. data/lib/action_controller/metal/strong_parameters.rb +36 -25
  53. data/lib/action_controller/metal/testing.rb +2 -6
  54. data/lib/action_controller/metal/url_for.rb +2 -0
  55. data/lib/action_controller/railtie.rb +16 -4
  56. data/lib/action_controller/railties/helpers.rb +2 -0
  57. data/lib/action_controller/renderer.rb +2 -0
  58. data/lib/action_controller/template_assertions.rb +2 -0
  59. data/lib/action_controller/test_case.rb +16 -10
  60. data/lib/action_dispatch.rb +9 -5
  61. data/lib/action_dispatch/http/cache.rb +22 -14
  62. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  63. data/lib/action_dispatch/http/filter_parameters.rb +4 -2
  64. data/lib/action_dispatch/http/filter_redirect.rb +2 -0
  65. data/lib/action_dispatch/http/headers.rb +2 -0
  66. data/lib/action_dispatch/http/mime_negotiation.rb +4 -8
  67. data/lib/action_dispatch/http/mime_type.rb +15 -13
  68. data/lib/action_dispatch/http/mime_types.rb +17 -2
  69. data/lib/action_dispatch/http/parameter_filter.rb +2 -0
  70. data/lib/action_dispatch/http/parameters.rb +6 -9
  71. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  72. data/lib/action_dispatch/http/request.rb +36 -16
  73. data/lib/action_dispatch/http/response.rb +11 -9
  74. data/lib/action_dispatch/http/upload.rb +2 -0
  75. data/lib/action_dispatch/http/url.rb +5 -6
  76. data/lib/action_dispatch/journey.rb +2 -0
  77. data/lib/action_dispatch/journey/formatter.rb +4 -2
  78. data/lib/action_dispatch/journey/gtg/builder.rb +2 -0
  79. data/lib/action_dispatch/journey/gtg/simulator.rb +2 -8
  80. data/lib/action_dispatch/journey/gtg/transition_table.rb +3 -2
  81. data/lib/action_dispatch/journey/nfa/builder.rb +2 -0
  82. data/lib/action_dispatch/journey/nfa/dot.rb +12 -10
  83. data/lib/action_dispatch/journey/nfa/simulator.rb +2 -0
  84. data/lib/action_dispatch/journey/nfa/transition_table.rb +2 -0
  85. data/lib/action_dispatch/journey/nodes/node.rb +2 -0
  86. data/lib/action_dispatch/journey/parser_extras.rb +2 -0
  87. data/lib/action_dispatch/journey/path/pattern.rb +4 -1
  88. data/lib/action_dispatch/journey/route.rb +15 -6
  89. data/lib/action_dispatch/journey/router.rb +3 -1
  90. data/lib/action_dispatch/journey/router/utils.rb +14 -7
  91. data/lib/action_dispatch/journey/routes.rb +3 -1
  92. data/lib/action_dispatch/journey/scanner.rb +1 -0
  93. data/lib/action_dispatch/journey/visitors.rb +5 -3
  94. data/lib/action_dispatch/middleware/callbacks.rb +2 -0
  95. data/lib/action_dispatch/middleware/cookies.rb +148 -91
  96. data/lib/action_dispatch/middleware/debug_exceptions.rb +4 -2
  97. data/lib/action_dispatch/middleware/debug_locks.rb +9 -7
  98. data/lib/action_dispatch/middleware/exception_wrapper.rb +5 -6
  99. data/lib/action_dispatch/middleware/executor.rb +2 -0
  100. data/lib/action_dispatch/middleware/flash.rb +4 -2
  101. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -4
  102. data/lib/action_dispatch/middleware/reloader.rb +2 -0
  103. data/lib/action_dispatch/middleware/remote_ip.rb +7 -5
  104. data/lib/action_dispatch/middleware/request_id.rb +3 -1
  105. data/lib/action_dispatch/middleware/session/abstract_store.rb +17 -1
  106. data/lib/action_dispatch/middleware/session/cache_store.rb +13 -6
  107. data/lib/action_dispatch/middleware/session/cookie_store.rb +31 -32
  108. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +2 -0
  109. data/lib/action_dispatch/middleware/show_exceptions.rb +3 -1
  110. data/lib/action_dispatch/middleware/ssl.rb +44 -38
  111. data/lib/action_dispatch/middleware/stack.rb +4 -2
  112. data/lib/action_dispatch/middleware/static.rb +14 -12
  113. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  114. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  115. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +1 -0
  116. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +6 -2
  117. data/lib/action_dispatch/railtie.rb +11 -1
  118. data/lib/action_dispatch/request/session.rb +16 -5
  119. data/lib/action_dispatch/request/utils.rb +6 -4
  120. data/lib/action_dispatch/routing.rb +3 -1
  121. data/lib/action_dispatch/routing/endpoint.rb +9 -2
  122. data/lib/action_dispatch/routing/inspector.rb +6 -4
  123. data/lib/action_dispatch/routing/mapper.rb +64 -52
  124. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -0
  125. data/lib/action_dispatch/routing/redirection.rb +7 -5
  126. data/lib/action_dispatch/routing/route_set.rb +29 -24
  127. data/lib/action_dispatch/routing/routes_proxy.rb +5 -2
  128. data/lib/action_dispatch/routing/url_for.rb +25 -5
  129. data/lib/action_dispatch/system_test_case.rb +22 -6
  130. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  131. data/lib/action_dispatch/system_testing/driver.rb +9 -3
  132. data/lib/action_dispatch/system_testing/server.rb +2 -16
  133. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +12 -14
  134. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +8 -2
  135. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  136. data/lib/action_dispatch/testing/assertion_response.rb +2 -0
  137. data/lib/action_dispatch/testing/assertions.rb +2 -0
  138. data/lib/action_dispatch/testing/assertions/response.rb +4 -2
  139. data/lib/action_dispatch/testing/assertions/routing.rb +5 -5
  140. data/lib/action_dispatch/testing/integration.rb +24 -21
  141. data/lib/action_dispatch/testing/request_encoder.rb +3 -1
  142. data/lib/action_dispatch/testing/test_process.rb +2 -0
  143. data/lib/action_dispatch/testing/test_request.rb +3 -1
  144. data/lib/action_dispatch/testing/test_response.rb +23 -3
  145. data/lib/action_pack.rb +3 -1
  146. data/lib/action_pack/gem_version.rb +5 -3
  147. data/lib/action_pack/version.rb +2 -0
  148. metadata +23 -11
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionController
2
4
  module Testing
3
5
  extend ActiveSupport::Concern
@@ -10,11 +12,5 @@ module ActionController
10
12
  self.params = nil
11
13
  end
12
14
  end
13
-
14
- module ClassMethods
15
- def before_filters
16
- _process_action_callbacks.find_all { |x| x.kind == :before }.map(&:name)
17
- end
18
- end
19
15
  end
20
16
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionController
2
4
  # Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing
3
5
  # the <tt>_routes</tt> method. Otherwise, an exception will be raised.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rails"
2
4
  require "action_controller"
3
5
  require "action_dispatch/railtie"
@@ -23,10 +25,6 @@ module ActionController
23
25
  options = app.config.action_controller
24
26
 
25
27
  ActiveSupport.on_load(:action_controller, run_once: true) do
26
- if options.delete(:raise_on_unfiltered_parameters)
27
- ActiveSupport::Deprecation.warn("raise_on_unfiltered_parameters is deprecated and has no effect in Rails 5.1.")
28
- end
29
-
30
28
  ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false }
31
29
  if app.config.action_controller[:always_permitted_parameters]
32
30
  ActionController::Parameters.always_permitted_parameters =
@@ -73,5 +71,19 @@ module ActionController
73
71
  config.compile_methods! if config.respond_to?(:compile_methods!)
74
72
  end
75
73
  end
74
+
75
+ initializer "action_controller.request_forgery_protection" do |app|
76
+ ActiveSupport.on_load(:action_controller_base) do
77
+ if app.config.action_controller.default_protect_from_forgery
78
+ protect_from_forgery with: :exception
79
+ end
80
+ end
81
+ end
82
+
83
+ initializer "action_controller.eager_load_actions" do
84
+ ActiveSupport.on_load(:after_initialize) do
85
+ ActionController::Metal.descendants.each(&:action_methods) if config.eager_load
86
+ end
87
+ end
76
88
  end
77
89
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionController
2
4
  module Railties
3
5
  module Helpers
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/core_ext/hash/keys"
2
4
 
3
5
  module ActionController
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionController
2
4
  module TemplateAssertions
3
5
  def assert_template(options = {}, message = nil)
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rack/session/abstract/id"
2
4
  require "active_support/core_ext/hash/conversions"
3
5
  require "active_support/core_ext/object/to_query"
4
6
  require "active_support/core_ext/module/anonymous"
7
+ require "active_support/core_ext/module/redefine_method"
5
8
  require "active_support/core_ext/hash/keys"
6
9
  require "active_support/testing/constant_lookup"
7
10
  require "action_controller/template_assertions"
@@ -17,7 +20,7 @@ module ActionController
17
20
  # the database on the main thread, so they could open a txn, then the
18
21
  # controller thread will open a new connection and try to access data
19
22
  # that's only visible to the main thread's txn. This is the problem in #23483.
20
- remove_method :new_controller_thread
23
+ silence_redefinition_of_method :new_controller_thread
21
24
  def new_controller_thread # :nodoc:
22
25
  yield
23
26
  end
@@ -253,7 +256,7 @@ module ActionController
253
256
  #
254
257
  # def test_create
255
258
  # json = {book: { title: "Love Hina" }}.to_json
256
- # post :create, json
259
+ # post :create, body: json
257
260
  # end
258
261
  #
259
262
  # == Special instance variables
@@ -454,13 +457,10 @@ module ActionController
454
457
  # respectively which will make tests more expressive.
455
458
  #
456
459
  # Note that the request method is not verified.
457
- def process(action, method: "GET", params: {}, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil)
460
+ def process(action, method: "GET", params: nil, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil)
458
461
  check_required_ivars
459
462
 
460
- if body
461
- @request.set_header "RAW_POST_DATA", body
462
- end
463
-
463
+ action = action.to_s.dup
464
464
  http_method = method.to_s.upcase
465
465
 
466
466
  @html_document = nil
@@ -475,6 +475,10 @@ module ActionController
475
475
  @response.request = @request
476
476
  @controller.recycle!
477
477
 
478
+ if body
479
+ @request.set_header "RAW_POST_DATA", body
480
+ end
481
+
478
482
  @request.set_header "REQUEST_METHOD", http_method
479
483
 
480
484
  if as
@@ -482,17 +486,17 @@ module ActionController
482
486
  format ||= as
483
487
  end
484
488
 
485
- parameters = params.symbolize_keys
489
+ parameters = (params || {}).symbolize_keys
486
490
 
487
491
  if format
488
492
  parameters[:format] = format
489
493
  end
490
494
 
491
- generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action.to_s))
495
+ generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action))
492
496
  generated_path = generated_path(generated_extras)
493
497
  query_string_keys = query_parameter_names(generated_extras)
494
498
 
495
- @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters, generated_path, query_string_keys)
499
+ @request.assign_parameters(@routes, controller_class_name, action, parameters, generated_path, query_string_keys)
496
500
 
497
501
  @request.session.update(session) if session
498
502
  @request.flash.update(flash || {})
@@ -601,6 +605,8 @@ module ActionController
601
605
  env.delete "action_dispatch.request.query_parameters"
602
606
  env.delete "action_dispatch.request.request_parameters"
603
607
  env["rack.input"] = StringIO.new
608
+ env.delete "CONTENT_LENGTH"
609
+ env.delete "RAW_POST_DATA"
604
610
  env
605
611
  end
606
612
 
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #--
2
- # Copyright (c) 2004-2017 David Heinemeier Hansson
4
+ # Copyright (c) 2004-2018 David Heinemeier Hansson
3
5
  #
4
6
  # Permission is hereby granted, free of charge, to any person obtaining
5
7
  # a copy of this software and associated documentation files (the
@@ -40,6 +42,7 @@ module ActionDispatch
40
42
 
41
43
  eager_autoload do
42
44
  autoload_under "http" do
45
+ autoload :ContentSecurityPolicy
43
46
  autoload :Request
44
47
  autoload :Response
45
48
  end
@@ -80,10 +83,11 @@ module ActionDispatch
80
83
  end
81
84
 
82
85
  module Session
83
- autoload :AbstractStore, "action_dispatch/middleware/session/abstract_store"
84
- autoload :CookieStore, "action_dispatch/middleware/session/cookie_store"
85
- autoload :MemCacheStore, "action_dispatch/middleware/session/mem_cache_store"
86
- autoload :CacheStore, "action_dispatch/middleware/session/cache_store"
86
+ autoload :AbstractStore, "action_dispatch/middleware/session/abstract_store"
87
+ autoload :AbstractSecureStore, "action_dispatch/middleware/session/abstract_store"
88
+ autoload :CookieStore, "action_dispatch/middleware/session/cookie_store"
89
+ autoload :MemCacheStore, "action_dispatch/middleware/session/mem_cache_store"
90
+ autoload :CacheStore, "action_dispatch/middleware/session/cache_store"
87
91
  end
88
92
 
89
93
  mattr_accessor :test_app
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionDispatch
2
4
  module Http
3
5
  module Cache
@@ -95,7 +97,7 @@ module ActionDispatch
95
97
  # support strong ETags and will ignore weak ETags entirely.
96
98
  #
97
99
  # Weak ETags are what we almost always need, so they're the default.
98
- # Check out `#strong_etag=` to provide a strong ETag validator.
100
+ # Check out #strong_etag= to provide a strong ETag validator.
99
101
  def etag=(weak_validators)
100
102
  self.weak_etag = weak_validators
101
103
  end
@@ -131,7 +133,7 @@ module ActionDispatch
131
133
  end
132
134
 
133
135
  def generate_strong_etag(validators)
134
- %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
136
+ %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
135
137
  end
136
138
 
137
139
  def cache_control_segments
@@ -164,19 +166,23 @@ module ActionDispatch
164
166
  @cache_control = cache_control_headers
165
167
  end
166
168
 
167
- def handle_conditional_get!
168
- if etag? || last_modified? || !@cache_control.empty?
169
- set_conditional_cache_control!(@cache_control)
170
- end
171
- end
172
-
173
169
  DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
174
170
  NO_CACHE = "no-cache".freeze
175
171
  PUBLIC = "public".freeze
176
172
  PRIVATE = "private".freeze
177
173
  MUST_REVALIDATE = "must-revalidate".freeze
178
174
 
179
- def set_conditional_cache_control!(cache_control)
175
+ def handle_conditional_get!
176
+ # Normally default cache control setting is handled by ETag
177
+ # middleware. But, if an etag is already set, the middleware
178
+ # defaults to `no-cache` unless a default `Cache-Control` value is
179
+ # previously set. So, set a default one here.
180
+ if (etag? || last_modified?) && !self._cache_control
181
+ self._cache_control = DEFAULT_CACHE_CONTROL
182
+ end
183
+ end
184
+
185
+ def merge_and_normalize_cache_control!(cache_control)
180
186
  control = {}
181
187
  cc_headers = cache_control_headers
182
188
  if extras = cc_headers.delete(:extras)
@@ -189,12 +195,14 @@ module ActionDispatch
189
195
  control.merge! cache_control
190
196
 
191
197
  if control.empty?
192
- self._cache_control = DEFAULT_CACHE_CONTROL
198
+ # Let middleware handle default behavior
193
199
  elsif control[:no_cache]
194
- self._cache_control = NO_CACHE
195
- if control[:extras]
196
- self._cache_control = _cache_control + ", #{control[:extras].join(', ')}"
197
- end
200
+ options = []
201
+ options << PUBLIC if control[:public]
202
+ options << NO_CACHE
203
+ options.concat(control[:extras]) if control[:extras]
204
+
205
+ self._cache_control = options.join(", ")
198
206
  else
199
207
  extras = control[:extras]
200
208
  max_age = control[:max_age]
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/deep_dup"
4
+
5
+ module ActionDispatch #:nodoc:
6
+ class ContentSecurityPolicy
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
11
+
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call(env)
17
+ request = ActionDispatch::Request.new env
18
+ _, headers, _ = response = @app.call(env)
19
+
20
+ return response unless html_response?(headers)
21
+ return response if policy_present?(headers)
22
+
23
+ if policy = request.content_security_policy
24
+ nonce = request.content_security_policy_nonce
25
+ context = request.controller_instance || request
26
+ headers[header_name(request)] = policy.build(context, nonce)
27
+ end
28
+
29
+ response
30
+ end
31
+
32
+ private
33
+
34
+ def html_response?(headers)
35
+ if content_type = headers[CONTENT_TYPE]
36
+ content_type =~ /html/
37
+ end
38
+ end
39
+
40
+ def header_name(request)
41
+ if request.content_security_policy_report_only
42
+ POLICY_REPORT_ONLY
43
+ else
44
+ POLICY
45
+ end
46
+ end
47
+
48
+ def policy_present?(headers)
49
+ headers[POLICY] || headers[POLICY_REPORT_ONLY]
50
+ end
51
+ end
52
+
53
+ module Request
54
+ POLICY = "action_dispatch.content_security_policy".freeze
55
+ POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only".freeze
56
+ NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator".freeze
57
+ NONCE = "action_dispatch.content_security_policy_nonce".freeze
58
+
59
+ def content_security_policy
60
+ get_header(POLICY)
61
+ end
62
+
63
+ def content_security_policy=(policy)
64
+ set_header(POLICY, policy)
65
+ end
66
+
67
+ def content_security_policy_report_only
68
+ get_header(POLICY_REPORT_ONLY)
69
+ end
70
+
71
+ def content_security_policy_report_only=(value)
72
+ set_header(POLICY_REPORT_ONLY, value)
73
+ end
74
+
75
+ def content_security_policy_nonce_generator
76
+ get_header(NONCE_GENERATOR)
77
+ end
78
+
79
+ def content_security_policy_nonce_generator=(generator)
80
+ set_header(NONCE_GENERATOR, generator)
81
+ end
82
+
83
+ def content_security_policy_nonce
84
+ if content_security_policy_nonce_generator
85
+ if nonce = get_header(NONCE)
86
+ nonce
87
+ else
88
+ set_header(NONCE, generate_content_security_policy_nonce)
89
+ end
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def generate_content_security_policy_nonce
96
+ content_security_policy_nonce_generator.call(self)
97
+ end
98
+ end
99
+
100
+ MAPPINGS = {
101
+ self: "'self'",
102
+ unsafe_eval: "'unsafe-eval'",
103
+ unsafe_inline: "'unsafe-inline'",
104
+ none: "'none'",
105
+ http: "http:",
106
+ https: "https:",
107
+ data: "data:",
108
+ mediastream: "mediastream:",
109
+ blob: "blob:",
110
+ filesystem: "filesystem:",
111
+ report_sample: "'report-sample'",
112
+ strict_dynamic: "'strict-dynamic'",
113
+ ws: "ws:",
114
+ wss: "wss:"
115
+ }.freeze
116
+
117
+ DIRECTIVES = {
118
+ base_uri: "base-uri",
119
+ child_src: "child-src",
120
+ connect_src: "connect-src",
121
+ default_src: "default-src",
122
+ font_src: "font-src",
123
+ form_action: "form-action",
124
+ frame_ancestors: "frame-ancestors",
125
+ frame_src: "frame-src",
126
+ img_src: "img-src",
127
+ manifest_src: "manifest-src",
128
+ media_src: "media-src",
129
+ object_src: "object-src",
130
+ script_src: "script-src",
131
+ style_src: "style-src",
132
+ worker_src: "worker-src"
133
+ }.freeze
134
+
135
+ NONCE_DIRECTIVES = %w[script-src].freeze
136
+
137
+ private_constant :MAPPINGS, :DIRECTIVES, :NONCE_DIRECTIVES
138
+
139
+ attr_reader :directives
140
+
141
+ def initialize
142
+ @directives = {}
143
+ yield self if block_given?
144
+ end
145
+
146
+ def initialize_copy(other)
147
+ @directives = other.directives.deep_dup
148
+ end
149
+
150
+ DIRECTIVES.each do |name, directive|
151
+ define_method(name) do |*sources|
152
+ if sources.first
153
+ @directives[directive] = apply_mappings(sources)
154
+ else
155
+ @directives.delete(directive)
156
+ end
157
+ end
158
+ end
159
+
160
+ def block_all_mixed_content(enabled = true)
161
+ if enabled
162
+ @directives["block-all-mixed-content"] = true
163
+ else
164
+ @directives.delete("block-all-mixed-content")
165
+ end
166
+ end
167
+
168
+ def plugin_types(*types)
169
+ if types.first
170
+ @directives["plugin-types"] = types
171
+ else
172
+ @directives.delete("plugin-types")
173
+ end
174
+ end
175
+
176
+ def report_uri(uri)
177
+ @directives["report-uri"] = [uri]
178
+ end
179
+
180
+ def require_sri_for(*types)
181
+ if types.first
182
+ @directives["require-sri-for"] = types
183
+ else
184
+ @directives.delete("require-sri-for")
185
+ end
186
+ end
187
+
188
+ def sandbox(*values)
189
+ if values.empty?
190
+ @directives["sandbox"] = true
191
+ elsif values.first
192
+ @directives["sandbox"] = values
193
+ else
194
+ @directives.delete("sandbox")
195
+ end
196
+ end
197
+
198
+ def upgrade_insecure_requests(enabled = true)
199
+ if enabled
200
+ @directives["upgrade-insecure-requests"] = true
201
+ else
202
+ @directives.delete("upgrade-insecure-requests")
203
+ end
204
+ end
205
+
206
+ def build(context = nil, nonce = nil)
207
+ build_directives(context, nonce).compact.join("; ")
208
+ end
209
+
210
+ private
211
+ def apply_mappings(sources)
212
+ sources.map do |source|
213
+ case source
214
+ when Symbol
215
+ apply_mapping(source)
216
+ when String, Proc
217
+ source
218
+ else
219
+ raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
220
+ end
221
+ end
222
+ end
223
+
224
+ def apply_mapping(source)
225
+ MAPPINGS.fetch(source) do
226
+ raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}"
227
+ end
228
+ end
229
+
230
+ def build_directives(context, nonce)
231
+ @directives.map do |directive, sources|
232
+ if sources.is_a?(Array)
233
+ if nonce && nonce_directive?(directive)
234
+ "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
235
+ else
236
+ "#{directive} #{build_directive(sources, context).join(' ')}"
237
+ end
238
+ elsif sources
239
+ directive
240
+ else
241
+ nil
242
+ end
243
+ end
244
+ end
245
+
246
+ def build_directive(sources, context)
247
+ sources.map { |source| resolve_source(source, context) }
248
+ end
249
+
250
+ def resolve_source(source, context)
251
+ case source
252
+ when String
253
+ source
254
+ when Symbol
255
+ source.to_s
256
+ when Proc
257
+ if context.nil?
258
+ raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}"
259
+ else
260
+ resolved = context.instance_exec(&source)
261
+ resolved.is_a?(Symbol) ? apply_mapping(resolved) : resolved
262
+ end
263
+ else
264
+ raise RuntimeError, "Unexpected content security policy source: #{source.inspect}"
265
+ end
266
+ end
267
+
268
+ def nonce_directive?(directive)
269
+ NONCE_DIRECTIVES.include?(directive)
270
+ end
271
+ end
272
+ end