diff options
Diffstat (limited to 'actionpack/lib')
92 files changed, 2514 insertions, 1905 deletions
diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb index fe9802e395..56c4033387 100644 --- a/actionpack/lib/abstract_controller.rb +++ b/actionpack/lib/abstract_controller.rb @@ -1,7 +1,5 @@  require 'action_pack'  require 'active_support/rails' -require 'active_support/core_ext/module/attr_internal' -require 'active_support/core_ext/module/anonymous'  require 'active_support/i18n'  module AbstractController diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index 96d701dba5..4501202b8c 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -1,8 +1,8 @@  require 'erubis' -require 'set'  require 'active_support/configurable'  require 'active_support/descendants_tracker'  require 'active_support/core_ext/module/anonymous' +require 'active_support/core_ext/module/attr_internal'  module AbstractController    class Error < StandardError #:nodoc: @@ -96,7 +96,7 @@ module AbstractController        # ==== Returns        # * <tt>String</tt>        def controller_path -        @controller_path ||= name.sub(/Controller$/, '').underscore unless anonymous? +        @controller_path ||= name.sub(/Controller$/, ''.freeze).underscore unless anonymous?        end        # Refresh the cached action_methods when a new action_method is added. @@ -148,9 +148,6 @@ module AbstractController      #      # ==== Parameters      # * <tt>action_name</tt> - The name of an action to be tested -    # -    # ==== Returns -    # * <tt>TrueClass</tt>, <tt>FalseClass</tt>      def available_action?(action_name)        _find_action_name(action_name).present?      end @@ -171,9 +168,6 @@ module AbstractController        # ==== Parameters        # * <tt>name</tt> - The name of an action to be tested        # -      # ==== Returns -      # * <tt>TrueClass</tt>, <tt>FalseClass</tt> -      #        # :api: private        def action_method?(name)          self.class.action_methods.include?(name) diff --git a/actionpack/lib/abstract_controller/collector.rb b/actionpack/lib/abstract_controller/collector.rb index ddd56b354a..55654be224 100644 --- a/actionpack/lib/abstract_controller/collector.rb +++ b/actionpack/lib/abstract_controller/collector.rb @@ -4,11 +4,10 @@ module AbstractController    module Collector      def self.generate_method_for_mime(mime)        sym = mime.is_a?(Symbol) ? mime : mime.to_sym -      const = sym.upcase        class_eval <<-RUBY, __FILE__, __LINE__ + 1 -        def #{sym}(*args, &block)                # def html(*args, &block) -          custom(Mime::#{const}, *args, &block)  #   custom(Mime::HTML, *args, &block) -        end                                      # end +        def #{sym}(*args, &block) +          custom(Mime[:#{sym}], *args, &block) +        end        RUBY      end @@ -23,9 +22,7 @@ module AbstractController    protected      def method_missing(symbol, &block) -      const_name = symbol.upcase - -      unless Mime.const_defined?(const_name) +      unless mime_constant = Mime[symbol]          raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \            "http://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \            "If you meant to respond to a variant like :tablet or :phone, not a custom format, " \ @@ -33,8 +30,6 @@ module AbstractController            "format.html { |html| html.tablet { ... } }"        end -      mime_constant = Mime.const_get(const_name) -        if Mime::SET.include?(mime_constant)          AbstractController::Collector.generate_method_for_mime(mime_constant)          send(symbol, &block) diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 109eff10eb..d84c238a62 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -181,7 +181,7 @@ module AbstractController        end        def default_helper_module! -        module_name = name.sub(/Controller$/, '') +        module_name = name.sub(/Controller$/, ''.freeze)          module_path = module_name.underscore          helper module_path        rescue LoadError => e diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 5514213ad8..a73f188623 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -22,9 +22,13 @@ module AbstractController      # :api: public      def render(*args, &block)        options = _normalize_render(*args, &block) -      self.response_body = render_to_body(options) -      _process_format(rendered_format, options) if rendered_format -      self.response_body +      rendered_body = render_to_body(options) +      if options[:html] +        _set_html_content_type +      else +        _set_rendered_content_type rendered_format +      end +      self.response_body = rendered_body      end      # Raw rendering of a template to a string. @@ -51,14 +55,14 @@ module AbstractController      # Returns Content-Type of rendered content      # :api: public      def rendered_format -      Mime::TEXT +      Mime[:text]      end -    DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %w( +    DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %i(        @_action_name @_response_body @_formats @_prefixes @_config        @_view_context_class @_view_renderer @_lookup_context        @_routes @_db_runtime -    ).map(&:to_sym) +    )      # This method should return a hash with assigns.      # You can overwrite this configuration per controller. @@ -99,7 +103,13 @@ module AbstractController      # Process the rendered format.      # :api: private -    def _process_format(format, options = {}) +    def _process_format(format) +    end + +    def _set_html_content_type # :nodoc: +    end + +    def _set_rendered_content_type(format) # :nodoc:      end      # Normalize args and options. @@ -107,7 +117,7 @@ module AbstractController      def _normalize_render(*args, &block)        options = _normalize_args(*args, &block)        #TODO: remove defined? when we restore AP <=> AV dependency -      if defined?(request) && request && request.variant.present? +      if defined?(request) && request.variant.present?          options[:variant] = request.variant        end        _normalize_options(options) diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 89fc4520d3..3d3af555c9 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -30,7 +30,6 @@ module ActionController      autoload :Instrumentation      autoload :MimeResponds      autoload :ParamsWrapper -    autoload :RackDelegation      autoload :Redirecting      autoload :Renderers      autoload :Rendering diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb index 3af63b8892..1a46d49a49 100644 --- a/actionpack/lib/action_controller/api.rb +++ b/actionpack/lib/action_controller/api.rb @@ -90,7 +90,7 @@ module ActionController      # Shortcut helper that returns all the ActionController::API modules except      # the ones passed as arguments:      # -    #   class MetalController +    #   class MyAPIBaseController < ActionController::Metal      #     ActionController::API.without_modules(:ForceSSL, :UrlFor).each do |left|      #       include left      #     end @@ -115,7 +115,6 @@ module ActionController        Rendering,        Renderers::All,        ConditionalGet, -      RackDelegation,        BasicImplicitRender,        StrongParameters, diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 2c3b3f4e05..04e5922ce8 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -183,7 +183,7 @@ module ActionController      # Shortcut helper that returns all the modules included in      # ActionController::Base except the ones passed as arguments:      # -    #   class MetalController +    #   class MyBaseController < ActionController::Metal      #     ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left|      #       include left      #     end @@ -213,7 +213,6 @@ module ActionController        Renderers::All,        ConditionalGet,        EtagWithTemplateDigest, -      RackDelegation,        Caching,        MimeResponds,        ImplicitRender, @@ -249,20 +248,17 @@ module ActionController      MODULES.each do |mod|        include mod      end +    setup_renderer!      # Define some internal variables that should not be propagated to the view.      PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [ -      :@_status, :@_headers, :@_params, :@_env, :@_response, :@_request, +      :@_params, :@_response, :@_request,        :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout ]      def _protected_ivars # :nodoc:        PROTECTED_IVARS      end -    def self.protected_instance_variables -      PROTECTED_IVARS -    end -      ActiveSupport.run_load_hooks(:action_controller, self)    end  end diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index de85e0c1a7..0b8fa2ea09 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -1,6 +1,5 @@  require 'fileutils'  require 'uri' -require 'set'  module ActionController    # \Caching is a cheap way of speeding up slow applications by keeping the result of @@ -8,7 +7,7 @@ module ActionController    #    # You can read more about each approach by clicking the modules below.    # -  # Note: To turn off all caching, set +  # Note: To turn off all caching provided by Action Controller, set    #   config.action_controller.perform_caching = false    #    # == \Caching stores @@ -46,7 +45,6 @@ module ActionController          end      end -    include RackDelegation      include AbstractController::Callbacks      include ConfigMethods diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 87609d8aa7..4c9f14e409 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -25,7 +25,7 @@ module ActionController            status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)          end          message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" -        message << " (#{additions.join(" | ")})" unless additions.blank? +        message << " (#{additions.join(" | ".freeze)})" unless additions.blank?          message        end      end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index ae111e4951..8e040bb465 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -1,5 +1,7 @@  require 'active_support/core_ext/array/extract_options'  require 'action_dispatch/middleware/stack' +require 'action_dispatch/http/request' +require 'action_dispatch/http/response'  module ActionController    # Extend ActionDispatch middleware stack to make it aware of options @@ -11,22 +13,14 @@ module ActionController    #    class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc:      class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc: -      def initialize(klass, *args, &block) -        options = args.extract_options! -        @only   = Array(options.delete(:only)).map(&:to_s) -        @except = Array(options.delete(:except)).map(&:to_s) -        args << options unless options.empty? -        super +      def initialize(klass, args, actions, strategy, block) +        @actions = actions +        @strategy = strategy +        super(klass, args, block)        end        def valid?(action) -        if @only.present? -          @only.include?(action) -        elsif @except.present? -          !@except.include?(action) -        else -          true -        end +        @strategy.call @actions, action        end      end @@ -37,6 +31,32 @@ module ActionController          middleware.valid?(action) ? middleware.build(a) : a        end      end + +    private + +    INCLUDE = ->(list, action) { list.include? action } +    EXCLUDE = ->(list, action) { !list.include? action } +    NULL    = ->(list, action) { true } + +    def build_middleware(klass, args, block) +      options = args.extract_options! +      only   = Array(options.delete(:only)).map(&:to_s) +      except = Array(options.delete(:except)).map(&:to_s) +      args << options unless options.empty? + +      strategy = NULL +      list     = nil + +      if only.any? +        strategy = INCLUDE +        list     = only +      elsif except.any? +        strategy = EXCLUDE +        list     = except +      end + +      Middleware.new(get_class(klass), args, list, strategy, block) +    end    end    # <tt>ActionController::Metal</tt> is the simplest possible controller, providing a @@ -98,11 +118,10 @@ module ActionController    class Metal < AbstractController::Base      abstract! -    attr_internal_writer :env -      def env -      @_env ||= {} +      @_request.env      end +    deprecate :env      # Returns the last part of the controller's name, underscored, without the ending      # <tt>Controller</tt>. For instance, PostsController returns <tt>posts</tt>. @@ -114,23 +133,23 @@ module ActionController        @controller_name ||= name.demodulize.sub(/Controller$/, '').underscore      end +    def self.make_response!(request) +      ActionDispatch::Response.create.tap do |res| +        res.request = request +      end +    end +      # Delegates to the class' <tt>controller_name</tt>      def controller_name        self.class.controller_name      end -    # The details below can be overridden to support a specific -    # Request and Response object. The default ActionController::Base -    # implementation includes RackDelegation, which makes a request -    # and response object available. You might wish to control the -    # environment and response manually for performance reasons. - -    attr_internal :headers, :response, :request +    attr_internal :response, :request      delegate :session, :to => "@_request" +    delegate :headers, :status=, :location=, :content_type=, +             :status, :location, :content_type, :to => "@_response"      def initialize -      @_headers = {"Content-Type" => "text/html"} -      @_status = 200        @_request = nil        @_response = nil        @_routes = nil @@ -145,64 +164,51 @@ module ActionController        @_params = val      end -    # Basic implementations for content_type=, location=, and headers are -    # provided to reduce the dependency on the RackDelegation module -    # in Renderer and Redirector. - -    def content_type=(type) -      headers["Content-Type"] = type.to_s -    end - -    def content_type -      headers["Content-Type"] -    end - -    def location -      headers["Location"] -    end - -    def location=(url) -      headers["Location"] = url -    end +    alias :response_code :status # :nodoc:      # Basic url_for that can be overridden for more robust functionality      def url_for(string)        string      end -    def status -      @_status -    end -    alias :response_code :status # :nodoc: - -    def status=(status) -      @_status = Rack::Utils.status_code(status) -    end -      def response_body=(body)        body = [body] unless body.nil? || body.respond_to?(:each) +      response.reset_body! +      body.each { |part| +        next if part.empty? +        response.write part +      }        super      end      # Tests if render or redirect has already happened.      def performed? -      response_body || (response && response.committed?) +      response_body || response.committed?      end -    def dispatch(name, request) #:nodoc: +    def dispatch(name, request, response) #:nodoc:        set_request!(request) +      set_response!(response)        process(name) +      request.commit_flash        to_a      end +    def set_response!(response) # :nodoc: +      @_response = response +    end +      def set_request!(request) #:nodoc:        @_request = request -      @_env = request.env -      @_env['action_controller.instance'] = self +      @_request.controller_instance = self      end      def to_a #:nodoc: -      response ? response.to_a : [status, headers, response_body] +      response.to_a +    end + +    def reset_session +      @_request.reset_session      end      class_attribute :middleware_stack @@ -230,15 +236,32 @@ module ActionController        req = ActionDispatch::Request.new env        action(req.path_parameters[:action]).call(env)      end +    class << self; deprecate :call; end      # Returns a Rack endpoint for the given action name. -    def self.action(name, klass = ActionDispatch::Request) +    def self.action(name)        if middleware_stack.any?          middleware_stack.build(name) do |env| -          new.dispatch(name, klass.new(env)) +          req = ActionDispatch::Request.new(env) +          res = make_response! req +          new.dispatch(name, req, res)          end        else -        lambda { |env| new.dispatch(name, klass.new(env)) } +        lambda { |env| +          req = ActionDispatch::Request.new(env) +          res = make_response! req +          new.dispatch(name, req, res) +        } +      end +    end + +    # Direct dispatch to the controller.  Instantiates the controller, then +    # executes the action named +name+. +    def self.dispatch(name, req, res) +      if middleware_stack.any? +        middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env +      else +        new.dispatch(name, req, res)        end      end    end diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index bb3ad9b850..89d589c486 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -4,7 +4,6 @@ module ActionController    module ConditionalGet      extend ActiveSupport::Concern -    include RackDelegation      include Head      included do diff --git a/actionpack/lib/action_controller/metal/cookies.rb b/actionpack/lib/action_controller/metal/cookies.rb index d787f014cd..f8efb2b076 100644 --- a/actionpack/lib/action_controller/metal/cookies.rb +++ b/actionpack/lib/action_controller/metal/cookies.rb @@ -2,8 +2,6 @@ module ActionController #:nodoc:    module Cookies      extend ActiveSupport::Concern -    include RackDelegation -      included do        helper_method :cookies      end diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 1abd8d3a33..957e7a3019 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -72,27 +72,7 @@ module ActionController #:nodoc:          self.status = options[:status] || 200          self.content_type = options[:content_type] if options.key?(:content_type) -        self.response_body = FileBody.new(path) -      end - -      # Avoid having to pass an open file handle as the response body. -      # Rack::Sendfile will usually intercept the response and uses -      # the path directly, so there is no reason to open the file. -      class FileBody #:nodoc: -        attr_reader :to_path - -        def initialize(path) -          @to_path = path -        end - -        # Stream the file's contents if Rack::Sendfile isn't present. -        def each -          File.open(to_path, 'rb') do |file| -            while chunk = file.read(16384) -              yield chunk -            end -          end -        end +        response.send_file path        end        # Sends the given binary data to the browser. This method is similar to @@ -126,7 +106,7 @@ module ActionController #:nodoc:        # See +send_file+ for more information on HTTP Content-* headers and caching.        def send_data(data, options = {}) #:doc:          send_file_headers! options -        render options.slice(:status, :content_type).merge(:text => data) +        render options.slice(:status, :content_type).merge(body: data)        end      private diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb index f9303efe6c..669cf55bca 100644 --- a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb +++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb @@ -25,7 +25,7 @@ module ActionController        class_attribute :etag_with_template_digest        self.etag_with_template_digest = true -      ActiveSupport.on_load :action_view, yield: true do |action_view_base| +      ActiveSupport.on_load :action_view, yield: true do          etag do |options|            determine_template_etag(options) if etag_with_template_digest          end diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index 18e003741d..5260dc0336 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -5,12 +5,10 @@ module ActionController    class BadRequest < ActionControllerError #:nodoc:      attr_reader :original_exception -    def initialize(type = nil, e = nil) -      return super() unless type && e - -      super("Invalid #{type} parameters: #{e.message}") +    def initialize(msg = nil, e = nil) +      super(msg)        @original_exception = e -      set_backtrace e.backtrace +      set_backtrace e.backtrace if e      end    end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index d920668184..e31d65aac2 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -55,10 +55,10 @@ module ActionController        # You can pass any of the following options to affect the before_action callback        # * <tt>only</tt>       - The callback should be run only for this action        # * <tt>except</tt>     - The callback should be run for all actions except this action -      # * <tt>if</tt>         - A symbol naming an instance method or a proc; the callback -      #                         will be called only when it returns a true value. -      # * <tt>unless</tt>     - A symbol naming an instance method or a proc; the callback -      #                         will be called only when it returns a false value. +      # * <tt>if</tt>         - A symbol naming an instance method or a proc; the +      #   callback will be called only when it returns a true value. +      # * <tt>unless</tt>     - A symbol naming an instance method or a proc; the +      #   callback will be called only when it returns a false value.        def force_ssl(options = {})          action_options = options.slice(*ACTION_OPTIONS)          redirect_options = options.except(*ACTION_OPTIONS) @@ -71,8 +71,8 @@ module ActionController      # Redirect the existing request to use the HTTPS protocol.      #      # ==== Parameters -    # * <tt>host_or_options</tt> - Either a host name or any of the url & redirect options -    #                              available to the <tt>force_ssl</tt> method. +    # * <tt>host_or_options</tt> - Either a host name or any of the url & +    #   redirect options available to the <tt>force_ssl</tt> method.      def force_ssl_redirect(host_or_options = nil)        unless request.ssl?          options = { diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index f445094bdc..b2110bf946 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -28,7 +28,7 @@ module ActionController        end        status ||= :ok -       +        location = options.delete(:location)        content_type = options.delete(:content_type) @@ -43,12 +43,9 @@ module ActionController        if include_content?(self.response_code)          self.content_type = content_type || (Mime[formats.first] if formats) -        self.response.charset = false if self.response -      else -        headers.delete('Content-Type') -        headers.delete('Content-Length') +        self.response.charset = false        end -       +        true      end diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index b4da381d26..d3853e2e83 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -7,8 +7,8 @@ module ActionController    # extract complicated logic or reusable functionality is strongly encouraged. By default, each controller    # will include all helpers. These helpers are only accessible on the controller through <tt>.helpers</tt>    # -  # In previous versions of \Rails the controller will include a helper whose -  # name matches that of the controller, e.g., <tt>MyController</tt> will automatically +  # In previous versions of \Rails the controller will include a helper which +  # matches the name of the controller, e.g., <tt>MyController</tt> will automatically    # include <tt>MyHelper</tt>. To return old behavior set +config.action_controller.include_all_helpers+ to +false+.    #    # Additional helpers can be specified using the +helper+ class method in ActionController::Base or any @@ -73,7 +73,7 @@ module ActionController        # Provides a proxy to access helpers methods from outside the view.        def helpers -        @helper_proxy ||= begin  +        @helper_proxy ||= begin            proxy = ActionView::Base.new            proxy.config = config.inheritable_copy            proxy.extend(_helpers) @@ -100,7 +100,7 @@ module ActionController        def all_helpers_from_path(path)          helpers = Array(path).flat_map do |_path|            extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ -          names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } +          names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1'.freeze) }            names.sort!          end          helpers.uniq! diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 39bed955a4..0a36fecd27 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -34,7 +34,7 @@ module ActionController      #      #       def authenticate      #         case request.format -    #         when Mime::XML, Mime::ATOM +    #         when Mime[:xml], Mime[:atom]      #           if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }      #             @current_user = user      #           else @@ -94,7 +94,7 @@ module ActionController        end        def has_basic_credentials?(request) -        request.authorization.present? && (auth_scheme(request) == 'Basic') +        request.authorization.present? && (auth_scheme(request).downcase == 'basic')        end        def user_name_and_password(request) @@ -203,7 +203,7 @@ module ActionController            password = password_procedure.call(credentials[:username])            return false unless password -          method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD'] +          method = request.get_header('rack.methodoverride.original_method') || request.get_header('REQUEST_METHOD')            uri    = credentials[:uri]            [true, false].any? do |trailing_question_mark| @@ -260,8 +260,8 @@ module ActionController        end        def secret_token(request) -        key_generator  = request.env["action_dispatch.key_generator"] -        http_auth_salt = request.env["action_dispatch.http_auth_salt"] +        key_generator  = request.key_generator +        http_auth_salt = request.http_auth_salt          key_generator.generate_key(http_auth_salt)        end @@ -361,7 +361,7 @@ module ActionController      #      #       def authenticate      #         case request.format -    #         when Mime::XML, Mime::ATOM +    #         when Mime[:xml], Mime[:atom]      #           if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) }      #             @current_user = user      #           else @@ -436,15 +436,17 @@ module ActionController          end        end -      # Parses the token and options out of the token authorization header. If -      # the header looks like this: +      # Parses the token and options out of the token authorization header. +      # The value for the Authorization header is expected to have the prefix +      # <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this:        #   Authorization: Token token="abc", nonce="def" -      # Then the returned token is "abc", and the options is {nonce: "def"} +      # Then the returned token is <tt>"abc"</tt>, and the options are +      # <tt>{nonce: "def"}</tt>        #        # request - ActionDispatch::Request instance with the current headers.        # -      # Returns an Array of [String, Hash] if a token is present. -      # Returns nil if no token is found. +      # Returns an +Array+ of <tt>[String, Hash]</tt> if a token is present. +      # Returns +nil+ if no token is found.        def token_and_options(request)          authorization_request = request.authorization.to_s          if authorization_request[TOKEN_REGEX] @@ -502,7 +504,7 @@ module ActionController        def authentication_request(controller, realm, message = nil)          message ||= "HTTP Token: Access denied.\n"          controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"'.freeze, "".freeze)}") -        controller.__send__ :render, :text => message, :status => :unauthorized +        controller.__send__ :render, plain: message, status: :unauthorized        end      end    end diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index a3e1a71b0a..3dbf34eb2a 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -11,7 +11,6 @@ module ActionController      extend ActiveSupport::Concern      include AbstractController::Logger -    include ActionController::RackDelegation      attr_internal :view_runtime diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 58150cd9a9..7db8d13e24 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -33,6 +33,20 @@ module ActionController    # the main thread. Make sure your actions are thread safe, and this shouldn't    # be a problem (don't share state across threads, etc).    module Live +    extend ActiveSupport::Concern + +    module ClassMethods +      def make_response!(request) +        if request.env["HTTP_VERSION"] == "HTTP/1.0" +          super +        else +          Live::Response.new.tap do |res| +            res.request = request +          end +        end +      end +    end +      # This class provides the ability to write an SSE (Server Sent Event)      # to an IO stream. The class is initialized with a stream and can be used      # to either write a JSON string or an object which can be converted to JSON. @@ -131,8 +145,8 @@ module ActionController        def write(string)          unless @response.committed? -          @response.headers["Cache-Control"] = "no-cache" -          @response.headers.delete "Content-Length" +          @response.set_header "Cache-Control", "no-cache" +          @response.delete_header "Content-Length"          end          super @@ -199,29 +213,6 @@ module ActionController      end      class Response < ActionDispatch::Response #:nodoc: all -      class Header < DelegateClass(Hash) # :nodoc: -        def initialize(response, header) -          @response = response -          super(header) -        end - -        def []=(k,v) -          if @response.committed? -            raise ActionDispatch::IllegalStateError, 'header already sent' -          end - -          super -        end - -        def merge(other) -          self.class.new @response, __getobj__.merge(other) -        end - -        def to_hash -          __getobj__.dup -        end -      end -        private        def before_committed @@ -242,14 +233,6 @@ module ActionController          body.each { |part| buf.write part }          buf        end - -      def merge_default_headers(original, default) -        Header.new self, super -      end - -      def handle_conditional_get! -        super unless committed? -      end      end      def process(name) @@ -311,12 +294,7 @@ module ActionController      end      def set_response!(request) -      if request.env["HTTP_VERSION"] == "HTTP/1.0" -        super -      else -        @_response         = Live::Response.new -        @_response.request = request -      end +      @_response = self.class.make_response! request      end    end  end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index fab1be3459..58df5c539e 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -156,16 +156,16 @@ module ActionController #:nodoc:      # It works for both inline:      #      #   respond_to do |format| -    #     format.html.any   { render text: "any"   } -    #     format.html.phone { render text: "phone" } +    #     format.html.any   { render html: "any"   } +    #     format.html.phone { render html: "phone" }      #   end      #      # and block syntax:      #      #   respond_to do |format|      #     format.html do |variant| -    #       variant.any(:tablet, :phablet){ render text: "any" } -    #       variant.phone { render text: "phone" } +    #       variant.any(:tablet, :phablet){ render html: "any" } +    #       variant.phone { render html: "phone" }      #     end      #   end      # @@ -191,6 +191,7 @@ module ActionController #:nodoc:        if format = collector.negotiate_format(request)          _process_format(format) +        _set_rendered_content_type format          response = collector.response          response ? response.call : render({})        else @@ -228,7 +229,7 @@ module ActionController #:nodoc:          @responses = {}          @variant = variant -        mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil } +        mimes.each { |mime| @responses[Mime[mime]] = nil }        end        def any(*args, &block) diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index cdfc523bd4..c38fc40b81 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -4,8 +4,8 @@ require 'active_support/core_ext/module/anonymous'  require 'action_dispatch/http/mime_type'  module ActionController -  # Wraps the parameters hash into a nested hash. This will allow clients to submit -  # POST requests without having to specify any root elements. +  # Wraps the parameters hash into a nested hash. This will allow clients to +  # submit requests without having to specify any root elements.    #    # This functionality is enabled in +config/initializers/wrap_parameters.rb+    # and can be customized. @@ -14,7 +14,7 @@ module ActionController    # a non-empty array:    #    #     class UsersController < ApplicationController -  #       wrap_parameters format: [:json, :xml] +  #       wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]    #     end    #    # If you enable +ParamsWrapper+ for +:json+ format, instead of having to @@ -276,7 +276,9 @@ module ActionController        # Checks if we should perform parameters wrapping.        def _wrapper_enabled? -        ref = request.content_mime_type.try(:ref) +        return false unless request.has_content_type? + +        ref = request.content_mime_type.ref          _wrapper_formats.include?(ref) && _wrapper_key && !request.request_parameters[_wrapper_key]        end    end diff --git a/actionpack/lib/action_controller/metal/rack_delegation.rb b/actionpack/lib/action_controller/metal/rack_delegation.rb deleted file mode 100644 index ae9d89cc8c..0000000000 --- a/actionpack/lib/action_controller/metal/rack_delegation.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'action_dispatch/http/request' -require 'action_dispatch/http/response' - -module ActionController -  module RackDelegation -    extend ActiveSupport::Concern - -    delegate :headers, :status=, :location=, :content_type=, -             :status, :location, :content_type, :response_code, :to => "@_response" - -    module ClassMethods -      def build_with_env(env = {}) #:nodoc: -        new.tap { |c| c.set_request! ActionDispatch::Request.new(env) } -      end -    end - -    def set_request!(request) #:nodoc: -      super -      set_response!(request) -    end - -    def response_body=(body) -      response.body = body if response -      super -    end - -    def reset_session -      @_request.reset_session -    end - -    private - -    def set_response!(request) -      @_response         = ActionDispatch::Response.new -      @_response.request = request -    end -  end -end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index acaa8227c9..0febc905f1 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -11,7 +11,6 @@ module ActionController      extend ActiveSupport::Concern      include AbstractController::Logger -    include ActionController::RackDelegation      include ActionController::UrlFor      # Redirects the browser to the target specified in +options+. This parameter can be any one of: diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 45d3962494..22e0bb5955 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -68,11 +68,11 @@ module ActionController      #   ActionController::Renderers.add :csv do |obj, options|      #     filename = options[:filename] || 'data'      #     str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s -    #     send_data str, type: Mime::CSV, +    #     send_data str, type: Mime[:csv],      #       disposition: "attachment; filename=#{filename}.csv"      #   end      # -    # Note that we used Mime::CSV for the csv mime type as it comes with Rails. +    # Note that we used Mime[:csv] for the csv mime type as it comes with Rails.      # For a custom renderer, you'll need to register a mime type with      # <tt>Mime::Type.register</tt>.      # @@ -94,7 +94,7 @@ module ActionController      # This method is the opposite of add method.      # -    # Usage: +    # To remove a csv renderer:      #      #   ActionController::Renderers.remove(:csv)      def self.remove(key) @@ -116,24 +116,24 @@ module ActionController        json = json.to_json(options) unless json.kind_of?(String)        if options[:callback].present? -        if content_type.nil? || content_type == Mime::JSON -          self.content_type = Mime::JS +        if content_type.nil? || content_type == Mime[:json] +          self.content_type = Mime[:js]          end          "/**/#{options[:callback]}(#{json})"        else -        self.content_type ||= Mime::JSON +        self.content_type ||= Mime[:json]          json        end      end      add :js do |js, options| -      self.content_type ||= Mime::JS +      self.content_type ||= Mime[:js]        js.respond_to?(:to_js) ? js.to_js(options) : js      end      add :xml do |xml, options| -      self.content_type ||= Mime::XML +      self.content_type ||= Mime[:xml]        xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml      end    end diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index b9ae8dd5ea..cce6fe7787 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/filters' +  module ActionController    module Rendering      extend ActiveSupport::Concern @@ -8,10 +10,17 @@ module ActionController        # Documentation at ActionController::Renderer#render        delegate :render, to: :renderer -      # Returns a renderer class (inherited from ActionController::Renderer) +      # Returns a renderer instance (inherited from ActionController::Renderer)        # for the controller. -      def renderer -        @renderer ||= Renderer.for(self) +      attr_reader :renderer + +      def setup_renderer! # :nodoc: +        @renderer = Renderer.for(self) +      end + +      def inherited(klass) +        klass.setup_renderer! +        super        end      end @@ -53,13 +62,13 @@ module ActionController        nil      end -    def _process_format(format, options = {}) -      super +    def _set_html_content_type +      self.content_type = Mime[:html].to_s +    end -      if options[:plain] -        self.content_type = Mime::TEXT -      else -        self.content_type ||= format.to_s +    def _set_rendered_content_type(format) +      unless response.content_type +        self.content_type = format.to_s        end      end @@ -74,6 +83,17 @@ module ActionController      def _normalize_options(options) #:nodoc:        _normalize_text(options) +      if options[:text] +        ActiveSupport::Deprecation.warn <<-WARNING.squish +          `render :text` is deprecated because it does not actually render a +          `text/plain` response. Switch to `render plain: 'plain text'` to +          render as `text/plain`, `render html: '<strong>HTML</strong>'` to +          render as `text/html`, or `render body: 'raw'` to match the deprecated +          behavior and render with the default Content-Type, which is +          `text/plain`. +        WARNING +      end +        if options[:html]          options[:html] = ERB::Util.html_escape(options[:html])        end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 4cb634477e..64f6f7cf51 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -20,7 +20,7 @@ module ActionController #:nodoc:    # Since HTML and JavaScript requests are typically made from the browser, we    # need to ensure to verify request authenticity for the web browser. We can    # use session-oriented authentication for these types of requests, by using -  # the `protect_form_forgery` method in our controllers. +  # the `protect_from_forgery` method in our controllers.    #    # GET requests are not protected since they don't have side effects like writing    # to the database and don't leak sensitive information. JavaScript requests are @@ -90,8 +90,10 @@ module ActionController #:nodoc:        #        #   class FooController < ApplicationController        #     protect_from_forgery except: :index +      #   end        #        # You can disable forgery protection on controller by skipping the verification before_action: +      #        #   skip_before_action :verify_authenticity_token        #        # Valid Options: @@ -136,17 +138,17 @@ module ActionController #:nodoc:          # This is the method that defines the application behavior when a request is found to be unverified.          def handle_unverified_request            request = @controller.request -          request.session = NullSessionHash.new(request.env) -          request.env['action_dispatch.request.flash_hash'] = nil -          request.env['rack.session.options'] = { skip: true } -          request.env['action_dispatch.cookies'] = NullCookieJar.build(request) +          request.session = NullSessionHash.new(request) +          request.flash = nil +          request.session_options = { skip: true } +          request.cookie_jar = NullCookieJar.build(request, {})          end          protected          class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc: -          def initialize(env) -            super(nil, env) +          def initialize(req) +            super(nil, req)              @data = {}              @loaded = true            end @@ -160,14 +162,6 @@ module ActionController #:nodoc:          end          class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc: -          def self.build(request) -            key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY] -            host          = request.host -            secure        = request.ssl? - -            new(key_generator, host, secure, options_for_env({})) -          end -            def write(*)              # nothing            end diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index af31de1f3a..a6115674aa 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -199,7 +199,7 @@ module ActionController #:nodoc:        def _process_options(options) #:nodoc:          super          if options[:stream] -          if env["HTTP_VERSION"] == "HTTP/1.0" +          if request.version == "HTTP/1.0"              options.delete(:stream)            else              headers["Cache-Control"] ||= "no-cache" diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 09867e2407..130ba61786 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -97,17 +97,18 @@ module ActionController    # environment they should only be set once at boot-time and never mutated at    # runtime.    # -  # <tt>ActionController::Parameters</tt> inherits from -  # <tt>ActiveSupport::HashWithIndifferentAccess</tt>, this means -  # that you can fetch values using either <tt>:key</tt> or <tt>"key"</tt>. +  # You can fetch values of <tt>ActionController::Parameters</tt> using either +  # <tt>:key</tt> or <tt>"key"</tt>.    #    #   params = ActionController::Parameters.new(key: 'value')    #   params[:key]  # => "value"    #   params["key"] # => "value" -  class Parameters < ActiveSupport::HashWithIndifferentAccess +  class Parameters      cattr_accessor :permit_all_parameters, instance_accessor: false      cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false +    delegate :keys, :key?, :has_key?, :empty?, :inspect, to: :@parameters +      # By default, never raise an UnpermittedParameters exception if these      # params are present. The default includes both 'controller' and 'action'      # because they are added by Rails and should be of no concern. One way @@ -144,11 +145,22 @@ module ActionController      #   params = ActionController::Parameters.new(name: 'Francesco')      #   params.permitted?  # => true      #   Person.new(params) # => #<Person id: nil, name: "Francesco"> -    def initialize(attributes = nil) -      super(attributes) +    def initialize(parameters = {}) +      @parameters = parameters.with_indifferent_access        @permitted = self.class.permit_all_parameters      end +    # Returns true if another +Parameters+ object contains the same content and +    # permitted flag, or other Hash-like object contains the same content. This +    # override is in place so you can perform a comparison with `Hash`. +    def ==(other_hash) +      if other_hash.respond_to?(:permitted?) +        super +      else +        @parameters == other_hash +      end +    end +      # Returns a safe +Hash+ representation of this parameter with all      # unpermitted keys removed.      # @@ -162,7 +174,7 @@ module ActionController      #   safe_params.to_h # => {"name"=>"Senjougahara Hitagi"}      def to_h        if permitted? -        to_hash +        @parameters.to_h        else          slice(*self.class.always_permitted_parameters).permit!.to_h        end @@ -170,20 +182,17 @@ module ActionController      # Returns an unsafe, unfiltered +Hash+ representation of this parameter.      def to_unsafe_h -      to_hash +      @parameters.to_h      end      alias_method :to_unsafe_hash, :to_unsafe_h      # Convert all hashes in values into parameters, then yield each pair like      # the same way as <tt>Hash#each_pair</tt>      def each_pair(&block) -      super do |key, value| -        convert_hashes_to_parameters(key, value) +      @parameters.each_pair do |key, value| +        yield key, convert_hashes_to_parameters(key, value)        end - -      super      end -      alias_method :each, :each_pair      # Attribute that keeps track of converted arrays, if any, to avoid double @@ -230,19 +239,58 @@ module ActionController        self      end -    # Ensures that a parameter is present. If it's present, returns -    # the parameter at the given +key+, otherwise raises an -    # <tt>ActionController::ParameterMissing</tt> error. +    # This method accepts both a single key and an array of keys. +    # +    # When passed a single key, if it exists and its associated value is +    # either present or the singleton +false+, returns said value:      #      #   ActionController::Parameters.new(person: { name: 'Francesco' }).require(:person)      #   # => {"name"=>"Francesco"}      # +    # Otherwise raises <tt>ActionController::ParameterMissing</tt>: +    # +    #   ActionController::Parameters.new.require(:person) +    #   # ActionController::ParameterMissing: param is missing or the value is empty: person +    #      #   ActionController::Parameters.new(person: nil).require(:person) -    #   # => ActionController::ParameterMissing: param is missing or the value is empty: person +    #   # ActionController::ParameterMissing: param is missing or the value is empty: person +    # +    #   ActionController::Parameters.new(person: "\t").require(:person) +    #   # ActionController::ParameterMissing: param is missing or the value is empty: person      #      #   ActionController::Parameters.new(person: {}).require(:person) -    #   # => ActionController::ParameterMissing: param is missing or the value is empty: person +    #   # ActionController::ParameterMissing: param is missing or the value is empty: person +    # +    # When given an array of keys, the method tries to require each one of them +    # in order. If it succeeds, an array with the respective return values is +    # returned: +    # +    #   params = ActionController::Parameters.new(user: { ... }, profile: { ... }) +    #   user_params, profile_params = params.require(:user, :profile) +    # +    # Otherwise, the method reraises the first exception found: +    # +    #   params = ActionController::Parameters.new(user: {}, profile: {}) +    #   user_params, profile_params = params.require(:user, :profile) +    #   # ActionController::ParameterMissing: param is missing or the value is empty: user +    # +    # Technically this method can be used to fetch terminal values: +    # +    #   # CAREFUL +    #   params = ActionController::Parameters.new(person: { name: 'Finn' }) +    #   name = params.require(:person).require(:name) # CAREFUL +    # +    # but take into account that at some point those ones have to be permitted: +    # +    #   def person_params +    #     params.require(:person).permit(:name).tap do |person_params| +    #       person_params.require(:name) # SAFER +    #     end +    #   end +    # +    # for example.      def require(key) +      return key.map { |k| require(k) } if key.is_a?(Array)        value = self[key]        if value.present? || value == false          value @@ -347,7 +395,13 @@ module ActionController      #   params[:person] # => {"name"=>"Francesco"}      #   params[:none]   # => nil      def [](key) -      convert_hashes_to_parameters(key, super) +      convert_hashes_to_parameters(key, @parameters[key]) +    end + +    # Assigns a value to a given +key+. The given key may still get filtered out +    # when +permit+ is called. +    def []=(key, value) +      @parameters[key] = value      end      # Returns a parameter for the given +key+. If the +key+ @@ -361,10 +415,16 @@ module ActionController      #   params.fetch(:none)                 # => ActionController::ParameterMissing: param is missing or the value is empty: none      #   params.fetch(:none, 'Francesco')    # => "Francesco"      #   params.fetch(:none) { 'Francesco' } # => "Francesco" -    def fetch(key, *args) -      convert_hashes_to_parameters(key, super, false) -    rescue KeyError -      raise ActionController::ParameterMissing.new(key) +    def fetch(key, *args, &block) +      convert_value_to_parameters( +        @parameters.fetch(key) { +          if block_given? +            yield +          else +            args.fetch(0) { raise ActionController::ParameterMissing.new(key) } +          end +        } +      )      end      # Returns a new <tt>ActionController::Parameters</tt> instance that @@ -375,7 +435,24 @@ module ActionController      #   params.slice(:a, :b) # => {"a"=>1, "b"=>2}      #   params.slice(:d)     # => {}      def slice(*keys) -      new_instance_with_inherited_permitted_status(super) +      new_instance_with_inherited_permitted_status(@parameters.slice(*keys)) +    end + +    # Returns current <tt>ActionController::Parameters</tt> instance which +    # contains only the given +keys+. +    def slice!(*keys) +      @parameters.slice!(*keys) +      self +    end + +    # Returns a new <tt>ActionController::Parameters</tt> instance that +    # filters out the given +keys+. +    # +    #   params = ActionController::Parameters.new(a: 1, b: 2, c: 3) +    #   params.except(:a, :b) # => {"c"=>3} +    #   params.except(:d)     # => {"a"=>1,"b"=>2,"c"=>3} +    def except(*keys) +      new_instance_with_inherited_permitted_status(@parameters.except(*keys))      end      # Removes and returns the key/value pairs matching the given keys. @@ -384,7 +461,7 @@ module ActionController      #   params.extract!(:a, :b) # => {"a"=>1, "b"=>2}      #   params                  # => {"c"=>3}      def extract!(*keys) -      new_instance_with_inherited_permitted_status(super) +      new_instance_with_inherited_permitted_status(@parameters.extract!(*keys))      end      # Returns a new <tt>ActionController::Parameters</tt> with the results of @@ -393,36 +470,80 @@ module ActionController      #   params = ActionController::Parameters.new(a: 1, b: 2, c: 3)      #   params.transform_values { |x| x * 2 }      #   # => {"a"=>2, "b"=>4, "c"=>6} -    def transform_values -      if block_given? -        new_instance_with_inherited_permitted_status(super) +    def transform_values(&block) +      if block +        new_instance_with_inherited_permitted_status( +          @parameters.transform_values(&block) +        )        else -        super +        @parameters.transform_values        end      end -    # This method is here only to make sure that the returned object has the -    # correct +permitted+ status. It should not matter since the parent of -    # this object is +HashWithIndifferentAccess+ -    def transform_keys # :nodoc: -      if block_given? -        new_instance_with_inherited_permitted_status(super) +    # Performs values transformation and returns the altered +    # <tt>ActionController::Parameters</tt> instance. +    def transform_values!(&block) +      @parameters.transform_values!(&block) +      self +    end + +    # Returns a new <tt>ActionController::Parameters</tt> instance with the +    # results of running +block+ once for every key. The values are unchanged. +    def transform_keys(&block) +      if block +        new_instance_with_inherited_permitted_status( +          @parameters.transform_keys(&block) +        )        else -        super +        @parameters.transform_keys        end      end +    # Performs keys transformation and returns the altered +    # <tt>ActionController::Parameters</tt> instance. +    def transform_keys!(&block) +      @parameters.transform_keys!(&block) +      self +    end +      # Deletes and returns a key-value pair from +Parameters+ whose key is equal      # to key. If the key is not found, returns the default value. If the      # optional code block is given and the key is not found, pass in the key      # and return the result of block.      def delete(key, &block) -      convert_hashes_to_parameters(key, super, false) +      convert_value_to_parameters(@parameters.delete(key)) +    end + +    # Returns a new instance of <tt>ActionController::Parameters</tt> with only +    # items that the block evaluates to true. +    def select(&block) +      new_instance_with_inherited_permitted_status(@parameters.select(&block))      end      # Equivalent to Hash#keep_if, but returns nil if no changes were made.      def select!(&block) -      convert_value_to_parameters(super) +      @parameters.select!(&block) +      self +    end +    alias_method :keep_if, :select! + +    # Returns a new instance of <tt>ActionController::Parameters</tt> with items +    # that the block evaluates to true removed. +    def reject(&block) +      new_instance_with_inherited_permitted_status(@parameters.reject(&block)) +    end + +    # Removes items that the block evaluates to true and returns self. +    def reject!(&block) +      @parameters.reject!(&block) +      self +    end +    alias_method :delete_if, :reject! + +    # Returns values that were assigned to the given +keys+. Note that all the +    # +Hash+ objects will be converted to <tt>ActionController::Parameters</tt>. +    def values_at(*keys) +      convert_value_to_parameters(@parameters.values_at(*keys))      end      # Returns an exact copy of the <tt>ActionController::Parameters</tt> @@ -439,11 +560,30 @@ module ActionController        end      end +    # Returns a new <tt>ActionController::Parameters</tt> with all keys from +    # +other_hash+ merges into current hash. +    def merge(other_hash) +      new_instance_with_inherited_permitted_status( +        @parameters.merge(other_hash) +      ) +    end + +    # This is required by ActiveModel attribute assignment, so that user can +    # pass +Parameters+ to a mass assignment methods in a model. It should not +    # matter as we are using +HashWithIndifferentAccess+ internally. +    def stringify_keys # :nodoc: +      dup +    end +      protected        def permitted=(new_permitted)          @permitted = new_permitted        end +      def fields_for_style? +        @parameters.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) } +      end +      private        def new_instance_with_inherited_permitted_status(hash)          self.class.new(hash).tap do |new_instance| @@ -451,40 +591,41 @@ module ActionController          end        end -      def convert_hashes_to_parameters(key, value, assign_if_converted=true) +      def convert_hashes_to_parameters(key, value)          converted = convert_value_to_parameters(value) -        self[key] = converted if assign_if_converted && !converted.equal?(value) +        @parameters[key] = converted unless converted.equal?(value)          converted        end        def convert_value_to_parameters(value) -        if value.is_a?(Array) && !converted_arrays.member?(value) +        case value +        when Array +          return value if converted_arrays.member?(value)            converted = value.map { |_| convert_value_to_parameters(_) }            converted_arrays << converted            converted -        elsif value.is_a?(Parameters) || !value.is_a?(Hash) -          value -        else +        when Hash            self.class.new(value) +        else +          value          end        end        def each_element(object) -        if object.is_a?(Array) -          object.map { |el| yield el }.compact -        elsif fields_for_style?(object) -          hash = object.class.new -          object.each { |k,v| hash[k] = yield v } -          hash -        else -          yield object +        case object +        when Array +          object.grep(Parameters).map { |el| yield el }.compact +        when Parameters +          if object.fields_for_style? +            hash = object.class.new +            object.each { |k,v| hash[k] = yield v } +            hash +          else +            yield object +          end          end        end -      def fields_for_style?(object) -        object.is_a?(Hash) && object.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) } -      end -        def unpermitted_parameters!(params)          unpermitted_keys = unpermitted_keys(params)          if unpermitted_keys.any? @@ -546,14 +687,8 @@ module ActionController        end        def array_of_permitted_scalars?(value) -        if value.is_a?(Array) -          value.all? {|element| permitted_scalar?(element)} -        end -      end - -      def array_of_permitted_scalars_filter(params, key) -        if has_key?(key) && array_of_permitted_scalars?(self[key]) -          params[key] = self[key] +        if value.is_a?(Array) && value.all? {|element| permitted_scalar?(element)} +          yield value          end        end @@ -564,17 +699,17 @@ module ActionController          # Slicing filters out non-declared keys.          slice(*filter.keys).each do |key, value|            next unless value +          next unless has_key? key            if filter[key] == EMPTY_ARRAY              # Declaration { comment_ids: [] }. -            array_of_permitted_scalars_filter(params, key) +            array_of_permitted_scalars?(self[key]) do |val| +              params[key] = val +            end            else              # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }.              params[key] = each_element(value) do |element| -              if element.is_a?(Hash) -                element = self.class.new(element) unless element.respond_to?(:permit) -                element.permit(*Array.wrap(filter[key])) -              end +              element.permit(*Array.wrap(filter[key]))              end            end          end diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index d01927b7cb..b2b3b4283f 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -2,14 +2,6 @@ module ActionController    module Testing      extend ActiveSupport::Concern -    include RackDelegation - -    # TODO : Rewrite tests using controller.headers= to use Rack env -    def headers=(new_headers) -      @_response ||= ActionDispatch::Response.new -      @_response.headers.replace(new_headers) -    end -      # Behavior specific to functional tests      module Functional # :nodoc:        def set_response!(request) diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 5a0e5c62e4..dbf7241a14 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -41,7 +41,11 @@ module ActionController          if original_script_name            options[:original_script_name] = original_script_name          else -          options[:script_name] = same_origin ? request.script_name.dup : script_name +          if same_origin +            options[:script_name] = request.script_name.empty? ? "".freeze : request.script_name.dup +          else +            options[:script_name] = script_name +          end          end          options.freeze        else diff --git a/actionpack/lib/action_controller/middleware.rb b/actionpack/lib/action_controller/middleware.rb deleted file mode 100644 index 437fec3dc6..0000000000 --- a/actionpack/lib/action_controller/middleware.rb +++ /dev/null @@ -1,39 +0,0 @@ -module ActionController -  class Middleware < Metal -    class ActionMiddleware -      def initialize(controller, app) -        @controller, @app = controller, app -      end - -      def call(env) -        request = ActionDispatch::Request.new(env) -        @controller.build(@app).dispatch(:index, request) -      end -    end - -    class << self -      alias build new - -      def new(app) -        ActionMiddleware.new(self, app) -      end -    end - -    attr_internal :app - -    def process(action) -      response = super -      self.status, self.headers, self.response_body = response if response.is_a?(Array) -      response -    end - -    def initialize(app) -      super() -      @_app = app -    end - -    def index -      call(env) -    end -  end -end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb index e8b29c5b5e..e4d19e9dba 100644 --- a/actionpack/lib/action_controller/renderer.rb +++ b/actionpack/lib/action_controller/renderer.rb @@ -34,67 +34,78 @@ module ActionController    #       ApplicationController.renderer.new(method: 'post', https: true)    #    class Renderer -    class_attribute :controller, :defaults -    # Rack environment to render templates in. -    attr_reader :env +    attr_reader :defaults, :controller -    class << self -      delegate :render, to: :new +    DEFAULTS = { +      http_host: 'example.org', +      https: false, +      method: 'get', +      script_name: '', +      input: '' +    }.freeze -      # Create a new renderer class for a specific controller class. -      def for(controller) -        Class.new self do -          self.controller = controller -          self.defaults = { -            http_host: 'example.org', -            https: false, -            method: 'get', -            script_name: '', -            'rack.input' => '' -          } -        end -      end +    # Create a new renderer instance for a specific controller class. +    def self.for(controller, env = {}, defaults = DEFAULTS) +      new(controller, env, defaults) +    end + +    # Create a new renderer for the same controller but with a new env. +    def new(env = {}) +      self.class.new controller, env, defaults +    end + +    # Create a new renderer for the same controller but with new defaults. +    def with_defaults(defaults) +      self.class.new controller, env, self.defaults.merge(defaults)      end      # Accepts a custom Rack environment to render templates in.      # It will be merged with ActionController::Renderer.defaults -    def initialize(env = {}) -      @env = normalize_keys(defaults).merge normalize_keys(env) -      @env['action_dispatch.routes'] = controller._routes +    def initialize(controller, env, defaults) +      @controller = controller +      @defaults = defaults +      @env = normalize_keys defaults.merge(env)      end      # Render templates with any options from ActionController::Base#render_to_string.      def render(*args) -      raise 'missing controller' unless controller? +      raise 'missing controller' unless controller -      instance = controller.build_with_env(env) +      request = ActionDispatch::Request.new @env +      request.routes = controller._routes + +      instance = controller.new +      instance.set_request! request +      instance.set_response! controller.make_response!(request)        instance.render_to_string(*args)      end      private        def normalize_keys(env) -        http_header_format(env).tap do |new_env| -          handle_method_key! new_env -          handle_https_key!  new_env -        end +        new_env = {} +        env.each_pair { |k,v| new_env[rack_key_for(k)] = rack_value_for(k, v) } +        new_env        end -      def http_header_format(env) -        env.transform_keys do |key| -          key.is_a?(Symbol) ? key.to_s.upcase : key -        end -      end +      RACK_KEY_TRANSLATION = { +        http_host:   'HTTP_HOST', +        https:       'HTTPS', +        method:      'REQUEST_METHOD', +        script_name: 'SCRIPT_NAME', +        input:       'rack.input' +      } -      def handle_method_key!(env) -        if method = env.delete('METHOD') -          env['REQUEST_METHOD'] = method.upcase -        end -      end +      IDENTITY = ->(_) { _ } + +      RACK_VALUE_TRANSLATION = { +        https: ->(v) { v ? 'on' : 'off' }, +        method: ->(v) { v.upcase }, +      } + +      def rack_key_for(key); RACK_KEY_TRANSLATION[key]; end -      def handle_https_key!(env) -        if env.has_key? 'HTTPS' -          env['HTTPS'] = env['HTTPS'] ? 'on' : 'off' -        end +      def rack_value_for(key, value) +        RACK_VALUE_TRANSLATION.fetch(key, IDENTITY).call value        end    end  end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 96f161fb09..380e9d29b4 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -1,4 +1,5 @@  require 'rack/session/abstract/id' +require 'active_support/core_ext/hash/conversions'  require 'active_support/core_ext/object/to_query'  require 'active_support/core_ext/module/anonymous'  require 'active_support/core_ext/hash/keys' @@ -10,28 +11,47 @@ module ActionController      DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup      DEFAULT_ENV.delete 'PATH_INFO' -    def initialize(env = {}) -      super +    def self.new_session +      TestSession.new +    end + +    # Create a new test request with default `env` values +    def self.create +      env = {} +      env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application +      env["rack.request.cookie_hash"] = {}.with_indifferent_access +      new(default_env.merge(env), new_session) +    end -      self.session = TestSession.new +    def self.default_env +      DEFAULT_ENV +    end +    private_class_method :default_env + +    def initialize(env, session) +      super(env) + +      self.session = session        self.session_options = TestSession::DEFAULT_OPTIONS +      @custom_param_parsers = { +        Mime[:xml] => lambda { |raw_post| Hash.from_xml(raw_post)['hash'] } +      }      end -    def assign_parameters(routes, controller_path, action, parameters = {}) -      parameters = parameters.symbolize_keys -      extra_keys = routes.extra_keys(parameters.merge(:controller => controller_path, :action => action)) -      non_path_parameters = get? ? query_parameters : request_parameters +    def query_string=(string) +      set_header Rack::QUERY_STRING, string +    end -      parameters.each do |key, value| -        if value.is_a?(Array) && (value.frozen? || value.any?(&:frozen?)) -          value = value.map{ |v| v.duplicable? ? v.dup : v } -        elsif value.is_a?(Hash) && (value.frozen? || value.any?{ |k,v| v.frozen? }) -          value = Hash[value.map{ |k,v| [k, v.duplicable? ? v.dup : v] }] -        elsif value.frozen? && value.duplicable? -          value = value.dup -        end +    def content_type=(type) +      set_header 'CONTENT_TYPE', type +    end + +    def assign_parameters(routes, controller_path, action, parameters, generated_path, query_string_keys) +      non_path_parameters = {} +      path_parameters = {} -        if extra_keys.include?(key) || key == :action || key == :controller +      parameters.each do |key, value| +        if query_string_keys.include?(key)            non_path_parameters[key] = value          else            if value.is_a?(Array) @@ -44,69 +64,88 @@ module ActionController          end        end +      if get? +        if self.query_string.blank? +          self.query_string = non_path_parameters.to_query +        end +      else +        if ENCODER.should_multipart?(non_path_parameters) +          self.content_type = ENCODER.content_type +          data = ENCODER.build_multipart non_path_parameters +        else +          fetch_header('CONTENT_TYPE') do |k| +            set_header k, 'application/x-www-form-urlencoded' +          end + +          case content_mime_type.to_sym +          when nil +            raise "Unknown Content-Type: #{content_type}" +          when :json +            data = ActiveSupport::JSON.encode(non_path_parameters) +          when :xml +            data = non_path_parameters.to_xml +          when :url_encoded_form +            data = non_path_parameters.to_query +          else +            @custom_param_parsers[content_mime_type] = ->(_) { non_path_parameters } +            data = non_path_parameters.to_query +          end +        end + +        set_header 'CONTENT_LENGTH', data.length.to_s +        set_header 'rack.input', StringIO.new(data) +      end + +      fetch_header("PATH_INFO") do |k| +        set_header k, generated_path +      end        path_parameters[:controller] = controller_path        path_parameters[:action] = action -      # Clear the combined params hash in case it was already referenced. -      @env.delete("action_dispatch.request.parameters") +      self.path_parameters = path_parameters +    end -      # Clear the filter cache variables so they're not stale -      @filtered_parameters = @filtered_env = @filtered_path = nil +    ENCODER = Class.new do +      include Rack::Test::Utils + +      def should_multipart?(params) +        # FIXME: lifted from Rack-Test. We should push this separation upstream +        multipart = false +        query = lambda { |value| +          case value +          when Array +            value.each(&query) +          when Hash +            value.values.each(&query) +          when Rack::Test::UploadedFile +            multipart = true +          end +        } +        params.values.each(&query) +        multipart +      end -      data = request_parameters.to_query -      @env['CONTENT_LENGTH'] = data.length.to_s -      @env['rack.input'] = StringIO.new(data) -    end +      public :build_multipart -    def recycle! -      @formats = nil -      @env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } -      @env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } -      @method = @request_method = nil -      @fullpath = @ip = @remote_ip = @protocol = nil -      @env['action_dispatch.request.query_parameters'] = {} -      @set_cookies ||= {} -      @set_cookies.update(Hash[cookie_jar.instance_variable_get("@set_cookies").map{ |k,o| [k,o[:value]] }]) -      deleted_cookies = cookie_jar.instance_variable_get("@delete_cookies") -      @set_cookies.reject!{ |k,v| deleted_cookies.include?(k) } -      cookie_jar.update(rack_cookies) -      cookie_jar.update(cookies) -      cookie_jar.update(@set_cookies) -      cookie_jar.recycle! -    end +      def content_type +        "multipart/form-data; boundary=#{Rack::Test::MULTIPART_BOUNDARY}" +      end +    end.new      private -    def default_env -      DEFAULT_ENV -    end -  end - -  class TestResponse < ActionDispatch::TestResponse -    def recycle! -      initialize +    def params_parsers +      super.merge @custom_param_parsers      end    end    class LiveTestResponse < Live::Response -    def recycle! -      @body = nil -      initialize -    end - -    def body -      @body ||= super -    end -      # Was the response successful?      alias_method :success?, :successful?      # Was the URL not found?      alias_method :missing?, :not_found? -    # Were we redirected? -    alias_method :redirect?, :redirection? -      # Was there a server-side error?      alias_method :error?, :server_error?    end @@ -114,7 +153,7 @@ module ActionController    # Methods #destroy and #load! are overridden to avoid calling methods on the    # @store object, which does not exist for the TestSession class.    class TestSession < Rack::Session::Abstract::SessionHash #:nodoc: -    DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS +    DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS      def initialize(session = {})        super(nil, nil) @@ -139,6 +178,10 @@ module ActionController        clear      end +    def fetch(key, *args, &block) +      @data.fetch(key.to_s, *args, &block) +    end +      private        def load! @@ -195,7 +238,7 @@ module ActionController    #      request. You can modify this object before sending the HTTP request. For example,    #      you might want to set some session properties before sending a GET request.    # <b>@response</b>:: -  #      An ActionController::TestResponse object, representing the response +  #      An ActionDispatch::TestResponse object, representing the response    #      of the last HTTP response. In the above example, <tt>@response</tt> becomes valid    #      after calling +post+. If the various assert methods are not sufficient, then you    #      may use this object to inspect the HTTP response in detail. @@ -317,7 +360,9 @@ module ActionController        # Note that the request method is not verified. The different methods are        # available to make the tests more expressive.        def get(action, *args) -        process_with_kwargs("GET", action, *args) +        res = process_with_kwargs("GET", action, *args) +        cookies.update res.cookies +        res        end        # Simulate a POST request with the given parameters and set/volley the response. @@ -357,7 +402,7 @@ module ActionController          MSG          @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' -        @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') +        @request.env['HTTP_ACCEPT'] ||= [Mime[:js], Mime[:html], Mime[:xml], 'text/xml', '*/*'].join(', ')          __send__(*args).tap do            @request.env.delete 'HTTP_X_REQUESTED_WITH'            @request.env.delete 'HTTP_ACCEPT' @@ -365,19 +410,6 @@ module ActionController        end        alias xhr :xml_http_request -      def paramify_values(hash_or_array_or_value) -        case hash_or_array_or_value -        when Hash -          Hash[hash_or_array_or_value.map{|key, value| [key, paramify_values(value)] }] -        when Array -          hash_or_array_or_value.map {|i| paramify_values(i)} -        when Rack::Test::UploadedFile, ActionDispatch::Http::UploadedFile -          hash_or_array_or_value -        else -          hash_or_array_or_value.to_param -        end -      end -        # Simulate a HTTP request to +action+ by specifying request method,        # parameters and set/volley the response.        # @@ -426,7 +458,7 @@ module ActionController          end          if body.present? -          @request.env['RAW_POST_DATA'] = body +          @request.set_header 'RAW_POST_DATA', body          end          if http_method.present? @@ -437,10 +469,6 @@ module ActionController          parameters ||= {} -        # Ensure that numbers and symbols passed as params are converted to -        # proper params, as is the case when engaging rack. -        parameters = paramify_values(parameters) if html_format?(parameters) -          if format.present?            parameters[:format] = format          end @@ -451,37 +479,52 @@ module ActionController            @controller.extend(Testing::Functional)          end -        @request.recycle! -        @response.recycle! +        self.cookies.update @request.cookies +        self.cookies.update_cookies_from_jar +        @request.set_header 'HTTP_COOKIE', cookies.to_header +        @request.delete_header 'action_dispatch.cookies' + +        @request          = TestRequest.new scrub_env!(@request.env), @request.session +        @response         = build_response @response_klass +        @response.request = @request          @controller.recycle! -        @request.env['REQUEST_METHOD'] = http_method +        @request.set_header 'REQUEST_METHOD', http_method -        controller_class_name = @controller.class.anonymous? ? -          "anonymous" : -          @controller.class.controller_path +        parameters = parameters.symbolize_keys -        @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters) +        generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action.to_s)) +        generated_path = generated_path(generated_extras) +        query_string_keys = query_parameter_names(generated_extras) + +        @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters, generated_path, query_string_keys)          @request.session.update(session) if session          @request.flash.update(flash || {})          if xhr -          @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' -          @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') +          @request.set_header 'HTTP_X_REQUESTED_WITH', 'XMLHttpRequest' +          @request.fetch_header('HTTP_ACCEPT') do |k| +            @request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], 'text/xml', '*/*'].join(', ') +          end          end          @controller.request  = @request          @controller.response = @response -        build_request_uri(controller_class_name, action, parameters) +        @request.fetch_header("SCRIPT_NAME") do |k| +          @request.set_header k, @controller.config.relative_url_root +        end          @controller.recycle!          @controller.process(action) -        if cookies = @request.env['action_dispatch.cookies'] +        @request.delete_header 'HTTP_COOKIE' + +        if @request.have_cookie_jar?            unless @response.committed? -            cookies.write(@response) +            @request.cookie_jar.write(@response) +            self.cookies.update(@request.cookie_jar.instance_variable_get(:@cookies))            end          end          @response.prepare! @@ -493,21 +536,34 @@ module ActionController          end          if xhr -          @request.env.delete 'HTTP_X_REQUESTED_WITH' -          @request.env.delete 'HTTP_ACCEPT' +          @request.delete_header 'HTTP_X_REQUESTED_WITH' +          @request.delete_header 'HTTP_ACCEPT'          end +        @request.query_string = ''          @response        end +      def controller_class_name +        @controller.class.anonymous? ? "anonymous" : @controller.class.controller_path +      end + +      def generated_path(generated_extras) +        generated_extras[0] +      end + +      def query_parameter_names(generated_extras) +        generated_extras[1] + [:controller, :action] +      end +        def setup_controller_request_and_response          @controller = nil unless defined? @controller -        response_klass = TestResponse +        @response_klass = ActionDispatch::TestResponse          if klass = self.class.controller_class            if klass < ActionController::Live -            response_klass = LiveTestResponse +            @response_klass = LiveTestResponse            end            unless @controller              begin @@ -518,8 +574,8 @@ module ActionController            end          end -        @request          = build_request -        @response         = build_response response_klass +        @request          = TestRequest.create +        @response         = build_response @response_klass          @response.request = @request          if @controller @@ -528,12 +584,8 @@ module ActionController          end        end -      def build_request -        TestRequest.new -      end -        def build_response(klass) -        klass.new +        klass.create        end        included do @@ -545,6 +597,14 @@ module ActionController        private +      def scrub_env!(env) +        env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } +        env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } +        env.delete 'action_dispatch.request.query_parameters' +        env.delete 'action_dispatch.request.request_parameters' +        env +      end +        def process_with_kwargs(http_method, action, *args)          if kwarg_request?(args)            args.first.merge!(method: http_method) @@ -591,23 +651,6 @@ module ActionController          end        end -      def build_request_uri(controller_class_name, action, parameters) -        unless @request.env["PATH_INFO"] -          options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters -          options.update( -            :controller => controller_class_name, -            :action => action, -            :relative_url_root => nil, -            :_recall => @request.path_parameters) - -          url, query_string = @routes.path_for(options).split("?", 2) - -          @request.env["SCRIPT_NAME"] = @controller.config.relative_url_root -          @request.env["PATH_INFO"] = url -          @request.env["QUERY_STRING"] = query_string || "" -        end -      end -        def html_format?(parameters)          return true unless parameters.key?(:format)          Mime.fetch(parameters[:format]) { Mime['html'] }.html? diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index dcd3ee0644..f6336c8c7a 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -52,6 +52,7 @@ module ActionDispatch      autoload :DebugExceptions      autoload :ExceptionWrapper      autoload :Flash +    autoload :LoadInterlock      autoload :ParamsParser      autoload :PublicExceptions      autoload :Reloader diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index cc1cb3f0f0..30ade14c26 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -1,4 +1,3 @@ -  module ActionDispatch    module Http      module Cache @@ -8,13 +7,13 @@ module ActionDispatch          HTTP_IF_NONE_MATCH     = 'HTTP_IF_NONE_MATCH'.freeze          def if_modified_since -          if since = env[HTTP_IF_MODIFIED_SINCE] +          if since = get_header(HTTP_IF_MODIFIED_SINCE)              Time.rfc2822(since) rescue nil            end          end          def if_none_match -          env[HTTP_IF_NONE_MATCH] +          get_header HTTP_IF_NONE_MATCH          end          def if_none_match_etags @@ -51,52 +50,51 @@ module ActionDispatch        end        module Response -        attr_reader :cache_control, :etag -        alias :etag? :etag +        attr_reader :cache_control          def last_modified -          if last = headers[LAST_MODIFIED] +          if last = get_header(LAST_MODIFIED)              Time.httpdate(last)            end          end          def last_modified? -          headers.include?(LAST_MODIFIED) +          has_header? LAST_MODIFIED          end          def last_modified=(utc_time) -          headers[LAST_MODIFIED] = utc_time.httpdate +          set_header LAST_MODIFIED, utc_time.httpdate          end          def date -          if date_header = headers[DATE] +          if date_header = get_header(DATE)              Time.httpdate(date_header)            end          end          def date? -          headers.include?(DATE) +          has_header? DATE          end          def date=(utc_time) -          headers[DATE] = utc_time.httpdate +          set_header DATE, utc_time.httpdate          end          def etag=(etag)            key = ActiveSupport::Cache.expand_cache_key(etag) -          @etag = self[ETAG] = %("#{Digest::MD5.hexdigest(key)}") +          super %("#{Digest::MD5.hexdigest(key)}")          end +        def etag?; etag; end +        private          DATE          = 'Date'.freeze          LAST_MODIFIED = "Last-Modified".freeze -        ETAG          = "ETag".freeze -        CACHE_CONTROL = "Cache-Control".freeze          SPECIAL_KEYS  = Set.new(%w[extras no-cache max-age public must-revalidate])          def cache_control_segments -          if cache_control = self[CACHE_CONTROL] +          if cache_control = _cache_control              cache_control.delete(' ').split(',')            else              [] @@ -123,12 +121,11 @@ module ActionDispatch          def prepare_cache_control!            @cache_control = cache_control_headers -          @etag = self[ETAG]          end          def handle_conditional_get!            if etag? || last_modified? || !@cache_control.empty? -            set_conditional_cache_control! +            set_conditional_cache_control!(@cache_control)            end          end @@ -138,24 +135,24 @@ module ActionDispatch          PRIVATE               = "private".freeze          MUST_REVALIDATE       = "must-revalidate".freeze -        def set_conditional_cache_control! +        def set_conditional_cache_control!(cache_control)            control = {}            cc_headers = cache_control_headers            if extras = cc_headers.delete(:extras) -            @cache_control[:extras] ||= [] -            @cache_control[:extras] += extras -            @cache_control[:extras].uniq! +            cache_control[:extras] ||= [] +            cache_control[:extras] += extras +            cache_control[:extras].uniq!            end            control.merge! cc_headers -          control.merge! @cache_control +          control.merge! cache_control            if control.empty? -            self[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL +            self._cache_control = DEFAULT_CACHE_CONTROL            elsif control[:no_cache] -            self[CACHE_CONTROL] = NO_CACHE +            self._cache_control = NO_CACHE              if control[:extras] -              self[CACHE_CONTROL] += ", #{control[:extras].join(', ')}" +              self._cache_control = _cache_control + ", #{control[:extras].join(', ')}"              end            else              extras  = control[:extras] @@ -167,7 +164,7 @@ module ActionDispatch              options << MUST_REVALIDATE if control[:must_revalidate]              options.concat(extras) if extras -            self[CACHE_CONTROL] = options.join(", ") +            self._cache_control = options.join(", ")            end          end        end diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 3170389b36..9dcab79c3a 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/object/duplicable'  require 'action_dispatch/http/parameter_filter'  module ActionDispatch @@ -25,19 +23,19 @@ module ActionDispatch        NULL_PARAM_FILTER = ParameterFilter.new # :nodoc:        NULL_ENV_FILTER   = ParameterFilter.new ENV_MATCH # :nodoc: -      def initialize(env) +      def initialize          super          @filtered_parameters = nil          @filtered_env        = nil          @filtered_path       = nil        end -      # Return a hash of parameters with all sensitive data replaced. +      # Returns a hash of parameters with all sensitive data replaced.        def filtered_parameters          @filtered_parameters ||= parameter_filter.filter(parameters)        end -      # Return a hash of request.env with all sensitive data replaced. +      # Returns a hash of request.env with all sensitive data replaced.        def filtered_env          @filtered_env ||= env_filter.filter(@env)        end @@ -50,13 +48,13 @@ module ActionDispatch      protected        def parameter_filter -        parameter_filter_for @env.fetch("action_dispatch.parameter_filter") { +        parameter_filter_for fetch_header("action_dispatch.parameter_filter") {            return NULL_PARAM_FILTER          }        end        def env_filter -        user_key = @env.fetch("action_dispatch.parameter_filter") { +        user_key = fetch_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..f4b806b8b5 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,20 +14,20 @@ module ActionDispatch      private -      def location_filter +      def location_filters          if request -          request.env['action_dispatch.redirect_filter'] || [] +          request.get_header('action_dispatch.redirect_filter') || []          else            []          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..12f81dc1a5 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -30,27 +30,37 @@ 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 + +      # Add a value to a multivalued header like Vary or Accept-Encoding. +      def add(key, value) +        @req.add_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 +68,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.fetch_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 +93,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..7acf91902d 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -10,17 +10,18 @@ module ActionDispatch          self.ignore_accept_header = false        end -      # The MIME type of the HTTP request, such as Mime::XML. +      # The MIME type of the HTTP request, such as Mime[:xml].        #        # 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'] =~ /^([^,\;]*)/ +        fetch_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 @@ -28,46 +29,52 @@ module ActionDispatch          content_mime_type && content_mime_type.to_s        end +      def has_content_type? +        has_header? 'CONTENT_TYPE' +      end +        # Returns the accepted MIME type for the request.        def accepts -        @env["action_dispatch.request.accepts"] ||= begin -          header = @env['HTTP_ACCEPT'].to_s.strip +        fetch_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        # Returns the MIME type for the \format used in the request.        # -      #   GET /posts/5.xml   | request.format => Mime::XML -      #   GET /posts/5.xhtml | request.format => Mime::HTML -      #   GET /posts/5       | request.format => Mime::HTML or MIME::JS, or request.accepts.first +      #   GET /posts/5.xml   | request.format => Mime[:xml] +      #   GET /posts/5.xhtml | request.format => Mime[:html] +      #   GET /posts/5       | request.format => Mime[:html] or Mime[:js], or request.accepts.first        #        def format(view_path = [])          formats.first || Mime::NullType.instance        end        def formats -        @env["action_dispatch.request.formats"] ||= begin +        fetch_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            elsif xhr? -            [Mime::JS] +            [Mime[:js]]            else -            [Mime::HTML] +            [Mime[:html]]            end +          set_header k, v          end        end @@ -102,7 +109,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 +128,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..b64f660ec5 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,23 +1,31 @@ -require 'set'  require 'singleton'  require 'active_support/core_ext/module/attribute_accessors'  require 'active_support/core_ext/string/starts_ends_with'  module Mime -  class Mimes < Array -    def symbols -      @symbols ||= map(&:to_sym) +  class Mimes +    include Enumerable + +    def initialize +      @mimes = [] +      @symbols = nil      end -    %w(<< concat shift unshift push pop []= clear compact! collect! -    delete delete_at delete_if flatten! map! insert reject! reverse! -    replace slice! sort! uniq!).each do |method| -      module_eval <<-CODE, __FILE__, __LINE__ + 1 -        def #{method}(*) -          @symbols = nil -          super -        end -      CODE +    def each +      @mimes.each { |x| yield x } +    end + +    def <<(type) +      @mimes << type +      @symbols = nil +    end + +    def delete_if +      @mimes.delete_if { |x| yield x }.tap { @symbols = nil } +    end + +    def symbols +      @symbols ||= map(&:to_sym)      end    end @@ -35,6 +43,42 @@ module Mime        return type if type.is_a?(Type)        EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k }      end + +    def const_missing(sym) +      ext = sym.downcase +      if Mime[ext] +        ActiveSupport::Deprecation.warn <<-eow +Accessing mime types via constants is deprecated.  Please change: + +  `Mime::#{sym}` + +to: + +  `Mime[:#{ext}]` +        eow +        Mime[ext] +      else +        super +      end +    end + +    def const_defined?(sym, inherit = true) +      ext = sym.downcase +      if Mime[ext] +        ActiveSupport::Deprecation.warn <<-eow +Accessing mime types via constants is deprecated.  Please change: + +  `Mime.const_defined?(#{sym})` + +to: + +  `Mime[:#{ext}]` +        eow +        true +      else +        super +      end +    end    end    # Encapsulates the notion of a mime type. Can be used at render time, for example, with: @@ -45,15 +89,12 @@ 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    #   end    class Type -    @@html_types = Set.new [:html, :all] -    cattr_reader :html_types -      attr_reader :symbol      @register_callbacks = [] @@ -66,7 +107,7 @@ module Mime        def initialize(index, name, q = nil)          @index = index          @name = name -        q ||= 0.0 if @name == Mime::ALL.to_s # default wildcard match to end of list +        q ||= 0.0 if @name == '*/*'.freeze # default wildcard match to end of list          @q = ((q || 1.0).to_f * 100).to_i        end @@ -91,7 +132,7 @@ module Mime            exchange_xml_items if app_xml_idx > text_xml_idx  # make sure app_xml is ahead of text_xml in the list            delete_at(text_xml_idx)                 # delete text_xml from the list          elsif text_xml_idx -          text_xml.name = Mime::XML.to_s +          text_xml.name = Mime[:xml].to_s          end          # Look for more specific XML-based types and sort them ahead of app/xml @@ -120,7 +161,7 @@ module Mime          end          def app_xml_idx -          @app_xml_idx ||= index(Mime::XML.to_s) +          @app_xml_idx ||= index(Mime[:xml].to_s)          end          def text_xml @@ -160,17 +201,17 @@ module Mime        end        def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false) -        Mime.const_set(symbol.upcase, Type.new(string, symbol, mime_type_synonyms)) +        new_mime = Type.new(string, symbol, mime_type_synonyms) -        new_mime = Mime.const_get(symbol.upcase)          SET << new_mime -        ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = SET.last } unless skip_lookup -        ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = SET.last } +        ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = new_mime } unless skip_lookup +        ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = new_mime }          @register_callbacks.each do |callback|            callback.call(new_mime)          end +        new_mime        end        def parse(accept_header) @@ -200,28 +241,27 @@ module Mime          parse_data_with_trailing_star($1) if accept_header =~ TRAILING_STAR_REGEXP        end -      # For an input of <tt>'text'</tt>, returns <tt>[Mime::JSON, Mime::XML, Mime::ICS, -      # Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT]</tt>. +      # For an input of <tt>'text'</tt>, returns <tt>[Mime[:json], Mime[:xml], Mime[:ics], +      # Mime[:html], Mime[:css], Mime[:csv], Mime[:js], Mime[:yaml], Mime[:text]</tt>.        # -      # For an input of <tt>'application'</tt>, returns <tt>[Mime::HTML, Mime::JS, -      # Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM]</tt>. -      def parse_data_with_trailing_star(input) -        Mime::SET.select { |m| m =~ input } +      # For an input of <tt>'application'</tt>, returns <tt>[Mime[:html], Mime[:js], +      # Mime[:xml], Mime[:yaml], Mime[:atom], Mime[:json], Mime[:rss], Mime[:url_encoded_form]</tt>. +      def parse_data_with_trailing_star(type) +        Mime::SET.select { |m| m =~ type }        end        # This method is opposite of register method.        # -      # Usage: +      # To unregister a MIME type:        #        #   Mime::Type.unregister(:mobile)        def unregister(symbol) -        symbol = symbol.upcase -        mime = Mime.const_get(symbol) -        Mime.instance_eval { remove_const(symbol) } - -        SET.delete_if { |v| v.eql?(mime) } -        LOOKUP.delete_if { |_,v| v.eql?(mime) } -        EXTENSION_LOOKUP.delete_if { |_,v| v.eql?(mime) } +        symbol = symbol.downcase +        if mime = Mime[symbol] +          SET.delete_if { |v| v.eql?(mime) } +          LOOKUP.delete_if { |_, v| v.eql?(mime) } +          EXTENSION_LOOKUP.delete_if { |_, v| v.eql?(mime) } +        end        end      end @@ -243,7 +283,7 @@ module Mime      end      def ref -      to_sym || to_s +      symbol || to_s      end      def ===(list) @@ -255,24 +295,23 @@ module Mime      end      def ==(mime_type) -      return false if mime_type.blank? +      return false unless mime_type        (@synonyms + [ self ]).any? do |synonym|          synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym        end      end      def =~(mime_type) -      return false if mime_type.blank? +      return false unless mime_type        regexp = Regexp.new(Regexp.quote(mime_type.to_s)) -      (@synonyms + [ self ]).any? do |synonym| -        synonym.to_s =~ regexp -      end +      @synonyms.any? { |synonym| synonym.to_s =~ regexp } || @string =~ regexp      end      def html? -      @@html_types.include?(to_sym) || @string =~ /html/ +      symbol == :html || @string =~ /html/      end +    def all?; false; end      private @@ -292,6 +331,22 @@ module Mime      end    end +  class AllType < Type +    include Singleton + +    def initialize +      super '*/*', :all +    end + +    def all?; true; end +    def html?; true; end +  end + +  # ALL isn't a real MIME type, so we don't register it for lookup with the +  # other concrete types. It's a wildcard match that we use for `respond_to` +  # negotiation internals. +  ALL = AllType.instance +    class NullType      include Singleton diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index 01a10c693b..87715205d9 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -31,6 +31,3 @@ Mime::Type.register "application/json", :json, %w( text/x-json application/jsonr  Mime::Type.register "application/pdf", :pdf, [], %w(pdf)  Mime::Type.register "application/zip", :zip, [], %w(zip) - -# Create Mime::ALL but do not add it to the SET. -Mime::ALL = Mime::Type.new("*/*", :all, []) diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb index df4b073a17..e826551f4b 100644 --- a/actionpack/lib/action_dispatch/http/parameter_filter.rb +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -30,36 +30,46 @@ module ActionDispatch              when Regexp                regexps << item              else -              strings << item.to_s +              strings << Regexp.escape(item.to_s)              end            end -          regexps << Regexp.new(strings.join('|'), true) unless strings.empty? -          new regexps, blocks +          deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.".freeze) } +          deep_strings, strings = strings.partition { |s| s.include?("\\.".freeze) } + +          regexps << Regexp.new(strings.join('|'.freeze), true) unless strings.empty? +          deep_regexps << Regexp.new(deep_strings.join('|'.freeze), true) unless deep_strings.empty? + +          new regexps, deep_regexps, blocks          end -        attr_reader :regexps, :blocks +        attr_reader :regexps, :deep_regexps, :blocks -        def initialize(regexps, blocks) +        def initialize(regexps, deep_regexps, blocks)            @regexps = regexps +          @deep_regexps = deep_regexps.any? ? deep_regexps : nil            @blocks  = blocks          end -        def call(original_params) +        def call(original_params, parents = [])            filtered_params = {}            original_params.each do |key, value| +            parents.push(key) if deep_regexps              if regexps.any? { |r| key =~ r }                value = FILTERED +            elsif deep_regexps && (joined = parents.join('.')) && deep_regexps.any? { |r| joined =~ r } +              value = FILTERED              elsif value.is_a?(Hash) -              value = call(value) +              value = call(value, parents)              elsif value.is_a?(Array) -              value = value.map { |v| v.is_a?(Hash) ? call(v) : v } +              value = value.map { |v| v.is_a?(Hash) ? call(v, parents) : v }              elsif blocks.any?                key = key.dup if key.duplicable?                value = value.dup if value.duplicable?                blocks.each { |b| b.call(key, value) }              end +            parents.pop if deep_regexps              filtered_params[key] = value            end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index c2f05ecc86..248ecfd676 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -1,27 +1,41 @@ -require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/hash/indifferent_access' -  module ActionDispatch    module Http      module Parameters        PARAMETERS_KEY = 'action_dispatch.request.path_parameters' +      DEFAULT_PARSERS = { +        Mime[:json] => lambda { |raw_post| +          data = ActiveSupport::JSON.decode(raw_post) +          data.is_a?(Hash) ? data : {:_json => data} +        } +      } + +      def self.included(klass) +        class << klass +          attr_accessor :parameter_parsers +        end + +        klass.parameter_parsers = DEFAULT_PARSERS +      end        # 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,31 +43,29 @@ module ActionDispatch        #        #   {'action' => 'my_action', 'controller' => 'my_controller'}        def path_parameters -        @env[PARAMETERS_KEY] ||= {} +        get_header(PARAMETERS_KEY) || {}        end -    private +      private -      # 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 +      def parse_formatted_parameters(parsers) +        return yield if content_length.zero? + +        strategy = parsers.fetch(content_mime_type) { return yield } + +        begin +          strategy.call(raw_post) +        rescue => e # JSON or Ruby code block errors +          my_logger = logger || ActiveSupport::Logger.new($stderr) +          my_logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{raw_post}" + +          raise ParamsParser::ParseError.new(e.message, e)          end        end + +      def params_parsers +        ActionDispatch::Request.parameter_parsers +      end      end    end  end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 3c62c055e5..c6ab4dbc9a 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -13,14 +13,14 @@ require 'action_dispatch/http/url'  require 'active_support/core_ext/array/conversions'  module ActionDispatch -  class Request < Rack::Request +  class Request +    include Rack::Request::Helpers      include ActionDispatch::Http::Cache::Request      include ActionDispatch::Http::MimeNegotiation      include ActionDispatch::Http::Parameters      include ActionDispatch::Http::FilterParameters      include ActionDispatch::Http::URL - -    HTTP_X_REQUEST_ID = "HTTP_X_REQUEST_ID".freeze # :nodoc: +    include Rack::Request::Env      autoload :Session, 'action_dispatch/request/session'      autoload :Utils,   'action_dispatch/request/utils' @@ -31,15 +31,20 @@ 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 HTTP_X_FORWARDED_HOST +        SERVER_ADDR +        ].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 @@ -60,13 +65,32 @@ module ActionDispatch        path_parameters.each do |key, value|          next unless value.respond_to?(:valid_encoding?)          unless value.valid_encoding? -          raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" +          raise ActionController::BadRequest, "Invalid parameter encoding: #{key} => #{value.inspect}"          end        end      end +    PASS_NOT_FOUND = Class.new { # :nodoc: +      def self.action(_); self; end +      def self.call(_); [404, {'X-Cascade' => 'pass'}, []]; end +    } + +    def controller_class +      check_path_parameters! +      params = path_parameters + +      if params.key?(:controller) +        controller_param = params[:controller].underscore +        params[:action] ||= 'index' +        const_name = "#{controller_param.camelize}Controller" +        ActiveSupport::Dependencies.constantize(const_name) +      else +        PASS_NOT_FOUND +      end +    end +      def key?(key) -      @env.key?(key) +      has_header? key      end      # List of HTTP request methods from the following RFCs: @@ -103,27 +127,50 @@ 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 http_auth_salt +      get_header "action_dispatch.http_auth_salt" +    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 +180,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 +192,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 +203,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 +242,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 +254,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 +270,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 +313,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,51 +324,68 @@ module ActionDispatch        else          self.session = {}        end -      @env['action_dispatch.request.flash_hash'] = nil +      self.flash = 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 || {})) +      fetch_header("action_dispatch.request.query_parameters") do |k| +        set_header k, Request::Utils.normalize_encode_params(super || {}) +      end      rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e -      raise ActionController::BadRequest.new(:query, e) +      raise ActionController::BadRequest.new("Invalid query parameters: #{e.message}", e)      end      alias :query_parameters :GET      # Override Rack's POST method to support indifferent access      def POST -      @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) +      fetch_header("action_dispatch.request.request_parameters") do +        pr = parse_formatted_parameters(params_parsers) do |params| +          super || {} +        end +        self.request_parameters = Request::Utils.normalize_encode_params(pr) +      end +    rescue ParamsParser::ParseError # one of the parse strategies blew up +      self.request_parameters = Request::Utils.normalize_encode_params(super || {}) +      raise      rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e -      raise ActionController::BadRequest.new(:request, e) +      raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}", e)      end      alias :request_parameters :POST      # 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. +    # True if the request came from localhost, 127.0.0.1, or ::1.      def local?        LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip      end -    protected -      def parse_query(*) -        Utils.deep_munge(super) -      end +    def request_parameters=(params) +      raise if params.nil? +      set_header("action_dispatch.request.request_parameters".freeze, params) +    end + +    def logger +      get_header("action_dispatch.logger".freeze) +    end + +    def commit_flash +    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..c54efb6541 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -32,14 +32,35 @@ module ActionDispatch # :nodoc:    #    end    #  end    class Response +    class Header < DelegateClass(Hash) # :nodoc: +      def initialize(response, header) +        @response = response +        super(header) +      end + +      def []=(k,v) +        if @response.sending? || @response.sent? +          raise ActionDispatch::IllegalStateError, 'header already sent' +        end + +        super +      end + +      def merge(other) +        self.class.new @response, __getobj__.merge(other) +      end + +      def to_hash +        __getobj__.dup +      end +    end +      # The request that the response is responding to.      attr_accessor :request      # The HTTP status code.      attr_reader :status -    attr_writer :sending_file -      # Get headers for this response.      attr_reader :header @@ -48,29 +69,19 @@ module ActionDispatch # :nodoc:      delegate :[], :[]=, :to => :@header      delegate :each, :to => :@stream -    # Sets the HTTP response's content MIME type. For example, in the controller -    # you could write this: -    # -    #  response.content_type = "text/plain" -    # -    # If a character set has been defined for this response (see charset=) then -    # the character set information will also be included in the content type -    # information. -    attr_reader   :content_type - -    # The charset of the response. HTML wants to know the encoding of the -    # content you're giving them, so we need to send that along. -    attr_reader :charset -      CONTENT_TYPE = "Content-Type".freeze      SET_COOKIE   = "Set-Cookie".freeze      LOCATION     = "Location".freeze -    NO_CONTENT_CODES = [204, 304] +    NO_CONTENT_CODES = [100, 101, 102, 204, 205, 304]      cattr_accessor(:default_charset) { "utf-8" }      cattr_accessor(:default_headers)      include Rack::Response::Helpers +    # Aliasing these off because AD::Http::Cache::Response defines them +    alias :_cache_control :cache_control +    alias :_cache_control= :cache_control= +      include ActionDispatch::Http::FilterRedirect      include ActionDispatch::Http::Cache::Response      include MonitorMixin @@ -80,11 +91,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 @@ -109,37 +130,41 @@ module ActionDispatch # :nodoc:        end      end +    def self.create(status = 200, header = {}, body = [], default_headers: self.default_headers) +      header = merge_default_headers(header, default_headers) +      new status, header, body +    end + +    def self.merge_default_headers(original, default) +      default.respond_to?(:merge) ? default.merge(original) : original +    end +      # The underlying body, as a streamable object.      attr_reader :stream -    def initialize(status = 200, header = {}, body = [], default_headers: self.class.default_headers) +    def initialize(status = 200, header = {}, body = [])        super() -      header = merge_default_headers(header, default_headers) -      @header = header +      @header = Header.new(self, header)        self.body, self.status = body, status -      @sending_file = false        @blank        = false        @cv           = new_cond        @committed    = false        @sending      = false        @sent         = false -      @content_type = nil -      @charset      = self.class.default_charset - -      if content_type = self[CONTENT_TYPE] -        type, charset = content_type.split(/;\s*charset=/) -        @content_type = Mime::Type.lookup(type) -        @charset = charset || self.class.default_charset -      end        prepare_cache_control!        yield self if block_given?      end +    def has_header?(key);   headers.key? key;   end +    def get_header(key);    headers[key];       end +    def set_header(key, v); headers[key] = v;   end +    def delete_header(key); headers.delete key; end +      def await_commit        synchronize do          @cv.wait_until { @committed } @@ -184,18 +209,51 @@ module ActionDispatch # :nodoc:      # Sets the HTTP content type.      def content_type=(content_type) -      @content_type = content_type.to_s +      header_info = parse_content_type +      set_content_type content_type.to_s, header_info.charset || self.class.default_charset      end -    # Sets the HTTP character set. +    # Sets the HTTP response's content MIME type. For example, in the controller +    # you could write this: +    # +    #  response.content_type = "text/plain" +    # +    # If a character set has been defined for this response (see charset=) then +    # the character set information will also be included in the content type +    # information. + +    def content_type +      parse_content_type.mime_type +    end + +    def sending_file=(v) +      if true == v +        self.charset = false +      end +    end + +    # 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 +      header_info = parse_content_type +      if false == charset +        set_header CONTENT_TYPE, header_info.mime_type        else -        @charset = charset +        content_type = header_info.mime_type +        set_content_type content_type, charset || self.class.default_charset        end      end +    # The charset of the response. HTML wants to know the encoding of the +    # content you're giving them, so we need to send that along. +    def charset +      header_info = parse_content_type +      header_info.charset || self.class.default_charset +    end +      # The response code of the request.      def response_code        @status @@ -222,9 +280,11 @@ 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 + +    def write(string) +      @stream.write string      end      EMPTY = " " @@ -242,31 +302,49 @@ module ActionDispatch # :nodoc:        end      end -    def body_parts -      parts = [] -      @stream.each { |x| parts << x } -      parts -    end +    # Avoid having to pass an open file handle as the response body. +    # Rack::Sendfile will usually intercept the response and uses +    # the path directly, so there is no reason to open the file. +    class FileBody #:nodoc: +      attr_reader :to_path -    def set_cookie(key, value) -      ::Rack::Utils.set_cookie_header!(header, key, value) +      def initialize(path) +        @to_path = path +      end + +      def body +        File.binread(to_path) +      end + +      # Stream the file's contents if Rack::Sendfile isn't present. +      def each +        File.open(to_path, 'rb') do |file| +          while chunk = file.read(16384) +            yield chunk +          end +        end +      end      end -    def delete_cookie(key, value={}) -      ::Rack::Utils.delete_cookie_header!(header, key, value) +    # Send the file stored at +path+ as the response body. +    def send_file(path) +      commit! +      @stream = FileBody.new(path)      end -    # The location header we'll be responding with. -    def location -      headers[LOCATION] +    def reset_body! +      @stream = build_buffer(self, [])      end -    alias_method :redirect_url, :location -    # Sets the location header we'll be responding with. -    def location=(url) -      headers[LOCATION] = url +    def body_parts +      parts = [] +      @stream.each { |x| parts << x } +      parts      end +    # The location header we'll be responding with. +    alias_method :redirect_url, :location +      def close        stream.close if stream.respond_to?(:close)      end @@ -297,7 +375,7 @@ module ActionDispatch # :nodoc:      #   assert_equal 'AuthorOfNewPage', r.cookies['author']      def cookies        cookies = {} -      if header = self[SET_COOKIE] +      if header = get_header(SET_COOKIE)          header = header.split("\n") if header.respond_to?(:to_str)          header.each do |cookie|            if pair = cookie.split(';').first @@ -311,19 +389,36 @@ module ActionDispatch # :nodoc:    private +    ContentTypeHeader = Struct.new :mime_type, :charset +    NullContentTypeHeader = ContentTypeHeader.new nil, nil + +    def parse_content_type +      content_type = get_header CONTENT_TYPE +      if content_type +        type, charset = content_type.split(/;\s*charset=/) +        type = nil if type.empty? +        ContentTypeHeader.new(type, charset) +      else +        NullContentTypeHeader +      end +    end + +    def set_content_type(content_type, charset) +      type = (content_type || '').dup +      type << "; charset=#{charset}" if charset +      set_header CONTENT_TYPE, type +    end +      def before_committed        return if committed?        assign_default_content_type_and_charset!        handle_conditional_get! +      handle_no_content!      end      def before_sending      end -    def merge_default_headers(original, default) -      default.respond_to?(:merge) ? default.merge(original) : original -    end -      def build_buffer(response, body)        Buffer.new response, body      end @@ -333,18 +428,11 @@ module ActionDispatch # :nodoc:      end      def assign_default_content_type_and_charset! -      return if self[CONTENT_TYPE].present? - -      @content_type ||= Mime::HTML - -      type = @content_type.to_s.dup -      type << "; charset=#{charset}" if append_charset? - -      self[CONTENT_TYPE] = type -    end +      return if content_type -    def append_charset? -      !@sending_file && @charset != false +      ct = parse_content_type +      set_content_type(ct.mime_type || Mime[:html].to_s, +                       ct.charset || self.class.default_charset)      end      class RackBody @@ -383,11 +471,15 @@ module ActionDispatch # :nodoc:        end      end -    def rack_response(status, header) -      header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join) - +    def handle_no_content!        if NO_CONTENT_CODES.include?(@status) -        header.delete CONTENT_TYPE +        @header.delete CONTENT_TYPE +        @header.delete 'Content-Length' +      end +    end + +    def rack_response(status, header) +      if NO_CONTENT_CODES.include?(status)          [status, header, []]        else          [status, header, RackBody.new(self)] 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..92b10b6d3b 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -1,11 +1,10 @@  require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/hash/slice'  module ActionDispatch    module Http      module URL        IP_HOST_REGEXP  = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ -      HOST_REGEXP     = /(^[^:]+:\/\/)?([^:]+)(?::(\d+$))?/ +      HOST_REGEXP     = /(^[^:]+:\/\/)?(\[[^\]]+\]|[^:]+)(?::(\d+$))?/        PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/        mattr_accessor :tld_length @@ -184,7 +183,7 @@ module ActionDispatch          end        end -      def initialize(env) +      def initialize          super          @protocol = nil          @port     = nil @@ -229,10 +228,10 @@ module ActionDispatch        #   req = Request.new 'HTTP_HOST' => 'example.com:8080'        #   req.raw_host_with_port # => "example.com:8080"        def raw_host_with_port -        if forwarded = env["HTTP_X_FORWARDED_HOST"].presence +        if forwarded = x_forwarded_host.presence            forwarded.split(/,\s?/).last          else -          env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}" +          get_header('HTTP_HOST') || "#{server_name || server_addr}:#{get_header('SERVER_PORT')}"          end        end @@ -245,7 +244,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 @@ -348,7 +347,7 @@ module ActionDispatch        end        def server_port -        @env['SERVER_PORT'].to_i +        get_header('SERVER_PORT').to_i        end        # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index c0566c6fc9..0323360faa 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.present?) || 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/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb index 47bf76bdbf..7063b44bb5 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/dot.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 -  module ActionDispatch    module Journey # :nodoc:      module NFA # :nodoc: diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb index bb01c087bc..2793c5668d 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,15 @@ module ActionDispatch          def symbol?; false; end          def literal?; false; end +        def terminal?; false; end +        def star?; false; end +        def cat?; false; end +        def group?; false; end        end        class Terminal < Node # :nodoc:          alias :symbol :left +        def terminal?; true; end        end        class Literal < Terminal # :nodoc: @@ -69,11 +74,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? @@ -89,9 +96,11 @@ module ActionDispatch        class Group < Unary # :nodoc:          def type; :GROUP; end +        def group?; true; end        end        class Star < Unary # :nodoc: +        def star?; true; end          def type; :STAR; end          def name @@ -111,6 +120,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..5ee8810066 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 @@ -42,7 +46,7 @@ module ActionDispatch          end          def names -          @names ||= spec.grep(Nodes::Symbol).map(&:name) +          @names ||= spec.find_all(&:symbol?).map(&:name)          end          def required_names @@ -50,36 +54,11 @@ module ActionDispatch          end          def optional_names -          @optional_names ||= spec.grep(Nodes::Group).flat_map { |group| -            group.grep(Nodes::Symbol) +          @optional_names ||= spec.find_all(&:group?).flat_map { |group| +            group.find_all(&:symbol?)            }.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..35c2b1b86e 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] || // +        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..306d2e674a 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 -  module ActionDispatch    module Journey # :nodoc:      class Format @@ -92,6 +90,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 +154,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 dd1f140051..2889acaeb8 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -1,15 +1,57 @@  require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/object/blank'  require 'active_support/key_generator'  require 'active_support/message_verifier'  require 'active_support/json'  module ActionDispatch -  class Request < Rack::Request +  class Request      def cookie_jar -      env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self) +      fetch_header('action_dispatch.cookies'.freeze) do +        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 +160,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 +180,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 +203,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 +214,18 @@ 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 + +      private + +      def upgrade_legacy_signed_cookies? +        request.secret_token.present? && request.secret_key_base.present? +      end      end      # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream @@ -187,7 +235,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) @@ -197,6 +245,11 @@ module ActionDispatch        rescue ActiveSupport::MessageVerifier::InvalidSignature          nil        end + +      private +        def parse(name, signed_message) +          super || verify_and_upgrade_legacy_signed_message(name, signed_message) +        end      end      class CookieJar #:nodoc: @@ -216,38 +269,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(request) -        env = request.env -        key_generator = env[GENERATOR_KEY] -        options = options_for_env env - -        host = request.host -        secure = request.ssl? - -        new(key_generator, host, secure, options).tap do |hash| -          hash.update(request.cookies) +      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 @@ -283,6 +316,17 @@ module ActionDispatch          self        end +      def update_cookies_from_jar +        request_jar = @request.cookie_jar.instance_variable_get(:@cookies) +        set_cookies = request_jar.reject { |k,_| @delete_cookies.key?(k) } + +        @cookies.update set_cookies if set_cookies +      end + +      def to_header +        @cookies.map { |k,v| "#{k}=#{v}" }.join ';' +      end +        def handle_options(options) #:nodoc:          options[:path] ||= "/" @@ -292,12 +336,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 @@ -352,47 +396,71 @@ module ActionDispatch        end        def write(headers) -        @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) } -        @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } -      end - -      def recycle! #:nodoc: -        @set_cookies = {} -        @delete_cookies = {} +        if header = make_set_cookie_header(headers[HTTP_HEADER]) +          headers[HTTP_HEADER] = header +        end        end        mattr_accessor :always_write_cookie        self.always_write_cookie = false        private -        def write_cookie?(cookie) -          @secure || !cookie[:secure] || always_write_cookie -        end + +      def make_set_cookie_header(header) +        header = @set_cookies.inject(header) { |m, (k, v)| +          if write_cookie?(v) +            ::Rack::Utils.add_cookie_to_header(m, k, v) +          else +            m +          end +        } +        @delete_cookies.inject(header) { |m, (k, v)| +          ::Rack::Utils.add_remove_cookie_to_header(m, k, v) +        } +      end + +      def write_cookie?(cookie) +        request.ssl? || !cookie[:secure] || always_write_cookie +      end      end -    class PermanentCookieJar #:nodoc: +    class AbstractCookieJar # :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) -        @parent_jar[name.to_s] +        if data = @parent_jar[name.to_s] +          parse name, data +        end        end        def []=(name, options)          if options.is_a?(Hash)            options.symbolize_keys!          else -          options = { :value => options } +          options = { value: options }          end -        options[:expires] = 20.years.from_now +        commit(options)          @parent_jar[name] = options        end + +      protected +        def request; @parent_jar.request; end + +      private +        def parse(name, data); data; end +        def commit(options); end +    end + +    class PermanentCookieJar < AbstractCookieJar # :nodoc: +      private +        def commit(options) +          options[:expires] = 20.years.from_now +        end      end      class JsonSerializer # :nodoc: @@ -410,7 +478,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 +498,7 @@ module ActionDispatch          end          def serializer -          serializer = @options[:serializer] || :marshal +          serializer = request.cookies_serializer || :marshal            case serializer            when :marshal              Marshal @@ -442,48 +510,32 @@ module ActionDispatch          end          def digest -          @options[:digest] || 'SHA1' +          request.cookies_digest || 'SHA1' +        end + +        def key_generator +          request.key_generator          end      end -    class SignedCookieJar #:nodoc: -      include ChainedCookieJars +    class SignedCookieJar < AbstractCookieJar # :nodoc:        include SerializedCookieJars -      def initialize(parent_jar, key_generator, options = {}) -        @parent_jar = parent_jar -        @options = options -        secret = key_generator.generate_key(@options[:signed_cookie_salt]) +      def initialize(parent_jar) +        super +        secret = key_generator.generate_key(request.signed_cookie_salt)          @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)        end -      # Returns the value of the cookie by +name+ if it is untampered, -      # returns +nil+ otherwise or if no such cookie exists. -      def [](name) -        if signed_message = @parent_jar[name] -          deserialize name, verify(signed_message) +      private +        def parse(name, signed_message) +          deserialize name, @verifier.verified(signed_message)          end -      end -      # Signs and sets the cookie named +name+. The second argument may be the cookie's -      # value or a hash of options as documented above. -      def []=(name, options) -        if options.is_a?(Hash) -          options.symbolize_keys! +        def commit(options)            options[:value] = @verifier.generate(serialize(options[:value])) -        else -          options = { :value => @verifier.generate(serialize(options)) } -        end - -        raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE -        @parent_jar[name] = options -      end -      private -        def verify(signed_message) -          @verifier.verify(signed_message) -        rescue ActiveSupport::MessageVerifier::InvalidSignature -          nil +          raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE          end      end @@ -493,60 +545,36 @@ module ActionDispatch      # re-saves them using the new key generator to provide a smooth upgrade path.      class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:        include VerifyAndUpgradeLegacySignedMessage - -      def [](name) -        if signed_message = @parent_jar[name] -          deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message) -        end -      end      end -    class EncryptedCookieJar #:nodoc: -      include ChainedCookieJars +    class EncryptedCookieJar < AbstractCookieJar # :nodoc:        include SerializedCookieJars -      def initialize(parent_jar, key_generator, options = {}) +      def initialize(parent_jar) +        super +          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 -      # Returns the value of the cookie by +name+ if it is untampered, -      # returns +nil+ otherwise or if no such cookie exists. -      def [](name) -        if encrypted_message = @parent_jar[name] -          deserialize name, decrypt_and_verify(encrypted_message) -        end -      end - -      # Encrypts and sets the cookie named +name+. The second argument may be the cookie's -      # value or a hash of options as documented above. -      def []=(name, options) -        if options.is_a?(Hash) -          options.symbolize_keys! -        else -          options = { :value => options } -        end - -        options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value])) - -        raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE -        @parent_jar[name] = options -      end -        private -        def decrypt_and_verify(encrypted_message) -          @encryptor.decrypt_and_verify(encrypted_message) +        def parse(name, encrypted_message) +          deserialize name, @encryptor.decrypt_and_verify(encrypted_message)          rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage            nil          end + +        def commit(options) +          options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value])) + +          raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE +        end      end      # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore @@ -555,12 +583,6 @@ module ActionDispatch      # encrypts and re-saves them using the new key generator to provide a smooth upgrade path.      class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:        include VerifyAndUpgradeLegacySignedMessage - -      def [](name) -        if encrypted_or_signed_message = @parent_jar[name] -          deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message) -        end -      end      end      def initialize(app) @@ -568,9 +590,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..5fd984cd07 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) @@ -61,7 +61,7 @@ module ActionDispatch      end      def traces -      appplication_trace_with_ids = [] +      application_trace_with_ids = []        framework_trace_with_ids = []        full_trace_with_ids = [] @@ -69,7 +69,7 @@ module ActionDispatch          trace_with_id = { id: idx, trace: trace }          if application_trace.include?(trace) -          appplication_trace_with_ids << trace_with_id +          application_trace_with_ids << trace_with_id          else            framework_trace_with_ids << trace_with_id          end @@ -78,7 +78,7 @@ module ActionDispatch        end        { -        "Application Trace" => appplication_trace_with_ids, +        "Application Trace" => application_trace_with_ids,          "Framework Trace" => framework_trace_with_ids,          "Full Trace" => full_trace_with_ids        } @@ -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..c51dcd542a 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -1,15 +1,6 @@  require 'active_support/core_ext/hash/keys'  module ActionDispatch -  class Request < Rack::Request -    # Access the contents of the flash. Use <tt>flash["notice"]</tt> to -    # 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"]) -    end -  end -    # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed    # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create    # action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can @@ -47,6 +38,40 @@ module ActionDispatch    class Flash      KEY = 'action_dispatch.request.flash_hash'.freeze +    module RequestMethods +      # Access the contents of the flash. Use <tt>flash["notice"]</tt> to +      # read a notice you put there or <tt>flash["notice"] = "hello"</tt> +      # to put a new one. +      def 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 + +      def commit_flash # :nodoc: +        session    = self.session || {} +        flash_hash = self.flash_hash + +        if flash_hash && (flash_hash.present? || session.key?('flash')) +          session["flash"] = flash_hash.to_session_value +          self.flash = flash_hash.dup +        end + +        if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) +            session.key?('flash') && session['flash'].nil? +          session.delete('flash') +        end +      end +    end +      class FlashNow #:nodoc:        attr_accessor :flash @@ -258,25 +283,10 @@ module ActionDispatch        end      end -    def initialize(app) -      @app = app -    end - -    def call(env) -      @app.call(env) -    ensure -      session    = Request::Session.find(env) || {} -      flash_hash = env[KEY] - -      if flash_hash && (flash_hash.present? || session.key?('flash')) -        session["flash"] = flash_hash.to_session_value -        env[KEY] = flash_hash.dup -      end +    def self.new(app) app; end +  end -      if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) -        session.key?('flash') && session['flash'].nil? -        session.delete('flash') -      end -    end +  class Request +    prepend Flash::RequestMethods    end  end diff --git a/actionpack/lib/action_dispatch/middleware/load_interlock.rb b/actionpack/lib/action_dispatch/middleware/load_interlock.rb new file mode 100644 index 0000000000..07f498319c --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/load_interlock.rb @@ -0,0 +1,21 @@ +require 'active_support/dependencies' +require 'rack/body_proxy' + +module ActionDispatch +  class LoadInterlock +    def initialize(app) +      @app = app +    end + +    def call(env) +      interlock = ActiveSupport::Dependencies.interlock +      interlock.start_running +      response = @app.call(env) +      body = Rack::BodyProxy.new(response[2]) { interlock.done_running } +      response[2] = body +      response +    ensure +      interlock.done_running unless body +    end +  end +end diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index 29d43faeed..18af0a583a 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 @@ -13,48 +18,13 @@ module ActionDispatch        end      end -    DEFAULT_PARSERS = { Mime::JSON => :json } - -    def initialize(app, parsers = {}) -      @app, @parsers = app, DEFAULT_PARSERS.merge(parsers) +    # 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 self.new(app, parsers = {}) +      ActionDispatch::Request.parameter_parsers = ActionDispatch::Request::DEFAULT_PARSERS.merge(parsers) +      app      end - -    def call(env) -      if params = parse_formatted_parameters(env) -        env["action_dispatch.request.request_parameters"] = params -      end - -      @app.call(env) -    end - -    private -      def parse_formatted_parameters(env) -        request = Request.new(env) - -        return false if request.content_length.zero? - -        strategy = @parsers[request.content_mime_type] - -        return false unless strategy - -        case strategy -        when Proc -          strategy.call(request.raw_post) -        when :json -          data = ActiveSupport::JSON.decode(request.raw_post) -          data = {:_json => data} unless data.is_a?(Hash) -          Request::Utils.deep_munge(data).with_indifferent_access -        else -          false -        end -      rescue => e # JSON or Ruby code block errors -        logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" - -        raise ParseError.new(e.message, e) -      end - -      def logger(env) -        env['action_dispatch.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/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index 6c7fba00cb..af9a29eb07 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -1,5 +1,3 @@ -require 'active_support/deprecation/reporting' -  module ActionDispatch    # ActionDispatch::Reloader provides prepare and cleanup callbacks,    # intended to assist with code reloading during development. 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..9e50fea3fc 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) @@ -74,15 +79,14 @@ module ActionDispatch        end      end -    class AbstractStore < Rack::Session::Abstract::ID +    class AbstractStore < Rack::Session::Abstract::Persisted        include Compatibility        include StaleSessionCheck        include SessionObject        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/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb index 857e49a682..589ae46e38 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb @@ -18,7 +18,7 @@ module ActionDispatch        end        # Get a session from the cache. -      def get_session(env, sid) +      def find_session(env, sid)          unless sid and session = @cache.read(cache_key(sid))            sid, session = generate_sid, {}          end @@ -26,7 +26,7 @@ module ActionDispatch        end        # Set a session in the cache. -      def set_session(env, sid, session, options) +      def write_session(env, sid, session, options)          key = cache_key(sid)          if session            @cache.write(key, session, :expires_in => options[:expire_after]) @@ -37,7 +37,7 @@ module ActionDispatch        end        # Remove a session from the cache. -      def destroy_session(env, sid, options) +      def delete_session(env, sid, options)          @cache.delete(cache_key(sid))          generate_sid        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..0e636b8257 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -53,7 +53,7 @@ module ActionDispatch      #      # Note that changing the secret key will invalidate all existing sessions!      # -    # Because CookieStore extends Rack::Session::Abstract::ID, many of the +    # Because CookieStore extends Rack::Session::Abstract::Persisted, many of the      # options described there can be used to customize the session cookie that      # is generated. For example:      # @@ -62,25 +62,21 @@ module ActionDispatch      # would set the session cookie to expire automatically 14 days after creation.      # Other useful options include <tt>:key</tt>, <tt>:secure</tt> and      # <tt>:httponly</tt>. -    class CookieStore < Rack::Session::Abstract::ID -      include Compatibility -      include StaleSessionCheck -      include SessionObject - +    class CookieStore < AbstractStore        def initialize(app, options={})          super(app, options.merge!(:cookie_only => true))        end -      def destroy_session(env, session_id, options) +      def delete_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 +84,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.fetch_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 +108,20 @@ module ActionDispatch          data        end -      def set_session(env, sid, session_data, options) +      def write_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/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 7b3d8bcc5b..47f475559a 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -1,72 +1,129 @@  module ActionDispatch +  # This middleware is added to the stack when `config.force_ssl = true`. +  # It does three jobs to enforce secure HTTP requests: +  # +  #   1. TLS redirect. http:// requests are permanently redirected to https:// +  #      with the same URL host, path, etc. Pass `:host` and/or `:port` to +  #      modify the destination URL. This is always enabled. +  # +  #   2. Secure cookies. Sets the `secure` flag on cookies to tell browsers they +  #      mustn't be sent along with http:// requests. This is always enabled. +  # +  #   3. HTTP Strict Transport Security (HSTS). Tells the browser to remember +  #      this site as TLS-only and automatically redirect non-TLS requests. +  #      Enabled by default. Pass `hsts: false` to disable. +  # +  # Configure HSTS with `hsts: { … }`: +  #   * `expires`: How long, in seconds, these settings will stick. Defaults to +  #     `180.days` (recommended). The minimum required to qualify for browser +  #     preload lists is `18.weeks`. +  #   * `subdomains`: Set to `true` to tell the browser to apply these settings +  #     to all subdomains. This protects your cookies from interception by a +  #     vulnerable site on a subdomain. Defaults to `false`. +  #   * `preload`: Advertise that this site may be included in browsers' +  #     preloaded HSTS lists. HSTS protects your site on every visit *except the +  #     first visit* since it hasn't seen your HSTS header yet. To close this +  #     gap, browser vendors include a baked-in list of HSTS-enabled sites. +  #     Go to https://hstspreload.appspot.com to submit your site for inclusion. +  # +  # Disabling HSTS: To turn off HSTS, omitting the header is not enough. +  # Browsers will remember the original HSTS directive until it expires. +  # Instead, use the header to tell browsers to expire HSTS immediately. +  # Setting `hsts: false` is a shortcut for `hsts: { expires: 0 }`.    class SSL -    YEAR = 31536000 +    # Default to 180 days, the low end for https://www.ssllabs.com/ssltest/ +    # and greater than the 18-week requirement for browser preload lists. +    HSTS_EXPIRES_IN = 15552000      def self.default_hsts_options -      { :expires => YEAR, :subdomains => false } +      { expires: HSTS_EXPIRES_IN, subdomains: false, preload: false }      end -    def initialize(app, options = {}) +    def initialize(app, redirect: {}, hsts: {}, **options)        @app = app -      @hsts = options.fetch(:hsts, {}) -      @hsts = {} if @hsts == true -      @hsts = self.class.default_hsts_options.merge(@hsts) if @hsts +      if options[:host] || options[:port] +        ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc +          The `:host` and `:port` options are moving within `:redirect`: +          `config.ssl_options = { redirect: { host: …, port: … }}`. +        end_warning +        @redirect = options.slice(:host, :port) +      else +        @redirect = redirect +      end -      @host    = options[:host] -      @port    = options[:port] +      @hsts_header = build_hsts_header(normalize_hsts_options(hsts))      end      def call(env) -      request = Request.new(env) +      request = Request.new env        if request.ssl? -        status, headers, body = @app.call(env) -        headers.reverse_merge!(hsts_headers) -        flag_cookies_as_secure!(headers) -        [status, headers, body] +        @app.call(env).tap do |status, headers, body| +          set_hsts_header! headers +          flag_cookies_as_secure! headers +        end        else -        redirect_to_https(request) +        redirect_to_https request        end      end      private -      def redirect_to_https(request) -        host = @host || request.host -        port = @port || request.port - -        location = "https://#{host}" -        location << ":#{port}" if port != 80 -        location << request.fullpath - -        headers = { 'Content-Type' => 'text/html', 'Location' => location } - -        [301, headers, []] +      def set_hsts_header!(headers) +        headers['Strict-Transport-Security'.freeze] ||= @hsts_header        end -      # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02 -      def hsts_headers -        if @hsts -          value = "max-age=#{@hsts[:expires].to_i}" -          value += "; includeSubDomains" if @hsts[:subdomains] -          { 'Strict-Transport-Security' => value } +      def normalize_hsts_options(options) +        case options +        # Explicitly disabling HSTS clears the existing setting from browsers +        # by setting expiry to 0. +        when false +          self.class.default_hsts_options.merge(expires: 0) +        # Default to enabled, with default options. +        when nil, true +          self.class.default_hsts_options          else -          {} +          self.class.default_hsts_options.merge(options)          end        end +      # http://tools.ietf.org/html/rfc6797#section-6.1 +      def build_hsts_header(hsts) +        value = "max-age=#{hsts[:expires].to_i}" +        value << "; includeSubDomains" if hsts[:subdomains] +        value << "; preload" if hsts[:preload] +        value +      end +        def flag_cookies_as_secure!(headers) -        if cookies = headers['Set-Cookie'] -          cookies = cookies.split("\n") +        if cookies = headers['Set-Cookie'.freeze] +          cookies = cookies.split("\n".freeze) -          headers['Set-Cookie'] = cookies.map { |cookie| +          headers['Set-Cookie'.freeze] = cookies.map { |cookie|              if cookie !~ /;\s*secure\s*(;|$)/i                "#{cookie}; secure"              else                cookie              end -          }.join("\n") +          }.join("\n".freeze)          end        end + +      def redirect_to_https(request) +        [ @redirect.fetch(:status, 301), +          { 'Content-Type' => 'text/html', +            'Location' => https_location_for(request) }, +          @redirect.fetch(:body, []) ] +      end + +      def https_location_for(request) +        host = @redirect[:host] || request.host +        port = @redirect[:port] || request.port + +        location = "https://#{host}" +        location << ":#{port}" if port != 80 && port != 443 +        location << request.fullpath +        location +      end    end  end diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index bbf734f103..90e2ae6802 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,46 @@ 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 +    private      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 + +    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..75f8e05a3f 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -3,8 +3,8 @@ require 'active_support/core_ext/uri'  module ActionDispatch    # This middleware returns a file's contents from disk in the body response. -  # When initialized, it can accept an optional 'Cache-Control' header, which -  # will be set when a response containing a file's contents is delivered. +  # When initialized, it can accept optional HTTP headers, which will be set +  # when a response containing a file's contents is delivered.    #    # This middleware will render the file specified in `env["PATH_INFO"]`    # where the base path is in the +root+ directory. For example, if the +root+ @@ -13,12 +13,11 @@ module ActionDispatch    # located at `public/assets/application.js` if the file exists. If the file    # does not exist, a 404 "File not Found" response will be returned.    class FileHandler -    def initialize(root, cache_control, index: 'index') +    def initialize(root, index: 'index', headers: {})        @root          = root.chomp('/')        @compiled_root = /^#{Regexp.escape(root)}/ -      headers        = cache_control && { 'Cache-Control' => cache_control } -      @file_server = ::Rack::File.new(@root, headers) -      @index = index +      @file_server   = ::Rack::File.new(@root, headers) +      @index         = index      end      # Takes a path to a file. If the file is found, has valid encoding, and has @@ -28,14 +27,14 @@ module ActionDispatch      # Used by the `Static` class to check the existence of a valid file      # in the server's `public/` directory (see Static#call).      def match?(path) -      path = URI.parser.unescape(path) +      path = ::Rack::Utils.unescape_path path        return false unless path.valid_encoding?        path = Rack::Utils.clean_path_info path        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 @@ -43,31 +42,35 @@ module ActionDispatch          end        } -        return ::Rack::Utils.escape(match) +        return ::Rack::Utils.escape_path(match)        end      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,17 +79,17 @@ 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)          can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/          gzip_path     = "#{path}.gz" -        if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape(gzip_path))) +        if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)))            gzip_path          else            false @@ -104,22 +107,30 @@ module ActionDispatch    # produce a directory traversal using this middleware. Only 'GET' and 'HEAD'    # requests will result in a file being returned.    class Static -    def initialize(app, path, cache_control = nil, index: 'index') +    def initialize(app, path, deprecated_cache_control = :not_set, index: 'index', headers: {}) +      if deprecated_cache_control != :not_set +        ActiveSupport::Deprecation.warn("The `cache_control` argument is deprecated," \ +                                        "replaced by `headers: { 'Cache-Control' => #{deprecated_cache_control} }`, " \ +                                        " and will be removed in Rails 5.1.") +        headers['Cache-Control'.freeze] = deprecated_cache_control +      end +        @app = app -      @file_handler = FileHandler.new(path, cache_control, index: index) +      @file_handler = FileHandler.new(path, index: index, headers: headers)      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..9e7fcbd849 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -1,41 +1,41 @@  require 'rack/session/abstract/id'  module ActionDispatch -  class Request < Rack::Request +  class 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(:delete_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..a8151a8224 100644 --- a/actionpack/lib/action_dispatch/request/utils.rb +++ b/actionpack/lib/action_dispatch/request/utils.rb @@ -1,28 +1,49 @@  module ActionDispatch -  class Request < Rack::Request +  class Request      class Utils # :nodoc:        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.rb b/actionpack/lib/action_dispatch/routing.rb index a42cf72f60..59c3f9248f 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -1,8 +1,3 @@ -# encoding: UTF-8 -require 'active_support/core_ext/object/to_param' -require 'active_support/core_ext/regexp' -require 'active_support/dependencies/autoload' -  module ActionDispatch    # The routing module provides URL rewriting in native Ruby. It's a way to    # redirect incoming requests to controllers and actions. This replaces @@ -58,7 +53,7 @@ module ActionDispatch    #     resources :posts, :comments    #   end    # -  # Alternately, you can add prefixes to your path without using a separate +  # Alternatively, you can add prefixes to your path without using a separate    # directory by using +scope+. +scope+ takes additional options which    # apply to all enclosed routes.    # @@ -151,6 +146,7 @@ module ActionDispatch    #     get 'geocode/:postalcode' => :show, constraints: {    #       postalcode: /\d{5}(-\d{4})?/    #     } +  #   end    #    # Constraints can include the 'ignorecase' and 'extended syntax' regular    # expression modifiers: diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index 48c10a7d4c..f3a5268d2e 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -16,10 +16,6 @@ module ActionDispatch          app.app        end -      def verb -        super.source.gsub(/[$^]/, '') -      end -        def path          super.spec.to_s        end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 7cfe4693c1..7c0404ca62 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,10 +1,8 @@ -require 'active_support/core_ext/hash/except'  require 'active_support/core_ext/hash/reverse_merge'  require 'active_support/core_ext/hash/slice'  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/core_ext/regexp'  require 'action_dispatch/routing/redirection'  require 'action_dispatch/routing/endpoint' @@ -16,7 +14,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 +27,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 +44,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 +56,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 +242,54 @@ 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 -            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}" +          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 normalize_defaults!(options) -            options.each_pair do |key, default| -              unless Regexp === default -                @defaults[key] = default +          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 -            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?" +              if requirement.multiline? +                raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}" +              end              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) -            elsif blocks.any? -              Constraints.new(dispatcher(defaults), blocks, true) +            if to.is_a?(Class) && to < ActionController::Metal +              Routing::RouteSet::StaticDispatcher.new to              else -              dispatcher(defaults) +              if to.respond_to?(:call) +                Constraints.new(to, blocks, Constraints::CALL) +              elsif blocks.any? +                Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE) +              else +                dispatcher(defaults.key?(:controller)) +              end              end            end @@ -303,40 +347,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) +            Routing::RouteSet::Dispatcher.new raise_on_name_error            end        end @@ -368,7 +401,8 @@ module ActionDispatch          # because this means it will be matched first. As this is the most popular route          # of most Rails applications, this is beneficial.          def root(options = {}) -          match '/', { :as => :root, :via => :get }.merge!(options) +          name = has_named_route?(:root) ? nil : :root +          match '/', { as: name, via:  :get }.merge!(options)          end          # Matches a url pattern to one or more routes. @@ -443,6 +477,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 a 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 +637,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 @@ -616,6 +665,7 @@ module ActionDispatch                    super(options)                  else                    prefix_options = options.slice(*_route.segment_keys) +                  prefix_options[:relative_url_root] = ''.freeze                    # we must actually delete prefix segment keys to avoid passing them to next url_for                    _route.segment_keys.each { |k| options.delete(k) }                    _routes.url_helpers.send("#{name}_path", prefix_options) @@ -670,7 +720,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 +827,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 +836,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 +866,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 +923,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 +998,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 +1033,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 +1060,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 +1115,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 +1139,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 +1169,7 @@ module ActionDispatch            end            def resource_scope -            { :controller => controller } +            controller            end            alias :collection_scope :path @@ -1115,17 +1192,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 +1228,8 @@ module ActionDispatch            alias :member_scope :path            alias :nested_scope :path + +          def singleton?; true; end          end          def resources_path_names(options) @@ -1187,20 +1264,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 +1425,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 +1466,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 +1490,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 +1505,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 +1518,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 +1541,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 +1582,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 +1590,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 +1679,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 +1697,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 +1742,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 +1770,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 +1777,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 +1794,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 +1812,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: @@ -1759,7 +1867,7 @@ module ActionDispatch                # and return nil in case it isn't. Otherwise, we pass the invalid name                # forward so the underlying router engine treats it and raises an exception.                if as.nil? -                candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate) +                candidate unless candidate !~ /\A[_a-z]/i || has_named_route?(candidate)                else                  candidate                end @@ -1781,6 +1889,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 +2007,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 +2062,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..339e2b7c4a 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,65 +20,45 @@ 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)) +          params     = req.path_parameters +          controller = controller req +          res        = controller.make_response! req +          dispatch(controller, params[:action], req, res) +        rescue NameError => e +          if @raise_on_name_error +            raise ActionController::RoutingError, e.message, e.backtrace +          else              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 -        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) +        def controller(req) +          req.controller_class          end -        def dispatch(controller, action, env) -          controller.action(action).call(env) +        def dispatch(controller, action, req, res) +          controller.dispatch(action, req, res)          end +      end -        def normalize_controller!(params) -          params[:controller] = params[:controller].underscore if params.key?(:controller) +      class StaticDispatcher < Dispatcher +        def initialize(controller_class) +          super(false) +          @controller_class = controller_class          end -        def merge_default_action!(params) -          params[:action] ||= 'index' -        end +        private + +        def controller(_); @controller_class; end        end        # A NamedRouteCollection instance is a collection of named routes, and also @@ -88,6 +67,7 @@ module ActionDispatch        class NamedRouteCollection          include Enumerable          attr_reader :routes, :url_helpers_module, :path_helpers_module +        private :routes          def initialize            @routes  = {} @@ -142,6 +122,7 @@ module ActionDispatch          end          def key?(name) +          return unless name            routes.key? name.to_sym          end @@ -199,9 +180,9 @@ module ActionDispatch              private              def optimized_helper(args) -              params = parameterize_args(args) { |k| +              params = parameterize_args(args) do                  raise_generation_error(args) -              } +              end                @route.format params              end @@ -267,9 +248,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 @@ -351,7 +336,7 @@ module ActionDispatch          @set    = Journey::Routes.new          @router = Journey::Router.new @set -        @formatter = Journey::Formatter.new @set +        @formatter = Journey::Formatter.new self        end        def relative_url_root @@ -366,6 +351,11 @@ module ActionDispatch          ActionDispatch::Request        end +      def make_request(env) +        request_class.new env +      end +      private :make_request +        def draw(&block)          clear! unless @disable_clear_and_finalize          eval_block(block) @@ -409,10 +399,6 @@ module ActionDispatch          @prepend.each { |blk| eval_block(blk) }        end -      def dispatcher(defaults) -        Routing::RouteSet::Dispatcher.new(defaults) -      end -        module MountedHelpers          extend ActiveSupport::Concern          include UrlFor @@ -508,7 +494,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 +505,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 +523,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 +546,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 +600,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 @@ -721,14 +656,18 @@ module ActionDispatch        RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,                            :trailing_slash, :anchor, :params, :only_path, :script_name, -                          :original_script_name] +                          :original_script_name, :relative_url_root]        def optimize_routes_generation?          default_url_options.empty?        end        def find_script_name(options) -        options.delete(:script_name) || relative_url_root || '' +        options.delete(:script_name) || find_relative_url_root(options) || '' +      end + +      def find_relative_url_root(options) +        options.delete(:relative_url_root) || relative_url_root        end        def path_for(options, route_name = nil) @@ -774,7 +713,7 @@ module ActionDispatch        end        def call(env) -        req = request_class.new(env) +        req = make_request(env)          req.path_info = Journey::Router::Utils.normalize_path(req.path_info)          @router.serve(req)        end @@ -790,7 +729,7 @@ module ActionDispatch            raise ActionController::RoutingError, e.message          end -        req = request_class.new(env) +        req = make_request(env)          @router.recognize(req) do |route, params|            params.merge!(extras)            params.each do |key, value| @@ -803,14 +742,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 eb554ec383..b6c031dcf4 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -1,7 +1,7 @@  module ActionDispatch    module Routing      # In <tt>config/routes.rb</tt> you define URL-to-controller mappings, but the reverse -    # is also possible: an URL can be generated from one of your routing definitions. +    # is also possible: a URL can be generated from one of your routing definitions.      # URL generation functionality is centralized in this module.      #      # See ActionDispatch::Routing for general information about routing and routes.rb. @@ -52,9 +52,11 @@ module ActionDispatch      # argument.      #      # For convenience reasons, mailers provide a shortcut for ActionController::UrlFor#url_for. -    # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlFor#url_for' -    # in full. However, mailers don't have hostname information, and that's why you'll still -    # have to specify the <tt>:host</tt> argument when generating URLs in mailers. +    # So within mailers, you only have to type +url_for+ instead of 'ActionController::UrlFor#url_for' +    # in full. However, mailers don't have hostname information, and you still have to provide +    # the +:host+ argument or set the default host that will be used in all mailers using the +    # configuration option +config.action_mailer.default_url_options+. For more information on +    # url_for in mailers read the ActionMailer#Base documentation.      #      #      # == URL generation for named routes @@ -147,6 +149,20 @@ module ActionDispatch        #    # => 'http://somehost.org/myapp/tasks/testing'        #    url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp", only_path: true        #    # => '/myapp/tasks/testing' +      # +      # Missing routes keys may be filled in from the current request's parameters +      # (e.g. +:controller+, +:action+, +:id+ and any other parameters that are +      # placed in the path). Given that the current action has been reached +      # through `GET /users/1`: +      # +      #   url_for(only_path: true)                        # => '/users/1' +      #   url_for(only_path: true, action: 'edit')        # => '/users/1/edit' +      #   url_for(only_path: true, action: 'edit', id: 2) # => '/users/2/edit' +      # +      # Notice that no +:id+ parameter was provided to the first +url_for+ call +      # and the helper used the one from the route's path. Any path parameter +      # implicitly used by +url_for+ can always be overwritten like shown on the +      # last +url_for+ calls.        def url_for(options = nil)          case options          when nil @@ -155,12 +171,17 @@ 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            HelperMethodBuilder.url.handle_string_call self, options          when Array -          polymorphic_url(options, options.extract_options!) +          components = options.dup +          polymorphic_url(components, components.extract_options!)          when Class            HelperMethodBuilder.url.handle_class_call self, options          else diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb index 21b3b89d22..715d94d406 100644 --- a/actionpack/lib/action_dispatch/testing/assertions.rb +++ b/actionpack/lib/action_dispatch/testing/assertions.rb @@ -12,7 +12,7 @@ module ActionDispatch      include Rails::Dom::Testing::Assertions      def html_document -      @html_document ||= if @response.content_type === Mime::XML +      @html_document ||= if @response.content_type.to_s =~ /xml$/          Nokogiri::XML::Document.parse(@response.body)        else          Nokogiri::HTML::Document.parse(@response.body) diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 13a72220b3..b6e21b0d28 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -3,6 +3,13 @@ module ActionDispatch    module Assertions      # A small suite of assertions that test responses from \Rails applications.      module ResponseAssertions +      RESPONSE_PREDICATES = { # :nodoc: +        success:  :successful?, +        missing:  :not_found?, +        redirect: :redirection?, +        error:    :server_error?, +      } +        # Asserts that the response is one of the following types:        #        # * <tt>:success</tt>   - Status code was in the 200-299 range @@ -20,11 +27,9 @@ module ActionDispatch        #   # assert that the response code was status code 401 (unauthorized)        #   assert_response 401        def assert_response(type, message = nil) -        message ||= "Expected response to be a <#{type}>, but was <#{@response.response_code}>" -          if Symbol === type            if [:success, :missing, :redirect, :error].include?(type) -            assert @response.send("#{type}?"), message +            assert_predicate @response, RESPONSE_PREDICATES[type], message            else              code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type]              if code.nil? diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index c94eea9134..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 @@ -183,7 +183,7 @@ module ActionDispatch            end            # Assume given controller -          request = ActionController::TestRequest.new +          request = ActionController::TestRequest.create            if path =~ %r{://}              fail_on(URI::InvalidURIError, msg) do diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index dc664d5540..7e59bb68cf 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 @@ -350,15 +354,15 @@ module ActionDispatch            if xhr              headers ||= {}              headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' -            headers['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') +            headers['HTTP_ACCEPT'] ||= [Mime[:js], Mime[:html], Mime[:xml], 'text/xml', '*/*'].join(', ')            end            # 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 415ef80cd2..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 -      @request.cookie_jar +      @cookie_jar ||= Cookies::CookieJar.build(@request, @request.cookies)      end      def redirect_to_url diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index 4b9a088265..ad1a7f7109 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -4,19 +4,22 @@ require 'rack/utils'  module ActionDispatch    class TestRequest < Request      DEFAULT_ENV = Rack::MockRequest.env_for('/', -      'HTTP_HOST'       => 'test.host', -      'REMOTE_ADDR'     => '0.0.0.0', -      'HTTP_USER_AGENT' => 'Rails Testing' +      'HTTP_HOST'                => 'test.host', +      'REMOTE_ADDR'              => '0.0.0.0', +      'HTTP_USER_AGENT'          => 'Rails Testing',      ) -    def self.new(env = {}) -      super +    # Create a new test request with default `env` values +    def self.create(env = {}) +      env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application +      env["rack.request.cookie_hash"] ||= {}.with_indifferent_access +      new(default_env.merge(env))      end -    def initialize(env = {}) -      env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application -      super(default_env.merge(env)) +    def self.default_env +      DEFAULT_ENV      end +    private_class_method :default_env      def request_method=(method)        @env['REQUEST_METHOD'] = method.to_s.upcase @@ -62,17 +65,5 @@ module ActionDispatch        @env.delete('action_dispatch.request.accepts')        @env['HTTP_ACCEPT'] = Array(mime_types).collect(&:to_s).join(",")      end - -    alias :rack_cookies :cookies - -    def cookies -      @cookies ||= {}.with_indifferent_access -    end - -    private - -    def default_env -      DEFAULT_ENV -    end    end  end diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb index a9b88ac5fd..4b79a90242 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -7,7 +7,7 @@ module ActionDispatch    # See Response for more information on controller response objects.    class TestResponse < Response      def self.from_response(response) -      new response.status, response.headers, response.body, default_headers: nil +      new response.status, response.headers, response.body      end      # Was the response successful? @@ -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  | 
