actionpack 4.2.10 → 7.2.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 (202) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +86 -600
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +13 -14
  5. data/lib/abstract_controller/asset_paths.rb +5 -1
  6. data/lib/abstract_controller/base.rb +166 -136
  7. data/lib/abstract_controller/caching/fragments.rb +149 -0
  8. data/lib/abstract_controller/caching.rb +68 -0
  9. data/lib/abstract_controller/callbacks.rb +126 -57
  10. data/lib/abstract_controller/collector.rb +13 -15
  11. data/lib/abstract_controller/deprecator.rb +9 -0
  12. data/lib/abstract_controller/error.rb +8 -0
  13. data/lib/abstract_controller/helpers.rb +181 -132
  14. data/lib/abstract_controller/logger.rb +5 -1
  15. data/lib/abstract_controller/railties/routes_helpers.rb +10 -3
  16. data/lib/abstract_controller/rendering.rb +56 -56
  17. data/lib/abstract_controller/translation.rb +29 -15
  18. data/lib/abstract_controller/url_for.rb +15 -11
  19. data/lib/abstract_controller.rb +21 -5
  20. data/lib/action_controller/api/api_rendering.rb +18 -0
  21. data/lib/action_controller/api.rb +154 -0
  22. data/lib/action_controller/base.rb +219 -155
  23. data/lib/action_controller/caching.rb +28 -68
  24. data/lib/action_controller/deprecator.rb +9 -0
  25. data/lib/action_controller/form_builder.rb +55 -0
  26. data/lib/action_controller/log_subscriber.rb +35 -22
  27. data/lib/action_controller/metal/allow_browser.rb +119 -0
  28. data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
  29. data/lib/action_controller/metal/conditional_get.rb +259 -122
  30. data/lib/action_controller/metal/content_security_policy.rb +86 -0
  31. data/lib/action_controller/metal/cookies.rb +9 -5
  32. data/lib/action_controller/metal/data_streaming.rb +87 -104
  33. data/lib/action_controller/metal/default_headers.rb +21 -0
  34. data/lib/action_controller/metal/etag_with_flash.rb +22 -0
  35. data/lib/action_controller/metal/etag_with_template_digest.rb +35 -26
  36. data/lib/action_controller/metal/exceptions.rb +71 -24
  37. data/lib/action_controller/metal/flash.rb +26 -19
  38. data/lib/action_controller/metal/head.rb +45 -36
  39. data/lib/action_controller/metal/helpers.rb +80 -64
  40. data/lib/action_controller/metal/http_authentication.rb +297 -244
  41. data/lib/action_controller/metal/implicit_render.rb +57 -9
  42. data/lib/action_controller/metal/instrumentation.rb +76 -64
  43. data/lib/action_controller/metal/live.rb +238 -176
  44. data/lib/action_controller/metal/logging.rb +22 -0
  45. data/lib/action_controller/metal/mime_responds.rb +177 -166
  46. data/lib/action_controller/metal/parameter_encoding.rb +84 -0
  47. data/lib/action_controller/metal/params_wrapper.rb +145 -118
  48. data/lib/action_controller/metal/permissions_policy.rb +38 -0
  49. data/lib/action_controller/metal/rate_limiting.rb +62 -0
  50. data/lib/action_controller/metal/redirecting.rb +203 -64
  51. data/lib/action_controller/metal/renderers.rb +108 -65
  52. data/lib/action_controller/metal/rendering.rb +216 -56
  53. data/lib/action_controller/metal/request_forgery_protection.rb +496 -163
  54. data/lib/action_controller/metal/rescue.rb +19 -21
  55. data/lib/action_controller/metal/streaming.rb +179 -138
  56. data/lib/action_controller/metal/strong_parameters.rb +1058 -382
  57. data/lib/action_controller/metal/testing.rb +11 -17
  58. data/lib/action_controller/metal/url_for.rb +37 -21
  59. data/lib/action_controller/metal.rb +236 -138
  60. data/lib/action_controller/railtie.rb +89 -11
  61. data/lib/action_controller/railties/helpers.rb +5 -1
  62. data/lib/action_controller/renderer.rb +161 -0
  63. data/lib/action_controller/template_assertions.rb +13 -0
  64. data/lib/action_controller/test_case.rb +425 -497
  65. data/lib/action_controller.rb +44 -22
  66. data/lib/action_dispatch/constants.rb +34 -0
  67. data/lib/action_dispatch/deprecator.rb +9 -0
  68. data/lib/action_dispatch/http/cache.rb +119 -63
  69. data/lib/action_dispatch/http/content_disposition.rb +47 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +364 -0
  71. data/lib/action_dispatch/http/filter_parameters.rb +36 -34
  72. data/lib/action_dispatch/http/filter_redirect.rb +24 -12
  73. data/lib/action_dispatch/http/headers.rb +66 -31
  74. data/lib/action_dispatch/http/mime_negotiation.rb +106 -75
  75. data/lib/action_dispatch/http/mime_type.rb +196 -136
  76. data/lib/action_dispatch/http/mime_types.rb +25 -7
  77. data/lib/action_dispatch/http/parameters.rb +97 -45
  78. data/lib/action_dispatch/http/permissions_policy.rb +187 -0
  79. data/lib/action_dispatch/http/rack_cache.rb +6 -0
  80. data/lib/action_dispatch/http/request.rb +299 -170
  81. data/lib/action_dispatch/http/response.rb +311 -160
  82. data/lib/action_dispatch/http/upload.rb +52 -23
  83. data/lib/action_dispatch/http/url.rb +201 -125
  84. data/lib/action_dispatch/journey/formatter.rb +110 -50
  85. data/lib/action_dispatch/journey/gtg/builder.rb +37 -50
  86. data/lib/action_dispatch/journey/gtg/simulator.rb +20 -17
  87. data/lib/action_dispatch/journey/gtg/transition_table.rb +96 -36
  88. data/lib/action_dispatch/journey/nfa/dot.rb +5 -14
  89. data/lib/action_dispatch/journey/nodes/node.rb +100 -20
  90. data/lib/action_dispatch/journey/parser.rb +19 -17
  91. data/lib/action_dispatch/journey/parser.y +4 -3
  92. data/lib/action_dispatch/journey/parser_extras.rb +14 -4
  93. data/lib/action_dispatch/journey/path/pattern.rb +79 -63
  94. data/lib/action_dispatch/journey/route.rb +108 -44
  95. data/lib/action_dispatch/journey/router/utils.rb +41 -29
  96. data/lib/action_dispatch/journey/router.rb +64 -57
  97. data/lib/action_dispatch/journey/routes.rb +23 -21
  98. data/lib/action_dispatch/journey/scanner.rb +28 -17
  99. data/lib/action_dispatch/journey/visitors.rb +100 -54
  100. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  101. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  102. data/lib/action_dispatch/journey.rb +7 -5
  103. data/lib/action_dispatch/log_subscriber.rb +25 -0
  104. data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
  105. data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
  106. data/lib/action_dispatch/middleware/callbacks.rb +7 -6
  107. data/lib/action_dispatch/middleware/cookies.rb +471 -328
  108. data/lib/action_dispatch/middleware/debug_exceptions.rb +149 -66
  109. data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
  110. data/lib/action_dispatch/middleware/debug_view.rb +73 -0
  111. data/lib/action_dispatch/middleware/exception_wrapper.rb +275 -73
  112. data/lib/action_dispatch/middleware/executor.rb +32 -0
  113. data/lib/action_dispatch/middleware/flash.rb +143 -101
  114. data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
  115. data/lib/action_dispatch/middleware/public_exceptions.rb +36 -27
  116. data/lib/action_dispatch/middleware/reloader.rb +10 -92
  117. data/lib/action_dispatch/middleware/remote_ip.rb +133 -107
  118. data/lib/action_dispatch/middleware/request_id.rb +29 -15
  119. data/lib/action_dispatch/middleware/server_timing.rb +78 -0
  120. data/lib/action_dispatch/middleware/session/abstract_store.rb +49 -27
  121. data/lib/action_dispatch/middleware/session/cache_store.rb +33 -16
  122. data/lib/action_dispatch/middleware/session/cookie_store.rb +86 -80
  123. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +15 -3
  124. data/lib/action_dispatch/middleware/show_exceptions.rb +66 -36
  125. data/lib/action_dispatch/middleware/ssl.rb +134 -36
  126. data/lib/action_dispatch/middleware/stack.rb +109 -44
  127. data/lib/action_dispatch/middleware/static.rb +159 -90
  128. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +7 -24
  132. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  133. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +46 -36
  136. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +26 -7
  139. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +3 -3
  140. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  141. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
  142. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +139 -15
  143. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -0
  144. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  145. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +6 -6
  146. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +7 -7
  147. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +9 -9
  148. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  149. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
  150. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  151. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +7 -4
  152. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +125 -93
  153. data/lib/action_dispatch/railtie.rb +44 -16
  154. data/lib/action_dispatch/request/session.rb +159 -69
  155. data/lib/action_dispatch/request/utils.rb +97 -23
  156. data/lib/action_dispatch/routing/endpoint.rb +11 -2
  157. data/lib/action_dispatch/routing/inspector.rb +195 -106
  158. data/lib/action_dispatch/routing/mapper.rb +1338 -955
  159. data/lib/action_dispatch/routing/polymorphic_routes.rb +234 -201
  160. data/lib/action_dispatch/routing/redirection.rb +78 -51
  161. data/lib/action_dispatch/routing/route_set.rb +460 -374
  162. data/lib/action_dispatch/routing/routes_proxy.rb +36 -12
  163. data/lib/action_dispatch/routing/url_for.rb +172 -124
  164. data/lib/action_dispatch/routing.rb +159 -158
  165. data/lib/action_dispatch/system_test_case.rb +206 -0
  166. data/lib/action_dispatch/system_testing/browser.rb +84 -0
  167. data/lib/action_dispatch/system_testing/driver.rb +85 -0
  168. data/lib/action_dispatch/system_testing/server.rb +33 -0
  169. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
  170. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
  171. data/lib/action_dispatch/testing/assertion_response.rb +48 -0
  172. data/lib/action_dispatch/testing/assertions/response.rb +71 -39
  173. data/lib/action_dispatch/testing/assertions/routing.rb +228 -103
  174. data/lib/action_dispatch/testing/assertions.rb +9 -6
  175. data/lib/action_dispatch/testing/integration.rb +486 -306
  176. data/lib/action_dispatch/testing/request_encoder.rb +60 -0
  177. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  178. data/lib/action_dispatch/testing/test_process.rb +35 -22
  179. data/lib/action_dispatch/testing/test_request.rb +29 -34
  180. data/lib/action_dispatch/testing/test_response.rb +48 -15
  181. data/lib/action_dispatch.rb +82 -40
  182. data/lib/action_pack/gem_version.rb +8 -4
  183. data/lib/action_pack/version.rb +6 -2
  184. data/lib/action_pack.rb +21 -18
  185. metadata +146 -56
  186. data/lib/action_controller/caching/fragments.rb +0 -103
  187. data/lib/action_controller/metal/force_ssl.rb +0 -97
  188. data/lib/action_controller/metal/hide_actions.rb +0 -40
  189. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  190. data/lib/action_controller/middleware.rb +0 -39
  191. data/lib/action_controller/model_naming.rb +0 -12
  192. data/lib/action_dispatch/http/parameter_filter.rb +0 -72
  193. data/lib/action_dispatch/journey/backwards.rb +0 -5
  194. data/lib/action_dispatch/journey/nfa/builder.rb +0 -76
  195. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  196. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -163
  197. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  198. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  199. data/lib/action_dispatch/middleware/templates/rescues/_source.erb +0 -27
  200. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  201. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  202. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,96 +1,173 @@
