diff options
Diffstat (limited to 'actionpack/lib')
66 files changed, 1504 insertions, 1117 deletions
diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index ebd02bd9a1..784092867c 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -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. 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/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 5514213ad8..887196b3d2 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -54,11 +54,11 @@ module AbstractController Mime::TEXT end - DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %w( + DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %i( @_action_name @_response_body @_formats @_prefixes @_config @_view_context_class @_view_renderer @_lookup_context @_routes @_db_runtime - ).map(&:to_sym) + ) # This method should return a hash with assigns. # You can overwrite this configuration per controller. diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb index 3af63b8892..b4594bf302 100644 --- a/actionpack/lib/action_controller/api.rb +++ b/actionpack/lib/action_controller/api.rb @@ -90,7 +90,7 @@ module ActionController # Shortcut helper that returns all the ActionController::API modules except # the ones passed as arguments: # - # class MetalController + # class MyAPIBaseController < ActionController::Metal # ActionController::API.without_modules(:ForceSSL, :UrlFor).each do |left| # include left # end diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 2c3b3f4e05..55734b9774 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -183,7 +183,7 @@ module ActionController # Shortcut helper that returns all the modules included in # ActionController::Base except the ones passed as arguments: # - # class MetalController + # class MyBaseController < ActionController::Metal # ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left| # include left # end @@ -252,7 +252,7 @@ module ActionController # Define some internal variables that should not be propagated to the view. PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [ - :@_status, :@_headers, :@_params, :@_env, :@_response, :@_request, + :@_status, :@_headers, :@_params, :@_response, :@_request, :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout ] def _protected_ivars # :nodoc: 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.rb b/actionpack/lib/action_controller/metal.rb index ae111e4951..914b0d4b30 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/array/extract_options' require 'action_dispatch/middleware/stack' +require 'active_support/deprecation' module ActionController # Extend ActionDispatch middleware stack to make it aware of options @@ -11,22 +12,14 @@ module ActionController # class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc: class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc: - def initialize(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? - super + def initialize(klass, args, actions, strategy, block) + @actions = actions + @strategy = strategy + super(klass, args, block) end def valid?(action) - if @only.present? - @only.include?(action) - elsif @except.present? - !@except.include?(action) - else - true - end + @strategy.call @actions, action end end @@ -37,6 +30,32 @@ module ActionController middleware.valid?(action) ? middleware.build(a) : a end end + + 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 + + Middleware.new(get_class(klass), args, list, strategy, block) + end end # <tt>ActionController::Metal</tt> is the simplest possible controller, providing a @@ -98,11 +117,10 @@ module ActionController class Metal < AbstractController::Base abstract! - attr_internal_writer :env - def env - @_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>. @@ -197,8 +215,7 @@ module ActionController def set_request!(request) #:nodoc: @_request = request - @_env = request.env - @_env['action_controller.instance'] = self + @_request.controller_instance = self end def to_a #:nodoc: @@ -232,13 +249,13 @@ module ActionController end # Returns a Rack endpoint for the given action name. - def self.action(name, klass = ActionDispatch::Request) + def self.action(name) if middleware_stack.any? middleware_stack.build(name) do |env| - new.dispatch(name, klass.new(env)) + new.dispatch(name, ActionDispatch::Request.new(env)) end else - lambda { |env| new.dispatch(name, klass.new(env)) } + lambda { |env| new.dispatch(name, ActionDispatch::Request.new(env)) } end 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..bbb38cf8fc 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -94,7 +94,7 @@ module ActionController end def has_basic_credentials?(request) - request.authorization.present? && (auth_scheme(request) == 'Basic') + request.authorization.present? && (auth_scheme(request).downcase == 'basic') end def user_name_and_password(request) @@ -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/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..e5f3cb8e8d 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 @@ -136,17 +136,17 @@ module ActionController #:nodoc: # This is the method that defines the application behavior when a request is found to be unverified. def handle_unverified_request request = @controller.request - request.session = NullSessionHash.new(request.env) + request.session = NullSessionHash.new(request) request.env['action_dispatch.request.flash_hash'] = nil request.env['rack.session.options'] = { skip: true } - request.env['action_dispatch.cookies'] = NullCookieJar.build(request) + request.cookie_jar = NullCookieJar.build(request, {}) end protected class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc: - def initialize(env) - super(nil, env) + def initialize(req) + super(nil, req) @data = {} @loaded = true end @@ -160,14 +160,6 @@ module ActionController #:nodoc: end class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc: - def self.build(request) - key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY] - host = request.host - secure = request.ssl? - - new(key_generator, host, secure, options_for_env({})) - end - def write(*) # nothing end diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index af31de1f3a..a6115674aa 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -199,7 +199,7 @@ module ActionController #:nodoc: def _process_options(options) #:nodoc: super if options[:stream] - if env["HTTP_VERSION"] == "HTTP/1.0" + if request.version == "HTTP/1.0" options.delete(:stream) else headers["Cache-Control"] ||= "no-cache" diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 09867e2407..fc8e345d43 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -104,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 @@ -144,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. # @@ -162,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 @@ -170,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 @@ -347,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+ @@ -361,10 +377,16 @@ module ActionController # 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 @@ -375,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. @@ -384,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 @@ -393,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 transformation 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> @@ -439,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| @@ -451,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? @@ -546,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 @@ -564,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/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 5a0e5c62e4..dbf7241a14 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -41,7 +41,11 @@ module ActionController if original_script_name options[:original_script_name] = original_script_name else - options[:script_name] = same_origin ? request.script_name.dup : script_name + if same_origin + options[:script_name] = request.script_name.empty? ? "".freeze : request.script_name.dup + else + options[:script_name] = script_name + end end options.freeze else diff --git a/actionpack/lib/action_controller/middleware.rb b/actionpack/lib/action_controller/middleware.rb deleted file mode 100644 index 437fec3dc6..0000000000 --- a/actionpack/lib/action_controller/middleware.rb +++ /dev/null @@ -1,39 +0,0 @@ -module ActionController - class Middleware < Metal - class ActionMiddleware - def initialize(controller, app) - @controller, @app = controller, app - end - - def call(env) - request = ActionDispatch::Request.new(env) - @controller.build(@app).dispatch(:index, request) - end - end - - class << self - alias build new - - def new(app) - ActionMiddleware.new(self, app) - end - end - - attr_internal :app - - def process(action) - response = super - self.status, self.headers, self.response_body = response if response.is_a?(Array) - response - end - - def initialize(app) - super() - @_app = app - end - - def index - call(env) - end - end -end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 43c481339a..d14483dc72 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -1,4 +1,5 @@ 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' @@ -35,21 +36,23 @@ module ActionController end def query_string=(string) - @env[Rack::QUERY_STRING] = string + set_header Rack::QUERY_STRING, string end def request_parameters=(params) - @env["action_dispatch.request.request_parameters"] = params + set_header "action_dispatch.request.request_parameters", params end - def assign_parameters(routes, controller_path, action, parameters = {}) - parameters = parameters.symbolize_keys - generated_path, extra_keys = routes.generate_extras(parameters.merge(:controller => controller_path, :action => action)) + def content_type=(type) + set_header 'CONTENT_TYPE', type + end + + def assign_parameters(routes, controller_path, action, parameters, generated_path, query_string_keys) non_path_parameters = {} path_parameters = {} parameters.each do |key, value| - if extra_keys.include?(key) || key == :action || key == :controller + if query_string_keys.include?(key) non_path_parameters[key] = value else if value.is_a?(Array) @@ -68,10 +71,12 @@ module ActionController end else if ENCODER.should_multipart?(non_path_parameters) - @env['CONTENT_TYPE'] = ENCODER.content_type + self.content_type = ENCODER.content_type data = ENCODER.build_multipart non_path_parameters else - @env['CONTENT_TYPE'] ||= 'application/x-www-form-urlencoded' + get_header('CONTENT_TYPE') do |k| + set_header k, 'application/x-www-form-urlencoded' + end # FIXME: setting `request_parametes` is normally handled by the # params parser middleware, and we should remove this roundtripping @@ -93,8 +98,8 @@ module ActionController end end - @env['CONTENT_LENGTH'] = data.length.to_s - @env['rack.input'] = StringIO.new(data) + set_header 'CONTENT_LENGTH', data.length.to_s + set_header 'rack.input', StringIO.new(data) end @env["PATH_INFO"] ||= generated_path @@ -132,23 +137,13 @@ module ActionController end.new end - class TestResponse < ActionDispatch::TestResponse - end - class LiveTestResponse < Live::Response - 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 @@ -181,6 +176,10 @@ module ActionController clear end + def fetch(*args, &block) + @data.fetch(*args, &block) + end + private def load! @@ -237,7 +236,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. @@ -457,7 +456,7 @@ module ActionController end if body.present? - @request.env['RAW_POST_DATA'] = body + @request.set_header 'RAW_POST_DATA', body end if http_method.present? @@ -479,44 +478,50 @@ module ActionController end self.cookies.update @request.cookies - @request.env['HTTP_COOKIE'] = cookies.to_header - @request.env['action_dispatch.cookies'] = nil + @request.set_header 'HTTP_COOKIE', cookies.to_header + @request.delete_header 'action_dispatch.cookies' @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 + @request.set_header 'REQUEST_METHOD', http_method + + parameters = parameters.symbolize_keys - controller_class_name = @controller.class.anonymous? ? - "anonymous" : - @controller.class.controller_path + generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action.to_s)) + generated_path = generated_path(generated_extras) + query_string_keys = query_parameter_names(generated_extras) - @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters) + @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters, generated_path, query_string_keys) @request.session.update(session) if session @request.flash.update(flash || {}) if xhr - @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + @request.set_header 'HTTP_X_REQUESTED_WITH', 'XMLHttpRequest' + @request.get_header('HTTP_ACCEPT') do |k| + @request.set_header k, [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + end end @controller.request = @request @controller.response = @response - @request.env["SCRIPT_NAME"] ||= @controller.config.relative_url_root + @request.get_header("SCRIPT_NAME") do |k| + @request.set_header k, @controller.config.relative_url_root + end @controller.recycle! @controller.process(action) - @request.env.delete 'HTTP_COOKIE' + @request.delete_header 'HTTP_COOKIE' - if cookies = @request.env['action_dispatch.cookies'] + if @request.have_cookie_jar? unless @response.committed? - cookies.write(@response) - self.cookies.update(cookies.instance_variable_get(:@cookies)) + @request.cookie_jar.write(@response) + self.cookies.update(@request.cookie_jar.instance_variable_get(:@cookies)) end end @response.prepare! @@ -528,18 +533,30 @@ module ActionController end if xhr - @request.env.delete 'HTTP_X_REQUESTED_WITH' - @request.env.delete 'HTTP_ACCEPT' + @request.delete_header 'HTTP_X_REQUESTED_WITH' + @request.delete_header 'HTTP_ACCEPT' end @request.query_string = '' @response end + def controller_class_name + @controller.class.anonymous? ? "anonymous" : @controller.class.controller_path + end + + def generated_path(generated_extras) + generated_extras[0] + end + + def query_parameter_names(generated_extras) + generated_extras[1] + [:controller, :action] + end + 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 diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 3170389b36..e70e90018c 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -50,13 +50,13 @@ module ActionDispatch protected def parameter_filter - parameter_filter_for @env.fetch("action_dispatch.parameter_filter") { + parameter_filter_for get_header("action_dispatch.parameter_filter") { return NULL_PARAM_FILTER } end def env_filter - user_key = @env.fetch("action_dispatch.parameter_filter") { + user_key = get_header("action_dispatch.parameter_filter") { return NULL_ENV_FILTER } parameter_filter_for(Array(user_key) + ENV_MATCH) diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb index bf79963351..94c1f2b41f 100644 --- a/actionpack/lib/action_dispatch/http/filter_redirect.rb +++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb @@ -5,8 +5,7 @@ module ActionDispatch FILTERED = '[FILTERED]'.freeze # :nodoc: def filtered_location # :nodoc: - filters = location_filter - if !filters.empty? && location_filter_match?(filters) + if location_filter_match? FILTERED else location @@ -15,7 +14,7 @@ module ActionDispatch private - def location_filter + def location_filters if request request.env['action_dispatch.redirect_filter'] || [] else @@ -23,12 +22,12 @@ module ActionDispatch end end - def location_filter_match?(filters) - filters.any? do |filter| + def location_filter_match? + location_filters.any? do |filter| if String === filter location.include?(filter) elsif Regexp === filter - location.match(filter) + location =~ filter end end end diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index bc5410dc38..fbdec6c132 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -30,27 +30,32 @@ module ActionDispatch HTTP_HEADER = /\A[A-Za-z0-9-]+\z/ include Enumerable - attr_reader :env - def initialize(env = {}) # :nodoc: - @env = env + def self.from_hash(hash) + new ActionDispatch::Request.new hash + end + + def initialize(request) # :nodoc: + @req = request end # Returns the value for the given key mapped to @env. def [](key) - @env[env_name(key)] + @req.get_header env_name(key) end # Sets the given value for the key mapped to @env. def []=(key, value) - @env[env_name(key)] = value + @req.set_header env_name(key), value end def key?(key) - @env.key? env_name(key) + @req.has_header? env_name(key) end alias :include? :key? + DEFAULT = Object.new # :nodoc: + # Returns the value for the given key mapped to @env. # # If the key is not found and an optional code block is not provided, @@ -58,18 +63,22 @@ module ActionDispatch # # If the code block is provided, then it will be run and # its result returned. - def fetch(key, *args, &block) - @env.fetch env_name(key), *args, &block + def fetch(key, default = DEFAULT) + @req.get_header(env_name(key)) do + return default unless default == DEFAULT + return yield if block_given? + raise NameError, key + end end def each(&block) - @env.each(&block) + @req.each_header(&block) end # Returns a new Http::Headers instance containing the contents of # <tt>headers_or_env</tt> and the original instance. def merge(headers_or_env) - headers = Http::Headers.new(env.dup) + headers = @req.dup.headers headers.merge!(headers_or_env) headers end @@ -79,11 +88,14 @@ module ActionDispatch # <tt>headers_or_env</tt>. def merge!(headers_or_env) headers_or_env.each do |key, value| - self[env_name(key)] = value + @req.set_header env_name(key), value end end + def env; @req.env.dup; end + private + # Converts a HTTP header name to an environment variable name if it is # not contained within the headers hash. def env_name(key) diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index ff336b7354..e01d5ecc8f 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -15,12 +15,13 @@ module ActionDispatch # For backward compatibility, the post \format is extracted from the # X-Post-Data-Format HTTP header if present. def content_mime_type - @env["action_dispatch.request.content_type"] ||= begin - if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/ + get_header("action_dispatch.request.content_type") do |k| + v = if get_header('CONTENT_TYPE') =~ /^([^,\;]*)/ Mime::Type.lookup($1.strip.downcase) else nil end + set_header k, v end end @@ -30,14 +31,15 @@ module ActionDispatch # Returns the accepted MIME type for the request. def accepts - @env["action_dispatch.request.accepts"] ||= begin - header = @env['HTTP_ACCEPT'].to_s.strip + get_header("action_dispatch.request.accepts") do |k| + header = get_header('HTTP_ACCEPT').to_s.strip - if header.empty? + v = if header.empty? [content_mime_type] else Mime::Type.parse(header) end + set_header k, v end end @@ -52,14 +54,14 @@ module ActionDispatch end def formats - @env["action_dispatch.request.formats"] ||= begin + get_header("action_dispatch.request.formats") do |k| params_readable = begin parameters[:format] rescue ActionController::BadRequest false end - if params_readable + v = if params_readable Array(Mime[parameters[:format]]) elsif use_accept_header && valid_accept_header accepts @@ -68,6 +70,7 @@ module ActionDispatch else [Mime::HTML] end + set_header k, v end end @@ -102,7 +105,7 @@ module ActionDispatch # end def format=(extension) parameters[:format] = extension.to_s - @env["action_dispatch.request.formats"] = [Mime::Type.lookup_by_extension(parameters[:format])] + set_header "action_dispatch.request.formats", [Mime::Type.lookup_by_extension(parameters[:format])] end # Sets the \formats by string extensions. This differs from #format= by allowing you @@ -121,9 +124,9 @@ module ActionDispatch # end def formats=(extensions) parameters[:format] = extensions.first.to_s - @env["action_dispatch.request.formats"] = extensions.collect do |extension| + set_header "action_dispatch.request.formats", extensions.collect { |extension| Mime::Type.lookup_by_extension(extension) - end + } end # Receives an array of mimes and return the first user sent mime that 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 6e058b829e..e826551f4b 100644 --- a/actionpack/lib/action_dispatch/http/parameter_filter.rb +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -34,11 +34,11 @@ module ActionDispatch end end - deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.") } - deep_strings, strings = strings.partition { |s| s.include?("\\.") } + 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('|'), true) unless strings.empty? - deep_regexps << Regexp.new(deep_strings.join('|'), true) unless deep_strings.empty? + 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 diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index c2f05ecc86..277207ae6e 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -8,20 +8,23 @@ module ActionDispatch # Returns both GET and POST \parameters in a single hash. def parameters - @env["action_dispatch.request.parameters"] ||= begin - params = begin - request_parameters.merge(query_parameters) - rescue EOFError - query_parameters.dup - end - params.merge!(path_parameters) - end + params = get_header("action_dispatch.request.parameters") + return params if params + + params = begin + request_parameters.merge(query_parameters) + rescue EOFError + query_parameters.dup + end + params.merge!(path_parameters) + set_header("action_dispatch.request.parameters", params) + params end alias :params :parameters def path_parameters=(parameters) #:nodoc: - @env.delete('action_dispatch.request.parameters') - @env[PARAMETERS_KEY] = parameters + delete_header('action_dispatch.request.parameters') + set_header PARAMETERS_KEY, parameters end # Returns a hash with the \parameters used to form the \path of the request. @@ -29,7 +32,7 @@ module ActionDispatch # # {'action' => 'my_action', 'controller' => 'my_controller'} def path_parameters - @env[PARAMETERS_KEY] ||= {} + get_header(PARAMETERS_KEY) || {} end private @@ -37,22 +40,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..4748a54550 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -20,8 +20,6 @@ module ActionDispatch include ActionDispatch::Http::FilterParameters include ActionDispatch::Http::URL - HTTP_X_REQUEST_ID = "HTTP_X_REQUEST_ID".freeze # :nodoc: - autoload :Session, 'action_dispatch/request/session' autoload :Utils, 'action_dispatch/request/utils' @@ -31,15 +29,19 @@ module ActionDispatch PATH_TRANSLATED REMOTE_HOST REMOTE_IDENT REMOTE_USER REMOTE_ADDR SERVER_NAME SERVER_PROTOCOL + ORIGINAL_SCRIPT_NAME HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM - HTTP_NEGOTIATE HTTP_PRAGMA ].freeze + HTTP_NEGOTIATE HTTP_PRAGMA HTTP_CLIENT_IP + HTTP_X_FORWARDED_FOR HTTP_VERSION + HTTP_X_REQUEST_ID + ].freeze ENV_METHODS.each do |env| class_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{env.sub(/^HTTP_/n, '').downcase} # def accept_charset - @env["#{env}"] # @env["HTTP_ACCEPT_CHARSET"] + get_header "#{env}".freeze # get_header "HTTP_ACCEPT_CHARSET".freeze end # end METHOD end @@ -65,8 +67,20 @@ module ActionDispatch end end + def controller_class + check_path_parameters! + params = path_parameters + controller_param = params[:controller].underscore if params.key?(:controller) + params[:action] ||= 'index' + + yield unless controller_param + + const_name = "#{controller_param.camelize}Controller" + ActiveSupport::Dependencies.constantize(const_name) + end + def key?(key) - @env.key?(key) + has_header? key end # List of HTTP request methods from the following RFCs: @@ -103,27 +117,46 @@ module ActionDispatch # the application should use), this \method returns the overridden # value, not the original. def request_method - @request_method ||= check_method(env["REQUEST_METHOD"]) + @request_method ||= check_method(super) end def routes # :nodoc: - env["action_dispatch.routes".freeze] + get_header("action_dispatch.routes".freeze) end - def original_script_name # :nodoc: - env['ORIGINAL_SCRIPT_NAME'.freeze] + def routes=(routes) # :nodoc: + set_header("action_dispatch.routes".freeze, routes) end def engine_script_name(_routes) # :nodoc: - env[_routes.env_key] + get_header(_routes.env_key) + end + + def engine_script_name=(name) # :nodoc: + set_header(routes.env_key, name.dup) end def request_method=(request_method) #:nodoc: if check_method(request_method) - @request_method = env["REQUEST_METHOD"] = request_method + @request_method = set_header("REQUEST_METHOD", request_method) end end + def controller_instance # :nodoc: + get_header('action_controller.instance'.freeze) + end + + def controller_instance=(controller) # :nodoc: + set_header('action_controller.instance'.freeze, controller) + end + + def show_exceptions? # :nodoc: + # We're treating `nil` as "unset", and we want the default setting to be + # `true`. This logic should be extracted to `env_config` and calculated + # once. + !(get_header('action_dispatch.show_exceptions'.freeze) == false) + end + # Returns a symbol form of the #request_method def request_method_symbol HTTP_METHOD_LOOKUP[request_method] @@ -133,7 +166,7 @@ module ActionDispatch # even if it was overridden by middleware. See #request_method for # more information. def method - @method ||= check_method(env["rack.methodoverride.original_method"] || env['REQUEST_METHOD']) + @method ||= check_method(get_header("rack.methodoverride.original_method") || get_header('REQUEST_METHOD')) end # Returns a symbol form of the #method @@ -145,7 +178,7 @@ module ActionDispatch # # request.headers["Content-Type"] # => "text/plain" def headers - @headers ||= Http::Headers.new(@env) + @headers ||= Http::Headers.new(self) end # Returns a +String+ with the last requested path including their params. @@ -156,7 +189,7 @@ module ActionDispatch # # get '/foo?bar' # request.original_fullpath # => '/foo?bar' def original_fullpath - @original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath) + @original_fullpath ||= (get_header("ORIGINAL_FULLPATH") || fullpath) end # Returns the +String+ full path including params of the last URL requested. @@ -195,7 +228,7 @@ module ActionDispatch # (case-insensitive), which may need to be manually added depending on the # choice of JavaScript libraries and frameworks. def xml_http_request? - @env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i + get_header('HTTP_X_REQUESTED_WITH') =~ /XMLHttpRequest/i end alias :xhr? :xml_http_request? @@ -207,7 +240,11 @@ module ActionDispatch # Returns the IP address of client as a +String+, #Â usually set by the RemoteIp middleware. def remote_ip - @remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s + @remote_ip ||= (get_header("action_dispatch.remote_ip") || ip).to_s + end + + def remote_ip=(remote_ip) + set_header "action_dispatch.remote_ip".freeze, remote_ip end ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # :nodoc: @@ -219,43 +256,39 @@ module ActionDispatch # This unique ID is useful for tracing a request from end-to-end as part of logging or debugging. # This relies on the rack variable set by the ActionDispatch::RequestId middleware. def request_id - env[ACTION_DISPATCH_REQUEST_ID] + get_header ACTION_DISPATCH_REQUEST_ID end def request_id=(id) # :nodoc: - env[ACTION_DISPATCH_REQUEST_ID] = id + set_header ACTION_DISPATCH_REQUEST_ID, id end alias_method :uuid, :request_id - def x_request_id # :nodoc: - @env[HTTP_X_REQUEST_ID] - end - # Returns the lowercase name of the HTTP server software. def server_software - (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil + (get_header('SERVER_SOFTWARE') && /^([a-zA-Z]+)/ =~ get_header('SERVER_SOFTWARE')) ? $1.downcase : nil end # Read the request \body. This is useful for web services that need to # work with raw requests directly. def raw_post - unless @env.include? 'RAW_POST_DATA' + unless has_header? 'RAW_POST_DATA' raw_post_body = body - @env['RAW_POST_DATA'] = raw_post_body.read(content_length) + set_header('RAW_POST_DATA', raw_post_body.read(content_length)) raw_post_body.rewind if raw_post_body.respond_to?(:rewind) end - @env['RAW_POST_DATA'] + get_header 'RAW_POST_DATA' end # The request body is an IO input stream. If the RAW_POST_DATA environment # variable is already set, wrap it in a StringIO. def body - if raw_post = @env['RAW_POST_DATA'] + if raw_post = get_header('RAW_POST_DATA') raw_post.force_encoding(Encoding::BINARY) StringIO.new(raw_post) else - @env['rack.input'] + body_stream end end @@ -266,7 +299,7 @@ module ActionDispatch end def body_stream #:nodoc: - @env['rack.input'] + get_header('rack.input') end # TODO This should be broken apart into AD::Request::Session and probably @@ -277,20 +310,20 @@ module ActionDispatch else self.session = {} end - @env['action_dispatch.request.flash_hash'] = nil + set_header('action_dispatch.request.flash_hash', nil) end def session=(session) #:nodoc: - Session.set @env, session + Session.set self, session end def session_options=(options) - Session::Options.set @env, options + Session::Options.set self, options end # 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 +331,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 @@ -307,10 +340,10 @@ module ActionDispatch # Returns the authorization header regardless of whether it was specified directly or through one of the # proxy alternatives. def authorization - @env['HTTP_AUTHORIZATION'] || - @env['X-HTTP_AUTHORIZATION'] || - @env['X_HTTP_AUTHORIZATION'] || - @env['REDIRECT_X_HTTP_AUTHORIZATION'] + get_header('HTTP_AUTHORIZATION') || + get_header('X-HTTP_AUTHORIZATION') || + get_header('X_HTTP_AUTHORIZATION') || + get_header('REDIRECT_X_HTTP_AUTHORIZATION') end # True if the request came from localhost, 127.0.0.1. @@ -318,10 +351,13 @@ module ActionDispatch LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip end - protected - def parse_query(*) - Utils.deep_munge(super) - end + def request_parameters=(params) + set_header("action_dispatch.request.request_parameters".freeze, params) + end + + def logger + get_header("action_dispatch.logger".freeze) + end private def check_method(name) diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index c5939adb9f..fd92e89231 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -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 @@ -187,13 +197,13 @@ module ActionDispatch # :nodoc: @content_type = content_type.to_s end - # Sets the HTTP character set. + # Sets the HTTP character set. In case of nil parameter + #Â it sets the charset to utf-8. + # + # response.charset = 'utf-16' # => 'utf-16' + # response.charset = nil # => 'utf-8' def charset=(charset) - if nil == charset - @charset = self.class.default_charset - else - @charset = charset - end + @charset = charset.nil? ? self.class.default_charset : charset end # The response code of the request. @@ -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/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 540e11a4a0..a221f4c5af 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -28,7 +28,13 @@ module ActionDispatch raise(ArgumentError, ':tempfile is required') unless @tempfile @original_filename = hash[:filename] - @original_filename &&= @original_filename.encode "UTF-8" + if @original_filename + begin + @original_filename.encode!(Encoding::UTF_8) + rescue EncodingError + @original_filename.force_encoding(Encoding::UTF_8) + end + end @content_type = hash[:type] @headers = hash[:head] end 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/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index c0566c6fc9..c19ff0f4db 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -14,7 +14,7 @@ module ActionDispatch def generate(name, options, path_parameters, parameterize = nil) constraints = path_parameters.merge(options) - missing_keys = [] + missing_keys = nil # need for variable scope match_route(name, constraints) do |route| parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize) @@ -25,22 +25,22 @@ module ActionDispatch next unless name || route.dispatcher? missing_keys = missing_keys(route, parameterized_parts) - next unless missing_keys.empty? + next if missing_keys && !missing_keys.empty? params = options.dup.delete_if do |key, _| parameterized_parts.key?(key) || route.defaults.key?(key) end defaults = route.defaults required_parts = route.required_parts - parameterized_parts.delete_if do |key, value| - value.to_s == defaults[key].to_s && !required_parts.include?(key) + parameterized_parts.keep_if do |key, value| + defaults[key].nil? || value.to_s != defaults[key].to_s || required_parts.include?(key) end return [route.format(parameterized_parts), params] end message = "No route matches #{Hash[constraints.sort_by{|k,v| k.to_s}].inspect}" - message << " missing required keys: #{missing_keys.sort.inspect}" unless missing_keys.empty? + message << " missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty? raise ActionController::UrlGenerationError, message end @@ -54,12 +54,12 @@ module ActionDispatch def extract_parameterized_parts(route, options, recall, parameterize = nil) parameterized_parts = recall.merge(options) - keys_to_keep = route.parts.reverse.drop_while { |part| + keys_to_keep = route.parts.reverse_each.drop_while { |part| !options.key?(part) || (options[part] || recall[part]).nil? } | route.required_parts - (parameterized_parts.keys - keys_to_keep).each do |bad_key| - parameterized_parts.delete(bad_key) + parameterized_parts.delete_if do |bad_key, _| + !keys_to_keep.include?(bad_key) end if parameterize @@ -110,15 +110,36 @@ module ActionDispatch routes end + module RegexCaseComparator + DEFAULT_INPUT = /[-_.a-zA-Z0-9]+\/[-_.a-zA-Z0-9]+/ + DEFAULT_REGEX = /\A#{DEFAULT_INPUT}\Z/ + + def self.===(regex) + DEFAULT_INPUT == regex + end + end + # Returns an array populated with missing keys if any are present. def missing_keys(route, parts) - missing_keys = [] + missing_keys = nil tests = route.path.requirements route.required_parts.each { |key| - if tests.key?(key) - missing_keys << key unless /\A#{tests[key]}\Z/ === parts[key] + case tests[key] + when nil + unless parts[key] + missing_keys ||= [] + missing_keys << key + end + when RegexCaseComparator + unless RegexCaseComparator::DEFAULT_REGEX === parts[key] + missing_keys ||= [] + missing_keys << key + end else - missing_keys << key unless parts[key] + unless /\A#{tests[key]}\Z/ === parts[key] + missing_keys ||= [] + missing_keys << key + end end } missing_keys @@ -134,7 +155,7 @@ module ActionDispatch def build_cache root = { ___routes: [] } - routes.each_with_index do |route, i| + routes.routes.each_with_index do |route, i| leaf = route.required_defaults.inject(root) do |h, tuple| h[tuple] ||= {} end diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb index bb01c087bc..d069bf0205 100644 --- a/actionpack/lib/action_dispatch/journey/nodes/node.rb +++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb @@ -14,15 +14,15 @@ module ActionDispatch end def each(&block) - Visitors::Each.new(block).accept(self) + Visitors::Each::INSTANCE.accept(self, block) end def to_s - Visitors::String.new.accept(self) + Visitors::String::INSTANCE.accept(self, '') end def to_dot - Visitors::Dot.new.accept(self) + Visitors::Dot::INSTANCE.accept(self) end def to_sym @@ -30,7 +30,7 @@ module ActionDispatch end def name - left.tr '*:', '' + left.tr '*:'.freeze, ''.freeze end def type @@ -39,10 +39,14 @@ module ActionDispatch def symbol?; false; end def literal?; false; end + def terminal?; false; end + def star?; false; end + def cat?; false; end end class Terminal < Node # :nodoc: alias :symbol :left + def terminal?; true; end end class Literal < Terminal # :nodoc: @@ -69,11 +73,13 @@ module ActionDispatch class Symbol < Terminal # :nodoc: attr_accessor :regexp alias :symbol :regexp + attr_reader :name DEFAULT_EXP = /[^\.\/\?]+/ def initialize(left) super @regexp = DEFAULT_EXP + @name = left.tr '*:'.freeze, ''.freeze end def default_regexp? @@ -92,6 +98,7 @@ module ActionDispatch end class Star < Unary # :nodoc: + def star?; true; end def type; :STAR; end def name @@ -111,6 +118,7 @@ module ActionDispatch end class Cat < Binary # :nodoc: + def cat?; true; end def type; :CAT; end end diff --git a/actionpack/lib/action_dispatch/journey/parser_extras.rb b/actionpack/lib/action_dispatch/journey/parser_extras.rb index 14892f4321..fff0299812 100644 --- a/actionpack/lib/action_dispatch/journey/parser_extras.rb +++ b/actionpack/lib/action_dispatch/journey/parser_extras.rb @@ -6,6 +6,10 @@ module ActionDispatch class Parser < Racc::Parser # :nodoc: include Journey::Nodes + def self.parse(string) + new.parse string + end + def initialize @scanner = Scanner.new end diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb index 64b48ca45f..e93970046c 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -1,5 +1,3 @@ -require 'action_dispatch/journey/router/strexp' - module ActionDispatch module Journey # :nodoc: module Path # :nodoc: @@ -7,14 +5,20 @@ module ActionDispatch attr_reader :spec, :requirements, :anchored def self.from_string string - new Journey::Router::Strexp.build(string, {}, ["/.?"], true) + build(string, {}, "/.?", true) + end + + def self.build(path, requirements, separators, anchored) + parser = Journey::Parser.new + ast = parser.parse path + new ast, requirements, separators, anchored end - def initialize(strexp) - @spec = strexp.ast - @requirements = strexp.requirements - @separators = strexp.separators.join - @anchored = strexp.anchor + def initialize(ast, requirements, separators, anchored) + @spec = ast + @requirements = requirements + @separators = separators + @anchored = anchored @names = nil @optional_names = nil @@ -28,12 +32,12 @@ module ActionDispatch end def ast - @spec.grep(Nodes::Symbol).each do |node| + @spec.find_all(&:symbol?).each do |node| re = @requirements[node.to_sym] node.regexp = re if re end - @spec.grep(Nodes::Star).each do |node| + @spec.find_all(&:star?).each do |node| node = node.left node.regexp = @requirements[node.to_sym] || /(.+)/ end @@ -55,31 +59,6 @@ module ActionDispatch }.map(&:name).uniq end - class RegexpOffsets < Journey::Visitors::Visitor # :nodoc: - attr_reader :offsets - - def initialize(matchers) - @matchers = matchers - @capture_count = [0] - end - - def visit(node) - super - @capture_count - end - - def visit_SYMBOL(node) - node = node.to_sym - - if @matchers.key?(node) - re = /#{@matchers[node]}|/ - @capture_count.push((re.match('').length - 1) + (@capture_count.last || 0)) - else - @capture_count << (@capture_count.last || 0) - end - end - end - class AnchoredRegexp < Journey::Visitors::Visitor # :nodoc: def initialize(separator, matchers) @separator = separator @@ -189,8 +168,20 @@ module ActionDispatch def offsets return @offsets if @offsets - viz = RegexpOffsets.new(@requirements) - @offsets = viz.accept(spec) + @offsets = [0] + + spec.find_all(&:symbol?).each do |node| + node = node.to_sym + + if @requirements.key?(node) + re = /#{@requirements[node]}|/ + @offsets.push((re.match('').length - 1) + @offsets.last) + else + @offsets << @offsets.last + end + end + + @offsets end end end diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index cbc985640a..f5c9abf1cc 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -1,36 +1,81 @@ module ActionDispatch module Journey # :nodoc: class Route # :nodoc: - attr_reader :app, :path, :defaults, :name + attr_reader :app, :path, :defaults, :name, :precedence attr_reader :constraints alias :conditions :constraints - attr_accessor :precedence + module VerbMatchers + VERBS = %w{ DELETE GET HEAD OPTIONS LINK PATCH POST PUT TRACE UNLINK } + VERBS.each do |v| + class_eval <<-eoc + class #{v} + def self.verb; name.split("::").last; end + def self.call(req); req.#{v.downcase}?; end + end + eoc + end + + class Unknown + attr_reader :verb + + def initialize(verb) + @verb = verb + end + + def call(request); @verb === request.request_method; end + end + + class All + def self.call(_); true; end + def self.verb; ''; end + end + + VERB_TO_CLASS = VERBS.each_with_object({ :all => All }) do |verb, hash| + klass = const_get verb + hash[verb] = klass + hash[verb.downcase] = klass + hash[verb.downcase.to_sym] = klass + end + + end + + def self.verb_matcher(verb) + VerbMatchers::VERB_TO_CLASS.fetch(verb) do + VerbMatchers::Unknown.new verb.to_s.dasherize.upcase + end + end + + def self.build(name, app, path, constraints, required_defaults, defaults) + request_method_match = verb_matcher(constraints.delete(:request_method)) + new name, app, path, constraints, required_defaults, defaults, request_method_match, 0 + end ## # +path+ is a path constraint. # +constraints+ is a hash of constraints to be applied to this route. - def initialize(name, app, path, constraints, required_defaults, defaults) + def initialize(name, app, path, constraints, required_defaults, defaults, request_method_match, precedence) @name = name @app = app @path = path + @request_method_match = request_method_match @constraints = constraints @defaults = defaults @required_defaults = nil - @_required_defaults = required_defaults || [] + @_required_defaults = required_defaults @required_parts = nil @parts = nil @decorated_ast = nil - @precedence = 0 + @precedence = precedence @path_formatter = @path.build_formatter end def ast @decorated_ast ||= begin decorated_ast = path.ast - decorated_ast.grep(Nodes::Terminal).each { |n| n.memo = self } + decorated_ast.find_all(&:terminal?).each { |n| n.memo = self } decorated_ast end end @@ -92,7 +137,8 @@ module ActionDispatch end def matches?(request) - constraints.all? do |method, value| + match_verb(request) && + constraints.all? { |method, value| case value when Regexp, String value === request.send(method).to_s @@ -105,15 +151,28 @@ module ActionDispatch else value === request.send(method) end - end + } end def ip constraints[:ip] || // end + def requires_matching_verb? + !@request_method_match.all? { |x| x == VerbMatchers::All } + end + def verb - constraints[:request_method] || // + %r[^#{verbs.join('|')}$] + end + + private + def verbs + @request_method_match.map(&:verb) + end + + def match_verb(request) + @request_method_match.any? { |m| m.call request } end end end diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index b84aad8eb6..f649588520 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -1,5 +1,4 @@ require 'action_dispatch/journey/router/utils' -require 'action_dispatch/journey/router/strexp' require 'action_dispatch/journey/routes' require 'action_dispatch/journey/formatter' @@ -102,7 +101,7 @@ module ActionDispatch } routes = - if req.request_method == "HEAD" + if req.head? match_head_routes(routes, req) else match_routes(routes, req) @@ -121,7 +120,7 @@ module ActionDispatch end def match_head_routes(routes, req) - verb_specific_routes = routes.reject { |route| route.verb == // } + verb_specific_routes = routes.select(&:requires_matching_verb?) head_routes = match_routes(verb_specific_routes, req) if head_routes.empty? diff --git a/actionpack/lib/action_dispatch/journey/router/strexp.rb b/actionpack/lib/action_dispatch/journey/router/strexp.rb deleted file mode 100644 index 4b7738f335..0000000000 --- a/actionpack/lib/action_dispatch/journey/router/strexp.rb +++ /dev/null @@ -1,27 +0,0 @@ -module ActionDispatch - module Journey # :nodoc: - class Router # :nodoc: - class Strexp # :nodoc: - class << self - alias :compile :new - end - - attr_reader :path, :requirements, :separators, :anchor, :ast - - def self.build(path, requirements, separators, anchor = true) - parser = Journey::Parser.new - ast = parser.parse path - new ast, path, requirements, separators, anchor - end - - def initialize(ast, path, requirements, separators, anchor = true) - @ast = ast - @path = path - @requirements = requirements - @separators = separators - @anchor = anchor - end - end - end - end -end 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/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb index 5990964b57..f7b009109e 100644 --- a/actionpack/lib/action_dispatch/journey/routes.rb +++ b/actionpack/lib/action_dispatch/journey/routes.rb @@ -5,11 +5,10 @@ module ActionDispatch class Routes # :nodoc: include Enumerable - attr_reader :routes, :named_routes, :custom_routes, :anchored_routes + attr_reader :routes, :custom_routes, :anchored_routes def initialize @routes = [] - @named_routes = {} @ast = nil @anchored_routes = [] @custom_routes = [] @@ -37,7 +36,6 @@ module ActionDispatch routes.clear anchored_routes.clear custom_routes.clear - named_routes.clear end def partition_route(route) @@ -62,13 +60,9 @@ module ActionDispatch end end - # Add a route to the routing table. - def add_route(app, path, conditions, required_defaults, defaults, name = nil) - route = Route.new(name, app, path, conditions, required_defaults, defaults) - - route.precedence = routes.length + def add_route(name, mapping) + route = mapping.make_route name, routes.length routes << route - named_routes[name] = route if name && !named_routes[name] partition_route(route) clear_cache! route diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index 52b4c8b489..537c9b2f5c 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -92,6 +92,45 @@ module ActionDispatch end end + class FunctionalVisitor # :nodoc: + DISPATCH_CACHE = {} + + def accept(node, seed) + visit(node, seed) + end + + def visit node, seed + send(DISPATCH_CACHE[node.type], node, seed) + end + + def binary(node, seed) + visit(node.right, visit(node.left, seed)) + end + def visit_CAT(n, seed); binary(n, seed); end + + def nary(node, seed) + node.children.inject(seed) { |s, c| visit(c, s) } + end + def visit_OR(n, seed); nary(n, seed); end + + def unary(node, seed) + visit(node.left, seed) + end + def visit_GROUP(n, seed); unary(n, seed); end + def visit_STAR(n, seed); unary(n, seed); end + + def terminal(node, seed); seed; end + def visit_LITERAL(n, seed); terminal(n, seed); end + def visit_SYMBOL(n, seed); terminal(n, seed); end + def visit_SLASH(n, seed); terminal(n, seed); end + def visit_DOT(n, seed); terminal(n, seed); end + + instance_methods(false).each do |pim| + next unless pim =~ /^visit_(.*)$/ + DISPATCH_CACHE[$1.to_sym] = pim + end + end + class FormatBuilder < Visitor # :nodoc: def accept(node); Journey::Format.new(super); end def terminal(node); [node.left]; end @@ -117,104 +156,110 @@ module ActionDispatch end # Loop through the requirements AST - class Each < Visitor # :nodoc: - attr_reader :block - - def initialize(block) - @block = block - end - - def visit(node) + class Each < FunctionalVisitor # :nodoc: + def visit(node, block) block.call(node) super end + + INSTANCE = new end - class String < Visitor # :nodoc: + class String < FunctionalVisitor # :nodoc: private - def binary(node) - [visit(node.left), visit(node.right)].join + def binary(node, seed) + visit(node.right, visit(node.left, seed)) end - def nary(node) - node.children.map { |c| visit(c) }.join '|' + def nary(node, seed) + last_child = node.children.last + node.children.inject(seed) { |s, c| + string = visit(c, s) + string << "|".freeze unless last_child == c + string + } end - def terminal(node) - node.left + def terminal(node, seed) + seed + node.left end - def visit_GROUP(node) - "(#{visit(node.left)})" + def visit_GROUP(node, seed) + visit(node.left, seed << "(".freeze) << ")".freeze end + + INSTANCE = new end - class Dot < Visitor # :nodoc: + class Dot < FunctionalVisitor # :nodoc: def initialize @nodes = [] @edges = [] end - def accept(node) + def accept(node, seed = [[], []]) super + nodes, edges = seed <<-eodot digraph parse_tree { size="8,5" node [shape = none]; edge [dir = none]; - #{@nodes.join "\n"} - #{@edges.join("\n")} + #{nodes.join "\n"} + #{edges.join("\n")} } eodot end private - def binary(node) - node.children.each do |c| - @edges << "#{node.object_id} -> #{c.object_id};" - end + def binary(node, seed) + seed.last.concat node.children.map { |c| + "#{node.object_id} -> #{c.object_id};" + } super end - def nary(node) - node.children.each do |c| - @edges << "#{node.object_id} -> #{c.object_id};" - end + def nary(node, seed) + seed.last.concat node.children.map { |c| + "#{node.object_id} -> #{c.object_id};" + } super end - def unary(node) - @edges << "#{node.object_id} -> #{node.left.object_id};" + def unary(node, seed) + seed.last << "#{node.object_id} -> #{node.left.object_id};" super end - def visit_GROUP(node) - @nodes << "#{node.object_id} [label=\"()\"];" + def visit_GROUP(node, seed) + seed.first << "#{node.object_id} [label=\"()\"];" super end - def visit_CAT(node) - @nodes << "#{node.object_id} [label=\"â—‹\"];" + def visit_CAT(node, seed) + seed.first << "#{node.object_id} [label=\"â—‹\"];" super end - def visit_STAR(node) - @nodes << "#{node.object_id} [label=\"*\"];" + def visit_STAR(node, seed) + seed.first << "#{node.object_id} [label=\"*\"];" super end - def visit_OR(node) - @nodes << "#{node.object_id} [label=\"|\"];" + def visit_OR(node, seed) + seed.first << "#{node.object_id} [label=\"|\"];" super end - def terminal(node) + def terminal(node, seed) value = node.left - @nodes << "#{node.object_id} [label=\"#{value}\"];" + seed.first << "#{node.object_id} [label=\"#{value}\"];" + seed end + INSTANCE = new end end end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 07d97bd6bd..e4c5b0bd29 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -8,8 +8,52 @@ require 'active_support/json' module ActionDispatch class Request < Rack::Request def cookie_jar - env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(env, host, ssl?, cookies) + get_header('action_dispatch.cookies'.freeze) do |k| + self.cookie_jar = Cookies::CookieJar.build(self, cookies) + end + end + + # :stopdoc: + def have_cookie_jar? + has_header? 'action_dispatch.cookies'.freeze + end + + def cookie_jar=(jar) + set_header 'action_dispatch.cookies'.freeze, jar + end + + def key_generator + get_header Cookies::GENERATOR_KEY + end + + def signed_cookie_salt + get_header Cookies::SIGNED_COOKIE_SALT + end + + def encrypted_cookie_salt + get_header Cookies::ENCRYPTED_COOKIE_SALT + end + + def encrypted_signed_cookie_salt + get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT + end + + def secret_token + get_header Cookies::SECRET_TOKEN + end + + def secret_key_base + get_header Cookies::SECRET_KEY_BASE + end + + def cookies_serializer + get_header Cookies::COOKIES_SERIALIZER + end + + def cookies_digest + get_header Cookies::COOKIES_DIGEST end + # :startdoc: end # \Cookies are read and written through ActionController#cookies. @@ -118,7 +162,7 @@ module ActionDispatch # cookies.permanent.signed[:remember_me] = current_user.id # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT def permanent - @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) + @permanent ||= PermanentCookieJar.new(self) end # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from @@ -138,10 +182,10 @@ module ActionDispatch # cookies.signed[:discount] # => 45 def signed @signed ||= - if @options[:upgrade_legacy_signed_cookies] - UpgradeLegacySignedCookieJar.new(self, @key_generator, @options) + if upgrade_legacy_signed_cookies? + UpgradeLegacySignedCookieJar.new(self) else - SignedCookieJar.new(self, @key_generator, @options) + SignedCookieJar.new(self) end end @@ -161,10 +205,10 @@ module ActionDispatch # cookies.encrypted[:discount] # => 45 def encrypted @encrypted ||= - if @options[:upgrade_legacy_signed_cookies] - UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options) + if upgrade_legacy_signed_cookies? + UpgradeLegacyEncryptedCookieJar.new(self) else - EncryptedCookieJar.new(self, @key_generator, @options) + EncryptedCookieJar.new(self) end end @@ -172,12 +216,26 @@ module ActionDispatch # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores. def signed_or_encrypted @signed_or_encrypted ||= - if @options[:secret_key_base].present? + if request.secret_key_base.present? encrypted else signed end end + + protected + + def request; @parent_jar.request; end + + private + + def upgrade_legacy_signed_cookies? + request.secret_token.present? && request.secret_key_base.present? + end + + def key_generator + request.key_generator + end end # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream @@ -187,7 +245,7 @@ module ActionDispatch module VerifyAndUpgradeLegacySignedMessage # :nodoc: def initialize(*args) super - @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer) + @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end def verify_and_upgrade_legacy_signed_message(name, signed_message) @@ -216,34 +274,18 @@ module ActionDispatch # $& => example.local DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/ - def self.options_for_env(env) #:nodoc: - { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '', - encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '', - encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '', - secret_token: env[SECRET_TOKEN], - secret_key_base: env[SECRET_KEY_BASE], - upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?, - serializer: env[COOKIES_SERIALIZER], - digest: env[COOKIES_DIGEST] - } - end - - def self.build(env, host, secure, cookies) - key_generator = env[GENERATOR_KEY] - options = options_for_env env - - new(key_generator, host, secure, options).tap do |hash| + def self.build(req, cookies) + new(req).tap do |hash| hash.update(cookies) end end - def initialize(key_generator, host = nil, secure = false, options = {}) - @key_generator = key_generator + attr_reader :request + + def initialize(request) @set_cookies = {} @delete_cookies = {} - @host = host - @secure = secure - @options = options + @request = request @cookies = {} @committed = false end @@ -292,12 +334,12 @@ module ActionDispatch # if host is not ip and matches domain regexp # (ip confirms to domain regexp so we explicitly check for ip) - options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp) + options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp) ".#{$&}" end elsif options[:domain].is_a? Array # if host matches one of the supplied domains without a dot in front of it - options[:domain] = options[:domain].find {|domain| @host.include? domain.sub(/^\./, '') } + options[:domain] = options[:domain].find {|domain| request.host.include? domain.sub(/^\./, '') } end end @@ -356,27 +398,20 @@ module ActionDispatch @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } end - def recycle! #:nodoc: - @set_cookies = {} - @delete_cookies = {} - end - mattr_accessor :always_write_cookie self.always_write_cookie = false private def write_cookie?(cookie) - @secure || !cookie[:secure] || always_write_cookie + request.ssl? || !cookie[:secure] || always_write_cookie end end class PermanentCookieJar #:nodoc: include ChainedCookieJars - def initialize(parent_jar, key_generator, options = {}) + def initialize(parent_jar) @parent_jar = parent_jar - @key_generator = key_generator - @options = options end def [](name) @@ -410,7 +445,7 @@ module ActionDispatch protected def needs_migration?(value) - @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE) + request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE) end def serialize(value) @@ -430,7 +465,7 @@ module ActionDispatch end def serializer - serializer = @options[:serializer] || :marshal + serializer = request.cookies_serializer || :marshal case serializer when :marshal Marshal @@ -442,7 +477,7 @@ module ActionDispatch end def digest - @options[:digest] || 'SHA1' + request.cookies_digest || 'SHA1' end end @@ -450,10 +485,9 @@ module ActionDispatch include ChainedCookieJars include SerializedCookieJars - def initialize(parent_jar, key_generator, options = {}) + def initialize(parent_jar) @parent_jar = parent_jar - @options = options - secret = key_generator.generate_key(@options[:signed_cookie_salt]) + secret = key_generator.generate_key(request.signed_cookie_salt) @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end @@ -505,16 +539,16 @@ module ActionDispatch include ChainedCookieJars include SerializedCookieJars - def initialize(parent_jar, key_generator, options = {}) + def initialize(parent_jar) + @parent_jar = parent_jar + if ActiveSupport::LegacyKeyGenerator === key_generator raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " + "Read the upgrade documentation to learn more about this new config option." end - @parent_jar = parent_jar - @options = options - secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) - sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt]) + secret = key_generator.generate_key(request.encrypted_cookie_salt || '') + sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || '') @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end @@ -568,9 +602,12 @@ module ActionDispatch end def call(env) + request = ActionDispatch::Request.new env + status, headers, body = @app.call(env) - if cookie_jar = env['action_dispatch.cookies'] + if request.have_cookie_jar? + cookie_jar = request.cookie_jar unless cookie_jar.committed? cookie_jar.write(headers) if headers[HTTP_HEADER].respond_to?(:join) diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 9082aac271..66bb74b9c5 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -44,6 +44,7 @@ module ActionDispatch end def call(env) + request = ActionDispatch::Request.new env _, headers, body = response = @app.call(env) if headers['X-Cascade'] == 'pass' @@ -53,18 +54,18 @@ module ActionDispatch response rescue Exception => exception - raise exception if env['action_dispatch.show_exceptions'] == false - render_exception(env, exception) + raise exception unless request.show_exceptions? + render_exception(request, exception) end private - def render_exception(env, exception) - wrapper = ExceptionWrapper.new(env, exception) - log_error(env, wrapper) + def render_exception(request, exception) + backtrace_cleaner = request.get_header('action_dispatch.backtrace_cleaner') + wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) + log_error(request, wrapper) - if env['action_dispatch.show_detailed_exceptions'] - request = Request.new(env) + if request.get_header('action_dispatch.show_detailed_exceptions') traces = wrapper.traces trace_to_show = 'Application Trace' @@ -106,8 +107,8 @@ module ActionDispatch [status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]] end - def log_error(env, wrapper) - logger = logger(env) + def log_error(request, wrapper) + logger = logger(request) return unless logger exception = wrapper.exception @@ -123,8 +124,8 @@ module ActionDispatch end end - def logger(env) - env['action_dispatch.logger'] || stderr_logger + def logger(request) + request.logger || stderr_logger end def stderr_logger diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 8c3d45584d..039efc3af8 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -31,10 +31,10 @@ module ActionDispatch 'ActionView::Template::Error' => 'template_error' ) - attr_reader :env, :exception, :line_number, :file + attr_reader :backtrace_cleaner, :exception, :line_number, :file - def initialize(env, exception) - @env = env + def initialize(backtrace_cleaner, exception) + @backtrace_cleaner = backtrace_cleaner @exception = original_exception(exception) expand_backtrace if exception.is_a?(SyntaxError) || exception.try(:original_exception).try(:is_a?, SyntaxError) @@ -125,10 +125,6 @@ module ActionDispatch end end - def backtrace_cleaner - @backtrace_cleaner ||= @env['action_dispatch.backtrace_cleaner'] - end - def source_fragment(path, line) return unless Rails.respond_to?(:root) && Rails.root full_path = Rails.root.join(path) diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 59639a010e..6041f84834 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -6,7 +6,17 @@ module ActionDispatch # read a notice you put there or <tt>flash["notice"] = "hello"</tt> # to put a new one. def flash - @env[Flash::KEY] ||= Flash::FlashHash.from_session_value(session["flash"]) + flash = flash_hash + return flash if flash + self.flash = Flash::FlashHash.from_session_value(session["flash"]) + end + + def flash=(flash) + set_header Flash::KEY, flash + end + + def flash_hash # :nodoc: + get_header Flash::KEY end end @@ -263,14 +273,15 @@ module ActionDispatch end def call(env) + req = ActionDispatch::Request.new env @app.call(env) ensure - session = Request::Session.find(env) || {} - flash_hash = env[KEY] + session = Request::Session.find(req) || {} + flash_hash = req.flash_hash if flash_hash && (flash_hash.present? || session.key?('flash')) session["flash"] = flash_hash.to_session_value - env[KEY] = flash_hash.dup + req.flash = flash_hash.dup end if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index e2b3b06fd8..402ad778fa 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -1,9 +1,14 @@ -require 'active_support/core_ext/hash/conversions' require 'action_dispatch/http/request' -require 'active_support/core_ext/hash/indifferent_access' module ActionDispatch + # ActionDispatch::ParamsParser works for all the requests having any Content-Length + # (like POST). It takes raw data from the request and puts it through the parser + # that is picked based on Content-Type header. + # + # In case of any error while parsing data ParamsParser::ParseError is raised. class ParamsParser + # Raised when raw data from the request cannot be parsed by the parser + # defined for request's content mime type. class ParseError < StandardError attr_reader :original_exception @@ -17,39 +22,42 @@ module ActionDispatch Mime::JSON => lambda { |raw_post| data = ActiveSupport::JSON.decode(raw_post) data = {:_json => data} unless data.is_a?(Hash) - Request::Utils.deep_munge(data).with_indifferent_access + Request::Utils.normalize_encode_params(data) } } + # Create a new +ParamsParser+ middleware instance. + # + # The +parsers+ argument can take Hash of parsers where key is identifying + # content mime type, and value is a lambda that is going to process data. def initialize(app, parsers = {}) @app, @parsers = app, DEFAULT_PARSERS.merge(parsers) end def call(env) - default = env["action_dispatch.request.request_parameters"] - env["action_dispatch.request.request_parameters"] = parse_formatted_parameters(env, @parsers, default) + request = Request.new(env) + + request.request_parameters = parse_formatted_parameters(request, @parsers) @app.call(env) end private - def parse_formatted_parameters(env, parsers, default) - request = Request.new(env) - - return default if request.content_length.zero? + def parse_formatted_parameters(request, parsers) + return if request.content_length.zero? - strategy = parsers.fetch(request.content_mime_type) { return default } + strategy = parsers.fetch(request.content_mime_type) { return nil } strategy.call(request.raw_post) rescue => e # JSON or Ruby code block errors - logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" + logger(request).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" raise ParseError.new(e.message, e) end - def logger(env) - env['action_dispatch.logger'] || ActiveSupport::Logger.new($stderr) + def logger(request) + request.logger || ActiveSupport::Logger.new($stderr) end end end diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb index 7cde76b30e..0f27984550 100644 --- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb @@ -17,8 +17,8 @@ module ActionDispatch end def call(env) - status = env["PATH_INFO"][1..-1].to_i request = ActionDispatch::Request.new(env) + status = request.path_info[1..-1].to_i content_type = request.formats.first body = { :status => status, :error => Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) } diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 9f894e2ec6..aee2334da9 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -74,16 +74,17 @@ module ActionDispatch # requests. For those requests that do need to know the IP, the # GetIp#calculate_ip method will calculate the memoized client IP address. def call(env) - env["action_dispatch.remote_ip"] = GetIp.new(env, check_ip, proxies) - @app.call(env) + req = ActionDispatch::Request.new env + req.remote_ip = GetIp.new(req, check_ip, proxies) + @app.call(req.env) end # The GetIp class exists as a way to defer processing of the request data # into an actual IP address. If the ActionDispatch::Request#remote_ip method # is called, this class will calculate the value and then memoize it. class GetIp - def initialize(env, check_ip, proxies) - @env = env + def initialize(req, check_ip, proxies) + @req = req @check_ip = check_ip @proxies = proxies end @@ -108,11 +109,11 @@ module ActionDispatch # the last address left, which was presumably set by one of those proxies. def calculate_ip # Set by the Rack web server, this is a single value. - remote_addr = ips_from('REMOTE_ADDR').last + remote_addr = ips_from(@req.remote_addr).last # Could be a CSV list and/or repeated headers that were concatenated. - client_ips = ips_from('HTTP_CLIENT_IP').reverse - forwarded_ips = ips_from('HTTP_X_FORWARDED_FOR').reverse + client_ips = ips_from(@req.client_ip).reverse + forwarded_ips = ips_from(@req.x_forwarded_for).reverse # +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set. # If they are both set, it means that this request passed through two @@ -123,8 +124,8 @@ module ActionDispatch if should_check_ip && !forwarded_ips.include?(client_ips.last) # We don't know which came from the proxy, and which from the user raise IpSpoofAttackError, "IP spoofing attack?! " + - "HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect} " + - "HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}" + "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " + + "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}" end # We assume these things about the IP headers: @@ -147,8 +148,9 @@ module ActionDispatch protected def ips_from(header) + return [] unless header # Split the comma-separated list into an array of strings - ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : [] + ips = header.strip.split(/[,\s]+/) ips.select do |ip| begin # Only return IPs that are valid according to the IPAddr#new method diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 84df55fd5a..b924df789f 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -36,6 +36,11 @@ module ActionDispatch @default_options.delete(:sidbits) @default_options.delete(:secure_random) end + + private + def make_request(env) + ActionDispatch::Request.new env + end end module StaleSessionCheck @@ -65,8 +70,8 @@ module ActionDispatch end module SessionObject # :nodoc: - def prepare_session(env) - Request::Session.create(self, env, @default_options) + def prepare_session(req) + Request::Session.create(self, req, @default_options) end def loaded_session?(session) @@ -81,8 +86,7 @@ module ActionDispatch private - def set_cookie(env, session_id, cookie) - request = ActionDispatch::Request.new(env) + def set_cookie(request, session_id, cookie) request.cookie_jar[key] = cookie end end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index d8f9614904..e225f356df 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -71,16 +71,16 @@ module ActionDispatch super(app, options.merge!(:cookie_only => true)) end - def destroy_session(env, session_id, options) + def destroy_session(req, session_id, options) new_sid = generate_sid unless options[:drop] # Reset hash and Assign the new session id - env["action_dispatch.request.unsigned_session_cookie"] = new_sid ? { "session_id" => new_sid } : {} + req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid } : {}) new_sid end - def load_session(env) + def load_session(req) stale_session_check! do - data = unpacked_cookie_data(env) + data = unpacked_cookie_data(req) data = persistent_session_id!(data) [data["session_id"], data] end @@ -88,20 +88,21 @@ module ActionDispatch private - def extract_session_id(env) + def extract_session_id(req) stale_session_check! do - unpacked_cookie_data(env)["session_id"] + unpacked_cookie_data(req)["session_id"] end end - def unpacked_cookie_data(env) - env["action_dispatch.request.unsigned_session_cookie"] ||= begin - stale_session_check! do - if data = get_cookie(env) + def unpacked_cookie_data(req) + req.get_header("action_dispatch.request.unsigned_session_cookie") do |k| + v = stale_session_check! do + if data = get_cookie(req) data.stringify_keys! end data || {} end + req.set_header k, v end end @@ -111,21 +112,20 @@ module ActionDispatch data end - def set_session(env, sid, session_data, options) + def set_session(req, sid, session_data, options) session_data["session_id"] = sid session_data end - def set_cookie(env, session_id, cookie) - cookie_jar(env)[@key] = cookie + def set_cookie(request, session_id, cookie) + cookie_jar(request)[@key] = cookie end - def get_cookie(env) - cookie_jar(env)[@key] + def get_cookie(req) + cookie_jar(req)[@key] end - def cookie_jar(env) - request = ActionDispatch::Request.new(env) + def cookie_jar(request) request.cookie_jar.signed_or_encrypted end end diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index f0779279c1..64695f9738 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -27,24 +27,26 @@ module ActionDispatch end def call(env) + request = ActionDispatch::Request.new env @app.call(env) rescue Exception => exception - if env['action_dispatch.show_exceptions'] == false - raise exception + if request.show_exceptions? + render_exception(request, exception) else - render_exception(env, exception) + raise exception end end private - def render_exception(env, exception) - wrapper = ExceptionWrapper.new(env, exception) + def render_exception(request, exception) + backtrace_cleaner = request.get_header 'action_dispatch.backtrace_cleaner' + wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) status = wrapper.status_code - env["action_dispatch.exception"] = wrapper.exception - env["action_dispatch.original_path"] = env["PATH_INFO"] - env["PATH_INFO"] = "/#{status}" - response = @exceptions_app.call(env) + request.set_header "action_dispatch.exception", wrapper.exception + request.set_header "action_dispatch.original_path", request.path_info + request.path_info = "/#{status}" + response = @exceptions_app.call(request.env) response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response rescue Exception => failsafe_error $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}" diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index bbf734f103..0430ce3b9a 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -4,36 +4,15 @@ require "active_support/dependencies" module ActionDispatch class MiddlewareStack class Middleware - attr_reader :args, :block, :name, :classcache + attr_reader :args, :block, :klass - def initialize(klass_or_name, *args, &block) - @klass = nil - - if klass_or_name.respond_to?(:name) - @klass = klass_or_name - @name = @klass.name - else - @name = klass_or_name.to_s - end - - @classcache = ActiveSupport::Dependencies::Reference - @args, @block = args, block + def initialize(klass, args, block) + @klass = klass + @args = args + @block = block end - def klass - @klass || classcache[@name] - end - - def ==(middleware) - case middleware - when Middleware - klass == middleware.klass - when Class - klass == middleware - else - normalize(@name) == normalize(middleware) - end - end + def name; klass.name; end def inspect klass.to_s @@ -42,12 +21,6 @@ module ActionDispatch def build(app) klass.new(app, *args, &block) end - - private - - def normalize(object) - object.to_s.strip.sub(/^::/, '') - end end include Enumerable @@ -75,19 +48,17 @@ module ActionDispatch middlewares[i] end - def unshift(*args, &block) - middleware = self.class::Middleware.new(*args, &block) - middlewares.unshift(middleware) + def unshift(klass, *args, &block) + middlewares.unshift(build_middleware(klass, args, block)) end def initialize_copy(other) self.middlewares = other.middlewares.dup end - def insert(index, *args, &block) + def insert(index, klass, *args, &block) index = assert_index(index, :before) - middleware = self.class::Middleware.new(*args, &block) - middlewares.insert(index, middleware) + middlewares.insert(index, build_middleware(klass, args, block)) end alias_method :insert_before, :insert @@ -104,26 +75,48 @@ module ActionDispatch end def delete(target) - middlewares.delete target + target = get_class target + middlewares.delete_if { |m| m.klass == target } end - def use(*args, &block) - middleware = self.class::Middleware.new(*args, &block) - middlewares.push(middleware) + def use(klass, *args, &block) + middlewares.push(build_middleware(klass, args, block)) end - def build(app = nil, &block) - app ||= block - raise "MiddlewareStack#build requires an app" unless app + def build(app = Proc.new) middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) } end protected def assert_index(index, where) - i = index.is_a?(Integer) ? index : middlewares.index(index) + index = get_class index + i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index } raise "No such middleware to insert #{where}: #{index.inspect}" unless i i end + + private + + def get_class(klass) + if klass.is_a?(String) || klass.is_a?(Symbol) + classcache = ActiveSupport::Dependencies::Reference + converted_klass = classcache[klass.to_s] + ActiveSupport::Deprecation.warn <<-eowarn +Passing strings or symbols to the middleware builder is deprecated, please change +them to actual class references. For example: + + "#{klass}" => #{converted_klass} + + eowarn + converted_klass + else + klass + end + end + + def build_middleware(klass, args, block) + Middleware.new(get_class(klass), args, block) + end end end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index b098ea389f..9462ae4278 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 @@ -48,26 +48,30 @@ module ActionDispatch end def call(env) - path = env['PATH_INFO'] + serve ActionDispatch::Request.new env + end + + def serve(request) + path = request.path_info gzip_path = gzip_file_path(path) - if gzip_path && gzip_encoding_accepted?(env) - env['PATH_INFO'] = gzip_path - status, headers, body = @file_server.call(env) + if gzip_path && gzip_encoding_accepted?(request) + request.path_info = gzip_path + status, headers, body = @file_server.call(request.env) if status == 304 return [status, headers, body] end headers['Content-Encoding'] = 'gzip' headers['Content-Type'] = content_type(path) else - status, headers, body = @file_server.call(env) + status, headers, body = @file_server.call(request.env) end headers['Vary'] = 'Accept-Encoding' if gzip_path return [status, headers, body] ensure - env['PATH_INFO'] = path + request.path_info = path end private @@ -76,11 +80,11 @@ 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) - env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/i + def gzip_encoding_accepted?(request) + request.accept_encoding =~ /\bgzip\b/i end def gzip_file_path(path) @@ -110,16 +114,17 @@ module ActionDispatch end def call(env) - case env['REQUEST_METHOD'] - when 'GET', 'HEAD' - path = env['PATH_INFO'].chomp('/') + req = ActionDispatch::Request.new env + + if req.get? || req.head? + path = req.path_info.chomp('/'.freeze) if match = @file_handler.match?(path) - env['PATH_INFO'] = match - return @file_handler.call(env) + req.path_info = match + return @file_handler.serve(req) end end - @app.call(env) + @app.call(req.env) end end end diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index a8a3cd20b9..b946ccb49f 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -4,38 +4,38 @@ module ActionDispatch class Request < Rack::Request # Session is responsible for lazily loading the session from store. class Session # :nodoc: - ENV_SESSION_KEY = Rack::Session::Abstract::ENV_SESSION_KEY # :nodoc: - ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY # :nodoc: + ENV_SESSION_KEY = Rack::RACK_SESSION # :nodoc: + ENV_SESSION_OPTIONS_KEY = Rack::RACK_SESSION_OPTIONS # :nodoc: # Singleton object used to determine if an optional param wasn't specified Unspecified = Object.new # Creates a session hash, merging the properties of the previous session if any - def self.create(store, env, default_options) - session_was = find env - session = Request::Session.new(store, env) + def self.create(store, req, default_options) + session_was = find req + session = Request::Session.new(store, req) session.merge! session_was if session_was - set(env, session) - Options.set(env, Request::Session::Options.new(store, default_options)) + set(req, session) + Options.set(req, Request::Session::Options.new(store, default_options)) session end - def self.find(env) - env[ENV_SESSION_KEY] + def self.find(req) + req.get_header ENV_SESSION_KEY end - def self.set(env, session) - env[ENV_SESSION_KEY] = session + def self.set(req, session) + req.set_header ENV_SESSION_KEY, session end class Options #:nodoc: - def self.set(env, options) - env[ENV_SESSION_OPTIONS_KEY] = options + def self.set(req, options) + req.set_header ENV_SESSION_OPTIONS_KEY, options end - def self.find(env) - env[ENV_SESSION_OPTIONS_KEY] + def self.find(req) + req.get_header ENV_SESSION_OPTIONS_KEY end def initialize(by, default_options) @@ -47,9 +47,9 @@ module ActionDispatch @delegate[key] end - def id(env) + def id(req) @delegate.fetch(:id) { - @by.send(:extract_session_id, env) + @by.send(:extract_session_id, req) } end @@ -58,26 +58,26 @@ module ActionDispatch def values_at(*args); @delegate.values_at(*args); end end - def initialize(by, env) + def initialize(by, req) @by = by - @env = env + @req = req @delegate = {} @loaded = false @exists = nil # we haven't checked yet end def id - options.id(@env) + options.id(@req) end def options - Options.find @env + Options.find @req end def destroy clear options = self.options || {} - @by.send(:destroy_session, @env, options.id(@env), options) + @by.send(:destroy_session, @req, options.id(@req), options) # Load the new sid to be written with the response @loaded = false @@ -181,7 +181,7 @@ module ActionDispatch def exists? return @exists unless @exists.nil? - @exists = @by.send(:session_exists?, @env) + @exists = @by.send(:session_exists?, @req) end def loaded? @@ -209,7 +209,7 @@ module ActionDispatch end def load! - id, session = @by.load_session @env + id, session = @by.load_session @req options[:id] = id @delegate.replace(stringify_keys(session)) @loaded = true 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..51bafb539f 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -5,6 +5,7 @@ require 'active_support/core_ext/enumerable' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/module/remove_method' require 'active_support/inflector' +require 'active_support/deprecation' require 'action_dispatch/routing/redirection' require 'action_dispatch/routing/endpoint' @@ -16,7 +17,10 @@ module ActionDispatch class Constraints < Endpoint #:nodoc: attr_reader :app, :constraints - def initialize(app, constraints, dispatcher_p) + SERVE = ->(app, req) { app.serve req } + CALL = ->(app, req) { app.call req.env } + + def initialize(app, constraints, strategy) # Unwrap Constraints objects. I don't actually think it's possible # to pass a Constraints object to this constructor, but there were # multiple places that kept testing children of this object. I @@ -26,12 +30,12 @@ module ActionDispatch app = app.app end - @dispatcher = dispatcher_p + @strategy = strategy @app, @constraints, = app, constraints end - def dispatcher?; @dispatcher; end + def dispatcher?; @strategy == SERVE; end def matches?(req) @constraints.all? do |constraint| @@ -43,11 +47,7 @@ module ActionDispatch def serve(req) return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req) - if dispatcher? - @app.serve req - else - @app.call req.env - end + @strategy.call @app, req end private @@ -59,101 +59,168 @@ module ActionDispatch class Mapping #:nodoc: ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} - attr_reader :requirements, :conditions, :defaults - attr_reader :to, :default_controller, :default_action, :as, :anchor + attr_reader :requirements, :defaults + attr_reader :to, :default_controller, :default_action + attr_reader :required_defaults, :ast - def self.build(scope, set, path, as, options) + def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options) options = scope[:options].merge(options) if scope[:options] - options.delete :only - options.delete :except - options.delete :shallow_path - options.delete :shallow_prefix - options.delete :shallow + defaults = (scope[:defaults] || {}).dup + scope_constraints = scope[:constraints] || {} - defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {} + new set, ast, defaults, controller, default_action, scope[:module], to, formatted, scope_constraints, scope[:blocks] || [], via, options_constraints, anchor, options + end - new scope, set, path, defaults, as, options + def self.check_via(via) + if via.empty? + msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \ + "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \ + "If you want to expose your action to GET, use `get` in the router:\n" \ + " Instead of: match \"controller#action\"\n" \ + " Do: get \"controller#action\"" + raise ArgumentError, msg + end + via end - def initialize(scope, set, path, defaults, as, options) - @requirements, @conditions = {}, {} - @defaults = defaults - @set = set + def self.normalize_path(path, format) + path = Mapper.normalize_path(path) + + if format == true + "#{path}.:format" + elsif optional_format?(path, format) + "#{path}(.:format)" + else + path + end + end - @to = options.delete :to - @default_controller = options.delete(:controller) || scope[:controller] - @default_action = options.delete(:action) || scope[:action] - @as = as - @anchor = options.delete :anchor + def self.optional_format?(path, format) + format != false && !path.include?(':format') && !path.end_with?('/') + end - formatted = options.delete :format - via = Array(options.delete(:via) { [] }) - options_constraints = options.delete :constraints + def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options) + @defaults = defaults + @set = set - path = normalize_path! path, formatted - ast = path_ast path - path_params = path_params ast + @to = to + @default_controller = controller + @default_action = default_action + @ast = ast + @anchor = anchor + @via = via - options = normalize_options!(options, formatted, path_params, ast, scope[:module]) + path_params = ast.find_all(&:symbol?).map(&:to_sym) + options = add_wildcard_options(options, formatted, ast) - split_constraints(path_params, scope[:constraints]) if scope[:constraints] - constraints = constraints(options, path_params) + options = normalize_options!(options, path_params, modyoule) - split_constraints path_params, constraints + split_options = constraints(options, path_params) - @blocks = blocks(options_constraints, scope[:blocks]) + constraints = scope_constraints.merge Hash[split_options[:constraints] || []] if options_constraints.is_a?(Hash) - split_constraints path_params, options_constraints - options_constraints.each do |key, default| - if URL_OPTIONS.include?(key) && (String === default || Fixnum === default) - @defaults[key] ||= default - end - end + @defaults = Hash[options_constraints.find_all { |key, default| + URL_OPTIONS.include?(key) && (String === default || Fixnum === default) + }].merge @defaults + @blocks = blocks + constraints.merge! options_constraints + else + @blocks = blocks(options_constraints) end - normalize_format!(formatted) + requirements, conditions = split_constraints path_params, constraints + verify_regexp_requirements requirements.map(&:last).grep(Regexp) + + formats = normalize_format(formatted) - @conditions[:path_info] = path - @conditions[:parsed_path_info] = ast + @requirements = formats[:requirements].merge Hash[requirements] + @conditions = Hash[conditions] + @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options)) - add_request_method(via, @conditions) - normalize_defaults!(options) + @required_defaults = (split_options[:required_defaults] || []).map(&:first) end - def to_route - [ app(@blocks), conditions, requirements, defaults, as, anchor ] + def make_route(name, precedence) + route = Journey::Route.new(name, + application, + path, + conditions, + required_defaults, + defaults, + request_method, + precedence) + + route end - private + def application + app(@blocks) + end - def normalize_path!(path, format) - path = Mapper.normalize_path(path) + def path + build_path @ast, requirements, @anchor + end - if format == true - "#{path}.:format" - elsif optional_format?(path, format) - "#{path}(.:format)" - else - path - end - end + def conditions + build_conditions @conditions, @set.request_class + end - def optional_format?(path, format) - format != false && !path.include?(':format') && !path.end_with?('/') + def build_conditions(current_conditions, request_class) + conditions = current_conditions.dup + + conditions.keep_if do |k, _| + request_class.public_method_defined?(k) end + end + private :build_conditions + + def request_method + @via.map { |x| Journey::Route.verb_matcher(x) } + end + private :request_method + + JOINED_SEPARATORS = SEPARATORS.join # :nodoc: - def normalize_options!(options, formatted, path_params, path_ast, modyoule) + def build_path(ast, requirements, anchor) + pattern = Journey::Path::Pattern.new(ast, requirements, JOINED_SEPARATORS, anchor) + + # Get all the symbol nodes followed by literals that are not the + # dummy node. + symbols = ast.find_all { |n| + n.cat? && n.left.symbol? && n.right.cat? && n.right.left.literal? + }.map(&:left) + + # Get all the symbol nodes preceded by literals. + symbols.concat ast.find_all { |n| + n.cat? && n.left.literal? && n.right.cat? && n.right.left.symbol? + }.map { |n| n.right.left } + + symbols.each { |x| + x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/ + } + + pattern + end + private :build_path + + + private + def add_wildcard_options(options, formatted, path_ast) # Add a constraint for wildcard route to make it non-greedy and match the # optional format part of the route by default if formatted != false - path_ast.grep(Journey::Nodes::Star) do |node| - options[node.name.to_sym] ||= /.+?/ - end + path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash| + hash[node.name.to_sym] ||= /.+?/ + }.merge options + else + options end + end + def normalize_options!(options, path_params, modyoule) if path_params.include?(:controller) raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule @@ -178,74 +245,50 @@ module ActionDispatch end def split_constraints(path_params, constraints) - constraints.each_pair do |key, requirement| - if path_params.include?(key) || key == :controller - verify_regexp_requirement(requirement) if requirement.is_a?(Regexp) - @requirements[key] = requirement - else - @conditions[key] = requirement - end + constraints.partition do |key, requirement| + path_params.include?(key) || key == :controller end end - def normalize_format!(formatted) - if formatted == true - @requirements[:format] ||= /.+/ - elsif Regexp === formatted - @requirements[:format] = formatted - @defaults[:format] = nil - elsif String === formatted - @requirements[:format] = Regexp.compile(formatted) - @defaults[:format] = formatted + def normalize_format(formatted) + case formatted + when true + { requirements: { format: /.+/ }, + defaults: {} } + when Regexp + { requirements: { format: formatted }, + defaults: { format: nil } } + when String + { requirements: { format: Regexp.compile(formatted) }, + defaults: { format: formatted } } + else + { requirements: { }, defaults: { } } end end - def verify_regexp_requirement(requirement) - if requirement.source =~ ANCHOR_CHARACTERS_REGEX - raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" - end - - if requirement.multiline? - raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}" - end - end + def verify_regexp_requirements(requirements) + requirements.each do |requirement| + if requirement.source =~ ANCHOR_CHARACTERS_REGEX + raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" + end - def normalize_defaults!(options) - options.each_pair do |key, default| - unless Regexp === default - @defaults[key] = default + if requirement.multiline? + raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}" end end end - def verify_callable_constraint(callable_constraint) - unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?) - raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?" - end - end - - def add_request_method(via, conditions) - return if via == [:all] - - if via.empty? - msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \ - "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \ - "If you want to expose your action to GET, use `get` in the router:\n" \ - " Instead of: match \"controller#action\"\n" \ - " Do: get \"controller#action\"" - raise ArgumentError, msg - end - - conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase } + def normalize_defaults(options) + Hash[options.reject { |_, default| Regexp === default }] end def app(blocks) if to.respond_to?(:call) - Constraints.new(to, blocks, false) + Constraints.new(to, blocks, Constraints::CALL) elsif blocks.any? - Constraints.new(dispatcher(defaults), blocks, true) + Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE) else - dispatcher(defaults) + dispatcher(defaults.key?(:controller)) end end @@ -303,40 +346,29 @@ module ActionDispatch yield end - def blocks(options_constraints, scope_blocks) - if options_constraints && !options_constraints.is_a?(Hash) - verify_callable_constraint(options_constraints) - [options_constraints] - else - scope_blocks || [] + def blocks(callable_constraint) + unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?) + raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?" end + [callable_constraint] end def constraints(options, path_params) - constraints = {} - required_defaults = [] - options.each_pair do |key, option| + options.group_by do |key, option| if Regexp === option - constraints[key] = option + :constraints else - required_defaults << key unless path_params.include?(key) + if path_params.include?(key) + :path_params + else + :required_defaults + end end end - @conditions[:required_defaults] = required_defaults - constraints - end - - def path_params(ast) - ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym } - end - - def path_ast(path) - parser = Journey::Parser.new - parser.parse path end - def dispatcher(defaults) - @set.dispatcher defaults + def dispatcher(raise_on_name_error) + @set.dispatcher raise_on_name_error end end @@ -443,6 +475,21 @@ module ActionDispatch # dynamic segment used to generate the routes). # You can access that segment from your controller using # <tt>params[<:param>]</tt>. + # In your router: + # + # resources :user, param: :name + # + # You can override <tt>ActiveRecord::Base#to_param</tt> of a related + # model to construct an URL: + # + # class User < ActiveRecord::Base + # def to_param + # name + # end + # end + # + # user = User.find_by(name: 'Phusion') + # user_path(user) # => "/users/Phusion" # # [:path] # The path prefix for the routes. @@ -588,7 +635,7 @@ module ActionDispatch # Query if the following named route was already defined. def has_named_route?(name) - @set.named_routes.routes[name.to_sym] + @set.named_routes.key? name end private @@ -670,7 +717,11 @@ module ActionDispatch def map_method(method, args, &block) options = args.extract_options! options[:via] = method - match(*args, options, &block) + if options.key?(:defaults) + defaults(options.delete(:defaults)) { match(*args, options, &block) } + else + match(*args, options, &block) + end self end end @@ -773,8 +824,8 @@ module ActionDispatch end if options[:constraints].is_a?(Hash) - defaults = options[:constraints].select do - |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) + defaults = options[:constraints].select do |k, v| + URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) end (options[:defaults] ||= {}).reverse_merge!(defaults) @@ -782,16 +833,25 @@ module ActionDispatch block, options[:constraints] = options[:constraints], {} end + if options.key?(:only) || options.key?(:except) + scope[:action_options] = { only: options.delete(:only), + except: options.delete(:except) } + end + + if options.key? :anchor + raise ArgumentError, 'anchor is ignored unless passed to `match`' + end + @scope.options.each do |option| if option == :blocks value = block elsif option == :options value = options else - value = options.delete(option) + value = options.delete(option) { POISON } end - if value + unless POISON == value scope[option] = send("merge_#{option}_scope", @scope[option], value) end end @@ -803,14 +863,18 @@ module ActionDispatch @scope = @scope.parent end + POISON = Object.new # :nodoc: + # Scopes routes to a specific controller # # controller "food" do # match "bacon", action: :bacon, via: :get # end - def controller(controller, options={}) - options[:controller] = controller - scope(options) { yield } + def controller(controller) + @scope = @scope.new(controller: controller) + yield + ensure + @scope = @scope.parent end # Scopes routes to a specific namespace. For example: @@ -856,13 +920,14 @@ module ActionDispatch defaults = { module: path, - path: options.fetch(:path, path), as: options.fetch(:as, path), shallow_path: options.fetch(:path, path), shallow_prefix: options.fetch(:as, path) } - scope(defaults.merge!(options)) { yield } + path_scope(options.delete(:path) { path }) do + scope(defaults.merge!(options)) { yield } + end end # === Parameter Restriction @@ -930,7 +995,10 @@ module ActionDispatch # end # Using this, the +:id+ parameter here will default to 'home'. def defaults(defaults = {}) - scope(:defaults => defaults) { yield } + @scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults)) + yield + ensure + @scope = @scope.parent end private @@ -962,6 +1030,14 @@ module ActionDispatch child end + def merge_via_scope(parent, child) #:nodoc: + child + end + + def merge_format_scope(parent, child) #:nodoc: + child + end + def merge_path_names_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end @@ -981,16 +1057,12 @@ module ActionDispatch end def merge_options_scope(parent, child) #:nodoc: - (parent || {}).except(*override_keys(child)).merge!(child) + (parent || {}).merge(child) end def merge_shallow_scope(parent, child) #:nodoc: child ? true : false end - - def override_keys(child) #:nodoc: - child.key?(:only) || child.key?(:except) ? [:only, :except] : [] - end end # Resource routing allows you to quickly declare all of the common routes @@ -1040,17 +1112,19 @@ module ActionDispatch CANONICAL_ACTIONS = %w(index create new show update destroy) class Resource #:nodoc: - attr_reader :controller, :path, :options, :param + attr_reader :controller, :path, :param - def initialize(entities, api_only = false, options = {}) + def initialize(entities, api_only, shallow, options = {}) @name = entities.to_s @path = (options[:path] || @name).to_s @controller = (options[:controller] || @name).to_s @as = options[:as] @param = (options[:param] || :id).to_sym @options = options - @shallow = false + @shallow = shallow @api_only = api_only + @only = options.delete :only + @except = options.delete :except end def default_actions @@ -1062,10 +1136,10 @@ module ActionDispatch end def actions - if only = @options[:only] - Array(only).map(&:to_sym) - elsif except = @options[:except] - default_actions - Array(except).map(&:to_sym) + if @only + Array(@only).map(&:to_sym) + elsif @except + default_actions - Array(@except).map(&:to_sym) else default_actions end @@ -1092,7 +1166,7 @@ module ActionDispatch end def resource_scope - { :controller => controller } + controller end alias :collection_scope :path @@ -1115,17 +1189,15 @@ module ActionDispatch "#{path}/:#{nested_param}" end - def shallow=(value) - @shallow = value - end - def shallow? @shallow end + + def singleton?; false; end end class SingletonResource < Resource #:nodoc: - def initialize(entities, api_only, options) + def initialize(entities, api_only, shallow, options) super @as = nil @controller = (options[:controller] || plural).to_s @@ -1153,6 +1225,8 @@ module ActionDispatch alias :member_scope :path alias :nested_scope :path + + def singleton?; true; end end def resources_path_names(options) @@ -1187,20 +1261,23 @@ module ActionDispatch return self end - resource_scope(:resource, SingletonResource.new(resources.pop, api_only?, options)) do - yield if block_given? + with_scope_level(:resource) do + options = apply_action_options options + resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do + yield if block_given? - concerns(options[:concerns]) if options[:concerns] + concerns(options[:concerns]) if options[:concerns] - collection do - post :create - end if parent_resource.actions.include?(:create) + collection do + post :create + end if parent_resource.actions.include?(:create) - new do - get :new - end if parent_resource.actions.include?(:new) + new do + get :new + end if parent_resource.actions.include?(:new) - set_member_mappings_for_resource + set_member_mappings_for_resource + end end self @@ -1345,21 +1422,24 @@ module ActionDispatch return self end - resource_scope(:resources, Resource.new(resources.pop, api_only?, options)) do - yield if block_given? + with_scope_level(:resources) do + options = apply_action_options options + resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do + yield if block_given? - concerns(options[:concerns]) if options[:concerns] + concerns(options[:concerns]) if options[:concerns] - collection do - get :index if parent_resource.actions.include?(:index) - post :create if parent_resource.actions.include?(:create) - end + collection do + get :index if parent_resource.actions.include?(:index) + post :create if parent_resource.actions.include?(:create) + end - new do - get :new - end if parent_resource.actions.include?(:new) + new do + get :new + end if parent_resource.actions.include?(:new) - set_member_mappings_for_resource + set_member_mappings_for_resource + end end self @@ -1383,7 +1463,7 @@ module ActionDispatch end with_scope_level(:collection) do - scope(parent_resource.collection_scope) do + path_scope(parent_resource.collection_scope) do yield end end @@ -1407,9 +1487,11 @@ module ActionDispatch with_scope_level(:member) do if shallow? - shallow_scope(parent_resource.member_scope) { yield } + shallow_scope { + path_scope(parent_resource.member_scope) { yield } + } else - scope(parent_resource.member_scope) { yield } + path_scope(parent_resource.member_scope) { yield } end end end @@ -1420,7 +1502,7 @@ module ActionDispatch end with_scope_level(:new) do - scope(parent_resource.new_scope(action_path(:new))) do + path_scope(parent_resource.new_scope(action_path(:new))) do yield end end @@ -1433,9 +1515,15 @@ module ActionDispatch with_scope_level(:nested) do if shallow? && shallow_nesting_depth >= 1 - shallow_scope(parent_resource.nested_scope, nested_options) { yield } + shallow_scope do + path_scope(parent_resource.nested_scope) do + scope(nested_options) { yield } + end + end else - scope(parent_resource.nested_scope, nested_options) { yield } + path_scope(parent_resource.nested_scope) do + scope(nested_options) { yield } + end end end end @@ -1450,13 +1538,14 @@ module ActionDispatch end def shallow - scope(:shallow => true) do - yield - end + @scope = @scope.new(shallow: true) + yield + ensure + @scope = @scope.parent end def shallow? - parent_resource.instance_of?(Resource) && @scope[:shallow] + !parent_resource.singleton? && @scope[:shallow] end # Matches a url pattern to one or more routes. @@ -1490,8 +1579,6 @@ module ActionDispatch paths = [path] + rest end - options[:anchor] = true unless options.key?(:anchor) - if options[:on] && !VALID_ON_OPTIONS.include?(options[:on]) raise ArgumentError, "Unknown scope #{on.inspect} given to :on" end @@ -1500,48 +1587,85 @@ module ActionDispatch options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}" end - paths.each do |_path| + controller = options.delete(:controller) || @scope[:controller] + option_path = options.delete :path + to = options.delete :to + via = Mapping.check_via Array(options.delete(:via) { + @scope[:via] + }) + formatted = options.delete(:format) { @scope[:format] } + anchor = options.delete(:anchor) { true } + options_constraints = options.delete(:constraints) || {} + + path_types = paths.group_by(&:class) + path_types.fetch(String, []).each do |_path| route_options = options.dup - route_options[:path] ||= _path if _path.is_a?(String) + if _path && option_path + ActiveSupport::Deprecation.warn <<-eowarn +Specifying strings for both :path and the route path is deprecated. Change things like this: + + match #{_path.inspect}, :path => #{option_path.inspect} - path_without_format = _path.to_s.sub(/\(\.:format\)$/, '') - if using_match_shorthand?(path_without_format, route_options) - route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1') - route_options[:to].tr!("-", "_") +to this: + + match #{option_path.inspect}, :as => #{_path.inspect}, :action => #{path.inspect} + eowarn + route_options[:action] = _path + route_options[:as] = _path + _path = option_path end + to = get_to_from_path(_path, to, route_options[:action]) + decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints) + end - decomposed_match(_path, route_options) + path_types.fetch(Symbol, []).each do |action| + route_options = options.dup + decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints) end + self end - def using_match_shorthand?(path, options) - path && (options[:to] || options[:action]).nil? && path =~ %r{^/?[-\w]+/[-\w/]+$} + def get_to_from_path(path, to, action) + return to if to || action + + path_without_format = path.sub(/\(\.:format\)$/, '') + if using_match_shorthand?(path_without_format) + path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1').tr("-", "_") + else + nil + end + end + + def using_match_shorthand?(path) + path =~ %r{^/?[-\w]+/[-\w/]+$} end - def decomposed_match(path, options) # :nodoc: + def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc: if on = options.delete(:on) - send(on) { decomposed_match(path, options) } + send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) } else case @scope.scope_level when :resources - nested { decomposed_match(path, options) } + nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) } when :resource - member { decomposed_match(path, options) } + member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) } else - add_route(path, options) + add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints) end end end - def add_route(action, options) # :nodoc: - path = path_for_action(action, options.delete(:path)) + def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc: + path = path_for_action(action, _path) raise ArgumentError, "path is required" if path.blank? - action = action.to_s.dup + action = action.to_s + + default_action = options.delete(:action) || @scope[:action] if action =~ /^[\w\-\/]+$/ - options[:action] ||= action.tr('-', '_') unless action.include?("/") + default_action ||= action.tr('-', '_') unless action.include?("/") else action = nil end @@ -1552,9 +1676,11 @@ module ActionDispatch name_for_action(options.delete(:as), action) end - mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options) - app, conditions, requirements, defaults, as, anchor = mapping.to_route - @set.add_route(app, conditions, requirements, defaults, as, anchor) + path = Mapping.normalize_path URI.parser.escape(path), formatted + ast = Journey::Parser.parse path + + mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options) + @set.add_route(mapping, ast, as, anchor) end def root(path, options={}) @@ -1568,7 +1694,7 @@ module ActionDispatch if @scope.resources? with_scope_level(:root) do - scope(parent_resource.path) do + path_scope(parent_resource.path) do super(options) end end @@ -1613,23 +1739,20 @@ module ActionDispatch return true end - unless action_options?(options) - options.merge!(scope_action_options) if scope_action_options? - end - false end - def action_options?(options) #:nodoc: - options[:only] || options[:except] + def apply_action_options(options) # :nodoc: + return options if action_options? options + options.merge scope_action_options end - def scope_action_options? #:nodoc: - @scope[:options] && (@scope[:options][:only] || @scope[:options][:except]) + def action_options?(options) #:nodoc: + options[:only] || options[:except] end def scope_action_options #:nodoc: - @scope[:options].slice(:only, :except) + @scope[:action_options] || {} end def resource_scope? #:nodoc: @@ -1644,18 +1767,6 @@ module ActionDispatch @scope.nested? end - def with_exclusive_scope - begin - @scope = @scope.new(:as => nil, :path => nil) - - with_scope_level(:exclusive) do - yield - end - ensure - @scope = @scope.parent - end - end - def with_scope_level(kind) @scope = @scope.new_level(kind) yield @@ -1663,16 +1774,11 @@ module ActionDispatch @scope = @scope.parent end - def resource_scope(kind, resource) #:nodoc: - resource.shallow = @scope[:shallow] + def resource_scope(resource) #:nodoc: @scope = @scope.new(:scope_level_resource => resource) - @nesting.push(resource) - with_scope_level(kind) do - scope(parent_resource.resource_scope) { yield } - end + controller(resource.resource_scope) { yield } ensure - @nesting.pop @scope = @scope.parent end @@ -1685,12 +1791,10 @@ module ActionDispatch options end - def nesting_depth #:nodoc: - @nesting.size - end - def shallow_nesting_depth #:nodoc: - @nesting.count(&:shallow?) + @scope.find_all { |node| + node.frame[:scope_level_resource] + }.count { |node| node.frame[:scope_level_resource].shallow? } end def param_constraint? #:nodoc: @@ -1705,27 +1809,28 @@ module ActionDispatch resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end - def shallow_scope(path, options = {}) #:nodoc: + def shallow_scope #:nodoc: scope = { :as => @scope[:shallow_prefix], :path => @scope[:shallow_path] } @scope = @scope.new scope - scope(path, options) { yield } + yield ensure @scope = @scope.parent end def path_for_action(action, path) #:nodoc: - if path.blank? && canonical_action?(action) + return "#{@scope[:path]}/#{path}" if path + + if canonical_action?(action) @scope[:path].to_s else - "#{@scope[:path]}/#{action_path(action, path)}" + "#{@scope[:path]}/#{action_path(action)}" end end - def action_path(name, path = nil) #:nodoc: - name = name.to_sym if name.is_a?(String) - path || @scope[:path_names][name] || name.to_s + def action_path(name) #:nodoc: + @scope[:path_names][name.to_sym] || name end def prefix_name_for_action(as, action) #:nodoc: @@ -1781,6 +1886,14 @@ module ActionDispatch def api_only? @set.api_only? end + private + + def path_scope(path) + @scope = @scope.new(path: merge_path_scope(@scope[:path], path)) + yield + ensure + @scope = @scope.parent + end end # Routing Concerns allow you to declare common routes that can be reused @@ -1891,14 +2004,14 @@ module ActionDispatch class Scope # :nodoc: OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, :controller, :action, :path_names, :constraints, - :shallow, :blocks, :defaults, :options] + :shallow, :blocks, :defaults, :via, :format, :options] RESOURCE_SCOPES = [:resource, :resources] RESOURCE_METHOD_SCOPES = [:collection, :member, :new] attr_reader :parent, :scope_level - def initialize(hash, parent = {}, scope_level = nil) + def initialize(hash, parent = NULL, scope_level = nil) @hash = hash @parent = parent @scope_level = scope_level @@ -1946,27 +2059,34 @@ module ActionDispatch end def new_level(level) - self.class.new(self, self, level) - end - - def fetch(key, &block) - @hash.fetch(key, &block) + self.class.new(frame, self, level) end def [](key) - @hash.fetch(key) { @parent[key] } + scope = find { |node| node.frame.key? key } + scope && scope.frame[key] end - def []=(k,v) - @hash[k] = v + include Enumerable + + def each + node = self + loop do + break if node.equal? NULL + yield node + node = node.parent + end end + + def frame; @hash; end + + NULL = Scope.new(nil, nil) end def initialize(set) #:nodoc: @set = set @scope = Scope.new({ :path_names => @set.resources_path_names }) @concerns = {} - @nesting = [] end include Base diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index 3c1c4fadf6..d6987f4d09 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -24,7 +24,7 @@ module ActionDispatch def serve(req) req.check_path_parameters! uri = URI.parse(path(req.path_parameters, req)) - + unless uri.host if relative_path?(uri.path) uri.path = "#{req.script_name}/#{uri.path}" @@ -32,7 +32,7 @@ module ActionDispatch uri.path = req.script_name.empty? ? "/" : req.script_name end end - + uri.scheme ||= req.scheme uri.host ||= req.host uri.port ||= req.port unless req.standard_port? @@ -124,7 +124,7 @@ module ActionDispatch url_options[:script_name] = request.script_name end end - + ActionDispatch::Http::URL.url_for url_options end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 454593b59f..af6f0de556 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,6 +1,5 @@ require 'action_dispatch/journey' require 'forwardable' -require 'thread_safe' require 'active_support/concern' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/hash/slice' @@ -21,64 +20,35 @@ module ActionDispatch alias inspect to_s class Dispatcher < Routing::Endpoint - def initialize(defaults) - @defaults = defaults - @controller_class_names = ThreadSafe::Cache.new + def initialize(raise_on_name_error) + @raise_on_name_error = raise_on_name_error end def dispatcher?; true; end def serve(req) - req.check_path_parameters! params = req.path_parameters - - prepare_params!(params) - - # Just raise undefined constant errors if a controller was specified as default. - unless controller = controller(params, @defaults.key?(:controller)) + controller = controller_reference(req) do return [404, {'X-Cascade' => 'pass'}, []] end - - dispatch(controller, params[:action], req.env) - end - - def prepare_params!(params) - normalize_controller!(params) - merge_default_action!(params) - end - - # If this is a default_controller (i.e. a controller specified by the user) - # we should raise an error in case it's not found, because it usually means - # a user error. However, if the controller was retrieved through a dynamic - # segment, as in :controller(/:action), we should simply return nil and - # delegate the control back to Rack cascade. Besides, if this is not a default - # controller, it means we should respect the @scope[:module] parameter. - def controller(params, default_controller=true) - if params && params.key?(:controller) - controller_param = params[:controller] - controller_reference(controller_param) - end + dispatch(controller, params[:action], req) rescue NameError => e - raise ActionController::RoutingError, e.message, e.backtrace if default_controller - end - - private - - def controller_reference(controller_param) - const_name = @controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller" - ActiveSupport::Dependencies.constantize(const_name) + if @raise_on_name_error + raise ActionController::RoutingError, e.message, e.backtrace + else + return [404, {'X-Cascade' => 'pass'}, []] + end end - def dispatch(controller, action, env) - controller.action(action).call(env) + protected + def controller_reference(req, &block) + req.controller_class(&block) end - def normalize_controller!(params) - params[:controller] = params[:controller].underscore if params.key?(:controller) - end + private - def merge_default_action!(params) - params[:action] ||= 'index' + def dispatch(controller, action, req) + controller.action(action).call(req.env) end end @@ -88,6 +58,7 @@ module ActionDispatch class NamedRouteCollection include Enumerable attr_reader :routes, :url_helpers_module, :path_helpers_module + private :routes def initialize @routes = {} @@ -142,6 +113,7 @@ module ActionDispatch end def key?(name) + return unless name routes.key? name.to_sym end @@ -267,9 +239,13 @@ module ActionDispatch path_params -= controller_options.keys path_params -= result.keys end - path_params -= inner_options.keys - path_params.take(args.size).each do |param| - result[param] = args.shift + inner_options.each_key do |key| + path_params.delete(key) + end + + args.each_with_index do |arg, index| + param = path_params[index] + result[param] = arg if param end end @@ -309,7 +285,7 @@ module ActionDispatch attr_accessor :formatter, :set, :named_routes, :default_scope, :router attr_accessor :disable_clear_and_finalize, :resources_path_names - attr_accessor :default_url_options + attr_accessor :default_url_options, :dispatcher_class attr_reader :env_key alias :routes :set @@ -351,7 +327,8 @@ module ActionDispatch @set = Journey::Routes.new @router = Journey::Router.new @set - @formatter = Journey::Formatter.new @set + @formatter = Journey::Formatter.new self + @dispatcher_class = Routing::RouteSet::Dispatcher end def relative_url_root @@ -409,8 +386,8 @@ module ActionDispatch @prepend.each { |blk| eval_block(blk) } end - def dispatcher(defaults) - Routing::RouteSet::Dispatcher.new(defaults) + def dispatcher(raise_on_name_error) + dispatcher_class.new(raise_on_name_error) end module MountedHelpers @@ -508,7 +485,7 @@ module ActionDispatch routes.empty? end - def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true) + def add_route(mapping, path_ast, name, anchor) raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i) if name && named_routes[name] @@ -519,74 +496,17 @@ module ActionDispatch "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created" end - path = conditions.delete :path_info - ast = conditions.delete :parsed_path_info - required_defaults = conditions.delete :required_defaults - path = build_path(path, ast, requirements, anchor) - conditions = build_conditions(conditions) - - route = @set.add_route(app, path, conditions, required_defaults, defaults, name) + route = @set.add_route(name, mapping) named_routes[name] = route if name route end - def build_path(path, ast, requirements, anchor) - strexp = Journey::Router::Strexp.new( - ast, - path, - requirements, - SEPARATORS, - anchor) - - pattern = Journey::Path::Pattern.new(strexp) - - builder = Journey::GTG::Builder.new pattern.spec - - # Get all the symbol nodes followed by literals that are not the - # dummy node. - symbols = pattern.spec.grep(Journey::Nodes::Symbol).find_all { |n| - builder.followpos(n).first.literal? - } - - # Get all the symbol nodes preceded by literals. - symbols.concat pattern.spec.find_all(&:literal?).map { |n| - builder.followpos(n).first - }.find_all(&:symbol?) - - symbols.each { |x| - x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/ - } - - pattern - end - private :build_path - - def build_conditions(current_conditions) - conditions = current_conditions.dup - - # Rack-Mount requires that :request_method be a regular expression. - # :request_method represents the HTTP verb that matches this route. - # - # Here we munge values before they get sent on to rack-mount. - verbs = conditions[:request_method] || [] - unless verbs.empty? - conditions[:request_method] = %r[^#{verbs.join('|')}$] - end - - conditions.keep_if do |k, _| - request_class.public_method_defined?(k) - end - end - private :build_conditions - class Generator PARAMETERIZE = lambda do |name, value| if name == :controller value - elsif value.is_a?(Array) - value.map(&:to_param).join('/') - elsif param = value.to_param - param + else + value.to_param end end @@ -594,8 +514,8 @@ module ActionDispatch def initialize(named_route, options, recall, set) @named_route = named_route - @options = options.dup - @recall = recall.dup + @options = options + @recall = recall @set = set normalize_recall! @@ -617,7 +537,7 @@ module ActionDispatch def use_recall_for(key) if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key]) if !named_route_exists? || segment_keys.include?(key) - @options[key] = @recall.delete(key) + @options[key] = @recall[key] end end end @@ -671,12 +591,18 @@ module ActionDispatch # Remove leading slashes from controllers def normalize_controller! - @options[:controller] = controller.sub(%r{^/}, '') if controller + if controller + if controller.start_with?("/".freeze) + @options[:controller] = controller[1..-1] + else + @options[:controller] = controller + end + end end # Move 'index' action from options to recall def normalize_action! - if @options[:action] == 'index' + if @options[:action] == 'index'.freeze @recall[:action] = @options.delete(:action) end end @@ -803,14 +729,13 @@ module ActionDispatch req.path_parameters = old_params.merge params app = route.app if app.matches?(req) && app.dispatcher? - dispatcher = app.app - - if dispatcher.controller(params, false) - dispatcher.prepare_params!(params) - return params - else + begin + req.controller_class + rescue NameError raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller" end + + return req.path_parameters end end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 8379d089df..967bbd62f8 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -171,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 543c7b78a1..54e24ed6bf 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -86,8 +86,8 @@ module ActionDispatch end # Load routes.rb if it hasn't been loaded. - generated_path, extra_keys = @routes.generate_extras(options, defaults) - found_extras = options.reject { |k, _| ! extra_keys.include? k } + generated_path, query_string_keys = @routes.generate_extras(options, defaults) + found_extras = options.reject { |k, _| ! query_string_keys.include? k } msg = message || sprintf("found extras <%s>, not <%s>", found_extras, extras) assert_equal(extras, found_extras, msg) @@ -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 diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index dc664d5540..4dfd4f3f71 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -325,7 +325,11 @@ module ActionDispatch if path =~ %r{://} location = URI.parse(path) https! URI::HTTPS === location if location.scheme - host! "#{location.host}:#{location.port}" if location.host + if url_host = location.host + default = Rack::Request::DEFAULT_PORTS[location.scheme] + url_host += ":#{location.port}" if default != location.port + host! url_host + end path = location.query ? "#{location.path}?#{location.query}" : location.path end @@ -355,10 +359,10 @@ module ActionDispatch # this modifies the passed request_env directly if headers.present? - Http::Headers.new(request_env).merge!(headers) + Http::Headers.from_hash(request_env).merge!(headers) end if env.present? - Http::Headers.new(request_env).merge!(env) + Http::Headers.from_hash(request_env).merge!(env) end session = Rack::Test::Session.new(_mock_session) @@ -374,7 +378,7 @@ module ActionDispatch @html_document = nil @url_options = nil - @controller = session.last_request.env['action_controller.instance'] + @controller = @request.controller_instance response.status end @@ -391,7 +395,7 @@ module ActionDispatch attr_reader :app - def before_setup + def before_setup # :nodoc: @app = nil @integration_session = nil super diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index 494644cd46..c28d701b48 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 - @cookie_jar ||= Cookies::CookieJar.build(@request.env, @request.host, @request.ssl?, @request.cookies) + @cookie_jar ||= Cookies::CookieJar.build(@request, @request.cookies) end def redirect_to_url 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 |