actionpack 6.0.3.6 → 6.1.0.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 (114) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +243 -251
  3. data/MIT-LICENSE +1 -1
  4. data/lib/abstract_controller.rb +1 -0
  5. data/lib/abstract_controller/base.rb +35 -2
  6. data/lib/abstract_controller/callbacks.rb +2 -2
  7. data/lib/abstract_controller/helpers.rb +105 -90
  8. data/lib/abstract_controller/rendering.rb +9 -9
  9. data/lib/abstract_controller/translation.rb +8 -2
  10. data/lib/action_controller.rb +2 -3
  11. data/lib/action_controller/api.rb +2 -2
  12. data/lib/action_controller/base.rb +4 -2
  13. data/lib/action_controller/caching.rb +0 -1
  14. data/lib/action_controller/log_subscriber.rb +3 -3
  15. data/lib/action_controller/metal.rb +2 -2
  16. data/lib/action_controller/metal/conditional_get.rb +10 -2
  17. data/lib/action_controller/metal/content_security_policy.rb +1 -1
  18. data/lib/action_controller/metal/data_streaming.rb +1 -1
  19. data/lib/action_controller/metal/etag_with_template_digest.rb +2 -4
  20. data/lib/action_controller/metal/exceptions.rb +33 -0
  21. data/lib/action_controller/metal/feature_policy.rb +46 -0
  22. data/lib/action_controller/metal/head.rb +7 -4
  23. data/lib/action_controller/metal/helpers.rb +11 -1
  24. data/lib/action_controller/metal/http_authentication.rb +4 -2
  25. data/lib/action_controller/metal/implicit_render.rb +1 -1
  26. data/lib/action_controller/metal/instrumentation.rb +11 -9
  27. data/lib/action_controller/metal/live.rb +1 -1
  28. data/lib/action_controller/metal/logging.rb +20 -0
  29. data/lib/action_controller/metal/mime_responds.rb +6 -2
  30. data/lib/action_controller/metal/parameter_encoding.rb +35 -4
  31. data/lib/action_controller/metal/params_wrapper.rb +14 -8
  32. data/lib/action_controller/metal/redirecting.rb +1 -1
  33. data/lib/action_controller/metal/rendering.rb +6 -0
  34. data/lib/action_controller/metal/request_forgery_protection.rb +48 -24
  35. data/lib/action_controller/metal/rescue.rb +1 -1
  36. data/lib/action_controller/metal/strong_parameters.rb +103 -15
  37. data/lib/action_controller/renderer.rb +24 -13
  38. data/lib/action_controller/test_case.rb +62 -56
  39. data/lib/action_dispatch.rb +3 -2
  40. data/lib/action_dispatch/http/cache.rb +12 -10
  41. data/lib/action_dispatch/http/content_disposition.rb +2 -2
  42. data/lib/action_dispatch/http/content_security_policy.rb +5 -1
  43. data/lib/action_dispatch/http/feature_policy.rb +168 -0
  44. data/lib/action_dispatch/http/filter_parameters.rb +1 -1
  45. data/lib/action_dispatch/http/filter_redirect.rb +1 -1
  46. data/lib/action_dispatch/http/headers.rb +3 -2
  47. data/lib/action_dispatch/http/mime_negotiation.rb +20 -8
  48. data/lib/action_dispatch/http/mime_type.rb +28 -15
  49. data/lib/action_dispatch/http/parameters.rb +1 -19
  50. data/lib/action_dispatch/http/request.rb +26 -8
  51. data/lib/action_dispatch/http/response.rb +17 -16
  52. data/lib/action_dispatch/http/url.rb +3 -2
  53. data/lib/action_dispatch/journey.rb +0 -2
  54. data/lib/action_dispatch/journey/formatter.rb +53 -28
  55. data/lib/action_dispatch/journey/gtg/builder.rb +22 -36
  56. data/lib/action_dispatch/journey/gtg/simulator.rb +8 -7
  57. data/lib/action_dispatch/journey/gtg/transition_table.rb +6 -4
  58. data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
  59. data/lib/action_dispatch/journey/nodes/node.rb +4 -3
  60. data/lib/action_dispatch/journey/parser.rb +13 -13
  61. data/lib/action_dispatch/journey/parser.y +1 -1
  62. data/lib/action_dispatch/journey/path/pattern.rb +13 -18
  63. data/lib/action_dispatch/journey/route.rb +7 -18
  64. data/lib/action_dispatch/journey/router.rb +26 -30
  65. data/lib/action_dispatch/journey/router/utils.rb +6 -4
  66. data/lib/action_dispatch/middleware/actionable_exceptions.rb +2 -2
  67. data/lib/action_dispatch/middleware/cookies.rb +74 -33
  68. data/lib/action_dispatch/middleware/debug_exceptions.rb +10 -17
  69. data/lib/action_dispatch/middleware/debug_view.rb +1 -1
  70. data/lib/action_dispatch/middleware/exception_wrapper.rb +29 -17
  71. data/lib/action_dispatch/middleware/host_authorization.rb +28 -17
  72. data/lib/action_dispatch/middleware/public_exceptions.rb +1 -1
  73. data/lib/action_dispatch/middleware/remote_ip.rb +5 -4
  74. data/lib/action_dispatch/middleware/request_id.rb +4 -5
  75. data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -2
  76. data/lib/action_dispatch/middleware/session/cookie_store.rb +2 -2
  77. data/lib/action_dispatch/middleware/ssl.rb +9 -6
  78. data/lib/action_dispatch/middleware/stack.rb +18 -0
  79. data/lib/action_dispatch/middleware/static.rb +154 -93
  80. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +18 -0
  81. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +2 -5
  82. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +2 -2
  83. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +2 -2
  84. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +88 -8
  85. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  86. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +12 -1
  87. data/lib/action_dispatch/railtie.rb +3 -2
  88. data/lib/action_dispatch/request/session.rb +2 -8
  89. data/lib/action_dispatch/request/utils.rb +26 -2
  90. data/lib/action_dispatch/routing/inspector.rb +8 -7
  91. data/lib/action_dispatch/routing/mapper.rb +102 -71
  92. data/lib/action_dispatch/routing/polymorphic_routes.rb +12 -11
  93. data/lib/action_dispatch/routing/redirection.rb +3 -3
  94. data/lib/action_dispatch/routing/route_set.rb +49 -41
  95. data/lib/action_dispatch/routing/url_for.rb +1 -0
  96. data/lib/action_dispatch/system_test_case.rb +29 -24
  97. data/lib/action_dispatch/system_testing/browser.rb +33 -27
  98. data/lib/action_dispatch/system_testing/driver.rb +6 -7
  99. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +47 -6
  100. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +4 -7
  101. data/lib/action_dispatch/testing/assertions.rb +1 -1
  102. data/lib/action_dispatch/testing/assertions/response.rb +2 -4
  103. data/lib/action_dispatch/testing/assertions/routing.rb +5 -5
  104. data/lib/action_dispatch/testing/integration.rb +38 -27
  105. data/lib/action_dispatch/testing/test_process.rb +29 -4
  106. data/lib/action_dispatch/testing/test_request.rb +3 -3
  107. data/lib/action_pack.rb +1 -1
  108. data/lib/action_pack/gem_version.rb +3 -3
  109. metadata +20 -21
  110. data/lib/action_controller/metal/force_ssl.rb +0 -58
  111. data/lib/action_dispatch/http/parameter_filter.rb +0 -12
  112. data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
  113. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  114. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -119
