diff options
Diffstat (limited to 'actionpack/lib/action_controller')
40 files changed, 1128 insertions, 800 deletions
diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb index 6bbebb7b4c..b192e496de 100644 --- a/actionpack/lib/action_controller/api.rb +++ b/actionpack/lib/action_controller/api.rb @@ -1,6 +1,8 @@ -require 'action_view' -require 'action_controller' -require 'action_controller/log_subscriber' +# frozen_string_literal: true + +require "action_view" +require "action_controller" +require "action_controller/log_subscriber" module ActionController # API Controller is a lightweight version of <tt>ActionController::Base</tt>, @@ -81,10 +83,9 @@ module ActionController # end # end # - # Quite straightforward. Make sure to check the modules included in - # <tt>ActionController::Base</tt> if you want to use any other - # functionality that is not provided by <tt>ActionController::API</tt> - # out of the box. + # Make sure to check the modules included in <tt>ActionController::Base</tt> + # if you want to use any other functionality that is not provided + # by <tt>ActionController::API</tt> out of the box. class API < Metal abstract! @@ -142,6 +143,7 @@ module ActionController include mod end + ActiveSupport.run_load_hooks(:action_controller_api, self) ActiveSupport.run_load_hooks(:action_controller, self) end end diff --git a/actionpack/lib/action_controller/api/api_rendering.rb b/actionpack/lib/action_controller/api/api_rendering.rb index 3a08d28c39..aca5265313 100644 --- a/actionpack/lib/action_controller/api/api_rendering.rb +++ b/actionpack/lib/action_controller/api/api_rendering.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionController module ApiRendering extend ActiveSupport::Concern diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index d546d7260c..204a3d400c 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -1,4 +1,6 @@ -require 'action_view' +# frozen_string_literal: true + +require "action_view" require "action_controller/log_subscriber" require "action_controller/metal/params_wrapper" @@ -8,7 +10,7 @@ module ActionController # on the controller, which will automatically be made accessible to the web-server through \Rails Routes. # # By default, only the ApplicationController in a \Rails application inherits from <tt>ActionController::Base</tt>. All other - # controllers in turn inherit from ApplicationController. This gives you one class to configure things such as + # controllers inherit from ApplicationController. This gives you one class to configure things such as # request forgery protection and filtering of sensitive request parameters. # # A sample controller could look like this: @@ -30,9 +32,9 @@ module ActionController # # Unlike index, the create action will not render a template. After performing its main purpose (creating a # new post), it initiates a redirect instead. This redirect works by returning an external - # "302 Moved" HTTP response that takes the user to the index action. + # <tt>302 Moved</tt> HTTP response that takes the user to the index action. # - # These two methods represent the two basic action archetypes used in Action Controllers. Get-and-show and do-and-redirect. + # These two methods represent the two basic action archetypes used in Action Controllers: Get-and-show and do-and-redirect. # Most actions are variations on these themes. # # == Requests @@ -51,16 +53,16 @@ module ActionController # == Parameters # # All request parameters, whether they come from a query string in the URL or form data submitted through a POST request are - # available through the params method which returns a hash. For example, an action that was performed through - # <tt>/posts?category=All&limit=5</tt> will include <tt>{ "category" => "All", "limit" => "5" }</tt> in params. + # available through the <tt>params</tt> method which returns a hash. For example, an action that was performed through + # <tt>/posts?category=All&limit=5</tt> will include <tt>{ "category" => "All", "limit" => "5" }</tt> in <tt>params</tt>. # # It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as: # # <input type="text" name="post[name]" value="david"> # <input type="text" name="post[address]" value="hyacintvej"> # - # A request stemming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>. - # If the address input had been named <tt>post[address][street]</tt>, the params would have included + # A request coming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>. + # If the address input had been named <tt>post[address][street]</tt>, the <tt>params</tt> would have included # <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting. # # == Sessions @@ -74,7 +76,7 @@ module ActionController # # session[:person] = Person.authenticate(user_name, password) # - # And retrieved again through the same hash: + # You can retrieve it again through the same hash: # # Hello #{session[:person]} # @@ -213,15 +215,17 @@ module ActionController Renderers::All, ConditionalGet, EtagWithTemplateDigest, + EtagWithFlash, Caching, MimeResponds, ImplicitRender, StrongParameters, - + ParameterEncoding, Cookies, Flash, FormBuilder, RequestForgeryProtection, + ContentSecurityPolicy, ForceSSL, Streaming, DataStreaming, @@ -260,6 +264,13 @@ 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 end diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index a9a8508abc..97775d1dc8 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionController # \Caching is a cheap way of speeding up slow applications by keeping the result of # calculations, renderings, and database calls around for subsequent requests. @@ -38,7 +40,7 @@ module ActionController end def instrument_name - "action_controller" + "action_controller".freeze end end end diff --git a/actionpack/lib/action_controller/form_builder.rb b/actionpack/lib/action_controller/form_builder.rb index f2656ca894..09d2ac1837 100644 --- a/actionpack/lib/action_controller/form_builder.rb +++ b/actionpack/lib/action_controller/form_builder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionController # Override the default form builder for all views rendered by this # controller and any of its descendants. Accepts a subclass of diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index a0917b4fdb..14f41eb55f 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionController class LogSubscriber < ActiveSupport::LogSubscriber INTERNAL_PARAMS = %w(controller action format _method only_path) @@ -24,7 +26,7 @@ module ActionController 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" + message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms".dup message << " (#{additions.join(" | ".freeze)})" unless additions.empty? message << "\n\n" if defined?(Rails.env) && Rails.env.development? @@ -51,7 +53,7 @@ module ActionController def unpermitted_parameters(event) debug do unpermitted_keys = event.payload[:keys] - "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}" + "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.map { |e| ":#{e}" }.join(", ")}" end end @@ -59,10 +61,10 @@ module ActionController expire_fragment expire_page write_page).each do |method| class_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{method}(event) - return unless logger.info? - key_or_path = event.payload[:key] || event.payload[:path] + return unless logger.info? && ActionController::Base.enable_fragment_cache_logging + key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path]) human_name = #{method.to_s.humanize.inspect} - info("\#{human_name} \#{key_or_path} (\#{event.duration.round(1)}ms)") + info("\#{human_name} \#{key} (\#{event.duration.round(1)}ms)") end METHOD end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index f6e67b02d7..457884ea08 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -1,7 +1,9 @@ -require 'active_support/core_ext/array/extract_options' -require 'action_dispatch/middleware/stack' -require 'action_dispatch/http/request' -require 'action_dispatch/http/response' +# frozen_string_literal: true + +require "active_support/core_ext/array/extract_options" +require "action_dispatch/middleware/stack" +require "action_dispatch/http/request" +require "action_dispatch/http/response" module ActionController # Extend ActionDispatch middleware stack to make it aware of options @@ -34,29 +36,29 @@ module ActionController private - INCLUDE = ->(list, action) { list.include? action } - EXCLUDE = ->(list, action) { !list.include? action } - NULL = ->(list, action) { true } - - def build_middleware(klass, args, block) - options = args.extract_options! - only = Array(options.delete(:only)).map(&:to_s) - except = Array(options.delete(:except)).map(&:to_s) - args << options unless options.empty? - - strategy = NULL - list = nil - - if only.any? - strategy = INCLUDE - list = only - elsif except.any? - strategy = EXCLUDE - list = except - end + INCLUDE = ->(list, action) { list.include? action } + EXCLUDE = ->(list, action) { !list.include? action } + NULL = ->(list, action) { true } + + def build_middleware(klass, args, block) + options = args.extract_options! + only = Array(options.delete(:only)).map(&:to_s) + except = Array(options.delete(:except)).map(&:to_s) + args << options unless options.empty? + + strategy = NULL + list = nil + + if only.any? + strategy = INCLUDE + list = only + elsif except.any? + strategy = EXCLUDE + list = except + end - Middleware.new(get_class(klass), args, list, strategy, block) - end + Middleware.new(klass, args, list, strategy, block) + end end # <tt>ActionController::Metal</tt> is the simplest possible controller, providing a @@ -118,11 +120,6 @@ module ActionController class Metal < AbstractController::Base abstract! - def env - @_request.env - end - deprecate :env - # Returns the last part of the controller's name, underscored, without the ending # <tt>Controller</tt>. For instance, PostsController returns <tt>posts</tt>. # Namespaces are left out, so Admin::PostsController returns <tt>posts</tt> as well. @@ -130,24 +127,28 @@ module ActionController # ==== Returns # * <tt>string</tt> def self.controller_name - @controller_name ||= name.demodulize.sub(/Controller$/, '').underscore + @controller_name ||= name.demodulize.sub(/Controller$/, "").underscore end def self.make_response!(request) - ActionDispatch::Response.create.tap do |res| + ActionDispatch::Response.new.tap do |res| res.request = request end end - # Delegates to the class' <tt>controller_name</tt> + def self.binary_params_for?(action) # :nodoc: + false + end + + # Delegates to the class' <tt>controller_name</tt>. def controller_name self.class.controller_name end attr_internal :response, :request - delegate :session, :to => "@_request" + delegate :session, to: "@_request" delegate :headers, :status=, :location=, :content_type=, - :status, :location, :content_type, :to => "@_response" + :status, :location, :content_type, to: "@_response" def initialize @_request = nil @@ -209,8 +210,7 @@ module ActionController @_request.reset_session end - class_attribute :middleware_stack - self.middleware_stack = ActionController::MiddlewareStack.new + class_attribute :middleware_stack, default: ActionController::MiddlewareStack.new def self.inherited(base) # :nodoc: base.middleware_stack = middleware_stack.dup @@ -228,14 +228,6 @@ module ActionController middleware_stack end - # Makes the controller a Rack endpoint that runs the action in the given - # +env+'s +action_dispatch.request.path_parameters+ key. - def self.call(env) - req = ActionDispatch::Request.new env - action(req.path_parameters[:action]).call(env) - end - class << self; deprecate :call; end - # Returns a Rack endpoint for the given action name. def self.action(name) if middleware_stack.any? @@ -253,7 +245,7 @@ module ActionController end end - # Direct dispatch to the controller. Instantiates the controller, then + # Direct dispatch to the controller. Instantiates the controller, then # executes the action named +name+. def self.dispatch(name, req, res) if middleware_stack.any? diff --git a/actionpack/lib/action_controller/metal/basic_implicit_render.rb b/actionpack/lib/action_controller/metal/basic_implicit_render.rb index cef65a362c..2dc990f303 100644 --- a/actionpack/lib/action_controller/metal/basic_implicit_render.rb +++ b/actionpack/lib/action_controller/metal/basic_implicit_render.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionController module BasicImplicitRender # :nodoc: def send_action(method, *args) diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index e21449f376..06b6a95ff8 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -1,4 +1,6 @@ -require 'active_support/core_ext/hash/keys' +# frozen_string_literal: true + +require "active_support/core_ext/hash/keys" module ActionController module ConditionalGet @@ -7,8 +9,7 @@ module ActionController include Head included do - class_attribute :etaggers - self.etaggers = [] + class_attribute :etaggers, default: [] end module ClassMethods @@ -227,25 +228,26 @@ module ActionController # expires_in 3.hours, public: true, must_revalidate: true # # This method will overwrite an existing Cache-Control header. - # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities. + # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities. # # 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) + max_age: seconds, + public: options.delete(:public), + must_revalidate: options.delete(:must_revalidate) ) options.delete(:private) - response.cache_control[:extras] = options.map {|k,v| "#{k}=#{v}"} + response.cache_control[:extras] = options.map { |k, v| "#{k}=#{v}" } response.date = Time.now unless response.date? end - # Sets an HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should - # occur by the browser or intermediate caches (like caching proxy servers). + # Sets an HTTP 1.1 Cache-Control header of <tt>no-cache</tt>. This means the + # resource will be marked as stale, so clients must always revalidate. + # Intermediate/browser caches may still store the asset. def expires_now - response.cache_control.replace(:no_cache => true) + response.cache_control.replace(no_cache: true) end # Cache or yield the block. The cache is supposed to never expire. diff --git a/actionpack/lib/action_controller/metal/content_security_policy.rb b/actionpack/lib/action_controller/metal/content_security_policy.rb new file mode 100644 index 0000000000..48a7109bea --- /dev/null +++ b/actionpack/lib/action_controller/metal/content_security_policy.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module ActionController #:nodoc: + module ContentSecurityPolicy + # TODO: Documentation + extend ActiveSupport::Concern + + module ClassMethods + def content_security_policy(**options, &block) + before_action(options) do + if block_given? + policy = request.content_security_policy.clone + yield policy + request.content_security_policy = policy + end + end + end + + def content_security_policy_report_only(report_only = true, **options) + before_action(options) do + request.content_security_policy_report_only = report_only + end + end + end + end +end diff --git a/actionpack/lib/action_controller/metal/cookies.rb b/actionpack/lib/action_controller/metal/cookies.rb index 44925641a1..ff46966693 100644 --- a/actionpack/lib/action_controller/metal/cookies.rb +++ b/actionpack/lib/action_controller/metal/cookies.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionController #:nodoc: module Cookies extend ActiveSupport::Concern diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 6cd6130032..5a82ccf668 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -1,4 +1,6 @@ -require 'action_controller/metal/exceptions' +# frozen_string_literal: true + +require "action_controller/metal/exceptions" module ActionController #:nodoc: # Methods for sending arbitrary data and for streaming files to the browser, @@ -8,10 +10,10 @@ 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".freeze #:nodoc: + DEFAULT_SEND_FILE_DISPOSITION = "attachment".freeze #:nodoc: - protected + private # Sends the file. This uses a server-appropriate method (such as X-Sendfile) # via the Rack::Sendfile middleware. The header to use is set via # +config.action_dispatch.x_sendfile_header+. @@ -54,17 +56,17 @@ module ActionController #:nodoc: # # Read about the other Content-* HTTP headers if you'd like to # provide the user with more information (such as Content-Description) in - # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11. + # https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11. # # Also be aware that the document may be cached by proxies and browsers. # The Pragma and Cache-Control headers declare how the file may be cached # by intermediaries. They default to require clients to validate with # the server before releasing cached responses. See - # http://www.mnot.net/cache_docs/ for an overview of web caching and - # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 + # https://www.mnot.net/cache_docs/ for an overview of web caching and + # https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 # for the Cache-Control header spec. def send_file(path, options = {}) #:doc: - raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path) + raise MissingFile, "Cannot read file #{path}" unless File.file?(path) && File.readable?(path) options[:filename] ||= File.basename(path) unless options[:url_based_filename] send_file_headers! options @@ -108,11 +110,13 @@ module ActionController #:nodoc: render options.slice(:status, :content_type).merge(body: data) end - private def send_file_headers!(options) type_provided = options.has_key?(:type) content_type = options.fetch(:type, DEFAULT_SEND_FILE_TYPE) + self.content_type = content_type + response.sending_file = true + raise ArgumentError, ":type option required" if content_type.nil? if content_type.is_a?(Symbol) @@ -122,7 +126,7 @@ module ActionController #:nodoc: else if !type_provided && options[:filename] # If type wasn't provided, try guessing from file extension. - content_type = Mime::Type.lookup_by_extension(File.extname(options[:filename]).downcase.delete('.')) || content_type + content_type = Mime::Type.lookup_by_extension(File.extname(options[:filename]).downcase.delete(".")) || content_type end self.content_type = content_type end @@ -131,12 +135,10 @@ module ActionController #:nodoc: unless disposition.nil? disposition = disposition.to_s disposition += %(; filename="#{options[:filename]}") if options[:filename] - headers['Content-Disposition'] = disposition + headers["Content-Disposition"] = disposition end - headers['Content-Transfer-Encoding'] = 'binary' - - response.sending_file = true + headers["Content-Transfer-Encoding"] = "binary" # Fix a problem with IE 6.0 on opening downloaded files: # If Cache-Control: no-cache is set (which Rails does by default), diff --git a/actionpack/lib/action_controller/metal/etag_with_flash.rb b/actionpack/lib/action_controller/metal/etag_with_flash.rb new file mode 100644 index 0000000000..38899e2f16 --- /dev/null +++ b/actionpack/lib/action_controller/metal/etag_with_flash.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module ActionController + # When you're using the flash, it's generally used as a conditional on the view. + # This means the content of the view depends on the flash. Which in turn means + # that the ETag for a response should be computed with the content of the flash + # in mind. This does that by including the content of the flash as a component + # in the ETag that's generated for a response. + module EtagWithFlash + extend ActiveSupport::Concern + + include ActionController::ConditionalGet + + included do + etag { flash unless flash.empty? } + end + end +end diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb index 669cf55bca..640c75536e 100644 --- a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb +++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionController # When our views change, they should bubble up into HTTP cache freshness # and bust browser caches. So the template digest for the current action @@ -22,8 +24,7 @@ module ActionController include ActionController::ConditionalGet included do - class_attribute :etag_with_template_digest - self.etag_with_template_digest = true + class_attribute :etag_with_template_digest, default: true ActiveSupport.on_load :action_view, yield: true do etag do |options| @@ -33,18 +34,24 @@ module ActionController end private - def determine_template_etag(options) - if template = pick_template_for_etag(options) - lookup_and_digest_template(template) + def determine_template_etag(options) + if template = pick_template_for_etag(options) + lookup_and_digest_template(template) + end end - end - def pick_template_for_etag(options) - options.fetch(:template) { "#{controller_name}/#{action_name}" } - end + # Pick the template digest to include in the ETag. If the +:template+ option + # is present, use the named template. If +:template+ is +nil+ or absent, use + # the default controller/action template. If +:template+ is false, omit the + # template digest from the ETag. + def pick_template_for_etag(options) + unless options[:template] == false + options[:template] || "#{controller_path}/#{action_name}" + end + end - def lookup_and_digest_template(template) - ActionView::Digestor.digest name: template, finder: lookup_context - end + def lookup_and_digest_template(template) + ActionView::Digestor.digest name: template, finder: lookup_context + end end end diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index 5c0ada37be..a65857d6ef 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -1,22 +1,14 @@ +# frozen_string_literal: true + module ActionController class ActionControllerError < StandardError #:nodoc: end class BadRequest < ActionControllerError #:nodoc: - def initialize(msg = nil, e = nil) - if e - ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ - "Exceptions will automatically capture the original exception.", caller) - end - + def initialize(msg = nil) super(msg) set_backtrace $!.backtrace if $! end - - def original_exception - ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) - cause - end end class RenderError < ActionControllerError #:nodoc: @@ -24,7 +16,7 @@ module ActionController class RoutingError < ActionControllerError #:nodoc: attr_reader :failures - def initialize(message, failures=[]) + def initialize(message, failures = []) super(message) @failures = failures end @@ -35,21 +27,18 @@ module ActionController class MethodNotAllowed < ActionControllerError #:nodoc: def initialize(*allowed_methods) - super("Only #{allowed_methods.to_sentence(:locale => :en)} requests are allowed.") + super("Only #{allowed_methods.to_sentence(locale: :en)} requests are allowed.") end end class NotImplemented < MethodNotAllowed #:nodoc: end - class UnknownController < ActionControllerError #:nodoc: - end - class MissingFile < ActionControllerError #:nodoc: end class SessionOverflowError < ActionControllerError #:nodoc: - DEFAULT_MESSAGE = 'Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data.' + DEFAULT_MESSAGE = "Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data." def initialize(message = nil) super(message || DEFAULT_MESSAGE) diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb index 65351284b9..5115c2fadf 100644 --- a/actionpack/lib/action_controller/metal/flash.rb +++ b/actionpack/lib/action_controller/metal/flash.rb @@ -1,10 +1,11 @@ +# frozen_string_literal: true + module ActionController #:nodoc: module Flash extend ActiveSupport::Concern included do - class_attribute :_flash_types, instance_accessor: false - self._flash_types = [] + class_attribute :_flash_types, instance_accessor: false, default: [] delegate :flash, to: :request add_flash_types(:alert, :notice) @@ -42,7 +43,7 @@ module ActionController #:nodoc: end end - protected + private def redirect_to(options = {}, response_status_and_flash = {}) #:doc: self.class._flash_types.each do |flash_type| if type = response_status_and_flash.delete(flash_type) diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index ea8e91ce24..0ba1f9f783 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -1,18 +1,20 @@ -require 'active_support/core_ext/hash/except' -require 'active_support/core_ext/hash/slice' +# frozen_string_literal: true + +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 HTTPS - # protocol. This will ensure that user's sensitive information will be + # 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 transferred via HTTPS protocol and prevent - # the user from getting their session hijacked when accessing the site over - # unsecured HTTP protocol. + # 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 extend ActiveSupport::Concern include AbstractController::Callbacks @@ -23,7 +25,7 @@ module ActionController module ClassMethods # Force the request to this particular controller or specified actions to be - # under HTTPS protocol. + # 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. @@ -71,15 +73,15 @@ module ActionController # Redirect the existing request to use the HTTPS protocol. # # ==== Parameters - # * <tt>host_or_options</tt> - Either a host name or any of the url & + # * <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 + protocol: "https://", + host: request.host, + path: request.fullpath, + status: :moved_permanently } if host_or_options.is_a?(Hash) @@ -89,7 +91,7 @@ module ActionController end secure_url = ActionDispatch::Http::URL.url_for(options.slice(*URL_OPTIONS)) - flash.keep if respond_to?(:flash) + flash.keep if respond_to?(:flash) && request.respond_to?(:flash) redirect_to secure_url, options.slice(*REDIRECT_OPTIONS) end end diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 5e9832fd4e..bac9bc5e5f 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionController module Head # Returns a response that has no content (merely headers). The options @@ -18,13 +20,7 @@ module ActionController # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols. def head(status, options = {}) if status.is_a?(Hash) - msg = status[:status] ? 'The :status option' : 'The implicit :ok status' - options, status = status, status.delete(:status) - - ActiveSupport::Deprecation.warn(<<-MSG.squish) - #{msg} on `head` has been deprecated and will be removed in Rails 5.1. - Please pass the status as a separate parameter before the options, instead. - MSG + raise ArgumentError, "#{status.inspect} is not a valid value for `status`." end status ||= :ok @@ -33,7 +29,7 @@ module ActionController content_type = options.delete(:content_type) options.each do |key, value| - headers[key.to_s.dasherize.split('-').each { |v| v[0] = v[0].chr.upcase }.join('-')] = value.to_s + headers[key.to_s.dasherize.split("-").each { |v| v[0] = v[0].chr.upcase }.join("-")] = value.to_s end self.status = status @@ -41,24 +37,24 @@ module ActionController self.response_body = "" - if include_content?(self.response_code) + if include_content?(response_code) self.content_type = content_type || (Mime[formats.first] if formats) - self.response.charset = false + response.charset = false end true end private - def include_content?(status) - case status - when 100..199 - false - when 204, 205, 304 - false - else - true + def include_content?(status) + case status + when 100..199 + false + when 204, 205, 304 + false + else + true + end end - end end end diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 295f0cb66f..22c84e440b 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionController # The \Rails framework provides a large number of helpers for working with assets, dates, forms, # numbers and model objects, to name a few. These helpers are available to all templates @@ -53,9 +55,8 @@ module ActionController include AbstractController::Helpers included do - class_attribute :helpers_path, :include_all_helpers - self.helpers_path ||= [] - self.include_all_helpers = true + class_attribute :helpers_path, default: [] + class_attribute :include_all_helpers, default: true end module ClassMethods @@ -108,10 +109,10 @@ module ActionController end private - # Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt> - def all_application_helpers - all_helpers_from_path(helpers_path) - end + # Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt> + def all_application_helpers + all_helpers_from_path(helpers_path) + end end # Provides a proxy to access helper methods from outside the view. diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 4639348509..01676f3237 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -1,5 +1,7 @@ -require 'base64' -require 'active_support/security_utils' +# frozen_string_literal: true + +require "base64" +require "active_support/security_utils" module ActionController # Makes it dead easy to do HTTP Basic, Digest and Token authentication. @@ -28,7 +30,7 @@ module ActionController # class ApplicationController < ActionController::Base # before_action :set_account, :authenticate # - # protected + # private # def set_account # @account = Account.find_by(url_name: request.subdomains.first) # end @@ -70,10 +72,10 @@ module ActionController 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 `variable_size_secure_compare` so that length information + # uses `secure_compare` so that length information # isn't leaked. - ActiveSupport::SecurityUtils.variable_size_secure_compare(name, options[:name]) & - ActiveSupport::SecurityUtils.variable_size_secure_compare(password, options[:password]) + ActiveSupport::SecurityUtils.secure_compare(name, options[:name]) & + ActiveSupport::SecurityUtils.secure_compare(password, options[:password]) end end end @@ -99,23 +101,23 @@ module ActionController end def has_basic_credentials?(request) - request.authorization.present? && (auth_scheme(request).downcase == 'basic') + request.authorization.present? && (auth_scheme(request).downcase == "basic") end def user_name_and_password(request) - decode_credentials(request).split(':', 2) + decode_credentials(request).split(":", 2) end def decode_credentials(request) - ::Base64.decode64(auth_param(request) || '') + ::Base64.decode64(auth_param(request) || "") end def auth_scheme(request) - request.authorization.to_s.split(' ', 2).first + request.authorization.to_s.split(" ", 2).first end def auth_param(request) - request.authorization.to_s.split(' ', 2).second + request.authorization.to_s.split(" ", 2).second end def encode_credentials(user_name, password) @@ -208,7 +210,7 @@ module ActionController password = password_procedure.call(credentials[:username]) return false unless password - method = request.get_header('rack.methodoverride.original_method') || request.get_header('REQUEST_METHOD') + method = request.get_header("rack.methodoverride.original_method") || request.get_header("REQUEST_METHOD") uri = credentials[:uri] [true, false].any? do |trailing_question_mark| @@ -224,19 +226,19 @@ module ActionController # Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+ # Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead # of a plain-text password. - def expected_response(http_method, uri, credentials, password, password_is_ha1=true) + def expected_response(http_method, uri, credentials, password, password_is_ha1 = true) ha1 = password_is_ha1 ? password : ha1(credentials, password) - ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(':')) - ::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(':')) + ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":")) + ::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":")) end def ha1(credentials, password) - ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':')) + ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":")) end def encode_credentials(http_method, credentials, password, password_is_ha1) credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1) - "Digest " + credentials.sort_by {|x| x[0].to_s }.map {|v| "#{v[0]}='#{v[1]}'" }.join(', ') + "Digest " + credentials.sort_by { |x| x[0].to_s }.map { |v| "#{v[0]}='#{v[1]}'" }.join(", ") end def decode_credentials_header(request) @@ -244,9 +246,9 @@ module ActionController end def decode_credentials(header) - ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, '').split(',').map do |pair| - key, value = pair.split('=', 2) - [key.strip, value.to_s.gsub(/^"|"$/,'').delete('\'')] + ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, "").split(",").map do |pair| + key, value = pair.split("=", 2) + [key.strip, value.to_s.gsub(/^"|"$/, "").delete("'")] end] end @@ -314,7 +316,7 @@ module ActionController # Can be much shorter if the Stale directive is implemented. This would # allow a user to use new nonce without prompting the user again for their # username and password. - def validate_nonce(secret_key, request, value, seconds_to_timeout=5*60) + def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60) return false if value.nil? t = ::Base64.decode64(value).split(":").first.to_i nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout @@ -324,7 +326,6 @@ module ActionController def opaque(secret_key) ::Digest::MD5.hexdigest(secret_key) end - end # Makes it dead easy to do HTTP Token authentication. @@ -349,10 +350,7 @@ module ActionController # authenticate_or_request_with_http_token do |token, options| # # Compare the tokens in a time-constant manner, to mitigate # # timing attacks. - # ActiveSupport::SecurityUtils.secure_compare( - # ::Digest::SHA256.hexdigest(token), - # ::Digest::SHA256.hexdigest(TOKEN) - # ) + # ActiveSupport::SecurityUtils.secure_compare(token, TOKEN) # end # end # end @@ -364,7 +362,7 @@ module ActionController # class ApplicationController < ActionController::Base # before_action :set_account, :authenticate # - # protected + # private # def set_account # @account = Account.find_by(url_name: request.subdomains.first) # end @@ -406,7 +404,7 @@ module ActionController # # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] module Token - TOKEN_KEY = 'token=' + TOKEN_KEY = "token=" TOKEN_REGEX = /^(Token|Bearer)\s+/ AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/ extend self @@ -446,7 +444,7 @@ module ActionController end end - # Parses the token and options out of the token authorization header. + # Parses the token and options out of the token Authorization header. # The value for the Authorization header is expected to have the prefix # <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this: # Authorization: Token token="abc", nonce="def" @@ -476,14 +474,14 @@ module ActionController # This removes the <tt>"</tt> characters wrapping the value. def rewrite_param_values(array_params) - array_params.each { |param| (param[1] || "").gsub! %r/^"|"$/, '' } + array_params.each { |param| (param[1] || "".dup).gsub! %r/^"|"$/, "" } end # This method takes an authorization body and splits up the key-value # pairs by the standardized <tt>:</tt>, <tt>;</tt>, or <tt>\t</tt> # delimiters defined in +AUTHN_PAIR_DELIMITERS+. def raw_params(auth) - _raw_params = auth.sub(TOKEN_REGEX, '').split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/) + _raw_params = auth.sub(TOKEN_REGEX, "").split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/) if !(_raw_params.first =~ %r{\A#{TOKEN_KEY}}) _raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}" diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index 6192fc0f9c..ac0c127cdc 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -1,14 +1,14 @@ -require 'active_support/core_ext/string/strip' +# frozen_string_literal: true module ActionController # Handles implicit rendering for a controller action that does not # explicitly respond with +render+, +respond_to+, +redirect+, or +head+. # - # For API controllers, the implicit response is always 204 No Content. + # For API controllers, the implicit response is always <tt>204 No Content</tt>. # # For all other controllers, we use these heuristics to decide whether to # render a template, raise an error for a missing template, or respond with - # 204 No Content: + # <tt>204 No Content</tt>: # # First, if we DO find a template, it's rendered. Template lookup accounts # for the action name, locales, format, variant, template handlers, and more @@ -25,9 +25,8 @@ module ActionController # <tt>ActionView::UnknownFormat</tt> with an explanation. # # Finally, if we DON'T find a template AND the request isn't a browser page - # load, then we implicitly respond with 204 No Content. + # load, then we implicitly respond with <tt>204 No Content</tt>. module ImplicitRender - # :stopdoc: include BasicImplicitRender @@ -49,7 +48,7 @@ module ActionController "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 " \ + "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." @@ -62,8 +61,8 @@ module ActionController def method_for_action(action_name) super || if template_exists?(action_name.to_s, _prefixes) - "default_render" - end + "default_render" + end end private diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index 624a6d5b76..be9449629f 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -1,9 +1,11 @@ -require 'benchmark' -require 'abstract_controller/logger' +# frozen_string_literal: true + +require "benchmark" +require "abstract_controller/logger" module ActionController # Adds instrumentation to several ends in ActionController::Base. It also provides - # some hooks related with process_action, this allows an ORM like Active Record + # some hooks related with process_action. This allows an ORM like Active Record # and/or DataMapper to plug in ActionController and show related information. # # Check ActiveRecord::Railties::ControllerRuntime for an example. @@ -16,13 +18,13 @@ module ActionController def process_action(*args) raw_payload = { - :controller => self.class.name, - :action => self.action_name, - :params => request.filtered_parameters, - :headers => request.headers, - :format => request.format.ref, - :method => request.request_method, - :path => request.fullpath + controller: self.class.name, + action: action_name, + params: request.filtered_parameters, + headers: request.headers, + format: request.format.ref, + method: request.request_method, + path: request.fullpath } ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup) @@ -46,9 +48,9 @@ module ActionController render_output end - def send_file(path, options={}) + def send_file(path, options = {}) ActiveSupport::Notifications.instrument("send_file.action_controller", - options.merge(:path => path)) do + options.merge(path: path)) do super end end @@ -72,7 +74,7 @@ module ActionController # A hook invoked every time a before callback is halted. def halted_callback_hook(filter) - ActiveSupport::Notifications.instrument("halted_callback.action_controller", :filter => filter) + ActiveSupport::Notifications.instrument("halted_callback.action_controller", filter: filter) end # A hook which allows you to clean up any time, wrongly taken into account in @@ -81,16 +83,13 @@ module ActionController # def cleanup_view_runtime # super - time_taken_in_something_expensive # end - # - # :api: plugin - def cleanup_view_runtime #:nodoc: + def cleanup_view_runtime # :doc: yield end # Every time after an action is processed, this method is invoked # with the payload, so you can add more information. - # :api: plugin - def append_info_to_payload(payload) #:nodoc: + def append_info_to_payload(payload) # :doc: payload[:view_runtime] = view_runtime end @@ -98,7 +97,6 @@ module ActionController # A hook which allows other frameworks to log what happened during # controller process action. This method should return an array # with the messages to be added. - # :api: plugin def log_process_action(payload) #:nodoc: messages, view_runtime = [], payload[:view_runtime] messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 5d395cd8bd..2f4c8fb83c 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -1,6 +1,8 @@ -require 'action_dispatch/http/response' -require 'delegate' -require 'active_support/json' +# frozen_string_literal: true + +require "action_dispatch/http/response" +require "delegate" +require "active_support/json" module ActionController # Mix this module into your controller, and all actions in that controller @@ -84,7 +86,6 @@ 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 ) def initialize(stream, options = {}) @@ -206,7 +207,12 @@ module ActionController private def each_chunk(&block) - while str = @buf.pop + loop do + str = nil + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + str = @buf.pop + end + break unless str yield str end end @@ -215,18 +221,18 @@ module ActionController class Response < ActionDispatch::Response #:nodoc: all private - def before_committed - super - jar = request.cookie_jar - # The response can be committed multiple times - jar.write self unless committed? - end + def before_committed + super + jar = request.cookie_jar + # The response can be committed multiple times + jar.write self unless committed? + end - def build_buffer(response, body) - buf = Live::Buffer.new response - body.each { |part| buf.write part } - buf - end + def build_buffer(response, body) + buf = Live::Buffer.new response + body.each { |part| buf.write part } + buf + end end def process(name) @@ -235,15 +241,15 @@ module ActionController error = nil # This processes the action in a child thread. It lets us return the - # response code and headers back up the rack stack, and still process - # the body in parallel with sending data to the client + # response code and headers back up the Rack stack, and still process + # the body in parallel with sending data to the client. new_controller_thread { ActiveSupport::Dependencies.interlock.running do t2 = Thread.current # Since we're processing the view in a different thread, copy the # thread locals from the main thread to the child thread. :'( - locals.each { |k,v| t2[k] = v } + locals.each { |k, v| t2[k] = v } begin super(name) @@ -274,9 +280,9 @@ module ActionController raise error if error end - # Spawn a new thread to serve up the controller in. This is to get + # 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 + # 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 { @@ -291,7 +297,7 @@ module ActionController return unless logger logger.fatal do - message = "\n#{exception.class} (#{exception.message}):\n" + 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" diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 2e89af1a5e..2233b93406 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -1,4 +1,6 @@ -require 'abstract_controller/collector' +# frozen_string_literal: true + +require "abstract_controller/collector" module ActionController #:nodoc: module MimeResponds @@ -181,8 +183,8 @@ module ActionController #:nodoc: # # request.variant = [:tablet, :phone] # - # which will work similarly to formats and MIME types negotiation. If there will be no - # +:tablet+ variant declared, +:phone+ variant will be picked: + # This will work similarly to formats and MIME types negotiation. If there + # is no +:tablet+ variant declared, the +:phone+ variant will be used: # # respond_to do |format| # format.html.none @@ -280,8 +282,8 @@ module ActionController #:nodoc: def any(*args, &block) if block_given? - if args.any? && args.none?{ |a| a == @variant } - args.each{ |v| @variants[v] = block } + if args.any? && args.none? { |a| a == @variant } + args.each { |v| @variants[v] = block } else @variants[:any] = block end diff --git a/actionpack/lib/action_controller/metal/parameter_encoding.rb b/actionpack/lib/action_controller/metal/parameter_encoding.rb new file mode 100644 index 0000000000..7a45732d31 --- /dev/null +++ b/actionpack/lib/action_controller/metal/parameter_encoding.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module ActionController + # Specify binary encoding for parameters for a given action. + module ParameterEncoding + extend ActiveSupport::Concern + + module ClassMethods + def inherited(klass) # :nodoc: + super + klass.setup_param_encode + end + + def setup_param_encode # :nodoc: + @_parameter_encodings = {} + end + + def binary_params_for?(action) # :nodoc: + @_parameter_encodings[action.to_s] + end + + # Specify that a given action's parameters should all be encoded as + # ASCII-8BIT (it "skips" the encoding default of UTF-8). + # + # For example, a controller would use it like this: + # + # class RepositoryController < ActionController::Base + # skip_parameter_encoding :show + # + # def show + # @repo = Repository.find_by_filesystem_path params[:file_path] + # + # # `repo_name` is guaranteed to be UTF-8, but was ASCII-8BIT, so + # # tag it as such + # @repo_name = params[:repo_name].force_encoding 'UTF-8' + # end + # + # def index + # @repositories = Repository.all + # end + # end + # + # The show action in the above controller would have all parameter values + # encoded as ASCII-8BIT. This is useful in the case where an application + # must handle data but encoding of the data is unknown, like file system data. + def skip_parameter_encoding(action) + @_parameter_encodings[action.to_s] = true + end + end + end +end diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index c38fc40b81..a678377d4f 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -1,7 +1,9 @@ -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/hash/except' -require 'active_support/core_ext/module/anonymous' -require 'action_dispatch/http/mime_type' +# frozen_string_literal: true + +require "active_support/core_ext/hash/slice" +require "active_support/core_ext/hash/except" +require "active_support/core_ext/module/anonymous" +require "action_dispatch/http/mime_type" module ActionController # Wraps the parameters hash into a nested hash. This will allow clients to @@ -71,7 +73,7 @@ module ActionController EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8) - require 'mutex_m' + require "mutex_m" class Options < Struct.new(:name, :format, :include, :exclude, :klass, :model) # :nodoc: include Mutex_m @@ -105,7 +107,19 @@ module ActionController unless super || exclude if m.respond_to?(:attribute_names) && m.attribute_names.any? - self.include = m.attribute_names + if m.respond_to?(:stored_attributes) && !m.stored_attributes.empty? + self.include = m.attribute_names + m.stored_attributes.values.flatten.map(&:to_s) + else + self.include = m.attribute_names + end + + if m.respond_to?(:nested_attributes_options) && m.nested_attributes_options.keys.any? + self.include += m.nested_attributes_options.keys.map do |key| + key.to_s.concat("_attributes") + end + end + + self.include end end end @@ -128,35 +142,34 @@ module ActionController end private - # Determine the wrapper model from the controller's name. By convention, - # this could be done by trying to find the defined model that has the - # same singular name as the controller. For example, +UsersController+ - # will try to find if the +User+ model exists. - # - # This method also does namespace lookup. Foo::Bar::UsersController will - # try to find Foo::Bar::User, Foo::User and finally User. - def _default_wrap_model #:nodoc: - return nil if klass.anonymous? - model_name = klass.name.sub(/Controller$/, '').classify - - begin - if model_klass = model_name.safe_constantize - model_klass - else - namespaces = model_name.split("::") - namespaces.delete_at(-2) - break if namespaces.last == model_name - model_name = namespaces.join("::") - end - end until model_klass + # Determine the wrapper model from the controller's name. By convention, + # this could be done by trying to find the defined model that has the + # same singular name as the controller. For example, +UsersController+ + # will try to find if the +User+ model exists. + # + # This method also does namespace lookup. Foo::Bar::UsersController will + # try to find Foo::Bar::User, Foo::User and finally User. + def _default_wrap_model + return nil if klass.anonymous? + model_name = klass.name.sub(/Controller$/, "").classify + + begin + if model_klass = model_name.safe_constantize + model_klass + else + namespaces = model_name.split("::") + namespaces.delete_at(-2) + break if namespaces.last == model_name + model_name = namespaces.join("::") + end + end until model_klass - model_klass - end + model_klass + end end included do - class_attribute :_wrapper_options - self._wrapper_options = Options.from_hash(format: []) + class_attribute :_wrapper_options, default: Options.from_hash(format: []) end module ClassMethods @@ -198,14 +211,14 @@ module ActionController when Hash options = name_or_model_or_options when false - options = options.merge(:format => []) + options = options.merge(format: []) when Symbol, String - options = options.merge(:name => name_or_model_or_options) + options = options.merge(name: name_or_model_or_options) else model = name_or_model_or_options end - opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options) + opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options) opts.model = model opts.klass = self @@ -213,7 +226,7 @@ module ActionController end # Sets the default wrapper key or model which will be used to determine - # wrapper key and attribute names. Will be called automatically when the + # wrapper key and attribute names. Called automatically when the # module is inherited. def inherited(klass) if klass._wrapper_options.format.any? @@ -225,24 +238,19 @@ module ActionController end end - # Performs parameters wrapping upon the request. Will be called automatically + # Performs parameters wrapping upon the request. Called automatically # by the metal call stack. def process_action(*args) if _wrapper_enabled? - if request.parameters[_wrapper_key].present? - wrapped_hash = _extract_parameters(request.parameters) - else - wrapped_hash = _wrap_parameters request.request_parameters - end - + 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 + # 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 + # This will display the wrapped hash in the log file. request.filtered_parameters.merge! wrapped_filtered_hash end super @@ -279,7 +287,7 @@ module ActionController return false unless request.has_content_type? ref = request.content_mime_type.ref - _wrapper_formats.include?(ref) && _wrapper_key && !request.request_parameters[_wrapper_key] + _wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key) end end end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 3c7cc15627..4c2b5120eb 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -1,12 +1,6 @@ -module ActionController - class RedirectBackError < AbstractController::Error #:nodoc: - DEFAULT_MESSAGE = 'No HTTP_REFERER was set in the request to this action, so redirect_to :back could not be called successfully. If this is a test, make sure to specify request.env["HTTP_REFERER"].' - - def initialize(message = nil) - super(message || DEFAULT_MESSAGE) - end - end +# frozen_string_literal: true +module ActionController module Redirecting extend ActiveSupport::Concern @@ -24,27 +18,27 @@ module ActionController # === Examples: # # redirect_to action: "show", id: 5 - # redirect_to post + # redirect_to @post # redirect_to "http://www.rubyonrails.org" # redirect_to "/images/screenshot.jpg" - # redirect_to articles_url + # redirect_to posts_url # redirect_to proc { edit_post_url(@post) } # - # The redirection happens as a "302 Found" header unless otherwise specified using the <tt>:status</tt> option: + # The redirection happens as a <tt>302 Found</tt> header unless otherwise specified using the <tt>:status</tt> option: # # redirect_to post_url(@post), status: :found # redirect_to action: 'atom', status: :moved_permanently # redirect_to post_url(@post), status: 301 # redirect_to action: 'atom', status: 302 # - # The status code can either be a standard {HTTP Status code}[http://www.iana.org/assignments/http-status-codes] as an + # The status code can either be a standard {HTTP Status code}[https://www.iana.org/assignments/http-status-codes] as an # integer, or a symbol representing the downcased, underscored and symbolized description. # Note that the status code must be a 3xx HTTP code, or redirection will not occur. # # If you are using XHR requests other than GET or POST and redirecting after the # request then some browsers will follow the redirect using the original request # method. This may lead to undesirable behavior such as a double DELETE. To work - # around this you can return a <tt>303 See Other</tt> status code which will be + # around this you can return a <tt>303 See Other</tt> status code which will be # followed using a GET request. # # redirect_to posts_url, status: :see_other @@ -58,39 +52,45 @@ module ActionController # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id } # redirect_to({ action: 'atom' }, alert: "Something serious happened") # - def redirect_to(options = {}, response_status = {}) #:doc: + # 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 = {}) 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.location = _compute_redirect_to_location(request, options) - self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>" + self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>" end # Redirects the browser to the page that issued the request (the referrer) # if possible, otherwise redirects to the provided default fallback # location. # - # The referrer information is pulled from the HTTP `Referer` (sic) header on + # The referrer information is pulled from the HTTP +Referer+ (sic) header on # the request. This is an optional header and its presence on the request is # subject to browser security settings and user preferences. If the request # is missing this header, the <tt>fallback_location</tt> will be used. # # redirect_back fallback_location: { action: "show", id: 5 } - # redirect_back fallback_location: post + # redirect_back fallback_location: @post # redirect_back fallback_location: "http://www.rubyonrails.org" - # redirect_back fallback_location: "/images/screenshot.jpg" - # redirect_back fallback_location: articles_url - # redirect_back fallback_location: proc { edit_post_url(@post) } + # redirect_back fallback_location: "/images/screenshot.jpg" + # redirect_back fallback_location: posts_url + # redirect_back fallback_location: proc { edit_post_url(@post) } + # redirect_back fallback_location: '/', allow_other_host: false # - # All options that can be passed to <tt>redirect_to</tt> are accepted as + # ==== Options + # * <tt>:fallback_location</tt> - The default fallback location that will be used on missing +Referer+ header. + # * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true. + # + # All other options that can be passed to <tt>redirect_to</tt> are accepted as # options and the behavior is identical. - def redirect_back(fallback_location:, **args) - if referer = request.headers["Referer"] - redirect_to referer, **args - else - redirect_to fallback_location, **args - end + def redirect_back(fallback_location:, allow_other_host: true, **args) + referer = request.headers["Referer"] + redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer)) + redirect_to redirect_to_referer ? referer : fallback_location, **args end def _compute_redirect_to_location(request, options) #:nodoc: @@ -98,20 +98,12 @@ module ActionController # The scheme name consist of a letter followed by any combination of # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") # characters; and is terminated by a colon (":"). - # See http://tools.ietf.org/html/rfc3986#section-3.1 + # See https://tools.ietf.org/html/rfc3986#section-3.1 # The protocol relative scheme starts with a double slash "//". when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i options when String request.protocol + request.host_with_port + options - when :back - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - `redirect_to :back` is deprecated and will be removed from Rails 5.1. - Please use `redirect_back(fallback_location: fallback_location)` where - `fallback_location` represents the location to use if the request has - no HTTP referer information. - MESSAGE - request.headers["Referer"] or raise RedirectBackError when Proc _compute_redirect_to_location request, options.call else @@ -131,5 +123,11 @@ module ActionController 302 end end + + def _url_host_allowed?(url) + URI(url.to_s).host == request.host + rescue ArgumentError, URI::Error + false + end end end diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 1735609cd9..b81d3ef539 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -1,4 +1,6 @@ -require 'set' +# frozen_string_literal: true + +require "set" module ActionController # See <tt>Renderers.add</tt> @@ -26,8 +28,7 @@ module ActionController RENDERERS = Set.new included do - class_attribute :_renderers - self._renderers = Set.new.freeze + class_attribute :_renderers, default: Set.new.freeze end # Used in <tt>ActionController::Base</tt> @@ -71,8 +72,6 @@ module ActionController # format.csv { render csv: @csvable, filename: @csvable.name } # end # end - # To use renderers and their mime types in more concise ways, see - # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt> def self.add(key, &block) define_method(_render_with_renderer_method_name(key), &block) RENDERERS << key.to_sym @@ -86,7 +85,7 @@ module ActionController def self.remove(key) RENDERERS.delete(key.to_sym) method_name = _render_with_renderer_method_name(key) - remove_method(method_name) if method_defined?(method_name) + remove_possible_method(method_name) end def self._render_with_renderer_method_name(key) @@ -94,7 +93,6 @@ module ActionController end module ClassMethods - # Adds, by name, a renderer or renderers to the +_renderers+ available # to call within controller actions. # @@ -107,7 +105,7 @@ module ActionController # # Since <tt>ActionController::Metal</tt> controllers cannot render, the controller # must include <tt>AbstractController::Rendering</tt>, <tt>ActionController::Rendering</tt>, - # and <tt>ActionController::Renderers</tt>, and have at lest one renderer. + # and <tt>ActionController::Renderers</tt>, and have at least one renderer. # # Rather than including <tt>ActionController::Renderers::All</tt> and including all renderers, # you may specify which renderers to include by passing the renderer name or names to diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index cce6fe7787..6d181e6456 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -1,10 +1,10 @@ -require 'active_support/core_ext/string/filters' +# frozen_string_literal: true module ActionController module Rendering extend ActiveSupport::Concern - RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html] + RENDER_FORMATS_IN_PRIORITY = [:body, :plain, :html] module ClassMethods # Documentation at ActionController::Renderer#render @@ -32,15 +32,15 @@ module ActionController # Check for double render errors and set the content_type after rendering. def render(*args) #:nodoc: - raise ::AbstractController::DoubleRenderError if self.response_body + raise ::AbstractController::DoubleRenderError if response_body super end - # Overwrite render_to_string because body can now be set to a rack body. + # Overwrite render_to_string because body can now be set to a Rack body. def render_to_string(*) result = super if result.respond_to?(:each) - string = "" + string = "".dup result.each { |r| string << r } string else @@ -49,84 +49,74 @@ module ActionController end def render_to_body(options = {}) - super || _render_in_priorities(options) || ' ' + super || _render_in_priorities(options) || " " end private - def _render_in_priorities(options) - RENDER_FORMATS_IN_PRIORITY.each do |format| - return options[format] if options.key?(format) + def _process_variant(options) + if defined?(request) && !request.nil? && request.variant.present? + options[:variant] = request.variant + end end - nil - end - - def _set_html_content_type - self.content_type = Mime[:html].to_s - end + def _render_in_priorities(options) + RENDER_FORMATS_IN_PRIORITY.each do |format| + return options[format] if options.key?(format) + end - def _set_rendered_content_type(format) - unless response.content_type - self.content_type = format.to_s + nil end - end - # Normalize arguments by catching blocks and setting them on :update. - def _normalize_args(action=nil, options={}, &blk) #:nodoc: - options = super - options[:update] = blk if block_given? - options - end - - # Normalize both text and status options. - def _normalize_options(options) #:nodoc: - _normalize_text(options) - - if options[:text] - ActiveSupport::Deprecation.warn <<-WARNING.squish - `render :text` is deprecated because it does not actually render a - `text/plain` response. Switch to `render plain: 'plain text'` to - render as `text/plain`, `render html: '<strong>HTML</strong>'` to - render as `text/html`, or `render body: 'raw'` to match the deprecated - behavior and render with the default Content-Type, which is - `text/plain`. - WARNING + def _set_html_content_type + self.content_type = Mime[:html].to_s end - if options[:html] - options[:html] = ERB::Util.html_escape(options[:html]) + def _set_rendered_content_type(format) + if format && !response.content_type + self.content_type = format.to_s + end end - if options.delete(:nothing) - ActiveSupport::Deprecation.warn("`:nothing` option is deprecated and will be removed in Rails 5.1. Use `head` method to respond with empty response body.") - options[:body] = nil + # Normalize arguments by catching blocks and setting them on :update. + def _normalize_args(action = nil, options = {}, &blk) + options = super + options[:update] = blk if block_given? + options end - if options[:status] - options[:status] = Rack::Utils.status_code(options[:status]) - end + # Normalize both text and status options. + def _normalize_options(options) + _normalize_text(options) - super - end + if options[:html] + options[:html] = ERB::Util.html_escape(options[:html]) + end - def _normalize_text(options) - RENDER_FORMATS_IN_PRIORITY.each do |format| - if options.key?(format) && options[format].respond_to?(:to_text) - options[format] = options[format].to_text + if options[:status] + options[:status] = Rack::Utils.status_code(options[:status]) end + + super end - end - # Process controller specific options, as status, content-type and location. - def _process_options(options) #:nodoc: - status, content_type, location = options.values_at(:status, :content_type, :location) + def _normalize_text(options) + RENDER_FORMATS_IN_PRIORITY.each do |format| + if options.key?(format) && options[format].respond_to?(:to_text) + options[format] = options[format].to_text + end + end + end - self.status = status if status - self.content_type = content_type if content_type - self.headers["Location"] = url_for(location) if location + # Process controller specific options, as status, content-type and location. + def _process_options(options) + status, content_type, location = options.values_at(:status, :content_type, :location) - super - end + self.status = status if status + self.content_type = content_type if content_type + headers["Location"] = url_for(location) if location + + super + end end end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 0559fbc6ce..767eddb361 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -1,6 +1,8 @@ -require 'rack/session/abstract/id' -require 'action_controller/metal/exceptions' -require 'active_support/security_utils' +# frozen_string_literal: true + +require "rack/session/abstract/id" +require "action_controller/metal/exceptions" +require "active_support/security_utils" module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: @@ -20,7 +22,7 @@ module ActionController #:nodoc: # 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 # use session-oriented authentication for these types of requests, by using - # the `protect_from_forgery` method in our controllers. + # the <tt>protect_from_forgery</tt> method in our controllers. # # GET requests are not protected since they don't have side effects like writing # to the database and don't leak sensitive information. JavaScript requests are @@ -85,6 +87,10 @@ module ActionController #:nodoc: config_accessor :per_form_csrf_tokens self.per_form_csrf_tokens = false + # Controls whether forgery protection is enabled by default. + config_accessor :default_protect_from_forgery + self.default_protect_from_forgery = false + helper_method :form_authenticity_token helper_method :protect_against_forgery? end @@ -109,10 +115,10 @@ module ActionController #:nodoc: # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. For example <tt>only: [ :create, :create_all ]</tt>. # * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference. # * <tt>:prepend</tt> - By default, the verification of the authentication token will be added at the position of the - # protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful - # when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth). + # protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful + # when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth). # - # If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>. + # If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>. # * <tt>:with</tt> - Set the method to handle unverified request. # # Valid unverified request handling methods are: @@ -128,13 +134,22 @@ module ActionController #:nodoc: append_after_action :verify_same_origin_request end + # Turn off request forgery protection. This is a wrapper for: + # + # skip_before_action :verify_authenticity_token + # + # See +skip_before_action+ for allowed options. + def skip_forgery_protection(options = {}) + skip_before_action :verify_authenticity_token, options + end + private - def protection_method_class(name) - ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify) - rescue NameError - raise ArgumentError, 'Invalid request forgery protection method, use :null_session, :exception, or :reset_session' - end + def protection_method_class(name) + ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify) + rescue NameError + raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, or :reset_session" + end end module ProtectionMethods @@ -152,28 +167,28 @@ module ActionController #:nodoc: request.cookie_jar = NullCookieJar.build(request, {}) end - protected + private - class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc: - def initialize(req) - super(nil, req) - @data = {} - @loaded = true - end + class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc: + def initialize(req) + super(nil, req) + @data = {} + @loaded = true + end - # no-op - def destroy; end + # no-op + def destroy; end - def exists? - true + def exists? + true + end end - end - class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc: - def write(*) - # nothing + class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc: + def write(*) + # nothing + end end - end end class ResetSession @@ -197,29 +212,33 @@ module ActionController #:nodoc: end end - protected + private # The actual before_action that is used to verify the CSRF token. # Don't override this directly. Provide your own forgery protection # strategy instead. If you override, you'll disable same-origin - # `<script>` verification. + # <tt><script></tt> verification. # # Lean on the protect_from_forgery declaration to mark which actions are # due for same-origin request verification. If protect_from_forgery is # enabled on an action, this before_action flags its after_action to # verify that JavaScript responses are for XHR requests, ensuring they # follow the browser's same-origin policy. - def verify_authenticity_token + def verify_authenticity_token # :doc: mark_for_same_origin_verification! if !verified_request? if logger && log_warning_on_csrf_failure - logger.warn "Can't verify CSRF token authenticity." + if valid_request_origin? + logger.warn "Can't verify CSRF token authenticity." + else + logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})" + end end handle_unverified_request end end - def handle_unverified_request + def handle_unverified_request # :doc: forgery_protection_strategy.new(self).handle_unverified_request end @@ -229,11 +248,12 @@ module ActionController #:nodoc: "If you know what you're doing, go ahead and disable forgery " \ "protection on this action to permit cross-origin JavaScript embedding." private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING + # :startdoc: - # If `verify_authenticity_token` was run (indicating that we have + # If +verify_authenticity_token+ was run (indicating that we have # forgery protection enabled for this request) then also verify that # we aren't serving an unauthorized cross-origin response. - def verify_same_origin_request + def verify_same_origin_request # :doc: if marked_for_same_origin_verification? && non_xhr_javascript_response? if logger && log_warning_on_csrf_failure logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING @@ -243,18 +263,18 @@ module ActionController #:nodoc: end # GET requests are checked for cross-origin JavaScript after rendering. - def mark_for_same_origin_verification! + def mark_for_same_origin_verification! # :doc: @marked_for_same_origin_verification = request.get? end - # If the `verify_authenticity_token` before_action ran, verify that + # If the +verify_authenticity_token+ before_action ran, verify that # JavaScript responses are only served to same-origin GET requests. - def marked_for_same_origin_verification? + def marked_for_same_origin_verification? # :doc: @marked_for_same_origin_verification ||= false end # Check for cross-origin JavaScript responses. - def non_xhr_javascript_response? + def non_xhr_javascript_response? # :doc: content_type =~ %r(\Atext/javascript) && !request.xhr? end @@ -262,23 +282,23 @@ module ActionController #:nodoc: # Returns true or false if a request is verified. Checks: # - # * Is it a GET or HEAD request? Gets should be safe and idempotent + # * Is it a GET or HEAD request? GETs should be safe and idempotent # * Does the form_authenticity_token match the given token value from the params? - # * Does the X-CSRF-Token header match the form_authenticity_token - def verified_request? + # * Does the X-CSRF-Token header match the form_authenticity_token? + def verified_request? # :doc: !protect_against_forgery? || request.get? || request.head? || (valid_request_origin? && any_authenticity_token_valid?) end # Checks if any of the authenticity tokens from the request are valid. - def any_authenticity_token_valid? + def any_authenticity_token_valid? # :doc: request_authenticity_tokens.any? do |token| valid_authenticity_token?(session, token) end end # Possible authenticity tokens sent in the request. - def request_authenticity_tokens + def request_authenticity_tokens # :doc: [form_authenticity_param, request.x_csrf_token] end @@ -290,7 +310,7 @@ module ActionController #:nodoc: # Creates a masked version of the authenticity token that varies # on each request. The masking is used to mitigate SSL attacks # like BREACH. - def masked_authenticity_token(session, form_options: {}) + def masked_authenticity_token(session, form_options: {}) # :doc: action, method = form_options.values_at(:action, :method) raw_token = if per_form_csrf_tokens && action && method @@ -309,7 +329,7 @@ module ActionController #:nodoc: # Checks the client's masked token to see if it matches the # session token. Essentially the inverse of # +masked_authenticity_token+. - def valid_authenticity_token?(session, encoded_masked_token) + def valid_authenticity_token?(session, encoded_masked_token) # :doc: if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String) return false end @@ -327,7 +347,7 @@ module ActionController #:nodoc: if masked_token.length == AUTHENTICITY_TOKEN_LENGTH # This is actually an unmasked token. This is expected if # you have just upgraded to masked tokens, but should stop - # happening shortly after installing this gem + # happening shortly after installing this gem. compare_with_real_token masked_token, session elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2 @@ -336,23 +356,23 @@ module ActionController #:nodoc: compare_with_real_token(csrf_token, session) || valid_per_form_csrf_token?(csrf_token, session) else - false # Token is malformed + false # Token is malformed. end end - def unmask_token(masked_token) + def unmask_token(masked_token) # :doc: # Split the token into the one-time pad and the encrypted - # value and decrypt it + # value and decrypt it. one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH] encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1] xor_byte_strings(one_time_pad, encrypted_csrf_token) end - def compare_with_real_token(token, session) - ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session)) + def compare_with_real_token(token, session) # :doc: + ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session)) end - def valid_per_form_csrf_token?(token, session) + def valid_per_form_csrf_token?(token, session) # :doc: if per_form_csrf_tokens correct_token = per_form_csrf_token( session, @@ -360,18 +380,18 @@ module ActionController #:nodoc: request.request_method ) - ActiveSupport::SecurityUtils.secure_compare(token, correct_token) + ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token) else false end end - def real_csrf_token(session) + def real_csrf_token(session) # :doc: session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH) Base64.strict_decode64(session[:_csrf_token]) end - def per_form_csrf_token(session, action_path, method) + def per_form_csrf_token(session, action_path, method) # :doc: OpenSSL::HMAC.digest( OpenSSL::Digest::SHA256.new, real_csrf_token(session), @@ -379,36 +399,46 @@ module ActionController #:nodoc: ) end - def xor_byte_strings(s1, s2) + 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_bytes.pack("C*") end # The form's authenticity parameter. Override to provide your own. - def form_authenticity_param + def form_authenticity_param # :doc: params[request_forgery_protection_token] end # Checks if the controller allows forgery protection. - def protect_against_forgery? + def protect_against_forgery? # :doc: allow_forgery_protection end + NULL_ORIGIN_MESSAGE = <<-MSG.strip_heredoc + 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 + 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 + Rails.application.config.action_controller.forgery_protection_origin_check setting. + MSG + # Checks if the request originated from the same origin by looking at the # Origin header. - def valid_request_origin? + def valid_request_origin? # :doc: if forgery_protection_origin_check # We accept blank origin headers because some user agents don't send it. + raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null" request.origin.nil? || request.origin == request.base_url else true end end - def normalize_action_path(action_path) + def normalize_action_path(action_path) # :doc: uri = URI.parse(action_path) - uri.path.chomp('/') + uri.path.chomp("/") end end end diff --git a/actionpack/lib/action_controller/metal/rescue.rb b/actionpack/lib/action_controller/metal/rescue.rb index 17f4030f25..44f7fb7a07 100644 --- a/actionpack/lib/action_controller/metal/rescue.rb +++ b/actionpack/lib/action_controller/metal/rescue.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module ActionController #:nodoc: - # This module is responsible for providing `rescue_from` helpers + # This module is responsible for providing +rescue_from+ helpers # to controllers and configuring when detailed exceptions must be # shown. module Rescue @@ -8,9 +10,9 @@ module ActionController #:nodoc: # Override this method if you want to customize when detailed # exceptions must be shown. This method is only called when - # consider_all_requests_local is false. By default, it returns - # false, but someone may set it to `request.local?` so local - # requests in production still shows the detailed exception pages. + # +consider_all_requests_local+ is +false+. By default, it returns + # +false+, but someone may set it to <tt>request.local?</tt> so local + # requests in production still show the detailed exception pages. def show_detailed_exceptions? false end @@ -19,7 +21,7 @@ module ActionController #:nodoc: def process_action(*args) super rescue Exception => exception - request.env['action_dispatch.show_detailed_exceptions'] ||= show_detailed_exceptions? + request.env["action_dispatch.show_detailed_exceptions"] ||= show_detailed_exceptions? rescue_with_handler(exception) || raise end end diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index a6115674aa..8dc01a5eb9 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -1,9 +1,11 @@ -require 'rack/chunked' +# frozen_string_literal: true + +require "rack/chunked" module ActionController #:nodoc: # Allows views to be streamed back to the client as they are rendered. # - # The default way Rails renders views is by first rendering the template + # By default, Rails renders views by first rendering the template # and then the layout. The response is sent to the client after the whole # template is rendered, all queries are made, and the layout is processed. # @@ -181,7 +183,7 @@ module ActionController #:nodoc: # unicorn_rails --config-file unicorn.config.rb # # You may also want to configure other parameters like <tt>:tcp_nodelay</tt>. - # Please check its documentation for more information: http://unicorn.bogomips.org/Unicorn/Configurator.html#method-i-listen + # Please check its documentation for more information: https://bogomips.org/unicorn/Unicorn/Configurator.html#method-i-listen # # If you are using Unicorn with NGINX, you may need to tweak NGINX. # Streaming should work out of the box on Rainbows. @@ -193,10 +195,10 @@ module ActionController #:nodoc: module Streaming extend ActiveSupport::Concern - protected + private # Set proper cache control and transfer encoding when streaming - def _process_options(options) #:nodoc: + def _process_options(options) super if options[:stream] if request.version == "HTTP/1.0" @@ -210,7 +212,7 @@ module ActionController #:nodoc: end # Call render_body if we are streaming instead of usual +render+. - def _render_template(options) #:nodoc: + def _render_template(options) if options.delete(:stream) Rack::Chunked::Body.new view_renderer.render_body(view_context, options) else diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 46589901fd..a56ac749f8 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -1,12 +1,16 @@ -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/rescuable' -require 'action_dispatch/http/upload' -require 'rack/test' -require 'stringio' -require 'set' +# 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" +require "active_support/rescuable" +require "action_dispatch/http/upload" +require "rack/test" +require "stringio" +require "set" +require "yaml" module ActionController # Raised when a required parameter is missing. @@ -31,13 +35,25 @@ module ActionController # # params = ActionController::Parameters.new(a: "123", b: "456") # params.permit(:c) - # # => ActionController::UnpermittedParameters: found unpermitted parameters: a, b + # # => ActionController::UnpermittedParameters: found unpermitted parameters: :a, :b class UnpermittedParameters < IndexError attr_reader :params # :nodoc: def initialize(params) # :nodoc: @params = params - super("found unpermitted parameter#{'s' if params.size > 1 }: #{params.join(", ")}") + super("found unpermitted parameter#{'s' if params.size > 1 }: #{params.map { |e| ":#{e}" }.join(", ")}") + end + end + + # Raised when a Parameters instance is not marked as permitted and + # an operation to transform it to hash is called. + # + # params = ActionController::Parameters.new(a: "123", b: "456") + # params.to_h + # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash + class UnfilteredParameters < ArgumentError + def initialize # :nodoc: + super("unable to convert unpermitted parameters to hash") end end @@ -51,15 +67,14 @@ module ActionController # # params = ActionController::Parameters.new({ # person: { - # name: 'Francesco', + # name: "Francesco", # age: 22, - # role: 'admin' + # role: "admin" # } # }) # # permitted = params.require(:person).permit(:name, :age) - # permitted # => {"name"=>"Francesco", "age"=>22} - # permitted.class # => ActionController::Parameters + # permitted # => <ActionController::Parameters {"name"=>"Francesco", "age"=>22} permitted: true> # permitted.permitted? # => true # # Person.first.update!(permitted) @@ -70,8 +85,8 @@ module ActionController # * +permit_all_parameters+ - If it's +true+, all the parameters will be # permitted by default. The default is +false+. # * +action_on_unpermitted_parameters+ - Allow to control the behavior when parameters - # that are not explicitly permitted are found. The values can be <tt>:log</tt> to - # write a message on the logger or <tt>:raise</tt> to raise + # that are not explicitly permitted are found. The values can be +false+ to just filter them + # out, <tt>:log</tt> to additionally write a message on the logger, or <tt>:raise</tt> to raise # ActionController::UnpermittedParameters exception. The default value is <tt>:log</tt> # in test and development environments, +false+ otherwise. # @@ -87,7 +102,7 @@ module ActionController # # params = ActionController::Parameters.new(a: "123", b: "456") # params.permit(:c) - # # => {} + # # => <ActionController::Parameters {} permitted: true> # # ActionController::Parameters.action_on_unpermitted_parameters = :raise # @@ -102,15 +117,95 @@ module ActionController # You can fetch values of <tt>ActionController::Parameters</tt> using either # <tt>:key</tt> or <tt>"key"</tt>. # - # params = ActionController::Parameters.new(key: 'value') + # params = ActionController::Parameters.new(key: "value") # params[:key] # => "value" # params["key"] # => "value" class Parameters - cattr_accessor :permit_all_parameters, instance_accessor: false + cattr_accessor :permit_all_parameters, instance_accessor: false, default: false + cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false + ## + # :method: as_json + # + # :call-seq: + # as_json(options=nil) + # + # Returns a hash that can be used as the JSON representation for the parameters. + + ## + # :method: empty? + # + # :call-seq: + # empty?() + # + # Returns true if the parameters have no key/value pairs. + + ## + # :method: has_key? + # + # :call-seq: + # has_key?(key) + # + # Returns true if the given key is present in the parameters. + + ## + # :method: has_value? + # + # :call-seq: + # has_value?(value) + # + # Returns true if the given value is present for some key in the parameters. + + ## + # :method: include? + # + # :call-seq: + # include?(key) + # + # Returns true if the given key is present in the parameters. + + ## + # :method: key? + # + # :call-seq: + # key?(key) + # + # Returns true if the given key is present in the parameters. + + ## + # :method: keys + # + # :call-seq: + # keys() + # + # Returns a new array of the keys of the parameters. + + ## + # :method: to_s + # + # :call-seq: + # to_s() + # + # Returns the content of the parameters as a string. + + ## + # :method: value? + # + # :call-seq: + # value?(value) + # + # Returns true if the given value is present for some key in the parameters. + + ## + # :method: values + # + # :call-seq: + # values() + # + # Returns a new array of the values of the parameters. delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?, - :as_json, to: :@parameters + :as_json, :to_s, to: :@parameters # By default, never raise an UnpermittedParameters exception if these # params are present. The default includes both 'controller' and 'action' @@ -119,8 +214,7 @@ module ActionController # config. For instance: # # config.always_permitted_parameters = %w( controller action format ) - cattr_accessor :always_permitted_parameters - self.always_permitted_parameters = %w( controller action ) + cattr_accessor :always_permitted_parameters, default: %w( controller action ) # Returns a new instance of <tt>ActionController::Parameters</tt>. # Also, sets the +permitted+ attribute to the default value of @@ -129,13 +223,13 @@ module ActionController # class Person < ActiveRecord::Base # end # - # params = ActionController::Parameters.new(name: 'Francesco') + # params = ActionController::Parameters.new(name: "Francesco") # params.permitted? # => false # Person.new(params) # => ActiveModel::ForbiddenAttributesError # # ActionController::Parameters.permit_all_parameters = true # - # params = ActionController::Parameters.new(name: 'Francesco') + # params = ActionController::Parameters.new(name: "Francesco") # params.permitted? # => true # Person.new(params) # => #<Person id: nil, name: "Francesco"> def initialize(parameters = {}) @@ -147,29 +241,21 @@ module ActionController # permitted flag. def ==(other) if other.respond_to?(:permitted?) - self.permitted? == other.permitted? && self.parameters == other.parameters - elsif other.is_a?(Hash) - ActiveSupport::Deprecation.warn <<-WARNING.squish - Comparing equality between `ActionController::Parameters` and a - `Hash` is deprecated and will be removed in Rails 5.1. Please only do - comparisons between instances of `ActionController::Parameters`. If - you need to compare to a hash, first convert it using - `ActionController::Parameters#new`. - WARNING - @parameters == other.with_indifferent_access + permitted? == other.permitted? && parameters == other.parameters else @parameters == other end end # Returns a safe <tt>ActiveSupport::HashWithIndifferentAccess</tt> - # representation of this parameter with all unpermitted keys removed. + # representation of the parameters with all unpermitted keys removed. # # params = ActionController::Parameters.new({ - # name: 'Senjougahara Hitagi', - # oddity: 'Heavy stone crab' + # name: "Senjougahara Hitagi", + # oddity: "Heavy stone crab" # }) - # params.to_h # => {} + # params.to_h + # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash # # safe_params = params.permit(:name) # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"} @@ -177,17 +263,66 @@ module ActionController if permitted? convert_parameters_to_hashes(@parameters, :to_h) else - slice(*self.class.always_permitted_parameters).permit!.to_h + raise UnfilteredParameters end end + # Returns a safe <tt>Hash</tt> representation of the parameters + # with all unpermitted keys removed. + # + # params = ActionController::Parameters.new({ + # name: "Senjougahara Hitagi", + # oddity: "Heavy stone crab" + # }) + # params.to_hash + # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash + # + # safe_params = params.permit(:name) + # safe_params.to_hash # => {"name"=>"Senjougahara Hitagi"} + def to_hash + to_h.to_hash + end + + # Returns a string representation of the receiver suitable for use as a URL + # query string: + # + # params = ActionController::Parameters.new({ + # name: "David", + # nationality: "Danish" + # }) + # params.to_query + # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash + # + # safe_params = params.permit(:name, :nationality) + # safe_params.to_query + # # => "name=David&nationality=Danish" + # + # An optional namespace can be passed to enclose key names: + # + # params = ActionController::Parameters.new({ + # name: "David", + # nationality: "Danish" + # }) + # safe_params = params.permit(:name, :nationality) + # safe_params.to_query("user") + # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish" + # + # The string pairs "key=value" that conform the query string + # are sorted lexicographically in ascending order. + # + # This method is also aliased as +to_param+. + def to_query(*args) + to_h.to_query(*args) + end + alias_method :to_param, :to_query + # Returns an unsafe, unfiltered - # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of this - # parameter. + # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of the + # parameters. # # params = ActionController::Parameters.new({ - # name: 'Senjougahara Hitagi', - # oddity: 'Heavy stone crab' + # name: "Senjougahara Hitagi", + # oddity: "Heavy stone crab" # }) # params.to_unsafe_h # # => {"name"=>"Senjougahara Hitagi", "oddity" => "Heavy stone crab"} @@ -197,10 +332,10 @@ module ActionController alias_method :to_unsafe_hash, :to_unsafe_h # Convert all hashes in values into parameters, then yield each pair in - # the same way as <tt>Hash#each_pair</tt> + # the same way as <tt>Hash#each_pair</tt>. def each_pair(&block) @parameters.each_pair do |key, value| - yield key, convert_hashes_to_parameters(key, value) + yield [key, convert_hashes_to_parameters(key, value)] end end alias_method :each, :each_pair @@ -232,7 +367,7 @@ module ActionController # class Person < ActiveRecord::Base # end # - # params = ActionController::Parameters.new(name: 'Francesco') + # params = ActionController::Parameters.new(name: "Francesco") # params.permitted? # => false # Person.new(params) # => ActiveModel::ForbiddenAttributesError # params.permit! @@ -254,8 +389,8 @@ module ActionController # When passed a single key, if it exists and its associated value is # either present or the singleton +false+, returns said value: # - # ActionController::Parameters.new(person: { name: 'Francesco' }).require(:person) - # # => {"name"=>"Francesco"} + # ActionController::Parameters.new(person: { name: "Francesco" }).require(:person) + # # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false> # # Otherwise raises <tt>ActionController::ParameterMissing</tt>: # @@ -276,18 +411,18 @@ module ActionController # returned: # # params = ActionController::Parameters.new(user: { ... }, profile: { ... }) - # user_params, profile_params = params.require(:user, :profile) + # user_params, profile_params = params.require([:user, :profile]) # # Otherwise, the method re-raises the first exception found: # # params = ActionController::Parameters.new(user: {}, profile: {}) - # user_params, profile_params = params.require(:user, :profile) + # user_params, profile_params = params.require([:user, :profile]) # # ActionController::ParameterMissing: param is missing or the value is empty: user # # Technically this method can be used to fetch terminal values: # # # CAREFUL - # params = ActionController::Parameters.new(person: { name: 'Finn' }) + # params = ActionController::Parameters.new(person: { name: "Finn" }) # name = params.require(:person).require(:name) # CAREFUL # # but take into account that at some point those ones have to be permitted: @@ -317,7 +452,7 @@ module ActionController # for the object to +true+. This is useful for limiting which attributes # should be allowed for mass updating. # - # params = ActionController::Parameters.new(user: { name: 'Francesco', age: 22, role: 'admin' }) + # params = ActionController::Parameters.new(user: { name: "Francesco", age: 22, role: "admin" }) # permitted = params.require(:user).permit(:name, :age) # permitted.permitted? # => true # permitted.has_key?(:name) # => true @@ -337,18 +472,27 @@ module ActionController # You may declare that the parameter should be an array of permitted scalars # by mapping it to an empty array: # - # params = ActionController::Parameters.new(tags: ['rails', 'parameters']) + # params = ActionController::Parameters.new(tags: ["rails", "parameters"]) # params.permit(tags: []) # + # Sometimes it is not possible or convenient to declare the valid keys of + # a hash parameter or its internal structure. Just map to an empty hash: + # + # params.permit(preferences: {}) + # + # Be careful because this opens the door to arbitrary input. In this + # case, +permit+ ensures values in the returned structure are permitted + # scalars and filters out anything else. + # # You can also use +permit+ on nested parameters, like: # # params = ActionController::Parameters.new({ # person: { - # name: 'Francesco', + # name: "Francesco", # age: 22, # pets: [{ - # name: 'Purplish', - # category: 'dogs' + # name: "Purplish", + # category: "dogs" # }] # } # }) @@ -367,20 +511,20 @@ module ActionController # params = ActionController::Parameters.new({ # person: { # contact: { - # email: 'none@test.com', - # phone: '555-1234' + # email: "none@test.com", + # phone: "555-1234" # } # } # }) # # params.require(:person).permit(:contact) - # # => {} + # # => <ActionController::Parameters {} permitted: true> # # params.require(:person).permit(contact: :phone) - # # => {"contact"=>{"phone"=>"555-1234"}} + # # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"phone"=>"555-1234"} permitted: true>} permitted: true> # # params.require(:person).permit(contact: [ :email, :phone ]) - # # => {"contact"=>{"email"=>"none@test.com", "phone"=>"555-1234"}} + # # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"email"=>"none@test.com", "phone"=>"555-1234"} permitted: true>} permitted: true> def permit(*filters) params = self.class.new @@ -388,7 +532,7 @@ module ActionController case filter when Symbol, String permitted_scalar_filter(params, filter) - when Hash then + when Hash hash_filter(params, filter) end end @@ -401,8 +545,8 @@ module ActionController # Returns a parameter for the given +key+. If not found, # returns +nil+. # - # params = ActionController::Parameters.new(person: { name: 'Francesco' }) - # params[:person] # => {"name"=>"Francesco"} + # params = ActionController::Parameters.new(person: { name: "Francesco" }) + # params[:person] # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false> # params[:none] # => nil def [](key) convert_hashes_to_parameters(key, @parameters[key]) @@ -420,11 +564,11 @@ module ActionController # if more arguments are given, then that will be returned; if a block # is given, then that will be run and its result returned. # - # params = ActionController::Parameters.new(person: { name: 'Francesco' }) - # params.fetch(:person) # => {"name"=>"Francesco"} + # 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, 'Francesco') # => "Francesco" - # params.fetch(:none) { 'Francesco' } # => "Francesco" + # params.fetch(:none, "Francesco") # => "Francesco" + # params.fetch(:none) { "Francesco" } # => "Francesco" def fetch(key, *args) convert_value_to_parameters( @parameters.fetch(key) { @@ -457,8 +601,8 @@ module ActionController # don't exist, returns an empty hash. # # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) - # params.slice(:a, :b) # => {"a"=>1, "b"=>2} - # params.slice(:d) # => {} + # params.slice(:a, :b) # => <ActionController::Parameters {"a"=>1, "b"=>2} permitted: false> + # params.slice(:d) # => <ActionController::Parameters {} permitted: false> def slice(*keys) new_instance_with_inherited_permitted_status(@parameters.slice(*keys)) end @@ -474,8 +618,8 @@ module ActionController # filters out the given +keys+. # # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) - # params.except(:a, :b) # => {"c"=>3} - # params.except(:d) # => {"a"=>1,"b"=>2,"c"=>3} + # params.except(:a, :b) # => <ActionController::Parameters {"c"=>3} permitted: false> + # params.except(:d) # => <ActionController::Parameters {"a"=>1, "b"=>2, "c"=>3} permitted: false> def except(*keys) new_instance_with_inherited_permitted_status(@parameters.except(*keys)) end @@ -483,8 +627,8 @@ module ActionController # Removes and returns the key/value pairs matching the given keys. # # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) - # params.extract!(:a, :b) # => {"a"=>1, "b"=>2} - # params # => {"c"=>3} + # params.extract!(:a, :b) # => <ActionController::Parameters {"a"=>1, "b"=>2} permitted: false> + # params # => <ActionController::Parameters {"c"=>3} permitted: false> def extract!(*keys) new_instance_with_inherited_permitted_status(@parameters.extract!(*keys)) end @@ -494,7 +638,7 @@ module ActionController # # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) # params.transform_values { |x| x * 2 } - # # => {"a"=>2, "b"=>4, "c"=>6} + # # => <ActionController::Parameters {"a"=>2, "b"=>4, "c"=>6} permitted: false> def transform_values(&block) if block new_instance_with_inherited_permitted_status( @@ -531,12 +675,12 @@ module ActionController self end - # Deletes and returns a key-value pair from +Parameters+ whose key is equal - # to key. If the key is not found, returns the default value. If the - # optional code block is given and the key is not found, pass in the key - # and return the result of block. - def delete(key) - convert_value_to_parameters(@parameters.delete(key)) + # Deletes a key-value pair from +Parameters+ and returns the value. If + # +key+ is not found, returns +nil+ (or, with optional code block, yields + # +key+ and returns the result). Cf. +#extract!+, which returns the + # corresponding +ActionController::Parameters+ object. + def delete(key, &block) + convert_value_to_parameters(@parameters.delete(key, &block)) end # Returns a new instance of <tt>ActionController::Parameters</tt> with only @@ -545,7 +689,7 @@ module ActionController new_instance_with_inherited_permitted_status(@parameters.select(&block)) end - # Equivalent to Hash#keep_if, but returns nil if no changes were made. + # Equivalent to Hash#keep_if, but returns +nil+ if no changes were made. def select!(&block) @parameters.select!(&block) self @@ -571,27 +715,37 @@ module ActionController convert_value_to_parameters(@parameters.values_at(*keys)) end - # Returns an exact copy of the <tt>ActionController::Parameters</tt> - # instance. +permitted+ state is kept on the duped object. - # - # params = ActionController::Parameters.new(a: 1) - # params.permit! - # params.permitted? # => true - # copy_params = params.dup # => {"a"=>1} - # copy_params.permitted? # => true - def dup - super.tap do |duplicate| - duplicate.permitted = @permitted - end + # Returns a new <tt>ActionController::Parameters</tt> with all keys from + # +other_hash+ merged into current hash. + def merge(other_hash) + new_instance_with_inherited_permitted_status( + @parameters.merge(other_hash.to_h) + ) + end + + # Returns current <tt>ActionController::Parameters</tt> instance with + # +other_hash+ merged into current hash. + def merge!(other_hash) + @parameters.merge!(other_hash.to_h) + self end # Returns a new <tt>ActionController::Parameters</tt> with all keys from - # +other_hash+ merges into current hash. - def merge(other_hash) + # current hash merged into +other_hash+. + def reverse_merge(other_hash) new_instance_with_inherited_permitted_status( - @parameters.merge(other_hash) + other_hash.to_h.merge(@parameters) ) end + alias_method :with_defaults, :reverse_merge + + # Returns current <tt>ActionController::Parameters</tt> instance with + # current hash merged into +other_hash+. + def reverse_merge!(other_hash) + @parameters.merge!(other_hash.to_h) { |key, left, right| left } + self + end + alias_method :with_defaults!, :reverse_merge! # This is required by ActiveModel attribute assignment, so that user can # pass +Parameters+ to a mass assignment methods in a model. It should not @@ -604,21 +758,37 @@ module ActionController "<#{self.class} #{@parameters} permitted: #{@permitted}>" end - def method_missing(method_sym, *args, &block) - if @parameters.respond_to?(method_sym) - message = <<-DEPRECATE.squish - Method #{method_sym} is deprecated and will be removed in Rails 5.1, - as `ActionController::Parameters` no longer inherits from - hash. Using this deprecated behavior exposes potential security - problems. If you continue to use this method you may be creating - a security vulnerability in your app that can be exploited. Instead, - consider using one of these documented methods which are not - deprecated: http://api.rubyonrails.org/v#{ActionPack.version}/classes/ActionController/Parameters.html - DEPRECATE - ActiveSupport::Deprecation.warn(message) - @parameters.public_send(method_sym, *args, &block) - else - super + def self.hook_into_yaml_loading # :nodoc: + # Wire up YAML format compatibility with Rails 4.2 and Psych 2.0.8 and 2.0.9+. + # Makes the YAML parser call `init_with` when it encounters the keys below + # instead of trying its own parsing routines. + YAML.load_tags["!ruby/hash-with-ivars:ActionController::Parameters"] = name + YAML.load_tags["!ruby/hash:ActionController::Parameters"] = name + end + hook_into_yaml_loading + + def init_with(coder) # :nodoc: + case coder.tag + when "!ruby/hash:ActionController::Parameters" + # YAML 2.0.8's format where hash instance variables weren't stored. + @parameters = coder.map.with_indifferent_access + @permitted = false + when "!ruby/hash-with-ivars:ActionController::Parameters" + # YAML 2.0.9's Hash subclass format where keys and values + # were stored under an elements hash and `permitted` within an ivars hash. + @parameters = coder.map["elements"].with_indifferent_access + @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. + @parameters, @permitted = coder.map["parameters"], coder.map["permitted"] + end + end + + # Returns duplicate of object including all parameters. + def deep_dup + self.class.new(@parameters.deep_dup).tap do |duplicate| + duplicate.permitted = @permitted end end @@ -682,7 +852,7 @@ module ActionController when Parameters if object.fields_for_style? hash = object.class.new - object.each { |k,v| hash[k] = yield v } + object.each { |k, v| hash[k] = yield v } hash else yield object @@ -704,7 +874,7 @@ module ActionController end def unpermitted_keys(params) - self.keys - params.keys - self.always_permitted_parameters + keys - params.keys - always_permitted_parameters end # @@ -735,7 +905,7 @@ module ActionController ] def permitted_scalar?(value) - PERMITTED_SCALAR_TYPES.any? {|type| value.is_a?(type)} + PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) } end def permitted_scalar_filter(params, key) @@ -751,7 +921,7 @@ module ActionController end def array_of_permitted_scalars?(value) - if value.is_a?(Array) && value.all? {|element| permitted_scalar?(element)} + if value.is_a?(Array) && value.all? { |element| permitted_scalar?(element) } yield value end end @@ -761,6 +931,7 @@ module ActionController end EMPTY_ARRAY = [] + EMPTY_HASH = {} def hash_filter(params, filter) filter = filter.with_indifferent_access @@ -774,6 +945,11 @@ module ActionController array_of_permitted_scalars?(self[key]) do |val| params[key] = val end + elsif filter[key] == EMPTY_HASH + # Declaration { preferences: {} }. + if value.is_a?(Parameters) + params[key] = permit_any_in_parameters(value) + end elsif non_scalar?(value) # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }. params[key] = each_element(value) do |element| @@ -782,6 +958,43 @@ module ActionController end end end + + def permit_any_in_parameters(params) + self.class.new.tap do |sanitized| + params.each do |key, value| + case value + when ->(v) { permitted_scalar?(v) } + sanitized[key] = value + when Array + sanitized[key] = permit_any_in_array(value) + when Parameters + sanitized[key] = permit_any_in_parameters(value) + else + # Filter this one out. + end + end + end + end + + def permit_any_in_array(array) + [].tap do |sanitized| + array.each do |element| + case element + when ->(e) { permitted_scalar?(e) } + sanitized << element + when Parameters + sanitized << permit_any_in_parameters(element) + else + # Filter this one out. + end + end + end + end + + def initialize_copy(source) + super + @parameters = @parameters.dup + end end # == Strong \Parameters @@ -792,7 +1005,7 @@ module ActionController # whitelisted. # # In addition, parameters can be marked as required and flow through a - # predefined raise/rescue flow to end up as a 400 Bad Request with no + # predefined raise/rescue flow to end up as a <tt>400 Bad Request</tt> with no # effort. # # class PeopleController < ActionController::Base @@ -805,7 +1018,7 @@ module ActionController # end # # # This will pass with flying colors as long as there's a person key in the - # # parameters, otherwise it'll raise an ActionController::MissingParameter + # # parameters, otherwise it'll raise an ActionController::ParameterMissing # # exception, which will get caught by ActionController::Base and turned # # into a 400 Bad Request reply. # def update @@ -816,7 +1029,7 @@ module ActionController # # private # # Using a private method to encapsulate the permissible parameters is - # # just a good pattern since you'll be able to reuse the same permit + # # a good pattern since you'll be able to reuse the same permit # # list between create and update. Also, you can specialize this method # # with per-user checking of permissible attributes. # def person_params diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index ac37b00010..6e8a95040f 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionController module Testing extend ActiveSupport::Concern @@ -10,11 +12,5 @@ module ActionController self.params = nil end end - - module ClassMethods - def before_filters - _process_action_callbacks.find_all{|x| x.kind == :before}.map(&:name) - end - end end end diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index dbf7241a14..84dbb59a63 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module ActionController # Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing # the <tt>_routes</tt> method. Otherwise, an exception will be raised. # # In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define - # url options like the +host+. In order to do so, this module requires the host class + # URL options like the +host+. In order to do so, this module requires the host class # to implement +env+ which needs to be Rack-compatible and +request+ # which is either an instance of +ActionDispatch::Request+ or an object # that responds to the +host+, +optional_port+, +protocol+ and @@ -27,10 +29,10 @@ module ActionController def url_options @_url_options ||= { - :host => request.host, - :port => request.optional_port, - :protocol => request.protocol, - :_recall => request.path_parameters + host: request.host, + port: request.optional_port, + protocol: request.protocol, + _recall: request.path_parameters }.merge!(super).freeze if (same_origin = _routes.equal?(request.routes)) || diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 28b20052b5..7d42f5d931 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rails" require "action_controller" require "action_dispatch/railtie" @@ -11,7 +13,7 @@ module ActionController config.eager_load_namespaces << ActionController - initializer "action_controller.assets_config", :group => :all do |app| + initializer "action_controller.assets_config", group: :all do |app| app.config.action_controller.assets_dir ||= app.config.paths["public"].first end @@ -22,13 +24,15 @@ module ActionController initializer "action_controller.parameters_config" do |app| options = app.config.action_controller - ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false } - if app.config.action_controller[:always_permitted_parameters] - ActionController::Parameters.always_permitted_parameters = - app.config.action_controller.delete(:always_permitted_parameters) - end - ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do - (Rails.env.test? || Rails.env.development?) ? :log : false + ActiveSupport.on_load(:action_controller, run_once: true) do + ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false } + if app.config.action_controller[:always_permitted_parameters] + ActionController::Parameters.always_permitted_parameters = + app.config.action_controller.delete(:always_permitted_parameters) + end + ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do + (Rails.env.test? || Rails.env.development?) ? :log : false + end end end @@ -42,7 +46,7 @@ module ActionController options.javascripts_dir ||= paths["public/javascripts"].first options.stylesheets_dir ||= paths["public/stylesheets"].first - # Ensure readers methods get compiled + # Ensure readers methods get compiled. options.asset_host ||= app.config.asset_host options.relative_url_root ||= app.config.relative_url_root @@ -51,7 +55,7 @@ module ActionController extend ::AbstractController::Railties::RoutesHelpers.with(app.routes) extend ::ActionController::Railties::Helpers - options.each do |k,v| + options.each do |k, v| k = "#{k}=" if respond_to?(k) send(k, v) @@ -67,5 +71,19 @@ module ActionController config.compile_methods! if config.respond_to?(:compile_methods!) end end + + initializer "action_controller.request_forgery_protection" do |app| + ActiveSupport.on_load(:action_controller_base) do + if app.config.action_controller.default_protect_from_forgery + protect_from_forgery with: :exception + end + end + end + + initializer "action_controller.eager_load_actions" do + ActiveSupport.on_load(:after_initialize) do + ActionController::Metal.descendants.each(&:action_methods) if config.eager_load + end + end end end diff --git a/actionpack/lib/action_controller/railties/helpers.rb b/actionpack/lib/action_controller/railties/helpers.rb index 3985c6b273..fa746fa9e8 100644 --- a/actionpack/lib/action_controller/railties/helpers.rb +++ b/actionpack/lib/action_controller/railties/helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionController module Railties module Helpers diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb index a8c8d66682..49c5b782f0 100644 --- a/actionpack/lib/action_controller/renderer.rb +++ b/actionpack/lib/action_controller/renderer.rb @@ -1,11 +1,13 @@ -require 'active_support/core_ext/hash/keys' +# 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. # # You get a concrete renderer class by invoking ActionController::Base#renderer. - # For example, + # For example: # # ApplicationController.renderer # @@ -18,7 +20,7 @@ module ActionController # ApplicationController.render template: '...' # # #render allows you to use the same options that you can use when rendering in a controller. - # For example, + # For example: # # FooController.render :action, locals: { ... }, assigns: { ... } # @@ -37,11 +39,11 @@ module ActionController attr_reader :defaults, :controller DEFAULTS = { - http_host: 'example.org', + http_host: "example.org", https: false, - method: 'get', - script_name: '', - input: '' + method: "get", + script_name: "", + input: "" }.freeze # Create a new renderer instance for a specific controller class. @@ -56,11 +58,12 @@ module ActionController # Create a new renderer for the same controller but with new defaults. def with_defaults(defaults) - self.class.new controller, env, self.defaults.merge(defaults) + self.class.new controller, @env, self.defaults.merge(defaults) end # Accepts a custom Rack environment to render templates in. - # It will be merged with ActionController::Renderer.defaults + # It will be merged with the default Rack environment defined by + # +ActionController::Renderer::DEFAULTS+. def initialize(controller, env, defaults) @controller = controller @defaults = defaults @@ -69,7 +72,7 @@ module ActionController # Render templates with any options from ActionController::Base#render_to_string. def render(*args) - raise 'missing controller' unless controller + raise "missing controller" unless controller request = ActionDispatch::Request.new @env request.routes = controller._routes @@ -83,26 +86,29 @@ module ActionController private def normalize_keys(env) new_env = {} - env.each_pair { |k,v| new_env[rack_key_for(k)] = rack_value_for(k, v) } + env.each_pair { |k, v| new_env[rack_key_for(k)] = rack_value_for(k, v) } + new_env["rack.url_scheme"] = new_env["HTTPS"] == "on" ? "https" : "http" new_env end RACK_KEY_TRANSLATION = { - http_host: 'HTTP_HOST', - https: 'HTTPS', - method: 'REQUEST_METHOD', - script_name: 'SCRIPT_NAME', - input: 'rack.input' + http_host: "HTTP_HOST", + https: "HTTPS", + method: "REQUEST_METHOD", + script_name: "SCRIPT_NAME", + input: "rack.input" } IDENTITY = ->(_) { _ } RACK_VALUE_TRANSLATION = { - https: ->(v) { v ? 'on' : 'off' }, + https: ->(v) { v ? "on" : "off" }, method: ->(v) { v.upcase }, } - def rack_key_for(key); RACK_KEY_TRANSLATION[key]; end + def rack_key_for(key) + RACK_KEY_TRANSLATION.fetch(key, key.to_s) + end def rack_value_for(key, value) RACK_VALUE_TRANSLATION.fetch(key, IDENTITY).call value diff --git a/actionpack/lib/action_controller/template_assertions.rb b/actionpack/lib/action_controller/template_assertions.rb index 0179f4afcd..dd83c1a283 100644 --- a/actionpack/lib/action_controller/template_assertions.rb +++ b/actionpack/lib/action_controller/template_assertions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionController module TemplateAssertions def assert_template(options = {}, message = nil) diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index b1b3e87934..4b408750a4 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -1,23 +1,26 @@ -require 'rack/session/abstract/id' -require 'active_support/core_ext/hash/conversions' -require 'active_support/core_ext/object/to_query' -require 'active_support/core_ext/module/anonymous' -require 'active_support/core_ext/hash/keys' -require 'action_controller/template_assertions' -require 'rails-dom-testing' +# frozen_string_literal: true + +require "rack/session/abstract/id" +require "active_support/core_ext/hash/conversions" +require "active_support/core_ext/object/to_query" +require "active_support/core_ext/module/anonymous" +require "active_support/core_ext/module/redefine_method" +require "active_support/core_ext/hash/keys" +require "active_support/testing/constant_lookup" +require "action_controller/template_assertions" +require "rails-dom-testing" module ActionController - # :stopdoc: class Metal include Testing::Functional end module Live - # Disable controller / rendering threads in tests. User tests can access + # Disable controller / rendering threads in tests. User tests can access # the database on the main thread, so they could open a txn, then the # controller thread will open a new connection and try to access data - # that's only visible to the main thread's txn. This is the problem in #23483 - remove_method :new_controller_thread + # that's only visible to the main thread's txn. This is the problem in #23483. + silence_redefinition_of_method :new_controller_thread def new_controller_thread # :nodoc: yield end @@ -27,18 +30,20 @@ module ActionController # Please use ActionDispatch::IntegrationTest going forward. class TestRequest < ActionDispatch::TestRequest #:nodoc: DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup - DEFAULT_ENV.delete 'PATH_INFO' + DEFAULT_ENV.delete "PATH_INFO" def self.new_session TestSession.new end - # Create a new test request with default `env` values - def self.create + attr_reader :controller_class + + # Create a new test request with default `env` values. + def self.create(controller_class) env = {} env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application env["rack.request.cookie_hash"] = {}.with_indifferent_access - new(default_env.merge(env), new_session) + new(default_env.merge(env), new_session, controller_class) end def self.default_env @@ -46,13 +51,14 @@ module ActionController end private_class_method :default_env - def initialize(env, session) + def initialize(env, session, controller_class) super(env) self.session = session - self.session_options = TestSession::DEFAULT_OPTIONS + self.session_options = TestSession::DEFAULT_OPTIONS.dup + @controller_class = controller_class @custom_param_parsers = { - xml: lambda { |raw_post| Hash.from_xml(raw_post)['hash'] } + xml: lambda { |raw_post| Hash.from_xml(raw_post)["hash"] } } end @@ -61,7 +67,7 @@ module ActionController end def content_type=(type) - set_header 'CONTENT_TYPE', type + set_header "CONTENT_TYPE", type end def assign_parameters(routes, controller_path, action, parameters, generated_path, query_string_keys) @@ -83,7 +89,7 @@ module ActionController end if get? - if self.query_string.blank? + if query_string.blank? self.query_string = non_path_parameters.to_query end else @@ -91,8 +97,8 @@ module ActionController self.content_type = ENCODER.content_type data = ENCODER.build_multipart non_path_parameters else - fetch_header('CONTENT_TYPE') do |k| - set_header k, 'application/x-www-form-urlencoded' + fetch_header("CONTENT_TYPE") do |k| + set_header k, "application/x-www-form-urlencoded" end case content_mime_type.to_sym @@ -110,8 +116,9 @@ module ActionController end end - set_header 'CONTENT_LENGTH', data.length.to_s - set_header 'rack.input', StringIO.new(data) + data_stream = StringIO.new(data) + set_header "CONTENT_LENGTH", data_stream.length.to_s + set_header "rack.input", data_stream end fetch_header("PATH_INFO") do |k| @@ -127,7 +134,7 @@ module ActionController include Rack::Test::Utils def should_multipart?(params) - # FIXME: lifted from Rack-Test. We should push this separation upstream + # FIXME: lifted from Rack-Test. We should push this separation upstream. multipart = false query = lambda { |value| case value @@ -152,9 +159,9 @@ module ActionController private - def params_parsers - super.merge @custom_param_parsers - end + def params_parsers + super.merge @custom_param_parsers + end end class LiveTestResponse < Live::Response @@ -208,10 +215,18 @@ module ActionController end # Superclass for ActionController functional tests. Functional tests allow you to - # test a single controller action per test method. This should not be confused with - # integration tests (see ActionDispatch::IntegrationTest), which are more like - # "stories" that can involve multiple controllers and multiple actions (i.e. multiple - # different HTTP requests). + # test a single controller action per test method. + # + # == Use integration style controller tests over functional style controller tests. + # + # Rails discourages the use of functional tests in favor of integration tests + # (use ActionDispatch::IntegrationTest). + # + # New Rails applications no longer generate functional style controller tests and they should + # only be used for backward compatibility. Integration style controller tests perform actual + # requests, whereas functional style controller tests merely simulate a request. Besides, + # integration tests are as fast as functional tests and provide lot of helpers such as +as+, + # +parsed_body+ for effective testing of controller actions including even API endpoints. # # == Basic example # @@ -288,7 +303,7 @@ module ActionController # assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave" # assert flash.empty? # makes sure that there's nothing in the flash # - # On top of the collections, you have the complete url that a given action redirected to available in <tt>redirect_to_url</tt>. + # On top of the collections, you have the complete URL that a given action redirected to available in <tt>redirect_to_url</tt>. # # For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another # action call which can then be asserted against. @@ -320,7 +335,6 @@ module ActionController attr_reader :response, :request module ClassMethods - # Sets the controller class name. Useful if the name can't be inferred from test class. # Normalizes +controller_class+ before using. # @@ -343,7 +357,7 @@ module ActionController end def controller_class - if current_controller_class = self._controller_class + if current_controller_class = _controller_class current_controller_class else self.controller_class = determine_default_controller_class(name) @@ -377,56 +391,41 @@ module ActionController # # Note that the request method is not verified. The different methods are # available to make the tests more expressive. - def get(action, *args) - res = process_with_kwargs("GET", action, *args) + def get(action, **args) + res = process(action, method: "GET", **args) cookies.update res.cookies res end # Simulate a POST request with the given parameters and set/volley the response. # See +get+ for more details. - def post(action, *args) - process_with_kwargs("POST", action, *args) + def post(action, **args) + process(action, method: "POST", **args) end # Simulate a PATCH request with the given parameters and set/volley the response. # See +get+ for more details. - def patch(action, *args) - process_with_kwargs("PATCH", action, *args) + def patch(action, **args) + process(action, method: "PATCH", **args) end # Simulate a PUT request with the given parameters and set/volley the response. # See +get+ for more details. - def put(action, *args) - process_with_kwargs("PUT", action, *args) + def put(action, **args) + process(action, method: "PUT", **args) end # Simulate a DELETE request with the given parameters and set/volley the response. # See +get+ for more details. - def delete(action, *args) - process_with_kwargs("DELETE", action, *args) + def delete(action, **args) + process(action, method: "DELETE", **args) end # Simulate a HEAD request with the given parameters and set/volley the response. # See +get+ for more details. - def head(action, *args) - process_with_kwargs("HEAD", action, *args) - end - - def xml_http_request(*args) - ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc) - xhr and xml_http_request methods are deprecated in favor of - `get :index, xhr: true` and `post :create, xhr: true` - MSG - - @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - @request.env['HTTP_ACCEPT'] ||= [Mime[:js], Mime[:html], Mime[:xml], 'text/xml', '*/*'].join(', ') - __send__(*args).tap do - @request.env.delete 'HTTP_X_REQUESTED_WITH' - @request.env.delete 'HTTP_ACCEPT' - end + def head(action, **args) + process(action, method: "HEAD", **args) end - alias xhr :xml_http_request # Simulate an HTTP request to +action+ by specifying request method, # parameters and set/volley the response. @@ -440,6 +439,8 @@ module ActionController # - +session+: A hash of parameters to store in the session. This may be +nil+. # - +flash+: A hash of parameters to store in the flash. This may be +nil+. # - +format+: Request format. Defaults to +nil+. Can be string or symbol. + # - +as+: Content type. Defaults to +nil+. Must be a symbol that corresponds + # to a mime type. # # Example calling +create+ action and sending two params: # @@ -456,56 +457,39 @@ module ActionController # respectively which will make tests more expressive. # # Note that the request method is not verified. - def process(action, *args) + def process(action, method: "GET", params: {}, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil) check_required_ivars - if kwarg_request?(args) - parameters, session, body, flash, http_method, format, xhr = args[0].values_at(:params, :session, :body, :flash, :method, :format, :xhr) - else - http_method, parameters, session, flash = args - format = nil - - if parameters.is_a?(String) && http_method != 'HEAD' - body = parameters - parameters = nil - end - - if parameters || session || flash - non_kwarg_request_warning - end - end - if body - @request.set_header 'RAW_POST_DATA', body - end - - if http_method - http_method = http_method.to_s.upcase - else - http_method = "GET" + @request.set_header "RAW_POST_DATA", body end - parameters ||= {} - - if format - parameters[:format] = format - end + http_method = method.to_s.upcase @html_document = nil - self.cookies.update @request.cookies - self.cookies.update_cookies_from_jar - @request.set_header 'HTTP_COOKIE', cookies.to_header - @request.delete_header 'action_dispatch.cookies' + cookies.update(@request.cookies) + cookies.update_cookies_from_jar + @request.set_header "HTTP_COOKIE", cookies.to_header + @request.delete_header "action_dispatch.cookies" - @request = TestRequest.new scrub_env!(@request.env), @request.session + @request = TestRequest.new scrub_env!(@request.env), @request.session, @controller.class @response = build_response @response_klass @response.request = @request @controller.recycle! - @request.set_header 'REQUEST_METHOD', http_method + @request.set_header "REQUEST_METHOD", http_method - parameters = parameters.symbolize_keys + if as + @request.content_type = Mime[as].to_s + format ||= as + end + + parameters = params.symbolize_keys + + if format + parameters[:format] = format + end generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action.to_s)) generated_path = generated_path(generated_extras) @@ -517,9 +501,9 @@ module ActionController @request.flash.update(flash || {}) if xhr - @request.set_header 'HTTP_X_REQUESTED_WITH', 'XMLHttpRequest' - @request.fetch_header('HTTP_ACCEPT') do |k| - @request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], 'text/xml', '*/*'].join(', ') + @request.set_header "HTTP_X_REQUESTED_WITH", "XMLHttpRequest" + @request.fetch_header("HTTP_ACCEPT") do |k| + @request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ") end end @@ -534,27 +518,25 @@ module ActionController @request = @controller.request @response = @controller.response - @request.delete_header 'HTTP_COOKIE' - if @request.have_cookie_jar? unless @request.cookie_jar.committed? @request.cookie_jar.write(@response) - self.cookies.update(@request.cookie_jar.instance_variable_get(:@cookies)) + cookies.update(@request.cookie_jar.instance_variable_get(:@cookies)) end end @response.prepare! if flash_value = @request.flash.to_session_value - @request.session['flash'] = flash_value + @request.session["flash"] = flash_value else - @request.session.delete('flash') + @request.session.delete("flash") end if xhr - @request.delete_header 'HTTP_X_REQUESTED_WITH' - @request.delete_header 'HTTP_ACCEPT' + @request.delete_header "HTTP_X_REQUESTED_WITH" + @request.delete_header "HTTP_ACCEPT" end - @request.query_string = '' + @request.query_string = "" @response.sent! end @@ -592,7 +574,7 @@ module ActionController end end - @request = TestRequest.create + @request = TestRequest.create(@controller.class) @response = build_response @response_klass @response.request = @request @@ -611,71 +593,35 @@ module ActionController include ActionDispatch::Assertions class_attribute :_controller_class setup :setup_controller_request_and_response + ActiveSupport.run_load_hooks(:action_controller_test_case, self) end private - def scrub_env!(env) - env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } - env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } - env.delete 'action_dispatch.request.query_parameters' - env.delete 'action_dispatch.request.request_parameters' - env - end - - def process_with_kwargs(http_method, action, *args) - if kwarg_request?(args) - args.first.merge!(method: http_method) - process(action, *args) - else - non_kwarg_request_warning if args.any? - - args = args.unshift(http_method) - process(action, *args) + def scrub_env!(env) + env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } + env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } + env.delete "action_dispatch.request.query_parameters" + env.delete "action_dispatch.request.request_parameters" + env["rack.input"] = StringIO.new + env end - end - REQUEST_KWARGS = %i(params session flash method body xhr) - def kwarg_request?(args) - args[0].respond_to?(:keys) && ( - (args[0].key?(:format) && args[0].keys.size == 1) || - args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) } - ) - end - - def non_kwarg_request_warning - ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc) - ActionController::TestCase HTTP request methods will accept only - keyword arguments in future Rails versions. - - Examples: - - get :show, params: { id: 1 }, session: { user_id: 1 } - process :update, method: :post, params: { id: 1 } - MSG - end - - def document_root_element - html_document.root - end + def document_root_element + html_document.root + end - def check_required_ivars - # Sanity check for required instance variables so we can give an - # understandable error message. - [:@routes, :@controller, :@request, :@response].each do |iv_name| - if !instance_variable_defined?(iv_name) || instance_variable_get(iv_name).nil? - raise "#{iv_name} is nil: make sure you set it in your test's setup method." + def check_required_ivars + # Sanity check for required instance variables so we can give an + # understandable error message. + [:@routes, :@controller, :@request, :@response].each do |iv_name| + if !instance_variable_defined?(iv_name) || instance_variable_get(iv_name).nil? + raise "#{iv_name} is nil: make sure you set it in your test's setup method." + end end end - end - - def html_format?(parameters) - return true unless parameters.key?(:format) - Mime.fetch(parameters[:format]) { Mime['html'] }.html? - end end include Behavior end - # :startdoc: end |