1
- module ActionController
2
- class RedirectBackError < AbstractController::Error #:nodoc:
3
- DEFAULT_MESSAGE = 'No HTTP_REFERER was set in the request to this action, so redirect_to :back could not be called successfully. If this is a test, make sure to specify request.env["HTTP_REFERER"].'
1
+ # frozen_string_literal: true
4
2
 
5
- def initialize(message = nil)
6
- super(message || DEFAULT_MESSAGE)
7
- end
8
- end
3
+ # :markup: markdown
9
4
 
5
+ module ActionController
10
6
  module Redirecting
11
7
  extend ActiveSupport::Concern
12
8
 
13
9
  include AbstractController::Logger
14
- include ActionController::RackDelegation
15
10
  include ActionController::UrlFor
16
11
 
17
- # Redirects the browser to the target specified in +options+. This parameter can be any one of:
12
+ class UnsafeRedirectError < StandardError; end
13
+
14
+ ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/
15
+
16
+ included do
17
+ mattr_accessor :raise_on_open_redirects, default: false
18
+ end
19
+
20
+ # Redirects the browser to the target specified in `options`. This parameter can
21
+ # be any one of:
22
+ #
23
+ # * `Hash` - The URL will be generated by calling url_for with the `options`.
24
+ # * `Record` - The URL will be generated by calling url_for with the
25
+ # `options`, which will reference a named URL for that record.
26
+ # * `String` starting with `protocol://` (like `http://`) or a protocol
27
+ # relative reference (like `//`) - Is passed straight through as the target
28
+ # for redirection.
29
+ # * `String` not containing a protocol - The current protocol and host is
30
+ # prepended to the string.
31
+ # * `Proc` - A block that will be executed in the controller's context. Should
32
+ # return any option accepted by `redirect_to`.
18
33
  #
