request.rb 27.2 KB
Newer Older
1 2 3 4
require 'tempfile'
require 'stringio'
require 'strscan'

5
require 'active_support/memoizable'
P
Pratik Naik 已提交
6
require 'action_controller/cgi_ext'
7

8
module ActionController
9
  # CgiRequest and TestRequest provide concrete implementations.
10
  class Request
11 12
    extend ActiveSupport::Memoizable

13 14 15
    class SessionFixationAttempt < StandardError #:nodoc:
    end

P
Pratik Naik 已提交
16 17
    # The hash of environment variables for this request,
    # such as { 'RAILS_ENV' => 'production' }.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
    attr_reader :env

    def initialize(env)
      @env = env
    end

    %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_INFO
        PATH_TRANSLATED REMOTE_HOST
        REMOTE_IDENT REMOTE_USER SCRIPT_NAME
        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

41 42 43
    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 }

P
Pratik Naik 已提交
44
    # The true HTTP request \method as a lowercase symbol, such as <tt>:get</tt>.
45 46
    # UnknownHttpMethod is raised for invalid methods not listed in ACCEPTED_HTTP_METHODS.
    def request_method
47 48
      method = @env['REQUEST_METHOD']
      HTTP_METHOD_LOOKUP[method] || raise(UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}")
49
    end
50
    memoize :request_method
51

P
Pratik Naik 已提交
52
    # The HTTP request \method as a lowercase symbol, such as <tt>:get</tt>.
53
    # Note, HEAD is returned as <tt>:get</tt> since the two are functionally
54
    # equivalent from the application's perspective.
D
Initial  
David Heinemeier Hansson 已提交
55
    def method
56
      request_method == :head ? :get : request_method
D
Initial  
David Heinemeier Hansson 已提交
57 58
    end

59
    # Is this a GET (or HEAD) request?  Equivalent to <tt>request.method == :get</tt>.
D
Initial  
David Heinemeier Hansson 已提交
60 61 62 63
    def get?
      method == :get
    end

64
    # Is this a POST request?  Equivalent to <tt>request.method == :post</tt>.
D
Initial  
David Heinemeier Hansson 已提交
65
    def post?
66
      request_method == :post
D
Initial  
David Heinemeier Hansson 已提交
67 68
    end

69
    # Is this a PUT request?  Equivalent to <tt>request.method == :put</tt>.
D
Initial  
David Heinemeier Hansson 已提交
70
    def put?
71
      request_method == :put
D
Initial  
David Heinemeier Hansson 已提交
72 73
    end

74
    # Is this a DELETE request?  Equivalent to <tt>request.method == :delete</tt>.
D
Initial  
David Heinemeier Hansson 已提交
75
    def delete?
76
      request_method == :delete
D
Initial  
David Heinemeier Hansson 已提交
77 78
    end

P
Pratik Naik 已提交
79 80
    # Is this a HEAD request? Since <tt>request.method</tt> sees HEAD as <tt>:get</tt>,
    # this \method checks the actual HTTP \method directly.
81
    def head?
82
      request_method == :head
83
    end
84

P
Pratik Naik 已提交
85
    # Provides access to the request's HTTP headers, for example:
P
Pratik Naik 已提交
86 87
    #
    #   request.headers["Content-Type"] # => "text/plain"
88
    def headers
89
      ActionController::Http::Headers.new(@env)
90
    end
91
    memoize :headers
92

P
Pratik Naik 已提交
93
    # Returns the content length of the request as an integer.
94
    def content_length
95
      @env['CONTENT_LENGTH'].to_i
96
    end
97
    memoize :content_length
98

99
    # The MIME type of the HTTP request, such as Mime::XML.
100
    #
P
Pratik Naik 已提交
101
    # For backward compatibility, the post \format is extracted from the
102
    # X-Post-Data-Format HTTP header if present.
103
    def content_type
104
      Mime::Type.lookup(content_type_without_parameters)
