diff options
Diffstat (limited to 'actionpack/lib/action_dispatch/http')
-rw-r--r-- | actionpack/lib/action_dispatch/http/cache.rb | 4 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/filter_parameters.rb | 5 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/filter_redirect.rb | 9 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/headers.rb | 92 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/mime_negotiation.rb | 35 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/mime_type.rb | 45 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/mime_types.rb | 1 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/parameter_filter.rb | 2 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/parameters.rb | 71 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/request.rb | 121 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/response.rb | 167 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/upload.rb | 28 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/url.rb | 274 |
13 files changed, 623 insertions, 231 deletions
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 0d6015d993..63a3cbc90b 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -92,7 +92,7 @@ module ActionDispatch LAST_MODIFIED = "Last-Modified".freeze ETAG = "ETag".freeze CACHE_CONTROL = "Cache-Control".freeze - SPESHUL_KEYS = %w[extras no-cache max-age public must-revalidate] + SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate]) def cache_control_segments if cache_control = self[CACHE_CONTROL] @@ -108,7 +108,7 @@ module ActionDispatch cache_control_segments.each do |segment| directive, argument = segment.split('=', 2) - if SPESHUL_KEYS.include? directive + if SPECIAL_KEYS.include? directive key = directive.tr('-', '_') cache_control[key.to_sym] = argument || true else diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 02ab49b44e..2b851cc28d 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -1,12 +1,13 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/object/duplicable' +require 'action_dispatch/http/parameter_filter' module ActionDispatch module Http # Allows you to specify sensitive parameters which will be replaced from # the request log by looking in the query string of the request and all - # subhashes of the params hash to filter. If a block is given, each key and - # value of the params hash and all subhashes is passed to it, the value + # sub-hashes of the params hash to filter. If a block is given, each key and + # value of the params hash and all sub-hashes is passed to it, the value # or key can be replaced using String#replace or similar method. # # env["action_dispatch.parameter_filter"] = [:password] diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb index 900ce1c646..cd603649c3 100644 --- a/actionpack/lib/action_dispatch/http/filter_redirect.rb +++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb @@ -5,7 +5,8 @@ module ActionDispatch FILTERED = '[FILTERED]'.freeze # :nodoc: def filtered_location - if !location_filter.empty? && location_filter_match? + filters = location_filter + if !filters.empty? && location_filter_match?(filters) FILTERED else location @@ -15,15 +16,15 @@ module ActionDispatch private def location_filter - if request.present? + if request request.env['action_dispatch.redirect_filter'] || [] else [] end end - def location_filter_match? - location_filter.any? do |filter| + def location_filter_match?(filters) + filters.any? do |filter| if String === filter location.include?(filter) elsif Regexp === filter diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index dc04d4577b..bc5410dc38 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -1,38 +1,98 @@ module ActionDispatch module Http + # Provides access to the request's HTTP headers from the environment. + # + # env = { "CONTENT_TYPE" => "text/plain" } + # headers = ActionDispatch::Http::Headers.new(env) + # headers["Content-Type"] # => "text/plain" class Headers + CGI_VARIABLES = Set.new(%W[ + AUTH_TYPE + CONTENT_LENGTH + CONTENT_TYPE + GATEWAY_INTERFACE + HTTPS + PATH_INFO + PATH_TRANSLATED + QUERY_STRING + REMOTE_ADDR + REMOTE_HOST + REMOTE_IDENT + REMOTE_USER + REQUEST_METHOD + SCRIPT_NAME + SERVER_NAME + SERVER_PORT + SERVER_PROTOCOL + SERVER_SOFTWARE + ]).freeze + + HTTP_HEADER = /\A[A-Za-z0-9-]+\z/ + include Enumerable + attr_reader :env + + def initialize(env = {}) # :nodoc: + @env = env + end - def initialize(env = {}) - @headers = env + # Returns the value for the given key mapped to @env. + def [](key) + @env[env_name(key)] end - def [](header_name) - @headers[env_name(header_name)] + # Sets the given value for the key mapped to @env. + def []=(key, value) + @env[env_name(key)] = value end - def []=(k,v); @headers[k] = v; end - def key?(k); @headers.key? k; end + def key?(key) + @env.key? env_name(key) + end alias :include? :key? - def fetch(header_name, *args, &block) - @headers.fetch env_name(header_name), *args, &block + # Returns the value for the given key mapped to @env. + # + # If the key is not found and an optional code block is not provided, + # raises a <tt>KeyError</tt> exception. + # + # 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 end def each(&block) - @headers.each(&block) + @env.each(&block) end - private + # 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.merge!(headers_or_env) + headers + end - # Converts a HTTP header name to an environment variable name if it is - # not contained within the headers hash. - def env_name(header_name) - @headers.include?(header_name) ? header_name : cgi_name(header_name) + # Adds the contents of <tt>headers_or_env</tt> to original instance + # entries; duplicate keys are overwritten with the values from + # <tt>headers_or_env</tt>. + def merge!(headers_or_env) + headers_or_env.each do |key, value| + self[env_name(key)] = value + end end - def cgi_name(k) - "HTTP_#{k.upcase.gsub(/-/, '_')}" + private + # Converts a HTTP header name to an environment variable name if it is + # not contained within the headers hash. + def env_name(key) + key = key.to_s + if key =~ HTTP_HEADER + key = key.upcase.tr('-', '_') + key = "HTTP_" + key unless CGI_VARIABLES.include?(key) + end + key end end end diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 57660e93c4..53a98c5d0a 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -10,6 +10,8 @@ module ActionDispatch self.ignore_accept_header = false end + attr_reader :variant + # The MIME type of the HTTP request, such as Mime::XML. # # For backward compatibility, the post \format is extracted from the @@ -48,12 +50,18 @@ module ActionDispatch # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first # def format(view_path = []) - formats.first + formats.first || Mime::NullType.instance end def formats - @env["action_dispatch.request.formats"] ||= - if parameters[:format] + @env["action_dispatch.request.formats"] ||= begin + params_readable = begin + parameters[:format] + rescue ActionController::BadRequest + false + end + + if params_readable Array(Mime[parameters[:format]]) elsif use_accept_header && valid_accept_header accepts @@ -62,6 +70,21 @@ module ActionDispatch else [Mime::HTML] end + end + end + + # Sets the \variant for template. + def variant=(variant) + if variant.is_a?(Symbol) + @variant = [variant] + elsif variant.nil? || variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) } + @variant = variant + else + raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols, not a #{variant.class}. " \ + "For security reasons, never directly set the variant to a user-provided value, " \ + "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \ + "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'" + end end # Sets the \format by string extension, which can be used to force custom formats @@ -113,7 +136,7 @@ module ActionDispatch end end - order.include?(Mime::ALL) ? formats.first : nil + order.include?(Mime::ALL) ? format : nil end protected @@ -121,8 +144,8 @@ module ActionDispatch BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/ def valid_accept_header - (xhr? && (accept || content_mime_type)) || - (accept && accept !~ BROWSER_LIKE_ACCEPTS) + (xhr? && (accept.present? || content_mime_type)) || + (accept.present? && accept !~ BROWSER_LIKE_ACCEPTS) end def use_accept_header diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 912da741b7..047a17937a 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,11 +1,12 @@ require 'set' -require 'active_support/core_ext/class/attribute_accessors' +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 { |m| m.to_sym } + @symbols ||= map(&:to_sym) end %w(<< concat shift unshift push pop []= clear compact! collect! @@ -27,7 +28,7 @@ module Mime class << self def [](type) return type if type.is_a?(Type) - Type.lookup_by_extension(type) || NullType.new + Type.lookup_by_extension(type) end def fetch(type) @@ -44,8 +45,8 @@ module Mime # # respond_to do |format| # format.html - # format.ics { render text: post.to_ics, mime_type: Mime::Type["text/calendar"] } - # format.xml { render xml: @people } + # format.ics { render text: @post.to_ics, mime_type: Mime::Type["text/calendar"] } + # format.xml { render xml: @post } # end # end # end @@ -53,10 +54,6 @@ module Mime @@html_types = Set.new [:html, :all] cattr_reader :html_types - # These are the content types which browsers can generate without using ajax, flash, etc - # i.e. following a link, getting an image or posting a form. CSRF protection - # only needs to protect against these types. - @@browser_generated_types = Set.new [:html, :url_encoded_form, :multipart_form, :text] attr_reader :symbol @register_callbacks = [] @@ -177,9 +174,9 @@ module Mime end def parse(accept_header) - if accept_header !~ /,/ + if !accept_header.include?(',') accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first - parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)] + parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact else list, index = AcceptList.new, 0 accept_header.split(',').each do |header| @@ -223,8 +220,8 @@ module Mime Mime.instance_eval { remove_const(symbol) } SET.delete_if { |v| v.eql?(mime) } - LOOKUP.delete_if { |k,v| v.eql?(mime) } - EXTENSION_LOOKUP.delete_if { |k,v| v.eql?(mime) } + LOOKUP.delete_if { |_,v| v.eql?(mime) } + EXTENSION_LOOKUP.delete_if { |_,v| v.eql?(mime) } end end @@ -272,18 +269,6 @@ module Mime end end - # Returns true if Action Pack should check requests using this Mime Type for possible request forgery. See - # ActionController::RequestForgeryProtection. - def verify_request? - ActiveSupport::Deprecation.warn "Mime::Type#verify_request? is deprecated and will be removed in Rails 4.1" - @@browser_generated_types.include?(to_sym) - end - - def self.browser_generated_types - ActiveSupport::Deprecation.warn "Mime::Type.browser_generated_types is deprecated and will be removed in Rails 4.1" - @@browser_generated_types - end - def html? @@html_types.include?(to_sym) || @string =~ /html/ end @@ -306,12 +291,20 @@ module Mime method.to_s.ends_with? '?' end end - + class NullType + include Singleton + def nil? true end + def ref; end + + def respond_to_missing?(method, include_private = false) + method.to_s.ends_with? '?' + end + private def method_missing(method, *args) false if method.to_s.ends_with? '?' diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index a6b3aee5e7..0e4da36038 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -7,6 +7,7 @@ Mime::Type.register "text/javascript", :js, %w( application/javascript applicati Mime::Type.register "text/css", :css Mime::Type.register "text/calendar", :ics Mime::Type.register "text/csv", :csv +Mime::Type.register "text/vcard", :vcf Mime::Type.register "image/png", :png, [], %w(png) Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg) diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb index b655a54865..df4b073a17 100644 --- a/actionpack/lib/action_dispatch/http/parameter_filter.rb +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -56,7 +56,7 @@ module ActionDispatch elsif value.is_a?(Array) value = value.map { |v| v.is_a?(Hash) ? call(v) : v } elsif blocks.any? - key = key.dup + key = key.dup if key.duplicable? value = value.dup if value.duplicable? blocks.each { |b| b.call(key, value) } end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 6610315da7..a5cd26a3c1 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -1,13 +1,11 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/deprecation' module ActionDispatch module Http module Parameters - def initialize(env) - super - @symbolized_path_params = nil - end + PARAMETERS_KEY = 'action_dispatch.request.path_parameters' # Returns both GET and POST \parameters in a single hash. def parameters @@ -18,71 +16,50 @@ module ActionDispatch query_parameters.dup end params.merge!(path_parameters) - encode_params(params).with_indifferent_access end end alias :params :parameters def path_parameters=(parameters) #:nodoc: - @symbolized_path_params = nil - @env.delete("action_dispatch.request.parameters") - @env["action_dispatch.request.path_parameters"] = parameters + @env.delete('action_dispatch.request.parameters') + @env[PARAMETERS_KEY] = parameters end - # The same as <tt>path_parameters</tt> with explicitly symbolized keys. def symbolized_path_parameters - @symbolized_path_params ||= path_parameters.symbolize_keys + ActiveSupport::Deprecation.warn( + '`symbolized_path_parameters` is deprecated. Please use `path_parameters`.' + ) + path_parameters 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 - - def reset_parameters #:nodoc: - @env.delete("action_dispatch.request.parameters") + @env[PARAMETERS_KEY] ||= {} end private - # TODO: Validate that the characters are UTF-8. If they aren't, - # you'll get a weird error down the road, but our form handling - # should really prevent that from happening - def encode_params(params) - if params.is_a?(String) - return params.force_encoding("UTF-8").encode! - elsif !params.is_a?(Hash) - return params - end - - params.each do |k, v| - case v - when Hash - encode_params(v) - when Array - v.map! {|el| encode_params(el) } + # Convert nested Hash to HashWithIndifferentAccess. + # + def normalize_encode_params(params) + case params + when Hash + if params.has_key?(:tempfile) + UploadedFile.new(params) else - encode_params(v) + 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 - end - end - - # Convert nested Hash 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 + params end end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index d60c8775af..d211ea2b77 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -1,12 +1,16 @@ -require 'tempfile' require 'stringio' -require 'strscan' -require 'active_support/core_ext/hash/indifferent_access' -require 'active_support/core_ext/string/access' require 'active_support/inflector' require 'action_dispatch/http/headers' require 'action_controller/metal/exceptions' +require 'rack/request' +require 'action_dispatch/http/cache' +require 'action_dispatch/http/mime_negotiation' +require 'action_dispatch/http/parameters' +require 'action_dispatch/http/filter_parameters' +require 'action_dispatch/http/upload' +require 'action_dispatch/http/url' +require 'active_support/core_ext/array/conversions' module ActionDispatch class Request < Rack::Request @@ -14,12 +18,12 @@ module ActionDispatch include ActionDispatch::Http::MimeNegotiation include ActionDispatch::Http::Parameters include ActionDispatch::Http::FilterParameters - include ActionDispatch::Http::Upload include ActionDispatch::Http::URL autoload :Session, 'action_dispatch/request/session' + autoload :Utils, 'action_dispatch/request/utils' - LOCALHOST = Regexp.union [/^127\.0\.0\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/] + LOCALHOST = Regexp.union [/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/] ENV_METHODS = %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_TRANSLATED REMOTE_HOST @@ -49,6 +53,17 @@ module ActionDispatch @uuid = nil end + def check_path_parameters! + # If any of the path parameters has an invalid encoding then + # raise since it's likely to trigger errors further on. + path_parameters.each do |key, value| + next unless value.respond_to?(:valid_encoding?) + unless value.valid_encoding? + raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" + end + end + end + def key?(key) @env.key?(key) end @@ -60,6 +75,7 @@ module ActionDispatch # Ordered Collections Protocol (WebDAV) (http://www.ietf.org/rfc/rfc3648.txt) # Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (http://www.ietf.org/rfc/rfc3744.txt) # Web Distributed Authoring and Versioning (WebDAV) SEARCH (http://www.ietf.org/rfc/rfc5323.txt) + # Calendar Extensions to WebDAV (http://www.ietf.org/rfc/rfc4791.txt) # PATCH Method for HTTP (http://www.ietf.org/rfc/rfc5789.txt) RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT) RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK) @@ -67,9 +83,10 @@ module ActionDispatch RFC3648 = %w(ORDERPATCH) RFC3744 = %w(ACL) RFC5323 = %w(SEARCH) + RFC4791 = %w(MKCALENDAR) RFC5789 = %w(PATCH) - HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789 + HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789 HTTP_METHOD_LOOKUP = {} @@ -88,6 +105,24 @@ module ActionDispatch @request_method ||= check_method(env["REQUEST_METHOD"]) end + def routes # :nodoc: + env["action_dispatch.routes".freeze] + end + + def original_script_name # :nodoc: + env['ORIGINAL_SCRIPT_NAME'.freeze] + end + + def engine_script_name(_routes) # :nodoc: + env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"] + end + + def request_method=(request_method) #:nodoc: + if check_method(request_method) + @request_method = env["REQUEST_METHOD"] = request_method + end + end + # Returns a symbol form of the #request_method def request_method_symbol HTTP_METHOD_LOOKUP[request_method] @@ -148,18 +183,40 @@ module ActionDispatch Http::Headers.new(@env) end + # Returns a +String+ with the last requested path including their params. + # + # # get '/foo' + # request.original_fullpath # => '/foo' + # + # # get '/foo?bar' + # request.original_fullpath # => '/foo?bar' def original_fullpath @original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath) end + # Returns the +String+ full path including params of the last URL requested. + # + # # get "/articles" + # request.fullpath # => "/articles" + # + # # get "/articles?page=2" + # request.fullpath # => "/articles?page=2" def fullpath @fullpath ||= super end + # Returns the original request URL as a +String+. + # + # # get "/articles?page=2" + # request.original_url # => "http://www.example.com/articles?page=2" def original_url base_url + original_fullpath end + # The +String+ MIME type of the request. + # + # # get "/articles" + # request.media_type # => "application/x-www-form-urlencoded" def media_type content_mime_type.to_s end @@ -170,8 +227,8 @@ module ActionDispatch end # Returns true if the "X-Requested-With" header contains "XMLHttpRequest" - # (case-insensitive). All major JavaScript libraries send this header with - # every Ajax request. + # (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 end @@ -186,7 +243,7 @@ module ActionDispatch @remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s end - # Returns the unique request id, which is based off either the X-Request-Id header that can + # Returns the unique request id, which is based on either the X-Request-Id header that can # be generated by a firewall, load balancer, or web server or by the RequestId middleware # (which sets the action_dispatch.request_id environment variable). # @@ -206,7 +263,7 @@ module ActionDispatch def raw_post unless @env.include? 'RAW_POST_DATA' raw_post_body = body - @env['RAW_POST_DATA'] = raw_post_body.read(@env['CONTENT_LENGTH'].to_i) + @env['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'] @@ -252,16 +309,16 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= (normalize_parameters(super) || {}) - rescue TypeError => e + @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) + rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e raise ActionController::BadRequest.new(:query, e) 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) || {}) - rescue TypeError => e + @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) + rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e raise ActionController::BadRequest.new(:request, e) end alias :request_parameters :POST @@ -280,32 +337,24 @@ module ActionDispatch LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip end - protected - - # Remove nils from the params hash + # Extracted into ActionDispatch::Request::Utils.deep_munge, but kept here for backwards compatibility. def deep_munge(hash) - hash.each_value do |v| - case v - when Array - v.grep(Hash) { |x| deep_munge(x) } - v.compact! - when Hash - deep_munge(v) - end - end + ActiveSupport::Deprecation.warn( + 'This method has been extracted into `ActionDispatch::Request::Utils.deep_munge`. Please start using that instead.' + ) - hash + Utils.deep_munge(hash) end - def parse_query(qs) - deep_munge(super) - end + protected + def parse_query(qs) + Utils.deep_munge(super) + end private - - def check_method(name) - HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") - name - end + def check_method(name) + HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}") + name + end end end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 91cf4784db..33de2f8b5f 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -1,5 +1,7 @@ -require 'digest/md5' -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/module/attribute_accessors' +require 'active_support/core_ext/string/filters' +require 'active_support/deprecation' +require 'action_dispatch/http/filter_redirect' require 'monitor' module ActionDispatch # :nodoc: @@ -32,10 +34,17 @@ module ActionDispatch # :nodoc: # end # end class Response - attr_accessor :request, :header + # The request that the response is responding to. + attr_accessor :request + + # The HTTP status code. attr_reader :status + attr_writer :sending_file + # Get and set headers for this response. + attr_accessor :header + alias_method :headers=, :header= alias_method :headers, :header @@ -50,12 +59,16 @@ module ActionDispatch # :nodoc: # 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_accessor :charset 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_accessor :charset + CONTENT_TYPE = "Content-Type".freeze SET_COOKIE = "Set-Cookie".freeze LOCATION = "Location".freeze + NO_CONTENT_CODES = [204, 304] cattr_accessor(:default_charset) { "utf-8" } cattr_accessor(:default_headers) @@ -80,7 +93,13 @@ module ActionDispatch # :nodoc: end def each(&block) - @buf.each(&block) + @response.sending! + x = @buf.each(&block) + @response.sent! + x + end + + def abort end def close @@ -93,6 +112,7 @@ module ActionDispatch # :nodoc: end end + # The underlying body, as a streamable object. attr_reader :stream def initialize(status = 200, header = {}, body = []) @@ -106,6 +126,8 @@ module ActionDispatch # :nodoc: @blank = false @cv = new_cond @committed = false + @sending = false + @sent = false @content_type = nil @charset = nil @@ -126,52 +148,72 @@ module ActionDispatch # :nodoc: end end + def await_sent + synchronize { @cv.wait_until { @sent } } + end + def commit! synchronize do + before_committed @committed = true @cv.broadcast end end - def committed? - @committed + def sending! + synchronize do + before_sending + @sending = true + @cv.broadcast + end end + def sent! + synchronize do + @sent = true + @cv.broadcast + end + end + + def sending?; synchronize { @sending }; end + def committed?; synchronize { @committed }; end + def sent?; synchronize { @sent }; end + + # Sets the HTTP status code. def status=(status) @status = Rack::Utils.status_code(status) end + # Sets the HTTP content type. def content_type=(content_type) @content_type = content_type.to_s end - # The response code of the request + # The response code of the request. def response_code @status end - # Returns a String to ensure compatibility with Net::HTTPResponse + # Returns a string to ensure compatibility with <tt>Net::HTTPResponse</tt>. def code @status.to_s end + # Returns the corresponding message for the current HTTP status code: + # + # response.status = 200 + # response.message # => "OK" + # + # response.status = 404 + # response.message # => "Not Found" + # def message Rack::Utils::HTTP_STATUS_CODES[@status] end alias_method :status_message, :message - def respond_to?(method) - if method.to_sym == :to_path - stream.respond_to?(:to_path) - else - super - end - end - - def to_path - stream.to_path - end - + # 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 } @@ -180,13 +222,16 @@ module ActionDispatch # :nodoc: EMPTY = " " + # Allows you to manually set or override the response body. def body=(body) @blank = true if body == EMPTY if body.respond_to?(:to_path) @stream = body else - @stream = build_buffer self, munge_body_object(body) + synchronize do + @stream = build_buffer self, munge_body_object(body) + end end end @@ -204,11 +249,13 @@ module ActionDispatch # :nodoc: ::Rack::Utils.delete_cookie_header!(header, key, value) end + # The location header we'll be responding with. def location headers[LOCATION] end alias_method :redirect_url, :location + # Sets the location header we'll be responding with. def location=(url) headers[LOCATION] = url end @@ -217,11 +264,39 @@ module ActionDispatch # :nodoc: stream.close if stream.respond_to?(:close) end + def abort + if stream.respond_to?(:abort) + stream.abort + elsif stream.respond_to?(:close) + # `stream.close` should really be reserved for a close from the + # other direction, but we must fall back to it for + # compatibility. + stream.close + end + end + + # Turns the Response into a Rack-compatible array of the status, headers, + # and body. Allows explict splatting: + # + # status, headers, body = *response def to_a rack_response @status, @header.to_hash end alias prepare! to_a - alias to_ary to_a # For implicit splat on 1.9.2 + + # Be super clear that a response object is not an Array. Defining this + # would make implicit splatting work, but it also makes adding responses + # as arrays work, and "flattening" responses, cascading to the rack body! + # Not sensible behavior. + def to_ary + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `ActionDispatch::Response#to_ary` no longer performs implicit conversion + to an array. Please use `response.to_a` instead, or a splat like `status, + headers, body = *response`. + MSG + + to_a + end # Returns the response cookies, converted to a Hash of (name => value) pairs # @@ -242,6 +317,12 @@ module ActionDispatch # :nodoc: private + def before_committed + end + + def before_sending + end + def merge_default_headers(original, default) return original unless default.respond_to?(:merge) @@ -272,17 +353,53 @@ module ActionDispatch # :nodoc: !@sending_file && @charset != false end + class RackBody + def initialize(response) + @response = response + end + + def each(*args, &block) + @response.each(*args, &block) + end + + def close + # Rack "close" maps to Response#abort, and *not* Response#close + # (which is used when the controller's finished writing) + @response.abort + end + + def body + @response.body + end + + def respond_to?(method, include_private = false) + if method.to_s == 'to_path' + @response.stream.respond_to?(method) + else + super + end + end + + def to_path + @response.stream.to_path + end + + def to_ary + nil + end + end + def rack_response(status, header) assign_default_content_type_and_charset!(header) handle_conditional_get! header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join) - if [204, 304].include?(@status) + if NO_CONTENT_CODES.include?(@status) header.delete CONTENT_TYPE [status, header, []] else - [status, header, self] + [status, header, RackBody.new(self)] end end end diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 79437d6e85..540e11a4a0 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -6,7 +6,7 @@ module ActionDispatch # of its interface is available directly for convenience. # # Uploaded files are temporary files whose lifespan is one request. When - # the object is finalized Ruby unlinks the file, so there is not need to + # the object is finalized Ruby unlinks the file, so there is no need to # clean them with a separate maintenance task. class UploadedFile # The basename of the file in the client. @@ -18,15 +18,17 @@ module ActionDispatch # A +Tempfile+ object with the actual uploaded file. Note that some of # its interface is available directly. attr_accessor :tempfile + alias :to_io :tempfile - # TODO. + # A string with the headers of the multipart request. attr_accessor :headers def initialize(hash) # :nodoc: @tempfile = hash[:tempfile] raise(ArgumentError, ':tempfile is required') unless @tempfile - @original_filename = encode_filename(hash[:filename]) + @original_filename = hash[:filename] + @original_filename &&= @original_filename.encode "UTF-8" @content_type = hash[:type] @headers = hash[:head] end @@ -65,26 +67,6 @@ module ActionDispatch def eof? @tempfile.eof? end - - private - - def encode_filename(filename) - # Encode the filename in the utf8 encoding, unless it is nil - filename.force_encoding("UTF-8").encode! if filename - end - end - - module Upload # :nodoc: - # Convert nested Hash to HashWithIndifferentAccess and replace - # file upload hash with UploadedFile objects - def normalize_parameters(value) - if Hash === value && value.has_key?(:tempfile) - UploadedFile.new(value) - else - super - end - end - private :normalize_parameters end end end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index bced7d84c0..001b14ec97 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -1,90 +1,187 @@ +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}$/ + IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ + HOST_REGEXP = /(^[^:]+:\/\/)?([^:]+)(?::(\d+$))?/ + PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/ mattr_accessor :tld_length self.tld_length = 1 class << self - def extract_domain(host, tld_length = @@tld_length) - host.split('.').last(1 + tld_length).join('.') if named_host?(host) + # Returns the domain part of a host given the domain level. + # + # # Top-level domain example + # extract_domain('www.example.com', 1) # => "example.com" + # # Second-level domain example + # extract_domain('dev.www.example.co.uk', 2) # => "example.co.uk" + def extract_domain(host, tld_length) + extract_domain_from(host, tld_length) if named_host?(host) end - def extract_subdomains(host, tld_length = @@tld_length) + # Returns the subdomains of a host as an Array given the domain level. + # + # # Top-level domain example + # extract_subdomains('www.example.com', 1) # => ["www"] + # # Second-level domain example + # extract_subdomains('dev.www.example.co.uk', 2) # => ["dev", "www"] + def extract_subdomains(host, tld_length) if named_host?(host) - parts = host.split('.') - parts[0..-(tld_length + 2)] + extract_subdomains_from(host, tld_length) else [] end end - def extract_subdomain(host, tld_length = @@tld_length) + # Returns the subdomains of a host as a String given the domain level. + # + # # Top-level domain example + # extract_subdomain('www.example.com', 1) # => "www" + # # Second-level domain example + # extract_subdomain('dev.www.example.co.uk', 2) # => "dev.www" + def extract_subdomain(host, tld_length) extract_subdomains(host, tld_length).join('.') end - def url_for(options = {}) - path = options.delete(:script_name).to_s.chomp("/") - path << options.delete(:path).to_s + def url_for(options) + if options[:only_path] + path_for options + else + full_url_for options + end + end - params = options[:params].is_a?(Hash) ? options[:params] : options.slice(:params) - params.reject! { |_,v| v.to_param.nil? } + def full_url_for(options) + host = options[:host] + protocol = options[:protocol] + port = options[:port] - result = build_host_url(options) - result << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) - result << "?#{params.to_query}" unless params.empty? - result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor] - result + unless host + raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true' + end + + build_host_url(host, port, protocol, options, path_for(options)) + end + + def path_for(options) + path = options[:script_name].to_s.chomp("/") + path << options[:path] if options.key?(:path) + + add_trailing_slash(path) if options[:trailing_slash] + add_params(path, options[:params]) if options.key?(:params) + add_anchor(path, options[:anchor]) if options.key?(:anchor) + + path end private - def build_host_url(options) - if options[:host].blank? && options[:only_path].blank? - raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true' + def add_params(path, params) + params = { params: params } unless params.is_a?(Hash) + params.reject! { |_,v| v.to_param.nil? } + path << "?#{params.to_query}" unless params.empty? + end + + def add_anchor(path, anchor) + if anchor + path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param)}" + end + end + + def extract_domain_from(host, tld_length) + host.split('.').last(1 + tld_length).join('.') + end + + def extract_subdomains_from(host, tld_length) + parts = host.split('.') + parts[0..-(tld_length + 2)] + end + + def add_trailing_slash(path) + # includes querysting + if path.include?('?') + path.sub!(/\?/, '/\&') + # does not have a .format + elsif !path.include?(".") + path.sub!(/[^\/]\z|\A\z/, '\&/') + end + end + + def build_host_url(host, port, protocol, options, path) + if match = host.match(HOST_REGEXP) + protocol ||= match[1] unless protocol == false + host = match[2] + port = match[3] unless options.key? :port end - result = "" - - unless options[:only_path] - unless options[:protocol] == false - result << (options[:protocol] || "http") - result << ":" unless result.match(%r{:|//}) - end - result << "//" unless result.match("//") - result << rewrite_authentication(options) - result << host_or_subdomain_and_domain(options) - result << ":#{options.delete(:port)}" if options[:port] + protocol = normalize_protocol protocol + host = normalize_host(host, options) + + result = protocol.dup + + if options[:user] && options[:password] + result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" end - result + + result << host + normalize_port(port, protocol) { |normalized_port| + result << ":#{normalized_port}" + } + + result.concat path end def named_host?(host) - host && IP_HOST_REGEXP !~ host + IP_HOST_REGEXP !~ host end - def rewrite_authentication(options) - if options[:user] && options[:password] - "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" + def normalize_protocol(protocol) + case protocol + when nil + "http://" + when false, "//" + "//" + when PROTOCOL_REGEXP + "#{$1}://" else - "" + raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}" end end - def host_or_subdomain_and_domain(options) - return options[:host] if !named_host?(options[:host]) || (options[:subdomain].nil? && options[:domain].nil?) + def normalize_host(_host, options) + return _host unless named_host?(_host) tld_length = options[:tld_length] || @@tld_length + subdomain = options.fetch :subdomain, true + domain = options[:domain] host = "" - unless options[:subdomain] == false - host << (options[:subdomain] || extract_subdomain(options[:host], tld_length)).to_param - host << "." + if subdomain == true + return _host if domain.nil? + + host << extract_subdomains_from(_host, tld_length).join('.') + elsif subdomain + host << subdomain.to_param end - host << (options[:domain] || extract_domain(options[:host], tld_length)) + host << "." unless host.empty? + host << (domain || extract_domain_from(_host, tld_length)) host end + + def normalize_port(port, protocol) + return unless port + + case protocol + when "//" then yield port + when "https://" + yield port unless port.to_i == 443 + else + yield port unless port.to_i == 80 + end + end end def initialize(env) @@ -94,16 +191,43 @@ module ActionDispatch end # Returns the complete URL used for this request. + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com' + # req.url # => "http://example.com" def url protocol + host_with_port + fullpath end # Returns 'https://' if this is an SSL request and 'http://' otherwise. + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com' + # req.protocol # => "http://" + # + # req = Request.new 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on' + # req.protocol # => "https://" def protocol @protocol ||= ssl? ? 'https://' : 'http://' end # Returns the \host for this request, such as "example.com". + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com' + # req.raw_host_with_port # => "example.com" + # + # 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"] forwarded.split(/,\s?/).last @@ -113,17 +237,44 @@ module ActionDispatch end # Returns the host for this request, such as example.com. + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req.host # => "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". + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com:80' + # req.host_with_port # => "example.com" + # + # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req.host_with_port # => "example.com:8080" def host_with_port "#{host}#{port_string}" end # Returns the port number of this request as an integer. + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com' + # req.port # => 80 + # + # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req.port # => 8080 def port @port ||= begin if raw_host_with_port =~ /:(\d+)$/ @@ -135,6 +286,13 @@ module ActionDispatch end # Returns the standard \port number for this request's protocol. + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req.standard_port # => 80 def standard_port case protocol when 'https://' then 443 @@ -143,18 +301,48 @@ module ActionDispatch end # Returns whether this request is using the standard port + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com:80' + # req.standard_port? # => true + # + # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req.standard_port? # => false def standard_port? port == standard_port end # Returns a number \port suffix like 8080 if the \port number of this request # is not the default HTTP \port 80 or HTTPS \port 443. + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com:80' + # req.optional_port # => nil + # + # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req.optional_port # => 8080 def optional_port standard_port? ? nil : port end # Returns a string \port suffix, including colon, like ":8080" if the \port # number of this request is not the default HTTP \port 80 or HTTPS \port 443. + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com:80' + # req.port_string # => "" + # + # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req.port_string # => ":8080" def port_string standard_port? ? '' : ":#{port}" end |