19
- # * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
20
- # * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record.
21
- # * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) or a protocol relative reference (like <tt>//</tt>) - Is passed straight through as the target for redirection.
22
- # * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string.
23
- # * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+.
24
- # * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places.
25
- # Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt>
26
34
  #
27
- # === Examples:
35
+ # ### Examples
28
36
  #
29
- # redirect_to action: "show", id: 5
30
- # redirect_to post
31
- # redirect_to "http://www.rubyonrails.org"
32
- # redirect_to "/images/screenshot.jpg"
33
- # redirect_to articles_url
34
- # redirect_to :back
35
- # redirect_to proc { edit_post_url(@post) }
37
+ # redirect_to action: "show", id: 5
38
+ # redirect_to @post
39
+ # redirect_to "http://www.rubyonrails.org"
40
+ # redirect_to "/images/screenshot.jpg"
41
+ # redirect_to posts_url
42
+ # redirect_to proc { edit_post_url(@post) }
36
43
  #
37
- # The redirection happens as a "302 Found" header unless otherwise specified using the <tt>:status</tt> option:
44
+ # The redirection happens as a `302 Found` header unless otherwise specified
45
+ # using the `:status` option:
38
46
  #
39
- # redirect_to post_url(@post), status: :found
40
- # redirect_to action: 'atom', status: :moved_permanently
41
- # redirect_to post_url(@post), status: 301
42
- # redirect_to action: 'atom', status: 302
47
+ # redirect_to post_url(@post), status: :found
48
+ # redirect_to action: 'atom', status: :moved_permanently
49
+ # redirect_to post_url(@post), status: 301
50
+ # redirect_to action: 'atom', status: 302
43
51
  #
