actionpack 4.2.8 → 5.2.4.2

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 (166) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +285 -444
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -7
  5. data/lib/abstract_controller.rb +12 -5
  6. data/lib/abstract_controller/asset_paths.rb +2 -0
  7. data/lib/abstract_controller/base.rb +45 -49
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/{action_controller → abstract_controller}/caching/fragments.rb +78 -15
  10. data/lib/abstract_controller/callbacks.rb +47 -31
  11. data/lib/abstract_controller/collector.rb +8 -11
  12. data/lib/abstract_controller/error.rb +6 -0
  13. data/lib/abstract_controller/helpers.rb +25 -25
  14. data/lib/abstract_controller/logger.rb +2 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +4 -2
  16. data/lib/abstract_controller/rendering.rb +42 -41
  17. data/lib/abstract_controller/translation.rb +10 -7
  18. data/lib/abstract_controller/url_for.rb +2 -0
  19. data/lib/action_controller.rb +29 -21
  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 +27 -19
  23. data/lib/action_controller/caching.rb +14 -57
  24. data/lib/action_controller/form_builder.rb +50 -0
  25. data/lib/action_controller/log_subscriber.rb +10 -15
  26. data/lib/action_controller/metal.rb +98 -83
  27. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  28. data/lib/action_controller/metal/conditional_get.rb +118 -44
  29. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  30. data/lib/action_controller/metal/cookies.rb +3 -3
  31. data/lib/action_controller/metal/data_streaming.rb +27 -46
  32. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  33. data/lib/action_controller/metal/etag_with_template_digest.rb +20 -13
  34. data/lib/action_controller/metal/exceptions.rb +8 -14
  35. data/lib/action_controller/metal/flash.rb +4 -3
  36. data/lib/action_controller/metal/force_ssl.rb +23 -21
  37. data/lib/action_controller/metal/head.rb +21 -19
  38. data/lib/action_controller/metal/helpers.rb +24 -14
  39. data/lib/action_controller/metal/http_authentication.rb +64 -57
  40. data/lib/action_controller/metal/implicit_render.rb +62 -8
  41. data/lib/action_controller/metal/instrumentation.rb +19 -21
  42. data/lib/action_controller/metal/live.rb +90 -106
  43. data/lib/action_controller/metal/mime_responds.rb +33 -46
  44. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  45. data/lib/action_controller/metal/params_wrapper.rb +61 -53
  46. data/lib/action_controller/metal/redirecting.rb +49 -28
  47. data/lib/action_controller/metal/renderers.rb +87 -44
  48. data/lib/action_controller/metal/rendering.rb +72 -50
  49. data/lib/action_controller/metal/request_forgery_protection.rb +203 -92
  50. data/lib/action_controller/metal/rescue.rb +9 -16
  51. data/lib/action_controller/metal/streaming.rb +12 -10
  52. data/lib/action_controller/metal/strong_parameters.rb +582 -165
  53. data/lib/action_controller/metal/testing.rb +2 -17
  54. data/lib/action_controller/metal/url_for.rb +19 -10
  55. data/lib/action_controller/railtie.rb +28 -10
  56. data/lib/action_controller/railties/helpers.rb +2 -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 +280 -411
  60. data/lib/action_dispatch.rb +27 -19
  61. data/lib/action_dispatch/http/cache.rb +93 -47
  62. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  63. data/lib/action_dispatch/http/filter_parameters.rb +26 -20
  64. data/lib/action_dispatch/http/filter_redirect.rb +10 -11
  65. data/lib/action_dispatch/http/headers.rb +55 -22
  66. data/lib/action_dispatch/http/mime_negotiation.rb +60 -41
  67. data/lib/action_dispatch/http/mime_type.rb +134 -121
  68. data/lib/action_dispatch/http/mime_types.rb +20 -6
  69. data/lib/action_dispatch/http/parameter_filter.rb +25 -11
  70. data/lib/action_dispatch/http/parameters.rb +98 -39
  71. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  72. data/lib/action_dispatch/http/request.rb +200 -118
  73. data/lib/action_dispatch/http/response.rb +225 -110
  74. data/lib/action_dispatch/http/upload.rb +12 -6
  75. data/lib/action_dispatch/http/url.rb +110 -28
  76. data/lib/action_dispatch/journey.rb +7 -5
  77. data/lib/action_dispatch/journey/formatter.rb +55 -32
  78. data/lib/action_dispatch/journey/gtg/builder.rb +7 -5
  79. data/lib/action_dispatch/journey/gtg/simulator.rb +3 -9
  80. data/lib/action_dispatch/journey/gtg/transition_table.rb +17 -16
  81. data/lib/action_dispatch/journey/nfa/builder.rb +5 -3
  82. data/lib/action_dispatch/journey/nfa/dot.rb +13 -13
  83. data/lib/action_dispatch/journey/nfa/simulator.rb +3 -1
  84. data/lib/action_dispatch/journey/nfa/transition_table.rb +5 -48
  85. data/lib/action_dispatch/journey/nodes/node.rb +18 -6
  86. data/lib/action_dispatch/journey/parser.rb +23 -22
  87. data/lib/action_dispatch/journey/parser.y +3 -2
  88. data/lib/action_dispatch/journey/parser_extras.rb +12 -4
  89. data/lib/action_dispatch/journey/path/pattern.rb +50 -44
  90. data/lib/action_dispatch/journey/route.rb +106 -28
  91. data/lib/action_dispatch/journey/router.rb +35 -23
  92. data/lib/action_dispatch/journey/router/utils.rb +20 -11
  93. data/lib/action_dispatch/journey/routes.rb +18 -16
  94. data/lib/action_dispatch/journey/scanner.rb +18 -15
  95. data/lib/action_dispatch/journey/visitors.rb +99 -52
  96. data/lib/action_dispatch/middleware/callbacks.rb +1 -2
  97. data/lib/action_dispatch/middleware/cookies.rb +304 -193
  98. data/lib/action_dispatch/middleware/debug_exceptions.rb +152 -57
  99. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  100. data/lib/action_dispatch/middleware/exception_wrapper.rb +68 -69
  101. data/lib/action_dispatch/middleware/executor.rb +21 -0
  102. data/lib/action_dispatch/middleware/flash.rb +78 -54
  103. data/lib/action_dispatch/middleware/public_exceptions.rb +27 -25
  104. data/lib/action_dispatch/middleware/reloader.rb +5 -91
  105. data/lib/action_dispatch/middleware/remote_ip.rb +41 -31
  106. data/lib/action_dispatch/middleware/request_id.rb +17 -9
  107. data/lib/action_dispatch/middleware/session/abstract_store.rb +41 -25
  108. data/lib/action_dispatch/middleware/session/cache_store.rb +24 -14
  109. data/lib/action_dispatch/middleware/session/cookie_store.rb +72 -67
  110. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +8 -2
  111. data/lib/action_dispatch/middleware/show_exceptions.rb +26 -22
  112. data/lib/action_dispatch/middleware/ssl.rb +114 -36
  113. data/lib/action_dispatch/middleware/stack.rb +31 -44
  114. data/lib/action_dispatch/middleware/static.rb +57 -50
  115. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +2 -14
  116. data/lib/action_dispatch/middleware/templates/rescues/{_source.erb → _source.html.erb} +0 -0
  117. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +1 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -1
  122. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  123. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +4 -4
  124. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +64 -64
  125. data/lib/action_dispatch/railtie.rb +19 -11
  126. data/lib/action_dispatch/request/session.rb +106 -59
  127. data/lib/action_dispatch/request/utils.rb +67 -24
  128. data/lib/action_dispatch/routing.rb +17 -18
  129. data/lib/action_dispatch/routing/endpoint.rb +9 -2
  130. data/lib/action_dispatch/routing/inspector.rb +58 -67
  131. data/lib/action_dispatch/routing/mapper.rb +734 -447
  132. data/lib/action_dispatch/routing/polymorphic_routes.rb +161 -139
  133. data/lib/action_dispatch/routing/redirection.rb +36 -26
  134. data/lib/action_dispatch/routing/route_set.rb +321 -291
  135. data/lib/action_dispatch/routing/routes_proxy.rb +32 -5
  136. data/lib/action_dispatch/routing/url_for.rb +65 -25
  137. data/lib/action_dispatch/system_test_case.rb +147 -0
  138. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  139. data/lib/action_dispatch/system_testing/driver.rb +59 -0
  140. data/lib/action_dispatch/system_testing/server.rb +31 -0
  141. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +96 -0
  142. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +31 -0
  143. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  144. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  145. data/lib/action_dispatch/testing/assertions.rb +6 -4
  146. data/lib/action_dispatch/testing/assertions/response.rb +45 -20
  147. data/lib/action_dispatch/testing/assertions/routing.rb +30 -26
  148. data/lib/action_dispatch/testing/integration.rb +347 -209
  149. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  150. data/lib/action_dispatch/testing/test_process.rb +28 -22
  151. data/lib/action_dispatch/testing/test_request.rb +27 -34
  152. data/lib/action_dispatch/testing/test_response.rb +35 -7
  153. data/lib/action_pack.rb +4 -2
  154. data/lib/action_pack/gem_version.rb +5 -3
  155. data/lib/action_pack/version.rb +3 -1
  156. metadata +56 -39
  157. data/lib/action_controller/metal/hide_actions.rb +0 -40
  158. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  159. data/lib/action_controller/middleware.rb +0 -39
  160. data/lib/action_controller/model_naming.rb +0 -12
  161. data/lib/action_dispatch/journey/backwards.rb +0 -5
  162. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  163. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  164. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  165. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  166. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,4 +1,6 @@