@@ -65,7 +65,7 @@ module ActionController
65
65
  def initialize(controller, env, defaults)
66
66
  @controller = controller
67
67
  @defaults = defaults
68
- @env = normalize_keys defaults.merge(env)
68
+ @env = normalize_keys defaults, env
69
69
  end
70
70
 
71
71
  # Render templates with any options from ActionController::Base#render_to_string.
@@ -82,8 +82,12 @@ module ActionController
82
82
  # need to call <tt>.to_json</tt> on the object you want to render.
83
83
  # * <tt>:body</tt> - Renders provided text and sets content type of <tt>text/plain</tt>.
84
84
  #
85
- # If no <tt>options</tt> hash is passed or if <tt>:update</tt> is specified, the default is
86
- # to render a partial and use the second parameter as the locals hash.
85
+ # If no <tt>options</tt> hash is passed or if <tt>:update</tt> is specified, then:
86
+ #
87
+ # If an object responding to `render_in` is passed, `render_in` is called on the object,
88
+ # passing in the current view context.
89
+ #
90
+ # Otherwise, a partial is rendered using the second parameter as the locals hash.
87
91
  def render(*args)
88
92
  raise "missing controller" unless controller
89
93
 
@@ -95,11 +99,18 @@ module ActionController
95
99
  instance.set_response! controller.make_response!(request)