105
    end
106
    memoize :content_type
107

P
Pratik Naik 已提交
108
    # Returns the accepted MIME type for the request.
109
    def accepts
110 111 112 113 114 115 116 117 118 119 120 121
      header = @env['HTTP_ACCEPT'].to_s.strip

      if header.empty?
        [content_type, Mime::ALL].compact
      else
        Mime::Type.parse(header)
      end
    end
    memoize :accepts

    def if_modified_since
      if since = env['HTTP_IF_MODIFIED_SINCE']
122
        Time.rfc2822(since) rescue nil
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
      end
    end
    memoize :if_modified_since

    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
140 141
    # If-Modified-Since and If-None-Match conditions. If both headers are
    # supplied, both must match, or the request is not considered fresh.
142
    def fresh?(response)
143
      case
144 145 146 147 148 149 150 151 152
      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
153
    end
154

P
Pratik Naik 已提交
155
    # Returns the Mime type for the \format used in the request.
156 157 158
    #
    #   GET /posts/5.xml   | request.format => Mime::XML
    #   GET /posts/5.xhtml | request.format => Mime::HTML
159
    #   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>
160
    def format
161
      @format ||=
162 163 164 165 166 167 168 169 170
        if parameters[:format]
          Mime::Type.lookup_by_extension(parameters[:format])
        elsif ActionController::Base.use_accept_header
          accepts.first
        elsif xhr?
          Mime::Type.lookup_by_extension("js")
        else
          Mime::Type.lookup_by_extension("html")
        end
171
    end
172 173


P
Pratik Naik 已提交
174 175
    # Sets the \format by string extension, which can be used to force custom formats
    # that are not controlled by the extension.
176 177 178
    #
    #   class ApplicationController < ActionController::Base
    #     before_filter :adjust_format_for_iphone
179
    #
180 181 182 183 184 185 186
    #     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
187
      @format = Mime::Type.lookup_by_extension(parameters[:format])
188
    end
189

190
    # Returns a symbolized version of the <tt>:format</tt> parameter of the request.
P
Pratik Naik 已提交
191
    # If no \format is given it returns <tt>:js</tt>for Ajax requests and <tt>:html</tt>
192
    # otherwise.
193 194 195
    def template_format
      parameter_format = parameters[:format]

196
      if parameter_format
M
Michael Koziarski 已提交
197
        parameter_format
198
      elsif xhr?
199 200
        :js
      else
201
        :html
202 203 204
      end
    end

205
    def cache_format
M
Michael Koziarski 已提交
206
      parameters[:format]
207 208
    end

209 210 211
    # Returns true if the request's "X-Requested-With" header contains
    # "XMLHttpRequest". (The Prototype Javascript library sends this header with
    # every Ajax request.)
212
    def xml_http_request?
213
      !(@env['HTTP_X_REQUESTED_WITH'] !~ /XMLHttpRequest/i)
214 215 216
    end
    alias xhr? :xml_http_request?

J
Jeremy Kemper 已提交
217 218 219 220
    # 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

P
Pratik Naik 已提交
221
    # Determines originating IP address.  REMOTE_ADDR is the standard
D
Initial  
David Heinemeier Hansson 已提交
222
    # but will fail if the user is behind a proxy.  HTTP_CLIENT_IP and/or
J
Jeremy Kemper 已提交
223 224 225 226
    # 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.
D
Initial  
David Heinemeier Hansson 已提交
227
    def remote_ip
J
Jeremy Kemper 已提交
228
      remote_addr_list = @env['REMOTE_ADDR'] && @env['REMOTE_ADDR'].scan(/[^,\s]+/)
J
Jeremy Kemper 已提交
229

230 231 232 233
      unless remote_addr_list.blank?
        not_trusted_addrs = remote_addr_list.reject {|addr| addr =~ TRUSTED_PROXIES}
        return not_trusted_addrs.first unless not_trusted_addrs.empty?
      end