1
- require 'set'
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
2
4
 
3
5
  module ActionController
4
6
  # See <tt>Renderers.add</tt>
@@ -11,6 +13,7 @@ module ActionController
11
13
  Renderers.remove(key)
12
14
  end
13
15
 
16
+ # See <tt>Responder#api_behavior</tt>
14
17
  class MissingRenderer < LoadError
15
18
  def initialize(format)
16
19
  super "No renderer defined for format: #{format}"
@@ -20,40 +23,24 @@ module ActionController
20
23
  module Renderers
21
24
  extend ActiveSupport::Concern
22
25
 
23
- included do
24
- class_attribute :_renderers
25
- self._renderers = Set.new.freeze
26
- end
26
+ # A Set containing renderer names that correspond to available renderer procs.
27
+ # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>.
28
+ RENDERERS = Set.new
27
29
 
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
30
+ included do
31
+ class_attribute :_renderers, default: Set.new.freeze
34
32
  end
35
33
 
36
- def render_to_body(options)
37
- _render_to_body_with_renderer(options) || super
38
- end
34
+ # Used in <tt>ActionController::Base</tt>
35
+ # and <tt>ActionController::API</tt> to include all
36
+ # renderers by default.
37
+ module All
38
+ extend ActiveSupport::Concern
39
+ include Renderers
39
40
 
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
41
+ included do
42
+ self._renderers = RENDERERS
47
43
  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