96
100
  instance.render_to_string(*args)
97
101
  end
102
+ alias_method :render_to_string, :render # :nodoc:
98
103
 
99
104
  private
100
- def normalize_keys(env)
105
+ def normalize_keys(defaults, env)
101
106
  new_env = {}
102
107
  env.each_pair { |k, v| new_env[rack_key_for(k)] = rack_value_for(k, v) }
108
+
109
+ defaults.each_pair do |k, v|
110
+ key = rack_key_for(k)
111
+ new_env[key] = rack_value_for(k, v) unless new_env.key?(key)
112
+ end
113
+
103
114
  new_env["rack.url_scheme"] = new_env["HTTPS"] == "on" ? "https" : "http"
104
115
  new_env
105
116
  end
@@ -112,19 +123,19 @@ module ActionController
112
123
  input: "rack.input"
113
124
  }
114
125
 
115
- IDENTITY = ->(_) { _ }
116
-
117
- RACK_VALUE_TRANSLATION = {
118
- https: ->(v) { v ? "on" : "off" },
119
- method: ->(v) { -v.upcase },
120
- }
121
-
122
126
  def rack_key_for(key)
123
- RACK_KEY_TRANSLATION.fetch(key, key.to_s)
127
+ RACK_KEY_TRANSLATION[key] || key.to_s
124
128
  end
125
129
 
126
130
  def rack_value_for(key, value)
127
- RACK_VALUE_TRANSLATION.fetch(key, IDENTITY).call value
131
+ case key
132
+ when :https
133
+ value ? "on" : "off"
134
+ when :method
135
+ -value.upcase
136
+ else
137
+ value
138
+ end
128
139
  end
129
140
  end
130
141
  end
@@ -84,7 +84,7 @@ module ActionController
84
84
  value = value.to_param
85
85
  end
86
86
 
87
- path_parameters[key] = value
87
+ path_parameters[key.to_sym] = value
88
88
  end
89
89
  end
90
90
 
@@ -492,57 +492,8 @@ module ActionController
492
492
  parameters[:format] = format
493
493
  end
494
494
 
495
- generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action))
496
- generated_path = generated_path(generated_extras)
497
- query_string_keys = query_parameter_names(generated_extras)
498
-
499
- @request.assign_parameters(@routes, controller_class_name, action, parameters, generated_path, query_string_keys)
500
-
501
- @request.session.update(session) if session
502
- @request.flash.update(flash || {})
503
-
504
- if xhr
505
- @request.set_header "HTTP_X_REQUESTED_WITH", "XMLHttpRequest"
506
- @request.fetch_header("HTTP_ACCEPT") do |k|
507
- @request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
508
- end
509
- end
510
-
511
- @request.fetch_header("SCRIPT_NAME") do |k|
512
- @request.set_header k, @controller.config.relative_url_root
513
- end
514
-
515
- begin
516
- @controller.recycle!
517
- @controller.dispatch(action, @request, @response)
518
- ensure
519
- @request = @controller.request
520
- @response = @controller.response
521
-
522
- if @request.have_cookie_jar?
523
- unless @request.cookie_jar.committed?
524
- @request.cookie_jar.write(@response)
525
- cookies.update(@request.cookie_jar.instance_variable_get(:@cookies))
526
- end
527
- end
528
- @response.prepare!
529
-
530
- if flash_value = @request.flash.to_session_value
531
- @request.session["flash"] = flash_value
532
- else
533
- @request.session.delete("flash")
534
- end
535
-
536
- if xhr
537
- @request.delete_header "HTTP_X_REQUESTED_WITH"
538
- @request.delete_header "HTTP_ACCEPT"
539
- end
540
- @request.query_string = ""
541
-
542
- @response.sent!
543
- end
544
-
545
- @response
495
+ setup_request(controller_class_name, action, parameters, session, flash, xhr)
496
+ process_controller_response(action, cookies, xhr)
546
497
  end
