actionpack 5.2.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 (170) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +429 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +57 -0
  5. data/lib/abstract_controller.rb +27 -0
  6. data/lib/abstract_controller/asset_paths.rb +12 -0
  7. data/lib/abstract_controller/base.rb +265 -0
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/caching/fragments.rb +166 -0
  10. data/lib/abstract_controller/callbacks.rb +212 -0
  11. data/lib/abstract_controller/collector.rb +43 -0
  12. data/lib/abstract_controller/error.rb +6 -0
  13. data/lib/abstract_controller/helpers.rb +194 -0
  14. data/lib/abstract_controller/logger.rb +14 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +20 -0
  16. data/lib/abstract_controller/rendering.rb +127 -0
  17. data/lib/abstract_controller/translation.rb +31 -0
  18. data/lib/abstract_controller/url_for.rb +35 -0
  19. data/lib/action_controller.rb +66 -0
  20. data/lib/action_controller/api.rb +149 -0
  21. data/lib/action_controller/api/api_rendering.rb +16 -0
  22. data/lib/action_controller/base.rb +276 -0
  23. data/lib/action_controller/caching.rb +46 -0
  24. data/lib/action_controller/form_builder.rb +50 -0
  25. data/lib/action_controller/log_subscriber.rb +78 -0
  26. data/lib/action_controller/metal.rb +256 -0
  27. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  28. data/lib/action_controller/metal/conditional_get.rb +274 -0
  29. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  30. data/lib/action_controller/metal/cookies.rb +16 -0
  31. data/lib/action_controller/metal/data_streaming.rb +152 -0
  32. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  33. data/lib/action_controller/metal/etag_with_template_digest.rb +57 -0
  34. data/lib/action_controller/metal/exceptions.rb +53 -0
  35. data/lib/action_controller/metal/flash.rb +61 -0
  36. data/lib/action_controller/metal/force_ssl.rb +99 -0
  37. data/lib/action_controller/metal/head.rb +60 -0
  38. data/lib/action_controller/metal/helpers.rb +123 -0
  39. data/lib/action_controller/metal/http_authentication.rb +519 -0
  40. data/lib/action_controller/metal/implicit_render.rb +73 -0
  41. data/lib/action_controller/metal/instrumentation.rb +107 -0
  42. data/lib/action_controller/metal/live.rb +312 -0
  43. data/lib/action_controller/metal/mime_responds.rb +313 -0
  44. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  45. data/lib/action_controller/metal/params_wrapper.rb +293 -0
  46. data/lib/action_controller/metal/redirecting.rb +133 -0
  47. data/lib/action_controller/metal/renderers.rb +181 -0
  48. data/lib/action_controller/metal/rendering.rb +122 -0
  49. data/lib/action_controller/metal/request_forgery_protection.rb +445 -0
  50. data/lib/action_controller/metal/rescue.rb +28 -0
  51. data/lib/action_controller/metal/streaming.rb +223 -0
  52. data/lib/action_controller/metal/strong_parameters.rb +1086 -0
  53. data/lib/action_controller/metal/testing.rb +16 -0
  54. data/lib/action_controller/metal/url_for.rb +58 -0
  55. data/lib/action_controller/railtie.rb +89 -0
  56. data/lib/action_controller/railties/helpers.rb +24 -0
  57. data/lib/action_controller/renderer.rb +117 -0
  58. data/lib/action_controller/template_assertions.rb +11 -0
  59. data/lib/action_controller/test_case.rb +629 -0
  60. data/lib/action_dispatch.rb +112 -0
  61. data/lib/action_dispatch/http/cache.rb +222 -0
  62. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  63. data/lib/action_dispatch/http/filter_parameters.rb +84 -0
  64. data/lib/action_dispatch/http/filter_redirect.rb +37 -0
  65. data/lib/action_dispatch/http/headers.rb +132 -0
  66. data/lib/action_dispatch/http/mime_negotiation.rb +175 -0
  67. data/lib/action_dispatch/http/mime_type.rb +342 -0
  68. data/lib/action_dispatch/http/mime_types.rb +50 -0
  69. data/lib/action_dispatch/http/parameter_filter.rb +86 -0
  70. data/lib/action_dispatch/http/parameters.rb +126 -0
  71. data/lib/action_dispatch/http/rack_cache.rb +63 -0
  72. data/lib/action_dispatch/http/request.rb +430 -0
  73. data/lib/action_dispatch/http/response.rb +519 -0
  74. data/lib/action_dispatch/http/upload.rb +84 -0
  75. data/lib/action_dispatch/http/url.rb +350 -0
  76. data/lib/action_dispatch/journey.rb +7 -0
  77. data/lib/action_dispatch/journey/formatter.rb +189 -0
  78. data/lib/action_dispatch/journey/gtg/builder.rb +164 -0
  79. data/lib/action_dispatch/journey/gtg/simulator.rb +41 -0
  80. data/lib/action_dispatch/journey/gtg/transition_table.rb +158 -0
  81. data/lib/action_dispatch/journey/nfa/builder.rb +78 -0
  82. data/lib/action_dispatch/journey/nfa/dot.rb +36 -0
  83. data/lib/action_dispatch/journey/nfa/simulator.rb +49 -0
  84. data/lib/action_dispatch/journey/nfa/transition_table.rb +120 -0
  85. data/lib/action_dispatch/journey/nodes/node.rb +140 -0
  86. data/lib/action_dispatch/journey/parser.rb +199 -0
  87. data/lib/action_dispatch/journey/parser.y +50 -0
  88. data/lib/action_dispatch/journey/parser_extras.rb +31 -0
  89. data/lib/action_dispatch/journey/path/pattern.rb +198 -0
  90. data/lib/action_dispatch/journey/route.rb +203 -0
  91. data/lib/action_dispatch/journey/router.rb +156 -0
  92. data/lib/action_dispatch/journey/router/utils.rb +102 -0
  93. data/lib/action_dispatch/journey/routes.rb +82 -0
  94. data/lib/action_dispatch/journey/scanner.rb +64 -0
  95. data/lib/action_dispatch/journey/visitors.rb +268 -0
  96. data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
  97. data/lib/action_dispatch/journey/visualizer/fsm.js +134 -0
  98. data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
  99. data/lib/action_dispatch/middleware/callbacks.rb +36 -0
  100. data/lib/action_dispatch/middleware/cookies.rb +685 -0
  101. data/lib/action_dispatch/middleware/debug_exceptions.rb +205 -0
  102. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  103. data/lib/action_dispatch/middleware/exception_wrapper.rb +147 -0
  104. data/lib/action_dispatch/middleware/executor.rb +21 -0
  105. data/lib/action_dispatch/middleware/flash.rb +300 -0
  106. data/lib/action_dispatch/middleware/public_exceptions.rb +57 -0
  107. data/lib/action_dispatch/middleware/reloader.rb +12 -0
  108. data/lib/action_dispatch/middleware/remote_ip.rb +183 -0
  109. data/lib/action_dispatch/middleware/request_id.rb +43 -0
  110. data/lib/action_dispatch/middleware/session/abstract_store.rb +92 -0
  111. data/lib/action_dispatch/middleware/session/cache_store.rb +54 -0
  112. data/lib/action_dispatch/middleware/session/cookie_store.rb +118 -0
  113. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +28 -0
  114. data/lib/action_dispatch/middleware/show_exceptions.rb +62 -0
  115. data/lib/action_dispatch/middleware/ssl.rb +150 -0
  116. data/lib/action_dispatch/middleware/stack.rb +116 -0
  117. data/lib/action_dispatch/middleware/static.rb +130 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +22 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +27 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  122. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +52 -0
  123. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
  124. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +16 -0
  125. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  126. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +161 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
  137. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +16 -0
  138. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +200 -0
  139. data/lib/action_dispatch/railtie.rb +55 -0
  140. data/lib/action_dispatch/request/session.rb +234 -0
  141. data/lib/action_dispatch/request/utils.rb +78 -0
  142. data/lib/action_dispatch/routing.rb +260 -0
  143. data/lib/action_dispatch/routing/endpoint.rb +17 -0
  144. data/lib/action_dispatch/routing/inspector.rb +225 -0
  145. data/lib/action_dispatch/routing/mapper.rb +2267 -0
  146. data/lib/action_dispatch/routing/polymorphic_routes.rb +352 -0
  147. data/lib/action_dispatch/routing/redirection.rb +201 -0
  148. data/lib/action_dispatch/routing/route_set.rb +890 -0
  149. data/lib/action_dispatch/routing/routes_proxy.rb +69 -0
  150. data/lib/action_dispatch/routing/url_for.rb +236 -0
  151. data/lib/action_dispatch/system_test_case.rb +147 -0
  152. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  153. data/lib/action_dispatch/system_testing/driver.rb +59 -0
  154. data/lib/action_dispatch/system_testing/server.rb +31 -0
  155. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +96 -0
  156. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +31 -0
  157. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  158. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  159. data/lib/action_dispatch/testing/assertions.rb +24 -0
  160. data/lib/action_dispatch/testing/assertions/response.rb +107 -0
  161. data/lib/action_dispatch/testing/assertions/routing.rb +222 -0
  162. data/lib/action_dispatch/testing/integration.rb +652 -0
  163. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  164. data/lib/action_dispatch/testing/test_process.rb +50 -0
  165. data/lib/action_dispatch/testing/test_request.rb +71 -0
  166. data/lib/action_dispatch/testing/test_response.rb +53 -0
  167. data/lib/action_pack.rb +26 -0
  168. data/lib/action_pack/gem_version.rb +17 -0
  169. data/lib/action_pack/version.rb +10 -0
  170. metadata +318 -0
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch/http/parameter_filter"
4
+
5
+ module ActionDispatch
6
+ module Http
7
+ # Allows you to specify sensitive parameters which will be replaced from
8
+ # the request log by looking in the query string of the request and all
9
+ # sub-hashes of the params hash to filter. Filtering only certain sub-keys
10
+ # from a hash is possible by using the dot notation: 'credit_card.number'.
11
+ # If a block is given, each key and value of the params hash and all
12
+ # sub-hashes is passed to it, where the value or the key can be replaced using
13
+ # String#replace or similar method.
14
+ #
15
+ # env["action_dispatch.parameter_filter"] = [:password]
16
+ # => replaces the value to all keys matching /password/i with "[FILTERED]"
17
+ #
18
+ # env["action_dispatch.parameter_filter"] = [:foo, "bar"]
19
+ # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
20
+ #
21
+ # env["action_dispatch.parameter_filter"] = [ "credit_card.code" ]
22
+ # => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not
23
+ # change { file: { code: "xxxx"} }
24
+ #
25
+ # env["action_dispatch.parameter_filter"] = -> (k, v) do
26
+ # v.reverse! if k =~ /secret/i
27
+ # end
28
+ # => reverses the value to all keys matching /secret/i
29
+ module FilterParameters
30
+ ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc:
31
+ NULL_PARAM_FILTER = ParameterFilter.new # :nodoc:
32
+ NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc:
33
+
34
+ def initialize
35
+ super
36
+ @filtered_parameters = nil
37
+ @filtered_env = nil
38
+ @filtered_path = nil
39
+ end
40
+
41
+ # Returns a hash of parameters with all sensitive data replaced.
42
+ def filtered_parameters
43
+ @filtered_parameters ||= parameter_filter.filter(parameters)
44
+ end
45
+
46
+ # Returns a hash of request.env with all sensitive data replaced.
47
+ def filtered_env
48
+ @filtered_env ||= env_filter.filter(@env)
49
+ end
50
+
51
+ # Reconstructs a path with all sensitive GET parameters replaced.
52
+ def filtered_path
53
+ @filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}"
54
+ end
55
+
56
+ private
57
+
58
+ def parameter_filter # :doc:
59
+ parameter_filter_for fetch_header("action_dispatch.parameter_filter") {
60
+ return NULL_PARAM_FILTER
61
+ }
62
+ end
63
+
64
+ def env_filter # :doc:
65
+ user_key = fetch_header("action_dispatch.parameter_filter") {
66
+ return NULL_ENV_FILTER
67
+ }
68
+ parameter_filter_for(Array(user_key) + ENV_MATCH)
69
+ end
70
+
71
+ def parameter_filter_for(filters) # :doc:
72
+ ParameterFilter.new(filters)
73
+ end
74
+
75
+ KV_RE = "[^&;=]+"
76
+ PAIR_RE = %r{(#{KV_RE})=(#{KV_RE})}
77
+ def filtered_query_string # :doc:
78
+ query_string.gsub(PAIR_RE) do |_|
79
+ parameter_filter.filter($1 => $2).first.join("=")
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ module Http
5
+ module FilterRedirect
6
+ FILTERED = "[FILTERED]".freeze # :nodoc:
7
+
8
+ def filtered_location # :nodoc:
9
+ if location_filter_match?
10
+ FILTERED
11
+ else
12
+ location
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def location_filters
19
+ if request
20
+ request.get_header("action_dispatch.redirect_filter") || []
21
+ else
22
+ []
23
+ end
24
+ end
25
+
26
+ def location_filter_match?
27
+ location_filters.any? do |filter|
28
+ if String === filter
29
+ location.include?(filter)
30
+ elsif Regexp === filter
31
+ location =~ filter
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ module Http
5
+ # Provides access to the request's HTTP headers from the environment.
6
+ #
7
+ # env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" }
8
+ # headers = ActionDispatch::Http::Headers.from_hash(env)
9
+ # headers["Content-Type"] # => "text/plain"
10
+ # headers["User-Agent"] # => "curl/7.43.0"
11
+ #
12
+ # Also note that when headers are mapped to CGI-like variables by the Rack
13
+ # server, both dashes and underscores are converted to underscores. This
14
+ # ambiguity cannot be resolved at this stage anymore. Both underscores and
15
+ # dashes have to be interpreted as if they were originally sent as dashes.
16
+ #
17
+ # # GET / HTTP/1.1
18
+ # # ...
19
+ # # User-Agent: curl/7.43.0
20
+ # # X_Custom_Header: token
21
+ #
22
+ # headers["X_Custom_Header"] # => nil
23
+ # headers["X-Custom-Header"] # => "token"
24
+ class Headers
25
+ CGI_VARIABLES = Set.new(%W[
26
+ AUTH_TYPE
27
+ CONTENT_LENGTH
28
+ CONTENT_TYPE
29
+ GATEWAY_INTERFACE
30
+ HTTPS
31
+ PATH_INFO
32
+ PATH_TRANSLATED
33
+ QUERY_STRING
34
+ REMOTE_ADDR
35
+ REMOTE_HOST
36
+ REMOTE_IDENT
37
+ REMOTE_USER
38
+ REQUEST_METHOD
39
+ SCRIPT_NAME
40
+ SERVER_NAME
41
+ SERVER_PORT
42
+ SERVER_PROTOCOL
43
+ SERVER_SOFTWARE
44
+ ]).freeze
45
+
46
+ HTTP_HEADER = /\A[A-Za-z0-9-]+\z/
47
+
48
+ include Enumerable
49
+
50
+ def self.from_hash(hash)
51
+ new ActionDispatch::Request.new hash
52
+ end
53
+
54
+ def initialize(request) # :nodoc:
55
+ @req = request
56
+ end
57
+
58
+ # Returns the value for the given key mapped to @env.
59
+ def [](key)
60
+ @req.get_header env_name(key)
61
+ end
62
+
63
+ # Sets the given value for the key mapped to @env.
64
+ def []=(key, value)
65
+ @req.set_header env_name(key), value
66
+ end
67
+
68
+ # Add a value to a multivalued header like Vary or Accept-Encoding.
69
+ def add(key, value)
70
+ @req.add_header env_name(key), value
71
+ end
72
+
73
+ def key?(key)
74
+ @req.has_header? env_name(key)
75
+ end
76
+ alias :include? :key?
77
+
78
+ DEFAULT = Object.new # :nodoc:
79
+
80
+ # Returns the value for the given key mapped to @env.
81
+ #
82
+ # If the key is not found and an optional code block is not provided,
83
+ # raises a <tt>KeyError</tt> exception.
84
+ #
85
+ # If the code block is provided, then it will be run and
86
+ # its result returned.
87
+ def fetch(key, default = DEFAULT)
88
+ @req.fetch_header(env_name(key)) do
89
+ return default unless default == DEFAULT
90
+ return yield if block_given?
91
+ raise KeyError, key
92
+ end
93
+ end
94
+
95
+ def each(&block)
96
+ @req.each_header(&block)
97
+ end
98
+
99
+ # Returns a new Http::Headers instance containing the contents of
100
+ # <tt>headers_or_env</tt> and the original instance.
101
+ def merge(headers_or_env)
102
+ headers = @req.dup.headers
103
+ headers.merge!(headers_or_env)
104
+ headers
105
+ end
106
+
107
+ # Adds the contents of <tt>headers_or_env</tt> to original instance
108
+ # entries; duplicate keys are overwritten with the values from
109
+ # <tt>headers_or_env</tt>.
110
+ def merge!(headers_or_env)
111
+ headers_or_env.each do |key, value|
112
+ @req.set_header env_name(key), value
113
+ end
114
+ end
115
+
116
+ def env; @req.env.dup; end
117
+
118
+ private
119
+
120
+ # Converts an HTTP header name to an environment variable name if it is
121
+ # not contained within the headers hash.
122
+ def env_name(key)
123
+ key = key.to_s
124
+ if key =~ HTTP_HEADER
125
+ key = key.upcase.tr("-", "_")
126
+ key = "HTTP_" + key unless CGI_VARIABLES.include?(key)
127
+ end
128
+ key
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module/attribute_accessors"
4
+
5
+ module ActionDispatch
6
+ module Http
7
+ module MimeNegotiation
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ mattr_accessor :ignore_accept_header, default: false
12
+ end
13
+
14
+ # The MIME type of the HTTP request, such as Mime[:xml].
15
+ def content_mime_type
16
+ fetch_header("action_dispatch.request.content_type") do |k|
17
+ v = if get_header("CONTENT_TYPE") =~ /^([^,\;]*)/
18
+ Mime::Type.lookup($1.strip.downcase)
19
+ else
20
+ nil
21
+ end
22
+ set_header k, v
23
+ end
24
+ end
25
+
26
+ def content_type
27
+ content_mime_type && content_mime_type.to_s
28
+ end
29
+
30
+ def has_content_type? # :nodoc:
31
+ get_header "CONTENT_TYPE"
32
+ end
33
+
34
+ # Returns the accepted MIME type for the request.
35
+ def accepts
36
+ fetch_header("action_dispatch.request.accepts") do |k|
37
+ header = get_header("HTTP_ACCEPT").to_s.strip
38
+
39
+ v = if header.empty?
40
+ [content_mime_type]
41
+ else
42
+ Mime::Type.parse(header)
43
+ end
44
+ set_header k, v
45
+ end
46
+ end
47
+
48
+ # Returns the MIME type for the \format used in the request.
49
+ #
50
+ # GET /posts/5.xml | request.format => Mime[:xml]
51
+ # GET /posts/5.xhtml | request.format => Mime[:html]
52
+ # GET /posts/5 | request.format => Mime[:html] or Mime[:js], or request.accepts.first
53
+ #
54
+ def format(view_path = [])
55
+ formats.first || Mime::NullType.instance
56
+ end
57
+
58
+ def formats
59
+ fetch_header("action_dispatch.request.formats") do |k|
60
+ params_readable = begin
61
+ parameters[:format]
62
+ rescue ActionController::BadRequest
63
+ false
64
+ end
65
+
66
+ v = if params_readable
67
+ Array(Mime[parameters[:format]])
68
+ elsif use_accept_header && valid_accept_header
69
+ accepts
70
+ elsif extension_format = format_from_path_extension
71
+ [extension_format]
72
+ elsif xhr?
73
+ [Mime[:js]]
74
+ else
75
+ [Mime[:html]]
76
+ end
77
+
78
+ v = v.select do |format|
79
+ format.symbol || format.ref == "*/*"
80
+ end
81
+
82
+ set_header k, v
83
+ end
84
+ end
85
+
86
+ # Sets the \variant for template.
87
+ def variant=(variant)
88
+ variant = Array(variant)
89
+
90
+ if variant.all? { |v| v.is_a?(Symbol) }
91
+ @variant = ActiveSupport::ArrayInquirer.new(variant)
92
+ else
93
+ raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols. " \
94
+ "For security reasons, never directly set the variant to a user-provided value, " \
95
+ "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \
96
+ "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'"
97
+ end
98
+ end
99
+
100
+ def variant
101
+ @variant ||= ActiveSupport::ArrayInquirer.new
102
+ end
103
+
104
+ # Sets the \format by string extension, which can be used to force custom formats
105
+ # that are not controlled by the extension.
106
+ #
107
+ # class ApplicationController < ActionController::Base
108
+ # before_action :adjust_format_for_iphone
109
+ #
110
+ # private
111
+ # def adjust_format_for_iphone
112
+ # request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/]
113
+ # end
114
+ # end
115
+ def format=(extension)
116
+ parameters[:format] = extension.to_s
117
+ set_header "action_dispatch.request.formats", [Mime::Type.lookup_by_extension(parameters[:format])]
118
+ end
119
+
120
+ # Sets the \formats by string extensions. This differs from #format= by allowing you
121
+ # to set multiple, ordered formats, which is useful when you want to have a fallback.
122
+ #
123
+ # In this example, the :iphone format will be used if it's available, otherwise it'll fallback
124
+ # to the :html format.
125
+ #
126
+ # class ApplicationController < ActionController::Base
127
+ # before_action :adjust_format_for_iphone_with_html_fallback
128
+ #
129
+ # private
130
+ # def adjust_format_for_iphone_with_html_fallback
131
+ # request.formats = [ :iphone, :html ] if request.env["HTTP_USER_AGENT"][/iPhone/]
132
+ # end
133
+ # end
134
+ def formats=(extensions)
135
+ parameters[:format] = extensions.first.to_s
136
+ set_header "action_dispatch.request.formats", extensions.collect { |extension|
137
+ Mime::Type.lookup_by_extension(extension)
138
+ }
139
+ end
140
+
141
+ # Returns the first MIME type that matches the provided array of MIME types.
142
+ def negotiate_mime(order)
143
+ formats.each do |priority|
144
+ if priority == Mime::ALL
145
+ return order.first
146
+ elsif order.include?(priority)
147
+ return priority
148
+ end
149
+ end
150
+
151
+ order.include?(Mime::ALL) ? format : nil
152
+ end
153
+
154
+ private
155
+
156
+ BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
157
+
158
+ def valid_accept_header # :doc:
159
+ (xhr? && (accept.present? || content_mime_type)) ||
160
+ (accept.present? && accept !~ BROWSER_LIKE_ACCEPTS)
161
+ end
162
+
163
+ def use_accept_header # :doc:
164
+ !self.class.ignore_accept_header
165
+ end
166
+
167
+ def format_from_path_extension # :doc:
168
+ path = get_header("action_dispatch.original_path") || get_header("PATH_INFO")
169
+ if match = path && path.match(/\.(\w+)\z/)
170
+ Mime[match.captures.first]
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ # -*- frozen-string-literal: true -*-
4
+
5
+ require "singleton"
6
+ require "active_support/core_ext/string/starts_ends_with"
7
+
8
+ module Mime
9
+ class Mimes
10
+ include Enumerable
11
+
12
+ def initialize
13
+ @mimes = []
14
+ @symbols = nil
15
+ end
16
+
17
+ def each
18
+ @mimes.each { |x| yield x }
19
+ end
20
+
21
+ def <<(type)
22
+ @mimes << type
23
+ @symbols = nil
24
+ end
25
+
26
+ def delete_if
27
+ @mimes.delete_if { |x| yield x }.tap { @symbols = nil }
28
+ end
29
+
30
+ def symbols
31
+ @symbols ||= map(&:to_sym)
32
+ end
33
+ end
34
+
35
+ SET = Mimes.new
36
+ EXTENSION_LOOKUP = {}
37
+ LOOKUP = {}
38
+
39
+ class << self
40
+ def [](type)
41
+ return type if type.is_a?(Type)
42
+ Type.lookup_by_extension(type)
43
+ end
44
+
45
+ def fetch(type)
46
+ return type if type.is_a?(Type)
47
+ EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k }
48
+ end
49
+ end
50
+
51
+ # Encapsulates the notion of a MIME type. Can be used at render time, for example, with:
52
+ #
53
+ # class PostsController < ActionController::Base
54
+ # def show
55
+ # @post = Post.find(params[:id])
56
+ #
57
+ # respond_to do |format|
58
+ # format.html
59
+ # format.ics { render body: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") }
60
+ # format.xml { render xml: @post }
61
+ # end
62
+ # end
63
+ # end
64
+ class Type
65
+ attr_reader :symbol
66
+
67
+ @register_callbacks = []
68
+
69
+ # A simple helper class used in parsing the accept header.
70
+ class AcceptItem #:nodoc:
71
+ attr_accessor :index, :name, :q
72
+ alias :to_s :name
73
+
74
+ def initialize(index, name, q = nil)
75
+ @index = index
76
+ @name = name
77
+ q ||= 0.0 if @name == "*/*".freeze # Default wildcard match to end of list.
78
+ @q = ((q || 1.0).to_f * 100).to_i
79
+ end
80
+
81
+ def <=>(item)
82
+ result = item.q <=> @q
83
+ result = @index <=> item.index if result == 0
84
+ result
85
+ end
86
+ end
87
+
88
+ class AcceptList #:nodoc:
89
+ def self.sort!(list)
90
+ list.sort!
91
+
92
+ text_xml_idx = find_item_by_name list, "text/xml"
93
+ app_xml_idx = find_item_by_name list, Mime[:xml].to_s
94
+
95
+ # Take care of the broken text/xml entry by renaming or deleting it.
96
+ if text_xml_idx && app_xml_idx
97
+ app_xml = list[app_xml_idx]
98
+ text_xml = list[text_xml_idx]
99
+
100
+ app_xml.q = [text_xml.q, app_xml.q].max # Set the q value to the max of the two.
101
+ if app_xml_idx > text_xml_idx # Make sure app_xml is ahead of text_xml in the list.
102
+ list[app_xml_idx], list[text_xml_idx] = text_xml, app_xml
103
+ app_xml_idx, text_xml_idx = text_xml_idx, app_xml_idx
104
+ end
105
+ list.delete_at(text_xml_idx) # Delete text_xml from the list.
106
+ elsif text_xml_idx
107
+ list[text_xml_idx].name = Mime[:xml].to_s
108
+ end
109
+
110
+ # Look for more specific XML-based types and sort them ahead of app/xml.
111
+ if app_xml_idx
112
+ app_xml = list[app_xml_idx]
113
+ idx = app_xml_idx
114
+
115
+ while idx < list.length
116
+ type = list[idx]
117
+ break if type.q < app_xml.q
118
+
119
+ if type.name.ends_with? "+xml"
120
+ list[app_xml_idx], list[idx] = list[idx], app_xml
121
+ app_xml_idx = idx
122
+ end
123
+ idx += 1
124
+ end
125
+ end
126
+
127
+ list.map! { |i| Mime::Type.lookup(i.name) }.uniq!
128
+ list
129
+ end
130
+
131
+ def self.find_item_by_name(array, name)
132
+ array.index { |item| item.name == name }
133
+ end
134
+ end
135
+
136
+ class << self
137
+ TRAILING_STAR_REGEXP = /^(text|application)\/\*/
138
+ PARAMETER_SEPARATOR_REGEXP = /;\s*\w+="?\w+"?/
139
+
140
+ def register_callback(&block)
141
+ @register_callbacks << block
142
+ end
143
+
144
+ def lookup(string)
145
+ LOOKUP[string] || Type.new(string)
146
+ end
147
+
148
+ def lookup_by_extension(extension)
149
+ EXTENSION_LOOKUP[extension.to_s]
150
+ end
151
+
152
+ # Registers an alias that's not used on MIME type lookup, but can be referenced directly. Especially useful for
153
+ # rendering different HTML versions depending on the user agent, like an iPhone.
154
+ def register_alias(string, symbol, extension_synonyms = [])
155
+ register(string, symbol, [], extension_synonyms, true)
156
+ end
157
+
158
+ def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false)
159
+ new_mime = Type.new(string, symbol, mime_type_synonyms)
160
+
161
+ SET << new_mime
162
+
163
+ ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = new_mime } unless skip_lookup
164
+ ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = new_mime }
165
+
166
+ @register_callbacks.each do |callback|
167
+ callback.call(new_mime)
168
+ end
169
+ new_mime
170
+ end
171
+
172
+ def parse(accept_header)
173
+ if !accept_header.include?(",")
174
+ accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first
175
+ parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact
176
+ else
177
+ list, index = [], 0
178
+ accept_header.split(",").each do |header|
179
+ params, q = header.split(PARAMETER_SEPARATOR_REGEXP)
180
+
181
+ next unless params
182
+ params.strip!
183
+ next if params.empty?
184
+
185
+ params = parse_trailing_star(params) || [params]
186
+
187
+ params.each do |m|
188
+ list << AcceptItem.new(index, m.to_s, q)
189
+ index += 1
190
+ end
191
+ end
192
+ AcceptList.sort! list
193
+ end
194
+ end
195
+
196
+ def parse_trailing_star(accept_header)
197
+ parse_data_with_trailing_star($1) if accept_header =~ TRAILING_STAR_REGEXP
198
+ end
199
+
200
+ # For an input of <tt>'text'</tt>, returns <tt>[Mime[:json], Mime[:xml], Mime[:ics],
201
+ # Mime[:html], Mime[:css], Mime[:csv], Mime[:js], Mime[:yaml], Mime[:text]</tt>.
202
+ #
203
+ # For an input of <tt>'application'</tt>, returns <tt>[Mime[:html], Mime[:js],
204
+ # Mime[:xml], Mime[:yaml], Mime[:atom], Mime[:json], Mime[:rss], Mime[:url_encoded_form]</tt>.
205
+ def parse_data_with_trailing_star(type)
206
+ Mime::SET.select { |m| m =~ type }
207
+ end
208
+
209
+ # This method is opposite of register method.
210
+ #
211
+ # To unregister a MIME type:
212
+ #
213
+ # Mime::Type.unregister(:mobile)
214
+ def unregister(symbol)
215
+ symbol = symbol.downcase
216
+ if mime = Mime[symbol]
217
+ SET.delete_if { |v| v.eql?(mime) }
218
+ LOOKUP.delete_if { |_, v| v.eql?(mime) }
219
+ EXTENSION_LOOKUP.delete_if { |_, v| v.eql?(mime) }
220
+ end
221
+ end
222
+ end
223
+
224
+ attr_reader :hash
225
+
226
+ def initialize(string, symbol = nil, synonyms = [])
227
+ @symbol, @synonyms = symbol, synonyms
228
+ @string = string
229
+ @hash = [@string, @synonyms, @symbol].hash
230
+ end
231
+
232
+ def to_s
233
+ @string
234
+ end
235
+
236
+ def to_str
237
+ to_s
238
+ end
239
+
240
+ def to_sym
241
+ @symbol
242
+ end
243
+
244
+ def ref
245
+ symbol || to_s
246
+ end
247
+
248
+ def ===(list)
249
+ if list.is_a?(Array)
250
+ (@synonyms + [ self ]).any? { |synonym| list.include?(synonym) }
251
+ else
252
+ super
253
+ end
254
+ end
255
+
256
+ def ==(mime_type)
257
+ return false unless mime_type
258
+ (@synonyms + [ self ]).any? do |synonym|
259
+ synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym
260
+ end
261
+ end
262
+
263
+ def eql?(other)
264
+ super || (self.class == other.class &&
265
+ @string == other.string &&
266
+ @synonyms == other.synonyms &&
267
+ @symbol == other.symbol)
268
+ end
269
+
270
+ def =~(mime_type)
271
+ return false unless mime_type
272
+ regexp = Regexp.new(Regexp.quote(mime_type.to_s))
273
+ @synonyms.any? { |synonym| synonym.to_s =~ regexp } || @string =~ regexp
274
+ end
275
+
276
+ def html?
277
+ symbol == :html || @string =~ /html/
278
+ end
279
+
280
+ def all?; false; end
281
+
282
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
283
+ # Workaround for Ruby 2.2 "private attribute?" warning.
284
+ protected
285
+
286
+ attr_reader :string, :synonyms
287
+
288
+ private
289
+
290
+ def to_ary; end
291
+ def to_a; end
292
+
293
+ def method_missing(method, *args)
294
+ if method.to_s.ends_with? "?"
295
+ method[0..-2].downcase.to_sym == to_sym
296
+ else
297
+ super
298
+ end
299
+ end
300
+
301
+ def respond_to_missing?(method, include_private = false)
302
+ (method.to_s.ends_with? "?") || super
303
+ end
304
+ end
305
+
306
+ class AllType < Type
307
+ include Singleton
308
+
309
+ def initialize
310
+ super "*/*", :all
311
+ end
312
+
313
+ def all?; true; end
314
+ def html?; true; end
315
+ end
316
+
317
+ # ALL isn't a real MIME type, so we don't register it for lookup with the
318
+ # other concrete types. It's a wildcard match that we use for `respond_to`
319
+ # negotiation internals.
320
+ ALL = AllType.instance
321
+
322
+ class NullType
323
+ include Singleton
324
+
325
+ def nil?
326
+ true
327
+ end
328
+
329
+ def ref; end
330
+
331
+ private
332
+ def respond_to_missing?(method, _)
333
+ method.to_s.ends_with? "?"
334
+ end
335
+
336
+ def method_missing(method, *args)
337
+ false if method.to_s.ends_with? "?"
338
+ end
339
+ end
340
+ end
341
+
342
+ require "action_dispatch/http/mime_types"