44
- # The status code can either be a standard {HTTP Status code}[http://www.iana.org/assignments/http-status-codes] as an
45
- # integer, or a symbol representing the downcased, underscored and symbolized description.
46
- # Note that the status code must be a 3xx HTTP code, or redirection will not occur.
52
+ # The status code can either be a standard [HTTP Status
53
+ # code](https://www.iana.org/assignments/http-status-codes) as an integer, or a
54
+ # symbol representing the downcased, underscored and symbolized description.
55
+ # Note that the status code must be a 3xx HTTP code, or redirection will not
56
+ # occur.
47
57
  #
48
58
  # If you are using XHR requests other than GET or POST and redirecting after the
49
59
  # request then some browsers will follow the redirect using the original request
50
60
  # method. This may lead to undesirable behavior such as a double DELETE. To work
51
- # around this you can return a <tt>303 See Other</tt> status code which will be
61
+ # around this you can return a `303 See Other` status code which will be
52
62
  # followed using a GET request.
53
63
  #
54
- # redirect_to posts_url, status: :see_other
55
- # redirect_to action: 'index', status: 303
64
+ # redirect_to posts_url, status: :see_other
65
+ # redirect_to action: 'index', status: 303
66
+ #
67
+ # It is also possible to assign a flash message as part of the redirection.
68
+ # There are two special accessors for the commonly used flash names `alert` and
69
+ # `notice` as well as a general purpose `flash` bucket.
70
+ #
71
+ # redirect_to post_url(@post), alert: "Watch it, mister!"
72
+ # redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
73
+ # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
74
+ # redirect_to({ action: 'atom' }, alert: "Something serious happened")
75
+ #
76
+ # Statements after `redirect_to` in our controller get executed, so
77
+ # `redirect_to` doesn't stop the execution of the function. To terminate the
78
+ # execution of the function immediately after the `redirect_to`, use return.
79
+ #
80
+ # redirect_to post_url(@post) and return
56
81
  #
57
- # It is also possible to assign a flash message as part of the redirection. There are two special accessors for the commonly used flash names
58
- # +alert+ and +notice+ as well as a general purpose +flash+ bucket.
82
+ # ### Open Redirect protection
59
83
  #
60
- # redirect_to post_url(@post), alert: "Watch it, mister!"
61
- # redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
62
- # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
63
- # redirect_to({ action: 'atom' }, alert: "Something serious happened")
84
+ # By default, Rails protects against redirecting to external hosts for your
85
+ # app's safety, so called open redirects. Note: this was a new default in Rails
86
+ # 7.0, after upgrading opt-in by uncommenting the line with
87
+ # `raise_on_open_redirects` in
88
+ # `config/initializers/new_framework_defaults_7_0.rb`
64
89
  #
