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 # :get. 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 :post. (Unlike #request_method, this # method returns :get 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 request.method == :get. def get? method == :get end # Is this a POST request? Equivalent to request.method == :post. def post? request_method == :post end # Is this a PUT request? Equivalent to request.method == :put. def put? request_method == :put end # Is this a DELETE request? Equivalent to request.method == :delete. def delete? request_method == :delete end # Is this a HEAD request? Since request.method sees HEAD as :get, # 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 if header.empty? [content_type] else Mime::Type.parse(header) end end end def if_modified_since if since = env['HTTP_IF_MODIFIED_SINCE'] Time.rfc2822(since) rescue nil end end def if_none_match env['HTTP_IF_NONE_MATCH'] end def not_modified?(modified_at) if_modified_since && modified_at && if_modified_since >= modified_at end def etag_matches?(etag) if_none_match && if_none_match == etag end # Check response freshness (Last-Modified and ETag) against request # If-Modified-Since and If-None-Match conditions. If both headers are # supplied, both must match, or the request is not considered fresh. def fresh?(response) 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 ActionController::Base.use_accept_header # def format(view_path = []) formats.first end def formats accept = @env['HTTP_ACCEPT'] @env["action_dispatch.request.formats"] ||= if parameters[:format] [Mime[parameters[:format]]] elsif xhr? || (accept && !accept.include?(?,)) accepts else [Mime::HTML] end end # Sets the \format by string extension, which can be used to force custom formats # that are not controlled by the extension. # # class ApplicationController < ActionController::Base # before_filter :adjust_format_for_iphone # # private # def adjust_format_for_iphone # request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/] # end # end def format=(extension) parameters[:format] = extension.to_s @env["action_dispatch.request.formats"] = [Mime::Type.lookup_by_extension(parameters[:format])] end # Returns a symbolized version of the :format parameter of the request. # If no \format is given it returns :jsfor Ajax requests and :html # 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(< 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 tld_length, 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 ["dev", "www"] would be # returned for "dev.www.rubyonrails.org". You can specify a different tld_length, # such as 2 to catch ["www"] instead of ["www", "rubyonrails"] # 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 path_parameters 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 symbolized_path_parameters 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