1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/actionpack/lib/action_dispatch/http/request.rb
Yehuda Katz 4bf516e072 More perf work:
* Move #set_cookie and #delete_cookie inline to optimize. These optimizations should
    almost certainly be sent back upstream to Rack. The optimization involves using
    an ivar for cookies instead of indexing into the headers each time.
  * Was able to use a bare Hash for headers now that cookies have their own joining
    semantics (some code assumed that the raw cookies were an Array).
  * Cache blankness of body on body=
  * Improve expand_cache_key for Arrays of a single element (common in our case)
  * Use a simple layout condition check unless conditions are used
  * Cache visible actions
  * Lazily load the UrlRewriter
  * Make etag an ivar that is set on prepare!
2009-08-11 15:03:53 -07:00

554 lines
17 KiB
Ruby
Executable file

require 'tempfile'
require 'stringio'
require 'strscan'
require 'active_support/memoizable'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/object/tap'
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(:locale => :en)}")
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
@env["action_dispatch.request.content_type"] ||= begin
if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/
Mime::Type.lookup($1.strip.downcase)
else
nil
end
end
end
def media_type
content_type.to_s
end
# Returns the accepted MIME type for the request.
def accepts
@env["action_dispatch.request.accepts"] ||= begin
header = @env['HTTP_ACCEPT'].to_s.strip
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
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 = [])
@env["action_dispatch.request.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
if ActionController::Base.use_accept_header
if param = parameters[:format]
Array.wrap(Mime[param])
else
accepts.dup
end
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
@env["action_dispatch.request.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 || addr =~ ActionController::Base.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 || ActionController::Base.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
def server_port
@env['SERVER_PORT'].to_i
end
# Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
# a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
def domain(tld_length = 1)
return nil unless named_host?(host)
host.split('.').last(1 + tld_length).join('.')
end
# Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be
# returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
# such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt>
# in "www.rubyonrails.co.uk".
def subdomains(tld_length = 1)
return [] unless named_host?(host)
parts = host.split('.')
parts[0..-(tld_length+2)]
end
# Returns the query string, accounting for server idiosyncrasies.
def query_string
@env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].to_s.split('?', 2)[1] || '')
end
# Returns the request URI, accounting for server idiosyncrasies.
# WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
def request_uri
if uri = @env['REQUEST_URI']
# Remove domain, which webrick puts into the request_uri.
(%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri
else
# Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO.
uri = @env['PATH_INFO'].to_s
if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
uri = uri.sub(/#{script_filename}\//, '')
end
env_qs = @env['QUERY_STRING'].to_s
uri += "?#{env_qs}" unless env_qs.empty?
if uri.blank?
@env.delete('REQUEST_URI')
else
@env['REQUEST_URI'] = uri
end
end
end
# Returns the interpreted \path to requested resource after all the installation
# directory of this application was taken into account.
def path
path = request_uri.to_s[/\A[^\?]*/]
path.sub!(/\A#{ActionController::Base.relative_url_root}/, '')
path
end
# Read the request \body. This is useful for web services that need to
# work with raw requests directly.
def raw_post
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
@env["action_dispatch.request.parameters"] ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access
end
alias_method :params, :parameters
def path_parameters=(parameters) #:nodoc:
@env.delete("action_dispatch.request.symbolized_path_parameters")
@env.delete("action_dispatch.request.parameters")
@env["action_dispatch.request.path_parameters"] = parameters
end
# The same as <tt>path_parameters</tt> with explicitly symbolized keys.
def symbolized_path_parameters
@env["action_dispatch.request.symbolized_path_parameters"] ||= path_parameters.symbolize_keys
end
# Returns a hash with the \parameters used to form the \path of the request.
# Returned hash keys are strings:
#
# {'action' => 'my_action', 'controller' => 'my_controller'}
#
# See <tt>symbolized_path_parameters</tt> for symbolized keys.
def path_parameters
@env["action_dispatch.request.path_parameters"] ||= {}
end
# The request body is an IO input stream. If the RAW_POST_DATA environment
# variable is already set, wrap it in a StringIO.
def body
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 indifferent access
def GET
@env["action_dispatch.request.query_parameters"] ||= normalize_parameters(super)
end
alias_method :query_parameters, :GET
# Override Rack's POST method to support indifferent access
def POST
@env["action_dispatch.request.request_parameters"] ||= normalize_parameters(super)
end
alias_method :request_parameters, :POST
def body_stream #:nodoc:
@env['rack.input']
end
def reset_session
self.session_options.delete(:id)
self.session = {}
end
def session=(session) #:nodoc:
@env['rack.session'] = session
end
def session_options=(options)
@env['rack.session.options'] = options
end
def flash
session['flash'] || {}
end
# Receives an array of mimes and return the first user sent mime that
# matches the order array.
#
def negotiate_mime(order)
formats.each do |priority|
if priority == Mime::ALL
return order.first
elsif order.include?(priority)
return priority
end
end
order.include?(Mime::ALL) ? formats.first : nil
end
private
def named_host?(host)
!(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
end
module UploadedFile
def self.extended(object)
object.class_eval do
attr_accessor :original_path, :content_type
alias_method :local_path, :path
end
end
# Take the basename of the upload's original filename.
# This handles the full Windows paths given by Internet Explorer
# (and perhaps other broken user agents) without affecting
# those which give the lone filename.
# The Windows regexp is adapted from Perl's File::Basename.
def original_filename
unless defined? @original_filename
@original_filename =
unless original_path.blank?
if original_path =~ /^(?:.*[:\\\/])?(.*)/m
$1
else
File.basename original_path
end
end
end
@original_filename
end
end
# Convert nested Hashs to HashWithIndifferentAccess and replace
# file upload hashs with UploadedFile objects
def normalize_parameters(value)
case value
when Hash
if value.has_key?(:tempfile)
upload = value[:tempfile]
upload.extend(UploadedFile)
upload.original_path = value[:filename]
upload.content_type = value[:type]
upload
else
h = {}
value.each { |k, v| h[k] = normalize_parameters(v) }
h.with_indifferent_access
end
when Array
value.map { |e| normalize_parameters(e) }
else
value
end
end
end
end