44
  end
58
45
 
59
46
  # Adds a new renderer to call within controller actions.
@@ -68,11 +55,11 @@ module ActionController
68
55
  # ActionController::Renderers.add :csv do |obj, options|
69
56
  # filename = options[:filename] || 'data'
70
57
  # str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s
71
- # send_data str, type: Mime::CSV,
58
+ # send_data str, type: Mime[:csv],
72
59
  # disposition: "attachment; filename=#{filename}.csv"
73
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
64
  # <tt>Mime::Type.register</tt>.
78
65
  #
@@ -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
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
97
+ # to call within controller actions.
98
+ #
99
+ # It is useful when rendering from an <tt>ActionController::Metal</tt> controller or
100
+ # otherwise to add an available renderer proc to a specific controller.
101
+ #
102
+ # Both <tt>ActionController::Base</tt> and <tt>ActionController::API</tt>
103
+ # include <tt>ActionController::Renderers::All</tt>, making all renderers
104
+ # available in the controller. See <tt>Renderers::RENDERERS</tt> and <tt>Renderers.add</tt>.
105
+ #
106
+ # Since <tt>ActionController::Metal</tt> controllers cannot render, the controller
107
+ # must include <tt>AbstractController::Rendering</tt>, <tt>ActionController::Rendering</tt>,
108
+ # and <tt>ActionController::Renderers</tt>, and have at least one renderer.
109
+ #
110
+ # Rather than including <tt>ActionController::Renderers::All</tt> and including all renderers,
111
+ # you may specify which renderers to include by passing the renderer name or names to
112
+ # +use_renderers+. For example, a controller that includes only the <tt>:json</tt> renderer
113
+ # (+_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 <tt>nil</tt>, 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 <tt>AbstractController::Rendering</tt>
137
+ # which sets the return value as the +response_body+.
138
+ #
139
+ # If no renderer is found, +super+ returns control to
140
+ # <tt>ActionView::Rendering.render_to_body</tt>, 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 content_type.nil? || content_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]
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]
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]
135
178
  xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
136
179
  end
137
180
  end
@@ -1,8 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionController
2
4
  module Rendering
3
5
  extend ActiveSupport::Concern
4
6
 
5
- RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html]
7
+ RENDER_FORMATS_IN_PRIORITY = [:body, :plain, :html]
8
+
9
+ module ClassMethods
10
+ # Documentation at ActionController::Renderer#render
11
+ delegate :render, to: :renderer
12
+
13
+ # Returns a renderer instance (inherited from ActionController::Renderer)
14
+ # for the controller.
15
+ attr_reader :renderer
16
+
17
+ def setup_renderer! # :nodoc:
18
+ @renderer = Renderer.for(self)
19
+ end
20
+
21
+ def inherited(klass)
22
+ klass.setup_renderer!
23
+ super
24
+ end
25
+ end
6
26
 
