actionpack 6.0.3.7 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +242 -268
- data/MIT-LICENSE +1 -1
- data/lib/abstract_controller.rb +1 -0
- data/lib/abstract_controller/base.rb +35 -2
- data/lib/abstract_controller/callbacks.rb +2 -2
- data/lib/abstract_controller/helpers.rb +105 -90
- data/lib/abstract_controller/rendering.rb +9 -9
- data/lib/abstract_controller/translation.rb +8 -2
- data/lib/action_controller.rb +2 -3
- data/lib/action_controller/api.rb +2 -2
- data/lib/action_controller/base.rb +4 -2
- data/lib/action_controller/caching.rb +0 -1
- data/lib/action_controller/log_subscriber.rb +3 -3
- data/lib/action_controller/metal.rb +2 -2
- data/lib/action_controller/metal/conditional_get.rb +10 -2
- data/lib/action_controller/metal/content_security_policy.rb +1 -1
- data/lib/action_controller/metal/data_streaming.rb +1 -1
- data/lib/action_controller/metal/etag_with_template_digest.rb +2 -4
- data/lib/action_controller/metal/exceptions.rb +33 -0
- data/lib/action_controller/metal/feature_policy.rb +46 -0
- data/lib/action_controller/metal/head.rb +7 -4
- data/lib/action_controller/metal/helpers.rb +11 -1
- data/lib/action_controller/metal/http_authentication.rb +5 -3
- data/lib/action_controller/metal/implicit_render.rb +1 -1
- data/lib/action_controller/metal/instrumentation.rb +11 -9
- data/lib/action_controller/metal/live.rb +1 -1
- data/lib/action_controller/metal/logging.rb +20 -0
- data/lib/action_controller/metal/mime_responds.rb +6 -2
- data/lib/action_controller/metal/parameter_encoding.rb +35 -4
- data/lib/action_controller/metal/params_wrapper.rb +14 -8
- data/lib/action_controller/metal/redirecting.rb +1 -1
- data/lib/action_controller/metal/rendering.rb +6 -0
- data/lib/action_controller/metal/request_forgery_protection.rb +48 -24
- data/lib/action_controller/metal/rescue.rb +1 -1
- data/lib/action_controller/metal/strong_parameters.rb +103 -15
- data/lib/action_controller/renderer.rb +24 -13
- data/lib/action_controller/test_case.rb +62 -56
- data/lib/action_dispatch.rb +3 -2
- data/lib/action_dispatch/http/cache.rb +12 -10
- data/lib/action_dispatch/http/content_disposition.rb +2 -2
- data/lib/action_dispatch/http/content_security_policy.rb +5 -1
- data/lib/action_dispatch/http/feature_policy.rb +168 -0
- data/lib/action_dispatch/http/filter_parameters.rb +1 -1
- data/lib/action_dispatch/http/filter_redirect.rb +1 -1
- data/lib/action_dispatch/http/headers.rb +3 -2
- data/lib/action_dispatch/http/mime_negotiation.rb +20 -8
- data/lib/action_dispatch/http/mime_type.rb +29 -16
- data/lib/action_dispatch/http/parameters.rb +1 -19
- data/lib/action_dispatch/http/request.rb +26 -8
- data/lib/action_dispatch/http/response.rb +17 -16
- data/lib/action_dispatch/http/url.rb +3 -2
- data/lib/action_dispatch/journey.rb +0 -2
- data/lib/action_dispatch/journey/formatter.rb +53 -28
- data/lib/action_dispatch/journey/gtg/builder.rb +22 -36
- data/lib/action_dispatch/journey/gtg/simulator.rb +8 -7
- data/lib/action_dispatch/journey/gtg/transition_table.rb +6 -4
- data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
- data/lib/action_dispatch/journey/nodes/node.rb +4 -3
- data/lib/action_dispatch/journey/parser.rb +13 -13
- data/lib/action_dispatch/journey/parser.y +1 -1
- data/lib/action_dispatch/journey/path/pattern.rb +13 -18
- data/lib/action_dispatch/journey/route.rb +7 -18
- data/lib/action_dispatch/journey/router.rb +26 -30
- data/lib/action_dispatch/journey/router/utils.rb +6 -4
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +2 -2
- data/lib/action_dispatch/middleware/cookies.rb +74 -33
- data/lib/action_dispatch/middleware/debug_exceptions.rb +10 -17
- data/lib/action_dispatch/middleware/debug_view.rb +1 -1
- data/lib/action_dispatch/middleware/exception_wrapper.rb +29 -17
- data/lib/action_dispatch/middleware/host_authorization.rb +28 -17
- data/lib/action_dispatch/middleware/public_exceptions.rb +1 -1
- data/lib/action_dispatch/middleware/remote_ip.rb +5 -4
- data/lib/action_dispatch/middleware/request_id.rb +4 -5
- data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -2
- data/lib/action_dispatch/middleware/session/cookie_store.rb +2 -2
- data/lib/action_dispatch/middleware/ssl.rb +9 -6
- data/lib/action_dispatch/middleware/stack.rb +18 -0
- data/lib/action_dispatch/middleware/static.rb +154 -93
- data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +18 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +2 -5
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +2 -2
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +2 -2
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +88 -8
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +12 -1
- data/lib/action_dispatch/railtie.rb +3 -2
- data/lib/action_dispatch/request/session.rb +2 -8
- data/lib/action_dispatch/request/utils.rb +26 -2
- data/lib/action_dispatch/routing/inspector.rb +8 -7
- data/lib/action_dispatch/routing/mapper.rb +102 -71
- data/lib/action_dispatch/routing/polymorphic_routes.rb +16 -19
- data/lib/action_dispatch/routing/redirection.rb +3 -3
- data/lib/action_dispatch/routing/route_set.rb +49 -41
- data/lib/action_dispatch/routing/url_for.rb +1 -0
- data/lib/action_dispatch/system_test_case.rb +29 -24
- data/lib/action_dispatch/system_testing/browser.rb +33 -27
- data/lib/action_dispatch/system_testing/driver.rb +6 -7
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +47 -6
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +4 -7
- data/lib/action_dispatch/testing/assertions.rb +1 -1
- data/lib/action_dispatch/testing/assertions/response.rb +2 -4
- data/lib/action_dispatch/testing/assertions/routing.rb +5 -5
- data/lib/action_dispatch/testing/integration.rb +38 -27
- data/lib/action_dispatch/testing/test_process.rb +29 -4
- data/lib/action_dispatch/testing/test_request.rb +3 -3
- data/lib/action_pack.rb +1 -1
- data/lib/action_pack/gem_version.rb +3 -3
- metadata +23 -24
- data/lib/action_controller/metal/force_ssl.rb +0 -58
- data/lib/action_dispatch/http/parameter_filter.rb +0 -12
- data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
- data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
- 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
|
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,
|
86
|
-
#
|
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
|
127
|
+
RACK_KEY_TRANSLATION[key] || key.to_s
|
124
128
|
end
|
125
129
|
|
126
130
|
def rack_value_for(key, value)
|
127
|
-
|
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
|
-
|
496
|
-
|
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
|
603
|
-
|
604
|
-
|
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"
|
data/lib/action_dispatch.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#--
|
4
|
-
# Copyright (c) 2004-
|
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.
|
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
|
-
|
154
|
-
cache_control[
|
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
|
-
|
187
|
-
if
|
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
|
197
|
-
|
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
|
-
|
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
|