Updated README.rdoc again
[feedcatcher.git] / vendor / rails / actionpack / lib / action_controller / request.rb
1 require 'tempfile'
2 require 'stringio'
3 require 'strscan'
4
5 require 'active_support/memoizable'
6 require 'action_controller/cgi_ext'
7
8 module ActionController
9 class Request < Rack::Request
10
11 %w[ AUTH_TYPE GATEWAY_INTERFACE
12 PATH_TRANSLATED REMOTE_HOST
13 REMOTE_IDENT REMOTE_USER REMOTE_ADDR
14 SERVER_NAME SERVER_PROTOCOL
15
16 HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
17 HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
18 HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env|
19 define_method(env.sub(/^HTTP_/n, '').downcase) do
20 @env[env]
21 end
22 end
23
24 def key?(key)
25 @env.key?(key)
26 end
27
28 HTTP_METHODS = %w(get head put post delete options)
29 HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h }
30
31 # Returns the true HTTP request \method as a lowercase symbol, such as
32 # <tt>:get</tt>. If the request \method is not listed in the HTTP_METHODS
33 # constant above, an UnknownHttpMethod exception is raised.
34 def request_method
35 @request_method ||= HTTP_METHOD_LOOKUP[super] || raise(UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
36 end
37
38 # Returns the HTTP request \method used for action processing as a
39 # lowercase symbol, such as <tt>:post</tt>. (Unlike #request_method, this
40 # method returns <tt>:get</tt> for a HEAD request because the two are
41 # functionally equivalent from the application's perspective.)
42 def method
43 request_method == :head ? :get : request_method
44 end
45
46 # Is this a GET (or HEAD) request? Equivalent to <tt>request.method == :get</tt>.
47 def get?
48 method == :get
49 end
50
51 # Is this a POST request? Equivalent to <tt>request.method == :post</tt>.
52 def post?
53 request_method == :post
54 end
55
56 # Is this a PUT request? Equivalent to <tt>request.method == :put</tt>.
57 def put?
58 request_method == :put
59 end
60
61 # Is this a DELETE request? Equivalent to <tt>request.method == :delete</tt>.
62 def delete?
63 request_method == :delete
64 end
65
66 # Is this a HEAD request? Since <tt>request.method</tt> sees HEAD as <tt>:get</tt>,
67 # this \method checks the actual HTTP \method directly.
68 def head?
69 request_method == :head
70 end
71
72 # Provides access to the request's HTTP headers, for example:
73 #
74 # request.headers["Content-Type"] # => "text/plain"
75 def headers
76 @headers ||= ActionController::Http::Headers.new(@env)
77 end
78
79 # Returns the content length of the request as an integer.
80 def content_length
81 super.to_i
82 end
83
84 # The MIME type of the HTTP request, such as Mime::XML.
85 #
86 # For backward compatibility, the post \format is extracted from the
87 # X-Post-Data-Format HTTP header if present.
88 def content_type
89 @content_type ||= begin
90 if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/
91 Mime::Type.lookup($1.strip.downcase)
92 else
93 nil
94 end
95 end
96 end
97
98 # Returns the accepted MIME type for the request.
99 def accepts
100 @accepts ||= begin
101 header = @env['HTTP_ACCEPT'].to_s.strip
102
103 if header.empty?
104 [content_type, Mime::ALL].compact
105 else
106 Mime::Type.parse(header)
107 end
108 end
109 end
110
111 def if_modified_since
112 if since = env['HTTP_IF_MODIFIED_SINCE']
113 Time.rfc2822(since) rescue nil
114 end
115 end
116
117 def if_none_match
118 env['HTTP_IF_NONE_MATCH']
119 end
120
121 def not_modified?(modified_at)
122 if_modified_since && modified_at && if_modified_since >= modified_at
123 end
124
125 def etag_matches?(etag)
126 if_none_match && if_none_match == etag
127 end
128
129 # Check response freshness (Last-Modified and ETag) against request
130 # If-Modified-Since and If-None-Match conditions. If both headers are
131 # supplied, both must match, or the request is not considered fresh.
132 def fresh?(response)
133 case
134 when if_modified_since && if_none_match
135 not_modified?(response.last_modified) && etag_matches?(response.etag)
136 when if_modified_since
137 not_modified?(response.last_modified)
138 when if_none_match
139 etag_matches?(response.etag)
140 else
141 false
142 end
143 end
144
145 # Returns the Mime type for the \format used in the request.
146 #
147 # GET /posts/5.xml | request.format => Mime::XML
148 # GET /posts/5.xhtml | request.format => Mime::HTML
149 # 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>
150 def format
151 @format ||=
152 if parameters[:format]
153 Mime::Type.lookup_by_extension(parameters[:format])
154 elsif ActionController::Base.use_accept_header
155 accepts.first
156 elsif xhr?
157 Mime::Type.lookup_by_extension("js")
158 else
159 Mime::Type.lookup_by_extension("html")
160 end
161 end
162
163
164 # Sets the \format by string extension, which can be used to force custom formats
165 # that are not controlled by the extension.
166 #
167 # class ApplicationController < ActionController::Base
168 # before_filter :adjust_format_for_iphone
169 #
170 # private
171 # def adjust_format_for_iphone
172 # request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/]
173 # end
174 # end
175 def format=(extension)
176 parameters[:format] = extension.to_s
177 @format = Mime::Type.lookup_by_extension(parameters[:format])
178 end
179
180 # Returns a symbolized version of the <tt>:format</tt> parameter of the request.
181 # If no \format is given it returns <tt>:js</tt>for Ajax requests and <tt>:html</tt>
182 # otherwise.
183 def template_format
184 parameter_format = parameters[:format]
185
186 if parameter_format
187 parameter_format
188 elsif xhr?
189 :js
190 else
191 :html
192 end
193 end
194
195 def cache_format
196 parameters[:format]
197 end
198
199 # Returns true if the request's "X-Requested-With" header contains
200 # "XMLHttpRequest". (The Prototype Javascript library sends this header with
201 # every Ajax request.)
202 def xml_http_request?
203 !(@env['HTTP_X_REQUESTED_WITH'] !~ /XMLHttpRequest/i)
204 end
205 alias xhr? :xml_http_request?
206
207 # Which IP addresses are "trusted proxies" that can be stripped from
208 # the right-hand-side of X-Forwarded-For
209 TRUSTED_PROXIES = /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i
210
211 # Determines originating IP address. REMOTE_ADDR is the standard
212 # but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or
213 # HTTP_X_FORWARDED_FOR are set by proxies so check for these if
214 # REMOTE_ADDR is a proxy. HTTP_X_FORWARDED_FOR may be a comma-
215 # delimited list in the case of multiple chained proxies; the last
216 # address which is not trusted is the originating IP.
217 def remote_ip
218 remote_addr_list = @env['REMOTE_ADDR'] && @env['REMOTE_ADDR'].scan(/[^,\s]+/)
219
220 unless remote_addr_list.blank?
221 not_trusted_addrs = remote_addr_list.reject {|addr| addr =~ TRUSTED_PROXIES}
222 return not_trusted_addrs.first unless not_trusted_addrs.empty?
223 end
224 remote_ips = @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',')
225
226 if @env.include? 'HTTP_CLIENT_IP'
227 if ActionController::Base.ip_spoofing_check && remote_ips && !remote_ips.include?(@env['HTTP_CLIENT_IP'])
228 # We don't know which came from the proxy, and which from the user
229 raise ActionControllerError.new(<<EOM)
230 IP spoofing attack?!
231 HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}
232 HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}
233 EOM
234 end
235
236 return @env['HTTP_CLIENT_IP']
237 end
238
239 if remote_ips
240 while remote_ips.size > 1 && TRUSTED_PROXIES =~ remote_ips.last.strip
241 remote_ips.pop
242 end
243
244 return remote_ips.last.strip
245 end
246
247 @env['REMOTE_ADDR']
248 end
249
250 # Returns the lowercase name of the HTTP server software.
251 def server_software
252 (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
253 end
254
255 # Returns the complete URL used for this request.
256 def url
257 protocol + host_with_port + request_uri
258 end
259
260 # Returns 'https://' if this is an SSL request and 'http://' otherwise.
261 def protocol
262 ssl? ? 'https://' : 'http://'
263 end
264
265 # Is this an SSL request?
266 def ssl?
267 @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
268 end
269
270 # Returns the \host for this request, such as "example.com".
271 def raw_host_with_port
272 if forwarded = env["HTTP_X_FORWARDED_HOST"]
273 forwarded.split(/,\s?/).last
274 else
275 env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
276 end
277 end
278
279 # Returns the host for this request, such as example.com.
280 def host
281 raw_host_with_port.sub(/:\d+$/, '')
282 end
283
284 # Returns a \host:\port string for this request, such as "example.com" or
285 # "example.com:8080".
286 def host_with_port
287 "#{host}#{port_string}"
288 end
289
290 # Returns the port number of this request as an integer.
291 def port
292 if raw_host_with_port =~ /:(\d+)$/
293 $1.to_i
294 else
295 standard_port
296 end
297 end
298
299 # Returns the standard \port number for this request's protocol.
300 def standard_port
301 case protocol
302 when 'https://' then 443
303 else 80
304 end
305 end
306
307 # Returns a \port suffix like ":8080" if the \port number of this request
308 # is not the default HTTP \port 80 or HTTPS \port 443.
309 def port_string
310 port == standard_port ? '' : ":#{port}"
311 end
312
313 # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
314 # a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
315 def domain(tld_length = 1)
316 return nil unless named_host?(host)
317
318 host.split('.').last(1 + tld_length).join('.')
319 end
320
321 # Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be
322 # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
323 # such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt>
324 # in "www.rubyonrails.co.uk".
325 def subdomains(tld_length = 1)
326 return [] unless named_host?(host)
327 parts = host.split('.')
328 parts[0..-(tld_length+2)]
329 end
330
331 # Returns the query string, accounting for server idiosyncrasies.
332 def query_string
333 @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '')
334 end
335
336 # Returns the request URI, accounting for server idiosyncrasies.
337 # WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
338 def request_uri
339 if uri = @env['REQUEST_URI']
340 # Remove domain, which webrick puts into the request_uri.
341 (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri
342 else
343 # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO.
344 uri = @env['PATH_INFO'].to_s
345
346 if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
347 uri = uri.sub(/#{script_filename}\//, '')
348 end
349
350 env_qs = @env['QUERY_STRING'].to_s
351 uri += "?#{env_qs}" unless env_qs.empty?
352
353 if uri.blank?
354 @env.delete('REQUEST_URI')
355 else
356 @env['REQUEST_URI'] = uri
357 end
358 end
359 end
360
361 # Returns the interpreted \path to requested resource after all the installation
362 # directory of this application was taken into account.
363 def path
364 path = request_uri.to_s[/\A[^\?]*/]
365 path.sub!(/\A#{ActionController::Base.relative_url_root}/, '')
366 path
367 end
368
369 # Read the request \body. This is useful for web services that need to
370 # work with raw requests directly.
371 def raw_post
372 unless @env.include? 'RAW_POST_DATA'
373 @env['RAW_POST_DATA'] = body.read(@env['CONTENT_LENGTH'].to_i)
374 body.rewind if body.respond_to?(:rewind)
375 end
376 @env['RAW_POST_DATA']
377 end
378
379 # Returns both GET and POST \parameters in a single hash.
380 def parameters
381 @parameters ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access
382 end
383 alias_method :params, :parameters
384
385 def path_parameters=(parameters) #:nodoc:
386 @env["rack.routing_args"] = parameters
387 @symbolized_path_parameters = @parameters = nil
388 end
389
390 # The same as <tt>path_parameters</tt> with explicitly symbolized keys.
391 def symbolized_path_parameters
392 @symbolized_path_parameters ||= path_parameters.symbolize_keys
393 end
394
395 # Returns a hash with the \parameters used to form the \path of the request.
396 # Returned hash keys are strings:
397 #
398 # {'action' => 'my_action', 'controller' => 'my_controller'}
399 #
400 # See <tt>symbolized_path_parameters</tt> for symbolized keys.
401 def path_parameters
402 @env["rack.routing_args"] ||= {}
403 end
404
405 # The request body is an IO input stream. If the RAW_POST_DATA environment
406 # variable is already set, wrap it in a StringIO.
407 def body
408 if raw_post = @env['RAW_POST_DATA']
409 raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding)
410 StringIO.new(raw_post)
411 else
412 @env['rack.input']
413 end
414 end
415
416 def form_data?
417 FORM_DATA_MEDIA_TYPES.include?(content_type.to_s)
418 end
419
420 # Override Rack's GET method to support indifferent access
421 def GET
422 @env["action_controller.request.query_parameters"] ||= normalize_parameters(super)
423 end
424 alias_method :query_parameters, :GET
425
426 # Override Rack's POST method to support indifferent access
427 def POST
428 @env["action_controller.request.request_parameters"] ||= normalize_parameters(super)
429 end
430 alias_method :request_parameters, :POST
431
432 def body_stream #:nodoc:
433 @env['rack.input']
434 end
435
436 def session
437 @env['rack.session'] ||= {}
438 end
439
440 def session=(session) #:nodoc:
441 @env['rack.session'] = session
442 end
443
444 def reset_session
445 @env['rack.session.options'].delete(:id)
446 @env['rack.session'] = {}
447 end
448
449 def session_options
450 @env['rack.session.options'] ||= {}
451 end
452
453 def session_options=(options)
454 @env['rack.session.options'] = options
455 end
456
457 def server_port
458 @env['SERVER_PORT'].to_i
459 end
460
461 private
462 def named_host?(host)
463 !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
464 end
465
466 # Convert nested Hashs to HashWithIndifferentAccess and replace
467 # file upload hashs with UploadedFile objects
468 def normalize_parameters(value)
469 case value
470 when Hash
471 if value.has_key?(:tempfile)
472 upload = value[:tempfile]
473 upload.extend(UploadedFile)
474 upload.original_path = value[:filename]
475 upload.content_type = value[:type]
476 upload
477 else
478 h = {}
479 value.each { |k, v| h[k] = normalize_parameters(v) }
480 h.with_indifferent_access
481 end
482 when Array
483 value.map { |e| normalize_parameters(e) }
484 else
485 value
486 end
487 end
488 end
489 end