diff options
Diffstat (limited to 'actionpack/lib/action_dispatch')
18 files changed, 959 insertions, 599 deletions
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb new file mode 100644 index 0000000000..428e62dc6b --- /dev/null +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -0,0 +1,123 @@ +module ActionDispatch +  module Http +    module Cache +      module Request +        def if_modified_since +          if since = env['HTTP_IF_MODIFIED_SINCE'] +            Time.rfc2822(since) rescue nil +          end +        end + +        def if_none_match +          env['HTTP_IF_NONE_MATCH'] +        end + +        def not_modified?(modified_at) +          if_modified_since && modified_at && if_modified_since >= modified_at +        end + +        def etag_matches?(etag) +          if_none_match && if_none_match == etag +        end + +        # Check response freshness (Last-Modified and ETag) against request +        # If-Modified-Since and If-None-Match conditions. If both headers are +        # supplied, both must match, or the request is not considered fresh. +        def fresh?(response) +          last_modified = if_modified_since +          etag          = if_none_match + +          return false unless last_modified || etag + +          success = true +          success &&= not_modified?(response.last_modified) if last_modified +          success &&= etag_matches?(response.etag) if etag +          success +        end +      end + +      module Response +        def cache_control +          @cache_control ||= {} +        end + +        def last_modified +          if last = headers['Last-Modified'] +            Time.httpdate(last) +          end +        end + +        def last_modified? +          headers.include?('Last-Modified') +        end + +        def last_modified=(utc_time) +          headers['Last-Modified'] = utc_time.httpdate +        end + +        def etag +          @etag +        end + +        def etag? +          @etag +        end + +        def etag=(etag) +          key = ActiveSupport::Cache.expand_cache_key(etag) +          @etag = %("#{Digest::MD5.hexdigest(key)}") +        end + +      private + +        def handle_conditional_get! +          if etag? || last_modified? || !@cache_control.empty? +            set_conditional_cache_control! +          elsif nonempty_ok_response? +            self.etag = @body + +            if request && request.etag_matches?(etag) +              self.status = 304 +              self.body = [] +            end + +            set_conditional_cache_control! +          else +            headers["Cache-Control"] = "no-cache" +          end +        end + +        def nonempty_ok_response? +          @status == 200 && string_body? +        end + +        def string_body? +          !@blank && @body.respond_to?(:all?) && @body.all? { |part| part.is_a?(String) } +        end + +        DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate" + +        def set_conditional_cache_control! +          control = @cache_control + +          if control.empty? +            headers["Cache-Control"] = DEFAULT_CACHE_CONTROL +          elsif @cache_control[:no_cache] +            headers["Cache-Control"] = "no-cache" +          else +            extras  = control[:extras] +            max_age = control[:max_age] + +            options = [] +            options << "max-age=#{max_age.to_i}" if max_age +            options << (control[:public] ? "public" : "private") +            options << "must-revalidate" if control[:must_revalidate] +            options.concat(extras) if extras + +            headers["Cache-Control"] = options.join(", ") +          end +        end +      end +    end +  end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb new file mode 100644 index 0000000000..40617e239a --- /dev/null +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -0,0 +1,101 @@ +module ActionDispatch +  module Http +    module MimeNegotiation +      # 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_type +        @env["action_dispatch.request.content_type"] ||= begin +          if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/ +            Mime::Type.lookup($1.strip.downcase) +          else +            nil +          end +        end +      end + +      # Returns the accepted MIME type for the request. +      def accepts +        @env["action_dispatch.request.accepts"] ||= begin +          header = @env['HTTP_ACCEPT'].to_s.strip + +          if header.empty? +            [content_type] +          else +            Mime::Type.parse(header) +          end +        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 depending on the value of <tt>ActionController::Base.use_accept_header</tt> +      # +      def format(view_path = []) +        formats.first +      end + +      def formats +        accept = @env['HTTP_ACCEPT'] + +        @env["action_dispatch.request.formats"] ||= +          if parameters[:format] +            Array(Mime[parameters[:format]]) +          elsif xhr? || (accept && !accept.include?(?,)) +            accepts +          else +            [Mime::HTML] +          end +      end + +      # Sets the \format by string extension, which can be used to force custom formats +      # that are not controlled by the extension. +      # +      #   class ApplicationController < ActionController::Base +      #     before_filter :adjust_format_for_iphone +      # +      #     private +      #       def adjust_format_for_iphone +      #         request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/] +      #       end +      #   end +      def format=(extension) +        parameters[:format] = extension.to_s +        @env["action_dispatch.request.formats"] = [Mime::Type.lookup_by_extension(parameters[:format])] +      end + +      # Returns a symbolized version of the <tt>:format</tt> parameter of the request. +      # If no \format is given it returns <tt>:js</tt>for Ajax requests and <tt>:html</tt> +      # otherwise. +      def template_format +        parameter_format = parameters[:format] + +        if parameter_format +          parameter_format +        elsif xhr? +          :js +        else +          :html +        end +      end + +      # Receives an array of mimes and return the first user sent mime that +      # matches the order array. +      # +      def negotiate_mime(order) +        formats.each do |priority| +          if priority == Mime::ALL +            return order.first +          elsif order.include?(priority) +            return priority +          end +        end + +        order.include?(Mime::ALL) ? formats.first : nil +      end +    end +  end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb new file mode 100644 index 0000000000..97546d5f93 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -0,0 +1,50 @@ +require 'active_support/core_ext/hash/keys' + +module ActionDispatch +  module Http +    module Parameters +      # Returns both GET and POST \parameters in a single hash. +      def parameters +        @env["action_dispatch.request.parameters"] ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access +      end +      alias :params :parameters + +      def path_parameters=(parameters) #:nodoc: +        @env.delete("action_dispatch.request.symbolized_path_parameters") +        @env.delete("action_dispatch.request.parameters") +        @env["action_dispatch.request.path_parameters"] = parameters +      end + +      # The same as <tt>path_parameters</tt> with explicitly symbolized keys. +      def symbolized_path_parameters +        @env["action_dispatch.request.symbolized_path_parameters"] ||= path_parameters.symbolize_keys +      end + +      # Returns a hash with the \parameters used to form the \path of the request. +      # Returned hash keys are strings: +      # +      #   {'action' => 'my_action', 'controller' => 'my_controller'} +      # +      # See <tt>symbolized_path_parameters</tt> for symbolized keys. +      def path_parameters +        @env["action_dispatch.request.path_parameters"] ||= {} +      end +   +    private + +      # Convert nested Hashs to HashWithIndifferentAccess +      def normalize_parameters(value) +        case value +        when Hash +          h = {} +          value.each { |k, v| h[k] = normalize_parameters(v) } +          h.with_indifferent_access +        when Array +          value.map { |e| normalize_parameters(e) } +        else +          value +        end +      end +    end +  end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 6e8a5dcb8a..187ce7c15d 100755 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -2,14 +2,17 @@ require 'tempfile'  require 'stringio'  require 'strscan' -require 'active_support/memoizable' -require 'active_support/core_ext/array/wrap'  require 'active_support/core_ext/hash/indifferent_access'  require 'active_support/core_ext/string/access'  require 'action_dispatch/http/headers'  module ActionDispatch    class Request < Rack::Request +    include ActionDispatch::Http::Cache::Request +    include ActionDispatch::Http::MimeNegotiation +    include ActionDispatch::Http::Parameters +    include ActionDispatch::Http::Upload +    include ActionDispatch::Http::URL      %w[ AUTH_TYPE GATEWAY_INTERFACE          PATH_TRANSLATED REMOTE_HOST @@ -19,9 +22,11 @@ module ActionDispatch          HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING          HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM          HTTP_NEGOTIATE HTTP_PRAGMA ].each do |env| -      define_method(env.sub(/^HTTP_/n, '').downcase) do -        @env[env] -      end +      class_eval <<-METHOD, __FILE__, __LINE__ + 1 +        def #{env.sub(/^HTTP_/n, '').downcase} +          @env["#{env}"] +        end +      METHOD      end      def key?(key) @@ -35,7 +40,8 @@ module ActionDispatch      # <tt>:get</tt>. If the request \method is not listed in the HTTP_METHODS      # constant above, an UnknownHttpMethod exception is raised.      def request_method -      HTTP_METHOD_LOOKUP[super] || raise(ActionController::UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") +      method = env["rack.methodoverride.original_method"] || env["REQUEST_METHOD"] +      HTTP_METHOD_LOOKUP[method] || raise(ActionController::UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")      end      # Returns the HTTP request \method used for action processing as a @@ -43,7 +49,8 @@ module ActionDispatch      # method returns <tt>:get</tt> for a HEAD request because the two are      # functionally equivalent from the application's perspective.)      def method -      request_method == :head ? :get : request_method +      method = env["REQUEST_METHOD"] +      HTTP_METHOD_LOOKUP[method] || raise(ActionController::UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")      end      # Is this a GET (or HEAD) request?  Equivalent to <tt>request.method == :get</tt>. @@ -53,17 +60,17 @@ module ActionDispatch      # Is this a POST request?  Equivalent to <tt>request.method == :post</tt>.      def post? -      request_method == :post +      method == :post      end      # Is this a PUT request?  Equivalent to <tt>request.method == :put</tt>.      def put? -      request_method == :put +      method == :put      end      # Is this a DELETE request?  Equivalent to <tt>request.method == :delete</tt>.      def delete? -      request_method == :delete +      method == :delete      end      # Is this a HEAD request? Since <tt>request.method</tt> sees HEAD as <tt>:get</tt>, @@ -79,25 +86,6 @@ module ActionDispatch        Http::Headers.new(@env)      end -    # Returns the content length of the request as an integer. -    def content_length -      super.to_i -    end - -    # 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_type -      @env["action_dispatch.request.content_type"] ||= begin -        if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/ -          Mime::Type.lookup($1.strip.downcase) -        else -          nil -        end -      end -    end -      def forgery_whitelisted?        method == :get || xhr? || content_type.nil? || !content_type.verify_request?      end @@ -106,104 +94,9 @@ module ActionDispatch        content_type.to_s      end -    # Returns the accepted MIME type for the request. -    def accepts -      @env["action_dispatch.request.accepts"] ||= begin -        header = @env['HTTP_ACCEPT'].to_s.strip - -        if header.empty? -          [content_type] -        else -          Mime::Type.parse(header) -        end -      end -    end - -    def if_modified_since -      if since = env['HTTP_IF_MODIFIED_SINCE'] -        Time.rfc2822(since) rescue nil -      end -    end - -    def if_none_match -      env['HTTP_IF_NONE_MATCH'] -    end - -    def not_modified?(modified_at) -      if_modified_since && modified_at && if_modified_since >= modified_at -    end - -    def etag_matches?(etag) -      if_none_match && if_none_match == etag -    end - -    # Check response freshness (Last-Modified and ETag) against request -    # If-Modified-Since and If-None-Match conditions. If both headers are -    # supplied, both must match, or the request is not considered fresh. -    def fresh?(response) -      last_modified = if_modified_since -      etag          = if_none_match - -      return false unless last_modified || etag - -      success = true -      success &&= not_modified?(response.last_modified) if last_modified -      success &&= etag_matches?(response.etag) if etag -      success -    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 depending on the value of <tt>ActionController::Base.use_accept_header</tt> -    # -    def format(view_path = []) -      formats.first -    end - -    def formats -      accept = @env['HTTP_ACCEPT'] - -      @env["action_dispatch.request.formats"] ||= -        if parameters[:format] -          Array.wrap(Mime[parameters[:format]]) -        elsif xhr? || (accept && !accept.include?(?,)) -          accepts -        else -          [Mime::HTML] -        end -    end - -    # Sets the \format by string extension, which can be used to force custom formats -    # that are not controlled by the extension. -    # -    #   class ApplicationController < ActionController::Base -    #     before_filter :adjust_format_for_iphone -    # -    #     private -    #       def adjust_format_for_iphone -    #         request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/] -    #       end -    #   end -    def format=(extension) -      parameters[:format] = extension.to_s -      @env["action_dispatch.request.formats"] = [Mime::Type.lookup_by_extension(parameters[:format])] -    end - -    # Returns a symbolized version of the <tt>:format</tt> parameter of the request. -    # If no \format is given it returns <tt>:js</tt>for Ajax requests and <tt>:html</tt> -    # otherwise. -    def template_format -      parameter_format = parameters[:format] - -      if parameter_format -        parameter_format -      elsif xhr? -        :js -      else -        :html -      end +    # Returns the content length of the request as an integer. +    def content_length +      super.to_i      end      # Returns true if the request's "X-Requested-With" header contains @@ -236,7 +129,7 @@ module ActionDispatch        if @env.include? 'HTTP_CLIENT_IP'          if ActionController::Base.ip_spoofing_check && remote_ips && !remote_ips.include?(@env['HTTP_CLIENT_IP'])            # We don't know which came from the proxy, and which from the user -          raise ActionController::ActionControllerError.new(<<EOM) +          raise ActionController::ActionControllerError.new <<EOM  IP spoofing attack?!  HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}  HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect} @@ -262,124 +155,6 @@ EOM        (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil      end -    # Returns the complete URL used for this request. -    def url -      protocol + host_with_port + request_uri -    end - -    # Returns 'https://' if this is an SSL request and 'http://' otherwise. -    def protocol -      ssl? ? 'https://' : 'http://' -    end - -    # Is this an SSL request? -    def ssl? -      @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https' -    end - -    # Returns the \host for this request, such as "example.com". -    def raw_host_with_port -      if forwarded = env["HTTP_X_FORWARDED_HOST"] -        forwarded.split(/,\s?/).last -      else -        env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}" -      end -    end - -    # Returns the host for this request, such as example.com. -    def host -      raw_host_with_port.sub(/:\d+$/, '') -    end - -    # Returns a \host:\port string for this request, such as "example.com" or -    # "example.com:8080". -    def host_with_port -      "#{host}#{port_string}" -    end - -    # Returns the port number of this request as an integer. -    def port -      if raw_host_with_port =~ /:(\d+)$/ -        $1.to_i -      else -        standard_port -      end -    end - -    # Returns the standard \port number for this request's protocol. -    def standard_port -      case protocol -        when 'https://' then 443 -        else 80 -      end -    end - -    # Returns a \port suffix like ":8080" if the \port number of this request -    # is not the default HTTP \port 80 or HTTPS \port 443. -    def port_string -      port == standard_port ? '' : ":#{port}" -    end - -    def server_port -      @env['SERVER_PORT'].to_i -    end - -    # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify -    # a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk". -    def domain(tld_length = 1) -      return nil unless named_host?(host) - -      host.split('.').last(1 + tld_length).join('.') -    end - -    # Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be -    # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>, -    # such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt> -    # in "www.rubyonrails.co.uk". -    def subdomains(tld_length = 1) -      return [] unless named_host?(host) -      parts = host.split('.') -      parts[0..-(tld_length+2)] -    end - -    # Returns the query string, accounting for server idiosyncrasies. -    def query_string -      @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].to_s.split('?', 2)[1] || '') -    end - -    # Returns the request URI, accounting for server idiosyncrasies. -    # WEBrick includes the full URL. IIS leaves REQUEST_URI blank. -    def request_uri -      if uri = @env['REQUEST_URI'] -        # Remove domain, which webrick puts into the request_uri. -        (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri -      else -        # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO. -        uri = @env['PATH_INFO'].to_s - -        if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$}) -          uri = uri.sub(/#{script_filename}\//, '') -        end - -        env_qs = @env['QUERY_STRING'].to_s -        uri += "?#{env_qs}" unless env_qs.empty? - -        if uri.blank? -          @env.delete('REQUEST_URI') -        else -          @env['REQUEST_URI'] = uri -        end -      end -    end - -    # Returns the interpreted \path to requested resource after all the installation -    # directory of this application was taken into account. -    def path -      path = request_uri.to_s[/\A[^\?]*/] -      path.sub!(/\A#{ActionController::Base.relative_url_root}/, '') -      path -    end -      # Read the request \body. This is useful for web services that need to      # work with raw requests directly.      def raw_post @@ -390,33 +165,6 @@ EOM        @env['RAW_POST_DATA']      end -    # Returns both GET and POST \parameters in a single hash. -    def parameters -      @env["action_dispatch.request.parameters"] ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access -    end -    alias_method :params, :parameters - -    def path_parameters=(parameters) #:nodoc: -      @env.delete("action_dispatch.request.symbolized_path_parameters") -      @env.delete("action_dispatch.request.parameters") -      @env["action_dispatch.request.path_parameters"] = parameters -    end - -    # The same as <tt>path_parameters</tt> with explicitly symbolized keys. -    def symbolized_path_parameters -      @env["action_dispatch.request.symbolized_path_parameters"] ||= path_parameters.symbolize_keys -    end - -    # Returns a hash with the \parameters used to form the \path of the request. -    # Returned hash keys are strings: -    # -    #   {'action' => 'my_action', 'controller' => 'my_controller'} -    # -    # See <tt>symbolized_path_parameters</tt> for symbolized keys. -    def path_parameters -      @env["action_dispatch.request.path_parameters"] ||= {} -    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 @@ -432,18 +180,6 @@ EOM        FORM_DATA_MEDIA_TYPES.include?(content_type.to_s)      end -    # Override Rack's GET method to support indifferent access -    def GET -      @env["action_dispatch.request.query_parameters"] ||= normalize_parameters(super) -    end -    alias_method :query_parameters, :GET - -    # Override Rack's POST method to support indifferent access -    def POST -      @env["action_dispatch.request.request_parameters"] ||= normalize_parameters(super) -    end -    alias_method :request_parameters, :POST -      def body_stream #:nodoc:        @env['rack.input']      end @@ -461,9 +197,18 @@ EOM        @env['rack.session.options'] = options      end -    def flash -      session['flash'] || {} +    # Override Rack's GET method to support indifferent access +    def GET +      @env["action_dispatch.request.query_parameters"] ||= normalize_parameters(super) +    end +    alias :query_parameters :GET + +    # Override Rack's POST method to support indifferent access +    def POST +      @env["action_dispatch.request.request_parameters"] ||= normalize_parameters(super)      end +    alias :request_parameters :POST +      # Returns the authorization header regardless of whether it was specified directly or through one of the      # proxy alternatives. @@ -473,77 +218,5 @@ EOM        @env['X_HTTP_AUTHORIZATION'] ||        @env['REDIRECT_X_HTTP_AUTHORIZATION']      end - -    # Receives an array of mimes and return the first user sent mime that -    # matches the order array. -    # -    def negotiate_mime(order) -      formats.each do |priority| -        if priority == Mime::ALL -          return order.first -        elsif order.include?(priority) -          return priority -        end -      end - -      order.include?(Mime::ALL) ? formats.first : nil -    end - -    private - -      def named_host?(host) -        !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) -      end - -      module UploadedFile -        def self.extended(object) -          object.class_eval do -            attr_accessor :original_path, :content_type -            alias_method :local_path, :path if method_defined?(:path) -          end -        end - -        # Take the basename of the upload's original filename. -        # This handles the full Windows paths given by Internet Explorer -        # (and perhaps other broken user agents) without affecting -        # those which give the lone filename. -        # The Windows regexp is adapted from Perl's File::Basename. -        def original_filename -          unless defined? @original_filename -            @original_filename = -              unless original_path.blank? -                if original_path =~ /^(?:.*[:\\\/])?(.*)/m -                  $1 -                else -                  File.basename original_path -                end -              end -          end -          @original_filename -        end -      end - -      # Convert nested Hashs to HashWithIndifferentAccess and replace -      # file upload hashs with UploadedFile objects -      def normalize_parameters(value) -        case value -        when Hash -          if value.has_key?(:tempfile) -            upload = value[:tempfile] -            upload.extend(UploadedFile) -            upload.original_path = value[:filename] -            upload.content_type = value[:type] -            upload -          else -            h = {} -            value.each { |k, v| h[k] = normalize_parameters(v) } -            h.with_indifferent_access -          end -        when Array -          value.map { |e| normalize_parameters(e) } -        else -          value -        end -      end    end  end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 8524bbd993..65df9b1f03 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -32,6 +32,8 @@ module ActionDispatch # :nodoc:    #    end    #  end    class Response < Rack::Response +    include ActionDispatch::Http::Cache::Response +      attr_accessor :request, :blank      attr_writer :header, :sending_file @@ -55,10 +57,6 @@ module ActionDispatch # :nodoc:        yield self if block_given?      end -    def cache_control -      @cache_control ||= {} -    end -      def status=(status)        @status = Rack::Utils.status_code(status)      end @@ -114,33 +112,6 @@ module ActionDispatch # :nodoc:      # information.      attr_accessor :charset, :content_type -    def last_modified -      if last = headers['Last-Modified'] -        Time.httpdate(last) -      end -    end - -    def last_modified? -      headers.include?('Last-Modified') -    end - -    def last_modified=(utc_time) -      headers['Last-Modified'] = utc_time.httpdate -    end - -    def etag -      @etag -    end - -    def etag? -      @etag -    end - -    def etag=(etag) -      key = ActiveSupport::Cache.expand_cache_key(etag) -      @etag = %("#{Digest::MD5.hexdigest(key)}") -    end -      CONTENT_TYPE    = "Content-Type"      cattr_accessor(:default_charset) { "utf-8" } @@ -222,31 +193,6 @@ module ActionDispatch # :nodoc:      end      private -      def handle_conditional_get! -        if etag? || last_modified? || !@cache_control.empty? -          set_conditional_cache_control! -        elsif nonempty_ok_response? -          self.etag = @body - -          if request && request.etag_matches?(etag) -            self.status = 304 -            self.body = [] -          end - -          set_conditional_cache_control! -        else -          headers["Cache-Control"] = "no-cache" -        end -      end - -      def nonempty_ok_response? -        @status == 200 && string_body? -      end - -      def string_body? -        !@blank && @body.respond_to?(:all?) && @body.all? { |part| part.is_a?(String) } -      end -        def assign_default_content_type_and_charset!          return if headers[CONTENT_TYPE].present? @@ -259,27 +205,5 @@ module ActionDispatch # :nodoc:          headers[CONTENT_TYPE] = type        end -      DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate" - -      def set_conditional_cache_control! -        control = @cache_control - -        if control.empty? -          headers["Cache-Control"] = DEFAULT_CACHE_CONTROL -        elsif @cache_control[:no_cache] -          headers["Cache-Control"] = "no-cache" -        else -          extras  = control[:extras] -          max_age = control[:max_age] - -          options = [] -          options << "max-age=#{max_age.to_i}" if max_age -          options << (control[:public] ? "public" : "private") -          options << "must-revalidate" if control[:must_revalidate] -          options.concat(extras) if extras - -          headers["Cache-Control"] = options.join(", ") -        end -      end    end  end diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb new file mode 100644 index 0000000000..dc6121b911 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -0,0 +1,48 @@ +module ActionDispatch +  module Http +    module UploadedFile +      def self.extended(object) +        object.class_eval do +          attr_accessor :original_path, :content_type +          alias_method :local_path, :path if method_defined?(:path) +        end +      end + +      # Take the basename of the upload's original filename. +      # This handles the full Windows paths given by Internet Explorer +      # (and perhaps other broken user agents) without affecting +      # those which give the lone filename. +      # The Windows regexp is adapted from Perl's File::Basename. +      def original_filename +        unless defined? @original_filename +          @original_filename = +            unless original_path.blank? +              if original_path =~ /^(?:.*[:\\\/])?(.*)/m +                $1 +              else +                File.basename original_path +              end +            end +        end +        @original_filename +      end +    end + +    module Upload +      # Convert nested Hashs to HashWithIndifferentAccess and replace +      # file upload hashs with UploadedFile objects +      def normalize_parameters(value) +        if Hash === value && value.has_key?(:tempfile) +          upload = value[:tempfile] +          upload.extend(UploadedFile) +          upload.original_path = value[:filename] +          upload.content_type = value[:type] +          upload +        else +          super +        end +      end +      private :normalize_parameters +    end +  end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb new file mode 100644 index 0000000000..40ceb5a9b6 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -0,0 +1,129 @@ +module ActionDispatch +  module Http +    module URL +      # Returns the complete URL used for this request. +      def url +        protocol + host_with_port + request_uri +      end + +      # Returns 'https://' if this is an SSL request and 'http://' otherwise. +      def protocol +        ssl? ? 'https://' : 'http://' +      end + +      # Is this an SSL request? +      def ssl? +        @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https' +      end + +      # Returns the \host for this request, such as "example.com". +      def raw_host_with_port +        if forwarded = env["HTTP_X_FORWARDED_HOST"] +          forwarded.split(/,\s?/).last +        else +          env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}" +        end +      end + +      # Returns the host for this request, such as example.com. +      def host +        raw_host_with_port.sub(/:\d+$/, '') +      end + +      # Returns a \host:\port string for this request, such as "example.com" or +      # "example.com:8080". +      def host_with_port +        "#{host}#{port_string}" +      end + +      # Returns the port number of this request as an integer. +      def port +        if raw_host_with_port =~ /:(\d+)$/ +          $1.to_i +        else +          standard_port +        end +      end + +      # Returns the standard \port number for this request's protocol. +      def standard_port +        case protocol +          when 'https://' then 443 +          else 80 +        end +      end + +      # Returns a \port suffix like ":8080" if the \port number of this request +      # is not the default HTTP \port 80 or HTTPS \port 443. +      def port_string +        port == standard_port ? '' : ":#{port}" +      end + +      def server_port +        @env['SERVER_PORT'].to_i +      end + +      # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify +      # a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk". +      def domain(tld_length = 1) +        return nil unless named_host?(host) + +        host.split('.').last(1 + tld_length).join('.') +      end + +      # Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be +      # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>, +      # such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt> +      # in "www.rubyonrails.co.uk". +      def subdomains(tld_length = 1) +        return [] unless named_host?(host) +        parts = host.split('.') +        parts[0..-(tld_length+2)] +      end + +      # Returns the query string, accounting for server idiosyncrasies. +      def query_string +        @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].to_s.split('?', 2)[1] || '') +      end + +      # Returns the request URI, accounting for server idiosyncrasies. +      # WEBrick includes the full URL. IIS leaves REQUEST_URI blank. +      def request_uri +        if uri = @env['REQUEST_URI'] +          # Remove domain, which webrick puts into the request_uri. +          (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri +        else +          # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO. +          uri = @env['PATH_INFO'].to_s + +          if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$}) +            uri = uri.sub(/#{script_filename}\//, '') +          end + +          env_qs = @env['QUERY_STRING'].to_s +          uri += "?#{env_qs}" unless env_qs.empty? + +          if uri.blank? +            @env.delete('REQUEST_URI') +          else +            @env['REQUEST_URI'] = uri +          end +        end +      end + +      # Returns the interpreted \path to requested resource after all the installation +      # directory of this application was taken into account. +      def path +        path = request_uri.to_s[/\A[^\?]*/] +        path.sub!(/\A#{ActionController::Base.relative_url_root}/, '') +        path +      end + +    private + +      def named_host?(host) +        !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) +      end +    end +  end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index 49bc20f11f..5ec406e134 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -1,4 +1,10 @@  module ActionDispatch +  # Provide callbacks to be executed before and after the request dispatch. +  # +  # It also provides a to_prepare callback, which is performed in all requests +  # in development by only once in production and notification callback for async +  # operations. +  #    class Callbacks      include ActiveSupport::Callbacks @@ -29,12 +35,6 @@ module ActionDispatch        set_callback(:call, :after, *args, &block)      end -    class << self -      # DEPRECATED -      alias_method :before_dispatch, :before -      alias_method :after_dispatch, :after -    end -      def initialize(app, prepare_each_request = false)        @app, @prepare_each_request = app, prepare_each_request        run_callbacks(:prepare) @@ -45,6 +45,8 @@ module ActionDispatch          run_callbacks(:prepare) if @prepare_each_request          @app.call(env)        end +    ensure +      ActiveSupport::Notifications.instrument "action_dispatch.callback"      end    end  end diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb new file mode 100644 index 0000000000..99b36366d6 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -0,0 +1,174 @@ +module ActionDispatch +  class 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 +      session['flash'] ||= Flash::FlashHash.new +    end +  end + +  # The flash provides a way to pass temporary objects 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] = "Successfully created"</tt> before redirecting to a display action that can +  # then expose the flash to its template. Actually, that exposure is automatically done. Example: +  # +  #   class PostsController < ActionController::Base +  #     def create +  #       # save post +  #       flash[:notice] = "Successfully created post" +  #       redirect_to posts_path(@post) +  #     end +  # +  #     def show +  #       # doesn't need to assign the flash notice to the template, that's done automatically +  #     end +  #   end +  # +  #   show.html.erb +  #     <% if flash[:notice] %> +  #       <div class="notice"><%= flash[:notice] %></div> +  #     <% end %> +  # +  # This example just places a string in the flash, but you can put any object in there. And of course, you can put as +  # many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed. +  # +  # See docs on the FlashHash class for more details about the flash. +  class Flash +    class FlashNow #:nodoc: +      def initialize(flash) +        @flash = flash +      end + +      def []=(k, v) +        @flash[k] = v +        @flash.discard(k) +        v +      end + +      def [](k) +        @flash[k] +      end +    end + +    class FlashHash < Hash +      def initialize #:nodoc: +        super +        @used = Set.new +      end + +      def []=(k, v) #:nodoc: +        keep(k) +        super +      end + +      def update(h) #:nodoc: +        h.keys.each { |k| keep(k) } +        super +      end + +      alias :merge! :update + +      def replace(h) #:nodoc: +        @used = Set.new +        super +      end + +      # Sets a flash that will not be available to the next action, only to the current. +      # +      #     flash.now[:message] = "Hello current action" +      # +      # This method enables you to use the flash as a central messaging system in your app. +      # When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>). +      # When you need to pass an object to the current action, you use <tt>now</tt>, and your object will +      # vanish when the current action is done. +      # +      # Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>. +      def now +        FlashNow.new(self) +      end + +      # Keeps either the entire current flash or a specific flash entry available for the next action: +      # +      #    flash.keep            # keeps the entire flash +      #    flash.keep(:notice)   # keeps only the "notice" entry, the rest of the flash is discarded +      def keep(k = nil) +        use(k, false) +      end + +      # Marks the entire flash or a single flash entry to be discarded by the end of the current action: +      # +      #     flash.discard              # discard the entire flash at the end of the current action +      #     flash.discard(:warning)    # discard only the "warning" entry at the end of the current action +      def discard(k = nil) +        use(k) +      end + +      # Mark for removal entries that were kept, and delete unkept ones. +      # +      # This method is called automatically by filters, so you generally don't need to care about it. +      def sweep #:nodoc: +        keys.each do |k| +          unless @used.include?(k) +            @used << k +          else +            delete(k) +            @used.delete(k) +          end +        end + +        # clean up after keys that could have been left over by calling reject! or shift on the flash +        (@used - keys).each{ |k| @used.delete(k) } +      end + +      # Convenience accessor for flash[:alert] +      def alert +        self[:alert] +      end + +      # Convenience accessor for flash[:alert]= +      def alert=(message) +        self[:alert] = message +      end + +      # Convenience accessor for flash[:notice] +      def notice +        self[:notice] +      end + +      # Convenience accessor for flash[:notice]= +      def notice=(message) +        self[:notice] = message +      end + +      private +        # Used internally by the <tt>keep</tt> and <tt>discard</tt> methods +        #     use()               # marks the entire flash as used +        #     use('msg')          # marks the "msg" entry as used +        #     use(nil, false)     # marks the entire flash as unused (keeps it around for one more action) +        #     use('msg', false)   # marks the "msg" entry as unused (keeps it around for one more action) +        # Returns the single value for the key you asked to be marked (un)used or the FlashHash itself +        # if no key is passed. +        def use(key = nil, used = true) +          Array(key || keys).each { |k| used ? @used << k : @used.delete(k) } +          return key ? self[key] : self +        end +    end + +    def initialize(app) +      @app = app +    end + +    def call(env) +      if (session = env['rack.session']) && (flash = session['flash']) +        flash.sweep +      end + +      @app.call(env) +    ensure +      if (session = env['rack.session']) && (flash = session['flash']) && flash.empty? +        session.delete('flash') +      end +    end +  end +end diff --git a/actionpack/lib/action_dispatch/middleware/head.rb b/actionpack/lib/action_dispatch/middleware/head.rb new file mode 100644 index 0000000000..56e2d2f2a8 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/head.rb @@ -0,0 +1,18 @@ +module ActionDispatch +  class Head +    def initialize(app) +      @app = app +    end + +    def call(env) +      if env["REQUEST_METHOD"] == "HEAD" +        env["REQUEST_METHOD"] = "GET" +        env["rack.methodoverride.original_method"] = "HEAD" +        status, headers, body = @app.call(env) +        [status, headers, []] +      else +        @app.call(env) +      end +    end +  end +end diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 7d4f0998ce..311880cabc 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -102,7 +102,7 @@ module ActionDispatch                  # Note that the regexp does not allow $1 to end with a ':'                  $1.constantize                rescue LoadError, NameError => const_error -                raise ActionDispatch::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n" +                raise ActionDispatch::Session::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n"                end                retry diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index 4ebc8a2ab9..10f04dcdf6 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -1,7 +1,24 @@  require 'active_support/core_ext/exception' +require 'active_support/notifications'  require 'action_dispatch/http/request'  module ActionDispatch +  # This middleware rescues any exception returned by the application and renders +  # nice exception pages if it's being rescued locally. +  # +  # Every time an exception is caught, a notification is published, becoming a good API +  # to deal with exceptions. So, if you want send an e-mail through ActionMailer +  # everytime this notification is published, you just need to do the following: +  # +  #   ActiveSupport::Notifications.subscribe "action_dispatch.show_exception" do |name, start, end, instrumentation_id, payload| +  #     ExceptionNotifier.deliver_exception(start, payload) +  #   end +  # +  # The payload is a hash which has two pairs: +  # +  # * :env - Contains the rack env for the given request; +  # * :exception - The exception raised; +  #    class ShowExceptions      LOCALHOST = '127.0.0.1'.freeze @@ -44,8 +61,11 @@ module ActionDispatch      def call(env)        @app.call(env)      rescue Exception => exception -      raise exception if env['action_dispatch.show_exceptions'] == false -      render_exception(env, exception) +      ActiveSupport::Notifications.instrument 'action_dispatch.show_exception', +        :env => env, :exception => exception do +        raise exception if env['action_dispatch.show_exceptions'] == false +        render_exception(env, exception) +      end      end      private diff --git a/actionpack/lib/action_dispatch/middleware/string_coercion.rb b/actionpack/lib/action_dispatch/middleware/string_coercion.rb deleted file mode 100644 index 232e947835..0000000000 --- a/actionpack/lib/action_dispatch/middleware/string_coercion.rb +++ /dev/null @@ -1,29 +0,0 @@ -module ActionDispatch -  class StringCoercion -    class UglyBody < ActiveSupport::BasicObject -      def initialize(body) -        @body = body -      end - -      def each -        @body.each do |part| -          yield part.to_s -        end -      end - -      private -        def method_missing(*args, &block) -          @body.__send__(*args, &block) -        end -    end - -    def initialize(app) -      @app = app -    end - -    def call(env) -      status, headers, body = @app.call(env) -      [status, headers, UglyBody.new(body)] -    end -  end -end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 8f33346a4f..9aaa4355f2 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -68,16 +68,11 @@ module ActionDispatch            end            def normalize_path(path) -            path = nil if path == "" -            path = "#{@scope[:path]}#{path}" if @scope[:path] -            path = Rack::Mount::Utils.normalize_path(path) if path - -            raise ArgumentError, "path is required" unless path - -            path +            path = "#{@scope[:path]}/#{path}" +            raise ArgumentError, "path is required" if path.empty? +            Mapper.normalize_path(path)            end -            def app              Constraints.new(                to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults), @@ -123,7 +118,6 @@ module ActionDispatch              end            end -            def blocks              if @options[:constraints].present? && !@options[:constraints].is_a?(Hash)                block = @options[:constraints] @@ -162,6 +156,14 @@ module ActionDispatch            end        end +      # Invokes Rack::Mount::Utils.normalize path and ensure that +      # (:locale) becomes (/:locale) instead of /(:locale). +      def self.normalize_path(path) +        path = Rack::Mount::Utils.normalize_path(path) +        path.sub!(%r{/\(+/?:}, '(/:') +        path +      end +        module Base          def initialize(set)            @set = set @@ -200,13 +202,22 @@ module ActionDispatch            path      = args.shift || block            path_proc = path.is_a?(Proc) ? path : proc { |params| path % params }            status    = options[:status] || 301 +          body      = 'Moved Permanently'            lambda do |env| -            req    = Rack::Request.new(env) -            params = path_proc.call(env["action_dispatch.request.path_parameters"]) -            url    = req.scheme + '://' + req.host + params +            req = Request.new(env) + +            uri = URI.parse(path_proc.call(req.params.symbolize_keys)) +            uri.scheme ||= req.scheme +            uri.host   ||= req.host +            uri.port   ||= req.port unless req.port == 80 -            [ status, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently'] ] +            headers = { +              'Location' => uri.to_s, +              'Content-Type' => 'text/html', +              'Content-Length' => body.length.to_s +            } +            [ status, headers, [body] ]            end          end @@ -236,46 +247,35 @@ module ActionDispatch              options[:controller] = args.first            end -          if path = options.delete(:path) -            path_set = true -            path, @scope[:path] = @scope[:path], Rack::Mount::Utils.normalize_path(@scope[:path].to_s + path.to_s) -          else -            path_set = false -          end +          recover = {} -          if name_prefix = options.delete(:name_prefix) -            name_prefix_set = true -            name_prefix, @scope[:name_prefix] = @scope[:name_prefix], (@scope[:name_prefix] ? "#{@scope[:name_prefix]}_#{name_prefix}" : name_prefix) -          else -            name_prefix_set = false +          options[:constraints] ||= {} +          unless options[:constraints].is_a?(Hash) +            block, options[:constraints] = options[:constraints], {}            end -          if controller = options.delete(:controller) -            controller_set = true -            controller, @scope[:controller] = @scope[:controller], controller -          else -            controller_set = false +          scope_options.each do |option| +            if value = options.delete(option) +              recover[option] = @scope[option] +              @scope[option]  = send("merge_#{option}_scope", @scope[option], value) +            end            end -          constraints = options.delete(:constraints) || {} -          unless constraints.is_a?(Hash) -            block, constraints = constraints, {} -          end -          constraints, @scope[:constraints] = @scope[:constraints], (@scope[:constraints] || {}).merge(constraints) -          blocks, @scope[:blocks] = @scope[:blocks], (@scope[:blocks] || []) + [block] +          recover[:block] = @scope[:blocks] +          @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block) -          options, @scope[:options] = @scope[:options], (@scope[:options] || {}).merge(options) +          recover[:options] = @scope[:options] +          @scope[:options]  = merge_options_scope(@scope[:options], options)            yield -            self          ensure -          @scope[:path]        = path        if path_set -          @scope[:name_prefix] = name_prefix if name_prefix_set -          @scope[:controller]  = controller  if controller_set -          @scope[:options]     = options -          @scope[:blocks]      = blocks -          @scope[:constraints] = constraints +          scope_options.each do |option| +            @scope[option] = recover[option] if recover.has_key?(option) +          end + +          @scope[:options] = recover[:options] +          @scope[:blocks]  = recover[:block]          end          def controller(controller) @@ -283,7 +283,7 @@ module ActionDispatch          end          def namespace(path) -          scope("/#{path}") { yield } +          scope(path.to_s, :name_prefix => path.to_s, :namespace => path.to_s) { yield }          end          def constraints(constraints = {}) @@ -304,25 +304,83 @@ module ActionDispatch            args.push(options)            super(*args)          end + +        private +          def scope_options +            @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym } +          end + +          def merge_path_scope(parent, child) +            Mapper.normalize_path("#{parent}/#{child}") +          end + +          def merge_name_prefix_scope(parent, child) +            parent ? "#{parent}_#{child}" : child +          end + +          def merge_namespace_scope(parent, child) +            parent ? "#{parent}/#{child}" : child +          end + +          def merge_controller_scope(parent, child) +            @scope[:namespace] ? "#{@scope[:namespace]}/#{child}" : child +          end + +          def merge_resources_path_names_scope(parent, child) +            merge_options_scope(parent, child) +          end + +          def merge_constraints_scope(parent, child) +            merge_options_scope(parent, child) +          end + +          def merge_blocks_scope(parent, child) +            (parent || []) + [child] +          end + +          def merge_options_scope(parent, child) +            (parent || {}).merge(child) +          end        end        module Resources +        CRUD_ACTIONS = [:index, :show, :create, :update, :destroy] +          class Resource #:nodoc: -          attr_reader :plural, :singular +          def self.default_actions +            [:index, :create, :new, :show, :update, :destroy, :edit] +          end + +          attr_reader :plural, :singular, :options            def initialize(entities, options = {})              entities = entities.to_s +            @options = options              @plural   = entities.pluralize              @singular = entities.singularize            end +          def default_actions +            self.class.default_actions +          end + +          def actions +            if only = options[:only] +              only.map(&:to_sym) +            elsif except = options[:except] +              default_actions - except.map(&:to_sym) +            else +              default_actions +            end +          end +            def name -            plural +            options[:as] || plural            end            def controller -            plural +            options[:controller] || plural            end            def member_name @@ -339,15 +397,24 @@ module ActionDispatch          end          class SingletonResource < Resource #:nodoc: +          def self.default_actions +            [:show, :create, :update, :destroy, :new, :edit] +          end +            def initialize(entity, options = {})              super            end            def name -            singular +            options[:as] || singular            end          end +        def initialize(*args) +          super +          @scope[:resources_path_names] = @set.resources_path_names +        end +          def resource(*resources, &block)            options = resources.extract_options! @@ -357,7 +424,14 @@ module ActionDispatch              return self            end -          resource = SingletonResource.new(resources.pop) +          if path_names = options.delete(:path_names) +            scope(:resources_path_names => path_names) do +              resource(resources, options) +            end +            return self +          end + +          resource = SingletonResource.new(resources.pop, options)            if @scope[:scope_level] == :resources              nested do @@ -366,16 +440,16 @@ module ActionDispatch              return self            end -          scope(:path => "/#{resource.name}", :controller => resource.controller) do +          scope(:path => resource.name.to_s, :controller => resource.controller) do              with_scope_level(:resource, resource) do                yield if block_given? -              get    "(.:format)",      :to => :show, :as => resource.member_name -              post   "(.:format)",      :to => :create -              put    "(.:format)",      :to => :update -              delete "(.:format)",      :to => :destroy -              get    "/new(.:format)",  :to => :new,  :as => "new_#{resource.singular}" -              get    "/edit(.:format)", :to => :edit, :as => "edit_#{resource.singular}" +              get    :show, :as => resource.member_name if resource.actions.include?(:show) +              post   :create if resource.actions.include?(:create) +              put    :update if resource.actions.include?(:update) +              delete :destroy if resource.actions.include?(:destroy) +              get    :new, :as => resource.singular if resource.actions.include?(:new) +              get    :edit, :as => resource.singular if resource.actions.include?(:edit)              end            end @@ -391,7 +465,14 @@ module ActionDispatch              return self            end -          resource = Resource.new(resources.pop) +          if path_names = options.delete(:path_names) +            scope(:resources_path_names => path_names) do +              resources(resources, options) +            end +            return self +          end + +          resource = Resource.new(resources.pop, options)            if @scope[:scope_level] == :resources              nested do @@ -400,28 +481,22 @@ module ActionDispatch              return self            end -          scope(:path => "/#{resource.name}", :controller => resource.controller) do +          scope(:path => resource.name.to_s, :controller => resource.controller) do              with_scope_level(:resources, resource) do                yield if block_given?                with_scope_level(:collection) do -                get  "(.:format)", :to => :index, :as => resource.collection_name -                post "(.:format)", :to => :create - -                with_exclusive_name_prefix :new do -                  get "/new(.:format)", :to => :new, :as => resource.singular -                end +                get  :index, :as => resource.collection_name if resource.actions.include?(:index) +                post :create if resource.actions.include?(:create) +                get  :new, :as => resource.singular if resource.actions.include?(:new)                end                with_scope_level(:member) do -                scope("/:id") do -                  get    "(.:format)", :to => :show, :as => resource.member_name -                  put    "(.:format)", :to => :update -                  delete "(.:format)", :to => :destroy - -                  with_exclusive_name_prefix :edit do -                    get "/edit(.:format)", :to => :edit, :as => resource.singular -                  end +                scope(':id') do +                  get    :show, :as => resource.member_name if resource.actions.include?(:show) +                  put    :update if resource.actions.include?(:update) +                  delete :destroy if resource.actions.include?(:destroy) +                  get    :edit, :as => resource.singular if resource.actions.include?(:edit)                  end                end              end @@ -448,7 +523,7 @@ module ActionDispatch            end            with_scope_level(:member) do -            scope("/:id", :name_prefix => parent_resource.member_name, :as => "") do +            scope(':id', :name_prefix => parent_resource.member_name, :as => "") do                yield              end            end @@ -460,7 +535,7 @@ module ActionDispatch            end            with_scope_level(:nested) do -            scope("/#{parent_resource.id_segment}", :name_prefix => parent_resource.member_name) do +            scope(parent_resource.id_segment, :name_prefix => parent_resource.member_name) do                yield              end            end @@ -474,9 +549,22 @@ module ActionDispatch              return self            end +          resources_path_names = options.delete(:path_names) +            if args.first.is_a?(Symbol) -            with_exclusive_name_prefix(args.first) do -              return match("/#{args.first}(.:format)", options.merge(:to => args.first.to_sym)) +            action = args.first +            if CRUD_ACTIONS.include?(action) +              begin +                old_path = @scope[:path] +                @scope[:path] = "#{@scope[:path]}(.:format)" +                return match(options.reverse_merge(:to => action)) +              ensure +                @scope[:path] = old_path +              end +            else +              with_exclusive_name_prefix(action) do +                return match("#{action_path(action, resources_path_names)}(.:format)", options.reverse_merge(:to => action)) +              end              end            end @@ -502,6 +590,11 @@ module ActionDispatch            end          private +          def action_path(name, path_names = nil) +            path_names ||= @scope[:resources_path_names] +            path_names[name.to_sym] || name.to_s +          end +            def with_exclusive_name_prefix(prefix)              begin                old_name_prefix = @scope[:name_prefix] diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index bd397432ce..660d28dbec 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -74,9 +74,8 @@ module ActionDispatch            @routes = {}            @helpers = [] -          @module ||= Module.new -          @module.instance_methods.each do |selector| -            @module.class_eval { remove_method selector } +          @module ||= Module.new do +            instance_methods.each { |selector| remove_method(selector) }            end          end @@ -138,67 +137,87 @@ module ActionDispatch              end            end -          def named_helper_module_eval(code, *args) -            @module.module_eval(code, *args) -          end -            def define_hash_access(route, name, kind, options)              selector = hash_access_name(name, kind) -            named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks + +            # We use module_eval to avoid leaks +            @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1                def #{selector}(options = nil)                                      # def hash_for_users_url(options = nil)                  options ? #{options.inspect}.merge(options) : #{options.inspect}  #   options ? {:only_path=>false}.merge(options) : {:only_path=>false}                end                                                                 # end                protected :#{selector}                                              # protected :hash_for_users_url -            end_eval +            END_EVAL              helpers << selector            end +          # Create a url helper allowing ordered parameters to be associated +          # with corresponding dynamic segments, so you can do: +          # +          #   foo_url(bar, baz, bang) +          # +          # Instead of: +          # +          #   foo_url(:bar => bar, :baz => baz, :bang => bang) +          # +          # Also allow options hash, so you can do: +          # +          #   foo_url(bar, baz, bang, :sort_by => 'baz') +          #            def define_url_helper(route, name, kind, options)              selector = url_helper_name(name, kind) -            # The segment keys used for positional parameters -              hash_access_method = hash_access_name(name, kind) -            # allow ordered parameters to be associated with corresponding -            # dynamic segments, so you can do +            # We use module_eval to avoid leaks.              # -            #   foo_url(bar, baz, bang) +            # def users_url(*args) +            #   if args.empty? || Hash === args.first +            #     options = hash_for_users_url(args.first || {}) +            #   else +            #     options = hash_for_users_url(args.extract_options!) +            #     default = default_url_options(options) if self.respond_to?(:default_url_options, true) +            #     options = (default ||= {}).merge(options)              # -            # instead of +            #     keys = [] +            #     keys -= options.keys if args.size < keys.size - 1              # -            #   foo_url(:bar => bar, :baz => baz, :bang => bang) +            #     args = args.zip(keys).inject({}) do |h, (v, k)| +            #       h[k] = v +            #       h +            #     end              # -            # Also allow options hash, so you can do +            #     # Tell url_for to skip default_url_options +            #     options[:use_defaults] = false +            #     options.merge!(args) +            #   end              # -            #   foo_url(bar, baz, bang, :sort_by => 'baz') -            # -            named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks -              def #{selector}(*args)                                                        # def users_url(*args) -                                                                                            # -                opts = if args.empty? || Hash === args.first                                #   opts = if args.empty? || Hash === args.first -                  args.first || {}                                                          #     args.first || {} -                else                                                                        #   else -                  options = args.extract_options!                                           #     options = args.extract_options! -                  args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)|  #     args = args.zip([]).inject({}) do |h, (v, k)| -                    h[k] = v                                                                #       h[k] = v -                    h                                                                       #       h -                  end                                                                       #     end -                  options.merge(args)                                                       #     options.merge(args) -                end                                                                         #   end -                                                                                            # -                url_for(#{hash_access_method}(opts))                                        #   url_for(hash_for_users_url(opts)) -                                                                                            # -              end                                                                           # end -              #Add an alias to support the now deprecated formatted_* URL.                  # #Add an alias to support the now deprecated formatted_* URL. -              def formatted_#{selector}(*args)                                              # def formatted_users_url(*args) -                ActiveSupport::Deprecation.warn(                                            #   ActiveSupport::Deprecation.warn( -                  "formatted_#{selector}() has been deprecated. " +                         #     "formatted_users_url() has been deprecated. " + -                  "Please pass format to the standard " +                                   #     "Please pass format to the standard " + -                  "#{selector} method instead.", caller)                                    #     "users_url method instead.", caller) -                #{selector}(*args)                                                          #   users_url(*args) -              end                                                                           # end -              protected :#{selector}                                                        # protected :users_url -            end_eval +            #   url_for(options) +            # end +            @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 +              def #{selector}(*args) +                if args.empty? || Hash === args.first +                  options = #{hash_access_method}(args.first || {}) +                else +                  options = #{hash_access_method}(args.extract_options!) +                  default = default_url_options(options) if self.respond_to?(:default_url_options, true) +                  options = (default ||= {}).merge(options) + +                  keys = #{route.segment_keys.inspect} +                  keys -= options.keys if args.size < keys.size - 1 # take format into account + +                  args = args.zip(keys).inject({}) do |h, (v, k)| +                    h[k] = v +                    h +                  end + +                  # Tell url_for to skip default_url_options +                  options[:use_defaults] = false +                  options.merge!(args) +                end + +                url_for(options) +              end +              protected :#{selector} +            END_EVAL              helpers << selector            end        end @@ -206,9 +225,16 @@ module ActionDispatch        attr_accessor :routes, :named_routes        attr_accessor :disable_clear_and_finalize +      def self.default_resources_path_names +        { :new => 'new', :edit => 'edit' } +      end + +      attr_accessor :resources_path_names +        def initialize          self.routes = []          self.named_routes = NamedRouteCollection.new +        self.resources_path_names = self.class.default_resources_path_names          @disable_clear_and_finalize = false        end diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 5686bbdbde..c2486d3730 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -2,6 +2,15 @@ module ActionDispatch    module Assertions      # A small suite of assertions that test responses from Rails applications.      module ResponseAssertions +      extend ActiveSupport::Concern + +      included do +        # TODO: Need to pull in AV::Template monkey patches that track which +        # templates are rendered. assert_template should probably be part +        # of AV instead of AD. +        require 'action_view/test_case' +      end +        # Asserts that the response is one of the following types:        #        # * <tt>:success</tt>   - Status code was 200 diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index c2dc591ff7..a6b1126e2b 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -524,7 +524,7 @@ module ActionDispatch          fix_content = lambda do |node|            # Gets around a bug in the Rails 1.1 HTML parser. -          node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { CGI.escapeHTML($1) } +          node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { Rack::Utils.escapeHTML($1) }          end          selected = elements.map do |element| diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 2a5f5dcd5c..4ec47d146c 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -1,6 +1,5 @@  require 'stringio'  require 'uri' -require 'active_support/test_case'  require 'active_support/core_ext/object/metaclass'  require 'rack/test'  | 
