loading
Generated 2020-08-25T22:54:39-04:00

All Files ( 48.67% covered at 54.03 hits/line )

143 files in total.
9268 relevant lines, 4511 lines covered and 4757 lines missed. ( 48.67% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/abstract_controller.rb 90.48 % 28 21 19 2 0.90
lib/abstract_controller/asset_paths.rb 100.00 % 12 5 5 0 1.00
lib/abstract_controller/base.rb 58.43 % 295 89 52 37 72.64
lib/abstract_controller/caching.rb 83.33 % 66 36 30 6 1.11
lib/abstract_controller/caching/fragments.rb 39.58 % 150 48 19 29 0.50
lib/abstract_controller/callbacks.rb 91.67 % 224 36 33 3 58.11
lib/abstract_controller/collector.rb 61.11 % 42 18 11 7 6.11
lib/abstract_controller/error.rb 100.00 % 6 2 2 0 1.00
lib/abstract_controller/helpers.rb 87.72 % 184 57 50 7 94.26
lib/abstract_controller/logger.rb 100.00 % 14 7 7 0 1.29
lib/abstract_controller/railties/routes_helpers.rb 100.00 % 20 10 10 0 132.90
lib/abstract_controller/rendering.rb 50.00 % 127 56 28 28 0.50
lib/abstract_controller/translation.rb 47.06 % 37 17 8 9 0.47
lib/abstract_controller/url_for.rb 61.54 % 35 13 8 5 0.62
lib/action_controller.rb 100.00 % 69 53 53 0 1.00
lib/action_controller/api.rb 80.00 % 150 15 12 3 1.73
lib/action_controller/api/api_rendering.rb 75.00 % 16 8 6 2 0.75
lib/action_controller/base.rb 80.00 % 273 20 16 4 2.45
lib/action_controller/caching.rb 80.00 % 44 10 8 2 0.90
lib/action_controller/form_builder.rb 90.00 % 50 10 9 1 0.90
lib/action_controller/log_subscriber.rb 34.15 % 81 41 14 27 0.46
lib/action_controller/metal.rb 63.21 % 258 106 67 39 7.01
lib/action_controller/metal/basic_implicit_render.rb 66.67 % 13 6 4 2 0.67
lib/action_controller/metal/conditional_get.rb 48.65 % 288 37 18 19 0.68
lib/action_controller/metal/content_security_policy.rb 62.96 % 51 27 17 10 0.89
lib/action_controller/metal/cookies.rb 87.50 % 16 8 7 1 1.00
lib/action_controller/metal/data_streaming.rb 32.43 % 151 37 12 25 0.32
lib/action_controller/metal/default_headers.rb 71.43 % 17 7 5 2 0.71
lib/action_controller/metal/etag_with_flash.rb 100.00 % 18 6 6 0 1.00
lib/action_controller/metal/etag_with_template_digest.rb 64.71 % 55 17 11 6 0.65
lib/action_controller/metal/exceptions.rb 59.57 % 107 47 28 19 0.60
lib/action_controller/metal/feature_policy.rb 60.00 % 46 10 6 4 0.80
lib/action_controller/metal/flash.rb 69.57 % 61 23 16 7 1.09
lib/action_controller/metal/head.rb 20.83 % 63 24 5 19 0.21
lib/action_controller/metal/helpers.rb 80.00 % 132 30 24 6 19.60
lib/action_controller/metal/http_authentication.rb 40.69 % 518 145 59 86 0.41
lib/action_controller/metal/implicit_render.rb 35.00 % 63 20 7 13 0.35
lib/action_controller/metal/instrumentation.rb 40.00 % 106 45 18 27 0.40
lib/action_controller/metal/live.rb 30.33 % 310 122 37 85 0.30
lib/action_controller/metal/logging.rb 100.00 % 20 6 6 0 1.17
lib/action_controller/metal/mime_responds.rb 34.38 % 328 64 22 42 0.34
lib/action_controller/metal/parameter_encoding.rb 92.31 % 51 13 12 1 63.31
lib/action_controller/metal/params_wrapper.rb 47.62 % 296 105 50 55 6.27
lib/action_controller/metal/redirecting.rb 39.39 % 133 33 13 20 0.39
lib/action_controller/metal/renderers.rb 58.18 % 181 55 32 23 0.75
lib/action_controller/metal/rendering.rb 41.27 % 127 63 26 37 13.86
lib/action_controller/metal/request_forgery_protection.rb 48.96 % 498 192 94 98 0.83
lib/action_controller/metal/rescue.rb 63.64 % 28 11 7 4 0.64
lib/action_controller/metal/streaming.rb 41.18 % 222 17 7 10 0.41
lib/action_controller/metal/strong_parameters.rb 38.59 % 1197 298 115 183 0.39
lib/action_controller/metal/testing.rb 62.50 % 16 8 5 3 0.63
lib/action_controller/metal/url_for.rb 29.41 % 58 17 5 12 0.29
lib/action_controller/railtie.rb 0.00 % 89 71 0 71 0.00
lib/action_controller/railties/helpers.rb 0.00 % 24 19 0 19 0.00
lib/action_controller/renderer.rb 76.92 % 140 39 30 9 247.95
lib/action_controller/template_assertions.rb 75.00 % 11 4 3 1 0.75
lib/action_controller/test_case.rb 34.62 % 643 260 90 170 1.00
lib/action_dispatch.rb 95.59 % 120 68 65 3 0.96
lib/action_dispatch/http/cache.rb 37.50 % 224 112 42 70 0.38
lib/action_dispatch/http/content_disposition.rb 56.52 % 45 23 13 10 0.57
lib/action_dispatch/http/content_security_policy.rb 47.01 % 286 134 63 71 0.84
lib/action_dispatch/http/feature_policy.rb 53.33 % 168 75 40 35 0.75
lib/action_dispatch/http/filter_parameters.rb 52.94 % 85 34 18 16 0.53
lib/action_dispatch/http/filter_redirect.rb 42.11 % 36 19 8 11 0.42
lib/action_dispatch/http/headers.rb 48.84 % 132 43 21 22 0.49
lib/action_dispatch/http/mime_negotiation.rb 33.33 % 182 78 26 52 0.33
lib/action_dispatch/http/mime_type.rb 52.88 % 361 191 101 90 3.77
lib/action_dispatch/http/mime_types.rb 100.00 % 50 34 34 0 1.00
lib/action_dispatch/http/parameter_filter.rb 0.00 % 12 9 0 9 0.00
lib/action_dispatch/http/parameters.rb 41.67 % 135 60 25 35 0.43
lib/action_dispatch/http/rack_cache.rb 52.94 % 63 34 18 16 0.53
lib/action_dispatch/http/request.rb 54.01 % 442 187 101 86 0.83
lib/action_dispatch/http/response.rb 42.13 % 539 254 107 147 0.42
lib/action_dispatch/http/upload.rb 47.22 % 92 36 17 19 0.47
lib/action_dispatch/http/url.rb 30.88 % 350 136 42 94 0.31
lib/action_dispatch/journey.rb 100.00 % 5 3 3 0 1.00
lib/action_dispatch/journey/formatter.rb 25.86 % 213 116 30 86 1.66
lib/action_dispatch/journey/gtg/builder.rb 18.99 % 149 79 15 64 0.19
lib/action_dispatch/journey/gtg/simulator.rb 57.14 % 42 21 12 9 0.57
lib/action_dispatch/journey/gtg/transition_table.rb 25.84 % 159 89 23 66 0.26
lib/action_dispatch/journey/nfa/dot.rb 62.50 % 25 8 5 3 0.63
lib/action_dispatch/journey/nodes/node.rb 93.75 % 142 80 75 5 961.84
lib/action_dispatch/journey/parser.rb 93.18 % 199 44 41 3 102.11
lib/action_dispatch/journey/parser_extras.rb 100.00 % 31 15 15 0 278.67
lib/action_dispatch/journey/path/pattern.rb 55.45 % 197 110 61 49 54.65
lib/action_dispatch/journey/route.rb 70.10 % 193 97 68 29 103.03
lib/action_dispatch/journey/router.rb 31.03 % 148 87 27 60 0.94
lib/action_dispatch/journey/router/utils.rb 75.00 % 104 52 39 13 98.12
lib/action_dispatch/journey/routes.rb 78.26 % 80 46 36 10 73.78
lib/action_dispatch/journey/scanner.rb 85.29 % 70 34 29 5 498.47
lib/action_dispatch/journey/visitors.rb 71.81 % 265 149 107 42 945.26
lib/action_dispatch/middleware/actionable_exceptions.rb 63.16 % 39 19 12 7 2.53
lib/action_dispatch/middleware/callbacks.rb 58.82 % 34 17 10 7 2.71
lib/action_dispatch/middleware/cookies.rb 41.64 % 702 305 127 178 0.53
lib/action_dispatch/middleware/debug_exceptions.rb 29.03 % 182 93 27 66 2.05
lib/action_dispatch/middleware/debug_locks.rb 0.00 % 124 76 0 76 0.00
lib/action_dispatch/middleware/debug_view.rb 0.00 % 66 52 0 52 0.00
lib/action_dispatch/middleware/exception_wrapper.rb 37.04 % 191 81 30 51 0.37
lib/action_dispatch/middleware/executor.rb 0.00 % 21 17 0 17 0.00
lib/action_dispatch/middleware/flash.rb 38.76 % 300 129 50 79 0.68
lib/action_dispatch/middleware/host_authorization.rb 0.00 % 101 75 0 75 0.00
lib/action_dispatch/middleware/public_exceptions.rb 37.04 % 60 27 10 17 1.78
lib/action_dispatch/middleware/reloader.rb 0.00 % 12 4 0 4 0.00
lib/action_dispatch/middleware/remote_ip.rb 0.00 % 181 70 0 70 0.00
lib/action_dispatch/middleware/request_id.rb 0.00 % 43 26 0 26 0.00
lib/action_dispatch/middleware/session/abstract_store.rb 66.10 % 105 59 39 20 0.66
lib/action_dispatch/middleware/session/cache_store.rb 0.00 % 59 39 0 39 0.00
lib/action_dispatch/middleware/session/cookie_store.rb 44.68 % 122 47 21 26 0.45
lib/action_dispatch/middleware/session/mem_cache_store.rb 0.00 % 28 20 0 20 0.00
lib/action_dispatch/middleware/show_exceptions.rb 42.86 % 61 28 12 16 3.00
lib/action_dispatch/middleware/ssl.rb 0.00 % 153 83 0 83 0.00
lib/action_dispatch/middleware/stack.rb 68.48 % 170 92 63 29 32.86
lib/action_dispatch/middleware/static.rb 0.00 % 190 118 0 118 0.00
lib/action_dispatch/railtie.rb 0.00 % 61 51 0 51 0.00
lib/action_dispatch/request/session.rb 39.50 % 241 119 47 72 0.39
lib/action_dispatch/request/utils.rb 0.00 % 78 65 0 65 0.00
lib/action_dispatch/routing.rb 100.00 % 261 11 11 0 1.00
lib/action_dispatch/routing/endpoint.rb 90.00 % 17 10 9 1 0.90
lib/action_dispatch/routing/inspector.rb 39.26 % 273 135 53 82 0.39
lib/action_dispatch/routing/mapper.rb 86.29 % 2319 795 686 109 230.87
lib/action_dispatch/routing/polymorphic_routes.rb 35.38 % 350 130 46 84 0.68
lib/action_dispatch/routing/redirection.rb 51.81 % 201 83 43 40 3.31
lib/action_dispatch/routing/route_set.rb 57.08 % 894 473 270 203 26.62
lib/action_dispatch/routing/routes_proxy.rb 0.00 % 69 52 0 52 0.00
lib/action_dispatch/routing/url_for.rb 50.00 % 236 44 22 22 5.09
lib/action_dispatch/system_test_case.rb 66.67 % 190 45 30 15 0.84
lib/action_dispatch/system_testing/browser.rb 94.87 % 86 39 37 2 1.59
lib/action_dispatch/system_testing/driver.rb 56.76 % 67 37 21 16 1.32
lib/action_dispatch/system_testing/server.rb 100.00 % 31 16 16 0 1.00
lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb 42.11 % 138 57 24 33 0.42
lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb 53.85 % 29 13 7 6 0.54
lib/action_dispatch/testing/assertion_response.rb 0.00 % 46 32 0 32 0.00
lib/action_dispatch/testing/assertions.rb 76.92 % 24 13 10 3 0.77
lib/action_dispatch/testing/assertions/response.rb 34.21 % 104 38 13 25 0.34
lib/action_dispatch/testing/assertions/routing.rb 18.60 % 235 86 16 70 0.19
lib/action_dispatch/testing/integration.rb 45.37 % 685 205 93 112 0.60
lib/action_dispatch/testing/request_encoder.rb 75.86 % 55 29 22 7 0.76
lib/action_dispatch/testing/test_process.rb 41.38 % 75 29 12 17 0.41
lib/action_dispatch/testing/test_request.rb 54.29 % 71 35 19 16 0.54
lib/action_dispatch/testing/test_response.rb 0.00 % 25 14 0 14 0.00
lib/action_pack.rb 100.00 % 26 1 1 0 1.00
lib/action_pack/gem_version.rb 88.89 % 17 9 8 1 0.89
lib/action_pack/version.rb 75.00 % 10 4 3 1 0.75

lib/abstract_controller.rb

90.48% lines covered

21 relevant lines. 19 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_pack"
  3. 1 require "active_support"
  4. 1 require "active_support/rails"
  5. 1 require "active_support/i18n"
  6. 1 module AbstractController
  7. 1 extend ActiveSupport::Autoload
  8. 1 autoload :ActionNotFound, "abstract_controller/base"
  9. 1 autoload :Base
  10. 1 autoload :Caching
  11. 1 autoload :Callbacks
  12. 1 autoload :Collector
  13. 1 autoload :DoubleRenderError, "abstract_controller/rendering"
  14. 1 autoload :Helpers
  15. 1 autoload :Logger
  16. 1 autoload :Rendering
  17. 1 autoload :Translation
  18. 1 autoload :AssetPaths
  19. 1 autoload :UrlFor
  20. 1 def self.eager_load!
  21. super
  22. AbstractController::Caching.eager_load!
  23. end
  24. end

lib/abstract_controller/asset_paths.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module AbstractController
  3. 1 module AssetPaths #:nodoc:
  4. 1 extend ActiveSupport::Concern
  5. 1 included do
  6. 1 config_accessor :asset_host, :assets_dir, :javascripts_dir,
  7. :stylesheets_dir, :default_asset_host_protocol, :relative_url_root
  8. end
  9. end
  10. end

lib/abstract_controller/base.rb

58.43% lines covered

89 relevant lines. 52 lines covered and 37 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "abstract_controller/error"
  3. 1 require "active_support/configurable"
  4. 1 require "active_support/descendants_tracker"
  5. 1 require "active_support/core_ext/module/anonymous"
  6. 1 require "active_support/core_ext/module/attr_internal"
  7. 1 module AbstractController
  8. # Raised when a non-existing controller action is triggered.
  9. 1 class ActionNotFound < StandardError
  10. 1 attr_reader :controller, :action
  11. 1 def initialize(message = nil, controller = nil, action = nil)
  12. @controller = controller
  13. @action = action
  14. super(message)
  15. end
  16. 1 class Correction
  17. 1 def initialize(error)
  18. @error = error
  19. end
  20. 1 def corrections
  21. if @error.action
  22. maybe_these = @error.controller.class.action_methods
  23. maybe_these.sort_by { |n|
  24. DidYouMean::Jaro.distance(@error.action.to_s, n)
  25. }.reverse.first(4)
  26. else
  27. []
  28. end
  29. end
  30. end
  31. # We may not have DYM, and DYM might not let us register error handlers
  32. 1 if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
  33. DidYouMean.correct_error(self, Correction)
  34. end
  35. end
  36. # AbstractController::Base is a low-level API. Nobody should be
  37. # using it directly, and subclasses (like ActionController::Base) are
  38. # expected to provide their own +render+ method, since rendering means
  39. # different things depending on the context.
  40. 1 class Base
  41. ##
  42. # Returns the body of the HTTP response sent by the controller.
  43. 1 attr_internal :response_body
  44. ##
  45. # Returns the name of the action this controller is processing.
  46. 1 attr_internal :action_name
  47. ##
  48. # Returns the formats that can be processed by the controller.
  49. 1 attr_internal :formats
  50. 1 include ActiveSupport::Configurable
  51. 1 extend ActiveSupport::DescendantsTracker
  52. 1 class << self
  53. 1 attr_reader :abstract
  54. 1 alias_method :abstract?, :abstract
  55. # Define a controller as abstract. See internal_methods for more
  56. # details.
  57. 1 def abstract!
  58. 6 @abstract = true
  59. end
  60. 1 def inherited(klass) # :nodoc:
  61. # Define the abstract ivar on subclasses so that we don't get
  62. # uninitialized ivar warnings
  63. 314 unless klass.instance_variable_defined?(:@abstract)
  64. 314 klass.instance_variable_set(:@abstract, false)
  65. end
  66. 314 super
  67. end
  68. # A list of all internal methods for a controller. This finds the first
  69. # abstract superclass of a controller, and gets a list of all public
  70. # instance methods on that abstract class. Public instance methods of
  71. # a controller would normally be considered action methods, so methods
  72. # declared on abstract classes are being removed.
  73. # (<tt>ActionController::Metal</tt> and ActionController::Base are defined as abstract)
  74. 1 def internal_methods
  75. controller = self
  76. controller = controller.superclass until controller.abstract?
  77. controller.public_instance_methods(true)
  78. end
  79. # A list of method names that should be considered actions. This
  80. # includes all public instance methods on a controller, less
  81. # any internal methods (see internal_methods), adding back in
  82. # any methods that are internal, but still exist on the class
  83. # itself.
  84. #
  85. # ==== Returns
  86. # * <tt>Set</tt> - A set of all methods that should be considered actions.
  87. 1 def action_methods
  88. @action_methods ||= begin
  89. # All public instance methods of this class, including ancestors
  90. methods = (public_instance_methods(true) -
  91. # Except for public instance methods of Base and its ancestors
  92. internal_methods +
  93. # Be sure to include shadowed public instance methods of this class
  94. public_instance_methods(false))
  95. methods.map!(&:to_s)
  96. methods.to_set
  97. end
  98. end
  99. # action_methods are cached and there is sometimes a need to refresh
  100. # them. ::clear_action_methods! allows you to do that, so next time
  101. # you run action_methods, they will be recalculated.
  102. 1 def clear_action_methods!
  103. 1639 @action_methods = nil
  104. end
  105. # Returns the full controller name, underscored, without the ending Controller.
  106. #
  107. # class MyApp::MyPostsController < AbstractController::Base
  108. #
  109. # end
  110. #
  111. # MyApp::MyPostsController.controller_path # => "my_app/my_posts"
  112. #
  113. # ==== Returns
  114. # * <tt>String</tt>
  115. 1 def controller_path
  116. 556 @controller_path ||= name.delete_suffix("Controller").underscore unless anonymous?
  117. end
  118. # Refresh the cached action_methods when a new action_method is added.
  119. 1 def method_added(name)
  120. 1639 super
  121. 1639 clear_action_methods!
  122. end
  123. end
  124. 1 abstract!
  125. # Calls the action going through the entire action dispatch stack.
  126. #
  127. # The actual method that is called is determined by calling
  128. # #method_for_action. If no method can handle the action, then an
  129. # AbstractController::ActionNotFound error is raised.
  130. #
  131. # ==== Returns
  132. # * <tt>self</tt>
  133. 1 def process(action, *args)
  134. @_action_name = action.to_s
  135. unless action_name = _find_action_name(@_action_name)
  136. raise ActionNotFound.new("The action '#{action}' could not be found for #{self.class.name}", self, action)
  137. end
  138. @_response_body = nil
  139. process_action(action_name, *args)
  140. end
  141. # Delegates to the class' ::controller_path
  142. 1 def controller_path
  143. self.class.controller_path
  144. end
  145. # Delegates to the class' ::action_methods
  146. 1 def action_methods
  147. self.class.action_methods
  148. end
  149. # Returns true if a method for the action is available and
  150. # can be dispatched, false otherwise.
  151. #
  152. # Notice that <tt>action_methods.include?("foo")</tt> may return
  153. # false and <tt>available_action?("foo")</tt> returns true because
  154. # this method considers actions that are also available
  155. # through other means, for example, implicit render ones.
  156. #
  157. # ==== Parameters
  158. # * <tt>action_name</tt> - The name of an action to be tested
  159. 1 def available_action?(action_name)
  160. _find_action_name(action_name)
  161. end
  162. # Tests if a response body is set. Used to determine if the
  163. # +process_action+ callback needs to be terminated in
  164. # +AbstractController::Callbacks+.
  165. 1 def performed?
  166. response_body
  167. end
  168. # Returns true if the given controller is capable of rendering
  169. # a path. A subclass of +AbstractController::Base+
  170. # may return false. An Email controller for example does not
  171. # support paths, only full URLs.
  172. 1 def self.supports_path?
  173. true
  174. end
  175. 1 private
  176. # Returns true if the name can be considered an action because
  177. # it has a method defined in the controller.
  178. #
  179. # ==== Parameters
  180. # * <tt>name</tt> - The name of an action to be tested
  181. 1 def action_method?(name)
  182. self.class.action_methods.include?(name)
  183. end
  184. # Call the action. Override this in a subclass to modify the
  185. # behavior around processing an action. This, and not #process,
  186. # is the intended way to override action dispatching.
  187. #
  188. # Notice that the first argument is the method to be dispatched
  189. # which is *not* necessarily the same as the action name.
  190. 1 def process_action(method_name, *args)
  191. send_action(method_name, *args)
  192. end
  193. # Actually call the method associated with the action. Override
  194. # this method if you wish to change how action methods are called,
  195. # not to add additional behavior around it. For example, you would
  196. # override #send_action if you want to inject arguments into the
  197. # method.
  198. 1 alias send_action send
  199. # If the action name was not found, but a method called "action_missing"
  200. # was found, #method_for_action will return "_handle_action_missing".
  201. # This method calls #action_missing with the current action name.
  202. 1 def _handle_action_missing(*args)
  203. action_missing(@_action_name, *args)
  204. end
  205. # Takes an action name and returns the name of the method that will
  206. # handle the action.
  207. #
  208. # It checks if the action name is valid and returns false otherwise.
  209. #
  210. # See method_for_action for more information.
  211. #
  212. # ==== Parameters
  213. # * <tt>action_name</tt> - An action name to find a method name for
  214. #
  215. # ==== Returns
  216. # * <tt>string</tt> - The name of the method that handles the action
  217. # * false - No valid method name could be found.
  218. # Raise +AbstractController::ActionNotFound+.
  219. 1 def _find_action_name(action_name)
  220. _valid_action_name?(action_name) && method_for_action(action_name)
  221. end
  222. # Takes an action name and returns the name of the method that will
  223. # handle the action. In normal cases, this method returns the same
  224. # name as it receives. By default, if #method_for_action receives
  225. # a name that is not an action, it will look for an #action_missing
  226. # method and return "_handle_action_missing" if one is found.
  227. #
  228. # Subclasses may override this method to add additional conditions
  229. # that should be considered an action. For instance, an HTTP controller
  230. # with a template matching the action name is considered to exist.
  231. #
  232. # If you override this method to handle additional cases, you may
  233. # also provide a method (like +_handle_method_missing+) to handle
  234. # the case.
  235. #
  236. # If none of these conditions are true, and +method_for_action+
  237. # returns +nil+, an +AbstractController::ActionNotFound+ exception will be raised.
  238. #
  239. # ==== Parameters
  240. # * <tt>action_name</tt> - An action name to find a method name for
  241. #
  242. # ==== Returns
  243. # * <tt>string</tt> - The name of the method that handles the action
  244. # * <tt>nil</tt> - No method name could be found.
  245. 1 def method_for_action(action_name)
  246. if action_method?(action_name)
  247. action_name
  248. elsif respond_to?(:action_missing, true)
  249. "_handle_action_missing"
  250. end
  251. end
  252. # Checks if the action name is valid and returns false otherwise.
  253. 1 def _valid_action_name?(action_name)
  254. !action_name.to_s.include? File::SEPARATOR
  255. end
  256. end
  257. end

lib/abstract_controller/caching.rb

83.33% lines covered

36 relevant lines. 30 lines covered and 6 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module AbstractController
  3. 1 module Caching
  4. 1 extend ActiveSupport::Concern
  5. 1 extend ActiveSupport::Autoload
  6. 1 eager_autoload do
  7. 1 autoload :Fragments
  8. end
  9. 1 module ConfigMethods
  10. 1 def cache_store
  11. config.cache_store
  12. end
  13. 1 def cache_store=(store)
  14. 1 config.cache_store = ActiveSupport::Cache.lookup_store(*store)
  15. end
  16. 1 private
  17. 1 def cache_configured?
  18. perform_caching && cache_store
  19. end
  20. end
  21. 1 include ConfigMethods
  22. 1 include AbstractController::Caching::Fragments
  23. 1 included do
  24. 2 extend ConfigMethods
  25. 2 config_accessor :default_static_extension
  26. 2 self.default_static_extension ||= ".html"
  27. 2 config_accessor :perform_caching
  28. 2 self.perform_caching = true if perform_caching.nil?
  29. 2 config_accessor :enable_fragment_cache_logging
  30. 2 self.enable_fragment_cache_logging = false
  31. 2 class_attribute :_view_cache_dependencies, default: []
  32. 2 helper_method :view_cache_dependencies if respond_to?(:helper_method)
  33. end
  34. 1 module ClassMethods
  35. 1 def view_cache_dependency(&dependency)
  36. 2 self._view_cache_dependencies += [dependency]
  37. end
  38. end
  39. 1 def view_cache_dependencies
  40. self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact
  41. end
  42. 1 private
  43. # Convenience accessor.
  44. 1 def cache(key, options = {}, &block) # :doc:
  45. if cache_configured?
  46. cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block)
  47. else
  48. yield
  49. end
  50. end
  51. end
  52. end

lib/abstract_controller/caching/fragments.rb

39.58% lines covered

48 relevant lines. 19 lines covered and 29 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module AbstractController
  3. 1 module Caching
  4. # Fragment caching is used for caching various blocks within
  5. # views without caching the entire action as a whole. This is
  6. # useful when certain elements of an action change frequently or
  7. # depend on complicated state while other parts rarely change or
  8. # can be shared amongst multiple parties. The caching is done using
  9. # the +cache+ helper available in the Action View. See
  10. # ActionView::Helpers::CacheHelper for more information.
  11. #
  12. # While it's strongly recommended that you use key-based cache
  13. # expiration (see links in CacheHelper for more information),
  14. # it is also possible to manually expire caches. For example:
  15. #
  16. # expire_fragment('name_of_cache')
  17. 1 module Fragments
  18. 1 extend ActiveSupport::Concern
  19. 1 included do
  20. 2 if respond_to?(:class_attribute)
  21. 2 class_attribute :fragment_cache_keys
  22. else
  23. mattr_writer :fragment_cache_keys
  24. end
  25. 2 self.fragment_cache_keys = []
  26. 2 if respond_to?(:helper_method)
  27. 1 helper_method :combined_fragment_cache_key
  28. end
  29. end
  30. 1 module ClassMethods
  31. # Allows you to specify controller-wide key prefixes for
  32. # cache fragments. Pass either a constant +value+, or a block
  33. # which computes a value each time a cache key is generated.
  34. #
  35. # For example, you may want to prefix all fragment cache keys
  36. # with a global version identifier, so you can easily
  37. # invalidate all caches.
  38. #
  39. # class ApplicationController
  40. # fragment_cache_key "v1"
  41. # end
  42. #
  43. # When it's time to invalidate all fragments, simply change
  44. # the string constant. Or, progressively roll out the cache
  45. # invalidation using a computed value:
  46. #
  47. # class ApplicationController
  48. # fragment_cache_key do
  49. # @account.id.odd? ? "v1" : "v2"
  50. # end
  51. # end
  52. 1 def fragment_cache_key(value = nil, &key)
  53. 2 self.fragment_cache_keys += [key || -> { value }]
  54. end
  55. end
  56. # Given a key (as described in +expire_fragment+), returns
  57. # a key array suitable for use in reading, writing, or expiring a
  58. # cached fragment. All keys begin with <tt>:views</tt>,
  59. # followed by <tt>ENV["RAILS_CACHE_ID"]</tt> or <tt>ENV["RAILS_APP_VERSION"]</tt> if set,
  60. # followed by any controller-wide key prefix values, ending
  61. # with the specified +key+ value.
  62. 1 def combined_fragment_cache_key(key)
  63. head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) }
  64. tail = key.is_a?(Hash) ? url_for(key).split("://").last : key
  65. cache_key = [:views, ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"], head, tail]
  66. cache_key.flatten!(1)
  67. cache_key.compact!
  68. cache_key
  69. end
  70. # Writes +content+ to the location signified by
  71. # +key+ (see +expire_fragment+ for acceptable formats).
  72. 1 def write_fragment(key, content, options = nil)
  73. return content unless cache_configured?
  74. key = combined_fragment_cache_key(key)
  75. instrument_fragment_cache :write_fragment, key do
  76. content = content.to_str
  77. cache_store.write(key, content, options)
  78. end
  79. content
  80. end
  81. # Reads a cached fragment from the location signified by +key+
  82. # (see +expire_fragment+ for acceptable formats).
  83. 1 def read_fragment(key, options = nil)
  84. return unless cache_configured?
  85. key = combined_fragment_cache_key(key)
  86. instrument_fragment_cache :read_fragment, key do
  87. result = cache_store.read(key, options)
  88. result.respond_to?(:html_safe) ? result.html_safe : result
  89. end
  90. end
  91. # Check if a cached fragment from the location signified by
  92. # +key+ exists (see +expire_fragment+ for acceptable formats).
  93. 1 def fragment_exist?(key, options = nil)
  94. return unless cache_configured?
  95. key = combined_fragment_cache_key(key)
  96. instrument_fragment_cache :exist_fragment?, key do
  97. cache_store.exist?(key, options)
  98. end
  99. end
  100. # Removes fragments from the cache.
  101. #
  102. # +key+ can take one of three forms:
  103. #
  104. # * String - This would normally take the form of a path, like
  105. # <tt>pages/45/notes</tt>.
  106. # * Hash - Treated as an implicit call to +url_for+, like
  107. # <tt>{ controller: 'pages', action: 'notes', id: 45}</tt>
  108. # * Regexp - Will remove any fragment that matches, so
  109. # <tt>%r{pages/\d*/notes}</tt> might remove all notes. Make sure you
  110. # don't use anchors in the regex (<tt>^</tt> or <tt>$</tt>) because
  111. # the actual filename matched looks like
  112. # <tt>./cache/filename/path.cache</tt>. Note: Regexp expiration is
  113. # only supported on caches that can iterate over all keys (unlike
  114. # memcached).
  115. #
  116. # +options+ is passed through to the cache store's +delete+
  117. # method (or <tt>delete_matched</tt>, for Regexp keys).
  118. 1 def expire_fragment(key, options = nil)
  119. return unless cache_configured?
  120. key = combined_fragment_cache_key(key) unless key.is_a?(Regexp)
  121. instrument_fragment_cache :expire_fragment, key do
  122. if key.is_a?(Regexp)
  123. cache_store.delete_matched(key, options)
  124. else
  125. cache_store.delete(key, options)
  126. end
  127. end
  128. end
  129. 1 def instrument_fragment_cache(name, key) # :nodoc:
  130. ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key)) { yield }
  131. end
  132. end
  133. end
  134. end

lib/abstract_controller/callbacks.rb

91.67% lines covered

36 relevant lines. 33 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module AbstractController
  3. # = Abstract Controller Callbacks
  4. #
  5. # Abstract Controller provides hooks during the life cycle of a controller action.
  6. # Callbacks allow you to trigger logic during this cycle. Available callbacks are:
  7. #
  8. # * <tt>after_action</tt>
  9. # * <tt>append_after_action</tt>
  10. # * <tt>append_around_action</tt>
  11. # * <tt>append_before_action</tt>
  12. # * <tt>around_action</tt>
  13. # * <tt>before_action</tt>
  14. # * <tt>prepend_after_action</tt>
  15. # * <tt>prepend_around_action</tt>
  16. # * <tt>prepend_before_action</tt>
  17. # * <tt>skip_after_action</tt>
  18. # * <tt>skip_around_action</tt>
  19. # * <tt>skip_before_action</tt>
  20. #
  21. # NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
  22. #
  23. 1 module Callbacks
  24. 1 extend ActiveSupport::Concern
  25. # Uses ActiveSupport::Callbacks as the base functionality. For
  26. # more details on the whole callback system, read the documentation
  27. # for ActiveSupport::Callbacks.
  28. 1 include ActiveSupport::Callbacks
  29. 1 included do
  30. 3 define_callbacks :process_action,
  31. terminator: ->(controller, result_lambda) { result_lambda.call; controller.performed? },
  32. skip_after_callbacks_if_terminated: true
  33. end
  34. # Override <tt>AbstractController::Base#process_action</tt> to run the
  35. # <tt>process_action</tt> callbacks around the normal behavior.
  36. 1 def process_action(*)
  37. run_callbacks(:process_action) do
  38. super
  39. end
  40. end
  41. 1 module ClassMethods
  42. # If +:only+ or +:except+ are used, convert the options into the
  43. # +:if+ and +:unless+ options of ActiveSupport::Callbacks.
  44. #
  45. # The basic idea is that <tt>:only => :index</tt> gets converted to
  46. # <tt>:if => proc {|c| c.action_name == "index" }</tt>.
  47. #
  48. # Note that <tt>:only</tt> has priority over <tt>:if</tt> in case they
  49. # are used together.
  50. #
  51. # only: :index, if: -> { true } # the :if option will be ignored.
  52. #
  53. # Note that <tt>:if</tt> has priority over <tt>:except</tt> in case they
  54. # are used together.
  55. #
  56. # except: :index, if: -> { true } # the :except option will be ignored.
  57. #
  58. # ==== Options
  59. # * <tt>only</tt> - The callback should be run only for this action.
  60. # * <tt>except</tt> - The callback should be run for all actions except this action.
  61. 1 def _normalize_callback_options(options)
  62. 167 _normalize_callback_option(options, :only, :if)
  63. 167 _normalize_callback_option(options, :except, :unless)
  64. end
  65. 1 def _normalize_callback_option(options, from, to) # :nodoc:
  66. 334 if from = options.delete(from)
  67. 73 _from = Array(from).map(&:to_s).to_set
  68. 73 from = proc { |c| _from.include? c.action_name }
  69. 73 options[to] = Array(options[to]).unshift(from)
  70. end
  71. end
  72. # Take callback names and an optional callback proc, normalize them,
  73. # then call the block with each callback. This allows us to abstract
  74. # the normalization across several methods that use it.
  75. #
  76. # ==== Parameters
  77. # * <tt>callbacks</tt> - An array of callbacks, with an optional
  78. # options hash as the last parameter.
  79. # * <tt>block</tt> - A proc that should be added to the callbacks.
  80. #
  81. # ==== Block Parameters
  82. # * <tt>name</tt> - The callback to be added.
  83. # * <tt>options</tt> - A hash of options to be used when adding the callback.
  84. 1 def _insert_callbacks(callbacks, block = nil)
  85. 167 options = callbacks.extract_options!
  86. 167 _normalize_callback_options(options)
  87. 167 callbacks.push(block) if block
  88. 167 callbacks.each do |callback|
  89. 172 yield callback, options
  90. end
  91. end
  92. ##
  93. # :method: before_action
  94. #
  95. # :call-seq: before_action(names, block)
  96. #
  97. # Append a callback before actions. See _insert_callbacks for parameter details.
  98. #
  99. # If the callback renders or redirects, the action will not run. If there
  100. # are additional callbacks scheduled to run after that callback, they are
  101. # also cancelled.
  102. ##
  103. # :method: prepend_before_action
  104. #
  105. # :call-seq: prepend_before_action(names, block)
  106. #
  107. # Prepend a callback before actions. See _insert_callbacks for parameter details.
  108. #
  109. # If the callback renders or redirects, the action will not run. If there
  110. # are additional callbacks scheduled to run after that callback, they are
  111. # also cancelled.
  112. ##
  113. # :method: skip_before_action
  114. #
  115. # :call-seq: skip_before_action(names)
  116. #
  117. # Skip a callback before actions. See _insert_callbacks for parameter details.
  118. ##
  119. # :method: append_before_action
  120. #
  121. # :call-seq: append_before_action(names, block)
  122. #
  123. # Append a callback before actions. See _insert_callbacks for parameter details.
  124. #
  125. # If the callback renders or redirects, the action will not run. If there
  126. # are additional callbacks scheduled to run after that callback, they are
  127. # also cancelled.
  128. ##
  129. # :method: after_action
  130. #
  131. # :call-seq: after_action(names, block)
  132. #
  133. # Append a callback after actions. See _insert_callbacks for parameter details.
  134. ##
  135. # :method: prepend_after_action
  136. #
  137. # :call-seq: prepend_after_action(names, block)
  138. #
  139. # Prepend a callback after actions. See _insert_callbacks for parameter details.
  140. ##
  141. # :method: skip_after_action
  142. #
  143. # :call-seq: skip_after_action(names)
  144. #
  145. # Skip a callback after actions. See _insert_callbacks for parameter details.
  146. ##
  147. # :method: append_after_action
  148. #
  149. # :call-seq: append_after_action(names, block)
  150. #
  151. # Append a callback after actions. See _insert_callbacks for parameter details.
  152. ##
  153. # :method: around_action
  154. #
  155. # :call-seq: around_action(names, block)
  156. #
  157. # Append a callback around actions. See _insert_callbacks for parameter details.
  158. ##
  159. # :method: prepend_around_action
  160. #
  161. # :call-seq: prepend_around_action(names, block)
  162. #
  163. # Prepend a callback around actions. See _insert_callbacks for parameter details.
  164. ##
  165. # :method: skip_around_action
  166. #
  167. # :call-seq: skip_around_action(names)
  168. #
  169. # Skip a callback around actions. See _insert_callbacks for parameter details.
  170. ##
  171. # :method: append_around_action
  172. #
  173. # :call-seq: append_around_action(names, block)
  174. #
  175. # Append a callback around actions. See _insert_callbacks for parameter details.
  176. # set up before_action, prepend_before_action, skip_before_action, etc.
  177. # for each of before, after, and around.
  178. 1 [:before, :after, :around].each do |callback|
  179. 3 define_method "#{callback}_action" do |*names, &blk|
  180. 145 _insert_callbacks(names, blk) do |name, options|
  181. 150 set_callback(:process_action, callback, name, options)
  182. end
  183. end
  184. 3 define_method "prepend_#{callback}_action" do |*names, &blk|
  185. 6 _insert_callbacks(names, blk) do |name, options|
  186. 6 set_callback(:process_action, callback, name, options.merge(prepend: true))
  187. end
  188. end
  189. # Skip a before, after or around callback. See _insert_callbacks
  190. # for details on the allowed parameters.
  191. 3 define_method "skip_#{callback}_action" do |*names|
  192. 16 _insert_callbacks(names) do |name, options|
  193. 16 skip_callback(:process_action, callback, name, options)
  194. end
  195. end
  196. # *_action is the same as append_*_action
  197. 3 alias_method :"append_#{callback}_action", :"#{callback}_action"
  198. end
  199. end
  200. end
  201. end

lib/abstract_controller/collector.rb

61.11% lines covered

18 relevant lines. 11 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_dispatch/http/mime_type"
  3. 1 module AbstractController
  4. 1 module Collector
  5. 1 def self.generate_method_for_mime(mime)
  6. 34 sym = mime.is_a?(Symbol) ? mime : mime.to_sym
  7. 34 class_eval <<-RUBY, __FILE__, __LINE__ + 1
  8. def #{sym}(*args, &block)
  9. custom(Mime[:#{sym}], *args, &block)
  10. end
  11. RUBY
  12. end
  13. 1 Mime::SET.each do |mime|
  14. 34 generate_method_for_mime(mime)
  15. end
  16. 1 Mime::Type.register_callback do |mime|
  17. generate_method_for_mime(mime) unless instance_methods.include?(mime.to_sym)
  18. end
  19. 1 private
  20. 1 def method_missing(symbol, &block)
  21. unless mime_constant = Mime[symbol]
  22. raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \
  23. "https://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \
  24. "If you meant to respond to a variant like :tablet or :phone, not a custom format, " \
  25. "be sure to nest your variant response within a format response: " \
  26. "format.html { |html| html.tablet { ... } }"
  27. end
  28. if Mime::SET.include?(mime_constant)
  29. AbstractController::Collector.generate_method_for_mime(mime_constant)
  30. send(symbol, &block)
  31. else
  32. super
  33. end
  34. end
  35. end
  36. end

lib/abstract_controller/error.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module AbstractController
  3. 1 class Error < StandardError #:nodoc:
  4. end
  5. end

lib/abstract_controller/helpers.rb

87.72% lines covered

57 relevant lines. 50 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/dependencies"
  3. 1 module AbstractController
  4. 1 module Helpers
  5. 1 extend ActiveSupport::Concern
  6. 1 included do
  7. 2 class_attribute :_helpers, default: define_helpers_module(self)
  8. 2 class_attribute :_helper_methods, default: Array.new
  9. end
  10. 1 class MissingHelperError < LoadError
  11. 1 def initialize(error, path)
  12. @error = error
  13. @path = "helpers/#{path}.rb"
  14. set_backtrace error.backtrace
  15. if /^#{path}(\.rb)?$/.match?(error.path)
  16. super("Missing helper file helpers/%s.rb" % path)
  17. else
  18. raise error
  19. end
  20. end
  21. end
  22. 1 module ClassMethods
  23. # When a class is inherited, wrap its helper module in a new module.
  24. # This ensures that the parent class's module can be changed
  25. # independently of the child class's.
  26. 1 def inherited(klass)
  27. 272 helpers = _helpers
  28. 272 klass._helpers = define_helpers_module(klass, helpers)
  29. 540 klass.class_eval { default_helper_module! } unless klass.anonymous?
  30. 272 super
  31. end
  32. # Declare a controller method as a helper. For example, the following
  33. # makes the +current_user+ and +logged_in?+ controller methods available
  34. # to the view:
  35. # class ApplicationController < ActionController::Base
  36. # helper_method :current_user, :logged_in?
  37. #
  38. # def current_user
  39. # @current_user ||= User.find_by(id: session[:user])
  40. # end
  41. #
  42. # def logged_in?
  43. # current_user != nil
  44. # end
  45. # end
  46. #
  47. # In a view:
  48. # <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%>
  49. #
  50. # ==== Parameters
  51. # * <tt>method[, method]</tt> - A name or names of a method on the controller
  52. # to be made available on the view.
  53. 1 def helper_method(*methods)
  54. 22 methods.flatten!
  55. 22 self._helper_methods += methods
  56. 22 location = caller_locations(1, 1).first
  57. 22 file, line = location.path, location.lineno
  58. 22 methods.each do |method|
  59. 22 _helpers.class_eval <<-ruby_eval, file, line
  60. def #{method}(*args, &block) # def current_user(*args, &block)
  61. controller.send(:'#{method}', *args, &block) # controller.send(:'current_user', *args, &block)
  62. end # end
  63. ruby2_keywords(:'#{method}') if respond_to?(:ruby2_keywords, true)
  64. ruby_eval
  65. end
  66. end
  67. # Includes the given modules in the template class.
  68. #
  69. # Modules can be specified in different ways. All of the following calls
  70. # include +FooHelper+:
  71. #
  72. # # Module, recommended.
  73. # helper FooHelper
  74. #
  75. # # String/symbol without the "helper" suffix, camel or snake case.
  76. # helper "Foo"
  77. # helper :Foo
  78. # helper "foo"
  79. # helper :foo
  80. #
  81. # The last two assume that <tt>"foo".camelize</tt> returns "Foo".
  82. #
  83. # When strings or symbols are passed, the method finds the actual module
  84. # object using +String#constantize+. Therefore, if the module has not been
  85. # yet loaded, it has to be autoloadable, which is normally the case.
  86. #
  87. # Namespaces are supported. The following calls include +Foo::BarHelper+:
  88. #
  89. # # Module, recommended.
  90. # helper Foo::BarHelper
  91. #
  92. # # String/symbol without the "helper" suffix, camel or snake case.
  93. # helper "Foo::Bar"
  94. # helper :"Foo::Bar"
  95. # helper "foo/bar"
  96. # helper :"foo/bar"
  97. #
  98. # The last two assume that <tt>"foo/bar".camelize</tt> returns "Foo::Bar".
  99. #
  100. # The method accepts a block too. If present, the block is evaluated in
  101. # the context of the controller helper module. This simple call makes the
  102. # +wadus+ method available in templates of the enclosing controller:
  103. #
  104. # helper do
  105. # def wadus
  106. # "wadus"
  107. # end
  108. # end
  109. #
  110. # Furthermore, all the above styles can be mixed together:
  111. #
  112. # helper FooHelper, "woo", "bar/baz" do
  113. # def wadus
  114. # "wadus"
  115. # end
  116. # end
  117. #
  118. 1 def helper(*args, &block)
  119. 274 modules_for_helpers(args).each do |mod|
  120. 14 _helpers.include(mod)
  121. end
  122. 11 _helpers.module_eval(&block) if block_given?
  123. end
  124. # Clears up all existing helpers in this class, only keeping the helper
  125. # with the same name as this class.
  126. 1 def clear_helpers
  127. 1 inherited_helper_methods = _helper_methods
  128. 1 self._helpers = Module.new
  129. 1 self._helper_methods = Array.new
  130. 11 inherited_helper_methods.each { |meth| helper_method meth }
  131. 1 default_helper_module! unless anonymous?
  132. end
  133. # Given an array of values like the ones accepted by +helper+, this method
  134. # returns an array with the corresponding modules, in the same order.
  135. 1 def modules_for_helpers(modules_or_helper_prefixes)
  136. 274 modules_or_helper_prefixes.flatten.map! do |module_or_helper_prefix|
  137. 277 case module_or_helper_prefix
  138. when Module
  139. 1 module_or_helper_prefix
  140. when String, Symbol
  141. 276 helper_prefix = module_or_helper_prefix.to_s
  142. 276 helper_prefix = helper_prefix.camelize unless helper_prefix.start_with?(/[A-Z]/)
  143. 276 "#{helper_prefix}Helper".constantize
  144. else
  145. raise ArgumentError, "helper must be a String, Symbol, or Module"
  146. end
  147. end
  148. end
  149. 1 private
  150. 1 def define_helpers_module(klass, helpers = nil)
  151. # In some tests inherited is called explicitly. In that case, just
  152. # return the module from the first time it was defined
  153. 274 return klass.const_get(:HelperMethods) if klass.const_defined?(:HelperMethods, false)
  154. 274 mod = Module.new
  155. 274 klass.const_set(:HelperMethods, mod)
  156. 274 mod.include(helpers) if helpers
  157. 274 mod
  158. end
  159. 1 def default_helper_module!
  160. 269 helper_prefix = name.delete_suffix("Controller")
  161. 269 helper(helper_prefix)
  162. rescue NameError => e
  163. 263 raise unless e.missing_name?("#{helper_prefix}Helper")
  164. end
  165. end
  166. end
  167. end

lib/abstract_controller/logger.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/benchmarkable"
  3. 1 module AbstractController
  4. 1 module Logger #:nodoc:
  5. 1 extend ActiveSupport::Concern
  6. 1 included do
  7. 2 config_accessor :logger
  8. 2 include ActiveSupport::Benchmarkable
  9. end
  10. end
  11. end

lib/abstract_controller/railties/routes_helpers.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module AbstractController
  3. 1 module Railties
  4. 1 module RoutesHelpers
  5. 1 def self.with(routes, include_path_helpers = true)
  6. 3 Module.new do
  7. 3 define_method(:inherited) do |klass|
  8. 284 super(klass)
  9. 751 if namespace = klass.module_parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) }
  10. 1 klass.include(namespace.railtie_routes_url_helpers(include_path_helpers))
  11. else
  12. 283 klass.include(routes.url_helpers(include_path_helpers))
  13. end
  14. end
  15. end
  16. end
  17. end
  18. end
  19. end

lib/abstract_controller/rendering.rb

50.0% lines covered

56 relevant lines. 28 lines covered and 28 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "abstract_controller/error"
  3. 1 require "action_view"
  4. 1 require "action_view/view_paths"
  5. 1 require "set"
  6. 1 module AbstractController
  7. 1 class DoubleRenderError < Error
  8. 1 DEFAULT_MESSAGE = "Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like \"redirect_to(...) and return\"."
  9. 1 def initialize(message = nil)
  10. super(message || DEFAULT_MESSAGE)
  11. end
  12. end
  13. 1 module Rendering
  14. 1 extend ActiveSupport::Concern
  15. 1 include ActionView::ViewPaths
  16. # Normalizes arguments, options and then delegates render_to_body and
  17. # sticks the result in <tt>self.response_body</tt>.
  18. 1 def render(*args, &block)
  19. options = _normalize_render(*args, &block)
  20. rendered_body = render_to_body(options)
  21. if options[:html]
  22. _set_html_content_type
  23. else
  24. _set_rendered_content_type rendered_format
  25. end
  26. _set_vary_header
  27. self.response_body = rendered_body
  28. end
  29. # Raw rendering of a template to a string.
  30. #
  31. # It is similar to render, except that it does not
  32. # set the +response_body+ and it should be guaranteed
  33. # to always return a string.
  34. #
  35. # If a component extends the semantics of +response_body+
  36. # (as ActionController extends it to be anything that
  37. # responds to the method each), this method needs to be
  38. # overridden in order to still return a string.
  39. 1 def render_to_string(*args, &block)
  40. options = _normalize_render(*args, &block)
  41. render_to_body(options)
  42. end
  43. # Performs the actual template rendering.
  44. 1 def render_to_body(options = {})
  45. end
  46. # Returns Content-Type of rendered content.
  47. 1 def rendered_format
  48. Mime[:text]
  49. end
  50. 1 DEFAULT_PROTECTED_INSTANCE_VARIABLES = %i(@_action_name @_response_body @_formats @_prefixes)
  51. # This method should return a hash with assigns.
  52. # You can overwrite this configuration per controller.
  53. 1 def view_assigns
  54. variables = instance_variables - _protected_ivars
  55. variables.each_with_object({}) do |name, hash|
  56. hash[name.slice(1, name.length)] = instance_variable_get(name)
  57. end
  58. end
  59. 1 private
  60. # Normalize args by converting <tt>render "foo"</tt> to
  61. # <tt>render :action => "foo"</tt> and <tt>render "foo/bar"</tt> to
  62. # <tt>render :file => "foo/bar"</tt>.
  63. 1 def _normalize_args(action = nil, options = {}) # :doc:
  64. if action.respond_to?(:permitted?)
  65. if action.permitted?
  66. action
  67. else
  68. raise ArgumentError, "render parameters are not permitted"
  69. end
  70. elsif action.is_a?(Hash)
  71. action
  72. else
  73. options
  74. end
  75. end
  76. # Normalize options.
  77. 1 def _normalize_options(options) # :doc:
  78. options
  79. end
  80. # Process extra options.
  81. 1 def _process_options(options) # :doc:
  82. options
  83. end
  84. # Process the rendered format.
  85. 1 def _process_format(format) # :nodoc:
  86. end
  87. 1 def _process_variant(options)
  88. end
  89. 1 def _set_html_content_type # :nodoc:
  90. end
  91. 1 def _set_vary_header # :nodoc:
  92. end
  93. 1 def _set_rendered_content_type(format) # :nodoc:
  94. end
  95. # Normalize args and options.
  96. 1 def _normalize_render(*args, &block) # :nodoc:
  97. options = _normalize_args(*args, &block)
  98. _process_variant(options)
  99. _normalize_options(options)
  100. options
  101. end
  102. 1 def _protected_ivars
  103. DEFAULT_PROTECTED_INSTANCE_VARIABLES
  104. end
  105. end
  106. end

lib/abstract_controller/translation.rb

47.06% lines covered

17 relevant lines. 8 lines covered and 9 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/symbol/starts_ends_with"
  3. 1 module AbstractController
  4. 1 module Translation
  5. 1 mattr_accessor :raise_on_missing_translations, default: false
  6. # Delegates to <tt>I18n.translate</tt>. Also aliased as <tt>t</tt>.
  7. #
  8. # When the given key starts with a period, it will be scoped by the current
  9. # controller and action. So if you call <tt>translate(".foo")</tt> from
  10. # <tt>PeopleController#index</tt>, it will convert the call to
  11. # <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive
  12. # to translate many keys within the same controller / action and gives you a
  13. # simple framework for scoping them consistently.
  14. 1 def translate(key, **options)
  15. if key.start_with?(".")
  16. path = controller_path.tr("/", ".")
  17. defaults = [:"#{path}#{key}"]
  18. defaults << options[:default] if options[:default]
  19. options[:default] = defaults.flatten
  20. key = "#{path}.#{action_name}#{key}"
  21. end
  22. i18n_raise = options.fetch(:raise, self.raise_on_missing_translations)
  23. I18n.translate(key, **options, raise: i18n_raise)
  24. end
  25. 1 alias :t :translate
  26. # Delegates to <tt>I18n.localize</tt>. Also aliased as <tt>l</tt>.
  27. 1 def localize(object, **options)
  28. I18n.localize(object, **options)
  29. end
  30. 1 alias :l :localize
  31. end
  32. end

lib/abstract_controller/url_for.rb

61.54% lines covered

13 relevant lines. 8 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module AbstractController
  3. # Includes +url_for+ into the host class (e.g. an abstract controller or mailer). The class
  4. # has to provide a +RouteSet+ by implementing the <tt>_routes</tt> methods. Otherwise, an
  5. # exception will be raised.
  6. #
  7. # Note that this module is completely decoupled from HTTP - the only requirement is a valid
  8. # <tt>_routes</tt> implementation.
  9. 1 module UrlFor
  10. 1 extend ActiveSupport::Concern
  11. 1 include ActionDispatch::Routing::UrlFor
  12. 1 def _routes
  13. raise "In order to use #url_for, you must include routing helpers explicitly. " \
  14. "For instance, `include Rails.application.routes.url_helpers`."
  15. end
  16. 1 module ClassMethods
  17. 1 def _routes
  18. nil
  19. end
  20. 1 def action_methods
  21. @action_methods ||= begin
  22. if _routes
  23. super - _routes.named_routes.helper_names
  24. else
  25. super
  26. end
  27. end
  28. end
  29. end
  30. end
  31. end

lib/action_controller.rb

100.0% lines covered

53 relevant lines. 53 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "abstract_controller"
  3. 1 require "action_dispatch"
  4. 1 require "action_controller/metal/strong_parameters"
  5. 1 module ActionController
  6. 1 extend ActiveSupport::Autoload
  7. 1 autoload :API
  8. 1 autoload :Base
  9. 1 autoload :Metal
  10. 1 autoload :Renderer
  11. 1 autoload :FormBuilder
  12. 1 eager_autoload do
  13. 1 autoload :Caching
  14. end
  15. 1 autoload_under "metal" do
  16. 1 eager_autoload do
  17. 1 autoload :Live
  18. end
  19. 1 autoload :ConditionalGet
  20. 1 autoload :ContentSecurityPolicy
  21. 1 autoload :Cookies
  22. 1 autoload :DataStreaming
  23. 1 autoload :DefaultHeaders
  24. 1 autoload :EtagWithTemplateDigest
  25. 1 autoload :EtagWithFlash
  26. 1 autoload :FeaturePolicy
  27. 1 autoload :Flash
  28. 1 autoload :Head
  29. 1 autoload :Helpers
  30. 1 autoload :HttpAuthentication
  31. 1 autoload :BasicImplicitRender
  32. 1 autoload :ImplicitRender
  33. 1 autoload :Instrumentation
  34. 1 autoload :Logging
  35. 1 autoload :MimeResponds
  36. 1 autoload :ParamsWrapper
  37. 1 autoload :Redirecting
  38. 1 autoload :Renderers
  39. 1 autoload :Rendering
  40. 1 autoload :RequestForgeryProtection
  41. 1 autoload :Rescue
  42. 1 autoload :Streaming
  43. 1 autoload :StrongParameters
  44. 1 autoload :ParameterEncoding
  45. 1 autoload :Testing
  46. 1 autoload :UrlFor
  47. end
  48. 1 autoload_under "api" do
  49. 1 autoload :ApiRendering
  50. end
  51. 1 autoload :TestCase, "action_controller/test_case"
  52. 1 autoload :TemplateAssertions, "action_controller/test_case"
  53. end
  54. # Common Active Support usage in Action Controller
  55. 1 require "active_support/core_ext/module/attribute_accessors"
  56. 1 require "active_support/core_ext/load_error"
  57. 1 require "active_support/core_ext/module/attr_internal"
  58. 1 require "active_support/core_ext/name_error"
  59. 1 require "active_support/core_ext/uri"
  60. 1 require "active_support/inflector"

lib/action_controller/api.rb

80.0% lines covered

15 relevant lines. 12 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_view"
  3. 1 require "action_controller"
  4. 1 require "action_controller/log_subscriber"
  5. 1 module ActionController
  6. # API Controller is a lightweight version of <tt>ActionController::Base</tt>,
  7. # created for applications that don't require all functionalities that a complete
  8. # \Rails controller provides, allowing you to create controllers with just the
  9. # features that you need for API only applications.
  10. #
  11. # An API Controller is different from a normal controller in the sense that
  12. # by default it doesn't include a number of features that are usually required
  13. # by browser access only: layouts and templates rendering,
  14. # flash, assets, and so on. This makes the entire controller stack thinner,
  15. # suitable for API applications. It doesn't mean you won't have such
  16. # features if you need them: they're all available for you to include in
  17. # your application, they're just not part of the default API controller stack.
  18. #
  19. # Normally, +ApplicationController+ is the only controller that inherits from
  20. # <tt>ActionController::API</tt>. All other controllers in turn inherit from
  21. # +ApplicationController+.
  22. #
  23. # A sample controller could look like this:
  24. #
  25. # class PostsController < ApplicationController
  26. # def index
  27. # posts = Post.all
  28. # render json: posts
  29. # end
  30. # end
  31. #
  32. # Request, response, and parameters objects all work the exact same way as
  33. # <tt>ActionController::Base</tt>.
  34. #
  35. # == Renders
  36. #
  37. # The default API Controller stack includes all renderers, which means you
  38. # can use <tt>render :json</tt> and brothers freely in your controllers. Keep
  39. # in mind that templates are not going to be rendered, so you need to ensure
  40. # your controller is calling either <tt>render</tt> or <tt>redirect_to</tt> in
  41. # all actions, otherwise it will return 204 No Content.
  42. #
  43. # def show
  44. # post = Post.find(params[:id])
  45. # render json: post
  46. # end
  47. #
  48. # == Redirects
  49. #
  50. # Redirects are used to move from one action to another. You can use the
  51. # <tt>redirect_to</tt> method in your controllers in the same way as in
  52. # <tt>ActionController::Base</tt>. For example:
  53. #
  54. # def create
  55. # redirect_to root_url and return if not_authorized?
  56. # # do stuff here
  57. # end
  58. #
  59. # == Adding New Behavior
  60. #
  61. # In some scenarios you may want to add back some functionality provided by
  62. # <tt>ActionController::Base</tt> that is not present by default in
  63. # <tt>ActionController::API</tt>, for instance <tt>MimeResponds</tt>. This
  64. # module gives you the <tt>respond_to</tt> method. Adding it is quite simple,
  65. # you just need to include the module in a specific controller or in
  66. # +ApplicationController+ in case you want it available in your entire
  67. # application:
  68. #
  69. # class ApplicationController < ActionController::API
  70. # include ActionController::MimeResponds
  71. # end
  72. #
  73. # class PostsController < ApplicationController
  74. # def index
  75. # posts = Post.all
  76. #
  77. # respond_to do |format|
  78. # format.json { render json: posts }
  79. # format.xml { render xml: posts }
  80. # end
  81. # end
  82. # end
  83. #
  84. # Make sure to check the modules included in <tt>ActionController::Base</tt>
  85. # if you want to use any other functionality that is not provided
  86. # by <tt>ActionController::API</tt> out of the box.
  87. 1 class API < Metal
  88. 1 abstract!
  89. # Shortcut helper that returns all the ActionController::API modules except
  90. # the ones passed as arguments:
  91. #
  92. # class MyAPIBaseController < ActionController::Metal
  93. # ActionController::API.without_modules(:UrlFor).each do |left|
  94. # include left
  95. # end
  96. # end
  97. #
  98. # This gives better control over what you want to exclude and makes it easier
  99. # to create an API controller class, instead of listing the modules required
  100. # manually.
  101. 1 def self.without_modules(*modules)
  102. modules = modules.map do |m|
  103. m.is_a?(Symbol) ? ActionController.const_get(m) : m
  104. end
  105. MODULES - modules
  106. end
  107. 1 MODULES = [
  108. AbstractController::Rendering,
  109. UrlFor,
  110. Redirecting,
  111. ApiRendering,
  112. Renderers::All,
  113. ConditionalGet,
  114. BasicImplicitRender,
  115. StrongParameters,
  116. DataStreaming,
  117. DefaultHeaders,
  118. Logging,
  119. # Before callbacks should also be executed as early as possible, so
  120. # also include them at the bottom.
  121. AbstractController::Callbacks,
  122. # Append rescue at the bottom to wrap as much as possible.
  123. Rescue,
  124. # Add instrumentations hooks at the bottom, to ensure they instrument
  125. # all the methods properly.
  126. Instrumentation,
  127. # Params wrapper should come before instrumentation so they are
  128. # properly showed in logs
  129. ParamsWrapper
  130. ]
  131. 1 MODULES.each do |mod|
  132. 15 include mod
  133. end
  134. 1 ActiveSupport.run_load_hooks(:action_controller_api, self)
  135. 1 ActiveSupport.run_load_hooks(:action_controller, self)
  136. end
  137. end

lib/action_controller/api/api_rendering.rb

75.0% lines covered

8 relevant lines. 6 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. 1 module ApiRendering
  4. 1 extend ActiveSupport::Concern
  5. 1 included do
  6. 1 include Rendering
  7. end
  8. 1 def render_to_body(options = {})
  9. _process_options(options)
  10. super
  11. end
  12. end
  13. end

lib/action_controller/base.rb

80.0% lines covered

20 relevant lines. 16 lines covered and 4 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_view"
  3. 1 require "action_controller/log_subscriber"
  4. 1 require "action_controller/metal/params_wrapper"
  5. 1 module ActionController
  6. # Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed
  7. # on request and then either it renders a template or redirects to another action. An action is defined as a public method
  8. # on the controller, which will automatically be made accessible to the web-server through \Rails Routes.
  9. #
  10. # By default, only the ApplicationController in a \Rails application inherits from <tt>ActionController::Base</tt>. All other
  11. # controllers inherit from ApplicationController. This gives you one class to configure things such as
  12. # request forgery protection and filtering of sensitive request parameters.
  13. #
  14. # A sample controller could look like this:
  15. #
  16. # class PostsController < ApplicationController
  17. # def index
  18. # @posts = Post.all
  19. # end
  20. #
  21. # def create
  22. # @post = Post.create params[:post]
  23. # redirect_to posts_path
  24. # end
  25. # end
  26. #
  27. # Actions, by default, render a template in the <tt>app/views</tt> directory corresponding to the name of the controller and action
  28. # after executing code in the action. For example, the +index+ action of the PostsController would render the
  29. # template <tt>app/views/posts/index.html.erb</tt> by default after populating the <tt>@posts</tt> instance variable.
  30. #
  31. # Unlike index, the create action will not render a template. After performing its main purpose (creating a
  32. # new post), it initiates a redirect instead. This redirect works by returning an external
  33. # <tt>302 Moved</tt> HTTP response that takes the user to the index action.
  34. #
  35. # These two methods represent the two basic action archetypes used in Action Controllers: Get-and-show and do-and-redirect.
  36. # Most actions are variations on these themes.
  37. #
  38. # == Requests
  39. #
  40. # For every request, the router determines the value of the +controller+ and +action+ keys. These determine which controller
  41. # and action are called. The remaining request parameters, the session (if one is available), and the full request with
  42. # all the HTTP headers are made available to the action through accessor methods. Then the action is performed.
  43. #
  44. # The full request object is available via the request accessor and is primarily used to query for HTTP headers:
  45. #
  46. # def server_ip
  47. # location = request.env["REMOTE_ADDR"]
  48. # render plain: "This server hosted at #{location}"
  49. # end
  50. #
  51. # == Parameters
  52. #
  53. # All request parameters, whether they come from a query string in the URL or form data submitted through a POST request are
  54. # available through the <tt>params</tt> method which returns a hash. For example, an action that was performed through
  55. # <tt>/posts?category=All&limit=5</tt> will include <tt>{ "category" => "All", "limit" => "5" }</tt> in <tt>params</tt>.
  56. #
  57. # It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as:
  58. #
  59. # <input type="text" name="post[name]" value="david">
  60. # <input type="text" name="post[address]" value="hyacintvej">
  61. #
  62. # A request coming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>.
  63. # If the address input had been named <tt>post[address][street]</tt>, the <tt>params</tt> would have included
  64. # <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting.
  65. #
  66. # == Sessions
  67. #
  68. # Sessions allow you to store objects in between requests. This is useful for objects that are not yet ready to be persisted,
  69. # such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such
  70. # as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely
  71. # they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at.
  72. #
  73. # You can place objects in the session by using the <tt>session</tt> method, which accesses a hash:
  74. #
  75. # session[:person] = Person.authenticate(user_name, password)
  76. #
  77. # You can retrieve it again through the same hash:
  78. #
  79. # "Hello #{session[:person]}"
  80. #
  81. # For removing objects from the session, you can either assign a single key to +nil+:
  82. #
  83. # # removes :person from session
  84. # session[:person] = nil
  85. #
  86. # or you can remove the entire session with +reset_session+.
  87. #
  88. # Sessions are stored by default in a browser cookie that's cryptographically signed, but unencrypted.
  89. # This prevents the user from tampering with the session but also allows them to see its contents.
  90. #
  91. # Do not put secret information in cookie-based sessions!
  92. #
  93. # == Responses
  94. #
  95. # Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response
  96. # object is generated automatically through the use of renders and redirects and requires no user intervention.
  97. #
  98. # == Renders
  99. #
  100. # Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering
  101. # of a template. Included in the Action Pack is the Action View, which enables rendering of ERB templates. It's automatically configured.
  102. # The controller passes objects to the view by assigning instance variables:
  103. #
  104. # def show
  105. # @post = Post.find(params[:id])
  106. # end
  107. #
  108. # Which are then automatically available to the view:
  109. #
  110. # Title: <%= @post.title %>
  111. #
  112. # You don't have to rely on the automated rendering. For example, actions that could result in the rendering of different templates
  113. # will use the manual rendering methods:
  114. #
  115. # def search
  116. # @results = Search.find(params[:query])
  117. # case @results.count
  118. # when 0 then render action: "no_results"
  119. # when 1 then render action: "show"
  120. # when 2..10 then render action: "show_many"
  121. # end
  122. # end
  123. #
  124. # Read more about writing ERB and Builder templates in ActionView::Base.
  125. #
  126. # == Redirects
  127. #
  128. # Redirects are used to move from one action to another. For example, after a <tt>create</tt> action, which stores a blog entry to the
  129. # database, we might like to show the user the new entry. Because we're following good DRY principles (Don't Repeat Yourself), we're
  130. # going to reuse (and redirect to) a <tt>show</tt> action that we'll assume has already been created. The code might look like this:
  131. #
  132. # def create
  133. # @entry = Entry.new(params[:entry])
  134. # if @entry.save
  135. # # The entry was saved correctly, redirect to show
  136. # redirect_to action: 'show', id: @entry.id
  137. # else
  138. # # things didn't go so well, do something else
  139. # end
  140. # end
  141. #
  142. # In this case, after saving our new entry to the database, the user is redirected to the <tt>show</tt> method, which is then executed.
  143. # Note that this is an external HTTP-level redirection which will cause the browser to make a second request (a GET to the show action),
  144. # and not some internal re-routing which calls both "create" and then "show" within one request.
  145. #
  146. # Learn more about <tt>redirect_to</tt> and what options you have in ActionController::Redirecting.
  147. #
  148. # == Calling multiple redirects or renders
  149. #
  150. # An action may contain only a single render or a single redirect. Attempting to try to do either again will result in a DoubleRenderError:
  151. #
  152. # def do_something
  153. # redirect_to action: "elsewhere"
  154. # render action: "overthere" # raises DoubleRenderError
  155. # end
  156. #
  157. # If you need to redirect on the condition of something, then be sure to add "and return" to halt execution.
  158. #
  159. # def do_something
  160. # redirect_to(action: "elsewhere") and return if monkeys.nil?
  161. # render action: "overthere" # won't be called if monkeys is nil
  162. # end
  163. #
  164. 1 class Base < Metal
  165. 1 abstract!
  166. # We document the request and response methods here because albeit they are
  167. # implemented in ActionController::Metal, the type of the returned objects
  168. # is unknown at that level.
  169. ##
  170. # :method: request
  171. #
  172. # Returns an ActionDispatch::Request instance that represents the
  173. # current request.
  174. ##
  175. # :method: response
  176. #
  177. # Returns an ActionDispatch::Response that represents the current
  178. # response.
  179. # Shortcut helper that returns all the modules included in
  180. # ActionController::Base except the ones passed as arguments:
  181. #
  182. # class MyBaseController < ActionController::Metal
  183. # ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left|
  184. # include left
  185. # end
  186. # end
  187. #
  188. # This gives better control over what you want to exclude and makes it
  189. # easier to create a bare controller class, instead of listing the modules
  190. # required manually.
  191. 1 def self.without_modules(*modules)
  192. modules = modules.map do |m|
  193. m.is_a?(Symbol) ? ActionController.const_get(m) : m
  194. end
  195. MODULES - modules
  196. end
  197. 1 MODULES = [
  198. AbstractController::Rendering,
  199. AbstractController::Translation,
  200. AbstractController::AssetPaths,
  201. Helpers,
  202. UrlFor,
  203. Redirecting,
  204. ActionView::Layouts,
  205. Rendering,
  206. Renderers::All,
  207. ConditionalGet,
  208. EtagWithTemplateDigest,
  209. EtagWithFlash,
  210. Caching,
  211. MimeResponds,
  212. ImplicitRender,
  213. StrongParameters,
  214. ParameterEncoding,
  215. Cookies,
  216. Flash,
  217. FormBuilder,
  218. RequestForgeryProtection,
  219. ContentSecurityPolicy,
  220. FeaturePolicy,
  221. Streaming,
  222. DataStreaming,
  223. HttpAuthentication::Basic::ControllerMethods,
  224. HttpAuthentication::Digest::ControllerMethods,
  225. HttpAuthentication::Token::ControllerMethods,
  226. DefaultHeaders,
  227. Logging,
  228. # Before callbacks should also be executed as early as possible, so
  229. # also include them at the bottom.
  230. AbstractController::Callbacks,
  231. # Append rescue at the bottom to wrap as much as possible.
  232. Rescue,
  233. # Add instrumentations hooks at the bottom, to ensure they instrument
  234. # all the methods properly.
  235. Instrumentation,
  236. # Params wrapper should come before instrumentation so they are
  237. # properly showed in logs
  238. ParamsWrapper
  239. ]
  240. 1 MODULES.each do |mod|
  241. 34 include mod
  242. end
  243. 1 setup_renderer!
  244. # Define some internal variables that should not be propagated to the view.
  245. 1 PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + %i(
  246. @_params @_response @_request @_config @_url_options @_action_has_layout @_view_context_class
  247. @_view_renderer @_lookup_context @_routes @_view_runtime @_db_runtime @_helper_proxy
  248. )
  249. 1 def _protected_ivars
  250. PROTECTED_IVARS
  251. end
  252. 1 private :_protected_ivars
  253. 1 ActiveSupport.run_load_hooks(:action_controller_base, self)
  254. 1 ActiveSupport.run_load_hooks(:action_controller, self)
  255. end
  256. end

lib/action_controller/caching.rb

80.0% lines covered

10 relevant lines. 8 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. # \Caching is a cheap way of speeding up slow applications by keeping the result of
  4. # calculations, renderings, and database calls around for subsequent requests.
  5. #
  6. # You can read more about each approach by clicking the modules below.
  7. #
  8. # Note: To turn off all caching provided by Action Controller, set
  9. # config.action_controller.perform_caching = false
  10. #
  11. # == \Caching stores
  12. #
  13. # All the caching stores from ActiveSupport::Cache are available to be used as backends
  14. # for Action Controller caching.
  15. #
  16. # Configuration examples (FileStore is the default):
  17. #
  18. # config.action_controller.cache_store = :memory_store
  19. # config.action_controller.cache_store = :file_store, '/path/to/cache/directory'
  20. # config.action_controller.cache_store = :mem_cache_store, 'localhost'
  21. # config.action_controller.cache_store = :mem_cache_store, Memcached::Rails.new('localhost:11211')
  22. # config.action_controller.cache_store = MyOwnStore.new('parameter')
  23. 1 module Caching
  24. 1 extend ActiveSupport::Concern
  25. 1 included do
  26. 2 include AbstractController::Caching
  27. end
  28. 1 private
  29. 1 def instrument_payload(key)
  30. {
  31. controller: controller_name,
  32. action: action_name,
  33. key: key
  34. }
  35. end
  36. 1 def instrument_name
  37. "action_controller"
  38. end
  39. end
  40. end

lib/action_controller/form_builder.rb

90.0% lines covered

10 relevant lines. 9 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. # Override the default form builder for all views rendered by this
  4. # controller and any of its descendants. Accepts a subclass of
  5. # +ActionView::Helpers::FormBuilder+.
  6. #
  7. # For example, given a form builder:
  8. #
  9. # class AdminFormBuilder < ActionView::Helpers::FormBuilder
  10. # def special_field(name)
  11. # end
  12. # end
  13. #
  14. # The controller specifies a form builder as its default:
  15. #
  16. # class AdminAreaController < ApplicationController
  17. # default_form_builder AdminFormBuilder
  18. # end
  19. #
  20. # Then in the view any form using +form_for+ will be an instance of the
  21. # specified form builder:
  22. #
  23. # <%= form_for(@instance) do |builder| %>
  24. # <%= builder.special_field(:name) %>
  25. # <% end %>
  26. 1 module FormBuilder
  27. 1 extend ActiveSupport::Concern
  28. 1 included do
  29. 1 class_attribute :_default_form_builder, instance_accessor: false
  30. end
  31. 1 module ClassMethods
  32. # Set the form builder to be used as the default for all forms
  33. # in the views rendered by this controller and its subclasses.
  34. #
  35. # ==== Parameters
  36. # * <tt>builder</tt> - Default form builder, an instance of +ActionView::Helpers::FormBuilder+
  37. 1 def default_form_builder(builder)
  38. 1 self._default_form_builder = builder
  39. end
  40. end
  41. # Default form builder for the controller
  42. 1 def default_form_builder
  43. self.class._default_form_builder
  44. end
  45. end
  46. end

lib/action_controller/log_subscriber.rb

34.15% lines covered

41 relevant lines. 14 lines covered and 27 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. 1 class LogSubscriber < ActiveSupport::LogSubscriber
  4. 1 INTERNAL_PARAMS = %w(controller action format _method only_path)
  5. 1 def start_processing(event)
  6. return unless logger.info?
  7. payload = event.payload
  8. params = payload[:params].except(*INTERNAL_PARAMS)
  9. format = payload[:format]
  10. format = format.to_s.upcase if format.is_a?(Symbol)
  11. format = "*/*" if format.nil?
  12. info "Processing by #{payload[:controller]}##{payload[:action]} as #{format}"
  13. info " Parameters: #{params.inspect}" unless params.empty?
  14. end
  15. 1 def process_action(event)
  16. info do
  17. payload = event.payload
  18. additions = ActionController::Base.log_process_action(payload)
  19. status = payload[:status]
  20. if status.nil? && (exception_class_name = payload[:exception].first)
  21. status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
  22. end
  23. additions << "Allocations: #{event.allocations}"
  24. message = +"Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
  25. message << " (#{additions.join(" | ")})"
  26. message << "\n\n" if defined?(Rails.env) && Rails.env.development?
  27. message
  28. end
  29. end
  30. 1 def halted_callback(event)
  31. info { "Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected" }
  32. end
  33. 1 def send_file(event)
  34. info { "Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)" }
  35. end
  36. 1 def redirect_to(event)
  37. info { "Redirected to #{event.payload[:location]}" }
  38. end
  39. 1 def send_data(event)
  40. info { "Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)" }
  41. end
  42. 1 def unpermitted_parameters(event)
  43. debug do
  44. unpermitted_keys = event.payload[:keys]
  45. color("Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.map { |e| ":#{e}" }.join(", ")}", RED)
  46. end
  47. end
  48. %w(write_fragment read_fragment exist_fragment?
  49. 1 expire_fragment expire_page write_page).each do |method|
  50. 6 class_eval <<-METHOD, __FILE__, __LINE__ + 1
  51. def #{method}(event)
  52. return unless logger.info? && ActionController::Base.enable_fragment_cache_logging
  53. key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path])
  54. human_name = #{method.to_s.humanize.inspect}
  55. info("\#{human_name} \#{key} (\#{event.duration.round(1)}ms)")
  56. end
  57. METHOD
  58. end
  59. 1 def logger
  60. ActionController::Base.logger
  61. end
  62. end
  63. end
  64. 1 ActionController::LogSubscriber.attach_to :action_controller

lib/action_controller/metal.rb

63.21% lines covered

106 relevant lines. 67 lines covered and 39 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/array/extract_options"
  3. 1 require "action_dispatch/middleware/stack"
  4. 1 require "action_dispatch/http/request"
  5. 1 require "action_dispatch/http/response"
  6. 1 module ActionController
  7. # Extend ActionDispatch middleware stack to make it aware of options
  8. # allowing the following syntax in controllers:
  9. #
  10. # class PostsController < ApplicationController
  11. # use AuthenticationMiddleware, except: [:index, :show]
  12. # end
  13. #
  14. 1 class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc:
  15. 1 class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc:
  16. 1 def initialize(klass, args, actions, strategy, block)
  17. 7 @actions = actions
  18. 7 @strategy = strategy
  19. 7 super(klass, args, block)
  20. end
  21. 1 def valid?(action)
  22. @strategy.call @actions, action
  23. end
  24. end
  25. 1 def build(action, app = nil, &block)
  26. action = action.to_s
  27. middlewares.reverse.inject(app || block) do |a, middleware|
  28. middleware.valid?(action) ? middleware.build(a) : a
  29. end
  30. end
  31. 1 private
  32. 1 INCLUDE = ->(list, action) { list.include? action }
  33. 1 EXCLUDE = ->(list, action) { !list.include? action }
  34. 1 NULL = ->(list, action) { true }
  35. 1 def build_middleware(klass, args, block)
  36. 7 options = args.extract_options!
  37. 7 only = Array(options.delete(:only)).map(&:to_s)
  38. 7 except = Array(options.delete(:except)).map(&:to_s)
  39. 7 args << options unless options.empty?
  40. 7 strategy = NULL
  41. 7 list = nil
  42. 7 if only.any?
  43. 1 strategy = INCLUDE
  44. 1 list = only
  45. 6 elsif except.any?
  46. 1 strategy = EXCLUDE
  47. 1 list = except
  48. end
  49. 7 Middleware.new(klass, args, list, strategy, block)
  50. end
  51. end
  52. # <tt>ActionController::Metal</tt> is the simplest possible controller, providing a
  53. # valid Rack interface without the additional niceties provided by
  54. # <tt>ActionController::Base</tt>.
  55. #
  56. # A sample metal controller might look like this:
  57. #
  58. # class HelloController < ActionController::Metal
  59. # def index
  60. # self.response_body = "Hello World!"
  61. # end
  62. # end
  63. #
  64. # And then to route requests to your metal controller, you would add
  65. # something like this to <tt>config/routes.rb</tt>:
  66. #
  67. # get 'hello', to: HelloController.action(:index)
  68. #
  69. # The +action+ method returns a valid Rack application for the \Rails
  70. # router to dispatch to.
  71. #
  72. # == Rendering Helpers
  73. #
  74. # <tt>ActionController::Metal</tt> by default provides no utilities for rendering
  75. # views, partials, or other responses aside from explicitly calling of
  76. # <tt>response_body=</tt>, <tt>content_type=</tt>, and <tt>status=</tt>. To
  77. # add the render helpers you're used to having in a normal controller, you
  78. # can do the following:
  79. #
  80. # class HelloController < ActionController::Metal
  81. # include AbstractController::Rendering
  82. # include ActionView::Layouts
  83. # append_view_path "#{Rails.root}/app/views"
  84. #
  85. # def index
  86. # render "hello/index"
  87. # end
  88. # end
  89. #
  90. # == Redirection Helpers
  91. #
  92. # To add redirection helpers to your metal controller, do the following:
  93. #
  94. # class HelloController < ActionController::Metal
  95. # include ActionController::Redirecting
  96. # include Rails.application.routes.url_helpers
  97. #
  98. # def index
  99. # redirect_to root_url
  100. # end
  101. # end
  102. #
  103. # == Other Helpers
  104. #
  105. # You can refer to the modules included in <tt>ActionController::Base</tt> to see
  106. # other features you can bring into your metal controller.
  107. #
  108. 1 class Metal < AbstractController::Base
  109. 1 abstract!
  110. # Returns the last part of the controller's name, underscored, without the ending
  111. # <tt>Controller</tt>. For instance, PostsController returns <tt>posts</tt>.
  112. # Namespaces are left out, so Admin::PostsController returns <tt>posts</tt> as well.
  113. #
  114. # ==== Returns
  115. # * <tt>string</tt>
  116. 1 def self.controller_name
  117. @controller_name ||= name.demodulize.delete_suffix("Controller").underscore
  118. end
  119. 1 def self.make_response!(request)
  120. ActionDispatch::Response.new.tap do |res|
  121. res.request = request
  122. end
  123. end
  124. 1 def self.binary_params_for?(action) # :nodoc:
  125. false
  126. end
  127. # Delegates to the class' <tt>controller_name</tt>.
  128. 1 def controller_name
  129. self.class.controller_name
  130. end
  131. 1 attr_internal :response, :request
  132. 1 delegate :session, to: "@_request"
  133. 1 delegate :headers, :status=, :location=, :content_type=,
  134. :status, :location, :content_type, :media_type, to: "@_response"
  135. 1 def initialize
  136. @_request = nil
  137. @_response = nil
  138. @_routes = nil
  139. super
  140. end
  141. 1 def params
  142. @_params ||= request.parameters
  143. end
  144. 1 def params=(val)
  145. @_params = val
  146. end
  147. 1 alias :response_code :status # :nodoc:
  148. # Basic url_for that can be overridden for more robust functionality.
  149. 1 def url_for(string)
  150. string
  151. end
  152. 1 def response_body=(body)
  153. body = [body] unless body.nil? || body.respond_to?(:each)
  154. response.reset_body!
  155. return unless body
  156. response.body = body
  157. super
  158. end
  159. # Tests if render or redirect has already happened.
  160. 1 def performed?
  161. response_body || response.committed?
  162. end
  163. 1 def dispatch(name, request, response) #:nodoc:
  164. set_request!(request)
  165. set_response!(response)
  166. process(name)
  167. request.commit_flash
  168. to_a
  169. end
  170. 1 def set_response!(response) # :nodoc:
  171. @_response = response
  172. end
  173. 1 def set_request!(request) #:nodoc:
  174. @_request = request
  175. @_request.controller_instance = self
  176. end
  177. 1 def to_a #:nodoc:
  178. response.to_a
  179. end
  180. 1 def reset_session
  181. @_request.reset_session
  182. end
  183. 1 class_attribute :middleware_stack, default: ActionController::MiddlewareStack.new
  184. 1 def self.inherited(base) # :nodoc:
  185. 301 base.middleware_stack = middleware_stack.dup
  186. 301 super
  187. end
  188. 1 class << self
  189. # Pushes the given Rack middleware and its arguments to the bottom of the
  190. # middleware stack.
  191. 1 def use(*args, &block)
  192. 5 middleware_stack.use(*args, &block)
  193. end
  194. 1 ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
  195. end
  196. # Alias for +middleware_stack+.
  197. 1 def self.middleware
  198. 2 middleware_stack
  199. end
  200. # Returns a Rack endpoint for the given action name.
  201. 1 def self.action(name)
  202. 1 app = lambda { |env|
  203. req = ActionDispatch::Request.new(env)
  204. res = make_response! req
  205. new.dispatch(name, req, res)
  206. }
  207. 1 if middleware_stack.any?
  208. middleware_stack.build(name, app)
  209. else
  210. 1 app
  211. end
  212. end
  213. # Direct dispatch to the controller. Instantiates the controller, then
  214. # executes the action named +name+.
  215. 1 def self.dispatch(name, req, res)
  216. if middleware_stack.any?
  217. middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env
  218. else
  219. new.dispatch(name, req, res)
  220. end
  221. end
  222. end
  223. end

lib/action_controller/metal/basic_implicit_render.rb

66.67% lines covered

6 relevant lines. 4 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. 1 module BasicImplicitRender # :nodoc:
  4. 1 def send_action(method, *args)
  5. super.tap { default_render unless performed? }
  6. end
  7. 1 def default_render
  8. head :no_content
  9. end
  10. end
  11. end

lib/action_controller/metal/conditional_get.rb

48.65% lines covered

37 relevant lines. 18 lines covered and 19 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/object/try"
  3. 1 require "active_support/core_ext/integer/time"
  4. 1 module ActionController
  5. 1 module ConditionalGet
  6. 1 extend ActiveSupport::Concern
  7. 1 include Head
  8. 1 included do
  9. 2 class_attribute :etaggers, default: []
  10. end
  11. 1 module ClassMethods
  12. # Allows you to consider additional controller-wide information when generating an ETag.
  13. # For example, if you serve pages tailored depending on who's logged in at the moment, you
  14. # may want to add the current user id to be part of the ETag to prevent unauthorized displaying
  15. # of cached pages.
  16. #
  17. # class InvoicesController < ApplicationController
  18. # etag { current_user&.id }
  19. #
  20. # def show
  21. # # Etag will differ even for the same invoice when it's viewed by a different current_user
  22. # @invoice = Invoice.find(params[:id])
  23. # fresh_when etag: @invoice
  24. # end
  25. # end
  26. 1 def etag(&etagger)
  27. 7 self.etaggers += [etagger]
  28. end
  29. end
  30. # Sets the +etag+, +last_modified+, or both on the response and renders a
  31. # <tt>304 Not Modified</tt> response if the request is already fresh.
  32. #
  33. # === Parameters:
  34. #
  35. # * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the
  36. # +:weak_etag+ option.
  37. # * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response.
  38. # Requests that set If-None-Match header may return a 304 Not Modified
  39. # response if it matches the ETag exactly. A weak ETag indicates semantic
  40. # equivalence, not byte-for-byte equality, so they're good for caching
  41. # HTML pages in browser caches. They can't be used for responses that
  42. # must be byte-identical, like serving Range requests within a PDF file.
  43. # * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response.
  44. # Requests that set If-None-Match header may return a 304 Not Modified
  45. # response if it matches the ETag exactly. A strong ETag implies exact
  46. # equality: the response must match byte for byte. This is necessary for
  47. # doing Range requests within a large video or PDF file, for example, or
  48. # for compatibility with some CDNs that don't support weak ETags.
  49. # * <tt>:last_modified</tt> Sets a "weak" last-update validator on the
  50. # response. Subsequent requests that set If-Modified-Since may return a
  51. # 304 Not Modified response if last_modified <= If-Modified-Since.
  52. # * <tt>:public</tt> By default the Cache-Control header is private, set this to
  53. # +true+ if you want your application to be cacheable by other devices (proxy caches).
  54. # * <tt>:template</tt> By default, the template digest for the current
  55. # controller/action is included in ETags. If the action renders a
  56. # different template, you can include its digest instead. If the action
  57. # doesn't render a template at all, you can pass <tt>template: false</tt>
  58. # to skip any attempt to check for a template digest.
  59. #
  60. # === Example:
  61. #
  62. # def show
  63. # @article = Article.find(params[:id])
  64. # fresh_when(etag: @article, last_modified: @article.updated_at, public: true)
  65. # end
  66. #
  67. # This will render the show template if the request isn't sending a matching ETag or
  68. # If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match.
  69. #
  70. # You can also just pass a record. In this case +last_modified+ will be set
  71. # by calling +updated_at+ and +etag+ by passing the object itself.
  72. #
  73. # def show
  74. # @article = Article.find(params[:id])
  75. # fresh_when(@article)
  76. # end
  77. #
  78. # You can also pass an object that responds to +maximum+, such as a
  79. # collection of active records. In this case +last_modified+ will be set by
  80. # calling <tt>maximum(:updated_at)</tt> on the collection (the timestamp of the
  81. # most recently updated record) and the +etag+ by passing the object itself.
  82. #
  83. # def index
  84. # @articles = Article.all
  85. # fresh_when(@articles)
  86. # end
  87. #
  88. # When passing a record or a collection, you can still set the public header:
  89. #
  90. # def show
  91. # @article = Article.find(params[:id])
  92. # fresh_when(@article, public: true)
  93. # end
  94. #
  95. # When rendering a different template than the default controller/action
  96. # style, you can indicate which digest to include in the ETag:
  97. #
  98. # before_action { fresh_when @article, template: 'widgets/show' }
  99. #
  100. 1 def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, template: nil)
  101. weak_etag ||= etag || object unless strong_etag
  102. last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at)
  103. if strong_etag
  104. response.strong_etag = combine_etags strong_etag,
  105. last_modified: last_modified, public: public, template: template
  106. elsif weak_etag || template
  107. response.weak_etag = combine_etags weak_etag,
  108. last_modified: last_modified, public: public, template: template
  109. end
  110. response.last_modified = last_modified if last_modified
  111. response.cache_control[:public] = true if public
  112. head :not_modified if request.fresh?(response)
  113. end
  114. # Sets the +etag+ and/or +last_modified+ on the response and checks it against
  115. # the client request. If the request doesn't match the options provided, the
  116. # request is considered stale and should be generated from scratch. Otherwise,
  117. # it's fresh and we don't need to generate anything and a reply of <tt>304 Not Modified</tt> is sent.
  118. #
  119. # === Parameters:
  120. #
  121. # * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the
  122. # +:weak_etag+ option.
  123. # * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response.
  124. # Requests that set If-None-Match header may return a 304 Not Modified
  125. # response if it matches the ETag exactly. A weak ETag indicates semantic
  126. # equivalence, not byte-for-byte equality, so they're good for caching
  127. # HTML pages in browser caches. They can't be used for responses that
  128. # must be byte-identical, like serving Range requests within a PDF file.
  129. # * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response.
  130. # Requests that set If-None-Match header may return a 304 Not Modified
  131. # response if it matches the ETag exactly. A strong ETag implies exact
  132. # equality: the response must match byte for byte. This is necessary for
  133. # doing Range requests within a large video or PDF file, for example, or
  134. # for compatibility with some CDNs that don't support weak ETags.
  135. # * <tt>:last_modified</tt> Sets a "weak" last-update validator on the
  136. # response. Subsequent requests that set If-Modified-Since may return a
  137. # 304 Not Modified response if last_modified <= If-Modified-Since.
  138. # * <tt>:public</tt> By default the Cache-Control header is private, set this to
  139. # +true+ if you want your application to be cacheable by other devices (proxy caches).
  140. # * <tt>:template</tt> By default, the template digest for the current
  141. # controller/action is included in ETags. If the action renders a
  142. # different template, you can include its digest instead. If the action
  143. # doesn't render a template at all, you can pass <tt>template: false</tt>
  144. # to skip any attempt to check for a template digest.
  145. #
  146. # === Example:
  147. #
  148. # def show
  149. # @article = Article.find(params[:id])
  150. #
  151. # if stale?(etag: @article, last_modified: @article.updated_at)
  152. # @statistics = @article.really_expensive_call
  153. # respond_to do |format|
  154. # # all the supported formats
  155. # end
  156. # end
  157. # end
  158. #
  159. # You can also just pass a record. In this case +last_modified+ will be set
  160. # by calling +updated_at+ and +etag+ by passing the object itself.
  161. #
  162. # def show
  163. # @article = Article.find(params[:id])
  164. #
  165. # if stale?(@article)
  166. # @statistics = @article.really_expensive_call
  167. # respond_to do |format|
  168. # # all the supported formats
  169. # end
  170. # end
  171. # end
  172. #
  173. # You can also pass an object that responds to +maximum+, such as a
  174. # collection of active records. In this case +last_modified+ will be set by
  175. # calling +maximum(:updated_at)+ on the collection (the timestamp of the
  176. # most recently updated record) and the +etag+ by passing the object itself.
  177. #
  178. # def index
  179. # @articles = Article.all
  180. #
  181. # if stale?(@articles)
  182. # @statistics = @articles.really_expensive_call
  183. # respond_to do |format|
  184. # # all the supported formats
  185. # end
  186. # end
  187. # end
  188. #
  189. # When passing a record or a collection, you can still set the public header:
  190. #
  191. # def show
  192. # @article = Article.find(params[:id])
  193. #
  194. # if stale?(@article, public: true)
  195. # @statistics = @article.really_expensive_call
  196. # respond_to do |format|
  197. # # all the supported formats
  198. # end
  199. # end
  200. # end
  201. #
  202. # When rendering a different template than the default controller/action
  203. # style, you can indicate which digest to include in the ETag:
  204. #
  205. # def show
  206. # super if stale? @article, template: 'widgets/show'
  207. # end
  208. #
  209. 1 def stale?(object = nil, **freshness_kwargs)
  210. fresh_when(object, **freshness_kwargs)
  211. !request.fresh?(response)
  212. end
  213. # Sets an HTTP 1.1 Cache-Control header. Defaults to issuing a +private+
  214. # instruction, so that intermediate caches must not cache the response.
  215. #
  216. # expires_in 20.minutes
  217. # expires_in 3.hours, public: true
  218. # expires_in 3.hours, public: true, must_revalidate: true
  219. #
  220. # This method will overwrite an existing Cache-Control header.
  221. # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
  222. #
  223. # HTTP Cache-Control Extensions for Stale Content. See https://tools.ietf.org/html/rfc5861
  224. # It helps to cache an asset and serve it while is being revalidated and/or returning with an error.
  225. #
  226. # expires_in 3.hours, public: true, stale_while_revalidate: 60.seconds
  227. # expires_in 3.hours, public: true, stale_while_revalidate: 60.seconds, stale_if_error: 5.minutes
  228. #
  229. # HTTP Cache-Control Extensions other values: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
  230. # Any additional key-value pairs are concatenated onto the `Cache-Control` header in the response:
  231. #
  232. # expires_in 3.hours, public: true, "s-maxage": 3.hours, "no-transform": true
  233. #
  234. # The method will also ensure an HTTP Date header for client compatibility.
  235. 1 def expires_in(seconds, options = {})
  236. response.cache_control.merge!(
  237. max_age: seconds,
  238. public: options.delete(:public),
  239. must_revalidate: options.delete(:must_revalidate),
  240. stale_while_revalidate: options.delete(:stale_while_revalidate),
  241. stale_if_error: options.delete(:stale_if_error),
  242. )
  243. options.delete(:private)
  244. response.cache_control[:extras] = options.map { |k, v| "#{k}=#{v}" }
  245. response.date = Time.now unless response.date?
  246. end
  247. # Sets an HTTP 1.1 Cache-Control header of <tt>no-cache</tt>. This means the
  248. # resource will be marked as stale, so clients must always revalidate.
  249. # Intermediate/browser caches may still store the asset.
  250. 1 def expires_now
  251. response.cache_control.replace(no_cache: true)
  252. end
  253. # Cache or yield the block. The cache is supposed to never expire.
  254. #
  255. # You can use this method when you have an HTTP response that never changes,
  256. # and the browser and proxies should cache it indefinitely.
  257. #
  258. # * +public+: By default, HTTP responses are private, cached only on the
  259. # user's web browser. To allow proxies to cache the response, set +true+ to
  260. # indicate that they can serve the cached response to all users.
  261. 1 def http_cache_forever(public: false)
  262. expires_in 100.years, public: public
  263. yield if stale?(etag: request.fullpath,
  264. last_modified: Time.new(2011, 1, 1).utc,
  265. public: public)
  266. end
  267. 1 private
  268. 1 def combine_etags(validator, options)
  269. [validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact
  270. end
  271. end
  272. end

lib/action_controller/metal/content_security_policy.rb

62.96% lines covered

27 relevant lines. 17 lines covered and 10 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController #:nodoc:
  3. 1 module ContentSecurityPolicy
  4. # TODO: Documentation
  5. 1 extend ActiveSupport::Concern
  6. 1 include AbstractController::Helpers
  7. 1 include AbstractController::Callbacks
  8. 1 included do
  9. 1 helper_method :content_security_policy?
  10. 1 helper_method :content_security_policy_nonce
  11. end
  12. 1 module ClassMethods
  13. 1 def content_security_policy(enabled = true, **options, &block)
  14. 8 before_action(options) do
  15. if block_given?
  16. policy = current_content_security_policy
  17. yield policy
  18. request.content_security_policy = policy
  19. end
  20. unless enabled
  21. request.content_security_policy = nil
  22. end
  23. end
  24. end
  25. 1 def content_security_policy_report_only(report_only = true, **options)
  26. 1 before_action(options) do
  27. request.content_security_policy_report_only = report_only
  28. end
  29. end
  30. end
  31. 1 private
  32. 1 def content_security_policy?
  33. request.content_security_policy
  34. end
  35. 1 def content_security_policy_nonce
  36. request.content_security_policy_nonce
  37. end
  38. 1 def current_content_security_policy
  39. request.content_security_policy&.clone || ActionDispatch::ContentSecurityPolicy.new
  40. end
  41. end
  42. end

lib/action_controller/metal/cookies.rb

87.5% lines covered

8 relevant lines. 7 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController #:nodoc:
  3. 1 module Cookies
  4. 1 extend ActiveSupport::Concern
  5. 1 included do
  6. 2 helper_method :cookies if defined?(helper_method)
  7. end
  8. 1 private
  9. 1 def cookies
  10. request.cookie_jar
  11. end
  12. end
  13. end

lib/action_controller/metal/data_streaming.rb

32.43% lines covered

37 relevant lines. 12 lines covered and 25 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_controller/metal/exceptions"
  3. 1 require "action_dispatch/http/content_disposition"
  4. 1 module ActionController #:nodoc:
  5. # Methods for sending arbitrary data and for streaming files to the browser,
  6. # instead of rendering.
  7. 1 module DataStreaming
  8. 1 extend ActiveSupport::Concern
  9. 1 include ActionController::Rendering
  10. 1 DEFAULT_SEND_FILE_TYPE = "application/octet-stream" #:nodoc:
  11. 1 DEFAULT_SEND_FILE_DISPOSITION = "attachment" #:nodoc:
  12. 1 private
  13. # Sends the file. This uses a server-appropriate method (such as X-Sendfile)
  14. # via the Rack::Sendfile middleware. The header to use is set via
  15. # +config.action_dispatch.x_sendfile_header+.
  16. # Your server can also configure this for you by setting the X-Sendfile-Type header.
  17. #
  18. # Be careful to sanitize the path parameter if it is coming from a web
  19. # page. <tt>send_file(params[:path])</tt> allows a malicious user to
  20. # download any file on your server.
  21. #
  22. # Options:
  23. # * <tt>:filename</tt> - suggests a filename for the browser to use.
  24. # Defaults to <tt>File.basename(path)</tt>.
  25. # * <tt>:type</tt> - specifies an HTTP content type.
  26. # You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json.
  27. # If omitted, the type will be inferred from the file extension specified in <tt>:filename</tt>.
  28. # If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
  29. # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
  30. # Valid values are 'inline' and 'attachment' (default).
  31. # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200.
  32. # * <tt>:url_based_filename</tt> - set to +true+ if you want the browser to guess the filename from
  33. # the URL, which is necessary for i18n filenames on certain browsers
  34. # (setting <tt>:filename</tt> overrides this option).
  35. #
  36. # The default Content-Type and Content-Disposition headers are
  37. # set to download arbitrary binary files in as many browsers as
  38. # possible. IE versions 4, 5, 5.5, and 6 are all known to have
  39. # a variety of quirks (especially when downloading over SSL).
  40. #
  41. # Simple download:
  42. #
  43. # send_file '/path/to.zip'
  44. #
  45. # Show a JPEG in the browser:
  46. #
  47. # send_file '/path/to.jpeg', type: 'image/jpeg', disposition: 'inline'
  48. #
  49. # Show a 404 page in the browser:
  50. #
  51. # send_file '/path/to/404.html', type: 'text/html; charset=utf-8', disposition: 'inline', status: 404
  52. #
  53. # Read about the other Content-* HTTP headers if you'd like to
  54. # provide the user with more information (such as Content-Description) in
  55. # https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11.
  56. #
  57. # Also be aware that the document may be cached by proxies and browsers.
  58. # The Pragma and Cache-Control headers declare how the file may be cached
  59. # by intermediaries. They default to require clients to validate with
  60. # the server before releasing cached responses. See
  61. # https://www.mnot.net/cache_docs/ for an overview of web caching and
  62. # https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
  63. # for the Cache-Control header spec.
  64. 1 def send_file(path, options = {}) #:doc:
  65. raise MissingFile, "Cannot read file #{path}" unless File.file?(path) && File.readable?(path)
  66. options[:filename] ||= File.basename(path) unless options[:url_based_filename]
  67. send_file_headers! options
  68. self.status = options[:status] || 200
  69. self.content_type = options[:content_type] if options.key?(:content_type)
  70. response.send_file path
  71. end
  72. # Sends the given binary data to the browser. This method is similar to
  73. # <tt>render plain: data</tt>, but also allows you to specify whether
  74. # the browser should display the response as a file attachment (i.e. in a
  75. # download dialog) or as inline data. You may also set the content type,
  76. # the file name, and other things.
  77. #
  78. # Options:
  79. # * <tt>:filename</tt> - suggests a filename for the browser to use.
  80. # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'.
  81. # You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json.
  82. # If omitted, type will be inferred from the file extension specified in <tt>:filename</tt>.
  83. # If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
  84. # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
  85. # Valid values are 'inline' and 'attachment' (default).
  86. # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200.
  87. #
  88. # Generic data download:
  89. #
  90. # send_data buffer
  91. #
  92. # Download a dynamically-generated tarball:
  93. #
  94. # send_data generate_tgz('dir'), filename: 'dir.tgz'
  95. #
  96. # Display an image Active Record in the browser:
  97. #
  98. # send_data image.data, type: image.content_type, disposition: 'inline'
  99. #
  100. # See +send_file+ for more information on HTTP Content-* headers and caching.
  101. 1 def send_data(data, options = {}) #:doc:
  102. send_file_headers! options
  103. render options.slice(:status, :content_type).merge(body: data)
  104. end
  105. 1 def send_file_headers!(options)
  106. type_provided = options.has_key?(:type)
  107. content_type = options.fetch(:type, DEFAULT_SEND_FILE_TYPE)
  108. self.content_type = content_type
  109. response.sending_file = true
  110. raise ArgumentError, ":type option required" if content_type.nil?
  111. if content_type.is_a?(Symbol)
  112. extension = Mime[content_type]
  113. raise ArgumentError, "Unknown MIME type #{options[:type]}" unless extension
  114. self.content_type = extension
  115. else
  116. if !type_provided && options[:filename]
  117. # If type wasn't provided, try guessing from file extension.
  118. content_type = Mime::Type.lookup_by_extension(File.extname(options[:filename]).downcase.delete(".")) || content_type
  119. end
  120. self.content_type = content_type
  121. end
  122. disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION)
  123. if disposition
  124. headers["Content-Disposition"] = ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: options[:filename])
  125. end
  126. headers["Content-Transfer-Encoding"] = "binary"
  127. # Fix a problem with IE 6.0 on opening downloaded files:
  128. # If Cache-Control: no-cache is set (which Rails does by default),
  129. # IE removes the file it just downloaded from its cache immediately
  130. # after it displays the "open/save" dialog, which means that if you
  131. # hit "open" the file isn't there anymore when the application that
  132. # is called for handling the download is run, so let's workaround that
  133. response.cache_control[:public] ||= false
  134. end
  135. end
  136. end

lib/action_controller/metal/default_headers.rb

71.43% lines covered

7 relevant lines. 5 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. # Allows configuring default headers that will be automatically merged into
  4. # each response.
  5. 1 module DefaultHeaders
  6. 1 extend ActiveSupport::Concern
  7. 1 module ClassMethods
  8. 1 def make_response!(request)
  9. ActionDispatch::Response.create.tap do |res|
  10. res.request = request
  11. end
  12. end
  13. end
  14. end
  15. end

lib/action_controller/metal/etag_with_flash.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. # When you're using the flash, it's generally used as a conditional on the view.
  4. # This means the content of the view depends on the flash. Which in turn means
  5. # that the ETag for a response should be computed with the content of the flash
  6. # in mind. This does that by including the content of the flash as a component
  7. # in the ETag that's generated for a response.
  8. 1 module EtagWithFlash
  9. 1 extend ActiveSupport::Concern
  10. 1 include ActionController::ConditionalGet
  11. 1 included do
  12. 1 etag { flash unless flash.empty? }
  13. end
  14. end
  15. end

lib/action_controller/metal/etag_with_template_digest.rb

64.71% lines covered

17 relevant lines. 11 lines covered and 6 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. # When our views change, they should bubble up into HTTP cache freshness
  4. # and bust browser caches. So the template digest for the current action
  5. # is automatically included in the ETag.
  6. #
  7. # Enabled by default for apps that use Action View. Disable by setting
  8. #
  9. # config.action_controller.etag_with_template_digest = false
  10. #
  11. # Override the template to digest by passing +:template+ to +fresh_when+
  12. # and +stale?+ calls. For example:
  13. #
  14. # # We're going to render widgets/show, not posts/show
  15. # fresh_when @post, template: 'widgets/show'
  16. #
  17. # # We're not going to render a template, so omit it from the ETag.
  18. # fresh_when @post, template: false
  19. #
  20. 1 module EtagWithTemplateDigest
  21. 1 extend ActiveSupport::Concern
  22. 1 include ActionController::ConditionalGet
  23. 1 included do
  24. 1 class_attribute :etag_with_template_digest, default: true
  25. 1 etag do |options|
  26. determine_template_etag(options) if etag_with_template_digest
  27. end
  28. end
  29. 1 private
  30. 1 def determine_template_etag(options)
  31. if template = pick_template_for_etag(options)
  32. lookup_and_digest_template(template)
  33. end
  34. end
  35. # Pick the template digest to include in the ETag. If the +:template+ option
  36. # is present, use the named template. If +:template+ is +nil+ or absent, use
  37. # the default controller/action template. If +:template+ is false, omit the
  38. # template digest from the ETag.
  39. 1 def pick_template_for_etag(options)
  40. unless options[:template] == false
  41. options[:template] || "#{controller_path}/#{action_name}"
  42. end
  43. end
  44. 1 def lookup_and_digest_template(template)
  45. ActionView::Digestor.digest name: template, format: nil, finder: lookup_context
  46. end
  47. end
  48. end

lib/action_controller/metal/exceptions.rb

59.57% lines covered

47 relevant lines. 28 lines covered and 19 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. 1 class ActionControllerError < StandardError #:nodoc:
  4. end
  5. 1 class BadRequest < ActionControllerError #:nodoc:
  6. 1 def initialize(msg = nil)
  7. super(msg)
  8. set_backtrace $!.backtrace if $!
  9. end
  10. end
  11. 1 class RenderError < ActionControllerError #:nodoc:
  12. end
  13. 1 class RoutingError < ActionControllerError #:nodoc:
  14. 1 attr_reader :failures
  15. 1 def initialize(message, failures = [])
  16. super(message)
  17. @failures = failures
  18. end
  19. end
  20. 1 class UrlGenerationError < ActionControllerError #:nodoc:
  21. 1 attr_reader :routes, :route_name, :method_name
  22. 1 def initialize(message, routes = nil, route_name = nil, method_name = nil)
  23. @routes = routes
  24. @route_name = route_name
  25. @method_name = method_name
  26. super(message)
  27. end
  28. 1 class Correction
  29. 1 def initialize(error)
  30. @error = error
  31. end
  32. 1 def corrections
  33. if @error.method_name
  34. maybe_these = @error.routes.named_routes.helper_names.grep(/#{@error.route_name}/)
  35. maybe_these -= [@error.method_name.to_s] # remove exact match
  36. maybe_these.sort_by { |n|
  37. DidYouMean::Jaro.distance(@error.route_name, n)
  38. }.reverse.first(4)
  39. else
  40. []
  41. end
  42. end
  43. end
  44. # We may not have DYM, and DYM might not let us register error handlers
  45. 1 if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
  46. DidYouMean.correct_error(self, Correction)
  47. end
  48. end
  49. 1 class MethodNotAllowed < ActionControllerError #:nodoc:
  50. 1 def initialize(*allowed_methods)
  51. super("Only #{allowed_methods.to_sentence} requests are allowed.")
  52. end
  53. end
  54. 1 class NotImplemented < MethodNotAllowed #:nodoc:
  55. end
  56. 1 class MissingFile < ActionControllerError #:nodoc:
  57. end
  58. 1 class SessionOverflowError < ActionControllerError #:nodoc:
  59. 1 DEFAULT_MESSAGE = "Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data."
  60. 1 def initialize(message = nil)
  61. super(message || DEFAULT_MESSAGE)
  62. end
  63. end
  64. 1 class UnknownHttpMethod < ActionControllerError #:nodoc:
  65. end
  66. 1 class UnknownFormat < ActionControllerError #:nodoc:
  67. end
  68. # Raised when a nested respond_to is triggered and the content types of each
  69. # are incompatible. For example:
  70. #
  71. # respond_to do |outer_type|
  72. # outer_type.js do
  73. # respond_to do |inner_type|
  74. # inner_type.html { render body: "HTML" }
  75. # end
  76. # end
  77. # end
  78. 1 class RespondToMismatchError < ActionControllerError
  79. 1 DEFAULT_MESSAGE = "respond_to was called multiple times and matched with conflicting formats in this action. Please note that you may only call respond_to and match on a single format per action."
  80. 1 def initialize(message = nil)
  81. super(message || DEFAULT_MESSAGE)
  82. end
  83. end
  84. 1 class MissingExactTemplate < UnknownFormat #:nodoc:
  85. end
  86. end

lib/action_controller/metal/feature_policy.rb

60.0% lines covered

10 relevant lines. 6 lines covered and 4 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController #:nodoc:
  3. # HTTP Feature Policy is a web standard for defining a mechanism to
  4. # allow and deny the use of browser features in its own context, and
  5. # in content within any <iframe> elements in the document.
  6. #
  7. # Full details of HTTP Feature Policy specification and guidelines can
  8. # be found at MDN:
  9. #
  10. # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
  11. #
  12. # Examples of usage:
  13. #
  14. # # Global policy
  15. # Rails.application.config.feature_policy do |f|
  16. # f.camera :none
  17. # f.gyroscope :none
  18. # f.microphone :none
  19. # f.usb :none
  20. # f.fullscreen :self
  21. # f.payment :self, "https://secure.example.com"
  22. # end
  23. #
  24. # # Controller level policy
  25. # class PagesController < ApplicationController
  26. # feature_policy do |p|
  27. # p.geolocation "https://example.com"
  28. # end
  29. # end
  30. 1 module FeaturePolicy
  31. 1 extend ActiveSupport::Concern
  32. 1 module ClassMethods
  33. 1 def feature_policy(**options, &block)
  34. 3 before_action(options) do
  35. if block_given?
  36. policy = request.feature_policy.clone
  37. yield policy
  38. request.feature_policy = policy
  39. end
  40. end
  41. end
  42. end
  43. end
  44. end

lib/action_controller/metal/flash.rb

69.57% lines covered

23 relevant lines. 16 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController #:nodoc:
  3. 1 module Flash
  4. 1 extend ActiveSupport::Concern
  5. 1 included do
  6. 1 class_attribute :_flash_types, instance_accessor: false, default: []
  7. 1 delegate :flash, to: :request
  8. 1 add_flash_types(:alert, :notice)
  9. end
  10. 1 module ClassMethods
  11. # Creates new flash types. You can pass as many types as you want to create
  12. # flash types other than the default <tt>alert</tt> and <tt>notice</tt> in
  13. # your controllers and views. For instance:
  14. #
  15. # # in application_controller.rb
  16. # class ApplicationController < ActionController::Base
  17. # add_flash_types :warning
  18. # end
  19. #
  20. # # in your controller
  21. # redirect_to user_path(@user), warning: "Incomplete profile"
  22. #
  23. # # in your view
  24. # <%= warning %>
  25. #
  26. # This method will automatically define a new method for each of the given
  27. # names, and it will be available in your views.
  28. 1 def add_flash_types(*types)
  29. 2 types.each do |type|
  30. 3 next if _flash_types.include?(type)
  31. 3 define_method(type) do
  32. request.flash[type]
  33. end
  34. 3 helper_method(type) if respond_to?(:helper_method)
  35. 3 self._flash_types += [type]
  36. end
  37. end
  38. end
  39. 1 private
  40. 1 def redirect_to(options = {}, response_options_and_flash = {}) #:doc:
  41. self.class._flash_types.each do |flash_type|
  42. if type = response_options_and_flash.delete(flash_type)
  43. flash[flash_type] = type
  44. end
  45. end
  46. if other_flashes = response_options_and_flash.delete(:flash)
  47. flash.update(other_flashes)
  48. end
  49. super(options, response_options_and_flash)
  50. end
  51. end
  52. end

lib/action_controller/metal/head.rb

20.83% lines covered

24 relevant lines. 5 lines covered and 19 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. 1 module Head
  4. # Returns a response that has no content (merely headers). The options
  5. # argument is interpreted to be a hash of header names and values.
  6. # This allows you to easily return a response that consists only of
  7. # significant headers:
  8. #
  9. # head :created, location: person_path(@person)
  10. #
  11. # head :created, location: @person
  12. #
  13. # It can also be used to return exceptional conditions:
  14. #
  15. # return head(:method_not_allowed) unless request.post?
  16. # return head(:bad_request) unless valid_request?
  17. # render
  18. #
  19. # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols.
  20. 1 def head(status, options = {})
  21. if status.is_a?(Hash)
  22. raise ArgumentError, "#{status.inspect} is not a valid value for `status`."
  23. end
  24. status ||= :ok
  25. location = options.delete(:location)
  26. content_type = options.delete(:content_type)
  27. options.each do |key, value|
  28. headers[key.to_s.split(/[-_]/).each { |v| v[0] = v[0].upcase }.join("-")] = value.to_s
  29. end
  30. self.status = status
  31. self.location = url_for(location) if location
  32. if include_content?(response_code)
  33. unless self.media_type
  34. self.content_type = content_type || (Mime[formats.first] if formats) || Mime[:html]
  35. end
  36. response.charset = false
  37. end
  38. self.response_body = ""
  39. true
  40. end
  41. 1 private
  42. 1 def include_content?(status)
  43. case status
  44. when 100..199
  45. false
  46. when 204, 205, 304
  47. false
  48. else
  49. true
  50. end
  51. end
  52. end
  53. end

lib/action_controller/metal/helpers.rb

80.0% lines covered

30 relevant lines. 24 lines covered and 6 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. # The \Rails framework provides a large number of helpers for working with assets, dates, forms,
  4. # numbers and model objects, to name a few. These helpers are available to all templates
  5. # by default.
  6. #
  7. # In addition to using the standard template helpers provided, creating custom helpers to
  8. # extract complicated logic or reusable functionality is strongly encouraged. By default, each controller
  9. # will include all helpers. These helpers are only accessible on the controller through <tt>#helpers</tt>
  10. #
  11. # In previous versions of \Rails the controller will include a helper which
  12. # matches the name of the controller, e.g., <tt>MyController</tt> will automatically
  13. # include <tt>MyHelper</tt>. You can revert to the old behavior with the following:
  14. #
  15. # # config/application.rb
  16. # class Application < Rails::Application
  17. # config.action_controller.include_all_helpers = false
  18. # end
  19. #
  20. # Additional helpers can be specified using the +helper+ class method in ActionController::Base or any
  21. # controller which inherits from it.
  22. #
  23. # The +to_s+ method from the \Time class can be wrapped in a helper method to display a custom message if
  24. # a \Time object is blank:
  25. #
  26. # module FormattedTimeHelper
  27. # def format_time(time, format=:long, blank_message="&nbsp;")
  28. # time.blank? ? blank_message : time.to_s(format)
  29. # end
  30. # end
  31. #
  32. # FormattedTimeHelper can now be included in a controller, using the +helper+ class method:
  33. #
  34. # class EventsController < ActionController::Base
  35. # helper FormattedTimeHelper
  36. # def index
  37. # @events = Event.all
  38. # end
  39. # end
  40. #
  41. # Then, in any view rendered by <tt>EventsController</tt>, the <tt>format_time</tt> method can be called:
  42. #
  43. # <% @events.each do |event| -%>
  44. # <p>
  45. # <%= format_time(event.time, :short, "N/A") %> | <%= event.name %>
  46. # </p>
  47. # <% end -%>
  48. #
  49. # Finally, assuming we have two event instances, one which has a time and one which does not,
  50. # the output might look like this:
  51. #
  52. # 23 Aug 11:30 | Carolina Railhawks Soccer Match
  53. # N/A | Carolina Railhawks Training Workshop
  54. #
  55. 1 module Helpers
  56. 1 extend ActiveSupport::Concern
  57. 2 class << self; attr_accessor :helpers_path; end
  58. 1 include AbstractController::Helpers
  59. 1 included do
  60. 2 class_attribute :helpers_path, default: []
  61. 2 class_attribute :include_all_helpers, default: true
  62. end
  63. 1 module ClassMethods
  64. # Declares helper accessors for controller attributes. For example, the
  65. # following adds new +name+ and <tt>name=</tt> instance methods to a
  66. # controller and makes them available to the view:
  67. # attr_accessor :name
  68. # helper_attr :name
  69. #
  70. # ==== Parameters
  71. # * <tt>attrs</tt> - Names of attributes to be converted into helpers.
  72. 1 def helper_attr(*attrs)
  73. attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
  74. end
  75. # Provides a proxy to access helper methods from outside the view.
  76. #
  77. # Note that the proxy is rendered under a different view context.
  78. # This may cause incorrect behaviour with capture methods. Consider
  79. # using {helper}[rdoc-ref:AbstractController::Helpers::ClassMethods#helper]
  80. # instead when using +capture+.
  81. 1 def helpers
  82. @helper_proxy ||= begin
  83. proxy = ActionView::Base.empty
  84. proxy.config = config.inheritable_copy
  85. proxy.extend(_helpers)
  86. end
  87. end
  88. # Overwrite modules_for_helpers to accept :all as argument, which loads
  89. # all helpers in helpers_path.
  90. #
  91. # ==== Parameters
  92. # * <tt>args</tt> - A list of helpers
  93. #
  94. # ==== Returns
  95. # * <tt>array</tt> - A normalized list of modules for the list of helpers provided.
  96. 1 def modules_for_helpers(args)
  97. 274 args += all_application_helpers if args.delete(:all)
  98. 274 super(args)
  99. end
  100. # Returns a list of helper names in a given path.
  101. #
  102. # ActionController::Base.all_helpers_from_path 'app/helpers'
  103. # # => ["application", "chart", "rubygems"]
  104. 1 def all_helpers_from_path(path)
  105. 2 helpers = Array(path).flat_map do |_path|
  106. 10 names = Dir["#{_path}/**/*_helper.rb"].map { |file| file[_path.to_s.size + 1..-"_helper.rb".size - 1] }
  107. 3 names.sort!
  108. end
  109. 2 helpers.uniq!
  110. 2 helpers
  111. end
  112. 1 private
  113. # Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt>
  114. 1 def all_application_helpers
  115. 2 all_helpers_from_path(helpers_path)
  116. end
  117. end
  118. # Provides a proxy to access helper methods from outside the view.
  119. 1 def helpers
  120. @_helper_proxy ||= view_context
  121. end
  122. end
  123. end

lib/action_controller/metal/http_authentication.rb

40.69% lines covered

145 relevant lines. 59 lines covered and 86 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "base64"
  3. 1 require "active_support/security_utils"
  4. 1 module ActionController
  5. # Makes it dead easy to do HTTP Basic, Digest and Token authentication.
  6. 1 module HttpAuthentication
  7. # Makes it dead easy to do HTTP \Basic authentication.
  8. #
  9. # === Simple \Basic example
  10. #
  11. # class PostsController < ApplicationController
  12. # http_basic_authenticate_with name: "dhh", password: "secret", except: :index
  13. #
  14. # def index
  15. # render plain: "Everyone can see me!"
  16. # end
  17. #
  18. # def edit
  19. # render plain: "I'm only accessible if you know the password"
  20. # end
  21. # end
  22. #
  23. # === Advanced \Basic example
  24. #
  25. # Here is a more advanced \Basic example where only Atom feeds and the XML API is protected by HTTP authentication,
  26. # the regular HTML interface is protected by a session approach:
  27. #
  28. # class ApplicationController < ActionController::Base
  29. # before_action :set_account, :authenticate
  30. #
  31. # private
  32. # def set_account
  33. # @account = Account.find_by(url_name: request.subdomains.first)
  34. # end
  35. #
  36. # def authenticate
  37. # case request.format
  38. # when Mime[:xml], Mime[:atom]
  39. # if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
  40. # @current_user = user
  41. # else
  42. # request_http_basic_authentication
  43. # end
  44. # else
  45. # if session_authenticated?
  46. # @current_user = @account.users.find(session[:authenticated][:user_id])
  47. # else
  48. # redirect_to(login_url) and return false
  49. # end
  50. # end
  51. # end
  52. # end
  53. #
  54. # In your integration tests, you can do something like this:
  55. #
  56. # def test_access_granted_from_xml
  57. # authorization = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
  58. #
  59. # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
  60. #
  61. # assert_equal 200, status
  62. # end
  63. 1 module Basic
  64. 1 extend self
  65. 1 module ControllerMethods
  66. 1 extend ActiveSupport::Concern
  67. 1 module ClassMethods
  68. 1 def http_basic_authenticate_with(name:, password:, realm: nil, **options)
  69. 1 before_action(options) { http_basic_authenticate_or_request_with name: name, password: password, realm: realm }
  70. end
  71. end
  72. 1 def http_basic_authenticate_or_request_with(name:, password:, realm: nil, message: nil)
  73. authenticate_or_request_with_http_basic(realm, message) do |given_name, given_password|
  74. ActiveSupport::SecurityUtils.secure_compare(given_name, name) &
  75. ActiveSupport::SecurityUtils.secure_compare(given_password, password)
  76. end
  77. end
  78. 1 def authenticate_or_request_with_http_basic(realm = nil, message = nil, &login_procedure)
  79. authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm || "Application", message)
  80. end
  81. 1 def authenticate_with_http_basic(&login_procedure)
  82. HttpAuthentication::Basic.authenticate(request, &login_procedure)
  83. end
  84. 1 def request_http_basic_authentication(realm = "Application", message = nil)
  85. HttpAuthentication::Basic.authentication_request(self, realm, message)
  86. end
  87. end
  88. 1 def authenticate(request, &login_procedure)
  89. if has_basic_credentials?(request)
  90. login_procedure.call(*user_name_and_password(request))
  91. end
  92. end
  93. 1 def has_basic_credentials?(request)
  94. request.authorization.present? && (auth_scheme(request).downcase == "basic")
  95. end
  96. 1 def user_name_and_password(request)
  97. decode_credentials(request).split(":", 2)
  98. end
  99. 1 def decode_credentials(request)
  100. ::Base64.decode64(auth_param(request) || "")
  101. end
  102. 1 def auth_scheme(request)
  103. request.authorization.to_s.split(" ", 2).first
  104. end
  105. 1 def auth_param(request)
  106. request.authorization.to_s.split(" ", 2).second
  107. end
  108. 1 def encode_credentials(user_name, password)
  109. "Basic #{::Base64.strict_encode64("#{user_name}:#{password}")}"
  110. end
  111. 1 def authentication_request(controller, realm, message)
  112. message ||= "HTTP Basic: Access denied.\n"
  113. controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"', "")}")
  114. controller.status = 401
  115. controller.response_body = message
  116. end
  117. end
  118. # Makes it dead easy to do HTTP \Digest authentication.
  119. #
  120. # === Simple \Digest example
  121. #
  122. # require "digest/md5"
  123. # class PostsController < ApplicationController
  124. # REALM = "SuperSecret"
  125. # USERS = {"dhh" => "secret", #plain text password
  126. # "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
  127. #
  128. # before_action :authenticate, except: [:index]
  129. #
  130. # def index
  131. # render plain: "Everyone can see me!"
  132. # end
  133. #
  134. # def edit
  135. # render plain: "I'm only accessible if you know the password"
  136. # end
  137. #
  138. # private
  139. # def authenticate
  140. # authenticate_or_request_with_http_digest(REALM) do |username|
  141. # USERS[username]
  142. # end
  143. # end
  144. # end
  145. #
  146. # === Notes
  147. #
  148. # The +authenticate_or_request_with_http_digest+ block must return the user's password
  149. # or the ha1 digest hash so the framework can appropriately hash to check the user's
  150. # credentials. Returning +nil+ will cause authentication to fail.
  151. #
  152. # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If
  153. # the password file or database is compromised, the attacker would be able to use the ha1 hash to
  154. # authenticate as the user at this +realm+, but would not have the user's password to try using at
  155. # other sites.
  156. #
  157. # In rare instances, web servers or front proxies strip authorization headers before
  158. # they reach your application. You can debug this situation by logging all environment
  159. # variables, and check for HTTP_AUTHORIZATION, amongst others.
  160. 1 module Digest
  161. 1 extend self
  162. 1 module ControllerMethods
  163. 1 def authenticate_or_request_with_http_digest(realm = "Application", message = nil, &password_procedure)
  164. authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm, message)
  165. end
  166. # Authenticate with HTTP Digest, returns true or false
  167. 1 def authenticate_with_http_digest(realm = "Application", &password_procedure)
  168. HttpAuthentication::Digest.authenticate(request, realm, &password_procedure)
  169. end
  170. # Render output including the HTTP Digest authentication header
  171. 1 def request_http_digest_authentication(realm = "Application", message = nil)
  172. HttpAuthentication::Digest.authentication_request(self, realm, message)
  173. end
  174. end
  175. # Returns false on a valid response, true otherwise
  176. 1 def authenticate(request, realm, &password_procedure)
  177. request.authorization && validate_digest_response(request, realm, &password_procedure)
  178. end
  179. # Returns false unless the request credentials response value matches the expected value.
  180. # First try the password as a ha1 digest password. If this fails, then try it as a plain
  181. # text password.
  182. 1 def validate_digest_response(request, realm, &password_procedure)
  183. secret_key = secret_token(request)
  184. credentials = decode_credentials_header(request)
  185. valid_nonce = validate_nonce(secret_key, request, credentials[:nonce])
  186. if valid_nonce && realm == credentials[:realm] && opaque(secret_key) == credentials[:opaque]
  187. password = password_procedure.call(credentials[:username])
  188. return false unless password
  189. method = request.get_header("rack.methodoverride.original_method") || request.get_header("REQUEST_METHOD")
  190. uri = credentials[:uri]
  191. [true, false].any? do |trailing_question_mark|
  192. [true, false].any? do |password_is_ha1|
  193. _uri = trailing_question_mark ? uri + "?" : uri
  194. expected = expected_response(method, _uri, credentials, password, password_is_ha1)
  195. expected == credentials[:response]
  196. end
  197. end
  198. end
  199. end
  200. # Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
  201. # Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead
  202. # of a plain-text password.
  203. 1 def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
  204. ha1 = password_is_ha1 ? password : ha1(credentials, password)
  205. ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
  206. ::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
  207. end
  208. 1 def ha1(credentials, password)
  209. ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
  210. end
  211. 1 def encode_credentials(http_method, credentials, password, password_is_ha1)
  212. credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
  213. "Digest " + credentials.sort_by { |x| x[0].to_s }.map { |v| "#{v[0]}='#{v[1]}'" }.join(", ")
  214. end
  215. 1 def decode_credentials_header(request)
  216. decode_credentials(request.authorization)
  217. end
  218. 1 def decode_credentials(header)
  219. ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, "").split(",").map do |pair|
  220. key, value = pair.split("=", 2)
  221. [key.strip, value.to_s.gsub(/^"|"$/, "").delete("'")]
  222. end]
  223. end
  224. 1 def authentication_header(controller, realm)
  225. secret_key = secret_token(controller.request)
  226. nonce = self.nonce(secret_key)
  227. opaque = opaque(secret_key)
  228. controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}")
  229. end
  230. 1 def authentication_request(controller, realm, message = nil)
  231. message ||= "HTTP Digest: Access denied.\n"
  232. authentication_header(controller, realm)
  233. controller.status = 401
  234. controller.response_body = message
  235. end
  236. 1 def secret_token(request)
  237. key_generator = request.key_generator
  238. http_auth_salt = request.http_auth_salt
  239. key_generator.generate_key(http_auth_salt)
  240. end
  241. # Uses an MD5 digest based on time to generate a value to be used only once.
  242. #
  243. # A server-specified data string which should be uniquely generated each time a 401 response is made.
  244. # It is recommended that this string be base64 or hexadecimal data.
  245. # Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed.
  246. #
  247. # The contents of the nonce are implementation dependent.
  248. # The quality of the implementation depends on a good choice.
  249. # A nonce might, for example, be constructed as the base 64 encoding of
  250. #
  251. # time-stamp H(time-stamp ":" ETag ":" private-key)
  252. #
  253. # where time-stamp is a server-generated time or other non-repeating value,
  254. # ETag is the value of the HTTP ETag header associated with the requested entity,
  255. # and private-key is data known only to the server.
  256. # With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and
  257. # reject the request if it did not match the nonce from that header or
  258. # if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity.
  259. # The inclusion of the ETag prevents a replay request for an updated version of the resource.
  260. # (Note: including the IP address of the client in the nonce would appear to offer the server the ability
  261. # to limit the reuse of the nonce to the same client that originally got it.
  262. # However, that would break proxy farms, where requests from a single user often go through different proxies in the farm.
  263. # Also, IP address spoofing is not that hard.)
  264. #
  265. # An implementation might choose not to accept a previously used nonce or a previously used digest, in order to
  266. # protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for
  267. # POST, PUT, or PATCH requests and a time-stamp for GET requests. For more details on the issues involved see Section 4
  268. # of this document.
  269. #
  270. # The nonce is opaque to the client. Composed of Time, and hash of Time with secret
  271. # key from the Rails session secret generated upon creation of project. Ensures
  272. # the time cannot be modified by client.
  273. 1 def nonce(secret_key, time = Time.now)
  274. t = time.to_i
  275. hashed = [t, secret_key]
  276. digest = ::Digest::MD5.hexdigest(hashed.join(":"))
  277. ::Base64.strict_encode64("#{t}:#{digest}")
  278. end
  279. # Might want a shorter timeout depending on whether the request
  280. # is a PATCH, PUT, or POST, and if the client is a browser or web service.
  281. # Can be much shorter if the Stale directive is implemented. This would
  282. # allow a user to use new nonce without prompting the user again for their
  283. # username and password.
  284. 1 def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60)
  285. return false if value.nil?
  286. t = ::Base64.decode64(value).split(":").first.to_i
  287. nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
  288. end
  289. # Opaque based on digest of secret key
  290. 1 def opaque(secret_key)
  291. ::Digest::MD5.hexdigest(secret_key)
  292. end
  293. end
  294. # Makes it dead easy to do HTTP Token authentication.
  295. #
  296. # Simple Token example:
  297. #
  298. # class PostsController < ApplicationController
  299. # TOKEN = "secret"
  300. #
  301. # before_action :authenticate, except: [ :index ]
  302. #
  303. # def index
  304. # render plain: "Everyone can see me!"
  305. # end
  306. #
  307. # def edit
  308. # render plain: "I'm only accessible if you know the password"
  309. # end
  310. #
  311. # private
  312. # def authenticate
  313. # authenticate_or_request_with_http_token do |token, options|
  314. # # Compare the tokens in a time-constant manner, to mitigate
  315. # # timing attacks.
  316. # ActiveSupport::SecurityUtils.secure_compare(token, TOKEN)
  317. # end
  318. # end
  319. # end
  320. #
  321. #
  322. # Here is a more advanced Token example where only Atom feeds and the XML API is protected by HTTP token authentication,
  323. # the regular HTML interface is protected by a session approach:
  324. #
  325. # class ApplicationController < ActionController::Base
  326. # before_action :set_account, :authenticate
  327. #
  328. # private
  329. # def set_account
  330. # @account = Account.find_by(url_name: request.subdomains.first)
  331. # end
  332. #
  333. # def authenticate
  334. # case request.format
  335. # when Mime[:xml], Mime[:atom]
  336. # if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) }
  337. # @current_user = user
  338. # else
  339. # request_http_token_authentication
  340. # end
  341. # else
  342. # if session_authenticated?
  343. # @current_user = @account.users.find(session[:authenticated][:user_id])
  344. # else
  345. # redirect_to(login_url) and return false
  346. # end
  347. # end
  348. # end
  349. # end
  350. #
  351. #
  352. # In your integration tests, you can do something like this:
  353. #
  354. # def test_access_granted_from_xml
  355. # authorization = ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
  356. #
  357. # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
  358. #
  359. # assert_equal 200, status
  360. # end
  361. #
  362. #
  363. # On shared hosts, Apache sometimes doesn't pass authentication headers to
  364. # FCGI instances. If your environment matches this description and you cannot
  365. # authenticate, try this rule in your Apache setup:
  366. #
  367. # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
  368. 1 module Token
  369. 1 TOKEN_KEY = "token="
  370. 1 TOKEN_REGEX = /^(Token|Bearer)\s+/
  371. 1 AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/
  372. 1 extend self
  373. 1 module ControllerMethods
  374. 1 def authenticate_or_request_with_http_token(realm = "Application", message = nil, &login_procedure)
  375. authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm, message)
  376. end
  377. 1 def authenticate_with_http_token(&login_procedure)
  378. Token.authenticate(self, &login_procedure)
  379. end
  380. 1 def request_http_token_authentication(realm = "Application", message = nil)
  381. Token.authentication_request(self, realm, message)
  382. end
  383. end
  384. # If token Authorization header is present, call the login
  385. # procedure with the present token and options.
  386. #
  387. # [controller]
  388. # ActionController::Base instance for the current request.
  389. #
  390. # [login_procedure]
  391. # Proc to call if a token is present. The Proc should take two arguments:
  392. #
  393. # authenticate(controller) { |token, options| ... }
  394. #
  395. # Returns the return value of <tt>login_procedure</tt> if a
  396. # token is found. Returns <tt>nil</tt> if no token is found.
  397. 1 def authenticate(controller, &login_procedure)
  398. token, options = token_and_options(controller.request)
  399. unless token.blank?
  400. login_procedure.call(token, options)
  401. end
  402. end
  403. # Parses the token and options out of the token Authorization header.
  404. # The value for the Authorization header is expected to have the prefix
  405. # <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this:
  406. # Authorization: Token token="abc", nonce="def"
  407. # Then the returned token is <tt>"abc"</tt>, and the options are
  408. # <tt>{nonce: "def"}</tt>
  409. #
  410. # request - ActionDispatch::Request instance with the current headers.
  411. #
  412. # Returns an +Array+ of <tt>[String, Hash]</tt> if a token is present.
  413. # Returns +nil+ if no token is found.
  414. 1 def token_and_options(request)
  415. authorization_request = request.authorization.to_s
  416. if authorization_request[TOKEN_REGEX]
  417. params = token_params_from authorization_request
  418. [params.shift[1], Hash[params].with_indifferent_access]
  419. end
  420. end
  421. 1 def token_params_from(auth)
  422. rewrite_param_values params_array_from raw_params auth
  423. end
  424. # Takes raw_params and turns it into an array of parameters
  425. 1 def params_array_from(raw_params)
  426. raw_params.map { |param| param.split %r/=(.+)?/ }
  427. end
  428. # This removes the <tt>"</tt> characters wrapping the value.
  429. 1 def rewrite_param_values(array_params)
  430. array_params.each { |param| (param[1] || +"").gsub! %r/^"|"$/, "" }
  431. end
  432. # This method takes an authorization body and splits up the key-value
  433. # pairs by the standardized <tt>:</tt>, <tt>;</tt>, or <tt>\t</tt>
  434. # delimiters defined in +AUTHN_PAIR_DELIMITERS+.
  435. 1 def raw_params(auth)
  436. _raw_params = auth.sub(TOKEN_REGEX, "").split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
  437. if !_raw_params.first.start_with?(TOKEN_KEY)
  438. _raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}"
  439. end
  440. _raw_params
  441. end
  442. # Encodes the given token and options into an Authorization header value.
  443. #
  444. # token - String token.
  445. # options - optional Hash of the options.
  446. #
  447. # Returns String.
  448. 1 def encode_credentials(token, options = {})
  449. values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value|
  450. "#{key}=#{value.to_s.inspect}"
  451. end
  452. "Token #{values * ", "}"
  453. end
  454. # Sets a WWW-Authenticate header to let the client know a token is desired.
  455. #
  456. # controller - ActionController::Base instance for the outgoing response.
  457. # realm - String realm to use in the header.
  458. #
  459. # Returns nothing.
  460. 1 def authentication_request(controller, realm, message = nil)
  461. message ||= "HTTP Token: Access denied.\n"
  462. controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"', "")}")
  463. controller.__send__ :render, plain: message, status: :unauthorized
  464. end
  465. end
  466. end
  467. end

lib/action_controller/metal/implicit_render.rb

35.0% lines covered

20 relevant lines. 7 lines covered and 13 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. # Handles implicit rendering for a controller action that does not
  4. # explicitly respond with +render+, +respond_to+, +redirect+, or +head+.
  5. #
  6. # For API controllers, the implicit response is always <tt>204 No Content</tt>.
  7. #
  8. # For all other controllers, we use these heuristics to decide whether to
  9. # render a template, raise an error for a missing template, or respond with
  10. # <tt>204 No Content</tt>:
  11. #
  12. # First, if we DO find a template, it's rendered. Template lookup accounts
  13. # for the action name, locales, format, variant, template handlers, and more
  14. # (see +render+ for details).
  15. #
  16. # Second, if we DON'T find a template but the controller action does have
  17. # templates for other formats, variants, etc., then we trust that you meant
  18. # to provide a template for this response, too, and we raise
  19. # <tt>ActionController::UnknownFormat</tt> with an explanation.
  20. #
  21. # Third, if we DON'T find a template AND the request is a page load in a web
  22. # browser (technically, a non-XHR GET request for an HTML response) where
  23. # you reasonably expect to have rendered a template, then we raise
  24. # <tt>ActionController::MissingExactTemplate</tt> with an explanation.
  25. #
  26. # Finally, if we DON'T find a template AND the request isn't a browser page
  27. # load, then we implicitly respond with <tt>204 No Content</tt>.
  28. 1 module ImplicitRender
  29. # :stopdoc:
  30. 1 include BasicImplicitRender
  31. 1 def default_render
  32. if template_exists?(action_name.to_s, _prefixes, variants: request.variant)
  33. render
  34. elsif any_templates?(action_name.to_s, _prefixes)
  35. message = "#{self.class.name}\##{action_name} is missing a template " \
  36. "for this request format and variant.\n" \
  37. "\nrequest.formats: #{request.formats.map(&:to_s).inspect}" \
  38. "\nrequest.variant: #{request.variant.inspect}"
  39. raise ActionController::UnknownFormat, message
  40. elsif interactive_browser_request?
  41. message = "#{self.class.name}\##{action_name} is missing a template for request formats: #{request.formats.map(&:to_s).join(',')}"
  42. raise ActionController::MissingExactTemplate, message
  43. else
  44. logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
  45. super
  46. end
  47. end
  48. 1 def method_for_action(action_name)
  49. super || if template_exists?(action_name.to_s, _prefixes)
  50. "default_render"
  51. end
  52. end
  53. 1 private
  54. 1 def interactive_browser_request?
  55. request.get? && request.format == Mime[:html] && !request.xhr?
  56. end
  57. end
  58. end

lib/action_controller/metal/instrumentation.rb

40.0% lines covered

45 relevant lines. 18 lines covered and 27 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "benchmark"
  3. 1 require "abstract_controller/logger"
  4. 1 module ActionController
  5. # Adds instrumentation to several ends in ActionController::Base. It also provides
  6. # some hooks related with process_action. This allows an ORM like Active Record
  7. # and/or DataMapper to plug in ActionController and show related information.
  8. #
  9. # Check ActiveRecord::Railties::ControllerRuntime for an example.
  10. 1 module Instrumentation
  11. 1 extend ActiveSupport::Concern
  12. 1 include AbstractController::Logger
  13. 1 attr_internal :view_runtime
  14. 1 def process_action(*)
  15. raw_payload = {
  16. controller: self.class.name,
  17. action: action_name,
  18. request: request,
  19. params: request.filtered_parameters,
  20. headers: request.headers,
  21. format: request.format.ref,
  22. method: request.request_method,
  23. path: request.fullpath
  24. }
  25. ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload)
  26. ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
  27. result = super
  28. payload[:response] = response
  29. payload[:status] = response.status
  30. result
  31. ensure
  32. append_info_to_payload(payload)
  33. end
  34. end
  35. 1 def render(*)
  36. render_output = nil
  37. self.view_runtime = cleanup_view_runtime do
  38. Benchmark.ms { render_output = super }
  39. end
  40. render_output
  41. end
  42. 1 def send_file(path, options = {})
  43. ActiveSupport::Notifications.instrument("send_file.action_controller",
  44. options.merge(path: path)) do
  45. super
  46. end
  47. end
  48. 1 def send_data(data, options = {})
  49. ActiveSupport::Notifications.instrument("send_data.action_controller", options) do
  50. super
  51. end
  52. end
  53. 1 def redirect_to(*)
  54. ActiveSupport::Notifications.instrument("redirect_to.action_controller", request: request) do |payload|
  55. result = super
  56. payload[:status] = response.status
  57. payload[:location] = response.filtered_location
  58. result
  59. end
  60. end
  61. 1 private
  62. # A hook invoked every time a before callback is halted.
  63. 1 def halted_callback_hook(filter, _)
  64. ActiveSupport::Notifications.instrument("halted_callback.action_controller", filter: filter)
  65. end
  66. # A hook which allows you to clean up any time, wrongly taken into account in
  67. # views, like database querying time.
  68. #
  69. # def cleanup_view_runtime
  70. # super - time_taken_in_something_expensive
  71. # end
  72. 1 def cleanup_view_runtime # :doc:
  73. yield
  74. end
  75. # Every time after an action is processed, this method is invoked
  76. # with the payload, so you can add more information.
  77. 1 def append_info_to_payload(payload) # :doc:
  78. payload[:view_runtime] = view_runtime
  79. end
  80. 1 module ClassMethods
  81. # A hook which allows other frameworks to log what happened during
  82. # controller process action. This method should return an array
  83. # with the messages to be added.
  84. 1 def log_process_action(payload) #:nodoc:
  85. messages, view_runtime = [], payload[:view_runtime]
  86. messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
  87. messages
  88. end
  89. end
  90. end
  91. end

lib/action_controller/metal/live.rb

30.33% lines covered

122 relevant lines. 37 lines covered and 85 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_dispatch/http/response"
  3. 1 require "delegate"
  4. 1 require "active_support/json"
  5. 1 module ActionController
  6. # Mix this module into your controller, and all actions in that controller
  7. # will be able to stream data to the client as it's written.
  8. #
  9. # class MyController < ActionController::Base
  10. # include ActionController::Live
  11. #
  12. # def stream
  13. # response.headers['Content-Type'] = 'text/event-stream'
  14. # 100.times {
  15. # response.stream.write "hello world\n"
  16. # sleep 1
  17. # }
  18. # ensure
  19. # response.stream.close
  20. # end
  21. # end
  22. #
  23. # There are a few caveats with this module. You *cannot* write headers after the
  24. # response has been committed (Response#committed? will return truthy).
  25. # Calling +write+ or +close+ on the response stream will cause the response
  26. # object to be committed. Make sure all headers are set before calling write
  27. # or close on your stream.
  28. #
  29. # You *must* call close on your stream when you're finished, otherwise the
  30. # socket may be left open forever.
  31. #
  32. # The final caveat is that your actions are executed in a separate thread than
  33. # the main thread. Make sure your actions are thread safe, and this shouldn't
  34. # be a problem (don't share state across threads, etc).
  35. 1 module Live
  36. 1 extend ActiveSupport::Concern
  37. 1 module ClassMethods
  38. 1 def make_response!(request)
  39. if request.get_header("HTTP_VERSION") == "HTTP/1.0"
  40. super
  41. else
  42. Live::Response.new.tap do |res|
  43. res.request = request
  44. end
  45. end
  46. end
  47. end
  48. # This class provides the ability to write an SSE (Server Sent Event)
  49. # to an IO stream. The class is initialized with a stream and can be used
  50. # to either write a JSON string or an object which can be converted to JSON.
  51. #
  52. # Writing an object will convert it into standard SSE format with whatever
  53. # options you have configured. You may choose to set the following options:
  54. #
  55. # 1) Event. If specified, an event with this name will be dispatched on
  56. # the browser.
  57. # 2) Retry. The reconnection time in milliseconds used when attempting
  58. # to send the event.
  59. # 3) Id. If the connection dies while sending an SSE to the browser, then
  60. # the server will receive a +Last-Event-ID+ header with value equal to +id+.
  61. #
  62. # After setting an option in the constructor of the SSE object, all future
  63. # SSEs sent across the stream will use those options unless overridden.
  64. #
  65. # Example Usage:
  66. #
  67. # class MyController < ActionController::Base
  68. # include ActionController::Live
  69. #
  70. # def index
  71. # response.headers['Content-Type'] = 'text/event-stream'
  72. # sse = SSE.new(response.stream, retry: 300, event: "event-name")
  73. # sse.write({ name: 'John'})
  74. # sse.write({ name: 'John'}, id: 10)
  75. # sse.write({ name: 'John'}, id: 10, event: "other-event")
  76. # sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
  77. # ensure
  78. # sse.close
  79. # end
  80. # end
  81. #
  82. # Note: SSEs are not currently supported by IE. However, they are supported
  83. # by Chrome, Firefox, Opera, and Safari.
  84. 1 class SSE
  85. 1 PERMITTED_OPTIONS = %w( retry event id )
  86. 1 def initialize(stream, options = {})
  87. @stream = stream
  88. @options = options
  89. end
  90. 1 def close
  91. @stream.close
  92. end
  93. 1 def write(object, options = {})
  94. case object
  95. when String
  96. perform_write(object, options)
  97. else
  98. perform_write(ActiveSupport::JSON.encode(object), options)
  99. end
  100. end
  101. 1 private
  102. 1 def perform_write(json, options)
  103. current_options = @options.merge(options).stringify_keys
  104. PERMITTED_OPTIONS.each do |option_name|
  105. if (option_value = current_options[option_name])
  106. @stream.write "#{option_name}: #{option_value}\n"
  107. end
  108. end
  109. message = json.gsub("\n", "\ndata: ")
  110. @stream.write "data: #{message}\n\n"
  111. end
  112. end
  113. 1 class ClientDisconnected < RuntimeError
  114. end
  115. 1 class Buffer < ActionDispatch::Response::Buffer #:nodoc:
  116. 1 include MonitorMixin
  117. # Ignore that the client has disconnected.
  118. #
  119. # If this value is `true`, calling `write` after the client
  120. # disconnects will result in the written content being silently
  121. # discarded. If this value is `false` (the default), a
  122. # ClientDisconnected exception will be raised.
  123. 1 attr_accessor :ignore_disconnect
  124. 1 def initialize(response)
  125. super(response, SizedQueue.new(10))
  126. @error_callback = lambda { true }
  127. @cv = new_cond
  128. @aborted = false
  129. @ignore_disconnect = false
  130. end
  131. 1 def write(string)
  132. unless @response.committed?
  133. @response.headers["Cache-Control"] ||= "no-cache"
  134. @response.delete_header "Content-Length"
  135. end
  136. super
  137. unless connected?
  138. @buf.clear
  139. unless @ignore_disconnect
  140. # Raise ClientDisconnected, which is a RuntimeError (not an
  141. # IOError), because that's more appropriate for something beyond
  142. # the developer's control.
  143. raise ClientDisconnected, "client disconnected"
  144. end
  145. end
  146. end
  147. # Write a 'close' event to the buffer; the producer/writing thread
  148. # uses this to notify us that it's finished supplying content.
  149. #
  150. # See also #abort.
  151. 1 def close
  152. synchronize do
  153. super
  154. @buf.push nil
  155. @cv.broadcast
  156. end
  157. end
  158. # Inform the producer/writing thread that the client has
  159. # disconnected; the reading thread is no longer interested in
  160. # anything that's being written.
  161. #
  162. # See also #close.
  163. 1 def abort
  164. synchronize do
  165. @aborted = true
  166. @buf.clear
  167. end
  168. end
  169. # Is the client still connected and waiting for content?
  170. #
  171. # The result of calling `write` when this is `false` is determined
  172. # by `ignore_disconnect`.
  173. 1 def connected?
  174. !@aborted
  175. end
  176. 1 def on_error(&block)
  177. @error_callback = block
  178. end
  179. 1 def call_on_error
  180. @error_callback.call
  181. end
  182. 1 private
  183. 1 def each_chunk(&block)
  184. loop do
  185. str = nil
  186. ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
  187. str = @buf.pop
  188. end
  189. break unless str
  190. yield str
  191. end
  192. end
  193. end
  194. 1 class Response < ActionDispatch::Response #:nodoc: all
  195. 1 private
  196. 1 def before_committed
  197. super
  198. jar = request.cookie_jar
  199. # The response can be committed multiple times
  200. jar.write self unless committed?
  201. end
  202. 1 def build_buffer(response, body)
  203. buf = Live::Buffer.new response
  204. body.each { |part| buf.write part }
  205. buf
  206. end
  207. end
  208. 1 def process(name)
  209. t1 = Thread.current
  210. locals = t1.keys.map { |key| [key, t1[key]] }
  211. error = nil
  212. # This processes the action in a child thread. It lets us return the
  213. # response code and headers back up the Rack stack, and still process
  214. # the body in parallel with sending data to the client.
  215. new_controller_thread {
  216. ActiveSupport::Dependencies.interlock.running do
  217. t2 = Thread.current
  218. # Since we're processing the view in a different thread, copy the
  219. # thread locals from the main thread to the child thread. :'(
  220. locals.each { |k, v| t2[k] = v }
  221. begin
  222. super(name)
  223. rescue => e
  224. if @_response.committed?
  225. begin
  226. @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html
  227. @_response.stream.call_on_error
  228. rescue => exception
  229. log_error(exception)
  230. ensure
  231. log_error(e)
  232. @_response.stream.close
  233. end
  234. else
  235. error = e
  236. end
  237. ensure
  238. @_response.commit!
  239. end
  240. end
  241. }
  242. ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
  243. @_response.await_commit
  244. end
  245. raise error if error
  246. end
  247. 1 def response_body=(body)
  248. super
  249. response.close if response
  250. end
  251. 1 private
  252. # Spawn a new thread to serve up the controller in. This is to get
  253. # around the fact that Rack isn't based around IOs and we need to use
  254. # a thread to stream data from the response bodies. Nobody should call
  255. # this method except in Rails internals. Seriously!
  256. 1 def new_controller_thread # :nodoc:
  257. Thread.new {
  258. t2 = Thread.current
  259. t2.abort_on_exception = true
  260. yield
  261. }
  262. end
  263. 1 def log_error(exception)
  264. logger = ActionController::Base.logger
  265. return unless logger
  266. logger.fatal do
  267. message = +"\n#{exception.class} (#{exception.message}):\n"
  268. message << exception.annotated_source_code.to_s if exception.respond_to?(:annotated_source_code)
  269. message << " " << exception.backtrace.join("\n ")
  270. "#{message}\n\n"
  271. end
  272. end
  273. end
  274. end

lib/action_controller/metal/logging.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. 1 module Logging
  4. 1 extend ActiveSupport::Concern
  5. 1 module ClassMethods
  6. # Set a different log level per request.
  7. #
  8. # # Use the debug log level if a particular cookie is set.
  9. # class ApplicationController < ActionController::Base
  10. # log_at :debug, if: -> { cookies[:debug] }
  11. # end
  12. #
  13. 1 def log_at(level, **options)
  14. 2 around_action ->(_, action) { logger.log_at(level, &action) }, **options
  15. end
  16. end
  17. end
  18. end

lib/action_controller/metal/mime_responds.rb

34.38% lines covered

64 relevant lines. 22 lines covered and 42 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "abstract_controller/collector"
  3. 1 module ActionController #:nodoc:
  4. 1 module MimeResponds
  5. # Without web-service support, an action which collects the data for displaying a list of people
  6. # might look something like this:
  7. #
  8. # def index
  9. # @people = Person.all
  10. # end
  11. #
  12. # That action implicitly responds to all formats, but formats can also be explicitly enumerated:
  13. #
  14. # def index
  15. # @people = Person.all
  16. # respond_to :html, :js
  17. # end
  18. #
  19. # Here's the same action, with web-service support baked in:
  20. #
  21. # def index
  22. # @people = Person.all
  23. #
  24. # respond_to do |format|
  25. # format.html
  26. # format.js
  27. # format.xml { render xml: @people }
  28. # end
  29. # end
  30. #
  31. # What that says is, "if the client wants HTML or JS in response to this action, just respond as we
  32. # would have before, but if the client wants XML, return them the list of people in XML format."
  33. # (Rails determines the desired response format from the HTTP Accept header submitted by the client.)
  34. #
  35. # Supposing you have an action that adds a new person, optionally creating their company
  36. # (by name) if it does not already exist, without web-services, it might look like this:
  37. #
  38. # def create
  39. # @company = Company.find_or_create_by(name: params[:company][:name])
  40. # @person = @company.people.create(params[:person])
  41. #
  42. # redirect_to(person_list_url)
  43. # end
  44. #
  45. # Here's the same action, with web-service support baked in:
  46. #
  47. # def create
  48. # company = params[:person].delete(:company)
  49. # @company = Company.find_or_create_by(name: company[:name])
  50. # @person = @company.people.create(params[:person])
  51. #
  52. # respond_to do |format|
  53. # format.html { redirect_to(person_list_url) }
  54. # format.js
  55. # format.xml { render xml: @person.to_xml(include: @company) }
  56. # end
  57. # end
  58. #
  59. # If the client wants HTML, we just redirect them back to the person list. If they want JavaScript,
  60. # then it is an Ajax request and we render the JavaScript template associated with this action.
  61. # Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also
  62. # include the person's company in the rendered XML, so you get something like this:
  63. #
  64. # <person>
  65. # <id>...</id>
  66. # ...
  67. # <company>
  68. # <id>...</id>
  69. # <name>...</name>
  70. # ...
  71. # </company>
  72. # </person>
  73. #
  74. # Note, however, the extra bit at the top of that action:
  75. #
  76. # company = params[:person].delete(:company)
  77. # @company = Company.find_or_create_by(name: company[:name])
  78. #
  79. # This is because the incoming XML document (if a web-service request is in process) can only contain a
  80. # single root-node. So, we have to rearrange things so that the request looks like this (url-encoded):
  81. #
  82. # person[name]=...&person[company][name]=...&...
  83. #
  84. # And, like this (xml-encoded):
  85. #
  86. # <person>
  87. # <name>...</name>
  88. # <company>
  89. # <name>...</name>
  90. # </company>
  91. # </person>
  92. #
  93. # In other words, we make the request so that it operates on a single entity's person. Then, in the action,
  94. # we extract the company data from the request, find or create the company, and then create the new person
  95. # with the remaining data.
  96. #
  97. # Note that you can define your own XML parameter parser which would allow you to describe multiple entities
  98. # in a single request (i.e., by wrapping them all in a single root node), but if you just go with the flow
  99. # and accept Rails' defaults, life will be much easier.
  100. #
  101. # If you need to use a MIME type which isn't supported by default, you can register your own handlers in
  102. # +config/initializers/mime_types.rb+ as follows.
  103. #
  104. # Mime::Type.register "image/jpg", :jpg
  105. #
  106. # +respond_to+ also allows you to specify a common block for different formats by using +any+:
  107. #
  108. # def index
  109. # @people = Person.all
  110. #
  111. # respond_to do |format|
  112. # format.html
  113. # format.any(:xml, :json) { render request.format.to_sym => @people }
  114. # end
  115. # end
  116. #
  117. # In the example above, if the format is xml, it will render:
  118. #
  119. # render xml: @people
  120. #
  121. # Or if the format is json:
  122. #
  123. # render json: @people
  124. #
  125. # +any+ can also be used with no arguments, in which case it will be used for any format requested by
  126. # the user:
  127. #
  128. # respond_to do |format|
  129. # format.html
  130. # format.any { redirect_to support_path }
  131. # end
  132. #
  133. # Formats can have different variants.
  134. #
  135. # The request variant is a specialization of the request format, like <tt>:tablet</tt>,
  136. # <tt>:phone</tt>, or <tt>:desktop</tt>.
  137. #
  138. # We often want to render different html/json/xml templates for phones,
  139. # tablets, and desktop browsers. Variants make it easy.
  140. #
  141. # You can set the variant in a +before_action+:
  142. #
  143. # request.variant = :tablet if /iPad/.match?(request.user_agent)
  144. #
  145. # Respond to variants in the action just like you respond to formats:
  146. #
  147. # respond_to do |format|
  148. # format.html do |variant|
  149. # variant.tablet # renders app/views/projects/show.html+tablet.erb
  150. # variant.phone { extra_setup; render ... }
  151. # variant.none { special_setup } # executed only if there is no variant set
  152. # end
  153. # end
  154. #
  155. # Provide separate templates for each format and variant:
  156. #
  157. # app/views/projects/show.html.erb
  158. # app/views/projects/show.html+tablet.erb
  159. # app/views/projects/show.html+phone.erb
  160. #
  161. # When you're not sharing any code within the format, you can simplify defining variants
  162. # using the inline syntax:
  163. #
  164. # respond_to do |format|
  165. # format.js { render "trash" }
  166. # format.html.phone { redirect_to progress_path }
  167. # format.html.none { render "trash" }
  168. # end
  169. #
  170. # Variants also support common +any+/+all+ block that formats have.
  171. #
  172. # It works for both inline:
  173. #
  174. # respond_to do |format|
  175. # format.html.any { render html: "any" }
  176. # format.html.phone { render html: "phone" }
  177. # end
  178. #
  179. # and block syntax:
  180. #
  181. # respond_to do |format|
  182. # format.html do |variant|
  183. # variant.any(:tablet, :phablet){ render html: "any" }
  184. # variant.phone { render html: "phone" }
  185. # end
  186. # end
  187. #
  188. # You can also set an array of variants:
  189. #
  190. # request.variant = [:tablet, :phone]
  191. #
  192. # This will work similarly to formats and MIME types negotiation. If there
  193. # is no +:tablet+ variant declared, the +:phone+ variant will be used:
  194. #
  195. # respond_to do |format|
  196. # format.html.none
  197. # format.html.phone # this gets rendered
  198. # end
  199. 1 def respond_to(*mimes)
  200. raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
  201. collector = Collector.new(mimes, request.variant)
  202. yield collector if block_given?
  203. if format = collector.negotiate_format(request)
  204. if media_type && media_type != format
  205. raise ActionController::RespondToMismatchError
  206. end
  207. _process_format(format)
  208. _set_rendered_content_type(format) unless collector.any_response?
  209. response = collector.response
  210. response.call if response
  211. else
  212. raise ActionController::UnknownFormat
  213. end
  214. end
  215. # A container for responses available from the current controller for
  216. # requests for different mime-types sent to a particular action.
  217. #
  218. # The public controller methods +respond_to+ may be called with a block
  219. # that is used to define responses to different mime-types, e.g.
  220. # for +respond_to+ :
  221. #
  222. # respond_to do |format|
  223. # format.html
  224. # format.xml { render xml: @people }
  225. # end
  226. #
  227. # In this usage, the argument passed to the block (+format+ above) is an
  228. # instance of the ActionController::MimeResponds::Collector class. This
  229. # object serves as a container in which available responses can be stored by
  230. # calling any of the dynamically generated, mime-type-specific methods such
  231. # as +html+, +xml+ etc on the Collector. Each response is represented by a
  232. # corresponding block if present.
  233. #
  234. # A subsequent call to #negotiate_format(request) will enable the Collector
  235. # to determine which specific mime-type it should respond with for the current
  236. # request, with this response then being accessible by calling #response.
  237. 1 class Collector
  238. 1 include AbstractController::Collector
  239. 1 attr_accessor :format
  240. 1 def initialize(mimes, variant = nil)
  241. @responses = {}
  242. @variant = variant
  243. mimes.each { |mime| @responses[Mime[mime]] = nil }
  244. end
  245. 1 def any(*args, &block)
  246. if args.any?
  247. args.each { |type| send(type, &block) }
  248. else
  249. custom(Mime::ALL, &block)
  250. end
  251. end
  252. 1 alias :all :any
  253. 1 def custom(mime_type, &block)
  254. mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
  255. @responses[mime_type] ||= if block_given?
  256. block
  257. else
  258. VariantCollector.new(@variant)
  259. end
  260. end
  261. 1 def any_response?
  262. !@responses.fetch(format, false) && @responses[Mime::ALL]
  263. end
  264. 1 def response
  265. response = @responses.fetch(format, @responses[Mime::ALL])
  266. if response.is_a?(VariantCollector) # `format.html.phone` - variant inline syntax
  267. response.variant
  268. elsif response.nil? || response.arity == 0 # `format.html` - just a format, call its block
  269. response
  270. else # `format.html{ |variant| variant.phone }` - variant block syntax
  271. variant_collector = VariantCollector.new(@variant)
  272. response.call(variant_collector) # call format block with variants collector
  273. variant_collector.variant
  274. end
  275. end
  276. 1 def negotiate_format(request)
  277. @format = request.negotiate_mime(@responses.keys)
  278. end
  279. 1 class VariantCollector #:nodoc:
  280. 1 def initialize(variant = nil)
  281. @variant = variant
  282. @variants = {}
  283. end
  284. 1 def any(*args, &block)
  285. if block_given?
  286. if args.any? && args.none? { |a| a == @variant }
  287. args.each { |v| @variants[v] = block }
  288. else
  289. @variants[:any] = block
  290. end
  291. end
  292. end
  293. 1 alias :all :any
  294. 1 def method_missing(name, *args, &block)
  295. @variants[name] = block if block_given?
  296. end
  297. 1 def variant
  298. if @variant.empty?
  299. @variants[:none] || @variants[:any]
  300. else
  301. @variants[variant_key]
  302. end
  303. end
  304. 1 private
  305. 1 def variant_key
  306. @variant.find { |variant| @variants.key?(variant) } || :any
  307. end
  308. end
  309. end
  310. end
  311. end

lib/action_controller/metal/parameter_encoding.rb

92.31% lines covered

13 relevant lines. 12 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. # Specify binary encoding for parameters for a given action.
  4. 1 module ParameterEncoding
  5. 1 extend ActiveSupport::Concern
  6. 1 module ClassMethods
  7. 1 def inherited(klass) # :nodoc:
  8. 271 super
  9. 271 klass.setup_param_encode
  10. end
  11. 1 def setup_param_encode # :nodoc:
  12. 271 @_parameter_encodings = {}
  13. end
  14. 1 def binary_params_for?(action) # :nodoc:
  15. @_parameter_encodings[action.to_s]
  16. end
  17. # Specify that a given action's parameters should all be encoded as
  18. # ASCII-8BIT (it "skips" the encoding default of UTF-8).
  19. #
  20. # For example, a controller would use it like this:
  21. #
  22. # class RepositoryController < ActionController::Base
  23. # skip_parameter_encoding :show
  24. #
  25. # def show
  26. # @repo = Repository.find_by_filesystem_path params[:file_path]
  27. #
  28. # # `repo_name` is guaranteed to be UTF-8, but was ASCII-8BIT, so
  29. # # tag it as such
  30. # @repo_name = params[:repo_name].force_encoding 'UTF-8'
  31. # end
  32. #
  33. # def index
  34. # @repositories = Repository.all
  35. # end
  36. # end
  37. #
  38. # The show action in the above controller would have all parameter values
  39. # encoded as ASCII-8BIT. This is useful in the case where an application
  40. # must handle data but encoding of the data is unknown, like file system data.
  41. 1 def skip_parameter_encoding(action)
  42. 2 @_parameter_encodings[action.to_s] = true
  43. end
  44. end
  45. end
  46. end

lib/action_controller/metal/params_wrapper.rb

47.62% lines covered

105 relevant lines. 50 lines covered and 55 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/hash/slice"
  3. 1 require "active_support/core_ext/hash/except"
  4. 1 require "active_support/core_ext/module/anonymous"
  5. 1 require "action_dispatch/http/mime_type"
  6. 1 module ActionController
  7. # Wraps the parameters hash into a nested hash. This will allow clients to
  8. # submit requests without having to specify any root elements.
  9. #
  10. # This functionality is enabled in +config/initializers/wrap_parameters.rb+
  11. # and can be customized.
  12. #
  13. # You could also turn it on per controller by setting the format array to
  14. # a non-empty array:
  15. #
  16. # class UsersController < ApplicationController
  17. # wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]
  18. # end
  19. #
  20. # If you enable +ParamsWrapper+ for +:json+ format, instead of having to
  21. # send JSON parameters like this:
  22. #
  23. # {"user": {"name": "Konata"}}
  24. #
  25. # You can send parameters like this:
  26. #
  27. # {"name": "Konata"}
  28. #
  29. # And it will be wrapped into a nested hash with the key name matching the
  30. # controller's name. For example, if you're posting to +UsersController+,
  31. # your new +params+ hash will look like this:
  32. #
  33. # {"name" => "Konata", "user" => {"name" => "Konata"}}
  34. #
  35. # You can also specify the key in which the parameters should be wrapped to,
  36. # and also the list of attributes it should wrap by using either +:include+ or
  37. # +:exclude+ options like this:
  38. #
  39. # class UsersController < ApplicationController
  40. # wrap_parameters :person, include: [:username, :password]
  41. # end
  42. #
  43. # On Active Record models with no +:include+ or +:exclude+ option set,
  44. # it will only wrap the parameters returned by the class method
  45. # <tt>attribute_names</tt>.
  46. #
  47. # If you're going to pass the parameters to an +ActiveModel+ object (such as
  48. # <tt>User.new(params[:user])</tt>), you might consider passing the model class to
  49. # the method instead. The +ParamsWrapper+ will actually try to determine the
  50. # list of attribute names from the model and only wrap those attributes:
  51. #
  52. # class UsersController < ApplicationController
  53. # wrap_parameters Person
  54. # end
  55. #
  56. # You still could pass +:include+ and +:exclude+ to set the list of attributes
  57. # you want to wrap.
  58. #
  59. # By default, if you don't specify the key in which the parameters would be
  60. # wrapped to, +ParamsWrapper+ will actually try to determine if there's
  61. # a model related to it or not. This controller, for example:
  62. #
  63. # class Admin::UsersController < ApplicationController
  64. # end
  65. #
  66. # will try to check if <tt>Admin::User</tt> or +User+ model exists, and use it to
  67. # determine the wrapper key respectively. If both models don't exist,
  68. # it will then fallback to use +user+ as the key.
  69. 1 module ParamsWrapper
  70. 1 extend ActiveSupport::Concern
  71. 1 EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8)
  72. 1 require "mutex_m"
  73. 1 class Options < Struct.new(:name, :format, :include, :exclude, :klass, :model) # :nodoc:
  74. 1 include Mutex_m
  75. 1 def self.from_hash(hash)
  76. 5 name = hash[:name]
  77. 5 format = Array(hash[:format])
  78. 5 include = hash[:include] && Array(hash[:include]).collect(&:to_s)
  79. 5 exclude = hash[:exclude] && Array(hash[:exclude]).collect(&:to_s)
  80. 5 new name, format, include, exclude, nil, nil
  81. end
  82. 1 def initialize(name, format, include, exclude, klass, model) # :nodoc:
  83. 5 super
  84. 5 @include_set = include
  85. 5 @name_set = name
  86. end
  87. 1 def model
  88. super || self.model = _default_wrap_model
  89. end
  90. 1 def include
  91. return super if @include_set
  92. m = model
  93. synchronize do
  94. return super if @include_set
  95. @include_set = true
  96. unless super || exclude
  97. if m.respond_to?(:attribute_names) && m.attribute_names.any?
  98. if m.respond_to?(:stored_attributes) && !m.stored_attributes.empty?
  99. self.include = m.attribute_names + m.stored_attributes.values.flatten.map(&:to_s)
  100. else
  101. self.include = m.attribute_names
  102. end
  103. if m.respond_to?(:nested_attributes_options) && m.nested_attributes_options.keys.any?
  104. self.include += m.nested_attributes_options.keys.map do |key|
  105. (+key.to_s).concat("_attributes")
  106. end
  107. end
  108. self.include
  109. end
  110. end
  111. end
  112. end
  113. 1 def name
  114. return super if @name_set
  115. m = model
  116. synchronize do
  117. return super if @name_set
  118. @name_set = true
  119. unless super || klass.anonymous?
  120. self.name = m ? m.to_s.demodulize.underscore :
  121. klass.controller_name.singularize
  122. end
  123. end
  124. end
  125. 1 private
  126. # Determine the wrapper model from the controller's name. By convention,
  127. # this could be done by trying to find the defined model that has the
  128. # same singular name as the controller. For example, +UsersController+
  129. # will try to find if the +User+ model exists.
  130. #
  131. # This method also does namespace lookup. Foo::Bar::UsersController will
  132. # try to find Foo::Bar::User, Foo::User and finally User.
  133. 1 def _default_wrap_model
  134. return nil if klass.anonymous?
  135. model_name = klass.name.delete_suffix("Controller").classify
  136. begin
  137. if model_klass = model_name.safe_constantize
  138. model_klass
  139. else
  140. namespaces = model_name.split("::")
  141. namespaces.delete_at(-2)
  142. break if namespaces.last == model_name
  143. model_name = namespaces.join("::")
  144. end
  145. end until model_klass
  146. model_klass
  147. end
  148. end
  149. 1 included do
  150. 2 class_attribute :_wrapper_options, default: Options.from_hash(format: [])
  151. end
  152. 1 module ClassMethods
  153. 1 def _set_wrapper_options(options)
  154. self._wrapper_options = Options.from_hash(options)
  155. end
  156. # Sets the name of the wrapper key, or the model which +ParamsWrapper+
  157. # would use to determine the attribute names from.
  158. #
  159. # ==== Examples
  160. # wrap_parameters format: :xml
  161. # # enables the parameter wrapper for XML format
  162. #
  163. # wrap_parameters :person
  164. # # wraps parameters into +params[:person]+ hash
  165. #
  166. # wrap_parameters Person
  167. # # wraps parameters by determining the wrapper key from Person class
  168. # # (+person+, in this case) and the list of attribute names
  169. #
  170. # wrap_parameters include: [:username, :title]
  171. # # wraps only +:username+ and +:title+ attributes from parameters.
  172. #
  173. # wrap_parameters false
  174. # # disables parameters wrapping for this controller altogether.
  175. #
  176. # ==== Options
  177. # * <tt>:format</tt> - The list of formats in which the parameters wrapper
  178. # will be enabled.
  179. # * <tt>:include</tt> - The list of attribute names which parameters wrapper
  180. # will wrap into a nested hash.
  181. # * <tt>:exclude</tt> - The list of attribute names which parameters wrapper
  182. # will exclude from a nested hash.
  183. 1 def wrap_parameters(name_or_model_or_options, options = {})
  184. 3 model = nil
  185. 3 case name_or_model_or_options
  186. when Hash
  187. 1 options = name_or_model_or_options
  188. when false
  189. options = options.merge(format: [])
  190. when Symbol, String
  191. 2 options = options.merge(name: name_or_model_or_options)
  192. else
  193. model = name_or_model_or_options
  194. end
  195. 3 opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options)
  196. 3 opts.model = model
  197. 3 opts.klass = self
  198. 3 self._wrapper_options = opts
  199. end
  200. # Sets the default wrapper key or model which will be used to determine
  201. # wrapper key and attribute names. Called automatically when the
  202. # module is inherited.
  203. 1 def inherited(klass)
  204. 282 if klass._wrapper_options.format.any?
  205. params = klass._wrapper_options.dup
  206. params.klass = klass
  207. klass._wrapper_options = params
  208. end
  209. 282 super
  210. end
  211. end
  212. # Performs parameters wrapping upon the request. Called automatically
  213. # by the metal call stack.
  214. 1 def process_action(*)
  215. _perform_parameter_wrapping if _wrapper_enabled?
  216. super
  217. end
  218. 1 private
  219. # Returns the wrapper key which will be used to store wrapped parameters.
  220. 1 def _wrapper_key
  221. _wrapper_options.name
  222. end
  223. # Returns the list of enabled formats.
  224. 1 def _wrapper_formats
  225. _wrapper_options.format
  226. end
  227. # Returns the list of parameters which will be selected for wrapped.
  228. 1 def _wrap_parameters(parameters)
  229. { _wrapper_key => _extract_parameters(parameters) }
  230. end
  231. 1 def _extract_parameters(parameters)
  232. if include_only = _wrapper_options.include
  233. parameters.slice(*include_only)
  234. else
  235. exclude = _wrapper_options.exclude || []
  236. parameters.except(*(exclude + EXCLUDE_PARAMETERS))
  237. end
  238. end
  239. # Checks if we should perform parameters wrapping.
  240. 1 def _wrapper_enabled?
  241. return false unless request.has_content_type?
  242. ref = request.content_mime_type.ref
  243. _wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key)
  244. end
  245. 1 def _perform_parameter_wrapping
  246. wrapped_hash = _wrap_parameters request.request_parameters
  247. wrapped_keys = request.request_parameters.keys
  248. wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)
  249. # This will make the wrapped hash accessible from controller and view.
  250. request.parameters.merge! wrapped_hash
  251. request.request_parameters.merge! wrapped_hash
  252. # This will display the wrapped hash in the log file.
  253. request.filtered_parameters.merge! wrapped_filtered_hash
  254. rescue ActionDispatch::Http::Parameters::ParseError
  255. # swallow parse error exception
  256. end
  257. end
  258. end

lib/action_controller/metal/redirecting.rb

39.39% lines covered

33 relevant lines. 13 lines covered and 20 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. 1 module Redirecting
  4. 1 extend ActiveSupport::Concern
  5. 1 include AbstractController::Logger
  6. 1 include ActionController::UrlFor
  7. # Redirects the browser to the target specified in +options+. This parameter can be any one of:
  8. #
  9. # * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
  10. # * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record.
  11. # * <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.
  12. # * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string.
  13. # * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+.
  14. #
  15. # === Examples:
  16. #
  17. # redirect_to action: "show", id: 5
  18. # redirect_to @post
  19. # redirect_to "http://www.rubyonrails.org"
  20. # redirect_to "/images/screenshot.jpg"
  21. # redirect_to posts_url
  22. # redirect_to proc { edit_post_url(@post) }
  23. #
  24. # The redirection happens as a <tt>302 Found</tt> header unless otherwise specified using the <tt>:status</tt> option:
  25. #
  26. # redirect_to post_url(@post), status: :found
  27. # redirect_to action: 'atom', status: :moved_permanently
  28. # redirect_to post_url(@post), status: 301
  29. # redirect_to action: 'atom', status: 302
  30. #
  31. # The status code can either be a standard {HTTP Status code}[https://www.iana.org/assignments/http-status-codes] as an
  32. # integer, or a symbol representing the downcased, underscored and symbolized description.
  33. # Note that the status code must be a 3xx HTTP code, or redirection will not occur.
  34. #
  35. # If you are using XHR requests other than GET or POST and redirecting after the
  36. # request then some browsers will follow the redirect using the original request
  37. # method. This may lead to undesirable behavior such as a double DELETE. To work
  38. # around this you can return a <tt>303 See Other</tt> status code which will be
  39. # followed using a GET request.
  40. #
  41. # redirect_to posts_url, status: :see_other
  42. # redirect_to action: 'index', status: 303
  43. #
  44. # 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
  45. # +alert+ and +notice+ as well as a general purpose +flash+ bucket.
  46. #
  47. # redirect_to post_url(@post), alert: "Watch it, mister!"
  48. # redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
  49. # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
  50. # redirect_to({ action: 'atom' }, alert: "Something serious happened")
  51. #
  52. # Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function.
  53. # To terminate the execution of the function immediately after the +redirect_to+, use return.
  54. # redirect_to post_url(@post) and return
  55. 1 def redirect_to(options = {}, response_options = {})
  56. raise ActionControllerError.new("Cannot redirect to nil!") unless options
  57. raise AbstractController::DoubleRenderError if response_body
  58. self.status = _extract_redirect_to_status(options, response_options)
  59. self.location = _compute_redirect_to_location(request, options)
  60. self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
  61. end
  62. # Redirects the browser to the page that issued the request (the referrer)
  63. # if possible, otherwise redirects to the provided default fallback
  64. # location.
  65. #
  66. # The referrer information is pulled from the HTTP +Referer+ (sic) header on
  67. # the request. This is an optional header and its presence on the request is
  68. # subject to browser security settings and user preferences. If the request
  69. # is missing this header, the <tt>fallback_location</tt> will be used.
  70. #
  71. # redirect_back fallback_location: { action: "show", id: 5 }
  72. # redirect_back fallback_location: @post
  73. # redirect_back fallback_location: "http://www.rubyonrails.org"
  74. # redirect_back fallback_location: "/images/screenshot.jpg"
  75. # redirect_back fallback_location: posts_url
  76. # redirect_back fallback_location: proc { edit_post_url(@post) }
  77. # redirect_back fallback_location: '/', allow_other_host: false
  78. #
  79. # ==== Options
  80. # * <tt>:fallback_location</tt> - The default fallback location that will be used on missing +Referer+ header.
  81. # * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true.
  82. #
  83. # All other options that can be passed to #redirect_to are accepted as
  84. # options and the behavior is identical.
  85. 1 def redirect_back(fallback_location:, allow_other_host: true, **args)
  86. referer = request.headers["Referer"]
  87. redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer))
  88. redirect_to redirect_to_referer ? referer : fallback_location, **args
  89. end
  90. 1 def _compute_redirect_to_location(request, options) #:nodoc:
  91. case options
  92. # The scheme name consist of a letter followed by any combination of
  93. # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
  94. # characters; and is terminated by a colon (":").
  95. # See https://tools.ietf.org/html/rfc3986#section-3.1
  96. # The protocol relative scheme starts with a double slash "//".
  97. when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
  98. options
  99. when String
  100. request.protocol + request.host_with_port + options
  101. when Proc
  102. _compute_redirect_to_location request, instance_eval(&options)
  103. else
  104. url_for(options)
  105. end.delete("\0\r\n")
  106. end
  107. 1 module_function :_compute_redirect_to_location
  108. 1 public :_compute_redirect_to_location
  109. 1 private
  110. 1 def _extract_redirect_to_status(options, response_options)
  111. if options.is_a?(Hash) && options.key?(:status)
  112. Rack::Utils.status_code(options.delete(:status))
  113. elsif response_options.key?(:status)
  114. Rack::Utils.status_code(response_options[:status])
  115. else
  116. 302
  117. end
  118. end
  119. 1 def _url_host_allowed?(url)
  120. URI(url.to_s).host == request.host
  121. rescue ArgumentError, URI::Error
  122. false
  123. end
  124. end
  125. end

lib/action_controller/metal/renderers.rb

58.18% lines covered

55 relevant lines. 32 lines covered and 23 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "set"
  3. 1 module ActionController
  4. # See <tt>Renderers.add</tt>
  5. 1 def self.add_renderer(key, &block)
  6. Renderers.add(key, &block)
  7. end
  8. # See <tt>Renderers.remove</tt>
  9. 1 def self.remove_renderer(key)
  10. Renderers.remove(key)
  11. end
  12. # See <tt>Responder#api_behavior</tt>
  13. 1 class MissingRenderer < LoadError
  14. 1 def initialize(format)
  15. super "No renderer defined for format: #{format}"
  16. end
  17. end
  18. 1 module Renderers
  19. 1 extend ActiveSupport::Concern
  20. # A Set containing renderer names that correspond to available renderer procs.
  21. # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>.
  22. 1 RENDERERS = Set.new
  23. 1 included do
  24. 3 class_attribute :_renderers, default: Set.new.freeze
  25. end
  26. # Used in <tt>ActionController::Base</tt>
  27. # and <tt>ActionController::API</tt> to include all
  28. # renderers by default.
  29. 1 module All
  30. 1 extend ActiveSupport::Concern
  31. 1 include Renderers
  32. 1 included do
  33. 2 self._renderers = RENDERERS
  34. end
  35. end
  36. # Adds a new renderer to call within controller actions.
  37. # A renderer is invoked by passing its name as an option to
  38. # <tt>AbstractController::Rendering#render</tt>. To create a renderer
  39. # pass it a name and a block. The block takes two arguments, the first
  40. # is the value paired with its key and the second is the remaining
  41. # hash of options passed to +render+.
  42. #
  43. # Create a csv renderer:
  44. #
  45. # ActionController::Renderers.add :csv do |obj, options|
  46. # filename = options[:filename] || 'data'
  47. # str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s
  48. # send_data str, type: Mime[:csv],
  49. # disposition: "attachment; filename=#{filename}.csv"
  50. # end
  51. #
  52. # Note that we used Mime[:csv] for the csv mime type as it comes with Rails.
  53. # For a custom renderer, you'll need to register a mime type with
  54. # <tt>Mime::Type.register</tt>.
  55. #
  56. # To use the csv renderer in a controller action:
  57. #
  58. # def show
  59. # @csvable = Csvable.find(params[:id])
  60. # respond_to do |format|
  61. # format.html
  62. # format.csv { render csv: @csvable, filename: @csvable.name }
  63. # end
  64. # end
  65. 1 def self.add(key, &block)
  66. 3 define_method(_render_with_renderer_method_name(key), &block)
  67. 3 RENDERERS << key.to_sym
  68. end
  69. # This method is the opposite of add method.
  70. #
  71. # To remove a csv renderer:
  72. #
  73. # ActionController::Renderers.remove(:csv)
  74. 1 def self.remove(key)
  75. RENDERERS.delete(key.to_sym)
  76. method_name = _render_with_renderer_method_name(key)
  77. remove_possible_method(method_name)
  78. end
  79. 1 def self._render_with_renderer_method_name(key)
  80. 3 "_render_with_renderer_#{key}"
  81. end
  82. 1 module ClassMethods
  83. # Adds, by name, a renderer or renderers to the +_renderers+ available
  84. # to call within controller actions.
  85. #
  86. # It is useful when rendering from an <tt>ActionController::Metal</tt> controller or
  87. # otherwise to add an available renderer proc to a specific controller.
  88. #
  89. # Both <tt>ActionController::Base</tt> and <tt>ActionController::API</tt>
  90. # include <tt>ActionController::Renderers::All</tt>, making all renderers
  91. # available in the controller. See <tt>Renderers::RENDERERS</tt> and <tt>Renderers.add</tt>.
  92. #
  93. # Since <tt>ActionController::Metal</tt> controllers cannot render, the controller
  94. # must include <tt>AbstractController::Rendering</tt>, <tt>ActionController::Rendering</tt>,
  95. # and <tt>ActionController::Renderers</tt>, and have at least one renderer.
  96. #
  97. # Rather than including <tt>ActionController::Renderers::All</tt> and including all renderers,
  98. # you may specify which renderers to include by passing the renderer name or names to
  99. # +use_renderers+. For example, a controller that includes only the <tt>:json</tt> renderer
  100. # (+_render_with_renderer_json+) might look like:
  101. #
  102. # class MetalRenderingController < ActionController::Metal
  103. # include AbstractController::Rendering
  104. # include ActionController::Rendering
  105. # include ActionController::Renderers
  106. #
  107. # use_renderers :json
  108. #
  109. # def show
  110. # render json: record
  111. # end
  112. # end
  113. #
  114. # You must specify a +use_renderer+, else the +controller.renderer+ and
  115. # +controller._renderers+ will be <tt>nil</tt>, and the action will fail.
  116. 1 def use_renderers(*args)
  117. 1 renderers = _renderers + args
  118. 1 self._renderers = renderers.freeze
  119. end
  120. 1 alias use_renderer use_renderers
  121. end
  122. # Called by +render+ in <tt>AbstractController::Rendering</tt>
  123. # which sets the return value as the +response_body+.
  124. #
  125. # If no renderer is found, +super+ returns control to
  126. # <tt>ActionView::Rendering.render_to_body</tt>, if present.
  127. 1 def render_to_body(options)
  128. _render_to_body_with_renderer(options) || super
  129. end
  130. 1 def _render_to_body_with_renderer(options)
  131. _renderers.each do |name|
  132. if options.key?(name)
  133. _process_options(options)
  134. method_name = Renderers._render_with_renderer_method_name(name)
  135. return send(method_name, options.delete(name), options)
  136. end
  137. end
  138. nil
  139. end
  140. 1 add :json do |json, options|
  141. json = json.to_json(options) unless json.kind_of?(String)
  142. if options[:callback].present?
  143. if media_type.nil? || media_type == Mime[:json]
  144. self.content_type = Mime[:js]
  145. end
  146. "/**/#{options[:callback]}(#{json})"
  147. else
  148. self.content_type = Mime[:json] if media_type.nil?
  149. json
  150. end
  151. end
  152. 1 add :js do |js, options|
  153. self.content_type = Mime[:js] if media_type.nil?
  154. js.respond_to?(:to_js) ? js.to_js(options) : js
  155. end
  156. 1 add :xml do |xml, options|
  157. self.content_type = Mime[:xml] if media_type.nil?
  158. xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
  159. end
  160. end
  161. end

lib/action_controller/metal/rendering.rb

41.27% lines covered

63 relevant lines. 26 lines covered and 37 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. 1 module Rendering
  4. 1 extend ActiveSupport::Concern
  5. 1 RENDER_FORMATS_IN_PRIORITY = [:body, :plain, :html]
  6. 1 module ClassMethods
  7. # Documentation at ActionController::Renderer#render
  8. 1 delegate :render, to: :renderer
  9. # Returns a renderer instance (inherited from ActionController::Renderer)
  10. # for the controller.
  11. 1 attr_reader :renderer
  12. 1 def setup_renderer! # :nodoc:
  13. 284 @renderer = Renderer.for(self)
  14. end
  15. 1 def inherited(klass)
  16. 283 klass.setup_renderer!
  17. 283 super
  18. end
  19. end
  20. # Before processing, set the request formats in current controller formats.
  21. 1 def process_action(*) #:nodoc:
  22. self.formats = request.formats.map(&:ref).compact
  23. super
  24. end
  25. # Check for double render errors and set the content_type after rendering.
  26. 1 def render(*args) #:nodoc:
  27. raise ::AbstractController::DoubleRenderError if response_body
  28. super
  29. end
  30. # Overwrite render_to_string because body can now be set to a Rack body.
  31. 1 def render_to_string(*)
  32. result = super
  33. if result.respond_to?(:each)
  34. string = +""
  35. result.each { |r| string << r }
  36. string
  37. else
  38. result
  39. end
  40. end
  41. 1 def render_to_body(options = {})
  42. super || _render_in_priorities(options) || " "
  43. end
  44. 1 private
  45. 1 def _process_variant(options)
  46. if defined?(request) && !request.nil? && request.variant.present?
  47. options[:variant] = request.variant
  48. end
  49. end
  50. 1 def _render_in_priorities(options)
  51. RENDER_FORMATS_IN_PRIORITY.each do |format|
  52. return options[format] if options.key?(format)
  53. end
  54. nil
  55. end
  56. 1 def _set_html_content_type
  57. self.content_type = Mime[:html].to_s
  58. end
  59. 1 def _set_rendered_content_type(format)
  60. if format && !response.media_type
  61. self.content_type = format.to_s
  62. end
  63. end
  64. 1 def _set_vary_header
  65. if self.headers["Vary"].blank? && request.should_apply_vary_header?
  66. self.headers["Vary"] = "Accept"
  67. end
  68. end
  69. # Normalize arguments by catching blocks and setting them on :update.
  70. 1 def _normalize_args(action = nil, options = {}, &blk)
  71. options = super
  72. options[:update] = blk if block_given?
  73. options
  74. end
  75. # Normalize both text and status options.
  76. 1 def _normalize_options(options)
  77. _normalize_text(options)
  78. if options[:html]
  79. options[:html] = ERB::Util.html_escape(options[:html])
  80. end
  81. if options[:status]
  82. options[:status] = Rack::Utils.status_code(options[:status])
  83. end
  84. super
  85. end
  86. 1 def _normalize_text(options)
  87. RENDER_FORMATS_IN_PRIORITY.each do |format|
  88. if options.key?(format) && options[format].respond_to?(:to_text)
  89. options[format] = options[format].to_text
  90. end
  91. end
  92. end
  93. # Process controller specific options, as status, content-type and location.
  94. 1 def _process_options(options)
  95. status, content_type, location = options.values_at(:status, :content_type, :location)
  96. self.status = status if status
  97. self.content_type = content_type if content_type
  98. headers["Location"] = url_for(location) if location
  99. super
  100. end
  101. end
  102. end

lib/action_controller/metal/request_forgery_protection.rb

48.96% lines covered

192 relevant lines. 94 lines covered and 98 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "rack/session/abstract/id"
  3. 1 require "action_controller/metal/exceptions"
  4. 1 require "active_support/security_utils"
  5. 1 module ActionController #:nodoc:
  6. 1 class InvalidAuthenticityToken < ActionControllerError #:nodoc:
  7. end
  8. 1 class InvalidCrossOriginRequest < ActionControllerError #:nodoc:
  9. end
  10. # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
  11. # by including a token in the rendered HTML for your application. This token is
  12. # stored as a random string in the session, to which an attacker does not have
  13. # access. When a request reaches your application, \Rails verifies the received
  14. # token with the token in the session. All requests are checked except GET requests
  15. # as these should be idempotent. Keep in mind that all session-oriented requests
  16. # are CSRF protected by default, including JavaScript and HTML requests.
  17. #
  18. # Since HTML and JavaScript requests are typically made from the browser, we
  19. # need to ensure to verify request authenticity for the web browser. We can
  20. # use session-oriented authentication for these types of requests, by using
  21. # the <tt>protect_from_forgery</tt> method in our controllers.
  22. #
  23. # GET requests are not protected since they don't have side effects like writing
  24. # to the database and don't leak sensitive information. JavaScript requests are
  25. # an exception: a third-party site can use a <script> tag to reference a JavaScript
  26. # URL on your site. When your JavaScript response loads on their site, it executes.
  27. # With carefully crafted JavaScript on their end, sensitive data in your JavaScript
  28. # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or
  29. # Ajax) requests are allowed to make requests for JavaScript responses.
  30. #
  31. # Subclasses of <tt>ActionController::Base</tt> are protected by default with the
  32. # <tt>:exception</tt> strategy, which raises an
  33. # <tt>ActionController::InvalidAuthenticityToken</tt> error on unverified requests.
  34. #
  35. # APIs may want to disable this behavior since they are typically designed to be
  36. # state-less: that is, the request API client handles the session instead of Rails.
  37. # One way to achieve this is to use the <tt>:null_session</tt> strategy instead,
  38. # which allows unverified requests to be handled, but with an empty session:
  39. #
  40. # class ApplicationController < ActionController::Base
  41. # protect_from_forgery with: :null_session
  42. # end
  43. #
  44. # Note that API only applications don't include this module or a session middleware
  45. # by default, and so don't require CSRF protection to be configured.
  46. #
  47. # The token parameter is named <tt>authenticity_token</tt> by default. The name and
  48. # value of this token must be added to every layout that renders forms by including
  49. # <tt>csrf_meta_tags</tt> in the HTML +head+.
  50. #
  51. # Learn more about CSRF attacks and securing your application in the
  52. # {Ruby on Rails Security Guide}[https://guides.rubyonrails.org/security.html].
  53. 1 module RequestForgeryProtection
  54. 1 extend ActiveSupport::Concern
  55. 1 include AbstractController::Helpers
  56. 1 include AbstractController::Callbacks
  57. 1 included do
  58. # Sets the token parameter name for RequestForgery. Calling +protect_from_forgery+
  59. # sets it to <tt>:authenticity_token</tt> by default.
  60. 1 config_accessor :request_forgery_protection_token
  61. 1 self.request_forgery_protection_token ||= :authenticity_token
  62. # Holds the class which implements the request forgery protection.
  63. 1 config_accessor :forgery_protection_strategy
  64. 1 self.forgery_protection_strategy = nil
  65. # Controls whether request forgery protection is turned on or not. Turned off by default only in test mode.
  66. 1 config_accessor :allow_forgery_protection
  67. 1 self.allow_forgery_protection = true if allow_forgery_protection.nil?
  68. # Controls whether a CSRF failure logs a warning. On by default.
  69. 1 config_accessor :log_warning_on_csrf_failure
  70. 1 self.log_warning_on_csrf_failure = true
  71. # Controls whether the Origin header is checked in addition to the CSRF token.
  72. 1 config_accessor :forgery_protection_origin_check
  73. 1 self.forgery_protection_origin_check = false
  74. # Controls whether form-action/method specific CSRF tokens are used.
  75. 1 config_accessor :per_form_csrf_tokens
  76. 1 self.per_form_csrf_tokens = false
  77. # Controls whether forgery protection is enabled by default.
  78. 1 config_accessor :default_protect_from_forgery
  79. 1 self.default_protect_from_forgery = false
  80. # Controls whether URL-safe CSRF tokens are generated.
  81. 1 config_accessor :urlsafe_csrf_tokens, instance_writer: false
  82. 1 self.urlsafe_csrf_tokens = false
  83. 1 helper_method :form_authenticity_token
  84. 1 helper_method :protect_against_forgery?
  85. end
  86. 1 module ClassMethods
  87. # Turn on request forgery protection. Bear in mind that GET and HEAD requests are not checked.
  88. #
  89. # class ApplicationController < ActionController::Base
  90. # protect_from_forgery
  91. # end
  92. #
  93. # class FooController < ApplicationController
  94. # protect_from_forgery except: :index
  95. # end
  96. #
  97. # You can disable forgery protection on controller by skipping the verification before_action:
  98. #
  99. # skip_before_action :verify_authenticity_token
  100. #
  101. # Valid Options:
  102. #
  103. # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. For example <tt>only: [ :create, :create_all ]</tt>.
  104. # * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference.
  105. # * <tt>:prepend</tt> - By default, the verification of the authentication token will be added at the position of the
  106. # protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful
  107. # when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth).
  108. #
  109. # If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>.
  110. # * <tt>:with</tt> - Set the method to handle unverified request.
  111. #
  112. # Valid unverified request handling methods are:
  113. # * <tt>:exception</tt> - Raises ActionController::InvalidAuthenticityToken exception.
  114. # * <tt>:reset_session</tt> - Resets the session.
  115. # * <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.
  116. 1 def protect_from_forgery(options = {})
  117. 12 options = options.reverse_merge(prepend: false)
  118. 12 self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
  119. 12 self.request_forgery_protection_token ||= :authenticity_token
  120. 12 before_action :verify_authenticity_token, options
  121. 12 append_after_action :verify_same_origin_request
  122. end
  123. # Turn off request forgery protection. This is a wrapper for:
  124. #
  125. # skip_before_action :verify_authenticity_token
  126. #
  127. # See +skip_before_action+ for allowed options.
  128. 1 def skip_forgery_protection(options = {})
  129. 1 skip_before_action :verify_authenticity_token, options
  130. end
  131. 1 private
  132. 1 def protection_method_class(name)
  133. 12 ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify)
  134. rescue NameError
  135. raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, or :reset_session"
  136. end
  137. end
  138. 1 module ProtectionMethods
  139. 1 class NullSession
  140. 1 def initialize(controller)
  141. @controller = controller
  142. end
  143. # This is the method that defines the application behavior when a request is found to be unverified.
  144. 1 def handle_unverified_request
  145. request = @controller.request
  146. request.session = NullSessionHash.new(request)
  147. request.flash = nil
  148. request.session_options = { skip: true }
  149. request.cookie_jar = NullCookieJar.build(request, {})
  150. end
  151. 1 private
  152. 1 class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
  153. 1 def initialize(req)
  154. super(nil, req)
  155. @data = {}
  156. @loaded = true
  157. end
  158. # no-op
  159. 1 def destroy; end
  160. 1 def exists?
  161. true
  162. end
  163. end
  164. 1 class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
  165. 1 def write(*)
  166. # nothing
  167. end
  168. end
  169. end
  170. 1 class ResetSession
  171. 1 def initialize(controller)
  172. @controller = controller
  173. end
  174. 1 def handle_unverified_request
  175. @controller.reset_session
  176. end
  177. end
  178. 1 class Exception
  179. 1 def initialize(controller)
  180. @controller = controller
  181. end
  182. 1 def handle_unverified_request
  183. raise ActionController::InvalidAuthenticityToken
  184. end
  185. end
  186. end
  187. 1 private
  188. # The actual before_action that is used to verify the CSRF token.
  189. # Don't override this directly. Provide your own forgery protection
  190. # strategy instead. If you override, you'll disable same-origin
  191. # <tt><script></tt> verification.
  192. #
  193. # Lean on the protect_from_forgery declaration to mark which actions are
  194. # due for same-origin request verification. If protect_from_forgery is
  195. # enabled on an action, this before_action flags its after_action to
  196. # verify that JavaScript responses are for XHR requests, ensuring they
  197. # follow the browser's same-origin policy.
  198. 1 def verify_authenticity_token # :doc:
  199. mark_for_same_origin_verification!
  200. if !verified_request?
  201. if logger && log_warning_on_csrf_failure
  202. if valid_request_origin?
  203. logger.warn "Can't verify CSRF token authenticity."
  204. else
  205. logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
  206. end
  207. end
  208. handle_unverified_request
  209. end
  210. end
  211. 1 def handle_unverified_request # :doc:
  212. forgery_protection_strategy.new(self).handle_unverified_request
  213. end
  214. #:nodoc:
  215. 1 CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
  216. "<script> tag on another site requested protected JavaScript. " \
  217. "If you know what you're doing, go ahead and disable forgery " \
  218. "protection on this action to permit cross-origin JavaScript embedding."
  219. 1 private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING
  220. # :startdoc:
  221. # If +verify_authenticity_token+ was run (indicating that we have
  222. # forgery protection enabled for this request) then also verify that
  223. # we aren't serving an unauthorized cross-origin response.
  224. 1 def verify_same_origin_request # :doc:
  225. if marked_for_same_origin_verification? && non_xhr_javascript_response?
  226. if logger && log_warning_on_csrf_failure
  227. logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING
  228. end
  229. raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING
  230. end
  231. end
  232. # GET requests are checked for cross-origin JavaScript after rendering.
  233. 1 def mark_for_same_origin_verification! # :doc:
  234. @marked_for_same_origin_verification = request.get?
  235. end
  236. # If the +verify_authenticity_token+ before_action ran, verify that
  237. # JavaScript responses are only served to same-origin GET requests.
  238. 1 def marked_for_same_origin_verification? # :doc:
  239. @marked_for_same_origin_verification ||= false
  240. end
  241. # Check for cross-origin JavaScript responses.
  242. 1 def non_xhr_javascript_response? # :doc:
  243. %r(\A(?:text|application)/javascript).match?(media_type) && !request.xhr?
  244. end
  245. 1 AUTHENTICITY_TOKEN_LENGTH = 32
  246. # Returns true or false if a request is verified. Checks:
  247. #
  248. # * Is it a GET or HEAD request? GETs should be safe and idempotent
  249. # * Does the form_authenticity_token match the given token value from the params?
  250. # * Does the X-CSRF-Token header match the form_authenticity_token?
  251. 1 def verified_request? # :doc:
  252. !protect_against_forgery? || request.get? || request.head? ||
  253. (valid_request_origin? && any_authenticity_token_valid?)
  254. end
  255. # Checks if any of the authenticity tokens from the request are valid.
  256. 1 def any_authenticity_token_valid? # :doc:
  257. request_authenticity_tokens.any? do |token|
  258. valid_authenticity_token?(session, token)
  259. end
  260. end
  261. # Possible authenticity tokens sent in the request.
  262. 1 def request_authenticity_tokens # :doc:
  263. [form_authenticity_param, request.x_csrf_token]
  264. end
  265. # Sets the token value for the current session.
  266. 1 def form_authenticity_token(form_options: {})
  267. masked_authenticity_token(session, form_options: form_options)
  268. end
  269. # Creates a masked version of the authenticity token that varies
  270. # on each request. The masking is used to mitigate SSL attacks
  271. # like BREACH.
  272. 1 def masked_authenticity_token(session, form_options: {}) # :doc:
  273. action, method = form_options.values_at(:action, :method)
  274. raw_token = if per_form_csrf_tokens && action && method
  275. action_path = normalize_action_path(action)
  276. per_form_csrf_token(session, action_path, method)
  277. else
  278. global_csrf_token(session)
  279. end
  280. mask_token(raw_token)
  281. end
  282. # Checks the client's masked token to see if it matches the
  283. # session token. Essentially the inverse of
  284. # +masked_authenticity_token+.
  285. 1 def valid_authenticity_token?(session, encoded_masked_token) # :doc:
  286. if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
  287. return false
  288. end
  289. begin
  290. masked_token = decode_csrf_token(encoded_masked_token)
  291. rescue ArgumentError # encoded_masked_token is invalid Base64
  292. return false
  293. end
  294. # See if it's actually a masked token or not. In order to
  295. # deploy this code, we should be able to handle any unmasked
  296. # tokens that we've issued without error.
  297. if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
  298. # This is actually an unmasked token. This is expected if
  299. # you have just upgraded to masked tokens, but should stop
  300. # happening shortly after installing this gem.
  301. compare_with_real_token masked_token, session
  302. elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
  303. csrf_token = unmask_token(masked_token)
  304. compare_with_global_token(csrf_token, session) ||
  305. compare_with_real_token(csrf_token, session) ||
  306. valid_per_form_csrf_token?(csrf_token, session)
  307. else
  308. false # Token is malformed.
  309. end
  310. end
  311. 1 def unmask_token(masked_token) # :doc:
  312. # Split the token into the one-time pad and the encrypted
  313. # value and decrypt it.
  314. one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
  315. encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
  316. xor_byte_strings(one_time_pad, encrypted_csrf_token)
  317. end
  318. 1 def mask_token(raw_token) # :doc:
  319. one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
  320. encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
  321. masked_token = one_time_pad + encrypted_csrf_token
  322. encode_csrf_token(masked_token)
  323. end
  324. 1 def compare_with_real_token(token, session) # :doc:
  325. ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
  326. end
  327. 1 def compare_with_global_token(token, session) # :doc:
  328. ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, global_csrf_token(session))
  329. end
  330. 1 def valid_per_form_csrf_token?(token, session) # :doc:
  331. if per_form_csrf_tokens
  332. correct_token = per_form_csrf_token(
  333. session,
  334. request.path.chomp("/"),
  335. request.request_method
  336. )
  337. ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token)
  338. else
  339. false
  340. end
  341. end
  342. 1 def real_csrf_token(session) # :doc:
  343. session[:_csrf_token] ||= generate_csrf_token
  344. decode_csrf_token(session[:_csrf_token])
  345. end
  346. 1 def per_form_csrf_token(session, action_path, method) # :doc:
  347. csrf_token_hmac(session, [action_path, method.downcase].join("#"))
  348. end
  349. 1 GLOBAL_CSRF_TOKEN_IDENTIFIER = "!real_csrf_token"
  350. 1 private_constant :GLOBAL_CSRF_TOKEN_IDENTIFIER
  351. 1 def global_csrf_token(session) # :doc:
  352. csrf_token_hmac(session, GLOBAL_CSRF_TOKEN_IDENTIFIER)
  353. end
  354. 1 def csrf_token_hmac(session, identifier) # :doc:
  355. OpenSSL::HMAC.digest(
  356. OpenSSL::Digest::SHA256.new,
  357. real_csrf_token(session),
  358. identifier
  359. )
  360. end
  361. 1 def xor_byte_strings(s1, s2) # :doc:
  362. s2 = s2.dup
  363. size = s1.bytesize
  364. i = 0
  365. while i < size
  366. s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i))
  367. i += 1
  368. end
  369. s2
  370. end
  371. # The form's authenticity parameter. Override to provide your own.
  372. 1 def form_authenticity_param # :doc:
  373. params[request_forgery_protection_token]
  374. end
  375. # Checks if the controller allows forgery protection.
  376. 1 def protect_against_forgery? # :doc:
  377. allow_forgery_protection
  378. end
  379. 1 NULL_ORIGIN_MESSAGE = <<~MSG
  380. The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually
  381. means you have the 'no-referrer' Referrer-Policy header enabled, or that the request came from a site that
  382. refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the
  383. best solution is to change your referrer policy to something less strict like same-origin or strict-origin.
  384. If you cannot change the referrer policy, you can disable origin checking with the
  385. Rails.application.config.action_controller.forgery_protection_origin_check setting.
  386. MSG
  387. # Checks if the request originated from the same origin by looking at the
  388. # Origin header.
  389. 1 def valid_request_origin? # :doc:
  390. if forgery_protection_origin_check
  391. # We accept blank origin headers because some user agents don't send it.
  392. raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null"
  393. request.origin.nil? || request.origin == request.base_url
  394. else
  395. true
  396. end
  397. end
  398. 1 def normalize_action_path(action_path) # :doc:
  399. uri = URI.parse(action_path)
  400. uri.path.chomp("/")
  401. end
  402. 1 def generate_csrf_token # :nodoc:
  403. if urlsafe_csrf_tokens
  404. SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH, padding: false)
  405. else
  406. SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
  407. end
  408. end
  409. 1 def encode_csrf_token(csrf_token) # :nodoc:
  410. if urlsafe_csrf_tokens
  411. Base64.urlsafe_encode64(csrf_token, padding: false)
  412. else
  413. Base64.strict_encode64(csrf_token)
  414. end
  415. end
  416. 1 def decode_csrf_token(encoded_csrf_token) # :nodoc:
  417. if urlsafe_csrf_tokens
  418. Base64.urlsafe_decode64(encoded_csrf_token)
  419. else
  420. begin
  421. Base64.strict_decode64(encoded_csrf_token)
  422. rescue ArgumentError
  423. Base64.urlsafe_decode64(encoded_csrf_token)
  424. end
  425. end
  426. end
  427. end
  428. end

lib/action_controller/metal/rescue.rb

63.64% lines covered

11 relevant lines. 7 lines covered and 4 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController #:nodoc:
  3. # This module is responsible for providing +rescue_from+ helpers
  4. # to controllers and configuring when detailed exceptions must be
  5. # shown.
  6. 1 module Rescue
  7. 1 extend ActiveSupport::Concern
  8. 1 include ActiveSupport::Rescuable
  9. # Override this method if you want to customize when detailed
  10. # exceptions must be shown. This method is only called when
  11. # +consider_all_requests_local+ is +false+. By default, it returns
  12. # +false+, but someone may set it to <tt>request.local?</tt> so local
  13. # requests in production still show the detailed exception pages.
  14. 1 def show_detailed_exceptions?
  15. false
  16. end
  17. 1 private
  18. 1 def process_action(*)
  19. super
  20. rescue Exception => exception
  21. request.env["action_dispatch.show_detailed_exceptions"] ||= show_detailed_exceptions?
  22. rescue_with_handler(exception) || raise
  23. end
  24. end
  25. end

lib/action_controller/metal/streaming.rb

41.18% lines covered

17 relevant lines. 7 lines covered and 10 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "rack/chunked"
  3. 1 module ActionController #:nodoc:
  4. # Allows views to be streamed back to the client as they are rendered.
  5. #
  6. # By default, Rails renders views by first rendering the template
  7. # and then the layout. The response is sent to the client after the whole
  8. # template is rendered, all queries are made, and the layout is processed.
  9. #
  10. # Streaming inverts the rendering flow by rendering the layout first and
  11. # streaming each part of the layout as they are processed. This allows the
  12. # header of the HTML (which is usually in the layout) to be streamed back
  13. # to client very quickly, allowing JavaScripts and stylesheets to be loaded
  14. # earlier than usual.
  15. #
  16. # This approach was introduced in Rails 3.1 and is still improving. Several
  17. # Rack middlewares may not work and you need to be careful when streaming.
  18. # Those points are going to be addressed soon.
  19. #
  20. # In order to use streaming, you will need to use a Ruby version that
  21. # supports fibers (fibers are supported since version 1.9.2 of the main
  22. # Ruby implementation).
  23. #
  24. # Streaming can be added to a given template easily, all you need to do is
  25. # to pass the :stream option.
  26. #
  27. # class PostsController
  28. # def index
  29. # @posts = Post.all
  30. # render stream: true
  31. # end
  32. # end
  33. #
  34. # == When to use streaming
  35. #
  36. # Streaming may be considered to be overkill for lightweight actions like
  37. # +new+ or +edit+. The real benefit of streaming is on expensive actions
  38. # that, for example, do a lot of queries on the database.
  39. #
  40. # In such actions, you want to delay queries execution as much as you can.
  41. # For example, imagine the following +dashboard+ action:
  42. #
  43. # def dashboard
  44. # @posts = Post.all
  45. # @pages = Page.all
  46. # @articles = Article.all
  47. # end
  48. #
  49. # Most of the queries here are happening in the controller. In order to benefit
  50. # from streaming you would want to rewrite it as:
  51. #
  52. # def dashboard
  53. # # Allow lazy execution of the queries
  54. # @posts = Post.all
  55. # @pages = Page.all
  56. # @articles = Article.all
  57. # render stream: true
  58. # end
  59. #
  60. # Notice that :stream only works with templates. Rendering :json
  61. # or :xml with :stream won't work.
  62. #
  63. # == Communication between layout and template
  64. #
  65. # When streaming, rendering happens top-down instead of inside-out.
  66. # Rails starts with the layout, and the template is rendered later,
  67. # when its +yield+ is reached.
  68. #
  69. # This means that, if your application currently relies on instance
  70. # variables set in the template to be used in the layout, they won't
  71. # work once you move to streaming. The proper way to communicate
  72. # between layout and template, regardless of whether you use streaming
  73. # or not, is by using +content_for+, +provide+ and +yield+.
  74. #
  75. # Take a simple example where the layout expects the template to tell
  76. # which title to use:
  77. #
  78. # <html>
  79. # <head><title><%= yield :title %></title></head>
  80. # <body><%= yield %></body>
  81. # </html>
  82. #
  83. # You would use +content_for+ in your template to specify the title:
  84. #
  85. # <%= content_for :title, "Main" %>
  86. # Hello
  87. #
  88. # And the final result would be:
  89. #
  90. # <html>
  91. # <head><title>Main</title></head>
  92. # <body>Hello</body>
  93. # </html>
  94. #
  95. # However, if +content_for+ is called several times, the final result
  96. # would have all calls concatenated. For instance, if we have the following
  97. # template:
  98. #
  99. # <%= content_for :title, "Main" %>
  100. # Hello
  101. # <%= content_for :title, " page" %>
  102. #
  103. # The final result would be:
  104. #
  105. # <html>
  106. # <head><title>Main page</title></head>
  107. # <body>Hello</body>
  108. # </html>
  109. #
  110. # This means that, if you have <code>yield :title</code> in your layout
  111. # and you want to use streaming, you would have to render the whole template
  112. # (and eventually trigger all queries) before streaming the title and all
  113. # assets, which kills the purpose of streaming. For this purpose, you can use
  114. # a helper called +provide+ that does the same as +content_for+ but tells the
  115. # layout to stop searching for other entries and continue rendering.
  116. #
  117. # For instance, the template above using +provide+ would be:
  118. #
  119. # <%= provide :title, "Main" %>
  120. # Hello
  121. # <%= content_for :title, " page" %>
  122. #
  123. # Giving:
  124. #
  125. # <html>
  126. # <head><title>Main</title></head>
  127. # <body>Hello</body>
  128. # </html>
  129. #
  130. # That said, when streaming, you need to properly check your templates
  131. # and choose when to use +provide+ and +content_for+.
  132. #
  133. # == Headers, cookies, session and flash
  134. #
  135. # When streaming, the HTTP headers are sent to the client right before
  136. # it renders the first line. This means that, modifying headers, cookies,
  137. # session or flash after the template starts rendering will not propagate
  138. # to the client.
  139. #
  140. # == Middlewares
  141. #
  142. # Middlewares that need to manipulate the body won't work with streaming.
  143. # You should disable those middlewares whenever streaming in development
  144. # or production. For instance, <tt>Rack::Bug</tt> won't work when streaming as it
  145. # needs to inject contents in the HTML body.
  146. #
  147. # Also <tt>Rack::Cache</tt> won't work with streaming as it does not support
  148. # streaming bodies yet. Whenever streaming Cache-Control is automatically
  149. # set to "no-cache".
  150. #
  151. # == Errors
  152. #
  153. # When it comes to streaming, exceptions get a bit more complicated. This
  154. # happens because part of the template was already rendered and streamed to
  155. # the client, making it impossible to render a whole exception page.
  156. #
  157. # Currently, when an exception happens in development or production, Rails
  158. # will automatically stream to the client:
  159. #
  160. # "><script>window.location = "/500.html"</script></html>
  161. #
  162. # The first two characters (">) are required in case the exception happens
  163. # while rendering attributes for a given tag. You can check the real cause
  164. # for the exception in your logger.
  165. #
  166. # == Web server support
  167. #
  168. # Not all web servers support streaming out-of-the-box. You need to check
  169. # the instructions for each of them.
  170. #
  171. # ==== Unicorn
  172. #
  173. # Unicorn supports streaming but it needs to be configured. For this, you
  174. # need to create a config file as follow:
  175. #
  176. # # unicorn.config.rb
  177. # listen 3000, tcp_nopush: false
  178. #
  179. # And use it on initialization:
  180. #
  181. # unicorn_rails --config-file unicorn.config.rb
  182. #
  183. # You may also want to configure other parameters like <tt>:tcp_nodelay</tt>.
  184. # Please check its documentation for more information: https://bogomips.org/unicorn/Unicorn/Configurator.html#method-i-listen
  185. #
  186. # If you are using Unicorn with NGINX, you may need to tweak NGINX.
  187. # Streaming should work out of the box on Rainbows.
  188. #
  189. # ==== Passenger
  190. #
  191. # To be described.
  192. #
  193. 1 module Streaming
  194. 1 extend ActiveSupport::Concern
  195. 1 private
  196. # Set proper cache control and transfer encoding when streaming
  197. 1 def _process_options(options)
  198. super
  199. if options[:stream]
  200. if request.version == "HTTP/1.0"
  201. options.delete(:stream)
  202. else
  203. headers["Cache-Control"] ||= "no-cache"
  204. headers["Transfer-Encoding"] = "chunked"
  205. headers.delete("Content-Length")
  206. end
  207. end
  208. end
  209. # Call render_body if we are streaming instead of usual +render+.
  210. 1 def _render_template(options)
  211. if options.delete(:stream)
  212. Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
  213. else
  214. super
  215. end
  216. end
  217. end
  218. end

lib/action_controller/metal/strong_parameters.rb

38.59% lines covered

298 relevant lines. 115 lines covered and 183 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/hash/indifferent_access"
  3. 1 require "active_support/core_ext/array/wrap"
  4. 1 require "active_support/core_ext/string/filters"
  5. 1 require "active_support/core_ext/object/to_query"
  6. 1 require "action_dispatch/http/upload"
  7. 1 require "rack/test"
  8. 1 require "stringio"
  9. 1 require "set"
  10. 1 require "yaml"
  11. 1 module ActionController
  12. # Raised when a required parameter is missing.
  13. #
  14. # params = ActionController::Parameters.new(a: {})
  15. # params.fetch(:b)
  16. # # => ActionController::ParameterMissing: param is missing or the value is empty: b
  17. # params.require(:a)
  18. # # => ActionController::ParameterMissing: param is missing or the value is empty: a
  19. 1 class ParameterMissing < KeyError
  20. 1 attr_reader :param, :keys # :nodoc:
  21. 1 def initialize(param, keys = nil) # :nodoc:
  22. @param = param
  23. @keys = keys
  24. super("param is missing or the value is empty: #{param}")
  25. end
  26. 1 class Correction
  27. 1 def initialize(error)
  28. @error = error
  29. end
  30. 1 def corrections
  31. if @error.param && @error.keys
  32. maybe_these = @error.keys
  33. maybe_these.sort_by { |n|
  34. DidYouMean::Jaro.distance(@error.param.to_s, n)
  35. }.reverse.first(4)
  36. else
  37. []
  38. end
  39. end
  40. end
  41. # We may not have DYM, and DYM might not let us register error handlers
  42. 1 if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
  43. DidYouMean.correct_error(self, Correction)
  44. end
  45. end
  46. # Raised when a supplied parameter is not expected and
  47. # ActionController::Parameters.action_on_unpermitted_parameters
  48. # is set to <tt>:raise</tt>.
  49. #
  50. # params = ActionController::Parameters.new(a: "123", b: "456")
  51. # params.permit(:c)
  52. # # => ActionController::UnpermittedParameters: found unpermitted parameters: :a, :b
  53. 1 class UnpermittedParameters < IndexError
  54. 1 attr_reader :params # :nodoc:
  55. 1 def initialize(params) # :nodoc:
  56. @params = params
  57. super("found unpermitted parameter#{'s' if params.size > 1 }: #{params.map { |e| ":#{e}" }.join(", ")}")
  58. end
  59. end
  60. # Raised when a Parameters instance is not marked as permitted and
  61. # an operation to transform it to hash is called.
  62. #
  63. # params = ActionController::Parameters.new(a: "123", b: "456")
  64. # params.to_h
  65. # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
  66. 1 class UnfilteredParameters < ArgumentError
  67. 1 def initialize # :nodoc:
  68. super("unable to convert unpermitted parameters to hash")
  69. end
  70. end
  71. # == Action Controller \Parameters
  72. #
  73. # Allows you to choose which attributes should be permitted for mass updating
  74. # and thus prevent accidentally exposing that which shouldn't be exposed.
  75. # Provides two methods for this purpose: #require and #permit. The former is
  76. # used to mark parameters as required. The latter is used to set the parameter
  77. # as permitted and limit which attributes should be allowed for mass updating.
  78. #
  79. # params = ActionController::Parameters.new({
  80. # person: {
  81. # name: "Francesco",
  82. # age: 22,
  83. # role: "admin"
  84. # }
  85. # })
  86. #
  87. # permitted = params.require(:person).permit(:name, :age)
  88. # permitted # => <ActionController::Parameters {"name"=>"Francesco", "age"=>22} permitted: true>
  89. # permitted.permitted? # => true
  90. #
  91. # Person.first.update!(permitted)
  92. # # => #<Person id: 1, name: "Francesco", age: 22, role: "user">
  93. #
  94. # It provides two options that controls the top-level behavior of new instances:
  95. #
  96. # * +permit_all_parameters+ - If it's +true+, all the parameters will be
  97. # permitted by default. The default is +false+.
  98. # * +action_on_unpermitted_parameters+ - Allow to control the behavior when parameters
  99. # that are not explicitly permitted are found. The values can be +false+ to just filter them
  100. # out, <tt>:log</tt> to additionally write a message on the logger, or <tt>:raise</tt> to raise
  101. # ActionController::UnpermittedParameters exception. The default value is <tt>:log</tt>
  102. # in test and development environments, +false+ otherwise.
  103. #
  104. # Examples:
  105. #
  106. # params = ActionController::Parameters.new
  107. # params.permitted? # => false
  108. #
  109. # ActionController::Parameters.permit_all_parameters = true
  110. #
  111. # params = ActionController::Parameters.new
  112. # params.permitted? # => true
  113. #
  114. # params = ActionController::Parameters.new(a: "123", b: "456")
  115. # params.permit(:c)
  116. # # => <ActionController::Parameters {} permitted: true>
  117. #
  118. # ActionController::Parameters.action_on_unpermitted_parameters = :raise
  119. #
  120. # params = ActionController::Parameters.new(a: "123", b: "456")
  121. # params.permit(:c)
  122. # # => ActionController::UnpermittedParameters: found unpermitted keys: a, b
  123. #
  124. # Please note that these options *are not thread-safe*. In a multi-threaded
  125. # environment they should only be set once at boot-time and never mutated at
  126. # runtime.
  127. #
  128. # You can fetch values of <tt>ActionController::Parameters</tt> using either
  129. # <tt>:key</tt> or <tt>"key"</tt>.
  130. #
  131. # params = ActionController::Parameters.new(key: "value")
  132. # params[:key] # => "value"
  133. # params["key"] # => "value"
  134. 1 class Parameters
  135. 1 cattr_accessor :permit_all_parameters, instance_accessor: false, default: false
  136. 1 cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false
  137. ##
  138. # :method: as_json
  139. #
  140. # :call-seq:
  141. # as_json(options=nil)
  142. #
  143. # Returns a hash that can be used as the JSON representation for the parameters.
  144. ##
  145. # :method: each_key
  146. #
  147. # :call-seq:
  148. # each_key()
  149. #
  150. # Calls block once for each key in the parameters, passing the key.
  151. # If no block is given, an enumerator is returned instead.
  152. ##
  153. # :method: empty?
  154. #
  155. # :call-seq:
  156. # empty?()
  157. #
  158. # Returns true if the parameters have no key/value pairs.
  159. ##
  160. # :method: has_key?
  161. #
  162. # :call-seq:
  163. # has_key?(key)
  164. #
  165. # Returns true if the given key is present in the parameters.
  166. ##
  167. # :method: has_value?
  168. #
  169. # :call-seq:
  170. # has_value?(value)
  171. #
  172. # Returns true if the given value is present for some key in the parameters.
  173. ##
  174. # :method: include?
  175. #
  176. # :call-seq:
  177. # include?(key)
  178. #
  179. # Returns true if the given key is present in the parameters.
  180. ##
  181. # :method: key?
  182. #
  183. # :call-seq:
  184. # key?(key)
  185. #
  186. # Returns true if the given key is present in the parameters.
  187. ##
  188. # :method: member?
  189. #
  190. # :call-seq:
  191. # member?(key)
  192. #
  193. # Returns true if the given key is present in the parameters.
  194. ##
  195. # :method: keys
  196. #
  197. # :call-seq:
  198. # keys()
  199. #
  200. # Returns a new array of the keys of the parameters.
  201. ##
  202. # :method: to_s
  203. #
  204. # :call-seq:
  205. # to_s()
  206. #
  207. # Returns the content of the parameters as a string.
  208. ##
  209. # :method: value?
  210. #
  211. # :call-seq:
  212. # value?(value)
  213. #
  214. # Returns true if the given value is present for some key in the parameters.
  215. ##
  216. # :method: values
  217. #
  218. # :call-seq:
  219. # values()
  220. #
  221. # Returns a new array of the values of the parameters.
  222. 1 delegate :keys, :key?, :has_key?, :member?, :values, :has_value?, :value?, :empty?, :include?,
  223. :as_json, :to_s, :each_key, to: :@parameters
  224. # By default, never raise an UnpermittedParameters exception if these
  225. # params are present. The default includes both 'controller' and 'action'
  226. # because they are added by Rails and should be of no concern. One way
  227. # to change these is to specify `always_permitted_parameters` in your
  228. # config. For instance:
  229. #
  230. # config.action_controller.always_permitted_parameters = %w( controller action format )
  231. 1 cattr_accessor :always_permitted_parameters, default: %w( controller action )
  232. 1 class << self
  233. 1 def nested_attribute?(key, value) # :nodoc:
  234. /\A-?\d+\z/.match?(key) && (value.is_a?(Hash) || value.is_a?(Parameters))
  235. end
  236. end
  237. # Returns a new instance of <tt>ActionController::Parameters</tt>.
  238. # Also, sets the +permitted+ attribute to the default value of
  239. # <tt>ActionController::Parameters.permit_all_parameters</tt>.
  240. #
  241. # class Person < ActiveRecord::Base
  242. # end
  243. #
  244. # params = ActionController::Parameters.new(name: "Francesco")
  245. # params.permitted? # => false
  246. # Person.new(params) # => ActiveModel::ForbiddenAttributesError
  247. #
  248. # ActionController::Parameters.permit_all_parameters = true
  249. #
  250. # params = ActionController::Parameters.new(name: "Francesco")
  251. # params.permitted? # => true
  252. # Person.new(params) # => #<Person id: nil, name: "Francesco">
  253. 1 def initialize(parameters = {})
  254. @parameters = parameters.with_indifferent_access
  255. @permitted = self.class.permit_all_parameters
  256. end
  257. # Returns true if another +Parameters+ object contains the same content and
  258. # permitted flag.
  259. 1 def ==(other)
  260. if other.respond_to?(:permitted?)
  261. permitted? == other.permitted? && parameters == other.parameters
  262. else
  263. @parameters == other
  264. end
  265. end
  266. 1 alias eql? ==
  267. 1 def hash
  268. [@parameters.hash, @permitted].hash
  269. end
  270. # Returns a safe <tt>ActiveSupport::HashWithIndifferentAccess</tt>
  271. # representation of the parameters with all unpermitted keys removed.
  272. #
  273. # params = ActionController::Parameters.new({
  274. # name: "Senjougahara Hitagi",
  275. # oddity: "Heavy stone crab"
  276. # })
  277. # params.to_h
  278. # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
  279. #
  280. # safe_params = params.permit(:name)
  281. # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"}
  282. 1 def to_h
  283. if permitted?
  284. convert_parameters_to_hashes(@parameters, :to_h)
  285. else
  286. raise UnfilteredParameters
  287. end
  288. end
  289. # Returns a safe <tt>Hash</tt> representation of the parameters
  290. # with all unpermitted keys removed.
  291. #
  292. # params = ActionController::Parameters.new({
  293. # name: "Senjougahara Hitagi",
  294. # oddity: "Heavy stone crab"
  295. # })
  296. # params.to_hash
  297. # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
  298. #
  299. # safe_params = params.permit(:name)
  300. # safe_params.to_hash # => {"name"=>"Senjougahara Hitagi"}
  301. 1 def to_hash
  302. to_h.to_hash
  303. end
  304. # Returns a string representation of the receiver suitable for use as a URL
  305. # query string:
  306. #
  307. # params = ActionController::Parameters.new({
  308. # name: "David",
  309. # nationality: "Danish"
  310. # })
  311. # params.to_query
  312. # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
  313. #
  314. # safe_params = params.permit(:name, :nationality)
  315. # safe_params.to_query
  316. # # => "name=David&nationality=Danish"
  317. #
  318. # An optional namespace can be passed to enclose key names:
  319. #
  320. # params = ActionController::Parameters.new({
  321. # name: "David",
  322. # nationality: "Danish"
  323. # })
  324. # safe_params = params.permit(:name, :nationality)
  325. # safe_params.to_query("user")
  326. # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish"
  327. #
  328. # The string pairs "key=value" that conform the query string
  329. # are sorted lexicographically in ascending order.
  330. #
  331. # This method is also aliased as +to_param+.
  332. 1 def to_query(*args)
  333. to_h.to_query(*args)
  334. end
  335. 1 alias_method :to_param, :to_query
  336. # Returns an unsafe, unfiltered
  337. # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of the
  338. # parameters.
  339. #
  340. # params = ActionController::Parameters.new({
  341. # name: "Senjougahara Hitagi",
  342. # oddity: "Heavy stone crab"
  343. # })
  344. # params.to_unsafe_h
  345. # # => {"name"=>"Senjougahara Hitagi", "oddity" => "Heavy stone crab"}
  346. 1 def to_unsafe_h
  347. convert_parameters_to_hashes(@parameters, :to_unsafe_h)
  348. end
  349. 1 alias_method :to_unsafe_hash, :to_unsafe_h
  350. # Convert all hashes in values into parameters, then yield each pair in
  351. # the same way as <tt>Hash#each_pair</tt>.
  352. 1 def each_pair(&block)
  353. return to_enum(__callee__) unless block_given?
  354. @parameters.each_pair do |key, value|
  355. yield [key, convert_hashes_to_parameters(key, value)]
  356. end
  357. self
  358. end
  359. 1 alias_method :each, :each_pair
  360. # Convert all hashes in values into parameters, then yield each value in
  361. # the same way as <tt>Hash#each_value</tt>.
  362. 1 def each_value(&block)
  363. return to_enum(:each_value) unless block_given?
  364. @parameters.each_pair do |key, value|
  365. yield convert_hashes_to_parameters(key, value)
  366. end
  367. self
  368. end
  369. # Attribute that keeps track of converted arrays, if any, to avoid double
  370. # looping in the common use case permit + mass-assignment. Defined in a
  371. # method to instantiate it only if needed.
  372. #
  373. # Testing membership still loops, but it's going to be faster than our own
  374. # loop that converts values. Also, we are not going to build a new array
  375. # object per fetch.
  376. 1 def converted_arrays
  377. @converted_arrays ||= Set.new
  378. end
  379. # Returns +true+ if the parameter is permitted, +false+ otherwise.
  380. #
  381. # params = ActionController::Parameters.new
  382. # params.permitted? # => false
  383. # params.permit!
  384. # params.permitted? # => true
  385. 1 def permitted?
  386. @permitted
  387. end
  388. # Sets the +permitted+ attribute to +true+. This can be used to pass
  389. # mass assignment. Returns +self+.
  390. #
  391. # class Person < ActiveRecord::Base
  392. # end
  393. #
  394. # params = ActionController::Parameters.new(name: "Francesco")
  395. # params.permitted? # => false
  396. # Person.new(params) # => ActiveModel::ForbiddenAttributesError
  397. # params.permit!
  398. # params.permitted? # => true
  399. # Person.new(params) # => #<Person id: nil, name: "Francesco">
  400. 1 def permit!
  401. each_pair do |key, value|
  402. Array.wrap(value).flatten.each do |v|
  403. v.permit! if v.respond_to? :permit!
  404. end
  405. end
  406. @permitted = true
  407. self
  408. end
  409. # This method accepts both a single key and an array of keys.
  410. #
  411. # When passed a single key, if it exists and its associated value is
  412. # either present or the singleton +false+, returns said value:
  413. #
  414. # ActionController::Parameters.new(person: { name: "Francesco" }).require(:person)
  415. # # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
  416. #
  417. # Otherwise raises <tt>ActionController::ParameterMissing</tt>:
  418. #
  419. # ActionController::Parameters.new.require(:person)
  420. # # ActionController::ParameterMissing: param is missing or the value is empty: person
  421. #
  422. # ActionController::Parameters.new(person: nil).require(:person)
  423. # # ActionController::ParameterMissing: param is missing or the value is empty: person
  424. #
  425. # ActionController::Parameters.new(person: "\t").require(:person)
  426. # # ActionController::ParameterMissing: param is missing or the value is empty: person
  427. #
  428. # ActionController::Parameters.new(person: {}).require(:person)
  429. # # ActionController::ParameterMissing: param is missing or the value is empty: person
  430. #
  431. # When given an array of keys, the method tries to require each one of them
  432. # in order. If it succeeds, an array with the respective return values is
  433. # returned:
  434. #
  435. # params = ActionController::Parameters.new(user: { ... }, profile: { ... })
  436. # user_params, profile_params = params.require([:user, :profile])
  437. #
  438. # Otherwise, the method re-raises the first exception found:
  439. #
  440. # params = ActionController::Parameters.new(user: {}, profile: {})
  441. # user_params, profile_params = params.require([:user, :profile])
  442. # # ActionController::ParameterMissing: param is missing or the value is empty: user
  443. #
  444. # Technically this method can be used to fetch terminal values:
  445. #
  446. # # CAREFUL
  447. # params = ActionController::Parameters.new(person: { name: "Finn" })
  448. # name = params.require(:person).require(:name) # CAREFUL
  449. #
  450. # but take into account that at some point those ones have to be permitted:
  451. #
  452. # def person_params
  453. # params.require(:person).permit(:name).tap do |person_params|
  454. # person_params.require(:name) # SAFER
  455. # end
  456. # end
  457. #
  458. # for example.
  459. 1 def require(key)
  460. return key.map { |k| require(k) } if key.is_a?(Array)
  461. value = self[key]
  462. if value.present? || value == false
  463. value
  464. else
  465. raise ParameterMissing.new(key, @parameters.keys)
  466. end
  467. end
  468. # Alias of #require.
  469. 1 alias :required :require
  470. # Returns a new <tt>ActionController::Parameters</tt> instance that
  471. # includes only the given +filters+ and sets the +permitted+ attribute
  472. # for the object to +true+. This is useful for limiting which attributes
  473. # should be allowed for mass updating.
  474. #
  475. # params = ActionController::Parameters.new(user: { name: "Francesco", age: 22, role: "admin" })
  476. # permitted = params.require(:user).permit(:name, :age)
  477. # permitted.permitted? # => true
  478. # permitted.has_key?(:name) # => true
  479. # permitted.has_key?(:age) # => true
  480. # permitted.has_key?(:role) # => false
  481. #
  482. # Only permitted scalars pass the filter. For example, given
  483. #
  484. # params.permit(:name)
  485. #
  486. # +:name+ passes if it is a key of +params+ whose associated value is of type
  487. # +String+, +Symbol+, +NilClass+, +Numeric+, +TrueClass+, +FalseClass+,
  488. # +Date+, +Time+, +DateTime+, +StringIO+, +IO+,
  489. # +ActionDispatch::Http::UploadedFile+ or +Rack::Test::UploadedFile+.
  490. # Otherwise, the key +:name+ is filtered out.
  491. #
  492. # You may declare that the parameter should be an array of permitted scalars
  493. # by mapping it to an empty array:
  494. #
  495. # params = ActionController::Parameters.new(tags: ["rails", "parameters"])
  496. # params.permit(tags: [])
  497. #
  498. # Sometimes it is not possible or convenient to declare the valid keys of
  499. # a hash parameter or its internal structure. Just map to an empty hash:
  500. #
  501. # params.permit(preferences: {})
  502. #
  503. # Be careful because this opens the door to arbitrary input. In this
  504. # case, +permit+ ensures values in the returned structure are permitted
  505. # scalars and filters out anything else.
  506. #
  507. # You can also use +permit+ on nested parameters, like:
  508. #
  509. # params = ActionController::Parameters.new({
  510. # person: {
  511. # name: "Francesco",
  512. # age: 22,
  513. # pets: [{
  514. # name: "Purplish",
  515. # category: "dogs"
  516. # }]
  517. # }
  518. # })
  519. #
  520. # permitted = params.permit(person: [ :name, { pets: :name } ])
  521. # permitted.permitted? # => true
  522. # permitted[:person][:name] # => "Francesco"
  523. # permitted[:person][:age] # => nil
  524. # permitted[:person][:pets][0][:name] # => "Purplish"
  525. # permitted[:person][:pets][0][:category] # => nil
  526. #
  527. # Note that if you use +permit+ in a key that points to a hash,
  528. # it won't allow all the hash. You also need to specify which
  529. # attributes inside the hash should be permitted.
  530. #
  531. # params = ActionController::Parameters.new({
  532. # person: {
  533. # contact: {
  534. # email: "none@test.com",
  535. # phone: "555-1234"
  536. # }
  537. # }
  538. # })
  539. #
  540. # params.require(:person).permit(:contact)
  541. # # => <ActionController::Parameters {} permitted: true>
  542. #
  543. # params.require(:person).permit(contact: :phone)
  544. # # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"phone"=>"555-1234"} permitted: true>} permitted: true>
  545. #
  546. # params.require(:person).permit(contact: [ :email, :phone ])
  547. # # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"email"=>"none@test.com", "phone"=>"555-1234"} permitted: true>} permitted: true>
  548. 1 def permit(*filters)
  549. params = self.class.new
  550. filters.flatten.each do |filter|
  551. case filter
  552. when Symbol, String
  553. permitted_scalar_filter(params, filter)
  554. when Hash
  555. hash_filter(params, filter)
  556. end
  557. end
  558. unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters
  559. params.permit!
  560. end
  561. # Returns a parameter for the given +key+. If not found,
  562. # returns +nil+.
  563. #
  564. # params = ActionController::Parameters.new(person: { name: "Francesco" })
  565. # params[:person] # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
  566. # params[:none] # => nil
  567. 1 def [](key)
  568. convert_hashes_to_parameters(key, @parameters[key])
  569. end
  570. # Assigns a value to a given +key+. The given key may still get filtered out
  571. # when +permit+ is called.
  572. 1 def []=(key, value)
  573. @parameters[key] = value
  574. end
  575. # Returns a parameter for the given +key+. If the +key+
  576. # can't be found, there are several options: With no other arguments,
  577. # it will raise an <tt>ActionController::ParameterMissing</tt> error;
  578. # if a second argument is given, then that is returned (converted to an
  579. # instance of ActionController::Parameters if possible); if a block
  580. # is given, then that will be run and its result returned.
  581. #
  582. # params = ActionController::Parameters.new(person: { name: "Francesco" })
  583. # params.fetch(:person) # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
  584. # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none
  585. # params.fetch(:none, {}) # => <ActionController::Parameters {} permitted: false>
  586. # params.fetch(:none, "Francesco") # => "Francesco"
  587. # params.fetch(:none) { "Francesco" } # => "Francesco"
  588. 1 def fetch(key, *args)
  589. convert_value_to_parameters(
  590. @parameters.fetch(key) {
  591. if block_given?
  592. yield
  593. else
  594. args.fetch(0) { raise ActionController::ParameterMissing.new(key, @parameters.keys) }
  595. end
  596. }
  597. )
  598. end
  599. # Extracts the nested parameter from the given +keys+ by calling +dig+
  600. # at each step. Returns +nil+ if any intermediate step is +nil+.
  601. #
  602. # params = ActionController::Parameters.new(foo: { bar: { baz: 1 } })
  603. # params.dig(:foo, :bar, :baz) # => 1
  604. # params.dig(:foo, :zot, :xyz) # => nil
  605. #
  606. # params2 = ActionController::Parameters.new(foo: [10, 11, 12])
  607. # params2.dig(:foo, 1) # => 11
  608. 1 def dig(*keys)
  609. convert_hashes_to_parameters(keys.first, @parameters[keys.first])
  610. @parameters.dig(*keys)
  611. end
  612. # Returns a new <tt>ActionController::Parameters</tt> instance that
  613. # includes only the given +keys+. If the given +keys+
  614. # don't exist, returns an empty hash.
  615. #
  616. # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
  617. # params.slice(:a, :b) # => <ActionController::Parameters {"a"=>1, "b"=>2} permitted: false>
  618. # params.slice(:d) # => <ActionController::Parameters {} permitted: false>
  619. 1 def slice(*keys)
  620. new_instance_with_inherited_permitted_status(@parameters.slice(*keys))
  621. end
  622. # Returns current <tt>ActionController::Parameters</tt> instance which
  623. # contains only the given +keys+.
  624. 1 def slice!(*keys)
  625. @parameters.slice!(*keys)
  626. self
  627. end
  628. # Returns a new <tt>ActionController::Parameters</tt> instance that
  629. # filters out the given +keys+.
  630. #
  631. # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
  632. # params.except(:a, :b) # => <ActionController::Parameters {"c"=>3} permitted: false>
  633. # params.except(:d) # => <ActionController::Parameters {"a"=>1, "b"=>2, "c"=>3} permitted: false>
  634. 1 def except(*keys)
  635. new_instance_with_inherited_permitted_status(@parameters.except(*keys))
  636. end
  637. # Removes and returns the key/value pairs matching the given keys.
  638. #
  639. # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
  640. # params.extract!(:a, :b) # => <ActionController::Parameters {"a"=>1, "b"=>2} permitted: false>
  641. # params # => <ActionController::Parameters {"c"=>3} permitted: false>
  642. 1 def extract!(*keys)
  643. new_instance_with_inherited_permitted_status(@parameters.extract!(*keys))
  644. end
  645. # Returns a new <tt>ActionController::Parameters</tt> with the results of
  646. # running +block+ once for every value. The keys are unchanged.
  647. #
  648. # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
  649. # params.transform_values { |x| x * 2 }
  650. # # => <ActionController::Parameters {"a"=>2, "b"=>4, "c"=>6} permitted: false>
  651. 1 def transform_values
  652. return to_enum(:transform_values) unless block_given?
  653. new_instance_with_inherited_permitted_status(
  654. @parameters.transform_values { |v| yield convert_value_to_parameters(v) }
  655. )
  656. end
  657. # Performs values transformation and returns the altered
  658. # <tt>ActionController::Parameters</tt> instance.
  659. 1 def transform_values!
  660. return to_enum(:transform_values!) unless block_given?
  661. @parameters.transform_values! { |v| yield convert_value_to_parameters(v) }
  662. self
  663. end
  664. # Returns a new <tt>ActionController::Parameters</tt> instance with the
  665. # results of running +block+ once for every key. The values are unchanged.
  666. 1 def transform_keys(&block)
  667. return to_enum(:transform_keys) unless block_given?
  668. new_instance_with_inherited_permitted_status(
  669. @parameters.transform_keys(&block)
  670. )
  671. end
  672. # Performs keys transformation and returns the altered
  673. # <tt>ActionController::Parameters</tt> instance.
  674. 1 def transform_keys!(&block)
  675. return to_enum(:transform_keys!) unless block_given?
  676. @parameters.transform_keys!(&block)
  677. self
  678. end
  679. # Returns a new <tt>ActionController::Parameters</tt> instance with the
  680. # results of running +block+ once for every key. This includes the keys
  681. # from the root hash and from all nested hashes and arrays. The values are unchanged.
  682. 1 def deep_transform_keys(&block)
  683. new_instance_with_inherited_permitted_status(
  684. @parameters.deep_transform_keys(&block)
  685. )
  686. end
  687. # Returns the <tt>ActionController::Parameters</tt> instance changing its keys.
  688. # This includes the keys from the root hash and from all nested hashes and arrays.
  689. # The values are unchanged.
  690. 1 def deep_transform_keys!(&block)
  691. @parameters.deep_transform_keys!(&block)
  692. self
  693. end
  694. # Deletes a key-value pair from +Parameters+ and returns the value. If
  695. # +key+ is not found, returns +nil+ (or, with optional code block, yields
  696. # +key+ and returns the result). Cf. +#extract!+, which returns the
  697. # corresponding +ActionController::Parameters+ object.
  698. 1 def delete(key, &block)
  699. convert_value_to_parameters(@parameters.delete(key, &block))
  700. end
  701. # Returns a new instance of <tt>ActionController::Parameters</tt> with only
  702. # items that the block evaluates to true.
  703. 1 def select(&block)
  704. new_instance_with_inherited_permitted_status(@parameters.select(&block))
  705. end
  706. # Equivalent to Hash#keep_if, but returns +nil+ if no changes were made.
  707. 1 def select!(&block)
  708. @parameters.select!(&block)
  709. self
  710. end
  711. 1 alias_method :keep_if, :select!
  712. # Returns a new instance of <tt>ActionController::Parameters</tt> with items
  713. # that the block evaluates to true removed.
  714. 1 def reject(&block)
  715. new_instance_with_inherited_permitted_status(@parameters.reject(&block))
  716. end
  717. # Removes items that the block evaluates to true and returns self.
  718. 1 def reject!(&block)
  719. @parameters.reject!(&block)
  720. self
  721. end
  722. 1 alias_method :delete_if, :reject!
  723. # Returns a new instance of <tt>ActionController::Parameters</tt> with +nil+ values removed.
  724. 1 def compact
  725. new_instance_with_inherited_permitted_status(@parameters.compact)
  726. end
  727. # Removes all +nil+ values in place and returns +self+, or +nil+ if no changes were made.
  728. 1 def compact!
  729. self if @parameters.compact!
  730. end
  731. # Returns a new instance of <tt>ActionController::Parameters</tt> without the blank values.
  732. # Uses Object#blank? for determining if a value is blank.
  733. 1 def compact_blank
  734. reject { |_k, v| v.blank? }
  735. end
  736. # Removes all blank values in place and returns self.
  737. # Uses Object#blank? for determining if a value is blank.
  738. 1 def compact_blank!
  739. reject! { |_k, v| v.blank? }
  740. end
  741. # Returns values that were assigned to the given +keys+. Note that all the
  742. # +Hash+ objects will be converted to <tt>ActionController::Parameters</tt>.
  743. 1 def values_at(*keys)
  744. convert_value_to_parameters(@parameters.values_at(*keys))
  745. end
  746. # Returns a new <tt>ActionController::Parameters</tt> with all keys from
  747. # +other_hash+ merged into current hash.
  748. 1 def merge(other_hash)
  749. new_instance_with_inherited_permitted_status(
  750. @parameters.merge(other_hash.to_h)
  751. )
  752. end
  753. # Returns current <tt>ActionController::Parameters</tt> instance with
  754. # +other_hash+ merged into current hash.
  755. 1 def merge!(other_hash)
  756. @parameters.merge!(other_hash.to_h)
  757. self
  758. end
  759. # Returns a new <tt>ActionController::Parameters</tt> with all keys from
  760. # current hash merged into +other_hash+.
  761. 1 def reverse_merge(other_hash)
  762. new_instance_with_inherited_permitted_status(
  763. other_hash.to_h.merge(@parameters)
  764. )
  765. end
  766. 1 alias_method :with_defaults, :reverse_merge
  767. # Returns current <tt>ActionController::Parameters</tt> instance with
  768. # current hash merged into +other_hash+.
  769. 1 def reverse_merge!(other_hash)
  770. @parameters.merge!(other_hash.to_h) { |key, left, right| left }
  771. self
  772. end
  773. 1 alias_method :with_defaults!, :reverse_merge!
  774. # This is required by ActiveModel attribute assignment, so that user can
  775. # pass +Parameters+ to a mass assignment methods in a model. It should not
  776. # matter as we are using +HashWithIndifferentAccess+ internally.
  777. 1 def stringify_keys # :nodoc:
  778. dup
  779. end
  780. 1 def inspect
  781. "#<#{self.class} #{@parameters} permitted: #{@permitted}>"
  782. end
  783. 1 def self.hook_into_yaml_loading # :nodoc:
  784. # Wire up YAML format compatibility with Rails 4.2 and Psych 2.0.8 and 2.0.9+.
  785. # Makes the YAML parser call `init_with` when it encounters the keys below
  786. # instead of trying its own parsing routines.
  787. 1 YAML.load_tags["!ruby/hash-with-ivars:ActionController::Parameters"] = name
  788. 1 YAML.load_tags["!ruby/hash:ActionController::Parameters"] = name
  789. end
  790. 1 hook_into_yaml_loading
  791. 1 def init_with(coder) # :nodoc:
  792. case coder.tag
  793. when "!ruby/hash:ActionController::Parameters"
  794. # YAML 2.0.8's format where hash instance variables weren't stored.
  795. @parameters = coder.map.with_indifferent_access
  796. @permitted = false
  797. when "!ruby/hash-with-ivars:ActionController::Parameters"
  798. # YAML 2.0.9's Hash subclass format where keys and values
  799. # were stored under an elements hash and `permitted` within an ivars hash.
  800. @parameters = coder.map["elements"].with_indifferent_access
  801. @permitted = coder.map["ivars"][:@permitted]
  802. when "!ruby/object:ActionController::Parameters"
  803. # YAML's Object format. Only needed because of the format
  804. # backwards compatibility above, otherwise equivalent to YAML's initialization.
  805. @parameters, @permitted = coder.map["parameters"], coder.map["permitted"]
  806. end
  807. end
  808. # Returns duplicate of object including all parameters.
  809. 1 def deep_dup
  810. self.class.new(@parameters.deep_dup).tap do |duplicate|
  811. duplicate.permitted = @permitted
  812. end
  813. end
  814. 1 protected
  815. 1 attr_reader :parameters
  816. 1 attr_writer :permitted
  817. 1 def nested_attributes?
  818. @parameters.any? { |k, v| Parameters.nested_attribute?(k, v) }
  819. end
  820. 1 def each_nested_attribute
  821. hash = self.class.new
  822. self.each { |k, v| hash[k] = yield v if Parameters.nested_attribute?(k, v) }
  823. hash
  824. end
  825. 1 private
  826. 1 def new_instance_with_inherited_permitted_status(hash)
  827. self.class.new(hash).tap do |new_instance|
  828. new_instance.permitted = @permitted
  829. end
  830. end
  831. 1 def convert_parameters_to_hashes(value, using)
  832. case value
  833. when Array
  834. value.map { |v| convert_parameters_to_hashes(v, using) }
  835. when Hash
  836. value.transform_values do |v|
  837. convert_parameters_to_hashes(v, using)
  838. end.with_indifferent_access
  839. when Parameters
  840. value.send(using)
  841. else
  842. value
  843. end
  844. end
  845. 1 def convert_hashes_to_parameters(key, value)
  846. converted = convert_value_to_parameters(value)
  847. @parameters[key] = converted unless converted.equal?(value)
  848. converted
  849. end
  850. 1 def convert_value_to_parameters(value)
  851. case value
  852. when Array
  853. return value if converted_arrays.member?(value)
  854. converted = value.map { |_| convert_value_to_parameters(_) }
  855. converted_arrays << converted
  856. converted
  857. when Hash
  858. self.class.new(value)
  859. else
  860. value
  861. end
  862. end
  863. 1 def each_element(object, &block)
  864. case object
  865. when Array
  866. object.grep(Parameters).map { |el| yield el }.compact
  867. when Parameters
  868. if object.nested_attributes?
  869. object.each_nested_attribute(&block)
  870. else
  871. yield object
  872. end
  873. end
  874. end
  875. 1 def unpermitted_parameters!(params)
  876. unpermitted_keys = unpermitted_keys(params)
  877. if unpermitted_keys.any?
  878. case self.class.action_on_unpermitted_parameters
  879. when :log
  880. name = "unpermitted_parameters.action_controller"
  881. ActiveSupport::Notifications.instrument(name, keys: unpermitted_keys)
  882. when :raise
  883. raise ActionController::UnpermittedParameters.new(unpermitted_keys)
  884. end
  885. end
  886. end
  887. 1 def unpermitted_keys(params)
  888. keys - params.keys - always_permitted_parameters
  889. end
  890. #
  891. # --- Filtering ----------------------------------------------------------
  892. #
  893. # This is a list of permitted scalar types that includes the ones
  894. # supported in XML and JSON requests.
  895. #
  896. # This list is in particular used to filter ordinary requests, String goes
  897. # as first element to quickly short-circuit the common case.
  898. #
  899. # If you modify this collection please update the API of +permit+ above.
  900. 1 PERMITTED_SCALAR_TYPES = [
  901. String,
  902. Symbol,
  903. NilClass,
  904. Numeric,
  905. TrueClass,
  906. FalseClass,
  907. Date,
  908. Time,
  909. # DateTimes are Dates, we document the type but avoid the redundant check.
  910. StringIO,
  911. IO,
  912. ActionDispatch::Http::UploadedFile,
  913. Rack::Test::UploadedFile,
  914. ]
  915. 1 def permitted_scalar?(value)
  916. PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) }
  917. end
  918. # Adds existing keys to the params if their values are scalar.
  919. #
  920. # For example:
  921. #
  922. # puts self.keys #=> ["zipcode(90210i)"]
  923. # params = {}
  924. #
  925. # permitted_scalar_filter(params, "zipcode")
  926. #
  927. # puts params.keys # => ["zipcode"]
  928. 1 def permitted_scalar_filter(params, permitted_key)
  929. permitted_key = permitted_key.to_s
  930. if has_key?(permitted_key) && permitted_scalar?(self[permitted_key])
  931. params[permitted_key] = self[permitted_key]
  932. end
  933. each_key do |key|
  934. next unless key =~ /\(\d+[if]?\)\z/
  935. next unless $~.pre_match == permitted_key
  936. params[key] = self[key] if permitted_scalar?(self[key])
  937. end
  938. end
  939. 1 def array_of_permitted_scalars?(value)
  940. if value.is_a?(Array) && value.all? { |element| permitted_scalar?(element) }
  941. yield value
  942. end
  943. end
  944. 1 def non_scalar?(value)
  945. value.is_a?(Array) || value.is_a?(Parameters)
  946. end
  947. 1 EMPTY_ARRAY = []
  948. 1 EMPTY_HASH = {}
  949. 1 def hash_filter(params, filter)
  950. filter = filter.with_indifferent_access
  951. # Slicing filters out non-declared keys.
  952. slice(*filter.keys).each do |key, value|
  953. next unless value
  954. next unless has_key? key
  955. if filter[key] == EMPTY_ARRAY
  956. # Declaration { comment_ids: [] }.
  957. array_of_permitted_scalars?(self[key]) do |val|
  958. params[key] = val
  959. end
  960. elsif filter[key] == EMPTY_HASH
  961. # Declaration { preferences: {} }.
  962. if value.is_a?(Parameters)
  963. params[key] = permit_any_in_parameters(value)
  964. end
  965. elsif non_scalar?(value)
  966. # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }.
  967. params[key] = each_element(value) do |element|
  968. element.permit(*Array.wrap(filter[key]))
  969. end
  970. end
  971. end
  972. end
  973. 1 def permit_any_in_parameters(params)
  974. self.class.new.tap do |sanitized|
  975. params.each do |key, value|
  976. case value
  977. when ->(v) { permitted_scalar?(v) }
  978. sanitized[key] = value
  979. when Array
  980. sanitized[key] = permit_any_in_array(value)
  981. when Parameters
  982. sanitized[key] = permit_any_in_parameters(value)
  983. else
  984. # Filter this one out.
  985. end
  986. end
  987. end
  988. end
  989. 1 def permit_any_in_array(array)
  990. [].tap do |sanitized|
  991. array.each do |element|
  992. case element
  993. when ->(e) { permitted_scalar?(e) }
  994. sanitized << element
  995. when Parameters
  996. sanitized << permit_any_in_parameters(element)
  997. else
  998. # Filter this one out.
  999. end
  1000. end
  1001. end
  1002. end
  1003. 1 def initialize_copy(source)
  1004. super
  1005. @parameters = @parameters.dup
  1006. end
  1007. end
  1008. # == Strong \Parameters
  1009. #
  1010. # It provides an interface for protecting attributes from end-user
  1011. # assignment. This makes Action Controller parameters forbidden
  1012. # to be used in Active Model mass assignment until they have been explicitly
  1013. # enumerated.
  1014. #
  1015. # In addition, parameters can be marked as required and flow through a
  1016. # predefined raise/rescue flow to end up as a <tt>400 Bad Request</tt> with no
  1017. # effort.
  1018. #
  1019. # class PeopleController < ActionController::Base
  1020. # # Using "Person.create(params[:person])" would raise an
  1021. # # ActiveModel::ForbiddenAttributesError exception because it'd
  1022. # # be using mass assignment without an explicit permit step.
  1023. # # This is the recommended form:
  1024. # def create
  1025. # Person.create(person_params)
  1026. # end
  1027. #
  1028. # # This will pass with flying colors as long as there's a person key in the
  1029. # # parameters, otherwise it'll raise an ActionController::ParameterMissing
  1030. # # exception, which will get caught by ActionController::Base and turned
  1031. # # into a 400 Bad Request reply.
  1032. # def update
  1033. # redirect_to current_account.people.find(params[:id]).tap { |person|
  1034. # person.update!(person_params)
  1035. # }
  1036. # end
  1037. #
  1038. # private
  1039. # # Using a private method to encapsulate the permissible parameters is
  1040. # # a good pattern since you'll be able to reuse the same permit
  1041. # # list between create and update. Also, you can specialize this method
  1042. # # with per-user checking of permissible attributes.
  1043. # def person_params
  1044. # params.require(:person).permit(:name, :age)
  1045. # end
  1046. # end
  1047. #
  1048. # In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you
  1049. # will need to specify which nested attributes should be permitted. You might want
  1050. # to allow +:id+ and +:_destroy+, see ActiveRecord::NestedAttributes for more information.
  1051. #
  1052. # class Person
  1053. # has_many :pets
  1054. # accepts_nested_attributes_for :pets
  1055. # end
  1056. #
  1057. # class PeopleController < ActionController::Base
  1058. # def create
  1059. # Person.create(person_params)
  1060. # end
  1061. #
  1062. # ...
  1063. #
  1064. # private
  1065. #
  1066. # def person_params
  1067. # # It's mandatory to specify the nested attributes that should be permitted.
  1068. # # If you use `permit` with just the key that points to the nested attributes hash,
  1069. # # it will return an empty hash.
  1070. # params.require(:person).permit(:name, :age, pets_attributes: [ :id, :name, :category ])
  1071. # end
  1072. # end
  1073. #
  1074. # See ActionController::Parameters.require and ActionController::Parameters.permit
  1075. # for more information.
  1076. 1 module StrongParameters
  1077. # Returns a new ActionController::Parameters object that
  1078. # has been instantiated with the <tt>request.parameters</tt>.
  1079. 1 def params
  1080. @_params ||= Parameters.new(request.parameters)
  1081. end
  1082. # Assigns the given +value+ to the +params+ hash. If +value+
  1083. # is a Hash, this will create an ActionController::Parameters
  1084. # object that has been instantiated with the given +value+ hash.
  1085. 1 def params=(value)
  1086. @_params = value.is_a?(Hash) ? Parameters.new(value) : value
  1087. end
  1088. end
  1089. end

lib/action_controller/metal/testing.rb

62.5% lines covered

8 relevant lines. 5 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. 1 module Testing
  4. 1 extend ActiveSupport::Concern
  5. # Behavior specific to functional tests
  6. 1 module Functional # :nodoc:
  7. 1 def recycle!
  8. @_url_options = nil
  9. self.formats = nil
  10. self.params = nil
  11. end
  12. end
  13. end
  14. end

lib/action_controller/metal/url_for.rb

29.41% lines covered

17 relevant lines. 5 lines covered and 12 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. # Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing
  4. # the <tt>_routes</tt> method. Otherwise, an exception will be raised.
  5. #
  6. # In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define
  7. # URL options like the +host+. In order to do so, this module requires the host class
  8. # to implement +env+ which needs to be Rack-compatible and +request+
  9. # which is either an instance of +ActionDispatch::Request+ or an object
  10. # that responds to the +host+, +optional_port+, +protocol+ and
  11. # +symbolized_path_parameter+ methods.
  12. #
  13. # class RootUrl
  14. # include ActionController::UrlFor
  15. # include Rails.application.routes.url_helpers
  16. #
  17. # delegate :env, :request, to: :controller
  18. #
  19. # def initialize(controller)
  20. # @controller = controller
  21. # @url = root_path # named route from the application.
  22. # end
  23. # end
  24. 1 module UrlFor
  25. 1 extend ActiveSupport::Concern
  26. 1 include AbstractController::UrlFor
  27. 1 def url_options
  28. @_url_options ||= {
  29. host: request.host,
  30. port: request.optional_port,
  31. protocol: request.protocol,
  32. _recall: request.path_parameters
  33. }.merge!(super).freeze
  34. if (same_origin = _routes.equal?(request.routes)) ||
  35. (script_name = request.engine_script_name(_routes)) ||
  36. (original_script_name = request.original_script_name)
  37. options = @_url_options.dup
  38. if original_script_name
  39. options[:original_script_name] = original_script_name
  40. else
  41. if same_origin
  42. options[:script_name] = request.script_name.empty? ? "" : request.script_name.dup
  43. else
  44. options[:script_name] = script_name
  45. end
  46. end
  47. options.freeze
  48. else
  49. @_url_options
  50. end
  51. end
  52. end
  53. end

lib/action_controller/railtie.rb

0.0% lines covered

71 relevant lines. 0 lines covered and 71 lines missed.
    
  1. # frozen_string_literal: true
  2. require "rails"
  3. require "action_controller"
  4. require "action_dispatch/railtie"
  5. require "abstract_controller/railties/routes_helpers"
  6. require "action_controller/railties/helpers"
  7. require "action_view/railtie"
  8. module ActionController
  9. class Railtie < Rails::Railtie #:nodoc:
  10. config.action_controller = ActiveSupport::OrderedOptions.new
  11. config.eager_load_namespaces << ActionController
  12. initializer "action_controller.assets_config", group: :all do |app|
  13. app.config.action_controller.assets_dir ||= app.config.paths["public"].first
  14. end
  15. initializer "action_controller.set_helpers_path" do |app|
  16. ActionController::Helpers.helpers_path = app.helpers_paths
  17. end
  18. initializer "action_controller.parameters_config" do |app|
  19. options = app.config.action_controller
  20. ActiveSupport.on_load(:action_controller, run_once: true) do
  21. ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false }
  22. if app.config.action_controller[:always_permitted_parameters]
  23. ActionController::Parameters.always_permitted_parameters =
  24. app.config.action_controller.delete(:always_permitted_parameters)
  25. end
  26. ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do
  27. (Rails.env.test? || Rails.env.development?) ? :log : false
  28. end
  29. end
  30. end
  31. initializer "action_controller.set_configs" do |app|
  32. paths = app.config.paths
  33. options = app.config.action_controller
  34. options.logger ||= Rails.logger
  35. options.cache_store ||= Rails.cache
  36. options.javascripts_dir ||= paths["public/javascripts"].first
  37. options.stylesheets_dir ||= paths["public/stylesheets"].first
  38. # Ensure readers methods get compiled.
  39. options.asset_host ||= app.config.asset_host
  40. options.relative_url_root ||= app.config.relative_url_root
  41. ActiveSupport.on_load(:action_controller) do
  42. include app.routes.mounted_helpers
  43. extend ::AbstractController::Railties::RoutesHelpers.with(app.routes)
  44. extend ::ActionController::Railties::Helpers
  45. options.each do |k, v|
  46. k = "#{k}="
  47. if respond_to?(k)
  48. send(k, v)
  49. elsif !Base.respond_to?(k)
  50. raise "Invalid option key: #{k}"
  51. end
  52. end
  53. end
  54. end
  55. initializer "action_controller.compile_config_methods" do
  56. ActiveSupport.on_load(:action_controller) do
  57. config.compile_methods! if config.respond_to?(:compile_methods!)
  58. end
  59. end
  60. initializer "action_controller.request_forgery_protection" do |app|
  61. ActiveSupport.on_load(:action_controller_base) do
  62. if app.config.action_controller.default_protect_from_forgery
  63. protect_from_forgery with: :exception
  64. end
  65. end
  66. end
  67. initializer "action_controller.eager_load_actions" do
  68. ActiveSupport.on_load(:after_initialize) do
  69. ActionController::Metal.descendants.each(&:action_methods) if config.eager_load
  70. end
  71. end
  72. end
  73. end

lib/action_controller/railties/helpers.rb

0.0% lines covered

19 relevant lines. 0 lines covered and 19 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionController
  3. module Railties
  4. module Helpers
  5. def inherited(klass)
  6. super
  7. return unless klass.respond_to?(:helpers_path=)
  8. if namespace = klass.module_parents.detect { |m| m.respond_to?(:railtie_helpers_paths) }
  9. paths = namespace.railtie_helpers_paths
  10. else
  11. paths = ActionController::Helpers.helpers_path
  12. end
  13. klass.helpers_path = paths
  14. if klass.superclass == ActionController::Base && ActionController::Base.include_all_helpers
  15. klass.helper :all
  16. end
  17. end
  18. end
  19. end
  20. end

lib/action_controller/renderer.rb

76.92% lines covered

39 relevant lines. 30 lines covered and 9 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. # ActionController::Renderer allows you to render arbitrary templates
  4. # without requirement of being in controller actions.
  5. #
  6. # You get a concrete renderer class by invoking ActionController::Base#renderer.
  7. # For example:
  8. #
  9. # ApplicationController.renderer
  10. #
  11. # It allows you to call method #render directly.
  12. #
  13. # ApplicationController.renderer.render template: '...'
  14. #
  15. # You can use this shortcut in a controller, instead of the previous example:
  16. #
  17. # ApplicationController.render template: '...'
  18. #
  19. # #render allows you to use the same options that you can use when rendering in a controller.
  20. # For example:
  21. #
  22. # FooController.render :action, locals: { ... }, assigns: { ... }
  23. #
  24. # The template will be rendered in a Rack environment which is accessible through
  25. # ActionController::Renderer#env. You can set it up in two ways:
  26. #
  27. # * by changing renderer defaults, like
  28. #
  29. # ApplicationController.renderer.defaults # => hash with default Rack environment
  30. #
  31. # * by initializing an instance of renderer by passing it a custom environment.
  32. #
  33. # ApplicationController.renderer.new(method: 'post', https: true)
  34. #
  35. 1 class Renderer
  36. 1 attr_reader :defaults, :controller
  37. 1 DEFAULTS = {
  38. http_host: "example.org",
  39. https: false,
  40. method: "get",
  41. script_name: "",
  42. input: ""
  43. }.freeze
  44. # Create a new renderer instance for a specific controller class.
  45. 1 def self.for(controller, env = {}, defaults = DEFAULTS.dup)
  46. 284 new(controller, env, defaults)
  47. end
  48. # Create a new renderer for the same controller but with a new env.
  49. 1 def new(env = {})
  50. self.class.new controller, env, defaults
  51. end
  52. # Create a new renderer for the same controller but with new defaults.
  53. 1 def with_defaults(defaults)
  54. self.class.new controller, @env, self.defaults.merge(defaults)
  55. end
  56. # Accepts a custom Rack environment to render templates in.
  57. # It will be merged with the default Rack environment defined by
  58. # +ActionController::Renderer::DEFAULTS+.
  59. 1 def initialize(controller, env, defaults)
  60. 284 @controller = controller
  61. 284 @defaults = defaults
  62. 284 @env = normalize_keys defaults, env
  63. end
  64. # Render templates with any options from ActionController::Base#render_to_string.
  65. #
  66. # The primary options are:
  67. # * <tt>:partial</tt> - See <tt>ActionView::PartialRenderer</tt> for details.
  68. # * <tt>:file</tt> - Renders an explicit template file. Add <tt>:locals</tt> to pass in, if so desired.
  69. # It shouldn’t be used directly with unsanitized user input due to lack of validation.
  70. # * <tt>:inline</tt> - Renders an ERB template string.
  71. # * <tt>:plain</tt> - Renders provided text and sets the content type as <tt>text/plain</tt>.
  72. # * <tt>:html</tt> - Renders the provided HTML safe string, otherwise
  73. # performs HTML escape on the string first. Sets the content type as <tt>text/html</tt>.
  74. # * <tt>:json</tt> - Renders the provided hash or object in JSON. You don't
  75. # need to call <tt>.to_json</tt> on the object you want to render.
  76. # * <tt>:body</tt> - Renders provided text and sets content type of <tt>text/plain</tt>.
  77. #
  78. # If no <tt>options</tt> hash is passed or if <tt>:update</tt> is specified, then:
  79. #
  80. # If an object responding to `render_in` is passed, `render_in` is called on the object,
  81. # passing in the current view context.
  82. #
  83. # Otherwise, a partial is rendered using the second parameter as the locals hash.
  84. 1 def render(*args)
  85. raise "missing controller" unless controller
  86. request = ActionDispatch::Request.new @env
  87. request.routes = controller._routes
  88. instance = controller.new
  89. instance.set_request! request
  90. instance.set_response! controller.make_response!(request)
  91. instance.render_to_string(*args)
  92. end
  93. 1 private
  94. 1 def normalize_keys(defaults, env)
  95. 284 new_env = {}
  96. 284 env.each_pair { |k, v| new_env[rack_key_for(k)] = rack_value_for(k, v) }
  97. 284 defaults.each_pair do |k, v|
  98. 1420 key = rack_key_for(k)
  99. 1420 new_env[key] = rack_value_for(k, v) unless new_env.key?(key)
  100. end
  101. 284 new_env["rack.url_scheme"] = new_env["HTTPS"] == "on" ? "https" : "http"
  102. 284 new_env
  103. end
  104. 1 RACK_KEY_TRANSLATION = {
  105. http_host: "HTTP_HOST",
  106. https: "HTTPS",
  107. method: "REQUEST_METHOD",
  108. script_name: "SCRIPT_NAME",
  109. input: "rack.input"
  110. }
  111. 1 def rack_key_for(key)
  112. 1420 RACK_KEY_TRANSLATION[key] || key.to_s
  113. end
  114. 1 def rack_value_for(key, value)
  115. 1420 case key
  116. when :https
  117. 284 value ? "on" : "off"
  118. when :method
  119. 284 -value.upcase
  120. else
  121. 852 value
  122. end
  123. end
  124. end
  125. end

lib/action_controller/template_assertions.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionController
  3. 1 module TemplateAssertions # :nodoc:
  4. 1 def assert_template(options = {}, message = nil)
  5. raise NoMethodError,
  6. "assert_template has been extracted to a gem. To continue using it,
  7. add `gem 'rails-controller-testing'` to your Gemfile."
  8. end
  9. end
  10. end

lib/action_controller/test_case.rb

34.62% lines covered

260 relevant lines. 90 lines covered and 170 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "rack/session/abstract/id"
  3. 1 require "active_support/core_ext/hash/conversions"
  4. 1 require "active_support/core_ext/object/to_query"
  5. 1 require "active_support/core_ext/module/anonymous"
  6. 1 require "active_support/core_ext/module/redefine_method"
  7. 1 require "active_support/core_ext/hash/keys"
  8. 1 require "active_support/testing/constant_lookup"
  9. 1 require "action_controller/template_assertions"
  10. 1 require "rails-dom-testing"
  11. 1 module ActionController
  12. 1 class Metal
  13. 1 include Testing::Functional
  14. end
  15. 1 module Live
  16. # Disable controller / rendering threads in tests. User tests can access
  17. # the database on the main thread, so they could open a txn, then the
  18. # controller thread will open a new connection and try to access data
  19. # that's only visible to the main thread's txn. This is the problem in #23483.
  20. 1 silence_redefinition_of_method :new_controller_thread
  21. 1 def new_controller_thread # :nodoc:
  22. yield
  23. end
  24. end
  25. # ActionController::TestCase will be deprecated and moved to a gem in the future.
  26. # Please use ActionDispatch::IntegrationTest going forward.
  27. 1 class TestRequest < ActionDispatch::TestRequest #:nodoc:
  28. 1 DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup
  29. 1 DEFAULT_ENV.delete "PATH_INFO"
  30. 1 def self.new_session
  31. TestSession.new
  32. end
  33. 1 attr_reader :controller_class
  34. # Create a new test request with default `env` values.
  35. 1 def self.create(controller_class)
  36. env = {}
  37. env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
  38. env["rack.request.cookie_hash"] = {}.with_indifferent_access
  39. new(default_env.merge(env), new_session, controller_class)
  40. end
  41. 1 def self.default_env
  42. DEFAULT_ENV
  43. end
  44. 1 private_class_method :default_env
  45. 1 def initialize(env, session, controller_class)
  46. super(env)
  47. self.session = session
  48. self.session_options = TestSession::DEFAULT_OPTIONS.dup
  49. @controller_class = controller_class
  50. @custom_param_parsers = {
  51. xml: lambda { |raw_post| Hash.from_xml(raw_post)["hash"] }
  52. }
  53. end
  54. 1 def query_string=(string)
  55. set_header Rack::QUERY_STRING, string
  56. end
  57. 1 def content_type=(type)
  58. set_header "CONTENT_TYPE", type
  59. end
  60. 1 def assign_parameters(routes, controller_path, action, parameters, generated_path, query_string_keys)
  61. non_path_parameters = {}
  62. path_parameters = {}
  63. if parameters[:format] == :json
  64. parameters = JSON.load(JSON.dump(parameters))
  65. query_string_keys = query_string_keys.map(&:to_s)
  66. end
  67. parameters.each do |key, value|
  68. if query_string_keys.include?(key)
  69. non_path_parameters[key] = value
  70. else
  71. unless parameters["format"] == "json"
  72. if value.is_a?(Array)
  73. value = value.map(&:to_param)
  74. else
  75. value = value.to_param
  76. end
  77. end
  78. path_parameters[key.to_sym] = value
  79. end
  80. end
  81. if get?
  82. if query_string.blank?
  83. self.query_string = non_path_parameters.to_query
  84. end
  85. else
  86. if ENCODER.should_multipart?(non_path_parameters)
  87. self.content_type = ENCODER.content_type
  88. data = ENCODER.build_multipart non_path_parameters
  89. else
  90. fetch_header("CONTENT_TYPE") do |k|
  91. set_header k, "application/x-www-form-urlencoded"
  92. end
  93. case content_mime_type.to_sym
  94. when nil
  95. raise "Unknown Content-Type: #{content_type}"
  96. when :json
  97. data = ActiveSupport::JSON.encode(non_path_parameters)
  98. when :xml
  99. data = non_path_parameters.to_xml
  100. when :url_encoded_form
  101. data = non_path_parameters.to_query
  102. else
  103. @custom_param_parsers[content_mime_type.symbol] = ->(_) { non_path_parameters }
  104. data = non_path_parameters.to_query
  105. end
  106. end
  107. data_stream = StringIO.new(data)
  108. set_header "CONTENT_LENGTH", data_stream.length.to_s
  109. set_header "rack.input", data_stream
  110. end
  111. fetch_header("PATH_INFO") do |k|
  112. set_header k, generated_path
  113. end
  114. path_parameters[:controller] = controller_path
  115. path_parameters[:action] = action
  116. self.path_parameters = path_parameters
  117. end
  118. 1 ENCODER = Class.new do
  119. 1 include Rack::Test::Utils
  120. 1 def should_multipart?(params)
  121. # FIXME: lifted from Rack-Test. We should push this separation upstream.
  122. multipart = false
  123. query = lambda { |value|
  124. case value
  125. when Array
  126. value.each(&query)
  127. when Hash
  128. value.values.each(&query)
  129. when Rack::Test::UploadedFile
  130. multipart = true
  131. end
  132. }
  133. params.values.each(&query)
  134. multipart
  135. end
  136. 1 public :build_multipart
  137. 1 def content_type
  138. "multipart/form-data; boundary=#{Rack::Test::MULTIPART_BOUNDARY}"
  139. end
  140. end.new
  141. 1 private
  142. 1 def params_parsers
  143. super.merge @custom_param_parsers
  144. end
  145. end
  146. 1 class LiveTestResponse < Live::Response
  147. # Was the response successful?
  148. 1 alias_method :success?, :successful?
  149. # Was the URL not found?
  150. 1 alias_method :missing?, :not_found?
  151. # Was there a server-side error?
  152. 1 alias_method :error?, :server_error?
  153. end
  154. # Methods #destroy and #load! are overridden to avoid calling methods on the
  155. # @store object, which does not exist for the TestSession class.
  156. 1 class TestSession < Rack::Session::Abstract::PersistedSecure::SecureSessionHash #:nodoc:
  157. 1 DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS
  158. 1 def initialize(session = {})
  159. super(nil, nil)
  160. @id = Rack::Session::SessionId.new(SecureRandom.hex(16))
  161. @data = stringify_keys(session)
  162. @loaded = true
  163. end
  164. 1 def exists?
  165. true
  166. end
  167. 1 def keys
  168. @data.keys
  169. end
  170. 1 def values
  171. @data.values
  172. end
  173. 1 def destroy
  174. clear
  175. end
  176. 1 def dig(*keys)
  177. keys = keys.map.with_index { |key, i| i.zero? ? key.to_s : key }
  178. @data.dig(*keys)
  179. end
  180. 1 def fetch(key, *args, &block)
  181. @data.fetch(key.to_s, *args, &block)
  182. end
  183. 1 private
  184. 1 def load!
  185. @id
  186. end
  187. end
  188. # Superclass for ActionController functional tests. Functional tests allow you to
  189. # test a single controller action per test method.
  190. #
  191. # == Use integration style controller tests over functional style controller tests.
  192. #
  193. # Rails discourages the use of functional tests in favor of integration tests
  194. # (use ActionDispatch::IntegrationTest).
  195. #
  196. # New Rails applications no longer generate functional style controller tests and they should
  197. # only be used for backward compatibility. Integration style controller tests perform actual
  198. # requests, whereas functional style controller tests merely simulate a request. Besides,
  199. # integration tests are as fast as functional tests and provide lot of helpers such as +as+,
  200. # +parsed_body+ for effective testing of controller actions including even API endpoints.
  201. #
  202. # == Basic example
  203. #
  204. # Functional tests are written as follows:
  205. # 1. First, one uses the +get+, +post+, +patch+, +put+, +delete+ or +head+ method to simulate
  206. # an HTTP request.
  207. # 2. Then, one asserts whether the current state is as expected. "State" can be anything:
  208. # the controller's HTTP response, the database contents, etc.
  209. #
  210. # For example:
  211. #
  212. # class BooksControllerTest < ActionController::TestCase
  213. # def test_create
  214. # # Simulate a POST response with the given HTTP parameters.
  215. # post(:create, params: { book: { title: "Love Hina" }})
  216. #
  217. # # Asserts that the controller tried to redirect us to
  218. # # the created book's URI.
  219. # assert_response :found
  220. #
  221. # # Asserts that the controller really put the book in the database.
  222. # assert_not_nil Book.find_by(title: "Love Hina")
  223. # end
  224. # end
  225. #
  226. # You can also send a real document in the simulated HTTP request.
  227. #
  228. # def test_create
  229. # json = {book: { title: "Love Hina" }}.to_json
  230. # post :create, body: json
  231. # end
  232. #
  233. # == Special instance variables
  234. #
  235. # ActionController::TestCase will also automatically provide the following instance
  236. # variables for use in the tests:
  237. #
  238. # <b>@controller</b>::
  239. # The controller instance that will be tested.
  240. # <b>@request</b>::
  241. # An ActionController::TestRequest, representing the current HTTP
  242. # request. You can modify this object before sending the HTTP request. For example,
  243. # you might want to set some session properties before sending a GET request.
  244. # <b>@response</b>::
  245. # An ActionDispatch::TestResponse object, representing the response
  246. # of the last HTTP response. In the above example, <tt>@response</tt> becomes valid
  247. # after calling +post+. If the various assert methods are not sufficient, then you
  248. # may use this object to inspect the HTTP response in detail.
  249. #
  250. # == Controller is automatically inferred
  251. #
  252. # ActionController::TestCase will automatically infer the controller under test
  253. # from the test class name. If the controller cannot be inferred from the test
  254. # class name, you can explicitly set it with +tests+.
  255. #
  256. # class SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase
  257. # tests WidgetController
  258. # end
  259. #
  260. # == \Testing controller internals
  261. #
  262. # In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
  263. # can be used against. These collections are:
  264. #
  265. # * session: Objects being saved in the session.
  266. # * flash: The flash objects currently in the session.
  267. # * cookies: \Cookies being sent to the user on this request.
  268. #
  269. # These collections can be used just like any other hash:
  270. #
  271. # assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
  272. # assert flash.empty? # makes sure that there's nothing in the flash
  273. #
  274. # On top of the collections, you have the complete URL that a given action redirected to available in <tt>redirect_to_url</tt>.
  275. #
  276. # For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
  277. # action call which can then be asserted against.
  278. #
  279. # == Manipulating session and cookie variables
  280. #
  281. # Sometimes you need to set up the session and cookie variables for a test.
  282. # To do this just assign a value to the session or cookie collection:
  283. #
  284. # session[:key] = "value"
  285. # cookies[:key] = "value"
  286. #
  287. # To clear the cookies for a test just clear the cookie collection:
  288. #
  289. # cookies.clear
  290. #
  291. # == \Testing named routes
  292. #
  293. # If you're using named routes, they can be easily tested using the original named routes' methods straight in the test case.
  294. #
  295. # assert_redirected_to page_url(title: 'foo')
  296. 1 class TestCase < ActiveSupport::TestCase
  297. 1 module Behavior
  298. 1 extend ActiveSupport::Concern
  299. 1 include ActionDispatch::TestProcess
  300. 1 include ActiveSupport::Testing::ConstantLookup
  301. 1 include Rails::Dom::Testing::Assertions
  302. 1 attr_reader :response, :request
  303. 1 module ClassMethods
  304. # Sets the controller class name. Useful if the name can't be inferred from test class.
  305. # Normalizes +controller_class+ before using.
  306. #
  307. # tests WidgetController
  308. # tests :widget
  309. # tests 'widget'
  310. 1 def tests(controller_class)
  311. 58 case controller_class
  312. when String, Symbol
  313. 2 self.controller_class = "#{controller_class.to_s.camelize}Controller".constantize
  314. when Class
  315. 56 self.controller_class = controller_class
  316. else
  317. raise ArgumentError, "controller class must be a String, Symbol, or Class"
  318. end
  319. end
  320. 1 def controller_class=(new_class)
  321. 58 self._controller_class = new_class
  322. end
  323. 1 def controller_class
  324. if current_controller_class = _controller_class
  325. current_controller_class
  326. else
  327. self.controller_class = determine_default_controller_class(name)
  328. end
  329. end
  330. 1 def determine_default_controller_class(name)
  331. determine_constant_from_test_name(name) do |constant|
  332. Class === constant && constant < ActionController::Metal
  333. end
  334. end
  335. end
  336. # Simulate a GET request with the given parameters.
  337. #
  338. # - +action+: The controller action to call.
  339. # - +params+: The hash with HTTP parameters that you want to pass. This may be +nil+.
  340. # - +body+: The request body with a string that is appropriately encoded
  341. # (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
  342. # - +session+: A hash of parameters to store in the session. This may be +nil+.
  343. # - +flash+: A hash of parameters to store in the flash. This may be +nil+.
  344. #
  345. # You can also simulate POST, PATCH, PUT, DELETE, and HEAD requests with
  346. # +post+, +patch+, +put+, +delete+, and +head+.
  347. # Example sending parameters, session and setting a flash message:
  348. #
  349. # get :show,
  350. # params: { id: 7 },
  351. # session: { user_id: 1 },
  352. # flash: { notice: 'This is flash message' }
  353. #
  354. # Note that the request method is not verified. The different methods are
  355. # available to make the tests more expressive.
  356. 1 def get(action, **args)
  357. res = process(action, method: "GET", **args)
  358. cookies.update res.cookies
  359. res
  360. end
  361. # Simulate a POST request with the given parameters and set/volley the response.
  362. # See +get+ for more details.
  363. 1 def post(action, **args)
  364. process(action, method: "POST", **args)
  365. end
  366. # Simulate a PATCH request with the given parameters and set/volley the response.
  367. # See +get+ for more details.
  368. 1 def patch(action, **args)
  369. process(action, method: "PATCH", **args)
  370. end
  371. # Simulate a PUT request with the given parameters and set/volley the response.
  372. # See +get+ for more details.
  373. 1 def put(action, **args)
  374. process(action, method: "PUT", **args)
  375. end
  376. # Simulate a DELETE request with the given parameters and set/volley the response.
  377. # See +get+ for more details.
  378. 1 def delete(action, **args)
  379. process(action, method: "DELETE", **args)
  380. end
  381. # Simulate a HEAD request with the given parameters and set/volley the response.
  382. # See +get+ for more details.
  383. 1 def head(action, **args)
  384. process(action, method: "HEAD", **args)
  385. end
  386. # Simulate an HTTP request to +action+ by specifying request method,
  387. # parameters and set/volley the response.
  388. #
  389. # - +action+: The controller action to call.
  390. # - +method+: Request method used to send the HTTP request. Possible values
  391. # are +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, +HEAD+. Defaults to +GET+. Can be a symbol.
  392. # - +params+: The hash with HTTP parameters that you want to pass. This may be +nil+.
  393. # - +body+: The request body with a string that is appropriately encoded
  394. # (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
  395. # - +session+: A hash of parameters to store in the session. This may be +nil+.
  396. # - +flash+: A hash of parameters to store in the flash. This may be +nil+.
  397. # - +format+: Request format. Defaults to +nil+. Can be string or symbol.
  398. # - +as+: Content type. Defaults to +nil+. Must be a symbol that corresponds
  399. # to a mime type.
  400. #
  401. # Example calling +create+ action and sending two params:
  402. #
  403. # process :create,
  404. # method: 'POST',
  405. # params: {
  406. # user: { name: 'Gaurish Sharma', email: 'user@example.com' }
  407. # },
  408. # session: { user_id: 1 },
  409. # flash: { notice: 'This is flash message' }
  410. #
  411. # To simulate +GET+, +POST+, +PATCH+, +PUT+, +DELETE+ and +HEAD+ requests
  412. # prefer using #get, #post, #patch, #put, #delete and #head methods
  413. # respectively which will make tests more expressive.
  414. #
  415. # Note that the request method is not verified.
  416. 1 def process(action, method: "GET", params: nil, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil)
  417. check_required_ivars
  418. action = +action.to_s
  419. http_method = method.to_s.upcase
  420. @html_document = nil
  421. cookies.update(@request.cookies)
  422. cookies.update_cookies_from_jar
  423. @request.set_header "HTTP_COOKIE", cookies.to_header
  424. @request.delete_header "action_dispatch.cookies"
  425. @request = TestRequest.new scrub_env!(@request.env), @request.session, @controller.class
  426. @response = build_response @response_klass
  427. @response.request = @request
  428. @controller.recycle!
  429. if body
  430. @request.set_header "RAW_POST_DATA", body
  431. end
  432. @request.set_header "REQUEST_METHOD", http_method
  433. if as
  434. @request.content_type = Mime[as].to_s
  435. format ||= as
  436. end
  437. parameters = (params || {}).symbolize_keys
  438. if format
  439. parameters[:format] = format
  440. end
  441. setup_request(controller_class_name, action, parameters, session, flash, xhr)
  442. process_controller_response(action, cookies, xhr)
  443. end
  444. 1 def controller_class_name
  445. @controller.class.anonymous? ? "anonymous" : @controller.class.controller_path
  446. end
  447. 1 def generated_path(generated_extras)
  448. generated_extras[0]
  449. end
  450. 1 def query_parameter_names(generated_extras)
  451. generated_extras[1] + [:controller, :action]
  452. end
  453. 1 def setup_controller_request_and_response
  454. @controller = nil unless defined? @controller
  455. @response_klass = ActionDispatch::TestResponse
  456. if klass = self.class.controller_class
  457. if klass < ActionController::Live
  458. @response_klass = LiveTestResponse
  459. end
  460. unless @controller
  461. begin
  462. @controller = klass.new
  463. rescue
  464. warn "could not construct controller #{klass}" if $VERBOSE
  465. end
  466. end
  467. end
  468. @request = TestRequest.create(@controller.class)
  469. @response = build_response @response_klass
  470. @response.request = @request
  471. if @controller
  472. @controller.request = @request
  473. @controller.params = {}
  474. end
  475. end
  476. 1 def build_response(klass)
  477. klass.create
  478. end
  479. 1 included do
  480. 1 include ActionController::TemplateAssertions
  481. 1 include ActionDispatch::Assertions
  482. 1 class_attribute :_controller_class
  483. 1 setup :setup_controller_request_and_response
  484. 1 ActiveSupport.run_load_hooks(:action_controller_test_case, self)
  485. end
  486. 1 private
  487. 1 def setup_request(controller_class_name, action, parameters, session, flash, xhr)
  488. generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action))
  489. generated_path = generated_path(generated_extras)
  490. query_string_keys = query_parameter_names(generated_extras)
  491. @request.assign_parameters(@routes, controller_class_name, action, parameters, generated_path, query_string_keys)
  492. @request.session.update(session) if session
  493. @request.flash.update(flash || {})
  494. if xhr
  495. @request.set_header "HTTP_X_REQUESTED_WITH", "XMLHttpRequest"
  496. @request.fetch_header("HTTP_ACCEPT") do |k|
  497. @request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
  498. end
  499. end
  500. @request.fetch_header("SCRIPT_NAME") do |k|
  501. @request.set_header k, @controller.config.relative_url_root
  502. end
  503. end
  504. 1 def process_controller_response(action, cookies, xhr)
  505. begin
  506. @controller.recycle!
  507. @controller.dispatch(action, @request, @response)
  508. ensure
  509. @request = @controller.request
  510. @response = @controller.response
  511. if @request.have_cookie_jar?
  512. unless @request.cookie_jar.committed?
  513. @request.cookie_jar.write(@response)
  514. cookies.update(@request.cookie_jar.instance_variable_get(:@cookies))
  515. end
  516. end
  517. @response.prepare!
  518. if flash_value = @request.flash.to_session_value
  519. @request.session["flash"] = flash_value
  520. else
  521. @request.session.delete("flash")
  522. end
  523. if xhr
  524. @request.delete_header "HTTP_X_REQUESTED_WITH"
  525. @request.delete_header "HTTP_ACCEPT"
  526. end
  527. @request.query_string = ""
  528. @response.sent!
  529. end
  530. @response
  531. end
  532. 1 def scrub_env!(env)
  533. env.delete_if do |k, _|
  534. k.start_with?("rack.request", "action_dispatch.request", "action_dispatch.rescue")
  535. end
  536. env["rack.input"] = StringIO.new
  537. env.delete "CONTENT_LENGTH"
  538. env.delete "RAW_POST_DATA"
  539. env
  540. end
  541. 1 def document_root_element
  542. html_document.root
  543. end
  544. 1 def check_required_ivars
  545. # Sanity check for required instance variables so we can give an
  546. # understandable error message.
  547. [:@routes, :@controller, :@request, :@response].each do |iv_name|
  548. if !instance_variable_defined?(iv_name) || instance_variable_get(iv_name).nil?
  549. raise "#{iv_name} is nil: make sure you set it in your test's setup method."
  550. end
  551. end
  552. end
  553. end
  554. 1 include Behavior
  555. end
  556. end

lib/action_dispatch.rb

95.59% lines covered

68 relevant lines. 65 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. #--
  3. # Copyright (c) 2004-2020 David Heinemeier Hansson
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining
  6. # a copy of this software and associated documentation files (the
  7. # "Software"), to deal in the Software without restriction, including
  8. # without limitation the rights to use, copy, modify, merge, publish,
  9. # distribute, sublicense, and/or sell copies of the Software, and to
  10. # permit persons to whom the Software is furnished to do so, subject to
  11. # the following conditions:
  12. #
  13. # The above copyright notice and this permission notice shall be
  14. # included in all copies or substantial portions of the Software.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  17. # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  18. # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  19. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  20. # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  21. # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  22. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  23. #++
  24. 1 require "active_support"
  25. 1 require "active_support/rails"
  26. 1 require "active_support/core_ext/module/attribute_accessors"
  27. 1 require "action_pack"
  28. 1 require "rack"
  29. 1 module Rack
  30. 1 autoload :Test, "rack/test"
  31. end
  32. 1 module ActionDispatch
  33. 1 extend ActiveSupport::Autoload
  34. 1 class IllegalStateError < StandardError
  35. end
  36. 1 class MissingController < NameError
  37. end
  38. 1 eager_autoload do
  39. 1 autoload_under "http" do
  40. 1 autoload :ContentSecurityPolicy
  41. 1 autoload :FeaturePolicy
  42. 1 autoload :Request
  43. 1 autoload :Response
  44. end
  45. end
  46. 1 autoload_under "middleware" do
  47. 1 autoload :HostAuthorization
  48. 1 autoload :RequestId
  49. 1 autoload :Callbacks
  50. 1 autoload :Cookies
  51. 1 autoload :ActionableExceptions
  52. 1 autoload :DebugExceptions
  53. 1 autoload :DebugLocks
  54. 1 autoload :DebugView
  55. 1 autoload :ExceptionWrapper
  56. 1 autoload :Executor
  57. 1 autoload :Flash
  58. 1 autoload :PublicExceptions
  59. 1 autoload :Reloader
  60. 1 autoload :RemoteIp
  61. 1 autoload :ShowExceptions
  62. 1 autoload :SSL
  63. 1 autoload :Static
  64. end
  65. 1 autoload :Journey
  66. 1 autoload :MiddlewareStack, "action_dispatch/middleware/stack"
  67. 1 autoload :Routing
  68. 1 module Http
  69. 1 extend ActiveSupport::Autoload
  70. 1 autoload :Cache
  71. 1 autoload :Headers
  72. 1 autoload :MimeNegotiation
  73. 1 autoload :Parameters
  74. 1 autoload :ParameterFilter
  75. 1 autoload :UploadedFile, "action_dispatch/http/upload"
  76. 1 autoload :URL
  77. end
  78. 1 module Session
  79. 1 autoload :AbstractStore, "action_dispatch/middleware/session/abstract_store"
  80. 1 autoload :AbstractSecureStore, "action_dispatch/middleware/session/abstract_store"
  81. 1 autoload :CookieStore, "action_dispatch/middleware/session/cookie_store"
  82. 1 autoload :MemCacheStore, "action_dispatch/middleware/session/mem_cache_store"
  83. 1 autoload :CacheStore, "action_dispatch/middleware/session/cache_store"
  84. end
  85. 1 mattr_accessor :test_app
  86. 1 autoload_under "testing" do
  87. 1 autoload :Assertions
  88. 1 autoload :Integration
  89. 1 autoload :IntegrationTest, "action_dispatch/testing/integration"
  90. 1 autoload :TestProcess
  91. 1 autoload :TestRequest
  92. 1 autoload :TestResponse
  93. 1 autoload :AssertionResponse
  94. end
  95. 1 autoload :SystemTestCase, "action_dispatch/system_test_case"
  96. end
  97. 1 autoload :Mime, "action_dispatch/http/mime_type"
  98. 1 ActiveSupport.on_load(:action_view) do
  99. ActionView::Base.default_formats ||= Mime::SET.symbols
  100. ActionView::Template::Types.delegate_to Mime
  101. ActionView::LookupContext::DetailsKey.clear
  102. end

lib/action_dispatch/http/cache.rb

37.5% lines covered

112 relevant lines. 42 lines covered and 70 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module Http
  4. 1 module Cache
  5. 1 module Request
  6. 1 HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE"
  7. 1 HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH"
  8. 1 def if_modified_since
  9. if since = get_header(HTTP_IF_MODIFIED_SINCE)
  10. Time.rfc2822(since) rescue nil
  11. end
  12. end
  13. 1 def if_none_match
  14. get_header HTTP_IF_NONE_MATCH
  15. end
  16. 1 def if_none_match_etags
  17. if_none_match ? if_none_match.split(/\s*,\s*/) : []
  18. end
  19. 1 def not_modified?(modified_at)
  20. if_modified_since && modified_at && if_modified_since >= modified_at
  21. end
  22. 1 def etag_matches?(etag)
  23. if etag
  24. validators = if_none_match_etags
  25. validators.include?(etag) || validators.include?("*")
  26. end
  27. end
  28. # Check response freshness (Last-Modified and ETag) against request
  29. # If-Modified-Since and If-None-Match conditions. If both headers are
  30. # supplied, both must match, or the request is not considered fresh.
  31. 1 def fresh?(response)
  32. last_modified = if_modified_since
  33. etag = if_none_match
  34. return false unless last_modified || etag
  35. success = true
  36. success &&= not_modified?(response.last_modified) if last_modified
  37. success &&= etag_matches?(response.etag) if etag
  38. success
  39. end
  40. end
  41. 1 module Response
  42. 1 attr_reader :cache_control
  43. 1 def last_modified
  44. if last = get_header(LAST_MODIFIED)
  45. Time.httpdate(last)
  46. end
  47. end
  48. 1 def last_modified?
  49. has_header? LAST_MODIFIED
  50. end
  51. 1 def last_modified=(utc_time)
  52. set_header LAST_MODIFIED, utc_time.httpdate
  53. end
  54. 1 def date
  55. if date_header = get_header(DATE)
  56. Time.httpdate(date_header)
  57. end
  58. end
  59. 1 def date?
  60. has_header? DATE
  61. end
  62. 1 def date=(utc_time)
  63. set_header DATE, utc_time.httpdate
  64. end
  65. # This method sets a weak ETag validator on the response so browsers
  66. # and proxies may cache the response, keyed on the ETag. On subsequent
  67. # requests, the If-None-Match header is set to the cached ETag. If it
  68. # matches the current ETag, we can return a 304 Not Modified response
  69. # with no body, letting the browser or proxy know that their cache is
  70. # current. Big savings in request time and network bandwidth.
  71. #
  72. # Weak ETags are considered to be semantically equivalent but not
  73. # byte-for-byte identical. This is perfect for browser caching of HTML
  74. # pages where we don't care about exact equality, just what the user
  75. # is viewing.
  76. #
  77. # Strong ETags are considered byte-for-byte identical. They allow a
  78. # browser or proxy cache to support Range requests, useful for paging
  79. # through a PDF file or scrubbing through a video. Some CDNs only
  80. # support strong ETags and will ignore weak ETags entirely.
  81. #
  82. # Weak ETags are what we almost always need, so they're the default.
  83. # Check out #strong_etag= to provide a strong ETag validator.
  84. 1 def etag=(weak_validators)
  85. self.weak_etag = weak_validators
  86. end
  87. 1 def weak_etag=(weak_validators)
  88. set_header "ETag", generate_weak_etag(weak_validators)
  89. end
  90. 1 def strong_etag=(strong_validators)
  91. set_header "ETag", generate_strong_etag(strong_validators)
  92. end
  93. 1 def etag?; etag; end
  94. # True if an ETag is set and it's a weak validator (preceded with W/)
  95. 1 def weak_etag?
  96. etag? && etag.start_with?('W/"')
  97. end
  98. # True if an ETag is set and it isn't a weak validator (not preceded with W/)
  99. 1 def strong_etag?
  100. etag? && !weak_etag?
  101. end
  102. 1 private
  103. 1 DATE = "Date"
  104. 1 LAST_MODIFIED = "Last-Modified"
  105. 1 SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate])
  106. 1 def generate_weak_etag(validators)
  107. "W/#{generate_strong_etag(validators)}"
  108. end
  109. 1 def generate_strong_etag(validators)
  110. %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
  111. end
  112. 1 def cache_control_segments
  113. if cache_control = _cache_control
  114. cache_control.delete(" ").split(",")
  115. else
  116. []
  117. end
  118. end
  119. 1 def cache_control_headers
  120. cache_control = {}
  121. cache_control_segments.each do |segment|
  122. directive, argument = segment.split("=", 2)
  123. if SPECIAL_KEYS.include? directive
  124. directive.tr!("-", "_")
  125. cache_control[directive.to_sym] = argument || true
  126. else
  127. cache_control[:extras] ||= []
  128. cache_control[:extras] << segment
  129. end
  130. end
  131. cache_control
  132. end
  133. 1 def prepare_cache_control!
  134. @cache_control = cache_control_headers
  135. end
  136. 1 DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
  137. 1 NO_CACHE = "no-cache"
  138. 1 PUBLIC = "public"
  139. 1 PRIVATE = "private"
  140. 1 MUST_REVALIDATE = "must-revalidate"
  141. 1 def handle_conditional_get!
  142. # Normally default cache control setting is handled by ETag
  143. # middleware. But, if an etag is already set, the middleware
  144. # defaults to `no-cache` unless a default `Cache-Control` value is
  145. # previously set. So, set a default one here.
  146. if (etag? || last_modified?) && !self._cache_control
  147. self._cache_control = DEFAULT_CACHE_CONTROL
  148. end
  149. end
  150. 1 def merge_and_normalize_cache_control!(cache_control)
  151. control = cache_control_headers
  152. return if control.empty? && cache_control.empty? # Let middleware handle default behavior
  153. if extras = control.delete(:extras)
  154. cache_control[:extras] ||= []
  155. cache_control[:extras] += extras
  156. cache_control[:extras].uniq!
  157. end
  158. control.merge! cache_control
  159. if control[:no_cache]
  160. options = []
  161. options << PUBLIC if control[:public]
  162. options << NO_CACHE
  163. options.concat(control[:extras]) if control[:extras]
  164. self._cache_control = options.join(", ")
  165. else
  166. extras = control[:extras]
  167. max_age = control[:max_age]
  168. stale_while_revalidate = control[:stale_while_revalidate]
  169. stale_if_error = control[:stale_if_error]
  170. options = []
  171. options << "max-age=#{max_age.to_i}" if max_age
  172. options << (control[:public] ? PUBLIC : PRIVATE)
  173. options << MUST_REVALIDATE if control[:must_revalidate]
  174. options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
  175. options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
  176. options.concat(extras) if extras
  177. self._cache_control = options.join(", ")
  178. end
  179. end
  180. end
  181. end
  182. end
  183. end

lib/action_dispatch/http/content_disposition.rb

56.52% lines covered

23 relevant lines. 13 lines covered and 10 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module Http
  4. 1 class ContentDisposition # :nodoc:
  5. 1 def self.format(disposition:, filename:)
  6. new(disposition: disposition, filename: filename).to_s
  7. end
  8. 1 attr_reader :disposition, :filename
  9. 1 def initialize(disposition:, filename:)
  10. @disposition = disposition
  11. @filename = filename
  12. end
  13. 1 TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/
  14. 1 def ascii_filename
  15. 'filename="' + percent_escape(I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"'
  16. end
  17. 1 RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/
  18. 1 def utf8_filename
  19. "filename*=UTF-8''" + percent_escape(filename, RFC_5987_ESCAPED_CHAR)
  20. end
  21. 1 def to_s
  22. if filename
  23. "#{disposition}; #{ascii_filename}; #{utf8_filename}"
  24. else
  25. "#{disposition}"
  26. end
  27. end
  28. 1 private
  29. 1 def percent_escape(string, pattern)
  30. string.gsub(pattern) do |char|
  31. char.bytes.map { |byte| "%%%02X" % byte }.join
  32. end
  33. end
  34. end
  35. end
  36. end

lib/action_dispatch/http/content_security_policy.rb

47.01% lines covered

134 relevant lines. 63 lines covered and 71 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/object/deep_dup"
  3. 1 module ActionDispatch #:nodoc:
  4. 1 class ContentSecurityPolicy
  5. 1 class Middleware
  6. 1 CONTENT_TYPE = "Content-Type"
  7. 1 POLICY = "Content-Security-Policy"
  8. 1 POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only"
  9. 1 def initialize(app)
  10. 4 @app = app
  11. end
  12. 1 def call(env)
  13. request = ActionDispatch::Request.new env
  14. _, headers, _ = response = @app.call(env)
  15. return response unless html_response?(headers)
  16. return response if policy_present?(headers)
  17. if policy = request.content_security_policy
  18. nonce = request.content_security_policy_nonce
  19. nonce_directives = request.content_security_policy_nonce_directives
  20. context = request.controller_instance || request
  21. headers[header_name(request)] = policy.build(context, nonce, nonce_directives)
  22. end
  23. response
  24. end
  25. 1 private
  26. 1 def html_response?(headers)
  27. if content_type = headers[CONTENT_TYPE]
  28. /html/.match?(content_type)
  29. end
  30. end
  31. 1 def header_name(request)
  32. if request.content_security_policy_report_only
  33. POLICY_REPORT_ONLY
  34. else
  35. POLICY
  36. end
  37. end
  38. 1 def policy_present?(headers)
  39. headers[POLICY] || headers[POLICY_REPORT_ONLY]
  40. end
  41. end
  42. 1 module Request
  43. 1 POLICY = "action_dispatch.content_security_policy"
  44. 1 POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only"
  45. 1 NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator"
  46. 1 NONCE = "action_dispatch.content_security_policy_nonce"
  47. 1 NONCE_DIRECTIVES = "action_dispatch.content_security_policy_nonce_directives"
  48. 1 def content_security_policy
  49. get_header(POLICY)
  50. end
  51. 1 def content_security_policy=(policy)
  52. set_header(POLICY, policy)
  53. end
  54. 1 def content_security_policy_report_only
  55. get_header(POLICY_REPORT_ONLY)
  56. end
  57. 1 def content_security_policy_report_only=(value)
  58. set_header(POLICY_REPORT_ONLY, value)
  59. end
  60. 1 def content_security_policy_nonce_generator
  61. get_header(NONCE_GENERATOR)
  62. end
  63. 1 def content_security_policy_nonce_generator=(generator)
  64. set_header(NONCE_GENERATOR, generator)
  65. end
  66. 1 def content_security_policy_nonce_directives
  67. get_header(NONCE_DIRECTIVES)
  68. end
  69. 1 def content_security_policy_nonce_directives=(generator)
  70. set_header(NONCE_DIRECTIVES, generator)
  71. end
  72. 1 def content_security_policy_nonce
  73. if content_security_policy_nonce_generator
  74. if nonce = get_header(NONCE)
  75. nonce
  76. else
  77. set_header(NONCE, generate_content_security_policy_nonce)
  78. end
  79. end
  80. end
  81. 1 private
  82. 1 def generate_content_security_policy_nonce
  83. content_security_policy_nonce_generator.call(self)
  84. end
  85. end
  86. 1 MAPPINGS = {
  87. self: "'self'",
  88. unsafe_eval: "'unsafe-eval'",
  89. unsafe_inline: "'unsafe-inline'",
  90. none: "'none'",
  91. http: "http:",
  92. https: "https:",
  93. data: "data:",
  94. mediastream: "mediastream:",
  95. blob: "blob:",
  96. filesystem: "filesystem:",
  97. report_sample: "'report-sample'",
  98. strict_dynamic: "'strict-dynamic'",
  99. ws: "ws:",
  100. wss: "wss:"
  101. }.freeze
  102. 1 DIRECTIVES = {
  103. base_uri: "base-uri",
  104. child_src: "child-src",
  105. connect_src: "connect-src",
  106. default_src: "default-src",
  107. font_src: "font-src",
  108. form_action: "form-action",
  109. frame_ancestors: "frame-ancestors",
  110. frame_src: "frame-src",
  111. img_src: "img-src",
  112. manifest_src: "manifest-src",
  113. media_src: "media-src",
  114. object_src: "object-src",
  115. prefetch_src: "prefetch-src",
  116. script_src: "script-src",
  117. script_src_attr: "script-src-attr",
  118. script_src_elem: "script-src-elem",
  119. style_src: "style-src",
  120. style_src_attr: "style-src-attr",
  121. style_src_elem: "style-src-elem",
  122. worker_src: "worker-src"
  123. }.freeze
  124. 1 DEFAULT_NONCE_DIRECTIVES = %w[script-src style-src].freeze
  125. 1 private_constant :MAPPINGS, :DIRECTIVES, :DEFAULT_NONCE_DIRECTIVES
  126. 1 attr_reader :directives
  127. 1 def initialize
  128. 3 @directives = {}
  129. 3 yield self if block_given?
  130. end
  131. 1 def initialize_copy(other)
  132. @directives = other.directives.deep_dup
  133. end
  134. 1 DIRECTIVES.each do |name, directive|
  135. 20 define_method(name) do |*sources|
  136. 6 if sources.first
  137. 6 @directives[directive] = apply_mappings(sources)
  138. else
  139. @directives.delete(directive)
  140. end
  141. end
  142. end
  143. 1 def block_all_mixed_content(enabled = true)
  144. if enabled
  145. @directives["block-all-mixed-content"] = true
  146. else
  147. @directives.delete("block-all-mixed-content")
  148. end
  149. end
  150. 1 def plugin_types(*types)
  151. if types.first
  152. @directives["plugin-types"] = types
  153. else
  154. @directives.delete("plugin-types")
  155. end
  156. end
  157. 1 def report_uri(uri)
  158. @directives["report-uri"] = [uri]
  159. end
  160. 1 def require_sri_for(*types)
  161. if types.first
  162. @directives["require-sri-for"] = types
  163. else
  164. @directives.delete("require-sri-for")
  165. end
  166. end
  167. 1 def sandbox(*values)
  168. if values.empty?
  169. @directives["sandbox"] = true
  170. elsif values.first
  171. @directives["sandbox"] = values
  172. else
  173. @directives.delete("sandbox")
  174. end
  175. end
  176. 1 def upgrade_insecure_requests(enabled = true)
  177. if enabled
  178. @directives["upgrade-insecure-requests"] = true
  179. else
  180. @directives.delete("upgrade-insecure-requests")
  181. end
  182. end
  183. 1 def build(context = nil, nonce = nil, nonce_directives = nil)
  184. nonce_directives = DEFAULT_NONCE_DIRECTIVES if nonce_directives.nil?
  185. build_directives(context, nonce, nonce_directives).compact.join("; ")
  186. end
  187. 1 private
  188. 1 def apply_mappings(sources)
  189. 6 sources.map do |source|
  190. 6 case source
  191. when Symbol
  192. 1 apply_mapping(source)
  193. when String, Proc
  194. 5 source
  195. else
  196. raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
  197. end
  198. end
  199. end
  200. 1 def apply_mapping(source)
  201. 1 MAPPINGS.fetch(source) do
  202. raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}"
  203. end
  204. end
  205. 1 def build_directives(context, nonce, nonce_directives)
  206. @directives.map do |directive, sources|
  207. if sources.is_a?(Array)
  208. if nonce && nonce_directive?(directive, nonce_directives)
  209. "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
  210. else
  211. "#{directive} #{build_directive(sources, context).join(' ')}"
  212. end
  213. elsif sources
  214. directive
  215. else
  216. nil
  217. end
  218. end
  219. end
  220. 1 def build_directive(sources, context)
  221. sources.map { |source| resolve_source(source, context) }
  222. end
  223. 1 def resolve_source(source, context)
  224. case source
  225. when String
  226. source
  227. when Symbol
  228. source.to_s
  229. when Proc
  230. if context.nil?
  231. raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}"
  232. else
  233. resolved = context.instance_exec(&source)
  234. resolved.is_a?(Symbol) ? apply_mapping(resolved) : resolved
  235. end
  236. else
  237. raise RuntimeError, "Unexpected content security policy source: #{source.inspect}"
  238. end
  239. end
  240. 1 def nonce_directive?(directive, nonce_directives)
  241. nonce_directives.include?(directive)
  242. end
  243. end
  244. end

lib/action_dispatch/http/feature_policy.rb

53.33% lines covered

75 relevant lines. 40 lines covered and 35 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/object/deep_dup"
  3. 1 module ActionDispatch #:nodoc:
  4. 1 class FeaturePolicy
  5. 1 class Middleware
  6. 1 CONTENT_TYPE = "Content-Type"
  7. 1 POLICY = "Feature-Policy"
  8. 1 def initialize(app)
  9. 1 @app = app
  10. end
  11. 1 def call(env)
  12. request = ActionDispatch::Request.new(env)
  13. _, headers, _ = response = @app.call(env)
  14. return response unless html_response?(headers)
  15. return response if policy_present?(headers)
  16. if policy = request.feature_policy
  17. headers[POLICY] = policy.build(request.controller_instance)
  18. end
  19. if policy_empty?(policy)
  20. headers.delete(POLICY)
  21. end
  22. response
  23. end
  24. 1 private
  25. 1 def html_response?(headers)
  26. if content_type = headers[CONTENT_TYPE]
  27. /html/.match?(content_type)
  28. end
  29. end
  30. 1 def policy_present?(headers)
  31. headers[POLICY]
  32. end
  33. 1 def policy_empty?(policy)
  34. policy&.directives&.empty?
  35. end
  36. end
  37. 1 module Request
  38. 1 POLICY = "action_dispatch.feature_policy"
  39. 1 def feature_policy
  40. get_header(POLICY)
  41. end
  42. 1 def feature_policy=(policy)
  43. set_header(POLICY, policy)
  44. end
  45. end
  46. 1 MAPPINGS = {
  47. self: "'self'",
  48. none: "'none'",
  49. }.freeze
  50. # List of available features can be found at
  51. # https://github.com/WICG/feature-policy/blob/master/features.md#policy-controlled-features
  52. 1 DIRECTIVES = {
  53. accelerometer: "accelerometer",
  54. ambient_light_sensor: "ambient-light-sensor",
  55. autoplay: "autoplay",
  56. camera: "camera",
  57. encrypted_media: "encrypted-media",
  58. fullscreen: "fullscreen",
  59. geolocation: "geolocation",
  60. gyroscope: "gyroscope",
  61. magnetometer: "magnetometer",
  62. microphone: "microphone",
  63. midi: "midi",
  64. payment: "payment",
  65. picture_in_picture: "picture-in-picture",
  66. speaker: "speaker",
  67. usb: "usb",
  68. vibrate: "vibrate",
  69. vr: "vr",
  70. }.freeze
  71. 1 private_constant :MAPPINGS, :DIRECTIVES
  72. 1 attr_reader :directives
  73. 1 def initialize
  74. 1 @directives = {}
  75. 1 yield self if block_given?
  76. end
  77. 1 def initialize_copy(other)
  78. @directives = other.directives.deep_dup
  79. end
  80. 1 DIRECTIVES.each do |name, directive|
  81. 17 define_method(name) do |*sources|
  82. 1 if sources.first
  83. 1 @directives[directive] = apply_mappings(sources)
  84. else
  85. @directives.delete(directive)
  86. end
  87. end
  88. end
  89. 1 def build(context = nil)
  90. build_directives(context).compact.join("; ")
  91. end
  92. 1 private
  93. 1 def apply_mappings(sources)
  94. 1 sources.map do |source|
  95. 1 case source
  96. when Symbol
  97. 1 apply_mapping(source)
  98. when String, Proc
  99. source
  100. else
  101. raise ArgumentError, "Invalid HTTP feature policy source: #{source.inspect}"
  102. end
  103. end
  104. end
  105. 1 def apply_mapping(source)
  106. 1 MAPPINGS.fetch(source) do
  107. raise ArgumentError, "Unknown HTTP feature policy source mapping: #{source.inspect}"
  108. end
  109. end
  110. 1 def build_directives(context)
  111. @directives.map do |directive, sources|
  112. if sources.is_a?(Array)
  113. "#{directive} #{build_directive(sources, context).join(' ')}"
  114. elsif sources
  115. directive
  116. else
  117. nil
  118. end
  119. end
  120. end
  121. 1 def build_directive(sources, context)
  122. sources.map { |source| resolve_source(source, context) }
  123. end
  124. 1 def resolve_source(source, context)
  125. case source
  126. when String
  127. source
  128. when Symbol
  129. source.to_s
  130. when Proc
  131. if context.nil?
  132. raise RuntimeError, "Missing context for the dynamic feature policy source: #{source.inspect}"
  133. else
  134. context.instance_exec(&source)
  135. end
  136. else
  137. raise RuntimeError, "Unexpected feature policy source: #{source.inspect}"
  138. end
  139. end
  140. end
  141. end

lib/action_dispatch/http/filter_parameters.rb

52.94% lines covered

34 relevant lines. 18 lines covered and 16 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/parameter_filter"
  3. 1 module ActionDispatch
  4. 1 module Http
  5. # Allows you to specify sensitive parameters which will be replaced from
  6. # the request log by looking in the query string of the request and all
  7. # sub-hashes of the params hash to filter. Filtering only certain sub-keys
  8. # from a hash is possible by using the dot notation: 'credit_card.number'.
  9. # If a block is given, each key and value of the params hash and all
  10. # sub-hashes are passed to it, where the value or the key can be replaced using
  11. # String#replace or similar methods.
  12. #
  13. # env["action_dispatch.parameter_filter"] = [:password]
  14. # => replaces the value to all keys matching /password/i with "[FILTERED]"
  15. #
  16. # env["action_dispatch.parameter_filter"] = [:foo, "bar"]
  17. # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
  18. #
  19. # env["action_dispatch.parameter_filter"] = [ "credit_card.code" ]
  20. # => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not
  21. # change { file: { code: "xxxx"} }
  22. #
  23. # env["action_dispatch.parameter_filter"] = -> (k, v) do
  24. # v.reverse! if k.match?(/secret/i)
  25. # end
  26. # => reverses the value to all keys matching /secret/i
  27. 1 module FilterParameters
  28. 1 ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc:
  29. 1 NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new # :nodoc:
  30. 1 NULL_ENV_FILTER = ActiveSupport::ParameterFilter.new ENV_MATCH # :nodoc:
  31. 1 def initialize
  32. super
  33. @filtered_parameters = nil
  34. @filtered_env = nil
  35. @filtered_path = nil
  36. end
  37. # Returns a hash of parameters with all sensitive data replaced.
  38. 1 def filtered_parameters
  39. @filtered_parameters ||= parameter_filter.filter(parameters)
  40. rescue ActionDispatch::Http::Parameters::ParseError
  41. @filtered_parameters = {}
  42. end
  43. # Returns a hash of request.env with all sensitive data replaced.
  44. 1 def filtered_env
  45. @filtered_env ||= env_filter.filter(@env)
  46. end
  47. # Reconstructs a path with all sensitive GET parameters replaced.
  48. 1 def filtered_path
  49. @filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}"
  50. end
  51. 1 private
  52. 1 def parameter_filter # :doc:
  53. parameter_filter_for fetch_header("action_dispatch.parameter_filter") {
  54. return NULL_PARAM_FILTER
  55. }
  56. end
  57. 1 def env_filter # :doc:
  58. user_key = fetch_header("action_dispatch.parameter_filter") {
  59. return NULL_ENV_FILTER
  60. }
  61. parameter_filter_for(Array(user_key) + ENV_MATCH)
  62. end
  63. 1 def parameter_filter_for(filters) # :doc:
  64. ActiveSupport::ParameterFilter.new(filters)
  65. end
  66. 1 KV_RE = "[^&;=]+"
  67. 1 PAIR_RE = %r{(#{KV_RE})=(#{KV_RE})}
  68. 1 def filtered_query_string # :doc:
  69. query_string.gsub(PAIR_RE) do |_|
  70. parameter_filter.filter($1 => $2).first.join("=")
  71. end
  72. end
  73. end
  74. end
  75. end

lib/action_dispatch/http/filter_redirect.rb

42.11% lines covered

19 relevant lines. 8 lines covered and 11 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module Http
  4. 1 module FilterRedirect
  5. 1 FILTERED = "[FILTERED]" # :nodoc:
  6. 1 def filtered_location # :nodoc:
  7. if location_filter_match?
  8. FILTERED
  9. else
  10. location
  11. end
  12. end
  13. 1 private
  14. 1 def location_filters
  15. if request
  16. request.get_header("action_dispatch.redirect_filter") || []
  17. else
  18. []
  19. end
  20. end
  21. 1 def location_filter_match?
  22. location_filters.any? do |filter|
  23. if String === filter
  24. location.include?(filter)
  25. elsif Regexp === filter
  26. location.match?(filter)
  27. end
  28. end
  29. end
  30. end
  31. end
  32. end

lib/action_dispatch/http/headers.rb

48.84% lines covered

43 relevant lines. 21 lines covered and 22 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module Http
  4. # Provides access to the request's HTTP headers from the environment.
  5. #
  6. # env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" }
  7. # headers = ActionDispatch::Http::Headers.from_hash(env)
  8. # headers["Content-Type"] # => "text/plain"
  9. # headers["User-Agent"] # => "curl/7.43.0"
  10. #
  11. # Also note that when headers are mapped to CGI-like variables by the Rack
  12. # server, both dashes and underscores are converted to underscores. This
  13. # ambiguity cannot be resolved at this stage anymore. Both underscores and
  14. # dashes have to be interpreted as if they were originally sent as dashes.
  15. #
  16. # # GET / HTTP/1.1
  17. # # ...
  18. # # User-Agent: curl/7.43.0
  19. # # X_Custom_Header: token
  20. #
  21. # headers["X_Custom_Header"] # => nil
  22. # headers["X-Custom-Header"] # => "token"
  23. 1 class Headers
  24. 1 CGI_VARIABLES = Set.new(%W[
  25. AUTH_TYPE
  26. CONTENT_LENGTH
  27. CONTENT_TYPE
  28. GATEWAY_INTERFACE
  29. HTTPS
  30. PATH_INFO
  31. PATH_TRANSLATED
  32. QUERY_STRING
  33. REMOTE_ADDR
  34. REMOTE_HOST
  35. REMOTE_IDENT
  36. REMOTE_USER
  37. REQUEST_METHOD
  38. SCRIPT_NAME
  39. SERVER_NAME
  40. SERVER_PORT
  41. SERVER_PROTOCOL
  42. SERVER_SOFTWARE
  43. ]).freeze
  44. 1 HTTP_HEADER = /\A[A-Za-z0-9-]+\z/
  45. 1 include Enumerable
  46. 1 def self.from_hash(hash)
  47. new ActionDispatch::Request.new hash
  48. end
  49. 1 def initialize(request) # :nodoc:
  50. @req = request
  51. end
  52. # Returns the value for the given key mapped to @env.
  53. 1 def [](key)
  54. @req.get_header env_name(key)
  55. end
  56. # Sets the given value for the key mapped to @env.
  57. 1 def []=(key, value)
  58. @req.set_header env_name(key), value
  59. end
  60. # Add a value to a multivalued header like Vary or Accept-Encoding.
  61. 1 def add(key, value)
  62. @req.add_header env_name(key), value
  63. end
  64. 1 def key?(key)
  65. @req.has_header? env_name(key)
  66. end
  67. 1 alias :include? :key?
  68. 1 DEFAULT = Object.new # :nodoc:
  69. # Returns the value for the given key mapped to @env.
  70. #
  71. # If the key is not found and an optional code block is not provided,
  72. # raises a <tt>KeyError</tt> exception.
  73. #
  74. # If the code block is provided, then it will be run and
  75. # its result returned.
  76. 1 def fetch(key, default = DEFAULT)
  77. @req.fetch_header(env_name(key)) do
  78. return default unless default == DEFAULT
  79. return yield if block_given?
  80. raise KeyError, key
  81. end
  82. end
  83. 1 def each(&block)
  84. @req.each_header(&block)
  85. end
  86. # Returns a new Http::Headers instance containing the contents of
  87. # <tt>headers_or_env</tt> and the original instance.
  88. 1 def merge(headers_or_env)
  89. headers = @req.dup.headers
  90. headers.merge!(headers_or_env)
  91. headers
  92. end
  93. # Adds the contents of <tt>headers_or_env</tt> to original instance
  94. # entries; duplicate keys are overwritten with the values from
  95. # <tt>headers_or_env</tt>.
  96. 1 def merge!(headers_or_env)
  97. headers_or_env.each do |key, value|
  98. @req.set_header env_name(key), value
  99. end
  100. end
  101. 1 def env; @req.env.dup; end
  102. 1 private
  103. # Converts an HTTP header name to an environment variable name if it is
  104. # not contained within the headers hash.
  105. 1 def env_name(key)
  106. key = key.to_s
  107. if HTTP_HEADER.match?(key)
  108. key = key.upcase
  109. key.tr!("-", "_")
  110. key.prepend("HTTP_") unless CGI_VARIABLES.include?(key)
  111. end
  112. key
  113. end
  114. end
  115. end
  116. end

lib/action_dispatch/http/mime_negotiation.rb

33.33% lines covered

78 relevant lines. 26 lines covered and 52 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/module/attribute_accessors"
  3. 1 module ActionDispatch
  4. 1 module Http
  5. 1 module MimeNegotiation
  6. 1 extend ActiveSupport::Concern
  7. 1 RESCUABLE_MIME_FORMAT_ERRORS = [
  8. ActionController::BadRequest,
  9. ActionDispatch::Http::Parameters::ParseError,
  10. ]
  11. 1 included do
  12. 1 mattr_accessor :ignore_accept_header, default: false
  13. end
  14. # The MIME type of the HTTP request, such as Mime[:xml].
  15. 1 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. 1 def content_type
  26. content_mime_type && content_mime_type.to_s
  27. end
  28. 1 def has_content_type? # :nodoc:
  29. get_header "CONTENT_TYPE"
  30. end
  31. # Returns the accepted MIME type for the request.
  32. 1 def accepts
  33. fetch_header("action_dispatch.request.accepts") do |k|
  34. header = get_header("HTTP_ACCEPT").to_s.strip
  35. v = if header.empty?
  36. [content_mime_type]
  37. else
  38. Mime::Type.parse(header)
  39. end
  40. set_header k, v
  41. end
  42. end
  43. # Returns the MIME type for the \format used in the request.
  44. #
  45. # GET /posts/5.xml | request.format => Mime[:xml]
  46. # GET /posts/5.xhtml | request.format => Mime[:html]
  47. # GET /posts/5 | request.format => Mime[:html] or Mime[:js], or request.accepts.first
  48. #
  49. 1 def format(view_path = [])
  50. formats.first || Mime::NullType.instance
  51. end
  52. 1 def formats
  53. fetch_header("action_dispatch.request.formats") do |k|
  54. v = if params_readable?
  55. Array(Mime[parameters[:format]])
  56. elsif use_accept_header && valid_accept_header
  57. accepts
  58. elsif extension_format = format_from_path_extension
  59. [extension_format]
  60. elsif xhr?
  61. [Mime[:js]]
  62. else
  63. [Mime[:html]]
  64. end
  65. v = v.select do |format|
  66. format.symbol || format.ref == "*/*"
  67. end
  68. set_header k, v
  69. end
  70. end
  71. # Sets the \variant for template.
  72. 1 def variant=(variant)
  73. variant = Array(variant)
  74. if variant.all? { |v| v.is_a?(Symbol) }
  75. @variant = ActiveSupport::ArrayInquirer.new(variant)
  76. else
  77. raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols."
  78. end
  79. end
  80. 1 def variant
  81. @variant ||= ActiveSupport::ArrayInquirer.new
  82. end
  83. # Sets the \format by string extension, which can be used to force custom formats
  84. # that are not controlled by the extension.
  85. #
  86. # class ApplicationController < ActionController::Base
  87. # before_action :adjust_format_for_iphone
  88. #
  89. # private
  90. # def adjust_format_for_iphone
  91. # request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/]
  92. # end
  93. # end
  94. 1 def format=(extension)
  95. parameters[:format] = extension.to_s
  96. set_header "action_dispatch.request.formats", [Mime::Type.lookup_by_extension(parameters[:format])]
  97. end
  98. # Sets the \formats by string extensions. This differs from #format= by allowing you
  99. # to set multiple, ordered formats, which is useful when you want to have a fallback.
  100. #
  101. # In this example, the :iphone format will be used if it's available, otherwise it'll fallback
  102. # to the :html format.
  103. #
  104. # class ApplicationController < ActionController::Base
  105. # before_action :adjust_format_for_iphone_with_html_fallback
  106. #
  107. # private
  108. # def adjust_format_for_iphone_with_html_fallback
  109. # request.formats = [ :iphone, :html ] if request.env["HTTP_USER_AGENT"][/iPhone/]
  110. # end
  111. # end
  112. 1 def formats=(extensions)
  113. parameters[:format] = extensions.first.to_s
  114. set_header "action_dispatch.request.formats", extensions.collect { |extension|
  115. Mime::Type.lookup_by_extension(extension)
  116. }
  117. end
  118. # Returns the first MIME type that matches the provided array of MIME types.
  119. 1 def negotiate_mime(order)
  120. formats.each do |priority|
  121. if priority == Mime::ALL
  122. return order.first
  123. elsif order.include?(priority)
  124. return priority
  125. end
  126. end
  127. order.include?(Mime::ALL) ? format : nil
  128. end
  129. 1 def should_apply_vary_header?
  130. !params_readable? && use_accept_header && valid_accept_header
  131. end
  132. 1 private
  133. # We use normal content negotiation unless you include */* in your list,
  134. # in which case we assume you're a browser and send HTML.
  135. 1 BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
  136. 1 def params_readable? # :doc:
  137. parameters[:format]
  138. rescue *RESCUABLE_MIME_FORMAT_ERRORS
  139. false
  140. end
  141. 1 def valid_accept_header # :doc:
  142. (xhr? && (accept.present? || content_mime_type)) ||
  143. (accept.present? && !accept.match?(BROWSER_LIKE_ACCEPTS))
  144. end
  145. 1 def use_accept_header # :doc:
  146. !self.class.ignore_accept_header
  147. end
  148. 1 def format_from_path_extension # :doc:
  149. path = get_header("action_dispatch.original_path") || get_header("PATH_INFO")
  150. if match = path && path.match(/\.(\w+)\z/)
  151. Mime[match.captures.first]
  152. end
  153. end
  154. end
  155. end
  156. end

lib/action_dispatch/http/mime_type.rb

52.88% lines covered

191 relevant lines. 101 lines covered and 90 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "singleton"
  3. 1 require "active_support/core_ext/symbol/starts_ends_with"
  4. 1 module Mime
  5. 1 class Mimes
  6. 1 attr_reader :symbols
  7. 1 include Enumerable
  8. 1 def initialize
  9. 1 @mimes = []
  10. 1 @symbols = []
  11. end
  12. 1 def each
  13. 35 @mimes.each { |x| yield x }
  14. end
  15. 1 def <<(type)
  16. 34 @mimes << type
  17. 34 @symbols << type.to_sym
  18. end
  19. 1 def delete_if
  20. @mimes.delete_if do |x|
  21. if yield x
  22. @symbols.delete(x.to_sym)
  23. true
  24. end
  25. end
  26. end
  27. end
  28. 1 SET = Mimes.new
  29. 1 EXTENSION_LOOKUP = {}
  30. 1 LOOKUP = {}
  31. 1 class << self
  32. 1 def [](type)
  33. 2 return type if type.is_a?(Type)
  34. 2 Type.lookup_by_extension(type)
  35. end
  36. 1 def fetch(type)
  37. return type if type.is_a?(Type)
  38. EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k }
  39. end
  40. end
  41. # Encapsulates the notion of a MIME type. Can be used at render time, for example, with:
  42. #
  43. # class PostsController < ActionController::Base
  44. # def show
  45. # @post = Post.find(params[:id])
  46. #
  47. # respond_to do |format|
  48. # format.html
  49. # format.ics { render body: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") }
  50. # format.xml { render xml: @post }
  51. # end
  52. # end
  53. # end
  54. 1 class Type
  55. 1 attr_reader :symbol
  56. 1 @register_callbacks = []
  57. # A simple helper class used in parsing the accept header.
  58. 1 class AcceptItem #:nodoc:
  59. 1 attr_accessor :index, :name, :q
  60. 1 alias :to_s :name
  61. 1 def initialize(index, name, q = nil)
  62. @index = index
  63. @name = name
  64. q ||= 0.0 if @name == "*/*" # Default wildcard match to end of list.
  65. @q = ((q || 1.0).to_f * 100).to_i
  66. end
  67. 1 def <=>(item)
  68. result = item.q <=> @q
  69. result = @index <=> item.index if result == 0
  70. result
  71. end
  72. end
  73. 1 class AcceptList #:nodoc:
  74. 1 def self.sort!(list)
  75. list.sort!
  76. text_xml_idx = find_item_by_name list, "text/xml"
  77. app_xml_idx = find_item_by_name list, Mime[:xml].to_s
  78. # Take care of the broken text/xml entry by renaming or deleting it.
  79. if text_xml_idx && app_xml_idx
  80. app_xml = list[app_xml_idx]
  81. text_xml = list[text_xml_idx]
  82. app_xml.q = [text_xml.q, app_xml.q].max # Set the q value to the max of the two.
  83. if app_xml_idx > text_xml_idx # Make sure app_xml is ahead of text_xml in the list.
  84. list[app_xml_idx], list[text_xml_idx] = text_xml, app_xml
  85. app_xml_idx, text_xml_idx = text_xml_idx, app_xml_idx
  86. end
  87. list.delete_at(text_xml_idx) # Delete text_xml from the list.
  88. elsif text_xml_idx
  89. list[text_xml_idx].name = Mime[:xml].to_s
  90. end
  91. # Look for more specific XML-based types and sort them ahead of app/xml.
  92. if app_xml_idx
  93. app_xml = list[app_xml_idx]
  94. idx = app_xml_idx
  95. while idx < list.length
  96. type = list[idx]
  97. break if type.q < app_xml.q
  98. if type.name.end_with? "+xml"
  99. list[app_xml_idx], list[idx] = list[idx], app_xml
  100. app_xml_idx = idx
  101. end
  102. idx += 1
  103. end
  104. end
  105. list.map! { |i| Mime::Type.lookup(i.name) }.uniq!
  106. list
  107. end
  108. 1 def self.find_item_by_name(array, name)
  109. array.index { |item| item.name == name }
  110. end
  111. end
  112. 1 class << self
  113. 1 TRAILING_STAR_REGEXP = /^(text|application)\/\*/
  114. 1 PARAMETER_SEPARATOR_REGEXP = /;\s*\w+="?\w+"?/
  115. 1 def register_callback(&block)
  116. 1 @register_callbacks << block
  117. end
  118. 1 def lookup(string)
  119. LOOKUP[string] || Type.new(string)
  120. end
  121. 1 def lookup_by_extension(extension)
  122. 2 EXTENSION_LOOKUP[extension.to_s]
  123. end
  124. # Registers an alias that's not used on MIME type lookup, but can be referenced directly. Especially useful for
  125. # rendering different HTML versions depending on the user agent, like an iPhone.
  126. 1 def register_alias(string, symbol, extension_synonyms = [])
  127. register(string, symbol, [], extension_synonyms, true)
  128. end
  129. 1 def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false)
  130. 34 new_mime = Type.new(string, symbol, mime_type_synonyms)
  131. 34 SET << new_mime
  132. 79 ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = new_mime } unless skip_lookup
  133. 104 ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = new_mime }
  134. 34 @register_callbacks.each do |callback|
  135. callback.call(new_mime)
  136. end
  137. 34 new_mime
  138. end
  139. 1 def parse(accept_header)
  140. if !accept_header.include?(",")
  141. accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first
  142. return [] unless accept_header
  143. parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact
  144. else
  145. list, index = [], 0
  146. accept_header.split(",").each do |header|
  147. params, q = header.split(PARAMETER_SEPARATOR_REGEXP)
  148. next unless params
  149. params.strip!
  150. next if params.empty?
  151. params = parse_trailing_star(params) || [params]
  152. params.each do |m|
  153. list << AcceptItem.new(index, m.to_s, q)
  154. index += 1
  155. end
  156. end
  157. AcceptList.sort! list
  158. end
  159. end
  160. 1 def parse_trailing_star(accept_header)
  161. parse_data_with_trailing_star($1) if accept_header =~ TRAILING_STAR_REGEXP
  162. end
  163. # For an input of <tt>'text'</tt>, returns <tt>[Mime[:json], Mime[:xml], Mime[:ics],
  164. # Mime[:html], Mime[:css], Mime[:csv], Mime[:js], Mime[:yaml], Mime[:text]</tt>.
  165. #
  166. # For an input of <tt>'application'</tt>, returns <tt>[Mime[:html], Mime[:js],
  167. # Mime[:xml], Mime[:yaml], Mime[:atom], Mime[:json], Mime[:rss], Mime[:url_encoded_form]</tt>.
  168. 1 def parse_data_with_trailing_star(type)
  169. Mime::SET.select { |m| m.match?(type) }
  170. end
  171. # This method is opposite of register method.
  172. #
  173. # To unregister a MIME type:
  174. #
  175. # Mime::Type.unregister(:mobile)
  176. 1 def unregister(symbol)
  177. symbol = symbol.downcase
  178. if mime = Mime[symbol]
  179. SET.delete_if { |v| v.eql?(mime) }
  180. LOOKUP.delete_if { |_, v| v.eql?(mime) }
  181. EXTENSION_LOOKUP.delete_if { |_, v| v.eql?(mime) }
  182. end
  183. end
  184. end
  185. 1 attr_reader :hash
  186. 1 MIME_NAME = "[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}"
  187. 1 MIME_PARAMETER_KEY = "[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}"
  188. 1 MIME_PARAMETER_VALUE = "#{Regexp.escape('"')}?[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}#{Regexp.escape('"')}?"
  189. 1 MIME_PARAMETER = "\s*\;\s*#{MIME_PARAMETER_KEY}(?:\=#{MIME_PARAMETER_VALUE})?"
  190. 1 MIME_REGEXP = /\A(?:\*\/\*|#{MIME_NAME}\/(?:\*|#{MIME_NAME})(?:\s*#{MIME_PARAMETER}\s*)*)\z/
  191. 1 class InvalidMimeType < StandardError; end
  192. 1 def initialize(string, symbol = nil, synonyms = [])
  193. 35 unless MIME_REGEXP.match?(string)
  194. raise InvalidMimeType, "#{string.inspect} is not a valid MIME type"
  195. end
  196. 35 @symbol, @synonyms = symbol, synonyms
  197. 35 @string = string
  198. 35 @hash = [@string, @synonyms, @symbol].hash
  199. end
  200. 1 def to_s
  201. @string
  202. end
  203. 1 def to_str
  204. to_s
  205. end
  206. 1 def to_sym
  207. 68 @symbol
  208. end
  209. 1 def ref
  210. symbol || to_s
  211. end
  212. 1 def ===(list)
  213. if list.is_a?(Array)
  214. (@synonyms + [ self ]).any? { |synonym| list.include?(synonym) }
  215. else
  216. super
  217. end
  218. end
  219. 1 def ==(mime_type)
  220. return false unless mime_type
  221. (@synonyms + [ self ]).any? do |synonym|
  222. synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym
  223. end
  224. end
  225. 1 def eql?(other)
  226. super || (self.class == other.class &&
  227. @string == other.string &&
  228. @synonyms == other.synonyms &&
  229. @symbol == other.symbol)
  230. end
  231. 1 def =~(mime_type)
  232. return false unless mime_type
  233. regexp = Regexp.new(Regexp.quote(mime_type.to_s))
  234. @synonyms.any? { |synonym| synonym.to_s =~ regexp } || @string =~ regexp
  235. end
  236. 1 def match?(mime_type)
  237. return false unless mime_type
  238. regexp = Regexp.new(Regexp.quote(mime_type.to_s))
  239. @synonyms.any? { |synonym| synonym.to_s.match?(regexp) } || @string.match?(regexp)
  240. end
  241. 1 def html?
  242. (symbol == :html) || /html/.match?(@string)
  243. end
  244. 1 def all?; false; end
  245. 1 protected
  246. 1 attr_reader :string, :synonyms
  247. 1 private
  248. 1 def to_ary; end
  249. 1 def to_a; end
  250. 1 def method_missing(method, *args)
  251. if method.end_with?("?")
  252. method[0..-2].downcase.to_sym == to_sym
  253. else
  254. super
  255. end
  256. end
  257. 1 def respond_to_missing?(method, include_private = false)
  258. method.end_with?("?") || super
  259. end
  260. end
  261. 1 class AllType < Type
  262. 1 include Singleton
  263. 1 def initialize
  264. 1 super "*/*", nil
  265. end
  266. 1 def all?; true; end
  267. 1 def html?; true; end
  268. end
  269. # ALL isn't a real MIME type, so we don't register it for lookup with the
  270. # other concrete types. It's a wildcard match that we use for `respond_to`
  271. # negotiation internals.
  272. 1 ALL = AllType.instance
  273. 1 class NullType
  274. 1 include Singleton
  275. 1 def nil?
  276. true
  277. end
  278. 1 def to_s
  279. ""
  280. end
  281. 1 def ref; end
  282. 1 private
  283. 1 def respond_to_missing?(method, _)
  284. method.end_with?("?")
  285. end
  286. 1 def method_missing(method, *args)
  287. false if method.end_with?("?")
  288. end
  289. end
  290. end
  291. 1 require "action_dispatch/http/mime_types"

lib/action_dispatch/http/mime_types.rb

100.0% lines covered

34 relevant lines. 34 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Build list of Mime types for HTTP responses
  3. # https://www.iana.org/assignments/media-types/
  4. 1 Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml )
  5. 1 Mime::Type.register "text/plain", :text, [], %w(txt)
  6. 1 Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript )
  7. 1 Mime::Type.register "text/css", :css
  8. 1 Mime::Type.register "text/calendar", :ics
  9. 1 Mime::Type.register "text/csv", :csv
  10. 1 Mime::Type.register "text/vcard", :vcf
  11. 1 Mime::Type.register "text/vtt", :vtt, %w(vtt)
  12. 1 Mime::Type.register "image/png", :png, [], %w(png)
  13. 1 Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg)
  14. 1 Mime::Type.register "image/gif", :gif, [], %w(gif)
  15. 1 Mime::Type.register "image/bmp", :bmp, [], %w(bmp)
  16. 1 Mime::Type.register "image/tiff", :tiff, [], %w(tif tiff)
  17. 1 Mime::Type.register "image/svg+xml", :svg
  18. 1 Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe)
  19. 1 Mime::Type.register "audio/mpeg", :mp3, [], %w(mp1 mp2 mp3)
  20. 1 Mime::Type.register "audio/ogg", :ogg, [], %w(oga ogg spx opus)
  21. 1 Mime::Type.register "audio/aac", :m4a, %w( audio/mp4 ), %w(m4a mpg4 aac)
  22. 1 Mime::Type.register "video/webm", :webm, [], %w(webm)
  23. 1 Mime::Type.register "video/mp4", :mp4, [], %w(mp4 m4v)
  24. 1 Mime::Type.register "font/otf", :otf, [], %w(otf)
  25. 1 Mime::Type.register "font/ttf", :ttf, [], %w(ttf)
  26. 1 Mime::Type.register "font/woff", :woff, [], %w(woff)
  27. 1 Mime::Type.register "font/woff2", :woff2, [], %w(woff2)
  28. 1 Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml )
  29. 1 Mime::Type.register "application/rss+xml", :rss
  30. 1 Mime::Type.register "application/atom+xml", :atom
  31. 1 Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml ), %w(yml yaml)
  32. 1 Mime::Type.register "multipart/form-data", :multipart_form
  33. 1 Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form
  34. # https://www.ietf.org/rfc/rfc4627.txt
  35. # http://www.json.org/JSONRequest.html
  36. 1 Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
  37. 1 Mime::Type.register "application/pdf", :pdf, [], %w(pdf)
  38. 1 Mime::Type.register "application/zip", :zip, [], %w(zip)
  39. 1 Mime::Type.register "application/gzip", :gzip, %w(application/x-gzip), %w(gz)

lib/action_dispatch/http/parameter_filter.rb

0.0% lines covered

9 relevant lines. 0 lines covered and 9 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/deprecation/constant_accessor"
  3. require "active_support/parameter_filter"
  4. module ActionDispatch
  5. module Http
  6. include ActiveSupport::Deprecation::DeprecatedConstantAccessor
  7. deprecate_constant "ParameterFilter", "ActiveSupport::ParameterFilter",
  8. message: "ActionDispatch::Http::ParameterFilter is deprecated and will be removed from Rails 6.1. Use ActiveSupport::ParameterFilter instead."
  9. end
  10. end

lib/action_dispatch/http/parameters.rb

41.67% lines covered

60 relevant lines. 25 lines covered and 35 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module Http
  4. 1 module Parameters
  5. 1 extend ActiveSupport::Concern
  6. 1 PARAMETERS_KEY = "action_dispatch.request.path_parameters"
  7. 1 DEFAULT_PARSERS = {
  8. Mime[:json].symbol => -> (raw_post) {
  9. data = ActiveSupport::JSON.decode(raw_post)
  10. data.is_a?(Hash) ? data : { _json: data }
  11. }
  12. }
  13. # Raised when raw data from the request cannot be parsed by the parser
  14. # defined for request's content MIME type.
  15. 1 class ParseError < StandardError
  16. 1 def initialize
  17. super($!.message)
  18. end
  19. end
  20. 1 included do
  21. 1 class << self
  22. # Returns the parameter parsers.
  23. 1 attr_reader :parameter_parsers
  24. end
  25. 1 self.parameter_parsers = DEFAULT_PARSERS
  26. end
  27. 1 module ClassMethods
  28. # Configure the parameter parser for a given MIME type.
  29. #
  30. # It accepts a hash where the key is the symbol of the MIME type
  31. # and the value is a proc.
  32. #
  33. # original_parsers = ActionDispatch::Request.parameter_parsers
  34. # xml_parser = -> (raw_post) { Hash.from_xml(raw_post) || {} }
  35. # new_parsers = original_parsers.merge(xml: xml_parser)
  36. # ActionDispatch::Request.parameter_parsers = new_parsers
  37. 1 def parameter_parsers=(parsers)
  38. 2 @parameter_parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key }
  39. end
  40. end
  41. # Returns both GET and POST \parameters in a single hash.
  42. 1 def parameters
  43. params = get_header("action_dispatch.request.parameters")
  44. return params if params
  45. params = begin
  46. request_parameters.merge(query_parameters)
  47. rescue EOFError
  48. query_parameters.dup
  49. end
  50. params.merge!(path_parameters)
  51. params = set_binary_encoding(params, params[:controller], params[:action])
  52. set_header("action_dispatch.request.parameters", params)
  53. params
  54. end
  55. 1 alias :params :parameters
  56. 1 def path_parameters=(parameters) #:nodoc:
  57. delete_header("action_dispatch.request.parameters")
  58. parameters = set_binary_encoding(parameters, parameters[:controller], parameters[:action])
  59. # If any of the path parameters has an invalid encoding then
  60. # raise since it's likely to trigger errors further on.
  61. Request::Utils.check_param_encoding(parameters)
  62. set_header PARAMETERS_KEY, parameters
  63. rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
  64. raise ActionController::BadRequest.new("Invalid path parameters: #{e.message}")
  65. end
  66. # Returns a hash with the \parameters used to form the \path of the request.
  67. # Returned hash keys are strings:
  68. #
  69. # {'action' => 'my_action', 'controller' => 'my_controller'}
  70. 1 def path_parameters
  71. get_header(PARAMETERS_KEY) || set_header(PARAMETERS_KEY, {})
  72. end
  73. 1 private
  74. 1 def set_binary_encoding(params, controller, action)
  75. return params unless controller && controller.valid_encoding?
  76. if binary_params_for?(controller, action)
  77. ActionDispatch::Request::Utils.each_param_value(params.except(:controller, :action)) do |param|
  78. param.force_encoding ::Encoding::ASCII_8BIT
  79. end
  80. end
  81. params
  82. end
  83. 1 def binary_params_for?(controller, action)
  84. controller_class_for(controller).binary_params_for?(action)
  85. rescue MissingController
  86. false
  87. end
  88. 1 def parse_formatted_parameters(parsers)
  89. return yield if content_length.zero? || content_mime_type.nil?
  90. strategy = parsers.fetch(content_mime_type.symbol) { return yield }
  91. begin
  92. strategy.call(raw_post)
  93. rescue # JSON or Ruby code block errors.
  94. log_parse_error_once
  95. raise ParseError
  96. end
  97. end
  98. 1 def log_parse_error_once
  99. @parse_error_logged ||= begin
  100. parse_logger = logger || ActiveSupport::Logger.new($stderr)
  101. parse_logger.debug <<~MSG.chomp
  102. Error occurred while parsing request parameters.
  103. Contents:
  104. #{raw_post}
  105. MSG
  106. end
  107. end
  108. 1 def params_parsers
  109. ActionDispatch::Request.parameter_parsers
  110. end
  111. end
  112. end
  113. end

lib/action_dispatch/http/rack_cache.rb

52.94% lines covered

34 relevant lines. 18 lines covered and 16 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "rack/cache"
  3. 1 require "rack/cache/context"
  4. 1 require "active_support/cache"
  5. 1 module ActionDispatch
  6. 1 class RailsMetaStore < Rack::Cache::MetaStore
  7. 1 def self.resolve(uri)
  8. new
  9. end
  10. 1 def initialize(store = Rails.cache)
  11. @store = store
  12. end
  13. 1 def read(key)
  14. if data = @store.read(key)
  15. Marshal.load(data)
  16. else
  17. []
  18. end
  19. end
  20. 1 def write(key, value)
  21. @store.write(key, Marshal.dump(value))
  22. end
  23. 1 ::Rack::Cache::MetaStore::RAILS = self
  24. end
  25. 1 class RailsEntityStore < Rack::Cache::EntityStore
  26. 1 def self.resolve(uri)
  27. new
  28. end
  29. 1 def initialize(store = Rails.cache)
  30. @store = store
  31. end
  32. 1 def exist?(key)
  33. @store.exist?(key)
  34. end
  35. 1 def open(key)
  36. @store.read(key)
  37. end
  38. 1 def read(key)
  39. body = open(key)
  40. body.join if body
  41. end
  42. 1 def write(body)
  43. buf = []
  44. key, size = slurp(body) { |part| buf << part }
  45. @store.write(key, buf)
  46. [key, size]
  47. end
  48. 1 ::Rack::Cache::EntityStore::RAILS = self
  49. end
  50. end

lib/action_dispatch/http/request.rb

54.01% lines covered

187 relevant lines. 101 lines covered and 86 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "stringio"
  3. 1 require "active_support/inflector"
  4. 1 require "action_dispatch/http/headers"
  5. 1 require "action_controller/metal/exceptions"
  6. 1 require "rack/request"
  7. 1 require "action_dispatch/http/cache"
  8. 1 require "action_dispatch/http/mime_negotiation"
  9. 1 require "action_dispatch/http/parameters"
  10. 1 require "action_dispatch/http/filter_parameters"
  11. 1 require "action_dispatch/http/upload"
  12. 1 require "action_dispatch/http/url"
  13. 1 require "active_support/core_ext/array/conversions"
  14. 1 module ActionDispatch
  15. 1 class Request
  16. 1 include Rack::Request::Helpers
  17. 1 include ActionDispatch::Http::Cache::Request
  18. 1 include ActionDispatch::Http::MimeNegotiation
  19. 1 include ActionDispatch::Http::Parameters
  20. 1 include ActionDispatch::Http::FilterParameters
  21. 1 include ActionDispatch::Http::URL
  22. 1 include ActionDispatch::ContentSecurityPolicy::Request
  23. 1 include ActionDispatch::FeaturePolicy::Request
  24. 1 include Rack::Request::Env
  25. 1 autoload :Session, "action_dispatch/request/session"
  26. 1 autoload :Utils, "action_dispatch/request/utils"
  27. 1 LOCALHOST = Regexp.union [/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/]
  28. 1 ENV_METHODS = %w[ AUTH_TYPE GATEWAY_INTERFACE
  29. PATH_TRANSLATED REMOTE_HOST
  30. REMOTE_IDENT REMOTE_USER REMOTE_ADDR
  31. SERVER_NAME SERVER_PROTOCOL
  32. ORIGINAL_SCRIPT_NAME
  33. HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
  34. HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
  35. HTTP_NEGOTIATE HTTP_PRAGMA HTTP_CLIENT_IP
  36. HTTP_X_FORWARDED_FOR HTTP_ORIGIN HTTP_VERSION
  37. HTTP_X_CSRF_TOKEN HTTP_X_REQUEST_ID HTTP_X_FORWARDED_HOST
  38. SERVER_ADDR
  39. ].freeze
  40. # TODO: Remove SERVER_ADDR when we remove support to Rack 2.1.
  41. # See https://github.com/rack/rack/commit/c173b188d81ee437b588c1e046a1c9f031dea550
  42. 1 ENV_METHODS.each do |env|
  43. 26 class_eval <<-METHOD, __FILE__, __LINE__ + 1
  44. # frozen_string_literal: true
  45. def #{env.delete_prefix("HTTP_").downcase} # def accept_charset
  46. get_header "#{env}" # get_header "HTTP_ACCEPT_CHARSET"
  47. end # end
  48. METHOD
  49. end
  50. 1 def self.empty
  51. new({})
  52. end
  53. 1 def initialize(env)
  54. super
  55. @method = nil
  56. @request_method = nil
  57. @remote_ip = nil
  58. @original_fullpath = nil
  59. @fullpath = nil
  60. @ip = nil
  61. end
  62. 1 def commit_cookie_jar! # :nodoc:
  63. end
  64. 1 PASS_NOT_FOUND = Class.new { # :nodoc:
  65. 1 def self.action(_); self; end
  66. 1 def self.call(_); [404, { "X-Cascade" => "pass" }, []]; end
  67. 1 def self.binary_params_for?(action); false; end
  68. }
  69. 1 def controller_class
  70. params = path_parameters
  71. params[:action] ||= "index"
  72. controller_class_for(params[:controller])
  73. end
  74. 1 def controller_class_for(name)
  75. if name
  76. controller_param = name.underscore
  77. const_name = controller_param.camelize << "Controller"
  78. begin
  79. ActiveSupport::Dependencies.constantize(const_name)
  80. rescue NameError => error
  81. if error.missing_name == const_name || const_name.start_with?("#{error.missing_name}::")
  82. raise MissingController.new(error.message, error.name)
  83. else
  84. raise
  85. end
  86. end
  87. else
  88. PASS_NOT_FOUND
  89. end
  90. end
  91. # Returns true if the request has a header matching the given key parameter.
  92. #
  93. # request.key? :ip_spoofing_check # => true
  94. 1 def key?(key)
  95. has_header? key
  96. end
  97. # List of HTTP request methods from the following RFCs:
  98. # Hypertext Transfer Protocol -- HTTP/1.1 (https://www.ietf.org/rfc/rfc2616.txt)
  99. # HTTP Extensions for Distributed Authoring -- WEBDAV (https://www.ietf.org/rfc/rfc2518.txt)
  100. # Versioning Extensions to WebDAV (https://www.ietf.org/rfc/rfc3253.txt)
  101. # Ordered Collections Protocol (WebDAV) (https://www.ietf.org/rfc/rfc3648.txt)
  102. # Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (https://www.ietf.org/rfc/rfc3744.txt)
  103. # Web Distributed Authoring and Versioning (WebDAV) SEARCH (https://www.ietf.org/rfc/rfc5323.txt)
  104. # Calendar Extensions to WebDAV (https://www.ietf.org/rfc/rfc4791.txt)
  105. # PATCH Method for HTTP (https://www.ietf.org/rfc/rfc5789.txt)
  106. 1 RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
  107. 1 RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
  108. 1 RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
  109. 1 RFC3648 = %w(ORDERPATCH)
  110. 1 RFC3744 = %w(ACL)
  111. 1 RFC5323 = %w(SEARCH)
  112. 1 RFC4791 = %w(MKCALENDAR)
  113. 1 RFC5789 = %w(PATCH)
  114. 1 HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789
  115. 1 HTTP_METHOD_LOOKUP = {}
  116. # Populate the HTTP method lookup cache.
  117. 1 HTTP_METHODS.each { |method|
  118. 31 HTTP_METHOD_LOOKUP[method] = method.underscore.to_sym
  119. }
  120. # Returns the HTTP \method that the application should see.
  121. # In the case where the \method was overridden by a middleware
  122. # (for instance, if a HEAD request was converted to a GET,
  123. # or if a _method parameter was used to determine the \method
  124. # the application should use), this \method returns the overridden
  125. # value, not the original.
  126. 1 def request_method
  127. @request_method ||= check_method(super)
  128. end
  129. 1 def routes # :nodoc:
  130. get_header("action_dispatch.routes")
  131. end
  132. 1 def routes=(routes) # :nodoc:
  133. set_header("action_dispatch.routes", routes)
  134. end
  135. 1 def engine_script_name(_routes) # :nodoc:
  136. get_header(_routes.env_key)
  137. end
  138. 1 def engine_script_name=(name) # :nodoc:
  139. set_header(routes.env_key, name.dup)
  140. end
  141. 1 def request_method=(request_method) #:nodoc:
  142. if check_method(request_method)
  143. @request_method = set_header("REQUEST_METHOD", request_method)
  144. end
  145. end
  146. 1 def controller_instance # :nodoc:
  147. get_header("action_controller.instance")
  148. end
  149. 1 def controller_instance=(controller) # :nodoc:
  150. set_header("action_controller.instance", controller)
  151. end
  152. 1 def http_auth_salt
  153. get_header "action_dispatch.http_auth_salt"
  154. end
  155. 1 def show_exceptions? # :nodoc:
  156. # We're treating `nil` as "unset", and we want the default setting to be
  157. # `true`. This logic should be extracted to `env_config` and calculated
  158. # once.
  159. !(get_header("action_dispatch.show_exceptions") == false)
  160. end
  161. # Returns a symbol form of the #request_method.
  162. 1 def request_method_symbol
  163. HTTP_METHOD_LOOKUP[request_method]
  164. end
  165. # Returns the original value of the environment's REQUEST_METHOD,
  166. # even if it was overridden by middleware. See #request_method for
  167. # more information.
  168. 1 def method
  169. @method ||= check_method(get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD"))
  170. end
  171. # Returns a symbol form of the #method.
  172. 1 def method_symbol
  173. HTTP_METHOD_LOOKUP[method]
  174. end
  175. # Provides access to the request's HTTP headers, for example:
  176. #
  177. # request.headers["Content-Type"] # => "text/plain"
  178. 1 def headers
  179. @headers ||= Http::Headers.new(self)
  180. end
  181. # Early Hints is an HTTP/2 status code that indicates hints to help a client start
  182. # making preparations for processing the final response.
  183. #
  184. # If the env contains +rack.early_hints+ then the server accepts HTTP2 push for Link headers.
  185. #
  186. # The +send_early_hints+ method accepts a hash of links as follows:
  187. #
  188. # send_early_hints("Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload")
  189. #
  190. # If you are using +javascript_include_tag+ or +stylesheet_link_tag+ the
  191. # Early Hints headers are included by default if supported.
  192. 1 def send_early_hints(links)
  193. return unless env["rack.early_hints"]
  194. env["rack.early_hints"].call(links)
  195. end
  196. # Returns a +String+ with the last requested path including their params.
  197. #
  198. # # get '/foo'
  199. # request.original_fullpath # => '/foo'
  200. #
  201. # # get '/foo?bar'
  202. # request.original_fullpath # => '/foo?bar'
  203. 1 def original_fullpath
  204. @original_fullpath ||= (get_header("ORIGINAL_FULLPATH") || fullpath)
  205. end
  206. # Returns the +String+ full path including params of the last URL requested.
  207. #
  208. # # get "/articles"
  209. # request.fullpath # => "/articles"
  210. #
  211. # # get "/articles?page=2"
  212. # request.fullpath # => "/articles?page=2"
  213. 1 def fullpath
  214. @fullpath ||= super
  215. end
  216. # Returns the original request URL as a +String+.
  217. #
  218. # # get "/articles?page=2"
  219. # request.original_url # => "http://www.example.com/articles?page=2"
  220. 1 def original_url
  221. base_url + original_fullpath
  222. end
  223. # The +String+ MIME type of the request.
  224. #
  225. # # get "/articles"
  226. # request.media_type # => "application/x-www-form-urlencoded"
  227. 1 def media_type
  228. content_mime_type.to_s
  229. end
  230. # Returns the content length of the request as an integer.
  231. 1 def content_length
  232. super.to_i
  233. end
  234. # Returns true if the "X-Requested-With" header contains "XMLHttpRequest"
  235. # (case-insensitive), which may need to be manually added depending on the
  236. # choice of JavaScript libraries and frameworks.
  237. 1 def xml_http_request?
  238. /XMLHttpRequest/i.match?(get_header("HTTP_X_REQUESTED_WITH"))
  239. end
  240. 1 alias :xhr? :xml_http_request?
  241. # Returns the IP address of client as a +String+.
  242. 1 def ip
  243. @ip ||= super
  244. end
  245. # Returns the IP address of client as a +String+,
  246. # usually set by the RemoteIp middleware.
  247. 1 def remote_ip
  248. @remote_ip ||= (get_header("action_dispatch.remote_ip") || ip).to_s
  249. end
  250. 1 def remote_ip=(remote_ip)
  251. @remote_ip = nil
  252. set_header "action_dispatch.remote_ip", remote_ip
  253. end
  254. 1 ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id" # :nodoc:
  255. # Returns the unique request id, which is based on either the X-Request-Id header that can
  256. # be generated by a firewall, load balancer, or web server or by the RequestId middleware
  257. # (which sets the action_dispatch.request_id environment variable).
  258. #
  259. # This unique ID is useful for tracing a request from end-to-end as part of logging or debugging.
  260. # This relies on the Rack variable set by the ActionDispatch::RequestId middleware.
  261. 1 def request_id
  262. get_header ACTION_DISPATCH_REQUEST_ID
  263. end
  264. 1 def request_id=(id) # :nodoc:
  265. set_header ACTION_DISPATCH_REQUEST_ID, id
  266. end
  267. 1 alias_method :uuid, :request_id
  268. # Returns the lowercase name of the HTTP server software.
  269. 1 def server_software
  270. (get_header("SERVER_SOFTWARE") && /^([a-zA-Z]+)/ =~ get_header("SERVER_SOFTWARE")) ? $1.downcase : nil
  271. end
  272. # Read the request \body. This is useful for web services that need to
  273. # work with raw requests directly.
  274. 1 def raw_post
  275. unless has_header? "RAW_POST_DATA"
  276. raw_post_body = body
  277. set_header("RAW_POST_DATA", raw_post_body.read(content_length))
  278. raw_post_body.rewind if raw_post_body.respond_to?(:rewind)
  279. end
  280. get_header "RAW_POST_DATA"
  281. end
  282. # The request body is an IO input stream. If the RAW_POST_DATA environment
  283. # variable is already set, wrap it in a StringIO.
  284. 1 def body
  285. if raw_post = get_header("RAW_POST_DATA")
  286. raw_post = (+raw_post).force_encoding(Encoding::BINARY)
  287. StringIO.new(raw_post)
  288. else
  289. body_stream
  290. end
  291. end
  292. # Determine whether the request body contains form-data by checking
  293. # the request Content-Type for one of the media-types:
  294. # "application/x-www-form-urlencoded" or "multipart/form-data". The
  295. # list of form-data media types can be modified through the
  296. # +FORM_DATA_MEDIA_TYPES+ array.
  297. #
  298. # A request body is not assumed to contain form-data when no
  299. # Content-Type header is provided and the request_method is POST.
  300. 1 def form_data?
  301. FORM_DATA_MEDIA_TYPES.include?(media_type)
  302. end
  303. 1 def body_stream #:nodoc:
  304. get_header("rack.input")
  305. end
  306. # TODO This should be broken apart into AD::Request::Session and probably
  307. # be included by the session middleware.
  308. 1 def reset_session
  309. if session && session.respond_to?(:destroy)
  310. session.destroy
  311. else
  312. self.session = {}
  313. end
  314. end
  315. 1 def session=(session) #:nodoc:
  316. Session.set self, session
  317. end
  318. 1 def session_options=(options)
  319. Session::Options.set self, options
  320. end
  321. # Override Rack's GET method to support indifferent access.
  322. 1 def GET
  323. fetch_header("action_dispatch.request.query_parameters") do |k|
  324. rack_query_params = super || {}
  325. # Check for non UTF-8 parameter values, which would cause errors later
  326. Request::Utils.check_param_encoding(rack_query_params)
  327. set_header k, Request::Utils.normalize_encode_params(rack_query_params)
  328. end
  329. rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
  330. raise ActionController::BadRequest.new("Invalid query parameters: #{e.message}")
  331. end
  332. 1 alias :query_parameters :GET
  333. # Override Rack's POST method to support indifferent access.
  334. 1 def POST
  335. fetch_header("action_dispatch.request.request_parameters") do
  336. pr = parse_formatted_parameters(params_parsers) do |params|
  337. super || {}
  338. end
  339. self.request_parameters = Request::Utils.normalize_encode_params(pr)
  340. end
  341. rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
  342. raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}")
  343. end
  344. 1 alias :request_parameters :POST
  345. # Returns the authorization header regardless of whether it was specified directly or through one of the
  346. # proxy alternatives.
  347. 1 def authorization
  348. get_header("HTTP_AUTHORIZATION") ||
  349. get_header("X-HTTP_AUTHORIZATION") ||
  350. get_header("X_HTTP_AUTHORIZATION") ||
  351. get_header("REDIRECT_X_HTTP_AUTHORIZATION")
  352. end
  353. # True if the request came from localhost, 127.0.0.1, or ::1.
  354. 1 def local?
  355. LOCALHOST.match?(remote_addr) && LOCALHOST.match?(remote_ip)
  356. end
  357. 1 def request_parameters=(params)
  358. raise if params.nil?
  359. set_header("action_dispatch.request.request_parameters", params)
  360. end
  361. 1 def logger
  362. get_header("action_dispatch.logger")
  363. end
  364. 1 def commit_flash
  365. end
  366. 1 def ssl?
  367. super || scheme == "wss"
  368. end
  369. 1 private
  370. 1 def check_method(name)
  371. HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}")
  372. name
  373. end
  374. end
  375. end
  376. 1 ActiveSupport.run_load_hooks :action_dispatch_request, ActionDispatch::Request

lib/action_dispatch/http/response.rb

42.13% lines covered

254 relevant lines. 107 lines covered and 147 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/module/attribute_accessors"
  3. 1 require "action_dispatch/http/filter_redirect"
  4. 1 require "action_dispatch/http/cache"
  5. 1 require "monitor"
  6. 1 module ActionDispatch # :nodoc:
  7. # Represents an HTTP response generated by a controller action. Use it to
  8. # retrieve the current state of the response, or customize the response. It can
  9. # either represent a real HTTP response (i.e. one that is meant to be sent
  10. # back to the web browser) or a TestResponse (i.e. one that is generated
  11. # from integration tests).
  12. #
  13. # \Response is mostly a Ruby on \Rails framework implementation detail, and
  14. # should never be used directly in controllers. Controllers should use the
  15. # methods defined in ActionController::Base instead. For example, if you want
  16. # to set the HTTP response's content MIME type, then use
  17. # ActionControllerBase#headers instead of Response#headers.
  18. #
  19. # Nevertheless, integration tests may want to inspect controller responses in
  20. # more detail, and that's when \Response can be useful for application
  21. # developers. Integration test methods such as
  22. # ActionDispatch::Integration::Session#get and
  23. # ActionDispatch::Integration::Session#post return objects of type
  24. # TestResponse (which are of course also of type \Response).
  25. #
  26. # For example, the following demo integration test prints the body of the
  27. # controller response to the console:
  28. #
  29. # class DemoControllerTest < ActionDispatch::IntegrationTest
  30. # def test_print_root_path_to_console
  31. # get('/')
  32. # puts response.body
  33. # end
  34. # end
  35. 1 class Response
  36. 1 class Header < DelegateClass(Hash) # :nodoc:
  37. 1 def initialize(response, header)
  38. @response = response
  39. super(header)
  40. end
  41. 1 def []=(k, v)
  42. if @response.sending? || @response.sent?
  43. raise ActionDispatch::IllegalStateError, "header already sent"
  44. end
  45. super
  46. end
  47. 1 def merge(other)
  48. self.class.new @response, __getobj__.merge(other)
  49. end
  50. 1 def to_hash
  51. __getobj__.dup
  52. end
  53. end
  54. # The request that the response is responding to.
  55. 1 attr_accessor :request
  56. # The HTTP status code.
  57. 1 attr_reader :status
  58. # Get headers for this response.
  59. 1 attr_reader :header
  60. 1 alias_method :headers, :header
  61. 1 delegate :[], :[]=, to: :@header
  62. 1 def each(&block)
  63. sending!
  64. x = @stream.each(&block)
  65. sent!
  66. x
  67. end
  68. 1 CONTENT_TYPE = "Content-Type"
  69. 1 SET_COOKIE = "Set-Cookie"
  70. 1 LOCATION = "Location"
  71. 1 NO_CONTENT_CODES = [100, 101, 102, 103, 204, 205, 304]
  72. 1 cattr_accessor :default_charset, default: "utf-8"
  73. 1 cattr_accessor :default_headers
  74. 1 cattr_accessor :return_only_media_type_on_content_type, default: false
  75. 1 include Rack::Response::Helpers
  76. # Aliasing these off because AD::Http::Cache::Response defines them.
  77. 1 alias :_cache_control :cache_control
  78. 1 alias :_cache_control= :cache_control=
  79. 1 include ActionDispatch::Http::FilterRedirect
  80. 1 include ActionDispatch::Http::Cache::Response
  81. 1 include MonitorMixin
  82. 1 class Buffer # :nodoc:
  83. 1 def initialize(response, buf)
  84. @response = response
  85. @buf = buf
  86. @closed = false
  87. @str_body = nil
  88. end
  89. 1 def body
  90. @str_body ||= begin
  91. buf = +""
  92. each { |chunk| buf << chunk }
  93. buf
  94. end
  95. end
  96. 1 def write(string)
  97. raise IOError, "closed stream" if closed?
  98. @str_body = nil
  99. @response.commit!
  100. @buf.push string
  101. end
  102. 1 def each(&block)
  103. if @str_body
  104. return enum_for(:each) unless block_given?
  105. yield @str_body
  106. else
  107. each_chunk(&block)
  108. end
  109. end
  110. 1 def abort
  111. end
  112. 1 def close
  113. @response.commit!
  114. @closed = true
  115. end
  116. 1 def closed?
  117. @closed
  118. end
  119. 1 private
  120. 1 def each_chunk(&block)
  121. @buf.each(&block)
  122. end
  123. end
  124. 1 def self.create(status = 200, header = {}, body = [], default_headers: self.default_headers)
  125. header = merge_default_headers(header, default_headers)
  126. new status, header, body
  127. end
  128. 1 def self.merge_default_headers(original, default)
  129. default.respond_to?(:merge) ? default.merge(original) : original
  130. end
  131. # The underlying body, as a streamable object.
  132. 1 attr_reader :stream
  133. 1 def initialize(status = 200, header = {}, body = [])
  134. super()
  135. @header = Header.new(self, header)
  136. self.body, self.status = body, status
  137. @cv = new_cond
  138. @committed = false
  139. @sending = false
  140. @sent = false
  141. prepare_cache_control!
  142. yield self if block_given?
  143. end
  144. 1 def has_header?(key); headers.key? key; end
  145. 1 def get_header(key); headers[key]; end
  146. 1 def set_header(key, v); headers[key] = v; end
  147. 1 def delete_header(key); headers.delete key; end
  148. 1 def await_commit
  149. synchronize do
  150. @cv.wait_until { @committed }
  151. end
  152. end
  153. 1 def await_sent
  154. synchronize { @cv.wait_until { @sent } }
  155. end
  156. 1 def commit!
  157. synchronize do
  158. before_committed
  159. @committed = true
  160. @cv.broadcast
  161. end
  162. end
  163. 1 def sending!
  164. synchronize do
  165. before_sending
  166. @sending = true
  167. @cv.broadcast
  168. end
  169. end
  170. 1 def sent!
  171. synchronize do
  172. @sent = true
  173. @cv.broadcast
  174. end
  175. end
  176. 1 def sending?; synchronize { @sending }; end
  177. 1 def committed?; synchronize { @committed }; end
  178. 1 def sent?; synchronize { @sent }; end
  179. # Sets the HTTP status code.
  180. 1 def status=(status)
  181. @status = Rack::Utils.status_code(status)
  182. end
  183. # Sets the HTTP response's content MIME type. For example, in the controller
  184. # you could write this:
  185. #
  186. # response.content_type = "text/plain"
  187. #
  188. # If a character set has been defined for this response (see charset=) then
  189. # the character set information will also be included in the content type
  190. # information.
  191. 1 def content_type=(content_type)
  192. return unless content_type
  193. new_header_info = parse_content_type(content_type.to_s)
  194. prev_header_info = parsed_content_type_header
  195. charset = new_header_info.charset || prev_header_info.charset
  196. charset ||= self.class.default_charset unless prev_header_info.mime_type
  197. set_content_type new_header_info.mime_type, charset
  198. end
  199. # Content type of response.
  200. 1 def content_type
  201. if self.class.return_only_media_type_on_content_type
  202. ActiveSupport::Deprecation.warn(
  203. "Rails 6.1 will return Content-Type header without modification." \
  204. " If you want just the MIME type, please use `#media_type` instead."
  205. )
  206. content_type = super
  207. content_type ? content_type.split(/;\s*charset=/)[0].presence : content_type
  208. else
  209. super.presence
  210. end
  211. end
  212. # Media type of response.
  213. 1 def media_type
  214. parsed_content_type_header.mime_type
  215. end
  216. 1 def sending_file=(v)
  217. if true == v
  218. self.charset = false
  219. end
  220. end
  221. # Sets the HTTP character set. In case of +nil+ parameter
  222. # it sets the charset to +default_charset+.
  223. #
  224. # response.charset = 'utf-16' # => 'utf-16'
  225. # response.charset = nil # => 'utf-8'
  226. 1 def charset=(charset)
  227. content_type = parsed_content_type_header.mime_type
  228. if false == charset
  229. set_content_type content_type, nil
  230. else
  231. set_content_type content_type, charset || self.class.default_charset
  232. end
  233. end
  234. # The charset of the response. HTML wants to know the encoding of the
  235. # content you're giving them, so we need to send that along.
  236. 1 def charset
  237. header_info = parsed_content_type_header
  238. header_info.charset || self.class.default_charset
  239. end
  240. # The response code of the request.
  241. 1 def response_code
  242. @status
  243. end
  244. # Returns a string to ensure compatibility with <tt>Net::HTTPResponse</tt>.
  245. 1 def code
  246. @status.to_s
  247. end
  248. # Returns the corresponding message for the current HTTP status code:
  249. #
  250. # response.status = 200
  251. # response.message # => "OK"
  252. #
  253. # response.status = 404
  254. # response.message # => "Not Found"
  255. #
  256. 1 def message
  257. Rack::Utils::HTTP_STATUS_CODES[@status]
  258. end
  259. 1 alias_method :status_message, :message
  260. # Returns the content of the response as a string. This contains the contents
  261. # of any calls to <tt>render</tt>.
  262. 1 def body
  263. @stream.body
  264. end
  265. 1 def write(string)
  266. @stream.write string
  267. end
  268. # Allows you to manually set or override the response body.
  269. 1 def body=(body)
  270. if body.respond_to?(:to_path)
  271. @stream = body
  272. else
  273. synchronize do
  274. @stream = build_buffer self, munge_body_object(body)
  275. end
  276. end
  277. end
  278. # Avoid having to pass an open file handle as the response body.
  279. # Rack::Sendfile will usually intercept the response and uses
  280. # the path directly, so there is no reason to open the file.
  281. 1 class FileBody #:nodoc:
  282. 1 attr_reader :to_path
  283. 1 def initialize(path)
  284. @to_path = path
  285. end
  286. 1 def body
  287. File.binread(to_path)
  288. end
  289. # Stream the file's contents if Rack::Sendfile isn't present.
  290. 1 def each
  291. File.open(to_path, "rb") do |file|
  292. while chunk = file.read(16384)
  293. yield chunk
  294. end
  295. end
  296. end
  297. end
  298. # Send the file stored at +path+ as the response body.
  299. 1 def send_file(path)
  300. commit!
  301. @stream = FileBody.new(path)
  302. end
  303. 1 def reset_body!
  304. @stream = build_buffer(self, [])
  305. end
  306. 1 def body_parts
  307. parts = []
  308. @stream.each { |x| parts << x }
  309. parts
  310. end
  311. # The location header we'll be responding with.
  312. 1 alias_method :redirect_url, :location
  313. 1 def close
  314. stream.close if stream.respond_to?(:close)
  315. end
  316. 1 def abort
  317. if stream.respond_to?(:abort)
  318. stream.abort
  319. elsif stream.respond_to?(:close)
  320. # `stream.close` should really be reserved for a close from the
  321. # other direction, but we must fall back to it for
  322. # compatibility.
  323. stream.close
  324. end
  325. end
  326. # Turns the Response into a Rack-compatible array of the status, headers,
  327. # and body. Allows explicit splatting:
  328. #
  329. # status, headers, body = *response
  330. 1 def to_a
  331. commit!
  332. rack_response @status, @header.to_hash
  333. end
  334. 1 alias prepare! to_a
  335. # Returns the response cookies, converted to a Hash of (name => value) pairs
  336. #
  337. # assert_equal 'AuthorOfNewPage', r.cookies['author']
  338. 1 def cookies
  339. cookies = {}
  340. if header = get_header(SET_COOKIE)
  341. header = header.split("\n") if header.respond_to?(:to_str)
  342. header.each do |cookie|
  343. if pair = cookie.split(";").first
  344. key, value = pair.split("=").map { |v| Rack::Utils.unescape(v) }
  345. cookies[key] = value
  346. end
  347. end
  348. end
  349. cookies
  350. end
  351. 1 private
  352. 1 ContentTypeHeader = Struct.new :mime_type, :charset
  353. 1 NullContentTypeHeader = ContentTypeHeader.new nil, nil
  354. 1 CONTENT_TYPE_PARSER = /
  355. \A
  356. (?<mime_type>[^;\s]+\s*(?:;\s*(?:(?!charset)[^;\s])+)*)?
  357. (?:;\s*charset=(?<quote>"?)(?<charset>[^;\s]+)\k<quote>)?
  358. /x # :nodoc:
  359. 1 def parse_content_type(content_type)
  360. if content_type && match = CONTENT_TYPE_PARSER.match(content_type)
  361. ContentTypeHeader.new(match[:mime_type], match[:charset])
  362. else
  363. NullContentTypeHeader
  364. end
  365. end
  366. # Small internal convenience method to get the parsed version of the current
  367. # content type header.
  368. 1 def parsed_content_type_header
  369. parse_content_type(get_header(CONTENT_TYPE))
  370. end
  371. 1 def set_content_type(content_type, charset)
  372. type = content_type || ""
  373. type = "#{type}; charset=#{charset.to_s.downcase}" if charset
  374. set_header CONTENT_TYPE, type
  375. end
  376. 1 def before_committed
  377. return if committed?
  378. assign_default_content_type_and_charset!
  379. merge_and_normalize_cache_control!(@cache_control)
  380. handle_conditional_get!
  381. handle_no_content!
  382. end
  383. 1 def before_sending
  384. # Normally we've already committed by now, but it's possible
  385. # (e.g., if the controller action tries to read back its own
  386. # response) to get here before that. In that case, we must force
  387. # an "early" commit: we're about to freeze the headers, so this is
  388. # our last chance.
  389. commit! unless committed?
  390. headers.freeze
  391. request.commit_cookie_jar! unless committed?
  392. end
  393. 1 def build_buffer(response, body)
  394. Buffer.new response, body
  395. end
  396. 1 def munge_body_object(body)
  397. body.respond_to?(:each) ? body : [body]
  398. end
  399. 1 def assign_default_content_type_and_charset!
  400. return if media_type
  401. ct = parsed_content_type_header
  402. set_content_type(ct.mime_type || Mime[:html].to_s,
  403. ct.charset || self.class.default_charset)
  404. end
  405. 1 class RackBody
  406. 1 def initialize(response)
  407. @response = response
  408. end
  409. 1 def each(*args, &block)
  410. @response.each(*args, &block)
  411. end
  412. 1 def close
  413. # Rack "close" maps to Response#abort, and *not* Response#close
  414. # (which is used when the controller's finished writing)
  415. @response.abort
  416. end
  417. 1 def body
  418. @response.body
  419. end
  420. 1 def respond_to?(method, include_private = false)
  421. if method.to_sym == :to_path
  422. @response.stream.respond_to?(method)
  423. else
  424. super
  425. end
  426. end
  427. 1 def to_path
  428. @response.stream.to_path
  429. end
  430. 1 def to_ary
  431. nil
  432. end
  433. end
  434. 1 def handle_no_content!
  435. if NO_CONTENT_CODES.include?(@status)
  436. @header.delete CONTENT_TYPE
  437. @header.delete "Content-Length"
  438. end
  439. end
  440. 1 def rack_response(status, header)
  441. if NO_CONTENT_CODES.include?(status)
  442. [status, header, []]
  443. else
  444. [status, header, RackBody.new(self)]
  445. end
  446. end
  447. end
  448. 1 ActiveSupport.run_load_hooks(:action_dispatch_response, Response)
  449. end

lib/action_dispatch/http/upload.rb

47.22% lines covered

36 relevant lines. 17 lines covered and 19 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module Http
  4. # Models uploaded files.
  5. #
  6. # The actual file is accessible via the +tempfile+ accessor, though some
  7. # of its interface is available directly for convenience.
  8. #
  9. # Uploaded files are temporary files whose lifespan is one request. When
  10. # the object is finalized Ruby unlinks the file, so there is no need to
  11. # clean them with a separate maintenance task.
  12. 1 class UploadedFile
  13. # The basename of the file in the client.
  14. 1 attr_accessor :original_filename
  15. # A string with the MIME type of the file.
  16. 1 attr_accessor :content_type
  17. # A +Tempfile+ object with the actual uploaded file. Note that some of
  18. # its interface is available directly.
  19. 1 attr_accessor :tempfile
  20. # A string with the headers of the multipart request.
  21. 1 attr_accessor :headers
  22. 1 def initialize(hash) # :nodoc:
  23. @tempfile = hash[:tempfile]
  24. raise(ArgumentError, ":tempfile is required") unless @tempfile
  25. if hash[:filename]
  26. @original_filename = hash[:filename].dup
  27. begin
  28. @original_filename.encode!(Encoding::UTF_8)
  29. rescue EncodingError
  30. @original_filename.force_encoding(Encoding::UTF_8)
  31. end
  32. else
  33. @original_filename = nil
  34. end
  35. @content_type = hash[:type]
  36. @headers = hash[:head]
  37. end
  38. # Shortcut for +tempfile.read+.
  39. 1 def read(length = nil, buffer = nil)
  40. @tempfile.read(length, buffer)
  41. end
  42. # Shortcut for +tempfile.open+.
  43. 1 def open
  44. @tempfile.open
  45. end
  46. # Shortcut for +tempfile.close+.
  47. 1 def close(unlink_now = false)
  48. @tempfile.close(unlink_now)
  49. end
  50. # Shortcut for +tempfile.path+.
  51. 1 def path
  52. @tempfile.path
  53. end
  54. # Shortcut for +tempfile.to_path+.
  55. 1 def to_path
  56. @tempfile.to_path
  57. end
  58. # Shortcut for +tempfile.rewind+.
  59. 1 def rewind
  60. @tempfile.rewind
  61. end
  62. # Shortcut for +tempfile.size+.
  63. 1 def size
  64. @tempfile.size
  65. end
  66. # Shortcut for +tempfile.eof?+.
  67. 1 def eof?
  68. @tempfile.eof?
  69. end
  70. 1 def to_io
  71. @tempfile.to_io
  72. end
  73. end
  74. end
  75. end

lib/action_dispatch/http/url.rb

30.88% lines covered

136 relevant lines. 42 lines covered and 94 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/module/attribute_accessors"
  3. 1 module ActionDispatch
  4. 1 module Http
  5. 1 module URL
  6. 1 IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
  7. 1 HOST_REGEXP = /(^[^:]+:\/\/)?(\[[^\]]+\]|[^:]+)(?::(\d+$))?/
  8. 1 PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/
  9. 1 mattr_accessor :secure_protocol, default: false
  10. 1 mattr_accessor :tld_length, default: 1
  11. 1 class << self
  12. # Returns the domain part of a host given the domain level.
  13. #
  14. # # Top-level domain example
  15. # extract_domain('www.example.com', 1) # => "example.com"
  16. # # Second-level domain example
  17. # extract_domain('dev.www.example.co.uk', 2) # => "example.co.uk"
  18. 1 def extract_domain(host, tld_length)
  19. extract_domain_from(host, tld_length) if named_host?(host)
  20. end
  21. # Returns the subdomains of a host as an Array given the domain level.
  22. #
  23. # # Top-level domain example
  24. # extract_subdomains('www.example.com', 1) # => ["www"]
  25. # # Second-level domain example
  26. # extract_subdomains('dev.www.example.co.uk', 2) # => ["dev", "www"]
  27. 1 def extract_subdomains(host, tld_length)
  28. if named_host?(host)
  29. extract_subdomains_from(host, tld_length)
  30. else
  31. []
  32. end
  33. end
  34. # Returns the subdomains of a host as a String given the domain level.
  35. #
  36. # # Top-level domain example
  37. # extract_subdomain('www.example.com', 1) # => "www"
  38. # # Second-level domain example
  39. # extract_subdomain('dev.www.example.co.uk', 2) # => "dev.www"
  40. 1 def extract_subdomain(host, tld_length)
  41. extract_subdomains(host, tld_length).join(".")
  42. end
  43. 1 def url_for(options)
  44. if options[:only_path]
  45. path_for options
  46. else
  47. full_url_for options
  48. end
  49. end
  50. 1 def full_url_for(options)
  51. host = options[:host]
  52. protocol = options[:protocol]
  53. port = options[:port]
  54. unless host
  55. raise ArgumentError, "Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true"
  56. end
  57. build_host_url(host, port, protocol, options, path_for(options))
  58. end
  59. 1 def path_for(options)
  60. path = options[:script_name].to_s.chomp("/")
  61. path << options[:path] if options.key?(:path)
  62. add_trailing_slash(path) if options[:trailing_slash]
  63. add_params(path, options[:params]) if options.key?(:params)
  64. add_anchor(path, options[:anchor]) if options.key?(:anchor)
  65. path
  66. end
  67. 1 private
  68. 1 def add_params(path, params)
  69. params = { params: params } unless params.is_a?(Hash)
  70. params.reject! { |_, v| v.to_param.nil? }
  71. query = params.to_query
  72. path << "?#{query}" unless query.empty?
  73. end
  74. 1 def add_anchor(path, anchor)
  75. if anchor
  76. path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param)}"
  77. end
  78. end
  79. 1 def extract_domain_from(host, tld_length)
  80. host.split(".").last(1 + tld_length).join(".")
  81. end
  82. 1 def extract_subdomains_from(host, tld_length)
  83. parts = host.split(".")
  84. parts[0..-(tld_length + 2)]
  85. end
  86. 1 def add_trailing_slash(path)
  87. if path.include?("?")
  88. path.sub!(/\?/, '/\&')
  89. elsif !path.include?(".")
  90. path.sub!(/[^\/]\z|\A\z/, '\&/')
  91. end
  92. end
  93. 1 def build_host_url(host, port, protocol, options, path)
  94. if match = host.match(HOST_REGEXP)
  95. protocol ||= match[1] unless protocol == false
  96. host = match[2]
  97. port = match[3] unless options.key? :port
  98. end
  99. protocol = normalize_protocol protocol
  100. host = normalize_host(host, options)
  101. result = protocol.dup
  102. if options[:user] && options[:password]
  103. result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
  104. end
  105. result << host
  106. normalize_port(port, protocol) { |normalized_port|
  107. result << ":#{normalized_port}"
  108. }
  109. result.concat path
  110. end
  111. 1 def named_host?(host)
  112. !IP_HOST_REGEXP.match?(host)
  113. end
  114. 1 def normalize_protocol(protocol)
  115. case protocol
  116. when nil
  117. secure_protocol ? "https://" : "http://"
  118. when false, "//"
  119. "//"
  120. when PROTOCOL_REGEXP
  121. "#{$1}://"
  122. else
  123. raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}"
  124. end
  125. end
  126. 1 def normalize_host(_host, options)
  127. return _host unless named_host?(_host)
  128. tld_length = options[:tld_length] || @@tld_length
  129. subdomain = options.fetch :subdomain, true
  130. domain = options[:domain]
  131. host = +""
  132. if subdomain == true
  133. return _host if domain.nil?
  134. host << extract_subdomains_from(_host, tld_length).join(".")
  135. elsif subdomain
  136. host << subdomain.to_param
  137. end
  138. host << "." unless host.empty?
  139. host << (domain || extract_domain_from(_host, tld_length))
  140. host
  141. end
  142. 1 def normalize_port(port, protocol)
  143. return unless port
  144. case protocol
  145. when "//" then yield port
  146. when "https://"
  147. yield port unless port.to_i == 443
  148. else
  149. yield port unless port.to_i == 80
  150. end
  151. end
  152. end
  153. 1 def initialize
  154. super
  155. @protocol = nil
  156. @port = nil
  157. end
  158. # Returns the complete URL used for this request.
  159. #
  160. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
  161. # req.url # => "http://example.com"
  162. 1 def url
  163. protocol + host_with_port + fullpath
  164. end
  165. # Returns 'https://' if this is an SSL request and 'http://' otherwise.
  166. #
  167. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
  168. # req.protocol # => "http://"
  169. #
  170. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on'
  171. # req.protocol # => "https://"
  172. 1 def protocol
  173. @protocol ||= ssl? ? "https://" : "http://"
  174. end
  175. # Returns the \host and port for this request, such as "example.com:8080".
  176. #
  177. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
  178. # req.raw_host_with_port # => "example.com"
  179. #
  180. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
  181. # req.raw_host_with_port # => "example.com:80"
  182. #
  183. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
  184. # req.raw_host_with_port # => "example.com:8080"
  185. 1 def raw_host_with_port
  186. if forwarded = x_forwarded_host.presence
  187. forwarded.split(/,\s?/).last
  188. else
  189. get_header("HTTP_HOST") || "#{server_name || server_addr}:#{get_header('SERVER_PORT')}"
  190. end
  191. end
  192. # Returns the host for this request, such as "example.com".
  193. #
  194. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
  195. # req.host # => "example.com"
  196. 1 def host
  197. raw_host_with_port.sub(/:\d+$/, "")
  198. end
  199. # Returns a \host:\port string for this request, such as "example.com" or
  200. # "example.com:8080". Port is only included if it is not a default port
  201. # (80 or 443)
  202. #
  203. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
  204. # req.host_with_port # => "example.com"
  205. #
  206. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
  207. # req.host_with_port # => "example.com"
  208. #
  209. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
  210. # req.host_with_port # => "example.com:8080"
  211. 1 def host_with_port
  212. "#{host}#{port_string}"
  213. end
  214. # Returns the port number of this request as an integer.
  215. #
  216. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
  217. # req.port # => 80
  218. #
  219. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
  220. # req.port # => 8080
  221. 1 def port
  222. @port ||= begin
  223. if raw_host_with_port =~ /:(\d+)$/
  224. $1.to_i
  225. else
  226. standard_port
  227. end
  228. end
  229. end
  230. # Returns the standard \port number for this request's protocol.
  231. #
  232. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
  233. # req.standard_port # => 80
  234. 1 def standard_port
  235. case protocol
  236. when "https://" then 443
  237. else 80
  238. end
  239. end
  240. # Returns whether this request is using the standard port
  241. #
  242. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
  243. # req.standard_port? # => true
  244. #
  245. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
  246. # req.standard_port? # => false
  247. 1 def standard_port?
  248. port == standard_port
  249. end
  250. # Returns a number \port suffix like 8080 if the \port number of this request
  251. # is not the default HTTP \port 80 or HTTPS \port 443.
  252. #
  253. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
  254. # req.optional_port # => nil
  255. #
  256. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
  257. # req.optional_port # => 8080
  258. 1 def optional_port
  259. standard_port? ? nil : port
  260. end
  261. # Returns a string \port suffix, including colon, like ":8080" if the \port
  262. # number of this request is not the default HTTP \port 80 or HTTPS \port 443.
  263. #
  264. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
  265. # req.port_string # => ""
  266. #
  267. # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
  268. # req.port_string # => ":8080"
  269. 1 def port_string
  270. standard_port? ? "" : ":#{port}"
  271. end
  272. # Returns the requested port, such as 8080, based on SERVER_PORT
  273. #
  274. # req = ActionDispatch::Request.new 'SERVER_PORT' => '80'
  275. # req.server_port # => 80
  276. #
  277. # req = ActionDispatch::Request.new 'SERVER_PORT' => '8080'
  278. # req.server_port # => 8080
  279. 1 def server_port
  280. get_header("SERVER_PORT").to_i
  281. end
  282. # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
  283. # a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
  284. 1 def domain(tld_length = @@tld_length)
  285. ActionDispatch::Http::URL.extract_domain(host, tld_length)
  286. end
  287. # Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be
  288. # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
  289. # such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt>
  290. # in "www.rubyonrails.co.uk".
  291. 1 def subdomains(tld_length = @@tld_length)
  292. ActionDispatch::Http::URL.extract_subdomains(host, tld_length)
  293. end
  294. # Returns all the \subdomains as a string, so <tt>"dev.www"</tt> would be
  295. # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
  296. # such as 2 to catch <tt>"www"</tt> instead of <tt>"www.rubyonrails"</tt>
  297. # in "www.rubyonrails.co.uk".
  298. 1 def subdomain(tld_length = @@tld_length)
  299. ActionDispatch::Http::URL.extract_subdomain(host, tld_length)
  300. end
  301. end
  302. end
  303. end

lib/action_dispatch/journey.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_dispatch/journey/router"
  3. 1 require "action_dispatch/journey/gtg/builder"
  4. 1 require "action_dispatch/journey/gtg/simulator"

lib/action_dispatch/journey/formatter.rb

25.86% lines covered

116 relevant lines. 30 lines covered and 86 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_controller/metal/exceptions"
  3. 1 module ActionDispatch
  4. # :stopdoc:
  5. 1 module Journey
  6. # The Formatter class is used for formatting URLs. For example, parameters
  7. # passed to +url_for+ in Rails will eventually call Formatter#generate.
  8. 1 class Formatter
  9. 1 attr_reader :routes
  10. 1 def initialize(routes)
  11. 56 @routes = routes
  12. 56 @cache = nil
  13. end
  14. 1 class RouteWithParams
  15. 1 attr_reader :params
  16. 1 def initialize(route, parameterized_parts, params)
  17. @route = route
  18. @parameterized_parts = parameterized_parts
  19. @params = params
  20. end
  21. 1 def path(_)
  22. @route.format(@parameterized_parts)
  23. end
  24. end
  25. 1 class MissingRoute
  26. 1 attr_reader :routes, :name, :constraints, :missing_keys, :unmatched_keys
  27. 1 def initialize(constraints, missing_keys, unmatched_keys, routes, name)
  28. @constraints = constraints
  29. @missing_keys = missing_keys
  30. @unmatched_keys = unmatched_keys
  31. @routes = routes
  32. @name = name
  33. end
  34. 1 def path(method_name)
  35. raise ActionController::UrlGenerationError.new(message, routes, name, method_name)
  36. end
  37. 1 def params
  38. path("unknown")
  39. end
  40. 1 def message
  41. message = +"No route matches #{Hash[constraints.sort_by { |k, v| k.to_s }].inspect}"
  42. message << ", missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty?
  43. message << ", possible unmatched constraints: #{unmatched_keys.sort.inspect}" if unmatched_keys && !unmatched_keys.empty?
  44. message
  45. end
  46. end
  47. 1 def generate(name, options, path_parameters)
  48. constraints = path_parameters.merge(options)
  49. missing_keys = nil
  50. match_route(name, constraints) do |route|
  51. parameterized_parts = extract_parameterized_parts(route, options, path_parameters)
  52. # Skip this route unless a name has been provided or it is a
  53. # standard Rails route since we can't determine whether an options
  54. # hash passed to url_for matches a Rack application or a redirect.
  55. next unless name || route.dispatcher?
  56. missing_keys = missing_keys(route, parameterized_parts)
  57. next if missing_keys && !missing_keys.empty?
  58. params = options.dup.delete_if do |key, _|
  59. parameterized_parts.key?(key) || route.defaults.key?(key)
  60. end
  61. defaults = route.defaults
  62. required_parts = route.required_parts
  63. route.parts.reverse_each do |key|
  64. break if defaults[key].nil? && parameterized_parts[key].present?
  65. next if parameterized_parts[key].to_s != defaults[key].to_s
  66. break if required_parts.include?(key)
  67. parameterized_parts.delete(key)
  68. end
  69. return RouteWithParams.new(route, parameterized_parts, params)
  70. end
  71. unmatched_keys = (missing_keys || []) & constraints.keys
  72. missing_keys = (missing_keys || []) - unmatched_keys
  73. MissingRoute.new(constraints, missing_keys, unmatched_keys, routes, name)
  74. end
  75. 1 def clear
  76. 54 @cache = nil
  77. end
  78. 1 private
  79. 1 def extract_parameterized_parts(route, options, recall)
  80. parameterized_parts = recall.merge(options)
  81. keys_to_keep = route.parts.reverse_each.drop_while { |part|
  82. !(options.key?(part) || route.scope_options.key?(part)) || (options[part] || recall[part]).nil?
  83. } | route.required_parts
  84. parameterized_parts.delete_if do |bad_key, _|
  85. !keys_to_keep.include?(bad_key)
  86. end
  87. parameterized_parts.each do |k, v|
  88. if k == :controller
  89. parameterized_parts[k] = v
  90. else
  91. parameterized_parts[k] = v.to_param
  92. end
  93. end
  94. parameterized_parts.keep_if { |_, v| v }
  95. parameterized_parts
  96. end
  97. 1 def named_routes
  98. routes.named_routes
  99. end
  100. 1 def match_route(name, options)
  101. if named_routes.key?(name)
  102. yield named_routes[name]
  103. else
  104. routes = non_recursive(cache, options)
  105. supplied_keys = options.each_with_object({}) do |(k, v), h|
  106. h[k.to_s] = true if v
  107. end
  108. hash = routes.group_by { |_, r| r.score(supplied_keys) }
  109. hash.keys.sort.reverse_each do |score|
  110. break if score < 0
  111. hash[score].sort_by { |i, _| i }.each do |_, route|
  112. yield route
  113. end
  114. end
  115. end
  116. end
  117. 1 def non_recursive(cache, options)
  118. routes = []
  119. queue = [cache]
  120. while queue.any?
  121. c = queue.shift
  122. routes.concat(c[:___routes]) if c.key?(:___routes)
  123. options.each do |pair|
  124. queue << c[pair] if c.key?(pair)
  125. end
  126. end
  127. routes
  128. end
  129. # Returns an array populated with missing keys if any are present.
  130. 1 def missing_keys(route, parts)
  131. missing_keys = nil
  132. tests = route.path.requirements_for_missing_keys_check
  133. route.required_parts.each { |key|
  134. case tests[key]
  135. when nil
  136. unless parts[key]
  137. missing_keys ||= []
  138. missing_keys << key
  139. end
  140. else
  141. unless tests[key].match?(parts[key])
  142. missing_keys ||= []
  143. missing_keys << key
  144. end
  145. end
  146. }
  147. missing_keys
  148. end
  149. 1 def possibles(cache, options, depth = 0)
  150. cache.fetch(:___routes) { [] } + options.find_all { |pair|
  151. cache.key?(pair)
  152. }.flat_map { |pair|
  153. possibles(cache[pair], options, depth + 1)
  154. }
  155. end
  156. 1 def build_cache
  157. root = { ___routes: [] }
  158. routes.routes.each_with_index do |route, i|
  159. leaf = route.required_defaults.inject(root) do |h, tuple|
  160. h[tuple] ||= {}
  161. end
  162. (leaf[:___routes] ||= []) << [i, route]
  163. end
  164. root
  165. end
  166. 1 def cache
  167. @cache ||= build_cache
  168. end
  169. end
  170. end
  171. # :startdoc:
  172. end

lib/action_dispatch/journey/gtg/builder.rb

18.99% lines covered

79 relevant lines. 15 lines covered and 64 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_dispatch/journey/gtg/transition_table"
  3. 1 module ActionDispatch
  4. 1 module Journey # :nodoc:
  5. 1 module GTG # :nodoc:
  6. 1 class Builder # :nodoc:
  7. 1 DUMMY = Nodes::Dummy.new
  8. 1 attr_reader :root, :ast, :endpoints
  9. 1 def initialize(root)
  10. @root = root
  11. @ast = Nodes::Cat.new root, DUMMY
  12. @followpos = build_followpos
  13. end
  14. 1 def transition_table
  15. dtrans = TransitionTable.new
  16. marked = {}
  17. state_id = Hash.new { |h, k| h[k] = h.length }
  18. dstates = [firstpos(root)]
  19. until dstates.empty?
  20. s = dstates.shift
  21. next if marked[s]
  22. marked[s] = true # mark s
  23. s.group_by { |state| symbol(state) }.each do |sym, ps|
  24. u = ps.flat_map { |l| @followpos[l] }
  25. next if u.empty?
  26. from = state_id[s]
  27. if u.all? { |pos| pos == DUMMY }
  28. to = state_id[Object.new]
  29. dtrans[from, to] = sym
  30. dtrans.add_accepting(to)
  31. ps.each { |state| dtrans.add_memo(to, state.memo) }
  32. else
  33. to = state_id[u]
  34. dtrans[from, to] = sym
  35. if u.include?(DUMMY)
  36. ps.each do |state|
  37. if @followpos[state].include?(DUMMY)
  38. dtrans.add_memo(to, state.memo)
  39. end
  40. end
  41. dtrans.add_accepting(to)
  42. end
  43. end
  44. dstates << u
  45. end
  46. end
  47. dtrans
  48. end
  49. 1 def nullable?(node)
  50. case node
  51. when Nodes::Group
  52. true
  53. when Nodes::Star
  54. true
  55. when Nodes::Or
  56. node.children.any? { |c| nullable?(c) }
  57. when Nodes::Cat
  58. nullable?(node.left) && nullable?(node.right)
  59. when Nodes::Terminal
  60. !node.left
  61. when Nodes::Unary
  62. nullable?(node.left)
  63. else
  64. raise ArgumentError, "unknown nullable: %s" % node.class.name
  65. end
  66. end
  67. 1 def firstpos(node)
  68. case node
  69. when Nodes::Star
  70. firstpos(node.left)
  71. when Nodes::Cat
  72. if nullable?(node.left)
  73. firstpos(node.left) | firstpos(node.right)
  74. else
  75. firstpos(node.left)
  76. end
  77. when Nodes::Or
  78. node.children.flat_map { |c| firstpos(c) }.tap(&:uniq!)
  79. when Nodes::Unary
  80. firstpos(node.left)
  81. when Nodes::Terminal
  82. nullable?(node) ? [] : [node]
  83. else
  84. raise ArgumentError, "unknown firstpos: %s" % node.class.name
  85. end
  86. end
  87. 1 def lastpos(node)
  88. case node
  89. when Nodes::Star
  90. firstpos(node.left)
  91. when Nodes::Or
  92. node.children.flat_map { |c| lastpos(c) }.tap(&:uniq!)
  93. when Nodes::Cat
  94. if nullable?(node.right)
  95. lastpos(node.left) | lastpos(node.right)
  96. else
  97. lastpos(node.right)
  98. end
  99. when Nodes::Terminal
  100. nullable?(node) ? [] : [node]
  101. when Nodes::Unary
  102. lastpos(node.left)
  103. else
  104. raise ArgumentError, "unknown lastpos: %s" % node.class.name
  105. end
  106. end
  107. 1 private
  108. 1 def build_followpos
  109. table = Hash.new { |h, k| h[k] = [] }
  110. @ast.each do |n|
  111. case n
  112. when Nodes::Cat
  113. lastpos(n.left).each do |i|
  114. table[i] += firstpos(n.right)
  115. end
  116. when Nodes::Star
  117. lastpos(n).each do |i|
  118. table[i] += firstpos(n)
  119. end
  120. end
  121. end
  122. table
  123. end
  124. 1 def symbol(edge)
  125. edge.symbol? ? edge.regexp : edge.left
  126. end
  127. end
  128. end
  129. end
  130. end

lib/action_dispatch/journey/gtg/simulator.rb

57.14% lines covered

21 relevant lines. 12 lines covered and 9 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "strscan"
  3. 1 module ActionDispatch
  4. 1 module Journey # :nodoc:
  5. 1 module GTG # :nodoc:
  6. 1 class MatchData # :nodoc:
  7. 1 attr_reader :memos
  8. 1 def initialize(memos)
  9. @memos = memos
  10. end
  11. end
  12. 1 class Simulator # :nodoc:
  13. 1 INITIAL_STATE = [0].freeze
  14. 1 attr_reader :tt
  15. 1 def initialize(transition_table)
  16. @tt = transition_table
  17. end
  18. 1 def memos(string)
  19. input = StringScanner.new(string)
  20. state = INITIAL_STATE
  21. while sym = input.scan(%r([/.?]|[^/.?]+))
  22. state = tt.move(state, sym)
  23. end
  24. acceptance_states = state.each_with_object([]) do |s, memos|
  25. memos.concat(tt.memo(s)) if tt.accepting?(s)
  26. end
  27. acceptance_states.empty? ? yield : acceptance_states
  28. end
  29. end
  30. end
  31. end
  32. end

lib/action_dispatch/journey/gtg/transition_table.rb

25.84% lines covered

89 relevant lines. 23 lines covered and 66 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_dispatch/journey/nfa/dot"
  3. 1 module ActionDispatch
  4. 1 module Journey # :nodoc:
  5. 1 module GTG # :nodoc:
  6. 1 class TransitionTable # :nodoc:
  7. 1 include Journey::NFA::Dot
  8. 1 attr_reader :memos
  9. 1 def initialize
  10. @regexp_states = {}
  11. @string_states = {}
  12. @accepting = {}
  13. @memos = Hash.new { |h, k| h[k] = [] }
  14. end
  15. 1 def add_accepting(state)
  16. @accepting[state] = true
  17. end
  18. 1 def accepting_states
  19. @accepting.keys
  20. end
  21. 1 def accepting?(state)
  22. @accepting[state]
  23. end
  24. 1 def add_memo(idx, memo)
  25. @memos[idx] << memo
  26. end
  27. 1 def memo(idx)
  28. @memos[idx]
  29. end
  30. 1 def eclosure(t)
  31. Array(t)
  32. end
  33. 1 def move(t, a)
  34. return [] if t.empty?
  35. regexps = []
  36. strings = []
  37. t.each { |s|
  38. if states = @regexp_states[s]
  39. states.each { |re, v| regexps << v if re.match?(a) && !v.nil? }
  40. end
  41. if states = @string_states[s]
  42. strings << states[a] unless states[a].nil?
  43. end
  44. }
  45. strings.concat regexps
  46. end
  47. 1 def as_json(options = nil)
  48. simple_regexp = Hash.new { |h, k| h[k] = {} }
  49. @regexp_states.each do |from, hash|
  50. hash.each do |re, to|
  51. simple_regexp[from][re.source] = to
  52. end
  53. end
  54. {
  55. regexp_states: simple_regexp,
  56. string_states: @string_states,
  57. accepting: @accepting
  58. }
  59. end
  60. 1 def to_svg
  61. svg = IO.popen("dot -Tsvg", "w+") { |f|
  62. f.write(to_dot)
  63. f.close_write
  64. f.readlines
  65. }
  66. 3.times { svg.shift }
  67. svg.join.sub(/width="[^"]*"/, "").sub(/height="[^"]*"/, "")
  68. end
  69. 1 def visualizer(paths, title = "FSM")
  70. viz_dir = File.join __dir__, "..", "visualizer"
  71. fsm_js = File.read File.join(viz_dir, "fsm.js")
  72. fsm_css = File.read File.join(viz_dir, "fsm.css")
  73. erb = File.read File.join(viz_dir, "index.html.erb")
  74. states = "function tt() { return #{to_json}; }"
  75. fun_routes = paths.sample(3).map do |ast|
  76. ast.map { |n|
  77. case n
  78. when Nodes::Symbol
  79. case n.left
  80. when ":id" then rand(100).to_s
  81. when ":format" then %w{ xml json }.sample
  82. else
  83. "omg"
  84. end
  85. when Nodes::Terminal then n.symbol
  86. else
  87. nil
  88. end
  89. }.compact.join
  90. end
  91. stylesheets = [fsm_css]
  92. svg = to_svg
  93. javascripts = [states, fsm_js]
  94. fun_routes = fun_routes
  95. stylesheets = stylesheets
  96. svg = svg
  97. javascripts = javascripts
  98. require "erb"
  99. template = ERB.new erb
  100. template.result(binding)
  101. end
  102. 1 def []=(from, to, sym)
  103. to_mappings = states_hash_for(sym)[from] ||= {}
  104. to_mappings[sym] = to
  105. end
  106. 1 def states
  107. ss = @string_states.keys + @string_states.values.flat_map(&:values)
  108. rs = @regexp_states.keys + @regexp_states.values.flat_map(&:values)
  109. (ss + rs).uniq
  110. end
  111. 1 def transitions
  112. @string_states.flat_map { |from, hash|
  113. hash.map { |s, to| [from, s, to] }
  114. } + @regexp_states.flat_map { |from, hash|
  115. hash.map { |s, to| [from, s, to] }
  116. }
  117. end
  118. 1 private
  119. 1 def states_hash_for(sym)
  120. case sym
  121. when String
  122. @string_states
  123. when Regexp
  124. @regexp_states
  125. else
  126. raise ArgumentError, "unknown symbol: %s" % sym.class
  127. end
  128. end
  129. end
  130. end
  131. end
  132. end

lib/action_dispatch/journey/nfa/dot.rb

62.5% lines covered

8 relevant lines. 5 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module Journey # :nodoc:
  4. 1 module NFA # :nodoc:
  5. 1 module Dot # :nodoc:
  6. 1 def to_dot
  7. edges = transitions.map { |from, sym, to|
  8. " #{from} -> #{to} [label=\"#{sym || 'ε'}\"];"
  9. }
  10. <<-eodot
  11. digraph nfa {
  12. rankdir=LR;
  13. node [shape = doublecircle];
  14. #{accepting_states.join ' '};
  15. node [shape = circle];
  16. #{edges.join "\n"}
  17. }
  18. eodot
  19. end
  20. end
  21. end
  22. end
  23. end

lib/action_dispatch/journey/nodes/node.rb

93.75% lines covered

80 relevant lines. 75 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_dispatch/journey/visitors"
  3. 1 module ActionDispatch
  4. 1 module Journey # :nodoc:
  5. 1 module Nodes # :nodoc:
  6. 1 class Node # :nodoc:
  7. 1 include Enumerable
  8. 1 attr_accessor :left, :memo
  9. 1 def initialize(left)
  10. 3828 @left = left
  11. 3828 @memo = nil
  12. end
  13. 1 def each(&block)
  14. 2146 Visitors::Each::INSTANCE.accept(self, block)
  15. end
  16. 1 def to_s
  17. Visitors::String::INSTANCE.accept(self, "")
  18. end
  19. 1 def to_dot
  20. Visitors::Dot::INSTANCE.accept(self)
  21. end
  22. 1 def to_sym
  23. 1520 name.to_sym
  24. end
  25. 1 def name
  26. -left.tr("*:", "")
  27. end
  28. 1 def type
  29. raise NotImplementedError
  30. end
  31. 13901 def symbol?; false; end
  32. 2605 def literal?; false; end
  33. 1882 def terminal?; false; end
  34. 3247 def star?; false; end
  35. 2007 def cat?; false; end
  36. 1983 def group?; false; end
  37. end
  38. 1 class Terminal < Node # :nodoc:
  39. 1 alias :symbol :left
  40. 1881 def terminal?; true; end
  41. end
  42. 1 class Literal < Terminal # :nodoc:
  43. 869 def literal?; true; end
  44. 3189 def type; :LITERAL; end
  45. end
  46. 1 class Dummy < Literal # :nodoc:
  47. 1 def initialize(x = Object.new)
  48. 1 super
  49. end
  50. 1 def literal?; false; end
  51. end
  52. 1 class Slash < Terminal # :nodoc:
  53. 4711 def type; :SLASH; end
  54. end
  55. 1 class Dot < Terminal # :nodoc:
  56. 2292 def type; :DOT; end
  57. end
  58. 1 class Symbol < Terminal # :nodoc:
  59. 1 attr_accessor :regexp
  60. 1 alias :symbol :regexp
  61. 1 attr_reader :name
  62. 1 DEFAULT_EXP = /[^\.\/\?]+/
  63. 1 GREEDY_EXP = /(.+)/
  64. 1 def initialize(left, regexp = DEFAULT_EXP)
  65. 508 super(left)
  66. 508 @regexp = regexp
  67. 508 @name = -left.tr("*:", "")
  68. end
  69. 1 def default_regexp?
  70. 472 regexp == DEFAULT_EXP
  71. end
  72. 3740 def type; :SYMBOL; end
  73. 2097 def symbol?; true; end
  74. end
  75. 1 class Unary < Node # :nodoc:
  76. 1 def children; [left] end
  77. end
  78. 1 class Group < Unary # :nodoc:
  79. 2390 def type; :GROUP; end
  80. 178 def group?; true; end
  81. end
  82. 1 class Star < Unary # :nodoc:
  83. 4 def star?; true; end
  84. 19 def type; :STAR; end
  85. 1 def name
  86. 3 left.name.tr "*:", ""
  87. end
  88. end
  89. 1 class Binary < Node # :nodoc:
  90. 1 attr_accessor :right
  91. 1 def initialize(left, right)
  92. 1594 super(left)
  93. 1594 @right = right
  94. end
  95. 1 def children; [left, right] end
  96. end
  97. 1 class Cat < Binary # :nodoc:
  98. 1938 def cat?; true; end
  99. 11460 def type; :CAT; end
  100. end
  101. 1 class Or < Node # :nodoc:
  102. 1 attr_reader :children
  103. 1 def initialize(children)
  104. @children = children
  105. end
  106. 1 def type; :OR; end
  107. end
  108. end
  109. end
  110. end

lib/action_dispatch/journey/parser.rb

93.18% lines covered

44 relevant lines. 41 lines covered and 3 lines missed.
    
  1. #
  2. # DO NOT MODIFY!!!!
  3. # This file is automatically generated by Racc 1.4.16
  4. # from Racc grammar file "".
  5. #
  6. 1 require 'racc/parser.rb'
  7. # :stopdoc:
  8. 1 require "action_dispatch/journey/parser_extras"
  9. 1 module ActionDispatch
  10. 1 module Journey
  11. 1 class Parser < Racc::Parser
  12. ##### State transition tables begin ###
  13. 1 racc_action_table = [
  14. 13, 15, 14, 7, 19, 16, 8, 19, 13, 15,
  15. 14, 7, 17, 16, 8, 13, 15, 14, 7, 21,
  16. 16, 8, 13, 15, 14, 7, 24, 16, 8 ]
  17. 1 racc_action_check = [
  18. 2, 2, 2, 2, 22, 2, 2, 2, 19, 19,
  19. 19, 19, 1, 19, 19, 7, 7, 7, 7, 17,
  20. 7, 7, 0, 0, 0, 0, 20, 0, 0 ]
  21. 1 racc_action_pointer = [
  22. 20, 12, -2, nil, nil, nil, nil, 13, nil, nil,
  23. nil, nil, nil, nil, nil, nil, nil, 19, nil, 6,
  24. 20, nil, -5, nil, nil ]
  25. 1 racc_action_default = [
  26. -19, -19, -2, -3, -4, -5, -6, -19, -10, -11,
  27. -12, -13, -14, -15, -16, -17, -18, -19, -1, -19,
  28. -19, 25, -8, -9, -7 ]
  29. 1 racc_goto_table = [
  30. 1, 22, 18, 23, nil, nil, nil, 20 ]
  31. 1 racc_goto_check = [
  32. 1, 2, 1, 3, nil, nil, nil, 1 ]
  33. 1 racc_goto_pointer = [
  34. nil, 0, -18, -16, nil, nil, nil, nil, nil, nil,
  35. nil ]
  36. 1 racc_goto_default = [
  37. nil, nil, 2, 3, 4, 5, 6, 9, 10, 11,
  38. 12 ]
  39. 1 racc_reduce_table = [
  40. 0, 0, :racc_error,
  41. 2, 11, :_reduce_1,
  42. 1, 11, :_reduce_2,
  43. 1, 11, :_reduce_none,
  44. 1, 12, :_reduce_none,
  45. 1, 12, :_reduce_none,
  46. 1, 12, :_reduce_none,
  47. 3, 15, :_reduce_7,
  48. 3, 13, :_reduce_8,
  49. 3, 13, :_reduce_9,
  50. 1, 16, :_reduce_10,
  51. 1, 14, :_reduce_none,
  52. 1, 14, :_reduce_none,
  53. 1, 14, :_reduce_none,
  54. 1, 14, :_reduce_none,
  55. 1, 19, :_reduce_15,
  56. 1, 17, :_reduce_16,
  57. 1, 18, :_reduce_17,
  58. 1, 20, :_reduce_18 ]
  59. 1 racc_reduce_n = 19
  60. 1 racc_shift_n = 25
  61. 1 racc_token_table = {
  62. false => 0,
  63. :error => 1,
  64. :SLASH => 2,
  65. :LITERAL => 3,
  66. :SYMBOL => 4,
  67. :LPAREN => 5,
  68. :RPAREN => 6,
  69. :DOT => 7,
  70. :STAR => 8,
  71. :OR => 9 }
  72. 1 racc_nt_base = 10
  73. 1 racc_use_result_var = false
  74. 1 Racc_arg = [
  75. racc_action_table,
  76. racc_action_check,
  77. racc_action_default,
  78. racc_action_pointer,
  79. racc_goto_table,
  80. racc_goto_check,
  81. racc_goto_default,
  82. racc_goto_pointer,
  83. racc_nt_base,
  84. racc_reduce_table,
  85. racc_token_table,
  86. racc_shift_n,
  87. racc_reduce_n,
  88. racc_use_result_var ]
  89. 1 Racc_token_to_s_table = [
  90. "$end",
  91. "error",
  92. "SLASH",
  93. "LITERAL",
  94. "SYMBOL",
  95. "LPAREN",
  96. "RPAREN",
  97. "DOT",
  98. "STAR",
  99. "OR",
  100. "$start",
  101. "expressions",
  102. "expression",
  103. "or",
  104. "terminal",
  105. "group",
  106. "star",
  107. "symbol",
  108. "literal",
  109. "slash",
  110. "dot" ]
  111. 1 Racc_debug_parser = false
  112. ##### State transition tables end #####
  113. # reduce 0 omitted
  114. 1 def _reduce_1(val, _values)
  115. 1594 Cat.new(val.first, val.last)
  116. end
  117. 1 def _reduce_2(val, _values)
  118. 636 val.first
  119. end
  120. # reduce 3 omitted
  121. # reduce 4 omitted
  122. # reduce 5 omitted
  123. # reduce 6 omitted
  124. 1 def _reduce_7(val, _values)
  125. 310 Group.new(val[1])
  126. end
  127. 1 def _reduce_8(val, _values)
  128. Or.new([val.first, val.last])
  129. end
  130. 1 def _reduce_9(val, _values)
  131. Or.new([val.first, val.last])
  132. end
  133. 1 def _reduce_10(val, _values)
  134. 3 Star.new(Symbol.new(val.last, Symbol::GREEDY_EXP))
  135. end
  136. # reduce 11 omitted
  137. # reduce 12 omitted
  138. # reduce 13 omitted
  139. # reduce 14 omitted
  140. 1 def _reduce_15(val, _values)
  141. 670 Slash.new(val.first)
  142. end
  143. 1 def _reduce_16(val, _values)
  144. 505 Symbol.new(val.first)
  145. end
  146. 1 def _reduce_17(val, _values)
  147. 447 Literal.new(val.first)
  148. end
  149. 1 def _reduce_18(val, _values)
  150. 295 Dot.new(val.first)
  151. end
  152. 1 def _reduce_none(val, _values)
  153. val[0]
  154. end
  155. end # class Parser
  156. end # module Journey
  157. end # module ActionDispatch

lib/action_dispatch/journey/parser_extras.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_dispatch/journey/scanner"
  3. 1 require "action_dispatch/journey/nodes/node"
  4. 1 module ActionDispatch
  5. # :stopdoc:
  6. 1 module Journey
  7. 1 class Parser < Racc::Parser
  8. 1 include Journey::Nodes
  9. 1 def self.parse(string)
  10. 326 new.parse string
  11. end
  12. 1 def initialize
  13. 326 @scanner = Scanner.new
  14. end
  15. 1 def parse(string)
  16. 326 @scanner.scan_setup(string)
  17. 326 do_parse
  18. end
  19. 1 def next_token
  20. 2866 @scanner.next_token
  21. end
  22. end
  23. end
  24. # :startdoc:
  25. end

lib/action_dispatch/journey/path/pattern.rb

55.45% lines covered

110 relevant lines. 61 lines covered and 49 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module Journey # :nodoc:
  4. 1 module Path # :nodoc:
  5. 1 class Pattern # :nodoc:
  6. 1 attr_reader :spec, :requirements, :anchored
  7. 1 def initialize(ast, requirements, separators, anchored)
  8. 326 @spec = ast
  9. 326 @requirements = requirements
  10. 326 @separators = separators
  11. 326 @anchored = anchored
  12. 326 @names = nil
  13. 326 @optional_names = nil
  14. 326 @required_names = nil
  15. 326 @re = nil
  16. 326 @offsets = nil
  17. end
  18. 1 def build_formatter
  19. 326 Visitors::FormatBuilder.new.accept(spec)
  20. end
  21. 1 def eager_load!
  22. required_names
  23. offsets
  24. to_regexp
  25. nil
  26. end
  27. 1 def ast
  28. 311 @spec.find_all(&:symbol?).each do |node|
  29. 504 re = @requirements[node.to_sym]
  30. 504 node.regexp = re if re
  31. end
  32. 311 @spec
  33. end
  34. 1 def names
  35. 518 @names ||= spec.find_all(&:symbol?).map(&:name)
  36. end
  37. 1 def required_names
  38. 192 @required_names ||= names - optional_names
  39. end
  40. 1 def optional_names
  41. 192 @optional_names ||= spec.find_all(&:group?).flat_map { |group|
  42. 177 group.find_all(&:symbol?)
  43. }.map(&:name).uniq
  44. end
  45. 1 class AnchoredRegexp < Journey::Visitors::Visitor # :nodoc:
  46. 1 def initialize(separator, matchers)
  47. @separator = separator
  48. @matchers = matchers
  49. @separator_re = "([^#{separator}]+)"
  50. super()
  51. end
  52. 1 def accept(node)
  53. %r{\A#{visit node}\Z}
  54. end
  55. 1 def visit_CAT(node)
  56. "#{visit(node.left)}#{visit(node.right)}"
  57. end
  58. 1 def visit_SYMBOL(node)
  59. node = node.to_sym
  60. return @separator_re unless @matchers.key?(node)
  61. re = @matchers[node]
  62. "(#{Regexp.union(re)})"
  63. end
  64. 1 def visit_GROUP(node)
  65. "(?:#{visit node.left})?"
  66. end
  67. 1 def visit_LITERAL(node)
  68. Regexp.escape(node.left)
  69. end
  70. 1 alias :visit_DOT :visit_LITERAL
  71. 1 def visit_SLASH(node)
  72. node.left
  73. end
  74. 1 def visit_STAR(node)
  75. re = @matchers[node.left.to_sym]
  76. re ? "(#{re})" : "(.+)"
  77. end
  78. 1 def visit_OR(node)
  79. children = node.children.map { |n| visit n }
  80. "(?:#{children.join(?|)})"
  81. end
  82. end
  83. 1 class UnanchoredRegexp < AnchoredRegexp # :nodoc:
  84. 1 def accept(node)
  85. path = visit node
  86. path == "/" ? %r{\A/} : %r{\A#{path}(?:\b|\Z|/)}
  87. end
  88. end
  89. 1 class MatchData # :nodoc:
  90. 1 attr_reader :names
  91. 1 def initialize(names, offsets, match)
  92. @names = names
  93. @offsets = offsets
  94. @match = match
  95. end
  96. 1 def captures
  97. Array.new(length - 1) { |i| self[i + 1] }
  98. end
  99. 1 def named_captures
  100. @names.zip(captures).to_h
  101. end
  102. 1 def [](x)
  103. idx = @offsets[x - 1] + x
  104. @match[idx]
  105. end
  106. 1 def length
  107. @offsets.length
  108. end
  109. 1 def post_match
  110. @match.post_match
  111. end
  112. 1 def to_s
  113. @match.to_s
  114. end
  115. end
  116. 1 def match(other)
  117. return unless match = to_regexp.match(other)
  118. MatchData.new(names, offsets, match)
  119. end
  120. 1 alias :=~ :match
  121. 1 def match?(other)
  122. to_regexp.match?(other)
  123. end
  124. 1 def source
  125. to_regexp.source
  126. end
  127. 1 def to_regexp
  128. @re ||= regexp_visitor.new(@separators, @requirements).accept spec
  129. end
  130. 1 def requirements_for_missing_keys_check
  131. @requirements_for_missing_keys_check ||= requirements.transform_values do |regex|
  132. /\A#{regex}\Z/
  133. end
  134. end
  135. 1 private
  136. 1 def regexp_visitor
  137. @anchored ? AnchoredRegexp : UnanchoredRegexp
  138. end
  139. 1 def offsets
  140. return @offsets if @offsets
  141. @offsets = [0]
  142. spec.find_all(&:symbol?).each do |node|
  143. node = node.to_sym
  144. if @requirements.key?(node)
  145. re = /#{Regexp.union(@requirements[node])}|/
  146. @offsets.push((re.match("").length - 1) + @offsets.last)
  147. else
  148. @offsets << @offsets.last
  149. end
  150. end
  151. @offsets
  152. end
  153. end
  154. end
  155. end
  156. end

lib/action_dispatch/journey/route.rb

70.1% lines covered

97 relevant lines. 68 lines covered and 29 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. # :stopdoc:
  4. 1 module Journey
  5. 1 class Route
  6. 1 attr_reader :app, :path, :defaults, :name, :precedence, :constraints,
  7. :internal, :scope_options
  8. 1 alias :conditions :constraints
  9. 1 module VerbMatchers
  10. 1 VERBS = %w{ DELETE GET HEAD OPTIONS LINK PATCH POST PUT TRACE UNLINK }
  11. 1 VERBS.each do |v|
  12. 10 class_eval <<-eoc, __FILE__, __LINE__ + 1
  13. # frozen_string_literal: true
  14. class #{v}
  15. def self.verb; name.split("::").last; end
  16. def self.call(req); req.#{v.downcase}?; end
  17. end
  18. eoc
  19. end
  20. 1 class Unknown
  21. 1 attr_reader :verb
  22. 1 def initialize(verb)
  23. @verb = verb
  24. end
  25. 1 def call(request); @verb == request.request_method; end
  26. end
  27. 1 class All
  28. 1 def self.call(_); true; end
  29. 1 def self.verb; ""; end
  30. end
  31. 1 VERB_TO_CLASS = VERBS.each_with_object(all: All) do |verb, hash|
  32. 10 klass = const_get verb
  33. 10 hash[verb] = klass
  34. 10 hash[verb.downcase] = klass
  35. 10 hash[verb.downcase.to_sym] = klass
  36. end
  37. end
  38. 1 def self.verb_matcher(verb)
  39. 326 VerbMatchers::VERB_TO_CLASS.fetch(verb) do
  40. VerbMatchers::Unknown.new verb.to_s.dasherize.upcase
  41. end
  42. end
  43. ##
  44. # +path+ is a path constraint.
  45. # +constraints+ is a hash of constraints to be applied to this route.
  46. 1 def initialize(name:, app: nil, path:, constraints: {}, required_defaults: [], defaults: {}, request_method_match: nil, precedence: 0, scope_options: {}, internal: false)
  47. 326 @name = name
  48. 326 @app = app
  49. 326 @path = path
  50. 326 @request_method_match = request_method_match
  51. 326 @constraints = constraints
  52. 326 @defaults = defaults
  53. 326 @required_defaults = nil
  54. 326 @_required_defaults = required_defaults
  55. 326 @required_parts = nil
  56. 326 @parts = nil
  57. 326 @decorated_ast = nil
  58. 326 @precedence = precedence
  59. 326 @path_formatter = @path.build_formatter
  60. 326 @scope_options = scope_options
  61. 326 @internal = internal
  62. end
  63. 1 def eager_load!
  64. path.eager_load!
  65. ast
  66. parts
  67. required_defaults
  68. nil
  69. end
  70. 1 def ast
  71. 311 @decorated_ast ||= begin
  72. 311 decorated_ast = path.ast
  73. 2191 decorated_ast.find_all(&:terminal?).each { |n| n.memo = self }
  74. 311 decorated_ast
  75. end
  76. end
  77. # Needed for `bin/rails routes`. Picks up succinctly defined requirements
  78. # for a route, for example route
  79. #
  80. # get 'photo/:id', :controller => 'photos', :action => 'show',
  81. # :id => /[A-Z]\d{5}/
  82. #
  83. # will have {:controller=>"photos", :action=>"show", :id=>/[A-Z]\d{5}/}
  84. # as requirements.
  85. 1 def requirements
  86. @defaults.merge(path.requirements).delete_if { |_, v|
  87. /.+?/ == v
  88. }
  89. end
  90. 1 def segments
  91. 326 path.names
  92. end
  93. 1 def required_keys
  94. required_parts + required_defaults.keys
  95. end
  96. 1 def score(supplied_keys)
  97. path.required_names.each do |k|
  98. return -1 unless supplied_keys.include?(k)
  99. end
  100. (required_defaults.length * 2) + path.names.count { |k| supplied_keys.include?(k) }
  101. end
  102. 1 def parts
  103. 855 @parts ||= segments.map(&:to_sym)
  104. end
  105. 1 alias :segment_keys :parts
  106. 1 def format(path_options)
  107. @path_formatter.evaluate path_options
  108. end
  109. 1 def required_parts
  110. 192 @required_parts ||= path.required_names.map(&:to_sym)
  111. end
  112. 1 def required_default?(key)
  113. @_required_defaults.include?(key)
  114. end
  115. 1 def required_defaults
  116. @required_defaults ||= @defaults.dup.delete_if do |k, _|
  117. parts.include?(k) || !required_default?(k)
  118. end
  119. end
  120. 1 def glob?
  121. 192 path.spec.any?(Nodes::Star)
  122. end
  123. 1 def dispatcher?
  124. @app.dispatcher?
  125. end
  126. 1 def matches?(request)
  127. match_verb(request) &&
  128. constraints.all? { |method, value|
  129. case value
  130. when Regexp, String
  131. value === request.send(method).to_s
  132. when Array
  133. value.include?(request.send(method))
  134. when TrueClass
  135. request.send(method).present?
  136. when FalseClass
  137. request.send(method).blank?
  138. else
  139. value === request.send(method)
  140. end
  141. }
  142. end
  143. 1 def ip
  144. constraints[:ip] || //
  145. end
  146. 1 def requires_matching_verb?
  147. !@request_method_match.all? { |x| x == VerbMatchers::All }
  148. end
  149. 1 def verb
  150. verbs.join("|")
  151. end
  152. 1 private
  153. 1 def verbs
  154. @request_method_match.map(&:verb)
  155. end
  156. 1 def match_verb(request)
  157. @request_method_match.any? { |m| m.call request }
  158. end
  159. end
  160. end
  161. # :startdoc:
  162. end

lib/action_dispatch/journey/router.rb

31.03% lines covered

87 relevant lines. 27 lines covered and 60 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_dispatch/journey/router/utils"
  3. 1 require "action_dispatch/journey/routes"
  4. 1 require "action_dispatch/journey/formatter"
  5. 1 before = $-w
  6. 1 $-w = false
  7. 1 require "action_dispatch/journey/parser"
  8. 1 $-w = before
  9. 1 require "action_dispatch/journey/route"
  10. 1 require "action_dispatch/journey/path/pattern"
  11. 1 module ActionDispatch
  12. 1 module Journey # :nodoc:
  13. 1 class Router # :nodoc:
  14. 1 attr_accessor :routes
  15. 1 def initialize(routes)
  16. 56 @routes = routes
  17. end
  18. 1 def eager_load!
  19. # Eagerly trigger the simulator's initialization so
  20. # it doesn't happen during a request cycle.
  21. simulator
  22. nil
  23. end
  24. 1 def serve(req)
  25. find_routes(req).each do |match, parameters, route|
  26. set_params = req.path_parameters
  27. path_info = req.path_info
  28. script_name = req.script_name
  29. unless route.path.anchored
  30. req.script_name = (script_name.to_s + match.to_s).chomp("/")
  31. req.path_info = match.post_match
  32. req.path_info = "/" + req.path_info unless req.path_info.start_with? "/"
  33. end
  34. tmp_params = set_params.merge route.defaults
  35. parameters.each_pair { |key, val|
  36. tmp_params[key] = val.force_encoding(::Encoding::UTF_8)
  37. }
  38. req.path_parameters = tmp_params
  39. status, headers, body = route.app.serve(req)
  40. if "pass" == headers["X-Cascade"]
  41. req.script_name = script_name
  42. req.path_info = path_info
  43. req.path_parameters = set_params
  44. next
  45. end
  46. return [status, headers, body]
  47. end
  48. [404, { "X-Cascade" => "pass" }, ["Not Found"]]
  49. end
  50. 1 def recognize(rails_req)
  51. find_routes(rails_req).each do |match, parameters, route|
  52. unless route.path.anchored
  53. rails_req.script_name = match.to_s
  54. rails_req.path_info = match.post_match
  55. rails_req.path_info = "/" + rails_req.path_info unless rails_req.path_info.start_with? "/"
  56. end
  57. parameters = route.defaults.merge parameters
  58. yield(route, parameters)
  59. end
  60. end
  61. 1 def visualizer
  62. tt = GTG::Builder.new(ast).transition_table
  63. groups = partitioned_routes.first.map(&:ast).group_by(&:to_s)
  64. asts = groups.values.map(&:first)
  65. tt.visualizer(asts)
  66. end
  67. 1 private
  68. 1 def partitioned_routes
  69. routes.partition { |r|
  70. r.path.anchored && r.ast.grep(Nodes::Symbol).all? { |n| n.default_regexp? }
  71. }
  72. end
  73. 1 def ast
  74. routes.ast
  75. end
  76. 1 def simulator
  77. routes.simulator
  78. end
  79. 1 def custom_routes
  80. routes.custom_routes
  81. end
  82. 1 def filter_routes(path)
  83. return [] unless ast
  84. simulator.memos(path) { [] }
  85. end
  86. 1 def find_routes(req)
  87. path_info = req.path_info
  88. routes = filter_routes(path_info).concat custom_routes.find_all { |r|
  89. r.path.match?(path_info)
  90. }
  91. if req.head?
  92. routes = match_head_routes(routes, req)
  93. else
  94. routes.select! { |r| r.matches?(req) }
  95. end
  96. routes.sort_by!(&:precedence)
  97. routes.map! { |r|
  98. match_data = r.path.match(path_info)
  99. path_parameters = {}
  100. match_data.names.each_with_index { |name, i|
  101. val = match_data[i + 1]
  102. path_parameters[name.to_sym] = Utils.unescape_uri(val) if val
  103. }
  104. [match_data, path_parameters, r]
  105. }
  106. end
  107. 1 def match_head_routes(routes, req)
  108. head_routes = routes.select { |r| r.requires_matching_verb? && r.matches?(req) }
  109. return head_routes unless head_routes.empty?
  110. begin
  111. req.request_method = "GET"
  112. routes.select! { |r| r.matches?(req) }
  113. routes
  114. ensure
  115. req.request_method = "HEAD"
  116. end
  117. end
  118. end
  119. end
  120. end

lib/action_dispatch/journey/router/utils.rb

75.0% lines covered

52 relevant lines. 39 lines covered and 13 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module Journey # :nodoc:
  4. 1 class Router # :nodoc:
  5. 1 class Utils # :nodoc:
  6. # Normalizes URI path.
  7. #
  8. # Strips off trailing slash and ensures there is a leading slash.
  9. # Also converts downcase URL encoded string to uppercase.
  10. #
  11. # normalize_path("/foo") # => "/foo"
  12. # normalize_path("/foo/") # => "/foo"
  13. # normalize_path("foo") # => "/foo"
  14. # normalize_path("") # => "/"
  15. # normalize_path("/%ab") # => "/%AB"
  16. 1 def self.normalize_path(path)
  17. 574 path ||= ""
  18. 574 encoding = path.encoding
  19. 574 path = +"/#{path}"
  20. 574 path.squeeze!("/")
  21. 574 unless path == "/"
  22. 556 path.delete_suffix!("/")
  23. 559 path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
  24. end
  25. 574 path.force_encoding(encoding)
  26. end
  27. # URI path and fragment escaping
  28. # https://tools.ietf.org/html/rfc3986
  29. 1 class UriEncoder # :nodoc:
  30. 1 ENCODE = "%%%02X"
  31. 1 US_ASCII = Encoding::US_ASCII
  32. 1 UTF_8 = Encoding::UTF_8
  33. 1 EMPTY = (+"").force_encoding(US_ASCII).freeze
  34. 513 DEC2HEX = (0..255).to_a.map { |i| ENCODE % i }.map { |s| s.force_encoding(US_ASCII) }
  35. 1 ALPHA = "a-zA-Z"
  36. 1 DIGIT = "0-9"
  37. 1 UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~"
  38. 1 SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;="
  39. 1 ESCAPED = /%[a-zA-Z0-9]{2}/.freeze
  40. 1 FRAGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/\?]/.freeze
  41. 1 SEGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@]/.freeze
  42. 1 PATH = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/]/.freeze
  43. 1 def escape_fragment(fragment)
  44. escape(fragment, FRAGMENT)
  45. end
  46. 1 def escape_path(path)
  47. escape(path, PATH)
  48. end
  49. 1 def escape_segment(segment)
  50. escape(segment, SEGMENT)
  51. end
  52. 1 def unescape_uri(uri)
  53. encoding = uri.encoding == US_ASCII ? UTF_8 : uri.encoding
  54. uri.gsub(ESCAPED) { |match| [match[1, 2].hex].pack("C") }.force_encoding(encoding)
  55. end
  56. 1 private
  57. 1 def escape(component, pattern)
  58. component.gsub(pattern) { |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII)
  59. end
  60. 1 def percent_encode(unsafe)
  61. safe = EMPTY.dup
  62. unsafe.each_byte { |b| safe << DEC2HEX[b] }
  63. safe
  64. end
  65. end
  66. 1 ENCODER = UriEncoder.new
  67. 1 def self.escape_path(path)
  68. ENCODER.escape_path(path.to_s)
  69. end
  70. 1 def self.escape_segment(segment)
  71. ENCODER.escape_segment(segment.to_s)
  72. end
  73. 1 def self.escape_fragment(fragment)
  74. ENCODER.escape_fragment(fragment.to_s)
  75. end
  76. # Replaces any escaped sequences with their unescaped representations.
  77. #
  78. # uri = "/topics?title=Ruby%20on%20Rails"
  79. # unescape_uri(uri) #=> "/topics?title=Ruby on Rails"
  80. 1 def self.unescape_uri(uri)
  81. ENCODER.unescape_uri(uri)
  82. end
  83. end
  84. end
  85. end
  86. end

lib/action_dispatch/journey/routes.rb

78.26% lines covered

46 relevant lines. 36 lines covered and 10 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module Journey # :nodoc:
  4. # The Routing table. Contains all routes for a system. Routes can be
  5. # added to the table by calling Routes#add_route.
  6. 1 class Routes # :nodoc:
  7. 1 include Enumerable
  8. 1 attr_reader :routes, :custom_routes, :anchored_routes
  9. 1 def initialize
  10. 56 @routes = []
  11. 56 @ast = nil
  12. 56 @anchored_routes = []
  13. 56 @custom_routes = []
  14. 56 @simulator = nil
  15. end
  16. 1 def empty?
  17. routes.empty?
  18. end
  19. 1 def length
  20. routes.length
  21. end
  22. 1 alias :size :length
  23. 1 def last
  24. routes.last
  25. end
  26. 1 def each(&block)
  27. routes.each(&block)
  28. end
  29. 1 def clear
  30. 54 routes.clear
  31. 54 anchored_routes.clear
  32. 54 custom_routes.clear
  33. end
  34. 1 def partition_route(route)
  35. 326 if route.path.anchored && route.ast.grep(Nodes::Symbol).all?(&:default_regexp?)
  36. 283 anchored_routes << route
  37. else
  38. 43 custom_routes << route
  39. end
  40. end
  41. 1 def ast
  42. @ast ||= begin
  43. asts = anchored_routes.map(&:ast)
  44. Nodes::Or.new(asts)
  45. end
  46. end
  47. 1 def simulator
  48. @simulator ||= begin
  49. gtg = GTG::Builder.new(ast).transition_table
  50. GTG::Simulator.new(gtg)
  51. end
  52. end
  53. 1 def add_route(name, mapping)
  54. 326 route = mapping.make_route name, routes.length
  55. 326 routes << route
  56. 326 partition_route(route)
  57. 326 clear_cache!
  58. 326 route
  59. end
  60. 1 private
  61. 1 def clear_cache!
  62. 326 @ast = nil
  63. 326 @simulator = nil
  64. end
  65. end
  66. end
  67. end

lib/action_dispatch/journey/scanner.rb

85.29% lines covered

34 relevant lines. 29 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "strscan"
  3. 1 module ActionDispatch
  4. 1 module Journey # :nodoc:
  5. 1 class Scanner # :nodoc:
  6. 1 def initialize
  7. 326 @ss = nil
  8. end
  9. 1 def scan_setup(str)
  10. 326 @ss = StringScanner.new(str)
  11. end
  12. 1 def eos?
  13. @ss.eos?
  14. end
  15. 1 def pos
  16. @ss.pos
  17. end
  18. 1 def pre_match
  19. @ss.pre_match
  20. end
  21. 1 def next_token
  22. 2866 return if @ss.eos?
  23. 2540 until token = scan || @ss.eos?; end
  24. 2540 token
  25. end
  26. 1 private
  27. # takes advantage of String @- deduping capabilities in Ruby 2.5 upwards
  28. # see: https://bugs.ruby-lang.org/issues/13077
  29. 1 def dedup_scan(regex)
  30. 1405 r = @ss.scan(regex)
  31. 1405 r ? -r : nil
  32. end
  33. 1 def scan
  34. case
  35. # /
  36. when @ss.skip(/\//)
  37. 670 [:SLASH, "/"]
  38. when @ss.skip(/\(/)
  39. 310 [:LPAREN, "("]
  40. when @ss.skip(/\)/)
  41. 310 [:RPAREN, ")"]
  42. when @ss.skip(/\|/)
  43. [:OR, "|"]
  44. when @ss.skip(/\./)
  45. 295 [:DOT, "."]
  46. when text = dedup_scan(/:\w+/)
  47. 505 [:SYMBOL, text]
  48. when text = dedup_scan(/\*\w+/)
  49. 3 [:STAR, text]
  50. when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\[:()])+/)
  51. 447 text.tr! "\\", ""
  52. 447 [:LITERAL, -text]
  53. # any char
  54. when text = dedup_scan(/./)
  55. [:LITERAL, text]
  56. 2540 end
  57. end
  58. end
  59. end
  60. end

lib/action_dispatch/journey/visitors.rb

71.81% lines covered

149 relevant lines. 107 lines covered and 42 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. # :stopdoc:
  4. 1 module Journey
  5. 1 class Format
  6. 1 ESCAPE_PATH = ->(value) { Router::Utils.escape_path(value) }
  7. 1 ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) }
  8. 1 Parameter = Struct.new(:name, :escaper) do
  9. 1 def escape(value); escaper.call value; end
  10. end
  11. 1 def self.required_path(symbol)
  12. 8 Parameter.new symbol, ESCAPE_PATH
  13. end
  14. 1 def self.required_segment(symbol)
  15. 500 Parameter.new symbol, ESCAPE_SEGMENT
  16. end
  17. 1 def initialize(parts)
  18. 636 @parts = parts
  19. 636 @children = []
  20. 636 @parameters = []
  21. 636 parts.each_with_index do |object, i|
  22. 2230 case object
  23. when Journey::Format
  24. 310 @children << i
  25. when Parameter
  26. 508 @parameters << i
  27. end
  28. end
  29. end
  30. 1 def evaluate(hash)
  31. parts = @parts.dup
  32. @parameters.each do |index|
  33. param = parts[index]
  34. value = hash[param.name]
  35. return "" unless value
  36. parts[index] = param.escape value
  37. end
  38. @children.each { |index| parts[index] = parts[index].evaluate(hash) }
  39. parts.join
  40. end
  41. end
  42. 1 module Visitors # :nodoc:
  43. 1 class Visitor # :nodoc:
  44. 1 DISPATCH_CACHE = {}
  45. 1 def accept(node)
  46. 326 visit(node)
  47. end
  48. 1 private
  49. 1 def visit(node)
  50. 3824 send(DISPATCH_CACHE[node.type], node)
  51. end
  52. 1 def binary(node)
  53. visit(node.left)
  54. visit(node.right)
  55. end
  56. 1595 def visit_CAT(n); binary(n); end
  57. 1 def nary(node)
  58. node.children.each { |c| visit(c) }
  59. end
  60. 1 def visit_OR(n); nary(n); end
  61. 1 def unary(node)
  62. 310 visit(node.left)
  63. end
  64. 1 def visit_GROUP(n); unary(n); end
  65. 1 def visit_STAR(n); unary(n); end
  66. 1 def terminal(node); end
  67. 448 def visit_LITERAL(n); terminal(n); end
  68. 1 def visit_SYMBOL(n); terminal(n); end
  69. 671 def visit_SLASH(n); terminal(n); end
  70. 296 def visit_DOT(n); terminal(n); end
  71. 1 private_instance_methods(false).each do |pim|
  72. 13 next unless pim =~ /^visit_(.*)$/
  73. 8 DISPATCH_CACHE[$1.to_sym] = pim
  74. end
  75. end
  76. 1 class FunctionalVisitor # :nodoc:
  77. 1 DISPATCH_CACHE = {}
  78. 1 def accept(node, seed)
  79. 2146 visit(node, seed)
  80. end
  81. 1 def visit(node, seed)
  82. 23970 send(DISPATCH_CACHE[node.type], node, seed)
  83. end
  84. 1 def binary(node, seed)
  85. 9865 visit(node.right, visit(node.left, seed))
  86. end
  87. 9866 def visit_CAT(n, seed); binary(n, seed); end
  88. 1 def nary(node, seed)
  89. node.children.inject(seed) { |s, c| visit(c, s) }
  90. end
  91. 1 def visit_OR(n, seed); nary(n, seed); end
  92. 1 def unary(node, seed)
  93. 2094 visit(node.left, seed)
  94. end
  95. 2080 def visit_GROUP(n, seed); unary(n, seed); end
  96. 16 def visit_STAR(n, seed); unary(n, seed); end
  97. 12012 def terminal(node, seed); seed; end
  98. 2742 def visit_LITERAL(n, seed); terminal(n, seed); end
  99. 3235 def visit_SYMBOL(n, seed); terminal(n, seed); end
  100. 4041 def visit_SLASH(n, seed); terminal(n, seed); end
  101. 1997 def visit_DOT(n, seed); terminal(n, seed); end
  102. 1 instance_methods(false).each do |pim|
  103. 14 next unless pim =~ /^visit_(.*)$/
  104. 8 DISPATCH_CACHE[$1.to_sym] = pim
  105. end
  106. end
  107. 1 class FormatBuilder < Visitor # :nodoc:
  108. 327 def accept(node); Journey::Format.new(super); end
  109. 1413 def terminal(node); [node.left]; end
  110. 1 def binary(node)
  111. 1594 visit(node.left) + visit(node.right)
  112. end
  113. 311 def visit_GROUP(n); [Journey::Format.new(unary(n))]; end
  114. 1 def visit_STAR(n)
  115. 3 [Journey::Format.required_path(n.left.to_sym)]
  116. end
  117. 1 def visit_SYMBOL(n)
  118. 505 symbol = n.to_sym
  119. 505 if symbol == :controller
  120. 5 [Journey::Format.required_path(symbol)]
  121. else
  122. 500 [Journey::Format.required_segment(symbol)]
  123. end
  124. end
  125. end
  126. # Loop through the requirements AST.
  127. 1 class Each < FunctionalVisitor # :nodoc:
  128. 1 def visit(node, block)
  129. 23970 block.call(node)
  130. 23970 super
  131. end
  132. 1 INSTANCE = new
  133. end
  134. 1 class String < FunctionalVisitor # :nodoc:
  135. 1 private
  136. 1 def binary(node, seed)
  137. visit(node.right, visit(node.left, seed))
  138. end
  139. 1 def nary(node, seed)
  140. last_child = node.children.last
  141. node.children.inject(seed) { |s, c|
  142. string = visit(c, s)
  143. string << "|" unless last_child == c
  144. string
  145. }
  146. end
  147. 1 def terminal(node, seed)
  148. seed + node.left
  149. end
  150. 1 def visit_GROUP(node, seed)
  151. visit(node.left, seed.dup << "(") << ")"
  152. end
  153. 1 INSTANCE = new
  154. end
  155. 1 class Dot < FunctionalVisitor # :nodoc:
  156. 1 def initialize
  157. 1 @nodes = []
  158. 1 @edges = []
  159. end
  160. 1 def accept(node, seed = [[], []])
  161. super
  162. nodes, edges = seed
  163. <<-eodot
  164. digraph parse_tree {
  165. size="8,5"
  166. node [shape = none];
  167. edge [dir = none];
  168. #{nodes.join "\n"}
  169. #{edges.join("\n")}
  170. }
  171. eodot
  172. end
  173. 1 private
  174. 1 def binary(node, seed)
  175. seed.last.concat node.children.map { |c|
  176. "#{node.object_id} -> #{c.object_id};"
  177. }
  178. super
  179. end
  180. 1 def nary(node, seed)
  181. seed.last.concat node.children.map { |c|
  182. "#{node.object_id} -> #{c.object_id};"
  183. }
  184. super
  185. end
  186. 1 def unary(node, seed)
  187. seed.last << "#{node.object_id} -> #{node.left.object_id};"
  188. super
  189. end
  190. 1 def visit_GROUP(node, seed)
  191. seed.first << "#{node.object_id} [label=\"()\"];"
  192. super
  193. end
  194. 1 def visit_CAT(node, seed)
  195. seed.first << "#{node.object_id} [label=\"○\"];"
  196. super
  197. end
  198. 1 def visit_STAR(node, seed)
  199. seed.first << "#{node.object_id} [label=\"*\"];"
  200. super
  201. end
  202. 1 def visit_OR(node, seed)
  203. seed.first << "#{node.object_id} [label=\"|\"];"
  204. super
  205. end
  206. 1 def terminal(node, seed)
  207. value = node.left
  208. seed.first << "#{node.object_id} [label=\"#{value}\"];"
  209. seed
  210. end
  211. 1 INSTANCE = new
  212. end
  213. end
  214. end
  215. # :startdoc:
  216. end

lib/action_dispatch/middleware/actionable_exceptions.rb

63.16% lines covered

19 relevant lines. 12 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "erb"
  3. 1 require "action_dispatch/http/request"
  4. 1 require "active_support/actionable_error"
  5. 1 module ActionDispatch
  6. 1 class ActionableExceptions # :nodoc:
  7. 1 cattr_accessor :endpoint, default: "/rails/actions"
  8. 1 def initialize(app)
  9. 37 @app = app
  10. end
  11. 1 def call(env)
  12. request = ActionDispatch::Request.new(env)
  13. return @app.call(env) unless actionable_request?(request)
  14. ActiveSupport::ActionableError.dispatch(request.params[:error].to_s.safe_constantize, request.params[:action])
  15. redirect_to request.params[:location]
  16. end
  17. 1 private
  18. 1 def actionable_request?(request)
  19. request.get_header("action_dispatch.show_detailed_exceptions") && request.post? && request.path == endpoint
  20. end
  21. 1 def redirect_to(location)
  22. body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>"
  23. [302, {
  24. "Content-Type" => "text/html; charset=#{Response.default_charset}",
  25. "Content-Length" => body.bytesize.to_s,
  26. "Location" => location,
  27. }, [body]]
  28. end
  29. end
  30. end

lib/action_dispatch/middleware/callbacks.rb

58.82% lines covered

17 relevant lines. 10 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. # Provides callbacks to be executed before and after dispatching the request.
  4. 1 class Callbacks
  5. 1 include ActiveSupport::Callbacks
  6. 1 define_callbacks :call
  7. 1 class << self
  8. 1 def before(*args, &block)
  9. set_callback(:call, :before, *args, &block)
  10. end
  11. 1 def after(*args, &block)
  12. set_callback(:call, :after, *args, &block)
  13. end
  14. end
  15. 1 def initialize(app)
  16. 37 @app = app
  17. end
  18. 1 def call(env)
  19. error = nil
  20. result = run_callbacks :call do
  21. @app.call(env)
  22. rescue => error
  23. end
  24. raise error if error
  25. result
  26. end
  27. end
  28. end

lib/action_dispatch/middleware/cookies.rb

41.64% lines covered

305 relevant lines. 127 lines covered and 178 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/hash/keys"
  3. 1 require "active_support/key_generator"
  4. 1 require "active_support/message_verifier"
  5. 1 require "active_support/json"
  6. 1 require "rack/utils"
  7. 1 module ActionDispatch
  8. 1 class Request
  9. 1 def cookie_jar
  10. fetch_header("action_dispatch.cookies") do
  11. self.cookie_jar = Cookies::CookieJar.build(self, cookies)
  12. end
  13. end
  14. # :stopdoc:
  15. 1 prepend Module.new {
  16. 1 def commit_cookie_jar!
  17. cookie_jar.commit!
  18. end
  19. }
  20. 1 def have_cookie_jar?
  21. has_header? "action_dispatch.cookies"
  22. end
  23. 1 def cookie_jar=(jar)
  24. set_header "action_dispatch.cookies", jar
  25. end
  26. 1 def key_generator
  27. get_header Cookies::GENERATOR_KEY
  28. end
  29. 1 def signed_cookie_salt
  30. get_header Cookies::SIGNED_COOKIE_SALT
  31. end
  32. 1 def encrypted_cookie_salt
  33. get_header Cookies::ENCRYPTED_COOKIE_SALT
  34. end
  35. 1 def encrypted_signed_cookie_salt
  36. get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT
  37. end
  38. 1 def authenticated_encrypted_cookie_salt
  39. get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT
  40. end
  41. 1 def use_authenticated_cookie_encryption
  42. get_header Cookies::USE_AUTHENTICATED_COOKIE_ENCRYPTION
  43. end
  44. 1 def encrypted_cookie_cipher
  45. get_header Cookies::ENCRYPTED_COOKIE_CIPHER
  46. end
  47. 1 def signed_cookie_digest
  48. get_header Cookies::SIGNED_COOKIE_DIGEST
  49. end
  50. 1 def secret_key_base
  51. get_header Cookies::SECRET_KEY_BASE
  52. end
  53. 1 def cookies_serializer
  54. get_header Cookies::COOKIES_SERIALIZER
  55. end
  56. 1 def cookies_same_site_protection
  57. get_header Cookies::COOKIES_SAME_SITE_PROTECTION
  58. end
  59. 1 def cookies_digest
  60. get_header Cookies::COOKIES_DIGEST
  61. end
  62. 1 def cookies_rotations
  63. get_header Cookies::COOKIES_ROTATIONS
  64. end
  65. 1 def use_cookies_with_metadata
  66. get_header Cookies::USE_COOKIES_WITH_METADATA
  67. end
  68. # :startdoc:
  69. end
  70. # Read and write data to cookies through ActionController#cookies.
  71. #
  72. # When reading cookie data, the data is read from the HTTP request header, Cookie.
  73. # When writing cookie data, the data is sent out in the HTTP response header, Set-Cookie.
  74. #
  75. # Examples of writing:
  76. #
  77. # # Sets a simple session cookie.
  78. # # This cookie will be deleted when the user's browser is closed.
  79. # cookies[:user_name] = "david"
  80. #
  81. # # Cookie values are String based. Other data types need to be serialized.
  82. # cookies[:lat_lon] = JSON.generate([47.68, -122.37])
  83. #
  84. # # Sets a cookie that expires in 1 hour.
  85. # cookies[:login] = { value: "XJ-122", expires: 1.hour }
  86. #
  87. # # Sets a cookie that expires at a specific time.
  88. # cookies[:login] = { value: "XJ-122", expires: Time.utc(2020, 10, 15, 5) }
  89. #
  90. # # Sets a signed cookie, which prevents users from tampering with its value.
  91. # # It can be read using the signed method `cookies.signed[:name]`
  92. # cookies.signed[:user_id] = current_user.id
  93. #
  94. # # Sets an encrypted cookie value before sending it to the client which
  95. # # prevent users from reading and tampering with its value.
  96. # # It can be read using the encrypted method `cookies.encrypted[:name]`
  97. # cookies.encrypted[:discount] = 45
  98. #
  99. # # Sets a "permanent" cookie (which expires in 20 years from now).
  100. # cookies.permanent[:login] = "XJ-122"
  101. #
  102. # # You can also chain these methods:
  103. # cookies.signed.permanent[:login] = "XJ-122"
  104. #
  105. # Examples of reading:
  106. #
  107. # cookies[:user_name] # => "david"
  108. # cookies.size # => 2
  109. # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37]
  110. # cookies.signed[:login] # => "XJ-122"
  111. # cookies.encrypted[:discount] # => 45
  112. #
  113. # Example for deleting:
  114. #
  115. # cookies.delete :user_name
  116. #
  117. # Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie:
  118. #
  119. # cookies[:name] = {
  120. # value: 'a yummy cookie',
  121. # expires: 1.year,
  122. # domain: 'domain.com'
  123. # }
  124. #
  125. # cookies.delete(:name, domain: 'domain.com')
  126. #
  127. # The option symbols for setting cookies are:
  128. #
  129. # * <tt>:value</tt> - The cookie's value.
  130. # * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root
  131. # of the application.
  132. # * <tt>:domain</tt> - The domain for which this cookie applies so you can
  133. # restrict to the domain level. If you use a schema like www.example.com
  134. # and want to share session with user.example.com set <tt>:domain</tt>
  135. # to <tt>:all</tt>. To support multiple domains, provide an array, and
  136. # the first domain matching <tt>request.host</tt> will be used. Make
  137. # sure to specify the <tt>:domain</tt> option with <tt>:all</tt> or
  138. # <tt>Array</tt> again when deleting cookies.
  139. #
  140. # domain: nil # Does not set cookie domain. (default)
  141. # domain: :all # Allow the cookie for the top most level
  142. # # domain and subdomains.
  143. # domain: %w(.example.com .example.org) # Allow the cookie
  144. # # for concrete domain names.
  145. #
  146. # * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly
  147. # set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD.
  148. # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 2.
  149. # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time or ActiveSupport::Duration object.
  150. # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers.
  151. # Default is +false+.
  152. # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
  153. # only HTTP. Defaults to +false+.
  154. 1 class Cookies
  155. 1 HTTP_HEADER = "Set-Cookie"
  156. 1 GENERATOR_KEY = "action_dispatch.key_generator"
  157. 1 SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt"
  158. 1 ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt"
  159. 1 ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt"
  160. 1 AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt"
  161. 1 USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption"
  162. 1 ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher"
  163. 1 SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest"
  164. 1 SECRET_KEY_BASE = "action_dispatch.secret_key_base"
  165. 1 COOKIES_SERIALIZER = "action_dispatch.cookies_serializer"
  166. 1 COOKIES_DIGEST = "action_dispatch.cookies_digest"
  167. 1 COOKIES_ROTATIONS = "action_dispatch.cookies_rotations"
  168. 1 COOKIES_SAME_SITE_PROTECTION = "action_dispatch.cookies_same_site_protection"
  169. 1 USE_COOKIES_WITH_METADATA = "action_dispatch.use_cookies_with_metadata"
  170. # Cookies can typically store 4096 bytes.
  171. 1 MAX_COOKIE_SIZE = 4096
  172. # Raised when storing more than 4K of session data.
  173. 1 CookieOverflow = Class.new StandardError
  174. # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed.
  175. 1 module ChainedCookieJars
  176. # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
  177. #
  178. # cookies.permanent[:prefers_open_id] = true
  179. # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
  180. #
  181. # This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
  182. #
  183. # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
  184. #
  185. # cookies.permanent.signed[:remember_me] = current_user.id
  186. # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
  187. 1 def permanent
  188. @permanent ||= PermanentCookieJar.new(self)
  189. end
  190. # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
  191. # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
  192. # cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
  193. #
  194. # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
  195. #
  196. # Example:
  197. #
  198. # cookies.signed[:discount] = 45
  199. # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
  200. #
  201. # cookies.signed[:discount] # => 45
  202. 1 def signed
  203. @signed ||= SignedKeyRotatingCookieJar.new(self)
  204. end
  205. # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
  206. # If the cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
  207. #
  208. # If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+
  209. # are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded.
  210. #
  211. # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
  212. #
  213. # Example:
  214. #
  215. # cookies.encrypted[:discount] = 45
  216. # # => Set-Cookie: discount=DIQ7fw==--K3n//8vvnSbGq9dA--7Xh91HfLpwzbj1czhBiwOg==; path=/
  217. #
  218. # cookies.encrypted[:discount] # => 45
  219. 1 def encrypted
  220. @encrypted ||= EncryptedKeyRotatingCookieJar.new(self)
  221. end
  222. # Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
  223. # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
  224. 1 def signed_or_encrypted
  225. @signed_or_encrypted ||=
  226. if request.secret_key_base.present?
  227. encrypted
  228. else
  229. signed
  230. end
  231. end
  232. 1 private
  233. 1 def upgrade_legacy_hmac_aes_cbc_cookies?
  234. request.secret_key_base.present? &&
  235. request.encrypted_signed_cookie_salt.present? &&
  236. request.encrypted_cookie_salt.present? &&
  237. request.use_authenticated_cookie_encryption
  238. end
  239. 1 def prepare_upgrade_legacy_hmac_aes_cbc_cookies?
  240. request.secret_key_base.present? &&
  241. request.authenticated_encrypted_cookie_salt.present? &&
  242. !request.use_authenticated_cookie_encryption
  243. end
  244. 1 def encrypted_cookie_cipher
  245. request.encrypted_cookie_cipher || "aes-256-gcm"
  246. end
  247. 1 def signed_cookie_digest
  248. request.signed_cookie_digest || "SHA1"
  249. end
  250. end
  251. 1 class CookieJar #:nodoc:
  252. 1 include Enumerable, ChainedCookieJars
  253. # This regular expression is used to split the levels of a domain.
  254. # The top level domain can be any string without a period or
  255. # **.**, ***.** style TLDs like co.uk or com.au
  256. #
  257. # www.example.co.uk gives:
  258. # $& => example.co.uk
  259. #
  260. # example.com gives:
  261. # $& => example.com
  262. #
  263. # lots.of.subdomains.example.local gives:
  264. # $& => example.local
  265. 1 DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
  266. 1 def self.build(req, cookies)
  267. jar = new(req)
  268. jar.update(cookies)
  269. jar
  270. end
  271. 1 attr_reader :request
  272. 1 def initialize(request)
  273. @set_cookies = {}
  274. @delete_cookies = {}
  275. @request = request
  276. @cookies = {}
  277. @committed = false
  278. end
  279. 1 def committed?; @committed; end
  280. 1 def commit!
  281. @committed = true
  282. @set_cookies.freeze
  283. @delete_cookies.freeze
  284. end
  285. 1 def each(&block)
  286. @cookies.each(&block)
  287. end
  288. # Returns the value of the cookie by +name+, or +nil+ if no such cookie exists.
  289. 1 def [](name)
  290. @cookies[name.to_s]
  291. end
  292. 1 def fetch(name, *args, &block)
  293. @cookies.fetch(name.to_s, *args, &block)
  294. end
  295. 1 def key?(name)
  296. @cookies.key?(name.to_s)
  297. end
  298. 1 alias :has_key? :key?
  299. # Returns the cookies as Hash.
  300. 1 alias :to_hash :to_h
  301. 1 def update(other_hash)
  302. @cookies.update other_hash.stringify_keys
  303. self
  304. end
  305. 1 def update_cookies_from_jar
  306. request_jar = @request.cookie_jar.instance_variable_get(:@cookies)
  307. set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) || @set_cookies.key?(k) }
  308. @cookies.update set_cookies if set_cookies
  309. end
  310. 1 def to_header
  311. @cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; "
  312. end
  313. # Sets the cookie named +name+. The second argument may be the cookie's
  314. # value or a hash of options as documented above.
  315. 1 def []=(name, options)
  316. if options.is_a?(Hash)
  317. options.symbolize_keys!
  318. value = options[:value]
  319. else
  320. value = options
  321. options = { value: value }
  322. end
  323. handle_options(options)
  324. if @cookies[name.to_s] != value || options[:expires]
  325. @cookies[name.to_s] = value
  326. @set_cookies[name.to_s] = options
  327. @delete_cookies.delete(name.to_s)
  328. end
  329. value
  330. end
  331. # Removes the cookie on the client machine by setting the value to an empty string
  332. # and the expiration date in the past. Like <tt>[]=</tt>, you can pass in
  333. # an options hash to delete cookies with extra data such as a <tt>:path</tt>.
  334. 1 def delete(name, options = {})
  335. return unless @cookies.has_key? name.to_s
  336. options.symbolize_keys!
  337. handle_options(options)
  338. value = @cookies.delete(name.to_s)
  339. @delete_cookies[name.to_s] = options
  340. value
  341. end
  342. # Whether the given cookie is to be deleted by this CookieJar.
  343. # Like <tt>[]=</tt>, you can pass in an options hash to test if a
  344. # deletion applies to a specific <tt>:path</tt>, <tt>:domain</tt> etc.
  345. 1 def deleted?(name, options = {})
  346. options.symbolize_keys!
  347. handle_options(options)
  348. @delete_cookies[name.to_s] == options
  349. end
  350. # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie.
  351. 1 def clear(options = {})
  352. @cookies.each_key { |k| delete(k, options) }
  353. end
  354. 1 def write(headers)
  355. if header = make_set_cookie_header(headers[HTTP_HEADER])
  356. headers[HTTP_HEADER] = header
  357. end
  358. end
  359. 1 mattr_accessor :always_write_cookie, default: false
  360. 1 private
  361. 1 def escape(string)
  362. ::Rack::Utils.escape(string)
  363. end
  364. 1 def make_set_cookie_header(header)
  365. header = @set_cookies.inject(header) { |m, (k, v)|
  366. if write_cookie?(v)
  367. ::Rack::Utils.add_cookie_to_header(m, k, v)
  368. else
  369. m
  370. end
  371. }
  372. @delete_cookies.inject(header) { |m, (k, v)|
  373. ::Rack::Utils.add_remove_cookie_to_header(m, k, v)
  374. }
  375. end
  376. 1 def write_cookie?(cookie)
  377. request.ssl? || !cookie[:secure] || always_write_cookie
  378. end
  379. 1 def handle_options(options)
  380. if options[:expires].respond_to?(:from_now)
  381. options[:expires] = options[:expires].from_now
  382. end
  383. options[:path] ||= "/"
  384. options[:same_site] ||= request.cookies_same_site_protection
  385. if options[:domain] == :all || options[:domain] == "all"
  386. # If there is a provided tld length then we use it otherwise default domain regexp.
  387. domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
  388. # If host is not ip and matches domain regexp.
  389. # (ip confirms to domain regexp so we explicitly check for ip)
  390. options[:domain] = if !request.host.match?(/^[\d.]+$/) && (request.host =~ domain_regexp)
  391. ".#{$&}"
  392. end
  393. elsif options[:domain].is_a? Array
  394. # If host matches one of the supplied domains.
  395. options[:domain] = options[:domain].find do |domain|
  396. domain = domain.delete_prefix(".")
  397. request.host == domain || request.host.end_with?(".#{domain}")
  398. end
  399. end
  400. end
  401. end
  402. 1 class AbstractCookieJar # :nodoc:
  403. 1 include ChainedCookieJars
  404. 1 def initialize(parent_jar)
  405. @parent_jar = parent_jar
  406. end
  407. 1 def [](name)
  408. if data = @parent_jar[name.to_s]
  409. result = parse(name, data, purpose: "cookie.#{name}")
  410. if result.nil?
  411. parse(name, data)
  412. else
  413. result
  414. end
  415. end
  416. end
  417. 1 def []=(name, options)
  418. if options.is_a?(Hash)
  419. options.symbolize_keys!
  420. else
  421. options = { value: options }
  422. end
  423. commit(name, options)
  424. @parent_jar[name] = options
  425. end
  426. 1 protected
  427. 1 def request; @parent_jar.request; end
  428. 1 private
  429. 1 def expiry_options(options)
  430. if options[:expires].respond_to?(:from_now)
  431. { expires_in: options[:expires] }
  432. else
  433. { expires_at: options[:expires] }
  434. end
  435. end
  436. 1 def cookie_metadata(name, options)
  437. expiry_options(options).tap do |metadata|
  438. metadata[:purpose] = "cookie.#{name}" if request.use_cookies_with_metadata
  439. end
  440. end
  441. 1 def parse(name, data, purpose: nil); data; end
  442. 1 def commit(name, options); end
  443. end
  444. 1 class PermanentCookieJar < AbstractCookieJar # :nodoc:
  445. 1 private
  446. 1 def commit(name, options)
  447. options[:expires] = 20.years.from_now
  448. end
  449. end
  450. 1 class MarshalWithJsonFallback # :nodoc:
  451. 1 def self.load(value)
  452. Marshal.load(value)
  453. rescue TypeError => e
  454. ActiveSupport::JSON.decode(value) rescue raise e
  455. end
  456. 1 def self.dump(value)
  457. Marshal.dump(value)
  458. end
  459. end
  460. 1 class JsonSerializer # :nodoc:
  461. 1 def self.load(value)
  462. ActiveSupport::JSON.decode(value)
  463. end
  464. 1 def self.dump(value)
  465. ActiveSupport::JSON.encode(value)
  466. end
  467. end
  468. 1 module SerializedCookieJars # :nodoc:
  469. 1 MARSHAL_SIGNATURE = "\x04\x08"
  470. 1 SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
  471. 1 protected
  472. 1 def needs_migration?(value)
  473. request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
  474. end
  475. 1 def serialize(value)
  476. serializer.dump(value)
  477. end
  478. 1 def deserialize(name)
  479. rotate = false
  480. value = yield -> { rotate = true }
  481. if value
  482. case
  483. when needs_migration?(value)
  484. Marshal.load(value).tap do |v|
  485. self[name] = { value: v }
  486. end
  487. when rotate
  488. serializer.load(value).tap do |v|
  489. self[name] = { value: v }
  490. end
  491. else
  492. serializer.load(value)
  493. end
  494. end
  495. end
  496. 1 def serializer
  497. serializer = request.cookies_serializer || :marshal
  498. case serializer
  499. when :marshal
  500. MarshalWithJsonFallback
  501. when :json, :hybrid
  502. JsonSerializer
  503. else
  504. serializer
  505. end
  506. end
  507. 1 def digest
  508. request.cookies_digest || "SHA1"
  509. end
  510. end
  511. 1 class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
  512. 1 include SerializedCookieJars
  513. 1 def initialize(parent_jar)
  514. super
  515. secret = request.key_generator.generate_key(request.signed_cookie_salt)
  516. @verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER)
  517. request.cookies_rotations.signed.each do |(*secrets)|
  518. options = secrets.extract_options!
  519. @verifier.rotate(*secrets, serializer: SERIALIZER, **options)
  520. end
  521. end
  522. 1 private
  523. 1 def parse(name, signed_message, purpose: nil)
  524. deserialize(name) do |rotate|
  525. @verifier.verified(signed_message, on_rotation: rotate, purpose: purpose)
  526. end
  527. end
  528. 1 def commit(name, options)
  529. options[:value] = @verifier.generate(serialize(options[:value]), **cookie_metadata(name, options))
  530. raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
  531. end
  532. end
  533. 1 class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
  534. 1 include SerializedCookieJars
  535. 1 def initialize(parent_jar)
  536. super
  537. if request.use_authenticated_cookie_encryption
  538. key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
  539. secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len)
  540. @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: SERIALIZER)
  541. else
  542. key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")
  543. secret = request.key_generator.generate_key(request.encrypted_cookie_salt, key_len)
  544. sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
  545. @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: SERIALIZER)
  546. end
  547. request.cookies_rotations.encrypted.each do |(*secrets)|
  548. options = secrets.extract_options!
  549. @encryptor.rotate(*secrets, serializer: SERIALIZER, **options)
  550. end
  551. if upgrade_legacy_hmac_aes_cbc_cookies?
  552. legacy_cipher = "aes-256-cbc"
  553. secret = request.key_generator.generate_key(request.encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(legacy_cipher))
  554. sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
  555. @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER)
  556. elsif prepare_upgrade_legacy_hmac_aes_cbc_cookies?
  557. future_cipher = encrypted_cookie_cipher
  558. secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(future_cipher))
  559. @encryptor.rotate(secret, nil, cipher: future_cipher, serializer: SERIALIZER)
  560. end
  561. end
  562. 1 private
  563. 1 def parse(name, encrypted_message, purpose: nil)
  564. deserialize(name) do |rotate|
  565. @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate, purpose: purpose)
  566. end
  567. rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
  568. nil
  569. end
  570. 1 def commit(name, options)
  571. options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), **cookie_metadata(name, options))
  572. raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
  573. end
  574. end
  575. 1 def initialize(app)
  576. 37 @app = app
  577. end
  578. 1 def call(env)
  579. request = ActionDispatch::Request.new env
  580. status, headers, body = @app.call(env)
  581. if request.have_cookie_jar?
  582. cookie_jar = request.cookie_jar
  583. unless cookie_jar.committed?
  584. cookie_jar.write(headers)
  585. if headers[HTTP_HEADER].respond_to?(:join)
  586. headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
  587. end
  588. end
  589. end
  590. [status, headers, body]
  591. end
  592. end
  593. end

lib/action_dispatch/middleware/debug_exceptions.rb

29.03% lines covered

93 relevant lines. 27 lines covered and 66 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_dispatch/http/request"
  3. 1 require "action_dispatch/middleware/exception_wrapper"
  4. 1 require "action_dispatch/routing/inspector"
  5. 1 require "action_view"
  6. 1 module ActionDispatch
  7. # This middleware is responsible for logging exceptions and
  8. # showing a debugging page in case the request is local.
  9. 1 class DebugExceptions
  10. 1 cattr_reader :interceptors, instance_accessor: false, default: []
  11. 1 def self.register_interceptor(object = nil, &block)
  12. interceptor = object || block
  13. interceptors << interceptor
  14. end
  15. 1 def initialize(app, routes_app = nil, response_format = :default, interceptors = self.class.interceptors)
  16. 42 @app = app
  17. 42 @routes_app = routes_app
  18. 42 @response_format = response_format
  19. 42 @interceptors = interceptors
  20. end
  21. 1 def call(env)
  22. request = ActionDispatch::Request.new env
  23. _, headers, body = response = @app.call(env)
  24. if headers["X-Cascade"] == "pass"
  25. body.close if body.respond_to?(:close)
  26. raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
  27. end
  28. response
  29. rescue Exception => exception
  30. invoke_interceptors(request, exception)
  31. raise exception unless request.show_exceptions?
  32. render_exception(request, exception)
  33. end
  34. 1 private
  35. 1 def invoke_interceptors(request, exception)
  36. backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
  37. wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
  38. @interceptors.each do |interceptor|
  39. interceptor.call(request, exception)
  40. rescue Exception
  41. log_error(request, wrapper)
  42. end
  43. end
  44. 1 def render_exception(request, exception)
  45. backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
  46. wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
  47. log_error(request, wrapper)
  48. if request.get_header("action_dispatch.show_detailed_exceptions")
  49. begin
  50. content_type = request.formats.first
  51. rescue Mime::Type::InvalidMimeType
  52. content_type = Mime[:text]
  53. end
  54. if api_request?(content_type)
  55. render_for_api_request(content_type, wrapper)
  56. else
  57. render_for_browser_request(request, wrapper)
  58. end
  59. else
  60. raise exception
  61. end
  62. end
  63. 1 def render_for_browser_request(request, wrapper)
  64. template = create_template(request, wrapper)
  65. file = "rescues/#{wrapper.rescue_template}"
  66. if request.xhr?
  67. body = template.render(template: file, layout: false, formats: [:text])
  68. format = "text/plain"
  69. else
  70. body = template.render(template: file, layout: "rescues/layout")
  71. format = "text/html"
  72. end
  73. render(wrapper.status_code, body, format)
  74. end
  75. 1 def render_for_api_request(content_type, wrapper)
  76. body = {
  77. status: wrapper.status_code,
  78. error: Rack::Utils::HTTP_STATUS_CODES.fetch(
  79. wrapper.status_code,
  80. Rack::Utils::HTTP_STATUS_CODES[500]
  81. ),
  82. exception: wrapper.exception.inspect,
  83. traces: wrapper.traces
  84. }
  85. to_format = "to_#{content_type.to_sym}"
  86. if content_type && body.respond_to?(to_format)
  87. formatted_body = body.public_send(to_format)
  88. format = content_type
  89. else
  90. formatted_body = body.to_json
  91. format = Mime[:json]
  92. end
  93. render(wrapper.status_code, formatted_body, format)
  94. end
  95. 1 def create_template(request, wrapper)
  96. DebugView.new(
  97. request: request,
  98. exception_wrapper: wrapper,
  99. exception: wrapper.exception,
  100. traces: wrapper.traces,
  101. show_source_idx: wrapper.source_to_show_id,
  102. trace_to_show: wrapper.trace_to_show,
  103. routes_inspector: routes_inspector(wrapper.exception),
  104. source_extracts: wrapper.source_extracts,
  105. line_number: wrapper.line_number,
  106. file: wrapper.file
  107. )
  108. end
  109. 1 def render(status, body, format)
  110. [status, { "Content-Type" => "#{format}; charset=#{Response.default_charset}", "Content-Length" => body.bytesize.to_s }, [body]]
  111. end
  112. 1 def log_error(request, wrapper)
  113. logger = logger(request)
  114. return unless logger
  115. exception = wrapper.exception
  116. trace = wrapper.exception_trace
  117. message = []
  118. message << " "
  119. message << "#{exception.class} (#{exception.message}):"
  120. message.concat(exception.annotated_source_code) if exception.respond_to?(:annotated_source_code)
  121. message << " "
  122. message.concat(trace)
  123. log_array(logger, message)
  124. end
  125. 1 def log_array(logger, array)
  126. lines = Array(array)
  127. return if lines.empty?
  128. if logger.formatter && logger.formatter.respond_to?(:tags_text)
  129. logger.fatal lines.join("\n#{logger.formatter.tags_text}")
  130. else
  131. logger.fatal lines.join("\n")
  132. end
  133. end
  134. 1 def logger(request)
  135. request.logger || ActionView::Base.logger || stderr_logger
  136. end
  137. 1 def stderr_logger
  138. @stderr_logger ||= ActiveSupport::Logger.new($stderr)
  139. end
  140. 1 def routes_inspector(exception)
  141. if @routes_app.respond_to?(:routes) && (exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error))
  142. ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes)
  143. end
  144. end
  145. 1 def api_request?(content_type)
  146. @response_format == :api && !content_type.html?
  147. end
  148. end
  149. end

lib/action_dispatch/middleware/debug_locks.rb

0.0% lines covered

76 relevant lines. 0 lines covered and 76 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionDispatch
  3. # This middleware can be used to diagnose deadlocks in the autoload interlock.
  4. #
  5. # To use it, insert it near the top of the middleware stack, using
  6. # <tt>config/application.rb</tt>:
  7. #
  8. # config.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocks
  9. #
  10. # After restarting the application and re-triggering the deadlock condition,
  11. # <tt>/rails/locks</tt> will show a summary of all threads currently known to
  12. # the interlock, which lock level they are holding or awaiting, and their
  13. # current backtrace.
  14. #
  15. # Generally a deadlock will be caused by the interlock conflicting with some
  16. # other external lock or blocking I/O call. These cannot be automatically
  17. # identified, but should be visible in the displayed backtraces.
  18. #
  19. # NOTE: The formatting and content of this middleware's output is intended for
  20. # human consumption, and should be expected to change between releases.
  21. #
  22. # This middleware exposes operational details of the server, with no access
  23. # control. It should only be enabled when in use, and removed thereafter.
  24. class DebugLocks
  25. def initialize(app, path = "/rails/locks")
  26. @app = app
  27. @path = path
  28. end
  29. def call(env)
  30. req = ActionDispatch::Request.new env
  31. if req.get?
  32. path = req.path_info.chomp("/")
  33. if path == @path
  34. return render_details(req)
  35. end
  36. end
  37. @app.call(env)
  38. end
  39. private
  40. def render_details(req)
  41. threads = ActiveSupport::Dependencies.interlock.raw_state do |raw_threads|
  42. # The Interlock itself comes to a complete halt as long as this block
  43. # is executing. That gives us a more consistent picture of everything,
  44. # but creates a pretty strong Observer Effect.
  45. #
  46. # Most directly, that means we need to do as little as possible in
  47. # this block. More widely, it means this middleware should remain a
  48. # strictly diagnostic tool (to be used when something has gone wrong),
  49. # and not for any sort of general monitoring.
  50. raw_threads.each.with_index do |(thread, info), idx|
  51. info[:index] = idx
  52. info[:backtrace] = thread.backtrace
  53. end
  54. raw_threads
  55. end
  56. str = threads.map do |thread, info|
  57. if info[:exclusive]
  58. lock_state = +"Exclusive"
  59. elsif info[:sharing] > 0
  60. lock_state = +"Sharing"
  61. lock_state << " x#{info[:sharing]}" if info[:sharing] > 1
  62. else
  63. lock_state = +"No lock"
  64. end
  65. if info[:waiting]
  66. lock_state << " (yielded share)"
  67. end
  68. msg = +"Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n"
  69. if info[:sleeper]
  70. msg << " Waiting in #{info[:sleeper]}"
  71. msg << " to #{info[:purpose].to_s.inspect}" unless info[:purpose].nil?
  72. msg << "\n"
  73. if info[:compatible]
  74. compat = info[:compatible].map { |c| c == false ? "share" : c.to_s.inspect }
  75. msg << " may be pre-empted for: #{compat.join(', ')}\n"
  76. end
  77. blockers = threads.values.select { |binfo| blocked_by?(info, binfo, threads.values) }
  78. msg << " blocked by: #{blockers.map { |i| i[:index] }.join(', ')}\n" if blockers.any?
  79. end
  80. blockees = threads.values.select { |binfo| blocked_by?(binfo, info, threads.values) }
  81. msg << " blocking: #{blockees.map { |i| i[:index] }.join(', ')}\n" if blockees.any?
  82. msg << "\n#{info[:backtrace].join("\n")}\n" if info[:backtrace]
  83. end.join("\n\n---\n\n\n")
  84. [200, { "Content-Type" => "text/plain", "Content-Length" => str.size }, [str]]
  85. end
  86. def blocked_by?(victim, blocker, all_threads)
  87. return false if victim.equal?(blocker)
  88. case victim[:sleeper]
  89. when :start_sharing
  90. blocker[:exclusive] ||
  91. (!victim[:waiting] && blocker[:compatible] && !blocker[:compatible].include?(false))
  92. when :start_exclusive
  93. blocker[:sharing] > 0 ||
  94. blocker[:exclusive] ||
  95. (blocker[:compatible] && !blocker[:compatible].include?(victim[:purpose]))
  96. when :yield_shares
  97. blocker[:exclusive]
  98. when :stop_exclusive
  99. blocker[:exclusive] ||
  100. victim[:compatible] &&
  101. victim[:compatible].include?(blocker[:purpose]) &&
  102. all_threads.all? { |other| !other[:compatible] || blocker.equal?(other) || other[:compatible].include?(blocker[:purpose]) }
  103. end
  104. end
  105. end
  106. end

lib/action_dispatch/middleware/debug_view.rb

0.0% lines covered

52 relevant lines. 0 lines covered and 52 lines missed.
    
  1. # frozen_string_literal: true
  2. require "pp"
  3. require "action_view"
  4. require "action_view/base"
  5. module ActionDispatch
  6. class DebugView < ActionView::Base # :nodoc:
  7. RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__)
  8. def initialize(assigns)
  9. paths = [RESCUES_TEMPLATE_PATH]
  10. lookup_context = ActionView::LookupContext.new(paths)
  11. super(lookup_context, assigns)
  12. end
  13. def compiled_method_container
  14. self.class
  15. end
  16. def debug_params(params)
  17. clean_params = params.clone
  18. clean_params.delete("action")
  19. clean_params.delete("controller")
  20. if clean_params.empty?
  21. "None"
  22. else
  23. PP.pp(clean_params, +"", 200)
  24. end
  25. end
  26. def debug_headers(headers)
  27. if headers.present?
  28. headers.inspect.gsub(",", ",\n")
  29. else
  30. "None"
  31. end
  32. end
  33. def debug_hash(object)
  34. object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
  35. end
  36. def render(*)
  37. logger = ActionView::Base.logger
  38. if logger && logger.respond_to?(:silence)
  39. logger.silence { super }
  40. else
  41. super
  42. end
  43. end
  44. def protect_against_forgery?
  45. false
  46. end
  47. def params_valid?
  48. @request.parameters
  49. rescue ActionController::BadRequest
  50. false
  51. end
  52. end
  53. end

lib/action_dispatch/middleware/exception_wrapper.rb

37.04% lines covered

81 relevant lines. 30 lines covered and 51 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/module/attribute_accessors"
  3. 1 require "rack/utils"
  4. 1 module ActionDispatch
  5. 1 class ExceptionWrapper
  6. 1 cattr_accessor :rescue_responses, default: Hash.new(:internal_server_error).merge!(
  7. "ActionController::RoutingError" => :not_found,
  8. "AbstractController::ActionNotFound" => :not_found,
  9. "ActionController::MethodNotAllowed" => :method_not_allowed,
  10. "ActionController::UnknownHttpMethod" => :method_not_allowed,
  11. "ActionController::NotImplemented" => :not_implemented,
  12. "ActionController::UnknownFormat" => :not_acceptable,
  13. "Mime::Type::InvalidMimeType" => :not_acceptable,
  14. "ActionController::MissingExactTemplate" => :not_acceptable,
  15. "ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
  16. "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
  17. "ActionDispatch::Http::Parameters::ParseError" => :bad_request,
  18. "ActionController::BadRequest" => :bad_request,
  19. "ActionController::ParameterMissing" => :bad_request,
  20. "Rack::QueryParser::ParameterTypeError" => :bad_request,
  21. "Rack::QueryParser::InvalidParameterError" => :bad_request
  22. )
  23. 1 cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!(
  24. "ActionView::MissingTemplate" => "missing_template",
  25. "ActionController::RoutingError" => "routing_error",
  26. "AbstractController::ActionNotFound" => "unknown_action",
  27. "ActiveRecord::StatementInvalid" => "invalid_statement",
  28. "ActionView::Template::Error" => "template_error",
  29. "ActionController::MissingExactTemplate" => "missing_exact_template",
  30. )
  31. 1 cattr_accessor :wrapper_exceptions, default: [
  32. "ActionView::Template::Error"
  33. ]
  34. 1 cattr_accessor :silent_exceptions, default: [
  35. "ActionController::RoutingError"
  36. ]
  37. 1 attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file
  38. 1 def initialize(backtrace_cleaner, exception)
  39. @backtrace_cleaner = backtrace_cleaner
  40. @exception = exception
  41. @exception_class_name = @exception.class.name
  42. @wrapped_causes = wrapped_causes_for(exception, backtrace_cleaner)
  43. expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
  44. end
  45. 1 def unwrapped_exception
  46. if wrapper_exceptions.include?(@exception_class_name)
  47. exception.cause
  48. else
  49. exception
  50. end
  51. end
  52. 1 def rescue_template
  53. @@rescue_templates[@exception_class_name]
  54. end
  55. 1 def status_code
  56. self.class.status_code_for_exception(unwrapped_exception.class.name)
  57. end
  58. 1 def exception_trace
  59. trace = application_trace
  60. trace = framework_trace if trace.empty? && !silent_exceptions.include?(@exception_class_name)
  61. trace
  62. end
  63. 1 def application_trace
  64. clean_backtrace(:silent)
  65. end
  66. 1 def framework_trace
  67. clean_backtrace(:noise)
  68. end
  69. 1 def full_trace
  70. clean_backtrace(:all)
  71. end
  72. 1 def traces
  73. application_trace_with_ids = []
  74. framework_trace_with_ids = []
  75. full_trace_with_ids = []
  76. full_trace.each_with_index do |trace, idx|
  77. trace_with_id = {
  78. exception_object_id: @exception.object_id,
  79. id: idx,
  80. trace: trace
  81. }
  82. if application_trace.include?(trace)
  83. application_trace_with_ids << trace_with_id
  84. else
  85. framework_trace_with_ids << trace_with_id
  86. end
  87. full_trace_with_ids << trace_with_id
  88. end
  89. {
  90. "Application Trace" => application_trace_with_ids,
  91. "Framework Trace" => framework_trace_with_ids,
  92. "Full Trace" => full_trace_with_ids
  93. }
  94. end
  95. 1 def self.status_code_for_exception(class_name)
  96. Rack::Utils.status_code(@@rescue_responses[class_name])
  97. end
  98. 1 def source_extracts
  99. backtrace.map do |trace|
  100. file, line_number = extract_file_and_line_number(trace)
  101. {
  102. code: source_fragment(file, line_number),
  103. line_number: line_number
  104. }
  105. end
  106. end
  107. 1 def trace_to_show
  108. if traces["Application Trace"].empty? && rescue_template != "routing_error"
  109. "Full Trace"
  110. else
  111. "Application Trace"
  112. end
  113. end
  114. 1 def source_to_show_id
  115. (traces[trace_to_show].first || {})[:id]
  116. end
  117. 1 private
  118. 1 def backtrace
  119. Array(@exception.backtrace)
  120. end
  121. 1 def causes_for(exception)
  122. return enum_for(__method__, exception) unless block_given?
  123. yield exception while exception = exception.cause
  124. end
  125. 1 def wrapped_causes_for(exception, backtrace_cleaner)
  126. causes_for(exception).map { |cause| self.class.new(backtrace_cleaner, cause) }
  127. end
  128. 1 def clean_backtrace(*args)
  129. if backtrace_cleaner
  130. backtrace_cleaner.clean(backtrace, *args)
  131. else
  132. backtrace
  133. end
  134. end
  135. 1 def source_fragment(path, line)
  136. return unless Rails.respond_to?(:root) && Rails.root
  137. full_path = Rails.root.join(path)
  138. if File.exist?(full_path)
  139. File.open(full_path, "r") do |file|
  140. start = [line - 3, 0].max
  141. lines = file.each_line.drop(start).take(6)
  142. Hash[*(start + 1..(lines.count + start)).zip(lines).flatten]
  143. end
  144. end
  145. end
  146. 1 def extract_file_and_line_number(trace)
  147. # Split by the first colon followed by some digits, which works for both
  148. # Windows and Unix path styles.
  149. file, line = trace.match(/^(.+?):(\d+).*$/, &:captures) || trace
  150. [file, line.to_i]
  151. end
  152. 1 def expand_backtrace
  153. @exception.backtrace.unshift(
  154. @exception.to_s.split("\n")
  155. ).flatten!
  156. end
  157. end
  158. end

lib/action_dispatch/middleware/executor.rb

0.0% lines covered

17 relevant lines. 0 lines covered and 17 lines missed.
    
  1. # frozen_string_literal: true
  2. require "rack/body_proxy"
  3. module ActionDispatch
  4. class Executor
  5. def initialize(app, executor)
  6. @app, @executor = app, executor
  7. end
  8. def call(env)
  9. state = @executor.run!
  10. begin
  11. response = @app.call(env)
  12. returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
  13. ensure
  14. state.complete! unless returned
  15. end
  16. end
  17. end
  18. end

lib/action_dispatch/middleware/flash.rb

38.76% lines covered

129 relevant lines. 50 lines covered and 79 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/hash/keys"
  3. 1 module ActionDispatch
  4. # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed
  5. # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
  6. # action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
  7. # then expose the flash to its template. Actually, that exposure is automatically done.
  8. #
  9. # class PostsController < ActionController::Base
  10. # def create
  11. # # save post
  12. # flash[:notice] = "Post successfully created"
  13. # redirect_to @post
  14. # end
  15. #
  16. # def show
  17. # # doesn't need to assign the flash notice to the template, that's done automatically
  18. # end
  19. # end
  20. #
  21. # show.html.erb
  22. # <% if flash[:notice] %>
  23. # <div class="notice"><%= flash[:notice] %></div>
  24. # <% end %>
  25. #
  26. # Since the +notice+ and +alert+ keys are a common idiom, convenience accessors are available:
  27. #
  28. # flash.alert = "You must be logged in"
  29. # flash.notice = "Post successfully created"
  30. #
  31. # This example places a string in the flash. And of course, you can put as many as you like at a time too. If you want to pass
  32. # non-primitive types, you will have to handle that in your application. Example: To show messages with links, you will have to
  33. # use sanitize helper.
  34. #
  35. # Just remember: They'll be gone by the time the next action has been performed.
  36. #
  37. # See docs on the FlashHash class for more details about the flash.
  38. 1 class Flash
  39. 1 KEY = "action_dispatch.request.flash_hash"
  40. 1 module RequestMethods
  41. # Access the contents of the flash. Use <tt>flash["notice"]</tt> to
  42. # read a notice you put there or <tt>flash["notice"] = "hello"</tt>
  43. # to put a new one.
  44. 1 def flash
  45. flash = flash_hash
  46. return flash if flash
  47. self.flash = Flash::FlashHash.from_session_value(session["flash"])
  48. end
  49. 1 def flash=(flash)
  50. set_header Flash::KEY, flash
  51. end
  52. 1 def flash_hash # :nodoc:
  53. get_header Flash::KEY
  54. end
  55. 1 def commit_flash # :nodoc:
  56. session = self.session || {}
  57. flash_hash = self.flash_hash
  58. if flash_hash && (flash_hash.present? || session.key?("flash"))
  59. session["flash"] = flash_hash.to_session_value
  60. self.flash = flash_hash.dup
  61. end
  62. if (!session.respond_to?(:loaded?) || session.loaded?) && # reset_session uses {}, which doesn't implement #loaded?
  63. session.key?("flash") && session["flash"].nil?
  64. session.delete("flash")
  65. end
  66. end
  67. 1 def reset_session # :nodoc:
  68. super
  69. self.flash = nil
  70. end
  71. end
  72. 1 class FlashNow #:nodoc:
  73. 1 attr_accessor :flash
  74. 1 def initialize(flash)
  75. @flash = flash
  76. end
  77. 1 def []=(k, v)
  78. k = k.to_s
  79. @flash[k] = v
  80. @flash.discard(k)
  81. v
  82. end
  83. 1 def [](k)
  84. @flash[k.to_s]
  85. end
  86. # Convenience accessor for <tt>flash.now[:alert]=</tt>.
  87. 1 def alert=(message)
  88. self[:alert] = message
  89. end
  90. # Convenience accessor for <tt>flash.now[:notice]=</tt>.
  91. 1 def notice=(message)
  92. self[:notice] = message
  93. end
  94. end
  95. 1 class FlashHash
  96. 1 include Enumerable
  97. 1 def self.from_session_value(value) #:nodoc:
  98. case value
  99. when FlashHash # Rails 3.1, 3.2
  100. flashes = value.instance_variable_get(:@flashes)
  101. if discard = value.instance_variable_get(:@used)
  102. flashes.except!(*discard)
  103. end
  104. new(flashes, flashes.keys)
  105. when Hash # Rails 4.0
  106. flashes = value["flashes"]
  107. if discard = value["discard"]
  108. flashes.except!(*discard)
  109. end
  110. new(flashes, flashes.keys)
  111. else
  112. new
  113. end
  114. end
  115. # Builds a hash containing the flashes to keep for the next request.
  116. # If there are none to keep, returns +nil+.
  117. 1 def to_session_value #:nodoc:
  118. flashes_to_keep = @flashes.except(*@discard)
  119. return nil if flashes_to_keep.empty?
  120. { "discard" => [], "flashes" => flashes_to_keep }
  121. end
  122. 1 def initialize(flashes = {}, discard = []) #:nodoc:
  123. @discard = Set.new(stringify_array(discard))
  124. @flashes = flashes.stringify_keys
  125. @now = nil
  126. end
  127. 1 def initialize_copy(other)
  128. if other.now_is_loaded?
  129. @now = other.now.dup
  130. @now.flash = self
  131. end
  132. super
  133. end
  134. 1 def []=(k, v)
  135. k = k.to_s
  136. @discard.delete k
  137. @flashes[k] = v
  138. end
  139. 1 def [](k)
  140. @flashes[k.to_s]
  141. end
  142. 1 def update(h) #:nodoc:
  143. @discard.subtract stringify_array(h.keys)
  144. @flashes.update h.stringify_keys
  145. self
  146. end
  147. 1 def keys
  148. @flashes.keys
  149. end
  150. 1 def key?(name)
  151. @flashes.key? name.to_s
  152. end
  153. 1 def delete(key)
  154. key = key.to_s
  155. @discard.delete key
  156. @flashes.delete key
  157. self
  158. end
  159. 1 def to_hash
  160. @flashes.dup
  161. end
  162. 1 def empty?
  163. @flashes.empty?
  164. end
  165. 1 def clear
  166. @discard.clear
  167. @flashes.clear
  168. end
  169. 1 def each(&block)
  170. @flashes.each(&block)
  171. end
  172. 1 alias :merge! :update
  173. 1 def replace(h) #:nodoc:
  174. @discard.clear
  175. @flashes.replace h.stringify_keys
  176. self
  177. end
  178. # Sets a flash that will not be available to the next action, only to the current.
  179. #
  180. # flash.now[:message] = "Hello current action"
  181. #
  182. # This method enables you to use the flash as a central messaging system in your app.
  183. # When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
  184. # When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
  185. # vanish when the current action is done.
  186. #
  187. # Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
  188. #
  189. # Also, brings two convenience accessors:
  190. #
  191. # flash.now.alert = "Beware now!"
  192. # # Equivalent to flash.now[:alert] = "Beware now!"
  193. #
  194. # flash.now.notice = "Good luck now!"
  195. # # Equivalent to flash.now[:notice] = "Good luck now!"
  196. 1 def now
  197. @now ||= FlashNow.new(self)
  198. end
  199. # Keeps either the entire current flash or a specific flash entry available for the next action:
  200. #
  201. # flash.keep # keeps the entire flash
  202. # flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
  203. 1 def keep(k = nil)
  204. k = k.to_s if k
  205. @discard.subtract Array(k || keys)
  206. k ? self[k] : self
  207. end
  208. # Marks the entire flash or a single flash entry to be discarded by the end of the current action:
  209. #
  210. # flash.discard # discard the entire flash at the end of the current action
  211. # flash.discard(:warning) # discard only the "warning" entry at the end of the current action
  212. 1 def discard(k = nil)
  213. k = k.to_s if k
  214. @discard.merge Array(k || keys)
  215. k ? self[k] : self
  216. end
  217. # Mark for removal entries that were kept, and delete unkept ones.
  218. #
  219. # This method is called automatically by filters, so you generally don't need to care about it.
  220. 1 def sweep #:nodoc:
  221. @discard.each { |k| @flashes.delete k }
  222. @discard.replace @flashes.keys
  223. end
  224. # Convenience accessor for <tt>flash[:alert]</tt>.
  225. 1 def alert
  226. self[:alert]
  227. end
  228. # Convenience accessor for <tt>flash[:alert]=</tt>.
  229. 1 def alert=(message)
  230. self[:alert] = message
  231. end
  232. # Convenience accessor for <tt>flash[:notice]</tt>.
  233. 1 def notice
  234. self[:notice]
  235. end
  236. # Convenience accessor for <tt>flash[:notice]=</tt>.
  237. 1 def notice=(message)
  238. self[:notice] = message
  239. end
  240. 1 protected
  241. 1 def now_is_loaded?
  242. @now
  243. end
  244. 1 private
  245. 1 def stringify_array(array) # :doc:
  246. array.map do |item|
  247. item.kind_of?(Symbol) ? item.to_s : item
  248. end
  249. end
  250. end
  251. 39 def self.new(app) app; end
  252. end
  253. 1 class Request
  254. 1 prepend Flash::RequestMethods
  255. end
  256. end

lib/action_dispatch/middleware/host_authorization.rb

0.0% lines covered

75 relevant lines. 0 lines covered and 75 lines missed.
    
  1. # frozen_string_literal: true
  2. require "action_dispatch/http/request"
  3. module ActionDispatch
  4. # This middleware guards from DNS rebinding attacks by explicitly permitting
  5. # the hosts a request can be sent to.
  6. #
  7. # When a request comes to an unauthorized host, the +response_app+
  8. # application will be executed and rendered. If no +response_app+ is given, a
  9. # default one will run, which responds with +403 Forbidden+.
  10. class HostAuthorization
  11. class Permissions # :nodoc:
  12. def initialize(hosts)
  13. @hosts = sanitize_hosts(hosts)
  14. end
  15. def empty?
  16. @hosts.empty?
  17. end
  18. def allows?(host)
  19. @hosts.any? do |allowed|
  20. allowed === host
  21. rescue
  22. # IPAddr#=== raises an error if you give it a hostname instead of
  23. # IP. Treat similar errors as blocked access.
  24. false
  25. end
  26. end
  27. private
  28. def sanitize_hosts(hosts)
  29. Array(hosts).map do |host|
  30. case host
  31. when Regexp then sanitize_regexp(host)
  32. when String then sanitize_string(host)
  33. else host
  34. end
  35. end
  36. end
  37. def sanitize_regexp(host)
  38. /\A#{host}\z/
  39. end
  40. def sanitize_string(host)
  41. if host.start_with?(".")
  42. /\A(.+\.)?#{Regexp.escape(host[1..-1])}\z/
  43. else
  44. host
  45. end
  46. end
  47. end
  48. DEFAULT_RESPONSE_APP = -> env do
  49. request = Request.new(env)
  50. format = request.xhr? ? "text/plain" : "text/html"
  51. template = DebugView.new(host: request.host)
  52. body = template.render(template: "rescues/blocked_host", layout: "rescues/layout")
  53. [403, {
  54. "Content-Type" => "#{format}; charset=#{Response.default_charset}",
  55. "Content-Length" => body.bytesize.to_s,
  56. }, [body]]
  57. end
  58. def initialize(app, hosts, response_app = nil)
  59. @app = app
  60. @permissions = Permissions.new(hosts)
  61. @response_app = response_app || DEFAULT_RESPONSE_APP
  62. end
  63. def call(env)
  64. return @app.call(env) if @permissions.empty?
  65. request = Request.new(env)
  66. if authorized?(request)
  67. mark_as_authorized(request)
  68. @app.call(env)
  69. else
  70. @response_app.call(env)
  71. end
  72. end
  73. private
  74. def authorized?(request)
  75. origin_host = request.get_header("HTTP_HOST").to_s.sub(/:\d+\z/, "")
  76. forwarded_host = request.x_forwarded_host.to_s.split(/,\s?/).last.to_s.sub(/:\d+\z/, "")
  77. @permissions.allows?(origin_host) &&
  78. (forwarded_host.blank? || @permissions.allows?(forwarded_host))
  79. end
  80. def mark_as_authorized(request)
  81. request.set_header("action_dispatch.authorized_host", request.host)
  82. end
  83. end
  84. end

lib/action_dispatch/middleware/public_exceptions.rb

37.04% lines covered

27 relevant lines. 10 lines covered and 17 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. # When called, this middleware renders an error page. By default if an HTML
  4. # response is expected it will render static error pages from the <tt>/public</tt>
  5. # directory. For example when this middleware receives a 500 response it will
  6. # render the template found in <tt>/public/500.html</tt>.
  7. # If an internationalized locale is set, this middleware will attempt to render
  8. # the template in <tt>/public/500.<locale>.html</tt>. If an internationalized template
  9. # is not found it will fall back on <tt>/public/500.html</tt>.
  10. #
  11. # When a request with a content type other than HTML is made, this middleware
  12. # will attempt to convert error information into the appropriate response type.
  13. 1 class PublicExceptions
  14. 1 attr_accessor :public_path
  15. 1 def initialize(public_path)
  16. 39 @public_path = public_path
  17. end
  18. 1 def call(env)
  19. request = ActionDispatch::Request.new(env)
  20. status = request.path_info[1..-1].to_i
  21. begin
  22. content_type = request.formats.first
  23. rescue Mime::Type::InvalidMimeType
  24. content_type = Mime[:text]
  25. end
  26. body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
  27. render(status, content_type, body)
  28. end
  29. 1 private
  30. 1 def render(status, content_type, body)
  31. format = "to_#{content_type.to_sym}" if content_type
  32. if format && body.respond_to?(format)
  33. render_format(status, content_type, body.public_send(format))
  34. else
  35. render_html(status)
  36. end
  37. end
  38. 1 def render_format(status, content_type, body)
  39. [status, { "Content-Type" => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
  40. "Content-Length" => body.bytesize.to_s }, [body]]
  41. end
  42. 1 def render_html(status)
  43. path = "#{public_path}/#{status}.#{I18n.locale}.html"
  44. path = "#{public_path}/#{status}.html" unless (found = File.exist?(path))
  45. if found || File.exist?(path)
  46. render_format(status, "text/html", File.read(path))
  47. else
  48. [404, { "X-Cascade" => "pass" }, []]
  49. end
  50. end
  51. end
  52. end

lib/action_dispatch/middleware/reloader.rb

0.0% lines covered

4 relevant lines. 0 lines covered and 4 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionDispatch
  3. # ActionDispatch::Reloader wraps the request with callbacks provided by ActiveSupport::Reloader
  4. # callbacks, intended to assist with code reloading during development.
  5. #
  6. # By default, ActionDispatch::Reloader is included in the middleware stack
  7. # only in the development environment; specifically, when +config.cache_classes+
  8. # is false.
  9. class Reloader < Executor
  10. end
  11. end

lib/action_dispatch/middleware/remote_ip.rb

0.0% lines covered

70 relevant lines. 0 lines covered and 70 lines missed.
    
  1. # frozen_string_literal: true
  2. require "ipaddr"
  3. module ActionDispatch
  4. # This middleware calculates the IP address of the remote client that is
  5. # making the request. It does this by checking various headers that could
  6. # contain the address, and then picking the last-set address that is not
  7. # on the list of trusted IPs. This follows the precedent set by e.g.
  8. # {the Tomcat server}[https://issues.apache.org/bugzilla/show_bug.cgi?id=50453],
  9. # with {reasoning explained at length}[https://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection]
  10. # by @gingerlime. A more detailed explanation of the algorithm is given
  11. # at GetIp#calculate_ip.
  12. #
  13. # Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2]
  14. # requires. Some Rack servers simply drop preceding headers, and only report
  15. # the value that was {given in the last header}[https://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
  16. # If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn)
  17. # then you should test your Rack server to make sure your data is good.
  18. #
  19. # IF YOU DON'T USE A PROXY, THIS MAKES YOU VULNERABLE TO IP SPOOFING.
  20. # This middleware assumes that there is at least one proxy sitting around
  21. # and setting headers with the client's remote IP address. If you don't use
  22. # a proxy, because you are hosted on e.g. Heroku without SSL, any client can
  23. # claim to have any IP address by setting the X-Forwarded-For header. If you
  24. # care about that, then you need to explicitly drop or ignore those headers
  25. # sometime before this middleware runs.
  26. class RemoteIp
  27. class IpSpoofAttackError < StandardError; end
  28. # The default trusted IPs list simply includes IP addresses that are
  29. # guaranteed by the IP specification to be private addresses. Those will
  30. # not be the ultimate client IP in production, and so are discarded. See
  31. # https://en.wikipedia.org/wiki/Private_network for details.
  32. TRUSTED_PROXIES = [
  33. "127.0.0.0/8", # localhost IPv4 range, per RFC-3330
  34. "::1", # localhost IPv6
  35. "fc00::/7", # private IPv6 range fc00::/7
  36. "10.0.0.0/8", # private IPv4 range 10.x.x.x
  37. "172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
  38. "192.168.0.0/16", # private IPv4 range 192.168.x.x
  39. ].map { |proxy| IPAddr.new(proxy) }
  40. attr_reader :check_ip, :proxies
  41. # Create a new +RemoteIp+ middleware instance.
  42. #
  43. # The +ip_spoofing_check+ option is on by default. When on, an exception
  44. # is raised if it looks like the client is trying to lie about its own IP
  45. # address. It makes sense to turn off this check on sites aimed at non-IP
  46. # clients (like WAP devices), or behind proxies that set headers in an
  47. # incorrect or confusing way (like AWS ELB).
  48. #
  49. # The +custom_proxies+ argument can take an Array of string, IPAddr, or
  50. # Regexp objects which will be used instead of +TRUSTED_PROXIES+. If a
  51. # single string, IPAddr, or Regexp object is provided, it will be used in
  52. # addition to +TRUSTED_PROXIES+. Any proxy setup will put the value you
  53. # want in the middle (or at the beginning) of the X-Forwarded-For list,
  54. # with your proxy servers after it. If your proxies aren't removed, pass
  55. # them in via the +custom_proxies+ parameter. That way, the middleware will
  56. # ignore those IP addresses, and return the one that you want.
  57. def initialize(app, ip_spoofing_check = true, custom_proxies = nil)
  58. @app = app
  59. @check_ip = ip_spoofing_check
  60. @proxies = if custom_proxies.blank?
  61. TRUSTED_PROXIES
  62. elsif custom_proxies.respond_to?(:any?)
  63. custom_proxies
  64. else
  65. Array(custom_proxies) + TRUSTED_PROXIES
  66. end
  67. end
  68. # Since the IP address may not be needed, we store the object here
  69. # without calculating the IP to keep from slowing down the majority of
  70. # requests. For those requests that do need to know the IP, the
  71. # GetIp#calculate_ip method will calculate the memoized client IP address.
  72. def call(env)
  73. req = ActionDispatch::Request.new env
  74. req.remote_ip = GetIp.new(req, check_ip, proxies)
  75. @app.call(req.env)
  76. end
  77. # The GetIp class exists as a way to defer processing of the request data
  78. # into an actual IP address. If the ActionDispatch::Request#remote_ip method
  79. # is called, this class will calculate the value and then memoize it.
  80. class GetIp
  81. def initialize(req, check_ip, proxies)
  82. @req = req
  83. @check_ip = check_ip
  84. @proxies = proxies
  85. end
  86. # Sort through the various IP address headers, looking for the IP most
  87. # likely to be the address of the actual remote client making this
  88. # request.
  89. #
  90. # REMOTE_ADDR will be correct if the request is made directly against the
  91. # Ruby process, on e.g. Heroku. When the request is proxied by another
  92. # server like HAProxy or NGINX, the IP address that made the original
  93. # request will be put in an X-Forwarded-For header. If there are multiple
  94. # proxies, that header may contain a list of IPs. Other proxy services
  95. # set the Client-Ip header instead, so we check that too.
  96. #
  97. # As discussed in {this post about Rails IP Spoofing}[https://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/],
  98. # while the first IP in the list is likely to be the "originating" IP,
  99. # it could also have been set by the client maliciously.
  100. #
  101. # In order to find the first address that is (probably) accurate, we
  102. # take the list of IPs, remove known and trusted proxies, and then take
  103. # the last address left, which was presumably set by one of those proxies.
  104. def calculate_ip
  105. # Set by the Rack web server, this is a single value.
  106. remote_addr = ips_from(@req.remote_addr).last
  107. # Could be a CSV list and/or repeated headers that were concatenated.
  108. client_ips = ips_from(@req.client_ip).reverse
  109. forwarded_ips = ips_from(@req.x_forwarded_for).reverse
  110. # +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set.
  111. # If they are both set, it means that either:
  112. #
  113. # 1) This request passed through two proxies with incompatible IP header
  114. # conventions.
  115. # 2) The client passed one of +Client-Ip+ or +X-Forwarded-For+
  116. # (whichever the proxy servers weren't using) themselves.
  117. #
  118. # Either way, there is no way for us to determine which header is the
  119. # right one after the fact. Since we have no idea, if we are concerned
  120. # about IP spoofing we need to give up and explode. (If you're not
  121. # concerned about IP spoofing you can turn the +ip_spoofing_check+
  122. # option off.)
  123. should_check_ip = @check_ip && client_ips.last && forwarded_ips.last
  124. if should_check_ip && !forwarded_ips.include?(client_ips.last)
  125. # We don't know which came from the proxy, and which from the user
  126. raise IpSpoofAttackError, "IP spoofing attack?! " \
  127. "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \
  128. "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
  129. end
  130. # We assume these things about the IP headers:
  131. #
  132. # - X-Forwarded-For will be a list of IPs, one per proxy, or blank
  133. # - Client-Ip is propagated from the outermost proxy, or is blank
  134. # - REMOTE_ADDR will be the IP that made the request to Rack
  135. ips = [forwarded_ips, client_ips].flatten.compact
  136. # If every single IP option is in the trusted list, return the IP
  137. # that's furthest away
  138. filter_proxies(ips + [remote_addr]).first || ips.last || remote_addr
  139. end
  140. # Memoizes the value returned by #calculate_ip and returns it for
  141. # ActionDispatch::Request to use.
  142. def to_s
  143. @ip ||= calculate_ip
  144. end
  145. private
  146. def ips_from(header) # :doc:
  147. return [] unless header
  148. # Split the comma-separated list into an array of strings.
  149. ips = header.strip.split(/[,\s]+/)
  150. ips.select do |ip|
  151. # Only return IPs that are valid according to the IPAddr#new method.
  152. range = IPAddr.new(ip).to_range
  153. # We want to make sure nobody is sneaking a netmask in.
  154. range.begin == range.end
  155. rescue ArgumentError
  156. nil
  157. end
  158. end
  159. def filter_proxies(ips) # :doc:
  160. ips.reject do |ip|
  161. @proxies.any? { |proxy| proxy === ip }
  162. end
  163. end
  164. end
  165. end
  166. end

lib/action_dispatch/middleware/request_id.rb

0.0% lines covered

26 relevant lines. 0 lines covered and 26 lines missed.
    
  1. # frozen_string_literal: true
  2. require "securerandom"
  3. require "active_support/core_ext/string/access"
  4. module ActionDispatch
  5. # Makes a unique request id available to the +action_dispatch.request_id+ env variable (which is then accessible
  6. # through <tt>ActionDispatch::Request#request_id</tt> or the alias <tt>ActionDispatch::Request#uuid</tt>) and sends
  7. # the same id to the client via the X-Request-Id header.
  8. #
  9. # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
  10. # by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
  11. # header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
  12. #
  13. # The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
  14. # from multiple pieces of the stack.
  15. class RequestId
  16. X_REQUEST_ID = "X-Request-Id" #:nodoc:
  17. def initialize(app)
  18. @app = app
  19. end
  20. def call(env)
  21. req = ActionDispatch::Request.new env
  22. req.request_id = make_request_id(req.x_request_id)
  23. @app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = req.request_id }
  24. end
  25. private
  26. def make_request_id(request_id)
  27. if request_id.presence
  28. request_id.gsub(/[^\w\-@]/, "").first(255)
  29. else
  30. internal_request_id
  31. end
  32. end
  33. def internal_request_id
  34. SecureRandom.uuid
  35. end
  36. end
  37. end

lib/action_dispatch/middleware/session/abstract_store.rb

66.1% lines covered

59 relevant lines. 39 lines covered and 20 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "rack/utils"
  3. 1 require "rack/request"
  4. 1 require "rack/session/abstract/id"
  5. 1 require "action_dispatch/middleware/cookies"
  6. 1 require "action_dispatch/request/session"
  7. 1 module ActionDispatch
  8. 1 module Session
  9. 1 class SessionRestoreError < StandardError #:nodoc:
  10. 1 def initialize
  11. super("Session contains objects whose class definition isn't available.\n" \
  12. "Remember to require the classes for all objects kept in the session.\n" \
  13. "(Original exception: #{$!.message} [#{$!.class}])\n")
  14. set_backtrace $!.backtrace
  15. end
  16. end
  17. 1 module Compatibility
  18. 1 def initialize(app, options = {})
  19. 1 options[:key] ||= "_session_id"
  20. 1 super
  21. end
  22. 1 def generate_sid
  23. sid = SecureRandom.hex(16)
  24. sid.encode!(Encoding::UTF_8)
  25. sid
  26. end
  27. 1 private
  28. 1 def initialize_sid # :doc:
  29. 1 @default_options.delete(:sidbits)
  30. 1 @default_options.delete(:secure_random)
  31. end
  32. 1 def make_request(env)
  33. ActionDispatch::Request.new env
  34. end
  35. end
  36. 1 module StaleSessionCheck
  37. 1 def load_session(env)
  38. stale_session_check! { super }
  39. end
  40. 1 def extract_session_id(env)
  41. stale_session_check! { super }
  42. end
  43. 1 def stale_session_check!
  44. yield
  45. rescue ArgumentError => argument_error
  46. if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
  47. begin
  48. # Note that the regexp does not allow $1 to end with a ':'.
  49. $1.constantize
  50. rescue LoadError, NameError
  51. raise ActionDispatch::Session::SessionRestoreError
  52. end
  53. retry
  54. else
  55. raise
  56. end
  57. end
  58. end
  59. 1 module SessionObject # :nodoc:
  60. 1 def prepare_session(req)
  61. Request::Session.create(self, req, @default_options)
  62. end
  63. 1 def loaded_session?(session)
  64. !session.is_a?(Request::Session) || session.loaded?
  65. end
  66. end
  67. 1 class AbstractStore < Rack::Session::Abstract::Persisted
  68. 1 include Compatibility
  69. 1 include StaleSessionCheck
  70. 1 include SessionObject
  71. 1 private
  72. 1 def set_cookie(request, response, cookie)
  73. request.cookie_jar[key] = cookie
  74. end
  75. end
  76. 1 class AbstractSecureStore < Rack::Session::Abstract::PersistedSecure
  77. 1 include Compatibility
  78. 1 include StaleSessionCheck
  79. 1 include SessionObject
  80. 1 def generate_sid
  81. Rack::Session::SessionId.new(super)
  82. end
  83. 1 private
  84. 1 def set_cookie(request, response, cookie)
  85. request.cookie_jar[key] = cookie
  86. end
  87. end
  88. end
  89. end

lib/action_dispatch/middleware/session/cache_store.rb

0.0% lines covered

39 relevant lines. 0 lines covered and 39 lines missed.
    
  1. # frozen_string_literal: true
  2. require "action_dispatch/middleware/session/abstract_store"
  3. module ActionDispatch
  4. module Session
  5. # A session store that uses an ActiveSupport::Cache::Store to store the sessions. This store is most useful
  6. # if you don't store critical data in your sessions and you don't need them to live for extended periods
  7. # of time.
  8. #
  9. # ==== Options
  10. # * <tt>cache</tt> - The cache to use. If it is not specified, <tt>Rails.cache</tt> will be used.
  11. # * <tt>expire_after</tt> - The length of time a session will be stored before automatically expiring.
  12. # By default, the <tt>:expires_in</tt> option of the cache is used.
  13. class CacheStore < AbstractSecureStore
  14. def initialize(app, options = {})
  15. @cache = options[:cache] || Rails.cache
  16. options[:expire_after] ||= @cache.options[:expires_in]
  17. super
  18. end
  19. # Get a session from the cache.
  20. def find_session(env, sid)
  21. unless sid && (session = get_session_with_fallback(sid))
  22. sid, session = generate_sid, {}
  23. end
  24. [sid, session]
  25. end
  26. # Set a session in the cache.
  27. def write_session(env, sid, session, options)
  28. key = cache_key(sid.private_id)
  29. if session
  30. @cache.write(key, session, expires_in: options[:expire_after])
  31. else
  32. @cache.delete(key)
  33. end
  34. sid
  35. end
  36. # Remove a session from the cache.
  37. def delete_session(env, sid, options)
  38. @cache.delete(cache_key(sid.private_id))
  39. @cache.delete(cache_key(sid.public_id))
  40. generate_sid
  41. end
  42. private
  43. # Turn the session id into a cache key.
  44. def cache_key(id)
  45. "_session_id:#{id}"
  46. end
  47. def get_session_with_fallback(sid)
  48. @cache.read(cache_key(sid.private_id)) || @cache.read(cache_key(sid.public_id))
  49. end
  50. end
  51. end
  52. end

lib/action_dispatch/middleware/session/cookie_store.rb

44.68% lines covered

47 relevant lines. 21 lines covered and 26 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/hash/keys"
  3. 1 require "action_dispatch/middleware/session/abstract_store"
  4. 1 require "rack/session/cookie"
  5. 1 module ActionDispatch
  6. 1 module Session
  7. # This cookie-based session store is the Rails default. It is
  8. # dramatically faster than the alternatives.
  9. #
  10. # Sessions typically contain at most a user_id and flash message; both fit
  11. # within the 4096 bytes cookie size limit. A CookieOverflow exception is raised if
  12. # you attempt to store more than 4096 bytes of data.
  13. #
  14. # The cookie jar used for storage is automatically configured to be the
  15. # best possible option given your application's configuration.
  16. #
  17. # Your cookies will be encrypted using your apps secret_key_base. This
  18. # goes a step further than signed cookies in that encrypted cookies cannot
  19. # be altered or read by users. This is the default starting in Rails 4.
  20. #
  21. # Configure your session store in an initializer:
  22. #
  23. # Rails.application.config.session_store :cookie_store, key: '_your_app_session'
  24. #
  25. # In the development and test environments your application's secret key base is
  26. # generated by Rails and stored in a temporary file in <tt>tmp/development_secret.txt</tt>.
  27. # In all other environments, it is stored encrypted in the
  28. # <tt>config/credentials.yml.enc</tt> file.
  29. #
  30. # If your application was not updated to Rails 5.2 defaults, the secret_key_base
  31. # will be found in the old <tt>config/secrets.yml</tt> file.
  32. #
  33. # Note that changing your secret_key_base will invalidate all existing session.
  34. # Additionally, you should take care to make sure you are not relying on the
  35. # ability to decode signed cookies generated by your app in external
  36. # applications or JavaScript before changing it.
  37. #
  38. # Because CookieStore extends Rack::Session::Abstract::Persisted, many of the
  39. # options described there can be used to customize the session cookie that
  40. # is generated. For example:
  41. #
  42. # Rails.application.config.session_store :cookie_store, expire_after: 14.days
  43. #
  44. # would set the session cookie to expire automatically 14 days after creation.
  45. # Other useful options include <tt>:key</tt>, <tt>:secure</tt> and
  46. # <tt>:httponly</tt>.
  47. 1 class CookieStore < AbstractSecureStore
  48. 1 class SessionId < DelegateClass(Rack::Session::SessionId)
  49. 1 attr_reader :cookie_value
  50. 1 def initialize(session_id, cookie_value = {})
  51. super(session_id)
  52. @cookie_value = cookie_value
  53. end
  54. end
  55. 1 def initialize(app, options = {})
  56. 1 super(app, options.merge!(cookie_only: true))
  57. end
  58. 1 def delete_session(req, session_id, options)
  59. new_sid = generate_sid unless options[:drop]
  60. # Reset hash and Assign the new session id
  61. req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid.public_id } : {})
  62. new_sid
  63. end
  64. 1 def load_session(req)
  65. stale_session_check! do
  66. data = unpacked_cookie_data(req)
  67. data = persistent_session_id!(data)
  68. [Rack::Session::SessionId.new(data["session_id"]), data]
  69. end
  70. end
  71. 1 private
  72. 1 def extract_session_id(req)
  73. stale_session_check! do
  74. sid = unpacked_cookie_data(req)["session_id"]
  75. sid && Rack::Session::SessionId.new(sid)
  76. end
  77. end
  78. 1 def unpacked_cookie_data(req)
  79. req.fetch_header("action_dispatch.request.unsigned_session_cookie") do |k|
  80. v = stale_session_check! do
  81. if data = get_cookie(req)
  82. data.stringify_keys!
  83. end
  84. data || {}
  85. end
  86. req.set_header k, v
  87. end
  88. end
  89. 1 def persistent_session_id!(data, sid = nil)
  90. data ||= {}
  91. data["session_id"] ||= sid || generate_sid.public_id
  92. data
  93. end
  94. 1 def write_session(req, sid, session_data, options)
  95. session_data["session_id"] = sid.public_id
  96. SessionId.new(sid, session_data)
  97. end
  98. 1 def set_cookie(request, session_id, cookie)
  99. cookie_jar(request)[@key] = cookie
  100. end
  101. 1 def get_cookie(req)
  102. cookie_jar(req)[@key]
  103. end
  104. 1 def cookie_jar(request)
  105. request.cookie_jar.signed_or_encrypted
  106. end
  107. end
  108. end
  109. end

lib/action_dispatch/middleware/session/mem_cache_store.rb

0.0% lines covered

20 relevant lines. 0 lines covered and 20 lines missed.
    
  1. # frozen_string_literal: true
  2. require "action_dispatch/middleware/session/abstract_store"
  3. begin
  4. require "rack/session/dalli"
  5. rescue LoadError => e
  6. $stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
  7. raise e
  8. end
  9. module ActionDispatch
  10. module Session
  11. # A session store that uses MemCache to implement storage.
  12. #
  13. # ==== Options
  14. # * <tt>expire_after</tt> - The length of time a session will be stored before automatically expiring.
  15. class MemCacheStore < Rack::Session::Dalli
  16. include Compatibility
  17. include StaleSessionCheck
  18. include SessionObject
  19. def initialize(app, options = {})
  20. options[:expire_after] ||= options[:expires]
  21. super
  22. end
  23. end
  24. end
  25. end

lib/action_dispatch/middleware/show_exceptions.rb

42.86% lines covered

28 relevant lines. 12 lines covered and 16 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_dispatch/http/request"
  3. 1 require "action_dispatch/middleware/exception_wrapper"
  4. 1 module ActionDispatch
  5. # This middleware rescues any exception returned by the application
  6. # and calls an exceptions app that will wrap it in a format for the end user.
  7. #
  8. # The exceptions app should be passed as parameter on initialization
  9. # of ShowExceptions. Every time there is an exception, ShowExceptions will
  10. # store the exception in env["action_dispatch.exception"], rewrite the
  11. # PATH_INFO to the exception status code and call the Rack app.
  12. #
  13. # If the application returns a "X-Cascade" pass response, this middleware
  14. # will send an empty response as result with the correct status code.
  15. # If any exception happens inside the exceptions app, this middleware
  16. # catches the exceptions and returns a FAILSAFE_RESPONSE.
  17. 1 class ShowExceptions
  18. 1 FAILSAFE_RESPONSE = [500, { "Content-Type" => "text/plain" },
  19. ["500 Internal Server Error\n" \
  20. "If you are the administrator of this website, then please read this web " \
  21. "application's log file and/or the web server's log file to find out what " \
  22. "went wrong."]]
  23. 1 def initialize(app, exceptions_app)
  24. 37 @app = app
  25. 37 @exceptions_app = exceptions_app
  26. end
  27. 1 def call(env)
  28. request = ActionDispatch::Request.new env
  29. @app.call(env)
  30. rescue Exception => exception
  31. if request.show_exceptions?
  32. render_exception(request, exception)
  33. else
  34. raise exception
  35. end
  36. end
  37. 1 private
  38. 1 def render_exception(request, exception)
  39. backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner"
  40. wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
  41. status = wrapper.status_code
  42. request.set_header "action_dispatch.exception", wrapper.unwrapped_exception
  43. request.set_header "action_dispatch.original_path", request.path_info
  44. request.path_info = "/#{status}"
  45. response = @exceptions_app.call(request.env)
  46. response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
  47. rescue Exception => failsafe_error
  48. $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
  49. FAILSAFE_RESPONSE
  50. end
  51. 1 def pass_response(status)
  52. [status, { "Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0" }, []]
  53. end
  54. end
  55. end

lib/action_dispatch/middleware/ssl.rb

0.0% lines covered

83 relevant lines. 0 lines covered and 83 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionDispatch
  3. # This middleware is added to the stack when <tt>config.force_ssl = true</tt>, and is passed
  4. # the options set in +config.ssl_options+. It does three jobs to enforce secure HTTP
  5. # requests:
  6. #
  7. # 1. <b>TLS redirect</b>: Permanently redirects +http://+ requests to +https://+
  8. # with the same URL host, path, etc. Enabled by default. Set +config.ssl_options+
  9. # to modify the destination URL
  10. # (e.g. <tt>redirect: { host: "secure.widgets.com", port: 8080 }</tt>), or set
  11. # <tt>redirect: false</tt> to disable this feature.
  12. #
  13. # Requests can opt-out of redirection with +exclude+:
  14. #
  15. # config.ssl_options = { redirect: { exclude: -> request { /healthcheck/.match?(request.path) } } }
  16. #
  17. # Cookies will not be flagged as secure for excluded requests.
  18. #
  19. # 2. <b>Secure cookies</b>: Sets the +secure+ flag on cookies to tell browsers they
  20. # must not be sent along with +http://+ requests. Enabled by default. Set
  21. # +config.ssl_options+ with <tt>secure_cookies: false</tt> to disable this feature.
  22. #
  23. # 3. <b>HTTP Strict Transport Security (HSTS)</b>: Tells the browser to remember
  24. # this site as TLS-only and automatically redirect non-TLS requests.
  25. # Enabled by default. Configure +config.ssl_options+ with <tt>hsts: false</tt> to disable.
  26. #
  27. # Set +config.ssl_options+ with <tt>hsts: { ... }</tt> to configure HSTS:
  28. #
  29. # * +expires+: How long, in seconds, these settings will stick. The minimum
  30. # required to qualify for browser preload lists is 1 year. Defaults to
  31. # 2 years (recommended).
  32. #
  33. # * +subdomains+: Set to +true+ to tell the browser to apply these settings
  34. # to all subdomains. This protects your cookies from interception by a
  35. # vulnerable site on a subdomain. Defaults to +true+.
  36. #
  37. # * +preload+: Advertise that this site may be included in browsers'
  38. # preloaded HSTS lists. HSTS protects your site on every visit <i>except the
  39. # first visit</i> since it hasn't seen your HSTS header yet. To close this
  40. # gap, browser vendors include a baked-in list of HSTS-enabled sites.
  41. # Go to https://hstspreload.org to submit your site for inclusion.
  42. # Defaults to +false+.
  43. #
  44. # To turn off HSTS, omitting the header is not enough. Browsers will remember the
  45. # original HSTS directive until it expires. Instead, use the header to tell browsers to
  46. # expire HSTS immediately. Setting <tt>hsts: false</tt> is a shortcut for
  47. # <tt>hsts: { expires: 0 }</tt>.
  48. class SSL
  49. # :stopdoc:
  50. # Default to 2 years as recommended on hstspreload.org.
  51. HSTS_EXPIRES_IN = 63072000
  52. def self.default_hsts_options
  53. { expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
  54. end
  55. def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, ssl_default_redirect_status: nil)
  56. @app = app
  57. @redirect = redirect
  58. @exclude = @redirect && @redirect[:exclude] || proc { !@redirect }
  59. @secure_cookies = secure_cookies
  60. @hsts_header = build_hsts_header(normalize_hsts_options(hsts))
  61. @ssl_default_redirect_status = ssl_default_redirect_status
  62. end
  63. def call(env)
  64. request = Request.new env
  65. if request.ssl?
  66. @app.call(env).tap do |status, headers, body|
  67. set_hsts_header! headers
  68. flag_cookies_as_secure! headers if @secure_cookies && !@exclude.call(request)
  69. end
  70. else
  71. return redirect_to_https request unless @exclude.call(request)
  72. @app.call(env)
  73. end
  74. end
  75. private
  76. def set_hsts_header!(headers)
  77. headers["Strict-Transport-Security"] ||= @hsts_header
  78. end
  79. def normalize_hsts_options(options)
  80. case options
  81. # Explicitly disabling HSTS clears the existing setting from browsers
  82. # by setting expiry to 0.
  83. when false
  84. self.class.default_hsts_options.merge(expires: 0)
  85. # Default to enabled, with default options.
  86. when nil, true
  87. self.class.default_hsts_options
  88. else
  89. self.class.default_hsts_options.merge(options)
  90. end
  91. end
  92. # https://tools.ietf.org/html/rfc6797#section-6.1
  93. def build_hsts_header(hsts)
  94. value = +"max-age=#{hsts[:expires].to_i}"
  95. value << "; includeSubDomains" if hsts[:subdomains]
  96. value << "; preload" if hsts[:preload]
  97. value
  98. end
  99. def flag_cookies_as_secure!(headers)
  100. if cookies = headers["Set-Cookie"]
  101. cookies = cookies.split("\n")
  102. headers["Set-Cookie"] = cookies.map { |cookie|
  103. if !/;\s*secure\s*(;|$)/i.match?(cookie)
  104. "#{cookie}; secure"
  105. else
  106. cookie
  107. end
  108. }.join("\n")
  109. end
  110. end
  111. def redirect_to_https(request)
  112. [ @redirect.fetch(:status, redirection_status(request)),
  113. { "Content-Type" => "text/html",
  114. "Location" => https_location_for(request) },
  115. (@redirect[:body] || []) ]
  116. end
  117. def redirection_status(request)
  118. if request.get? || request.head?
  119. 301 # Issue a permanent redirect via a GET request.
  120. elsif @ssl_default_redirect_status
  121. @ssl_default_redirect_status
  122. else
  123. 307 # Issue a fresh request redirect to preserve the HTTP method.
  124. end
  125. end
  126. def https_location_for(request)
  127. host = @redirect[:host] || request.host
  128. port = @redirect[:port] || request.port
  129. location = +"https://#{host}"
  130. location << ":#{port}" if port != 80 && port != 443
  131. location << request.fullpath
  132. location
  133. end
  134. end
  135. end

lib/action_dispatch/middleware/stack.rb

68.48% lines covered

92 relevant lines. 63 lines covered and 29 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/inflector/methods"
  3. 1 require "active_support/dependencies"
  4. 1 module ActionDispatch
  5. 1 class MiddlewareStack
  6. 1 class Middleware
  7. 1 attr_reader :args, :block, :klass
  8. 1 def initialize(klass, args, block)
  9. 316 @klass = klass
  10. 316 @args = args
  11. 316 @block = block
  12. end
  13. 1 def name; klass.name; end
  14. 1 def ==(middleware)
  15. case middleware
  16. when Middleware
  17. klass == middleware.klass
  18. when Class
  19. klass == middleware
  20. end
  21. end
  22. 1 def inspect
  23. if klass.is_a?(Class)
  24. klass.to_s
  25. else
  26. klass.class.to_s
  27. end
  28. end
  29. 1 def build(app)
  30. 308 klass.new(app, *args, &block)
  31. end
  32. 1 def build_instrumented(app)
  33. InstrumentationProxy.new(build(app), inspect)
  34. end
  35. end
  36. # This class is used to instrument the execution of a single middleware.
  37. # It proxies the `call` method transparently and instruments the method
  38. # call.
  39. 1 class InstrumentationProxy
  40. 1 EVENT_NAME = "process_middleware.action_dispatch"
  41. 1 def initialize(middleware, class_name)
  42. @middleware = middleware
  43. @payload = {
  44. middleware: class_name,
  45. }
  46. end
  47. 1 def call(env)
  48. ActiveSupport::Notifications.instrument(EVENT_NAME, @payload) do
  49. @middleware.call(env)
  50. end
  51. end
  52. end
  53. 1 include Enumerable
  54. 1 attr_accessor :middlewares
  55. 1 def initialize(*args)
  56. 40 @middlewares = []
  57. 40 yield(self) if block_given?
  58. end
  59. 1 def each
  60. 1 @middlewares.each { |x| yield x }
  61. end
  62. 1 def size
  63. middlewares.size
  64. end
  65. 1 def last
  66. middlewares.last
  67. end
  68. 1 def [](i)
  69. middlewares[i]
  70. end
  71. 1 def unshift(klass, *args, &block)
  72. middlewares.unshift(build_middleware(klass, args, block))
  73. end
  74. 1 ruby2_keywords(:unshift) if respond_to?(:ruby2_keywords, true)
  75. 1 def initialize_copy(other)
  76. 301 self.middlewares = other.middlewares.dup
  77. end
  78. 1 def insert(index, klass, *args, &block)
  79. 2 index = assert_index(index, :before)
  80. 2 middlewares.insert(index, build_middleware(klass, args, block))
  81. end
  82. 1 ruby2_keywords(:insert) if respond_to?(:ruby2_keywords, true)
  83. 1 alias_method :insert_before, :insert
  84. 1 def insert_after(index, *args, &block)
  85. index = assert_index(index, :after)
  86. insert(index + 1, *args, &block)
  87. end
  88. 1 ruby2_keywords(:insert_after) if respond_to?(:ruby2_keywords, true)
  89. 1 def swap(target, *args, &block)
  90. index = assert_index(target, :before)
  91. insert(index, *args, &block)
  92. middlewares.delete_at(index + 1)
  93. end
  94. 1 ruby2_keywords(:swap) if respond_to?(:ruby2_keywords, true)
  95. 1 def delete(target)
  96. 12 middlewares.delete_if { |m| m.klass == target }
  97. end
  98. 1 def move(target, source)
  99. source_index = assert_index(source, :before)
  100. source_middleware = middlewares.delete_at(source_index)
  101. target_index = assert_index(target, :before)
  102. middlewares.insert(target_index, source_middleware)
  103. end
  104. 1 alias_method :move_before, :move
  105. 1 def move_after(target, source)
  106. source_index = assert_index(source, :after)
  107. source_middleware = middlewares.delete_at(source_index)
  108. target_index = assert_index(target, :after)
  109. middlewares.insert(target_index + 1, source_middleware)
  110. end
  111. 1 def use(klass, *args, &block)
  112. 314 middlewares.push(build_middleware(klass, args, block))
  113. end
  114. 1 ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
  115. 1 def build(app = nil, &block)
  116. 39 instrumenting = ActiveSupport::Notifications.notifier.listening?(InstrumentationProxy::EVENT_NAME)
  117. 39 middlewares.freeze.reverse.inject(app || block) do |a, e|
  118. 308 if instrumenting
  119. e.build_instrumented(a)
  120. else
  121. 308 e.build(a)
  122. end
  123. end
  124. end
  125. 1 private
  126. 1 def assert_index(index, where)
  127. 5 i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }
  128. 2 raise "No such middleware to insert #{where}: #{index.inspect}" unless i
  129. 2 i
  130. end
  131. 1 def build_middleware(klass, args, block)
  132. 309 Middleware.new(klass, args, block)
  133. end
  134. end
  135. end

lib/action_dispatch/middleware/static.rb

0.0% lines covered

118 relevant lines. 0 lines covered and 118 lines missed.
    
  1. # frozen_string_literal: true
  2. require "rack/utils"
  3. require "active_support/core_ext/uri"
  4. module ActionDispatch
  5. # This middleware serves static files from disk, if available.
  6. # If no file is found, it hands off to the main app.
  7. #
  8. # In Rails apps, this middleware is configured to serve assets from
  9. # the +public/+ directory.
  10. #
  11. # Only GET and HEAD requests are served. POST and other HTTP methods
  12. # are handed off to the main app.
  13. #
  14. # Only files in the root directory are served; path traversal is denied.
  15. class Static
  16. def initialize(app, path, index: "index", headers: {})
  17. @app = app
  18. @file_handler = FileHandler.new(path, index: index, headers: headers)
  19. end
  20. def call(env)
  21. @file_handler.attempt(env) || @app.call(env)
  22. end
  23. end
  24. # This endpoint serves static files from disk using Rack::File.
  25. #
  26. # URL paths are matched with static files according to expected
  27. # conventions: +path+, +path+.html, +path+/index.html.
  28. #
  29. # Precompressed versions of these files are checked first. Brotli (.br)
  30. # and gzip (.gz) files are supported. If +path+.br exists, this
  31. # endpoint returns that file with a +Content-Encoding: br+ header.
  32. #
  33. # If no matching file is found, this endpoint responds 404 Not Found.
  34. #
  35. # Pass the +root+ directory to search for matching files, an optional
  36. # +index: "index"+ to change the default +path+/index.html, and optional
  37. # additional response headers.
  38. class FileHandler
  39. # Accept-Encoding value -> file extension
  40. PRECOMPRESSED = {
  41. "br" => ".br",
  42. "gzip" => ".gz",
  43. "identity" => nil
  44. }
  45. def initialize(root, index: "index", headers: {}, precompressed: %i[ br gzip ], compressible_content_types: /\A(?:text\/|application\/javascript)/)
  46. @root = root.chomp("/").b
  47. @index = index
  48. @precompressed = Array(precompressed).map(&:to_s) | %w[ identity ]
  49. @compressible_content_types = compressible_content_types
  50. @file_server = ::Rack::File.new(@root, headers)
  51. end
  52. def call(env)
  53. attempt(env) || @file_server.call(env)
  54. end
  55. def attempt(env)
  56. request = Rack::Request.new env
  57. if request.get? || request.head?
  58. if found = find_file(request.path_info, accept_encoding: request.accept_encoding)
  59. serve request, *found
  60. end
  61. end
  62. end
  63. private
  64. def serve(request, filepath, content_headers)
  65. original, request.path_info =
  66. request.path_info, ::Rack::Utils.escape_path(filepath).b
  67. @file_server.call(request.env).tap do |status, headers, body|
  68. # Omit Content-Encoding/Type/etc headers for 304 Not Modified
  69. if status != 304
  70. headers.update(content_headers)
  71. end
  72. end
  73. ensure
  74. request.path_info = original
  75. end
  76. # Match a URI path to a static file to be served.
  77. #
  78. # Used by the +Static+ class to negotiate a servable file in the
  79. # +public/+ directory (see Static#call).
  80. #
  81. # Checks for +path+, +path+.html, and +path+/index.html files,
  82. # in that order, including .br and .gzip compressed extensions.
  83. #
  84. # If a matching file is found, the path and necessary response headers
  85. # (Content-Type, Content-Encoding) are returned.
  86. def find_file(path_info, accept_encoding:)
  87. each_candidate_filepath(path_info) do |filepath, content_type|
  88. if response = try_files(filepath, content_type, accept_encoding: accept_encoding)
  89. return response
  90. end
  91. end
  92. end
  93. def try_files(filepath, content_type, accept_encoding:)
  94. headers = { "Content-Type" => content_type }
  95. if compressible? content_type
  96. try_precompressed_files filepath, headers, accept_encoding: accept_encoding
  97. elsif file_readable? filepath
  98. [ filepath, headers ]
  99. end
  100. end
  101. def try_precompressed_files(filepath, headers, accept_encoding:)
  102. each_precompressed_filepath(filepath) do |content_encoding, precompressed_filepath|
  103. if file_readable? precompressed_filepath
  104. # Identity encoding is default, so we skip Accept-Encoding
  105. # negotiation and needn't set Content-Encoding.
  106. #
  107. # Vary header is expected when we've found other available
  108. # encodings that Accept-Encoding ruled out.
  109. if content_encoding == "identity"
  110. return precompressed_filepath, headers
  111. else
  112. headers["Vary"] = "Accept-Encoding"
  113. if accept_encoding.any? { |enc, _| /\b#{content_encoding}\b/i.match?(enc) }
  114. headers["Content-Encoding"] = content_encoding
  115. return precompressed_filepath, headers
  116. end
  117. end
  118. end
  119. end
  120. end
  121. def file_readable?(path)
  122. file_stat = File.stat(File.join(@root, path.b))
  123. rescue SystemCallError
  124. false
  125. else
  126. file_stat.file? && file_stat.readable?
  127. end
  128. def compressible?(content_type)
  129. @compressible_content_types.match?(content_type)
  130. end
  131. def each_precompressed_filepath(filepath)
  132. @precompressed.each do |content_encoding|
  133. precompressed_ext = PRECOMPRESSED.fetch(content_encoding)
  134. yield content_encoding, "#{filepath}#{precompressed_ext}"
  135. end
  136. nil
  137. end
  138. def each_candidate_filepath(path_info)
  139. return unless path = clean_path(path_info)
  140. ext = ::File.extname(path)
  141. content_type = ::Rack::Mime.mime_type(ext, nil)
  142. yield path, content_type || "text/plain"
  143. # Tack on .html and /index.html only for paths that don't have
  144. # an explicit, resolvable file extension. No need to check
  145. # for foo.js.html and foo.js/index.html.
  146. unless content_type
  147. default_ext = ::ActionController::Base.default_static_extension
  148. if ext != default_ext
  149. default_content_type = ::Rack::Mime.mime_type(default_ext, "text/plain")
  150. yield "#{path}#{default_ext}", default_content_type
  151. yield "#{path}/#{@index}#{default_ext}", default_content_type
  152. end
  153. end
  154. nil
  155. end
  156. def clean_path(path_info)
  157. path = ::Rack::Utils.unescape_path path_info.chomp("/")
  158. if ::Rack::Utils.valid_path? path
  159. ::Rack::Utils.clean_path_info path
  160. end
  161. end
  162. end
  163. end

lib/action_dispatch/railtie.rb

0.0% lines covered

51 relevant lines. 0 lines covered and 51 lines missed.
    
  1. # frozen_string_literal: true
  2. require "action_dispatch"
  3. require "active_support/messages/rotation_configuration"
  4. module ActionDispatch
  5. class Railtie < Rails::Railtie # :nodoc:
  6. config.action_dispatch = ActiveSupport::OrderedOptions.new
  7. config.action_dispatch.x_sendfile_header = nil
  8. config.action_dispatch.ip_spoofing_check = true
  9. config.action_dispatch.show_exceptions = true
  10. config.action_dispatch.tld_length = 1
  11. config.action_dispatch.ignore_accept_header = false
  12. config.action_dispatch.rescue_templates = {}
  13. config.action_dispatch.rescue_responses = {}
  14. config.action_dispatch.default_charset = nil
  15. config.action_dispatch.rack_cache = false
  16. config.action_dispatch.http_auth_salt = "http authentication"
  17. config.action_dispatch.signed_cookie_salt = "signed cookie"
  18. config.action_dispatch.encrypted_cookie_salt = "encrypted cookie"
  19. config.action_dispatch.encrypted_signed_cookie_salt = "signed encrypted cookie"
  20. config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie"
  21. config.action_dispatch.use_authenticated_cookie_encryption = false
  22. config.action_dispatch.use_cookies_with_metadata = false
  23. config.action_dispatch.perform_deep_munge = true
  24. config.action_dispatch.return_only_media_type_on_content_type = true
  25. config.action_dispatch.default_headers = {
  26. "X-Frame-Options" => "SAMEORIGIN",
  27. "X-XSS-Protection" => "1; mode=block",
  28. "X-Content-Type-Options" => "nosniff",
  29. "X-Download-Options" => "noopen",
  30. "X-Permitted-Cross-Domain-Policies" => "none",
  31. "Referrer-Policy" => "strict-origin-when-cross-origin"
  32. }
  33. config.action_dispatch.cookies_rotations = ActiveSupport::Messages::RotationConfiguration.new
  34. config.eager_load_namespaces << ActionDispatch
  35. initializer "action_dispatch.configure" do |app|
  36. ActionDispatch::Http::URL.secure_protocol = app.config.force_ssl
  37. ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length
  38. ActionDispatch::Request.ignore_accept_header = app.config.action_dispatch.ignore_accept_header
  39. ActionDispatch::Request::Utils.perform_deep_munge = app.config.action_dispatch.perform_deep_munge
  40. ActiveSupport.on_load(:action_dispatch_response) do
  41. self.default_charset = app.config.action_dispatch.default_charset || app.config.encoding
  42. self.default_headers = app.config.action_dispatch.default_headers
  43. self.return_only_media_type_on_content_type = app.config.action_dispatch.return_only_media_type_on_content_type
  44. end
  45. ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses)
  46. ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates)
  47. config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil?
  48. ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie
  49. ActionDispatch.test_app = app
  50. end
  51. end
  52. end

lib/action_dispatch/request/session.rb

39.5% lines covered

119 relevant lines. 47 lines covered and 72 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "rack/session/abstract/id"
  3. 1 module ActionDispatch
  4. 1 class Request
  5. # Session is responsible for lazily loading the session from store.
  6. 1 class Session # :nodoc:
  7. 1 ENV_SESSION_KEY = Rack::RACK_SESSION # :nodoc:
  8. 1 ENV_SESSION_OPTIONS_KEY = Rack::RACK_SESSION_OPTIONS # :nodoc:
  9. # Singleton object used to determine if an optional param wasn't specified.
  10. 1 Unspecified = Object.new
  11. # Creates a session hash, merging the properties of the previous session if any.
  12. 1 def self.create(store, req, default_options)
  13. session_was = find req
  14. session = Request::Session.new(store, req)
  15. session.merge! session_was if session_was
  16. set(req, session)
  17. Options.set(req, Request::Session::Options.new(store, default_options))
  18. session
  19. end
  20. 1 def self.find(req)
  21. req.get_header ENV_SESSION_KEY
  22. end
  23. 1 def self.set(req, session)
  24. req.set_header ENV_SESSION_KEY, session
  25. end
  26. 1 class Options #:nodoc:
  27. 1 def self.set(req, options)
  28. req.set_header ENV_SESSION_OPTIONS_KEY, options
  29. end
  30. 1 def self.find(req)
  31. req.get_header ENV_SESSION_OPTIONS_KEY
  32. end
  33. 1 def initialize(by, default_options)
  34. @by = by
  35. @delegate = default_options.dup
  36. end
  37. 1 def [](key)
  38. @delegate[key]
  39. end
  40. 1 def id(req)
  41. @delegate.fetch(:id) {
  42. @by.send(:extract_session_id, req)
  43. }
  44. end
  45. 1 def []=(k, v); @delegate[k] = v; end
  46. 1 def to_hash; @delegate.dup; end
  47. 1 def values_at(*args); @delegate.values_at(*args); end
  48. end
  49. 1 def initialize(by, req)
  50. @by = by
  51. @req = req
  52. @delegate = {}
  53. @loaded = false
  54. @exists = nil # We haven't checked yet.
  55. end
  56. 1 def id
  57. options.id(@req)
  58. end
  59. 1 def options
  60. Options.find @req
  61. end
  62. 1 def destroy
  63. clear
  64. options = self.options || {}
  65. @by.send(:delete_session, @req, options.id(@req), options)
  66. # Load the new sid to be written with the response.
  67. @loaded = false
  68. load_for_write!
  69. end
  70. # Returns value of the key stored in the session or
  71. # +nil+ if the given key is not found in the session.
  72. 1 def [](key)
  73. load_for_read!
  74. key = key.to_s
  75. if key == "session_id"
  76. id&.public_id
  77. else
  78. @delegate[key]
  79. end
  80. end
  81. # Returns the nested value specified by the sequence of keys, returning
  82. # +nil+ if any intermediate step is +nil+.
  83. 1 def dig(*keys)
  84. load_for_read!
  85. keys = keys.map.with_index { |key, i| i.zero? ? key.to_s : key }
  86. @delegate.dig(*keys)
  87. end
  88. # Returns true if the session has the given key or false.
  89. 1 def has_key?(key)
  90. load_for_read!
  91. @delegate.key?(key.to_s)
  92. end
  93. 1 alias :key? :has_key?
  94. 1 alias :include? :has_key?
  95. # Returns keys of the session as Array.
  96. 1 def keys
  97. load_for_read!
  98. @delegate.keys
  99. end
  100. # Returns values of the session as Array.
  101. 1 def values
  102. load_for_read!
  103. @delegate.values
  104. end
  105. # Writes given value to given key of the session.
  106. 1 def []=(key, value)
  107. load_for_write!
  108. @delegate[key.to_s] = value
  109. end
  110. # Clears the session.
  111. 1 def clear
  112. load_for_write!
  113. @delegate.clear
  114. end
  115. # Returns the session as Hash.
  116. 1 def to_hash
  117. load_for_read!
  118. @delegate.dup.delete_if { |_, v| v.nil? }
  119. end
  120. 1 alias :to_h :to_hash
  121. # Updates the session with given Hash.
  122. #
  123. # session.to_hash
  124. # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2"}
  125. #
  126. # session.update({ "foo" => "bar" })
  127. # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2", "foo" => "bar"}
  128. #
  129. # session.to_hash
  130. # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2", "foo" => "bar"}
  131. 1 def update(hash)
  132. load_for_write!
  133. @delegate.update hash.stringify_keys
  134. end
  135. # Deletes given key from the session.
  136. 1 def delete(key)
  137. load_for_write!
  138. @delegate.delete key.to_s
  139. end
  140. # Returns value of the given key from the session, or raises +KeyError+
  141. # if can't find the given key and no default value is set.
  142. # Returns default value if specified.
  143. #
  144. # session.fetch(:foo)
  145. # # => KeyError: key not found: "foo"
  146. #
  147. # session.fetch(:foo, :bar)
  148. # # => :bar
  149. #
  150. # session.fetch(:foo) do
  151. # :bar
  152. # end
  153. # # => :bar
  154. 1 def fetch(key, default = Unspecified, &block)
  155. load_for_read!
  156. if default == Unspecified
  157. @delegate.fetch(key.to_s, &block)
  158. else
  159. @delegate.fetch(key.to_s, default, &block)
  160. end
  161. end
  162. 1 def inspect
  163. if loaded?
  164. super
  165. else
  166. "#<#{self.class}:0x#{(object_id << 1).to_s(16)} not yet loaded>"
  167. end
  168. end
  169. 1 def exists?
  170. return @exists unless @exists.nil?
  171. @exists = @by.send(:session_exists?, @req)
  172. end
  173. 1 def loaded?
  174. @loaded
  175. end
  176. 1 def empty?
  177. load_for_read!
  178. @delegate.empty?
  179. end
  180. 1 def merge!(other)
  181. load_for_write!
  182. @delegate.merge!(other)
  183. end
  184. 1 def each(&block)
  185. to_hash.each(&block)
  186. end
  187. 1 private
  188. 1 def load_for_read!
  189. load! if !loaded? && exists?
  190. end
  191. 1 def load_for_write!
  192. load! unless loaded?
  193. end
  194. 1 def load!
  195. id, session = @by.load_session @req
  196. options[:id] = id
  197. @delegate.replace(session.stringify_keys)
  198. @loaded = true
  199. end
  200. end
  201. end
  202. end

lib/action_dispatch/request/utils.rb

0.0% lines covered

65 relevant lines. 0 lines covered and 65 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/core_ext/hash/indifferent_access"
  3. module ActionDispatch
  4. class Request
  5. class Utils # :nodoc:
  6. mattr_accessor :perform_deep_munge, default: true
  7. def self.each_param_value(params, &block)
  8. case params
  9. when Array
  10. params.each { |element| each_param_value(element, &block) }
  11. when Hash
  12. params.each_value { |value| each_param_value(value, &block) }
  13. when String
  14. block.call params
  15. end
  16. end
  17. def self.normalize_encode_params(params)
  18. if perform_deep_munge
  19. NoNilParamEncoder.normalize_encode_params params
  20. else
  21. ParamEncoder.normalize_encode_params params
  22. end
  23. end
  24. def self.check_param_encoding(params)
  25. case params
  26. when Array
  27. params.each { |element| check_param_encoding(element) }
  28. when Hash
  29. params.each_value { |value| check_param_encoding(value) }
  30. when String
  31. unless params.valid_encoding?
  32. # Raise Rack::Utils::InvalidParameterError for consistency with Rack.
  33. # ActionDispatch::Request#GET will re-raise as a BadRequest error.
  34. raise Rack::Utils::InvalidParameterError, "Invalid encoding for parameter: #{params.scrub}"
  35. end
  36. end
  37. end
  38. class ParamEncoder # :nodoc:
  39. # Convert nested Hash to HashWithIndifferentAccess.
  40. def self.normalize_encode_params(params)
  41. case params
  42. when Array
  43. handle_array params
  44. when Hash
  45. if params.has_key?(:tempfile)
  46. ActionDispatch::Http::UploadedFile.new(params)
  47. else
  48. params.transform_values do |val|
  49. normalize_encode_params(val)
  50. end.with_indifferent_access
  51. end
  52. else
  53. params
  54. end
  55. end
  56. def self.handle_array(params)
  57. params.map! { |el| normalize_encode_params(el) }
  58. end
  59. end
  60. # Remove nils from the params hash.
  61. class NoNilParamEncoder < ParamEncoder # :nodoc:
  62. def self.handle_array(params)
  63. list = super
  64. list.compact!
  65. list
  66. end
  67. end
  68. end
  69. end
  70. end

lib/action_dispatch/routing.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/string/filters"
  3. 1 module ActionDispatch
  4. # The routing module provides URL rewriting in native Ruby. It's a way to
  5. # redirect incoming requests to controllers and actions. This replaces
  6. # mod_rewrite rules. Best of all, Rails' \Routing works with any web server.
  7. # Routes are defined in <tt>config/routes.rb</tt>.
  8. #
  9. # Think of creating routes as drawing a map for your requests. The map tells
  10. # them where to go based on some predefined pattern:
  11. #
  12. # Rails.application.routes.draw do
  13. # Pattern 1 tells some request to go to one place
  14. # Pattern 2 tell them to go to another
  15. # ...
  16. # end
  17. #
  18. # The following symbols are special:
  19. #
  20. # :controller maps to your controller name
  21. # :action maps to an action with your controllers
  22. #
  23. # Other names simply map to a parameter as in the case of <tt>:id</tt>.
  24. #
  25. # == Resources
  26. #
  27. # Resource routing allows you to quickly declare all of the common routes
  28. # for a given resourceful controller. Instead of declaring separate routes
  29. # for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+
  30. # actions, a resourceful route declares them in a single line of code:
  31. #
  32. # resources :photos
  33. #
  34. # Sometimes, you have a resource that clients always look up without
  35. # referencing an ID. A common example, /profile always shows the profile of
  36. # the currently logged in user. In this case, you can use a singular resource
  37. # to map /profile (rather than /profile/:id) to the show action.
  38. #
  39. # resource :profile
  40. #
  41. # It's common to have resources that are logically children of other
  42. # resources:
  43. #
  44. # resources :magazines do
  45. # resources :ads
  46. # end
  47. #
  48. # You may wish to organize groups of controllers under a namespace. Most
  49. # commonly, you might group a number of administrative controllers under
  50. # an +admin+ namespace. You would place these controllers under the
  51. # <tt>app/controllers/admin</tt> directory, and you can group them together
  52. # in your router:
  53. #
  54. # namespace "admin" do
  55. # resources :posts, :comments
  56. # end
  57. #
  58. # Alternatively, you can add prefixes to your path without using a separate
  59. # directory by using +scope+. +scope+ takes additional options which
  60. # apply to all enclosed routes.
  61. #
  62. # scope path: "/cpanel", as: 'admin' do
  63. # resources :posts, :comments
  64. # end
  65. #
  66. # For more, see <tt>Routing::Mapper::Resources#resources</tt>,
  67. # <tt>Routing::Mapper::Scoping#namespace</tt>, and
  68. # <tt>Routing::Mapper::Scoping#scope</tt>.
  69. #
  70. # == Non-resourceful routes
  71. #
  72. # For routes that don't fit the <tt>resources</tt> mold, you can use the HTTP helper
  73. # methods <tt>get</tt>, <tt>post</tt>, <tt>patch</tt>, <tt>put</tt> and <tt>delete</tt>.
  74. #
  75. # get 'post/:id', to: 'posts#show'
  76. # post 'post/:id', to: 'posts#create_comment'
  77. #
  78. # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same
  79. # URL will route to the <tt>show</tt> action.
  80. #
  81. # If your route needs to respond to more than one HTTP method (or all methods) then using the
  82. # <tt>:via</tt> option on <tt>match</tt> is preferable.
  83. #
  84. # match 'post/:id', to: 'posts#show', via: [:get, :post]
  85. #
  86. # == Named routes
  87. #
  88. # Routes can be named by passing an <tt>:as</tt> option,
  89. # allowing for easy reference within your source as +name_of_route_url+
  90. # for the full URL and +name_of_route_path+ for the URI path.
  91. #
  92. # Example:
  93. #
  94. # # In config/routes.rb
  95. # get '/login', to: 'accounts#login', as: 'login'
  96. #
  97. # # With render, redirect_to, tests, etc.
  98. # redirect_to login_url
  99. #
  100. # Arguments can be passed as well.
  101. #
  102. # redirect_to show_item_path(id: 25)
  103. #
  104. # Use <tt>root</tt> as a shorthand to name a route for the root path "/".
  105. #
  106. # # In config/routes.rb
  107. # root to: 'blogs#index'
  108. #
  109. # # would recognize http://www.example.com/ as
  110. # params = { controller: 'blogs', action: 'index' }
  111. #
  112. # # and provide these named routes
  113. # root_url # => 'http://www.example.com/'
  114. # root_path # => '/'
  115. #
  116. # Note: when using +controller+, the route is simply named after the
  117. # method you call on the block parameter rather than map.
  118. #
  119. # # In config/routes.rb
  120. # controller :blog do
  121. # get 'blog/show', to: :list
  122. # get 'blog/delete', to: :delete
  123. # get 'blog/edit', to: :edit
  124. # end
  125. #
  126. # # provides named routes for show, delete, and edit
  127. # link_to @article.title, blog_show_path(id: @article.id)
  128. #
  129. # == Pretty URLs
  130. #
  131. # Routes can generate pretty URLs. For example:
  132. #
  133. # get '/articles/:year/:month/:day', to: 'articles#find_by_id', constraints: {
  134. # year: /\d{4}/,
  135. # month: /\d{1,2}/,
  136. # day: /\d{1,2}/
  137. # }
  138. #
  139. # Using the route above, the URL "http://localhost:3000/articles/2005/11/06"
  140. # maps to
  141. #
  142. # params = {year: '2005', month: '11', day: '06'}
  143. #
  144. # == Regular Expressions and parameters
  145. # You can specify a regular expression to define a format for a parameter.
  146. #
  147. # controller 'geocode' do
  148. # get 'geocode/:postalcode', to: :show, constraints: {
  149. # postalcode: /\d{5}(-\d{4})?/
  150. # }
  151. # end
  152. #
  153. # Constraints can include the 'ignorecase' and 'extended syntax' regular
  154. # expression modifiers:
  155. #
  156. # controller 'geocode' do
  157. # get 'geocode/:postalcode', to: :show, constraints: {
  158. # postalcode: /hx\d\d\s\d[a-z]{2}/i
  159. # }
  160. # end
  161. #
  162. # controller 'geocode' do
  163. # get 'geocode/:postalcode', to: :show, constraints: {
  164. # postalcode: /# Postalcode format
  165. # \d{5} #Prefix
  166. # (-\d{4})? #Suffix
  167. # /x
  168. # }
  169. # end
  170. #
  171. # Using the multiline modifier will raise an +ArgumentError+.
  172. # Encoding regular expression modifiers are silently ignored. The
  173. # match will always use the default encoding or ASCII.
  174. #
  175. # == External redirects
  176. #
  177. # You can redirect any path to another path using the redirect helper in your router:
  178. #
  179. # get "/stories", to: redirect("/posts")
  180. #
  181. # == Unicode character routes
  182. #
  183. # You can specify unicode character routes in your router:
  184. #
  185. # get "こんにちは", to: "welcome#index"
  186. #
  187. # == Routing to Rack Applications
  188. #
  189. # Instead of a String, like <tt>posts#index</tt>, which corresponds to the
  190. # index action in the PostsController, you can specify any Rack application
  191. # as the endpoint for a matcher:
  192. #
  193. # get "/application.js", to: Sprockets
  194. #
  195. # == Reloading routes
  196. #
  197. # You can reload routes if you feel you must:
  198. #
  199. # Rails.application.reload_routes!
  200. #
  201. # This will clear all named routes and reload config/routes.rb if the file has been modified from
  202. # last load. To absolutely force reloading, use <tt>reload!</tt>.
  203. #
  204. # == Testing Routes
  205. #
  206. # The two main methods for testing your routes:
  207. #
  208. # === +assert_routing+
  209. #
  210. # def test_movie_route_properly_splits
  211. # opts = {controller: "plugin", action: "checkout", id: "2"}
  212. # assert_routing "plugin/checkout/2", opts
  213. # end
  214. #
  215. # +assert_routing+ lets you test whether or not the route properly resolves into options.
  216. #
  217. # === +assert_recognizes+
  218. #
  219. # def test_route_has_options
  220. # opts = {controller: "plugin", action: "show", id: "12"}
  221. # assert_recognizes opts, "/plugins/show/12"
  222. # end
  223. #
  224. # Note the subtle difference between the two: +assert_routing+ tests that
  225. # a URL fits options while +assert_recognizes+ tests that a URL
  226. # breaks into parameters properly.
  227. #
  228. # In tests you can simply pass the URL or named route to +get+ or +post+.
  229. #
  230. # def send_to_jail
  231. # get '/jail'
  232. # assert_response :success
  233. # end
  234. #
  235. # def goes_to_login
  236. # get login_url
  237. # #...
  238. # end
  239. #
  240. # == View a list of all your routes
  241. #
  242. # rails routes
  243. #
  244. # Target a specific controller with <tt>-c</tt>, or grep routes
  245. # using <tt>-g</tt>. Useful in conjunction with <tt>--expanded</tt>
  246. # which displays routes vertically.
  247. 1 module Routing
  248. 1 extend ActiveSupport::Autoload
  249. 1 autoload :Mapper
  250. 1 autoload :RouteSet
  251. 1 autoload :RoutesProxy
  252. 1 autoload :UrlFor
  253. 1 autoload :PolymorphicRoutes
  254. 1 SEPARATORS = %w( / . ? ) #:nodoc:
  255. 1 HTTP_METHODS = [:get, :head, :post, :patch, :put, :delete, :options] #:nodoc:
  256. end
  257. end

lib/action_dispatch/routing/endpoint.rb

90.0% lines covered

10 relevant lines. 9 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module Routing
  4. 1 class Endpoint # :nodoc:
  5. 1 def dispatcher?; false; end
  6. 1 def redirect?; false; end
  7. 1 def matches?(req); true; end
  8. 1 def app; self; end
  9. 1 def rack_app; app; end
  10. 1 def engine?
  11. rack_app.is_a?(Class) && rack_app < Rails::Engine
  12. end
  13. end
  14. end
  15. end

lib/action_dispatch/routing/inspector.rb

39.26% lines covered

135 relevant lines. 53 lines covered and 82 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "delegate"
  3. 1 require "io/console/size"
  4. 1 module ActionDispatch
  5. 1 module Routing
  6. 1 class RouteWrapper < SimpleDelegator
  7. 1 def endpoint
  8. app.dispatcher? ? "#{controller}##{action}" : rack_app.inspect
  9. end
  10. 1 def constraints
  11. requirements.except(:controller, :action)
  12. end
  13. 1 def rack_app
  14. app.rack_app
  15. end
  16. 1 def path
  17. super.spec.to_s
  18. end
  19. 1 def name
  20. super.to_s
  21. end
  22. 1 def reqs
  23. @reqs ||= begin
  24. reqs = endpoint
  25. reqs += " #{constraints}" unless constraints.empty?
  26. reqs
  27. end
  28. end
  29. 1 def controller
  30. parts.include?(:controller) ? ":controller" : requirements[:controller]
  31. end
  32. 1 def action
  33. parts.include?(:action) ? ":action" : requirements[:action]
  34. end
  35. 1 def internal?
  36. internal
  37. end
  38. 1 def engine?
  39. app.engine?
  40. end
  41. end
  42. ##
  43. # This class is just used for displaying route information when someone
  44. # executes `bin/rails routes` or looks at the RoutingError page.
  45. # People should not use this class.
  46. 1 class RoutesInspector # :nodoc:
  47. 1 def initialize(routes)
  48. @engines = {}
  49. @routes = routes
  50. end
  51. 1 def format(formatter, filter = {})
  52. routes_to_display = filter_routes(normalize_filter(filter))
  53. routes = collect_routes(routes_to_display)
  54. if routes.none?
  55. formatter.no_routes(collect_routes(@routes), filter)
  56. return formatter.result
  57. end
  58. formatter.header routes
  59. formatter.section routes
  60. @engines.each do |name, engine_routes|
  61. formatter.section_title "Routes for #{name}"
  62. formatter.section engine_routes
  63. end
  64. formatter.result
  65. end
  66. 1 private
  67. 1 def normalize_filter(filter)
  68. if filter[:controller]
  69. { controller: /#{filter[:controller].underscore.sub(/_?controller\z/, "")}/ }
  70. elsif filter[:grep]
  71. { controller: /#{filter[:grep]}/, action: /#{filter[:grep]}/,
  72. verb: /#{filter[:grep]}/, name: /#{filter[:grep]}/, path: /#{filter[:grep]}/ }
  73. end
  74. end
  75. 1 def filter_routes(filter)
  76. if filter
  77. @routes.select do |route|
  78. route_wrapper = RouteWrapper.new(route)
  79. filter.any? { |default, value| value.match?(route_wrapper.send(default)) }
  80. end
  81. else
  82. @routes
  83. end
  84. end
  85. 1 def collect_routes(routes)
  86. routes.collect do |route|
  87. RouteWrapper.new(route)
  88. end.reject(&:internal?).collect do |route|
  89. collect_engine_routes(route)
  90. { name: route.name,
  91. verb: route.verb,
  92. path: route.path,
  93. reqs: route.reqs }
  94. end
  95. end
  96. 1 def collect_engine_routes(route)
  97. name = route.endpoint
  98. return unless route.engine?
  99. return if @engines[name]
  100. routes = route.rack_app.routes
  101. if routes.is_a?(ActionDispatch::Routing::RouteSet)
  102. @engines[name] = collect_routes(routes.routes)
  103. end
  104. end
  105. end
  106. 1 module ConsoleFormatter
  107. 1 class Base
  108. 1 def initialize
  109. @buffer = []
  110. end
  111. 1 def result
  112. @buffer.join("\n")
  113. end
  114. 1 def section_title(title)
  115. end
  116. 1 def section(routes)
  117. end
  118. 1 def header(routes)
  119. end
  120. 1 def no_routes(routes, filter)
  121. @buffer <<
  122. if routes.none?
  123. <<~MESSAGE
  124. You don't have any routes defined!
  125. Please add some routes in config/routes.rb.
  126. MESSAGE
  127. elsif filter.key?(:controller)
  128. "No routes were found for this controller."
  129. elsif filter.key?(:grep)
  130. "No routes were found for this grep pattern."
  131. end
  132. @buffer << "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html."
  133. end
  134. end
  135. 1 class Sheet < Base
  136. 1 def section_title(title)
  137. @buffer << "\n#{title}:"
  138. end
  139. 1 def section(routes)
  140. @buffer << draw_section(routes)
  141. end
  142. 1 def header(routes)
  143. @buffer << draw_header(routes)
  144. end
  145. 1 private
  146. 1 def draw_section(routes)
  147. header_lengths = ["Prefix", "Verb", "URI Pattern"].map(&:length)
  148. name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max)
  149. routes.map do |r|
  150. "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
  151. end
  152. end
  153. 1 def draw_header(routes)
  154. name_width, verb_width, path_width = widths(routes)
  155. "#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action"
  156. end
  157. 1 def widths(routes)
  158. [routes.map { |r| r[:name].length }.max || 0,
  159. routes.map { |r| r[:verb].length }.max || 0,
  160. routes.map { |r| r[:path].length }.max || 0]
  161. end
  162. end
  163. 1 class Expanded < Base
  164. 1 def initialize(width: IO.console_size[1])
  165. @width = width
  166. super()
  167. end
  168. 1 def section_title(title)
  169. @buffer << "\n#{"[ #{title} ]"}"
  170. end
  171. 1 def section(routes)
  172. @buffer << draw_expanded_section(routes)
  173. end
  174. 1 private
  175. 1 def draw_expanded_section(routes)
  176. routes.map.each_with_index do |r, i|
  177. <<~MESSAGE.chomp
  178. #{route_header(index: i + 1)}
  179. Prefix | #{r[:name]}
  180. Verb | #{r[:verb]}
  181. URI | #{r[:path]}
  182. Controller#Action | #{r[:reqs]}
  183. MESSAGE
  184. end
  185. end
  186. 1 def route_header(index:)
  187. "--[ Route #{index} ]".ljust(@width, "-")
  188. end
  189. end
  190. end
  191. 1 class HtmlTableFormatter
  192. 1 def initialize(view)
  193. @view = view
  194. @buffer = []
  195. end
  196. 1 def section_title(title)
  197. @buffer << %(<tr><th colspan="4">#{title}</th></tr>)
  198. end
  199. 1 def section(routes)
  200. @buffer << @view.render(partial: "routes/route", collection: routes)
  201. end
  202. # The header is part of the HTML page, so we don't construct it here.
  203. 1 def header(routes)
  204. end
  205. 1 def no_routes(*)
  206. @buffer << <<~MESSAGE
  207. <p>You don't have any routes defined!</p>
  208. <ul>
  209. <li>Please add some routes in <tt>config/routes.rb</tt>.</li>
  210. <li>
  211. For more information about routes, please see the Rails guide
  212. <a href="https://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>.
  213. </li>
  214. </ul>
  215. MESSAGE
  216. end
  217. 1 def result
  218. @view.raw @view.render(layout: "routes/table") {
  219. @view.raw @buffer.join("\n")
  220. }
  221. end
  222. end
  223. end
  224. end

lib/action_dispatch/routing/mapper.rb

86.29% lines covered

795 relevant lines. 686 lines covered and 109 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/hash/slice"
  3. 1 require "active_support/core_ext/enumerable"
  4. 1 require "active_support/core_ext/array/extract_options"
  5. 1 require "active_support/core_ext/regexp"
  6. 1 require "active_support/core_ext/symbol/starts_ends_with"
  7. 1 require "action_dispatch/routing/redirection"
  8. 1 require "action_dispatch/routing/endpoint"
  9. 1 module ActionDispatch
  10. 1 module Routing
  11. 1 class Mapper
  12. 1 URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
  13. 1 class Constraints < Routing::Endpoint #:nodoc:
  14. 1 attr_reader :app, :constraints
  15. 1 SERVE = ->(app, req) { app.serve req }
  16. 1 CALL = ->(app, req) { app.call req.env }
  17. 1 def initialize(app, constraints, strategy)
  18. # Unwrap Constraints objects. I don't actually think it's possible
  19. # to pass a Constraints object to this constructor, but there were
  20. # multiple places that kept testing children of this object. I
  21. # *think* they were just being defensive, but I have no idea.
  22. 91 if app.is_a?(self.class)
  23. constraints += app.constraints
  24. app = app.app
  25. end
  26. 91 @strategy = strategy
  27. 91 @app, @constraints, = app, constraints
  28. end
  29. 1 def dispatcher?; @strategy == SERVE; end
  30. 1 def matches?(req)
  31. @constraints.all? do |constraint|
  32. (constraint.respond_to?(:matches?) && constraint.matches?(req)) ||
  33. (constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req)))
  34. end
  35. end
  36. 1 def serve(req)
  37. return [ 404, { "X-Cascade" => "pass" }, [] ] unless matches?(req)
  38. @strategy.call @app, req
  39. end
  40. 1 private
  41. 1 def constraint_args(constraint, request)
  42. arity = if constraint.respond_to?(:arity)
  43. constraint.arity
  44. else
  45. constraint.method(:call).arity
  46. end
  47. if arity < 1
  48. []
  49. elsif arity == 1
  50. [request]
  51. else
  52. [request.path_parameters, request]
  53. end
  54. end
  55. end
  56. 1 class Mapping #:nodoc:
  57. 1 ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
  58. 1 OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z}
  59. 1 attr_reader :requirements, :defaults, :to, :default_controller,
  60. :default_action, :required_defaults, :ast, :scope_options
  61. 1 def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
  62. 326 scope_params = {
  63. blocks: scope[:blocks] || [],
  64. constraints: scope[:constraints] || {},
  65. 326 defaults: (scope[:defaults] || {}).dup,
  66. module: scope[:module],
  67. options: scope[:options] || {}
  68. }
  69. 326 new set: set, ast: ast, controller: controller, default_action: default_action,
  70. to: to, formatted: formatted, via: via, options_constraints: options_constraints,
  71. anchor: anchor, scope_params: scope_params, options: scope_params[:options].merge(options)
  72. end
  73. 1 def self.check_via(via)
  74. 326 if via.empty?
  75. msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
  76. "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
  77. "If you want to expose your action to GET, use `get` in the router:\n" \
  78. " Instead of: match \"controller#action\"\n" \
  79. " Do: get \"controller#action\""
  80. raise ArgumentError, msg
  81. end
  82. 326 via
  83. end
  84. 1 def self.normalize_path(path, format)
  85. 326 path = Mapper.normalize_path(path)
  86. 326 if format == true
  87. 1 "#{path}.:format"
  88. 325 elsif optional_format?(path, format)
  89. 292 "#{path}(.:format)"
  90. else
  91. 33 path
  92. end
  93. end
  94. 1 def self.optional_format?(path, format)
  95. 325 format != false && !path.match?(OPTIONAL_FORMAT_REGEX)
  96. end
  97. 1 def initialize(set:, ast:, controller:, default_action:, to:, formatted:, via:, options_constraints:, anchor:, scope_params:, options:)
  98. 326 @defaults = scope_params[:defaults]
  99. 326 @set = set
  100. 326 @to = intern(to)
  101. 326 @default_controller = intern(controller)
  102. 326 @default_action = intern(default_action)
  103. 326 @ast = ast
  104. 326 @anchor = anchor
  105. 326 @via = via
  106. 326 @internal = options.delete(:internal)
  107. 326 @scope_options = scope_params[:options]
  108. 326 path_params = []
  109. 326 wildcard_options = {}
  110. 326 ast.each do |node|
  111. 3827 if node.symbol?
  112. 508 path_params << node.to_sym
  113. 3319 elsif formatted != false && node.star?
  114. # Add a constraint for wildcard route to make it non-greedy and match the
  115. # optional format part of the route by default.
  116. 3 wildcard_options[node.name.to_sym] ||= /.+?/
  117. 3316 elsif node.cat?
  118. 1594 alter_regex_for_custom_routes(node)
  119. end
  120. end
  121. 326 options = wildcard_options.merge!(options)
  122. 326 options = normalize_options!(options, path_params, scope_params[:module])
  123. 326 split_options = constraints(options, path_params)
  124. 326 constraints = scope_params[:constraints].merge Hash[split_options[:constraints] || []]
  125. 326 if options_constraints.is_a?(Hash)
  126. 321 @defaults = Hash[options_constraints.find_all { |key, default|
  127. 15 URL_OPTIONS.include?(key) && (String === default || Integer === default)
  128. }].merge @defaults
  129. 321 @blocks = scope_params[:blocks]
  130. 321 constraints.merge! options_constraints
  131. else
  132. 5 @blocks = blocks(options_constraints)
  133. end
  134. 326 requirements, conditions = split_constraints path_params, constraints
  135. 326 verify_regexp_requirements requirements.map(&:last).grep(Regexp)
  136. 326 formats = normalize_format(formatted)
  137. 326 @requirements = formats[:requirements].merge Hash[requirements]
  138. 326 @conditions = Hash[conditions]
  139. 326 @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))
  140. 326 if path_params.include?(:action) && !@requirements.key?(:action)
  141. 6 @defaults[:action] ||= "index"
  142. end
  143. 326 @required_defaults = (split_options[:required_defaults] || []).map(&:first)
  144. end
  145. 1 def make_route(name, precedence)
  146. 326 Journey::Route.new(name: name, app: application, path: path, constraints: conditions,
  147. required_defaults: required_defaults, defaults: defaults,
  148. request_method_match: request_method, precedence: precedence,
  149. scope_options: scope_options, internal: @internal)
  150. end
  151. 1 def application
  152. 326 app(@blocks)
  153. end
  154. 1 JOINED_SEPARATORS = SEPARATORS.join # :nodoc:
  155. 1 def path
  156. 326 Journey::Path::Pattern.new(@ast, requirements, JOINED_SEPARATORS, @anchor)
  157. end
  158. 1 def conditions
  159. 326 build_conditions @conditions, @set.request_class
  160. end
  161. 1 def build_conditions(current_conditions, request_class)
  162. 326 conditions = current_conditions.dup
  163. 326 conditions.keep_if do |k, _|
  164. 10 request_class.public_method_defined?(k)
  165. end
  166. end
  167. 1 private :build_conditions
  168. 1 def request_method
  169. 652 @via.map { |x| Journey::Route.verb_matcher(x) }
  170. end
  171. 1 private :request_method
  172. 1 private
  173. # Find all the symbol nodes that are adjacent to literal nodes and alter
  174. # the regexp so that Journey will partition them into custom routes.
  175. 1 def alter_regex_for_custom_routes(node)
  176. 1594 if node.left.literal? && node.right.symbol?
  177. 2 symbol = node.right
  178. 1592 elsif node.left.literal? && node.right.cat? && node.right.left.symbol?
  179. 2 symbol = node.right.left
  180. 1590 elsif node.left.symbol? && node.right.literal?
  181. 2 symbol = node.left
  182. 1588 elsif node.left.symbol? && node.right.cat? && node.right.left.literal?
  183. 2 symbol = node.left
  184. end
  185. 1594 if symbol
  186. 8 symbol.regexp = /(?:#{Regexp.union(symbol.regexp, '-')})+/
  187. end
  188. end
  189. 1 def intern(object)
  190. 978 object.is_a?(String) ? -object : object
  191. end
  192. 1 def normalize_options!(options, path_params, modyoule)
  193. 326 if path_params.include?(:controller)
  194. 5 raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
  195. # Add a default constraint for :controller path segments that matches namespaced
  196. # controllers with default routes like :controller/:action/:id(.:format), e.g:
  197. # GET /admin/products/show/1
  198. # => { controller: 'admin/products', action: 'show', id: '1' }
  199. 5 options[:controller] ||= /.+?/
  200. end
  201. 326 if to.respond_to?(:action) || to.respond_to?(:call)
  202. 87 options
  203. else
  204. 239 to_endpoint = split_to to
  205. 239 controller = to_endpoint[0] || default_controller
  206. 239 action = to_endpoint[1] || default_action
  207. 239 controller = add_controller_module(controller, modyoule)
  208. 239 options.merge! check_controller_and_action(path_params, controller, action)
  209. end
  210. end
  211. 1 def split_constraints(path_params, constraints)
  212. 326 constraints.partition do |key, requirement|
  213. 29 path_params.include?(key) || key == :controller
  214. end
  215. end
  216. 1 def normalize_format(formatted)
  217. 326 case formatted
  218. when true
  219. 1 { requirements: { format: /.+/ },
  220. defaults: {} }
  221. when Regexp
  222. { requirements: { format: formatted },
  223. defaults: { format: nil } }
  224. when String
  225. 1 { requirements: { format: Regexp.compile(formatted) },
  226. defaults: { format: formatted } }
  227. else
  228. 324 { requirements: {}, defaults: {} }
  229. end
  230. end
  231. 1 def verify_regexp_requirements(requirements)
  232. 326 requirements.each do |requirement|
  233. 17 if ANCHOR_CHARACTERS_REGEX.match?(requirement.source)
  234. raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
  235. end
  236. 17 if requirement.multiline?
  237. raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
  238. end
  239. end
  240. end
  241. 1 def normalize_defaults(options)
  242. 816 Hash[options.reject { |_, default| Regexp === default }]
  243. end
  244. 1 def app(blocks)
  245. 326 if to.respond_to?(:action)
  246. Routing::RouteSet::StaticDispatcher.new to
  247. 326 elsif to.respond_to?(:call)
  248. 87 Constraints.new(to, blocks, Constraints::CALL)
  249. 239 elsif blocks.any?
  250. 4 Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
  251. else
  252. 235 dispatcher(defaults.key?(:controller))
  253. end
  254. end
  255. 1 def check_controller_and_action(path_params, controller, action)
  256. 239 hash = check_part(:controller, controller, path_params, {}) do |part|
  257. 234 translate_controller(part) {
  258. message = +"'#{part}' is not a supported controller name. This can lead to potential routing problems."
  259. message << " See https://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
  260. raise ArgumentError, message
  261. }
  262. end
  263. 239 check_part(:action, action, path_params, hash) { |part|
  264. 234 part.is_a?(Regexp) ? part : part.to_s
  265. }
  266. end
  267. 1 def check_part(name, part, path_params, hash)
  268. 478 if part
  269. 468 hash[name] = yield(part)
  270. else
  271. 10 unless path_params.include?(name)
  272. message = "Missing :#{name} key on routes definition, please check your routes."
  273. raise ArgumentError, message
  274. end
  275. end
  276. 478 hash
  277. end
  278. 1 def split_to(to)
  279. 239 if /#/.match?(to)
  280. 75 to.split("#").map!(&:-@)
  281. else
  282. 164 []
  283. end
  284. end
  285. 1 def add_controller_module(controller, modyoule)
  286. 239 if modyoule && !controller.is_a?(Regexp)
  287. 36 if controller&.start_with?("/")
  288. -controller[1..-1]
  289. else
  290. 36 -[modyoule, controller].compact.join("/")
  291. end
  292. else
  293. 203 controller
  294. end
  295. end
  296. 1 def translate_controller(controller)
  297. 234 return controller if Regexp === controller
  298. 234 return controller.to_s if /\A[a-z_0-9][a-z_0-9\/]*\z/.match?(controller)
  299. yield
  300. end
  301. 1 def blocks(callable_constraint)
  302. 5 unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
  303. raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
  304. end
  305. 5 [callable_constraint]
  306. end
  307. 1 def constraints(options, path_params)
  308. 326 options.group_by do |key, option|
  309. 490 if Regexp === option
  310. 13 :constraints
  311. else
  312. 477 if path_params.include?(key)
  313. 1 :path_params
  314. else
  315. 476 :required_defaults
  316. end
  317. end
  318. end
  319. end
  320. 1 def dispatcher(raise_on_name_error)
  321. 239 Routing::RouteSet::Dispatcher.new raise_on_name_error
  322. end
  323. end
  324. # Invokes Journey::Router::Utils.normalize_path, then ensures that
  325. # /(:locale) becomes (/:locale). Except for root cases, where the
  326. # former is the correct one.
  327. 1 def self.normalize_path(path)
  328. 574 path = Journey::Router::Utils.normalize_path(path)
  329. # the path for a root URL at this point can be something like
  330. # "/(/:locale)(/:platform)/(:browser)", and we would want
  331. # "/(:locale)(/:platform)(/:browser)"
  332. # reverse "/(", "/((" etc to "(/", "((/" etc
  333. 574 path.gsub!(%r{/(\(+)/?}, '\1/')
  334. # if a path is all optional segments, change the leading "(/" back to
  335. # "/(" so it evaluates to "/" when interpreted with no options.
  336. # Unless, however, at least one secondary segment consists of a static
  337. # part, ex. "(/:locale)(/pages/:page)"
  338. 574 path.sub!(%r{^(\(+)/}, '/\1') if %r{^(\(+[^)]+\))(\(+/:[^)]+\))*$}.match?(path)
  339. 574 path
  340. end
  341. 1 def self.normalize_name(name)
  342. 158 normalize_path(name)[1..-1].tr("/", "_")
  343. end
  344. 1 module Base
  345. # Matches a URL pattern to one or more routes.
  346. #
  347. # You should not use the +match+ method in your router
  348. # without specifying an HTTP method.
  349. #
  350. # If you want to expose your action to both GET and POST, use:
  351. #
  352. # # sets :controller, :action and :id in params
  353. # match ':controller/:action/:id', via: [:get, :post]
  354. #
  355. # Note that +:controller+, +:action+ and +:id+ are interpreted as URL
  356. # query parameters and thus available through +params+ in an action.
  357. #
  358. # If you want to expose your action to GET, use +get+ in the router:
  359. #
  360. # Instead of:
  361. #
  362. # match ":controller/:action/:id"
  363. #
  364. # Do:
  365. #
  366. # get ":controller/:action/:id"
  367. #
  368. # Two of these symbols are special, +:controller+ maps to the controller
  369. # and +:action+ to the controller's action. A pattern can also map
  370. # wildcard segments (globs) to params:
  371. #
  372. # get 'songs/*category/:title', to: 'songs#show'
  373. #
  374. # # 'songs/rock/classic/stairway-to-heaven' sets
  375. # # params[:category] = 'rock/classic'
  376. # # params[:title] = 'stairway-to-heaven'
  377. #
  378. # To match a wildcard parameter, it must have a name assigned to it.
  379. # Without a variable name to attach the glob parameter to, the route
  380. # can't be parsed.
  381. #
  382. # When a pattern points to an internal route, the route's +:action+ and
  383. # +:controller+ should be set in options or hash shorthand. Examples:
  384. #
  385. # match 'photos/:id' => 'photos#show', via: :get
  386. # match 'photos/:id', to: 'photos#show', via: :get
  387. # match 'photos/:id', controller: 'photos', action: 'show', via: :get
  388. #
  389. # A pattern can also point to a +Rack+ endpoint i.e. anything that
  390. # responds to +call+:
  391. #
  392. # match 'photos/:id', to: -> (hash) { [200, {}, ["Coming soon"]] }, via: :get
  393. # match 'photos/:id', to: PhotoRackApp, via: :get
  394. # # Yes, controller actions are just rack endpoints
  395. # match 'photos/:id', to: PhotosController.action(:show), via: :get
  396. #
  397. # Because requesting various HTTP verbs with a single action has security
  398. # implications, you must either specify the actions in
  399. # the via options or use one of the HttpHelpers[rdoc-ref:HttpHelpers]
  400. # instead +match+
  401. #
  402. # === Options
  403. #
  404. # Any options not seen here are passed on as params with the URL.
  405. #
  406. # [:controller]
  407. # The route's controller.
  408. #
  409. # [:action]
  410. # The route's action.
  411. #
  412. # [:param]
  413. # Overrides the default resource identifier +:id+ (name of the
  414. # dynamic segment used to generate the routes).
  415. # You can access that segment from your controller using
  416. # <tt>params[<:param>]</tt>.
  417. # In your router:
  418. #
  419. # resources :users, param: :name
  420. #
  421. # The +users+ resource here will have the following routes generated for it:
  422. #
  423. # GET /users(.:format)
  424. # POST /users(.:format)
  425. # GET /users/new(.:format)
  426. # GET /users/:name/edit(.:format)
  427. # GET /users/:name(.:format)
  428. # PATCH/PUT /users/:name(.:format)
  429. # DELETE /users/:name(.:format)
  430. #
  431. # You can override <tt>ActiveRecord::Base#to_param</tt> of a related
  432. # model to construct a URL:
  433. #
  434. # class User < ActiveRecord::Base
  435. # def to_param
  436. # name
  437. # end
  438. # end
  439. #
  440. # user = User.find_by(name: 'Phusion')
  441. # user_path(user) # => "/users/Phusion"
  442. #
  443. # [:path]
  444. # The path prefix for the routes.
  445. #
  446. # [:module]
  447. # The namespace for :controller.
  448. #
  449. # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get
  450. # # => Sekret::PostsController
  451. #
  452. # See <tt>Scoping#namespace</tt> for its scope equivalent.
  453. #
  454. # [:as]
  455. # The name used to generate routing helpers.
  456. #
  457. # [:via]
  458. # Allowed HTTP verb(s) for route.
  459. #
  460. # match 'path', to: 'c#a', via: :get
  461. # match 'path', to: 'c#a', via: [:get, :post]
  462. # match 'path', to: 'c#a', via: :all
  463. #
  464. # [:to]
  465. # Points to a +Rack+ endpoint. Can be an object that responds to
  466. # +call+ or a string representing a controller's action.
  467. #
  468. # match 'path', to: 'controller#action', via: :get
  469. # match 'path', to: -> (env) { [200, {}, ["Success!"]] }, via: :get
  470. # match 'path', to: RackApp, via: :get
  471. #
  472. # [:on]
  473. # Shorthand for wrapping routes in a specific RESTful context. Valid
  474. # values are +:member+, +:collection+, and +:new+. Only use within
  475. # <tt>resource(s)</tt> block. For example:
  476. #
  477. # resource :bar do
  478. # match 'foo', to: 'c#a', on: :member, via: [:get, :post]
  479. # end
  480. #
  481. # Is equivalent to:
  482. #
  483. # resource :bar do
  484. # member do
  485. # match 'foo', to: 'c#a', via: [:get, :post]
  486. # end
  487. # end
  488. #
  489. # [:constraints]
  490. # Constrains parameters with a hash of regular expressions
  491. # or an object that responds to <tt>matches?</tt>. In addition, constraints
  492. # other than path can also be specified with any object
  493. # that responds to <tt>===</tt> (e.g. String, Array, Range, etc.).
  494. #
  495. # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get
  496. #
  497. # match 'json_only', constraints: { format: 'json' }, via: :get
  498. #
  499. # class PermitList
  500. # def matches?(request) request.remote_ip == '1.2.3.4' end
  501. # end
  502. # match 'path', to: 'c#a', constraints: PermitList.new, via: :get
  503. #
  504. # See <tt>Scoping#constraints</tt> for more examples with its scope
  505. # equivalent.
  506. #
  507. # [:defaults]
  508. # Sets defaults for parameters
  509. #
  510. # # Sets params[:format] to 'jpg' by default
  511. # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get
  512. #
  513. # See <tt>Scoping#defaults</tt> for its scope equivalent.
  514. #
  515. # [:anchor]
  516. # Boolean to anchor a <tt>match</tt> pattern. Default is true. When set to
  517. # false, the pattern matches any request prefixed with the given path.
  518. #
  519. # # Matches any request starting with 'path'
  520. # match 'path', to: 'c#a', anchor: false, via: :get
  521. #
  522. # [:format]
  523. # Allows you to specify the default value for optional +format+
  524. # segment or disable it by supplying +false+.
  525. 1 def match(path, options = nil)
  526. end
  527. # Mount a Rack-based application to be used within the application.
  528. #
  529. # mount SomeRackApp, at: "some_route"
  530. #
  531. # Alternatively:
  532. #
  533. # mount(SomeRackApp => "some_route")
  534. #
  535. # For options, see +match+, as +mount+ uses it internally.
  536. #
  537. # All mounted applications come with routing helpers to access them.
  538. # These are named after the class specified, so for the above example
  539. # the helper is either +some_rack_app_path+ or +some_rack_app_url+.
  540. # To customize this helper's name, use the +:as+ option:
  541. #
  542. # mount(SomeRackApp => "some_route", as: "exciting")
  543. #
  544. # This will generate the +exciting_path+ and +exciting_url+ helpers
  545. # which can be used to navigate to this mounted app.
  546. 1 def mount(app, options = nil)
  547. 14 if options
  548. 10 path = options.delete(:at)
  549. 4 elsif Hash === app
  550. 4 options = app
  551. 8 app, path = options.find { |k, _| k.respond_to?(:call) }
  552. 4 options.delete(app) if app
  553. end
  554. 14 raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
  555. 14 raise ArgumentError, <<~MSG unless path
  556. Must be called with mount point
  557. mount SomeRackApp, at: "some_route"
  558. or
  559. mount(SomeRackApp => "some_route")
  560. MSG
  561. 14 rails_app = rails_app? app
  562. 14 options[:as] ||= app_name(app, rails_app)
  563. 14 target_as = name_for_action(options[:as], path)
  564. 14 options[:via] ||= :all
  565. 14 match(path, options.merge(to: app, anchor: false, format: false))
  566. 14 define_generate_prefix(app, target_as) if rails_app
  567. 14 self
  568. end
  569. 1 def default_url_options=(options)
  570. 3 @set.default_url_options = options
  571. end
  572. 1 alias_method :default_url_options, :default_url_options=
  573. 1 def with_default_scope(scope, &block)
  574. 2 scope(scope) do
  575. 2 instance_exec(&block)
  576. end
  577. end
  578. # Query if the following named route was already defined.
  579. 1 def has_named_route?(name)
  580. 233 @set.named_routes.key?(name)
  581. end
  582. 1 private
  583. 1 def rails_app?(app)
  584. 14 app.is_a?(Class) && app < Rails::Railtie
  585. end
  586. 1 def app_name(app, rails_app)
  587. 8 if rails_app
  588. app.railtie_name
  589. 8 elsif app.is_a?(Class)
  590. 2 class_name = app.name
  591. 2 ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
  592. end
  593. end
  594. 1 def define_generate_prefix(app, name)
  595. 3 _route = @set.named_routes.get name
  596. 3 _routes = @set
  597. 3 _url_helpers = @set.url_helpers
  598. 3 script_namer = ->(options) do
  599. prefix_options = options.slice(*_route.segment_keys)
  600. prefix_options[:relative_url_root] = ""
  601. if options[:_recall]
  602. prefix_options.reverse_merge!(options[:_recall].slice(*_route.segment_keys))
  603. end
  604. # We must actually delete prefix segment keys to avoid passing them to next url_for.
  605. _route.segment_keys.each { |k| options.delete(k) }
  606. _url_helpers.send("#{name}_path", prefix_options)
  607. end
  608. 3 app.routes.define_mounted_helper(name, script_namer)
  609. 3 app.routes.extend Module.new {
  610. 3 def optimize_routes_generation?; false; end
  611. 3 define_method :find_script_name do |options|
  612. if options.key? :script_name
  613. super(options)
  614. else
  615. script_namer.call(options)
  616. end
  617. end
  618. }
  619. end
  620. end
  621. 1 module HttpHelpers
  622. # Define a route that only recognizes HTTP GET.
  623. # For supported arguments, see match[rdoc-ref:Base#match]
  624. #
  625. # get 'bacon', to: 'food#bacon'
  626. 1 def get(*args, &block)
  627. 228 map_method(:get, args, &block)
  628. end
  629. # Define a route that only recognizes HTTP POST.
  630. # For supported arguments, see match[rdoc-ref:Base#match]
  631. #
  632. # post 'bacon', to: 'food#bacon'
  633. 1 def post(*args, &block)
  634. 21 map_method(:post, args, &block)
  635. end
  636. # Define a route that only recognizes HTTP PATCH.
  637. # For supported arguments, see match[rdoc-ref:Base#match]
  638. #
  639. # patch 'bacon', to: 'food#bacon'
  640. 1 def patch(*args, &block)
  641. 20 map_method(:patch, args, &block)
  642. end
  643. # Define a route that only recognizes HTTP PUT.
  644. # For supported arguments, see match[rdoc-ref:Base#match]
  645. #
  646. # put 'bacon', to: 'food#bacon'
  647. 1 def put(*args, &block)
  648. 20 map_method(:put, args, &block)
  649. end
  650. # Define a route that only recognizes HTTP DELETE.
  651. # For supported arguments, see match[rdoc-ref:Base#match]
  652. #
  653. # delete 'broccoli', to: 'food#broccoli'
  654. 1 def delete(*args, &block)
  655. 19 map_method(:delete, args, &block)
  656. end
  657. # Define a route that only recognizes HTTP OPTIONS.
  658. # For supported arguments, see match[rdoc-ref:Base#match]
  659. #
  660. # options 'carrots', to: 'food#carrots'
  661. 1 def options(*args, &block)
  662. map_method(:options, args, &block)
  663. end
  664. 1 private
  665. 1 def map_method(method, args, &block)
  666. 308 options = args.extract_options!
  667. 308 options[:via] = method
  668. 308 match(*args, options, &block)
  669. 308 self
  670. end
  671. end
  672. # You may wish to organize groups of controllers under a namespace.
  673. # Most commonly, you might group a number of administrative controllers
  674. # under an +admin+ namespace. You would place these controllers under
  675. # the <tt>app/controllers/admin</tt> directory, and you can group them
  676. # together in your router:
  677. #
  678. # namespace "admin" do
  679. # resources :posts, :comments
  680. # end
  681. #
  682. # This will create a number of routes for each of the posts and comments
  683. # controller. For <tt>Admin::PostsController</tt>, Rails will create:
  684. #
  685. # GET /admin/posts
  686. # GET /admin/posts/new
  687. # POST /admin/posts
  688. # GET /admin/posts/1
  689. # GET /admin/posts/1/edit
  690. # PATCH/PUT /admin/posts/1
  691. # DELETE /admin/posts/1
  692. #
  693. # If you want to route /posts (without the prefix /admin) to
  694. # <tt>Admin::PostsController</tt>, you could use
  695. #
  696. # scope module: "admin" do
  697. # resources :posts
  698. # end
  699. #
  700. # or, for a single case
  701. #
  702. # resources :posts, module: "admin"
  703. #
  704. # If you want to route /admin/posts to +PostsController+
  705. # (without the <tt>Admin::</tt> module prefix), you could use
  706. #
  707. # scope "/admin" do
  708. # resources :posts
  709. # end
  710. #
  711. # or, for a single case
  712. #
  713. # resources :posts, path: "/admin/posts"
  714. #
  715. # In each of these cases, the named routes remain the same as if you did
  716. # not use scope. In the last case, the following paths map to
  717. # +PostsController+:
  718. #
  719. # GET /admin/posts
  720. # GET /admin/posts/new
  721. # POST /admin/posts
  722. # GET /admin/posts/1
  723. # GET /admin/posts/1/edit
  724. # PATCH/PUT /admin/posts/1
  725. # DELETE /admin/posts/1
  726. 1 module Scoping
  727. # Scopes a set of routes to the given default options.
  728. #
  729. # Take the following route definition as an example:
  730. #
  731. # scope path: ":account_id", as: "account" do
  732. # resources :projects
  733. # end
  734. #
  735. # This generates helpers such as +account_projects_path+, just like +resources+ does.
  736. # The difference here being that the routes generated are like /:account_id/projects,
  737. # rather than /accounts/:account_id/projects.
  738. #
  739. # === Options
  740. #
  741. # Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
  742. #
  743. # # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt>
  744. # scope module: "admin" do
  745. # resources :posts
  746. # end
  747. #
  748. # # prefix the posts resource's requests with '/admin'
  749. # scope path: "/admin" do
  750. # resources :posts
  751. # end
  752. #
  753. # # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
  754. # scope as: "sekret" do
  755. # resources :posts
  756. # end
  757. 1 def scope(*args)
  758. 34 options = args.extract_options!.dup
  759. 34 scope = {}
  760. 34 options[:path] = args.flatten.join("/") if args.any?
  761. 34 options[:constraints] ||= {}
  762. 34 unless nested_scope?
  763. 24 options[:shallow_path] ||= options[:path] if options.key?(:path)
  764. 24 options[:shallow_prefix] ||= options[:as] if options.key?(:as)
  765. end
  766. 34 if options[:constraints].is_a?(Hash)
  767. 34 defaults = options[:constraints].select do |k, v|
  768. 2 URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Integer))
  769. end
  770. 34 options[:defaults] = defaults.merge(options[:defaults] || {})
  771. else
  772. block, options[:constraints] = options[:constraints], {}
  773. end
  774. 34 if options.key?(:only) || options.key?(:except)
  775. scope[:action_options] = { only: options.delete(:only),
  776. except: options.delete(:except) }
  777. end
  778. 34 if options.key? :anchor
  779. raise ArgumentError, "anchor is ignored unless passed to `match`"
  780. end
  781. 34 @scope.options.each do |option|
  782. 544 if option == :blocks
  783. 34 value = block
  784. 510 elsif option == :options
  785. 34 value = options
  786. else
  787. 838 value = options.delete(option) { POISON }
  788. end
  789. 544 unless POISON == value
  790. 182 scope[option] = send("merge_#{option}_scope", @scope[option], value)
  791. end
  792. end
  793. 34 @scope = @scope.new scope
  794. 34 yield
  795. 34 self
  796. ensure
  797. 34 @scope = @scope.parent
  798. end
  799. 1 POISON = Object.new # :nodoc:
  800. # Scopes routes to a specific controller
  801. #
  802. # controller "food" do
  803. # match "bacon", action: :bacon, via: :get
  804. # end
  805. 1 def controller(controller)
  806. 21 @scope = @scope.new(controller: controller)
  807. 21 yield
  808. ensure
  809. 21 @scope = @scope.parent
  810. end
  811. # Scopes routes to a specific namespace. For example:
  812. #
  813. # namespace :admin do
  814. # resources :posts
  815. # end
  816. #
  817. # This generates the following routes:
  818. #
  819. # admin_posts GET /admin/posts(.:format) admin/posts#index
  820. # admin_posts POST /admin/posts(.:format) admin/posts#create
  821. # new_admin_post GET /admin/posts/new(.:format) admin/posts#new
  822. # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit
  823. # admin_post GET /admin/posts/:id(.:format) admin/posts#show
  824. # admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update
  825. # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy
  826. #
  827. # === Options
  828. #
  829. # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+
  830. # options all default to the name of the namespace.
  831. #
  832. # For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
  833. # <tt>Resources#resources</tt>.
  834. #
  835. # # accessible through /sekret/posts rather than /admin/posts
  836. # namespace :admin, path: "sekret" do
  837. # resources :posts
  838. # end
  839. #
  840. # # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
  841. # namespace :admin, module: "sekret" do
  842. # resources :posts
  843. # end
  844. #
  845. # # generates +sekret_posts_path+ rather than +admin_posts_path+
  846. # namespace :admin, as: "sekret" do
  847. # resources :posts
  848. # end
  849. 1 def namespace(path, options = {})
  850. 3 path = path.to_s
  851. 3 defaults = {
  852. module: path,
  853. as: options.fetch(:as, path),
  854. shallow_path: options.fetch(:path, path),
  855. shallow_prefix: options.fetch(:as, path)
  856. }
  857. 6 path_scope(options.delete(:path) { path }) do
  858. 6 scope(defaults.merge!(options)) { yield }
  859. end
  860. end
  861. # === Parameter Restriction
  862. # Allows you to constrain the nested routes based on a set of rules.
  863. # For instance, in order to change the routes to allow for a dot character in the +id+ parameter:
  864. #
  865. # constraints(id: /\d+\.\d+/) do
  866. # resources :posts
  867. # end
  868. #
  869. # Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be.
  870. # The +id+ parameter must match the constraint passed in for this example.
  871. #
  872. # You may use this to also restrict other parameters:
  873. #
  874. # resources :posts do
  875. # constraints(post_id: /\d+\.\d+/) do
  876. # resources :comments
  877. # end
  878. # end
  879. #
  880. # === Restricting based on IP
  881. #
  882. # Routes can also be constrained to an IP or a certain range of IP addresses:
  883. #
  884. # constraints(ip: /192\.168\.\d+\.\d+/) do
  885. # resources :posts
  886. # end
  887. #
  888. # Any user connecting from the 192.168.* range will be able to see this resource,
  889. # where as any user connecting outside of this range will be told there is no such route.
  890. #
  891. # === Dynamic request matching
  892. #
  893. # Requests to routes can be constrained based on specific criteria:
  894. #
  895. # constraints(-> (req) { /iPhone/.match?(req.env["HTTP_USER_AGENT"]) }) do
  896. # resources :iphones
  897. # end
  898. #
  899. # You are able to move this logic out into a class if it is too complex for routes.
  900. # This class must have a +matches?+ method defined on it which either returns +true+
  901. # if the user should be given access to that route, or +false+ if the user should not.
  902. #
  903. # class Iphone
  904. # def self.matches?(request)
  905. # /iPhone/.match?(request.env["HTTP_USER_AGENT"])
  906. # end
  907. # end
  908. #
  909. # An expected place for this code would be +lib/constraints+.
  910. #
  911. # This class is then used like this:
  912. #
  913. # constraints(Iphone) do
  914. # resources :iphones
  915. # end
  916. 1 def constraints(constraints = {})
  917. 2 scope(constraints: constraints) { yield }
  918. end
  919. # Allows you to set default parameters for a route, such as this:
  920. # defaults id: 'home' do
  921. # match 'scoped_pages/(:id)', to: 'pages#show'
  922. # end
  923. # Using this, the +:id+ parameter here will default to 'home'.
  924. 1 def defaults(defaults = {})
  925. @scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults))
  926. yield
  927. ensure
  928. @scope = @scope.parent
  929. end
  930. 1 private
  931. 1 def merge_path_scope(parent, child)
  932. 81 Mapper.normalize_path("#{parent}/#{child}")
  933. end
  934. 1 def merge_shallow_path_scope(parent, child)
  935. 9 Mapper.normalize_path("#{parent}/#{child}")
  936. end
  937. 1 def merge_as_scope(parent, child)
  938. 13 parent ? "#{parent}_#{child}" : child
  939. end
  940. 1 def merge_shallow_prefix_scope(parent, child)
  941. 3 parent ? "#{parent}_#{child}" : child
  942. end
  943. 1 def merge_module_scope(parent, child)
  944. 14 parent ? "#{parent}/#{child}" : child
  945. end
  946. 1 def merge_controller_scope(parent, child)
  947. child
  948. end
  949. 1 def merge_action_scope(parent, child)
  950. child
  951. end
  952. 1 def merge_via_scope(parent, child)
  953. child
  954. end
  955. 1 def merge_format_scope(parent, child)
  956. 1 child
  957. end
  958. 1 def merge_path_names_scope(parent, child)
  959. merge_options_scope(parent, child)
  960. end
  961. 1 def merge_constraints_scope(parent, child)
  962. 34 merge_options_scope(parent, child)
  963. end
  964. 1 def merge_defaults_scope(parent, child)
  965. 34 merge_options_scope(parent, child)
  966. end
  967. 1 def merge_blocks_scope(parent, child)
  968. 34 merged = parent ? parent.dup : []
  969. 34 merged << child if child
  970. 34 merged
  971. end
  972. 1 def merge_options_scope(parent, child)
  973. 102 (parent || {}).merge(child)
  974. end
  975. 1 def merge_shallow_scope(parent, child)
  976. child ? true : false
  977. end
  978. 1 def merge_to_scope(parent, child)
  979. child
  980. end
  981. end
  982. # Resource routing allows you to quickly declare all of the common routes
  983. # for a given resourceful controller. Instead of declaring separate routes
  984. # for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+
  985. # actions, a resourceful route declares them in a single line of code:
  986. #
  987. # resources :photos
  988. #
  989. # Sometimes, you have a resource that clients always look up without
  990. # referencing an ID. A common example, /profile always shows the profile of
  991. # the currently logged in user. In this case, you can use a singular resource
  992. # to map /profile (rather than /profile/:id) to the show action.
  993. #
  994. # resource :profile
  995. #
  996. # It's common to have resources that are logically children of other
  997. # resources:
  998. #
  999. # resources :magazines do
  1000. # resources :ads
  1001. # end
  1002. #
  1003. # You may wish to organize groups of controllers under a namespace. Most
  1004. # commonly, you might group a number of administrative controllers under
  1005. # an +admin+ namespace. You would place these controllers under the
  1006. # <tt>app/controllers/admin</tt> directory, and you can group them together
  1007. # in your router:
  1008. #
  1009. # namespace "admin" do
  1010. # resources :posts, :comments
  1011. # end
  1012. #
  1013. # By default the +:id+ parameter doesn't accept dots. If you need to
  1014. # use dots as part of the +:id+ parameter add a constraint which
  1015. # overrides this restriction, e.g:
  1016. #
  1017. # resources :articles, id: /[^\/]+/
  1018. #
  1019. # This allows any character other than a slash as part of your +:id+.
  1020. #
  1021. 1 module Resources
  1022. # CANONICAL_ACTIONS holds all actions that does not need a prefix or
  1023. # a path appended since they fit properly in their scope level.
  1024. 1 VALID_ON_OPTIONS = [:new, :collection, :member]
  1025. 1 RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
  1026. 1 CANONICAL_ACTIONS = %w(index create new show update destroy)
  1027. 1 class Resource #:nodoc:
  1028. 1 attr_reader :controller, :path, :param
  1029. 1 def initialize(entities, api_only, shallow, options = {})
  1030. 21 if options[:param].to_s.include?(":")
  1031. raise ArgumentError, ":param option can't contain colons"
  1032. end
  1033. 21 @name = entities.to_s
  1034. 21 @path = (options[:path] || @name).to_s
  1035. 21 @controller = (options[:controller] || @name).to_s
  1036. 21 @as = options[:as]
  1037. 21 @param = (options[:param] || :id).to_sym
  1038. 21 @options = options
  1039. 21 @shallow = shallow
  1040. 21 @api_only = api_only
  1041. 21 @only = options.delete :only
  1042. 21 @except = options.delete :except
  1043. end
  1044. 1 def default_actions
  1045. 126 if @api_only
  1046. [:index, :create, :show, :update, :destroy]
  1047. else
  1048. 126 [:index, :create, :new, :show, :update, :destroy, :edit]
  1049. end
  1050. end
  1051. 1 def actions
  1052. 145 if @except
  1053. 7 available_actions - Array(@except).map(&:to_sym)
  1054. else
  1055. 138 available_actions
  1056. end
  1057. end
  1058. 1 def available_actions
  1059. 145 if @only
  1060. 7 Array(@only).map(&:to_sym)
  1061. else
  1062. 138 default_actions
  1063. end
  1064. end
  1065. 1 def name
  1066. 42 @as || @name
  1067. end
  1068. 1 def plural
  1069. 292 @plural ||= name.to_s
  1070. end
  1071. 1 def singular
  1072. 304 @singular ||= name.to_s.singularize
  1073. end
  1074. 1 alias :member_name :singular
  1075. # Checks for uncountable plurals, and appends "_index" if the plural
  1076. # and singular form are the same.
  1077. 1 def collection_name
  1078. 146 singular == plural ? "#{plural}_index" : plural
  1079. end
  1080. 1 def resource_scope
  1081. 21 controller
  1082. end
  1083. 1 alias :collection_scope :path
  1084. 1 def member_scope
  1085. 19 "#{path}/:#{param}"
  1086. end
  1087. 1 alias :shallow_scope :member_scope
  1088. 1 def new_scope(new_path)
  1089. 20 "#{path}/#{new_path}"
  1090. end
  1091. 1 def nested_param
  1092. 6 :"#{singular}_#{param}"
  1093. end
  1094. 1 def nested_scope
  1095. 6 "#{path}/:#{nested_param}"
  1096. end
  1097. 1 def shallow?
  1098. @shallow
  1099. end
  1100. 26 def singleton?; false; end
  1101. end
  1102. 1 class SingletonResource < Resource #:nodoc:
  1103. 1 def initialize(entities, api_only, shallow, options)
  1104. 2 super
  1105. 2 @as = nil
  1106. 2 @controller = (options[:controller] || plural).to_s
  1107. 2 @as = options[:as]
  1108. end
  1109. 1 def default_actions
  1110. 12 if @api_only
  1111. [:show, :create, :update, :destroy]
  1112. else
  1113. 12 [:show, :create, :update, :destroy, :new, :edit]
  1114. end
  1115. end
  1116. 1 def plural
  1117. 2 @plural ||= name.to_s.pluralize
  1118. end
  1119. 1 def singular
  1120. 32 @singular ||= name.to_s
  1121. end
  1122. 1 alias :member_name :singular
  1123. 1 alias :collection_name :singular
  1124. 1 alias :member_scope :path
  1125. 1 alias :nested_scope :path
  1126. 7 def singleton?; true; end
  1127. end
  1128. 1 def resources_path_names(options)
  1129. @scope[:path_names].merge!(options)
  1130. end
  1131. # Sometimes, you have a resource that clients always look up without
  1132. # referencing an ID. A common example, /profile always shows the
  1133. # profile of the currently logged in user. In this case, you can use
  1134. # a singular resource to map /profile (rather than /profile/:id) to
  1135. # the show action:
  1136. #
  1137. # resource :profile
  1138. #
  1139. # This creates six different routes in your application, all mapping to
  1140. # the +Profiles+ controller (note that the controller is named after
  1141. # the plural):
  1142. #
  1143. # GET /profile/new
  1144. # GET /profile
  1145. # GET /profile/edit
  1146. # PATCH/PUT /profile
  1147. # DELETE /profile
  1148. # POST /profile
  1149. #
  1150. # === Options
  1151. # Takes same options as resources[rdoc-ref:#resources]
  1152. 1 def resource(*resources, &block)
  1153. 3 options = resources.extract_options!.dup
  1154. 3 if apply_common_behavior_for(:resource, resources, options, &block)
  1155. 1 return self
  1156. end
  1157. 2 with_scope_level(:resource) do
  1158. 2 options = apply_action_options options
  1159. 2 resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do
  1160. 2 yield if block_given?
  1161. 2 concerns(options[:concerns]) if options[:concerns]
  1162. new do
  1163. 2 get :new
  1164. 2 end if parent_resource.actions.include?(:new)
  1165. 2 set_member_mappings_for_resource
  1166. collection do
  1167. 2 post :create
  1168. 2 end if parent_resource.actions.include?(:create)
  1169. end
  1170. end
  1171. 2 self
  1172. end
  1173. # In Rails, a resourceful route provides a mapping between HTTP verbs
  1174. # and URLs and controller actions. By convention, each action also maps
  1175. # to particular CRUD operations in a database. A single entry in the
  1176. # routing file, such as
  1177. #
  1178. # resources :photos
  1179. #
  1180. # creates seven different routes in your application, all mapping to
  1181. # the +Photos+ controller:
  1182. #
  1183. # GET /photos
  1184. # GET /photos/new
  1185. # POST /photos
  1186. # GET /photos/:id
  1187. # GET /photos/:id/edit
  1188. # PATCH/PUT /photos/:id
  1189. # DELETE /photos/:id
  1190. #
  1191. # Resources can also be nested infinitely by using this block syntax:
  1192. #
  1193. # resources :photos do
  1194. # resources :comments
  1195. # end
  1196. #
  1197. # This generates the following comments routes:
  1198. #
  1199. # GET /photos/:photo_id/comments
  1200. # GET /photos/:photo_id/comments/new
  1201. # POST /photos/:photo_id/comments
  1202. # GET /photos/:photo_id/comments/:id
  1203. # GET /photos/:photo_id/comments/:id/edit
  1204. # PATCH/PUT /photos/:photo_id/comments/:id
  1205. # DELETE /photos/:photo_id/comments/:id
  1206. #
  1207. # === Options
  1208. # Takes same options as match[rdoc-ref:Base#match] as well as:
  1209. #
  1210. # [:path_names]
  1211. # Allows you to change the segment component of the +edit+ and +new+ actions.
  1212. # Actions not specified are not changed.
  1213. #
  1214. # resources :posts, path_names: { new: "brand_new" }
  1215. #
  1216. # The above example will now change /posts/new to /posts/brand_new.
  1217. #
  1218. # [:path]
  1219. # Allows you to change the path prefix for the resource.
  1220. #
  1221. # resources :posts, path: 'postings'
  1222. #
  1223. # The resource and all segments will now route to /postings instead of /posts.
  1224. #
  1225. # [:only]
  1226. # Only generate routes for the given actions.
  1227. #
  1228. # resources :cows, only: :show
  1229. # resources :cows, only: [:show, :index]
  1230. #
  1231. # [:except]
  1232. # Generate all routes except for the given actions.
  1233. #
  1234. # resources :cows, except: :show
  1235. # resources :cows, except: [:show, :index]
  1236. #
  1237. # [:shallow]
  1238. # Generates shallow routes for nested resource(s). When placed on a parent resource,
  1239. # generates shallow routes for all nested resources.
  1240. #
  1241. # resources :posts, shallow: true do
  1242. # resources :comments
  1243. # end
  1244. #
  1245. # Is the same as:
  1246. #
  1247. # resources :posts do
  1248. # resources :comments, except: [:show, :edit, :update, :destroy]
  1249. # end
  1250. # resources :comments, only: [:show, :edit, :update, :destroy]
  1251. #
  1252. # This allows URLs for resources that otherwise would be deeply nested such
  1253. # as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt>
  1254. # to be shortened to just <tt>/comments/1234</tt>.
  1255. #
  1256. # Set <tt>shallow: false</tt> on a child resource to ignore a parent's shallow parameter.
  1257. #
  1258. # [:shallow_path]
  1259. # Prefixes nested shallow routes with the specified path.
  1260. #
  1261. # scope shallow_path: "sekret" do
  1262. # resources :posts do
  1263. # resources :comments, shallow: true
  1264. # end
  1265. # end
  1266. #
  1267. # The +comments+ resource here will have the following routes generated for it:
  1268. #
  1269. # post_comments GET /posts/:post_id/comments(.:format)
  1270. # post_comments POST /posts/:post_id/comments(.:format)
  1271. # new_post_comment GET /posts/:post_id/comments/new(.:format)
  1272. # edit_comment GET /sekret/comments/:id/edit(.:format)
  1273. # comment GET /sekret/comments/:id(.:format)
  1274. # comment PATCH/PUT /sekret/comments/:id(.:format)
  1275. # comment DELETE /sekret/comments/:id(.:format)
  1276. #
  1277. # [:shallow_prefix]
  1278. # Prefixes nested shallow route names with specified prefix.
  1279. #
  1280. # scope shallow_prefix: "sekret" do
  1281. # resources :posts do
  1282. # resources :comments, shallow: true
  1283. # end
  1284. # end
  1285. #
  1286. # The +comments+ resource here will have the following routes generated for it:
  1287. #
  1288. # post_comments GET /posts/:post_id/comments(.:format)
  1289. # post_comments POST /posts/:post_id/comments(.:format)
  1290. # new_post_comment GET /posts/:post_id/comments/new(.:format)
  1291. # edit_sekret_comment GET /comments/:id/edit(.:format)
  1292. # sekret_comment GET /comments/:id(.:format)
  1293. # sekret_comment PATCH/PUT /comments/:id(.:format)
  1294. # sekret_comment DELETE /comments/:id(.:format)
  1295. #
  1296. # [:format]
  1297. # Allows you to specify the default value for optional +format+
  1298. # segment or disable it by supplying +false+.
  1299. #
  1300. # [:param]
  1301. # Allows you to override the default param name of +:id+ in the URL.
  1302. #
  1303. # === Examples
  1304. #
  1305. # # routes call <tt>Admin::PostsController</tt>
  1306. # resources :posts, module: "admin"
  1307. #
  1308. # # resource actions are at /admin/posts.
  1309. # resources :posts, path: "admin/posts"
  1310. 1 def resources(*resources, &block)
  1311. 30 options = resources.extract_options!.dup
  1312. 30 if apply_common_behavior_for(:resources, resources, options, &block)
  1313. 11 return self
  1314. end
  1315. 19 with_scope_level(:resources) do
  1316. 19 options = apply_action_options options
  1317. 19 resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do
  1318. 19 yield if block_given?
  1319. 19 concerns(options[:concerns]) if options[:concerns]
  1320. 19 collection do
  1321. 19 get :index if parent_resource.actions.include?(:index)
  1322. 19 post :create if parent_resource.actions.include?(:create)
  1323. end
  1324. new do
  1325. 18 get :new
  1326. 19 end if parent_resource.actions.include?(:new)
  1327. 19 set_member_mappings_for_resource
  1328. end
  1329. end
  1330. 19 self
  1331. end
  1332. # To add a route to the collection:
  1333. #
  1334. # resources :photos do
  1335. # collection do
  1336. # get 'search'
  1337. # end
  1338. # end
  1339. #
  1340. # This will enable Rails to recognize paths such as <tt>/photos/search</tt>
  1341. # with GET, and route to the search action of +PhotosController+. It will also
  1342. # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
  1343. # route helpers.
  1344. 1 def collection
  1345. 21 unless resource_scope?
  1346. raise ArgumentError, "can't use collection outside resource(s) scope"
  1347. end
  1348. 21 with_scope_level(:collection) do
  1349. 21 path_scope(parent_resource.collection_scope) do
  1350. 21 yield
  1351. end
  1352. end
  1353. end
  1354. # To add a member route, add a member block into the resource block:
  1355. #
  1356. # resources :photos do
  1357. # member do
  1358. # get 'preview'
  1359. # end
  1360. # end
  1361. #
  1362. # This will recognize <tt>/photos/1/preview</tt> with GET, and route to the
  1363. # preview action of +PhotosController+. It will also create the
  1364. # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
  1365. 1 def member
  1366. 21 unless resource_scope?
  1367. raise ArgumentError, "can't use member outside resource(s) scope"
  1368. end
  1369. 21 with_scope_level(:member) do
  1370. 21 if shallow?
  1371. shallow_scope {
  1372. path_scope(parent_resource.member_scope) { yield }
  1373. }
  1374. else
  1375. 42 path_scope(parent_resource.member_scope) { yield }
  1376. end
  1377. end
  1378. end
  1379. 1 def new
  1380. 20 unless resource_scope?
  1381. raise ArgumentError, "can't use new outside resource(s) scope"
  1382. end
  1383. 20 with_scope_level(:new) do
  1384. 20 path_scope(parent_resource.new_scope(action_path(:new))) do
  1385. 20 yield
  1386. end
  1387. end
  1388. end
  1389. 1 def nested
  1390. 10 unless resource_scope?
  1391. raise ArgumentError, "can't use nested outside resource(s) scope"
  1392. end
  1393. 10 with_scope_level(:nested) do
  1394. 10 if shallow? && shallow_nesting_depth >= 1
  1395. shallow_scope do
  1396. path_scope(parent_resource.nested_scope) do
  1397. scope(nested_options) { yield }
  1398. end
  1399. end
  1400. else
  1401. 10 path_scope(parent_resource.nested_scope) do
  1402. 20 scope(nested_options) { yield }
  1403. end
  1404. end
  1405. end
  1406. end
  1407. # See ActionDispatch::Routing::Mapper::Scoping#namespace.
  1408. 1 def namespace(path, options = {})
  1409. 3 if resource_scope?
  1410. nested { super }
  1411. else
  1412. 3 super
  1413. end
  1414. end
  1415. 1 def shallow
  1416. @scope = @scope.new(shallow: true)
  1417. yield
  1418. ensure
  1419. @scope = @scope.parent
  1420. end
  1421. 1 def shallow?
  1422. 31 !parent_resource.singleton? && @scope[:shallow]
  1423. end
  1424. 1 def draw(name)
  1425. path = @draw_paths.find do |_path|
  1426. File.exist? "#{_path}/#{name}.rb"
  1427. end
  1428. unless path
  1429. msg = "Your router tried to #draw the external file #{name}.rb,\n" \
  1430. "but the file was not found in:\n\n"
  1431. msg += @draw_paths.map { |_path| " * #{_path}" }.join("\n")
  1432. raise ArgumentError, msg
  1433. end
  1434. route_path = "#{path}/#{name}.rb"
  1435. instance_eval(File.read(route_path), route_path.to_s)
  1436. end
  1437. # Matches a URL pattern to one or more routes.
  1438. # For more information, see match[rdoc-ref:Base#match].
  1439. #
  1440. # match 'path' => 'controller#action', via: :patch
  1441. # match 'path', to: 'controller#action', via: :post
  1442. # match 'path', 'otherpath', on: :member, via: :get
  1443. 1 def match(path, *rest, &block)
  1444. 326 if rest.empty? && Hash === path
  1445. 42 options = path
  1446. 84 path, to = options.find { |name, _value| name.is_a?(String) }
  1447. 42 raise ArgumentError, "Route path not specified" if path.nil?
  1448. 42 case to
  1449. when Symbol
  1450. options[:action] = to
  1451. when String
  1452. 13 if /#/.match?(to)
  1453. 13 options[:to] = to
  1454. else
  1455. options[:controller] = to
  1456. end
  1457. else
  1458. 29 options[:to] = to
  1459. end
  1460. 42 options.delete(path)
  1461. 42 paths = [path]
  1462. else
  1463. 284 options = rest.pop || {}
  1464. 284 paths = [path] + rest
  1465. end
  1466. 326 if options.key?(:defaults)
  1467. defaults(options.delete(:defaults)) { map_match(paths, options, &block) }
  1468. else
  1469. 326 map_match(paths, options, &block)
  1470. end
  1471. end
  1472. # You can specify what Rails should route "/" to with the root method:
  1473. #
  1474. # root to: 'pages#main'
  1475. #
  1476. # For options, see +match+, as +root+ uses it internally.
  1477. #
  1478. # You can also pass a string which will expand
  1479. #
  1480. # root 'pages#main'
  1481. #
  1482. # You should put the root route at the top of <tt>config/routes.rb</tt>,
  1483. # because this means it will be matched first. As this is the most popular route
  1484. # of most Rails applications, this is beneficial.
  1485. 1 def root(path, options = {})
  1486. 3 if path.is_a?(String)
  1487. options[:to] = path
  1488. 3 elsif path.is_a?(Hash) && options.empty?
  1489. 3 options = path
  1490. else
  1491. raise ArgumentError, "must be called with a path and/or options"
  1492. end
  1493. 3 if @scope.resources?
  1494. with_scope_level(:root) do
  1495. path_scope(parent_resource.path) do
  1496. match_root_route(options)
  1497. end
  1498. end
  1499. else
  1500. 3 match_root_route(options)
  1501. end
  1502. end
  1503. 1 private
  1504. 1 def parent_resource
  1505. 915 @scope[:scope_level_resource]
  1506. end
  1507. 1 def apply_common_behavior_for(method, resources, options, &block)
  1508. 33 if resources.length > 1
  1509. 5 resources.each { |r| send(method, r, options, &block) }
  1510. 1 return true
  1511. end
  1512. 32 if options[:shallow]
  1513. options.delete(:shallow)
  1514. shallow do
  1515. send(method, resources.pop, options, &block)
  1516. end
  1517. return true
  1518. end
  1519. 32 if resource_scope?
  1520. 18 nested { send(method, resources.pop, options, &block) }
  1521. 9 return true
  1522. end
  1523. 23 options.keys.each do |k|
  1524. 9 (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
  1525. end
  1526. 23 scope_options = options.slice!(*RESOURCE_OPTIONS)
  1527. 23 unless scope_options.empty?
  1528. 2 scope(scope_options) do
  1529. 2 send(method, resources.pop, options, &block)
  1530. end
  1531. 2 return true
  1532. end
  1533. 21 false
  1534. end
  1535. 1 def apply_action_options(options)
  1536. 21 return options if action_options? options
  1537. 19 options.merge scope_action_options
  1538. end
  1539. 1 def action_options?(options)
  1540. 21 options[:only] || options[:except]
  1541. end
  1542. 1 def scope_action_options
  1543. 19 @scope[:action_options] || {}
  1544. end
  1545. 1 def resource_scope?
  1546. 107 @scope.resource_scope?
  1547. end
  1548. 1 def resource_method_scope?
  1549. 429 @scope.resource_method_scope?
  1550. end
  1551. 1 def nested_scope?
  1552. 34 @scope.nested?
  1553. end
  1554. 1 def with_scope_level(kind) # :doc:
  1555. 93 @scope = @scope.new_level(kind)
  1556. 93 yield
  1557. ensure
  1558. 93 @scope = @scope.parent
  1559. end
  1560. 1 def resource_scope(resource)
  1561. 21 @scope = @scope.new(scope_level_resource: resource)
  1562. 42 controller(resource.resource_scope) { yield }
  1563. ensure
  1564. 21 @scope = @scope.parent
  1565. end
  1566. 1 def nested_options
  1567. 10 options = { as: parent_resource.member_name }
  1568. options[:constraints] = {
  1569. parent_resource.nested_param => param_constraint
  1570. 10 } if param_constraint?
  1571. 10 options
  1572. end
  1573. 1 def shallow_nesting_depth
  1574. @scope.find_all { |node|
  1575. node.frame[:scope_level_resource]
  1576. }.count { |node| node.frame[:scope_level_resource].shallow? }
  1577. end
  1578. 1 def param_constraint?
  1579. 10 @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
  1580. end
  1581. 1 def param_constraint
  1582. @scope[:constraints][parent_resource.param]
  1583. end
  1584. 1 def canonical_action?(action)
  1585. 429 resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
  1586. end
  1587. 1 def shallow_scope
  1588. scope = { as: @scope[:shallow_prefix],
  1589. path: @scope[:shallow_path] }
  1590. @scope = @scope.new scope
  1591. yield
  1592. ensure
  1593. @scope = @scope.parent
  1594. end
  1595. 1 def path_for_action(action, path)
  1596. 326 return "#{@scope[:path]}/#{path}" if path
  1597. 158 if canonical_action?(action)
  1598. 138 @scope[:path].to_s
  1599. else
  1600. 20 "#{@scope[:path]}/#{action_path(action)}"
  1601. end
  1602. end
  1603. 1 def action_path(name)
  1604. 40 @scope[:path_names][name.to_sym] || name
  1605. end
  1606. 1 def prefix_name_for_action(as, action)
  1607. 334 if as
  1608. 63 prefix = as
  1609. 271 elsif !canonical_action?(action)
  1610. 133 prefix = action
  1611. end
  1612. 334 if prefix && prefix != "/" && !prefix.empty?
  1613. 158 Mapper.normalize_name prefix.to_s.tr("-", "_")
  1614. end
  1615. end
  1616. 1 def name_for_action(as, action)
  1617. 334 prefix = prefix_name_for_action(as, action)
  1618. 334 name_prefix = @scope[:as]
  1619. 334 if parent_resource
  1620. 160 return nil unless as || action
  1621. 160 collection_name = parent_resource.collection_name
  1622. 160 member_name = parent_resource.member_name
  1623. end
  1624. 334 action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
  1625. 334 candidate = action_name.select(&:present?).join("_")
  1626. 334 unless candidate.empty?
  1627. # If a name was not explicitly given, we check if it is valid
  1628. # and return nil in case it isn't. Otherwise, we pass the invalid name
  1629. # forward so the underlying router engine treats it and raises an exception.
  1630. 296 if as.nil?
  1631. 233 candidate unless !candidate.match?(/\A[_a-z]/i) || has_named_route?(candidate)
  1632. else
  1633. 63 candidate
  1634. end
  1635. end
  1636. end
  1637. 1 def set_member_mappings_for_resource # :doc:
  1638. 21 member do
  1639. 21 get :edit if parent_resource.actions.include?(:edit)
  1640. 21 get :show if parent_resource.actions.include?(:show)
  1641. 21 if parent_resource.actions.include?(:update)
  1642. 20 patch :update
  1643. 20 put :update
  1644. end
  1645. 21 delete :destroy if parent_resource.actions.include?(:destroy)
  1646. end
  1647. end
  1648. 1 def api_only? # :doc:
  1649. 21 @set.api_only?
  1650. end
  1651. 1 def path_scope(path)
  1652. 75 @scope = @scope.new(path: merge_path_scope(@scope[:path], path))
  1653. 75 yield
  1654. ensure
  1655. 75 @scope = @scope.parent
  1656. end
  1657. 1 def map_match(paths, options)
  1658. 326 if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
  1659. raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
  1660. end
  1661. 326 if @scope[:to]
  1662. options[:to] ||= @scope[:to]
  1663. end
  1664. 326 if @scope[:controller] && @scope[:action]
  1665. options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
  1666. end
  1667. 326 controller = options.delete(:controller) || @scope[:controller]
  1668. 326 option_path = options.delete :path
  1669. 326 to = options.delete :to
  1670. 326 via = Mapping.check_via Array(options.delete(:via) {
  1671. @scope[:via]
  1672. })
  1673. 636 formatted = options.delete(:format) { @scope[:format] }
  1674. 637 anchor = options.delete(:anchor) { true }
  1675. 326 options_constraints = options.delete(:constraints) || {}
  1676. 326 path_types = paths.group_by(&:class)
  1677. 326 (path_types[String] || []).each do |_path|
  1678. 168 route_options = options.dup
  1679. 168 if _path && option_path
  1680. raise ArgumentError, "Ambiguous route definition. Both :path and the route path were specified as strings."
  1681. end
  1682. 168 to = get_to_from_path(_path, to, route_options[:action])
  1683. 168 decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
  1684. end
  1685. 326 (path_types[Symbol] || []).each do |action|
  1686. 158 route_options = options.dup
  1687. 158 decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints)
  1688. end
  1689. 326 self
  1690. end
  1691. 1 def get_to_from_path(path, to, action)
  1692. 168 return to if to || action
  1693. 5 path_without_format = path.sub(/\(\.:format\)$/, "")
  1694. 5 if using_match_shorthand?(path_without_format)
  1695. path_without_format.delete_prefix("/").sub(%r{/([^/]*)$}, '#\1').tr("-", "_")
  1696. else
  1697. nil
  1698. end
  1699. end
  1700. 1 def using_match_shorthand?(path)
  1701. 5 %r{^/?[-\w]+/[-\w/]+$}.match?(path)
  1702. end
  1703. 1 def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
  1704. 327 if on = options.delete(:on)
  1705. send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
  1706. else
  1707. 327 case @scope.scope_level
  1708. when :resources
  1709. 2 nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
  1710. when :resource
  1711. member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
  1712. else
  1713. 326 add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
  1714. end
  1715. end
  1716. end
  1717. 1 def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints)
  1718. 326 path = path_for_action(action, _path)
  1719. 326 raise ArgumentError, "path is required" if path.blank?
  1720. 326 action = action.to_s
  1721. 326 default_action = options.delete(:action) || @scope[:action]
  1722. 326 if /^[\w\-\/]+$/.match?(action)
  1723. 270 default_action ||= action.tr("-", "_") unless action.include?("/")
  1724. else
  1725. 56 action = nil
  1726. end
  1727. 326 as = if !options.fetch(:as, true) # if it's set to nil or false
  1728. 6 options.delete(:as)
  1729. else
  1730. 320 name_for_action(options.delete(:as), action)
  1731. end
  1732. 326 path = Mapping.normalize_path URI::DEFAULT_PARSER.escape(path), formatted
  1733. 326 ast = Journey::Parser.parse path
  1734. 326 mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
  1735. 326 @set.add_route(mapping, as)
  1736. end
  1737. 1 def match_root_route(options)
  1738. 3 args = ["/", { as: :root, via: :get }.merge(options)]
  1739. 3 match(*args)
  1740. end
  1741. end
  1742. # Routing Concerns allow you to declare common routes that can be reused
  1743. # inside others resources and routes.
  1744. #
  1745. # concern :commentable do
  1746. # resources :comments
  1747. # end
  1748. #
  1749. # concern :image_attachable do
  1750. # resources :images, only: :index
  1751. # end
  1752. #
  1753. # These concerns are used in Resources routing:
  1754. #
  1755. # resources :messages, concerns: [:commentable, :image_attachable]
  1756. #
  1757. # or in a scope or namespace:
  1758. #
  1759. # namespace :posts do
  1760. # concerns :commentable
  1761. # end
  1762. 1 module Concerns
  1763. # Define a routing concern using a name.
  1764. #
  1765. # Concerns may be defined inline, using a block, or handled by
  1766. # another object, by passing that object as the second parameter.
  1767. #
  1768. # The concern object, if supplied, should respond to <tt>call</tt>,
  1769. # which will receive two parameters:
  1770. #
  1771. # * The current mapper
  1772. # * A hash of options which the concern object may use
  1773. #
  1774. # Options may also be used by concerns defined in a block by accepting
  1775. # a block parameter. So, using a block, you might do something as
  1776. # simple as limit the actions available on certain resources, passing
  1777. # standard resource options through the concern:
  1778. #
  1779. # concern :commentable do |options|
  1780. # resources :comments, options
  1781. # end
  1782. #
  1783. # resources :posts, concerns: :commentable
  1784. # resources :archived_posts do
  1785. # # Don't allow comments on archived posts
  1786. # concerns :commentable, only: [:index, :show]
  1787. # end
  1788. #
  1789. # Or, using a callable object, you might implement something more
  1790. # specific to your application, which would be out of place in your
  1791. # routes file.
  1792. #
  1793. # # purchasable.rb
  1794. # class Purchasable
  1795. # def initialize(defaults = {})
  1796. # @defaults = defaults
  1797. # end
  1798. #
  1799. # def call(mapper, options = {})
  1800. # options = @defaults.merge(options)
  1801. # mapper.resources :purchases
  1802. # mapper.resources :receipts
  1803. # mapper.resources :returns if options[:returnable]
  1804. # end
  1805. # end
  1806. #
  1807. # # routes.rb
  1808. # concern :purchasable, Purchasable.new(returnable: true)
  1809. #
  1810. # resources :toys, concerns: :purchasable
  1811. # resources :electronics, concerns: :purchasable
  1812. # resources :pets do
  1813. # concerns :purchasable, returnable: false
  1814. # end
  1815. #
  1816. # Any routing helpers can be used inside a concern. If using a
  1817. # callable, they're accessible from the Mapper that's passed to
  1818. # <tt>call</tt>.
  1819. 1 def concern(name, callable = nil, &block)
  1820. 9 callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
  1821. 3 @concerns[name] = callable
  1822. end
  1823. # Use the named concerns
  1824. #
  1825. # resources :posts do
  1826. # concerns :commentable
  1827. # end
  1828. #
  1829. # Concerns also work in any routes helper that you want to use:
  1830. #
  1831. # namespace :posts do
  1832. # concerns :commentable
  1833. # end
  1834. 1 def concerns(*args)
  1835. 6 options = args.extract_options!
  1836. 6 args.flatten.each do |name|
  1837. 8 if concern = @concerns[name]
  1838. 8 concern.call(self, options)
  1839. else
  1840. raise ArgumentError, "No concern named #{name} was found!"
  1841. end
  1842. end
  1843. end
  1844. end
  1845. 1 module CustomUrls
  1846. # Define custom URL helpers that will be added to the application's
  1847. # routes. This allows you to override and/or replace the default behavior
  1848. # of routing helpers, e.g:
  1849. #
  1850. # direct :homepage do
  1851. # "https://rubyonrails.org"
  1852. # end
  1853. #
  1854. # direct :commentable do |model|
  1855. # [ model, anchor: model.dom_id ]
  1856. # end
  1857. #
  1858. # direct :main do
  1859. # { controller: "pages", action: "index", subdomain: "www" }
  1860. # end
  1861. #
  1862. # The return value from the block passed to +direct+ must be a valid set of
  1863. # arguments for +url_for+ which will actually build the URL string. This can
  1864. # be one of the following:
  1865. #
  1866. # * A string, which is treated as a generated URL
  1867. # * A hash, e.g. <tt>{ controller: "pages", action: "index" }</tt>
  1868. # * An array, which is passed to +polymorphic_url+
  1869. # * An Active Model instance
  1870. # * An Active Model class
  1871. #
  1872. # NOTE: Other URL helpers can be called in the block but be careful not to invoke
  1873. # your custom URL helper again otherwise it will result in a stack overflow error.
  1874. #
  1875. # You can also specify default options that will be passed through to
  1876. # your URL helper definition, e.g:
  1877. #
  1878. # direct :browse, page: 1, size: 10 do |options|
  1879. # [ :products, options.merge(params.permit(:page, :size).to_h.symbolize_keys) ]
  1880. # end
  1881. #
  1882. # In this instance the +params+ object comes from the context in which the
  1883. # block is executed, e.g. generating a URL inside a controller action or a view.
  1884. # If the block is executed where there isn't a +params+ object such as this:
  1885. #
  1886. # Rails.application.routes.url_helpers.browse_path
  1887. #
  1888. # then it will raise a +NameError+. Because of this you need to be aware of the
  1889. # context in which you will use your custom URL helper when defining it.
  1890. #
  1891. # NOTE: The +direct+ method can't be used inside of a scope block such as
  1892. # +namespace+ or +scope+ and will raise an error if it detects that it is.
  1893. 1 def direct(name, options = {}, &block)
  1894. 12 unless @scope.root?
  1895. raise RuntimeError, "The direct method can't be used inside a routes scope block"
  1896. end
  1897. 12 @set.add_url_helper(name, options, &block)
  1898. end
  1899. # Define custom polymorphic mappings of models to URLs. This alters the
  1900. # behavior of +polymorphic_url+ and consequently the behavior of
  1901. # +link_to+ and +form_for+ when passed a model instance, e.g:
  1902. #
  1903. # resource :basket
  1904. #
  1905. # resolve "Basket" do
  1906. # [:basket]
  1907. # end
  1908. #
  1909. # This will now generate "/basket" when a +Basket+ instance is passed to
  1910. # +link_to+ or +form_for+ instead of the standard "/baskets/:id".
  1911. #
  1912. # NOTE: This custom behavior only applies to simple polymorphic URLs where
  1913. # a single model instance is passed and not more complicated forms, e.g:
  1914. #
  1915. # # config/routes.rb
  1916. # resource :profile
  1917. # namespace :admin do
  1918. # resources :users
  1919. # end
  1920. #
  1921. # resolve("User") { [:profile] }
  1922. #
  1923. # # app/views/application/_menu.html.erb
  1924. # link_to "Profile", @current_user
  1925. # link_to "Profile", [:admin, @current_user]
  1926. #
  1927. # The first +link_to+ will generate "/profile" but the second will generate
  1928. # the standard polymorphic URL of "/admin/users/1".
  1929. #
  1930. # You can pass options to a polymorphic mapping - the arity for the block
  1931. # needs to be two as the instance is passed as the first argument, e.g:
  1932. #
  1933. # resolve "Basket", anchor: "items" do |basket, options|
  1934. # [:basket, options]
  1935. # end
  1936. #
  1937. # This generates the URL "/basket#items" because when the last item in an
  1938. # array passed to +polymorphic_url+ is a hash then it's treated as options
  1939. # to the URL helper that gets called.
  1940. #
  1941. # NOTE: The +resolve+ method can't be used inside of a scope block such as
  1942. # +namespace+ or +scope+ and will raise an error if it detects that it is.
  1943. 1 def resolve(*args, &block)
  1944. 6 unless @scope.root?
  1945. raise RuntimeError, "The resolve method can't be used inside a routes scope block"
  1946. end
  1947. 6 options = args.extract_options!
  1948. 6 args = args.flatten(1)
  1949. 6 args.each do |klass|
  1950. 8 @set.add_polymorphic_mapping(klass, options, &block)
  1951. end
  1952. end
  1953. end
  1954. 1 class Scope # :nodoc:
  1955. 1 OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
  1956. :controller, :action, :path_names, :constraints,
  1957. :shallow, :blocks, :defaults, :via, :format, :options, :to]
  1958. 1 RESOURCE_SCOPES = [:resource, :resources]
  1959. 1 RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
  1960. 1 attr_reader :parent, :scope_level
  1961. 1 def initialize(hash, parent = NULL, scope_level = nil)
  1962. 299 @hash = hash
  1963. 299 @parent = parent
  1964. 299 @scope_level = scope_level
  1965. end
  1966. 1 def nested?
  1967. 34 scope_level == :nested
  1968. end
  1969. 1 def null?
  1970. 18 @hash.nil? && @parent.nil?
  1971. end
  1972. 1 def root?
  1973. 18 @parent.null?
  1974. end
  1975. 1 def resources?
  1976. 3 scope_level == :resources
  1977. end
  1978. 1 def resource_method_scope?
  1979. 429 RESOURCE_METHOD_SCOPES.include? scope_level
  1980. end
  1981. 1 def action_name(name_prefix, prefix, collection_name, member_name)
  1982. 334 case scope_level
  1983. when :nested
  1984. 1 [name_prefix, prefix]
  1985. when :collection
  1986. 39 [prefix, name_prefix, collection_name]
  1987. when :new
  1988. 20 [prefix, :new, name_prefix, member_name]
  1989. when :member
  1990. 99 [prefix, name_prefix, member_name]
  1991. when :root
  1992. [name_prefix, collection_name, prefix]
  1993. else
  1994. 175 [name_prefix, member_name, prefix]
  1995. end
  1996. end
  1997. 1 def resource_scope?
  1998. 107 RESOURCE_SCOPES.include? scope_level
  1999. end
  2000. 1 def options
  2001. 34 OPTIONS
  2002. end
  2003. 1 def new(hash)
  2004. 151 self.class.new hash, self, scope_level
  2005. end
  2006. 1 def new_level(level)
  2007. 93 self.class.new(frame, self, level)
  2008. end
  2009. 1 def [](key)
  2010. 24998 scope = find { |node| node.frame.key? key }
  2011. 5351 scope && scope.frame[key]
  2012. end
  2013. 1 include Enumerable
  2014. 1 def each
  2015. 5351 node = self
  2016. 5351 until node.equal? NULL
  2017. 19647 yield node
  2018. 17684 node = node.parent
  2019. end
  2020. end
  2021. 21704 def frame; @hash; end
  2022. 1 NULL = Scope.new(nil, nil)
  2023. end
  2024. 1 def initialize(set) #:nodoc:
  2025. 54 @set = set
  2026. 54 @draw_paths = set.draw_paths
  2027. 54 @scope = Scope.new(path_names: @set.resources_path_names)
  2028. 54 @concerns = {}
  2029. end
  2030. 1 include Base
  2031. 1 include HttpHelpers
  2032. 1 include Redirection
  2033. 1 include Scoping
  2034. 1 include Concerns
  2035. 1 include Resources
  2036. 1 include CustomUrls
  2037. end
  2038. end
  2039. end

lib/action_dispatch/routing/polymorphic_routes.rb

35.38% lines covered

130 relevant lines. 46 lines covered and 84 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module Routing
  4. # Polymorphic URL helpers are methods for smart resolution to a named route call when
  5. # given an Active Record model instance. They are to be used in combination with
  6. # ActionController::Resources.
  7. #
  8. # These methods are useful when you want to generate the correct URL or path to a RESTful
  9. # resource without having to know the exact type of the record in question.
  10. #
  11. # Nested resources and/or namespaces are also supported, as illustrated in the example:
  12. #
  13. # polymorphic_url([:admin, @article, @comment])
  14. #
  15. # results in:
  16. #
  17. # admin_article_comment_url(@article, @comment)
  18. #
  19. # == Usage within the framework
  20. #
  21. # Polymorphic URL helpers are used in a number of places throughout the \Rails framework:
  22. #
  23. # * <tt>url_for</tt>, so you can use it with a record as the argument, e.g.
  24. # <tt>url_for(@article)</tt>;
  25. # * ActionView::Helpers::FormHelper uses <tt>polymorphic_path</tt>, so you can write
  26. # <tt>form_for(@article)</tt> without having to specify <tt>:url</tt> parameter for the form
  27. # action;
  28. # * <tt>redirect_to</tt> (which, in fact, uses <tt>url_for</tt>) so you can write
  29. # <tt>redirect_to(post)</tt> in your controllers;
  30. # * ActionView::Helpers::AtomFeedHelper, so you don't have to explicitly specify URLs
  31. # for feed entries.
  32. #
  33. # == Prefixed polymorphic helpers
  34. #
  35. # In addition to <tt>polymorphic_url</tt> and <tt>polymorphic_path</tt> methods, a
  36. # number of prefixed helpers are available as a shorthand to <tt>action: "..."</tt>
  37. # in options. Those are:
  38. #
  39. # * <tt>edit_polymorphic_url</tt>, <tt>edit_polymorphic_path</tt>
  40. # * <tt>new_polymorphic_url</tt>, <tt>new_polymorphic_path</tt>
  41. #
  42. # Example usage:
  43. #
  44. # edit_polymorphic_path(@post) # => "/posts/1/edit"
  45. # polymorphic_path(@post, format: :pdf) # => "/posts/1.pdf"
  46. #
  47. # == Usage with mounted engines
  48. #
  49. # If you are using a mounted engine and you need to use a polymorphic_url
  50. # pointing at the engine's routes, pass in the engine's route proxy as the first
  51. # argument to the method. For example:
  52. #
  53. # polymorphic_url([blog, @post]) # calls blog.post_path(@post)
  54. # form_for([blog, @post]) # => "/blog/posts/1"
  55. #
  56. 1 module PolymorphicRoutes
  57. # Constructs a call to a named RESTful route for the given record and returns the
  58. # resulting URL string. For example:
  59. #
  60. # # calls post_url(post)
  61. # polymorphic_url(post) # => "http://example.com/posts/1"
  62. # polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1"
  63. # polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1"
  64. # polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1"
  65. # polymorphic_url(Comment) # => "http://example.com/comments"
  66. #
  67. # ==== Options
  68. #
  69. # * <tt>:action</tt> - Specifies the action prefix for the named route:
  70. # <tt>:new</tt> or <tt>:edit</tt>. Default is no prefix.
  71. # * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>.
  72. # Default is <tt>:url</tt>.
  73. #
  74. # Also includes all the options from <tt>url_for</tt>. These include such
  75. # things as <tt>:anchor</tt> or <tt>:trailing_slash</tt>. Example usage
  76. # is given below:
  77. #
  78. # polymorphic_url([blog, post], anchor: 'my_anchor')
  79. # # => "http://example.com/blogs/1/posts/1#my_anchor"
  80. # polymorphic_url([blog, post], anchor: 'my_anchor', script_name: "/my_app")
  81. # # => "http://example.com/my_app/blogs/1/posts/1#my_anchor"
  82. #
  83. # For all of these options, see the documentation for {url_for}[rdoc-ref:ActionDispatch::Routing::UrlFor].
  84. #
  85. # ==== Functionality
  86. #
  87. # # an Article record
  88. # polymorphic_url(record) # same as article_url(record)
  89. #
  90. # # a Comment record
  91. # polymorphic_url(record) # same as comment_url(record)
  92. #
  93. # # it recognizes new records and maps to the collection
  94. # record = Comment.new
  95. # polymorphic_url(record) # same as comments_url()
  96. #
  97. # # the class of a record will also map to the collection
  98. # polymorphic_url(Comment) # same as comments_url()
  99. #
  100. 1 def polymorphic_url(record_or_hash_or_array, options = {})
  101. if Hash === record_or_hash_or_array
  102. options = record_or_hash_or_array.merge(options)
  103. record = options.delete :id
  104. return polymorphic_url record, options
  105. end
  106. if mapping = polymorphic_mapping(record_or_hash_or_array)
  107. return mapping.call(self, [record_or_hash_or_array, options], false)
  108. end
  109. opts = options.dup
  110. action = opts.delete :action
  111. type = opts.delete(:routing_type) || :url
  112. HelperMethodBuilder.polymorphic_method self,
  113. record_or_hash_or_array,
  114. action,
  115. type,
  116. opts
  117. end
  118. # Returns the path component of a URL for the given record.
  119. 1 def polymorphic_path(record_or_hash_or_array, options = {})
  120. if Hash === record_or_hash_or_array
  121. options = record_or_hash_or_array.merge(options)
  122. record = options.delete :id
  123. return polymorphic_path record, options
  124. end
  125. if mapping = polymorphic_mapping(record_or_hash_or_array)
  126. return mapping.call(self, [record_or_hash_or_array, options], true)
  127. end
  128. opts = options.dup
  129. action = opts.delete :action
  130. type = :path
  131. HelperMethodBuilder.polymorphic_method self,
  132. record_or_hash_or_array,
  133. action,
  134. type,
  135. opts
  136. end
  137. 1 %w(edit new).each do |action|
  138. 2 module_eval <<-EOT, __FILE__, __LINE__ + 1
  139. # frozen_string_literal: true
  140. def #{action}_polymorphic_url(record_or_hash, options = {})
  141. polymorphic_url_for_action("#{action}", record_or_hash, options)
  142. end
  143. def #{action}_polymorphic_path(record_or_hash, options = {})
  144. polymorphic_path_for_action("#{action}", record_or_hash, options)
  145. end
  146. EOT
  147. end
  148. 1 private
  149. 1 def polymorphic_url_for_action(action, record_or_hash, options)
  150. polymorphic_url(record_or_hash, options.merge(action: action))
  151. end
  152. 1 def polymorphic_path_for_action(action, record_or_hash, options)
  153. polymorphic_path(record_or_hash, options.merge(action: action))
  154. end
  155. 1 def polymorphic_mapping(record)
  156. if record.respond_to?(:to_model)
  157. _routes.polymorphic_mappings[record.to_model.model_name.name]
  158. else
  159. _routes.polymorphic_mappings[record.class.name]
  160. end
  161. end
  162. 1 class HelperMethodBuilder # :nodoc:
  163. 1 CACHE = { path: {}, url: {} }
  164. 1 def self.get(action, type)
  165. type = type.to_sym
  166. CACHE[type].fetch(action) { build action, type }
  167. end
  168. 1 def self.url; CACHE[:url][nil]; end
  169. 1 def self.path; CACHE[:path][nil]; end
  170. 1 def self.build(action, type)
  171. 6 prefix = action ? "#{action}_" : ""
  172. 6 suffix = type
  173. 6 if action.to_s == "new"
  174. 2 HelperMethodBuilder.singular prefix, suffix
  175. else
  176. 4 HelperMethodBuilder.plural prefix, suffix
  177. end
  178. end
  179. 1 def self.singular(prefix, suffix)
  180. 2 new(->(name) { name.singular_route_key }, prefix, suffix)
  181. end
  182. 1 def self.plural(prefix, suffix)
  183. 4 new(->(name) { name.route_key }, prefix, suffix)
  184. end
  185. 1 def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options)
  186. builder = get action, type
  187. case record_or_hash_or_array
  188. when Array
  189. record_or_hash_or_array = record_or_hash_or_array.compact
  190. if record_or_hash_or_array.empty?
  191. raise ArgumentError, "Nil location provided. Can't build URI."
  192. end
  193. if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy)
  194. recipient = record_or_hash_or_array.shift
  195. end
  196. method, args = builder.handle_list record_or_hash_or_array
  197. when String, Symbol
  198. method, args = builder.handle_string record_or_hash_or_array
  199. when Class
  200. method, args = builder.handle_class record_or_hash_or_array
  201. when nil
  202. raise ArgumentError, "Nil location provided. Can't build URI."
  203. else
  204. method, args = builder.handle_model record_or_hash_or_array
  205. end
  206. if options.empty?
  207. recipient.send(method, *args)
  208. else
  209. recipient.send(method, *args, options)
  210. end
  211. end
  212. 1 attr_reader :suffix, :prefix
  213. 1 def initialize(key_strategy, prefix, suffix)
  214. 6 @key_strategy = key_strategy
  215. 6 @prefix = prefix
  216. 6 @suffix = suffix
  217. end
  218. 1 def handle_string(record)
  219. [get_method_for_string(record), []]
  220. end
  221. 1 def handle_string_call(target, str)
  222. target.send get_method_for_string str
  223. end
  224. 1 def handle_class(klass)
  225. [get_method_for_class(klass), []]
  226. end
  227. 1 def handle_class_call(target, klass)
  228. target.send get_method_for_class klass
  229. end
  230. 1 def handle_model(record)
  231. args = []
  232. model = record.to_model
  233. named_route = if model.persisted?
  234. args << model
  235. get_method_for_string model.model_name.singular_route_key
  236. else
  237. get_method_for_class model
  238. end
  239. [named_route, args]
  240. end
  241. 1 def handle_model_call(target, record)
  242. if mapping = polymorphic_mapping(target, record)
  243. mapping.call(target, [record], suffix == "path")
  244. else
  245. method, args = handle_model(record)
  246. target.send(method, *args)
  247. end
  248. end
  249. 1 def handle_list(list)
  250. record_list = list.dup
  251. record = record_list.pop
  252. args = []
  253. route = record_list.map { |parent|
  254. case parent
  255. when Symbol, String
  256. parent.to_s
  257. when Class
  258. args << parent
  259. parent.model_name.singular_route_key
  260. else
  261. args << parent.to_model
  262. parent.to_model.model_name.singular_route_key
  263. end
  264. }
  265. route <<
  266. case record
  267. when Symbol, String
  268. record.to_s
  269. when Class
  270. @key_strategy.call record.model_name
  271. else
  272. model = record.to_model
  273. if model.persisted?
  274. args << model
  275. model.model_name.singular_route_key
  276. else
  277. @key_strategy.call model.model_name
  278. end
  279. end
  280. route << suffix
  281. named_route = prefix + route.join("_")
  282. [named_route, args]
  283. end
  284. 1 private
  285. 1 def polymorphic_mapping(target, record)
  286. if record.respond_to?(:to_model)
  287. target._routes.polymorphic_mappings[record.to_model.model_name.name]
  288. else
  289. target._routes.polymorphic_mappings[record.class.name]
  290. end
  291. end
  292. 1 def get_method_for_class(klass)
  293. name = @key_strategy.call klass.model_name
  294. get_method_for_string name
  295. end
  296. 1 def get_method_for_string(str)
  297. "#{prefix}#{str}_#{suffix}"
  298. end
  299. 1 [nil, "new", "edit"].each do |action|
  300. 3 CACHE[:url][action] = build action, "url"
  301. 3 CACHE[:path][action] = build action, "path"
  302. end
  303. end
  304. end
  305. end
  306. end

lib/action_dispatch/routing/redirection.rb

51.81% lines covered

83 relevant lines. 43 lines covered and 40 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_dispatch/http/request"
  3. 1 require "active_support/core_ext/uri"
  4. 1 require "active_support/core_ext/array/extract_options"
  5. 1 require "rack/utils"
  6. 1 require "action_controller/metal/exceptions"
  7. 1 require "action_dispatch/routing/endpoint"
  8. 1 module ActionDispatch
  9. 1 module Routing
  10. 1 class Redirect < Endpoint # :nodoc:
  11. 1 attr_reader :status, :block
  12. 1 def initialize(status, block)
  13. 32 @status = status
  14. 32 @block = block
  15. end
  16. 1 def redirect?; true; end
  17. 1 def call(env)
  18. serve Request.new env
  19. end
  20. 1 def serve(req)
  21. uri = URI.parse(path(req.path_parameters, req))
  22. unless uri.host
  23. if relative_path?(uri.path)
  24. uri.path = "#{req.script_name}/#{uri.path}"
  25. elsif uri.path.empty?
  26. uri.path = req.script_name.empty? ? "/" : req.script_name
  27. end
  28. end
  29. uri.scheme ||= req.scheme
  30. uri.host ||= req.host
  31. uri.port ||= req.port unless req.standard_port?
  32. req.commit_flash
  33. body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>)
  34. headers = {
  35. "Location" => uri.to_s,
  36. "Content-Type" => "text/html",
  37. "Content-Length" => body.length.to_s
  38. }
  39. [ status, headers, [body] ]
  40. end
  41. 1 def path(params, request)
  42. block.call params, request
  43. end
  44. 1 def inspect
  45. "redirect(#{status})"
  46. end
  47. 1 private
  48. 1 def relative_path?(path)
  49. path && !path.empty? && path[0] != "/"
  50. end
  51. 1 def escape(params)
  52. params.transform_values { |v| Rack::Utils.escape(v) }
  53. end
  54. 1 def escape_fragment(params)
  55. params.transform_values { |v| Journey::Router::Utils.escape_fragment(v) }
  56. end
  57. 1 def escape_path(params)
  58. params.transform_values { |v| Journey::Router::Utils.escape_path(v) }
  59. end
  60. end
  61. 1 class PathRedirect < Redirect
  62. 1 URL_PARTS = /\A([^?]+)?(\?[^#]+)?(#.+)?\z/
  63. 1 def path(params, request)
  64. if block.match(URL_PARTS)
  65. path = interpolation_required?($1, params) ? $1 % escape_path(params) : $1
  66. query = interpolation_required?($2, params) ? $2 % escape(params) : $2
  67. fragment = interpolation_required?($3, params) ? $3 % escape_fragment(params) : $3
  68. "#{path}#{query}#{fragment}"
  69. else
  70. interpolation_required?(block, params) ? block % escape(params) : block
  71. end
  72. end
  73. 1 def inspect
  74. "redirect(#{status}, #{block})"
  75. end
  76. 1 private
  77. 1 def interpolation_required?(string, params)
  78. !params.empty? && string && string.match(/%\{\w*\}/)
  79. end
  80. end
  81. 1 class OptionRedirect < Redirect # :nodoc:
  82. 1 alias :options :block
  83. 1 def path(params, request)
  84. url_options = {
  85. protocol: request.protocol,
  86. host: request.host,
  87. port: request.optional_port,
  88. path: request.path,
  89. params: request.query_parameters
  90. }.merge! options
  91. if !params.empty? && url_options[:path].match(/%\{\w*\}/)
  92. url_options[:path] = (url_options[:path] % escape_path(params))
  93. end
  94. unless options[:host] || options[:domain]
  95. if relative_path?(url_options[:path])
  96. url_options[:path] = "/#{url_options[:path]}"
  97. url_options[:script_name] = request.script_name
  98. elsif url_options[:path].empty?
  99. url_options[:path] = request.script_name.empty? ? "/" : ""
  100. url_options[:script_name] = request.script_name
  101. end
  102. end
  103. ActionDispatch::Http::URL.url_for url_options
  104. end
  105. 1 def inspect
  106. "redirect(#{status}, #{options.map { |k, v| "#{k}: #{v}" }.join(', ')})"
  107. end
  108. end
  109. 1 module Redirection
  110. # Redirect any path to another path:
  111. #
  112. # get "/stories" => redirect("/posts")
  113. #
  114. # This will redirect the user, while ignoring certain parts of the request, including query string, etc.
  115. # <tt>/stories</tt>, <tt>/stories?foo=bar</tt>, etc all redirect to <tt>/posts</tt>.
  116. #
  117. # You can also use interpolation in the supplied redirect argument:
  118. #
  119. # get 'docs/:article', to: redirect('/wiki/%{article}')
  120. #
  121. # Note that if you return a path without a leading slash then the URL is prefixed with the
  122. # current SCRIPT_NAME environment variable. This is typically '/' but may be different in
  123. # a mounted engine or where the application is deployed to a subdirectory of a website.
  124. #
  125. # Alternatively you can use one of the other syntaxes:
  126. #
  127. # The block version of redirect allows for the easy encapsulation of any logic associated with
  128. # the redirect in question. Either the params and request are supplied as arguments, or just
  129. # params, depending of how many arguments your block accepts. A string is required as a
  130. # return value.
  131. #
  132. # get 'jokes/:number', to: redirect { |params, request|
  133. # path = (params[:number].to_i.even? ? "wheres-the-beef" : "i-love-lamp")
  134. # "http://#{request.host_with_port}/#{path}"
  135. # }
  136. #
  137. # Note that the +do end+ syntax for the redirect block wouldn't work, as Ruby would pass
  138. # the block to +get+ instead of +redirect+. Use <tt>{ ... }</tt> instead.
  139. #
  140. # The options version of redirect allows you to supply only the parts of the URL which need
  141. # to change, it also supports interpolation of the path similar to the first example.
  142. #
  143. # get 'stores/:name', to: redirect(subdomain: 'stores', path: '/%{name}')
  144. # get 'stores/:name(*all)', to: redirect(subdomain: 'stores', path: '/%{name}%{all}')
  145. # get '/stories', to: redirect(path: '/posts')
  146. #
  147. # This will redirect the user, while changing only the specified parts of the request,
  148. # for example the +path+ option in the last example.
  149. # <tt>/stories</tt>, <tt>/stories?foo=bar</tt>, redirect to <tt>/posts</tt> and <tt>/posts?foo=bar</tt> respectively.
  150. #
  151. # Finally, an object which responds to call can be supplied to redirect, allowing you to reuse
  152. # common redirect routes. The call method must accept two arguments, params and request, and return
  153. # a string.
  154. #
  155. # get 'accounts/:name' => redirect(SubdomainRedirector.new('api'))
  156. #
  157. 1 def redirect(*args, &block)
  158. 32 options = args.extract_options!
  159. 32 status = options.delete(:status) || 301
  160. 32 path = args.shift
  161. 32 return OptionRedirect.new(status, options) if options.any?
  162. 23 return PathRedirect.new(status, path) if String === path
  163. 9 block = path if path.respond_to? :call
  164. 9 raise ArgumentError, "redirection argument not supported" unless block
  165. 9 Redirect.new status, block
  166. end
  167. end
  168. end
  169. end

lib/action_dispatch/routing/route_set.rb

57.08% lines covered

473 relevant lines. 270 lines covered and 203 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_dispatch/journey"
  3. 1 require "active_support/core_ext/object/to_query"
  4. 1 require "active_support/core_ext/module/redefine_method"
  5. 1 require "active_support/core_ext/module/remove_method"
  6. 1 require "active_support/core_ext/array/extract_options"
  7. 1 require "action_controller/metal/exceptions"
  8. 1 require "action_dispatch/http/request"
  9. 1 require "action_dispatch/routing/endpoint"
  10. 1 module ActionDispatch
  11. 1 module Routing
  12. # :stopdoc:
  13. 1 class RouteSet
  14. # Since the router holds references to many parts of the system
  15. # like engines, controllers and the application itself, inspecting
  16. # the route set can actually be really slow, therefore we default
  17. # alias inspect to to_s.
  18. 1 alias inspect to_s
  19. 1 class Dispatcher < Routing::Endpoint
  20. 1 def initialize(raise_on_name_error)
  21. 239 @raise_on_name_error = raise_on_name_error
  22. end
  23. 1 def dispatcher?; true; end
  24. 1 def serve(req)
  25. params = req.path_parameters
  26. controller = controller req
  27. res = controller.make_response! req
  28. dispatch(controller, params[:action], req, res)
  29. rescue ActionController::RoutingError
  30. if @raise_on_name_error
  31. raise
  32. else
  33. [404, { "X-Cascade" => "pass" }, []]
  34. end
  35. end
  36. 1 private
  37. 1 def controller(req)
  38. req.controller_class
  39. rescue NameError => e
  40. raise ActionController::RoutingError, e.message, e.backtrace
  41. end
  42. 1 def dispatch(controller, action, req, res)
  43. controller.dispatch(action, req, res)
  44. end
  45. end
  46. 1 class StaticDispatcher < Dispatcher
  47. 1 def initialize(controller_class)
  48. super(false)
  49. @controller_class = controller_class
  50. end
  51. 1 private
  52. 1 def controller(_); @controller_class; end
  53. end
  54. # A NamedRouteCollection instance is a collection of named routes, and also
  55. # maintains an anonymous module that can be used to install helpers for the
  56. # named routes.
  57. 1 class NamedRouteCollection
  58. 1 include Enumerable
  59. 1 attr_reader :routes, :url_helpers_module, :path_helpers_module
  60. 1 private :routes
  61. 1 def initialize
  62. 56 @routes = {}
  63. 56 @path_helpers = Set.new
  64. 56 @url_helpers = Set.new
  65. 56 @url_helpers_module = Module.new
  66. 56 @path_helpers_module = Module.new
  67. end
  68. 1 def route_defined?(name)
  69. key = name.to_sym
  70. @path_helpers.include?(key) || @url_helpers.include?(key)
  71. end
  72. 1 def helper_names
  73. @path_helpers.map(&:to_s) + @url_helpers.map(&:to_s)
  74. end
  75. 1 def clear!
  76. 54 @path_helpers.each do |helper|
  77. @path_helpers_module.remove_method helper
  78. end
  79. 54 @url_helpers.each do |helper|
  80. @url_helpers_module.remove_method helper
  81. end
  82. 54 @routes.clear
  83. 54 @path_helpers.clear
  84. 54 @url_helpers.clear
  85. end
  86. 1 def add(name, route)
  87. 203 key = name.to_sym
  88. 203 path_name = :"#{name}_path"
  89. 203 url_name = :"#{name}_url"
  90. 203 if routes.key? key
  91. @path_helpers_module.undef_method path_name
  92. @url_helpers_module.undef_method url_name
  93. end
  94. 203 routes[key] = route
  95. 203 helper = UrlHelper.create(route, route.defaults, name)
  96. 203 define_url_helper @path_helpers_module, path_name, helper, PATH
  97. 203 define_url_helper @url_helpers_module, url_name, helper, UNKNOWN
  98. 203 @path_helpers << path_name
  99. 203 @url_helpers << url_name
  100. end
  101. 1 def get(name)
  102. 206 routes[name.to_sym]
  103. end
  104. 1 def key?(name)
  105. 233 return unless name
  106. 233 routes.key? name.to_sym
  107. end
  108. 1 alias []= add
  109. 1 alias [] get
  110. 1 alias clear clear!
  111. 1 def each
  112. routes.each { |name, route| yield name, route }
  113. self
  114. end
  115. 1 def names
  116. routes.keys
  117. end
  118. 1 def length
  119. routes.length
  120. end
  121. # Given a +name+, defines name_path and name_url helpers.
  122. # Used by 'direct', 'resolve', and 'polymorphic' route helpers.
  123. 1 def add_url_helper(name, defaults, &block)
  124. 12 helper = CustomUrlHelper.new(name, defaults, &block)
  125. 12 path_name = :"#{name}_path"
  126. 12 url_name = :"#{name}_url"
  127. 12 @path_helpers_module.module_eval do
  128. 12 redefine_method(path_name) do |*args|
  129. helper.call(self, args, true)
  130. end
  131. end
  132. 12 @url_helpers_module.module_eval do
  133. 12 redefine_method(url_name) do |*args|
  134. helper.call(self, args, false)
  135. end
  136. end
  137. 12 @path_helpers << path_name
  138. 12 @url_helpers << url_name
  139. 12 self
  140. end
  141. 1 class UrlHelper
  142. 1 def self.create(route, options, route_name)
  143. 203 if optimize_helper?(route)
  144. 192 OptimizedUrlHelper.new(route, options, route_name)
  145. else
  146. 11 new(route, options, route_name)
  147. end
  148. end
  149. 1 def self.optimize_helper?(route)
  150. 203 route.path.requirements.empty? && !route.glob?
  151. end
  152. 1 attr_reader :route_name
  153. 1 class OptimizedUrlHelper < UrlHelper
  154. 1 attr_reader :arg_size
  155. 1 def initialize(route, options, route_name)
  156. 192 super
  157. 192 @required_parts = @route.required_parts
  158. 192 @arg_size = @required_parts.size
  159. end
  160. 1 def call(t, method_name, args, inner_options, url_strategy)
  161. if args.size == arg_size && !inner_options && optimize_routes_generation?(t)
  162. options = t.url_options.merge @options
  163. options[:path] = optimized_helper(args)
  164. original_script_name = options.delete(:original_script_name)
  165. script_name = t._routes.find_script_name(options)
  166. if original_script_name
  167. script_name = original_script_name + script_name
  168. end
  169. options[:script_name] = script_name
  170. url_strategy.call options
  171. else
  172. super
  173. end
  174. end
  175. 1 private
  176. 1 def optimized_helper(args)
  177. params = parameterize_args(args) do
  178. raise_generation_error(args)
  179. end
  180. @route.format params
  181. end
  182. 1 def optimize_routes_generation?(t)
  183. t.send(:optimize_routes_generation?)
  184. end
  185. 1 def parameterize_args(args)
  186. params = {}
  187. @arg_size.times { |i|
  188. key = @required_parts[i]
  189. value = args[i].to_param
  190. yield key if value.nil? || value.empty?
  191. params[key] = value
  192. }
  193. params
  194. end
  195. 1 def raise_generation_error(args)
  196. missing_keys = []
  197. params = parameterize_args(args) { |missing_key|
  198. missing_keys << missing_key
  199. }
  200. constraints = Hash[@route.requirements.merge(params).sort_by { |k, v| k.to_s }]
  201. message = +"No route matches #{constraints.inspect}"
  202. message << ", missing required keys: #{missing_keys.sort.inspect}"
  203. raise ActionController::UrlGenerationError, message
  204. end
  205. end
  206. 1 def initialize(route, options, route_name)
  207. 203 @options = options
  208. 203 @segment_keys = route.segment_keys.uniq
  209. 203 @route = route
  210. 203 @route_name = route_name
  211. end
  212. 1 def call(t, method_name, args, inner_options, url_strategy)
  213. controller_options = t.url_options
  214. options = controller_options.merge @options
  215. hash = handle_positional_args(controller_options,
  216. inner_options || {},
  217. args,
  218. options,
  219. @segment_keys)
  220. t._routes.url_for(hash, route_name, url_strategy, method_name)
  221. end
  222. 1 def handle_positional_args(controller_options, inner_options, args, result, path_params)
  223. if args.size > 0
  224. # take format into account
  225. if path_params.include?(:format)
  226. path_params_size = path_params.size - 1
  227. else
  228. path_params_size = path_params.size
  229. end
  230. if args.size < path_params_size
  231. path_params -= controller_options.keys
  232. path_params -= result.keys
  233. else
  234. path_params = path_params.dup
  235. end
  236. inner_options.each_key do |key|
  237. path_params.delete(key)
  238. end
  239. args.each_with_index do |arg, index|
  240. param = path_params[index]
  241. result[param] = arg if param
  242. end
  243. end
  244. result.merge!(inner_options)
  245. end
  246. end
  247. 1 private
  248. # Create a URL helper allowing ordered parameters to be associated
  249. # with corresponding dynamic segments, so you can do:
  250. #
  251. # foo_url(bar, baz, bang)
  252. #
  253. # Instead of:
  254. #
  255. # foo_url(bar: bar, baz: baz, bang: bang)
  256. #
  257. # Also allow options hash, so you can do:
  258. #
  259. # foo_url(bar, baz, bang, sort_by: 'baz')
  260. #
  261. 1 def define_url_helper(mod, name, helper, url_strategy)
  262. 406 mod.define_method(name) do |*args|
  263. last = args.last
  264. options = \
  265. case last
  266. when Hash
  267. args.pop
  268. when ActionController::Parameters
  269. args.pop.to_h
  270. end
  271. helper.call(self, name, args, options, url_strategy)
  272. end
  273. end
  274. end
  275. # strategy for building URLs to send to the client
  276. 1 PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) }
  277. 1 UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) }
  278. 1 attr_accessor :formatter, :set, :named_routes, :default_scope, :router
  279. 1 attr_accessor :disable_clear_and_finalize, :resources_path_names
  280. 1 attr_accessor :default_url_options, :draw_paths
  281. 1 attr_reader :env_key, :polymorphic_mappings
  282. 1 alias :routes :set
  283. 1 def self.default_resources_path_names
  284. 56 { new: "new", edit: "edit" }
  285. end
  286. 1 def self.new_with_config(config)
  287. 4 route_set_config = DEFAULT_CONFIG
  288. # engines apparently don't have this set
  289. 4 if config.respond_to? :relative_url_root
  290. route_set_config.relative_url_root = config.relative_url_root
  291. end
  292. 4 if config.respond_to? :api_only
  293. route_set_config.api_only = config.api_only
  294. end
  295. 4 new route_set_config
  296. end
  297. 1 Config = Struct.new :relative_url_root, :api_only
  298. 1 DEFAULT_CONFIG = Config.new(nil, false)
  299. 1 def initialize(config = DEFAULT_CONFIG)
  300. 56 self.named_routes = NamedRouteCollection.new
  301. 56 self.resources_path_names = self.class.default_resources_path_names
  302. 56 self.default_url_options = {}
  303. 56 self.draw_paths = []
  304. 56 @config = config
  305. 56 @append = []
  306. 56 @prepend = []
  307. 56 @disable_clear_and_finalize = false
  308. 56 @finalized = false
  309. 56 @env_key = "ROUTES_#{object_id}_SCRIPT_NAME"
  310. 56 @set = Journey::Routes.new
  311. 56 @router = Journey::Router.new @set
  312. 56 @formatter = Journey::Formatter.new self
  313. 56 @polymorphic_mappings = {}
  314. end
  315. 1 def eager_load!
  316. router.eager_load!
  317. routes.each(&:eager_load!)
  318. nil
  319. end
  320. 1 def relative_url_root
  321. @config.relative_url_root
  322. end
  323. 1 def api_only?
  324. 21 @config.api_only
  325. end
  326. 1 def request_class
  327. 324 ActionDispatch::Request
  328. end
  329. 1 def make_request(env)
  330. request_class.new env
  331. end
  332. 1 private :make_request
  333. 1 def draw(&block)
  334. 54 clear! unless @disable_clear_and_finalize
  335. 54 eval_block(block)
  336. 54 finalize! unless @disable_clear_and_finalize
  337. nil
  338. end
  339. 1 def append(&block)
  340. @append << block
  341. end
  342. 1 def prepend(&block)
  343. @prepend << block
  344. end
  345. 1 def eval_block(block)
  346. 54 mapper = Mapper.new(self)
  347. 54 if default_scope
  348. 2 mapper.with_default_scope(default_scope, &block)
  349. else
  350. 52 mapper.instance_exec(&block)
  351. end
  352. end
  353. 1 private :eval_block
  354. 1 def finalize!
  355. 54 return if @finalized
  356. 54 @append.each { |blk| eval_block(blk) }
  357. 54 @finalized = true
  358. end
  359. 1 def clear!
  360. 54 @finalized = false
  361. 54 named_routes.clear
  362. 54 set.clear
  363. 54 formatter.clear
  364. 54 @polymorphic_mappings.clear
  365. 54 @prepend.each { |blk| eval_block(blk) }
  366. end
  367. 1 module MountedHelpers
  368. 1 extend ActiveSupport::Concern
  369. 1 include UrlFor
  370. end
  371. # Contains all the mounted helpers across different
  372. # engines and the `main_app` helper for the application.
  373. # You can include this in your classes if you want to
  374. # access routes for other engines.
  375. 1 def mounted_helpers
  376. 5 MountedHelpers
  377. end
  378. 1 def define_mounted_helper(name, script_namer = nil)
  379. 4 return if MountedHelpers.method_defined?(name)
  380. 4 routes = self
  381. 4 helpers = routes.url_helpers
  382. 4 MountedHelpers.class_eval do
  383. 4 define_method "_#{name}" do
  384. RoutesProxy.new(routes, _routes_context, helpers, script_namer)
  385. end
  386. end
  387. 4 MountedHelpers.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
  388. def #{name}
  389. @_#{name} ||= _#{name}
  390. end
  391. RUBY
  392. end
  393. 1 def url_helpers(supports_path = true)
  394. 325 if supports_path
  395. 325 @url_helpers_with_paths ||= generate_url_helpers(true)
  396. else
  397. @url_helpers_without_paths ||= generate_url_helpers(false)
  398. end
  399. end
  400. 1 def generate_url_helpers(supports_path)
  401. 35 routes = self
  402. 35 Module.new do
  403. 35 extend ActiveSupport::Concern
  404. 35 include UrlFor
  405. # Define url_for in the singleton level so one can do:
  406. # Rails.application.routes.url_helpers.url_for(args)
  407. 35 proxy_class = Class.new do
  408. 35 include UrlFor
  409. 35 include routes.named_routes.path_helpers_module
  410. 35 include routes.named_routes.url_helpers_module
  411. 35 attr_reader :_routes
  412. 35 def initialize(routes)
  413. 35 @_routes = routes
  414. end
  415. 35 def optimize_routes_generation?
  416. @_routes.optimize_routes_generation?
  417. end
  418. end
  419. 35 @_proxy = proxy_class.new(routes)
  420. 35 class << self
  421. 35 def url_for(options)
  422. @_proxy.url_for(options)
  423. end
  424. 35 def full_url_for(options)
  425. @_proxy.full_url_for(options)
  426. end
  427. 35 def route_for(name, *args)
  428. @_proxy.route_for(name, *args)
  429. end
  430. 35 def optimize_routes_generation?
  431. @_proxy.optimize_routes_generation?
  432. end
  433. 35 def polymorphic_url(record_or_hash_or_array, options = {})
  434. @_proxy.polymorphic_url(record_or_hash_or_array, options)
  435. end
  436. 35 def polymorphic_path(record_or_hash_or_array, options = {})
  437. @_proxy.polymorphic_path(record_or_hash_or_array, options)
  438. end
  439. 35 def _routes; @_proxy._routes; end
  440. 35 def url_options; {}; end
  441. end
  442. 35 url_helpers = routes.named_routes.url_helpers_module
  443. # Make named_routes available in the module singleton
  444. # as well, so one can do:
  445. # Rails.application.routes.url_helpers.posts_path
  446. 35 extend url_helpers
  447. # Any class that includes this module will get all
  448. # named routes...
  449. 35 include url_helpers
  450. 35 if supports_path
  451. 35 path_helpers = routes.named_routes.path_helpers_module
  452. 35 include path_helpers
  453. 35 extend path_helpers
  454. end
  455. # plus a singleton class method called _routes ...
  456. 35 included do
  457. 228 redefine_singleton_method(:_routes) { routes }
  458. end
  459. # And an instance method _routes. Note that
  460. # UrlFor (included in this module) add extra
  461. # conveniences for working with @_routes.
  462. 35 define_method(:_routes) { @_routes || routes }
  463. 35 define_method(:_generate_paths_by_default) do
  464. supports_path
  465. end
  466. 35 private :_generate_paths_by_default
  467. end
  468. end
  469. 1 def empty?
  470. routes.empty?
  471. end
  472. 1 def add_route(mapping, name)
  473. 326 raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
  474. 326 if name && named_routes[name]
  475. raise ArgumentError, "Invalid route name, already in use: '#{name}' \n" \
  476. "You may have defined two routes with the same name using the `:as` option, or " \
  477. "you may be overriding a route already defined by a resource with the same naming. " \
  478. "For the latter, you can restrict the routes created with `resources` as explained here: \n" \
  479. "https://guides.rubyonrails.org/routing.html#restricting-the-routes-created"
  480. end
  481. 326 route = @set.add_route(name, mapping)
  482. 326 named_routes[name] = route if name
  483. 326 if route.segment_keys.include?(:controller)
  484. 5 ActiveSupport::Deprecation.warn(<<-MSG.squish)
  485. Using a dynamic :controller segment in a route is deprecated and
  486. will be removed in Rails 6.1.
  487. MSG
  488. end
  489. 326 if route.segment_keys.include?(:action)
  490. 6 ActiveSupport::Deprecation.warn(<<-MSG.squish)
  491. Using a dynamic :action segment in a route is deprecated and
  492. will be removed in Rails 6.1.
  493. MSG
  494. end
  495. 326 route
  496. end
  497. 1 def add_polymorphic_mapping(klass, options, &block)
  498. 8 @polymorphic_mappings[klass] = CustomUrlHelper.new(klass, options, &block)
  499. end
  500. 1 def add_url_helper(name, options, &block)
  501. 12 named_routes.add_url_helper(name, options, &block)
  502. end
  503. 1 class CustomUrlHelper
  504. 1 attr_reader :name, :defaults, :block
  505. 1 def initialize(name, defaults, &block)
  506. 20 @name = name
  507. 20 @defaults = defaults
  508. 20 @block = block
  509. end
  510. 1 def call(t, args, only_path = false)
  511. options = args.extract_options!
  512. url = t.full_url_for(eval_block(t, args, options))
  513. if only_path
  514. "/" + url.partition(%r{(?<!/)/(?!/)}).last
  515. else
  516. url
  517. end
  518. end
  519. 1 private
  520. 1 def eval_block(t, args, options)
  521. t.instance_exec(*args, merge_defaults(options), &block)
  522. end
  523. 1 def merge_defaults(options)
  524. defaults ? defaults.merge(options) : options
  525. end
  526. end
  527. 1 class Generator
  528. 1 attr_reader :options, :recall, :set, :named_route
  529. 1 def initialize(named_route, options, recall, set)
  530. @named_route = named_route
  531. @options = options
  532. @recall = recall
  533. @set = set
  534. normalize_options!
  535. normalize_controller_action_id!
  536. use_relative_controller!
  537. normalize_controller!
  538. end
  539. 1 def controller
  540. @options[:controller]
  541. end
  542. 1 def current_controller
  543. @recall[:controller]
  544. end
  545. 1 def use_recall_for(key)
  546. if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
  547. if !named_route_exists? || segment_keys.include?(key)
  548. @options[key] = @recall[key]
  549. end
  550. end
  551. end
  552. 1 def normalize_options!
  553. # If an explicit :controller was given, always make :action explicit
  554. # too, so that action expiry works as expected for things like
  555. #
  556. # generate({controller: 'content'}, {controller: 'content', action: 'show'})
  557. #
  558. # (the above is from the unit tests). In the above case, because the
  559. # controller was explicitly given, but no action, the action is implied to
  560. # be "index", not the recalled action of "show".
  561. if options[:controller]
  562. options[:action] ||= "index"
  563. options[:controller] = options[:controller].to_s
  564. end
  565. if options.key?(:action)
  566. options[:action] = (options[:action] || "index").to_s
  567. end
  568. end
  569. # This pulls :controller, :action, and :id out of the recall.
  570. # The recall key is only used if there is no key in the options
  571. # or if the key in the options is identical. If any of
  572. # :controller, :action or :id is not found, don't pull any
  573. # more keys from the recall.
  574. 1 def normalize_controller_action_id!
  575. use_recall_for(:controller) || return
  576. use_recall_for(:action) || return
  577. use_recall_for(:id)
  578. end
  579. # if the current controller is "foo/bar/baz" and controller: "baz/bat"
  580. # is specified, the controller becomes "foo/baz/bat"
  581. 1 def use_relative_controller!
  582. if !named_route && different_controller? && !controller.start_with?("/")
  583. old_parts = current_controller.split("/")
  584. size = controller.count("/") + 1
  585. parts = old_parts[0...-size] << controller
  586. @options[:controller] = parts.join("/")
  587. end
  588. end
  589. # Remove leading slashes from controllers
  590. 1 def normalize_controller!
  591. if controller
  592. if controller.start_with?("/")
  593. @options[:controller] = controller[1..-1]
  594. else
  595. @options[:controller] = controller
  596. end
  597. end
  598. end
  599. # Generates a path from routes, returns a RouteWithParams or MissingRoute.
  600. # MissingRoute will raise ActionController::UrlGenerationError.
  601. 1 def generate
  602. @set.formatter.generate(named_route, options, recall)
  603. end
  604. 1 def different_controller?
  605. return false unless current_controller
  606. controller.to_param != current_controller.to_param
  607. end
  608. 1 private
  609. 1 def named_route_exists?
  610. named_route && set.named_routes[named_route]
  611. end
  612. 1 def segment_keys
  613. set.named_routes[named_route].segment_keys
  614. end
  615. end
  616. # Generate the path indicated by the arguments, and return an array of
  617. # the keys that were not used to generate it.
  618. 1 def extra_keys(options, recall = {})
  619. generate_extras(options, recall).last
  620. end
  621. 1 def generate_extras(options, recall = {})
  622. if recall
  623. options = options.merge(_recall: recall)
  624. end
  625. route_name = options.delete :use_route
  626. path = path_for(options, route_name, [])
  627. uri = URI.parse(path)
  628. params = Rack::Utils.parse_nested_query(uri.query).symbolize_keys
  629. [uri.path, params.keys]
  630. end
  631. 1 def generate(route_name, options, recall = {}, method_name = nil)
  632. Generator.new(route_name, options, recall, self).generate
  633. end
  634. 1 private :generate
  635. 1 RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
  636. :trailing_slash, :anchor, :params, :only_path, :script_name,
  637. :original_script_name, :relative_url_root]
  638. 1 def optimize_routes_generation?
  639. default_url_options.empty?
  640. end
  641. 1 def find_script_name(options)
  642. options.delete(:script_name) || find_relative_url_root(options) || ""
  643. end
  644. 1 def find_relative_url_root(options)
  645. options.delete(:relative_url_root) || relative_url_root
  646. end
  647. 1 def path_for(options, route_name = nil, reserved = RESERVED_OPTIONS)
  648. url_for(options, route_name, PATH, nil, reserved)
  649. end
  650. # The +options+ argument must be a hash whose keys are *symbols*.
  651. 1 def url_for(options, route_name = nil, url_strategy = UNKNOWN, method_name = nil, reserved = RESERVED_OPTIONS)
  652. options = default_url_options.merge options
  653. user = password = nil
  654. if options[:user] && options[:password]
  655. user = options.delete :user
  656. password = options.delete :password
  657. end
  658. recall = options.delete(:_recall) { {} }
  659. original_script_name = options.delete(:original_script_name)
  660. script_name = find_script_name options
  661. if original_script_name
  662. script_name = original_script_name + script_name
  663. end
  664. path_options = options.dup
  665. reserved.each { |ro| path_options.delete ro }
  666. route_with_params = generate(route_name, path_options, recall)
  667. path = route_with_params.path(method_name)
  668. params = route_with_params.params
  669. if options.key? :params
  670. params.merge! options[:params]
  671. end
  672. options[:path] = path
  673. options[:script_name] = script_name
  674. options[:params] = params
  675. options[:user] = user
  676. options[:password] = password
  677. url_strategy.call options
  678. end
  679. 1 def call(env)
  680. req = make_request(env)
  681. req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
  682. @router.serve(req)
  683. end
  684. 1 def recognize_path(path, environment = {})
  685. method = (environment[:method] || "GET").to_s.upcase
  686. path = Journey::Router::Utils.normalize_path(path) unless %r{://}.match?(path)
  687. extras = environment[:extras] || {}
  688. begin
  689. env = Rack::MockRequest.env_for(path, method: method)
  690. rescue URI::InvalidURIError => e
  691. raise ActionController::RoutingError, e.message
  692. end
  693. req = make_request(env)
  694. recognize_path_with_request(req, path, extras)
  695. end
  696. 1 def recognize_path_with_request(req, path, extras, raise_on_missing: true)
  697. @router.recognize(req) do |route, params|
  698. params.merge!(extras)
  699. params.each do |key, value|
  700. if value.is_a?(String)
  701. value = value.dup.force_encoding(Encoding::BINARY)
  702. params[key] = URI::DEFAULT_PARSER.unescape(value)
  703. end
  704. end
  705. req.path_parameters = params
  706. app = route.app
  707. if app.matches?(req) && app.dispatcher?
  708. begin
  709. req.controller_class
  710. rescue NameError
  711. raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller"
  712. end
  713. return req.path_parameters
  714. elsif app.matches?(req) && app.engine?
  715. path_parameters = app.rack_app.routes.recognize_path_with_request(req, path, extras, raise_on_missing: false)
  716. return path_parameters if path_parameters
  717. end
  718. end
  719. if raise_on_missing
  720. raise ActionController::RoutingError, "No route matches #{path.inspect}"
  721. end
  722. end
  723. end
  724. # :startdoc:
  725. end
  726. end

lib/action_dispatch/routing/routes_proxy.rb

0.0% lines covered

52 relevant lines. 0 lines covered and 52 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/core_ext/array/extract_options"
  3. module ActionDispatch
  4. module Routing
  5. class RoutesProxy #:nodoc:
  6. include ActionDispatch::Routing::UrlFor
  7. attr_accessor :scope, :routes
  8. alias :_routes :routes
  9. def initialize(routes, scope, helpers, script_namer = nil)
  10. @routes, @scope = routes, scope
  11. @helpers = helpers
  12. @script_namer = script_namer
  13. end
  14. def url_options
  15. scope.send(:_with_routes, routes) do
  16. scope.url_options
  17. end
  18. end
  19. private
  20. def respond_to_missing?(method, _)
  21. super || @helpers.respond_to?(method)
  22. end
  23. def method_missing(method, *args)
  24. if @helpers.respond_to?(method)
  25. self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
  26. def #{method}(*args)
  27. options = args.extract_options!
  28. options = url_options.merge((options || {}).symbolize_keys)
  29. if @script_namer
  30. options[:script_name] = merge_script_names(
  31. options[:script_name],
  32. @script_namer.call(options)
  33. )
  34. end
  35. args << options
  36. @helpers.#{method}(*args)
  37. end
  38. RUBY
  39. public_send(method, *args)
  40. else
  41. super
  42. end
  43. end
  44. # Keeps the part of the script name provided by the global
  45. # context via ENV["SCRIPT_NAME"], which `mount` doesn't know
  46. # about since it depends on the specific request, but use our
  47. # script name resolver for the mount point dependent part.
  48. def merge_script_names(previous_script_name, new_script_name)
  49. return new_script_name unless previous_script_name
  50. resolved_parts = new_script_name.count("/")
  51. previous_parts = previous_script_name.count("/")
  52. context_parts = previous_parts - resolved_parts + 1
  53. (previous_script_name.split("/").slice(0, context_parts).join("/")) + new_script_name
  54. end
  55. end
  56. end
  57. end

lib/action_dispatch/routing/url_for.rb

50.0% lines covered

44 relevant lines. 22 lines covered and 22 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module Routing
  4. # In <tt>config/routes.rb</tt> you define URL-to-controller mappings, but the reverse
  5. # is also possible: a URL can be generated from one of your routing definitions.
  6. # URL generation functionality is centralized in this module.
  7. #
  8. # See ActionDispatch::Routing for general information about routing and routes.rb.
  9. #
  10. # <b>Tip:</b> If you need to generate URLs from your models or some other place,
  11. # then ActionController::UrlFor is what you're looking for. Read on for
  12. # an introduction. In general, this module should not be included on its own,
  13. # as it is usually included by url_helpers (as in Rails.application.routes.url_helpers).
  14. #
  15. # == URL generation from parameters
  16. #
  17. # As you may know, some functions, such as ActionController::Base#url_for
  18. # and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set
  19. # of parameters. For example, you've probably had the chance to write code
  20. # like this in one of your views:
  21. #
  22. # <%= link_to('Click here', controller: 'users',
  23. # action: 'new', message: 'Welcome!') %>
  24. # # => <a href="/users/new?message=Welcome%21">Click here</a>
  25. #
  26. # link_to, and all other functions that require URL generation functionality,
  27. # actually use ActionController::UrlFor under the hood. And in particular,
  28. # they use the ActionController::UrlFor#url_for method. One can generate
  29. # the same path as the above example by using the following code:
  30. #
  31. # include UrlFor
  32. # url_for(controller: 'users',
  33. # action: 'new',
  34. # message: 'Welcome!',
  35. # only_path: true)
  36. # # => "/users/new?message=Welcome%21"
  37. #
  38. # Notice the <tt>only_path: true</tt> part. This is because UrlFor has no
  39. # information about the website hostname that your Rails app is serving. So if you
  40. # want to include the hostname as well, then you must also pass the <tt>:host</tt>
  41. # argument:
  42. #
  43. # include UrlFor
  44. # url_for(controller: 'users',
  45. # action: 'new',
  46. # message: 'Welcome!',
  47. # host: 'www.example.com')
  48. # # => "http://www.example.com/users/new?message=Welcome%21"
  49. #
  50. # By default, all controllers and views have access to a special version of url_for,
  51. # that already knows what the current hostname is. So if you use url_for in your
  52. # controllers or your views, then you don't need to explicitly pass the <tt>:host</tt>
  53. # argument.
  54. #
  55. # For convenience reasons, mailers provide a shortcut for ActionController::UrlFor#url_for.
  56. # So within mailers, you only have to type +url_for+ instead of 'ActionController::UrlFor#url_for'
  57. # in full. However, mailers don't have hostname information, and you still have to provide
  58. # the +:host+ argument or set the default host that will be used in all mailers using the
  59. # configuration option +config.action_mailer.default_url_options+. For more information on
  60. # url_for in mailers read the ActionMailer#Base documentation.
  61. #
  62. #
  63. # == URL generation for named routes
  64. #
  65. # UrlFor also allows one to access methods that have been auto-generated from
  66. # named routes. For example, suppose that you have a 'users' resource in your
  67. # <tt>config/routes.rb</tt>:
  68. #
  69. # resources :users
  70. #
  71. # This generates, among other things, the method <tt>users_path</tt>. By default,
  72. # this method is accessible from your controllers, views and mailers. If you need
  73. # to access this auto-generated method from other places (such as a model), then
  74. # you can do that by including Rails.application.routes.url_helpers in your class:
  75. #
  76. # class User < ActiveRecord::Base
  77. # include Rails.application.routes.url_helpers
  78. #
  79. # def base_uri
  80. # user_path(self)
  81. # end
  82. # end
  83. #
  84. # User.find(1).base_uri # => "/users/1"
  85. #
  86. 1 module UrlFor
  87. 1 extend ActiveSupport::Concern
  88. 1 include PolymorphicRoutes
  89. 1 included do
  90. 42 unless method_defined?(:default_url_options)
  91. # Including in a class uses an inheritable hash. Modules get a plain hash.
  92. 41 if respond_to?(:class_attribute)
  93. 41 class_attribute :default_url_options
  94. else
  95. mattr_writer :default_url_options
  96. end
  97. 41 self.default_url_options = {}
  98. end
  99. 42 include(*_url_for_modules) if respond_to?(:_url_for_modules)
  100. end
  101. 1 def initialize(*)
  102. @_routes = nil
  103. super
  104. end
  105. 1 ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
  106. # Hook overridden in controller to add request information
  107. # with +default_url_options+. Application logic should not
  108. # go into url_options.
  109. 1 def url_options
  110. default_url_options
  111. end
  112. # Generate a URL based on the options provided, default_url_options and the
  113. # routes defined in routes.rb. The following options are supported:
  114. #
  115. # * <tt>:only_path</tt> - If true, the relative URL is returned. Defaults to +false+.
  116. # * <tt>:protocol</tt> - The protocol to connect to. Defaults to 'http'.
  117. # * <tt>:host</tt> - Specifies the host the link should be targeted at.
  118. # If <tt>:only_path</tt> is false, this option must be
  119. # provided either explicitly, or via +default_url_options+.
  120. # * <tt>:subdomain</tt> - Specifies the subdomain of the link, using the +tld_length+
  121. # to split the subdomain from the host.
  122. # If false, removes all subdomains from the host part of the link.
  123. # * <tt>:domain</tt> - Specifies the domain of the link, using the +tld_length+
  124. # to split the domain from the host.
  125. # * <tt>:tld_length</tt> - Number of labels the TLD id composed of, only used if
  126. # <tt>:subdomain</tt> or <tt>:domain</tt> are supplied. Defaults to
  127. # <tt>ActionDispatch::Http::URL.tld_length</tt>, which in turn defaults to 1.
  128. # * <tt>:port</tt> - Optionally specify the port to connect to.
  129. # * <tt>:anchor</tt> - An anchor name to be appended to the path.
  130. # * <tt>:params</tt> - The query parameters to be appended to the path.
  131. # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/"
  132. # * <tt>:script_name</tt> - Specifies application path relative to domain root. If provided, prepends application path.
  133. #
  134. # Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to
  135. # +url_for+ is forwarded to the Routes module.
  136. #
  137. # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', port: '8080'
  138. # # => 'http://somehost.org:8080/tasks/testing'
  139. # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', anchor: 'ok', only_path: true
  140. # # => '/tasks/testing#ok'
  141. # url_for controller: 'tasks', action: 'testing', trailing_slash: true
  142. # # => 'http://somehost.org/tasks/testing/'
  143. # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', number: '33'
  144. # # => 'http://somehost.org/tasks/testing?number=33'
  145. # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp"
  146. # # => 'http://somehost.org/myapp/tasks/testing'
  147. # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp", only_path: true
  148. # # => '/myapp/tasks/testing'
  149. #
  150. # Missing routes keys may be filled in from the current request's parameters
  151. # (e.g. +:controller+, +:action+, +:id+ and any other parameters that are
  152. # placed in the path). Given that the current action has been reached
  153. # through <tt>GET /users/1</tt>:
  154. #
  155. # url_for(only_path: true) # => '/users/1'
  156. # url_for(only_path: true, action: 'edit') # => '/users/1/edit'
  157. # url_for(only_path: true, action: 'edit', id: 2) # => '/users/2/edit'
  158. #
  159. # Notice that no +:id+ parameter was provided to the first +url_for+ call
  160. # and the helper used the one from the route's path. Any path parameter
  161. # implicitly used by +url_for+ can always be overwritten like shown on the
  162. # last +url_for+ calls.
  163. 1 def url_for(options = nil)
  164. full_url_for(options)
  165. end
  166. 1 def full_url_for(options = nil) # :nodoc:
  167. case options
  168. when nil
  169. _routes.url_for(url_options.symbolize_keys)
  170. when Hash, ActionController::Parameters
  171. route_name = options.delete :use_route
  172. merged_url_options = options.to_h.symbolize_keys.reverse_merge!(url_options)
  173. _routes.url_for(merged_url_options, route_name)
  174. when String
  175. options
  176. when Symbol
  177. HelperMethodBuilder.url.handle_string_call self, options
  178. when Array
  179. components = options.dup
  180. polymorphic_url(components, components.extract_options!)
  181. when Class
  182. HelperMethodBuilder.url.handle_class_call self, options
  183. else
  184. HelperMethodBuilder.url.handle_model_call self, options
  185. end
  186. end
  187. # Allows calling direct or regular named route.
  188. #
  189. # resources :buckets
  190. #
  191. # direct :recordable do |recording|
  192. # route_for(:bucket, recording.bucket)
  193. # end
  194. #
  195. # direct :threadable do |threadable|
  196. # route_for(:recordable, threadable.parent)
  197. # end
  198. #
  199. # This maintains the context of the original caller on
  200. # whether to return a path or full URL, e.g:
  201. #
  202. # threadable_path(threadable) # => "/buckets/1"
  203. # threadable_url(threadable) # => "http://example.com/buckets/1"
  204. #
  205. 1 def route_for(name, *args)
  206. public_send(:"#{name}_url", *args)
  207. end
  208. 1 protected
  209. 1 def optimize_routes_generation?
  210. _routes.optimize_routes_generation? && default_url_options.empty?
  211. end
  212. 1 private
  213. 1 def _with_routes(routes) # :doc:
  214. old_routes, @_routes = @_routes, routes
  215. yield
  216. ensure
  217. @_routes = old_routes
  218. end
  219. 1 def _routes_context # :doc:
  220. self
  221. end
  222. end
  223. end
  224. end

lib/action_dispatch/system_test_case.rb

66.67% lines covered

45 relevant lines. 30 lines covered and 15 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 gem "capybara", ">= 3.26"
  3. 1 require "capybara/dsl"
  4. 1 require "capybara/minitest"
  5. 1 require "action_controller"
  6. 1 require "action_dispatch/system_testing/driver"
  7. 1 require "action_dispatch/system_testing/browser"
  8. 1 require "action_dispatch/system_testing/server"
  9. 1 require "action_dispatch/system_testing/test_helpers/screenshot_helper"
  10. 1 require "action_dispatch/system_testing/test_helpers/setup_and_teardown"
  11. 1 module ActionDispatch
  12. # = System Testing
  13. #
  14. # System tests let you test applications in the browser. Because system
  15. # tests use a real browser experience, you can test all of your JavaScript
  16. # easily from your test suite.
  17. #
  18. # To create a system test in your application, extend your test class
  19. # from <tt>ApplicationSystemTestCase</tt>. System tests use Capybara as a
  20. # base and allow you to configure the settings through your
  21. # <tt>application_system_test_case.rb</tt> file that is generated with a new
  22. # application or scaffold.
  23. #
  24. # Here is an example system test:
  25. #
  26. # require "application_system_test_case"
  27. #
  28. # class Users::CreateTest < ApplicationSystemTestCase
  29. # test "adding a new user" do
  30. # visit users_path
  31. # click_on 'New User'
  32. #
  33. # fill_in 'Name', with: 'Arya'
  34. # click_on 'Create User'
  35. #
  36. # assert_text 'Arya'
  37. # end
  38. # end
  39. #
  40. # When generating an application or scaffold, an +application_system_test_case.rb+
  41. # file will also be generated containing the base class for system testing.
  42. # This is where you can change the driver, add Capybara settings, and other
  43. # configuration for your system tests.
  44. #
  45. # require "test_helper"
  46. #
  47. # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  48. # driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
  49. # end
  50. #
  51. # By default, <tt>ActionDispatch::SystemTestCase</tt> is driven by the
  52. # Selenium driver, with the Chrome browser, and a browser size of 1400x1400.
  53. #
  54. # Changing the driver configuration options is easy. Let's say you want to use
  55. # the Firefox browser instead of Chrome. In your +application_system_test_case.rb+
  56. # file add the following:
  57. #
  58. # require "test_helper"
  59. #
  60. # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  61. # driven_by :selenium, using: :firefox
  62. # end
  63. #
  64. # +driven_by+ has a required argument for the driver name. The keyword
  65. # arguments are +:using+ for the browser and +:screen_size+ to change the
  66. # size of the browser screen. These two options are not applicable for
  67. # headless drivers and will be silently ignored if passed.
  68. #
  69. # Headless browsers such as headless Chrome and headless Firefox are also supported.
  70. # You can use these browsers by setting the +:using+ argument to +:headless_chrome+ or +:headless_firefox+.
  71. #
  72. # To use a headless driver, like Poltergeist, update your Gemfile to use
  73. # Poltergeist instead of Selenium and then declare the driver name in the
  74. # +application_system_test_case.rb+ file. In this case, you would leave out
  75. # the +:using+ option because the driver is headless, but you can still use
  76. # +:screen_size+ to change the size of the browser screen, also you can use
  77. # +:options+ to pass options supported by the driver. Please refer to your
  78. # driver documentation to learn about supported options.
  79. #
  80. # require "test_helper"
  81. # require "capybara/poltergeist"
  82. #
  83. # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  84. # driven_by :poltergeist, screen_size: [1400, 1400], options:
  85. # { js_errors: true }
  86. # end
  87. #
  88. # Some drivers require browser capabilities to be passed as a block instead
  89. # of through the +options+ hash.
  90. #
  91. # As an example, if you want to add mobile emulation on chrome, you'll have to
  92. # create an instance of selenium's +Chrome::Options+ object and add
  93. # capabilities with a block.
  94. #
  95. # The block will be passed an instance of <tt><Driver>::Options</tt> where you can
  96. # define the capabilities you want. Please refer to your driver documentation
  97. # to learn about supported options.
  98. #
  99. # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  100. # driven_by :selenium, using: :chrome, screen_size: [1024, 768] do |driver_option|
  101. # driver_option.add_emulation(device_name: 'iPhone 6')
  102. # driver_option.add_extension('path/to/chrome_extension.crx')
  103. # end
  104. # end
  105. #
  106. # Because <tt>ActionDispatch::SystemTestCase</tt> is a shim between Capybara
  107. # and Rails, any driver that is supported by Capybara is supported by system
  108. # tests as long as you include the required gems and files.
  109. 1 class SystemTestCase < ActiveSupport::TestCase
  110. 1 include Capybara::DSL
  111. 1 include Capybara::Minitest::Assertions
  112. 1 include SystemTesting::TestHelpers::SetupAndTeardown
  113. 1 include SystemTesting::TestHelpers::ScreenshotHelper
  114. 1 def initialize(*) # :nodoc:
  115. super
  116. self.class.driven_by(:selenium) unless self.class.driver?
  117. self.class.driver.use
  118. end
  119. 1 def self.start_application # :nodoc:
  120. 1 Capybara.app = Rack::Builder.new do
  121. 1 map "/" do
  122. run Rails.application
  123. end
  124. end
  125. 1 SystemTesting::Server.new.run
  126. end
  127. 1 class_attribute :driver, instance_accessor: false
  128. # System Test configuration options
  129. #
  130. # The default settings are Selenium, using Chrome, with a screen size
  131. # of 1400x1400.
  132. #
  133. # Examples:
  134. #
  135. # driven_by :poltergeist
  136. #
  137. # driven_by :selenium, screen_size: [800, 800]
  138. #
  139. # driven_by :selenium, using: :chrome
  140. #
  141. # driven_by :selenium, using: :headless_chrome
  142. #
  143. # driven_by :selenium, using: :firefox
  144. #
  145. # driven_by :selenium, using: :headless_firefox
  146. 1 def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {}, &capabilities)
  147. 5 driver_options = { using: using, screen_size: screen_size, options: options }
  148. 5 self.driver = SystemTesting::Driver.new(driver, **driver_options, &capabilities)
  149. end
  150. 1 private
  151. 1 def url_helpers
  152. @url_helpers ||=
  153. if ActionDispatch.test_app
  154. Class.new do
  155. include ActionDispatch.test_app.routes.url_helpers
  156. include ActionDispatch.test_app.routes.mounted_helpers
  157. def url_options
  158. default_url_options.reverse_merge(host: Capybara.app_host || Capybara.current_session.server_url)
  159. end
  160. end.new
  161. end
  162. end
  163. 1 def method_missing(name, *args, &block)
  164. if url_helpers.respond_to?(name)
  165. url_helpers.public_send(name, *args, &block)
  166. else
  167. super
  168. end
  169. end
  170. 1 def respond_to_missing?(name, include_private = false)
  171. url_helpers.respond_to?(name)
  172. end
  173. end
  174. end
  175. 1 ActiveSupport.run_load_hooks :action_dispatch_system_test_case, ActionDispatch::SystemTestCase
  176. 1 ActionDispatch::SystemTestCase.start_application

lib/action_dispatch/system_testing/browser.rb

94.87% lines covered

39 relevant lines. 37 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module SystemTesting
  4. 1 class Browser # :nodoc:
  5. 1 attr_reader :name, :options
  6. 1 def initialize(name)
  7. 5 @name = name
  8. 5 set_default_options
  9. end
  10. 1 def type
  11. 5 case name
  12. when :headless_chrome
  13. 2 :chrome
  14. when :headless_firefox
  15. 2 :firefox
  16. else
  17. 1 name
  18. end
  19. end
  20. 1 def configure
  21. 2 initialize_options
  22. 2 yield options if block_given? && options
  23. end
  24. # driver_path can be configured as a proc. The webdrivers gem uses this
  25. # proc to update web drivers. Running this proc early allows us to only
  26. # update the webdriver once and avoid race conditions when using
  27. # parallel tests.
  28. 1 def preload
  29. 3 case type
  30. when :chrome
  31. 2 if ::Selenium::WebDriver::Service.respond_to? :driver_path=
  32. 2 ::Selenium::WebDriver::Chrome::Service.driver_path&.call
  33. else
  34. # Selenium <= v3.141.0
  35. ::Selenium::WebDriver::Chrome.driver_path
  36. end
  37. when :firefox
  38. 1 if ::Selenium::WebDriver::Service.respond_to? :driver_path=
  39. 1 ::Selenium::WebDriver::Firefox::Service.driver_path&.call
  40. else
  41. # Selenium <= v3.141.0
  42. ::Selenium::WebDriver::Firefox.driver_path
  43. end
  44. end
  45. end
  46. 1 private
  47. 1 def initialize_options
  48. 2 @options ||=
  49. case type
  50. when :chrome
  51. 1 ::Selenium::WebDriver::Chrome::Options.new
  52. when :firefox
  53. 1 ::Selenium::WebDriver::Firefox::Options.new
  54. end
  55. end
  56. 1 def set_default_options
  57. 5 case name
  58. when :headless_chrome
  59. 1 set_headless_chrome_browser_options
  60. when :headless_firefox
  61. 1 set_headless_firefox_browser_options
  62. end
  63. end
  64. 1 def set_headless_chrome_browser_options
  65. 1 configure do |capabilities|
  66. 1 capabilities.args << "--headless"
  67. 1 capabilities.args << "--disable-gpu" if Gem.win_platform?
  68. end
  69. end
  70. 1 def set_headless_firefox_browser_options
  71. 1 configure do |capabilities|
  72. 1 capabilities.args << "-headless"
  73. end
  74. end
  75. end
  76. end
  77. end

lib/action_dispatch/system_testing/driver.rb

56.76% lines covered

37 relevant lines. 21 lines covered and 16 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module SystemTesting
  4. 1 class Driver # :nodoc:
  5. 1 def initialize(name, **options, &capabilities)
  6. 5 @name = name
  7. 5 @browser = Browser.new(options[:using])
  8. 5 @screen_size = options[:screen_size]
  9. 5 @options = options[:options] || {}
  10. 5 @capabilities = capabilities
  11. 5 if name == :selenium
  12. 3 require "selenium/webdriver"
  13. 3 @browser.preload
  14. end
  15. end
  16. 1 def use
  17. register if registerable?
  18. setup
  19. end
  20. 1 private
  21. 1 def registerable?
  22. [:selenium, :poltergeist, :webkit].include?(@name)
  23. end
  24. 1 def register
  25. @browser.configure(&@capabilities)
  26. Capybara.register_driver @name do |app|
  27. case @name
  28. when :selenium then register_selenium(app)
  29. when :poltergeist then register_poltergeist(app)
  30. when :webkit then register_webkit(app)
  31. end
  32. end
  33. end
  34. 1 def browser_options
  35. @options.merge(options: @browser.options).compact
  36. end
  37. 1 def register_selenium(app)
  38. Capybara::Selenium::Driver.new(app, **{ browser: @browser.type }.merge(browser_options)).tap do |driver|
  39. driver.browser.manage.window.size = Selenium::WebDriver::Dimension.new(*@screen_size)
  40. end
  41. end
  42. 1 def register_poltergeist(app)
  43. Capybara::Poltergeist::Driver.new(app, @options.merge(window_size: @screen_size))
  44. end
  45. 1 def register_webkit(app)
  46. Capybara::Webkit::Driver.new(app, Capybara::Webkit::Configuration.to_hash.merge(@options)).tap do |driver|
  47. driver.resize_window_to(driver.current_window_handle, *@screen_size)
  48. end
  49. end
  50. 1 def setup
  51. Capybara.current_driver = @name
  52. end
  53. end
  54. end
  55. end

lib/action_dispatch/system_testing/server.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module SystemTesting
  4. 1 class Server # :nodoc:
  5. 1 class << self
  6. 1 attr_accessor :silence_puma
  7. end
  8. 1 self.silence_puma = false
  9. 1 def run
  10. 1 setup
  11. end
  12. 1 private
  13. 1 def setup
  14. 1 set_server
  15. 1 set_port
  16. end
  17. 1 def set_server
  18. 1 Capybara.server = :puma, { Silent: self.class.silence_puma } if Capybara.server == Capybara.servers[:default]
  19. end
  20. 1 def set_port
  21. 1 Capybara.always_include_port = true
  22. end
  23. end
  24. end
  25. end

lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb

42.11% lines covered

57 relevant lines. 24 lines covered and 33 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module SystemTesting
  4. 1 module TestHelpers
  5. # Screenshot helper for system testing.
  6. 1 module ScreenshotHelper
  7. # Takes a screenshot of the current page in the browser.
  8. #
  9. # +take_screenshot+ can be used at any point in your system tests to take
  10. # a screenshot of the current state. This can be useful for debugging or
  11. # automating visual testing. You can take multiple screenshots per test
  12. # to investigate changes at different points during your test. These will be
  13. # named with a sequential prefix (or 'failed' for failing tests)
  14. #
  15. # The screenshot will be displayed in your console, if supported.
  16. #
  17. # You can set the +RAILS_SYSTEM_TESTING_SCREENSHOT_HTML+ environment variable to
  18. # save the HTML from the page that is being screenhoted so you can investigate the
  19. # elements on the page at the time of the screenshot
  20. #
  21. # You can set the +RAILS_SYSTEM_TESTING_SCREENSHOT+ environment variable to
  22. # control the output. Possible values are:
  23. # * [+simple+ (default)] Only displays the screenshot path.
  24. # This is the default value.
  25. # * [+inline+] Display the screenshot in the terminal using the
  26. # iTerm image protocol (https://iterm2.com/documentation-images.html).
  27. # * [+artifact+] Display the screenshot in the terminal, using the terminal
  28. # artifact format (https://buildkite.github.io/terminal-to-html/inline-images/).
  29. 1 def take_screenshot
  30. increment_unique
  31. save_html if save_html?
  32. save_image
  33. puts display_image
  34. end
  35. # Takes a screenshot of the current page in the browser if the test
  36. # failed.
  37. #
  38. # +take_failed_screenshot+ is included in <tt>application_system_test_case.rb</tt>
  39. # that is generated with the application. To take screenshots when a test
  40. # fails add +take_failed_screenshot+ to the teardown block before clearing
  41. # sessions.
  42. 1 def take_failed_screenshot
  43. take_screenshot if failed? && supports_screenshot?
  44. end
  45. 1 private
  46. 1 attr_accessor :_screenshot_counter
  47. 1 def save_html?
  48. ENV["RAILS_SYSTEM_TESTING_SCREENSHOT_HTML"] == "1"
  49. end
  50. 1 def increment_unique
  51. @_screenshot_counter ||= 0
  52. @_screenshot_counter += 1
  53. end
  54. 1 def unique
  55. failed? ? "failures" : (_screenshot_counter || 0).to_s
  56. end
  57. 1 def image_name
  58. sanitized_method_name = method_name.tr("/\\", "--")
  59. name = "#{unique}_#{sanitized_method_name}"
  60. name[0...225]
  61. end
  62. 1 def image_path
  63. absolute_image_path.to_s
  64. end
  65. 1 def html_path
  66. absolute_html_path.to_s
  67. end
  68. 1 def absolute_path
  69. Rails.root.join("tmp/screenshots/#{image_name}")
  70. end
  71. 1 def absolute_image_path
  72. "#{absolute_path}.png"
  73. end
  74. 1 def absolute_html_path
  75. "#{absolute_path}.html"
  76. end
  77. 1 def save_html
  78. page.save_page(absolute_html_path)
  79. end
  80. 1 def save_image
  81. page.save_screenshot(absolute_image_path)
  82. end
  83. 1 def output_type
  84. # Environment variables have priority
  85. output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] || ENV["CAPYBARA_INLINE_SCREENSHOT"]
  86. # Default to outputting a path to the screenshot
  87. output_type ||= "simple"
  88. output_type
  89. end
  90. 1 def display_image
  91. message = +"[Screenshot Image]: #{image_path}\n"
  92. message << +"[Screenshot HTML]: #{html_path}\n" if save_html?
  93. case output_type
  94. when "artifact"
  95. message << "\e]1338;url=artifact://#{absolute_image_path}\a\n"
  96. when "inline"
  97. name = inline_base64(File.basename(absolute_image_path))
  98. image = inline_base64(File.read(absolute_image_path))
  99. message << "\e]1337;File=name=#{name};height=400px;inline=1:#{image}\a\n"
  100. end
  101. message
  102. end
  103. 1 def inline_base64(path)
  104. Base64.strict_encode64(path)
  105. end
  106. 1 def failed?
  107. !passed? && !skipped?
  108. end
  109. 1 def supports_screenshot?
  110. Capybara.current_driver != :rack_test
  111. end
  112. end
  113. end
  114. end
  115. end

lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb

53.85% lines covered

13 relevant lines. 7 lines covered and 6 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module SystemTesting
  4. 1 module TestHelpers
  5. 1 module SetupAndTeardown # :nodoc:
  6. 1 def host!(host)
  7. ActiveSupport::Deprecation.warn \
  8. "ActionDispatch::SystemTestCase#host! is deprecated with no replacement. " \
  9. "Set Capybara.app_host directly or rely on Capybara's default host."
  10. Capybara.app_host = host
  11. end
  12. 1 def before_teardown
  13. take_failed_screenshot
  14. ensure
  15. super
  16. end
  17. 1 def after_teardown
  18. Capybara.reset_sessions!
  19. ensure
  20. super
  21. end
  22. end
  23. end
  24. end
  25. end

lib/action_dispatch/testing/assertion_response.rb

0.0% lines covered

32 relevant lines. 0 lines covered and 32 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionDispatch
  3. # This is a class that abstracts away an asserted response. It purposely
  4. # does not inherit from Response because it doesn't need it. That means it
  5. # does not have headers or a body.
  6. class AssertionResponse
  7. attr_reader :code, :name
  8. GENERIC_RESPONSE_CODES = { # :nodoc:
  9. success: "2XX",
  10. missing: "404",
  11. redirect: "3XX",
  12. error: "5XX"
  13. }
  14. # Accepts a specific response status code as an Integer (404) or String
  15. # ('404') or a response status range as a Symbol pseudo-code (:success,
  16. # indicating any 200-299 status code).
  17. def initialize(code_or_name)
  18. if code_or_name.is_a?(Symbol)
  19. @name = code_or_name
  20. @code = code_from_name(code_or_name)
  21. else
  22. @name = name_from_code(code_or_name)
  23. @code = code_or_name
  24. end
  25. raise ArgumentError, "Invalid response name: #{name}" if @code.nil?
  26. raise ArgumentError, "Invalid response code: #{code}" if @name.nil?
  27. end
  28. def code_and_name
  29. "#{code}: #{name}"
  30. end
  31. private
  32. def code_from_name(name)
  33. GENERIC_RESPONSE_CODES[name] || Rack::Utils::SYMBOL_TO_STATUS_CODE[name]
  34. end
  35. def name_from_code(code)
  36. GENERIC_RESPONSE_CODES.invert[code] || Rack::Utils::HTTP_STATUS_CODES[code]
  37. end
  38. end
  39. end

lib/action_dispatch/testing/assertions.rb

76.92% lines covered

13 relevant lines. 10 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "rails-dom-testing"
  3. 1 module ActionDispatch
  4. 1 module Assertions
  5. 1 autoload :ResponseAssertions, "action_dispatch/testing/assertions/response"
  6. 1 autoload :RoutingAssertions, "action_dispatch/testing/assertions/routing"
  7. 1 extend ActiveSupport::Concern
  8. 1 include ResponseAssertions
  9. 1 include RoutingAssertions
  10. 1 include Rails::Dom::Testing::Assertions
  11. 1 def html_document
  12. @html_document ||= if @response.media_type&.end_with?("xml")
  13. Nokogiri::XML::Document.parse(@response.body)
  14. else
  15. Nokogiri::HTML::Document.parse(@response.body)
  16. end
  17. end
  18. end
  19. end

lib/action_dispatch/testing/assertions/response.rb

34.21% lines covered

38 relevant lines. 13 lines covered and 25 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 module Assertions
  4. # A small suite of assertions that test responses from \Rails applications.
  5. 1 module ResponseAssertions
  6. 1 RESPONSE_PREDICATES = { # :nodoc:
  7. success: :successful?,
  8. missing: :not_found?,
  9. redirect: :redirection?,
  10. error: :server_error?,
  11. }
  12. # Asserts that the response is one of the following types:
  13. #
  14. # * <tt>:success</tt> - Status code was in the 200-299 range
  15. # * <tt>:redirect</tt> - Status code was in the 300-399 range
  16. # * <tt>:missing</tt> - Status code was 404
  17. # * <tt>:error</tt> - Status code was in the 500-599 range
  18. #
  19. # You can also pass an explicit status number like <tt>assert_response(501)</tt>
  20. # or its symbolic equivalent <tt>assert_response(:not_implemented)</tt>.
  21. # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list.
  22. #
  23. # # Asserts that the response was a redirection
  24. # assert_response :redirect
  25. #
  26. # # Asserts that the response code was status code 401 (unauthorized)
  27. # assert_response 401
  28. 1 def assert_response(type, message = nil)
  29. message ||= generate_response_message(type)
  30. if RESPONSE_PREDICATES.keys.include?(type)
  31. assert @response.send(RESPONSE_PREDICATES[type]), message
  32. else
  33. assert_equal AssertionResponse.new(type).code, @response.response_code, message
  34. end
  35. end
  36. # Asserts that the response is a redirect to a URL matching the given options.
  37. #
  38. # # Asserts that the redirection was to the "index" action on the WeblogController
  39. # assert_redirected_to controller: "weblog", action: "index"
  40. #
  41. # # Asserts that the redirection was to the named route login_url
  42. # assert_redirected_to login_url
  43. #
  44. # # Asserts that the redirection was to the URL for @customer
  45. # assert_redirected_to @customer
  46. #
  47. # # Asserts that the redirection matches the regular expression
  48. # assert_redirected_to %r(\Ahttp://example.org)
  49. 1 def assert_redirected_to(options = {}, message = nil)
  50. assert_response(:redirect, message)
  51. return true if options === @response.location
  52. redirect_is = normalize_argument_to_redirection(@response.location)
  53. redirect_expected = normalize_argument_to_redirection(options)
  54. message ||= "Expected response to be a redirect to <#{redirect_expected}> but was a redirect to <#{redirect_is}>"
  55. assert_operator redirect_expected, :===, redirect_is, message
  56. end
  57. 1 private
  58. # Proxy to to_param if the object will respond to it.
  59. 1 def parameterize(value)
  60. value.respond_to?(:to_param) ? value.to_param : value
  61. end
  62. 1 def normalize_argument_to_redirection(fragment)
  63. if Regexp === fragment
  64. fragment
  65. else
  66. handle = @controller || ActionController::Redirecting
  67. handle._compute_redirect_to_location(@request, fragment)
  68. end
  69. end
  70. 1 def generate_response_message(expected, actual = @response.response_code)
  71. (+"Expected response to be a <#{code_with_name(expected)}>,"\
  72. " but was a <#{code_with_name(actual)}>").concat(location_if_redirected).concat(response_body_if_short)
  73. end
  74. 1 def response_body_if_short
  75. return "" if @response.body.size > 500
  76. "\nResponse body: #{@response.body}"
  77. end
  78. 1 def location_if_redirected
  79. return "" unless @response.redirection? && @response.location.present?
  80. location = normalize_argument_to_redirection(@response.location)
  81. " redirect to <#{location}>"
  82. end
  83. 1 def code_with_name(code_or_name)
  84. if RESPONSE_PREDICATES.values.include?("#{code_or_name}?".to_sym)
  85. code_or_name = RESPONSE_PREDICATES.invert["#{code_or_name}?".to_sym]
  86. end
  87. AssertionResponse.new(code_or_name).code_and_name
  88. end
  89. end
  90. end
  91. end

lib/action_dispatch/testing/assertions/routing.rb

18.6% lines covered

86 relevant lines. 16 lines covered and 70 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "uri"
  3. 1 require "active_support/core_ext/hash/indifferent_access"
  4. 1 require "active_support/core_ext/string/access"
  5. 1 require "action_controller/metal/exceptions"
  6. 1 module ActionDispatch
  7. 1 module Assertions
  8. # Suite of assertions to test routes generated by \Rails and the handling of requests made to them.
  9. 1 module RoutingAssertions
  10. 1 def setup # :nodoc:
  11. @routes ||= nil
  12. super
  13. end
  14. # Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash)
  15. # match +path+. Basically, it asserts that \Rails recognizes the route given by +expected_options+.
  16. #
  17. # Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes
  18. # requiring a specific HTTP method. The hash should contain a :path with the incoming request path
  19. # and a :method containing the required HTTP verb.
  20. #
  21. # # Asserts that POSTing to /items will call the create action on ItemsController
  22. # assert_recognizes({controller: 'items', action: 'create'}, {path: 'items', method: :post})
  23. #
  24. # You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used
  25. # to assert that values in the query string will end up in the params hash correctly. To test query strings you must use the extras
  26. # argument because appending the query string on the path directly will not work. For example:
  27. #
  28. # # Asserts that a path of '/items/list/1?view=print' returns the correct options
  29. # assert_recognizes({controller: 'items', action: 'list', id: '1', view: 'print'}, 'items/list/1', { view: "print" })
  30. #
  31. # The +message+ parameter allows you to pass in an error message that is displayed upon failure.
  32. #
  33. # # Check the default route (i.e., the index action)
  34. # assert_recognizes({controller: 'items', action: 'index'}, 'items')
  35. #
  36. # # Test a specific action
  37. # assert_recognizes({controller: 'items', action: 'list'}, 'items/list')
  38. #
  39. # # Test an action with a parameter
  40. # assert_recognizes({controller: 'items', action: 'destroy', id: '1'}, 'items/destroy/1')
  41. #
  42. # # Test a custom route
  43. # assert_recognizes({controller: 'items', action: 'show', id: '1'}, 'view/item1')
  44. 1 def assert_recognizes(expected_options, path, extras = {}, msg = nil)
  45. if path.is_a?(Hash) && path[:method].to_s == "all"
  46. [:get, :post, :put, :delete].each do |method|
  47. assert_recognizes(expected_options, path.merge(method: method), extras, msg)
  48. end
  49. else
  50. request = recognized_request_for(path, extras, msg)
  51. expected_options = expected_options.clone
  52. expected_options.stringify_keys!
  53. msg = message(msg, "") {
  54. sprintf("The recognized options <%s> did not match <%s>, difference:",
  55. request.path_parameters, expected_options)
  56. }
  57. assert_equal(expected_options, request.path_parameters, msg)
  58. end
  59. end
  60. # Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+.
  61. # The +extras+ parameter is used to tell the request the names and values of additional request parameters that would be in
  62. # a query string. The +message+ parameter allows you to specify a custom error message for assertion failures.
  63. #
  64. # The +defaults+ parameter is unused.
  65. #
  66. # # Asserts that the default action is generated for a route with no action
  67. # assert_generates "/items", controller: "items", action: "index"
  68. #
  69. # # Tests that the list action is properly routed
  70. # assert_generates "/items/list", controller: "items", action: "list"
  71. #
  72. # # Tests the generation of a route with a parameter
  73. # assert_generates "/items/list/1", { controller: "items", action: "list", id: "1" }
  74. #
  75. # # Asserts that the generated route gives us our custom route
  76. # assert_generates "changesets/12", { controller: 'scm', action: 'show_diff', revision: "12" }
  77. 1 def assert_generates(expected_path, options, defaults = {}, extras = {}, message = nil)
  78. if %r{://}.match?(expected_path)
  79. fail_on(URI::InvalidURIError, message) do
  80. uri = URI.parse(expected_path)
  81. expected_path = uri.path.to_s.empty? ? "/" : uri.path
  82. end
  83. else
  84. expected_path = "/#{expected_path}" unless expected_path.start_with?("/")
  85. end
  86. # Load routes.rb if it hasn't been loaded.
  87. options = options.clone
  88. generated_path, query_string_keys = @routes.generate_extras(options, defaults)
  89. found_extras = options.reject { |k, _| ! query_string_keys.include? k }
  90. msg = message || sprintf("found extras <%s>, not <%s>", found_extras, extras)
  91. assert_equal(extras, found_extras, msg)
  92. msg = message || sprintf("The generated path <%s> did not match <%s>", generated_path,
  93. expected_path)
  94. assert_equal(expected_path, generated_path, msg)
  95. end
  96. # Asserts that path and options match both ways; in other words, it verifies that <tt>path</tt> generates
  97. # <tt>options</tt> and then that <tt>options</tt> generates <tt>path</tt>. This essentially combines +assert_recognizes+
  98. # and +assert_generates+ into one step.
  99. #
  100. # The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The
  101. # +message+ parameter allows you to specify a custom error message to display upon failure.
  102. #
  103. # # Asserts a basic route: a controller with the default action (index)
  104. # assert_routing '/home', controller: 'home', action: 'index'
  105. #
  106. # # Test a route generated with a specific controller, action, and parameter (id)
  107. # assert_routing '/entries/show/23', controller: 'entries', action: 'show', id: 23
  108. #
  109. # # Asserts a basic route (controller + default action), with an error message if it fails
  110. # assert_routing '/store', { controller: 'store', action: 'index' }, {}, {}, 'Route for store index not generated properly'
  111. #
  112. # # Tests a route, providing a defaults hash
  113. # assert_routing 'controller/action/9', {id: "9", item: "square"}, {controller: "controller", action: "action"}, {}, {item: "square"}
  114. #
  115. # # Tests a route with an HTTP method
  116. # assert_routing({ method: 'put', path: '/product/321' }, { controller: "product", action: "update", id: "321" })
  117. 1 def assert_routing(path, options, defaults = {}, extras = {}, message = nil)
  118. assert_recognizes(options, path, extras, message)
  119. controller, default_controller = options[:controller], defaults[:controller]
  120. if controller && controller.include?(?/) && default_controller && default_controller.include?(?/)
  121. options[:controller] = "/#{controller}"
  122. end
  123. generate_options = options.dup.delete_if { |k, _| defaults.key?(k) }
  124. assert_generates(path.is_a?(Hash) ? path[:path] : path, generate_options, defaults, extras, message)
  125. end
  126. # A helper to make it easier to test different route configurations.
  127. # This method temporarily replaces @routes with a new RouteSet instance.
  128. #
  129. # The new instance is yielded to the passed block. Typically the block
  130. # will create some routes using <tt>set.draw { match ... }</tt>:
  131. #
  132. # with_routing do |set|
  133. # set.draw do
  134. # resources :users
  135. # end
  136. # assert_equal "/users", users_path
  137. # end
  138. #
  139. 1 def with_routing
  140. old_routes, @routes = @routes, ActionDispatch::Routing::RouteSet.new
  141. if defined?(@controller) && @controller
  142. old_controller, @controller = @controller, @controller.clone
  143. _routes = @routes
  144. @controller.singleton_class.include(_routes.url_helpers)
  145. if @controller.respond_to? :view_context_class
  146. view_context_class = Class.new(@controller.view_context_class) do
  147. include _routes.url_helpers
  148. end
  149. custom_view_context = Module.new {
  150. define_method(:view_context_class) do
  151. view_context_class
  152. end
  153. }
  154. @controller.extend(custom_view_context)
  155. end
  156. end
  157. yield @routes
  158. ensure
  159. @routes = old_routes
  160. if defined?(@controller) && @controller
  161. @controller = old_controller
  162. end
  163. end
  164. # ROUTES TODO: These assertions should really work in an integration context
  165. 1 def method_missing(selector, *args, &block)
  166. if defined?(@controller) && @controller && defined?(@routes) && @routes && @routes.named_routes.route_defined?(selector)
  167. @controller.send(selector, *args, &block)
  168. else
  169. super
  170. end
  171. end
  172. 1 private
  173. # Recognizes the route for a given path.
  174. 1 def recognized_request_for(path, extras = {}, msg)
  175. if path.is_a?(Hash)
  176. method = path[:method]
  177. path = path[:path]
  178. else
  179. method = :get
  180. end
  181. controller = @controller if defined?(@controller)
  182. request = ActionController::TestRequest.create controller&.class
  183. if %r{://}.match?(path)
  184. fail_on(URI::InvalidURIError, msg) do
  185. uri = URI.parse(path)
  186. request.env["rack.url_scheme"] = uri.scheme || "http"
  187. request.host = uri.host if uri.host
  188. request.port = uri.port if uri.port
  189. request.path = uri.path.to_s.empty? ? "/" : uri.path
  190. end
  191. else
  192. path = "/#{path}" unless path.start_with?("/")
  193. request.path = path
  194. end
  195. request.request_method = method if method
  196. params = fail_on(ActionController::RoutingError, msg) do
  197. @routes.recognize_path(path, method: method, extras: extras)
  198. end
  199. request.path_parameters = params.with_indifferent_access
  200. request
  201. end
  202. 1 def fail_on(exception_class, message)
  203. yield
  204. rescue exception_class => e
  205. raise Minitest::Assertion, message || e.message
  206. end
  207. end
  208. end
  209. end

lib/action_dispatch/testing/integration.rb

45.37% lines covered

205 relevant lines. 93 lines covered and 112 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "stringio"
  3. 1 require "uri"
  4. 1 require "rack/test"
  5. 1 require "minitest"
  6. 1 require "action_dispatch/testing/request_encoder"
  7. 1 module ActionDispatch
  8. 1 module Integration #:nodoc:
  9. 1 module RequestHelpers
  10. # Performs a GET request with the given parameters. See ActionDispatch::Integration::Session#process
  11. # for more details.
  12. 1 def get(path, **args)
  13. process(:get, path, **args)
  14. end
  15. # Performs a POST request with the given parameters. See ActionDispatch::Integration::Session#process
  16. # for more details.
  17. 1 def post(path, **args)
  18. process(:post, path, **args)
  19. end
  20. # Performs a PATCH request with the given parameters. See ActionDispatch::Integration::Session#process
  21. # for more details.
  22. 1 def patch(path, **args)
  23. process(:patch, path, **args)
  24. end
  25. # Performs a PUT request with the given parameters. See ActionDispatch::Integration::Session#process
  26. # for more details.
  27. 1 def put(path, **args)
  28. process(:put, path, **args)
  29. end
  30. # Performs a DELETE request with the given parameters. See ActionDispatch::Integration::Session#process
  31. # for more details.
  32. 1 def delete(path, **args)
  33. process(:delete, path, **args)
  34. end
  35. # Performs a HEAD request with the given parameters. See ActionDispatch::Integration::Session#process
  36. # for more details.
  37. 1 def head(path, **args)
  38. process(:head, path, **args)
  39. end
  40. # Performs an OPTIONS request with the given parameters. See ActionDispatch::Integration::Session#process
  41. # for more details.
  42. 1 def options(path, **args)
  43. process(:options, path, **args)
  44. end
  45. # Follow a single redirect response. If the last response was not a
  46. # redirect, an exception will be raised. Otherwise, the redirect is
  47. # performed on the location header. If the redirection is a 307 or 308 redirect,
  48. # the same HTTP verb will be used when redirecting, otherwise a GET request
  49. # will be performed. Any arguments are passed to the
  50. # underlying request.
  51. 1 def follow_redirect!(**args)
  52. raise "not a redirect! #{status} #{status_message}" unless redirect?
  53. method =
  54. if [307, 308].include?(response.status)
  55. request.method.downcase
  56. else
  57. :get
  58. end
  59. public_send(method, response.location, **args)
  60. status
  61. end
  62. end
  63. # An instance of this class represents a set of requests and responses
  64. # performed sequentially by a test process. Because you can instantiate
  65. # multiple sessions and run them side-by-side, you can also mimic (to some
  66. # limited extent) multiple simultaneous users interacting with your system.
  67. #
  68. # Typically, you will instantiate a new session using
  69. # IntegrationTest#open_session, rather than instantiating
  70. # Integration::Session directly.
  71. 1 class Session
  72. 1 DEFAULT_HOST = "www.example.com"
  73. 1 include Minitest::Assertions
  74. 1 include TestProcess, RequestHelpers, Assertions
  75. 1 delegate :status, :status_message, :headers, :body, :redirect?, to: :response, allow_nil: true
  76. 1 delegate :path, to: :request, allow_nil: true
  77. # The hostname used in the last request.
  78. 1 def host
  79. @host || DEFAULT_HOST
  80. end
  81. 1 attr_writer :host
  82. # The remote_addr used in the last request.
  83. 1 attr_accessor :remote_addr
  84. # The Accept header to send.
  85. 1 attr_accessor :accept
  86. # A map of the cookies returned by the last response, and which will be
  87. # sent with the next request.
  88. 1 def cookies
  89. _mock_session.cookie_jar
  90. end
  91. # A reference to the controller instance used by the last request.
  92. 1 attr_reader :controller
  93. # A reference to the request instance used by the last request.
  94. 1 attr_reader :request
  95. # A reference to the response instance used by the last request.
  96. 1 attr_reader :response
  97. # A running counter of the number of requests processed.
  98. 1 attr_accessor :request_count
  99. 1 include ActionDispatch::Routing::UrlFor
  100. # Create and initialize a new Session instance.
  101. 1 def initialize(app)
  102. super()
  103. @app = app
  104. reset!
  105. end
  106. 1 def url_options
  107. @url_options ||= default_url_options.dup.tap do |url_options|
  108. url_options.reverse_merge!(controller.url_options) if controller.respond_to?(:url_options)
  109. if @app.respond_to?(:routes)
  110. url_options.reverse_merge!(@app.routes.default_url_options)
  111. end
  112. url_options.reverse_merge!(host: host, protocol: https? ? "https" : "http")
  113. end
  114. end
  115. # Resets the instance. This can be used to reset the state information
  116. # in an existing session instance, so it can be used from a clean-slate
  117. # condition.
  118. #
  119. # session.reset!
  120. 1 def reset!
  121. @https = false
  122. @controller = @request = @response = nil
  123. @_mock_session = nil
  124. @request_count = 0
  125. @url_options = nil
  126. self.host = DEFAULT_HOST
  127. self.remote_addr = "127.0.0.1"
  128. self.accept = "text/xml,application/xml,application/xhtml+xml," \
  129. "text/html;q=0.9,text/plain;q=0.8,image/png," \
  130. "*/*;q=0.5"
  131. unless defined? @named_routes_configured
  132. # the helpers are made protected by default--we make them public for
  133. # easier access during testing and troubleshooting.
  134. @named_routes_configured = true
  135. end
  136. end
  137. # Specify whether or not the session should mimic a secure HTTPS request.
  138. #
  139. # session.https!
  140. # session.https!(false)
  141. 1 def https!(flag = true)
  142. @https = flag
  143. end
  144. # Returns +true+ if the session is mimicking a secure HTTPS request.
  145. #
  146. # if session.https?
  147. # ...
  148. # end
  149. 1 def https?
  150. @https
  151. end
  152. # Performs the actual request.
  153. #
  154. # - +method+: The HTTP method (GET, POST, PATCH, PUT, DELETE, HEAD, OPTIONS)
  155. # as a symbol.
  156. # - +path+: The URI (as a String) on which you want to perform the
  157. # request.
  158. # - +params+: The HTTP parameters that you want to pass. This may
  159. # be +nil+,
  160. # a Hash, or a String that is appropriately encoded
  161. # (<tt>application/x-www-form-urlencoded</tt> or
  162. # <tt>multipart/form-data</tt>).
  163. # - +headers+: Additional headers to pass, as a Hash. The headers will be
  164. # merged into the Rack env hash.
  165. # - +env+: Additional env to pass, as a Hash. The headers will be
  166. # merged into the Rack env hash.
  167. # - +xhr+: Set to `true` if you want to make and Ajax request.
  168. # Adds request headers characteristic of XMLHttpRequest e.g. HTTP_X_REQUESTED_WITH.
  169. # The headers will be merged into the Rack env hash.
  170. # - +as+: Used for encoding the request with different content type.
  171. # Supports `:json` by default and will set the appropriate request headers.
  172. # The headers will be merged into the Rack env hash.
  173. #
  174. # This method is rarely used directly. Use +#get+, +#post+, or other standard
  175. # HTTP methods in integration tests. +#process+ is only required when using a
  176. # request method that doesn't have a method defined in the integration tests.
  177. #
  178. # This method returns the response status, after performing the request.
  179. # Furthermore, if this method was called from an ActionDispatch::IntegrationTest object,
  180. # then that object's <tt>@response</tt> instance variable will point to a Response object
  181. # which one can use to inspect the details of the response.
  182. #
  183. # Example:
  184. # process :get, '/author', params: { since: 201501011400 }
  185. 1 def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil)
  186. request_encoder = RequestEncoder.encoder(as)
  187. headers ||= {}
  188. if method == :get && as == :json && params
  189. headers["X-Http-Method-Override"] = "GET"
  190. method = :post
  191. end
  192. if %r{://}.match?(path)
  193. path = build_expanded_path(path) do |location|
  194. https! URI::HTTPS === location if location.scheme
  195. if url_host = location.host
  196. default = Rack::Request::DEFAULT_PORTS[location.scheme]
  197. url_host += ":#{location.port}" if default != location.port
  198. host! url_host
  199. end
  200. end
  201. end
  202. hostname, port = host.split(":")
  203. request_env = {
  204. :method => method,
  205. :params => request_encoder.encode_params(params),
  206. "SERVER_NAME" => hostname,
  207. "SERVER_PORT" => port || (https? ? "443" : "80"),
  208. "HTTPS" => https? ? "on" : "off",
  209. "rack.url_scheme" => https? ? "https" : "http",
  210. "REQUEST_URI" => path,
  211. "HTTP_HOST" => host,
  212. "REMOTE_ADDR" => remote_addr,
  213. "CONTENT_TYPE" => request_encoder.content_type,
  214. "HTTP_ACCEPT" => request_encoder.accept_header || accept
  215. }
  216. wrapped_headers = Http::Headers.from_hash({})
  217. wrapped_headers.merge!(headers) if headers
  218. if xhr
  219. wrapped_headers["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest"
  220. wrapped_headers["HTTP_ACCEPT"] ||= [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
  221. end
  222. # This modifies the passed request_env directly.
  223. if wrapped_headers.present?
  224. Http::Headers.from_hash(request_env).merge!(wrapped_headers)
  225. end
  226. if env.present?
  227. Http::Headers.from_hash(request_env).merge!(env)
  228. end
  229. session = Rack::Test::Session.new(_mock_session)
  230. # NOTE: rack-test v0.5 doesn't build a default uri correctly
  231. # Make sure requested path is always a full URI.
  232. session.request(build_full_uri(path, request_env), request_env)
  233. @request_count += 1
  234. @request = ActionDispatch::Request.new(session.last_request.env)
  235. response = _mock_session.last_response
  236. @response = ActionDispatch::TestResponse.from_response(response)
  237. @response.request = @request
  238. @html_document = nil
  239. @url_options = nil
  240. @controller = @request.controller_instance
  241. response.status
  242. end
  243. # Set the host name to use in the next request.
  244. #
  245. # session.host! "www.example.com"
  246. 1 alias :host! :host=
  247. 1 private
  248. 1 def _mock_session
  249. @_mock_session ||= Rack::MockSession.new(@app, host)
  250. end
  251. 1 def build_full_uri(path, env)
  252. "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}"
  253. end
  254. 1 def build_expanded_path(path)
  255. location = URI.parse(path)
  256. yield location if block_given?
  257. path = location.path
  258. location.query ? "#{path}?#{location.query}" : path
  259. end
  260. end
  261. 1 module Runner
  262. 1 include ActionDispatch::Assertions
  263. 1 APP_SESSIONS = {}
  264. 1 attr_reader :app
  265. 1 attr_accessor :root_session # :nodoc:
  266. 1 def initialize(*args, &blk)
  267. super(*args, &blk)
  268. @integration_session = nil
  269. end
  270. 1 def before_setup # :nodoc:
  271. @app = nil
  272. super
  273. end
  274. 1 def integration_session
  275. @integration_session ||= create_session(app)
  276. end
  277. # Reset the current session. This is useful for testing multiple sessions
  278. # in a single test case.
  279. 1 def reset!
  280. @integration_session = create_session(app)
  281. end
  282. 1 def create_session(app)
  283. klass = APP_SESSIONS[app] ||= Class.new(Integration::Session) {
  284. # If the app is a Rails app, make url_helpers available on the session.
  285. # This makes app.url_for and app.foo_path available in the console.
  286. if app.respond_to?(:routes) && app.routes.is_a?(ActionDispatch::Routing::RouteSet)
  287. include app.routes.url_helpers
  288. include app.routes.mounted_helpers
  289. end
  290. }
  291. klass.new(app)
  292. end
  293. 1 def remove! # :nodoc:
  294. @integration_session = nil
  295. end
  296. 1 %w(get post patch put head delete cookies assigns follow_redirect!).each do |method|
  297. # reset the html_document variable, except for cookies/assigns calls
  298. 9 unless method == "cookies" || method == "assigns"
  299. 7 reset_html_document = "@html_document = nil"
  300. end
  301. 9 definition = RUBY_VERSION >= "2.7" ? "..." : "*args"
  302. 9 module_eval <<~RUBY, __FILE__, __LINE__ + 1
  303. def #{method}(#{definition})
  304. #{reset_html_document}
  305. result = integration_session.#{method}(#{definition})
  306. copy_session_variables!
  307. result
  308. end
  309. RUBY
  310. end
  311. # Open a new session instance. If a block is given, the new session is
  312. # yielded to the block before being returned.
  313. #
  314. # session = open_session do |sess|
  315. # sess.extend(CustomAssertions)
  316. # end
  317. #
  318. # By default, a single session is automatically created for you, but you
  319. # can use this method to open multiple sessions that ought to be tested
  320. # simultaneously.
  321. 1 def open_session
  322. dup.tap do |session|
  323. session.reset!
  324. session.root_session = self.root_session || self
  325. yield session if block_given?
  326. end
  327. end
  328. 1 def assertions # :nodoc:
  329. root_session ? root_session.assertions : super
  330. end
  331. 1 def assertions=(assertions) # :nodoc:
  332. root_session ? root_session.assertions = assertions : super
  333. end
  334. # Copy the instance variables from the current session instance into the
  335. # test instance.
  336. 1 def copy_session_variables! #:nodoc:
  337. @controller = @integration_session.controller
  338. @response = @integration_session.response
  339. @request = @integration_session.request
  340. end
  341. 1 def default_url_options
  342. integration_session.default_url_options
  343. end
  344. 1 def default_url_options=(options)
  345. integration_session.default_url_options = options
  346. end
  347. 1 private
  348. 1 def respond_to_missing?(method, _)
  349. integration_session.respond_to?(method) || super
  350. end
  351. # Delegate unhandled messages to the current session instance.
  352. 1 def method_missing(method, *args, &block)
  353. if integration_session.respond_to?(method)
  354. integration_session.public_send(method, *args, &block).tap do
  355. copy_session_variables!
  356. end
  357. else
  358. super
  359. end
  360. end
  361. 1 ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
  362. end
  363. end
  364. # An integration test spans multiple controllers and actions,
  365. # tying them all together to ensure they work together as expected. It tests
  366. # more completely than either unit or functional tests do, exercising the
  367. # entire stack, from the dispatcher to the database.
  368. #
  369. # At its simplest, you simply extend <tt>IntegrationTest</tt> and write your tests
  370. # using the get/post methods:
  371. #
  372. # require "test_helper"
  373. #
  374. # class ExampleTest < ActionDispatch::IntegrationTest
  375. # fixtures :people
  376. #
  377. # def test_login
  378. # # get the login page
  379. # get "/login"
  380. # assert_equal 200, status
  381. #
  382. # # post the login and follow through to the home page
  383. # post "/login", params: { username: people(:jamis).username,
  384. # password: people(:jamis).password }
  385. # follow_redirect!
  386. # assert_equal 200, status
  387. # assert_equal "/home", path
  388. # end
  389. # end
  390. #
  391. # However, you can also have multiple session instances open per test, and
  392. # even extend those instances with assertions and methods to create a very
  393. # powerful testing DSL that is specific for your application. You can even
  394. # reference any named routes you happen to have defined.
  395. #
  396. # require "test_helper"
  397. #
  398. # class AdvancedTest < ActionDispatch::IntegrationTest
  399. # fixtures :people, :rooms
  400. #
  401. # def test_login_and_speak
  402. # jamis, david = login(:jamis), login(:david)
  403. # room = rooms(:office)
  404. #
  405. # jamis.enter(room)
  406. # jamis.speak(room, "anybody home?")
  407. #
  408. # david.enter(room)
  409. # david.speak(room, "hello!")
  410. # end
  411. #
  412. # private
  413. #
  414. # module CustomAssertions
  415. # def enter(room)
  416. # # reference a named route, for maximum internal consistency!
  417. # get(room_url(id: room.id))
  418. # assert(...)
  419. # ...
  420. # end
  421. #
  422. # def speak(room, message)
  423. # post "/say/#{room.id}", xhr: true, params: { message: message }
  424. # assert(...)
  425. # ...
  426. # end
  427. # end
  428. #
  429. # def login(who)
  430. # open_session do |sess|
  431. # sess.extend(CustomAssertions)
  432. # who = people(who)
  433. # sess.post "/login", params: { username: who.username,
  434. # password: who.password }
  435. # assert(...)
  436. # end
  437. # end
  438. # end
  439. #
  440. # Another longer example would be:
  441. #
  442. # A simple integration test that exercises multiple controllers:
  443. #
  444. # require "test_helper"
  445. #
  446. # class UserFlowsTest < ActionDispatch::IntegrationTest
  447. # test "login and browse site" do
  448. # # login via https
  449. # https!
  450. # get "/login"
  451. # assert_response :success
  452. #
  453. # post "/login", params: { username: users(:david).username, password: users(:david).password }
  454. # follow_redirect!
  455. # assert_equal '/welcome', path
  456. # assert_equal 'Welcome david!', flash[:notice]
  457. #
  458. # https!(false)
  459. # get "/articles/all"
  460. # assert_response :success
  461. # assert_select 'h1', 'Articles'
  462. # end
  463. # end
  464. #
  465. # As you can see the integration test involves multiple controllers and
  466. # exercises the entire stack from database to dispatcher. In addition you can
  467. # have multiple session instances open simultaneously in a test and extend
  468. # those instances with assertion methods to create a very powerful testing
  469. # DSL (domain-specific language) just for your application.
  470. #
  471. # Here's an example of multiple sessions and custom DSL in an integration test
  472. #
  473. # require "test_helper"
  474. #
  475. # class UserFlowsTest < ActionDispatch::IntegrationTest
  476. # test "login and browse site" do
  477. # # User david logs in
  478. # david = login(:david)
  479. # # User guest logs in
  480. # guest = login(:guest)
  481. #
  482. # # Both are now available in different sessions
  483. # assert_equal 'Welcome david!', david.flash[:notice]
  484. # assert_equal 'Welcome guest!', guest.flash[:notice]
  485. #
  486. # # User david can browse site
  487. # david.browses_site
  488. # # User guest can browse site as well
  489. # guest.browses_site
  490. #
  491. # # Continue with other assertions
  492. # end
  493. #
  494. # private
  495. #
  496. # module CustomDsl
  497. # def browses_site
  498. # get "/products/all"
  499. # assert_response :success
  500. # assert_select 'h1', 'Products'
  501. # end
  502. # end
  503. #
  504. # def login(user)
  505. # open_session do |sess|
  506. # sess.extend(CustomDsl)
  507. # u = users(user)
  508. # sess.https!
  509. # sess.post "/login", params: { username: u.username, password: u.password }
  510. # assert_equal '/welcome', sess.path
  511. # sess.https!(false)
  512. # end
  513. # end
  514. # end
  515. #
  516. # See the {request helpers documentation}[rdoc-ref:ActionDispatch::Integration::RequestHelpers] for help on how to
  517. # use +get+, etc.
  518. #
  519. # === Changing the request encoding
  520. #
  521. # You can also test your JSON API easily by setting what the request should
  522. # be encoded as:
  523. #
  524. # require "test_helper"
  525. #
  526. # class ApiTest < ActionDispatch::IntegrationTest
  527. # test "creates articles" do
  528. # assert_difference -> { Article.count } do
  529. # post articles_path, params: { article: { title: "Ahoy!" } }, as: :json
  530. # end
  531. #
  532. # assert_response :success
  533. # assert_equal({ id: Article.last.id, title: "Ahoy!" }, response.parsed_body)
  534. # end
  535. # end
  536. #
  537. # The +as+ option passes an "application/json" Accept header (thereby setting
  538. # the request format to JSON unless overridden), sets the content type to
  539. # "application/json" and encodes the parameters as JSON.
  540. #
  541. # Calling +parsed_body+ on the response parses the response body based on the
  542. # last response MIME type.
  543. #
  544. # Out of the box, only <tt>:json</tt> is supported. But for any custom MIME
  545. # types you've registered, you can add your own encoders with:
  546. #
  547. # ActionDispatch::IntegrationTest.register_encoder :wibble,
  548. # param_encoder: -> params { params.to_wibble },
  549. # response_parser: -> body { body }
  550. #
  551. # Where +param_encoder+ defines how the params should be encoded and
  552. # +response_parser+ defines how the response body should be parsed through
  553. # +parsed_body+.
  554. #
  555. # Consult the Rails Testing Guide for more.
  556. 1 class IntegrationTest < ActiveSupport::TestCase
  557. 1 include TestProcess::FixtureFile
  558. 1 module UrlOptions
  559. 1 extend ActiveSupport::Concern
  560. 1 def url_options
  561. integration_session.url_options
  562. end
  563. end
  564. 1 module Behavior
  565. 1 extend ActiveSupport::Concern
  566. 1 include Integration::Runner
  567. 1 include ActionController::TemplateAssertions
  568. 1 included do
  569. 1 include ActionDispatch::Routing::UrlFor
  570. 1 include UrlOptions # don't let UrlFor override the url_options method
  571. 1 ActiveSupport.run_load_hooks(:action_dispatch_integration_test, self)
  572. 1 @@app = nil
  573. end
  574. 1 module ClassMethods
  575. 1 def app
  576. 1 if defined?(@@app) && @@app
  577. 1 @@app
  578. else
  579. ActionDispatch.test_app
  580. end
  581. end
  582. 1 def app=(app)
  583. 1 @@app = app
  584. end
  585. 1 def register_encoder(*args, **options)
  586. RequestEncoder.register_encoder(*args, **options)
  587. end
  588. end
  589. 1 def app
  590. super || self.class.app
  591. end
  592. 1 def document_root_element
  593. html_document.root
  594. end
  595. end
  596. 1 include Behavior
  597. end
  598. end

lib/action_dispatch/testing/request_encoder.rb

75.86% lines covered

29 relevant lines. 22 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionDispatch
  3. 1 class RequestEncoder # :nodoc:
  4. 1 class IdentityEncoder
  5. 1 def content_type; end
  6. 1 def accept_header; end
  7. 1 def encode_params(params); params; end
  8. 1 def response_parser; -> body { body }; end
  9. end
  10. 1 @encoders = { identity: IdentityEncoder.new }
  11. 1 attr_reader :response_parser
  12. 1 def initialize(mime_name, param_encoder, response_parser)
  13. 1 @mime = Mime[mime_name]
  14. 1 unless @mime
  15. raise ArgumentError, "Can't register a request encoder for " \
  16. "unregistered MIME Type: #{mime_name}. See `Mime::Type.register`."
  17. end
  18. 1 @response_parser = response_parser || -> body { body }
  19. 1 @param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc
  20. end
  21. 1 def content_type
  22. @mime.to_s
  23. end
  24. 1 def accept_header
  25. @mime.to_s
  26. end
  27. 1 def encode_params(params)
  28. @param_encoder.call(params) if params
  29. end
  30. 1 def self.parser(content_type)
  31. type = Mime::Type.lookup(content_type).ref if content_type
  32. encoder(type).response_parser
  33. end
  34. 1 def self.encoder(name)
  35. @encoders[name] || @encoders[:identity]
  36. end
  37. 1 def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil)
  38. 1 @encoders[mime_name] = new(mime_name, param_encoder, response_parser)
  39. end
  40. 1 register_encoder :json, response_parser: -> body { JSON.parse(body) }
  41. end
  42. end

lib/action_dispatch/testing/test_process.rb

41.38% lines covered

29 relevant lines. 12 lines covered and 17 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "action_dispatch/middleware/cookies"
  3. 1 require "action_dispatch/middleware/flash"
  4. 1 module ActionDispatch
  5. 1 module TestProcess
  6. 1 module FixtureFile
  7. # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.file_fixture_path, path), type)</tt>:
  8. #
  9. # post :change_avatar, params: { avatar: fixture_file_upload('spongebob.png', 'image/png') }
  10. #
  11. # Default fixture files location is <tt>test/fixtures/files</tt>.
  12. #
  13. # To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter.
  14. # This will not affect other platforms:
  15. #
  16. # post :change_avatar, params: { avatar: fixture_file_upload('spongebob.png', 'image/png', :binary) }
  17. 1 def fixture_file_upload(path, mime_type = nil, binary = false)
  18. if self.class.respond_to?(:fixture_path) && self.class.fixture_path &&
  19. !File.exist?(path)
  20. original_path = path
  21. path = Pathname.new(self.class.fixture_path).join(path)
  22. if !self.class.file_fixture_path
  23. ActiveSupport::Deprecation.warn(<<~EOM)
  24. Passing a path to `fixture_file_upload` relative to `fixture_path` is deprecated.
  25. In Rails 6.2, the path needs to be relative to `file_fixture_path` which you
  26. haven't set yet. Set `file_fixture_path` to discard this warning.
  27. EOM
  28. elsif path.exist?
  29. non_deprecated_path = path.relative_path_from(Pathname(self.class.file_fixture_path))
  30. ActiveSupport::Deprecation.warn(<<~EOM)
  31. Passing a path to `fixture_file_upload` relative to `fixture_path` is deprecated.
  32. In Rails 6.2, the path needs to be relative to `file_fixture_path`.
  33. Please modify the call from
  34. `fixture_file_upload("#{original_path}")` to `fixture_file_upload("#{non_deprecated_path}")`.
  35. EOM
  36. else
  37. path = file_fixture(original_path)
  38. end
  39. elsif self.class.file_fixture_path && !File.exist?(path)
  40. path = file_fixture(path)
  41. end
  42. Rack::Test::UploadedFile.new(path, mime_type, binary)
  43. end
  44. end
  45. 1 include FixtureFile
  46. 1 def assigns(key = nil)
  47. raise NoMethodError,
  48. "assigns has been extracted to a gem. To continue using it,
  49. add `gem 'rails-controller-testing'` to your Gemfile."
  50. end
  51. 1 def session
  52. @request.session
  53. end
  54. 1 def flash
  55. @request.flash
  56. end
  57. 1 def cookies
  58. @cookie_jar ||= Cookies::CookieJar.build(@request, @request.cookies)
  59. end
  60. 1 def redirect_to_url
  61. @response.redirect_url
  62. end
  63. end
  64. end

lib/action_dispatch/testing/test_request.rb

54.29% lines covered

35 relevant lines. 19 lines covered and 16 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/hash/indifferent_access"
  3. 1 require "rack/utils"
  4. 1 module ActionDispatch
  5. 1 class TestRequest < Request
  6. 1 DEFAULT_ENV = Rack::MockRequest.env_for("/",
  7. "HTTP_HOST" => "test.host".b,
  8. "REMOTE_ADDR" => "0.0.0.0".b,
  9. "HTTP_USER_AGENT" => "Rails Testing".b,
  10. )
  11. # Create a new test request with default +env+ values.
  12. 1 def self.create(env = {})
  13. env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
  14. env["rack.request.cookie_hash"] ||= {}.with_indifferent_access
  15. new(default_env.merge(env))
  16. end
  17. 1 def self.default_env
  18. DEFAULT_ENV
  19. end
  20. 1 private_class_method :default_env
  21. 1 def request_method=(method)
  22. super(method.to_s.upcase)
  23. end
  24. 1 def host=(host)
  25. set_header("HTTP_HOST", host)
  26. end
  27. 1 def port=(number)
  28. set_header("SERVER_PORT", number.to_i)
  29. end
  30. 1 def request_uri=(uri)
  31. set_header("REQUEST_URI", uri)
  32. end
  33. 1 def path=(path)
  34. set_header("PATH_INFO", path)
  35. end
  36. 1 def action=(action_name)
  37. path_parameters[:action] = action_name.to_s
  38. end
  39. 1 def if_modified_since=(last_modified)
  40. set_header("HTTP_IF_MODIFIED_SINCE", last_modified)
  41. end
  42. 1 def if_none_match=(etag)
  43. set_header("HTTP_IF_NONE_MATCH", etag)
  44. end
  45. 1 def remote_addr=(addr)
  46. set_header("REMOTE_ADDR", addr)
  47. end
  48. 1 def user_agent=(user_agent)
  49. set_header("HTTP_USER_AGENT", user_agent)
  50. end
  51. 1 def accept=(mime_types)
  52. delete_header("action_dispatch.request.accepts")
  53. set_header("HTTP_ACCEPT", Array(mime_types).collect(&:to_s).join(","))
  54. end
  55. end
  56. end

lib/action_dispatch/testing/test_response.rb

0.0% lines covered

14 relevant lines. 0 lines covered and 14 lines missed.
    
  1. # frozen_string_literal: true
  2. require "action_dispatch/testing/request_encoder"
  3. module ActionDispatch
  4. # Integration test methods such as ActionDispatch::Integration::Session#get
  5. # and ActionDispatch::Integration::Session#post return objects of class
  6. # TestResponse, which represent the HTTP response results of the requested
  7. # controller actions.
  8. #
  9. # See Response for more information on controller response objects.
  10. class TestResponse < Response
  11. def self.from_response(response)
  12. new response.status, response.headers, response.body
  13. end
  14. def parsed_body
  15. @parsed_body ||= response_parser.call(body)
  16. end
  17. def response_parser
  18. @response_parser ||= RequestEncoder.parser(media_type)
  19. end
  20. end
  21. end

lib/action_pack.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. #--
  3. # Copyright (c) 2004-2020 David Heinemeier Hansson
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining
  6. # a copy of this software and associated documentation files (the
  7. # "Software"), to deal in the Software without restriction, including
  8. # without limitation the rights to use, copy, modify, merge, publish,
  9. # distribute, sublicense, and/or sell copies of the Software, and to
  10. # permit persons to whom the Software is furnished to do so, subject to
  11. # the following conditions:
  12. #
  13. # The above copyright notice and this permission notice shall be
  14. # included in all copies or substantial portions of the Software.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  17. # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  18. # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  19. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  20. # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  21. # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  22. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  23. #++
  24. 1 require "action_pack/version"

lib/action_pack/gem_version.rb

88.89% lines covered

9 relevant lines. 8 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActionPack
  3. # Returns the version of the currently loaded Action Pack as a <tt>Gem::Version</tt>
  4. 1 def self.gem_version
  5. Gem::Version.new VERSION::STRING
  6. end
  7. 1 module VERSION
  8. 1 MAJOR = 6
  9. 1 MINOR = 1
  10. 1 TINY = 0
  11. 1 PRE = "alpha"
  12. 1 STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
  13. end
  14. end

lib/action_pack/version.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "gem_version"
  3. 1 module ActionPack
  4. # Returns the version of the currently loaded ActionPack as a <tt>Gem::Version</tt>
  5. 1 def self.version
  6. gem_version
  7. end
  8. end