65
- # When using <tt>redirect_to :back</tt>, if there is no referrer,
66
- # <tt>ActionController::RedirectBackError</tt> will be raised. You
67
- # may specify some fallback behavior for this case by rescuing
68
- # <tt>ActionController::RedirectBackError</tt>.
69
- def redirect_to(options = {}, response_status = {}) #:doc:
90
+ # Here #redirect_to automatically validates the potentially-unsafe URL:
91
+ #
92
+ # redirect_to params[:redirect_url]
93
+ #
94
+ # Raises UnsafeRedirectError in the case of an unsafe redirect.
95
+ #
96
+ # To allow any external redirects pass `allow_other_host: true`, though using a
97
+ # user-provided param in that case is unsafe.
98
+ #
99
+ # redirect_to "https://rubyonrails.org", allow_other_host: true
100
+ #
101
+ # See #url_from for more information on what an internal and safe URL is, or how
102
+ # to fall back to an alternate redirect URL in the unsafe case.
103
+ def redirect_to(options = {}, response_options = {})
70
104
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
71
- raise ActionControllerError.new("Cannot redirect to a parameter hash!") if options.is_a?(ActionController::Parameters)
72
105
  raise AbstractController::DoubleRenderError if response_body
73
106
 
74
- self.status = _extract_redirect_to_status(options, response_status)
75
- self.location = _compute_redirect_to_location(request, options)
76
- self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>"
107
+ allow_other_host = response_options.delete(:allow_other_host) { _allow_other_host }
108
+
109
+ self.status = _extract_redirect_to_status(options, response_options)
110
+
111
+ redirect_to_location = _compute_redirect_to_location(request, options)
112
+ _ensure_url_is_http_header_safe(redirect_to_location)
113
+
114
+ self.location = _enforce_open_redirect_protection(redirect_to_location, allow_other_host: allow_other_host)
115
+ self.response_body = ""
116
+ end
117
+
118
+ # Soft deprecated alias for #redirect_back_or_to where the `fallback_location`
119
+ # location is supplied as a keyword argument instead of the first positional
120
+ # argument.
121
+ def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **args)
122
+ redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args
123
+ end
124
+
125
+ # Redirects the browser to the page that issued the request (the referrer) if
126
+ # possible, otherwise redirects to the provided default fallback location.
127
+ #
128
+ # The referrer information is pulled from the HTTP `Referer` (sic) header on the
129
+ # request. This is an optional header and its presence on the request is subject
130
+ # to browser security settings and user preferences. If the request is missing
131
+ # this header, the `fallback_location` will be used.
132
+ #
133
+ # redirect_back_or_to({ action: "show", id: 5 })
134
+ # redirect_back_or_to @post
135
+ # redirect_back_or_to "http://www.rubyonrails.org"
136
+ # redirect_back_or_to "/images/screenshot.jpg"
137
+ # redirect_back_or_to posts_url
138
+ # redirect_back_or_to proc { edit_post_url(@post) }
139
+ # redirect_back_or_to '/', allow_other_host: false
140
+ #
141
+ # #### Options
142
+ # * `:allow_other_host` - Allow or disallow redirection to the host that is
143
+ # different to the current host, defaults to true.
144
+ #
145
+ #
146
+ # All other options that can be passed to #redirect_to are accepted as options,
147
+ # and the behavior is identical.
148
+ def redirect_back_or_to(fallback_location, allow_other_host: _allow_other_host, **options)
149
+ if request.referer && (allow_other_host || _url_host_allowed?(request.referer))
150
+ redirect_to request.referer, allow_other_host: allow_other_host, **options
151
+ else
152
+ # The method level `allow_other_host` doesn't apply in the fallback case, omit
153
+ # and let the `redirect_to` handling take over.
154
+ redirect_to fallback_location, **options
155
+ end
77
156
  end
78
157
 
79
- def _compute_redirect_to_location(request, options) #:nodoc:
158
+ def _compute_redirect_to_location(request, options) # :nodoc:
80
159
  case options
