aboutsummaryrefslogblamecommitdiffstats
path: root/actionpack/lib/action_controller/request_parser.rb
blob: 82ee4c84c4fd192787102bcd0c32ebaeae63b3ca (plain) (tree)

























































































































































































































































































































                                                                                                         
module ActionController
  class RequestParser
    def initialize(env)
      @env = env
    end

    def request_parameters
      @request_parameters ||= parse_formatted_request_parameters
    end

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

    # Returns the query string, accounting for server idiosyncrasies.
    def query_string
      @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '')
    end

    # 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
        @env['rack.input']
      end
    end

    # The raw content type string with its parameters stripped off.
    def content_type_without_parameters
      self.class.extract_content_type_without_parameters(content_type_with_parameters)
    end

    def raw_post
      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']
    end

    private

    def parse_formatted_request_parameters
      return {} if content_length.zero?

      content_type, boundary = self.class.extract_multipart_boundary(content_type_with_parameters)

      # Don't parse params for unknown requests.
      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.
      body = (strategy && strategy != :multipart_form) ? raw_post : self.body

      case strategy
        when Proc
          strategy.call(body)
        when :url_encoded_form
          self.class.clean_up_ajax_request_body! body
          self.class.parse_query_parameters(body)
        when :multipart_form
          self.class.parse_multipart_form_parameters(body, boundary, content_length, @env)
        when :xml_simple, :xml_node
          body.blank? ? {} : Hash.from_xml(body).with_indifferent_access
        when :yaml
          YAML.load(body)
        when :json
          if body.blank?
            {}
          else
            data = ActiveSupport::JSON.decode(body)
            data = {:_json => data} unless data.is_a?(Hash)
            data.with_indifferent_access
          end
        else
          {}
      end
    rescue Exception => e # YAML, XML or Ruby code block errors
      raise
      { "body" => body,
        "content_type" => content_type_with_parameters,
        "content_length" => content_length,
        "exception" => "#{e.message} (#{e.class})",
        "backtrace" => e.backtrace }
    end

    def content_length
      @content_length ||= @env['CONTENT_LENGTH'].to_i
    end

    # 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.
    # Overridden by the X-POST_DATA_FORMAT header for backward compatibility.
    def content_type_with_parameters
      content_type_from_legacy_post_data_format_header || @env['CONTENT_TYPE'].to_s
    end

    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
          when 'yaml';  'application/x-yaml'
          when 'xml';   'application/xml'
        end
      end
    end

    class << self
      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

      def parse_multipart_form_parameters(body, boundary, body_size, env)
        parse_request_parameters(read_multipart(body, boundary, body_size, env))
      end

      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


      private
        def get_typed_value(value)
          case value
            when String
              value
            when NilClass
              ''
            when Array
              value.map { |v| get_typed_value(v) }
            else
              if value.respond_to? :original_filename
                # Uploaded file
                if value.original_filename
                  value
                # Multipart param
                else
                  result = value.read
                  value.rewind
                  result
                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"

        def read_multipart(body, boundary, body_size, env)
          params = Hash.new([])
          boundary = "--" + boundary
          quoted_boundary = Regexp.quote(boundary)
          buf = ""
          bufsize = 10 * 1024
          boundary_end=""

          # start multipart/form-data
          body.binmode if defined? body.binmode
          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
          boundary_size = boundary.size + EOL.size
          body_size -= boundary_size
          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 =
              if 10240 < body_size
                UploadedTempfile.new("CGI")
              else
                UploadedStringIO.new
              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

              c = if bufsize < body_size
                    body.read(bufsize)
                  else
                    body.read(body_size)
                  end
              if c.nil? || c.empty?
                raise EOFError, "bad content body"
              end
              buf.concat(c)
              body_size -= c.size
            end

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

            content.rewind

            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
            end

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

            head =~ /Content-Disposition:.* name="?([^\";]*)"?/ni
            name = $1.dup if $1

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

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

          params
        end
    end # class << self
  end
end