1 # frozen_string_literal: true
6 require_relative 'update_env'
11 ## boundary = 'foobar1234' # or nil
12 ## multipart = MultiPart.new(boundary)
13 ## multipart.append('name1', 'value1')
14 ## multipart.append('file1', File.read('file1.html'), 'file1.html')
15 ## str = multipart.close()
16 ## str.each_line {|line| p line }
18 ## # "--foobar1234\r\n"
19 ## # "Content-Disposition: form-data: name=\"name1\"\r\n"
22 ## # "--foobar1234\r\n"
23 ## # "Content-Disposition: form-data: name=\"file1\"; filename=\"file1.html\"\r\n"
24 ## # "Content-Type: text/html\r\n"
27 ## # "<body><p>Hello</p></body>\n"
30 ## # "--foobar1234--\r\n"
34 def initialize(boundary=nil)
35 @boundary = boundary || create_boundary()
37 @buf.force_encoding(::Encoding::ASCII_8BIT) if defined?(::Encoding)
41 def append(name, value, filename=nil, content_type=nil)
42 content_type = detect_content_type(filename) if filename && content_type.nil?
43 s = filename ? "; filename=\"#{filename}\"" : ''
45 buf << "--#{boundary}\r\n"
46 buf << "Content-Disposition: form-data: name=\"#{name}\"#{s}\r\n"
47 buf << "Content-Type: #{content_type}\r\n" if content_type
57 return buf << "--#{boundary}--\r\n"
60 def create_boundary() #:nodoc:
61 return "--boundary#{rand().to_s[2..-1]}"
64 def detect_content_type(filename) #:nodoc:
65 filename =~ /\.(\w+)\z/
66 return MIME_TYPES[$1] || 'application/octet-stream'
71 'jpg' => 'image/jpeg',
72 'jpeg' => 'image/jpeg',
75 'tif' => 'image/tiff',
76 'tiff' => 'image/tiff',
78 'html' => 'text/html',
80 'txt' => 'text/plain',
81 'text' => 'text/plain',
83 'mpg' => 'video/mpeg',
84 'mpeg' => 'video/mpeg',
85 'mov' => 'video/quicktime',
86 'avi' => 'video/x-msvideo',
87 'mp3' => 'audio/mpeg',
88 'mid' => 'audio/midi',
89 'wav' => 'audio/x-wav',
90 'zip' => 'application/zip',
91 #'tar.gz' => 'application/gtar',
92 'gz' => 'application/gzip',
93 'bz2' => 'application/bzip2',
94 'rtf' => 'application/rtf',
95 'pdf' => 'application/pdf',
96 'ps' => 'application/postscript',
97 'js' => 'application/x-javascript',
98 'xls' => 'application/vnd.ms-excel',
99 'doc' => 'application/msword',
100 'ppt' => 'application/vnd.ms-powerpoint',
107 class CGIMultipartTest < Test::Unit::TestCase
114 'REQUEST_METHOD' => 'POST',
115 'CONTENT_TYPE' => nil,
116 'CONTENT_LENGTH' => nil,
123 $stdin.close() if $stdin.is_a?(Tempfile)
131 ## create multipart input
132 multipart = MultiPart.new(defined?(@boundary) ? @boundary : nil)
134 multipart.append(hash[:name], hash[:value], hash[:filename])
136 input = multipart.close()
137 input = yield(input) if block_given?
138 #$stderr.puts "*** debug: input=\n#{input.collect{|line| line.inspect}.join("\n")}"
139 @boundary ||= multipart.boundary
141 ENV['CONTENT_TYPE'] = "multipart/form-data; boundary=#{@boundary}"
142 ENV['CONTENT_LENGTH'] = input.length.to_s
143 ENV['REQUEST_METHOD'] = 'POST'
145 tmpfile = Tempfile.new('test_cgi_multipart')
146 @tempfiles << tmpfile
153 def _test_multipart(cgi_options={})
154 caller(0).find {|s| s =~ /in `test_(.*?)'/ }
156 #$stderr.puts "*** debug: testname=#{testname.inspect}"
158 options = {:accept_charset=>"UTF-8"}
159 options.merge! cgi_options
160 cgi = CGI.new(options)
161 expected_names = @data.collect{|hash| hash[:name] }.sort
162 assert_equal(expected_names, cgi.params.keys.sort)
166 expected = hash[:value]
167 if hash[:filename] #if file
168 expected_class = @expected_class || (hash[:value].length < threshold ? StringIO : Tempfile)
169 assert(cgi.files.keys.member?(hash[:name]))
171 expected_class = String
172 assert_equal(expected, cgi[name])
173 assert_equal(false,cgi.files.keys.member?(hash[:name]))
175 assert_kind_of(expected_class, cgi[name])
176 assert_equal(expected, cgi[name].read())
177 assert_equal(hash[:filename] || '', cgi[name].original_filename) #if hash[:filename]
178 assert_equal(hash[:content_type] || '', cgi[name].content_type) #if hash[:content_type]
182 cgi.params.each {|name, vals|
184 if val.kind_of?(Tempfile) && val.path
194 filename = File.join(File.dirname(__FILE__), 'testdata', basename)
195 s = File.open(filename, 'rb') {|f| f.read() }
201 def test_cgi_multipart_stringio
202 @boundary = '----WebKitFormBoundaryAAfvAII+YL9102cX'
204 {:name=>'hidden1', :value=>'foobar'},
205 {:name=>'text1', :value=>"\xE3\x81\x82\xE3\x81\x84\xE3\x81\x86\xE3\x81\x88\xE3\x81\x8A".dup},
206 {:name=>'file1', :value=>_read('file1.html'),
207 :filename=>'file1.html', :content_type=>'text/html'},
208 {:name=>'image1', :value=>_read('small.png'),
209 :filename=>'small.png', :content_type=>'image/png'}, # small image
211 @data[1][:value].force_encoding(::Encoding::UTF_8) if defined?(::Encoding)
212 @expected_class = StringIO
217 def test_cgi_multipart_tempfile
218 @boundary = '----WebKitFormBoundaryAAfvAII+YL9102cX'
220 {:name=>'hidden1', :value=>'foobar'},
221 {:name=>'text1', :value=>"\xE3\x81\x82\xE3\x81\x84\xE3\x81\x86\xE3\x81\x88\xE3\x81\x8A".dup},
222 {:name=>'file1', :value=>_read('file1.html'),
223 :filename=>'file1.html', :content_type=>'text/html'},
224 {:name=>'image1', :value=>_read('large.png'),
225 :filename=>'large.png', :content_type=>'image/png'}, # large image
227 @data[1][:value].force_encoding(::Encoding::UTF_8) if defined?(::Encoding)
228 @expected_class = Tempfile
233 def _set_const(klass, name, value)
236 old = const_get(name)
238 const_set(name, value)
244 def test_cgi_multipart_maxmultipartlength
246 {:name=>'image1', :value=>_read('large.png'),
247 :filename=>'large.png', :content_type=>'image/png'}, # large image
250 ex = assert_raise(StandardError) do
251 _test_multipart(:max_multipart_length=>2 * 1024) # set via simple scalar
253 assert_equal("too large multipart data.", ex.message)
259 def test_cgi_multipart_maxmultipartlength_lambda
261 {:name=>'image1', :value=>_read('large.png'),
262 :filename=>'large.png', :content_type=>'image/png'}, # large image
265 ex = assert_raise(StandardError) do
266 _test_multipart(:max_multipart_length=>lambda{2*1024}) # set via lambda
268 assert_equal("too large multipart data.", ex.message)
274 def test_cgi_multipart_maxmultipartcount
276 {:name=>'file1', :value=>_read('file1.html'),
277 :filename=>'file1.html', :content_type=>'text/html'},
280 500.times { @data << item }
281 #original = _set_const(CGI, :MAX_MULTIPART_COUNT, 128)
283 ex = assert_raise(StandardError) do
286 assert_equal("too many parameters.", ex.message)
288 #_set_const(CGI, :MAX_MULTIPART_COUNT, original)
290 end if CGI.const_defined?(:MAX_MULTIPART_COUNT)
293 def test_cgi_multipart_badbody ## [ruby-dev:28470]
295 {:name=>'file1', :value=>_read('file1.html'),
296 :filename=>'file1.html', :content_type=>'text/html'},
298 _prepare(@data) do |input|
299 input2 = input.sub(/--(\r\n)?\z/, "\r\n")
300 assert input2 != input
304 ex = assert_raise(EOFError) do
305 CGI.new(:accept_charset=>"UTF-8")
307 assert_equal("bad content body", ex.message)
309 _prepare(@data) do |input|
310 input2 = input.sub(/--(\r\n)?\z/, "")
311 assert input2 != input
315 ex = assert_raise(EOFError) do
316 CGI.new(:accept_charset=>"UTF-8")
318 assert_equal("bad content body", ex.message)
322 def test_cgi_multipart_quoteboundary ## [JVN#84798830]
323 @boundary = '(.|\n)*'
325 {:name=>'hidden1', :value=>'foobar'},
326 {:name=>'text1', :value=>"\xE3\x81\x82\xE3\x81\x84\xE3\x81\x86\xE3\x81\x88\xE3\x81\x8A".dup},
327 {:name=>'file1', :value=>_read('file1.html'),
328 :filename=>'file1.html', :content_type=>'text/html'},
329 {:name=>'image1', :value=>_read('small.png'),
330 :filename=>'small.png', :content_type=>'image/png'}, # small image
332 @data[1][:value].force_encoding("UTF-8")
334 cgi = CGI.new(:accept_charset=>"UTF-8")
335 assert_equal('file1.html', cgi['file1'].original_filename)
338 def test_cgi_multipart_boundary_10240 # [Bug #3866]
341 {:name=>'file', :value=>"b"*10134,
342 :filename=>'file.txt', :content_type=>'text/plain'},
343 {:name=>'foo', :value=>"bar"},
346 cgi = CGI.new(:accept_charset=>"UTF-8")
347 assert_equal(cgi['foo'], 'bar')
348 assert_equal(cgi['file'].read, 'b'*10134)
349 cgi['file'].close! if cgi['file'].kind_of? Tempfile
352 def test_cgi_multipart_without_tempfile
353 assert_in_out_err([], <<-'EOM')
356 ENV['REQUEST_METHOD'] = 'POST'
357 ENV['CONTENT_TYPE'] = 'multipart/form-data; boundary=foobar1234'
358 body = <<-BODY.gsub(/\n/, "\r\n")
360 Content-Disposition: form-data: name=\"name1\"
364 Content-Disposition: form-data: name=\"file1\"; filename=\"file1.html\"
365 Content-Type: text/html
368 <body><p>Hello</p></body>
373 ENV['CONTENT_LENGTH'] = body.size.to_s
374 $stdin = StringIO.new(body)
381 self.instance_methods.each do |method|
382 private method if method =~ /^test_(.*)/ && $1 != ENV['TEST']