234 235
      remote_ips = @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',')

J
Jeremy Kemper 已提交
236
      if @env.include? 'HTTP_CLIENT_IP'
237
        if ActionController::Base.ip_spoofing_check && remote_ips && !remote_ips.include?(@env['HTTP_CLIENT_IP'])
J
Jeremy Kemper 已提交
238 239 240 241 242 243 244
          # We don't know which came from the proxy, and which from the user
          raise ActionControllerError.new(<<EOM)
IP spoofing attack?!
HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}
HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}
EOM
        end
245

J
Jeremy Kemper 已提交
246 247
        return @env['HTTP_CLIENT_IP']
      end
248

249
      if remote_ips
J
Jeremy Kemper 已提交
250 251
        while remote_ips.size > 1 && TRUSTED_PROXIES =~ remote_ips.last.strip
          remote_ips.pop
252 253
        end

J
Jeremy Kemper 已提交
254
        return remote_ips.last.strip
D
Initial  
David Heinemeier Hansson 已提交
255
      end
256

257
      @env['REMOTE_ADDR']
D
Initial  
David Heinemeier Hansson 已提交
258
    end
259
    memoize :remote_ip
D
Initial  
David Heinemeier Hansson 已提交
260

261 262 263 264
    # 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
265
    memoize :server_software
266

P
Pratik Naik 已提交
267
    # Returns the complete URL used for this request.
268
    def url
D
David Heinemeier Hansson 已提交
269
      protocol + host_with_port + request_uri
270
    end
271
    memoize :url
272

P
Pratik Naik 已提交
273
    # Returns 'https://' if this is an SSL request and 'http://' otherwise.
274 275 276
    def protocol
      ssl? ? 'https://' : 'http://'
    end
277
    memoize :protocol
278 279 280 281 282 283

    # Is this an SSL request?
    def ssl?
      @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
    end

P
Pratik Naik 已提交
284
    # Returns the \host for this request, such as "example.com".
285
    def raw_host_with_port
286 287 288 289 290 291 292
      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

293 294
    # Returns the host for this request, such as example.com.
    def host
295
      raw_host_with_port.sub(/:\d+$/, '')
296
    end
297
    memoize :host
298

P
Pratik Naik 已提交
299 300
    # Returns a \host:\port string for this request, such as "example.com" or
    # "example.com:8080".
301
    def host_with_port
302
      "#{host}#{port_string}"
303
    end
304
    memoize :host_with_port
305 306 307

    # Returns the port number of this request as an integer.
    def port
308
      if raw_host_with_port =~ /:(\d+)$/
309 310 311 312
        $1.to_i
      else
        standard_port
      end
313
    end
314
    memoize :port
315

P
Pratik Naik 已提交
316
    # Returns the standard \port number for this request's protocol.
317 318 319 320 321 322 323
    def standard_port
      case protocol
        when 'https://' then 443
        else 80
      end
    end

P
Pratik Naik 已提交
324 325
    # Returns a \port suffix like ":8080" if the \port number of this request
    # is not the default HTTP \port 80 or HTTPS \port 443.
326
    def port_string
327
      port == standard_port ? '' : ":#{port}"
328 329
    end

P
Pratik Naik 已提交
330
    # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
331 332
    # a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
    def domain(tld_length = 1)
333
      return nil unless named_host?(host)
334

335
      host.split('.').last(1 + tld_length).join('.')
336 337
    end

P
Pratik Naik 已提交
338 339 340
    # Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be
    # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
    # such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt>
341 342
    # in "www.rubyonrails.co.uk".
    def subdomains(tld_length = 1)
343
      return [] unless named_host?(host)
344
      parts = host.split('.')
345
      parts[0..-(tld_length+2)]
346 347
    end

P
Pratik Naik 已提交
348
    # Returns the query string, accounting for server idiosyncrasies.
349
    def query_string
350
      @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '')
351
    end