7
27
  # Before processing, set the request formats in current controller formats.
8
28
  def process_action(*) #:nodoc:
@@ -12,15 +32,15 @@ module ActionController
12
32
 
13
33
  # Check for double render errors and set the content_type after rendering.
14
34
  def render(*args) #:nodoc:
15
- raise ::AbstractController::DoubleRenderError if self.response_body
35
+ raise ::AbstractController::DoubleRenderError if response_body
16
36
  super
17
37
  end
18
38
 
19
- # Overwrite render_to_string because body can now be set to a rack body.
39
+ # Overwrite render_to_string because body can now be set to a Rack body.
20
40
  def render_to_string(*)
21
41
  result = super
22
42
  if result.respond_to?(:each)
23
- string = ""
43
+ string = "".dup
24
44
  result.each { |r| string << r }
25
45
  string
26
46
  else
@@ -29,72 +49,74 @@ module ActionController
29
49
  end
30
50
 
31
51
  def render_to_body(options = {})
32
- super || _render_in_priorities(options) || ' '
52
+ super || _render_in_priorities(options) || " "
33
53
  end
34
54
 
35
55
  private
36
56
 
37
- def _render_in_priorities(options)
38
- RENDER_FORMATS_IN_PRIORITY.each do |format|
39
- return options[format] if options.key?(format)
57
+ def _process_variant(options)
58
+ if defined?(request) && !request.nil? && request.variant.present?
59
+ options[:variant] = request.variant
60
+ end
40
61
  end
41
62
 
42
- nil
43
- end
44
-
45
- def _process_format(format, options = {})
46
- super
63
+ def _render_in_priorities(options)
64
+ RENDER_FORMATS_IN_PRIORITY.each do |format|
65
+ return options[format] if options.key?(format)
66
+ end
47
67
 
48
- if options[:plain]
49
- self.content_type = Mime::TEXT
50
- else
51
- self.content_type ||= format.to_s
68
+ nil
52
69
  end
53
- end
54
70
 
55
- # Normalize arguments by catching blocks and setting them on :update.
56
- def _normalize_args(action=nil, options={}, &blk) #:nodoc:
57
- options = super
58
- options[:update] = blk if block_given?
59
- options
60
- end
61
-
62
- # Normalize both text and status options.
63
- def _normalize_options(options) #:nodoc:
64
- _normalize_text(options)
65
-
66
- if options[:html]
67
- options[:html] = ERB::Util.html_escape(options[:html])
71
+ def _set_html_content_type
72
+ self.content_type = Mime[:html].to_s
68
73
  end
69
74
 
70
- if options.delete(:nothing)
71
- options[:body] = nil
75
+ def _set_rendered_content_type(format)
76
+ if format && !response.content_type
77
+ self.content_type = format.to_s
78
+ end
72
79
  end
73
80
 
74
- if options[:status]
75
- options[:status] = Rack::Utils.status_code(options[:status])
81
+ # Normalize arguments by catching blocks and setting them on :update.
82
+ def _normalize_args(action = nil, options = {}, &blk)
83
+ options = super
84
+ options[:update] = blk if block_given?
85
+ options
76
86
  end
77
87
 
78
- super
79
- end
88
+ # Normalize both text and status options.
89
+ def _normalize_options(options)
90
+ _normalize_text(options)
91
+
92
+ if options[:html]
93
+ options[:html] = ERB::Util.html_escape(options[:html])
94
+ end
80
95
 
81
- def _normalize_text(options)
82
- RENDER_FORMATS_IN_PRIORITY.each do |format|
83
- if options.key?(format) && options[format].respond_to?(:to_text)
84
- options[format] = options[format].to_text
96
+ if options[:status]
97
+ options[:status] = Rack::Utils.status_code(options[:status])
85
98
  end
99
+
100
+ super
86
101
  end
87
- end
88
102
 
89
- # Process controller specific options, as status, content-type and location.
90
- def _process_options(options) #:nodoc:
91
- status, content_type, location = options.values_at(:status, :content_type, :location)
103
+ def _normalize_text(options)
104
+ RENDER_FORMATS_IN_PRIORITY.each do |format|
105
+ if options.key?(format) && options[format].respond_to?(:to_text)
106
+ options[format] = options[format].to_text
107
+ end
108
+ end
109
+ end
92
110
 
93
- self.status = status if status
94
- self.content_type = content_type if content_type
95
- self.headers["Location"] = url_for(location) if location
111
+ # Process controller specific options, as status, content-type and location.
112
+ def _process_options(options)
113
+ status, content_type, location = options.values_at(:status, :content_type, :location)
96
114
 