81
- # The scheme name consist of a letter followed by any combination of
82
- # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
83
- # characters; and is terminated by a colon (":").
84
- # See http://tools.ietf.org/html/rfc3986#section-3.1
85
- # The protocol relative scheme starts with a double slash "//".
86
- when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
87
- options
160
+ # The scheme name consist of a letter followed by any combination of letters,
161
+ # digits, and the plus ("+"), period ("."), or hyphen ("-") characters; and is
162
+ # terminated by a colon (":"). See
163
+ # https://tools.ietf.org/html/rfc3986#section-3.1 The protocol relative scheme
164
+ # starts with a double slash "//".
165
+ when /\A([a-z][a-z\d\-+.]*:|\/\/).*/i
166
+ options.to_str
88
167
  when String
89
168
  request.protocol + request.host_with_port + options
90
- when :back
91
- request.headers["Referer"] or raise RedirectBackError
92
169
  when Proc
93
- _compute_redirect_to_location request, options.call
170
+ _compute_redirect_to_location request, instance_eval(&options)
94
171
  else
95
172
  url_for(options)
96
173
  end.delete("\0\r\n")
@@ -98,15 +175,77 @@ module ActionController
98
175
  module_function :_compute_redirect_to_location
99
176
  public :_compute_redirect_to_location
100
177
 
178
+ # Verifies the passed `location` is an internal URL that's safe to redirect to
179
+ # and returns it, or nil if not. Useful to wrap a params provided redirect URL
180
+ # and fall back to an alternate URL to redirect to:
181
+ #
182
+ # redirect_to url_from(params[:redirect_url]) || root_url
183
+ #
184
+ # The `location` is considered internal, and safe, if it's on the same host as
185
+ # `request.host`:
186
+ #
187
+ # # If request.host is example.com:
188
+ # url_from("https://example.com/profile") # => "https://example.com/profile"
189
+ # url_from("http://example.com/profile") # => "http://example.com/profile"
190
+ # url_from("http://evil.com/profile") # => nil
191
+ #
192
+ # Subdomains are considered part of the host:
193
+ #
194
+ # # If request.host is on https://example.com or https://app.example.com, you'd get:
195
+ # url_from("https://dev.example.com/profile") # => nil
196
+ #
197
+ # NOTE: there's a similarity with
198
+ # [url_for](rdoc-ref:ActionDispatch::Routing::UrlFor#url_for), which generates
199
+ # an internal URL from various options from within the app, e.g.
200
+ # `url_for(@post)`. However, #url_from is meant to take an external parameter to
201
+ # verify as in `url_from(params[:redirect_url])`.
202
+ def url_from(location)
203
+ location = location.presence
204
+ location if location && _url_host_allowed?(location)
205
+ end
206
+
101
207
  private
102
- def _extract_redirect_to_status(options, response_status)
208
+ def _allow_other_host
209
+ !raise_on_open_redirects
210
+ end
211
+
212
+ def _extract_redirect_to_status(options, response_options)
103
213
  if options.is_a?(Hash) && options.key?(:status)
104
214
  Rack::Utils.status_code(options.delete(:status))
105
- elsif response_status.key?(:status)
106
- Rack::Utils.status_code(response_status[:status])
215
+ elsif response_options.key?(:status)
216
+ Rack::Utils.status_code(response_options[:status])
107
217
  else
108
218
  302
109
219
  end
110
220
  end
221
+
222
+ def _enforce_open_redirect_protection(location, allow_other_host:)
223
+ if allow_other_host || _url_host_allowed?(location)
224
+ location
225
+ else
226
+ raise UnsafeRedirectError, "Unsafe redirect to #{location.truncate(100).inspect}, pass allow_other_host: true to redirect anyway."
227
+ end
228
+ end
229
+
230
+ def _url_host_allowed?(url)
231
+ host = URI(url.to_s).host
232
+
233
+ return true if host == request.host
234
+ return false unless host.nil?
235
+ return false unless url.to_s.start_with?("/")
236
+ !url.to_s.start_with?("//")
237
+ rescue ArgumentError, URI::Error
238
+ false
239
+ end
240
+
241
+ def _ensure_url_is_http_header_safe(url)
242
+ # Attempt to comply with the set of valid token characters defined for an HTTP
243
+ # header value in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
244
+ if url.match?(ILLEGAL_HEADER_VALUE_REGEX)
245
+ msg = "The redirect URL #{url} contains one or more illegal HTTP header field character. " \
246
+ "Set of legal characters defined in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6"
247
+ raise UnsafeRedirectError, msg
248
+ end
249
+ end
111
250
  end
112
251
  end
@@ -1,16 +1,21 @@
1
- require 'set'
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "set"
2
6
 
3
7
  module ActionController
4
- # See <tt>Renderers.add</tt>
8
+ # See Renderers.add
5
9
  def self.add_renderer(key, &block)