547
498
 
548
499
  def controller_class_name
@@ -598,11 +549,66 @@ module ActionController
598
549
  end
599
550
 
600
551
  private
552
+ def setup_request(controller_class_name, action, parameters, session, flash, xhr)
553
+ generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action))
554
+ generated_path = generated_path(generated_extras)
555
+ query_string_keys = query_parameter_names(generated_extras)
556
+
557
+ @request.assign_parameters(@routes, controller_class_name, action, parameters, generated_path, query_string_keys)
558
+
559
+ @request.session.update(session) if session
560
+ @request.flash.update(flash || {})
561
+
562
+ if xhr
563
+ @request.set_header "HTTP_X_REQUESTED_WITH", "XMLHttpRequest"
564
+ @request.fetch_header("HTTP_ACCEPT") do |k|
565
+ @request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
566
+ end
567
+ end
568
+
569
+ @request.fetch_header("SCRIPT_NAME") do |k|
570
+ @request.set_header k, @controller.config.relative_url_root
571
+ end
572
+ end
573
+
574
+ def process_controller_response(action, cookies, xhr)
575
+ begin
576
+ @controller.recycle!
577
+ @controller.dispatch(action, @request, @response)
578
+ ensure
579
+ @request = @controller.request
580
+ @response = @controller.response
581
+
582
+ if @request.have_cookie_jar?
583
+ unless @request.cookie_jar.committed?
584
+ @request.cookie_jar.write(@response)
585
+ cookies.update(@request.cookie_jar.instance_variable_get(:@cookies))
586
+ end
587
+ end
588
+ @response.prepare!
589
+
590
+ if flash_value = @request.flash.to_session_value
591
+ @request.session["flash"] = flash_value
592
+ else
593
+ @request.session.delete("flash")
594
+ end
595
+
596
+ if xhr
597
+ @request.delete_header "HTTP_X_REQUESTED_WITH"
598
+ @request.delete_header "HTTP_ACCEPT"
599
+ end
600
+ @request.query_string = ""
601
+
602
+ @response.sent!
603
+ end
604
+
605
+ @response
606
+ end
607
+
601
608
  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"
609
+ env.delete_if do |k, _|
610
+ k.start_with?("rack.request", "action_dispatch.request", "action_dispatch.rescue")
611
+ end
606
612
  env["rack.input"] = StringIO.new
607
613
  env.delete "CONTENT_LENGTH"
608
614
  env.delete "RAW_POST_DATA"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # Copyright (c) 2004-2019 David Heinemeier Hansson
4
+ # Copyright (c) 2004-2020 David Heinemeier Hansson
5
5
  #
6
6
  # Permission is hereby granted, free of charge, to any person obtaining
7
7
  # a copy of this software and associated documentation files (the
@@ -46,6 +46,7 @@ module ActionDispatch
46
46
  eager_autoload do
47
47
  autoload_under "http" do
48
48
  autoload :ContentSecurityPolicy
49
+ autoload :FeaturePolicy
49
50
  autoload :Request
50
51
  autoload :Response
51
52
  end
@@ -82,7 +83,6 @@ module ActionDispatch
82
83
  autoload :Headers
83
84
  autoload :MimeNegotiation
84
85
  autoload :Parameters
85
- autoload :ParameterFilter
86
86
  autoload :UploadedFile, "action_dispatch/http/upload"
87
87
  autoload :URL
88
88
  end
@@ -115,4 +115,5 @@ autoload :Mime, "action_dispatch/http/mime_type"
115
115
  ActiveSupport.on_load(:action_view) do
116
116
  ActionView::Base.default_formats ||= Mime::SET.symbols
117
117
  ActionView::Template::Types.delegate_to Mime
118
+ ActionView::LookupContext::DetailsKey.clear
118
119
  end