97
- super
98
- end
115
+ self.status = status if status
116
+ self.content_type = content_type if content_type
117
+ headers["Location"] = url_for(location) if location
118
+
119
+ super
120
+ end
99
121
  end
100
122
  end
@@ -1,6 +1,9 @@
1
- require 'rack/session/abstract/id'
2
- require 'action_controller/metal/exceptions'
3
- require 'active_support/security_utils'
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/session/abstract/id"
4
+ require "action_controller/metal/exceptions"
5
+ require "active_support/security_utils"
6
+ require "active_support/core_ext/string/strip"
4
7
 
5
8
  module ActionController #:nodoc:
6
9
  class InvalidAuthenticityToken < ActionControllerError #:nodoc:
@@ -13,9 +16,14 @@ module ActionController #:nodoc:
13
16
  # by including a token in the rendered HTML for your application. This token is
14
17
  # stored as a random string in the session, to which an attacker does not have
15
18
  # access. When a request reaches your application, \Rails verifies the received
16
- # token with the token in the session. Only HTML and JavaScript requests are checked,
17
- # so this will not protect your XML API (presumably you'll have a different
18
- # authentication scheme there anyway).
19
+ # token with the token in the session. All requests are checked except GET requests
20
+ # as these should be idempotent. Keep in mind that all session-oriented requests
21
+ # should be CSRF protected, including JavaScript and HTML requests.
22
+ #
23
+ # Since HTML and JavaScript requests are typically made from the browser, we
24
+ # need to ensure to verify request authenticity for the web browser. We can
25
+ # use session-oriented authentication for these types of requests, by using
26
+ # the <tt>protect_from_forgery</tt> method in our controllers.
19
27
  #
20
28
  # GET requests are not protected since they don't have side effects like writing
21
29
  # to the database and don't leak sensitive information. JavaScript requests are
@@ -26,22 +34,21 @@ module ActionController #:nodoc:
26
34
  # Ajax) requests are allowed to make GET requests for JavaScript responses.
27
35
  #
28
36
  # It's important to remember that XML or JSON requests are also affected and if
29
- # you're building an API you'll need something like:
37
+ # you're building an API you should change forgery protection method in
38
+ # <tt>ApplicationController</tt> (by default: <tt>:exception</tt>):
30
39
  #
31
40
  # class ApplicationController < ActionController::Base
32
- # protect_from_forgery
33
- # skip_before_action :verify_authenticity_token, if: :json_request?
34
- #
35
- # protected
36
- #
37
- # def json_request?
38
- # request.format.json?
39
- # end
41
+ # protect_from_forgery unless: -> { request.format.json? }
40
42
  # end
41
43
  #
42
- # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method,
43
- # which checks the token and resets the session if it doesn't match what was expected.
44
- # A call to this method is generated for new \Rails applications by default.
44
+ # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method.
45
+ # By default <tt>protect_from_forgery</tt> protects your session with
46
+ # <tt>:null_session</tt> method, which provides an empty session
47
+ # during request.
48
+ #
49
+ # We may want to disable CSRF protection for APIs since they are typically
50
+ # designed to be state-less. That is, the request API client will handle
51
+ # the session for you instead of Rails.
45
52
  #
46
53
  # The token parameter is named <tt>authenticity_token</tt> by default. The name and
47
54
  # value of this token must be added to every layout that renders forms by including
@@ -73,6 +80,18 @@ module ActionController #:nodoc:
73
80
  config_accessor :log_warning_on_csrf_failure
74
81
  self.log_warning_on_csrf_failure = true
75
82
 
83
+ # Controls whether the Origin header is checked in addition to the CSRF token.
84
+ config_accessor :forgery_protection_origin_check
85
+ self.forgery_protection_origin_check = false
86
+
87
+ # Controls whether form-action/method specific CSRF tokens are used.
88
+ config_accessor :per_form_csrf_tokens
89
+ self.per_form_csrf_tokens = false
90
+
91
+ # Controls whether forgery protection is enabled by default.
92
+ config_accessor :default_protect_from_forgery
93
+ self.default_protect_from_forgery = false
94
+
76
95
  helper_method :form_authenticity_token
77
96
  helper_method :protect_against_forgery?
78
97
  end
@@ -86,13 +105,21 @@ module ActionController #:nodoc:
86
105
  #
87
106
  # class FooController < ApplicationController
88
107
  # protect_from_forgery except: :index
108
+ # end
109
+ #
110
+ # You can disable forgery protection on controller by skipping the verification before_action:
89
111
  #
90
- # You can disable CSRF protection on controller by skipping the verification before_action:
91
112
  # skip_before_action :verify_authenticity_token