6
10
  Renderers.add(key, &block)
7
11
  end
8
12
 
9
- # See <tt>Renderers.remove</tt>
13
+ # See Renderers.remove
10
14
  def self.remove_renderer(key)
11
15
  Renderers.remove(key)
12
16
  end
13
17
 
18
+ # See `Responder#api_behavior`
14
19
  class MissingRenderer < LoadError
15
20
  def initialize(format)
16
21
  super "No renderer defined for format: #{format}"
@@ -20,71 +25,53 @@ module ActionController
20
25
  module Renderers
21
26
  extend ActiveSupport::Concern
22
27
 
23
- included do
24
- class_attribute :_renderers
25
- self._renderers = Set.new.freeze
26
- end
28
+ # A Set containing renderer names that correspond to available renderer procs.
29
+ # Default values are `:json`, `:js`, `:xml`.
30
+ RENDERERS = Set.new
27
31
 
28
- module ClassMethods
29
- def use_renderers(*args)
30
- renderers = _renderers + args
31
- self._renderers = renderers.freeze
32
- end
33
- alias use_renderer use_renderers
32
+ included do
33
+ class_attribute :_renderers, default: Set.new.freeze
34
34
  end
35
35
 
36
- def render_to_body(options)
37
- _render_to_body_with_renderer(options) || super
38
- end
36
+ # Used in ActionController::Base and ActionController::API to include all
37
+ # renderers by default.
38
+ module All
39
+ extend ActiveSupport::Concern
40
+ include Renderers
39
41
 
40
- def _render_to_body_with_renderer(options)
41
- _renderers.each do |name|
42
- if options.key?(name)
43
- _process_options(options)
44
- method_name = Renderers._render_with_renderer_method_name(name)
45
- return send(method_name, options.delete(name), options)
46
- end
42
+ included do
43
+ self._renderers = RENDERERS
47
44
  end
48
- nil
49
- end
50
-
51
- # A Set containing renderer names that correspond to available renderer procs.
52
- # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>.
53
- RENDERERS = Set.new
54
-
55
- def self._render_with_renderer_method_name(key)
56
- "_render_with_renderer_#{key}"
57
45
  end
58
46
 
59
- # Adds a new renderer to call within controller actions.
60
- # A renderer is invoked by passing its name as an option to
61
- # <tt>AbstractController::Rendering#render</tt>. To create a renderer
62
- # pass it a name and a block. The block takes two arguments, the first
63
- # is the value paired with its key and the second is the remaining
64
- # hash of options passed to +render+.
47
+ # Adds a new renderer to call within controller actions. A renderer is invoked
48
+ # by passing its name as an option to AbstractController::Rendering#render. To
49
+ # create a renderer pass it a name and a block. The block takes two arguments,
50
+ # the first is the value paired with its key and the second is the remaining
51
+ # hash of options passed to `render`.
65
52
  #
66
53
  # Create a csv renderer:
67
54
  #
68
- # ActionController::Renderers.add :csv do |obj, options|
69
- # filename = options[:filename] || 'data'
70
- # str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s
71
- # send_data str, type: Mime::CSV,
72
- # disposition: "attachment; filename=#{filename}.csv"
73
- # end
55
+ # ActionController::Renderers.add :csv do |obj, options|
56
+ # filename = options[:filename] || 'data'
57
+ # str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s
58
+ # send_data str, type: Mime[:csv],
59
+ # disposition: "attachment; filename=#{filename}.csv"
60
+ # end
74
61
  #
75
- # Note that we used Mime::CSV for the csv mime type as it comes with Rails.
62
+ # Note that we used [Mime](:csv) for the csv mime type as it comes with Rails.
76
63
  # For a custom renderer, you'll need to register a mime type with
77
- # <tt>Mime::Type.register</tt>.
64
+ # `Mime::Type.register`.
78
65
  #
79
66
  # To use the csv renderer in a controller action:
80
67
  #
81
- # def show
82
- # @csvable = Csvable.find(params[:id])
83
- # respond_to do |format|
84
- # format.html
85
- # format.csv { render csv: @csvable, filename: @csvable.name }
68
+ # def show
69
+ # @csvable = Csvable.find(params[:id])
70
+ # respond_to do |format|
71
+ # format.html
72
+ # format.csv { render csv: @csvable, filename: @csvable.name }
73
+ # end
86
74
  # end
87
- # end
88
75
  def self.add(key, &block)
89
76
  define_method(_render_with_renderer_method_name(key), &block)