@@ -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/)
@@ -125,7 +125,7 @@ module ActionDispatch
125
125
  private
126
126
  DATE = "Date"
127
127
  LAST_MODIFIED = "Last-Modified"
128
- SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate])
128
+ SPECIAL_KEYS = Set.new(%w[extras no-store no-cache max-age public private must-revalidate])
129
129
 
130
130
  def generate_weak_etag(validators)
131
131
  "W/#{generate_strong_etag(validators)}"
@@ -150,8 +150,8 @@ module ActionDispatch
150
150
  directive, argument = segment.split("=", 2)
151
151
 
152
152
  if SPECIAL_KEYS.include? directive
153
- key = directive.tr("-", "_")
154
- cache_control[key.to_sym] = argument || true
153
+ directive.tr!("-", "_")
154
+ cache_control[directive.to_sym] = argument || true
155
155
  else
156
156
  cache_control[:extras] ||= []
157
157
  cache_control[:extras] << segment
@@ -166,6 +166,7 @@ module ActionDispatch
166
166
  end
167
167
 
168
168
  DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
169
+ NO_STORE = "no-store"
169
170
  NO_CACHE = "no-cache"
170
171
  PUBLIC = "public"
171
172
  PRIVATE = "private"
@@ -182,19 +183,20 @@ module ActionDispatch
182
183
  end
183
184
 
184
185
  def merge_and_normalize_cache_control!(cache_control)
185
- control = {}
186
- cc_headers = cache_control_headers
187
- 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)
188
191
  cache_control[:extras] ||= []
189
192
  cache_control[:extras] += extras
190
193
  cache_control[:extras].uniq!
191
194
  end
192
195
 
193
- control.merge! cc_headers
194
196
  control.merge! cache_control
195
197
 
196
- if control.empty?
197
- # Let middleware handle default behavior
198
+ if control[:no_store]
199
+ self._cache_control = NO_STORE
198
200
  elsif control[:no_cache]
199
201
  options = []
200
202
  options << PUBLIC if control[:public]
@@ -14,13 +14,13 @@ module ActionDispatch
14
14
  @filename = filename
15
15
  end
16
16
 
17
- TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/
17
+ TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!\#$+.^_`|~-]/
18
18
 
19
19
  def ascii_filename
20
20
  'filename="' + percent_escape(I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"'
21
21
  end
22
22
 
23
- RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/
23
+ RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!\#$&+.^_`|~-]/
24
24
 
25
25
  def utf8_filename
26
26
  "filename*=UTF-8''" + percent_escape(filename, RFC_5987_ESCAPED_CHAR)
@@ -33,7 +33,7 @@ module ActionDispatch #:nodoc:
33
33
  private
34
34
  def html_response?(headers)
35
35
  if content_type = headers[CONTENT_TYPE]
36
- content_type =~ /html/
36
+ /html/.match?(content_type)
37
37
  end
38
38
  end
39
39
 
@@ -137,7 +137,11 @@ module ActionDispatch #:nodoc:
137
137
  object_src: "object-src",
138
138
  prefetch_src: "prefetch-src",
139
139
  script_src: "script-src",
140
+ script_src_attr: "script-src-attr",
141
+ script_src_elem: "script-src-elem",
140
142
  style_src: "style-src",
143
+ style_src_attr: "style-src-attr",
144
+ style_src_elem: "style-src-elem",
141
145
  worker_src: "worker-src"
142
146
  }.freeze
