aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/http
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_dispatch/http')
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb33
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb214
-rw-r--r--actionpack/lib/action_dispatch/http/mime_types.rb21
-rwxr-xr-xactionpack/lib/action_dispatch/http/request.rb479
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb255
-rw-r--r--actionpack/lib/action_dispatch/http/status_codes.rb88
6 files changed, 1090 insertions, 0 deletions
diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb
new file mode 100644
index 0000000000..2a41b4dbad
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -0,0 +1,33 @@
+require 'active_support/memoizable'
+
+module ActionDispatch
+ module Http
+ class Headers < ::Hash
+ extend ActiveSupport::Memoizable
+
+ def initialize(*args)
+ if args.size == 1 && args[0].is_a?(Hash)
+ super()
+ update(args[0])
+ else
+ super
+ end
+ end
+
+ def [](header_name)
+ if include?(header_name)
+ super
+ else
+ super(env_name(header_name))
+ end
+ end
+
+ private
+ # Converts a HTTP header name to an environment variable name.
+ def env_name(header_name)
+ "HTTP_#{header_name.upcase.gsub(/-/, '_')}"
+ end
+ memoize :env_name
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
new file mode 100644
index 0000000000..0a7b1001c8
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -0,0 +1,214 @@
+require 'set'
+
+module Mime
+ SET = []
+ EXTENSION_LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? }
+ LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? }
+
+ def self.[](type)
+ Type.lookup_by_extension(type.to_s)
+ end
+
+ # Encapsulates the notion of a mime type. Can be used at render time, for example, with:
+ #
+ # class PostsController < ActionController::Base
+ # def show
+ # @post = Post.find(params[:id])
+ #
+ # respond_to do |format|
+ # format.html
+ # format.ics { render :text => post.to_ics, :mime_type => Mime::Type["text/calendar"] }
+ # format.xml { render :xml => @people.to_xml }
+ # end
+ # end
+ # end
+ class Type
+ @@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]
+ cattr_reader :browser_generated_types
+ attr_reader :symbol
+
+ @@unverifiable_types = Set.new [:text, :json, :csv, :xml, :rss, :atom, :yaml]
+ def self.unverifiable_types
+ ActiveSupport::Deprecation.warn("unverifiable_types is deprecated and has no effect", caller)
+ @@unverifiable_types
+ end
+
+ # A simple helper class used in parsing the accept header
+ class AcceptItem #:nodoc:
+ attr_accessor :order, :name, :q
+
+ def initialize(order, name, q=nil)
+ @order = order
+ @name = name.strip
+ q ||= 0.0 if @name == Mime::ALL # default wilcard match to end of list
+ @q = ((q || 1.0).to_f * 100).to_i
+ end
+
+ def to_s
+ @name
+ end
+
+ def <=>(item)
+ result = item.q <=> q
+ result = order <=> item.order if result == 0
+ result
+ end
+
+ def ==(item)
+ name == (item.respond_to?(:name) ? item.name : item)
+ end
+ end
+
+ class << self
+ def lookup(string)
+ LOOKUP[string]
+ end
+
+ def lookup_by_extension(extension)
+ EXTENSION_LOOKUP[extension]
+ end
+
+ # Registers an alias that's not used on mime type lookup, but can be referenced directly. Especially useful for
+ # rendering different HTML versions depending on the user agent, like an iPhone.
+ def register_alias(string, symbol, extension_synonyms = [])
+ register(string, symbol, [], extension_synonyms, true)
+ end
+
+ def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false)
+ Mime.instance_eval { const_set symbol.to_s.upcase, Type.new(string, symbol, mime_type_synonyms) }
+
+ SET << Mime.const_get(symbol.to_s.upcase)
+
+ ([string] + mime_type_synonyms).each { |string| LOOKUP[string] = SET.last } unless skip_lookup
+ ([symbol.to_s] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext] = SET.last }
+ end
+
+ def parse(accept_header)
+ if accept_header !~ /,/
+ [Mime::Type.lookup(accept_header)]
+ else
+ # keep track of creation order to keep the subsequent sort stable
+ list = []
+ accept_header.split(/,/).each_with_index do |header, index|
+ params, q = header.split(/;\s*q=/)
+ if params
+ params.strip!
+ list << AcceptItem.new(index, params, q) unless params.empty?
+ end
+ end
+ list.sort!
+
+ # Take care of the broken text/xml entry by renaming or deleting it
+ text_xml = list.index("text/xml")
+ app_xml = list.index(Mime::XML.to_s)
+
+ if text_xml && app_xml
+ # set the q value to the max of the two
+ list[app_xml].q = [list[text_xml].q, list[app_xml].q].max
+
+ # make sure app_xml is ahead of text_xml in the list
+ if app_xml > text_xml
+ list[app_xml], list[text_xml] = list[text_xml], list[app_xml]
+ app_xml, text_xml = text_xml, app_xml
+ end
+
+ # delete text_xml from the list
+ list.delete_at(text_xml)
+
+ elsif text_xml
+ list[text_xml].name = Mime::XML.to_s
+ end
+
+ # Look for more specific XML-based types and sort them ahead of app/xml
+
+ if app_xml
+ idx = app_xml
+ app_xml_type = list[app_xml]
+
+ while(idx < list.length)
+ type = list[idx]
+ break if type.q < app_xml_type.q
+ if type.name =~ /\+xml$/
+ list[app_xml], list[idx] = list[idx], list[app_xml]
+ app_xml = idx
+ end
+ idx += 1
+ end
+ end
+
+ list.map! { |i| Mime::Type.lookup(i.name) }.uniq!
+ list
+ end
+ end
+ end
+
+ def initialize(string, symbol = nil, synonyms = [])
+ @symbol, @synonyms = symbol, synonyms
+ @string = string
+ end
+
+ def to_s
+ @string
+ end
+
+ def to_str
+ to_s
+ end
+
+ def to_sym
+ @symbol || @string.to_sym
+ end
+
+ def ===(list)
+ if list.is_a?(Array)
+ (@synonyms + [ self ]).any? { |synonym| list.include?(synonym) }
+ else
+ super
+ end
+ end
+
+ def ==(mime_type)
+ return false if mime_type.blank?
+ (@synonyms + [ self ]).any? do |synonym|
+ require "ruby-debug"
+ debugger if mime_type.is_a?(Array)
+ 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?
+ regexp = Regexp.new(Regexp.quote(mime_type.to_s))
+ (@synonyms + [ self ]).any? do |synonym|
+ synonym.to_s =~ regexp
+ end
+ end
+
+ # Returns true if Action Pack should check requests using this Mime Type for possible request forgery. See
+ # ActionController::RequestForgeryProtection.
+ def verify_request?
+ @@browser_generated_types.include?(to_sym)
+ end
+
+ def html?
+ @@html_types.include?(to_sym) || @string =~ /html/
+ end
+
+ private
+ def method_missing(method, *args)
+ if method.to_s =~ /(\w+)\?$/
+ $1.downcase.to_sym == to_sym
+ else
+ super
+ end
+ end
+ end
+end
+
+require 'action_dispatch/http/mime_types'
diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb
new file mode 100644
index 0000000000..2d7fba1173
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/mime_types.rb
@@ -0,0 +1,21 @@
+# Build list of Mime types for HTTP responses
+# http://www.iana.org/assignments/media-types/
+
+Mime::Type.register "*/*", :all
+Mime::Type.register "text/plain", :text, [], %w(txt)
+Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml )
+Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript )
+Mime::Type.register "text/css", :css
+Mime::Type.register "text/calendar", :ics
+Mime::Type.register "text/csv", :csv
+Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml )
+Mime::Type.register "application/rss+xml", :rss
+Mime::Type.register "application/atom+xml", :atom
+Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml )
+
+Mime::Type.register "multipart/form-data", :multipart_form
+Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form
+
+# http://www.ietf.org/rfc/rfc4627.txt
+# http://www.json.org/JSONRequest.html
+Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest ) \ No newline at end of file
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
new file mode 100755
index 0000000000..366ac26421
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -0,0 +1,479 @@
+require 'tempfile'
+require 'stringio'
+require 'strscan'
+
+require 'active_support/memoizable'
+require 'action_controller/cgi_ext'
+
+module ActionDispatch
+ class Request < Rack::Request
+
+ %w[ AUTH_TYPE GATEWAY_INTERFACE
+ PATH_TRANSLATED REMOTE_HOST
+ REMOTE_IDENT REMOTE_USER REMOTE_ADDR
+ SERVER_NAME SERVER_PROTOCOL
+
+ HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
+ HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
+ HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env|
+ define_method(env.sub(/^HTTP_/n, '').downcase) do
+ @env[env]
+ end
+ end
+
+ def key?(key)
+ @env.key?(key)
+ end
+
+ HTTP_METHODS = %w(get head put post delete options)
+ HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h }
+
+ # Returns the true HTTP request \method as a lowercase symbol, such as
+ # <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}")
+ end
+
+ # Returns the HTTP request \method used for action processing as a
+ # lowercase symbol, such as <tt>:post</tt>. (Unlike #request_method, this
+ # 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
+ end
+
+ # Is this a GET (or HEAD) request? Equivalent to <tt>request.method == :get</tt>.
+ def get?
+ method == :get
+ end
+
+ # Is this a POST request? Equivalent to <tt>request.method == :post</tt>.
+ def post?
+ request_method == :post
+ end
+
+ # Is this a PUT request? Equivalent to <tt>request.method == :put</tt>.
+ def put?
+ request_method == :put
+ end
+
+ # Is this a DELETE request? Equivalent to <tt>request.method == :delete</tt>.
+ def delete?
+ request_method == :delete
+ end
+
+ # Is this a HEAD request? Since <tt>request.method</tt> sees HEAD as <tt>:get</tt>,
+ # this \method checks the actual HTTP \method directly.
+ def head?
+ request_method == :head
+ end
+
+ # Provides access to the request's HTTP headers, for example:
+ #
+ # request.headers["Content-Type"] # => "text/plain"
+ def headers
+ 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
+ @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
+ @accepts ||= begin
+ header = @env['HTTP_ACCEPT'].to_s.strip
+
+ fallback = xhr? ? Mime::JS : Mime::HTML
+
+ if header.empty?
+ [content_type, fallback, Mime::ALL].compact
+ else
+ ret = Mime::Type.parse(header)
+ if ret.last == Mime::ALL
+ ret.insert(-2, fallback)
+ end
+ ret
+ 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)
+ case
+ when if_modified_since && if_none_match
+ not_modified?(response.last_modified) && etag_matches?(response.etag)
+ when if_modified_since
+ not_modified?(response.last_modified)
+ when if_none_match
+ etag_matches?(response.etag)
+ else
+ false
+ end
+ end
+
+ ONLY_ALL = [Mime::ALL].freeze
+
+ # 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 = [])
+ @format ||=
+ if parameters[:format]
+ Mime[parameters[:format]]
+ elsif ActionController::Base.use_accept_header && !(accepts == ONLY_ALL)
+ accepts.first
+ elsif xhr? then Mime::JS
+ else Mime::HTML
+ end
+ end
+
+ def formats
+ @formats =
+ if ActionController::Base.use_accept_header
+ Array(Mime[parameters[:format]] || accepts)
+ else
+ [format]
+ 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
+ @format = 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
+
+ def cache_format
+ parameters[:format]
+ end
+
+ # Returns true if the request's "X-Requested-With" header contains
+ # "XMLHttpRequest". (The Prototype Javascript library sends this header with
+ # every Ajax request.)
+ def xml_http_request?
+ !(@env['HTTP_X_REQUESTED_WITH'] !~ /XMLHttpRequest/i)
+ end
+ alias xhr? :xml_http_request?
+
+ # Which IP addresses are "trusted proxies" that can be stripped from
+ # the right-hand-side of X-Forwarded-For
+ TRUSTED_PROXIES = /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i
+
+ # Determines originating IP address. REMOTE_ADDR is the standard
+ # but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or
+ # HTTP_X_FORWARDED_FOR are set by proxies so check for these if
+ # REMOTE_ADDR is a proxy. HTTP_X_FORWARDED_FOR may be a comma-
+ # delimited list in the case of multiple chained proxies; the last
+ # address which is not trusted is the originating IP.
+ def remote_ip
+ remote_addr_list = @env['REMOTE_ADDR'] && @env['REMOTE_ADDR'].scan(/[^,\s]+/)
+
+ unless remote_addr_list.blank?
+ not_trusted_addrs = remote_addr_list.reject {|addr| addr =~ TRUSTED_PROXIES}
+ return not_trusted_addrs.first unless not_trusted_addrs.empty?
+ end
+ remote_ips = @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',')
+
+ 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)
+IP spoofing attack?!
+HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}
+HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}
+EOM
+ end
+
+ return @env['HTTP_CLIENT_IP']
+ end
+
+ if remote_ips
+ while remote_ips.size > 1 && TRUSTED_PROXIES =~ remote_ips.last.strip
+ remote_ips.pop
+ end
+
+ return remote_ips.last.strip
+ end
+
+ @env['REMOTE_ADDR']
+ 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
+ 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
+
+ # 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'].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
+ unless @env.include? 'RAW_POST_DATA'
+ @env['RAW_POST_DATA'] = body.read(@env['CONTENT_LENGTH'].to_i)
+ body.rewind if body.respond_to?(:rewind)
+ end
+ @env['RAW_POST_DATA']
+ end
+
+ # Returns both GET and POST \parameters in a single hash.
+ def parameters
+ @parameters ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access
+ end
+ alias_method :params, :parameters
+
+ def path_parameters=(parameters) #:nodoc:
+ @env["rack.routing_args"] = parameters
+ @symbolized_path_parameters = @parameters = nil
+ end
+
+ # The same as <tt>path_parameters</tt> with explicitly symbolized keys.
+ def symbolized_path_parameters
+ @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["rack.routing_args"] ||= {}
+ 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']
+ raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding)
+ StringIO.new(raw_post)
+ else
+ @env['rack.input']
+ end
+ end
+
+ def form_data?
+ FORM_DATA_MEDIA_TYPES.include?(content_type.to_s)
+ end
+
+ # Override Rack's GET method to support nested query strings
+ def GET
+ @env["action_controller.request.query_parameters"] ||= UrlEncodedPairParser.parse_query_parameters(query_string)
+ end
+ alias_method :query_parameters, :GET
+
+ # Override Rack's POST method to support nested query strings
+ def POST
+ @env["action_controller.request.request_parameters"] ||= UrlEncodedPairParser.parse_hash_parameters(super)
+ end
+ alias_method :request_parameters, :POST
+
+ def body_stream #:nodoc:
+ @env['rack.input']
+ end
+
+ def session
+ @env['rack.session'] ||= {}
+ end
+
+ def session=(session) #:nodoc:
+ @env['rack.session'] = session
+ end
+
+ def reset_session
+ @env['rack.session'] = {}
+ end
+
+ def session_options
+ @env['rack.session.options'] ||= {}
+ end
+
+ def session_options=(options)
+ @env['rack.session.options'] = options
+ end
+
+ def server_port
+ @env['SERVER_PORT'].to_i
+ 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
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
new file mode 100644
index 0000000000..e1d8ee3527
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -0,0 +1,255 @@
+require 'digest/md5'
+
+module ActionDispatch # :nodoc:
+ # Represents an HTTP response generated by a controller action. One can use
+ # an ActionController::Response object to retrieve the current state
+ # of the response, or customize the response. An Response object can
+ # either represent a "real" HTTP response (i.e. one that is meant to be sent
+ # back to the web browser) or a test response (i.e. one that is generated
+ # from integration tests). See CgiResponse and TestResponse, respectively.
+ #
+ # Response is mostly a Ruby on Rails framework implement detail, and
+ # should never be used directly in controllers. Controllers should use the
+ # methods defined in ActionController::Base instead. For example, if you want
+ # to set the HTTP response's content MIME type, then use
+ # ActionControllerBase#headers instead of Response#headers.
+ #
+ # Nevertheless, integration tests may want to inspect controller responses in
+ # more detail, and that's when Response can be useful for application
+ # developers. Integration test methods such as
+ # ActionController::Integration::Session#get and
+ # ActionController::Integration::Session#post return objects of type
+ # TestResponse (which are of course also of type Response).
+ #
+ # For example, the following demo integration "test" prints the body of the
+ # controller response to the console:
+ #
+ # class DemoControllerTest < ActionController::IntegrationTest
+ # def test_print_root_path_to_console
+ # get('/')
+ # puts @response.body
+ # end
+ # end
+ class Response < Rack::Response
+ DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
+ attr_accessor :request
+
+ attr_accessor :session, :assigns, :template, :layout
+ attr_accessor :redirected_to, :redirected_to_method_params
+
+ delegate :default_charset, :to => 'ActionController::Base'
+
+ def initialize
+ @status = 200
+ @header = DEFAULT_HEADERS.dup
+
+ @writer = lambda { |x| @body << x }
+ @block = nil
+
+ @body = "",
+ @session, @assigns = [], []
+ end
+
+ def location; headers['Location'] end
+ def location=(url) headers['Location'] = url 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=(mime_type)
+ self.headers["Content-Type"] =
+ if mime_type =~ /charset/ || (c = charset).nil?
+ mime_type.to_s
+ else
+ "#{mime_type}; charset=#{c}"
+ end
+ end
+
+ # Returns the response's content MIME type, or nil if content type has been set.
+ def content_type
+ content_type = String(headers["Content-Type"] || headers["type"]).split(";")[0]
+ content_type.blank? ? nil : content_type
+ end
+
+ # Set the charset of the Content-Type header. Set to nil to remove it.
+ # If no content type is set, it defaults to HTML.
+ def charset=(charset)
+ headers["Content-Type"] =
+ if charset
+ "#{content_type || Mime::HTML}; charset=#{charset}"
+ else
+ content_type || Mime::HTML.to_s
+ end
+ end
+
+ def charset
+ charset = String(headers["Content-Type"] || headers["type"]).split(";")[1]
+ charset.blank? ? nil : charset.strip.split("=")[1]
+ 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
+ headers['ETag']
+ end
+
+ def etag?
+ headers.include?('ETag')
+ end
+
+ def etag=(etag)
+ if etag.blank?
+ headers.delete('ETag')
+ else
+ headers['ETag'] = %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))}")
+ end
+ end
+
+ def redirect(url, status)
+ self.status = status
+ self.location = url.gsub(/[\r\n]/, '')
+ self.body = "<html><body>You are being <a href=\"#{CGI.escapeHTML(url)}\">redirected</a>.</body></html>"
+ end
+
+ def sending_file?
+ headers["Content-Transfer-Encoding"] == "binary"
+ end
+
+ def assign_default_content_type_and_charset!
+ self.content_type ||= Mime::HTML
+ self.charset ||= default_charset unless sending_file?
+ end
+
+ def prepare!
+ assign_default_content_type_and_charset!
+ handle_conditional_get!
+ set_content_length!
+ convert_content_type!
+ convert_language!
+ convert_expires!
+ convert_cookies!
+ end
+
+ def each(&callback)
+ if @body.respond_to?(:call)
+ @writer = lambda { |x| callback.call(x) }
+ @body.call(self, self)
+ elsif @body.is_a?(String)
+ @body.each_line(&callback)
+ else
+ @body.each(&callback)
+ end
+
+ @writer = callback
+ @block.call(self) if @block
+ end
+
+ def write(str)
+ @writer.call str.to_s
+ str
+ end
+
+ # Over Rack::Response#set_cookie to add HttpOnly option
+ def set_cookie(key, value)
+ case value
+ when Hash
+ domain = "; domain=" + value[:domain] if value[:domain]
+ path = "; path=" + value[:path] if value[:path]
+ # According to RFC 2109, we need dashes here.
+ # N.B.: cgi.rb uses spaces...
+ expires = "; expires=" + value[:expires].clone.gmtime.
+ strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
+ secure = "; secure" if value[:secure]
+ httponly = "; HttpOnly" if value[:http_only]
+ value = value[:value]
+ end
+ value = [value] unless Array === value
+ cookie = ::Rack::Utils.escape(key) + "=" +
+ value.map { |v| ::Rack::Utils.escape v }.join("&") +
+ "#{domain}#{path}#{expires}#{secure}#{httponly}"
+
+ case self["Set-Cookie"]
+ when Array
+ self["Set-Cookie"] << cookie
+ when String
+ self["Set-Cookie"] = [self["Set-Cookie"], cookie]
+ when nil
+ self["Set-Cookie"] = cookie
+ end
+ end
+
+ private
+ def handle_conditional_get!
+ if etag? || last_modified?
+ set_conditional_cache_control!
+ elsif nonempty_ok_response?
+ self.etag = body
+
+ if request && request.etag_matches?(etag)
+ self.status = '304 Not Modified'
+ self.body = ''
+ end
+
+ set_conditional_cache_control!
+ end
+ end
+
+ def nonempty_ok_response?
+ ok = !status || status.to_s[0..2] == '200'
+ ok && body.is_a?(String) && !body.empty?
+ end
+
+ def set_conditional_cache_control!
+ if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control']
+ headers['Cache-Control'] = 'private, max-age=0, must-revalidate'
+ end
+ end
+
+ def convert_content_type!
+ headers['Content-Type'] ||= "text/html"
+ headers['Content-Type'] += "; charset=" + headers.delete('charset') if headers['charset']
+ end
+
+ # Don't set the Content-Length for block-based bodies as that would mean
+ # reading it all into memory. Not nice for, say, a 2GB streaming file.
+ def set_content_length!
+ if status && status.to_s[0..2] == '204'
+ headers.delete('Content-Length')
+ elsif length = headers['Content-Length']
+ headers['Content-Length'] = length.to_s
+ elsif !body.respond_to?(:call) && (!status || status.to_s[0..2] != '304')
+ headers["Content-Length"] = body.size.to_s
+ end
+ end
+
+ def convert_language!
+ headers["Content-Language"] = headers.delete("language") if headers["language"]
+ end
+
+ def convert_expires!
+ headers["Expires"] = headers.delete("") if headers["expires"]
+ end
+
+ def convert_cookies!
+ headers['Set-Cookie'] = Array(headers['Set-Cookie']).compact
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/status_codes.rb b/actionpack/lib/action_dispatch/http/status_codes.rb
new file mode 100644
index 0000000000..cec9d2e3a1
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/status_codes.rb
@@ -0,0 +1,88 @@
+module ActionDispatch
+ module StatusCodes #:nodoc:
+ # Defines the standard HTTP status codes, by integer, with their
+ # corresponding default message texts.
+ # Source: http://www.iana.org/assignments/http-status-codes
+ STATUS_CODES = {
+ 100 => "Continue",
+ 101 => "Switching Protocols",
+ 102 => "Processing",
+
+ 200 => "OK",
+ 201 => "Created",
+ 202 => "Accepted",
+ 203 => "Non-Authoritative Information",
+ 204 => "No Content",
+ 205 => "Reset Content",
+ 206 => "Partial Content",
+ 207 => "Multi-Status",
+ 226 => "IM Used",
+
+ 300 => "Multiple Choices",
+ 301 => "Moved Permanently",
+ 302 => "Found",
+ 303 => "See Other",
+ 304 => "Not Modified",
+ 305 => "Use Proxy",
+ 307 => "Temporary Redirect",
+
+ 400 => "Bad Request",
+ 401 => "Unauthorized",
+ 402 => "Payment Required",
+ 403 => "Forbidden",
+ 404 => "Not Found",
+ 405 => "Method Not Allowed",
+ 406 => "Not Acceptable",
+ 407 => "Proxy Authentication Required",
+ 408 => "Request Timeout",
+ 409 => "Conflict",
+ 410 => "Gone",
+ 411 => "Length Required",
+ 412 => "Precondition Failed",
+ 413 => "Request Entity Too Large",
+ 414 => "Request-URI Too Long",
+ 415 => "Unsupported Media Type",
+ 416 => "Requested Range Not Satisfiable",
+ 417 => "Expectation Failed",
+ 422 => "Unprocessable Entity",
+ 423 => "Locked",
+ 424 => "Failed Dependency",
+ 426 => "Upgrade Required",
+
+ 500 => "Internal Server Error",
+ 501 => "Not Implemented",
+ 502 => "Bad Gateway",
+ 503 => "Service Unavailable",
+ 504 => "Gateway Timeout",
+ 505 => "HTTP Version Not Supported",
+ 507 => "Insufficient Storage",
+ 510 => "Not Extended"
+ }
+
+ # Provides a symbol-to-fixnum lookup for converting a symbol (like
+ # :created or :not_implemented) into its corresponding HTTP status
+ # code (like 200 or 501).
+ SYMBOL_TO_STATUS_CODE = STATUS_CODES.inject({}) do |hash, (code, message)|
+ hash[message.gsub(/ /, "").underscore.to_sym] = code
+ hash
+ end
+
+ # Given a status parameter, determine whether it needs to be converted
+ # to a string. If it is a fixnum, use the STATUS_CODES hash to lookup
+ # the default message. If it is a symbol, use the SYMBOL_TO_STATUS_CODE
+ # hash to convert it.
+ def interpret_status(status)
+ case status
+ when Fixnum then
+ "#{status} #{STATUS_CODES[status]}".strip
+ when Symbol then
+ interpret_status(SYMBOL_TO_STATUS_CODE[status] ||
+ "500 Unknown Status #{status.inspect}")
+ else
+ status.to_s
+ end
+ end
+ private :interpret_status
+
+ end
+end \ No newline at end of file