90
77
  RENDERERS << key.to_sym
@@ -92,46 +79,102 @@ module ActionController
92
79
 
93
80
  # This method is the opposite of add method.
94
81
  #
95
- # Usage:
82
+ # To remove a csv renderer:
96
83
  #
97
- # ActionController::Renderers.remove(:csv)
84
+ # ActionController::Renderers.remove(:csv)
98
85
  def self.remove(key)
99
86
  RENDERERS.delete(key.to_sym)
100
87
  method_name = _render_with_renderer_method_name(key)
101
- remove_method(method_name) if method_defined?(method_name)
88
+ remove_possible_method(method_name)
102
89
  end
103
90
 
104
- module All
105
- extend ActiveSupport::Concern
106
- include Renderers
91
+ def self._render_with_renderer_method_name(key)
92
+ "_render_with_renderer_#{key}"
93
+ end
107
94
 
108
- included do
109
- self._renderers = RENDERERS
95
+ module ClassMethods
96
+ # Adds, by name, a renderer or renderers to the `_renderers` available to call
97
+ # within controller actions.
98
+ #
99
+ # It is useful when rendering from an ActionController::Metal controller or
100
+ # otherwise to add an available renderer proc to a specific controller.
101
+ #
102
+ # Both ActionController::Base and ActionController::API include
103
+ # ActionController::Renderers::All, making all renderers available in the
104
+ # controller. See Renderers::RENDERERS and Renderers.add.
105
+ #
106
+ # Since ActionController::Metal controllers cannot render, the controller must
107
+ # include AbstractController::Rendering, ActionController::Rendering, and
108
+ # ActionController::Renderers, and have at least one renderer.
109
+ #
110
+ # Rather than including ActionController::Renderers::All and including all
111
+ # renderers, you may specify which renderers to include by passing the renderer
112
+ # name or names to `use_renderers`. For example, a controller that includes only
113
+ # the `:json` renderer (`_render_with_renderer_json`) might look like:
114
+ #
115
+ # class MetalRenderingController < ActionController::Metal
116
+ # include AbstractController::Rendering
117
+ # include ActionController::Rendering
118
+ # include ActionController::Renderers
119
+ #
120
+ # use_renderers :json
121
+ #
122
+ # def show
123
+ # render json: record
124
+ # end
125
+ # end
126
+ #
127
+ # You must specify a `use_renderer`, else the `controller.renderer` and
128
+ # `controller._renderers` will be `nil`, and the action will fail.
129
+ def use_renderers(*args)
130
+ renderers = _renderers + args
131
+ self._renderers = renderers.freeze
110
132
  end
133
+ alias use_renderer use_renderers
134
+ end
135
+
136
+ # Called by `render` in AbstractController::Rendering which sets the return
137
+ # value as the `response_body`.
138
+ #
139
+ # If no renderer is found, `super` returns control to
140
+ # `ActionView::Rendering.render_to_body`, if present.
141
+ def render_to_body(options)
142
+ _render_to_body_with_renderer(options) || super
143
+ end
144
+
145
+ def _render_to_body_with_renderer(options)
146
+ _renderers.each do |name|
147
+ if options.key?(name)
148
+ _process_options(options)
149
+ method_name = Renderers._render_with_renderer_method_name(name)
150
+ return send(method_name, options.delete(name), options)
151
+ end
152
+ end
153
+ nil
111
154
  end
112
155
 
113
156
  add :json do |json, options|
114
157
  json = json.to_json(options) unless json.kind_of?(String)
115
158
 
116
159
  if options[:callback].present?
117
- if content_type.nil? || content_type == Mime::JSON
118
- self.content_type = Mime::JS
160
+ if media_type.nil? || media_type == Mime[:json]
161
+ self.content_type = Mime[:js]
119
162
  end
120
163
 
121
164
  "/**/#{options[:callback]}(#{json})"
122
165
  else
123
- self.content_type ||= Mime::JSON
166
+ self.content_type = Mime[:json] if media_type.nil?
124
167
  json
125
168
  end
126
169
  end
127
170
 
128
171
  add :js do |js, options|
129
- self.content_type ||= Mime::JS
172
+ self.content_type = Mime[:js] if media_type.nil?
130
173
  js.respond_to?(:to_js) ? js.to_js(options) : js
131
174
  end
132
175
 
133
176
  add :xml do |xml, options|
134
- self.content_type ||= Mime::XML
177
+ self.content_type = Mime[:xml] if media_type.nil?
135
178
  xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
136
179
  end
137
180
  end