92
113
  #
93
114
  # Valid Options:
94
115
  #
95
- # * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified.
116
+ # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. For example <tt>only: [ :create, :create_all ]</tt>.
117
+ # * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference.
118
+ # * <tt>:prepend</tt> - By default, the verification of the authentication token will be added at the position of the
119
+ # protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful
120
+ # when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth).
121
+ #
122
+ # If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>.
96
123
  # * <tt>:with</tt> - Set the method to handle unverified request.
97
124
  #
98
125
  # Valid unverified request handling methods are:
@@ -100,19 +127,30 @@ module ActionController #:nodoc:
100
127
  # * <tt>:reset_session</tt> - Resets the session.
101
128
  # * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified.
102
129
  def protect_from_forgery(options = {})
130
+ options = options.reverse_merge(prepend: false)
131
+
103
132
  self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
104
133
  self.request_forgery_protection_token ||= :authenticity_token
105
- prepend_before_action :verify_authenticity_token, options
134
+ before_action :verify_authenticity_token, options
106
135
  append_after_action :verify_same_origin_request
107
136
  end
108
137
 
138
+ # Turn off request forgery protection. This is a wrapper for:
139
+ #
140
+ # skip_before_action :verify_authenticity_token
141
+ #
142
+ # See +skip_before_action+ for allowed options.
143
+ def skip_forgery_protection(options = {})
144
+ skip_before_action :verify_authenticity_token, options
145
+ end
146
+
109
147
  private
110
148
 
111
- def protection_method_class(name)
112
- ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify)
113
- rescue NameError
114
- raise ArgumentError, 'Invalid request forgery protection method, use :null_session, :exception, or :reset_session'
115
- end
149
+ def protection_method_class(name)
150
+ ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify)
151
+ rescue NameError
152
+ raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, or :reset_session"
153
+ end
116
154
  end
117
155
 
118
156
  module ProtectionMethods
@@ -124,42 +162,34 @@ module ActionController #:nodoc:
124
162
  # This is the method that defines the application behavior when a request is found to be unverified.
125
163
  def handle_unverified_request
126
164
  request = @controller.request
127
- request.session = NullSessionHash.new(request.env)
128
- request.env['action_dispatch.request.flash_hash'] = nil
129
- request.env['rack.session.options'] = { skip: true }
130
- request.env['action_dispatch.cookies'] = NullCookieJar.build(request)
165
+ request.session = NullSessionHash.new(request)
166
+ request.flash = nil
167
+ request.session_options = { skip: true }
168
+ request.cookie_jar = NullCookieJar.build(request, {})
131
169
  end
132
170
 
133
- protected
171
+ private
134
172
 
135
- class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
136
- def initialize(env)
137
- super(nil, env)
138
- @data = {}
139
- @loaded = true
140
- end
173
+ class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
174
+ def initialize(req)
175
+ super(nil, req)
176
+ @data = {}
177
+ @loaded = true
178
+ end
141
179
 
142
- # no-op
143
- def destroy; end
180
+ # no-op
181
+ def destroy; end
144
182
 
145
- def exists?
146
- true
147
- end
148
- end
149
-
150
- class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
151
- def self.build(request)
152
- key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY]
153
- host = request.host
154
- secure = request.ssl?
155
-
156
- new(key_generator, host, secure, options_for_env({}))
183
+ def exists?
184
+ true
185
+ end
157
186
  end
158
187
 
159
- def write(*)
160
- # nothing
188
+ class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
189
+ def write(*)
190
+ # nothing
191
+ end
161
192
  end
162
- end
163
193
  end
164
194
 
165
195
  class ResetSession
@@ -183,29 +213,33 @@ module ActionController #:nodoc:
183
213
  end
184
214
  end
185
215
 
186
- protected
216
+ private
187
217
  # The actual before_action that is used to verify the CSRF token.
188
218
  # Don't override this directly. Provide your own forgery protection
189
219
  # strategy instead. If you override, you'll disable same-origin
190
- # `<script>` verification.
220
+ # <tt><script></tt> verification.
191
221
  #
192
222
  # Lean on the protect_from_forgery declaration to mark which actions are
193
223
  # due for same-origin request verification. If protect_from_forgery is
194
224
  # enabled on an action, this before_action flags its after_action to
195
225
  # verify that JavaScript responses are for XHR requests, ensuring they
196
226
  # follow the browser's same-origin policy.
197
- def verify_authenticity_token
227
+ def verify_authenticity_token # :doc:
198
228
  mark_for_same_origin_verification!
199
229
 
200
230
  if !verified_request?
201
231
  if logger && log_warning_on_csrf_failure
