diff options
Diffstat (limited to 'actionpack/lib/action_dispatch')
44 files changed, 1195 insertions, 914 deletions
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  | 
