diff options
Diffstat (limited to 'actionpack/lib/action_dispatch/http')
-rw-r--r-- | actionpack/lib/action_dispatch/http/cache.rb | 49 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/filter_parameters.rb | 12 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/filter_redirect.rb | 2 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/headers.rb | 39 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/mime_negotiation.rb | 39 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/mime_type.rb | 141 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/mime_types.rb | 3 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/parameters.rb | 65 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/request.rb | 134 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/response.rb | 218 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/url.rb | 11 |
11 files changed, 463 insertions, 250 deletions
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index cc1cb3f0f0..30ade14c26 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -1,4 +1,3 @@ - module ActionDispatch module Http module Cache @@ -8,13 +7,13 @@ module ActionDispatch HTTP_IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH'.freeze def if_modified_since - if since = env[HTTP_IF_MODIFIED_SINCE] + if since = get_header(HTTP_IF_MODIFIED_SINCE) Time.rfc2822(since) rescue nil end end def if_none_match - env[HTTP_IF_NONE_MATCH] + get_header HTTP_IF_NONE_MATCH end def if_none_match_etags @@ -51,52 +50,51 @@ module ActionDispatch end module Response - attr_reader :cache_control, :etag - alias :etag? :etag + attr_reader :cache_control def last_modified - if last = headers[LAST_MODIFIED] + if last = get_header(LAST_MODIFIED) Time.httpdate(last) end end def last_modified? - headers.include?(LAST_MODIFIED) + has_header? LAST_MODIFIED end def last_modified=(utc_time) - headers[LAST_MODIFIED] = utc_time.httpdate + set_header LAST_MODIFIED, utc_time.httpdate end def date - if date_header = headers[DATE] + if date_header = get_header(DATE) Time.httpdate(date_header) end end def date? - headers.include?(DATE) + has_header? DATE end def date=(utc_time) - headers[DATE] = utc_time.httpdate + set_header DATE, utc_time.httpdate end def etag=(etag) key = ActiveSupport::Cache.expand_cache_key(etag) - @etag = self[ETAG] = %("#{Digest::MD5.hexdigest(key)}") + super %("#{Digest::MD5.hexdigest(key)}") end + def etag?; etag; end + private DATE = 'Date'.freeze LAST_MODIFIED = "Last-Modified".freeze - ETAG = "ETag".freeze - CACHE_CONTROL = "Cache-Control".freeze SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate]) def cache_control_segments - if cache_control = self[CACHE_CONTROL] + if cache_control = _cache_control cache_control.delete(' ').split(',') else [] @@ -123,12 +121,11 @@ module ActionDispatch def prepare_cache_control! @cache_control = cache_control_headers - @etag = self[ETAG] end def handle_conditional_get! if etag? || last_modified? || !@cache_control.empty? - set_conditional_cache_control! + set_conditional_cache_control!(@cache_control) end end @@ -138,24 +135,24 @@ module ActionDispatch PRIVATE = "private".freeze MUST_REVALIDATE = "must-revalidate".freeze - def set_conditional_cache_control! + def set_conditional_cache_control!(cache_control) control = {} cc_headers = cache_control_headers if extras = cc_headers.delete(:extras) - @cache_control[:extras] ||= [] - @cache_control[:extras] += extras - @cache_control[:extras].uniq! + cache_control[:extras] ||= [] + cache_control[:extras] += extras + cache_control[:extras].uniq! end control.merge! cc_headers - control.merge! @cache_control + control.merge! cache_control if control.empty? - self[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL + self._cache_control = DEFAULT_CACHE_CONTROL elsif control[:no_cache] - self[CACHE_CONTROL] = NO_CACHE + self._cache_control = NO_CACHE if control[:extras] - self[CACHE_CONTROL] += ", #{control[:extras].join(', ')}" + self._cache_control = _cache_control + ", #{control[:extras].join(', ')}" end else extras = control[:extras] @@ -167,7 +164,7 @@ module ActionDispatch options << MUST_REVALIDATE if control[:must_revalidate] options.concat(extras) if extras - self[CACHE_CONTROL] = options.join(", ") + self._cache_control = options.join(", ") end end end diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 3170389b36..9dcab79c3a 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/object/duplicable' require 'action_dispatch/http/parameter_filter' module ActionDispatch @@ -25,19 +23,19 @@ module ActionDispatch NULL_PARAM_FILTER = ParameterFilter.new # :nodoc: NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc: - def initialize(env) + def initialize super @filtered_parameters = nil @filtered_env = nil @filtered_path = nil end - # Return a hash of parameters with all sensitive data replaced. + # Returns a hash of parameters with all sensitive data replaced. def filtered_parameters @filtered_parameters ||= parameter_filter.filter(parameters) end - # Return a hash of request.env with all sensitive data replaced. + # Returns a hash of request.env with all sensitive data replaced. def filtered_env @filtered_env ||= env_filter.filter(@env) end @@ -50,13 +48,13 @@ module ActionDispatch protected def parameter_filter - parameter_filter_for @env.fetch("action_dispatch.parameter_filter") { + parameter_filter_for fetch_header("action_dispatch.parameter_filter") { return NULL_PARAM_FILTER } end def env_filter - user_key = @env.fetch("action_dispatch.parameter_filter") { + user_key = fetch_header("action_dispatch.parameter_filter") { return NULL_ENV_FILTER } parameter_filter_for(Array(user_key) + ENV_MATCH) diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb index 94c1f2b41f..f4b806b8b5 100644 --- a/actionpack/lib/action_dispatch/http/filter_redirect.rb +++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb @@ -16,7 +16,7 @@ module ActionDispatch def location_filters if request - request.env['action_dispatch.redirect_filter'] || [] + request.get_header('action_dispatch.redirect_filter') || [] else [] end diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index bc5410dc38..12f81dc1a5 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -30,27 +30,37 @@ module ActionDispatch HTTP_HEADER = /\A[A-Za-z0-9-]+\z/ include Enumerable - attr_reader :env - def initialize(env = {}) # :nodoc: - @env = env + def self.from_hash(hash) + new ActionDispatch::Request.new hash + end + + def initialize(request) # :nodoc: + @req = request end # Returns the value for the given key mapped to @env. def [](key) - @env[env_name(key)] + @req.get_header env_name(key) end # Sets the given value for the key mapped to @env. def []=(key, value) - @env[env_name(key)] = value + @req.set_header env_name(key), value + end + + # Add a value to a multivalued header like Vary or Accept-Encoding. + def add(key, value) + @req.add_header env_name(key), value end def key?(key) - @env.key? env_name(key) + @req.has_header? env_name(key) end alias :include? :key? + DEFAULT = Object.new # :nodoc: + # Returns the value for the given key mapped to @env. # # If the key is not found and an optional code block is not provided, @@ -58,18 +68,22 @@ module ActionDispatch # # If the code block is provided, then it will be run and # its result returned. - def fetch(key, *args, &block) - @env.fetch env_name(key), *args, &block + def fetch(key, default = DEFAULT) + @req.fetch_header(env_name(key)) do + return default unless default == DEFAULT + return yield if block_given? + raise NameError, key + end end def each(&block) - @env.each(&block) + @req.each_header(&block) end # Returns a new Http::Headers instance containing the contents of # <tt>headers_or_env</tt> and the original instance. def merge(headers_or_env) - headers = Http::Headers.new(env.dup) + headers = @req.dup.headers headers.merge!(headers_or_env) headers end @@ -79,11 +93,14 @@ module ActionDispatch # <tt>headers_or_env</tt>. def merge!(headers_or_env) headers_or_env.each do |key, value| - self[env_name(key)] = value + @req.set_header env_name(key), value end end + def env; @req.env.dup; end + private + # Converts a HTTP header name to an environment variable name if it is # not contained within the headers hash. def env_name(key) diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index ff336b7354..7acf91902d 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -10,17 +10,18 @@ module ActionDispatch self.ignore_accept_header = false end - # The MIME type of the HTTP request, such as Mime::XML. + # The MIME type of the HTTP request, such as Mime[:xml]. # # For backward compatibility, the post \format is extracted from the # X-Post-Data-Format HTTP header if present. def content_mime_type - @env["action_dispatch.request.content_type"] ||= begin - if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/ + fetch_header("action_dispatch.request.content_type") do |k| + v = if get_header('CONTENT_TYPE') =~ /^([^,\;]*)/ Mime::Type.lookup($1.strip.downcase) else nil end + set_header k, v end end @@ -28,46 +29,52 @@ module ActionDispatch content_mime_type && content_mime_type.to_s end + def has_content_type? + has_header? 'CONTENT_TYPE' + end + # Returns the accepted MIME type for the request. def accepts - @env["action_dispatch.request.accepts"] ||= begin - header = @env['HTTP_ACCEPT'].to_s.strip + fetch_header("action_dispatch.request.accepts") do |k| + header = get_header('HTTP_ACCEPT').to_s.strip - if header.empty? + v = if header.empty? [content_mime_type] else Mime::Type.parse(header) end + set_header k, v end end # Returns the MIME type for the \format used in the request. # - # GET /posts/5.xml | request.format => Mime::XML - # GET /posts/5.xhtml | request.format => Mime::HTML - # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first + # GET /posts/5.xml | request.format => Mime[:xml] + # GET /posts/5.xhtml | request.format => Mime[:html] + # GET /posts/5 | request.format => Mime[:html] or Mime[:js], or request.accepts.first # def format(view_path = []) formats.first || Mime::NullType.instance end def formats - @env["action_dispatch.request.formats"] ||= begin + fetch_header("action_dispatch.request.formats") do |k| params_readable = begin parameters[:format] rescue ActionController::BadRequest false end - if params_readable + v = if params_readable Array(Mime[parameters[:format]]) elsif use_accept_header && valid_accept_header accepts elsif xhr? - [Mime::JS] + [Mime[:js]] else - [Mime::HTML] + [Mime[:html]] end + set_header k, v end end @@ -102,7 +109,7 @@ module ActionDispatch # end def format=(extension) parameters[:format] = extension.to_s - @env["action_dispatch.request.formats"] = [Mime::Type.lookup_by_extension(parameters[:format])] + set_header "action_dispatch.request.formats", [Mime::Type.lookup_by_extension(parameters[:format])] end # Sets the \formats by string extensions. This differs from #format= by allowing you @@ -121,9 +128,9 @@ module ActionDispatch # end def formats=(extensions) parameters[:format] = extensions.first.to_s - @env["action_dispatch.request.formats"] = extensions.collect do |extension| + set_header "action_dispatch.request.formats", extensions.collect { |extension| Mime::Type.lookup_by_extension(extension) - end + } end # Receives an array of mimes and return the first user sent mime that diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index a639f8a8f8..b64f660ec5 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,23 +1,31 @@ -require 'set' require 'singleton' require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/string/starts_ends_with' module Mime - class Mimes < Array - def symbols - @symbols ||= map(&:to_sym) + class Mimes + include Enumerable + + def initialize + @mimes = [] + @symbols = nil end - %w(<< concat shift unshift push pop []= clear compact! collect! - delete delete_at delete_if flatten! map! insert reject! reverse! - replace slice! sort! uniq!).each do |method| - module_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{method}(*) - @symbols = nil - super - end - CODE + def each + @mimes.each { |x| yield x } + end + + def <<(type) + @mimes << type + @symbols = nil + end + + def delete_if + @mimes.delete_if { |x| yield x }.tap { @symbols = nil } + end + + def symbols + @symbols ||= map(&:to_sym) end end @@ -35,6 +43,42 @@ module Mime return type if type.is_a?(Type) EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k } end + + def const_missing(sym) + ext = sym.downcase + if Mime[ext] + ActiveSupport::Deprecation.warn <<-eow +Accessing mime types via constants is deprecated. Please change: + + `Mime::#{sym}` + +to: + + `Mime[:#{ext}]` + eow + Mime[ext] + else + super + end + end + + def const_defined?(sym, inherit = true) + ext = sym.downcase + if Mime[ext] + ActiveSupport::Deprecation.warn <<-eow +Accessing mime types via constants is deprecated. Please change: + + `Mime.const_defined?(#{sym})` + +to: + + `Mime[:#{ext}]` + eow + true + else + super + end + end end # Encapsulates the notion of a mime type. Can be used at render time, for example, with: @@ -51,9 +95,6 @@ module Mime # end # end class Type - @@html_types = Set.new [:html, :all] - cattr_reader :html_types - attr_reader :symbol @register_callbacks = [] @@ -66,7 +107,7 @@ module Mime def initialize(index, name, q = nil) @index = index @name = name - q ||= 0.0 if @name == Mime::ALL.to_s # default wildcard match to end of list + q ||= 0.0 if @name == '*/*'.freeze # default wildcard match to end of list @q = ((q || 1.0).to_f * 100).to_i end @@ -91,7 +132,7 @@ module Mime exchange_xml_items if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list delete_at(text_xml_idx) # delete text_xml from the list elsif text_xml_idx - text_xml.name = Mime::XML.to_s + text_xml.name = Mime[:xml].to_s end # Look for more specific XML-based types and sort them ahead of app/xml @@ -120,7 +161,7 @@ module Mime end def app_xml_idx - @app_xml_idx ||= index(Mime::XML.to_s) + @app_xml_idx ||= index(Mime[:xml].to_s) end def text_xml @@ -160,17 +201,17 @@ module Mime end def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false) - Mime.const_set(symbol.upcase, Type.new(string, symbol, mime_type_synonyms)) + new_mime = Type.new(string, symbol, mime_type_synonyms) - new_mime = Mime.const_get(symbol.upcase) SET << new_mime - ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = SET.last } unless skip_lookup - ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = SET.last } + ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = new_mime } unless skip_lookup + ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = new_mime } @register_callbacks.each do |callback| callback.call(new_mime) end + new_mime end def parse(accept_header) @@ -200,13 +241,13 @@ module Mime parse_data_with_trailing_star($1) if accept_header =~ TRAILING_STAR_REGEXP end - # For an input of <tt>'text'</tt>, returns <tt>[Mime::JSON, Mime::XML, Mime::ICS, - # Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT]</tt>. + # For an input of <tt>'text'</tt>, returns <tt>[Mime[:json], Mime[:xml], Mime[:ics], + # Mime[:html], Mime[:css], Mime[:csv], Mime[:js], Mime[:yaml], Mime[:text]</tt>. # - # For an input of <tt>'application'</tt>, returns <tt>[Mime::HTML, Mime::JS, - # Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM]</tt>. - def parse_data_with_trailing_star(input) - Mime::SET.select { |m| m =~ input } + # For an input of <tt>'application'</tt>, returns <tt>[Mime[:html], Mime[:js], + # Mime[:xml], Mime[:yaml], Mime[:atom], Mime[:json], Mime[:rss], Mime[:url_encoded_form]</tt>. + def parse_data_with_trailing_star(type) + Mime::SET.select { |m| m =~ type } end # This method is opposite of register method. @@ -215,13 +256,12 @@ module Mime # # Mime::Type.unregister(:mobile) def unregister(symbol) - symbol = symbol.upcase - mime = Mime.const_get(symbol) - Mime.instance_eval { remove_const(symbol) } - - SET.delete_if { |v| v.eql?(mime) } - LOOKUP.delete_if { |_,v| v.eql?(mime) } - EXTENSION_LOOKUP.delete_if { |_,v| v.eql?(mime) } + symbol = symbol.downcase + if mime = Mime[symbol] + SET.delete_if { |v| v.eql?(mime) } + LOOKUP.delete_if { |_, v| v.eql?(mime) } + EXTENSION_LOOKUP.delete_if { |_, v| v.eql?(mime) } + end end end @@ -243,7 +283,7 @@ module Mime end def ref - to_sym || to_s + symbol || to_s end def ===(list) @@ -255,24 +295,23 @@ module Mime end def ==(mime_type) - return false if mime_type.blank? + return false unless mime_type (@synonyms + [ self ]).any? do |synonym| synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym end end def =~(mime_type) - return false if mime_type.blank? + return false unless mime_type regexp = Regexp.new(Regexp.quote(mime_type.to_s)) - (@synonyms + [ self ]).any? do |synonym| - synonym.to_s =~ regexp - end + @synonyms.any? { |synonym| synonym.to_s =~ regexp } || @string =~ regexp end def html? - @@html_types.include?(to_sym) || @string =~ /html/ + symbol == :html || @string =~ /html/ end + def all?; false; end private @@ -292,6 +331,22 @@ module Mime end end + class AllType < Type + include Singleton + + def initialize + super '*/*', :all + end + + def all?; true; end + def html?; true; end + end + + # ALL isn't a real MIME type, so we don't register it for lookup with the + # other concrete types. It's a wildcard match that we use for `respond_to` + # negotiation internals. + ALL = AllType.instance + class NullType include Singleton diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index 01a10c693b..87715205d9 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -31,6 +31,3 @@ Mime::Type.register "application/json", :json, %w( text/x-json application/jsonr Mime::Type.register "application/pdf", :pdf, [], %w(pdf) Mime::Type.register "application/zip", :zip, [], %w(zip) - -# Create Mime::ALL but do not add it to the SET. -Mime::ALL = Mime::Type.new("*/*", :all, []) diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 4defb7f858..248ecfd676 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -1,27 +1,41 @@ -require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/hash/indifferent_access' - module ActionDispatch module Http module Parameters PARAMETERS_KEY = 'action_dispatch.request.path_parameters' + DEFAULT_PARSERS = { + Mime[:json] => lambda { |raw_post| + data = ActiveSupport::JSON.decode(raw_post) + data.is_a?(Hash) ? data : {:_json => data} + } + } + + def self.included(klass) + class << klass + attr_accessor :parameter_parsers + end + + klass.parameter_parsers = DEFAULT_PARSERS + end # Returns both GET and POST \parameters in a single hash. def parameters - @env["action_dispatch.request.parameters"] ||= begin - params = begin - request_parameters.merge(query_parameters) - rescue EOFError - query_parameters.dup - end - params.merge!(path_parameters) - end + params = get_header("action_dispatch.request.parameters") + return params if params + + params = begin + request_parameters.merge(query_parameters) + rescue EOFError + query_parameters.dup + end + params.merge!(path_parameters) + set_header("action_dispatch.request.parameters", params) + params end alias :params :parameters def path_parameters=(parameters) #:nodoc: - @env.delete('action_dispatch.request.parameters') - @env[PARAMETERS_KEY] = parameters + delete_header('action_dispatch.request.parameters') + set_header PARAMETERS_KEY, parameters end # Returns a hash with the \parameters used to form the \path of the request. @@ -29,15 +43,28 @@ module ActionDispatch # # {'action' => 'my_action', 'controller' => 'my_controller'} def path_parameters - @env[PARAMETERS_KEY] ||= {} + get_header(PARAMETERS_KEY) || {} end - private + private - # Convert nested Hash to HashWithIndifferentAccess. - # - def normalize_encode_params(params) - ActionDispatch::Request::Utils.normalize_encode_params params + def parse_formatted_parameters(parsers) + return yield if content_length.zero? + + strategy = parsers.fetch(content_mime_type) { return yield } + + begin + strategy.call(raw_post) + rescue => e # JSON or Ruby code block errors + my_logger = logger || ActiveSupport::Logger.new($stderr) + my_logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{raw_post}" + + raise ParamsParser::ParseError.new(e.message, e) + end + end + + def params_parsers + ActionDispatch::Request.parameter_parsers end end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index de28cd0998..c6ab4dbc9a 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -13,14 +13,14 @@ require 'action_dispatch/http/url' require 'active_support/core_ext/array/conversions' module ActionDispatch - class Request < Rack::Request + class Request + include Rack::Request::Helpers include ActionDispatch::Http::Cache::Request include ActionDispatch::Http::MimeNegotiation include ActionDispatch::Http::Parameters include ActionDispatch::Http::FilterParameters include ActionDispatch::Http::URL - - HTTP_X_REQUEST_ID = "HTTP_X_REQUEST_ID".freeze # :nodoc: + include Rack::Request::Env autoload :Session, 'action_dispatch/request/session' autoload :Utils, 'action_dispatch/request/utils' @@ -31,17 +31,20 @@ module ActionDispatch PATH_TRANSLATED REMOTE_HOST REMOTE_IDENT REMOTE_USER REMOTE_ADDR SERVER_NAME SERVER_PROTOCOL + ORIGINAL_SCRIPT_NAME HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM HTTP_NEGOTIATE HTTP_PRAGMA HTTP_CLIENT_IP HTTP_X_FORWARDED_FOR HTTP_VERSION + HTTP_X_REQUEST_ID HTTP_X_FORWARDED_HOST + SERVER_ADDR ].freeze ENV_METHODS.each do |env| class_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{env.sub(/^HTTP_/n, '').downcase} # def accept_charset - @env["#{env}".freeze] # @env["HTTP_ACCEPT_CHARSET".freeze] + get_header "#{env}".freeze # get_header "HTTP_ACCEPT_CHARSET".freeze end # end METHOD end @@ -62,13 +65,32 @@ module ActionDispatch path_parameters.each do |key, value| next unless value.respond_to?(:valid_encoding?) unless value.valid_encoding? - raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" + raise ActionController::BadRequest, "Invalid parameter encoding: #{key} => #{value.inspect}" end end end + PASS_NOT_FOUND = Class.new { # :nodoc: + def self.action(_); self; end + def self.call(_); [404, {'X-Cascade' => 'pass'}, []]; end + } + + def controller_class + check_path_parameters! + params = path_parameters + + if params.key?(:controller) + controller_param = params[:controller].underscore + params[:action] ||= 'index' + const_name = "#{controller_param.camelize}Controller" + ActiveSupport::Dependencies.constantize(const_name) + else + PASS_NOT_FOUND + end + end + def key?(key) - @env.key?(key) + has_header? key end # List of HTTP request methods from the following RFCs: @@ -109,44 +131,44 @@ module ActionDispatch end def routes # :nodoc: - env["action_dispatch.routes".freeze] + get_header("action_dispatch.routes".freeze) end def routes=(routes) # :nodoc: - env["action_dispatch.routes".freeze] = routes - end - - def original_script_name # :nodoc: - env['ORIGINAL_SCRIPT_NAME'.freeze] + set_header("action_dispatch.routes".freeze, routes) end def engine_script_name(_routes) # :nodoc: - env[_routes.env_key] + get_header(_routes.env_key) end def engine_script_name=(name) # :nodoc: - env[routes.env_key] = name.dup + set_header(routes.env_key, name.dup) end def request_method=(request_method) #:nodoc: if check_method(request_method) - @request_method = env["REQUEST_METHOD"] = request_method + @request_method = set_header("REQUEST_METHOD", request_method) end end def controller_instance # :nodoc: - env['action_controller.instance'.freeze] + get_header('action_controller.instance'.freeze) end def controller_instance=(controller) # :nodoc: - env['action_controller.instance'.freeze] = controller + set_header('action_controller.instance'.freeze, controller) + end + + def http_auth_salt + get_header "action_dispatch.http_auth_salt" end def show_exceptions? # :nodoc: # We're treating `nil` as "unset", and we want the default setting to be # `true`. This logic should be extracted to `env_config` and calculated # once. - !(env['action_dispatch.show_exceptions'.freeze] == false) + !(get_header('action_dispatch.show_exceptions'.freeze) == false) end # Returns a symbol form of the #request_method @@ -158,7 +180,7 @@ module ActionDispatch # even if it was overridden by middleware. See #request_method for # more information. def method - @method ||= check_method(env["rack.methodoverride.original_method"] || env['REQUEST_METHOD']) + @method ||= check_method(get_header("rack.methodoverride.original_method") || get_header('REQUEST_METHOD')) end # Returns a symbol form of the #method @@ -170,7 +192,7 @@ module ActionDispatch # # request.headers["Content-Type"] # => "text/plain" def headers - @headers ||= Http::Headers.new(@env) + @headers ||= Http::Headers.new(self) end # Returns a +String+ with the last requested path including their params. @@ -181,7 +203,7 @@ module ActionDispatch # # get '/foo?bar' # request.original_fullpath # => '/foo?bar' def original_fullpath - @original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath) + @original_fullpath ||= (get_header("ORIGINAL_FULLPATH") || fullpath) end # Returns the +String+ full path including params of the last URL requested. @@ -220,7 +242,7 @@ module ActionDispatch # (case-insensitive), which may need to be manually added depending on the # choice of JavaScript libraries and frameworks. def xml_http_request? - @env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i + get_header('HTTP_X_REQUESTED_WITH') =~ /XMLHttpRequest/i end alias :xhr? :xml_http_request? @@ -232,11 +254,11 @@ module ActionDispatch # Returns the IP address of client as a +String+, # usually set by the RemoteIp middleware. def remote_ip - @remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s + @remote_ip ||= (get_header("action_dispatch.remote_ip") || ip).to_s end def remote_ip=(remote_ip) - @env["action_dispatch.remote_ip".freeze] = remote_ip + set_header "action_dispatch.remote_ip".freeze, remote_ip end ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # :nodoc: @@ -248,43 +270,39 @@ module ActionDispatch # This unique ID is useful for tracing a request from end-to-end as part of logging or debugging. # This relies on the rack variable set by the ActionDispatch::RequestId middleware. def request_id - env[ACTION_DISPATCH_REQUEST_ID] + get_header ACTION_DISPATCH_REQUEST_ID end def request_id=(id) # :nodoc: - env[ACTION_DISPATCH_REQUEST_ID] = id + set_header ACTION_DISPATCH_REQUEST_ID, id end alias_method :uuid, :request_id - def x_request_id # :nodoc: - @env[HTTP_X_REQUEST_ID] - end - # Returns the lowercase name of the HTTP server software. def server_software - (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil + (get_header('SERVER_SOFTWARE') && /^([a-zA-Z]+)/ =~ get_header('SERVER_SOFTWARE')) ? $1.downcase : nil end # Read the request \body. This is useful for web services that need to # work with raw requests directly. def raw_post - unless @env.include? 'RAW_POST_DATA' + unless has_header? 'RAW_POST_DATA' raw_post_body = body - @env['RAW_POST_DATA'] = raw_post_body.read(content_length) + set_header('RAW_POST_DATA', raw_post_body.read(content_length)) raw_post_body.rewind if raw_post_body.respond_to?(:rewind) end - @env['RAW_POST_DATA'] + get_header 'RAW_POST_DATA' end # The request body is an IO input stream. If the RAW_POST_DATA environment # variable is already set, wrap it in a StringIO. def body - if raw_post = @env['RAW_POST_DATA'] + if raw_post = get_header('RAW_POST_DATA') raw_post.force_encoding(Encoding::BINARY) StringIO.new(raw_post) else - @env['rack.input'] + body_stream end end @@ -295,7 +313,7 @@ module ActionDispatch end def body_stream #:nodoc: - @env['rack.input'] + get_header('rack.input') end # TODO This should be broken apart into AD::Request::Session and probably @@ -306,53 +324,67 @@ module ActionDispatch else self.session = {} end - @env['action_dispatch.request.flash_hash'] = nil + self.flash = nil end def session=(session) #:nodoc: - Session.set @env, session + Session.set self, session end def session_options=(options) - Session::Options.set @env, options + Session::Options.set self, options end # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= normalize_encode_params(super || {}) + fetch_header("action_dispatch.request.query_parameters") do |k| + set_header k, Request::Utils.normalize_encode_params(super || {}) + end rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e - raise ActionController::BadRequest.new(:query, e) + raise ActionController::BadRequest.new("Invalid query parameters: #{e.message}", e) end alias :query_parameters :GET # Override Rack's POST method to support indifferent access def POST - @env["action_dispatch.request.request_parameters"] ||= normalize_encode_params(super || {}) + fetch_header("action_dispatch.request.request_parameters") do + pr = parse_formatted_parameters(params_parsers) do |params| + super || {} + end + self.request_parameters = Request::Utils.normalize_encode_params(pr) + end + rescue ParamsParser::ParseError # one of the parse strategies blew up + self.request_parameters = Request::Utils.normalize_encode_params(super || {}) + raise rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e - raise ActionController::BadRequest.new(:request, e) + raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}", e) end alias :request_parameters :POST # Returns the authorization header regardless of whether it was specified directly or through one of the # proxy alternatives. def authorization - @env['HTTP_AUTHORIZATION'] || - @env['X-HTTP_AUTHORIZATION'] || - @env['X_HTTP_AUTHORIZATION'] || - @env['REDIRECT_X_HTTP_AUTHORIZATION'] + get_header('HTTP_AUTHORIZATION') || + get_header('X-HTTP_AUTHORIZATION') || + get_header('X_HTTP_AUTHORIZATION') || + get_header('REDIRECT_X_HTTP_AUTHORIZATION') end - # True if the request came from localhost, 127.0.0.1. + # True if the request came from localhost, 127.0.0.1, or ::1. def local? LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip end def request_parameters=(params) - env["action_dispatch.request.request_parameters".freeze] = params + raise if params.nil? + set_header("action_dispatch.request.request_parameters".freeze, params) end def logger - env["action_dispatch.logger".freeze] + get_header("action_dispatch.logger".freeze) + end + + def commit_flash end private diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index fd92e89231..c54efb6541 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -32,14 +32,35 @@ module ActionDispatch # :nodoc: # end # end class Response + class Header < DelegateClass(Hash) # :nodoc: + def initialize(response, header) + @response = response + super(header) + end + + def []=(k,v) + if @response.sending? || @response.sent? + raise ActionDispatch::IllegalStateError, 'header already sent' + end + + super + end + + def merge(other) + self.class.new @response, __getobj__.merge(other) + end + + def to_hash + __getobj__.dup + end + end + # The request that the response is responding to. attr_accessor :request # The HTTP status code. attr_reader :status - attr_writer :sending_file - # Get headers for this response. attr_reader :header @@ -48,29 +69,19 @@ module ActionDispatch # :nodoc: delegate :[], :[]=, :to => :@header delegate :each, :to => :@stream - # Sets the HTTP response's content MIME type. For example, in the controller - # you could write this: - # - # response.content_type = "text/plain" - # - # If a character set has been defined for this response (see charset=) then - # the character set information will also be included in the content type - # information. - attr_reader :content_type - - # The charset of the response. HTML wants to know the encoding of the - # content you're giving them, so we need to send that along. - attr_reader :charset - CONTENT_TYPE = "Content-Type".freeze SET_COOKIE = "Set-Cookie".freeze LOCATION = "Location".freeze - NO_CONTENT_CODES = [204, 304] + NO_CONTENT_CODES = [100, 101, 102, 204, 205, 304] cattr_accessor(:default_charset) { "utf-8" } cattr_accessor(:default_headers) include Rack::Response::Helpers + # Aliasing these off because AD::Http::Cache::Response defines them + alias :_cache_control :cache_control + alias :_cache_control= :cache_control= + include ActionDispatch::Http::FilterRedirect include ActionDispatch::Http::Cache::Response include MonitorMixin @@ -119,37 +130,41 @@ module ActionDispatch # :nodoc: end end + def self.create(status = 200, header = {}, body = [], default_headers: self.default_headers) + header = merge_default_headers(header, default_headers) + new status, header, body + end + + def self.merge_default_headers(original, default) + default.respond_to?(:merge) ? default.merge(original) : original + end + # The underlying body, as a streamable object. attr_reader :stream - def initialize(status = 200, header = {}, body = [], default_headers: self.class.default_headers) + def initialize(status = 200, header = {}, body = []) super() - header = merge_default_headers(header, default_headers) - @header = header + @header = Header.new(self, header) self.body, self.status = body, status - @sending_file = false @blank = false @cv = new_cond @committed = false @sending = false @sent = false - @content_type = nil - @charset = self.class.default_charset - - if content_type = self[CONTENT_TYPE] - type, charset = content_type.split(/;\s*charset=/) - @content_type = Mime::Type.lookup(type) - @charset = charset || self.class.default_charset - end prepare_cache_control! yield self if block_given? end + def has_header?(key); headers.key? key; end + def get_header(key); headers[key]; end + def set_header(key, v); headers[key] = v; end + def delete_header(key); headers.delete key; end + def await_commit synchronize do @cv.wait_until { @committed } @@ -194,7 +209,27 @@ module ActionDispatch # :nodoc: # Sets the HTTP content type. def content_type=(content_type) - @content_type = content_type.to_s + header_info = parse_content_type + set_content_type content_type.to_s, header_info.charset || self.class.default_charset + end + + # Sets the HTTP response's content MIME type. For example, in the controller + # you could write this: + # + # response.content_type = "text/plain" + # + # If a character set has been defined for this response (see charset=) then + # the character set information will also be included in the content type + # information. + + def content_type + parse_content_type.mime_type + end + + def sending_file=(v) + if true == v + self.charset = false + end end # Sets the HTTP character set. In case of nil parameter @@ -203,7 +238,20 @@ module ActionDispatch # :nodoc: # response.charset = 'utf-16' # => 'utf-16' # response.charset = nil # => 'utf-8' def charset=(charset) - @charset = charset.nil? ? self.class.default_charset : charset + header_info = parse_content_type + if false == charset + set_header CONTENT_TYPE, header_info.mime_type + else + content_type = header_info.mime_type + set_content_type content_type, charset || self.class.default_charset + end + end + + # The charset of the response. HTML wants to know the encoding of the + # content you're giving them, so we need to send that along. + def charset + header_info = parse_content_type + header_info.charset || self.class.default_charset end # The response code of the request. @@ -235,6 +283,10 @@ module ActionDispatch # :nodoc: @stream.body end + def write(string) + @stream.write string + end + EMPTY = " " # Allows you to manually set or override the response body. @@ -250,31 +302,49 @@ module ActionDispatch # :nodoc: end end - def body_parts - parts = [] - @stream.each { |x| parts << x } - parts - end + # Avoid having to pass an open file handle as the response body. + # Rack::Sendfile will usually intercept the response and uses + # the path directly, so there is no reason to open the file. + class FileBody #:nodoc: + attr_reader :to_path + + def initialize(path) + @to_path = path + end + + def body + File.binread(to_path) + end - def set_cookie(key, value) - ::Rack::Utils.set_cookie_header!(header, key, value) + # Stream the file's contents if Rack::Sendfile isn't present. + def each + File.open(to_path, 'rb') do |file| + while chunk = file.read(16384) + yield chunk + end + end + end end - def delete_cookie(key, value={}) - ::Rack::Utils.delete_cookie_header!(header, key, value) + # Send the file stored at +path+ as the response body. + def send_file(path) + commit! + @stream = FileBody.new(path) end - # The location header we'll be responding with. - def location - headers[LOCATION] + def reset_body! + @stream = build_buffer(self, []) end - alias_method :redirect_url, :location - # Sets the location header we'll be responding with. - def location=(url) - headers[LOCATION] = url + def body_parts + parts = [] + @stream.each { |x| parts << x } + parts end + # The location header we'll be responding with. + alias_method :redirect_url, :location + def close stream.close if stream.respond_to?(:close) end @@ -305,7 +375,7 @@ module ActionDispatch # :nodoc: # assert_equal 'AuthorOfNewPage', r.cookies['author'] def cookies cookies = {} - if header = self[SET_COOKIE] + if header = get_header(SET_COOKIE) header = header.split("\n") if header.respond_to?(:to_str) header.each do |cookie| if pair = cookie.split(';').first @@ -319,19 +389,36 @@ module ActionDispatch # :nodoc: private + ContentTypeHeader = Struct.new :mime_type, :charset + NullContentTypeHeader = ContentTypeHeader.new nil, nil + + def parse_content_type + content_type = get_header CONTENT_TYPE + if content_type + type, charset = content_type.split(/;\s*charset=/) + type = nil if type.empty? + ContentTypeHeader.new(type, charset) + else + NullContentTypeHeader + end + end + + def set_content_type(content_type, charset) + type = (content_type || '').dup + type << "; charset=#{charset}" if charset + set_header CONTENT_TYPE, type + end + def before_committed return if committed? assign_default_content_type_and_charset! handle_conditional_get! + handle_no_content! end def before_sending end - def merge_default_headers(original, default) - default.respond_to?(:merge) ? default.merge(original) : original - end - def build_buffer(response, body) Buffer.new response, body end @@ -341,18 +428,11 @@ module ActionDispatch # :nodoc: end def assign_default_content_type_and_charset! - return if self[CONTENT_TYPE].present? - - @content_type ||= Mime::HTML - - type = @content_type.to_s.dup - type << "; charset=#{charset}" if append_charset? - - self[CONTENT_TYPE] = type - end + return if content_type - def append_charset? - !@sending_file && @charset != false + ct = parse_content_type + set_content_type(ct.mime_type || Mime[:html].to_s, + ct.charset || self.class.default_charset) end class RackBody @@ -391,11 +471,15 @@ module ActionDispatch # :nodoc: end end - def rack_response(status, header) - header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join) - + def handle_no_content! if NO_CONTENT_CODES.include?(@status) - header.delete CONTENT_TYPE + @header.delete CONTENT_TYPE + @header.delete 'Content-Length' + end + end + + def rack_response(status, header) + if NO_CONTENT_CODES.include?(status) [status, header, []] else [status, header, RackBody.new(self)] diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 6fcf49030b..92b10b6d3b 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -1,11 +1,10 @@ require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/hash/slice' module ActionDispatch module Http module URL IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ - HOST_REGEXP = /(^[^:]+:\/\/)?([^:]+)(?::(\d+$))?/ + HOST_REGEXP = /(^[^:]+:\/\/)?(\[[^\]]+\]|[^:]+)(?::(\d+$))?/ PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/ mattr_accessor :tld_length @@ -184,7 +183,7 @@ module ActionDispatch end end - def initialize(env) + def initialize super @protocol = nil @port = nil @@ -229,10 +228,10 @@ module ActionDispatch # req = Request.new 'HTTP_HOST' => 'example.com:8080' # req.raw_host_with_port # => "example.com:8080" def raw_host_with_port - if forwarded = env["HTTP_X_FORWARDED_HOST"].presence + if forwarded = x_forwarded_host.presence forwarded.split(/,\s?/).last else - env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}" + get_header('HTTP_HOST') || "#{server_name || server_addr}:#{get_header('SERVER_PORT')}" end end @@ -348,7 +347,7 @@ module ActionDispatch end def server_port - @env['SERVER_PORT'].to_i + get_header('SERVER_PORT').to_i end # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify |