c079895683b8817e3fc38f428eca324a767feaf5
[depot.git] / request.rb
1 require 'tempfile'
2 require 'stringio'
3 require 'strscan'
4
5 require 'active_support/memoizable'
6
7 module ActionController
8 # CgiRequest and TestRequest provide concrete implementations.
9 class AbstractRequest
10 extend ActiveSupport::Memoizable
11
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
17 end
18
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 }
21
22 # The hash of environment variables for this request,
23 # such as { 'RAILS_ENV' => 'production' }.
24 attr_reader :env
25
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.
28 def request_method
29 method = @env['REQUEST_METHOD']
30 method = parameters[:_method] if method == 'POST' && !parameters[:_method].blank?
31
32 HTTP_METHOD_LOOKUP[method] || raise(UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}")
33 end
34 memoize :request_method
35
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.
39 def method
40 request_method == :head ? :get : request_method
41 end
42
43 # Is this a GET (or HEAD) request? Equivalent to <tt>request.method == :get</tt>.
44 def get?
45 method == :get
46 end
47
48 # Is this a POST request? Equivalent to <tt>request.method == :post</tt>.
49 def post?
50 request_method == :post
51 end
52
53 # Is this a PUT request? Equivalent to <tt>request.method == :put</tt>.
54 def put?
55 request_method == :put
56 end
57
58 # Is this a DELETE request? Equivalent to <tt>request.method == :delete</tt>.
59 def delete?
60 request_method == :delete
61 end
62
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.
65 def head?
66 request_method == :head
67 end
68
69 # Provides access to the request's HTTP headers, for example:
70 #
71 # request.headers["Content-Type"] # => "text/plain"
72 def headers
73 ActionController::Http::Headers.new(@env)
74 end
75 memoize :headers
76
77 # Returns the content length of the request as an integer.
78 def content_length
79 @env['CONTENT_LENGTH'].to_i
80 end
81 memoize :content_length
82
83 # The MIME type of the HTTP request, such as Mime::XML.
84 #
85 # For backward compatibility, the post \format is extracted from the
86 # X-Post-Data-Format HTTP header if present.
87 def content_type
88 Mime::Type.lookup(content_type_without_parameters)
89 end
90 memoize :content_type
91
92 # Returns the accepted MIME type for the request.
93 def accepts
94 header = @env['HTTP_ACCEPT'].to_s.strip
95
96 if header.empty?
97 [content_type, Mime::ALL].compact
98 else
99 Mime::Type.parse(header)
100 end
101 end
102 memoize :accepts
103
104 def if_modified_since
105 if since = env['HTTP_IF_MODIFIED_SINCE']
106 Time.rfc2822(since) rescue nil
107 end
108 end
109 memoize :if_modified_since
110
111 def if_none_match
112 env['HTTP_IF_NONE_MATCH']
113 end
114
115 def not_modified?(modified_at)
116 if_modified_since && modified_at && if_modified_since >= modified_at
117 end
118
119 def etag_matches?(etag)
120 if_none_match && if_none_match == etag
121 end
122
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.
126 def fresh?(response)
127 case
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)
132 when if_none_match
133 etag_matches?(response.etag)
134 else
135 false
136 end
137 end
138
139 # Returns the Mime type for the \format used in the request.
140 #
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>
144 def format
145 @format ||=
146 if parameters[:format]
147 Mime::Type.lookup_by_extension(parameters[:format])
148 elsif ActionController::Base.use_accept_header
149 accepts.first
150 elsif xhr?
151 Mime::Type.lookup_by_extension("js")
152 else
153 Mime::Type.lookup_by_extension("html")
154 end
155 end
156
157
158 # Sets the \format by string extension, which can be used to force custom formats
159 # that are not controlled by the extension.
160 #
161 # class ApplicationController < ActionController::Base
162 # before_filter :adjust_format_for_iphone
163 #
164 # private
165 # def adjust_format_for_iphone
166 # request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/]
167 # end
168 # end
169 def format=(extension)
170 parameters[:format] = extension.to_s
171 @format = Mime::Type.lookup_by_extension(parameters[:format])
172 end
173
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>
176 # otherwise.
177 def template_format
178 parameter_format = parameters[:format]
179
180 if parameter_format
181 parameter_format
182 elsif xhr?
183 :js
184 else
185 :html
186 end
187 end
188
189 def cache_format
190 parameters[:format]
191 end
192
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)
198 end
199 alias xhr? :xml_http_request?
200
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
204
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.
211 def remote_ip
212 remote_addr_list = @env['REMOTE_ADDR'] && @env['REMOTE_ADDR'].split(',').collect(&:strip)
213
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?
217 end
218 remote_ips = @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',')
219
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)
224 IP spoofing attack?!
225 HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}
226 HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}
227 EOM
228 end
229
230 return @env['HTTP_CLIENT_IP']
231 end
232
233 if remote_ips
234 while remote_ips.size > 1 && TRUSTED_PROXIES =~ remote_ips.last.strip
235 remote_ips.pop
236 end
237
238 return remote_ips.last.strip
239 end
240
241 @env['REMOTE_ADDR']
242 end
243 memoize :remote_ip
244
245 # Returns the lowercase name of the HTTP server software.
246 def server_software
247 (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
248 end
249 memoize :server_software
250
251
252 # Returns the complete URL used for this request.
253 def url
254 protocol + host_with_port + request_uri
255 end
256 memoize :url
257
258 # Returns 'https://' if this is an SSL request and 'http://' otherwise.
259 def protocol
260 ssl? ? 'https://' : 'http://'
261 end
262 memoize :protocol
263
264 # Is this an SSL request?
265 def ssl?
266 @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
267 end
268
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
273 else
274 env['HTTP_HOST'] || env['SERVER_NAME'] || "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
275 end
276 end
277
278 # Returns the host for this request, such as example.com.
279 def host
280 raw_host_with_port.sub(/:\d+$/, '')
281 end
282 memoize :host
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 memoize :host_with_port
290
291 # Returns the port number of this request as an integer.
292 def port
293 if raw_host_with_port =~ /:(\d+)$/
294 $1.to_i
295 else
296 standard_port
297 end
298 end
299 memoize :port
300
301 # Returns the standard \port number for this request's protocol.
302 def standard_port
303 case protocol
304 when 'https://' then 443
305 else 80
306 end
307 end
308
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.
311 def port_string
312 port == standard_port ? '' : ":#{port}"
313 end
314
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)
319
320 host.split('.').last(1 + tld_length).join('.')
321 end
322
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)]
331 end
332
333 # Returns the query string, accounting for server idiosyncrasies.
334 def query_string
335 if uri = @env['REQUEST_URI']
336 uri.split('?', 2)[1] || ''
337 else
338 @env['QUERY_STRING'] || ''
339 end
340 end
341 memoize :query_string
342
343 # Returns the request URI, accounting for server idiosyncrasies.
344 # WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
345 def request_uri
346 if uri = @env['REQUEST_URI']
347 # Remove domain, which webrick puts into the request_uri.
348 (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri
349 else
350 # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO.
351 uri = @env['PATH_INFO'].to_s
352
353 if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
354 uri = uri.sub(/#{script_filename}\//, '')
355 end
356
357 env_qs = @env['QUERY_STRING'].to_s
358 uri += "?#{env_qs}" unless env_qs.empty?
359
360 if uri.blank?
361 @env.delete('REQUEST_URI')
362 else
363 @env['REQUEST_URI'] = uri
364 end
365 end
366 end
367 memoize :request_uri
368
369 # Returns the interpreted \path to requested resource after all the installation
370 # directory of this application was taken into account.
371 def path
372 path = (uri = request_uri) ? uri.split('?').first.to_s : ''
373
374 # Cut off the path to the installation directory if given
375 path.sub!(%r/^#{ActionController::Base.relative_url_root}/, '')
376 path || ''
377 end
378 memoize :path
379
380 # Read the request \body. This is useful for web services that need to
381 # work with raw requests directly.
382 def raw_post
383 unless env.include? 'RAW_POST_DATA'
384 env['RAW_POST_DATA'] = body.read(content_length)
385 body.rewind if body.respond_to?(:rewind)
386 end
387 env['RAW_POST_DATA']
388 end
389
390 # Returns both GET and POST \parameters in a single hash.
391 def parameters
392 @parameters ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access
393 end
394
395 def path_parameters=(parameters) #:nodoc:
396 @path_parameters = parameters
397 @symbolized_path_parameters = @parameters = nil
398 end
399
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
403 end
404
405 # Returns a hash with the \parameters used to form the \path of the request.
406 # Returned hash keys are strings:
407 #
408 # {'action' => 'my_action', 'controller' => 'my_controller'}
409 #
410 # See <tt>symbolized_path_parameters</tt> for symbolized keys.
411 def path_parameters
412 @path_parameters ||= {}
413 end
414
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.
417 def body
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)
421 else
422 body_stream
423 end
424 end
425
426 def remote_addr
427 @env['REMOTE_ADDR']
428 end
429
430 def referrer
431 @env['HTTP_REFERER']
432 end
433 alias referer referrer
434
435
436 def query_parameters
437 @query_parameters ||= self.class.parse_query_parameters(query_string)
438 end
439
440 def request_parameters
441 @request_parameters ||= parse_formatted_request_parameters
442 end
443
444
445 #--
446 # Must be implemented in the concrete request
447 #++
448
449 def body_stream #:nodoc:
450 end
451
452 def cookies #:nodoc:
453 end
454
455 def session #:nodoc:
456 end
457
458 def session=(session) #:nodoc:
459 @session = session
460 end
461
462 def reset_session #:nodoc:
463 end
464
465 protected
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
472 end
473
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)
477 end
478 memoize :content_type_without_parameters
479
480 private
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'
486 end
487 end
488 end
489
490 def parse_formatted_request_parameters
491 return {} if content_length.zero?
492
493 content_type, boundary = self.class.extract_multipart_boundary(content_type_with_parameters)
494
495 # Don't parse params for unknown requests.
496 return {} if content_type.blank?
497
498 mime_type = Mime::Type.lookup(content_type)
499 strategy = ActionController::Base.param_parsers[mime_type]
500
501 # Only multipart form parsing expects a stream.
502 body = (strategy && strategy != :multipart_form) ? raw_post : self.body
503
504 case strategy
505 when Proc
506 strategy.call(body)
507 when :url_encoded_form
508 self.class.clean_up_ajax_request_body! body
509 self.class.parse_query_parameters(body)
510 when :multipart_form
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
514 when :yaml
515 YAML.load(body)
516 when :json
517 if body.blank?
518 {}
519 else
520 data = ActiveSupport::JSON.decode(body)
521 data = {:_json => data} unless data.is_a?(Hash)
522 data.with_indifferent_access
523 end
524 else
525 {}
526 end
527 rescue Exception => e # YAML, XML or Ruby code block errors
528 raise
529 { "body" => body,
530 "content_type" => content_type_with_parameters,
531 "content_length" => content_length,
532 "exception" => "#{e.message} (#{e.class})",
533 "backtrace" => e.backtrace }
534 end
535
536 def named_host?(host)
537 !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
538 end
539
540 class << self
541 def parse_query_parameters(query_string)
542 return {} if query_string.blank?
543
544 pairs = query_string.split('&').collect do |chunk|
545 next if chunk.empty?
546 key, value = chunk.split('=', 2)
547 next if key.empty?
548 value = value.nil? ? nil : CGI.unescape(value)
549 [ CGI.unescape(key), value ]
550 end.compact
551
552 UrlEncodedPairParser.new(pairs).result
553 end
554
555 def parse_request_parameters(params)
556 parser = UrlEncodedPairParser.new
557
558 params = params.dup
559 until params.empty?
560 for key, value in params
561 if key.blank?
562 params.delete key
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])
567 params.delete key
568 elsif value.is_a?(Array)
569 parser.parse(key, get_typed_value(value.shift))
570 params.delete key if value.empty?
571 else
572 raise TypeError, "Expected array, found #{value.inspect}"
573 end
574 end
575 end
576
577 parser.result
578 end
579
580 def parse_multipart_form_parameters(body, boundary, body_size, env)
581 parse_request_parameters(read_multipart(body, boundary, body_size, env))
582 end
583
584 def extract_multipart_boundary(content_type_with_parameters)
585 if content_type_with_parameters =~ MULTIPART_BOUNDARY
586 ['multipart/form-data', $1.dup]
587 else
588 extract_content_type_without_parameters(content_type_with_parameters)
589 end
590 end
591
592 def extract_content_type_without_parameters(content_type_with_parameters)
593 $1.strip.downcase if content_type_with_parameters =~ /^([^,\;]*)/
594 end
595
596 def clean_up_ajax_request_body!(body)
597 body.chop! if body[-1] == 0
598 body.gsub!(/&_=$/, '')
599 end
600
601
602 private
603 def get_typed_value(value)
604 case value
605 when String
606 value
607 when NilClass
608 ''
609 when Array
610 value.map { |v| get_typed_value(v) }
611 else
612 if value.respond_to? :original_filename
613 # Uploaded file
614 if value.original_filename
615 value
616 # Multipart param
617 else
618 result = value.read
619 value.rewind
620 result
621 end
622 # Unknown value, neither string nor multipart.
623 else
624 raise "Unknown form value: #{value.inspect}"
625 end
626 end
627 end
628
629 MULTIPART_BOUNDARY = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n
630
631 EOL = "\015\012"
632
633 def read_multipart(body, boundary, body_size, env)
634 params = Hash.new([])
635 boundary = "--" + boundary
636 quoted_boundary = Regexp.quote(boundary)
637 buf = ""
638 bufsize = 10 * 1024
639 boundary_end=""
640
641 # start multipart/form-data
642 body.binmode if defined? body.binmode
643 case body
644 when File
645 body.set_encoding(Encoding::BINARY) if body.respond_to?(:set_encoding)
646 when StringIO
647 body.string.force_encoding(Encoding::BINARY) if body.string.respond_to?(:force_encoding)
648 end
649 boundary_size = boundary.size + EOL.size
650 body_size -= boundary_size
651 status = body.read(boundary_size)
652 if nil == status
653 raise EOFError, "no content body"
654 elsif boundary + EOL != status
655 raise EOFError, "bad content body"
656 end
657
658 loop do
659 head = nil
660 content =
661 if 10240 < body_size
662 UploadedTempfile.new("CGI")
663 else
664 UploadedStringIO.new
665 end
666 content.binmode if defined? content.binmode
667
668 until head and /#{quoted_boundary}(?:#{EOL}|--)/n.match(buf)
669
670 if (not head) and /#{EOL}#{EOL}/n.match(buf)
671 buf = buf.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do
672 head = $1.dup
673 ""
674 end
675 next
676 end
677
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)] = ""
681 end
682
683 c = if bufsize < body_size
684 body.read(bufsize)
685 else
686 body.read(body_size)
687 end
688 if c.nil? || c.empty?
689 raise EOFError, "bad content body"
690 end
691 buf.concat(c)
692 body_size -= c.size
693 end
694
695 buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do
696 content.print $1
697 if "--" == $2
698 body_size = -1
699 end
700 boundary_end = $2.dup
701 ""
702 end
703
704 content.rewind
705
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)
712 end
713 content.original_path = filename.dup
714 end
715
716 head =~ /Content-Type: ([^\r]*)/ni
717 content.content_type = $1.dup if $1
718
719 head =~ /Content-Disposition:.* name="?([^\";]*)"?/ni
720 name = $1.dup if $1
721
722 if params.has_key?(name)
723 params[name].push(content)
724 else
725 params[name] = [content]
726 end
727 break if body_size == -1
728 end
729 raise EOFError, "bad boundary end of body part" unless boundary_end=~/--/
730
731 begin
732 body.rewind if body.respond_to?(:rewind)
733 rescue Errno::ESPIPE
734 # Handles exceptions raised by input streams that cannot be rewound
735 # such as when using plain CGI under Apache
736 end
737
738 params
739 end
740 end
741 end
742
743 class UrlEncodedPairParser < StringScanner #:nodoc:
744 attr_reader :top, :parent, :result
745
746 def initialize(pairs = [])
747 super('')
748 @result = {}
749 pairs.each { |key, value| parse(key, value) }
750 end
751
752 KEY_REGEXP = %r{([^\[\]=&]+)}
753 BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
754
755 # Parse the query string
756 def parse(key, value)
757 self.string = key
758 @top, @parent = result, nil
759
760 # First scan the bare key
761 key = scan(KEY_REGEXP) or return
762 key = post_key_check(key)
763
764 # Then scan as many nestings as present
765 until eos?
766 r = scan(BRACKETED_KEY_REGEXP) or return
767 key = self[1]
768 key = post_key_check(key)
769 end
770
771 bind(key, value)
772 end
773
774 private
775 # After we see a key, we must look ahead to determine our next action. Cases:
776 #
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)
784 nil
785 elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
786 container(key, Hash)
787 nil
788 else # End of key? We do nothing.
789 key
790 end
791 end
792
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)
798 push(value)
799 end
800
801 # Push a value onto the 'stack', which is actually only the top 2 items.
802 def push(value)
803 @parent, @top = @top, value
804 end
805
806 # Bind a key (which may be nil for items in an array) to the provided value.
807 def bind(key, value)
808 if top.is_a? Array
809 if key
810 if top[-1].is_a?(Hash) && ! top[-1].key?(key)
811 top[-1][key] = value
812 else
813 top << {key => value}.with_indifferent_access
814 push top.last
815 value = top[key]
816 end
817 else
818 top << value
819 end
820 elsif top.is_a? Hash
821 key = CGI.unescape(key)
822 parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
823 top[key] ||= value
824 return top[key]
825 else
826 raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
827 end
828
829 return value
830 end
831
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}.)"
834 end
835 end
836
837 module UploadedFile
838 def self.included(base)
839 base.class_eval do
840 attr_accessor :original_path, :content_type
841 alias_method :local_path, :path
842 end
843 end
844
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
852 @original_filename =
853 unless original_path.blank?
854 if original_path =~ /^(?:.*[:\\\/])?(.*)/m
855 $1
856 else
857 File.basename original_path
858 end
859 end
860 end
861 @original_filename
862 end
863 end
864
865 class UploadedStringIO < StringIO
866 include UploadedFile
867 end
868
869 class UploadedTempfile < Tempfile
870 include UploadedFile
871 end
872 end