352
    memoize :query_string
353

P
Pratik Naik 已提交
354
    # Returns the request URI, accounting for server idiosyncrasies.
355
    # WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
D
Initial  
David Heinemeier Hansson 已提交
356
    def request_uri
357
      if uri = @env['REQUEST_URI']
358 359 360 361
        # 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.
362 363 364 365
        uri = @env['PATH_INFO'].to_s

        if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
          uri = uri.sub(/#{script_filename}\//, '')
366
        end
367

368 369 370 371
        env_qs = @env['QUERY_STRING'].to_s
        uri += "?#{env_qs}" unless env_qs.empty?

        if uri.blank?
372 373 374 375
          @env.delete('REQUEST_URI')
        else
          @env['REQUEST_URI'] = uri
        end
376
      end
377
    end
378
    memoize :request_uri
D
Initial  
David Heinemeier Hansson 已提交
379

P
Pratik Naik 已提交
380 381
    # Returns the interpreted \path to requested resource after all the installation
    # directory of this application was taken into account.
D
Initial  
David Heinemeier Hansson 已提交
382
    def path
J
Jeremy Kemper 已提交
383 384 385
      path = request_uri.to_s[/\A[^\?]*/]
      path.sub!(/\A#{ActionController::Base.relative_url_root}/, '')
      path
D
Initial  
David Heinemeier Hansson 已提交
386
    end
387
    memoize :path
D
Initial  
David Heinemeier Hansson 已提交
388

P
Pratik Naik 已提交
389
    # Read the request \body. This is useful for web services that need to
390
    # work with raw requests directly.
391
    def raw_post
392 393 394 395 396
      unless env.include? 'RAW_POST_DATA'
        env['RAW_POST_DATA'] = body.read(content_length)
        body.rewind if body.respond_to?(:rewind)
      end
      env['RAW_POST_DATA']
397 398
    end

P
Pratik Naik 已提交
399
    # Returns both GET and POST \parameters in a single hash.
400
    def parameters
401
      @parameters ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access
D
Initial  
David Heinemeier Hansson 已提交
402
    end
403

404
    def path_parameters=(parameters) #:nodoc:
405
      @path_parameters = parameters
406 407
      @symbolized_path_parameters = @parameters = nil
    end
408

P
Pratik Naik 已提交
409
    # The same as <tt>path_parameters</tt> with explicitly symbolized keys.
410
    def symbolized_path_parameters
411
      @symbolized_path_parameters ||= path_parameters.symbolize_keys
412
    end
D
Initial  
David Heinemeier Hansson 已提交
413

P
Pratik Naik 已提交
414 415
    # Returns a hash with the \parameters used to form the \path of the request.
    # Returned hash keys are strings:
416
    #
417
    #   {'action' => 'my_action', 'controller' => 'my_controller'}
P
Pratik Naik 已提交
418 419
    #
    # See <tt>symbolized_path_parameters</tt> for symbolized keys.
420 421 422
    def path_parameters
      @path_parameters ||= {}
    end
423

424 425 426 427 428 429 430 431 432 433
    # 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
        body_stream
      end
    end
434

435 436 437
    def remote_addr
      @env['REMOTE_ADDR']
    end
438

439 440 441 442 443 444 445
    def referrer
      @env['HTTP_REFERER']
    end
    alias referer referrer

    def query_parameters
      @query_parameters ||= self.class.parse_query_parameters(query_string)
446 447
    end

448 449
    def request_parameters
      @request_parameters ||= parse_formatted_request_parameters
D
Initial  
David Heinemeier Hansson 已提交
450 451
    end

452
    def body_stream #:nodoc:
453
      @env['rack.input']
D
Initial  
David Heinemeier Hansson 已提交
454 455
    end

456 457
    def cookies
      Rack::Request.new(@env).cookies
D
Initial  
David Heinemeier Hansson 已提交
458 459
    end

460 461
    def session
      @env['rack.session'] ||= {}
D
Initial  
David Heinemeier Hansson 已提交
462 463
    end

464 465 466 467
    def session=(session) #:nodoc:
      @session = session
    end

468 469 470 471 472 473 474 475 476 477 478 479 480 481
    def reset_session
      @env['rack.session'] = {}
    end

    def session_options
      @env['rack.session.options'] ||= {}
    end

    def session_options=(options)
      @env['rack.session.options'] = options
    end

    def server_port
      @env['SERVER_PORT'].to_i
482
    end
483

484 485 486
    protected
      # The raw content type string. Use when you need parameters such as
      # charset or boundary which aren't included in the content_type MIME type.
487
      # Overridden by the X-POST_DATA_FORMAT header for backward compatibility.
488
      def content_type_with_parameters
489 490
        content_type_from_legacy_post_data_format_header ||
          env['CONTENT_TYPE'].to_s
491 492 493 494
      end

      # The raw content type string with its parameters stripped off.
      def content_type_without_parameters
495
        self.class.extract_content_type_without_parameters(content_type_with_parameters)
496
      end
497
      memoize :content_type_without_parameters
498 499 500 501 502

    private
      def content_type_from_legacy_post_data_format_header
        if x_post_format = @env['HTTP_X_POST_DATA_FORMAT']
          case x_post_format.to_s.downcase
503 504
            when 'yaml';  'application/x-yaml'
            when 'xml';   'application/xml'
505 506 507 508
          end
        end
      end

509
      def parse_formatted_request_parameters
510 511
        return {} if content_length.zero?

512
        content_type, boundary = self.class.extract_multipart_boundary(content_type_with_parameters)
513 514

        # Don't parse params for unknown requests.
515 516 517 518 519 520
        return {} if content_type.blank?

        mime_type = Mime::Type.lookup(content_type)
        strategy = ActionController::Base.param_parsers[mime_type]

        # Only multipart form parsing expects a stream.
J
Jeremy Kemper 已提交
521
        body = (strategy && strategy != :multipart_form) ? raw_post : self.body
522 523 524 525 526

        case strategy
          when Proc
            strategy.call(body)
          when :url_encoded_form
527 528
            self.class.clean_up_ajax_request_body! body
            self.class.parse_query_parameters(body)
529
          when :multipart_form
530
            self.class.parse_multipart_form_parameters(body, boundary, content_length, env)
531 532 533 534
          when :xml_simple, :xml_node
            body.blank? ? {} : Hash.from_xml(body).with_indifferent_access
          when :yaml
            YAML.load(body)
535 536 537 538 539 540 541 542
          when :json
            if body.blank?
              {}
            else
              data = ActiveSupport::JSON.decode(body)
              data = {:_json => data} unless data.is_a?(Hash)
              data.with_indifferent_access
            end
543 544 545 546 547 548
          else
            {}
        end
      rescue Exception => e # YAML, XML or Ruby code block errors
        raise
        { "body" => body,
549
          "content_type" => content_type_with_parameters,
550 551 552 553 554
          "content_length" => content_length,
          "exception" => "#{e.message} (#{e.class})",
          "backtrace" => e.backtrace }
      end

555 556 557 558
      def named_host?(host)
        !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
      end

559
    class << self
560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598
      def parse_query_parameters(query_string)
        return {} if query_string.blank?

        pairs = query_string.split('&').collect do |chunk|
          next if chunk.empty?
          key, value = chunk.split('=', 2)
          next if key.empty?
          value = value.nil? ? nil : CGI.unescape(value)
          [ CGI.unescape(key), value ]
        end.compact

        UrlEncodedPairParser.new(pairs).result
      end

      def parse_request_parameters(params)
        parser = UrlEncodedPairParser.new

        params = params.dup
        until params.empty?
          for key, value in params
            if key.blank?
              params.delete key
            elsif !key.include?('[')
              # much faster to test for the most common case first (GET)
              # and avoid the call to build_deep_hash
              parser.result[key] = get_typed_value(value[0])
              params.delete key
            elsif value.is_a?(Array)
              parser.parse(key, get_typed_value(value.shift))
              params.delete key if value.empty?
            else
              raise TypeError, "Expected array, found #{value.inspect}"
            end
          end
        end

        parser.result
      end

599 600
      def parse_multipart_form_parameters(body, boundary, body_size, env)
        parse_request_parameters(read_multipart(body, boundary, body_size, env))
601
      end
602

603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620
      def extract_multipart_boundary(content_type_with_parameters)
        if content_type_with_parameters =~ MULTIPART_BOUNDARY
          ['multipart/form-data', $1.dup]
        else
          extract_content_type_without_parameters(content_type_with_parameters)
        end
      end

      def extract_content_type_without_parameters(content_type_with_parameters)
        $1.strip.downcase if content_type_with_parameters =~ /^([^,\;]*)/
      end

      def clean_up_ajax_request_body!(body)
        body.chop! if body[-1] == 0
        body.gsub!(/&_=$/, '')
      end


621 622 623 624 625 626 627 628 629 630
      private
        def get_typed_value(value)
          case value
            when String
              value
            when NilClass
              ''
            when Array
              value.map { |v| get_typed_value(v) }
            else
631
              if value.respond_to? :original_filename
632 633 634 635 636 637 638 639
                # Uploaded file
                if value.original_filename
                  value
                # Multipart param
                else
                  result = value.read
                  value.rewind
                  result
640 641 642 643 644 645 646 647 648 649 650 651
                end
              # Unknown value, neither string nor multipart.
              else
                raise "Unknown form value: #{value.inspect}"
              end
          end
        end

        MULTIPART_BOUNDARY = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n

        EOL = "\015\012"

652
        def read_multipart(body, boundary, body_size, env)
653 654
          params = Hash.new([])
          boundary = "--" + boundary
655
          quoted_boundary = Regexp.quote(boundary)
656 657 658 659 660 661
          buf = ""
          bufsize = 10 * 1024
          boundary_end=""

          # start multipart/form-data
          body.binmode if defined? body.binmode
662 663 664 665 666 667
          case body
          when File
            body.set_encoding(Encoding::BINARY) if body.respond_to?(:set_encoding)
          when StringIO
            body.string.force_encoding(Encoding::BINARY) if body.string.respond_to?(:force_encoding)
          end
668
          boundary_size = boundary.size + EOL.size
669
          body_size -= boundary_size
670 671 672 673 674 675 676 677 678 679
          status = body.read(boundary_size)
          if nil == status
            raise EOFError, "no content body"
          elsif boundary + EOL != status
            raise EOFError, "bad content body"
          end

          loop do
            head = nil
            content =
680
              if 10240 < body_size
681
                UploadedTempfile.new("CGI")
682
              else
683
                UploadedStringIO.new
684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701
              end
            content.binmode if defined? content.binmode

            until head and /#{quoted_boundary}(?:#{EOL}|--)/n.match(buf)

              if (not head) and /#{EOL}#{EOL}/n.match(buf)
                buf = buf.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do
                  head = $1.dup
                  ""
                end
                next
              end

              if head and ( (EOL + boundary + EOL).size < buf.size )
                content.print buf[0 ... (buf.size - (EOL + boundary + EOL).size)]
                buf[0 ... (buf.size - (EOL + boundary + EOL).size)] = ""
              end

702
              c = if bufsize < body_size
703 704
                    body.read(bufsize)
                  else
705
                    body.read(body_size)
706 707 708 709 710
                  end
              if c.nil? || c.empty?
                raise EOFError, "bad content body"
              end
              buf.concat(c)
711
              body_size -= c.size
712 713 714 715 716
            end

            buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do
              content.print $1
              if "--" == $2
717
                body_size = -1
718
              end
719
              boundary_end = $2.dup
720 721 722 723 724
              ""
            end

            content.rewind

725 726 727 728 729 730 731 732
            head =~ /Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;]*))/ni
            if filename = $1 || $2
              if /Mac/ni.match(env['HTTP_USER_AGENT']) and
                  /Mozilla/ni.match(env['HTTP_USER_AGENT']) and
                  (not /MSIE/ni.match(env['HTTP_USER_AGENT']))
                filename = CGI.unescape(filename)
              end
              content.original_path = filename.dup
