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

Changeset 6764

Show
Ignore:
Timestamp:
05/18/07 06:24:50 (1 year ago)
Author:
bitsweat
Message:

Parse url-encoded and multipart requests ourselves instead of delegating to CGI.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/actionpack/CHANGELOG

    r6763 r6764  
    11*SVN* 
     2 
     3* Parse url-encoded and multipart requests ourselves instead of delegating to CGI.  [Jeremy Kemper] 
    24 
    35* select :include_blank option can be set to a string instead of true, which just uses an empty string.  #7664 [Wizard] 
  • trunk/actionpack/lib/action_controller/base.rb

    r6758 r6764  
    272272    # 
    273273    #   ActionController::Base.param_parsers[Mime::YAML] = :yaml 
    274     @@param_parsers = { Mime::XML => :xml_simple } 
     274    @@param_parsers = { Mime::MULTIPART_FORM => :multipart_form, 
     275                        Mime::URL_ENCODED_FORM => :url_encoded_form, 
     276                        Mime::XML => :xml_simple } 
    275277    cattr_accessor :param_parsers 
    276278 
  • trunk/actionpack/lib/action_controller/cgi_ext.rb

    r6740 r6764  
    11require 'action_controller/cgi_ext/stdinput' 
    2 require 'action_controller/cgi_ext/parameters' 
    32require 'action_controller/cgi_ext/query_extension' 
    43require 'action_controller/cgi_ext/cookie' 
     
    76class CGI #:nodoc: 
    87  include ActionController::CgiExt::Stdinput 
    9   include ActionController::CgiExt::Parameters 
    108 
    119  class << self 
  • trunk/actionpack/lib/action_controller/cgi_ext/query_extension.rb

    r6740 r6764  
    66    remove_method :initialize_query 
    77 
    8     # Initialize the data from the query. 
    9     # 
    10     # Handles multipart forms (in particular, forms that involve file uploads). 
    11     # Reads query parameters in the @params field, and cookies into @cookies. 
     8    # Neuter CGI parameter parsing. 
    129    def initialize_query 
    13       @cookies = CGI::Cookie::parse(env_table['HTTP_COOKIE'] || env_table['COOKIE']) 
     10      # Fix some strange request environments. 
     11      env_table['REQUEST_METHOD'] ||= 'GET' 
    1412 
    15       # Fix some strange request environments. 
    16       if method = env_table['REQUEST_METHOD'] 
    17         method = method.to_s.downcase.intern 
    18       else 
    19         method = :get 
     13      # POST assumes missing Content-Type is application/x-www-form-urlencoded. 
     14      if env_table['CONTENT_TYPE'].blank? && env_table['REQUEST_METHOD'] == 'POST' 
     15        env_table['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' 
    2016      end 
    2117 
    22       # POST assumes missing Content-Type is application/x-www-form-urlencoded. 
    23       content_type = env_table['CONTENT_TYPE'] 
    24       if content_type.blank? && method == :post 
    25         content_type = 'application/x-www-form-urlencoded' 
    26       end 
    27  
    28       # Force content length to zero if missing. 
    29       content_length = env_table['CONTENT_LENGTH'].to_i 
    30  
    31       # Set multipart to false by default. 
    32       @multipart = false 
    33  
    34       # POST and PUT may have params in entity body. If content type is missing 
    35       # or non-urlencoded, don't read the body or parse parameters: assume it's 
    36       # binary data. 
    37       if method == :post || method == :put 
    38         if boundary = extract_multipart_form_boundary(content_type) 
    39           @multipart = true 
    40           @params = read_multipart(boundary, content_length) 
    41         elsif content_type.blank? || content_type !~ %r{application/x-www-form-urlencoded}i 
    42           @params = {} 
    43         end 
    44       end 
    45  
    46       @params ||= CGI.parse(read_params(method, content_length)) 
     18      @cookies = CGI::Cookie::parse(env_table['HTTP_COOKIE'] || env_table['COOKIE']) 
     19      @params = {} 
    4720    end 
    48  
    49     private 
    50       unless defined?(MULTIPART_FORM_BOUNDARY_RE) 
    51         MULTIPART_FORM_BOUNDARY_RE = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n #" 
    52       end 
    53  
    54       def extract_multipart_form_boundary(content_type) 
    55         MULTIPART_FORM_BOUNDARY_RE.match(content_type).to_a.pop 
    56       end 
    57  
    58       if defined? MOD_RUBY 
    59         def read_query 
    60           Apache::request.args || '' 
    61         end 
    62       else 
    63         def read_query 
    64           # fixes CGI querystring parsing for lighttpd 
    65           env_qs = env_table['QUERY_STRING'] 
    66           if env_qs.blank? && !(uri = env_table['REQUEST_URI']).blank? 
    67             uri.split('?', 2)[1] || '' 
    68           else 
    69             env_qs || '' 
    70           end 
    71         end 
    72       end 
    73  
    74       def read_body(content_length) 
    75         stdinput.binmode if stdinput.respond_to?(:binmode) 
    76         content = stdinput.read(content_length) || '' 
    77         # Fix for Safari Ajax postings that always append \000 
    78         content.chop! if content[-1] == 0 
    79         content.gsub!(/&_=$/, '') 
    80         env_table['RAW_POST_DATA'] = content.freeze 
    81       end 
    82  
    83       def read_params(method, content_length) 
    84         case method 
    85           when :get 
    86             read_query 
    87           when :post, :put 
    88             read_body(content_length) 
    89           when :cmd 
    90             read_from_cmdline 
    91           else # :head, :delete, :options, :trace, :connect 
    92             read_query 
    93         end 
    94       end 
    95   end # module QueryExtension 
     21  end 
    9622end 
  • trunk/actionpack/lib/action_controller/cgi_process.rb

    r6743 r6764  
    4848 
    4949    def query_string 
    50       if (qs = @cgi.query_string) && !qs.empty? 
     50      qs = @cgi.query_string 
     51      if !qs.blank? 
    5152        qs 
    5253      elsif uri = @env['REQUEST_URI'] 
    53         parts = uri.split('?') 
    54         parts.shift 
    55         parts.join('?') 
     54        uri.split('?', 2).last 
    5655      else 
    5756        @env['QUERY_STRING'] || '' 
     
    7069 
    7170    def query_parameters 
    72       @query_parameters ||= CGI.parse_query_parameters(query_string) 
     71      @query_parameters ||= self.class.parse_query_parameters(query_string) 
    7372    end 
    7473 
    7574    def request_parameters 
    76       @request_parameters ||= 
    77         if ActionController::Base.param_parsers.has_key?(content_type) 
    78           self.class.parse_formatted_request_parameters(content_type, body.read) 
    79         else 
    80           CGI.parse_request_parameters(@cgi.params) 
    81         end 
     75      @request_parameters ||= self.class.parse_formatted_request_parameters(body, content_type_with_parameters, content_length, env) 
    8276    end 
    8377 
  • trunk/actionpack/lib/action_controller/integration.rb

    r6759 r6764  
    307307                            "HTTP_HOST"      => host, 
    308308                            "SERVER_PORT"    => https? ? "443" : "80", 
    309                             "HTTPS"          => https? ? "on" : "off")                           
     309                            "HTTPS"          => https? ? "on" : "off") 
    310310          ActionController::UrlRewriter.new(ActionController::CgiRequest.new(cgi), {}) 
    311311        end 
  • trunk/actionpack/lib/action_controller/request.rb

    r6742 r6764  
     1require 'tempfile' 
     2require 'stringio' 
     3require 'strscan' 
     4 
    15module ActionController 
    26  # CgiRequest and TestRequest provide concrete implementations. 
     
    5660    end 
    5761 
     62    def content_length 
     63      @content_length ||= env['CONTENT_LENGTH'].to_i 
     64    end 
     65 
     66    def content_type_with_parameters 
     67      @content_type_with_parameters ||= env['CONTENT_TYPE'].to_s 
     68    end 
     69 
    5870    # Determine whether the body of a HTTP call is URL-encoded (default) 
    5971    # or matches one of the registered param_parsers.  
     
    6577        begin 
    6678          # Receive header sans any charset information. 
    67           content_type = @env['CONTENT_TYPE'].to_s.sub(/\s*\;.*$/, '').strip.downcase 
     79          content_type = content_type_with_parameters.sub(/\s*\;.*$/, '').strip.downcase 
    6880 
    6981          if x_post_format = @env['HTTP_X_POST_DATA_FORMAT'] 
     
    298310 
    299311 
    300     def self.parse_formatted_request_parameters(mime_type, body) 
    301       case strategy = ActionController::Base.param_parsers[mime_type] 
    302         when Proc 
    303           strategy.call(body) 
    304         when :xml_simple, :xml_node 
    305           body.blank? ? {} : Hash.from_xml(body).with_indifferent_access 
    306         when :yaml 
    307           YAML.load(body) 
     312    class << self 
     313      def parse_formatted_request_parameters(body, content_type, content_length, env = {}) 
     314        content_length = content_length.to_i 
     315        return {} if content_length.zero? 
     316 
     317        content_type, boundary = extract_multipart_boundary(content_type.to_s) 
     318        return {} if content_type.blank? 
     319 
     320        mime_type = Mime::Type.lookup(content_type) 
     321        strategy = ActionController::Base.param_parsers[mime_type] 
     322 
     323        raise [content_type, content_length, mime_type, ActionController::Base.param_parsers].inspect unless strategy 
     324 
     325        # Only multipart form parsing expects a stream. 
     326        if strategy && strategy != :multipart_form 
     327          body = body.read(content_length) 
     328        end 
     329 
     330        case strategy 
     331          when Proc 
     332            strategy.call(body) 
     333          when :url_encoded_form 
     334            clean_up_ajax_request_body! body 
     335            parse_query_parameters(body) 
     336          when :multipart_form 
     337            parse_multipart_form_parameters(body, boundary, content_length, env) 
     338          when :xml_simple, :xml_node 
     339            body.blank? ? {} : Hash.from_xml(body).with_indifferent_access 
     340          when :yaml 
     341            YAML.load(body) 
     342          else 
     343            {} 
     344        end 
     345      rescue Exception => e # YAML, XML or Ruby code block errors 
     346        raise 
     347        { "body" => body, 
     348          "content_type" => content_type, 
     349          "content_length" => content_length, 
     350          "exception" => "#{e.message} (#{e.class})", 
     351          "backtrace" => e.backtrace } 
     352      end 
     353 
     354      def parse_query_parameters(query_string) 
     355        return {} if query_string.blank? 
     356 
     357        pairs = query_string.split('&').collect do |chunk| 
     358          next if chunk.empty? 
     359          key, value = chunk.split('=', 2) 
     360          next if key.empty? 
     361          value = value.nil? ? nil : CGI.unescape(value) 
     362          [ CGI.unescape(key), value ] 
     363        end.compact 
     364 
     365        UrlEncodedPairParser.new(pairs).result 
     366      end 
     367 
     368      def parse_request_parameters(params) 
     369        parser = UrlEncodedPairParser.new 
     370 
     371        params = params.dup 
     372        until params.empty? 
     373          for key, value in params 
     374            if key.blank? 
     375              params.delete key 
     376            elsif !key.include?('[') 
     377              # much faster to test for the most common case first (GET) 
     378              # and avoid the call to build_deep_hash 
     379              parser.result[key] = get_typed_value(value[0]) 
     380              params.delete key 
     381            elsif value.is_a?(Array) 
     382              parser.parse(key, get_typed_value(value.shift)) 
     383              params.delete key if value.empty? 
     384            else 
     385              raise TypeError, "Expected array, found #{value.inspect}" 
     386            end 
     387          end 
     388        end 
     389 
     390        parser.result 
     391      end 
     392 
     393      def parse_multipart_form_parameters(body, boundary, content_length, env) 
     394        parse_request_parameters(read_multipart(body, boundary, content_length, env)) 
     395      end 
     396 
     397      private 
     398        def get_typed_value(value) 
     399          case value 
     400            when String 
     401              value 
     402            when NilClass 
     403              '' 
     404            when Array 
     405              value.map { |v| get_typed_value(v) } 
     406            else 
     407              # Uploaded file provides content type and filename. 
     408              if value.respond_to?(:content_type) && 
     409                    !value.content_type.blank? && 
     410                    !value.original_filename.blank? 
     411                unless value.respond_to?(:full_original_filename) 
     412                  class << value 
     413                    alias_method :full_original_filename, :original_filename 
     414 
     415                    # Take the basename of the upload's original filename. 
     416                    # This handles the full Windows paths given by Internet Explorer 
     417                    # (and perhaps other broken user agents) without affecting 
     418                    # those which give the lone filename. 
     419                    # The Windows regexp is adapted from Perl's File::Basename. 
     420                    def original_filename 
     421                      if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename) 
     422                        md.captures.first 
     423                      else 
     424                        File.basename full_original_filename 
     425                      end 
     426                    end 
     427                  end 
     428                end 
     429 
     430                # Return the same value after overriding original_filename. 
     431                value 
     432 
     433              # Multipart values may have content type, but no filename. 
     434              elsif value.respond_to?(:read) 
     435                result = value.read 
     436                value.rewind 
     437                result 
     438 
     439              # Unknown value, neither string nor multipart. 
     440              else 
     441                raise "Unknown form value: #{value.inspect}" 
     442              end 
     443          end 
     444        end 
     445 
     446 
     447        MULTIPART_BOUNDARY = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n 
     448 
     449        def extract_multipart_boundary(content_type) 
     450          if content_type =~ MULTIPART_BOUNDARY 
     451            ['multipart/form-data', $1.dup] 
     452          else 
     453            content_type 
     454          end 
     455        end 
     456 
     457        def clean_up_ajax_request_body!(body) 
     458          body.chop! if body[-1] == 0 
     459          body.gsub!(/&_=$/, '') 
     460        end 
     461 
     462 
     463        EOL = "\015\012" 
     464 
     465        def read_multipart(body, boundary, content_length, env) 
     466          params = Hash.new([]) 
     467          boundary = "--" + boundary 
     468          quoted_boundary = Regexp.quote(boundary, "n") 
     469          buf = "" 
     470          bufsize = 10 * 1024 
     471          boundary_end="" 
     472 
     473          # start multipart/form-data 
     474          body.binmode if defined? body.binmode 
     475          boundary_size = boundary.size + EOL.size 
     476          content_length -= boundary_size 
     477          status = body.read(boundary_size) 
     478          if nil == status 
     479            raise EOFError, "no content body" 
     480          elsif boundary + EOL != status 
     481            raise EOFError, "bad content body" 
     482          end 
     483 
     484          loop do 
     485            head = nil 
     486            content = 
     487              if 10240 < content_length 
     488                Tempfile.new("CGI") 
     489              else 
     490                StringIO.new 
     491              end 
     492            content.binmode if defined? content.binmode 
     493 
     494            until head and /#{quoted_boundary}(?:#{EOL}|--)/n.match(buf) 
     495 
     496              if (not head) and /#{EOL}#{EOL}/n.match(buf) 
     497                buf = buf.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do 
     498                  head = $1.dup 
     499                  "" 
     500                end 
     501                next 
     502              end 
     503 
     504              if head and ( (EOL + boundary + EOL).size < buf.size ) 
     505                content.print buf[0 ... (buf.size - (EOL + boundary + EOL).size)] 
     506                buf[0 ... (buf.size - (EOL + boundary + EOL).size)] = "" 
     507              end 
     508 
     509              c = if bufsize < content_length 
     510                    body.read(bufsize) 
     511                  else 
     512                    body.read(content_length) 
     513                  end 
     514              if c.nil? || c.empty? 
     515                raise EOFError, "bad content body" 
     516              end 
     517              buf.concat(c) 
     518              content_length -= c.size 
     519            end 
     520 
     521            buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do 
     522              content.print $1 
     523              if "--" == $2 
     524                content_length = -1 
     525              end 
     526             boundary_end = $2.dup 
     527              "" 
     528            end 
     529 
     530            content.rewind 
     531 
     532            /Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;]*))/ni.match(head) 
     533            filename = ($1 or $2 or "") 
     534            if /Mac/ni.match(env['HTTP_USER_AGENT']) and 
     535                /Mozilla/ni.match(env['HTTP_USER_AGENT']) and 
     536                (not /MSIE/ni.match(env['HTTP_USER_AGENT'])) 
     537              filename = CGI.unescape(filename) 
     538            end 
     539 
     540            /Content-Type: (.*)/ni.match(head) 
     541            content_type = ($1 or "") 
     542 
     543            (class << content; self; end).class_eval do 
     544              alias local_path path 
     545              define_method(:original_filename) {filename.dup.taint} 
     546              define_method(:content_type) {content_type.dup.taint} 
     547            end 
     548 
     549            /Content-Disposition:.* name="?([^\";]*)"?/ni.match(head) 
     550            name = $1.dup 
     551 
     552            if params.has_key?(name) 
     553              params[name].push(content) 
     554            else 
     555              params[name] = [content] 
     556            end 
     557            break if buf.size == 0 
     558            break if content_length == -1 
     559          end 
     560          raise EOFError, "bad boundary end of body part" unless boundary_end=~/--/ 
     561 
     562          params 
     563        end 
     564    end 
     565  end 
     566 
     567  class UrlEncodedPairParser < StringScanner #:nodoc: 
     568    attr_reader :top, :parent, :result 
     569 
     570    def initialize(pairs = []) 
     571      super('') 
     572      @result = {} 
     573      pairs.each { |key, value| parse(key, value) } 
     574    end 
     575 
     576    KEY_REGEXP = %r{([^\[\]=&]+)} 
     577    BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]} 
     578 
     579    # Parse the query string 
     580    def parse(key, value) 
     581      self.string = key 
     582      @top, @parent = result, nil 
     583 
     584      # First scan the bare key 
     585      key = scan(KEY_REGEXP) or return 
     586      key = post_key_check(key) 
     587 
     588      # Then scan as many nestings as present 
     589      until eos? 
     590        r = scan(BRACKETED_KEY_REGEXP) or return 
     591        key = self[1] 
     592        key = post_key_check(key) 
     593      end 
     594 
     595      bind(key, value) 
     596    end 
     597 
     598    private 
     599      # After we see a key, we must look ahead to determine our next action. Cases: 
     600      # 
     601      #   [] follows the key. Then the value must be an array. 
     602      #   = follows the key. (A value comes next) 
     603      #   & or the end of string follows the key. Then the key is a flag. 
     604      #   otherwise, a hash follows the key. 
     605      def post_key_check(key) 
     606        if scan(/\[\]/) # a[b][] indicates that b is an array 
     607          container(key, Array) 
     608          nil 
     609        elsif check(/\[[^\]]/) # a[b] indicates that a is a hash 
     610          container(key, Hash) 
     611          nil 
     612        else # End of key? We do nothing. 
     613          key 
     614        end 
     615      end 
     616 
     617      # Add a container to the stack. 
     618      def container(key, klass) 
     619        type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass) 
     620        value = bind(key, klass.new) 
     621        type_conflict! klass, value unless value.is_a?(klass) 
     622        push(value) 
     623      end 
     624 
     625      # Push a value onto the 'stack', which is actually only the top 2 items. 
     626      def push(value) 
     627        @parent, @top = @top, value 
     628      end 
     629 
     630      # Bind a key (which may be nil for items in an array) to the provided value. 
     631      def bind(key, value) 
     632        if top.is_a? Array 
     633          if key 
     634            if top[-1].is_a?(Hash) && ! top[-1].key?(key) 
     635              top[-1][key] = value 
     636            else 
     637              top << {key => value}.with_indifferent_access 
     638              push top.last 
     639            end 
     640          else 
     641            top << value 
     642          end 
     643        elsif top.is_a? Hash 
     644          key = CGI.unescape(key) 
     645          parent << (@top = {}) if top.key?(key) && parent.is_a?(Array) 
     646          return top[key] ||= value 
    308647        else 
    309           {} 
    310       end 
    311     rescue Exception => e # YAML, XML or Ruby code block errors 
    312       { "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace, 
    313         "body" => body, "format" => mime_type } 
    314     end 
     648          raise ArgumentError, "Don't know what to do: top is #{top.inspect}" 
     649        end 
     650 
     651        return value 
     652      end 
     653 
     654      def type_conflict!(klass, value) 
     655        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." 
     656      end 
    315657  end 
    316658end 
  • trunk/actionpack/test/controller/cgi_test.rb

    r6742 r6764  
    22require 'action_controller/cgi_process' 
    33 
    4 class CGITest < Test::Unit::TestCase 
    5   def setup 
    6     @query_string = "action=create_customer&full_name=David%20Heinemeier%20Hansson&customerId=1" 
    7     @query_string_with_empty = "action=create_customer&full_name=" 
    8     @query_string_with_array = "action=create_customer&selected[]=1&selected[]=2&selected[]=3" 
    9     @query_string_with_amps  = "action=create_customer&name=Don%27t+%26+Does" 
    10     @query_string_with_multiple_of_same_name =  
    11       "action=update_order&full_name=Lau%20Taarnskov&products=4&products=2&products=3" 
    12     @query_string_with_many_equal = "action=create_customer&full_name=abc=def=ghi" 
    13     @query_string_without_equal = "action" 
    14     @query_string_with_many_ampersands = 
    15       "&action=create_customer&&&full_name=David%20Heinemeier%20Hansson" 
    16     @query_string_with_empty_key = "action=create_customer&full_name=David%20Heinemeier%20Hansson&=Save" 
    17   end 
    18  
    19   def test_query_string 
    20     assert_equal( 
    21       { "action" => "create_customer", "full_name" => "David Heinemeier Hansson", "customerId" => "1"}, 
    22       CGI.parse_query_parameters(@query_string) 
    23     ) 
    24   end 
    25    
    26   def test_deep_query_string 
    27     expected = {'x' => {'y' => {'z' => '10'}}} 
    28     assert_equal(expected, CGI.parse_query_parameters('x[y][z]=10')) 
    29   end 
    30    
    31   def test_deep_query_string_with_array 
    32     assert_equal({'x' => {'y' => {'z' => ['10']}}}, CGI.parse_query_parameters('x[y][z][]=10')) 
    33     assert_equal({'x' => {'y' => {'z' => ['10', '5']}}}, CGI.parse_query_parameters('x[y][z][]=10&x[y][z][]=5')) 
    34   end 
    35  
    36   def test_deep_query_string_with_array_of_hash 
    37     assert_equal({'x' => {'y' => [{'z' => '10'}]}}, CGI.parse_query_parameters('x[y][][z]=10')) 
    38     assert_equal({'x' => {'y' => [{'z' => '10', 'w' => '10'}]}}, CGI.parse_query_parameters('x[y][][z]=10&x[y][][w]=10')) 
    39   end 
    40  
    41   def test_deep_query_string_with_array_of_hashes_with_one_pair 
    42     assert_equal({'x' => {'y' => [{'z' => '10'}, {'z' => '20'}]}}, CGI.parse_query_parameters('x[y][][z]=10&x[y][][z]=20')) 
    43     assert_equal("10", CGI.parse_query_parameters('x[y][][z]=10&x[y][][z]=20')["x"]["y"].first["z"]) 
    44     assert_equal("10", CGI.parse_query_parameters('x[y][][z]=10&x[y][][z]=20').with_indifferent_access[:x][:y].first[:z]) 
    45   end 
    46    
    47   def test_request_hash_parsing 
    48     query = { 
    49       "note[viewers][viewer][][type]" => ["User", "Group"], 
    50       "note[viewers][viewer][][id]"   => ["1", "2"] 
    51     } 
    52  
    53     expected = { "note" => { "viewers"=>{"viewer"=>[{ "id"=>"1", "type"=>"User"}, {"type"=>"Group", "id"=>"2"} ]} } } 
    54  
    55     assert_equal(expected, CGI.parse_request_parameters(query)) 
    56   end 
    57    
    58   def test_deep_query_string_with_array_of_hashes_with_multiple_pairs 
    59     assert_equal( 
    60       {'x' => {'y' => [{'z' => '10', 'w' => 'a'}, {'z' => '20', 'w' => 'b'}]}},  
    61       CGI.parse_query_parameters('x[y][][z]=10&x[y][][w]=a&x[y][][z]=20&x[y][][w]=b') 
    62     ) 
    63   end 
    64    
    65   def test_query_string_with_nil 
    66     assert_equal( 
    67       { "action" => "create_customer", "full_name" => ''}, 
    68       CGI.parse_query_parameters(@query_string_with_empty) 
    69     ) 
    70   end 
    71  
    72   def test_query_string_with_array 
    73     assert_equal( 
    74       { "action" => "create_customer", "selected" => ["1", "2", "3"]}, 
    75       CGI.parse_query_parameters(@query_string_with_array) 
    76     ) 
    77   end 
    78  
    79   def test_query_string_with_amps 
    80     assert_equal( 
    81       { "action" => "create_customer", "name" => "Don't & Does"}, 
    82       CGI.parse_query_parameters(@query_string_with_amps) 
    83     )     
    84   end 
    85    
    86   def test_query_string_with_many_equal 
    87     assert_equal( 
    88       { "action" => "create_customer", "full_name" => "abc=def=ghi"}, 
    89       CGI.parse_query_parameters(@query_string_with_many_equal) 
    90     )     
    91   end 
    92    
    93   def test_query_string_without_equal 
    94     assert_equal( 
    95       { "action" => nil }, 
    96       CGI.parse_query_parameters(@query_string_without_equal) 
    97     )     
    98   end 
    99    
    100   def test_query_string_with_empty_key 
    101     assert_equal( 
    102       { "action" => "create_customer", "full_name" => "David Heinemeier Hansson" }, 
    103       CGI.parse_query_parameters(@query_string_with_empty_key) 
    104     ) 
    105   end 
    106  
    107   def test_query_string_with_many_ampersands 
    108     assert_equal( 
    109       { "action" => "create_customer", "full_name" => "David Heinemeier Hansson"}, 
    110       CGI.parse_query_parameters(@query_string_with_many_ampersands) 
    111     ) 
    112   end 
    113  
    114   def test_parse_params 
    115     input = { 
    116       "customers[boston][first][name]" => [ "David" ], 
    117       "customers[boston][first][url]" => [ "http://David" ], 
    118       "customers[boston][second][name]" => [ "Allan" ], 
    119       "customers[boston][second][url]" => [ "http://Allan" ], 
    120       "something_else" => [ "blah" ], 
    121       "something_nil" => [ nil ], 
    122       "something_empty" => [ "" ], 
    123       "products[first]" => [ "Apple Computer" ], 
    124       "products[second]" => [ "Pc" ], 
    125       "" => [ 'Save' ] 
    126     } 
    127      
    128     expected_output =  { 
    129       "customers" => { 
    130         "boston" => { 
    131           "first" => { 
    132             "name" => "David", 
    133             "url" => "http://David" 
    134           }, 
    135           "second" => { 
    136             "name" => "Allan", 
    137             "url" => "http://Allan" 
    138           } 
    139         } 
    140       }, 
    141       "something_else" => "blah", 
    142       "something_empty" => "", 
    143       "something_nil" => "", 
    144       "products" => { 
    145         "first" => "Apple Computer", 
    146         "second" => "Pc" 
    147       } 
    148     } 
    149  
    150     assert_equal expected_output, CGI.parse_request_parameters(input) 
    151   end 
    152    
    153   def test_parse_params_from_multipart_upload 
    154     mockup = Struct.new(:content_type, :original_filename, :read, :rewind) 
    155     file = mockup.new('img/jpeg', 'foo.jpg') 
    156     ie_file = mockup.new('img/jpeg', 'c:\\Documents and Settings\\foo\\Desktop\\bar.jpg') 
    157     non_file_text_part = mockup.new('text/plain', '', 'abc') 
    158    
    159     input = { 
    160       "something" => [ StringIO.new("") ], 
    161       "array_of_stringios" => [[ StringIO.new("One"), StringIO.new("Two") ]], 
    162       "mixed_types_array" => [[ StringIO.new("Three"), "NotStringIO" ]], 
    163       "mixed_types_as_checkboxes[strings][nested]" => [[ file, "String", StringIO.new("StringIO")]], 
    164       "ie_mixed_types_as_checkboxes[strings][nested]" => [[ ie_file, "String", StringIO.new("StringIO")]], 
    165       "products[string]" => [ StringIO.new("Apple Computer") ], 
    166       "products[file]" => [ file ], 
    167       "ie_products[string]" => [ StringIO.new("Microsoft") ], 
    168       "ie_products[file]" => [ ie_file ], 
    169       "text_part" => [non_file_text_part] 
    170     } 
    171  
    172     expected_output =  { 
    173       "something" => "", 
    174       "array_of_stringios" => ["One", "Two"], 
    175       "mixed_types_array" => [ "Three", "NotStringIO" ], 
    176       "mixed_types_as_checkboxes" => { 
    177          "strings" => { 
    178             "nested" => [ file, "String", "StringIO" ] 
    179          }, 
    180       }, 
    181       "ie_mixed_types_as_checkboxes" => { 
    182          "strings" => { 
    183             "nested" => [ ie_file, "String", "StringIO" ] 
    184          }, 
    185       }, 
    186       "products" => { 
    187         "string" => "Apple Computer", 
    188         "file" => file 
    189       }, 
    190       "ie_products" => { 
    191         "string" => "Microsoft", 
    192         "file" => ie_file 
    193       }, 
    194       "text_part" => "abc" 
    195     } 
    196  
    197     params = CGI.parse_request_parameters(input) 
    198     assert_equal expected_output, params 
    199  
    200     # Lone filenames are preserved. 
    201     assert_equal 'foo.jpg', params['mixed_types_as_checkboxes']['strings']['nested'].first.original_filename 
    202     assert_equal 'foo.jpg', params['products']['file'].original_filename 
    203  
    204     # But full Windows paths are reduced to their basename. 
    205     assert_equal 'bar.jpg', params['ie_mixed_types_as_checkboxes']['strings']['nested'].first.original_filename 
    206     assert_equal 'bar.jpg', params['ie_products']['file'].original_filename 
    207   end 
    208    
    209   def test_parse_params_with_file 
    210     input = { 
    211       "customers[boston][first][name]" => [ "David" ], 
    212       "something_else" => [ "blah" ], 
    213       "logo" => [ File.new(File.dirname(__FILE__) + "/cgi_test.rb").path ] 
    214     } 
    215      
    216     expected_output = { 
    217       "customers" => { 
    218         "boston" => { 
    219           "first" => { 
    220             "name" => "David" 
    221           } 
    222         } 
    223       }, 
    224       "something_else" => "blah", 
    225       "logo" => File.new(File.dirname(__FILE__) + "/cgi_test.rb").path, 
    226     } 
    227  
    228     assert_equal expected_output, CGI.parse_request_parameters(input) 
    229   end 
    230  
    231   def test_parse_params_with_array 
    232     input = { "selected[]" =>  [ "1", "2", "3" ] } 
    233  
    234     expected_output = { "selected" => [ "1", "2", "3" ] } 
    235  
    236     assert_equal expected_output, CGI.parse_request_parameters(input) 
    237   end 
    238  
    239   def test_parse_params_with_non_alphanumeric_name 
    240     input     = { "a/b[c]" =>  %w(d) } 
    241     expected  = { "a/b" => { "c" => "d" }} 
    242     assert_equal expected, CGI.parse_request_parameters(input) 
    243   end 
    244  
    245   def test_parse_params_with_single_brackets_in_middle 
    246     input     = { "a/b[c]d" =>  %w(e) } 
    247     expected  = { "a/b" => {} } 
    248     assert_equal expected, CGI.parse_request_parameters(input) 
    249   end 
    250  
    251   def test_parse_params_with_separated_brackets 
    252     input     = { "a/b@[c]d[e]" =>  %w(f) } 
    253     expected  = { "a/b@" => { }} 
    254     assert_equal expected, CGI.parse_request_parameters(input) 
    255   end 
    256  
    257   def test_parse_params_with_separated_brackets_and_array 
    258     input     = { "a/b@[c]d[e][]" =>  %w(f) } 
    259     expected  = { "a/b@" => { }} 
    260     assert_equal expected , CGI.parse_request_parameters(input) 
    261   end 
    262  
    263   def test_parse_params_with_unmatched_brackets_and_array 
    264     input     = { "a/b@[c][d[e][]" =>  %w(f) } 
    265     expected  = { "a/b@" => { "c" => { }}} 
    266     assert_equal expected, CGI.parse_request_parameters(input) 
    267   end 
    268    
    269   def test_parse_params_with_nil_key 
    270     input = { nil => nil, "test2" => %w(value1) } 
    271     expected = { "test2" => "value1" } 
    272     assert_equal expected, CGI.parse_request_parameters(input) 
    273   end 
    274 end 
    275  
    276  
    277 class MultipartCGITest < Test::Unit::TestCase 
    278   FIXTURE_PATH = File.dirname(__FILE__) + '/../fixtures/multipart' 
    279  
    280   def setup 
    281     ENV['REQUEST_METHOD'] = 'POST' 
    282     ENV['CONTENT_LENGTH'] = '0' 
    283     ENV['CONTENT_TYPE']   = 'multipart/form-data, boundary=AaB03x' 
    284   end 
    285  
    286   def test_single_parameter 
    287     params = process('single_parameter') 
    288     assert_equal({ 'foo' => 'bar' }, params) 
    289   end 
    290  
    291   def test_text_file 
    292     params = process('text_file') 
    293     assert_equal %w(file foo), params.keys.sort 
    294     assert_equal 'bar', params['foo'] 
    295  
    296     file = params['file'] 
    297     assert_kind_of StringIO, file 
    298     assert_equal 'file.txt', file.original_filename 
    299     assert_equal "text/plain\r", file.content_type 
    300     assert_equal 'contents', file.read 
    301   end 
    302  
    303   def test_large_text_file 
    304     params = process('large_text_file') 
    305     assert_equal %w(file foo), params.keys.sort 
    306     assert_equal 'bar', params['foo'] 
    307  
    308     file = params['file'] 
    309     assert_kind_of Tempfile, file 
    310     assert_equal 'file.txt', file.original_filename 
    311     assert_equal "text/plain\r", file.content_type 
    312     assert ('a' * 20480) == file.read 
    313   end 
    314  
    315   def test_binary_file 
    316     params = process('binary_file') 
    317     assert_equal %w(file flowers foo), params.keys.sort 
    318     assert_equal 'bar', params['foo'] 
    319  
    320     file = params['file'] 
    321     assert_kind_of StringIO, file 
    322     assert_equal 'file.txt', file.original_filename 
    323     assert_equal "text/plain\r", file.content_type 
    324     assert_equal 'contents', file.read 
    325  
    326     file = params['flowers'] 
    327     assert_kind_of StringIO, file 
    328     assert_equal 'flowers.jpg', file.original_filename 
    329     assert_equal "image/jpeg\r", file.content_type 
    330     assert_equal 19512, file.size 
    331     #assert_equal File.read(File.dirname(__FILE__) + '/../../../activerecord/test/fixtures/flowers.jpg'), file.read 
    332   end 
    333  
    334   def test_mixed_files 
    335     params = process('mixed_files') 
    336     assert_equal %w(files foo), params.keys.sort 
    337     assert_equal 'bar', params['foo'] 
    338  
    339     # Ruby CGI doesn't handle multipart/mixed for us. 
    340     assert_kind_of String, params['files'] 
    341     assert_equal 19756, params['files'].size 
    342   end 
    343  
    344   # Rewind readable cgi params so others may reread them (such as CGI::Session 
    345   # when passing the session id in a multipart form). 
    346   def test_multipart_param_rewound 
    347     params = process('text_file') 
    348     assert_equal 'bar', @cgi.params['foo'][0].read 
    349   end 
    350  
    351   private 
    352     def process(name) 
    353       File.open(File.join(FIXTURE_PATH, name), 'rb') do |file| 
    354         ENV['CONTENT_LENGTH'] = file.stat.size.to_s 
    355         @cgi = CGI.new('query', file) 
    356         CGI.parse_request_parameters @cgi.params 
    357       end 
    358     end 
    359 end 
    360  
    361 # Ensures that PUT works with multipart as well as POST. 
    362 class PutMultipartCGITest < MultipartCGITest 
    363   def setup 
    364     super 
    365     ENV['REQUEST_METHOD'] = 'PUT' 
    366   end 
    367 end 
    368  
    369  
    370 class CGIRequestTest < Test::Unit::TestCase 
     4class CgiRequestTest < Test::Unit::TestCase 
    3715  def setup 
    3726    @request_hash = {"HTTP_MAX_FORWARDS"=>"10", "SERVER_NAME"=>"glu.ttono.us:8007", "FCGI_ROLE"=>"RESPONDER", "HTTP_X_FORWARDED_HOST"=>"glu.ttono.us", "HTTP_ACCEPT_ENCODING"=>"gzip, deflate", "HTTP_USER_AGENT"=>"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1", "PATH_INFO"=>"", "HTTP_ACCEPT_LANGUAGE"=>"en", "HTTP_HOST"=>"glu.ttono.us:8007", "SERVER_PROTOCOL"=>"HTTP/1.1", "REDIRECT_URI"=>"/dispatch.fcgi", "SCRIPT_NAME"=>"/dispatch.fcgi", "SERVER_ADDR"=>"207.7.108.53", "REMOTE_ADDR"=>"207.7.108.53", "SERVER_SOFTWARE"=>"lighttpd/1.4.5", "HTTP_COOKIE"=>"_session_id=c84ace84796670c052c6ceb2451fb0f2; is_admin=yes", "HTTP_X_FORWARDED_SERVER"=>"glu.ttono.us", "REQUEST_URI"=>"/admin", "DOCUMENT_ROOT"=>"/home/kevinc/sites/typo/public", "SERVER_PORT"=>"8007", "QUERY_STRING"=>"", "REMOTE_PORT"=>"63137", "GATEWAY_INTERFACE"=>"CGI/1.1", "HTTP_X_FORWARDED_FOR"=>"65.88.180.234", "HTTP_ACCEPT"=>"*/*", "SCRIPT_FILENAME"=>"/home/kevinc/sites/typo/public/dispatch.fcgi", "REDIRECT_STATUS"=>"200", "REQUEST_METHOD"=>"GET"} 
     
    37610    @request = ActionController::CgiRequest.new(@fake_cgi) 
    37711  end 
    378    
     12 
    37913  def test_proxy_request 
    38014    assert_equal 'glu.ttono.us', @request.host_with_port 
    38115  end 
    382    
     16 
    38317  def test_http_host 
    38418    @request_hash.delete "HTTP_X_FORWARDED_HOST" 
    38519    @request_hash['HTTP_HOST'] = "rubyonrails.org:8080" 
    38620    assert_equal "rubyonrails.org:8080", @request.host_with_port 
    387      
     21 
    38822    @request_hash['HTTP_X_FORWARDED_HOST'] = "www.firsthost.org, www.secondhost.org" 
    38923    assert_equal "www.secondhost.org", @request.host 
    39024  end 
    391    
     25 
    39226  def test_http_host_with_default_port_overrides_server_port 
    39327    @request_hash.delete "HTTP_X_FORWARDED_HOST" 
     
    41347    assert_equal ["c84ace84796670c052c6ceb2451fb0f2"], cookies["_session_id"] 
    41448    assert_equal ["yes"], cookies["is_admin"] 
    415      
     49 
    41650    alt_cookies = CGI::Cookie::parse(@alt_cookie_fmt_request_hash["HTTP_COOKIE"]); 
    41751    assert_equal ["c84ace84796670c052c6ceb2451fb0f2"], alt_cookies["_session_id"] 
    41852    assert_equal ["yes"], alt_cookies["is_admin"] 
    41953  end 
    420    
    421   def test_unbalanced_query_string_with_array 
    422    assert_equal( 
    423      {'location' => ["1", "2"], 'age_group' => ["2"]}, 
    424   CGI.parse_query_parameters("location[]=1&location[]=2&age_group[]=2") 
    425    ) 
    426    assert_equal( 
    427      {'location' => ["1", "2"], 'age_group' => ["2"]}, 
    428      CGI.parse_request_parameters({'location[]' => ["1", "2"], 
    429   'age_group[]' => ["2"]}) 
    430    ) 
    431   end 
    43254end 
  • trunk/actionpack/test/controller/mime_responds_test.rb

    r6517 r6764  
    233233  end 
    234234   
    235   def test_with_content_type 
     235  def test_with_atom_content_type 
    236236    @request.env["CONTENT_TYPE"] = "application/atom+xml" 
    237237    get :made_for_content_type 
    238238    assert_equal "ATOM", @response.body 
    239  
     239  end 
     240 
     241  def test_with_rss_content_type 
    240242    @request.env["CONTENT_TYPE"] = "application/rss+xml" 
    241243    get :made_for_content_type 
  • trunk/actionpack/test/controller/request_test.rb

    r6742 r6764  
    4545    @request.host = "www.rubyonrails.co.uk" 
    4646    assert_equal "rubyonrails.co.uk", @request.domain(2) 
    47      
     47 
    4848    @request.host = "192.168.1.200" 
    4949    assert_nil @request.domain 
    …<