diff options
Diffstat (limited to 'actionpack/lib')
112 files changed, 1508 insertions, 1000 deletions
diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb index 0477e7f1c9..3a98931167 100644 --- a/actionpack/lib/abstract_controller.rb +++ b/actionpack/lib/abstract_controller.rb @@ -7,6 +7,7 @@ require "active_support/i18n" module AbstractController extend ActiveSupport::Autoload + autoload :ActionNotFound, "abstract_controller/base" autoload :Base autoload :Caching autoload :Callbacks diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index a312af6715..bb42f2e119 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -78,7 +78,9 @@ module AbstractController # Except for public instance methods of Base and its ancestors internal_methods + # Be sure to include shadowed public instance methods of this class - public_instance_methods(false)).uniq.map(&:to_s) + public_instance_methods(false)) + + methods.map!(&:to_s) methods.to_set end @@ -102,7 +104,7 @@ module AbstractController # ==== Returns # * <tt>String</tt> def controller_path - @controller_path ||= name.sub(/Controller$/, "".freeze).underscore unless anonymous? + @controller_path ||= name.sub(/Controller$/, "").underscore unless anonymous? end # Refresh the cached action_methods when a new action_method is added. diff --git a/actionpack/lib/abstract_controller/caching/fragments.rb b/actionpack/lib/abstract_controller/caching/fragments.rb index f99b0830b2..18677ddd18 100644 --- a/actionpack/lib/abstract_controller/caching/fragments.rb +++ b/actionpack/lib/abstract_controller/caching/fragments.rb @@ -28,7 +28,6 @@ module AbstractController self.fragment_cache_keys = [] if respond_to?(:helper_method) - helper_method :fragment_cache_key helper_method :combined_fragment_cache_key end end @@ -61,34 +60,19 @@ module AbstractController end # Given a key (as described in +expire_fragment+), returns - # a key suitable for use in reading, writing, or expiring a - # cached fragment. All keys begin with <tt>views/</tt>, - # followed by any controller-wide key prefix values, ending - # with the specified +key+ value. The key is expanded using - # ActiveSupport::Cache.expand_cache_key. - def fragment_cache_key(key) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Calling fragment_cache_key directly is deprecated and will be removed in Rails 6.0. - All fragment accessors now use the combined_fragment_cache_key method that retains the key as an array, - such that the caching stores can interrogate the parts for cache versions used in - recyclable cache keys. - MSG - - head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) } - tail = key.is_a?(Hash) ? url_for(key).split("://").last : key - ActiveSupport::Cache.expand_cache_key([*head, *tail], :views) - end - - # Given a key (as described in +expire_fragment+), returns # a key array suitable for use in reading, writing, or expiring a # cached fragment. All keys begin with <tt>:views</tt>, - # followed by ENV["RAILS_CACHE_ID"] or ENV["RAILS_APP_VERSION"] if set, + # followed by <tt>ENV["RAILS_CACHE_ID"]</tt> or <tt>ENV["RAILS_APP_VERSION"]</tt> if set, # followed by any controller-wide key prefix values, ending # with the specified +key+ value. def combined_fragment_cache_key(key) head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) } tail = key.is_a?(Hash) ? url_for(key).split("://").last : key - [ :views, (ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]), *head, *tail ].compact + + cache_key = [:views, ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"], head, tail] + cache_key.flatten!(1) + cache_key.compact! + cache_key end # Writes +content+ to the location signified by diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 146d17cf40..42bab411d2 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -103,6 +103,10 @@ module AbstractController # :call-seq: before_action(names, block) # # Append a callback before actions. See _insert_callbacks for parameter details. + # + # If the callback renders or redirects, the action will not run. If there + # are additional callbacks scheduled to run after that callback, they are + # also cancelled. ## # :method: prepend_before_action @@ -110,6 +114,10 @@ module AbstractController # :call-seq: prepend_before_action(names, block) # # Prepend a callback before actions. See _insert_callbacks for parameter details. + # + # If the callback renders or redirects, the action will not run. If there + # are additional callbacks scheduled to run after that callback, they are + # also cancelled. ## # :method: skip_before_action @@ -124,6 +132,10 @@ module AbstractController # :call-seq: append_before_action(names, block) # # Append a callback before actions. See _insert_callbacks for parameter details. + # + # If the callback renders or redirects, the action will not run. If there + # are additional callbacks scheduled to run after that callback, they are + # also cancelled. ## # :method: after_action diff --git a/actionpack/lib/abstract_controller/collector.rb b/actionpack/lib/abstract_controller/collector.rb index 297ec5ca40..d4a078ab32 100644 --- a/actionpack/lib/abstract_controller/collector.rb +++ b/actionpack/lib/abstract_controller/collector.rb @@ -26,7 +26,7 @@ module AbstractController def method_missing(symbol, &block) unless mime_constant = Mime[symbol] raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \ - "http://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \ + "https://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \ "If you meant to respond to a variant like :tablet or :phone, not a custom format, " \ "be sure to nest your variant response within a format response: " \ "format.html { |html| html.tablet { ... } }" diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 35b462bc92..3913259ecc 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -17,7 +17,7 @@ module AbstractController @path = "helpers/#{path}.rb" set_backtrace error.backtrace - if error.path =~ /^#{path}(\.rb)?$/ + if /^#{path}(\.rb)?$/.match?(error.path) super("Missing helper file helpers/%s.rb" % path) else raise error @@ -181,7 +181,7 @@ module AbstractController end def default_helper_module! - module_name = name.sub(/Controller$/, "".freeze) + module_name = name.sub(/Controller$/, "") module_path = module_name.underscore helper module_path rescue LoadError => e diff --git a/actionpack/lib/abstract_controller/railties/routes_helpers.rb b/actionpack/lib/abstract_controller/railties/routes_helpers.rb index b6e5631a4e..fbd93705ed 100644 --- a/actionpack/lib/abstract_controller/railties/routes_helpers.rb +++ b/actionpack/lib/abstract_controller/railties/routes_helpers.rb @@ -7,7 +7,7 @@ module AbstractController Module.new do define_method(:inherited) do |klass| super(klass) - if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) } + if namespace = klass.module_parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) } klass.include(namespace.railtie_routes_url_helpers(include_path_helpers)) else klass.include(routes.url_helpers(include_path_helpers)) diff --git a/actionpack/lib/abstract_controller/translation.rb b/actionpack/lib/abstract_controller/translation.rb index 666e154e4c..4dad2a2b93 100644 --- a/actionpack/lib/abstract_controller/translation.rb +++ b/actionpack/lib/abstract_controller/translation.rb @@ -11,6 +11,7 @@ module AbstractController # to translate many keys within the same controller / action and gives you a # simple framework for scoping them consistently. def translate(key, options = {}) + options = options.dup if key.to_s.first == "." path = controller_path.tr("/", ".") defaults = [:"#{path}#{key}"] diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index f43784f9f2..29d61c3ceb 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -25,6 +25,7 @@ module ActionController autoload :ContentSecurityPolicy autoload :Cookies autoload :DataStreaming + autoload :DefaultHeaders autoload :EtagWithTemplateDigest autoload :EtagWithFlash autoload :Flash diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb index b192e496de..c276ee57c0 100644 --- a/actionpack/lib/action_controller/api.rb +++ b/actionpack/lib/action_controller/api.rb @@ -12,7 +12,7 @@ module ActionController # # An API Controller is different from a normal controller in the sense that # by default it doesn't include a number of features that are usually required - # by browser access only: layouts and templates rendering, cookies, sessions, + # by browser access only: layouts and templates rendering, # flash, assets, and so on. This makes the entire controller stack thinner, # suitable for API applications. It doesn't mean you won't have such # features if you need them: they're all available for you to include in @@ -122,6 +122,7 @@ module ActionController ForceSSL, DataStreaming, + DefaultHeaders, # Before callbacks should also be executed as early as possible, so # also include them at the bottom. diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 204a3d400c..2e565d5d44 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -78,7 +78,7 @@ module ActionController # # You can retrieve it again through the same hash: # - # Hello #{session[:person]} + # "Hello #{session[:person]}" # # For removing objects from the session, you can either assign a single key to +nil+: # @@ -232,6 +232,7 @@ module ActionController HttpAuthentication::Basic::ControllerMethods, HttpAuthentication::Digest::ControllerMethods, HttpAuthentication::Token::ControllerMethods, + DefaultHeaders, # Before callbacks should also be executed as early as possible, so # also include them at the bottom. @@ -264,12 +265,6 @@ module ActionController PROTECTED_IVARS end - def self.make_response!(request) - ActionDispatch::Response.create.tap do |res| - res.request = request - end - end - ActiveSupport.run_load_hooks(:action_controller_base, self) ActiveSupport.run_load_hooks(:action_controller, self) end diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 97775d1dc8..bf3b00a7b7 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -40,7 +40,7 @@ module ActionController end def instrument_name - "action_controller".freeze + "action_controller" end end end diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 14f41eb55f..d8b04d8ddb 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -18,16 +18,19 @@ module ActionController def process_action(event) info do - payload = event.payload + payload = event.payload additions = ActionController::Base.log_process_action(payload) - status = payload[:status] + if status.nil? && payload[:exception].present? exception_class_name = payload[:exception].first status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) end - message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms".dup - message << " (#{additions.join(" | ".freeze)})" unless additions.empty? + + additions << "Allocations: #{event.allocations}" + + message = +"Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" + message << " (#{additions.join(" | ")})" unless additions.empty? message << "\n\n" if defined?(Rails.env) && Rails.env.development? message @@ -53,7 +56,7 @@ module ActionController def unpermitted_parameters(event) debug do unpermitted_keys = event.payload[:keys] - "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.map { |e| ":#{e}" }.join(", ")}" + color("Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.map { |e| ":#{e}" }.join(", ")}", RED) end end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 457884ea08..f875aa5e6b 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -230,18 +230,16 @@ module ActionController # Returns a Rack endpoint for the given action name. def self.action(name) + app = lambda { |env| + req = ActionDispatch::Request.new(env) + res = make_response! req + new.dispatch(name, req, res) + } + if middleware_stack.any? - middleware_stack.build(name) do |env| - req = ActionDispatch::Request.new(env) - res = make_response! req - new.dispatch(name, req, res) - end + middleware_stack.build(name, app) else - lambda { |env| - req = ActionDispatch::Request.new(env) - res = make_response! req - new.dispatch(name, req, res) - } + app end end diff --git a/actionpack/lib/action_controller/metal/basic_implicit_render.rb b/actionpack/lib/action_controller/metal/basic_implicit_render.rb index 2dc990f303..f9a758ff0e 100644 --- a/actionpack/lib/action_controller/metal/basic_implicit_render.rb +++ b/actionpack/lib/action_controller/metal/basic_implicit_render.rb @@ -6,7 +6,7 @@ module ActionController super.tap { default_render unless performed? } end - def default_render(*args) + def default_render head :no_content end end diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 06b6a95ff8..29d1919ec5 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/hash/keys" - module ActionController module ConditionalGet extend ActiveSupport::Concern @@ -230,12 +228,20 @@ module ActionController # This method will overwrite an existing Cache-Control header. # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities. # + # HTTP Cache-Control Extensions for Stale Content. See https://tools.ietf.org/html/rfc5861 + # It helps to cache an asset and serve it while is being revalidated and/or returning with an error. + # + # expires_in 3.hours, public: true, stale_while_revalidate: 60.seconds + # expires_in 3.hours, public: true, stale_while_revalidate: 60.seconds, stale_if_error: 5.minutes + # # The method will also ensure an HTTP Date header for client compatibility. def expires_in(seconds, options = {}) response.cache_control.merge!( max_age: seconds, public: options.delete(:public), - must_revalidate: options.delete(:must_revalidate) + must_revalidate: options.delete(:must_revalidate), + stale_while_revalidate: options.delete(:stale_while_revalidate), + stale_if_error: options.delete(:stale_if_error), ) options.delete(:private) diff --git a/actionpack/lib/action_controller/metal/content_security_policy.rb b/actionpack/lib/action_controller/metal/content_security_policy.rb index 48a7109bea..b8fab4ebe3 100644 --- a/actionpack/lib/action_controller/metal/content_security_policy.rb +++ b/actionpack/lib/action_controller/metal/content_security_policy.rb @@ -5,14 +5,26 @@ module ActionController #:nodoc: # TODO: Documentation extend ActiveSupport::Concern + include AbstractController::Helpers + include AbstractController::Callbacks + + included do + helper_method :content_security_policy? + helper_method :content_security_policy_nonce + end + module ClassMethods - def content_security_policy(**options, &block) + def content_security_policy(enabled = true, **options, &block) before_action(options) do if block_given? - policy = request.content_security_policy.clone + policy = current_content_security_policy yield policy request.content_security_policy = policy end + + unless enabled + request.content_security_policy = nil + end end end @@ -22,5 +34,19 @@ module ActionController #:nodoc: end end end + + private + + def content_security_policy? + request.content_security_policy + end + + def content_security_policy_nonce + request.content_security_policy_nonce + end + + def current_content_security_policy + request.content_security_policy.try(:clone) || ActionDispatch::ContentSecurityPolicy.new + end end end diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 5a82ccf668..9ef4f50df1 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "action_controller/metal/exceptions" +require "action_dispatch/http/content_disposition" module ActionController #:nodoc: # Methods for sending arbitrary data and for streaming files to the browser, @@ -10,8 +11,8 @@ module ActionController #:nodoc: include ActionController::Rendering - DEFAULT_SEND_FILE_TYPE = "application/octet-stream".freeze #:nodoc: - DEFAULT_SEND_FILE_DISPOSITION = "attachment".freeze #:nodoc: + DEFAULT_SEND_FILE_TYPE = "application/octet-stream" #:nodoc: + DEFAULT_SEND_FILE_DISPOSITION = "attachment" #:nodoc: private # Sends the file. This uses a server-appropriate method (such as X-Sendfile) @@ -132,10 +133,8 @@ module ActionController #:nodoc: end disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION) - unless disposition.nil? - disposition = disposition.to_s - disposition += %(; filename="#{options[:filename]}") if options[:filename] - headers["Content-Disposition"] = disposition + if disposition + headers["Content-Disposition"] = ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: options[:filename]) end headers["Content-Transfer-Encoding"] = "binary" diff --git a/actionpack/lib/action_controller/metal/default_headers.rb b/actionpack/lib/action_controller/metal/default_headers.rb new file mode 100644 index 0000000000..eef0602fcd --- /dev/null +++ b/actionpack/lib/action_controller/metal/default_headers.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module ActionController + # Allows configuring default headers that will be automatically merged into + # each response. + module DefaultHeaders + extend ActiveSupport::Concern + + module ClassMethods + def make_response!(request) + ActionDispatch::Response.create.tap do |res| + res.request = request + end + end + end + end +end diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index a65857d6ef..e1e0c6f456 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -22,7 +22,7 @@ module ActionController end end - class ActionController::UrlGenerationError < ActionControllerError #:nodoc: + class UrlGenerationError < ActionControllerError #:nodoc: end class MethodNotAllowed < ActionControllerError #:nodoc: @@ -50,4 +50,25 @@ module ActionController class UnknownFormat < ActionControllerError #:nodoc: end + + # Raised when a nested respond_to is triggered and the content types of each + # are incompatible. For example: + # + # respond_to do |outer_type| + # outer_type.js do + # respond_to do |inner_type| + # inner_type.html { render body: "HTML" } + # end + # end + # end + class RespondToMismatchError < ActionControllerError + 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." + + def initialize(message = nil) + super(message || DEFAULT_MESSAGE) + end + end + + class MissingExactTemplate < UnknownFormat #:nodoc: + end end diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb index 5115c2fadf..a4861dc2c0 100644 --- a/actionpack/lib/action_controller/metal/flash.rb +++ b/actionpack/lib/action_controller/metal/flash.rb @@ -36,7 +36,7 @@ module ActionController #:nodoc: define_method(type) do request.flash[type] end - helper_method type + helper_method(type) if respond_to?(:helper_method) self._flash_types += [type] end @@ -44,18 +44,18 @@ module ActionController #:nodoc: end private - def redirect_to(options = {}, response_status_and_flash = {}) #:doc: + def redirect_to(options = {}, response_options_and_flash = {}) #:doc: self.class._flash_types.each do |flash_type| - if type = response_status_and_flash.delete(flash_type) + if type = response_options_and_flash.delete(flash_type) flash[flash_type] = type end end - if other_flashes = response_status_and_flash.delete(:flash) + if other_flashes = response_options_and_flash.delete(:flash) flash.update(other_flashes) end - super(options, response_status_and_flash) + super(options, response_options_and_flash) end end end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index 0ba1f9f783..93fd57b640 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -4,18 +4,10 @@ require "active_support/core_ext/hash/except" require "active_support/core_ext/hash/slice" module ActionController - # This module provides a method which will redirect the browser to use the secured HTTPS - # protocol. This will ensure that users' sensitive information will be - # transferred safely over the internet. You _should_ always force the browser - # to use HTTPS when you're transferring sensitive information such as - # user authentication, account information, or credit card information. - # - # Note that if you are really concerned about your application security, - # you might consider using +config.force_ssl+ in your config file instead. - # That will ensure all the data is transferred via HTTPS, and will - # prevent the user from getting their session hijacked when accessing the - # site over unsecured HTTP protocol. - module ForceSSL + # This module is deprecated in favor of +config.force_ssl+ in your environment + # config file. This will ensure all endpoints not explicitly marked otherwise + # will have all communication served over HTTPS. + module ForceSSL # :nodoc: extend ActiveSupport::Concern include AbstractController::Callbacks @@ -23,45 +15,17 @@ module ActionController URL_OPTIONS = [:protocol, :host, :domain, :subdomain, :port, :path] REDIRECT_OPTIONS = [:status, :flash, :alert, :notice] - module ClassMethods - # Force the request to this particular controller or specified actions to be - # through the HTTPS protocol. - # - # If you need to disable this for any reason (e.g. development) then you can use - # an +:if+ or +:unless+ condition. - # - # class AccountsController < ApplicationController - # force_ssl if: :ssl_configured? - # - # def ssl_configured? - # !Rails.env.development? - # end - # end - # - # ==== URL Options - # You can pass any of the following options to affect the redirect url - # * <tt>host</tt> - Redirect to a different host name - # * <tt>subdomain</tt> - Redirect to a different subdomain - # * <tt>domain</tt> - Redirect to a different domain - # * <tt>port</tt> - Redirect to a non-standard port - # * <tt>path</tt> - Redirect to a different path - # - # ==== Redirect Options - # You can pass any of the following options to affect the redirect status and response - # * <tt>status</tt> - Redirect with a custom status (default is 301 Moved Permanently) - # * <tt>flash</tt> - Set a flash message when redirecting - # * <tt>alert</tt> - Set an alert message when redirecting - # * <tt>notice</tt> - Set a notice message when redirecting - # - # ==== Action Options - # You can pass any of the following options to affect the before_action callback - # * <tt>only</tt> - The callback should be run only for this action - # * <tt>except</tt> - The callback should be run for all actions except this action - # * <tt>if</tt> - A symbol naming an instance method or a proc; the - # callback will be called only when it returns a true value. - # * <tt>unless</tt> - A symbol naming an instance method or a proc; the - # callback will be called only when it returns a false value. + module ClassMethods # :nodoc: def force_ssl(options = {}) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + Controller-level `force_ssl` is deprecated and will be removed from + Rails 6.1. Please enable `config.force_ssl` in your environment + configuration to enable the ActionDispatch::SSL middleware to more + fully enforce that your application communicate over HTTPS. If needed, + you can use `config.ssl_options` to exempt matching endpoints from + being redirected to HTTPS. + MESSAGE + action_options = options.slice(*ACTION_OPTIONS) redirect_options = options.except(*ACTION_OPTIONS) before_action(action_options) do @@ -70,18 +34,13 @@ module ActionController end end - # Redirect the existing request to use the HTTPS protocol. - # - # ==== Parameters - # * <tt>host_or_options</tt> - Either a host name or any of the url and - # redirect options available to the <tt>force_ssl</tt> method. def force_ssl_redirect(host_or_options = nil) unless request.ssl? options = { protocol: "https://", host: request.host, path: request.fullpath, - status: :moved_permanently + status: :moved_permanently, } if host_or_options.is_a?(Hash) diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index bac9bc5e5f..3c84bebb85 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -38,7 +38,7 @@ module ActionController self.response_body = "" if include_content?(response_code) - self.content_type = content_type || (Mime[formats.first] if formats) + self.content_type = content_type || (Mime[formats.first] if formats) || Mime[:html] response.charset = false end diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 22c84e440b..f1fb7ab0f7 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -75,7 +75,7 @@ module ActionController # Provides a proxy to access helper methods from outside the view. def helpers @helper_proxy ||= begin - proxy = ActionView::Base.new + proxy = ActionView::Base.empty proxy.config = config.inheritable_copy proxy.extend(_helpers) end @@ -100,8 +100,7 @@ module ActionController # # => ["application", "chart", "rubygems"] def all_helpers_from_path(path) helpers = Array(path).flat_map do |_path| - extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ - names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1'.freeze) } + names = Dir["#{_path}/**/*_helper.rb"].map { |file| file[_path.to_s.size + 1..-"_helper.rb".size - 1] } names.sort! end helpers.uniq! diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 01676f3237..6a274d35cb 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -56,8 +56,9 @@ module ActionController # In your integration tests, you can do something like this: # # def test_access_granted_from_xml - # @request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) - # get "/notes/1.xml" + # authorization = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) + # + # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization } # # assert_equal 200, status # end @@ -68,21 +69,20 @@ module ActionController extend ActiveSupport::Concern module ClassMethods - def http_basic_authenticate_with(options = {}) - before_action(options.except(:name, :password, :realm)) do - authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password| - # This comparison uses & so that it doesn't short circuit and - # uses `secure_compare` so that length information - # isn't leaked. - ActiveSupport::SecurityUtils.secure_compare(name, options[:name]) & - ActiveSupport::SecurityUtils.secure_compare(password, options[:password]) - end - end + def http_basic_authenticate_with(name:, password:, realm: nil, **options) + before_action(options) { http_basic_authenticate_or_request_with name: name, password: password, realm: realm } + end + end + + def http_basic_authenticate_or_request_with(name:, password:, realm: nil, message: nil) + authenticate_or_request_with_http_basic(realm, message) do |given_name, given_password| + ActiveSupport::SecurityUtils.secure_compare(given_name, name) & + ActiveSupport::SecurityUtils.secure_compare(given_password, password) end end - def authenticate_or_request_with_http_basic(realm = "Application", message = nil, &login_procedure) - authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm, message) + def authenticate_or_request_with_http_basic(realm = nil, message = nil, &login_procedure) + authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm || "Application", message) end def authenticate_with_http_basic(&login_procedure) @@ -126,7 +126,7 @@ module ActionController def authentication_request(controller, realm, message) message ||= "HTTP Basic: Access denied.\n" - controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"'.freeze, "".freeze)}") + controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"', "")}") controller.status = 401 controller.response_body = message end @@ -389,10 +389,9 @@ module ActionController # In your integration tests, you can do something like this: # # def test_access_granted_from_xml - # get( - # "/notes/1.xml", nil, - # 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token) - # ) + # authorization = ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token) + # + # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization } # # assert_equal 200, status # end @@ -474,7 +473,7 @@ module ActionController # This removes the <tt>"</tt> characters wrapping the value. def rewrite_param_values(array_params) - array_params.each { |param| (param[1] || "".dup).gsub! %r/^"|"$/, "" } + array_params.each { |param| (param[1] || +"").gsub! %r/^"|"$/, "" } end # This method takes an authorization body and splits up the key-value @@ -511,7 +510,7 @@ module ActionController # Returns nothing. def authentication_request(controller, realm, message = nil) message ||= "HTTP Token: Access denied.\n" - controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"'.freeze, "".freeze)}") + controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"', "")}") controller.__send__ :render, plain: message, status: :unauthorized end end diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index ac0c127cdc..8365ddca57 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -30,9 +30,9 @@ module ActionController # :stopdoc: include BasicImplicitRender - def default_render(*args) + def default_render if template_exists?(action_name.to_s, _prefixes, variants: request.variant) - render(*args) + render elsif any_templates?(action_name.to_s, _prefixes) message = "#{self.class.name}\##{action_name} is missing a template " \ "for this request format and variant.\n" \ @@ -41,18 +41,8 @@ module ActionController raise ActionController::UnknownFormat, message elsif interactive_browser_request? - message = "#{self.class.name}\##{action_name} is missing a template " \ - "for this request format and variant.\n\n" \ - "request.formats: #{request.formats.map(&:to_s).inspect}\n" \ - "request.variant: #{request.variant.inspect}\n\n" \ - "NOTE! For XHR/Ajax or API requests, this action would normally " \ - "respond with 204 No Content: an empty white screen. Since you're " \ - "loading it in a web browser, we assume that you expected to " \ - "actually render a template, not nothing, so we're showing an " \ - "error to be extra-clear. If you expect 204 No Content, carry on. " \ - "That's what you'll get from an XHR or API request. Give it a shot." - - raise ActionController::UnknownFormat, message + message = "#{self.class.name}\##{action_name} is missing a template for request formats: #{request.formats.map(&:to_s).join(',')}" + raise ActionController::MissingExactTemplate, message else logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger super diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index be9449629f..51fac08749 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -30,13 +30,11 @@ module ActionController ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup) ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload| - begin - result = super + super.tap do payload[:status] = response.status - result - ensure - append_info_to_payload(payload) end + ensure + append_info_to_payload(payload) end end diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 2f4c8fb83c..083b762f5a 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -86,7 +86,7 @@ module ActionController # Note: SSEs are not currently supported by IE. However, they are supported # by Chrome, Firefox, Opera, and Safari. class SSE - WHITELISTED_OPTIONS = %w( retry event id ) + PERMITTED_OPTIONS = %w( retry event id ) def initialize(stream, options = {}) @stream = stream @@ -111,13 +111,13 @@ module ActionController def perform_write(json, options) current_options = @options.merge(options).stringify_keys - WHITELISTED_OPTIONS.each do |option_name| + PERMITTED_OPTIONS.each do |option_name| if (option_value = current_options[option_name]) @stream.write "#{option_name}: #{option_value}\n" end end - message = json.gsub("\n".freeze, "\ndata: ".freeze) + message = json.gsub("\n", "\ndata: ") @stream.write "data: #{message}\n\n" end end @@ -280,33 +280,35 @@ module ActionController raise error if error end - # Spawn a new thread to serve up the controller in. This is to get - # around the fact that Rack isn't based around IOs and we need to use - # a thread to stream data from the response bodies. Nobody should call - # this method except in Rails internals. Seriously! - def new_controller_thread # :nodoc: - Thread.new { - t2 = Thread.current - t2.abort_on_exception = true - yield - } + def response_body=(body) + super + response.close if response end - def log_error(exception) - logger = ActionController::Base.logger - return unless logger + private - logger.fatal do - message = "\n#{exception.class} (#{exception.message}):\n".dup - message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) - message << " " << exception.backtrace.join("\n ") - "#{message}\n\n" + # Spawn a new thread to serve up the controller in. This is to get + # around the fact that Rack isn't based around IOs and we need to use + # a thread to stream data from the response bodies. Nobody should call + # this method except in Rails internals. Seriously! + def new_controller_thread # :nodoc: + Thread.new { + t2 = Thread.current + t2.abort_on_exception = true + yield + } end - end - def response_body=(body) - super - response.close if response - end + def log_error(exception) + logger = ActionController::Base.logger + return unless logger + + logger.fatal do + message = +"\n#{exception.class} (#{exception.message}):\n" + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) + message << " " << exception.backtrace.join("\n ") + "#{message}\n\n" + end + end end end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 2233b93406..bf5e7a433f 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -11,7 +11,7 @@ module ActionController #:nodoc: # @people = Person.all # end # - # That action implicitly responds to all formats, but formats can also be whitelisted: + # That action implicitly responds to all formats, but formats can also be explicitly enumerated: # # def index # @people = Person.all @@ -105,7 +105,7 @@ module ActionController #:nodoc: # # Mime::Type.register "image/jpg", :jpg # - # Respond to also allows you to specify a common block for different formats by using +any+: + # +respond_to+ also allows you to specify a common block for different formats by using +any+: # # def index # @people = Person.all @@ -124,6 +124,14 @@ module ActionController #:nodoc: # # render json: @people # + # +any+ can also be used with no arguments, in which case it will be used for any format requested by + # the user: + # + # respond_to do |format| + # format.html + # format.any { redirect_to support_path } + # end + # # Formats can have different variants. # # The request variant is a specialization of the request format, like <tt>:tablet</tt>, @@ -197,6 +205,9 @@ module ActionController #:nodoc: yield collector if block_given? if format = collector.negotiate_format(request) + if content_type && content_type != format + raise ActionController::RespondToMismatchError + end _process_format(format) _set_rendered_content_type format response = collector.response diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index a678377d4f..09716f7588 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -241,18 +241,7 @@ module ActionController # Performs parameters wrapping upon the request. Called automatically # by the metal call stack. def process_action(*args) - if _wrapper_enabled? - wrapped_hash = _wrap_parameters request.request_parameters - wrapped_keys = request.request_parameters.keys - wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys) - - # This will make the wrapped hash accessible from controller and view. - request.parameters.merge! wrapped_hash - request.request_parameters.merge! wrapped_hash - - # This will display the wrapped hash in the log file. - request.filtered_parameters.merge! wrapped_filtered_hash - end + _perform_parameter_wrapping if _wrapper_enabled? super end @@ -289,5 +278,20 @@ module ActionController ref = request.content_mime_type.ref _wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key) end + + def _perform_parameter_wrapping + wrapped_hash = _wrap_parameters request.request_parameters + wrapped_keys = request.request_parameters.keys + wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys) + + # This will make the wrapped hash accessible from controller and view. + request.parameters.merge! wrapped_hash + request.request_parameters.merge! wrapped_hash + + # This will display the wrapped hash in the log file. + request.filtered_parameters.merge! wrapped_filtered_hash + rescue ActionDispatch::Http::Parameters::ParseError + # swallow parse error exception + end end end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 4c2b5120eb..67c198d150 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -55,11 +55,11 @@ module ActionController # Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function. # To terminate the execution of the function immediately after the +redirect_to+, use return. # redirect_to post_url(@post) and return - def redirect_to(options = {}, response_status = {}) + def redirect_to(options = {}, response_options = {}) raise ActionControllerError.new("Cannot redirect to nil!") unless options raise AbstractController::DoubleRenderError if response_body - self.status = _extract_redirect_to_status(options, response_status) + self.status = _extract_redirect_to_status(options, response_options) self.location = _compute_redirect_to_location(request, options) self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>" end @@ -105,7 +105,7 @@ module ActionController when String request.protocol + request.host_with_port + options when Proc - _compute_redirect_to_location request, options.call + _compute_redirect_to_location request, instance_eval(&options) else url_for(options) end.delete("\0\r\n") @@ -114,11 +114,11 @@ module ActionController public :_compute_redirect_to_location private - def _extract_redirect_to_status(options, response_status) + def _extract_redirect_to_status(options, response_options) if options.is_a?(Hash) && options.key?(:status) Rack::Utils.status_code(options.delete(:status)) - elsif response_status.key?(:status) - Rack::Utils.status_code(response_status[:status]) + elsif response_options.key?(:status) + Rack::Utils.status_code(response_options[:status]) else 302 end diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 6d181e6456..7d0a944381 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -40,7 +40,7 @@ module ActionController def render_to_string(*) result = super if result.respond_to?(:each) - string = "".dup + string = +"" result.each { |r| string << r } string else diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 767eddb361..cb109c6ad8 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -17,7 +17,7 @@ module ActionController #:nodoc: # access. When a request reaches your application, \Rails verifies the received # token with the token in the session. All requests are checked except GET requests # as these should be idempotent. Keep in mind that all session-oriented requests - # should be CSRF protected, including JavaScript and HTML requests. + # are CSRF protected by default, including JavaScript and HTML requests. # # Since HTML and JavaScript requests are typically made from the browser, we # need to ensure to verify request authenticity for the web browser. We can @@ -30,16 +30,23 @@ module ActionController #:nodoc: # URL on your site. When your JavaScript response loads on their site, it executes. # With carefully crafted JavaScript on their end, sensitive data in your JavaScript # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or - # Ajax) requests are allowed to make GET requests for JavaScript responses. + # Ajax) requests are allowed to make requests for JavaScript responses. # - # It's important to remember that XML or JSON requests are also affected and if - # you're building an API you should change forgery protection method in + # It's important to remember that XML or JSON requests are also checked by default. If + # you're building an API or an SPA you could change forgery protection method in # <tt>ApplicationController</tt> (by default: <tt>:exception</tt>): # # class ApplicationController < ActionController::Base # protect_from_forgery unless: -> { request.format.json? } # end # + # It is generally safe to exclude XHR requests from CSRF protection + # (like the code snippet above does), because XHR requests can only be made from + # the same origin. Note however that any cross-origin third party domain + # allowed via {CORS}[https://en.wikipedia.org/wiki/Cross-origin_resource_sharing] + # will also be able to create XHR requests. Be sure to check your + # CORS configuration before disabling forgery protection for XHR. + # # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method. # By default <tt>protect_from_forgery</tt> protects your session with # <tt>:null_session</tt> method, which provides an empty session @@ -54,7 +61,7 @@ module ActionController #:nodoc: # <tt>csrf_meta_tags</tt> in the HTML +head+. # # Learn more about CSRF attacks and securing your application in the - # {Ruby on Rails Security Guide}[http://guides.rubyonrails.org/security.html]. + # {Ruby on Rails Security Guide}[https://guides.rubyonrails.org/security.html]. module RequestForgeryProtection extend ActiveSupport::Concern @@ -275,7 +282,7 @@ module ActionController #:nodoc: # Check for cross-origin JavaScript responses. def non_xhr_javascript_response? # :doc: - content_type =~ %r(\Atext/javascript) && !request.xhr? + content_type =~ %r(\A(?:text|application)/javascript) && !request.xhr? end AUTHENTICITY_TOKEN_LENGTH = 32 @@ -400,9 +407,14 @@ module ActionController #:nodoc: end def xor_byte_strings(s1, s2) # :doc: - s2_bytes = s2.bytes - s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 } - s2_bytes.pack("C*") + s2 = s2.dup + size = s1.bytesize + i = 0 + while i < size + s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i)) + i += 1 + end + s2 end # The form's authenticity parameter. Override to provide your own. @@ -415,9 +427,9 @@ module ActionController #:nodoc: allow_forgery_protection end - NULL_ORIGIN_MESSAGE = <<-MSG.strip_heredoc + NULL_ORIGIN_MESSAGE = <<~MSG The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually - means you have the 'no-referrer' Referrer-Policy header enabled, or that you the request came from a site that + means you have the 'no-referrer' Referrer-Policy header enabled, or that the request came from a site that refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the best solution is to change your referrer policy to something less strict like same-origin or strict-same-origin. If you cannot change the referrer policy, you can disable origin checking with the diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index a56ac749f8..815f82a1f2 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "active_support/core_ext/hash/indifferent_access" -require "active_support/core_ext/hash/transform_values" require "active_support/core_ext/array/wrap" require "active_support/core_ext/string/filters" require "active_support/core_ext/object/to_query" @@ -59,7 +58,7 @@ module ActionController # == Action Controller \Parameters # - # Allows you to choose which attributes should be whitelisted for mass updating + # Allows you to choose which attributes should be permitted for mass updating # and thus prevent accidentally exposing that which shouldn't be exposed. # Provides two methods for this purpose: #require and #permit. The former is # used to mark parameters as required. The latter is used to set the parameter @@ -134,6 +133,15 @@ module ActionController # Returns a hash that can be used as the JSON representation for the parameters. ## + # :method: each_key + # + # :call-seq: + # each_key() + # + # Calls block once for each key in the parameters, passing the key. + # If no block is given, an enumerator is returned instead. + + ## # :method: empty? # # :call-seq: @@ -205,7 +213,7 @@ module ActionController # # Returns a new array of the values of the parameters. delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?, - :as_json, :to_s, to: :@parameters + :as_json, :to_s, :each_key, to: :@parameters # By default, never raise an UnpermittedParameters exception if these # params are present. The default includes both 'controller' and 'action' @@ -340,6 +348,14 @@ module ActionController end alias_method :each, :each_pair + # Convert all hashes in values into parameters, then yield each value in + # the same way as <tt>Hash#each_value</tt>. + def each_value(&block) + @parameters.each_pair do |key, value| + yield convert_hashes_to_parameters(key, value) + end + end + # Attribute that keeps track of converted arrays, if any, to avoid double # looping in the common use case permit + mass-assignment. Defined in a # method to instantiate it only if needed. @@ -375,7 +391,7 @@ module ActionController # Person.new(params) # => #<Person id: nil, name: "Francesco"> def permit! each_pair do |key, value| - Array.wrap(value).each do |v| + Array.wrap(value).flatten.each do |v| v.permit! if v.respond_to? :permit! end end @@ -506,7 +522,7 @@ module ActionController # # Note that if you use +permit+ in a key that points to a hash, # it won't allow all the hash. You also need to specify which - # attributes inside the hash should be whitelisted. + # attributes inside the hash should be permitted. # # params = ActionController::Parameters.new({ # person: { @@ -561,12 +577,14 @@ module ActionController # Returns a parameter for the given +key+. If the +key+ # can't be found, there are several options: With no other arguments, # it will raise an <tt>ActionController::ParameterMissing</tt> error; - # if more arguments are given, then that will be returned; if a block + # if a second argument is given, then that is returned (converted to an + # instance of ActionController::Parameters if possible); if a block # is given, then that will be run and its result returned. # # params = ActionController::Parameters.new(person: { name: "Francesco" }) # params.fetch(:person) # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false> # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none + # params.fetch(:none, {}) # => <ActionController::Parameters {} permitted: false> # params.fetch(:none, "Francesco") # => "Francesco" # params.fetch(:none) { "Francesco" } # => "Francesco" def fetch(key, *args) @@ -581,19 +599,18 @@ module ActionController ) end - if Hash.method_defined?(:dig) - # Extracts the nested parameter from the given +keys+ by calling +dig+ - # at each step. Returns +nil+ if any intermediate step is +nil+. - # - # params = ActionController::Parameters.new(foo: { bar: { baz: 1 } }) - # params.dig(:foo, :bar, :baz) # => 1 - # params.dig(:foo, :zot, :xyz) # => nil - # - # params2 = ActionController::Parameters.new(foo: [10, 11, 12]) - # params2.dig(:foo, 1) # => 11 - def dig(*keys) - convert_value_to_parameters(@parameters.dig(*keys)) - end + # Extracts the nested parameter from the given +keys+ by calling +dig+ + # at each step. Returns +nil+ if any intermediate step is +nil+. + # + # params = ActionController::Parameters.new(foo: { bar: { baz: 1 } }) + # params.dig(:foo, :bar, :baz) # => 1 + # params.dig(:foo, :zot, :xyz) # => nil + # + # params2 = ActionController::Parameters.new(foo: [10, 11, 12]) + # params2.dig(:foo, 1) # => 11 + def dig(*keys) + convert_hashes_to_parameters(keys.first, @parameters[keys.first]) + @parameters.dig(*keys) end # Returns a new <tt>ActionController::Parameters</tt> instance that @@ -639,20 +656,18 @@ module ActionController # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) # params.transform_values { |x| x * 2 } # # => <ActionController::Parameters {"a"=>2, "b"=>4, "c"=>6} permitted: false> - def transform_values(&block) - if block - new_instance_with_inherited_permitted_status( - @parameters.transform_values(&block) - ) - else - @parameters.transform_values - end + def transform_values + return to_enum(:transform_values) unless block_given? + new_instance_with_inherited_permitted_status( + @parameters.transform_values { |v| yield convert_value_to_parameters(v) } + ) end # Performs values transformation and returns the altered # <tt>ActionController::Parameters</tt> instance. - def transform_values!(&block) - @parameters.transform_values!(&block) + def transform_values! + return to_enum(:transform_values!) unless block_given? + @parameters.transform_values! { |v| yield convert_value_to_parameters(v) } self end @@ -780,7 +795,7 @@ module ActionController @permitted = coder.map["ivars"][:@permitted] when "!ruby/object:ActionController::Parameters" # YAML's Object format. Only needed because of the format - # backwardscompability above, otherwise equivalent to YAML's initialization. + # backwards compatibility above, otherwise equivalent to YAML's initialization. @parameters, @permitted = coder.map["parameters"], coder.map["permitted"] end end @@ -795,9 +810,7 @@ module ActionController protected attr_reader :parameters - def permitted=(new_permitted) - @permitted = new_permitted - end + attr_writer :permitted def fields_for_style? @parameters.all? { |k, v| k =~ /\A-?\d+\z/ && (v.is_a?(Hash) || v.is_a?(Parameters)) } @@ -908,15 +921,28 @@ module ActionController PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) } end - def permitted_scalar_filter(params, key) - if has_key?(key) && permitted_scalar?(self[key]) - params[key] = self[key] + # Adds existing keys to the params if their values are scalar. + # + # For example: + # + # puts self.keys #=> ["zipcode(90210i)"] + # params = {} + # + # permitted_scalar_filter(params, "zipcode") + # + # puts params.keys # => ["zipcode"] + def permitted_scalar_filter(params, permitted_key) + permitted_key = permitted_key.to_s + + if has_key?(permitted_key) && permitted_scalar?(self[permitted_key]) + params[permitted_key] = self[permitted_key] end - keys.grep(/\A#{Regexp.escape(key)}\(\d+[if]?\)\z/) do |k| - if permitted_scalar?(self[k]) - params[k] = self[k] - end + each_key do |key| + next unless key =~ /\(\d+[if]?\)\z/ + next unless $~.pre_match == permitted_key + + params[key] = self[key] if permitted_scalar?(self[key]) end end @@ -1001,8 +1027,8 @@ module ActionController # # It provides an interface for protecting attributes from end-user # assignment. This makes Action Controller parameters forbidden - # to be used in Active Model mass assignment until they have been - # whitelisted. + # to be used in Active Model mass assignment until they have been explicitly + # enumerated. # # In addition, parameters can be marked as required and flow through a # predefined raise/rescue flow to end up as a <tt>400 Bad Request</tt> with no @@ -1038,7 +1064,7 @@ module ActionController # end # # In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you - # will need to specify which nested attributes should be whitelisted. You might want + # will need to specify which nested attributes should be permitted. You might want # to allow +:id+ and +:_destroy+, see ActiveRecord::NestedAttributes for more information. # # class Person @@ -1056,7 +1082,7 @@ module ActionController # private # # def person_params - # # It's mandatory to specify the nested attributes that should be whitelisted. + # # It's mandatory to specify the nested attributes that should be permitted. # # If you use `permit` with just the key that points to the nested attributes hash, # # it will return an empty hash. # params.require(:person).permit(:name, :age, pets_attributes: [ :id, :name, :category ]) diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 84dbb59a63..f077e765ab 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -44,7 +44,7 @@ module ActionController options[:original_script_name] = original_script_name else if same_origin - options[:script_name] = request.script_name.empty? ? "".freeze : request.script_name.dup + options[:script_name] = request.script_name.empty? ? "" : request.script_name.dup else options[:script_name] = script_name end diff --git a/actionpack/lib/action_controller/railties/helpers.rb b/actionpack/lib/action_controller/railties/helpers.rb index fa746fa9e8..75938108d6 100644 --- a/actionpack/lib/action_controller/railties/helpers.rb +++ b/actionpack/lib/action_controller/railties/helpers.rb @@ -7,7 +7,7 @@ module ActionController super return unless klass.respond_to?(:helpers_path=) - if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_helpers_paths) } + if namespace = klass.module_parents.detect { |m| m.respond_to?(:railtie_helpers_paths) } paths = namespace.railtie_helpers_paths else paths = ActionController::Helpers.helpers_path diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb index 49c5b782f0..cf8c0159e2 100644 --- a/actionpack/lib/action_controller/renderer.rb +++ b/actionpack/lib/action_controller/renderer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/hash/keys" - module ActionController # ActionController::Renderer allows you to render arbitrary templates # without requirement of being in controller actions. @@ -71,6 +69,21 @@ module ActionController end # Render templates with any options from ActionController::Base#render_to_string. + # + # The primary options are: + # * <tt>:partial</tt> - See <tt>ActionView::PartialRenderer</tt> for details. + # * <tt>:file</tt> - Renders an explicit template file. Add <tt>:locals</tt> to pass in, if so desired. + # It shouldn’t be used directly with unsanitized user input due to lack of validation. + # * <tt>:inline</tt> - Renders a ERB template string. + # * <tt>:plain</tt> - Renders provided text and sets the content type as <tt>text/plain</tt>. + # * <tt>:html</tt> - Renders the provided HTML safe string, otherwise + # performs HTML escape on the string first. Sets the content type as <tt>text/html</tt>. + # * <tt>:json</tt> - Renders the provided hash or object in JSON. You don't + # need to call <tt>.to_json</tt> on the object you want to render. + # * <tt>:body</tt> - Renders provided text and sets content type of <tt>text/plain</tt>. + # + # If no <tt>options</tt> hash is passed or if <tt>:update</tt> is specified, the default is + # to render a partial and use the second parameter as the locals hash. def render(*args) raise "missing controller" unless controller diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 4b408750a4..57921f32b7 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -26,7 +26,7 @@ module ActionController end end - # ActionController::TestCase will be deprecated and moved to a gem in Rails 5.1. + # ActionController::TestCase will be deprecated and moved to a gem in the future. # Please use ActionDispatch::IntegrationTest going forward. class TestRequest < ActionDispatch::TestRequest #:nodoc: DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup @@ -256,7 +256,7 @@ module ActionController # # def test_create # json = {book: { title: "Love Hina" }}.to_json - # post :create, json + # post :create, body: json # end # # == Special instance variables @@ -276,9 +276,6 @@ module ActionController # after calling +post+. If the various assert methods are not sufficient, then you # may use this object to inspect the HTTP response in detail. # - # (Earlier versions of \Rails required each functional test to subclass - # Test::Unit::TestCase and define @controller, @request, @response in +setup+.) - # # == Controller is automatically inferred # # ActionController::TestCase will automatically infer the controller under test @@ -457,13 +454,9 @@ module ActionController # respectively which will make tests more expressive. # # Note that the request method is not verified. - def process(action, method: "GET", params: {}, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil) + def process(action, method: "GET", params: nil, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil) check_required_ivars - if body - @request.set_header "RAW_POST_DATA", body - end - http_method = method.to_s.upcase @html_document = nil @@ -478,6 +471,10 @@ module ActionController @response.request = @request @controller.recycle! + if body + @request.set_header "RAW_POST_DATA", body + end + @request.set_header "REQUEST_METHOD", http_method if as @@ -485,7 +482,7 @@ module ActionController format ||= as end - parameters = params.symbolize_keys + parameters = (params || {}).symbolize_keys if format parameters[:format] = format @@ -604,6 +601,8 @@ module ActionController env.delete "action_dispatch.request.query_parameters" env.delete "action_dispatch.request.request_parameters" env["rack.input"] = StringIO.new + env.delete "CONTENT_LENGTH" + env.delete "RAW_POST_DATA" env end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 6fed911d0a..8f39b88d56 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2017 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -49,11 +49,13 @@ module ActionDispatch end autoload_under "middleware" do + autoload :HostAuthorization autoload :RequestId autoload :Callbacks autoload :Cookies autoload :DebugExceptions autoload :DebugLocks + autoload :DebugView autoload :ExceptionWrapper autoload :Executor autoload :Flash diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index a8febc32b3..8cc84ff36c 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -4,8 +4,8 @@ module ActionDispatch module Http module Cache module Request - HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE".freeze - HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH".freeze + HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE" + HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH" def if_modified_since if since = get_header(HTTP_IF_MODIFIED_SINCE) @@ -124,8 +124,8 @@ module ActionDispatch private - DATE = "Date".freeze - LAST_MODIFIED = "Last-Modified".freeze + DATE = "Date" + LAST_MODIFIED = "Last-Modified" SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate]) def generate_weak_etag(validators) @@ -166,11 +166,11 @@ module ActionDispatch @cache_control = cache_control_headers end - DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze - NO_CACHE = "no-cache".freeze - PUBLIC = "public".freeze - PRIVATE = "private".freeze - MUST_REVALIDATE = "must-revalidate".freeze + DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate" + NO_CACHE = "no-cache" + PUBLIC = "public" + PRIVATE = "private" + MUST_REVALIDATE = "must-revalidate" def handle_conditional_get! # Normally default cache control setting is handled by ETag @@ -197,18 +197,24 @@ module ActionDispatch if control.empty? # Let middleware handle default behavior elsif control[:no_cache] - self._cache_control = NO_CACHE - if control[:extras] - self._cache_control = _cache_control + ", #{control[:extras].join(', ')}" - end + options = [] + options << PUBLIC if control[:public] + options << NO_CACHE + options.concat(control[:extras]) if control[:extras] + + self._cache_control = options.join(", ") else - extras = control[:extras] + extras = control[:extras] max_age = control[:max_age] + stale_while_revalidate = control[:stale_while_revalidate] + stale_if_error = control[:stale_if_error] options = [] options << "max-age=#{max_age.to_i}" if max_age options << (control[:public] ? PUBLIC : PRIVATE) options << MUST_REVALIDATE if control[:must_revalidate] + options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate + options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error options.concat(extras) if extras self._cache_control = options.join(", ") diff --git a/actionpack/lib/action_dispatch/http/content_disposition.rb b/actionpack/lib/action_dispatch/http/content_disposition.rb new file mode 100644 index 0000000000..58164c1522 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/content_disposition.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module ActionDispatch + module Http + class ContentDisposition # :nodoc: + def self.format(disposition:, filename:) + new(disposition: disposition, filename: filename).to_s + end + + attr_reader :disposition, :filename + + def initialize(disposition:, filename:) + @disposition = disposition + @filename = filename + end + + TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/ + + def ascii_filename + 'filename="' + percent_escape(I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"' + end + + RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/ + + def utf8_filename + "filename*=UTF-8''" + percent_escape(filename, RFC_5987_ESCAPED_CHAR) + end + + def to_s + if filename + "#{disposition}; #{ascii_filename}; #{utf8_filename}" + else + "#{disposition}" + end + end + + private + def percent_escape(string, pattern) + string.gsub(pattern) do |char| + char.bytes.map { |byte| "%%%02X" % byte }.join + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb index 4883e23d24..b1e5a28be5 100644 --- a/actionpack/lib/action_dispatch/http/content_security_policy.rb +++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb @@ -5,9 +5,9 @@ require "active_support/core_ext/object/deep_dup" module ActionDispatch #:nodoc: class ContentSecurityPolicy class Middleware - CONTENT_TYPE = "Content-Type".freeze - POLICY = "Content-Security-Policy".freeze - POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze + CONTENT_TYPE = "Content-Type" + POLICY = "Content-Security-Policy" + POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only" def initialize(app) @app = app @@ -21,7 +21,9 @@ module ActionDispatch #:nodoc: return response if policy_present?(headers) if policy = request.content_security_policy - headers[header_name(request)] = policy.build(request.controller_instance) + nonce = request.content_security_policy_nonce + context = request.controller_instance || request + headers[header_name(request)] = policy.build(context, nonce) end response @@ -49,8 +51,10 @@ module ActionDispatch #:nodoc: end module Request - POLICY = "action_dispatch.content_security_policy".freeze - POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only".freeze + POLICY = "action_dispatch.content_security_policy" + POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only" + NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator" + NONCE = "action_dispatch.content_security_policy_nonce" def content_security_policy get_header(POLICY) @@ -67,6 +71,30 @@ module ActionDispatch #:nodoc: def content_security_policy_report_only=(value) set_header(POLICY_REPORT_ONLY, value) end + + def content_security_policy_nonce_generator + get_header(NONCE_GENERATOR) + end + + def content_security_policy_nonce_generator=(generator) + set_header(NONCE_GENERATOR, generator) + end + + def content_security_policy_nonce + if content_security_policy_nonce_generator + if nonce = get_header(NONCE) + nonce + else + set_header(NONCE, generate_content_security_policy_nonce) + end + end + end + + private + + def generate_content_security_policy_nonce + content_security_policy_nonce_generator.call(self) + end end MAPPINGS = { @@ -81,7 +109,9 @@ module ActionDispatch #:nodoc: blob: "blob:", filesystem: "filesystem:", report_sample: "'report-sample'", - strict_dynamic: "'strict-dynamic'" + strict_dynamic: "'strict-dynamic'", + ws: "ws:", + wss: "wss:" }.freeze DIRECTIVES = { @@ -97,12 +127,15 @@ module ActionDispatch #:nodoc: manifest_src: "manifest-src", media_src: "media-src", object_src: "object-src", + prefetch_src: "prefetch-src", script_src: "script-src", style_src: "style-src", worker_src: "worker-src" }.freeze - private_constant :MAPPINGS, :DIRECTIVES + NONCE_DIRECTIVES = %w[script-src style-src].freeze + + private_constant :MAPPINGS, :DIRECTIVES, :NONCE_DIRECTIVES attr_reader :directives @@ -171,8 +204,8 @@ module ActionDispatch #:nodoc: end end - def build(context = nil) - build_directives(context).compact.join("; ") + ";" + def build(context = nil, nonce = nil) + build_directives(context, nonce).compact.join("; ") end private @@ -195,10 +228,14 @@ module ActionDispatch #:nodoc: end end - def build_directives(context) + def build_directives(context, nonce) @directives.map do |directive, sources| if sources.is_a?(Array) - "#{directive} #{build_directive(sources, context).join(' ')}" + if nonce && nonce_directive?(directive) + "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'" + else + "#{directive} #{build_directive(sources, context).join(' ')}" + end elsif sources directive else @@ -221,11 +258,16 @@ module ActionDispatch #:nodoc: if context.nil? raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}" else - context.instance_exec(&source) + resolved = context.instance_exec(&source) + resolved.is_a?(Symbol) ? apply_mapping(resolved) : resolved end else raise RuntimeError, "Unexpected content security policy source: #{source.inspect}" end end + + def nonce_directive?(directive) + NONCE_DIRECTIVES.include?(directive) + end end end diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index ec86b8bc47..cbb772175c 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "action_dispatch/http/parameter_filter" +require "active_support/parameter_filter" module ActionDispatch module Http @@ -9,8 +9,8 @@ module ActionDispatch # sub-hashes of the params hash to filter. Filtering only certain sub-keys # from a hash is possible by using the dot notation: 'credit_card.number'. # If a block is given, each key and value of the params hash and all - # sub-hashes is passed to it, where the value or the key can be replaced using - # String#replace or similar method. + # sub-hashes are passed to it, where the value or the key can be replaced using + # String#replace or similar methods. # # env["action_dispatch.parameter_filter"] = [:password] # => replaces the value to all keys matching /password/i with "[FILTERED]" @@ -28,8 +28,8 @@ module ActionDispatch # => reverses the value to all keys matching /secret/i module FilterParameters ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc: - NULL_PARAM_FILTER = ParameterFilter.new # :nodoc: - NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc: + NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new # :nodoc: + NULL_ENV_FILTER = ActiveSupport::ParameterFilter.new ENV_MATCH # :nodoc: def initialize super @@ -41,6 +41,8 @@ module ActionDispatch # Returns a hash of parameters with all sensitive data replaced. def filtered_parameters @filtered_parameters ||= parameter_filter.filter(parameters) + rescue ActionDispatch::Http::Parameters::ParseError + @filtered_parameters = {} end # Returns a hash of request.env with all sensitive data replaced. @@ -69,7 +71,7 @@ module ActionDispatch end def parameter_filter_for(filters) # :doc: - ParameterFilter.new(filters) + ActiveSupport::ParameterFilter.new(filters) end KV_RE = "[^&;=]+" diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb index 25394fe5dd..8c4e852235 100644 --- a/actionpack/lib/action_dispatch/http/filter_redirect.rb +++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb @@ -3,7 +3,7 @@ module ActionDispatch module Http module FilterRedirect - FILTERED = "[FILTERED]".freeze # :nodoc: + FILTERED = "[FILTERED]" # :nodoc: def filtered_location # :nodoc: if location_filter_match? diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index c3c2a9d8c5..6c7d24d2d0 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -121,7 +121,7 @@ module ActionDispatch # not contained within the headers hash. def env_name(key) key = key.to_s - if key =~ HTTP_HEADER + if HTTP_HEADER.match?(key) key = key.upcase.tr("-", "_") key = "HTTP_" + key unless CGI_VARIABLES.include?(key) end diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index d7435fa8df..498b1e6695 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -7,6 +7,11 @@ module ActionDispatch module MimeNegotiation extend ActiveSupport::Concern + RESCUABLE_MIME_FORMAT_ERRORS = [ + ActionController::BadRequest, + ActionDispatch::Http::Parameters::ParseError, + ] + included do mattr_accessor :ignore_accept_header, default: false end @@ -59,7 +64,7 @@ module ActionDispatch fetch_header("action_dispatch.request.formats") do |k| params_readable = begin parameters[:format] - rescue ActionController::BadRequest + rescue *RESCUABLE_MIME_FORMAT_ERRORS false end @@ -85,10 +90,7 @@ module ActionDispatch if variant.all? { |v| v.is_a?(Symbol) } @variant = ActiveSupport::ArrayInquirer.new(variant) else - raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols. " \ - "For security reasons, never directly set the variant to a user-provided value, " \ - "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \ - "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'" + raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols." end end diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index d2b2106845..c3e0ea3c89 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -# -*- frozen-string-literal: true -*- - require "singleton" require "active_support/core_ext/string/starts_ends_with" @@ -74,7 +72,7 @@ module Mime def initialize(index, name, q = nil) @index = index @name = name - q ||= 0.0 if @name == "*/*".freeze # Default wildcard match to end of list. + q ||= 0.0 if @name == "*/*" # Default wildcard match to end of list. @q = ((q || 1.0).to_f * 100).to_i end @@ -279,8 +277,6 @@ module Mime def all?; false; end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :string, :synonyms diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb index 1d58964862..ddeb3d81e2 100644 --- a/actionpack/lib/action_dispatch/http/parameter_filter.rb +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -1,86 +1,12 @@ # frozen_string_literal: true -require "active_support/core_ext/object/duplicable" +require "active_support/deprecation/constant_accessor" +require "active_support/parameter_filter" module ActionDispatch module Http - class ParameterFilter - FILTERED = "[FILTERED]".freeze # :nodoc: - - def initialize(filters = []) - @filters = filters - end - - def filter(params) - compiled_filter.call(params) - end - - private - - def compiled_filter - @compiled_filter ||= CompiledFilter.compile(@filters) - end - - class CompiledFilter # :nodoc: - def self.compile(filters) - return lambda { |params| params.dup } if filters.empty? - - strings, regexps, blocks = [], [], [] - - filters.each do |item| - case item - when Proc - blocks << item - when Regexp - regexps << item - else - strings << Regexp.escape(item.to_s) - end - end - - deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.".freeze) } - deep_strings, strings = strings.partition { |s| s.include?("\\.".freeze) } - - regexps << Regexp.new(strings.join("|".freeze), true) unless strings.empty? - deep_regexps << Regexp.new(deep_strings.join("|".freeze), true) unless deep_strings.empty? - - new regexps, deep_regexps, blocks - end - - attr_reader :regexps, :deep_regexps, :blocks - - def initialize(regexps, deep_regexps, blocks) - @regexps = regexps - @deep_regexps = deep_regexps.any? ? deep_regexps : nil - @blocks = blocks - end - - def call(original_params, parents = []) - filtered_params = original_params.class.new - - original_params.each do |key, value| - parents.push(key) if deep_regexps - if regexps.any? { |r| key =~ r } - value = FILTERED - elsif deep_regexps && (joined = parents.join(".")) && deep_regexps.any? { |r| joined =~ r } - value = FILTERED - elsif value.is_a?(Hash) - value = call(value, parents) - elsif value.is_a?(Array) - value = value.map { |v| v.is_a?(Hash) ? call(v, parents) : v } - elsif blocks.any? - key = key.dup if key.duplicable? - value = value.dup if value.duplicable? - blocks.each { |b| b.call(key, value) } - end - parents.pop if deep_regexps - - filtered_params[key] = value - end - - filtered_params - end - end - end + include ActiveSupport::Deprecation::DeprecatedConstantAccessor + deprecate_constant "ParameterFilter", "ActiveSupport::ParameterFilter", + message: "ActionDispatch::Http::ParameterFilter is deprecated and will be removed from Rails 6.1. Use ActiveSupport::ParameterFilter instead." end end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 8d7431fd6b..13d0963a33 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -111,13 +111,23 @@ module ActionDispatch begin strategy.call(raw_post) rescue # JSON or Ruby code block errors. - my_logger = logger || ActiveSupport::Logger.new($stderr) - my_logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{raw_post}" - + log_parse_error_once raise ParseError end end + def log_parse_error_once + @parse_error_logged ||= begin + parse_logger = logger || ActiveSupport::Logger.new($stderr) + parse_logger.debug <<~MSG.chomp + Error occurred while parsing request parameters. + Contents: + + #{raw_post} + MSG + end + end + def params_parsers ActionDispatch::Request.parameter_parsers end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 3838b84a7a..44f23940d3 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -136,11 +136,11 @@ module ActionDispatch end def routes # :nodoc: - get_header("action_dispatch.routes".freeze) + get_header("action_dispatch.routes") end def routes=(routes) # :nodoc: - set_header("action_dispatch.routes".freeze, routes) + set_header("action_dispatch.routes", routes) end def engine_script_name(_routes) # :nodoc: @@ -158,11 +158,11 @@ module ActionDispatch end def controller_instance # :nodoc: - get_header("action_controller.instance".freeze) + get_header("action_controller.instance") end def controller_instance=(controller) # :nodoc: - set_header("action_controller.instance".freeze, controller) + set_header("action_controller.instance", controller) end def http_auth_salt @@ -173,7 +173,7 @@ module ActionDispatch # We're treating `nil` as "unset", and we want the default setting to be # `true`. This logic should be extracted to `env_config` and calculated # once. - !(get_header("action_dispatch.show_exceptions".freeze) == false) + !(get_header("action_dispatch.show_exceptions") == false) end # Returns a symbol form of the #request_method. @@ -280,10 +280,10 @@ module ActionDispatch end def remote_ip=(remote_ip) - set_header "action_dispatch.remote_ip".freeze, remote_ip + set_header "action_dispatch.remote_ip", remote_ip end - ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # :nodoc: + ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id" # :nodoc: # Returns the unique request id, which is based on either the X-Request-Id header that can # be generated by a firewall, load balancer, or web server or by the RequestId middleware @@ -383,9 +383,6 @@ module ActionDispatch end self.request_parameters = Request::Utils.normalize_encode_params(pr) end - rescue Http::Parameters::ParseError # one of the parse strategies blew up - self.request_parameters = Request::Utils.normalize_encode_params(super || {}) - raise rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}") end @@ -407,18 +404,18 @@ module ActionDispatch def request_parameters=(params) raise if params.nil? - set_header("action_dispatch.request.request_parameters".freeze, params) + set_header("action_dispatch.request.request_parameters", params) end def logger - get_header("action_dispatch.logger".freeze) + get_header("action_dispatch.logger") end def commit_flash end def ssl? - super || scheme == "wss".freeze + super || scheme == "wss" end private diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 7e50cb6d23..1d38942a31 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -78,9 +78,9 @@ module ActionDispatch # :nodoc: x end - CONTENT_TYPE = "Content-Type".freeze - SET_COOKIE = "Set-Cookie".freeze - LOCATION = "Location".freeze + CONTENT_TYPE = "Content-Type" + SET_COOKIE = "Set-Cookie" + LOCATION = "Location" NO_CONTENT_CODES = [100, 101, 102, 204, 205, 304] cattr_accessor :default_charset, default: "utf-8" @@ -105,7 +105,7 @@ module ActionDispatch # :nodoc: def body @str_body ||= begin - buf = "".dup + buf = +"" each { |chunk| buf << chunk } buf end @@ -224,16 +224,6 @@ module ActionDispatch # :nodoc: @status = Rack::Utils.status_code(status) end - # Sets the HTTP content type. - def content_type=(content_type) - return unless content_type - new_header_info = parse_content_type(content_type.to_s) - prev_header_info = parsed_content_type_header - charset = new_header_info.charset || prev_header_info.charset - charset ||= self.class.default_charset unless prev_header_info.mime_type - set_content_type new_header_info.mime_type, charset - end - # Sets the HTTP response's content MIME type. For example, in the controller # you could write this: # @@ -242,7 +232,17 @@ module ActionDispatch # :nodoc: # If a character set has been defined for this response (see charset=) then # the character set information will also be included in the content type # information. + def content_type=(content_type) + return unless content_type + new_header_info = parse_content_type(content_type.to_s) + prev_header_info = parsed_content_type_header + charset = new_header_info.charset || prev_header_info.charset + charset ||= self.class.default_charset unless prev_header_info.mime_type + set_content_type new_header_info.mime_type, charset + end + # Content type of response. + # It returns just MIME type and does NOT contain charset part. def content_type parsed_content_type_header.mime_type end diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 0b162dc7f1..827f022ca2 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -65,6 +65,11 @@ module ActionDispatch @tempfile.path end + # Shortcut for +tempfile.to_path+. + def to_path + @tempfile.to_path + end + # Shortcut for +tempfile.rewind+. def rewind @tempfile.rewind diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index f0344fd927..8227749986 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -67,7 +67,7 @@ module ActionDispatch end def path_for(options) - path = options[:script_name].to_s.chomp("/".freeze) + path = options[:script_name].to_s.chomp("/") path << options[:path] if options.key?(:path) add_trailing_slash(path) if options[:trailing_slash] @@ -79,108 +79,108 @@ module ActionDispatch private - def add_params(path, params) - params = { params: params } unless params.is_a?(Hash) - params.reject! { |_, v| v.to_param.nil? } - query = params.to_query - path << "?#{query}" unless query.empty? - end - - def add_anchor(path, anchor) - if anchor - path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param)}" + def add_params(path, params) + params = { params: params } unless params.is_a?(Hash) + params.reject! { |_, v| v.to_param.nil? } + query = params.to_query + path << "?#{query}" unless query.empty? end - end - def extract_domain_from(host, tld_length) - host.split(".").last(1 + tld_length).join(".") - end + def add_anchor(path, anchor) + if anchor + path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param)}" + end + end - def extract_subdomains_from(host, tld_length) - parts = host.split(".") - parts[0..-(tld_length + 2)] - end + def extract_domain_from(host, tld_length) + host.split(".").last(1 + tld_length).join(".") + end - def add_trailing_slash(path) - if path.include?("?") - path.sub!(/\?/, '/\&') - elsif !path.include?(".") - path.sub!(/[^\/]\z|\A\z/, '\&/') + def extract_subdomains_from(host, tld_length) + parts = host.split(".") + parts[0..-(tld_length + 2)] end - end - def build_host_url(host, port, protocol, options, path) - if match = host.match(HOST_REGEXP) - protocol ||= match[1] unless protocol == false - host = match[2] - port = match[3] unless options.key? :port + def add_trailing_slash(path) + if path.include?("?") + path.sub!(/\?/, '/\&') + elsif !path.include?(".") + path.sub!(/[^\/]\z|\A\z/, '\&/') + end end - protocol = normalize_protocol protocol - host = normalize_host(host, options) + def build_host_url(host, port, protocol, options, path) + if match = host.match(HOST_REGEXP) + protocol ||= match[1] unless protocol == false + host = match[2] + port = match[3] unless options.key? :port + end - result = protocol.dup + protocol = normalize_protocol protocol + host = normalize_host(host, options) - if options[:user] && options[:password] - result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" - end + result = protocol.dup - result << host - normalize_port(port, protocol) { |normalized_port| - result << ":#{normalized_port}" - } + if options[:user] && options[:password] + result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" + end - result.concat path - end + result << host + normalize_port(port, protocol) { |normalized_port| + result << ":#{normalized_port}" + } - def named_host?(host) - IP_HOST_REGEXP !~ host - end + result.concat path + end - def normalize_protocol(protocol) - case protocol - when nil - "http://" - when false, "//" - "//" - when PROTOCOL_REGEXP - "#{$1}://" - else - raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}" + def named_host?(host) + IP_HOST_REGEXP !~ host end - end - def normalize_host(_host, options) - return _host unless named_host?(_host) + def normalize_protocol(protocol) + case protocol + when nil + "http://" + when false, "//" + "//" + when PROTOCOL_REGEXP + "#{$1}://" + else + raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}" + end + end - tld_length = options[:tld_length] || @@tld_length - subdomain = options.fetch :subdomain, true - domain = options[:domain] + def normalize_host(_host, options) + return _host unless named_host?(_host) - host = "".dup - if subdomain == true - return _host if domain.nil? + tld_length = options[:tld_length] || @@tld_length + subdomain = options.fetch :subdomain, true + domain = options[:domain] - host << extract_subdomains_from(_host, tld_length).join(".") - elsif subdomain - host << subdomain.to_param + host = +"" + if subdomain == true + return _host if domain.nil? + + host << extract_subdomains_from(_host, tld_length).join(".") + elsif subdomain + host << subdomain.to_param + end + host << "." unless host.empty? + host << (domain || extract_domain_from(_host, tld_length)) + host end - host << "." unless host.empty? - host << (domain || extract_domain_from(_host, tld_length)) - host - end - def normalize_port(port, protocol) - return unless port + def normalize_port(port, protocol) + return unless port - case protocol - when "//" then yield port - when "https://" - yield port unless port.to_i == 443 - else - yield port unless port.to_i == 80 + case protocol + when "//" then yield port + when "https://" + yield port unless port.to_i == 443 + else + yield port unless port.to_i == 80 + end end - end end def initialize @@ -231,7 +231,7 @@ module ActionDispatch # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req.host # => "example.com" def host - raw_host_with_port.sub(/:\d+$/, "".freeze) + raw_host_with_port.sub(/:\d+$/, "") end # Returns a \host:\port string for this request, such as "example.com" or @@ -274,7 +274,7 @@ module ActionDispatch def standard_port case protocol when "https://" then 443 - else 80 + else 80 end end diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 0f04839d9b..52396ec901 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -50,7 +50,7 @@ module ActionDispatch unmatched_keys = (missing_keys || []) & constraints.keys missing_keys = (missing_keys || []) - unmatched_keys - message = "No route matches #{Hash[constraints.sort_by { |k, v| k.to_s }].inspect}".dup + message = +"No route matches #{Hash[constraints.sort_by { |k, v| k.to_s }].inspect}" message << ", missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty? message << ", possible unmatched constraints: #{unmatched_keys.sort.inspect}" if unmatched_keys && !unmatched_keys.empty? diff --git a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb index 8efe48d91c..002f6feb97 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb @@ -25,8 +25,6 @@ module ActionDispatch state = tt.eclosure(0) until input.eos? sym = input.scan(%r([/.?]|[^/.?]+)) - - # FIXME: tt.eclosure is not needed for the GTG state = tt.eclosure(tt.move(state, sym)) end diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb index 08b931a3cd..086d6a3e07 100644 --- a/actionpack/lib/action_dispatch/journey/nodes/node.rb +++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb @@ -32,7 +32,7 @@ module ActionDispatch end def name - left.tr "*:".freeze, "".freeze + -left.tr("*:", "") end def type @@ -65,12 +65,12 @@ module ActionDispatch def literal?; false; end end - %w{ Symbol Slash Dot }.each do |t| - class_eval <<-eoruby, __FILE__, __LINE__ + 1 - class #{t} < Terminal; - def type; :#{t.upcase}; end - end - eoruby + class Slash < Terminal # :nodoc: + def type; :SLASH; end + end + + class Dot < Terminal # :nodoc: + def type; :DOT; end end class Symbol < Terminal # :nodoc: @@ -82,13 +82,14 @@ module ActionDispatch def initialize(left) super @regexp = DEFAULT_EXP - @name = left.tr "*:".freeze, "".freeze + @name = -left.tr("*:", "") end def default_regexp? regexp == DEFAULT_EXP end + def type; :SYMBOL; end def symbol?; true; end end diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb index 2d85a89a56..697f5b9d8b 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -90,7 +90,7 @@ module ActionDispatch return @separator_re unless @matchers.key?(node) re = @matchers[node] - "(#{re})" + "(#{Regexp.union(re)})" end def visit_GROUP(node) @@ -119,7 +119,7 @@ module ActionDispatch class UnanchoredRegexp < AnchoredRegexp # :nodoc: def accept(node) - %r{\A#{visit node}} + %r{\A#{visit node}(?:\b|\Z)} end end @@ -183,7 +183,7 @@ module ActionDispatch node = node.to_sym if @requirements.key?(node) - re = /#{@requirements[node]}|/ + re = /#{Regexp.union(@requirements[node])}|/ @offsets.push((re.match("").length - 1) + @offsets.last) else @offsets << @offsets.last diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index 30af3ff930..89a164f968 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -15,9 +15,6 @@ require "action_dispatch/journey/path/pattern" module ActionDispatch module Journey # :nodoc: class Router # :nodoc: - class RoutingError < ::StandardError # :nodoc: - end - attr_accessor :routes def initialize(routes) diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index df3f79a407..3c8b9a6eaa 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -17,11 +17,11 @@ module ActionDispatch def self.normalize_path(path) path ||= "" encoding = path.encoding - path = "/#{path}".dup - path.squeeze!("/".freeze) - path.sub!(%r{/+\Z}, "".freeze) + path = +"/#{path}" + path.squeeze!("/") + path.sub!(%r{/+\Z}, "") path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase } - path = "/".dup if path == "".freeze + path = +"/" if path == "" path.force_encoding(encoding) path end @@ -29,16 +29,16 @@ module ActionDispatch # URI path and fragment escaping # https://tools.ietf.org/html/rfc3986 class UriEncoder # :nodoc: - ENCODE = "%%%02X".freeze + ENCODE = "%%%02X" US_ASCII = Encoding::US_ASCII UTF_8 = Encoding::UTF_8 - EMPTY = "".dup.force_encoding(US_ASCII).freeze + EMPTY = (+"").force_encoding(US_ASCII).freeze DEC2HEX = (0..255).to_a.map { |i| ENCODE % i }.map { |s| s.force_encoding(US_ASCII) } - ALPHA = "a-zA-Z".freeze - DIGIT = "0-9".freeze - UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~".freeze - SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;=".freeze + ALPHA = "a-zA-Z" + DIGIT = "0-9" + UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~" + SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;=" ESCAPED = /%[a-zA-Z0-9]{2}/.freeze diff --git a/actionpack/lib/action_dispatch/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb index 639c063495..c0377459d5 100644 --- a/actionpack/lib/action_dispatch/journey/routes.rb +++ b/actionpack/lib/action_dispatch/journey/routes.rb @@ -51,11 +51,12 @@ module ActionDispatch def ast @ast ||= begin asts = anchored_routes.map(&:ast) - Nodes::Or.new(asts) unless asts.empty? + Nodes::Or.new(asts) end end def simulator + return if ast.nil? @simulator ||= begin gtg = GTG::Builder.new(ast).transition_table GTG::Simulator.new(gtg) diff --git a/actionpack/lib/action_dispatch/journey/scanner.rb b/actionpack/lib/action_dispatch/journey/scanner.rb index 4ae77903fa..2a075862e9 100644 --- a/actionpack/lib/action_dispatch/journey/scanner.rb +++ b/actionpack/lib/action_dispatch/journey/scanner.rb @@ -34,6 +34,13 @@ module ActionDispatch private + # takes advantage of String @- deduping capabilities in Ruby 2.5 upwards + # see: https://bugs.ruby-lang.org/issues/13077 + def dedup_scan(regex) + r = @ss.scan(regex) + r ? -r : nil + end + def scan case # / @@ -47,15 +54,15 @@ module ActionDispatch [:OR, "|"] when @ss.skip(/\./) [:DOT, "."] - when text = @ss.scan(/:\w+/) + when text = dedup_scan(/:\w+/) [:SYMBOL, text] - when text = @ss.scan(/\*\w+/) + when text = dedup_scan(/\*\w+/) [:STAR, text] when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\[:()])+/) text.tr! "\\", "" - [:LITERAL, text] + [:LITERAL, -text] # any char - when text = @ss.scan(/./) + when text = dedup_scan(/./) [:LITERAL, text] end end diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index 3395471a85..d2619cbf3a 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -40,7 +40,7 @@ module ActionDispatch @parameters.each do |index| param = parts[index] value = hash[param.name] - return "".freeze unless value + return "" unless value parts[index] = param.escape value end diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index 5b2ad36dd5..87fe19225b 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -24,10 +24,8 @@ module ActionDispatch def call(env) error = nil result = run_callbacks :call do - begin - @app.call(env) - rescue => error - end + @app.call(env) + rescue => error end raise error if error result diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index ea4156c972..1611a8b3dd 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -9,7 +9,7 @@ require "rack/utils" module ActionDispatch class Request def cookie_jar - fetch_header("action_dispatch.cookies".freeze) do + fetch_header("action_dispatch.cookies") do self.cookie_jar = Cookies::CookieJar.build(self, cookies) end end @@ -22,11 +22,11 @@ module ActionDispatch } def have_cookie_jar? - has_header? "action_dispatch.cookies".freeze + has_header? "action_dispatch.cookies" end def cookie_jar=(jar) - set_header "action_dispatch.cookies".freeze, jar + set_header "action_dispatch.cookies", jar end def key_generator @@ -61,10 +61,6 @@ module ActionDispatch get_header Cookies::SIGNED_COOKIE_DIGEST end - def secret_token - get_header Cookies::SECRET_TOKEN - end - def secret_key_base get_header Cookies::SECRET_KEY_BASE end @@ -81,6 +77,10 @@ module ActionDispatch get_header Cookies::COOKIES_ROTATIONS end + def use_cookies_with_metadata + get_header Cookies::USE_COOKIES_WITH_METADATA + end + # :startdoc: end @@ -168,20 +168,20 @@ module ActionDispatch # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or # only HTTP. Defaults to +false+. class Cookies - HTTP_HEADER = "Set-Cookie".freeze - GENERATOR_KEY = "action_dispatch.key_generator".freeze - SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze - ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze - ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze - AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt".freeze - USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption".freeze - ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher".freeze - SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest".freeze - SECRET_TOKEN = "action_dispatch.secret_token".freeze - SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze - COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze - COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze - COOKIES_ROTATIONS = "action_dispatch.cookies_rotations".freeze + HTTP_HEADER = "Set-Cookie" + GENERATOR_KEY = "action_dispatch.key_generator" + SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt" + ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt" + ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt" + AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt" + USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption" + ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher" + SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest" + SECRET_KEY_BASE = "action_dispatch.secret_key_base" + COOKIES_SERIALIZER = "action_dispatch.cookies_serializer" + COOKIES_DIGEST = "action_dispatch.cookies_digest" + COOKIES_ROTATIONS = "action_dispatch.cookies_rotations" + USE_COOKIES_WITH_METADATA = "action_dispatch.use_cookies_with_metadata" # Cookies can typically store 4096 bytes. MAX_COOKIE_SIZE = 4096 @@ -210,9 +210,6 @@ module ActionDispatch # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed # cookie was tampered with by the user (or a 3rd party), +nil+ will be returned. # - # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, - # legacy cookies signed with the old key generator will be transparently upgraded. - # # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+. # # Example: @@ -228,9 +225,6 @@ module ActionDispatch # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. # If the cookie was tampered with by the user (or a 3rd party), +nil+ will be returned. # - # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, - # legacy cookies signed with the old key generator will be transparently upgraded. - # # If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+ # are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded. # @@ -259,10 +253,6 @@ module ActionDispatch private - def upgrade_legacy_signed_cookies? - request.secret_token.present? && request.secret_key_base.present? - end - def upgrade_legacy_hmac_aes_cbc_cookies? request.secret_key_base.present? && request.encrypted_signed_cookie_salt.present? && @@ -338,6 +328,9 @@ module ActionDispatch end alias :has_key? :key? + # Returns the cookies as Hash. + alias :to_hash :to_h + def update(other_hash) @cookies.update other_hash.stringify_keys self @@ -467,7 +460,7 @@ module ActionDispatch def [](name) if data = @parent_jar[name.to_s] - parse name, data + parse(name, data, purpose: "cookie.#{name}") || parse(name, data) end end @@ -478,7 +471,7 @@ module ActionDispatch options = { value: options } end - commit(options) + commit(name, options) @parent_jar[name] = options end @@ -494,13 +487,19 @@ module ActionDispatch end end - def parse(name, data); data; end - def commit(options); end + def cookie_metadata(name, options) + expiry_options(options).tap do |metadata| + metadata[:purpose] = "cookie.#{name}" if request.use_cookies_with_metadata + end + end + + def parse(name, data, purpose: nil); data; end + def commit(name, options); end end class PermanentCookieJar < AbstractCookieJar # :nodoc: private - def commit(options) + def commit(name, options) options[:expires] = 20.years.from_now end end @@ -516,7 +515,7 @@ module ActionDispatch end module SerializedCookieJars # :nodoc: - MARSHAL_SIGNATURE = "\x04\x08".freeze + MARSHAL_SIGNATURE = "\x04\x08" SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer protected @@ -573,21 +572,17 @@ module ActionDispatch request.cookies_rotations.signed.each do |*secrets, **options| @verifier.rotate(*secrets, serializer: SERIALIZER, **options) end - - if upgrade_legacy_signed_cookies? - @verifier.rotate request.secret_token, serializer: SERIALIZER - end end private - def parse(name, signed_message) + def parse(name, signed_message, purpose: nil) deserialize(name) do |rotate| - @verifier.verified(signed_message, on_rotation: rotate) + @verifier.verified(signed_message, on_rotation: rotate, purpose: purpose) end end - def commit(options) - options[:value] = @verifier.generate(serialize(options[:value]), expiry_options(options)) + def commit(name, options) + options[:value] = @verifier.generate(serialize(options[:value]), cookie_metadata(name, options)) raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE end @@ -621,36 +616,22 @@ module ActionDispatch @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER) end - - if upgrade_legacy_signed_cookies? - @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, digest: digest, serializer: SERIALIZER) - end end private - def parse(name, encrypted_message) + def parse(name, encrypted_message, purpose: nil) deserialize(name) do |rotate| - @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate) + @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate, purpose: purpose) end rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature - parse_legacy_signed_message(name, encrypted_message) + nil end - def commit(options) - options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), expiry_options(options)) + def commit(name, options) + options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), cookie_metadata(name, options)) raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE end - - def parse_legacy_signed_message(name, legacy_signed_message) - if defined?(@legacy_verifier) - deserialize(name) do |rotate| - rotate.call - - @legacy_verifier.verified(legacy_signed_message) - end - end - end end def initialize(app) diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 511306eb0e..61773d97a2 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -3,57 +3,26 @@ require "action_dispatch/http/request" require "action_dispatch/middleware/exception_wrapper" require "action_dispatch/routing/inspector" + require "action_view" require "action_view/base" -require "pp" - module ActionDispatch # This middleware is responsible for logging exceptions and # showing a debugging page in case the request is local. class DebugExceptions - RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__) - - class DebugView < ActionView::Base - def debug_params(params) - clean_params = params.clone - clean_params.delete("action") - clean_params.delete("controller") - - if clean_params.empty? - "None" - else - PP.pp(clean_params, "".dup, 200) - end - end - - def debug_headers(headers) - if headers.present? - headers.inspect.gsub(",", ",\n") - else - "None" - end - end - - def debug_hash(object) - object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") - end - - def render(*) - logger = ActionView::Base.logger + cattr_reader :interceptors, instance_accessor: false, default: [] - if logger && logger.respond_to?(:silence) - logger.silence { super } - else - super - end - end + def self.register_interceptor(object = nil, &block) + interceptor = object || block + interceptors << interceptor end - def initialize(app, routes_app = nil, response_format = :default) + def initialize(app, routes_app = nil, response_format = :default, interceptors = self.class.interceptors) @app = app @routes_app = routes_app @response_format = response_format + @interceptors = interceptors end def call(env) @@ -67,12 +36,24 @@ module ActionDispatch response rescue Exception => exception + invoke_interceptors(request, exception) raise exception unless request.show_exceptions? render_exception(request, exception) end private + def invoke_interceptors(request, exception) + backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner") + wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) + + @interceptors.each do |interceptor| + interceptor.call(request, exception) + rescue Exception + log_error(request, wrapper) + end + end + def render_exception(request, exception) backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner") wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) @@ -130,23 +111,13 @@ module ActionDispatch end def create_template(request, wrapper) - traces = wrapper.traces - - trace_to_show = "Application Trace" - if traces[trace_to_show].empty? && wrapper.rescue_template != "routing_error" - trace_to_show = "Full Trace" - end - - if source_to_show = traces[trace_to_show].first - source_to_show_id = source_to_show[:id] - end - - DebugView.new([RESCUES_TEMPLATE_PATH], + DebugView.new( request: request, + exception_wrapper: wrapper, exception: wrapper.exception, - traces: traces, - show_source_idx: source_to_show_id, - trace_to_show: trace_to_show, + traces: wrapper.traces, + show_source_idx: wrapper.source_to_show_id, + trace_to_show: wrapper.trace_to_show, routes_inspector: routes_inspector(wrapper.exception), source_extracts: wrapper.source_extracts, line_number: wrapper.line_number, @@ -168,11 +139,14 @@ module ActionDispatch trace = wrapper.framework_trace if trace.empty? ActiveSupport::Deprecation.silence do - logger.fatal " " - logger.fatal "#{exception.class} (#{exception.message}):" - log_array logger, exception.annoted_source_code if exception.respond_to?(:annoted_source_code) - logger.fatal " " - log_array logger, trace + message = [] + message << " " + message << "#{exception.class} (#{exception.message}):" + message.concat(exception.annoted_source_code) if exception.respond_to?(:annoted_source_code) + message << " " + message.concat(trace) + + log_array(logger, message) end end diff --git a/actionpack/lib/action_dispatch/middleware/debug_locks.rb b/actionpack/lib/action_dispatch/middleware/debug_locks.rb index 03760438f7..93c6c85a71 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_locks.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_locks.rb @@ -32,7 +32,7 @@ module ActionDispatch req = ActionDispatch::Request.new env if req.get? - path = req.path_info.chomp("/".freeze) + path = req.path_info.chomp("/") if path == @path return render_details(req) end @@ -63,19 +63,19 @@ module ActionDispatch str = threads.map do |thread, info| if info[:exclusive] - lock_state = "Exclusive".dup + lock_state = +"Exclusive" elsif info[:sharing] > 0 - lock_state = "Sharing".dup + lock_state = +"Sharing" lock_state << " x#{info[:sharing]}" if info[:sharing] > 1 else - lock_state = "No lock".dup + lock_state = +"No lock" end if info[:waiting] lock_state << " (yielded share)" end - msg = "Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n".dup + msg = +"Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n" if info[:sleeper] msg << " Waiting in #{info[:sleeper]}" diff --git a/actionpack/lib/action_dispatch/middleware/debug_view.rb b/actionpack/lib/action_dispatch/middleware/debug_view.rb new file mode 100644 index 0000000000..43c0a84504 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/debug_view.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require "pp" + +require "action_view" +require "action_view/base" + +module ActionDispatch + class DebugView < ActionView::Base # :nodoc: + RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__) + + def initialize(assigns) + paths = [RESCUES_TEMPLATE_PATH] + lookup_context = ActionView::LookupContext.new(paths) + super(lookup_context, assigns) + end + + def compiled_method_container + self.class + end + + def debug_params(params) + clean_params = params.clone + clean_params.delete("action") + clean_params.delete("controller") + + if clean_params.empty? + "None" + else + PP.pp(clean_params, +"", 200) + end + end + + def debug_headers(headers) + if headers.present? + headers.inspect.gsub(",", ",\n") + else + "None" + end + end + + def debug_hash(object) + object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") + end + + def render(*) + logger = ActionView::Base.logger + + if logger && logger.respond_to?(:silence) + logger.silence { super } + else + super + end + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 4f69abfa6f..1fb3e9db00 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -12,6 +12,7 @@ module ActionDispatch "ActionController::UnknownHttpMethod" => :method_not_allowed, "ActionController::NotImplemented" => :not_implemented, "ActionController::UnknownFormat" => :not_acceptable, + "ActionController::MissingExactTemplate" => :not_acceptable, "ActionController::InvalidAuthenticityToken" => :unprocessable_entity, "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity, "ActionDispatch::Http::Parameters::ParseError" => :bad_request, @@ -22,27 +23,42 @@ module ActionDispatch ) cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!( - "ActionView::MissingTemplate" => "missing_template", - "ActionController::RoutingError" => "routing_error", - "AbstractController::ActionNotFound" => "unknown_action", - "ActionView::Template::Error" => "template_error" + "ActionView::MissingTemplate" => "missing_template", + "ActionController::RoutingError" => "routing_error", + "AbstractController::ActionNotFound" => "unknown_action", + "ActiveRecord::StatementInvalid" => "invalid_statement", + "ActionView::Template::Error" => "template_error", + "ActionController::MissingExactTemplate" => "missing_exact_template", ) - attr_reader :backtrace_cleaner, :exception, :line_number, :file + cattr_accessor :wrapper_exceptions, default: [ + "ActionView::Template::Error" + ] + + attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file def initialize(backtrace_cleaner, exception) @backtrace_cleaner = backtrace_cleaner - @exception = original_exception(exception) + @exception = exception + @wrapped_causes = wrapped_causes_for(exception, backtrace_cleaner) expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError) end + def unwrapped_exception + if wrapper_exceptions.include?(exception.class.to_s) + exception.cause + else + exception + end + end + def rescue_template @@rescue_templates[@exception.class.name] end def status_code - self.class.status_code_for_exception(@exception.class.name) + self.class.status_code_for_exception(unwrapped_exception.class.name) end def application_trace @@ -63,7 +79,11 @@ module ActionDispatch full_trace_with_ids = [] full_trace.each_with_index do |trace, idx| - trace_with_id = { id: idx, trace: trace } + trace_with_id = { + exception_object_id: @exception.object_id, + id: idx, + trace: trace + } if application_trace.include?(trace) application_trace_with_ids << trace_with_id @@ -96,18 +116,32 @@ module ActionDispatch end end + def trace_to_show + if traces["Application Trace"].empty? && rescue_template != "routing_error" + "Full Trace" + else + "Application Trace" + end + end + + def source_to_show_id + (traces[trace_to_show].first || {})[:id] + end + private def backtrace Array(@exception.backtrace) end - def original_exception(exception) - if @@rescue_responses.has_key?(exception.cause.class.name) - exception.cause - else - exception - end + def causes_for(exception) + return enum_for(__method__, exception) unless block_given? + + yield exception while exception = exception.cause + end + + def wrapped_causes_for(exception, backtrace_cleaner) + causes_for(exception).map { |cause| self.class.new(backtrace_cleaner, cause) } end def clean_backtrace(*args) diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 3e11846778..cf9165d008 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -38,7 +38,7 @@ module ActionDispatch # # See docs on the FlashHash class for more details about the flash. class Flash - KEY = "action_dispatch.request.flash_hash".freeze + KEY = "action_dispatch.request.flash_hash" module RequestMethods # Access the contents of the flash. Use <tt>flash["notice"]</tt> to @@ -73,7 +73,7 @@ module ActionDispatch end end - def reset_session # :nodoc + def reset_session # :nodoc: super self.flash = nil end diff --git a/actionpack/lib/action_dispatch/middleware/host_authorization.rb b/actionpack/lib/action_dispatch/middleware/host_authorization.rb new file mode 100644 index 0000000000..b7dff1df41 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/host_authorization.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require "action_dispatch/http/request" + +module ActionDispatch + # This middleware guards from DNS rebinding attacks by explicitly permitting + # the hosts a request can be sent to. + # + # When a request comes to an unauthorized host, the +response_app+ + # application will be executed and rendered. If no +response_app+ is given, a + # default one will run, which responds with +403 Forbidden+. + class HostAuthorization + class Permissions # :nodoc: + def initialize(hosts) + @hosts = sanitize_hosts(hosts) + end + + def empty? + @hosts.empty? + end + + def allows?(host) + @hosts.any? do |allowed| + allowed === host + rescue + # IPAddr#=== raises an error if you give it a hostname instead of + # IP. Treat similar errors as blocked access. + false + end + end + + private + + def sanitize_hosts(hosts) + Array(hosts).map do |host| + case host + when Regexp then sanitize_regexp(host) + when String then sanitize_string(host) + else host + end + end + end + + def sanitize_regexp(host) + /\A#{host}\z/ + end + + def sanitize_string(host) + if host.start_with?(".") + /\A(.+\.)?#{Regexp.escape(host[1..-1])}\z/ + else + host + end + end + end + + DEFAULT_RESPONSE_APP = -> env do + request = Request.new(env) + + format = request.xhr? ? "text/plain" : "text/html" + template = DebugView.new(host: request.host) + body = template.render(template: "rescues/blocked_host", layout: "rescues/layout") + + [403, { + "Content-Type" => "#{format}; charset=#{Response.default_charset}", + "Content-Length" => body.bytesize.to_s, + }, [body]] + end + + def initialize(app, hosts, response_app = nil) + @app = app + @permissions = Permissions.new(hosts) + @response_app = response_app || DEFAULT_RESPONSE_APP + end + + def call(env) + return @app.call(env) if @permissions.empty? + + request = Request.new(env) + + if authorized?(request) + mark_as_authorized(request) + @app.call(env) + else + @response_app.call(env) + end + end + + private + + def authorized?(request) + origin_host = request.get_header("HTTP_HOST").to_s.sub(/:\d+\z/, "") + forwarded_host = request.x_forwarded_host.to_s.split(/,\s?/).last.to_s.sub(/:\d+\z/, "") + + @permissions.allows?(origin_host) && + (forwarded_host.blank? || @permissions.allows?(forwarded_host)) + end + + def mark_as_authorized(request) + request.set_header("action_dispatch.authorized_host", request.host) + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 35158f9062..a5667573f4 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -162,14 +162,12 @@ module ActionDispatch # Split the comma-separated list into an array of strings. ips = header.strip.split(/[,\s]+/) ips.select do |ip| - begin - # Only return IPs that are valid according to the IPAddr#new method. - range = IPAddr.new(ip).to_range - # We want to make sure nobody is sneaking a netmask in. - range.begin == range.end - rescue ArgumentError - nil - end + # Only return IPs that are valid according to the IPAddr#new method. + range = IPAddr.new(ip).to_range + # We want to make sure nobody is sneaking a netmask in. + range.begin == range.end + rescue ArgumentError + nil end end diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index 805d3f2148..fcc0c72240 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -15,7 +15,7 @@ module ActionDispatch # The unique request id can be used to trace a request end-to-end and would typically end up being part of log files # from multiple pieces of the stack. class RequestId - X_REQUEST_ID = "X-Request-Id".freeze #:nodoc: + X_REQUEST_ID = "X-Request-Id" #:nodoc: def initialize(app) @app = app @@ -30,7 +30,7 @@ module ActionDispatch private def make_request_id(request_id) if request_id.presence - request_id.gsub(/[^\w\-]/, "".freeze).first(255) + request_id.gsub(/[^\w\-@]/, "").first(255) else internal_request_id end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 4ea96196d3..02ccfbc81a 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -16,16 +16,11 @@ module ActionDispatch # The cookie jar used for storage is automatically configured to be the # best possible option given your application's configuration. # - # If you only have secret_token set, your cookies will be signed, but - # not encrypted. This means a user cannot alter their +user_id+ without - # knowing your app's secret key, but can easily read their +user_id+. This - # was the default for Rails 3 apps. - # # Your cookies will be encrypted using your apps secret_key_base. This # goes a step further than signed cookies in that encrypted cookies cannot # be altered or read by users. This is the default starting in Rails 4. # - # Configure your session store in <tt>config/initializers/session_store.rb</tt>: + # Configure your session store in an initializer: # # Rails.application.config.session_store :cookie_store, key: '_your_app_session' # diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index 3c88afd4d3..767143a368 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -45,7 +45,7 @@ module ActionDispatch backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner" wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) status = wrapper.status_code - request.set_header "action_dispatch.exception", wrapper.exception + request.set_header "action_dispatch.exception", wrapper.unwrapped_exception request.set_header "action_dispatch.original_path", request.path_info request.path_info = "/#{status}" response = @exceptions_app.call(request.env) diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index ef633aadc6..00902ede21 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -15,6 +15,8 @@ module ActionDispatch # # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } } # + # Cookies will not be flagged as secure for excluded requests. + # # 2. <b>Secure cookies</b>: Sets the +secure+ flag on cookies to tell browsers they # must not be sent along with +http://+ requests. Enabled by default. Set # +config.ssl_options+ with <tt>secure_cookies: false</tt> to disable this feature. @@ -26,8 +28,8 @@ module ActionDispatch # Set +config.ssl_options+ with <tt>hsts: { ... }</tt> to configure HSTS: # # * +expires+: How long, in seconds, these settings will stick. The minimum - # required to qualify for browser preload lists is 18 weeks. Defaults to - # 180 days (recommended). + # required to qualify for browser preload lists is 1 year. Defaults to + # 1 year (recommended). # # * +subdomains+: Set to +true+ to tell the browser to apply these settings # to all subdomains. This protects your cookies from interception by a @@ -47,9 +49,8 @@ module ActionDispatch class SSL # :stopdoc: - # Default to 180 days, the low end for https://www.ssllabs.com/ssltest/ - # and greater than the 18-week requirement for browser preload lists. - HSTS_EXPIRES_IN = 15552000 + # Default to 1 year, the minimum for browser preload lists. + HSTS_EXPIRES_IN = 31536000 def self.default_hsts_options { expires: HSTS_EXPIRES_IN, subdomains: true, preload: false } @@ -72,7 +73,7 @@ module ActionDispatch if request.ssl? @app.call(env).tap do |status, headers, body| set_hsts_header! headers - flag_cookies_as_secure! headers if @secure_cookies + flag_cookies_as_secure! headers if @secure_cookies && !@exclude.call(request) end else return redirect_to_https request unless @exclude.call(request) @@ -82,7 +83,7 @@ module ActionDispatch private def set_hsts_header!(headers) - headers["Strict-Transport-Security".freeze] ||= @hsts_header + headers["Strict-Transport-Security"] ||= @hsts_header end def normalize_hsts_options(options) @@ -101,23 +102,23 @@ module ActionDispatch # https://tools.ietf.org/html/rfc6797#section-6.1 def build_hsts_header(hsts) - value = "max-age=#{hsts[:expires].to_i}".dup + value = +"max-age=#{hsts[:expires].to_i}" value << "; includeSubDomains" if hsts[:subdomains] value << "; preload" if hsts[:preload] value end def flag_cookies_as_secure!(headers) - if cookies = headers["Set-Cookie".freeze] - cookies = cookies.split("\n".freeze) + if cookies = headers["Set-Cookie"] + cookies = cookies.split("\n") - headers["Set-Cookie".freeze] = cookies.map { |cookie| - if cookie !~ /;\s*secure\s*(;|$)/i + headers["Set-Cookie"] = cookies.map { |cookie| + if !/;\s*secure\s*(;|$)/i.match?(cookie) "#{cookie}; secure" else cookie end - }.join("\n".freeze) + }.join("\n") end end @@ -140,7 +141,7 @@ module ActionDispatch host = @redirect[:host] || request.host port = @redirect[:port] || request.port - location = "https://#{host}".dup + location = +"https://#{host}" location << ":#{port}" if port != 80 && port != 443 location << request.fullpath location diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index 23492e14eb..1f2f7757a3 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -16,7 +16,7 @@ module ActionDispatch # does not exist, a 404 "File not Found" response will be returned. class FileHandler def initialize(root, index: "index", headers: {}) - @root = root.chomp("/") + @root = root.chomp("/").b @file_server = ::Rack::File.new(@root, headers) @index = index end @@ -35,15 +35,14 @@ module ActionDispatch paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"] if match = paths.detect { |p| - path = File.join(@root, p.dup.force_encoding(Encoding::UTF_8)) + path = File.join(@root, p.b) begin File.file?(path) && File.readable?(path) rescue SystemCallError false end - } - return ::Rack::Utils.escape_path(match) + return ::Rack::Utils.escape_path(match).b end end @@ -69,7 +68,7 @@ module ActionDispatch headers["Vary"] = "Accept-Encoding" if gzip_path - return [status, headers, body] + [status, headers, body] ensure request.path_info = path end @@ -80,7 +79,7 @@ module ActionDispatch end def content_type(path) - ::Rack::Mime.mime_type(::File.extname(path), "text/plain".freeze) + ::Rack::Mime.mime_type(::File.extname(path), "text/plain") end def gzip_encoding_accepted?(request) @@ -117,7 +116,7 @@ module ActionDispatch req = Rack::Request.new env if req.get? || req.head? - path = req.path_info.chomp("/".freeze) + path = req.path_info.chomp("/") if match = @file_handler.match?(path) req.path_info = match return @file_handler.serve(req) diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb index e7b913bbe4..88a8e6ad83 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb @@ -1,6 +1,8 @@ -<% @source_extracts.each_with_index do |source_extract, index| %> +<% error_index = local_assigns[:error_index] || 0 %> + +<% source_extracts.each_with_index do |source_extract, index| %> <% if source_extract[:code] %> - <div class="source <%="hidden" if @show_source_idx != index%>" id="frame-source-<%=index%>"> + <div class="source <%= "hidden" if show_source_idx != index %>" id="frame-source-<%= error_index %>-<%= index %>"> <div class="info"> Extracted source (around line <strong>#<%= source_extract[:line_number] %></strong>): </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb index ab57b11c7d..835ca8d260 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb @@ -1,52 +1,62 @@ -<% names = @traces.keys %> +<% names = traces.keys %> +<% error_index = local_assigns[:error_index] || 0 %> <p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p> -<div id="traces"> +<div id="traces-<%= error_index %>"> <% names.each do |name| %> <% - show = "show('#{name.gsub(/\s/, '-')}');" - hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}');"} + show = "show('#{name.gsub(/\s/, '-')}-#{error_index}');" + hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}-#{error_index}');"} %> <a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %> <% end %> - <% @traces.each do |name, trace| %> - <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == @trace_to_show) ? 'block' : 'none' %>;"> - <pre><code><% trace.each do |frame| %><a class="trace-frames" data-frame-id="<%= frame[:id] %>" href="#"><%= frame[:trace] %></a><br><% end %></code></pre> + <% traces.each do |name, trace| %> + <div id="<%= "#{name.gsub(/\s/, '-')}-#{error_index}" %>" style="display: <%= (name == trace_to_show) ? 'block' : 'none' %>;"> + <code style="font-size: 11px;"> + <% trace.each do |frame| %> + <a class="trace-frames trace-frames-<%= error_index %>" data-exception-object-id="<%= frame[:exception_object_id] %>" data-frame-id="<%= frame[:id] %>" href="#"> + <%= frame[:trace] %> + </a> + <br> + <% end %> + </code> </div> <% end %> <script type="text/javascript"> - var traceFrames = document.getElementsByClassName('trace-frames'); - var selectedFrame, currentSource = document.getElementById('frame-source-0'); - - // Add click listeners for all stack frames - for (var i = 0; i < traceFrames.length; i++) { - traceFrames[i].addEventListener('click', function(e) { - e.preventDefault(); - var target = e.target; - var frame_id = target.dataset.frameId; - - if (selectedFrame) { - selectedFrame.className = selectedFrame.className.replace("selected", ""); - } - - target.className += " selected"; - selectedFrame = target; - - // Change the extracted source code - changeSourceExtract(frame_id); - }); - - function changeSourceExtract(frame_id) { - var el = document.getElementById('frame-source-' + frame_id); - if (currentSource && el) { - currentSource.className += " hidden"; - el.className = el.className.replace(" hidden", ""); - currentSource = el; + (function() { + var traceFrames = document.getElementsByClassName('trace-frames-<%= error_index %>'); + var selectedFrame, currentSource = document.getElementById('frame-source-<%= error_index %>-0'); + + // Add click listeners for all stack frames + for (var i = 0; i < traceFrames.length; i++) { + traceFrames[i].addEventListener('click', function(e) { + e.preventDefault(); + var target = e.target; + var frame_id = target.dataset.frameId; + + if (selectedFrame) { + selectedFrame.className = selectedFrame.className.replace("selected", ""); + } + + target.className += " selected"; + selectedFrame = target; + + // Change the extracted source code + changeSourceExtract(frame_id); + }); + + function changeSourceExtract(frame_id) { + var el = document.getElementById('frame-source-<%= error_index %>-' + frame_id); + if (currentSource && el) { + currentSource.className += " hidden"; + el.className = el.className.replace(" hidden", ""); + currentSource = el; + } } } - } + })(); </script> </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb new file mode 100644 index 0000000000..1fbc107e28 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb @@ -0,0 +1,7 @@ +<header> + <h1>Blocked host: <%= @host %></h1> +</header> +<div id="container"> + <h2>To allow requests to <%= @host %>, add the following to your environment configuration:</h2> + <pre>config.hosts << "<%= @host %>"</pre> +</div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb new file mode 100644 index 0000000000..a94dd982a7 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb @@ -0,0 +1,5 @@ +Blocked host: <%= @host %> + +To allow requests to <%= @host %>, add the following to your environment configuration: + + config.hosts << "<%= @host %>" diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb index f154021ae6..bde26f46c2 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb @@ -10,7 +10,25 @@ <div id="container"> <h2><%= h @exception.message %></h2> - <%= render template: "rescues/_source" %> - <%= render template: "rescues/_trace" %> + <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx, error_index: 0 %> + <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show, error_index: 0 %> + + <% if @exception.cause %> + <h2>Exception Causes</h2> + <% end %> + + <% @exception_wrapper.wrapped_causes.each.with_index(1) do |wrapper, index| %> + <div class="details"> + <a class="summary" href="#" style="color: #F0F0F0; text-decoration: none; background: #C52F24; border-bottom: none;" onclick="return toggle(<%= wrapper.exception.object_id %>)"> + <%= wrapper.exception.class.name %>: <%= h wrapper.exception.message %> + </a> + </div> + + <div id="<%= wrapper.exception.object_id %>" style="display: none;"> + <%= render "rescues/source", source_extracts: wrapper.source_extracts, show_source_idx: wrapper.source_to_show_id, error_index: index %> + <%= render "rescues/trace", traces: wrapper.traces, trace_to_show: wrapper.trace_to_show, error_index: index %> + </div> + <% end %> + <%= render template: "rescues/_request_and_response" %> </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb new file mode 100644 index 0000000000..d144fc1cd2 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb @@ -0,0 +1,21 @@ +<header> + <h1> + <%= @exception.class.to_s %> + <% if @request.parameters['controller'] %> + in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %> + <% end %> + </h1> +</header> + +<div id="container"> + <h2> + <%= h @exception.message %> + <% if defined?(ActiveStorage) && @exception.message.match?(%r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}}) %> + <br />To resolve this issue run: rails active_storage:install + <% end %> + </h2> + + <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx %> + <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show %> + <%= render template: "rescues/_request_and_response" %> +</div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb new file mode 100644 index 0000000000..55aaf58713 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb @@ -0,0 +1,13 @@ +<%= @exception.class.to_s %><% + if @request.parameters['controller'] +%> in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %> +<% end %> + +<%= @exception.message %> +<% if defined?(ActiveStorage) && @exception.message.match?(%r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}}) %> +To resolve this issue run: rails active_storage:install +<% end %> + +<%= render template: "rescues/_source" %> +<%= render template: "rescues/_trace" %> +<%= render template: "rescues/_request_and_response" %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb new file mode 100644 index 0000000000..76ab1691b5 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb @@ -0,0 +1,19 @@ +<header> + <h1>No template for interactive request</h1> +</header> + +<div id="container"> + <h2><%= h @exception.message %></h2> + + <p class="summary"> + <strong>NOTE!</strong><br> + Unless told otherwise, Rails expects an action to render a template with the same name,<br> + contained in a folder named after its controller. + + If this controller is an API responding with 204 (No Content), <br> + which does not require a template, + then this error will occur when trying to access it via browser,<br> + since we expect an HTML template + to be rendered for such requests. If that's the case, carry on. + </p> +</div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb new file mode 100644 index 0000000000..fcdbe6069d --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb @@ -0,0 +1,3 @@ +Missing exact template + +<%= @exception.message %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb index 2a65fd06ad..22eb6e9b4e 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb @@ -5,7 +5,7 @@ <div id="container"> <h2><%= h @exception.message %></h2> - <%= render template: "rescues/_source" %> - <%= render template: "rescues/_trace" %> + <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx %> + <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show %> <%= render template: "rescues/_request_and_response" %> </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb index 55dd5ddc7b..2b8f3f2a5e 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb @@ -14,7 +14,7 @@ </p> <% end %> - <%= render template: "rescues/_trace" %> + <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show %> <% if @routes_inspector %> <h2> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb index 5060da9369..324ef1567a 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb @@ -11,10 +11,10 @@ </p> <pre><code><%= h @exception.message %></code></pre> - <%= render template: "rescues/_source" %> + <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx %> <p><%= @exception.sub_template_message %></p> - <%= render template: "rescues/_trace" %> + <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show %> <%= render template: "rescues/_request_and_response" %> </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb index 1fa0691303..0242b706b2 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -197,4 +197,7 @@ setupMatchPaths(); setupRouteToggleHelperLinks(); + + // Focus the search input after page has loaded + document.getElementById('search').focus(); </script> diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 95e99987a0..efc3988bc3 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -21,6 +21,7 @@ module ActionDispatch config.action_dispatch.encrypted_signed_cookie_salt = "signed encrypted cookie" config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie" config.action_dispatch.use_authenticated_cookie_encryption = false + config.action_dispatch.use_cookies_with_metadata = false config.action_dispatch.perform_deep_munge = true config.action_dispatch.default_headers = { @@ -28,7 +29,8 @@ module ActionDispatch "X-XSS-Protection" => "1; mode=block", "X-Content-Type-Options" => "nosniff", "X-Download-Options" => "noopen", - "X-Permitted-Cross-Domain-Policies" => "none" + "X-Permitted-Cross-Domain-Policies" => "none", + "Referrer-Policy" => "strict-origin-when-cross-origin" } config.action_dispatch.cookies_rotations = ActiveSupport::Messages::RotationConfiguration.new diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index d86d0b10c2..bc5e0670e0 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -93,6 +93,14 @@ module ActionDispatch @delegate[key.to_s] end + # Returns the nested value specified by the sequence of keys, returning + # +nil+ if any intermediate step is +nil+. + def dig(*keys) + load_for_read! + keys = keys.map.with_index { |key, i| i.zero? ? key.to_s : key } + @delegate.dig(*keys) + end + # Returns true if the session has the given key or false. def has_key?(key) load_for_read! @@ -130,6 +138,7 @@ module ActionDispatch load_for_read! @delegate.dup.delete_if { |_, v| v.nil? } end + alias :to_h :to_hash # Updates the session with given Hash. # diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb index 0ae464082d..fb0efb9a58 100644 --- a/actionpack/lib/action_dispatch/request/utils.rb +++ b/actionpack/lib/action_dispatch/request/utils.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/core_ext/hash/indifferent_access" + module ActionDispatch class Request class Utils # :nodoc: diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 72f7407c6e..f832719f19 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -74,8 +74,8 @@ module ActionDispatch # For routes that don't fit the <tt>resources</tt> mold, you can use the HTTP helper # methods <tt>get</tt>, <tt>post</tt>, <tt>patch</tt>, <tt>put</tt> and <tt>delete</tt>. # - # get 'post/:id' => 'posts#show' - # post 'post/:id' => 'posts#create_comment' + # get 'post/:id', to: 'posts#show' + # post 'post/:id', to: 'posts#create_comment' # # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same # URL will route to the <tt>show</tt> action. @@ -83,7 +83,7 @@ module ActionDispatch # If your route needs to respond to more than one HTTP method (or all methods) then using the # <tt>:via</tt> option on <tt>match</tt> is preferable. # - # match 'post/:id' => 'posts#show', via: [:get, :post] + # match 'post/:id', to: 'posts#show', via: [:get, :post] # # == Named routes # @@ -94,7 +94,7 @@ module ActionDispatch # Example: # # # In config/routes.rb - # get '/login' => 'accounts#login', as: 'login' + # get '/login', to: 'accounts#login', as: 'login' # # # With render, redirect_to, tests, etc. # redirect_to login_url @@ -120,9 +120,9 @@ module ActionDispatch # # # In config/routes.rb # controller :blog do - # get 'blog/show' => :list - # get 'blog/delete' => :delete - # get 'blog/edit' => :edit + # get 'blog/show', to: :list + # get 'blog/delete', to: :delete + # get 'blog/edit', to: :edit # end # # # provides named routes for show, delete, and edit @@ -132,7 +132,7 @@ module ActionDispatch # # Routes can generate pretty URLs. For example: # - # get '/articles/:year/:month/:day' => 'articles#find_by_id', constraints: { + # get '/articles/:year/:month/:day', to: 'articles#find_by_id', constraints: { # year: /\d{4}/, # month: /\d{1,2}/, # day: /\d{1,2}/ @@ -147,7 +147,7 @@ module ActionDispatch # You can specify a regular expression to define a format for a parameter. # # controller 'geocode' do - # get 'geocode/:postalcode' => :show, constraints: { + # get 'geocode/:postalcode', to: :show, constraints: { # postalcode: /\d{5}(-\d{4})?/ # } # end @@ -156,13 +156,13 @@ module ActionDispatch # expression modifiers: # # controller 'geocode' do - # get 'geocode/:postalcode' => :show, constraints: { + # get 'geocode/:postalcode', to: :show, constraints: { # postalcode: /hx\d\d\s\d[a-z]{2}/i # } # end # # controller 'geocode' do - # get 'geocode/:postalcode' => :show, constraints: { + # get 'geocode/:postalcode', to: :show, constraints: { # postalcode: /# Postalcode format # \d{5} #Prefix # (-\d{4})? #Suffix @@ -178,13 +178,13 @@ module ActionDispatch # # You can redirect any path to another path using the redirect helper in your router: # - # get "/stories" => redirect("/posts") + # get "/stories", to: redirect("/posts") # # == Unicode character routes # # You can specify unicode character routes in your router: # - # get "こんにちは" => "welcome#index" + # get "こんにちは", to: "welcome#index" # # == Routing to Rack Applications # @@ -192,7 +192,7 @@ module ActionDispatch # index action in the PostsController, you can specify any Rack application # as the endpoint for a matcher: # - # get "/application.js" => Sprockets + # get "/application.js", to: Sprockets # # == Reloading routes # @@ -243,8 +243,9 @@ module ActionDispatch # # rails routes # - # Target specific controllers by prefixing the command with <tt>-c</tt> option. - # + # Target a specific controller with <tt>-c</tt>, or grep routes + # using <tt>-g</tt>. Useful in conjunction with <tt>--expanded</tt> + # which displays routes vertically. module Routing extend ActiveSupport::Autoload diff --git a/actionpack/lib/action_dispatch/routing/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb index 24dced1efd..28bb20d688 100644 --- a/actionpack/lib/action_dispatch/routing/endpoint.rb +++ b/actionpack/lib/action_dispatch/routing/endpoint.rb @@ -3,12 +3,15 @@ module ActionDispatch module Routing class Endpoint # :nodoc: - def dispatcher?; false; end - def redirect?; false; end - def engine?; rack_app.respond_to?(:routes); end - def matches?(req); true; end - def app; self; end - def rack_app; app; end + def dispatcher?; false; end + def redirect?; false; end + def matches?(req); true; end + def app; self; end + def rack_app; app; end + + def engine? + rack_app.is_a?(Class) && rack_app < Rails::Engine + end end end end diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index a2205569b4..413e524ef6 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "delegate" -require "active_support/core_ext/string/strip" +require "io/console/size" module ActionDispatch module Routing @@ -61,11 +61,11 @@ module ActionDispatch @routes = routes end - def format(formatter, filter = nil) + def format(formatter, filter = {}) routes_to_display = filter_routes(normalize_filter(filter)) routes = collect_routes(routes_to_display) if routes.none? - formatter.no_routes(collect_routes(@routes)) + formatter.no_routes(collect_routes(@routes), filter) return formatter.result end @@ -81,12 +81,12 @@ module ActionDispatch end private - def normalize_filter(filter) - if filter.is_a?(Hash) && filter[:controller] - { controller: /#{filter[:controller].downcase.sub(/_?controller\z/, '').sub('::', '/')}/ } - elsif filter - { controller: /#{filter}/, action: /#{filter}/, verb: /#{filter}/, name: /#{filter}/, path: /#{filter}/ } + if filter[:controller] + { controller: /#{filter[:controller].underscore.sub(/_?controller\z/, "")}/ } + elsif filter[:grep] + { controller: /#{filter[:grep]}/, action: /#{filter[:grep]}/, + verb: /#{filter[:grep]}/, name: /#{filter[:grep]}/, path: /#{filter[:grep]}/ } end end @@ -126,62 +126,111 @@ module ActionDispatch end end - class ConsoleFormatter - def initialize - @buffer = [] - end + module ConsoleFormatter + class Base + def initialize + @buffer = [] + end - def result - @buffer.join("\n") - end + def result + @buffer.join("\n") + end - def section_title(title) - @buffer << "\n#{title}:" - end + def section_title(title) + end - def section(routes) - @buffer << draw_section(routes) - end + def section(routes) + end - def header(routes) - @buffer << draw_header(routes) - end + def header(routes) + end - def no_routes(routes) - @buffer << - if routes.none? - <<-MESSAGE.strip_heredoc - You don't have any routes defined! + def no_routes(routes, filter) + @buffer << + if routes.none? + <<~MESSAGE + You don't have any routes defined! + + Please add some routes in config/routes.rb. + MESSAGE + elsif filter.key?(:controller) + "No routes were found for this controller." + elsif filter.key?(:grep) + "No routes were found for this grep pattern." + end - Please add some routes in config/routes.rb. - MESSAGE - else - "No routes were found for this controller" + @buffer << "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html." end - @buffer << "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." end - private - def draw_section(routes) - header_lengths = ["Prefix", "Verb", "URI Pattern"].map(&:length) - name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max) + class Sheet < Base + def section_title(title) + @buffer << "\n#{title}:" + end - routes.map do |r| - "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}" - end + def section(routes) + @buffer << draw_section(routes) + end + + def header(routes) + @buffer << draw_header(routes) end - def draw_header(routes) - name_width, verb_width, path_width = widths(routes) + private + + def draw_section(routes) + header_lengths = ["Prefix", "Verb", "URI Pattern"].map(&:length) + name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max) + + routes.map do |r| + "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}" + end + end + + def draw_header(routes) + name_width, verb_width, path_width = widths(routes) + + "#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action" + end + + def widths(routes) + [routes.map { |r| r[:name].length }.max || 0, + routes.map { |r| r[:verb].length }.max || 0, + routes.map { |r| r[:path].length }.max || 0] + end + end - "#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action" + class Expanded < Base + def section_title(title) + @buffer << "\n#{"[ #{title} ]"}" end - def widths(routes) - [routes.map { |r| r[:name].length }.max || 0, - routes.map { |r| r[:verb].length }.max || 0, - routes.map { |r| r[:path].length }.max || 0] + def section(routes) + @buffer << draw_expanded_section(routes) end + + private + + def draw_expanded_section(routes) + routes.map.each_with_index do |r, i| + <<~MESSAGE.chomp + #{route_header(index: i + 1)} + Prefix | #{r[:name]} + Verb | #{r[:verb]} + URI | #{r[:path]} + Controller#Action | #{r[:reqs]} + MESSAGE + end + end + + def route_header(index:) + console_width = IO.console_size.second + header_prefix = "--[ Route #{index} ]" + dash_remainder = [console_width - header_prefix.size, 0].max + + "#{header_prefix}#{'-' * dash_remainder}" + end + end end class HtmlTableFormatter @@ -203,16 +252,16 @@ module ActionDispatch end def no_routes(*) - @buffer << <<-MESSAGE.strip_heredoc + @buffer << <<~MESSAGE <p>You don't have any routes defined!</p> <ul> <li>Please add some routes in <tt>config/routes.rb</tt>.</li> <li> For more information about routes, please see the Rails guide - <a href="http://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>. + <a href="https://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>. </li> </ul> - MESSAGE + MESSAGE end def result diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index d87a23a58c..d67044b4ac 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -50,7 +50,19 @@ module ActionDispatch private def constraint_args(constraint, request) - constraint.arity == 1 ? [request] : [request.path_parameters, request] + arity = if constraint.respond_to?(:arity) + constraint.arity + else + constraint.method(:call).arity + end + + if arity < 1 + [] + elsif arity == 1 + [request] + else + [request.path_parameters, request] + end end end @@ -148,17 +160,8 @@ module ActionDispatch end def make_route(name, precedence) - route = Journey::Route.new(name, - application, - path, - conditions, - required_defaults, - defaults, - request_method, - precedence, - @internal) - - route + Journey::Route.new(name, application, path, conditions, required_defaults, + defaults, request_method, precedence, @internal) end def application @@ -279,7 +282,7 @@ module ActionDispatch def verify_regexp_requirements(requirements) requirements.each do |requirement| - if requirement.source =~ ANCHOR_CHARACTERS_REGEX + if ANCHOR_CHARACTERS_REGEX.match?(requirement.source) raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" end @@ -308,8 +311,8 @@ module ActionDispatch def check_controller_and_action(path_params, controller, action) hash = check_part(:controller, controller, path_params, {}) do |part| translate_controller(part) { - message = "'#{part}' is not a supported controller name. This can lead to potential routing problems.".dup - message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use" + message = +"'#{part}' is not a supported controller name. This can lead to potential routing problems." + message << " See https://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use" raise ArgumentError, message } @@ -333,7 +336,7 @@ module ActionDispatch end def split_to(to) - if to =~ /#/ + if /#/.match?(to) to.split("#") else [] @@ -342,7 +345,7 @@ module ActionDispatch def add_controller_module(controller, modyoule) if modyoule && !controller.is_a?(Regexp) - if controller =~ %r{\A/} + if %r{\A/}.match?(controller) controller[1..-1] else [modyoule, controller].compact.join("/") @@ -390,7 +393,7 @@ module ActionDispatch # for root cases, where the latter is the correct one. def self.normalize_path(path) path = Journey::Router::Utils.normalize_path(path) - path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$} + path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/(\(+[^)]+\)){1,}$} path end @@ -553,10 +556,10 @@ module ActionDispatch # # match 'json_only', constraints: { format: 'json' }, via: :get # - # class Whitelist + # class PermitList # def matches?(request) request.remote_ip == '1.2.3.4' end # end - # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get + # match 'path', to: 'c#a', constraints: PermitList.new, via: :get # # See <tt>Scoping#constraints</tt> for more examples with its scope # equivalent. @@ -611,7 +614,7 @@ module ActionDispatch end raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call) - raise ArgumentError, <<-MSG.strip_heredoc unless path + raise ArgumentError, <<~MSG unless path Must be called with mount point mount SomeRackApp, at: "some_route" @@ -644,7 +647,7 @@ module ActionDispatch # Query if the following named route was already defined. def has_named_route?(name) - @set.named_routes.key? name + @set.named_routes.key?(name) end private @@ -664,10 +667,11 @@ module ActionDispatch def define_generate_prefix(app, name) _route = @set.named_routes.get name _routes = @set + _url_helpers = @set.url_helpers script_namer = ->(options) do prefix_options = options.slice(*_route.segment_keys) - prefix_options[:relative_url_root] = "".freeze + prefix_options[:relative_url_root] = "" if options[:_recall] prefix_options.reverse_merge!(options[:_recall].slice(*_route.segment_keys)) @@ -675,7 +679,7 @@ module ActionDispatch # We must actually delete prefix segment keys to avoid passing them to next url_for. _route.segment_keys.each { |k| options.delete(k) } - _routes.url_helpers.send("#{name}_path", prefix_options) + _url_helpers.send("#{name}_path", prefix_options) end app.routes.define_mounted_helper(name, script_namer) @@ -1158,10 +1162,16 @@ module ActionDispatch end def actions + if @except + available_actions - Array(@except).map(&:to_sym) + else + available_actions + end + end + + def available_actions if @only Array(@only).map(&:to_sym) - elsif @except - default_actions - Array(@except).map(&:to_sym) else default_actions end @@ -1573,7 +1583,7 @@ module ActionDispatch # Matches a URL pattern to one or more routes. # For more information, see match[rdoc-ref:Base#match]. # - # match 'path' => 'controller#action', via: patch + # match 'path' => 'controller#action', via: :patch # match 'path', to: 'controller#action', via: :post # match 'path', 'otherpath', on: :member, via: :get def match(path, *rest, &block) @@ -1587,7 +1597,7 @@ module ActionDispatch when Symbol options[:action] = to when String - if to =~ /#/ + if /#/.match?(to) options[:to] = to else options[:controller] = to @@ -1913,7 +1923,7 @@ module ActionDispatch default_action = options.delete(:action) || @scope[:action] - if action =~ /^[\w\-\/]+$/ + if /^[\w\-\/]+$/.match?(action) default_action ||= action.tr("-", "_") unless action.include?("/") else action = nil @@ -1933,9 +1943,7 @@ module ActionDispatch end def match_root_route(options) - name = has_named_route?(name_for_action(:root, nil)) ? nil : :root - args = ["/", { as: name, via: :get }.merge!(options)] - + args = ["/", { as: :root, via: :get }.merge(options)] match(*args) end end @@ -2082,9 +2090,9 @@ module ActionDispatch # [ :products, options.merge(params.permit(:page, :size).to_h.symbolize_keys) ] # end # - # In this instance the +params+ object comes from the context in which the the + # In this instance the +params+ object comes from the context in which the # block is executed, e.g. generating a URL inside a controller action or a view. - # If the block is executed where there isn't a params object such as this: + # If the block is executed where there isn't a +params+ object such as this: # # Rails.application.routes.url_helpers.browse_path # diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 6da869c0c2..4de5f9e2f7 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -120,8 +120,7 @@ module ActionDispatch opts end - # Returns the path component of a URL for the given record. It uses - # <tt>polymorphic_url</tt> with <tt>routing_type: :path</tt>. + # Returns the path component of a URL for the given record. def polymorphic_path(record_or_hash_or_array, options = {}) if Hash === record_or_hash_or_array options = record_or_hash_or_array.merge(options) @@ -182,8 +181,8 @@ module ActionDispatch CACHE[type].fetch(action) { build action, type } end - def self.url; CACHE["url".freeze][nil]; end - def self.path; CACHE["path".freeze][nil]; end + def self.url; CACHE["url"][nil]; end + def self.path; CACHE["path"][nil]; end def self.build(action, type) prefix = action ? "#{action}_" : "" diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 9eff30fa53..972953d4f3 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -2,7 +2,6 @@ require "action_dispatch/journey" require "active_support/core_ext/object/to_query" -require "active_support/core_ext/hash/slice" require "active_support/core_ext/module/redefine_method" require "active_support/core_ext/module/remove_method" require "active_support/core_ext/array/extract_options" @@ -36,7 +35,7 @@ module ActionDispatch if @raise_on_name_error raise else - return [404, { "X-Cascade" => "pass" }, []] + [404, { "X-Cascade" => "pass" }, []] end end @@ -91,11 +90,11 @@ module ActionDispatch def clear! @path_helpers.each do |helper| - @path_helpers_module.send :remove_method, helper + @path_helpers_module.remove_method helper end @url_helpers.each do |helper| - @url_helpers_module.send :remove_method, helper + @url_helpers_module.remove_method helper end @routes.clear @@ -109,8 +108,8 @@ module ActionDispatch url_name = :"#{name}_url" if routes.key? key - @path_helpers_module.send :undef_method, path_name - @url_helpers_module.send :undef_method, url_name + @path_helpers_module.undef_method path_name + @url_helpers_module.undef_method url_name end routes[key] = route define_url_helper @path_helpers_module, route, path_name, route.defaults, name, PATH @@ -154,13 +153,13 @@ module ActionDispatch url_name = :"#{name}_url" @path_helpers_module.module_eval do - define_method(path_name) do |*args| + redefine_method(path_name) do |*args| helper.call(self, args, true) end end @url_helpers_module.module_eval do - define_method(url_name) do |*args| + redefine_method(url_name) do |*args| helper.call(self, args, false) end end @@ -246,7 +245,7 @@ module ActionDispatch missing_keys << missing_key } constraints = Hash[@route.requirements.merge(params).sort_by { |k, v| k.to_s }] - message = "No route matches #{constraints.inspect}".dup + message = +"No route matches #{constraints.inspect}" message << ", missing required keys: #{missing_keys.sort.inspect}" raise ActionController::UrlGenerationError, message @@ -378,7 +377,7 @@ module ActionDispatch @prepend = [] @disable_clear_and_finalize = false @finalized = false - @env_key = "ROUTES_#{object_id}_SCRIPT_NAME".freeze + @env_key = "ROUTES_#{object_id}_SCRIPT_NAME" @set = Journey::Routes.new @router = Journey::Router.new @set @@ -585,7 +584,7 @@ module ActionDispatch "You may have defined two routes with the same name using the `:as` option, or " \ "you may be overriding a route already defined by a resource with the same naming. " \ "For the latter, you can restrict the routes created with `resources` as explained here: \n" \ - "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created" + "https://guides.rubyonrails.org/routing.html#restricting-the-routes-created" end route = @set.add_route(name, mapping) @@ -730,7 +729,7 @@ module ActionDispatch # Remove leading slashes from controllers def normalize_controller! if controller - if controller.start_with?("/".freeze) + if controller.start_with?("/") @options[:controller] = controller[1..-1] else @options[:controller] = controller @@ -855,7 +854,7 @@ module ActionDispatch recognize_path_with_request(req, path, extras) end - def recognize_path_with_request(req, path, extras) + def recognize_path_with_request(req, path, extras, raise_on_missing: true) @router.recognize(req) do |route, params| params.merge!(extras) params.each do |key, value| @@ -875,12 +874,14 @@ module ActionDispatch return req.path_parameters elsif app.matches?(req) && app.engine? - path_parameters = app.rack_app.routes.recognize_path_with_request(req, path, extras) - return path_parameters + path_parameters = app.rack_app.routes.recognize_path_with_request(req, path, extras, raise_on_missing: false) + return path_parameters if path_parameters end end - raise ActionController::RoutingError, "No route matches #{path.inspect}" + if raise_on_missing + raise ActionController::RoutingError, "No route matches #{path.inspect}" + end end end # :startdoc: diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index fa345dccdf..fcb8ae296b 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -133,6 +133,7 @@ module ActionDispatch # <tt>ActionDispatch::Http::URL.tld_length</tt>, which in turn defaults to 1. # * <tt>:port</tt> - Optionally specify the port to connect to. # * <tt>:anchor</tt> - An anchor name to be appended to the path. + # * <tt>:params</tt> - The query parameters to be appended to the path. # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/" # * <tt>:script_name</tt> - Specifies application path relative to domain root. If provided, prepends application path. # @@ -191,7 +192,25 @@ module ActionDispatch end end - def route_for(name, *args) # :nodoc: + # Allows calling direct or regular named route. + # + # resources :buckets + # + # direct :recordable do |recording| + # route_for(:bucket, recording.bucket) + # end + # + # direct :threadable do |threadable| + # route_for(:recordable, threadable.parent) + # end + # + # This maintains the context of the original caller on + # whether to return a path or full URL, e.g: + # + # threadable_path(threadable) # => "/buckets/1" + # threadable_url(threadable) # => "http://example.com/buckets/1" + # + def route_for(name, *args) public_send(:"#{name}_url", *args) end diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb index 99d0c06751..066daa4a12 100644 --- a/actionpack/lib/action_dispatch/system_test_case.rb +++ b/actionpack/lib/action_dispatch/system_test_case.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true -gem "capybara", "~> 2.15" +gem "capybara", ">= 2.15" require "capybara/dsl" require "capybara/minitest" require "action_controller" require "action_dispatch/system_testing/driver" +require "action_dispatch/system_testing/browser" require "action_dispatch/system_testing/server" require "action_dispatch/system_testing/test_helpers/screenshot_helper" require "action_dispatch/system_testing/test_helpers/setup_and_teardown" @@ -69,6 +70,9 @@ module ActionDispatch # size of the browser screen. These two options are not applicable for # headless drivers and will be silently ignored if passed. # + # Headless browsers such as headless Chrome and headless Firefox are also supported. + # You can use these browsers by setting the +:using+ argument to +:headless_chrome+ or +:headless_firefox+. + # # To use a headless driver, like Poltergeist, update your Gemfile to use # Poltergeist instead of Selenium and then declare the driver name in the # +application_system_test_case.rb+ file. In this case, you would leave out @@ -85,6 +89,24 @@ module ActionDispatch # { js_errors: true } # end # + # Some drivers require browser capabilities to be passed as a block instead + # of through the +options+ hash. + # + # As an example, if you want to add mobile emulation on chrome, you'll have to + # create an instance of selenium's +Chrome::Options+ object and add + # capabilities with a block. + # + # The block will be passed an instance of <tt><Driver>::Options</tt> where you can + # define the capabilities you want. Please refer to your driver documentation + # to learn about supported options. + # + # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + # driven_by :selenium, using: :chrome, screen_size: [1024, 768] do |driver_option| + # driver_option.add_emulation(device_name: 'iPhone 6') + # driver_option.add_extension('path/to/chrome_extension.crx') + # end + # end + # # Because <tt>ActionDispatch::SystemTestCase</tt> is a shim between Capybara # and Rails, any driver that is supported by Capybara is supported by system # tests as long as you include the required gems and files. @@ -130,8 +152,10 @@ module ActionDispatch # driven_by :selenium, using: :firefox # # driven_by :selenium, using: :headless_firefox - def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {}) - self.driver = SystemTesting::Driver.new(driver, using: using, screen_size: screen_size, options: options) + def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {}, &capabilities) + driver_options = { using: using, screen_size: screen_size, options: options } + + self.driver = SystemTesting::Driver.new(driver, driver_options, &capabilities) end driven_by :selenium diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb new file mode 100644 index 0000000000..c34907b6cb --- /dev/null +++ b/actionpack/lib/action_dispatch/system_testing/browser.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module ActionDispatch + module SystemTesting + class Browser # :nodoc: + attr_reader :name + + def initialize(name) + @name = name + end + + def type + case name + when :headless_chrome + :chrome + when :headless_firefox + :firefox + else + name + end + end + + def options + case name + when :headless_chrome + headless_chrome_browser_options + when :headless_firefox + headless_firefox_browser_options + end + end + + def capabilities + @option ||= + case type + when :chrome + ::Selenium::WebDriver::Chrome::Options.new + when :firefox + ::Selenium::WebDriver::Firefox::Options.new + end + end + + private + def headless_chrome_browser_options + capabilities.args << "--headless" + capabilities.args << "--disable-gpu" if Gem.win_platform? + + capabilities + end + + def headless_firefox_browser_options + capabilities.args << "-headless" + + capabilities + end + end + end +end diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb index 280989a146..25a09dd918 100644 --- a/actionpack/lib/action_dispatch/system_testing/driver.rb +++ b/actionpack/lib/action_dispatch/system_testing/driver.rb @@ -3,11 +3,12 @@ module ActionDispatch module SystemTesting class Driver # :nodoc: - def initialize(name, **options) + def initialize(name, **options, &capabilities) @name = name - @browser = options[:using] + @browser = Browser.new(options[:using]) @screen_size = options[:screen_size] @options = options[:options] + @capabilities = capabilities end def use @@ -22,6 +23,8 @@ module ActionDispatch end def register + define_browser_capabilities(@browser.capabilities) + Capybara.register_driver @name do |app| case @name when :selenium then register_selenium(app) @@ -31,35 +34,16 @@ module ActionDispatch end end - def browser_options - if @browser == :headless_chrome - browser_options = Selenium::WebDriver::Chrome::Options.new - browser_options.args << "--headless" - browser_options.args << "--disable-gpu" - - @options.merge(options: browser_options) - elsif @browser == :headless_firefox - browser_options = Selenium::WebDriver::Firefox::Options.new - browser_options.args << "-headless" - - @options.merge(options: browser_options) - else - @options - end + def define_browser_capabilities(capabilities) + @capabilities.call(capabilities) if @capabilities end - def browser - if @browser == :headless_chrome - :chrome - elsif @browser == :headless_firefox - :firefox - else - @browser - end + def browser_options + @options.merge(options: @browser.options).compact end def register_selenium(app) - Capybara::Selenium::Driver.new(app, { browser: browser }.merge(browser_options)).tap do |driver| + Capybara::Selenium::Driver.new(app, { browser: @browser.type }.merge(browser_options)).tap do |driver| driver.browser.manage.window.size = Selenium::WebDriver::Dimension.new(*@screen_size) end end diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb index df0c5d3f0e..79359a0c8b 100644 --- a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb @@ -20,7 +20,7 @@ module ActionDispatch # * [+inline+] Display the screenshot in the terminal using the # iTerm image protocol (https://iterm2.com/documentation-images.html). # * [+artifact+] Display the screenshot in the terminal, using the terminal - # artifact format (https://buildkite.github.io/terminal/inline-images/). + # artifact format (https://buildkite.github.io/terminal-to-html/inline-images/). def take_screenshot save_image puts display_image @@ -43,7 +43,7 @@ module ActionDispatch end def image_path - @image_path ||= absolute_image_path.relative_path_from(Pathname.pwd).to_s + @image_path ||= absolute_image_path.to_s end def absolute_image_path @@ -65,7 +65,7 @@ module ActionDispatch end def display_image - message = "[Screenshot]: #{image_path}\n".dup + message = +"[Screenshot]: #{image_path}\n" case output_type when "artifact" @@ -80,7 +80,7 @@ module ActionDispatch end def inline_base64(path) - Base64.encode64(path).gsub("\n", "") + Base64.strict_encode64(path) end def failed? diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb index ffa85f4e14..600e9c733b 100644 --- a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb @@ -17,8 +17,12 @@ module ActionDispatch end def after_teardown - take_failed_screenshot - Capybara.reset_sessions! + begin + take_failed_screenshot + ensure + Capybara.reset_sessions! + end + ensure super end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 98b1965d22..8595ea03cf 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -79,9 +79,8 @@ module ActionDispatch end def generate_response_message(expected, actual = @response.response_code) - "Expected response to be a <#{code_with_name(expected)}>,"\ - " but was a <#{code_with_name(actual)}>" - .dup.concat(location_if_redirected).concat(response_body_if_short) + (+"Expected response to be a <#{code_with_name(expected)}>,"\ + " but was a <#{code_with_name(actual)}>").concat(location_if_redirected).concat(response_body_if_short) end def response_body_if_short diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 5390581139..28cde6704e 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -9,6 +9,11 @@ module ActionDispatch module Assertions # Suite of assertions to test routes generated by \Rails and the handling of requests made to them. module RoutingAssertions + def setup # :nodoc: + @routes ||= nil + super + end + # Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash) # match +path+. Basically, it asserts that \Rails recognizes the route given by +expected_options+. # @@ -78,7 +83,7 @@ module ActionDispatch # # Asserts that the generated route gives us our custom route # assert_generates "changesets/12", { controller: 'scm', action: 'show_diff', revision: "12" } def assert_generates(expected_path, options, defaults = {}, extras = {}, message = nil) - if expected_path =~ %r{://} + if %r{://}.match?(expected_path) fail_on(URI::InvalidURIError, message) do uri = URI.parse(expected_path) expected_path = uri.path.to_s.empty? ? "/" : uri.path @@ -155,9 +160,16 @@ module ActionDispatch @controller.singleton_class.include(_routes.url_helpers) if @controller.respond_to? :view_context_class - @controller.view_context_class = Class.new(@controller.view_context_class) do + view_context_class = Class.new(@controller.view_context_class) do include _routes.url_helpers end + + custom_view_context = Module.new { + define_method(:view_context_class) do + view_context_class + end + } + @controller.extend(custom_view_context) end end yield @routes @@ -189,7 +201,7 @@ module ActionDispatch request = ActionController::TestRequest.create @controller.class - if path =~ %r{://} + if %r{://}.match?(path) fail_on(URI::InvalidURIError, msg) do uri = URI.parse(path) request.env["rack.url_scheme"] = uri.scheme || "http" diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 7171b6942c..10d85037ae 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -50,10 +50,11 @@ module ActionDispatch # Follow a single redirect response. If the last response was not a # redirect, an exception will be raised. Otherwise, the redirect is - # performed on the location header. - def follow_redirect! + # performed on the location header. Any arguments are passed to the + # underlying call to `get`. + def follow_redirect!(**args) raise "not a redirect! #{status} #{status_message}" unless redirect? - get(response.location) + get(response.location, **args) status end end @@ -189,6 +190,12 @@ module ActionDispatch # merged into the Rack env hash. # - +env+: Additional env to pass, as a Hash. The headers will be # merged into the Rack env hash. + # - +xhr+: Set to `true` if you want to make and Ajax request. + # Adds request headers characteristic of XMLHttpRequest e.g. HTTP_X_REQUESTED_WITH. + # The headers will be merged into the Rack env hash. + # - +as+: Used for encoding the request with different content type. + # Supports `:json` by default and will set the approriate request headers. + # The headers will be merged into the Rack env hash. # # This method is rarely used directly. Use +#get+, +#post+, or other standard # HTTP methods in integration tests. +#process+ is only required when using a @@ -210,7 +217,7 @@ module ActionDispatch method = :post end - if path =~ %r{://} + if %r{://}.match?(path) path = build_expanded_path(path) do |location| https! URI::HTTPS === location if location.scheme @@ -328,7 +335,7 @@ module ActionDispatch klass = APP_SESSIONS[app] ||= Class.new(Integration::Session) { # If the app is a Rails app, make url_helpers available on the session. # This makes app.url_for and app.foo_path available in the console. - if app.respond_to?(:routes) + if app.respond_to?(:routes) && app.routes.is_a?(ActionDispatch::Routing::RouteSet) include app.routes.url_helpers include app.routes.mounted_helpers end diff --git a/actionpack/lib/action_dispatch/testing/request_encoder.rb b/actionpack/lib/action_dispatch/testing/request_encoder.rb index 01246b7a2e..9889f61951 100644 --- a/actionpack/lib/action_dispatch/testing/request_encoder.rb +++ b/actionpack/lib/action_dispatch/testing/request_encoder.rb @@ -34,7 +34,7 @@ module ActionDispatch end def encode_params(params) - @param_encoder.call(params) + @param_encoder.call(params) if params end def self.parser(content_type) diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index 8ac50c730d..0b98f27f11 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -8,12 +8,12 @@ module ActionDispatch module FixtureFile # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.fixture_path, path), type)</tt>: # - # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png') + # post :change_avatar, params: { avatar: fixture_file_upload('files/spongebob.png', 'image/png') } # # To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter. # This will not affect other platforms: # - # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png', :binary) + # post :change_avatar, params: { avatar: fixture_file_upload('files/spongebob.png', 'image/png', :binary) } def fixture_file_upload(path, mime_type = nil, binary = false) if self.class.respond_to?(:fixture_path) && self.class.fixture_path && !File.exist?(path) diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb index 1e6b21f235..6f7c86fdcf 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -14,40 +14,12 @@ module ActionDispatch new response.status, response.headers, response.body end - def initialize(*) # :nodoc: - super - @response_parser = RequestEncoder.parser(content_type) - end - - # Was the response successful? - def success? - ActiveSupport::Deprecation.warn(<<-MSG.squish) - The success? predicate is deprecated and will be removed in Rails 6.0. - Please use successful? as provided by Rack::Response::Helpers. - MSG - successful? - end - - # Was the URL not found? - def missing? - ActiveSupport::Deprecation.warn(<<-MSG.squish) - The missing? predicate is deprecated and will be removed in Rails 6.0. - Please use not_found? as provided by Rack::Response::Helpers. - MSG - not_found? - end - - # Was there a server-side error? - def error? - ActiveSupport::Deprecation.warn(<<-MSG.squish) - The error? predicate is deprecated and will be removed in Rails 6.0. - Please use server_error? as provided by Rack::Response::Helpers. - MSG - server_error? + def parsed_body + @parsed_body ||= response_parser.call(body) end - def parsed_body - @parsed_body ||= @response_parser.call(body) + def response_parser + @response_parser ||= RequestEncoder.parser(content_type) end end end diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index 95fdd3affb..36ee77c693 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2017 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb index 97f4934b58..c7cb3cb91e 100644 --- a/actionpack/lib/action_pack/gem_version.rb +++ b/actionpack/lib/action_pack/gem_version.rb @@ -7,10 +7,10 @@ module ActionPack end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 - PRE = "beta2" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end |