actionpack 7.2.2.1 → 8.1.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +408 -95
- data/README.rdoc +1 -1
- data/lib/abstract_controller/asset_paths.rb +4 -2
- data/lib/abstract_controller/base.rb +12 -17
- data/lib/abstract_controller/caching.rb +6 -3
- data/lib/abstract_controller/callbacks.rb +6 -0
- data/lib/abstract_controller/collector.rb +1 -1
- data/lib/abstract_controller/helpers.rb +1 -1
- data/lib/abstract_controller/logger.rb +2 -1
- data/lib/abstract_controller/rendering.rb +0 -1
- data/lib/action_controller/api.rb +1 -0
- data/lib/action_controller/base.rb +3 -2
- data/lib/action_controller/caching.rb +1 -2
- data/lib/action_controller/form_builder.rb +4 -4
- data/lib/action_controller/log_subscriber.rb +22 -3
- data/lib/action_controller/metal/allow_browser.rb +12 -2
- data/lib/action_controller/metal/conditional_get.rb +30 -1
- data/lib/action_controller/metal/data_streaming.rb +5 -5
- data/lib/action_controller/metal/exceptions.rb +5 -0
- data/lib/action_controller/metal/flash.rb +1 -4
- data/lib/action_controller/metal/head.rb +3 -1
- data/lib/action_controller/metal/instrumentation.rb +1 -2
- data/lib/action_controller/metal/live.rb +66 -26
- data/lib/action_controller/metal/params_wrapper.rb +3 -3
- data/lib/action_controller/metal/permissions_policy.rb +9 -0
- data/lib/action_controller/metal/rate_limiting.rb +39 -9
- data/lib/action_controller/metal/redirecting.rb +109 -16
- data/lib/action_controller/metal/renderers.rb +29 -9
- data/lib/action_controller/metal/rendering.rb +8 -2
- data/lib/action_controller/metal/request_forgery_protection.rb +21 -11
- data/lib/action_controller/metal/rescue.rb +9 -0
- data/lib/action_controller/metal/streaming.rb +5 -84
- data/lib/action_controller/metal/strong_parameters.rb +277 -92
- data/lib/action_controller/railtie.rb +33 -15
- data/lib/action_controller/renderer.rb +0 -1
- data/lib/action_controller/structured_event_subscriber.rb +116 -0
- data/lib/action_controller/test_case.rb +12 -2
- data/lib/action_dispatch/constants.rb +6 -0
- data/lib/action_dispatch/http/cache.rb +138 -11
- data/lib/action_dispatch/http/content_security_policy.rb +14 -1
- data/lib/action_dispatch/http/filter_parameters.rb +5 -3
- data/lib/action_dispatch/http/mime_negotiation.rb +63 -4
- data/lib/action_dispatch/http/mime_types.rb +1 -0
- data/lib/action_dispatch/http/param_builder.rb +187 -0
- data/lib/action_dispatch/http/param_error.rb +26 -0
- data/lib/action_dispatch/http/parameters.rb +3 -3
- data/lib/action_dispatch/http/permissions_policy.rb +6 -0
- data/lib/action_dispatch/http/query_parser.rb +55 -0
- data/lib/action_dispatch/http/request.rb +73 -23
- data/lib/action_dispatch/http/response.rb +65 -17
- data/lib/action_dispatch/http/url.rb +112 -16
- data/lib/action_dispatch/journey/formatter.rb +8 -3
- data/lib/action_dispatch/journey/gtg/simulator.rb +33 -12
- data/lib/action_dispatch/journey/gtg/transition_table.rb +37 -45
- data/lib/action_dispatch/journey/nodes/node.rb +2 -1
- data/lib/action_dispatch/journey/parser.rb +99 -196
- data/lib/action_dispatch/journey/route.rb +45 -31
- data/lib/action_dispatch/journey/router/utils.rb +8 -14
- data/lib/action_dispatch/journey/router.rb +59 -81
- data/lib/action_dispatch/journey/routes.rb +7 -0
- data/lib/action_dispatch/journey/scanner.rb +44 -42
- data/lib/action_dispatch/journey/visitors.rb +55 -23
- data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
- data/lib/action_dispatch/log_subscriber.rb +7 -3
- data/lib/action_dispatch/middleware/cookies.rb +8 -4
- data/lib/action_dispatch/middleware/debug_exceptions.rb +26 -5
- data/lib/action_dispatch/middleware/debug_view.rb +11 -5
- data/lib/action_dispatch/middleware/exception_wrapper.rb +14 -14
- data/lib/action_dispatch/middleware/executor.rb +17 -4
- data/lib/action_dispatch/middleware/public_exceptions.rb +6 -6
- data/lib/action_dispatch/middleware/remote_ip.rb +11 -5
- data/lib/action_dispatch/middleware/request_id.rb +2 -1
- data/lib/action_dispatch/middleware/session/cache_store.rb +17 -0
- data/lib/action_dispatch/middleware/ssl.rb +13 -3
- data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -5
- data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
- data/lib/action_dispatch/railtie.rb +21 -0
- data/lib/action_dispatch/request/session.rb +1 -0
- data/lib/action_dispatch/request/utils.rb +9 -3
- data/lib/action_dispatch/routing/inspector.rb +80 -57
- data/lib/action_dispatch/routing/mapper.rb +409 -228
- data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -2
- data/lib/action_dispatch/routing/redirection.rb +10 -7
- data/lib/action_dispatch/routing/route_set.rb +21 -12
- data/lib/action_dispatch/routing/routes_proxy.rb +1 -0
- data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
- data/lib/action_dispatch/system_test_case.rb +3 -3
- data/lib/action_dispatch/system_testing/browser.rb +12 -21
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +2 -2
- data/lib/action_dispatch/testing/assertion_response.rb +1 -1
- data/lib/action_dispatch/testing/assertions/response.rb +26 -2
- data/lib/action_dispatch/testing/assertions/routing.rb +27 -15
- data/lib/action_dispatch/testing/integration.rb +16 -7
- data/lib/action_dispatch/testing/request_encoder.rb +9 -9
- data/lib/action_dispatch/testing/test_process.rb +1 -2
- data/lib/action_dispatch.rb +14 -4
- data/lib/action_pack/gem_version.rb +3 -3
- metadata +19 -38
- data/lib/action_dispatch/journey/parser.y +0 -50
- data/lib/action_dispatch/journey/parser_extras.rb +0 -33
|
@@ -96,7 +96,6 @@ module ActionController
|
|
|
96
96
|
# * `:script_name` - The portion of the incoming request's URL path that
|
|
97
97
|
# corresponds to the application. Converts to Rack's `SCRIPT_NAME`.
|
|
98
98
|
# * `:input` - The input stream. Converts to Rack's `rack.input`.
|
|
99
|
-
#
|
|
100
99
|
# * `defaults` - Default values for the Rack env. Entries are specified in the
|
|
101
100
|
# same format as `env`. `env` will be merged on top of these values.
|
|
102
101
|
# `defaults` will be retained when calling #new on a renderer instance.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionController
|
|
4
|
+
class StructuredEventSubscriber < ActiveSupport::StructuredEventSubscriber # :nodoc:
|
|
5
|
+
INTERNAL_PARAMS = %w(controller action format _method only_path)
|
|
6
|
+
|
|
7
|
+
def start_processing(event)
|
|
8
|
+
payload = event.payload
|
|
9
|
+
params = {}
|
|
10
|
+
payload[:params].each_pair do |k, v|
|
|
11
|
+
params[k] = v unless INTERNAL_PARAMS.include?(k)
|
|
12
|
+
end
|
|
13
|
+
format = payload[:format]
|
|
14
|
+
format = format.to_s.upcase if format.is_a?(Symbol)
|
|
15
|
+
format = "*/*" if format.nil?
|
|
16
|
+
|
|
17
|
+
emit_event("action_controller.request_started",
|
|
18
|
+
controller: payload[:controller],
|
|
19
|
+
action: payload[:action],
|
|
20
|
+
format:,
|
|
21
|
+
params:,
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def process_action(event)
|
|
26
|
+
payload = event.payload
|
|
27
|
+
status = payload[:status]
|
|
28
|
+
|
|
29
|
+
if status.nil? && (exception_class_name = payload[:exception]&.first)
|
|
30
|
+
status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
emit_event("action_controller.request_completed", {
|
|
34
|
+
controller: payload[:controller],
|
|
35
|
+
action: payload[:action],
|
|
36
|
+
status: status,
|
|
37
|
+
**additions_for(payload),
|
|
38
|
+
duration_ms: event.duration.round(2),
|
|
39
|
+
gc_time_ms: event.gc_time.round(1),
|
|
40
|
+
}.compact)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def halted_callback(event)
|
|
44
|
+
emit_event("action_controller.callback_halted", filter: event.payload[:filter])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def rescue_from_callback(event)
|
|
48
|
+
exception = event.payload[:exception]
|
|
49
|
+
|
|
50
|
+
exception_backtrace = exception.backtrace&.first
|
|
51
|
+
exception_backtrace = exception_backtrace&.delete_prefix("#{Rails.root}/") if defined?(Rails.root) && Rails.root
|
|
52
|
+
|
|
53
|
+
emit_event("action_controller.rescue_from_handled",
|
|
54
|
+
exception_class: exception.class.name,
|
|
55
|
+
exception_message: exception.message,
|
|
56
|
+
exception_backtrace:
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def send_file(event)
|
|
61
|
+
emit_event("action_controller.file_sent", path: event.payload[:path], duration_ms: event.duration.round(1))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def redirect_to(event)
|
|
65
|
+
emit_event("action_controller.redirected", location: event.payload[:location])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def send_data(event)
|
|
69
|
+
emit_event("action_controller.data_sent", filename: event.payload[:filename], duration_ms: event.duration.round(1))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def unpermitted_parameters(event)
|
|
73
|
+
unpermitted_keys = event.payload[:keys]
|
|
74
|
+
context = event.payload[:context]
|
|
75
|
+
|
|
76
|
+
emit_debug_event("action_controller.unpermitted_parameters",
|
|
77
|
+
unpermitted_keys:,
|
|
78
|
+
context: context.except(:request)
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
debug_only :unpermitted_parameters
|
|
82
|
+
|
|
83
|
+
def write_fragment(event)
|
|
84
|
+
fragment_cache(__method__, event)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def read_fragment(event)
|
|
88
|
+
fragment_cache(__method__, event)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def exist_fragment?(event)
|
|
92
|
+
fragment_cache(__method__, event)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def expire_fragment(event)
|
|
96
|
+
fragment_cache(__method__, event)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
def fragment_cache(method_name, event)
|
|
101
|
+
key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path])
|
|
102
|
+
|
|
103
|
+
emit_event("action_controller.fragment_cache",
|
|
104
|
+
method: "#{method_name}",
|
|
105
|
+
key: key,
|
|
106
|
+
duration_ms: event.duration.round(1)
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def additions_for(payload)
|
|
111
|
+
payload.slice(:view_runtime, :db_runtime, :queries_count, :cached_queries_count)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
ActionController::StructuredEventSubscriber.attach_to :action_controller
|
|
@@ -22,11 +22,21 @@ module ActionController
|
|
|
22
22
|
# database on the main thread, so they could open a txn, then the controller
|
|
23
23
|
# thread will open a new connection and try to access data that's only visible
|
|
24
24
|
# to the main thread's txn. This is the problem in #23483.
|
|
25
|
+
alias_method :original_new_controller_thread, :new_controller_thread
|
|
26
|
+
|
|
25
27
|
silence_redefinition_of_method :new_controller_thread
|
|
26
28
|
def new_controller_thread # :nodoc:
|
|
27
29
|
yield
|
|
28
30
|
end
|
|
29
31
|
|
|
32
|
+
# Because of the above, we need to prevent the clearing of thread locals, since
|
|
33
|
+
# no new thread is actually spawned in the test environment.
|
|
34
|
+
alias_method :original_clean_up_thread_locals, :clean_up_thread_locals
|
|
35
|
+
|
|
36
|
+
silence_redefinition_of_method :clean_up_thread_locals
|
|
37
|
+
def clean_up_thread_locals(*args) # :nodoc:
|
|
38
|
+
end
|
|
39
|
+
|
|
30
40
|
# Avoid a deadlock from the queue filling up
|
|
31
41
|
Buffer.queue_size = nil
|
|
32
42
|
end
|
|
@@ -106,7 +116,7 @@ module ActionController
|
|
|
106
116
|
set_header k, "application/x-www-form-urlencoded"
|
|
107
117
|
end
|
|
108
118
|
|
|
109
|
-
case content_mime_type
|
|
119
|
+
case content_mime_type&.to_sym
|
|
110
120
|
when nil
|
|
111
121
|
raise "Unknown Content-Type: #{content_type}"
|
|
112
122
|
when :json
|
|
@@ -121,7 +131,7 @@ module ActionController
|
|
|
121
131
|
end
|
|
122
132
|
end
|
|
123
133
|
|
|
124
|
-
data_stream = StringIO.new(data)
|
|
134
|
+
data_stream = StringIO.new(data.b)
|
|
125
135
|
set_header "CONTENT_LENGTH", data_stream.length.to_s
|
|
126
136
|
set_header "rack.input", data_stream
|
|
127
137
|
end
|
|
@@ -30,5 +30,11 @@ module ActionDispatch
|
|
|
30
30
|
SERVER_TIMING = "server-timing"
|
|
31
31
|
STRICT_TRANSPORT_SECURITY = "strict-transport-security"
|
|
32
32
|
end
|
|
33
|
+
|
|
34
|
+
if Gem::Version.new(Rack::RELEASE) < Gem::Version.new("3.1")
|
|
35
|
+
UNPROCESSABLE_CONTENT = :unprocessable_entity
|
|
36
|
+
else
|
|
37
|
+
UNPROCESSABLE_CONTENT = :unprocessable_content
|
|
38
|
+
end
|
|
33
39
|
end
|
|
34
40
|
end
|
|
@@ -9,6 +9,8 @@ module ActionDispatch
|
|
|
9
9
|
HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE"
|
|
10
10
|
HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH"
|
|
11
11
|
|
|
12
|
+
mattr_accessor :strict_freshness, default: false
|
|
13
|
+
|
|
12
14
|
def if_modified_since
|
|
13
15
|
if since = get_header(HTTP_IF_MODIFIED_SINCE)
|
|
14
16
|
Time.rfc2822(since) rescue nil
|
|
@@ -34,19 +36,140 @@ module ActionDispatch
|
|
|
34
36
|
end
|
|
35
37
|
end
|
|
36
38
|
|
|
37
|
-
# Check response freshness (`Last-Modified` and ETag) against request
|
|
38
|
-
# `If-Modified-Since` and `If-None-Match` conditions.
|
|
39
|
-
# supplied,
|
|
39
|
+
# Check response freshness (`Last-Modified` and `ETag`) against request
|
|
40
|
+
# `If-Modified-Since` and `If-None-Match` conditions.
|
|
41
|
+
# If both headers are supplied, based on configuration, either `ETag` is preferred over `Last-Modified`
|
|
42
|
+
# or both are considered equally. You can adjust the preference with
|
|
43
|
+
# `config.action_dispatch.strict_freshness`.
|
|
44
|
+
# Reference: http://tools.ietf.org/html/rfc7232#section-6
|
|
40
45
|
def fresh?(response)
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
if Request.strict_freshness
|
|
47
|
+
if if_none_match
|
|
48
|
+
etag_matches?(response.etag)
|
|
49
|
+
elsif if_modified_since
|
|
50
|
+
not_modified?(response.last_modified)
|
|
51
|
+
else
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
last_modified = if_modified_since
|
|
56
|
+
etag = if_none_match
|
|
57
|
+
|
|
58
|
+
return false unless last_modified || etag
|
|
59
|
+
|
|
60
|
+
success = true
|
|
61
|
+
success &&= not_modified?(response.last_modified) if last_modified
|
|
62
|
+
success &&= etag_matches?(response.etag) if etag
|
|
63
|
+
success
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def cache_control_directives
|
|
68
|
+
@cache_control_directives ||= CacheControlDirectives.new(get_header("HTTP_CACHE_CONTROL"))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Represents the HTTP Cache-Control header for requests,
|
|
72
|
+
# providing methods to access various cache control directives
|
|
73
|
+
# Reference: https://www.rfc-editor.org/rfc/rfc9111.html#name-request-directives
|
|
74
|
+
class CacheControlDirectives
|
|
75
|
+
def initialize(cache_control_header)
|
|
76
|
+
@only_if_cached = false
|
|
77
|
+
@no_cache = false
|
|
78
|
+
@no_store = false
|
|
79
|
+
@no_transform = false
|
|
80
|
+
@max_age = nil
|
|
81
|
+
@max_stale = nil
|
|
82
|
+
@min_fresh = nil
|
|
83
|
+
@stale_if_error = false
|
|
84
|
+
parse_directives(cache_control_header)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns true if the only-if-cached directive is present.
|
|
88
|
+
# This directive indicates that the client only wishes to obtain a
|
|
89
|
+
# stored response. If a valid stored response is not available,
|
|
90
|
+
# the server should respond with a 504 (Gateway Timeout) status.
|
|
91
|
+
def only_if_cached?
|
|
92
|
+
@only_if_cached
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Returns true if the no-cache directive is present.
|
|
96
|
+
# This directive indicates that a cache must not use the response
|
|
97
|
+
# to satisfy subsequent requests without successful validation on the origin server.
|
|
98
|
+
def no_cache?
|
|
99
|
+
@no_cache
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns true if the no-store directive is present.
|
|
103
|
+
# This directive indicates that a cache must not store any part of the
|
|
104
|
+
# request or response.
|
|
105
|
+
def no_store?
|
|
106
|
+
@no_store
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Returns true if the no-transform directive is present.
|
|
110
|
+
# This directive indicates that a cache or proxy must not transform the payload.
|
|
111
|
+
def no_transform?
|
|
112
|
+
@no_transform
|
|
113
|
+
end
|
|
43
114
|
|
|
44
|
-
|
|
115
|
+
# Returns the value of the max-age directive.
|
|
116
|
+
# This directive indicates that the client is willing to accept a response
|
|
117
|
+
# whose age is no greater than the specified number of seconds.
|
|
118
|
+
attr_reader :max_age
|
|
45
119
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
120
|
+
# Returns the value of the max-stale directive.
|
|
121
|
+
# When max-stale is present with a value, returns that integer value.
|
|
122
|
+
# When max-stale is present without a value, returns true (unlimited staleness).
|
|
123
|
+
# When max-stale is not present, returns nil.
|
|
124
|
+
attr_reader :max_stale
|
|
125
|
+
|
|
126
|
+
# Returns true if max-stale directive is present (with or without a value)
|
|
127
|
+
def max_stale?
|
|
128
|
+
!@max_stale.nil?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Returns true if max-stale directive is present without a value (unlimited staleness)
|
|
132
|
+
def max_stale_unlimited?
|
|
133
|
+
@max_stale == true
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Returns the value of the min-fresh directive.
|
|
137
|
+
# This directive indicates that the client is willing to accept a response
|
|
138
|
+
# whose freshness lifetime is no less than its current age plus the specified time in seconds.
|
|
139
|
+
attr_reader :min_fresh
|
|
140
|
+
|
|
141
|
+
# Returns the value of the stale-if-error directive.
|
|
142
|
+
# This directive indicates that the client is willing to accept a stale response
|
|
143
|
+
# if the check for a fresh one fails with an error for the specified number of seconds.
|
|
144
|
+
attr_reader :stale_if_error
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
def parse_directives(header_value)
|
|
148
|
+
return unless header_value
|
|
149
|
+
|
|
150
|
+
header_value.delete(" ").downcase.split(",").each do |directive|
|
|
151
|
+
name, value = directive.split("=", 2)
|
|
152
|
+
|
|
153
|
+
case name
|
|
154
|
+
when "max-age"
|
|
155
|
+
@max_age = value.to_i
|
|
156
|
+
when "min-fresh"
|
|
157
|
+
@min_fresh = value.to_i
|
|
158
|
+
when "stale-if-error"
|
|
159
|
+
@stale_if_error = value.to_i
|
|
160
|
+
when "no-cache"
|
|
161
|
+
@no_cache = true
|
|
162
|
+
when "no-store"
|
|
163
|
+
@no_store = true
|
|
164
|
+
when "no-transform"
|
|
165
|
+
@no_transform = true
|
|
166
|
+
when "only-if-cached"
|
|
167
|
+
@only_if_cached = true
|
|
168
|
+
when "max-stale"
|
|
169
|
+
@max_stale = value ? value.to_i : true
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
50
173
|
end
|
|
51
174
|
end
|
|
52
175
|
|
|
@@ -127,7 +250,7 @@ module ActionDispatch
|
|
|
127
250
|
private
|
|
128
251
|
DATE = "Date"
|
|
129
252
|
LAST_MODIFIED = "Last-Modified"
|
|
130
|
-
SPECIAL_KEYS = Set.new(%w[extras no-store no-cache max-age public private must-revalidate])
|
|
253
|
+
SPECIAL_KEYS = Set.new(%w[extras no-store no-cache max-age public private must-revalidate must-understand])
|
|
131
254
|
|
|
132
255
|
def generate_weak_etag(validators)
|
|
133
256
|
"W/#{generate_strong_etag(validators)}"
|
|
@@ -171,6 +294,8 @@ module ActionDispatch
|
|
|
171
294
|
PUBLIC = "public"
|
|
172
295
|
PRIVATE = "private"
|
|
173
296
|
MUST_REVALIDATE = "must-revalidate"
|
|
297
|
+
IMMUTABLE = "immutable"
|
|
298
|
+
MUST_UNDERSTAND = "must-understand"
|
|
174
299
|
|
|
175
300
|
def handle_conditional_get!
|
|
176
301
|
# Normally default cache control setting is handled by ETag middleware. But, if
|
|
@@ -205,6 +330,7 @@ module ActionDispatch
|
|
|
205
330
|
|
|
206
331
|
if control[:no_store]
|
|
207
332
|
options << PRIVATE if control[:private]
|
|
333
|
+
options << MUST_UNDERSTAND if control[:must_understand]
|
|
208
334
|
options << NO_STORE
|
|
209
335
|
elsif control[:no_cache]
|
|
210
336
|
options << PUBLIC if control[:public]
|
|
@@ -221,6 +347,7 @@ module ActionDispatch
|
|
|
221
347
|
options << MUST_REVALIDATE if control[:must_revalidate]
|
|
222
348
|
options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
|
|
223
349
|
options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
|
|
350
|
+
options << IMMUTABLE if control[:immutable]
|
|
224
351
|
options.concat(extras) if extras
|
|
225
352
|
end
|
|
226
353
|
|
|
@@ -128,6 +128,7 @@ module ActionDispatch # :nodoc:
|
|
|
128
128
|
MAPPINGS = {
|
|
129
129
|
self: "'self'",
|
|
130
130
|
unsafe_eval: "'unsafe-eval'",
|
|
131
|
+
wasm_unsafe_eval: "'wasm-unsafe-eval'",
|
|
131
132
|
unsafe_hashes: "'unsafe-hashes'",
|
|
132
133
|
unsafe_inline: "'unsafe-inline'",
|
|
133
134
|
none: "'none'",
|
|
@@ -170,6 +171,8 @@ module ActionDispatch # :nodoc:
|
|
|
170
171
|
worker_src: "worker-src"
|
|
171
172
|
}.freeze
|
|
172
173
|
|
|
174
|
+
HASH_SOURCE_ALGORITHM_PREFIXES = ["sha256-", "sha384-", "sha512-"].freeze
|
|
175
|
+
|
|
173
176
|
DEFAULT_NONCE_DIRECTIVES = %w[script-src style-src].freeze
|
|
174
177
|
|
|
175
178
|
private_constant :MAPPINGS, :DIRECTIVES, :DEFAULT_NONCE_DIRECTIVES
|
|
@@ -304,7 +307,13 @@ module ActionDispatch # :nodoc:
|
|
|
304
307
|
case source
|
|
305
308
|
when Symbol
|
|
306
309
|
apply_mapping(source)
|
|
307
|
-
when String
|
|
310
|
+
when String
|
|
311
|
+
if hash_source?(source)
|
|
312
|
+
"'#{source}'"
|
|
313
|
+
else
|
|
314
|
+
source
|
|
315
|
+
end
|
|
316
|
+
when Proc
|
|
308
317
|
source
|
|
309
318
|
else
|
|
310
319
|
raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
|
|
@@ -373,5 +382,9 @@ module ActionDispatch # :nodoc:
|
|
|
373
382
|
def nonce_directive?(directive, nonce_directives)
|
|
374
383
|
nonce_directives.include?(directive)
|
|
375
384
|
end
|
|
385
|
+
|
|
386
|
+
def hash_source?(source)
|
|
387
|
+
source.start_with?(*HASH_SOURCE_ALGORITHM_PREFIXES)
|
|
388
|
+
end
|
|
376
389
|
end
|
|
377
390
|
end
|
|
@@ -17,9 +17,11 @@ module ActionDispatch
|
|
|
17
17
|
# For more information about filter behavior, see
|
|
18
18
|
# ActiveSupport::ParameterFilter.
|
|
19
19
|
module FilterParameters
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
# :stopdoc:
|
|
21
|
+
ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"]
|
|
22
|
+
NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new
|
|
23
|
+
NULL_ENV_FILTER = ActiveSupport::ParameterFilter.new ENV_MATCH
|
|
24
|
+
# :startdoc:
|
|
23
25
|
|
|
24
26
|
def initialize
|
|
25
27
|
super
|
|
@@ -56,9 +56,14 @@ module ActionDispatch
|
|
|
56
56
|
|
|
57
57
|
# Returns the MIME type for the format used in the request.
|
|
58
58
|
#
|
|
59
|
-
# GET /posts/5.xml
|
|
60
|
-
#
|
|
61
|
-
#
|
|
59
|
+
# # GET /posts/5.xml
|
|
60
|
+
# request.format # => Mime[:xml]
|
|
61
|
+
#
|
|
62
|
+
# # GET /posts/5.xhtml
|
|
63
|
+
# request.format # => Mime[:html]
|
|
64
|
+
#
|
|
65
|
+
# # GET /posts/5
|
|
66
|
+
# request.format # => Mime[:html] or Mime[:js], or request.accepts.first
|
|
62
67
|
#
|
|
63
68
|
def format(_view_path = nil)
|
|
64
69
|
formats.first || Mime::NullType.instance
|
|
@@ -86,7 +91,49 @@ module ActionDispatch
|
|
|
86
91
|
end
|
|
87
92
|
end
|
|
88
93
|
|
|
89
|
-
# Sets the variant for template.
|
|
94
|
+
# Sets the \variant for the response template.
|
|
95
|
+
#
|
|
96
|
+
# When determining which template to render, Action View will incorporate
|
|
97
|
+
# all variants from the request. For example, if an
|
|
98
|
+
# `ArticlesController#index` action needs to respond to
|
|
99
|
+
# `request.variant = [:ios, :turbo_native]`, it will render the
|
|
100
|
+
# first template file it can find in the following list:
|
|
101
|
+
#
|
|
102
|
+
# - `app/views/articles/index.html+ios.erb`
|
|
103
|
+
# - `app/views/articles/index.html+turbo_native.erb`
|
|
104
|
+
# - `app/views/articles/index.html.erb`
|
|
105
|
+
#
|
|
106
|
+
# Variants add context to the requests that views render appropriately.
|
|
107
|
+
# Variant names are arbitrary, and can communicate anything from the
|
|
108
|
+
# request's platform (`:android`, `:ios`, `:linux`, `:macos`, `:windows`)
|
|
109
|
+
# to its browser (`:chrome`, `:edge`, `:firefox`, `:safari`), to the type
|
|
110
|
+
# of user (`:admin`, `:guest`, `:user`).
|
|
111
|
+
#
|
|
112
|
+
# Note: Adding many new variant templates with similarities to existing
|
|
113
|
+
# template files can make maintaining your view code more difficult.
|
|
114
|
+
#
|
|
115
|
+
# #### Parameters
|
|
116
|
+
#
|
|
117
|
+
# * `variant` - a symbol name or an array of symbol names for variants
|
|
118
|
+
# used to render the response template
|
|
119
|
+
#
|
|
120
|
+
# #### Examples
|
|
121
|
+
#
|
|
122
|
+
# class ApplicationController < ActionController::Base
|
|
123
|
+
# before_action :determine_variants
|
|
124
|
+
#
|
|
125
|
+
# private
|
|
126
|
+
# def determine_variants
|
|
127
|
+
# variants = []
|
|
128
|
+
#
|
|
129
|
+
# # some code to determine the variant(s) to use
|
|
130
|
+
#
|
|
131
|
+
# variants << :ios if request.user_agent.include?("iOS")
|
|
132
|
+
# variants << :turbo_native if request.user_agent.include?("Turbo Native")
|
|
133
|
+
#
|
|
134
|
+
# request.variant = variants
|
|
135
|
+
# end
|
|
136
|
+
# end
|
|
90
137
|
def variant=(variant)
|
|
91
138
|
variant = Array(variant)
|
|
92
139
|
|
|
@@ -97,6 +144,18 @@ module ActionDispatch
|
|
|
97
144
|
end
|
|
98
145
|
end
|
|
99
146
|
|
|
147
|
+
# Returns the \variant for the response template as an instance of
|
|
148
|
+
# ActiveSupport::ArrayInquirer.
|
|
149
|
+
#
|
|
150
|
+
# request.variant = :phone
|
|
151
|
+
# request.variant.phone? # => true
|
|
152
|
+
# request.variant.tablet? # => false
|
|
153
|
+
#
|
|
154
|
+
# request.variant = [:phone, :tablet]
|
|
155
|
+
# request.variant.phone? # => true
|
|
156
|
+
# request.variant.desktop? # => false
|
|
157
|
+
# request.variant.any?(:phone, :desktop) # => true
|
|
158
|
+
# request.variant.any?(:desktop, :watch) # => false
|
|
100
159
|
def variant
|
|
101
160
|
@variant ||= ActiveSupport::ArrayInquirer.new
|
|
102
161
|
end
|
|
@@ -13,6 +13,7 @@ Mime::Type.register "text/calendar", :ics
|
|
|
13
13
|
Mime::Type.register "text/csv", :csv
|
|
14
14
|
Mime::Type.register "text/vcard", :vcf
|
|
15
15
|
Mime::Type.register "text/vtt", :vtt, %w(vtt)
|
|
16
|
+
Mime::Type.register "text/markdown", :md, [], %w(md markdown)
|
|
16
17
|
|
|
17
18
|
Mime::Type.register "image/png", :png, [], %w(png)
|
|
18
19
|
Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg)
|