733 734
            end

735 736
            head =~ /Content-Type: ([^\r]*)/ni
            content.content_type = $1.dup if $1
737

738 739
            head =~ /Content-Disposition:.* name="?([^\";]*)"?/ni
            name = $1.dup if $1
740 741 742 743 744 745

            if params.has_key?(name)
              params[name].push(content)
            else
              params[name] = [content]
            end
746
            break if body_size == -1
747 748 749
          end
          raise EOFError, "bad boundary end of body part" unless boundary_end=~/--/

750
          begin
751
            body.rewind if body.respond_to?(:rewind)
752
          rescue Errno::ESPIPE
753 754
            # Handles exceptions raised by input streams that cannot be rewound
            # such as when using plain CGI under Apache
755
          end
756

757 758 759 760 761 762 763 764 765 766 767 768
          params
        end
    end
  end

  class UrlEncodedPairParser < StringScanner #:nodoc:
    attr_reader :top, :parent, :result

    def initialize(pairs = [])
      super('')
      @result = {}
      pairs.each { |key, value| parse(key, value) }
769
    end
770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833

    KEY_REGEXP = %r{([^\[\]=&]+)}
    BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}

    # Parse the query string
    def parse(key, value)
      self.string = key
      @top, @parent = result, nil

      # First scan the bare key
      key = scan(KEY_REGEXP) or return
      key = post_key_check(key)

      # Then scan as many nestings as present
      until eos?
        r = scan(BRACKETED_KEY_REGEXP) or return
        key = self[1]
        key = post_key_check(key)
      end

      bind(key, value)
    end

    private
      # After we see a key, we must look ahead to determine our next action. Cases:
      #
      #   [] follows the key. Then the value must be an array.
      #   = follows the key. (A value comes next)
      #   & or the end of string follows the key. Then the key is a flag.
      #   otherwise, a hash follows the key.
      def post_key_check(key)
        if scan(/\[\]/) # a[b][] indicates that b is an array
          container(key, Array)
          nil
        elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
          container(key, Hash)
          nil
        else # End of key? We do nothing.
          key
        end
      end

      # Add a container to the stack.
      def container(key, klass)
        type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
        value = bind(key, klass.new)
        type_conflict! klass, value unless value.is_a?(klass)
        push(value)
      end

      # Push a value onto the 'stack', which is actually only the top 2 items.
      def push(value)
        @parent, @top = @top, value
      end

      # Bind a key (which may be nil for items in an array) to the provided value.
      def bind(key, value)
        if top.is_a? Array
          if key
            if top[-1].is_a?(Hash) && ! top[-1].key?(key)
              top[-1][key] = value
            else
              top << {key => value}.with_indifferent_access
              push top.last
834
              value = top[key]
835 836 837 838 839 840 841
            end
          else
            top << value
          end
        elsif top.is_a? Hash
          key = CGI.unescape(key)
          parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
842 843
          top[key] ||= value
          return top[key]
844 845 846 847 848 849 850 851
        else
          raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
        end

        return value
      end

      def type_conflict!(klass, value)
852
        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}.)"
853
      end
D
Initial  
David Heinemeier Hansson 已提交
854
  end
855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890

  module UploadedFile
    def self.included(base)
      base.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

  class UploadedStringIO < StringIO
    include UploadedFile
  end

  class UploadedTempfile < Tempfile
    include UploadedFile
  end
891
end