202
- logger.warn "Can't verify CSRF token authenticity"
232
+ if valid_request_origin?
233
+ logger.warn "Can't verify CSRF token authenticity."
234
+ else
235
+ logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
236
+ end
203
237
  end
204
238
  handle_unverified_request
205
239
  end
206
240
  end
207
241
 
208
- def handle_unverified_request
242
+ def handle_unverified_request # :doc:
209
243
  forgery_protection_strategy.new(self).handle_unverified_request
210
244
  end
211
245
 
@@ -215,30 +249,33 @@ module ActionController #:nodoc:
215
249
  "If you know what you're doing, go ahead and disable forgery " \
216
250
  "protection on this action to permit cross-origin JavaScript embedding."
217
251
  private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING
252
+ # :startdoc:
218
253
 
219
- # If `verify_authenticity_token` was run (indicating that we have
254
+ # If +verify_authenticity_token+ was run (indicating that we have
220
255
  # forgery protection enabled for this request) then also verify that
221
256
  # we aren't serving an unauthorized cross-origin response.
222
- def verify_same_origin_request
257
+ def verify_same_origin_request # :doc:
223
258
  if marked_for_same_origin_verification? && non_xhr_javascript_response?
224
- logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING if logger
259
+ if logger && log_warning_on_csrf_failure
260
+ logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING
261
+ end
225
262
  raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING
226
263
  end
227
264
  end
228
265
 
229
266
  # GET requests are checked for cross-origin JavaScript after rendering.
230
- def mark_for_same_origin_verification!
267
+ def mark_for_same_origin_verification! # :doc:
231
268
  @marked_for_same_origin_verification = request.get?
232
269
  end
233
270
 
234
- # If the `verify_authenticity_token` before_action ran, verify that
271
+ # If the +verify_authenticity_token+ before_action ran, verify that
235
272
  # JavaScript responses are only served to same-origin GET requests.
236
- def marked_for_same_origin_verification?
273
+ def marked_for_same_origin_verification? # :doc:
237
274
  @marked_for_same_origin_verification ||= false
238
275
  end
239
276
 
240
277
  # Check for cross-origin JavaScript responses.
241
- def non_xhr_javascript_response?
278
+ def non_xhr_javascript_response? # :doc:
242
279
  content_type =~ %r(\Atext/javascript) && !request.xhr?
243
280
  end
244
281
 
@@ -246,26 +283,46 @@ module ActionController #:nodoc:
246
283
 
247
284
  # Returns true or false if a request is verified. Checks:
248
285
  #
249
- # * is it a GET or HEAD request? Gets should be safe and idempotent
286
+ # * Is it a GET or HEAD request? GETs should be safe and idempotent
250
287
  # * Does the form_authenticity_token match the given token value from the params?
251
- # * Does the X-CSRF-Token header match the form_authenticity_token
252
- def verified_request?
288
+ # * Does the X-CSRF-Token header match the form_authenticity_token?
289
+ def verified_request? # :doc:
253
290
  !protect_against_forgery? || request.get? || request.head? ||
254
- valid_authenticity_token?(session, form_authenticity_param) ||
255
- valid_authenticity_token?(session, request.headers['X-CSRF-Token'])
291
+ (valid_request_origin? && any_authenticity_token_valid?)
292
+ end
293
+
294
+ # Checks if any of the authenticity tokens from the request are valid.
295
+ def any_authenticity_token_valid? # :doc:
296
+ request_authenticity_tokens.any? do |token|
297
+ valid_authenticity_token?(session, token)
298
+ end
299
+ end
300
+
301
+ # Possible authenticity tokens sent in the request.
302
+ def request_authenticity_tokens # :doc:
303
+ [form_authenticity_param, request.x_csrf_token]
256
304
  end
257
305
 
258
306
  # Sets the token value for the current session.
259
- def form_authenticity_token
260
- masked_authenticity_token(session)
307
+ def form_authenticity_token(form_options: {})
308
+ masked_authenticity_token(session, form_options: form_options)
261
309
  end
262
310
 
263
311
  # Creates a masked version of the authenticity token that varies
264
312
  # on each request. The masking is used to mitigate SSL attacks
265
313
  # like BREACH.
266
- def masked_authenticity_token(session)
314
+ def masked_authenticity_token(session, form_options: {}) # :doc:
315
+ action, method = form_options.values_at(:action, :method)
316
+
317
+ raw_token = if per_form_csrf_tokens && action && method
318
+ action_path = normalize_action_path(action)
319
+ per_form_csrf_token(session, action_path, method)
320
+ else
321
+ real_csrf_token(session)
322
+ end
323
+
267
324
  one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
