diff options
Diffstat (limited to 'actionpack/lib')
39 files changed, 515 insertions, 326 deletions
diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index c95b9a4097..784092867c 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -88,7 +88,7 @@ module AbstractController # Returns the full controller name, underscored, without the ending Controller. # # class MyApp::MyPostsController < AbstractController::Base - # end + # # end # # MyApp::MyPostsController.controller_path # => "my_app/my_posts" @@ -96,7 +96,7 @@ module AbstractController # ==== Returns # * <tt>String</tt> def controller_path - @controller_path ||= name.sub(/Controller$/, '').underscore unless anonymous? + @controller_path ||= name.sub(/Controller$/, ''.freeze).underscore unless anonymous? end # Refresh the cached action_methods when a new action_method is added. @@ -148,9 +148,6 @@ module AbstractController # # ==== Parameters # * <tt>action_name</tt> - The name of an action to be tested - # - # ==== Returns - # * <tt>TrueClass</tt>, <tt>FalseClass</tt> def available_action?(action_name) _find_action_name(action_name).present? end @@ -171,9 +168,6 @@ module AbstractController # ==== Parameters # * <tt>name</tt> - The name of an action to be tested # - # ==== Returns - # * <tt>TrueClass</tt>, <tt>FalseClass</tt> - # # :api: private def action_method?(name) self.class.action_methods.include?(name) diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 109eff10eb..d84c238a62 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -181,7 +181,7 @@ module AbstractController end def default_helper_module! - module_name = name.sub(/Controller$/, '') + module_name = name.sub(/Controller$/, ''.freeze) module_path = module_name.underscore helper module_path rescue LoadError => e diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index de85e0c1a7..a4e4992cfe 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -8,7 +8,7 @@ module ActionController # # You can read more about each approach by clicking the modules below. # - # Note: To turn off all caching, set + # Note: To turn off all caching provided by Action Controller, set # config.action_controller.perform_caching = false # # == \Caching stores diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 87609d8aa7..4c9f14e409 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -25,7 +25,7 @@ module ActionController 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 << " (#{additions.join(" | ")})" unless additions.blank? + message << " (#{additions.join(" | ".freeze)})" unless additions.blank? message end end diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 1abd8d3a33..e6d7f958bb 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -85,6 +85,10 @@ module ActionController #:nodoc: @to_path = path end + def body + File.binread(to_path) + end + # Stream the file's contents if Rack::Sendfile isn't present. def each File.open(to_path, 'rb') do |file| @@ -126,7 +130,7 @@ module ActionController #:nodoc: # See +send_file+ for more information on HTTP Content-* headers and caching. def send_data(data, options = {}) #:doc: send_file_headers! options - render options.slice(:status, :content_type).merge(:text => data) + render options.slice(:status, :content_type).merge(body: data) end private diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index d920668184..e31d65aac2 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -55,10 +55,10 @@ module ActionController # You can pass any of the following options to affect the before_action callback # * <tt>only</tt> - The callback should be run only for this action # * <tt>except</tt> - The callback should be run for all actions except this action - # * <tt>if</tt> - A symbol naming an instance method or a proc; the callback - # will be called only when it returns a true value. - # * <tt>unless</tt> - A symbol naming an instance method or a proc; the callback - # will be called only when it returns a false value. + # * <tt>if</tt> - A symbol naming an instance method or a proc; the + # callback will be called only when it returns a true value. + # * <tt>unless</tt> - A symbol naming an instance method or a proc; the + # callback will be called only when it returns a false value. def force_ssl(options = {}) action_options = options.slice(*ACTION_OPTIONS) redirect_options = options.except(*ACTION_OPTIONS) @@ -71,8 +71,8 @@ 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 & redirect options - # available to the <tt>force_ssl</tt> method. + # * <tt>host_or_options</tt> - Either a host name or any of the url & + # redirect options available to the <tt>force_ssl</tt> method. def force_ssl_redirect(host_or_options = nil) unless request.ssl? options = { diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index b4da381d26..fcaf3e6425 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -73,7 +73,7 @@ module ActionController # Provides a proxy to access helpers methods from outside the view. def helpers - @helper_proxy ||= begin + @helper_proxy ||= begin proxy = ActionView::Base.new proxy.config = config.inheritable_copy proxy.extend(_helpers) @@ -100,7 +100,7 @@ module ActionController def all_helpers_from_path(path) helpers = Array(path).flat_map do |_path| extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ - names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } + names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1'.freeze) } names.sort! end helpers.uniq! diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 39bed955a4..032275ac64 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -502,7 +502,7 @@ module ActionController def authentication_request(controller, realm, message = nil) message ||= "HTTP Token: Access denied.\n" controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"'.freeze, "".freeze)}") - controller.__send__ :render, :text => message, :status => :unauthorized + controller.__send__ :render, plain: message, status: :unauthorized end end end diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index d66b2214ce..17fcc2fa02 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -3,12 +3,27 @@ module ActionController include BasicImplicitRender + # Renders the template corresponding to the controller action, if it exists. + # The action name, format, and variant are all taken into account. + # For example, the "new" action with an HTML format and variant "phone" + # would try to render the <tt>new.html+phone.erb</tt> template. + # + # If no template is found <tt>ActionController::BasicImplicitRender</tt>'s implementation is called, unless + # a block is passed. In that case, it will override the super implementation. + # + # default_render do + # head 404 # No template was found + # end def default_render(*args) if template_exists?(action_name.to_s, _prefixes, variants: request.variant) render(*args) else - logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger - super + if block_given? + yield(*args) + else + logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger + super + end end end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index fab1be3459..1db68db20f 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -156,16 +156,16 @@ module ActionController #:nodoc: # It works for both inline: # # respond_to do |format| - # format.html.any { render text: "any" } - # format.html.phone { render text: "phone" } + # format.html.any { render html: "any" } + # format.html.phone { render html: "phone" } # end # # and block syntax: # # respond_to do |format| # format.html do |variant| - # variant.any(:tablet, :phablet){ render text: "any" } - # variant.phone { render text: "phone" } + # variant.any(:tablet, :phablet){ render html: "any" } + # variant.phone { render html: "phone" } # end # end # diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index cdfc523bd4..e680432127 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -4,8 +4,8 @@ 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 submit - # POST requests without having to specify any root elements. + # Wraps the parameters hash into a nested hash. This will allow clients to + # submit requests without having to specify any root elements. # # This functionality is enabled in +config/initializers/wrap_parameters.rb+ # and can be customized. @@ -14,7 +14,7 @@ module ActionController # a non-empty array: # # class UsersController < ApplicationController - # wrap_parameters format: [:json, :xml] + # wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form] # end # # If you enable +ParamsWrapper+ for +:json+ format, instead of having to diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 45d3962494..cb74c4f0d4 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -94,7 +94,7 @@ module ActionController # This method is the opposite of add method. # - # Usage: + # To remove a csv renderer: # # ActionController::Renderers.remove(:csv) def self.remove(key) diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index b9ae8dd5ea..a3b0645dc0 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -1,3 +1,6 @@ +require 'active_support/deprecation' +require 'active_support/core_ext/string/filters' + module ActionController module Rendering extend ActiveSupport::Concern @@ -74,6 +77,17 @@ module ActionController 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 + end + if options[:html] options[:html] = ERB::Util.html_escape(options[:html]) end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 4cb634477e..9e09242872 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -20,7 +20,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_form_forgery` method in our controllers. + # the `protect_from_forgery` 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 diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index c98e937423..e78f1f0d7e 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -11,9 +11,9 @@ module ActionController # # params = ActionController::Parameters.new(a: {}) # params.fetch(:b) - # # => ActionController::ParameterMissing: param not found: b + # # => ActionController::ParameterMissing: param is missing or the value is empty: b # params.require(:a) - # # => ActionController::ParameterMissing: param not found: a + # # => ActionController::ParameterMissing: param is missing or the value is empty: a class ParameterMissing < KeyError attr_reader :param # :nodoc: @@ -23,11 +23,13 @@ module ActionController end end - # Raised when a supplied parameter is not expected. + # Raised when a supplied parameter is not expected and + # ActionController::Parameters.action_on_unpermitted_parameters + # is set to <tt>:raise</tt>. # # params = ActionController::Parameters.new(a: "123", b: "456") # params.permit(:c) - # # => ActionController::UnpermittedParameters: found unexpected keys: a, b + # # => ActionController::UnpermittedParameters: found unpermitted parameters: a, b class UnpermittedParameters < IndexError attr_reader :params # :nodoc: @@ -102,10 +104,12 @@ module ActionController # params = ActionController::Parameters.new(key: 'value') # params[:key] # => "value" # params["key"] # => "value" - class Parameters < ActiveSupport::HashWithIndifferentAccess + class Parameters cattr_accessor :permit_all_parameters, instance_accessor: false cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false + delegate :keys, :key?, :has_key?, :empty?, :inspect, to: :@parameters + # By default, never raise an UnpermittedParameters exception if these # params are present. The default includes both 'controller' and 'action' # because they are added by Rails and should be of no concern. One way @@ -142,11 +146,22 @@ module ActionController # params = ActionController::Parameters.new(name: 'Francesco') # params.permitted? # => true # Person.new(params) # => #<Person id: nil, name: "Francesco"> - def initialize(attributes = nil) - super(attributes) + def initialize(parameters = {}) + @parameters = parameters.with_indifferent_access @permitted = self.class.permit_all_parameters end + # Returns true if another +Parameters+ object contains the same content and + # permitted flag, or other Hash-like object contains the same content. This + # override is in place so you can perform a comparison with `Hash`. + def ==(other_hash) + if other_hash.respond_to?(:permitted?) + super + else + @parameters == other_hash + end + end + # Returns a safe +Hash+ representation of this parameter with all # unpermitted keys removed. # @@ -160,7 +175,7 @@ module ActionController # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"} def to_h if permitted? - to_hash + @parameters.to_h else slice(*self.class.always_permitted_parameters).permit!.to_h end @@ -168,20 +183,17 @@ module ActionController # Returns an unsafe, unfiltered +Hash+ representation of this parameter. def to_unsafe_h - to_hash + @parameters.to_h end alias_method :to_unsafe_hash, :to_unsafe_h # Convert all hashes in values into parameters, then yield each pair like # the same way as <tt>Hash#each_pair</tt> def each_pair(&block) - super do |key, value| - convert_hashes_to_parameters(key, value) + @parameters.each_pair do |key, value| + yield key, convert_hashes_to_parameters(key, value) end - - super end - alias_method :each, :each_pair # Attribute that keeps track of converted arrays, if any, to avoid double @@ -236,10 +248,10 @@ module ActionController # # => {"name"=>"Francesco"} # # ActionController::Parameters.new(person: nil).require(:person) - # # => ActionController::ParameterMissing: param not found: person + # # => ActionController::ParameterMissing: param is missing or the value is empty: person # # ActionController::Parameters.new(person: {}).require(:person) - # # => ActionController::ParameterMissing: param not found: person + # # => ActionController::ParameterMissing: param is missing or the value is empty: person def require(key) value = self[key] if value.present? || value == false @@ -345,7 +357,13 @@ module ActionController # params[:person] # => {"name"=>"Francesco"} # params[:none] # => nil def [](key) - convert_hashes_to_parameters(key, super) + convert_hashes_to_parameters(key, @parameters[key]) + end + + # Assigns a value to a given +key+. The given key may still get filtered out + # when +permit+ is called. + def []=(key, value) + @parameters[key] = value end # Returns a parameter for the given +key+. If the +key+ @@ -356,13 +374,19 @@ module ActionController # # params = ActionController::Parameters.new(person: { name: 'Francesco' }) # params.fetch(:person) # => {"name"=>"Francesco"} - # params.fetch(:none) # => ActionController::ParameterMissing: param not found: none + # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none # params.fetch(:none, 'Francesco') # => "Francesco" # params.fetch(:none) { 'Francesco' } # => "Francesco" - def fetch(key, *args) - convert_hashes_to_parameters(key, super, false) - rescue KeyError - raise ActionController::ParameterMissing.new(key) + def fetch(key, *args, &block) + convert_value_to_parameters( + @parameters.fetch(key) { + if block_given? + yield + else + args.fetch(0) { raise ActionController::ParameterMissing.new(key) } + end + } + ) end # Returns a new <tt>ActionController::Parameters</tt> instance that @@ -373,7 +397,24 @@ module ActionController # params.slice(:a, :b) # => {"a"=>1, "b"=>2} # params.slice(:d) # => {} def slice(*keys) - new_instance_with_inherited_permitted_status(super) + new_instance_with_inherited_permitted_status(@parameters.slice(*keys)) + end + + # Returns current <tt>ActionController::Parameters</tt> instance which + # contains only the given +keys+. + def slice!(*keys) + @parameters.slice!(*keys) + self + end + + # Returns a new <tt>ActionController::Parameters</tt> instance that + # 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} + def except(*keys) + new_instance_with_inherited_permitted_status(@parameters.except(*keys)) end # Removes and returns the key/value pairs matching the given keys. @@ -382,7 +423,7 @@ module ActionController # params.extract!(:a, :b) # => {"a"=>1, "b"=>2} # params # => {"c"=>3} def extract!(*keys) - new_instance_with_inherited_permitted_status(super) + new_instance_with_inherited_permitted_status(@parameters.extract!(*keys)) end # Returns a new <tt>ActionController::Parameters</tt> with the results of @@ -391,36 +432,80 @@ module ActionController # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) # params.transform_values { |x| x * 2 } # # => {"a"=>2, "b"=>4, "c"=>6} - def transform_values - if block_given? - new_instance_with_inherited_permitted_status(super) + def transform_values(&block) + if block + new_instance_with_inherited_permitted_status( + @parameters.transform_values(&block) + ) else - super + @parameters.transform_values end end - # This method is here only to make sure that the returned object has the - # correct +permitted+ status. It should not matter since the parent of - # this object is +HashWithIndifferentAccess+ - def transform_keys # :nodoc: - if block_given? - new_instance_with_inherited_permitted_status(super) + # Performs values transformation and returns the altered + # <tt>ActionController::Parameters</tt> instance. + def transform_values!(&block) + @parameters.transform_values!(&block) + self + end + + # Returns a new <tt>ActionController::Parameters</tt> instance with the + # results of running +block+ once for every key. The values are unchanged. + def transform_keys(&block) + if block + new_instance_with_inherited_permitted_status( + @parameters.transform_keys(&block) + ) else - super + @parameters.transform_keys end end + # Performs keys transfomration and returns the altered + # <tt>ActionController::Parameters</tt> instance. + def transform_keys!(&block) + @parameters.transform_keys!(&block) + 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, &block) - convert_hashes_to_parameters(key, super, false) + convert_value_to_parameters(@parameters.delete(key)) + end + + # Returns a new instance of <tt>ActionController::Parameters</tt> with only + # items that the block evaluates to true. + def select(&block) + new_instance_with_inherited_permitted_status(@parameters.select(&block)) end # Equivalent to Hash#keep_if, but returns nil if no changes were made. def select!(&block) - convert_value_to_parameters(super) + @parameters.select!(&block) + self + end + alias_method :keep_if, :select! + + # Returns a new instance of <tt>ActionController::Parameters</tt> with items + # that the block evaluates to true removed. + def reject(&block) + new_instance_with_inherited_permitted_status(@parameters.reject(&block)) + end + + # Removes items that the block evaluates to true and returns self. + def reject!(&block) + @parameters.reject!(&block) + self + end + alias_method :delete_if, :reject! + + # Return values that were assigned to the given +keys+. Note that all the + # +Hash+ objects will be converted to <tt>ActionController::Parameters</tt>. + def values_at(*keys) + convert_value_to_parameters(@parameters.values_at(*keys)) end # Returns an exact copy of the <tt>ActionController::Parameters</tt> @@ -437,11 +522,30 @@ module ActionController end end + # Returns a new <tt>ActionController::Parameters</tt> with all keys from + # +other_hash+ merges into current hash. + def merge(other_hash) + new_instance_with_inherited_permitted_status( + @parameters.merge(other_hash) + ) + end + + # This is required by ActiveModel attribute assignment, so that user can + # pass +Parameters+ to a mass assignment methods in a model. It should not + # matter as we are using +HashWithIndifferentAccess+ internally. + def stringify_keys # :nodoc: + dup + end + protected def permitted=(new_permitted) @permitted = new_permitted end + def fields_for_style? + @parameters.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) } + end + private def new_instance_with_inherited_permitted_status(hash) self.class.new(hash).tap do |new_instance| @@ -449,40 +553,41 @@ module ActionController end end - def convert_hashes_to_parameters(key, value, assign_if_converted=true) + def convert_hashes_to_parameters(key, value) converted = convert_value_to_parameters(value) - self[key] = converted if assign_if_converted && !converted.equal?(value) + @parameters[key] = converted unless converted.equal?(value) converted end def convert_value_to_parameters(value) - if value.is_a?(Array) && !converted_arrays.member?(value) + case value + when Array + return value if converted_arrays.member?(value) converted = value.map { |_| convert_value_to_parameters(_) } converted_arrays << converted converted - elsif value.is_a?(Parameters) || !value.is_a?(Hash) - value - else + when Hash self.class.new(value) + else + value end end def each_element(object) - if object.is_a?(Array) - object.map { |el| yield el }.compact - elsif fields_for_style?(object) - hash = object.class.new - object.each { |k,v| hash[k] = yield v } - hash - else - yield object + case object + when Array + object.grep(Parameters).map { |el| yield el }.compact + when Parameters + if object.fields_for_style? + hash = object.class.new + object.each { |k,v| hash[k] = yield v } + hash + else + yield object + end end end - def fields_for_style?(object) - object.is_a?(Hash) && object.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) } - end - def unpermitted_parameters!(params) unpermitted_keys = unpermitted_keys(params) if unpermitted_keys.any? @@ -544,14 +649,8 @@ module ActionController end def array_of_permitted_scalars?(value) - if value.is_a?(Array) - value.all? {|element| permitted_scalar?(element)} - end - end - - def array_of_permitted_scalars_filter(params, key) - if has_key?(key) && array_of_permitted_scalars?(self[key]) - params[key] = self[key] + if value.is_a?(Array) && value.all? {|element| permitted_scalar?(element)} + yield value end end @@ -562,17 +661,17 @@ module ActionController # Slicing filters out non-declared keys. slice(*filter.keys).each do |key, value| next unless value + next unless has_key? key if filter[key] == EMPTY_ARRAY # Declaration { comment_ids: [] }. - array_of_permitted_scalars_filter(params, key) + array_of_permitted_scalars?(self[key]) do |val| + params[key] = val + end else # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }. params[key] = each_element(value) do |element| - if element.is_a?(Hash) - element = self.class.new(element) unless element.respond_to?(:permit) - element.permit(*Array.wrap(filter[key])) - end + element.permit(*Array.wrap(filter[key])) end end end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 96f161fb09..f922e134f7 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -10,27 +10,45 @@ module ActionController DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup DEFAULT_ENV.delete 'PATH_INFO' - def initialize(env = {}) - super + def self.new_session + TestSession.new + end + + # Create a new test request with default `env` values + def self.create + 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) + end + + def self.default_env + DEFAULT_ENV + end + private_class_method :default_env + + def initialize(env, session) + super(env) - self.session = TestSession.new + self.session = session self.session_options = TestSession::DEFAULT_OPTIONS end + def query_string=(string) + @env[Rack::QUERY_STRING] = string + end + + def request_parameters=(params) + @env["action_dispatch.request.request_parameters"] = params + end + def assign_parameters(routes, controller_path, action, parameters = {}) parameters = parameters.symbolize_keys - extra_keys = routes.extra_keys(parameters.merge(:controller => controller_path, :action => action)) - non_path_parameters = get? ? query_parameters : request_parameters + generated_path, extra_keys = routes.generate_extras(parameters.merge(:controller => controller_path, :action => action)) + non_path_parameters = {} + path_parameters = {} parameters.each do |key, value| - if value.is_a?(Array) && (value.frozen? || value.any?(&:frozen?)) - value = value.map{ |v| v.duplicable? ? v.dup : v } - elsif value.is_a?(Hash) && (value.frozen? || value.any?{ |k,v| v.frozen? }) - value = Hash[value.map{ |k,v| [k, v.duplicable? ? v.dup : v] }] - elsif value.frozen? && value.duplicable? - value = value.dup - end - if extra_keys.include?(key) || key == :action || key == :controller non_path_parameters[key] = value else @@ -44,69 +62,83 @@ module ActionController end end - path_parameters[:controller] = controller_path - path_parameters[:action] = action + if get? + if self.query_string.blank? + self.query_string = non_path_parameters.to_query + end + else + if ENCODER.should_multipart?(non_path_parameters) + @env['CONTENT_TYPE'] = ENCODER.content_type + data = ENCODER.build_multipart non_path_parameters + else + @env['CONTENT_TYPE'] ||= 'application/x-www-form-urlencoded' + + # FIXME: setting `request_parametes` is normally handled by the + # params parser middleware, and we should remove this roundtripping + # when we switch to caling `call` on the controller + + case content_mime_type.ref + when :json + data = ActiveSupport::JSON.encode(non_path_parameters) + params = ActiveSupport::JSON.decode(data).with_indifferent_access + self.request_parameters = params + when :xml + data = non_path_parameters.to_xml + params = Hash.from_xml(data)['hash'] + self.request_parameters = params + when :url_encoded_form + data = non_path_parameters.to_query + else + raise "Unknown Content-Type: #{content_type}" + end + end - # Clear the combined params hash in case it was already referenced. - @env.delete("action_dispatch.request.parameters") + @env['CONTENT_LENGTH'] = data.length.to_s + @env['rack.input'] = StringIO.new(data) + end - # Clear the filter cache variables so they're not stale - @filtered_parameters = @filtered_env = @filtered_path = nil + @env["PATH_INFO"] ||= generated_path + path_parameters[:controller] = controller_path + path_parameters[:action] = action - data = request_parameters.to_query - @env['CONTENT_LENGTH'] = data.length.to_s - @env['rack.input'] = StringIO.new(data) + self.path_parameters = path_parameters end - def recycle! - @formats = nil - @env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } - @env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } - @method = @request_method = nil - @fullpath = @ip = @remote_ip = @protocol = nil - @env['action_dispatch.request.query_parameters'] = {} - @set_cookies ||= {} - @set_cookies.update(Hash[cookie_jar.instance_variable_get("@set_cookies").map{ |k,o| [k,o[:value]] }]) - deleted_cookies = cookie_jar.instance_variable_get("@delete_cookies") - @set_cookies.reject!{ |k,v| deleted_cookies.include?(k) } - cookie_jar.update(rack_cookies) - cookie_jar.update(cookies) - cookie_jar.update(@set_cookies) - cookie_jar.recycle! - end + ENCODER = Class.new do + include Rack::Test::Utils + + def should_multipart?(params) + # FIXME: lifted from Rack-Test. We should push this separation upstream + multipart = false + query = lambda { |value| + case value + when Array + value.each(&query) + when Hash + value.values.each(&query) + when Rack::Test::UploadedFile + multipart = true + end + } + params.values.each(&query) + multipart + end - private + public :build_multipart - def default_env - DEFAULT_ENV - end - end - - class TestResponse < ActionDispatch::TestResponse - def recycle! - initialize - end + def content_type + "multipart/form-data; boundary=#{Rack::Test::MULTIPART_BOUNDARY}" + end + end.new end class LiveTestResponse < Live::Response - def recycle! - @body = nil - initialize - end - - def body - @body ||= super - end - # Was the response successful? alias_method :success?, :successful? # Was the URL not found? alias_method :missing?, :not_found? - # Were we redirected? - alias_method :redirect?, :redirection? - # Was there a server-side error? alias_method :error?, :server_error? end @@ -195,7 +227,7 @@ module ActionController # request. You can modify this object before sending the HTTP request. For example, # you might want to set some session properties before sending a GET request. # <b>@response</b>:: - # An ActionController::TestResponse object, representing the response + # An ActionDispatch::TestResponse object, representing the response # of the last HTTP response. In the above example, <tt>@response</tt> becomes valid # after calling +post+. If the various assert methods are not sufficient, then you # may use this object to inspect the HTTP response in detail. @@ -317,7 +349,9 @@ 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) - process_with_kwargs("GET", action, *args) + res = process_with_kwargs("GET", action, *args) + cookies.update res.cookies + res end # Simulate a POST request with the given parameters and set/volley the response. @@ -365,19 +399,6 @@ module ActionController end alias xhr :xml_http_request - def paramify_values(hash_or_array_or_value) - case hash_or_array_or_value - when Hash - Hash[hash_or_array_or_value.map{|key, value| [key, paramify_values(value)] }] - when Array - hash_or_array_or_value.map {|i| paramify_values(i)} - when Rack::Test::UploadedFile, ActionDispatch::Http::UploadedFile - hash_or_array_or_value - else - hash_or_array_or_value.to_param - end - end - # Simulate a HTTP request to +action+ by specifying request method, # parameters and set/volley the response. # @@ -437,10 +458,6 @@ module ActionController parameters ||= {} - # Ensure that numbers and symbols passed as params are converted to - # proper params, as is the case when engaging rack. - parameters = paramify_values(parameters) if html_format?(parameters) - if format.present? parameters[:format] = format end @@ -451,8 +468,13 @@ module ActionController @controller.extend(Testing::Functional) end - @request.recycle! - @response.recycle! + self.cookies.update @request.cookies + @request.env['HTTP_COOKIE'] = cookies.to_header + @request.env['action_dispatch.cookies'] = nil + + @request = TestRequest.new scrub_env!(@request.env), @request.session + @response = build_response @response_klass + @response.request = @request @controller.recycle! @request.env['REQUEST_METHOD'] = http_method @@ -474,14 +496,17 @@ module ActionController @controller.request = @request @controller.response = @response - build_request_uri(controller_class_name, action, parameters) + @request.env["SCRIPT_NAME"] ||= @controller.config.relative_url_root @controller.recycle! @controller.process(action) + @request.env.delete 'HTTP_COOKIE' + if cookies = @request.env['action_dispatch.cookies'] unless @response.committed? cookies.write(@response) + self.cookies.update(cookies.instance_variable_get(:@cookies)) end end @response.prepare! @@ -496,6 +521,7 @@ module ActionController @request.env.delete 'HTTP_X_REQUESTED_WITH' @request.env.delete 'HTTP_ACCEPT' end + @request.query_string = '' @response end @@ -503,11 +529,11 @@ module ActionController def setup_controller_request_and_response @controller = nil unless defined? @controller - response_klass = TestResponse + @response_klass = ActionDispatch::TestResponse if klass = self.class.controller_class if klass < ActionController::Live - response_klass = LiveTestResponse + @response_klass = LiveTestResponse end unless @controller begin @@ -518,8 +544,8 @@ module ActionController end end - @request = build_request - @response = build_response response_klass + @request = TestRequest.create + @response = build_response @response_klass @response.request = @request if @controller @@ -528,10 +554,6 @@ module ActionController end end - def build_request - TestRequest.new - end - def build_response(klass) klass.new end @@ -545,6 +567,14 @@ module ActionController 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) @@ -591,23 +621,6 @@ module ActionController end end - def build_request_uri(controller_class_name, action, parameters) - unless @request.env["PATH_INFO"] - options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters - options.update( - :controller => controller_class_name, - :action => action, - :relative_url_root => nil, - :_recall => @request.path_parameters) - - url, query_string = @routes.path_for(options).split("?", 2) - - @request.env["SCRIPT_NAME"] = @controller.config.relative_url_root - @request.env["PATH_INFO"] = url - @request.env["QUERY_STRING"] = query_string || "" - end - end - def html_format?(parameters) return true unless parameters.key?(:format) Mime.fetch(parameters[:format]) { Mime['html'] }.html? diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index dcd3ee0644..f6336c8c7a 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -52,6 +52,7 @@ module ActionDispatch autoload :DebugExceptions autoload :ExceptionWrapper autoload :Flash + autoload :LoadInterlock autoload :ParamsParser autoload :PublicExceptions autoload :Reloader diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 7e585aa244..a639f8a8f8 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -45,7 +45,7 @@ module Mime # # respond_to do |format| # format.html - # format.ics { render text: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") } + # format.ics { render body: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") } # format.xml { render xml: @post } # end # end @@ -211,7 +211,7 @@ module Mime # This method is opposite of register method. # - # Usage: + # To unregister a MIME type: # # Mime::Type.unregister(:mobile) def unregister(symbol) diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb index df4b073a17..e826551f4b 100644 --- a/actionpack/lib/action_dispatch/http/parameter_filter.rb +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -30,36 +30,46 @@ module ActionDispatch when Regexp regexps << item else - strings << item.to_s + strings << Regexp.escape(item.to_s) end end - regexps << Regexp.new(strings.join('|'), true) unless strings.empty? - new regexps, blocks + deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.".freeze) } + deep_strings, strings = strings.partition { |s| s.include?("\\.".freeze) } + + regexps << Regexp.new(strings.join('|'.freeze), true) unless strings.empty? + deep_regexps << Regexp.new(deep_strings.join('|'.freeze), true) unless deep_strings.empty? + + new regexps, deep_regexps, blocks end - attr_reader :regexps, :blocks + attr_reader :regexps, :deep_regexps, :blocks - def initialize(regexps, blocks) + def initialize(regexps, deep_regexps, blocks) @regexps = regexps + @deep_regexps = deep_regexps.any? ? deep_regexps : nil @blocks = blocks end - def call(original_params) + def call(original_params, parents = []) filtered_params = {} original_params.each do |key, value| + parents.push(key) if deep_regexps if regexps.any? { |r| key =~ r } value = FILTERED + elsif deep_regexps && (joined = parents.join('.')) && deep_regexps.any? { |r| joined =~ r } + value = FILTERED elsif value.is_a?(Hash) - value = call(value) + value = call(value, parents) elsif value.is_a?(Array) - value = value.map { |v| v.is_a?(Hash) ? call(v) : v } + value = value.map { |v| v.is_a?(Hash) ? call(v, parents) : v } elsif blocks.any? key = key.dup if key.duplicable? value = value.dup if value.duplicable? blocks.each { |b| b.call(key, value) } end + parents.pop if deep_regexps filtered_params[key] = value end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index c2f05ecc86..4defb7f858 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -37,22 +37,7 @@ module ActionDispatch # Convert nested Hash to HashWithIndifferentAccess. # def normalize_encode_params(params) - case params - when Hash - if params.has_key?(:tempfile) - UploadedFile.new(params) - else - params.each_with_object({}) do |(key, val), new_hash| - new_hash[key] = if val.is_a?(Array) - val.map! { |el| normalize_encode_params(el) } - else - normalize_encode_params(val) - end - end.with_indifferent_access - end - else - params - end + ActionDispatch::Request::Utils.normalize_encode_params params end end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 3c62c055e5..6985cec5f5 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -290,7 +290,7 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) + @env["action_dispatch.request.query_parameters"] ||= normalize_encode_params(super || {}) rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e raise ActionController::BadRequest.new(:query, e) end @@ -298,7 +298,7 @@ module ActionDispatch # Override Rack's POST method to support indifferent access def POST - @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) + @env["action_dispatch.request.request_parameters"] ||= normalize_encode_params(super || {}) rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e raise ActionController::BadRequest.new(:request, e) end @@ -318,11 +318,6 @@ module ActionDispatch LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip end - protected - def parse_query(*) - Utils.deep_munge(super) - end - private def check_method(name) HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}") diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 8c965793cd..37e26faffb 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -40,7 +40,7 @@ module ActionDispatch # :nodoc: attr_writer :sending_file - # Get and set headers for this response. + # Get headers for this response. attr_reader :header alias_method :headers, :header @@ -80,11 +80,21 @@ module ActionDispatch # :nodoc: @response = response @buf = buf @closed = false + @str_body = nil + end + + def body + @str_body ||= begin + buf = '' + each { |chunk| buf << chunk } + buf + end end def write(string) raise IOError, "closed stream" if closed? + @str_body = nil @response.commit! @buf.push string end @@ -222,9 +232,7 @@ module ActionDispatch # :nodoc: # Returns the content of the response as a string. This contains the contents # of any calls to <tt>render</tt>. def body - strings = [] - each { |part| strings << part.to_s } - strings.join + @stream.body end EMPTY = " " diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index f5b709ccd6..6fcf49030b 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -245,7 +245,7 @@ module ActionDispatch # req = Request.new 'HTTP_HOST' => 'example.com:8080' # req.host # => "example.com" def host - raw_host_with_port.sub(/:\d+$/, '') + raw_host_with_port.sub(/:\d+$/, ''.freeze) end # Returns a \host:\port string for this request, such as "example.com" or diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb index bb01c087bc..cf6542b370 100644 --- a/actionpack/lib/action_dispatch/journey/nodes/node.rb +++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb @@ -30,7 +30,7 @@ module ActionDispatch end def name - left.tr '*:', '' + left.tr '*:'.freeze, ''.freeze end def type diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index cc4bd6105d..b84aad8eb6 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -121,7 +121,8 @@ module ActionDispatch end def match_head_routes(routes, req) - head_routes = match_routes(routes, req) + verb_specific_routes = routes.reject { |route| route.verb == // } + head_routes = match_routes(verb_specific_routes, req) if head_routes.empty? begin diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index d02ed96d0d..9793ca1c7a 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -14,10 +14,10 @@ module ActionDispatch # normalize_path("/%ab") # => "/%AB" def self.normalize_path(path) path = "/#{path}" - path.squeeze!('/') - path.sub!(%r{/+\Z}, '') + path.squeeze!('/'.freeze) + path.sub!(%r{/+\Z}, ''.freeze) path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase } - path = '/' if path == '' + path = '/' if path == ''.freeze path end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index dd1f140051..07d97bd6bd 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -8,7 +8,7 @@ require 'active_support/json' module ActionDispatch class Request < Rack::Request def cookie_jar - env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self) + env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(env, host, ssl?, cookies) end end @@ -228,16 +228,12 @@ module ActionDispatch } end - def self.build(request) - env = request.env + def self.build(env, host, secure, cookies) key_generator = env[GENERATOR_KEY] options = options_for_env env - host = request.host - secure = request.ssl? - new(key_generator, host, secure, options).tap do |hash| - hash.update(request.cookies) + hash.update(cookies) end end @@ -283,6 +279,10 @@ module ActionDispatch self end + def to_header + @cookies.map { |k,v| "#{k}=#{v}" }.join ';' + end + def handle_options(options) #:nodoc: options[:path] ||= "/" diff --git a/actionpack/lib/action_dispatch/middleware/load_interlock.rb b/actionpack/lib/action_dispatch/middleware/load_interlock.rb new file mode 100644 index 0000000000..07f498319c --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/load_interlock.rb @@ -0,0 +1,21 @@ +require 'active_support/dependencies' +require 'rack/body_proxy' + +module ActionDispatch + class LoadInterlock + def initialize(app) + @app = app + end + + def call(env) + interlock = ActiveSupport::Dependencies.interlock + interlock.start_running + response = @app.call(env) + body = Rack::BodyProxy.new(response[2]) { interlock.done_running } + response[2] = body + response + ensure + interlock.done_running unless body + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index 29d43faeed..2617956c74 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -13,40 +13,35 @@ module ActionDispatch end end - DEFAULT_PARSERS = { Mime::JSON => :json } + DEFAULT_PARSERS = { + Mime::JSON => lambda { |raw_post| + data = ActiveSupport::JSON.decode(raw_post) + data = {:_json => data} unless data.is_a?(Hash) + Request::Utils.normalize_encode_params(data) + } + } def initialize(app, parsers = {}) @app, @parsers = app, DEFAULT_PARSERS.merge(parsers) end def call(env) - if params = parse_formatted_parameters(env) - env["action_dispatch.request.request_parameters"] = params - end + default = env["action_dispatch.request.request_parameters"] + env["action_dispatch.request.request_parameters"] = parse_formatted_parameters(env, @parsers, default) @app.call(env) end private - def parse_formatted_parameters(env) + def parse_formatted_parameters(env, parsers, default) request = Request.new(env) - return false if request.content_length.zero? + return default if request.content_length.zero? - strategy = @parsers[request.content_mime_type] + strategy = parsers.fetch(request.content_mime_type) { return default } - return false unless strategy + strategy.call(request.raw_post) - case strategy - when Proc - strategy.call(request.raw_post) - when :json - data = ActiveSupport::JSON.decode(request.raw_post) - data = {:_json => data} unless data.is_a?(Hash) - Request::Utils.deep_munge(data).with_indifferent_access - else - false - end rescue => e # JSON or Ruby code block errors logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index b098ea389f..f38da4fdf6 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -35,7 +35,7 @@ module ActionDispatch paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"] if match = paths.detect { |p| - path = File.join(@root, p.force_encoding('UTF-8')) + path = File.join(@root, p.force_encoding('UTF-8'.freeze)) begin File.file?(path) && File.readable?(path) rescue SystemCallError @@ -76,7 +76,7 @@ module ActionDispatch end def content_type(path) - ::Rack::Mime.mime_type(::File.extname(path), 'text/plain') + ::Rack::Mime.mime_type(::File.extname(path), 'text/plain'.freeze) end def gzip_encoding_accepted?(env) @@ -112,7 +112,7 @@ module ActionDispatch def call(env) case env['REQUEST_METHOD'] when 'GET', 'HEAD' - path = env['PATH_INFO'].chomp('/') + path = env['PATH_INFO'].chomp('/'.freeze) if match = @file_handler.match?(path) env['PATH_INFO'] = match return @file_handler.call(env) diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb index 1c9371d89c..3973ea6346 100644 --- a/actionpack/lib/action_dispatch/request/utils.rb +++ b/actionpack/lib/action_dispatch/request/utils.rb @@ -5,24 +5,45 @@ module ActionDispatch mattr_accessor :perform_deep_munge self.perform_deep_munge = true - class << self - # Remove nils from the params hash - def deep_munge(hash, keys = []) - return hash unless perform_deep_munge + def self.normalize_encode_params(params) + if perform_deep_munge + NoNilParamEncoder.normalize_encode_params params + else + ParamEncoder.normalize_encode_params params + end + end - hash.each do |k, v| - keys << k - case v - when Array - v.grep(Hash) { |x| deep_munge(x, keys) } - v.compact! - when Hash - deep_munge(v, keys) + class ParamEncoder # :nodoc: + # Convert nested Hash to HashWithIndifferentAccess. + # + def self.normalize_encode_params(params) + case params + when Array + handle_array params + when Hash + if params.has_key?(:tempfile) + ActionDispatch::Http::UploadedFile.new(params) + else + params.each_with_object({}) do |(key, val), new_hash| + new_hash[key] = normalize_encode_params(val) + end.with_indifferent_access end - keys.pop + else + params end + end + + def self.handle_array(params) + params.map! { |el| normalize_encode_params(el) } + end + end - hash + # Remove nils from the params hash + class NoNilParamEncoder < ParamEncoder # :nodoc: + def self.handle_array(params) + list = super + list.compact! + list end end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 7cfe4693c1..ec530c6e8a 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1538,7 +1538,7 @@ module ActionDispatch path = path_for_action(action, options.delete(:path)) raise ArgumentError, "path is required" if path.blank? - action = action.to_s.dup + action = action.to_s if action =~ /^[\w\-\/]+$/ options[:action] ||= action.tr('-', '_') unless action.include?("/") diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 454593b59f..42512cad91 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -671,7 +671,7 @@ module ActionDispatch # Remove leading slashes from controllers def normalize_controller! - @options[:controller] = controller.sub(%r{^/}, '') if controller + @options[:controller] = controller.sub(%r{^/}, ''.freeze) if controller end # Move 'index' action from options to recall diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index eb554ec383..967bbd62f8 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -52,9 +52,11 @@ module ActionDispatch # argument. # # For convenience reasons, mailers provide a shortcut for ActionController::UrlFor#url_for. - # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlFor#url_for' - # in full. However, mailers don't have hostname information, and that's why you'll still - # have to specify the <tt>:host</tt> argument when generating URLs in mailers. + # So within mailers, you only have to type +url_for+ instead of 'ActionController::UrlFor#url_for' + # in full. However, mailers don't have hostname information, and you still have to provide + # the +:host+ argument or set the default host that will be used in all mailers using the + # configuration option +config.action_mailer.default_url_options+. For more information on + # url_for in mailers read the ActionMailer#Base documentation. # # # == URL generation for named routes @@ -147,6 +149,20 @@ module ActionDispatch # # => 'http://somehost.org/myapp/tasks/testing' # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp", only_path: true # # => '/myapp/tasks/testing' + # + # Missing routes keys may be filled in from the current request's parameters + # (e.g. +:controller+, +:action+, +:id+ and any other parameters that are + # placed in the path). Given that the current action has been reached + # through `GET /users/1`: + # + # url_for(only_path: true) # => '/users/1' + # url_for(only_path: true, action: 'edit') # => '/users/1/edit' + # url_for(only_path: true, action: 'edit', id: 2) # => '/users/2/edit' + # + # Notice that no +:id+ parameter was provided to the first +url_for+ call + # and the helper used the one from the route's path. Any path parameter + # implicitly used by +url_for+ can always be overwritten like shown on the + # last +url_for+ calls. def url_for(options = nil) case options when nil @@ -155,6 +171,10 @@ module ActionDispatch route_name = options.delete :use_route _routes.url_for(options.symbolize_keys.reverse_merge!(url_options), route_name) + when ActionController::Parameters + route_name = options.delete :use_route + _routes.url_for(options.to_unsafe_h.symbolize_keys. + reverse_merge!(url_options), route_name) when String options when Symbol diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 13a72220b3..b6e21b0d28 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -3,6 +3,13 @@ module ActionDispatch module Assertions # A small suite of assertions that test responses from \Rails applications. module ResponseAssertions + RESPONSE_PREDICATES = { # :nodoc: + success: :successful?, + missing: :not_found?, + redirect: :redirection?, + error: :server_error?, + } + # Asserts that the response is one of the following types: # # * <tt>:success</tt> - Status code was in the 200-299 range @@ -20,11 +27,9 @@ module ActionDispatch # # assert that the response code was status code 401 (unauthorized) # assert_response 401 def assert_response(type, message = nil) - message ||= "Expected response to be a <#{type}>, but was <#{@response.response_code}>" - if Symbol === type if [:success, :missing, :redirect, :error].include?(type) - assert @response.send("#{type}?"), message + assert_predicate @response, RESPONSE_PREDICATES[type], message else code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type] if code.nil? diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index c94eea9134..d0e3ea818e 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -165,7 +165,7 @@ module ActionDispatch # ROUTES TODO: These assertions should really work in an integration context def method_missing(selector, *args, &block) - if defined?(@controller) && @controller && @routes && @routes.named_routes.route_defined?(selector) + if defined?(@controller) && @controller && defined?(@routes) && @routes && @routes.named_routes.route_defined?(selector) @controller.send(selector, *args, &block) else super @@ -183,7 +183,7 @@ module ActionDispatch end # Assume given controller - request = ActionController::TestRequest.new + request = ActionController::TestRequest.create if path =~ %r{://} fail_on(URI::InvalidURIError, msg) do diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index 415ef80cd2..494644cd46 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -19,7 +19,7 @@ module ActionDispatch end def cookies - @request.cookie_jar + @cookie_jar ||= Cookies::CookieJar.build(@request.env, @request.host, @request.ssl?, @request.cookies) end def redirect_to_url diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index 4b9a088265..ad1a7f7109 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -4,19 +4,22 @@ require 'rack/utils' module ActionDispatch class TestRequest < Request DEFAULT_ENV = Rack::MockRequest.env_for('/', - 'HTTP_HOST' => 'test.host', - 'REMOTE_ADDR' => '0.0.0.0', - 'HTTP_USER_AGENT' => 'Rails Testing' + 'HTTP_HOST' => 'test.host', + 'REMOTE_ADDR' => '0.0.0.0', + 'HTTP_USER_AGENT' => 'Rails Testing', ) - def self.new(env = {}) - super + # Create a new test request with default `env` values + def self.create(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)) end - def initialize(env = {}) - env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application - super(default_env.merge(env)) + def self.default_env + DEFAULT_ENV end + private_class_method :default_env def request_method=(method) @env['REQUEST_METHOD'] = method.to_s.upcase @@ -62,17 +65,5 @@ module ActionDispatch @env.delete('action_dispatch.request.accepts') @env['HTTP_ACCEPT'] = Array(mime_types).collect(&:to_s).join(",") end - - alias :rack_cookies :cookies - - def cookies - @cookies ||= {}.with_indifferent_access - end - - private - - def default_env - DEFAULT_ENV - end end end diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb index a9b88ac5fd..6a31d6243f 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -16,9 +16,6 @@ module ActionDispatch # Was the URL not found? alias_method :missing?, :not_found? - # Were we redirected? - alias_method :redirect?, :redirection? - # Was there a server-side error? alias_method :error?, :server_error? end |