5 require 'active_support/memoizable'
7 module ActionController
8 # CgiRequest and TestRequest provide concrete implementations.
10 extend ActiveSupport
::Memoizable
12 def self.relative_url_root
=(relative_url_root
)
13 ActiveSupport
::Deprecation.warn(
14 "ActionController::AbstractRequest.relative_url_root= has been renamed." +
15 "You can now set it with config.action_controller.relative_url_root=", caller
)
16 ActionController
::Base.relative_url_root
=relative_url_root
19 HTTP_METHODS
= %w(get head put post delete options
)
20 HTTP_METHOD_LOOKUP
= HTTP_METHODS
.inject({}) { |h
, m
| h
[m
] = h
[m
.upcase
] = m
.to_sym
; h
}
22 # The hash of environment variables for this request,
23 # such as { 'RAILS_ENV' => 'production' }.
26 # The true HTTP request \method as a lowercase symbol, such as <tt>:get</tt>.
27 # UnknownHttpMethod is raised for invalid methods not listed in ACCEPTED_HTTP_METHODS.
29 method
= @env['REQUEST_METHOD']
30 method
= parameters
[:_method] if method
== 'POST' && !parameters
[:_method].blank
?
32 HTTP_METHOD_LOOKUP
[method
] || raise(UnknownHttpMethod
, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}")
34 memoize
:request_method
36 # The HTTP request \method as a lowercase symbol, such as <tt>:get</tt>.
37 # Note, HEAD is returned as <tt>:get</tt> since the two are functionally
38 # equivalent from the application's perspective.
40 request_method
== :head ? :get : request_method
43 # Is this a GET (or HEAD) request? Equivalent to <tt>request.method == :get</tt>.
48 # Is this a POST request? Equivalent to <tt>request.method == :post</tt>.
50 request_method
== :post
53 # Is this a PUT request? Equivalent to <tt>request.method == :put</tt>.
55 request_method
== :put
58 # Is this a DELETE request? Equivalent to <tt>request.method == :delete</tt>.
60 request_method
== :delete
63 # Is this a HEAD request? Since <tt>request.method</tt> sees HEAD as <tt>:get</tt>,
64 # this \method checks the actual HTTP \method directly.
66 request_method
== :head
69 # Provides access to the request's HTTP headers, for example:
71 # request.headers["Content-Type"] # => "text/plain"
73 ActionController
::Http::Headers.new(@env)
77 # Returns the content length of the request as an integer.
79 @env['CONTENT_LENGTH'].to_i
81 memoize
:content_length
83 # The MIME type of the HTTP request, such as Mime::XML.
85 # For backward compatibility, the post \format is extracted from the
86 # X-Post-Data-Format HTTP header if present.
88 Mime
::Type.lookup(content_type_without_parameters
)
92 # Returns the accepted MIME type for the request.
94 header
= @env['HTTP_ACCEPT'].to_s
.strip
97 [content_type
, Mime
::ALL].compact
99 Mime
::Type.parse(header
)
104 def if_modified_since
105 if since
= env['HTTP_IF_MODIFIED_SINCE']
106 Time
.rfc2822(since
) rescue nil
109 memoize
:if_modified_since
112 env['HTTP_IF_NONE_MATCH']
115 def not_modified
?(modified_at
)
116 if_modified_since
&& modified_at
&& if_modified_since
>= modified_at
119 def etag_matches
?(etag
)
120 if_none_match
&& if_none_match
== etag
123 # Check response freshness (Last-Modified and ETag) against request
124 # If-Modified-Since and If-None-Match conditions. If both headers are
125 # supplied, both must match, or the request is not considered fresh.
128 when if_modified_since
&& if_none_match
129 not_modified
?(response
.last_modified
) && etag_matches
?(response
.etag
)
130 when if_modified_since
131 not_modified
?(response
.last_modified
)
133 etag_matches
?(response
.etag
)
139 # Returns the Mime type for the \format used in the request.
141 # GET /posts/5.xml | request.format => Mime::XML
142 # GET /posts/5.xhtml | request.format => Mime::HTML
143 # 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>
146 if parameters
[:format]
147 Mime
::Type.lookup_by_extension(parameters
[:format])
148 elsif ActionController
::Base.use_accept_header
151 Mime
::Type.lookup_by_extension("js")
153 Mime
::Type.lookup_by_extension("html")
158 # Sets the \format by string extension, which can be used to force custom formats
159 # that are not controlled by the extension.
161 # class ApplicationController < ActionController::Base
162 # before_filter :adjust_format_for_iphone
165 # def adjust_format_for_iphone
166 # request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/]
169 def format
=(extension
)
170 parameters
[:format] = extension
.to_s
171 @format = Mime
::Type.lookup_by_extension(parameters
[:format])
174 # Returns a symbolized version of the <tt>:format</tt> parameter of the request.
175 # If no \format is given it returns <tt>:js</tt>for Ajax requests and <tt>:html</tt>
178 parameter_format
= parameters
[:format]
193 # Returns true if the request's "X-Requested-With" header contains
194 # "XMLHttpRequest". (The Prototype Javascript library sends this header with
195 # every Ajax request.)
196 def xml_http_request
?
197 !(@env['HTTP_X_REQUESTED_WITH'] !~
/XMLHttpRequest/i
)
199 alias xhr
? :xml_http_request?
201 # Which IP addresses are "trusted proxies" that can be stripped from
202 # the right-hand-side of X-Forwarded-For
203 TRUSTED_PROXIES
= /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i
205 # Determines originating IP address. REMOTE_ADDR is the standard
206 # but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or
207 # HTTP_X_FORWARDED_FOR are set by proxies so check for these if
208 # REMOTE_ADDR is a proxy. HTTP_X_FORWARDED_FOR may be a comma-
209 # delimited list in the case of multiple chained proxies; the last
210 # address which is not trusted is the originating IP.
212 remote_addr_list
= @env['REMOTE_ADDR'] && @env['REMOTE_ADDR'].split(',').collect(&:strip)
214 unless remote_addr_list
.blank
?
215 not_trusted_addrs
= remote_addr_list
.reject
{|addr
| addr
=~ TRUSTED_PROXIES
}
216 return not_trusted_addrs
.first
unless not_trusted_addrs
.empty
?
218 remote_ips
= @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',')
220 if @env.include? 'HTTP_CLIENT_IP'
221 if remote_ips
&& !remote_ips
.include?(@env['HTTP_CLIENT_IP'])
222 # We don't know which came from the proxy, and which from the user
223 raise ActionControllerError
.new(<<EOM)
225 HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}
226 HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}
230 return @env['HTTP_CLIENT_IP']
234 while remote_ips
.size
> 1 && TRUSTED_PROXIES
=~ remote_ips
.last
.strip
238 return remote_ips
.last
.strip
245 # Returns the lowercase name of the HTTP server software.
247 (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~
@env['SERVER_SOFTWARE']) ? $1.downcase
: nil
249 memoize
:server_software
252 # Returns the complete URL used for this request.
254 protocol
+ host_with_port
+ request_uri
258 # Returns 'https://' if this is an SSL request and 'http://' otherwise.
260 ssl
? ? 'https://' : 'http://'
264 # Is this an SSL request?
266 @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
269 # Returns the \host for this request, such as "example.com".
270 def raw_host_with_port
271 if forwarded
= env["HTTP_X_FORWARDED_HOST"]
272 forwarded
.split(/,\s?/).last
274 env['HTTP_HOST'] || env['SERVER_NAME'] || "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
278 # Returns the host for this request, such as example.com.
280 raw_host_with_port
.sub(/:\d+$/, '')
284 # Returns a \host:\port string for this request, such as "example.com" or
285 # "example.com:8080".
287 "#{host}#{port_string}"
289 memoize
:host_with_port
291 # Returns the port number of this request as an integer.
293 if raw_host_with_port
=~
/:(\d+)$/
301 # Returns the standard \port number for this request's protocol.
304 when 'https://' then 443
309 # Returns a \port suffix like ":8080" if the \port number of this request
310 # is not the default HTTP \port 80 or HTTPS \port 443.
312 port
== standard_port
? '' : ":#{port}"
315 # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
316 # a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
317 def domain(tld_length
= 1)
318 return nil unless named_host
?(host
)
320 host
.split('.').last(1 + tld_length
).join('.')
323 # Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be
324 # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
325 # such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt>
326 # in "www.rubyonrails.co.uk".
327 def subdomains(tld_length
= 1)
328 return [] unless named_host
?(host
)
329 parts
= host
.split('.')
330 parts
[0..-(tld_length
+2)]
333 # Returns the query string, accounting for server idiosyncrasies.
335 if uri
= @env['REQUEST_URI']
336 uri
.split('?', 2)[1] || ''
338 @env['QUERY_STRING'] || ''
341 memoize
:query_string
343 # Returns the request URI, accounting for server idiosyncrasies.
344 # WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
346 if uri
= @env['REQUEST_URI']
347 # Remove domain, which webrick puts into the request_uri.
348 (%r
{^\w
+\
://[^
/]+(/.*|$
)$
} =~ uri
) ? $1 : uri
350 # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO.
351 uri
= @env['PATH_INFO'].to_s
353 if script_filename
= @env['SCRIPT_NAME'].to_s
.match(%r
{[^
/]+$
})
354 uri
= uri
.sub(/#{script_filename}\//, '')
357 env_qs
= @env['QUERY_STRING'].to_s
358 uri
+= "?#{env_qs}" unless env_qs
.empty
?
361 @env.delete('REQUEST_URI')
363 @env['REQUEST_URI'] = uri
369 # Returns the interpreted \path to requested resource after all the installation
370 # directory of this application was taken into account.
372 path
= (uri
= request_uri
) ? uri
.split('?').first
.to_s
: ''
374 # Cut off the path to the installation directory if given
375 path
.sub
!(%r
/^#{ActionController::Base.relative_url_root}/, '')
380 # Read the request \body. This is useful for web services that need to
381 # work with raw requests directly.
383 unless env.include? 'RAW_POST_DATA'
384 env['RAW_POST_DATA'] = body
.read(content_length
)
385 body
.rewind
if body
.respond_to
?(:rewind)
390 # Returns both GET and POST \parameters in a single hash.
392 @parameters ||= request_parameters
.merge(query_parameters
).update(path_parameters
).with_indifferent_access
395 def path_parameters
=(parameters
) #:nodoc:
396 @path_parameters = parameters
397 @symbolized_path_parameters = @parameters = nil
400 # The same as <tt>path_parameters</tt> with explicitly symbolized keys.
401 def symbolized_path_parameters
402 @symbolized_path_parameters ||= path_parameters
.symbolize_keys
405 # Returns a hash with the \parameters used to form the \path of the request.
406 # Returned hash keys are strings:
408 # {'action' => 'my_action', 'controller' => 'my_controller'}
410 # See <tt>symbolized_path_parameters</tt> for symbolized keys.
412 @path_parameters ||= {}
415 # The request body is an IO input stream. If the RAW_POST_DATA environment
416 # variable is already set, wrap it in a StringIO.
418 if raw_post
= env['RAW_POST_DATA']
419 raw_post
.force_encoding(Encoding
::BINARY) if raw_post
.respond_to
?(:force_encoding)
420 StringIO
.new(raw_post
)
433 alias referer referrer
437 @query_parameters ||= self.class.parse_query_parameters(query_string
)
440 def request_parameters
441 @request_parameters ||= parse_formatted_request_parameters
446 # Must be implemented in the concrete request
449 def body_stream
#:nodoc:
458 def session
=(session
) #:nodoc:
462 def reset_session
#:nodoc:
466 # The raw content type string. Use when you need parameters such as
467 # charset or boundary which aren't included in the content_type MIME type.
468 # Overridden by the X-POST_DATA_FORMAT header for backward compatibility.
469 def content_type_with_parameters
470 content_type_from_legacy_post_data_format_header
||
471 env['CONTENT_TYPE'].to_s
474 # The raw content type string with its parameters stripped off.
475 def content_type_without_parameters
476 self.class.extract_content_type_without_parameters(content_type_with_parameters
)
478 memoize
:content_type_without_parameters
481 def content_type_from_legacy_post_data_format_header
482 if x_post_format
= @env['HTTP_X_POST_DATA_FORMAT']
483 case x_post_format
.to_s
.downcase
484 when 'yaml'; 'application/x-yaml'
485 when 'xml'; 'application/xml'
490 def parse_formatted_request_parameters
491 return {} if content_length
.zero
?
493 content_type
, boundary
= self.class.extract_multipart_boundary(content_type_with_parameters
)
495 # Don't parse params for unknown requests.
496 return {} if content_type
.blank
?
498 mime_type
= Mime
::Type.lookup(content_type
)
499 strategy
= ActionController
::Base.param_parsers
[mime_type
]
501 # Only multipart form parsing expects a stream.
502 body
= (strategy
&& strategy
!= :multipart_form) ? raw_post
: self.body
507 when :url_encoded_form
508 self.class.clean_up_ajax_request_body
! body
509 self.class.parse_query_parameters(body
)
511 self.class.parse_multipart_form_parameters(body
, boundary
, content_length
, env)
512 when :xml_simple, :xml_node
513 body
.blank
? ? {} : Hash
.from_xml(body
).with_indifferent_access
520 data = ActiveSupport
::JSON.decode(body
)
521 data = {:_json => data} unless data.is_a
?(Hash
)
522 data.with_indifferent_access
527 rescue Exception
=> e
# YAML, XML or Ruby code block errors
530 "content_type" => content_type_with_parameters
,
531 "content_length" => content_length
,
532 "exception" => "#{e.message} (#{e.class})",
533 "backtrace" => e
.backtrace
}
536 def named_host
?(host
)
537 !(host
.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host
))
541 def parse_query_parameters(query_string
)
542 return {} if query_string
.blank
?
544 pairs
= query_string
.split('&').collect
do |chunk
|
546 key
, value
= chunk
.split('=', 2)
548 value
= value
.nil? ? nil : CGI
.unescape(value
)
549 [ CGI
.unescape(key
), value
]
552 UrlEncodedPairParser
.new(pairs
).result
555 def parse_request_parameters(params
)
556 parser
= UrlEncodedPairParser
.new
560 for key
, value
in params
563 elsif !key
.include?('[')
564 # much faster to test for the most common case first (GET)
565 # and avoid the call to build_deep_hash
566 parser
.result
[key
] = get_typed_value(value
[0])
568 elsif value
.is_a
?(Array
)
569 parser
.parse(key
, get_typed_value(value
.shift
))
570 params
.delete key
if value
.empty
?
572 raise TypeError
, "Expected array, found #{value.inspect}"
580 def parse_multipart_form_parameters(body
, boundary
, body_size
, env)
581 parse_request_parameters(read_multipart(body
, boundary
, body_size
, env))
584 def extract_multipart_boundary(content_type_with_parameters
)
585 if content_type_with_parameters
=~ MULTIPART_BOUNDARY
586 ['multipart/form-data', $1.dup
]
588 extract_content_type_without_parameters(content_type_with_parameters
)
592 def extract_content_type_without_parameters(content_type_with_parameters
)
593 $1.strip
.downcase
if content_type_with_parameters
=~
/^([^,\;]*)/
596 def clean_up_ajax_request_body
!(body
)
597 body
.chop
! if body
[-1] == 0
598 body
.gsub
!(/&_=$/, '')
603 def get_typed_value(value
)
610 value
.map
{ |v
| get_typed_value(v
) }
612 if value
.respond_to
? :original_filename
614 if value
.original_filename
622 # Unknown value, neither string nor multipart.
624 raise "Unknown form value: #{value.inspect}"
629 MULTIPART_BOUNDARY
= %r
|\Amultipart
/form-data
.*boundary
=\"?([^
\";,]+)\"?|n
633 def read_multipart(body
, boundary
, body_size
, env)
634 params
= Hash
.new([])
635 boundary
= "--" + boundary
636 quoted_boundary
= Regexp
.quote(boundary
)
641 # start multipart/form-data
642 body
.binmode
if defined? body
.binmode
645 body
.set_encoding(Encoding
::BINARY) if body
.respond_to
?(:set_encoding)
647 body
.string
.force_encoding(Encoding
::BINARY) if body
.string
.respond_to
?(:force_encoding)
649 boundary_size
= boundary
.size
+ EOL
.size
650 body_size
-= boundary_size
651 status
= body
.read(boundary_size
)
653 raise EOFError
, "no content body"
654 elsif boundary
+ EOL
!= status
655 raise EOFError
, "bad content body"
662 UploadedTempfile
.new("CGI")
666 content
.binmode
if defined? content
.binmode
668 until head
and /#{quoted_boundary}(?:#{EOL}|--)/n
.match(buf
)
670 if (not head
) and /#{EOL}#{EOL}/n
.match(buf
)
671 buf
= buf
.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n
) do
678 if head
and ( (EOL
+ boundary
+ EOL
).size
< buf
.size
)
679 content
.print buf
[0 ... (buf
.size
- (EOL
+ boundary
+ EOL
).size
)]
680 buf
[0 ... (buf
.size
- (EOL
+ boundary
+ EOL
).size
)] = ""
683 c
= if bufsize
< body_size
688 if c
.nil? || c
.empty
?
689 raise EOFError
, "bad content body"
695 buf
= buf
.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n
) do
700 boundary_end
= $2.dup
706 head
=~
/Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;]*))/ni
707 if filename
= $1 || $2
708 if /Mac/ni
.match(env['HTTP_USER_AGENT']) and
709 /Mozilla/ni
.match(env['HTTP_USER_AGENT']) and
710 (not /MSIE/ni
.match(env['HTTP_USER_AGENT']))
711 filename
= CGI
.unescape(filename
)
713 content
.original_path
= filename
.dup
716 head
=~
/Content-Type: ([^\r]*)/ni
717 content
.content_type
= $1.dup
if $1
719 head
=~
/Content-Disposition:.* name="?([^\";]*)"?/ni
722 if params
.has_key
?(name
)
723 params
[name
].push(content
)
725 params
[name
] = [content
]
727 break if body_size
== -1
729 raise EOFError
, "bad boundary end of body part" unless boundary_end
=~
/--/
732 body
.rewind
if body
.respond_to
?(:rewind)
734 # Handles exceptions raised by input streams that cannot be rewound
735 # such as when using plain CGI under Apache
743 class UrlEncodedPairParser
< StringScanner
#:nodoc:
744 attr_reader
:top, :parent, :result
746 def initialize(pairs
= [])
749 pairs
.each
{ |key
, value
| parse(key
, value
) }
752 KEY_REGEXP
= %r
{([^\
[\
]=&]+)}
753 BRACKETED_KEY_REGEXP
= %r
{\
[([^\
[\
]=&]+)\
]}
755 # Parse the query string
756 def parse(key
, value
)
758 @top, @parent = result
, nil
760 # First scan the bare key
761 key
= scan(KEY_REGEXP
) or return
762 key
= post_key_check(key
)
764 # Then scan as many nestings as present
766 r
= scan(BRACKETED_KEY_REGEXP
) or return
768 key
= post_key_check(key
)
775 # After we see a key, we must look ahead to determine our next action. Cases:
777 # [] follows the key. Then the value must be an array.
778 # = follows the key. (A value comes next)
779 # & or the end of string follows the key. Then the key is a flag.
780 # otherwise, a hash follows the key.
781 def post_key_check(key
)
782 if scan(/\[\]/) # a[b][] indicates that b is an array
783 container(key
, Array
)
785 elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
788 else # End of key? We do nothing.
793 # Add a container to the stack.
794 def container(key
, klass
)
795 type_conflict
! klass
, top
[key
] if top
.is_a
?(Hash
) && top
.key
?(key
) && ! top
[key
].is_a
?(klass
)
796 value
= bind(key
, klass
.new
)
797 type_conflict
! klass
, value
unless value
.is_a
?(klass
)
801 # Push a value onto the 'stack', which is actually only the top 2 items.
803 @parent, @top = @top, value
806 # Bind a key (which may be nil for items in an array) to the provided value.
810 if top
[-1].is_a
?(Hash
) && ! top
[-1].key
?(key
)
813 top
<< {key
=> value
}.with_indifferent_access
821 key
= CGI
.unescape(key
)
822 parent
<< (@top = {}) if top
.key
?(key
) && parent
.is_a
?(Array
)
826 raise ArgumentError
, "Don't know what to do: top is #{top.inspect}"
832 def type_conflict
!(klass
, value
)
833 raise TypeError
, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)"
838 def self.included(base
)
840 attr_accessor
:original_path, :content_type
841 alias_method
:local_path, :path
845 # Take the basename of the upload's original filename.
846 # This handles the full Windows paths given by Internet Explorer
847 # (and perhaps other broken user agents) without affecting
848 # those which give the lone filename.
849 # The Windows regexp is adapted from Perl's File::Basename.
850 def original_filename
851 unless defined? @original_filename
853 unless original_path
.blank
?
854 if original_path
=~
/^(?:.*[:\\\/])?(.*)/m
857 File
.basename original_path
865 class UploadedStringIO
< StringIO
869 class UploadedTempfile
< Tempfile