268
- encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session))
325
+ encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
269
326
  masked_token = one_time_pad + encrypted_csrf_token
270
327
  Base64.strict_encode64(masked_token)
271
328
  end
@@ -273,7 +330,7 @@ module ActionController #:nodoc:
273
330
  # Checks the client's masked token to see if it matches the
274
331
  # session token. Essentially the inverse of
275
332
  # +masked_authenticity_token+.
276
- def valid_authenticity_token?(session, encoded_masked_token)
333
+ def valid_authenticity_token?(session, encoded_masked_token) # :doc:
277
334
  if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
278
335
  return false
279
336
  end
@@ -291,44 +348,98 @@ module ActionController #:nodoc:
291
348
  if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
292
349
  # This is actually an unmasked token. This is expected if
293
350
  # you have just upgraded to masked tokens, but should stop
294
- # happening shortly after installing this gem
351
+ # happening shortly after installing this gem.
295
352
  compare_with_real_token masked_token, session
296
353
 
297
354
  elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
298
- # Split the token into the one-time pad and the encrypted
299
- # value and decrypt it
300
- one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
301
- encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
302
- csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token)
303
-
304
- compare_with_real_token csrf_token, session
355
+ csrf_token = unmask_token(masked_token)
305
356
 
357
+ compare_with_real_token(csrf_token, session) ||
358
+ valid_per_form_csrf_token?(csrf_token, session)
306
359
  else
307
- false # Token is malformed
360
+ false # Token is malformed.
308
361
  end
309
362
  end
310
363
 
311
- def compare_with_real_token(token, session)
312
- ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
364
+ def unmask_token(masked_token) # :doc:
365
+ # Split the token into the one-time pad and the encrypted
366
+ # value and decrypt it.
367
+ one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
368
+ encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
369
+ xor_byte_strings(one_time_pad, encrypted_csrf_token)
370
+ end
371
+
372
+ def compare_with_real_token(token, session) # :doc:
373
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
313
374
  end
314
375
 
315
- def real_csrf_token(session)
376
+ def valid_per_form_csrf_token?(token, session) # :doc:
377
+ if per_form_csrf_tokens
378
+ correct_token = per_form_csrf_token(
379
+ session,
380
+ normalize_action_path(request.fullpath),
381
+ request.request_method
382
+ )
383
+
384
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token)
385
+ else
386
+ false
387
+ end
388
+ end
389
+
390
+ def real_csrf_token(session) # :doc:
316
391
  session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
317
392
  Base64.strict_decode64(session[:_csrf_token])
318
393
  end
319
394
 
320
- def xor_byte_strings(s1, s2)
321
- s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
395
+ def per_form_csrf_token(session, action_path, method) # :doc:
396
+ OpenSSL::HMAC.digest(
397
+ OpenSSL::Digest::SHA256.new,
398
+ real_csrf_token(session),
399
+ [action_path, method.downcase].join("#")
400
+ )
401
+ end
402
+
403
+ def xor_byte_strings(s1, s2) # :doc:
404
+ s2_bytes = s2.bytes
405
+ s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 }
406
+ s2_bytes.pack("C*")
322
407
  end
323
408
 
324
409
  # The form's authenticity parameter. Override to provide your own.
325
- def form_authenticity_param
410
+ def form_authenticity_param # :doc:
326
411
  params[request_forgery_protection_token]
327
412
  end
328
413
 
329
414
  # Checks if the controller allows forgery protection.
330
- def protect_against_forgery?
415
+ def protect_against_forgery? # :doc:
331
416
  allow_forgery_protection
332
417
  end
418
+
419
+ NULL_ORIGIN_MESSAGE = <<-MSG.strip_heredoc
420
+ The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually
421
+ means you have the 'no-referrer' Referrer-Policy header enabled, or that the request came from a site that
422
+ refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the
423
+ best solution is to change your referrer policy to something less strict like same-origin or strict-same-origin.
424
+ If you cannot change the referrer policy, you can disable origin checking with the
425
+ Rails.application.config.action_controller.forgery_protection_origin_check setting.
426
+ MSG
427
+
428
+ # Checks if the request originated from the same origin by looking at the
429
+ # Origin header.
430
+ def valid_request_origin? # :doc:
431
+ if forgery_protection_origin_check
432
+ # We accept blank origin headers because some user agents don't send it.
433
+ raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null"
434
+ request.origin.nil? || request.origin == request.base_url
435
+ else
436
+ true
437
+ end
438
+ end
439
+
440
+ def normalize_action_path(action_path) # :doc:
441
+ uri = URI.parse(action_path)
442
+ uri.path.chomp("/")
443
+ end
333
444
  end
334
445
  end