143
147
 
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/deep_dup"
4
+
5
+ module ActionDispatch #:nodoc:
6
+ class FeaturePolicy
7
+ class Middleware
8
+ CONTENT_TYPE = "Content-Type"
9
+ POLICY = "Feature-Policy"
10
+
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env)
16
+ request = ActionDispatch::Request.new(env)
17
+ _, headers, _ = response = @app.call(env)
18
+
19
+ return response unless html_response?(headers)
20
+ return response if policy_present?(headers)
21
+
22
+ if policy = request.feature_policy
23
+ headers[POLICY] = policy.build(request.controller_instance)
24
+ end
25
+
26
+ if policy_empty?(policy)
27
+ headers.delete(POLICY)
28
+ end
29
+
30
+ response
31
+ end
32
+
33
+ private
34
+ def html_response?(headers)
35
+ if content_type = headers[CONTENT_TYPE]
36
+ /html/.match?(content_type)
37
+ end
38
+ end
39
+
40
+ def policy_present?(headers)
41
+ headers[POLICY]
42
+ end
43
+
44
+ def policy_empty?(policy)
45
+ policy&.directives&.empty?
46
+ end
47
+ end
48
+
49
+ module Request
50
+ POLICY = "action_dispatch.feature_policy"
51
+
52
+ def feature_policy
53
+ get_header(POLICY)
54
+ end
55
+
56
+ def feature_policy=(policy)
57
+ set_header(POLICY, policy)
58
+ end
59
+ end
60
+
61
+ MAPPINGS = {
62
+ self: "'self'",
63
+ none: "'none'",
64
+ }.freeze
65
+
66
+ # List of available features can be found at
67
+ # https://github.com/WICG/feature-policy/blob/master/features.md#policy-controlled-features
68
+ DIRECTIVES = {
69
+ accelerometer: "accelerometer",
70
+ ambient_light_sensor: "ambient-light-sensor",
71
+ autoplay: "autoplay",
72
+ camera: "camera",
73
+ encrypted_media: "encrypted-media",
74
+ fullscreen: "fullscreen",
75
+ geolocation: "geolocation",
76
+ gyroscope: "gyroscope",
77
+ magnetometer: "magnetometer",
78
+ microphone: "microphone",
79
+ midi: "midi",
80
+ payment: "payment",
81
+ picture_in_picture: "picture-in-picture",
82
+ speaker: "speaker",
83
+ usb: "usb",
84
+ vibrate: "vibrate",
85
+ vr: "vr",
86
+ }.freeze
87
+
88
+ private_constant :MAPPINGS, :DIRECTIVES
89
+
90
+ attr_reader :directives
91
+
92
+ def initialize
93
+ @directives = {}
94
+ yield self if block_given?
95
+ end
96
+
97
+ def initialize_copy(other)
98
+ @directives = other.directives.deep_dup
99
+ end
100
+
101
+ DIRECTIVES.each do |name, directive|
102
+ define_method(name) do |*sources|
103
+ if sources.first
104
+ @directives[directive] = apply_mappings(sources)
105
+ else
106
+ @directives.delete(directive)
107
+ end
108
+ end
109
+ end
110
+
111
+ def build(context = nil)
112
+ build_directives(context).compact.join("; ")
113
+ end
114
+
115
+ private
116
+ def apply_mappings(sources)
117
+ sources.map do |source|
118
+ case source
119
+ when Symbol
120
+ apply_mapping(source)
121
+ when String, Proc
122
+ source
123
+ else
124
+ raise ArgumentError, "Invalid HTTP feature policy source: #{source.inspect}"
125
+ end
126
+ end
127
+ end
128
+
129
+ def apply_mapping(source)
130
+ MAPPINGS.fetch(source) do
131
+ raise ArgumentError, "Unknown HTTP feature policy source mapping: #{source.inspect}"
132
+ end
133
+ end
134
+
135
+ def build_directives(context)
136
+ @directives.map do |directive, sources|
137
+ if sources.is_a?(Array)
138
+ "#{directive} #{build_directive(sources, context).join(' ')}"
139
+ elsif sources
140
+ directive
141
+ else
142
+ nil
143
+ end
144
+ end
145
+ end
146
+
147
+ def build_directive(sources, context)
148
+ sources.map { |source| resolve_source(source, context) }
149
+ end
150
+
151
+ def resolve_source(source, context)
152
+ case source
153
+ when String
154
+ source
155
+ when Symbol
156
+ source.to_s
157
+ when Proc
158
+ if context.nil?
159
+ raise RuntimeError, "Missing context for the dynamic feature policy source: #{source.inspect}"
160
+ else
161
+ context.instance_exec(&source)
162
+ end
163
+ else
164
+ raise RuntimeError, "Unexpected feature policy source: #{source.inspect}"
165
+ end
166
+ end
167
+ end
168
+ end