Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/actionpack/lib/action_controller/request.rb

Revision 9242, 23.8 kB (checked in by rick, 6 months ago)

Automatically parse posted JSON content for Mime::JSON requests. [rick]

  • Property svn:executable set to *
Line 
1 require 'tempfile'
2 require 'stringio'
3 require 'strscan'
4
5 module ActionController
6   # HTTP methods which are accepted by default.
7   ACCEPTED_HTTP_METHODS = Set.new(%w( get head put post delete options ))
8
9   # CgiRequest and TestRequest provide concrete implementations.
10   class AbstractRequest
11     cattr_accessor :relative_url_root
12     remove_method :relative_url_root
13
14     # The hash of environment variables for this request,
15     # such as { 'RAILS_ENV' => 'production' }.
16     attr_reader :env
17
18     # The true HTTP request method as a lowercase symbol, such as :get.
19     # UnknownHttpMethod is raised for invalid methods not listed in ACCEPTED_HTTP_METHODS.
20     def request_method
21       @request_method ||= begin
22         method = ((@env['REQUEST_METHOD'] == 'POST' && !parameters[:_method].blank?) ? parameters[:_method].to_s : @env['REQUEST_METHOD']).downcase
23         if ACCEPTED_HTTP_METHODS.include?(method)
24           method.to_sym
25         else
26           raise UnknownHttpMethod, "#{method}, accepted HTTP methods are #{ACCEPTED_HTTP_METHODS.to_a.to_sentence}"
27         end
28       end
29     end
30
31     # The HTTP request method as a lowercase symbol, such as :get.
32     # Note, HEAD is returned as :get since the two are functionally
33     # equivalent from the application's perspective.
34     def method
35       request_method == :head ? :get : request_method
36     end
37
38     # Is this a GET (or HEAD) request?  Equivalent to request.method == :get
39     def get?
40       method == :get
41     end
42
43     # Is this a POST request?  Equivalent to request.method == :post
44     def post?
45       request_method == :post
46     end
47
48     # Is this a PUT request?  Equivalent to request.method == :put
49     def put?
50       request_method == :put
51     end
52
53     # Is this a DELETE request?  Equivalent to request.method == :delete
54     def delete?
55       request_method == :delete
56     end
57
58     # Is this a HEAD request? request.method sees HEAD as :get, so check the
59     # HTTP method directly.
60     def head?
61       request_method == :head
62     end
63
64     # Provides acccess to the request's HTTP headers, for example:
65     #  request.headers["Content-Type"] # => "text/plain"
66     def headers
67       @headers ||= ActionController::Http::Headers.new(@env)
68     end
69
70     def content_length
71       @content_length ||= env['CONTENT_LENGTH'].to_i
72     end
73
74     # The MIME type of the HTTP request, such as Mime::XML.
75     #
76     # For backward compatibility, the post format is extracted from the
77     # X-Post-Data-Format HTTP header if present.
78     def content_type
79       @content_type ||= Mime::Type.lookup(content_type_without_parameters)
80     end
81
82     # Returns the accepted MIME type for the request
83     def accepts
84       @accepts ||=
85         if @env['HTTP_ACCEPT'].to_s.strip.empty?
86           [ content_type, Mime::ALL ].compact # make sure content_type being nil is not included
87         else
88           Mime::Type.parse(@env['HTTP_ACCEPT'])
89         end
90     end
91
92     # Returns the Mime type for the format used in the request. If there is no format available, the first of the
93     # accept types will be used. Examples:
94     #
95     #   GET /posts/5.xml   | request.format => Mime::XML
96     #   GET /posts/5.xhtml | request.format => Mime::HTML
97     #   GET /posts/5       | request.format => request.accepts.first (usually Mime::HTML for browsers)
98     def format
99       @format ||= parameters[:format] ? Mime::Type.lookup_by_extension(parameters[:format]) : accepts.first
100     end
101    
102    
103     # Sets the format by string extension, which can be used to force custom formats that are not controlled by the extension.
104     # Example:
105     #
106     #   class ApplicationController < ActionController::Base
107     #     before_filter :adjust_format_for_iphone
108     #   
109     #     private
110     #       def adjust_format_for_iphone
111     #         request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/]
112     #       end
113     #   end
114     def format=(extension)
115       parameters[:format] = extension.to_s
116       @format = Mime::Type.lookup_by_extension(parameters[:format])
117     end
118
119     # Returns true if the request's "X-Requested-With" header contains
120     # "XMLHttpRequest". (The Prototype Javascript library sends this header with
121     # every Ajax request.)
122     def xml_http_request?
123       !(@env['HTTP_X_REQUESTED_WITH'] !~ /XMLHttpRequest/i)
124     end
125     alias xhr? :xml_http_request?
126
127     # Which IP addresses are "trusted proxies" that can be stripped from
128     # the right-hand-side of X-Forwarded-For
129     TRUSTED_PROXIES = /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i
130
131     # Determine originating IP address.  REMOTE_ADDR is the standard
132     # but will fail if the user is behind a proxy.  HTTP_CLIENT_IP and/or
133     # HTTP_X_FORWARDED_FOR are set by proxies so check for these if
134     # REMOTE_ADDR is a proxy.  HTTP_X_FORWARDED_FOR may be a comma-
135     # delimited list in the case of multiple chained proxies; the last
136     # address which is not trusted is the originating IP.
137
138     def remote_ip
139       if TRUSTED_PROXIES !~ @env['REMOTE_ADDR']
140         return @env['REMOTE_ADDR']
141       end
142
143       if @env.include? 'HTTP_CLIENT_IP'
144         if @env.include? 'HTTP_X_FORWARDED_FOR'
145           # We don't know which came from the proxy, and which from the user
146           raise ActionControllerError.new(<<EOM)
147 IP spoofing attack?!
148 HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}
149 HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}
150 EOM
151         end
152         return @env['HTTP_CLIENT_IP']
153       end
154
155       if @env.include? 'HTTP_X_FORWARDED_FOR' then
156         remote_ips = @env['HTTP_X_FORWARDED_FOR'].split(',')
157         while remote_ips.size > 1 && TRUSTED_PROXIES =~ remote_ips.last.strip
158           remote_ips.pop
159         end
160
161         return remote_ips.last.strip
162       end
163
164       @env['REMOTE_ADDR']
165     end
166
167     # Returns the lowercase name of the HTTP server software.
168     def server_software
169       (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
170     end
171
172
173     # Returns the complete URL used for this request
174     def url
175       protocol + host_with_port + request_uri
176     end
177
178     # Return 'https://' if this is an SSL request and 'http://' otherwise.
179     def protocol
180       ssl? ? 'https://' : 'http://'
181     end
182
183     # Is this an SSL request?
184     def ssl?
185       @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
186     end
187
188     # Returns the host for this request, such as example.com.
189     def host
190     end
191
192     # Returns a host:port string for this request, such as example.com or
193     # example.com:8080.
194     def host_with_port
195       @host_with_port ||= host + port_string
196     end
197
198     # Returns the port number of this request as an integer.
199     def port
200       @port_as_int ||= @env['SERVER_PORT'].to_i
201     end
202
203     # Returns the standard port number for this request's protocol
204     def standard_port
205       case protocol
206         when 'https://' then 443
207         else 80
208       end
209     end
210
211     # Returns a port suffix like ":8080" if the port number of this request
212     # is not the default HTTP port 80 or HTTPS port 443.
213     def port_string
214       (port == standard_port) ? '' : ":#{port}"
215     end
216
217     # Returns the domain part of a host, such as rubyonrails.org in "www.rubyonrails.org". You can specify
218     # a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
219     def domain(tld_length = 1)
220       return nil unless named_host?(host)
221
222       host.split('.').last(1 + tld_length).join('.')
223     end
224
225     # Returns all the subdomains as an array, so ["dev", "www"] would be returned for "dev.www.rubyonrails.org".
226     # You can specify a different <tt>tld_length</tt>, such as 2 to catch ["www"] instead of ["www", "rubyonrails"]
227     # in "www.rubyonrails.co.uk".
228     def subdomains(tld_length = 1)
229       return [] unless named_host?(host)
230       parts = host.split('.')
231       parts[0..-(tld_length+2)]
232     end
233
234     # Return the query string, accounting for server idiosyncracies.
235     def query_string
236       if uri = @env['REQUEST_URI']
237         uri.split('?', 2)[1] || ''
238       else
239         @env['QUERY_STRING'] || ''
240       end
241     end
242
243     # Return the request URI, accounting for server idiosyncracies.
244     # WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
245     def request_uri
246       if uri = @env['REQUEST_URI']
247         # Remove domain, which webrick puts into the request_uri.
248         (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri
249       else
250         # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO.
251         script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
252         uri = @env['PATH_INFO']
253         uri = uri.sub(/#{script_filename}\//, '') unless script_filename.nil?
254         unless (env_qs = @env['QUERY_STRING']).nil? || env_qs.empty?
255           uri << '?' << env_qs
256         end
257
258         if uri.nil?
259           @env.delete('REQUEST_URI')
260           uri
261         else
262           @env['REQUEST_URI'] = uri
263         end
264       end
265     end
266
267     # Returns the interpreted path to requested resource after all the installation directory of this application was taken into account
268     def path
269       path = (uri = request_uri) ? uri.split('?').first.to_s : ''
270
271       # Cut off the path to the installation directory if given
272       path.sub!(%r/^#{relative_url_root}/, '')
273       path || ''     
274     end
275    
276     # Returns the path minus the web server relative installation directory.
277     # This can be set with the environment variable RAILS_RELATIVE_URL_ROOT.
278     # It can be automatically extracted for Apache setups. If the server is not
279     # Apache, this method returns an empty string.
280     def relative_url_root
281       @@relative_url_root ||= case
282         when @env["RAILS_RELATIVE_URL_ROOT"]
283           @env["RAILS_RELATIVE_URL_ROOT"]
284         when server_software == 'apache'
285           @env["SCRIPT_NAME"].to_s.sub(/\/dispatch\.(fcgi|rb|cgi)$/, '')
286         else
287           ''
288       end
289     end
290
291
292     # Read the request body. This is useful for web services that need to
293     # work with raw requests directly.
294     def raw_post
295       unless env.include? 'RAW_POST_DATA'
296         env['RAW_POST_DATA'] = body.read(content_length)
297         body.rewind if body.respond_to?(:rewind)
298       end
299       env['RAW_POST_DATA']
300     end
301
302     # Returns both GET and POST parameters in a single hash.
303     def parameters
304       @parameters ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access
305     end
306
307     def path_parameters=(parameters) #:nodoc:
308       @path_parameters = parameters
309       @symbolized_path_parameters = @parameters = nil
310     end
311
312     # The same as <tt>path_parameters</tt> with explicitly symbolized keys
313     def symbolized_path_parameters
314       @symbolized_path_parameters ||= path_parameters.symbolize_keys
315     end
316
317     # Returns a hash with the parameters used to form the path of the request.
318     # Returned hash keys are strings.  See <tt>symbolized_path_parameters</tt> for symbolized keys.
319     #
320     # Example:
321     #
322     #   {'action' => 'my_action', 'controller' => 'my_controller'}
323     def path_parameters
324       @path_parameters ||= {}
325     end
326
327
328     #--
329     # Must be implemented in the concrete request
330     #++
331
332     # The request body is an IO input stream.
333     def body
334     end
335
336     def query_parameters #:nodoc:
337     end
338
339     def request_parameters #:nodoc:
340     end
341
342     def cookies #:nodoc:
343     end
344
345     def session #:nodoc:
346     end
347
348     def session=(session) #:nodoc:
349       @session = session
350     end
351
352     def reset_session #:nodoc:
353     end
354
355     protected
356       # The raw content type string. Use when you need parameters such as
357       # charset or boundary which aren't included in the content_type MIME type.
358       # Overridden by the X-POST_DATA_FORMAT header for backward compatibility.
359       def content_type_with_parameters
360         content_type_from_legacy_post_data_format_header ||
361           env['CONTENT_TYPE'].to_s
362       end
363
364       # The raw content type string with its parameters stripped off.
365       def content_type_without_parameters
366         @content_type_without_parameters ||= self.class.extract_content_type_without_parameters(content_type_with_parameters)
367       end
368
369     private
370       def content_type_from_legacy_post_data_format_header
371         if x_post_format = @env['HTTP_X_POST_DATA_FORMAT']
372           case x_post_format.to_s.downcase
373             when 'yaml';  'application/x-yaml'
374             when 'xml';   'application/xml'
375           end
376         end
377       end
378
379       def parse_formatted_request_parameters
380         return {} if content_length.zero?
381
382         content_type, boundary = self.class.extract_multipart_boundary(content_type_with_parameters)
383
384         # Don't parse params for unknown requests.
385         return {} if content_type.blank?
386
387         mime_type = Mime::Type.lookup(content_type)
388         strategy = ActionController::Base.param_parsers[mime_type]
389
390         # Only multipart form parsing expects a stream.
391         body = (strategy && strategy != :multipart_form) ? raw_post : self.body
392
393         case strategy
394           when Proc
395             strategy.call(body)
396           when :url_encoded_form
397             self.class.clean_up_ajax_request_body! body
398             self.class.parse_query_parameters(body)
399           when :multipart_form
400             self.class.parse_multipart_form_parameters(body, boundary, content_length, env)
401           when :xml_simple, :xml_node
402             body.blank? ? {} : Hash.from_xml(body).with_indifferent_access
403           when :yaml
404             YAML.load(body)
405           when :json
406             if body.blank?
407               {}
408             else
409               data = ActiveSupport::JSON.decode(body)
410               data = {:_json => data} unless data.is_a?(Hash)
411               data.with_indifferent_access
412             end
413           else
414             {}
415         end
416       rescue Exception => e # YAML, XML or Ruby code block errors
417         raise
418         { "body" => body,
419           "content_type" => content_type_with_parameters,
420           "content_length" => content_length,
421           "exception" => "#{e.message} (#{e.class})",
422           "backtrace" => e.backtrace }
423       end
424
425       def named_host?(host)
426         !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
427       end
428
429     class << self
430       def parse_query_parameters(query_string)
431         return {} if query_string.blank?
432
433         pairs = query_string.split('&').collect do |chunk|
434           next if chunk.empty?
435           key, value = chunk.split('=', 2)
436           next if key.empty?
437           value = value.nil? ? nil : CGI.unescape(value)
438           [ CGI.unescape(key), value ]
439         end.compact
440
441         UrlEncodedPairParser.new(pairs).result
442       end
443
444       def parse_request_parameters(params)
445         parser = UrlEncodedPairParser.new
446
447         params = params.dup
448         until params.empty?
449           for key, value in params
450             if key.blank?
451               params.delete key
452             elsif !key.include?('[')
453               # much faster to test for the most common case first (GET)
454               # and avoid the call to build_deep_hash
455               parser.result[key] = get_typed_value(value[0])
456               params.delete key
457             elsif value.is_a?(Array)
458               parser.parse(key, get_typed_value(value.shift))
459               params.delete key if value.empty?
460             else
461               raise TypeError, "Expected array, found #{value.inspect}"
462             end
463           end
464         end
465
466         parser.result
467       end
468
469       def parse_multipart_form_parameters(body, boundary, content_length, env)
470         parse_request_parameters(read_multipart(body, boundary, content_length, env))
471       end
472
473       def extract_multipart_boundary(content_type_with_parameters)
474         if content_type_with_parameters =~ MULTIPART_BOUNDARY
475           ['multipart/form-data', $1.dup]
476         else
477           extract_content_type_without_parameters(content_type_with_parameters)
478         end
479       end
480
481       def extract_content_type_without_parameters(content_type_with_parameters)
482         $1.strip.downcase if content_type_with_parameters =~ /^([^,\;]*)/
483       end
484
485       def clean_up_ajax_request_body!(body)
486         body.chop! if body[-1] == 0
487         body.gsub!(/&_=$/, '')
488       end
489
490
491       private
492         def get_typed_value(value)
493           case value
494             when String
495               value
496             when NilClass
497               ''
498             when Array
499               value.map { |v| get_typed_value(v) }
500             else
501               if value.respond_to? :original_filename
502                 # Uploaded file
503                 if value.original_filename
504                   value
505                 # Multipart param
506                 else
507                   result = value.read
508                   value.rewind
509                   result
510                 end
511               # Unknown value, neither string nor multipart.
512               else
513                 raise "Unknown form value: #{value.inspect}"
514               end
515           end
516         end
517
518         MULTIPART_BOUNDARY = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n
519
520         EOL = "\015\012"
521
522         def read_multipart(body, boundary, content_length, env)
523           params = Hash.new([])
524           boundary = "--" + boundary
525           quoted_boundary = Regexp.quote(boundary)
526           buf = ""
527           bufsize = 10 * 1024
528           boundary_end=""
529
530           # start multipart/form-data
531           body.binmode if defined? body.binmode
532           boundary_size = boundary.size + EOL.size
533           content_length -= boundary_size
534           status = body.read(boundary_size)
535           if nil == status
536             raise EOFError, "no content body"
537           elsif boundary + EOL != status
538             raise EOFError, "bad content body"
539           end
540
541           loop do
542             head = nil
543             content =
544               if 10240 < content_length
545                 UploadedTempfile.new("CGI")
546               else
547                 UploadedStringIO.new
548               end
549             content.binmode if defined? content.binmode
550
551             until head and /#{quoted_boundary}(?:#{EOL}|--)/n.match(buf)
552
553               if (not head) and /#{EOL}#{EOL}/n.match(buf)
554                 buf = buf.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do
555                   head = $1.dup
556                   ""
557                 end
558                 next
559               end
560
561               if head and ( (EOL + boundary + EOL).size < buf.size )
562                 content.print buf[0 ... (buf.size - (EOL + boundary + EOL).size)]
563                 buf[0 ... (buf.size - (EOL + boundary + EOL).size)] = ""
564               end
565
566               c = if bufsize < content_length
567                     body.read(bufsize)
568                   else
569                     body.read(content_length)
570                   end
571               if c.nil? || c.empty?
572                 raise EOFError, "bad content body"
573               end
574               buf.concat(c)
575               content_length -= c.size
576             end
577
578             buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do
579               content.print $1
580               if "--" == $2
581                 content_length =