1#
2#===============================================================================
3#
4#         FILE:  uploads.t
5#
6#  DESCRIPTION:  Test of multipart/form-data uploads
7#
8#        FILES:  good_upload.txt
9#         BUGS:  ---
10#        NOTES:  This borrows very heavily from upload.t in CGI.pm
11#       AUTHOR:  Pete Houston (cpan@openstrike.co.uk)
12#      COMPANY:  Openstrike
13#      CREATED:  20/05/14 14:01:34
14#===============================================================================
15
16use strict;
17use warnings;
18
19use Test::More tests => 14269;
20
21use lib './lib';
22
23# Test exits and outputs;
24my $have_test_trap;
25our $trap; # Imported
26BEGIN {
27	eval {
28		require Test::Trap;
29		Test::Trap->import (qw/trap $trap :flow
30		:stderr(systemsafe)
31		:stdout(systemsafe)
32		:warn/);
33		$have_test_trap = 1;
34	};
35}
36
37BEGIN { use_ok ('CGI::Lite') }
38
39# Set up a CGI environment
40$ENV{REQUEST_METHOD}  = 'POST';
41$ENV{PATH_INFO}       = '/somewhere/else';
42$ENV{PATH_TRANSLATED} = '/usr/local/somewhere/else';
43$ENV{SCRIPT_NAME}     ='/cgi-bin/foo.cgi';
44$ENV{SERVER_PROTOCOL} = 'HTTP/1.0';
45$ENV{SERVER_PORT}     = 8080;
46$ENV{SERVER_NAME}     = 'there.is.no.try.com';
47$ENV{QUERY_STRING}    = '';
48my $datafile          = 't/good_upload.txt';
49$ENV{CONTENT_LENGTH}  = (stat ($datafile))[7];
50$ENV{CONTENT_TYPE}    = q#multipart/form-data; boundary=`!"$%^&*()-+[]{}'@.?~\#|aaa#;
51
52my $uploaddir = 'tmpcgilite';
53mkdir $uploaddir unless -d $uploaddir;
54
55
56my ($cgi, $form) = post_data ($datafile, $uploaddir);
57
58is ($cgi->is_error, 0, 'Parsing data with POST');
59like ($form->{'does_not_exist_gif'}, qr/[0-9]+__does_not_exist\.gif/, 'Second file');
60like ($form->{'100;100_gif'}, qr/[0-9]+__100;100\.gif/, 'Third file');
61like ($form->{'300x300_gif'}, qr/[0-9]+__300x300\.gif/, 'Fourth file');
62is ($cgi->get_upload_type ('300x300_gif'), 'image/gif', 'MIME Type');
63
64# Same, but check it can also return as a hash
65($cgi, $form) = post_data ($datafile, $uploaddir, undef, 1);
66is ($cgi->is_error, 0, 'Parsing data with POST into hash');
67like ($form->{'does_not_exist_gif'}, qr/[0-9]+__does_not_exist\.gif/,
68	'Second file from hash');
69like ($form->{'100;100_gif'}, qr/[0-9]+__100;100\.gif/,
70	'Third file from hash');
71like ($form->{'300x300_gif'}, qr/[0-9]+__300x300\.gif/,
72	'Fourth file from hash');
73
74my @files = (0, 0);
75
76is (ref $form->{'hello_world'}, 'ARRAY',
77	'Duplicate file fieldnames become array') and
78	@files = @{$form->{'hello_world'}};
79like ($files[0], qr/[0-9]+__goodbye_world\.txt/,
80	'First duplicate file has correct name');
81like ($files[1], qr/[0-9]+__hello_world\.txt/,
82	'Second duplicate file has correct name');
83my $res = $cgi->get_upload_type ('hello_world');
84ok (defined $res, 'Duplicate fields have upload type set');
85is (ref $res, 'ARRAY', 'Duplicate fields have array ref of upload types');
86is ($res->[0], 'text/plain', 'Duplicate fields have correct upload types');
87
88@files = qw/does_not_exist_gif 100;100_gif 300x300_gif/;
89my @sizes = qw/0 896 1656/;
90for my $i (0..2) {
91	my $file = "$uploaddir/$form->{$files[$i]}";
92	ok (-e $file, "Uploaded file exists ($i)") or warn "Name = '$file'\n" . $cgi->get_error_message;
93	is ((stat($file))[7], $sizes[$i], "File size check ($i)") or
94		warn_tail ($file);
95}
96
97is ($cgi->set_directory ('/srhslgvsgnlsenhglsgslvngh'), 0,
98	'Set directory (non-existant)');
99
100my $testdir = 'testperms';
101mkdir $testdir, 0400;
102SKIP: {
103	skip "subdir '$testdir' could not be created", 3 unless (-d $testdir);
104
105	# See http://www.perlmonks.org/?node_id=587550 for a discussion of
106	# the futility of chmod and friends on MS Windows systems.
107	SKIP: {
108		skip "Not available on $^O", 2 if ($^O eq 'MSWin32' or $^O eq 'cygwin');
109		skip "Running as privileged user: $ENV{USER}", 2 unless $>;
110		is ($cgi->set_directory ($testdir), 0, 'Set directory (unwriteable)');
111		chmod 0200, $testdir;
112		is ($cgi->set_directory ($testdir), 0, 'Set directory (unreadable)');
113	}
114	rmdir $testdir and open my $td, '>', $testdir;
115	print $td "Test\n";
116	close $td;
117	is ($cgi->set_directory ($testdir), 0, 'Set directory (non-directory)');
118	unlink $testdir;
119}
120
121# Mime type tests
122# Documentation says get_mime_types can return an arrayref, but
123# that seems not to be the case.
124
125my @mimetypes = $cgi->get_mime_types ();
126ok ($#mimetypes > 0, 'get_mime_types returns array');
127is_deeply (\@mimetypes, [ 'text/html', 'text/plain' ],
128	'default mime types');
129
130is ($cgi->add_mime_type (), 0, 'Undefined mime type');
131
132$cgi->add_mime_type ('application/json');
133@mimetypes = $cgi->get_mime_types ();
134is ($#mimetypes, 2, 'added a mime type');
135is ($mimetypes[0], 'application/json', 'added mime type is correct');
136is ($cgi->add_mime_type ('application/json'), 0, 'added mime type again');
137
138is ($cgi->remove_mime_type ('foo/bar'), 0,
139	'removed non-existant mime type');
140is ($cgi->remove_mime_type ('text/html'), 1,
141	'removed existant mime type');
142@mimetypes = $cgi->get_mime_types ();
143is ($#mimetypes, 1, 'Count of mime types after removal');
144is_deeply (\@mimetypes, [ 'application/json', 'text/plain' ],
145	'Correct mime types after removal');
146
147# Filename tests
148$cgi->add_timestamp (-1);
149is ($cgi->{timestamp}, 1, 'Timestamp < 0');
150$cgi->add_timestamp (3);
151is ($cgi->{timestamp}, 1, 'Timestamp > 3');
152
153$cgi->add_timestamp (0);
154is ($cgi->{timestamp}, 0, 'timestamp is zero');
155($cgi, $form) = post_data ($datafile, $uploaddir, $cgi);
156is ($cgi->is_error, 0, 'Parsing data with POST');
157like ($form->{'does_not_exist_gif'}, qr/^does_not_exist\.gif/, 'Second file');
158like ($form->{'100;100_gif'}, qr/^100;100\.gif/, 'Third file');
159like ($form->{'300x300_gif'}, qr/^300x300\.gif/, 'Fourth file');
160
161unlink ("$uploaddir/300x300.gif");
162
163$cgi->add_timestamp (1);
164is ($cgi->{timestamp}, 1, 'timestamp is 1');
165$cgi->add_timestamp (2);
166is ($cgi->{timestamp}, 2, 'timestamp is 2');
167($cgi, $form) = post_data ($datafile, $uploaddir, $cgi);
168is ($cgi->is_error, 0, 'Parsing data with POST');
169like ($form->{'does_not_exist_gif'}, qr/[0-9]+__does_not_exist\.gif/, 'Second file');
170like ($form->{'100;100_gif'}, qr/[0-9]+__100;100\.gif/, 'Third file');
171like ($form->{'300x300_gif'}, qr/^300x300\.gif/, 'Fourth file');
172
173sub cleanfile {
174	my $name = shift;
175	$name =~ s/[^a-z0-9\._-]+/_/ig;
176	return $name
177}
178
179unlink "$uploaddir/100_100.gif" if -e "$uploaddir/100_100.gif";
180
181$cgi->filter_filename (\&cleanfile);
182ok (defined $cgi->{filter}, 'Filename filter set');
183($cgi, $form) = post_data ($datafile, $uploaddir, $cgi);
184is ($cgi->is_error, 0, 'Parsing data with POST');
185like ($form->{'does_not_exist_gif'}, qr/^[0-9]+__does_not_exist\.gif/, 'Second file');
186like ($form->{'100;100_gif'}, qr/^100_100\.gif/, 'Third file');
187like ($form->{'300x300_gif'}, qr/^[0-9]+__300x300\.gif/, 'Fourth file');
188
189
190# Buffer size setting tests
191is ($cgi->set_buffer_size(1), 256, 'Buffer size too low');
192is ($cgi->set_buffer_size(1000000), $ENV{CONTENT_LENGTH}, 'Buffer size too high');
193
194# Tests without CONTENT_LENGTH
195my $tmpcl = $ENV{CONTENT_LENGTH};
196$ENV{CONTENT_LENGTH} = 0;
197is ($cgi->set_buffer_size(1), 0, 'Buffer size unset without CONTENT_LENGTH');
198$ENV{CONTENT_LENGTH} = $tmpcl;
199
200# File type tests
201
202unlink "$uploaddir/100_100.gif" if -e "$uploaddir/100_100.gif";
203$cgi->set_file_type ('jibber');
204is ($cgi->{file_type}, 'name', 'File type defaults to name');
205$cgi->set_file_type ('handle');
206is ($cgi->{file_type}, 'handle', 'File type set to handle');
207
208($cgi, $form) = post_data ($datafile, $uploaddir, $cgi);
209is ($cgi->is_error, 0, 'Parsing data with POST');
210like ($form->{'does_not_exist_gif'}, qr/^[0-9]+__does_not_exist\.gif/, 'Second file');
211like ($form->{'100;100_gif'}, qr/^100_100\.gif/, 'Third file');
212like ($form->{'300x300_gif'}, qr/^[0-9]+__300x300\.gif/, 'Fourth file');
213# Check the handles
214my $imgdata = '';
215my $handle = $form->{'100;100_gif'};
216while (<$handle>) {
217	$imgdata .= $_;
218}
219is (length ($imgdata), 896, 'File handle upload');
220
221is (eof ($form->{'300x300_gif'}), '', 'File open');
222$cgi->close_all_files;
223is (eof ($form->{'300x300_gif'}), 1, 'File closed');
224
225#	Tests required for these:
226#	check mime types are honoured on upload
227#	The text/plain should be altered, but the text/html should not.
228#	Run this with a wide window of buffer sizes to ensure there are no
229#	edge cases.
230$datafile             = 't/mime_upload.txt';
231$ENV{CONTENT_LENGTH}  = (stat ($datafile))[7];
232$cgi->add_timestamp (0);
233$cgi->set_file_type ('name');
234@files = qw/plain_txt html_txt plain_win_txt html_win_txt/;
235@sizes = qw/186 212 186 219/;
236@sizes = qw/191 212 191 219/ if $^O eq 'MSWin32';
237for my $buf_size (256 .. 1500) {
238	$cgi->set_buffer_size($buf_size);
239	($cgi, $form) = post_data ($datafile, $uploaddir, $cgi);
240	is ($cgi->is_error, 0, "Parsing data with POST (buffer size $buf_size)");
241
242	for my $i (0..3) {
243		my $file = "$uploaddir/$form->{$files[$i]}";
244		ok (-e $file, "Uploaded file exists ($i - buffer size $buf_size") or
245			warn "Name = '$file'\n" . $cgi->get_error_message;
246		is ((stat($file))[7], $sizes[$i],
247			"File size check ($i - buffer size $buf_size)") or
248			warn_tail ($file);
249		unlink ($file);
250	}
251}
252
253is ($cgi->deny_uploads (), 0, 'Set deny_uploads undef');
254is ($cgi->deny_uploads (0), 0, 'Set deny_uploads false');
255
256is ($cgi->deny_uploads (1), 1, 'Set deny_uploads true');
257($cgi, $form) = post_data ($datafile, $uploaddir, $cgi);
258is ($cgi->is_error, 1, "Upload successfully denied");
259
260# Upload but no files
261$datafile = 't/upload_no_files.txt';
262$ENV{CONTENT_LENGTH}  = (stat ($datafile))[7];
263($cgi, $form) = post_data ($datafile);
264is ($cgi->is_error, 0, 'Parsing upload data with no files');
265
266# Special case where the file uploads appear not last
267$datafile = 't/upload_no_trailing_files.txt';
268$ENV{CONTENT_LENGTH}  = (stat ($datafile))[7];
269($cgi, $form) = post_data ($datafile, $uploaddir);
270is ($cgi->is_error, 0, 'Parsing upload data with no trailling files');
271
272
273$datafile = 't/large_file_upload.txt';
274$ENV{CONTENT_LENGTH}  = (stat ($datafile))[7];
275@sizes = (1027);
276@sizes = (1049) if $^O eq 'MSWin32';
277for my $buf_size (256 .. 1250) {
278	$cgi->set_buffer_size ($buf_size);
279	($cgi, $form) = post_data ($datafile, $uploaddir, $cgi);
280	is ($cgi->is_error, 0,
281		"Parsing upload data with a large file - buffer size $buf_size");
282	my $file = "$uploaddir/$form->{plain_txt}";
283	ok (-e $file, "Uploaded file exists ($file - buffer size $buf_size") or
284	            warn "Name = '$file'\n" . $cgi->get_error_message;
285	is ((stat($file))[7], $sizes[0],
286		"File size check ($file - buffer size $buf_size)") or
287		warn_tail ($file);
288	unlink ($file);
289}
290
291$ENV{CONTENT_LENGTH} += 500;
292($cgi, $form) = post_data ($datafile, $uploaddir, $cgi);
293is ($cgi->is_error, 1, 'Parsing upload data with over large content length');
294
295{
296	$datafile = 't/other_boundary.txt';
297	local $ENV{CONTENT_TYPE}    = q#multipart/form-data; boundary=otherstring#;
298	($cgi, $form) = post_data ($datafile, $uploaddir, $cgi);
299	$ENV{CONTENT_LENGTH} = (stat ($datafile))[7];
300	($cgi, $form) = post_data ($datafile, $uploaddir, $cgi);
301	is ($cgi->is_error, 0, 'Parsing upload data with different boundary');
302	ok (exists $form->{other_file}, 'Parsing of different boundary complete');
303	my $file = "$uploaddir/$form->{other_file}";
304	ok (-e $file, "Uploaded file exists for different boundary ($file)") or
305	            warn "Name = '$file'\n" . $cgi->get_error_message;
306	is ((stat($file))[7], $sizes[0],
307		"File size check for different boundary ($file)") or
308		warn_tail ($file);
309	unlink ($file);
310}
311
312# Use Test::Trap where available to test lack of wanrings
313SKIP: {
314	skip "Test::Trap not available", 2 unless $have_test_trap;
315	$datafile = 't/upload_no_headers.txt';
316	$ENV{CONTENT_LENGTH}  = (stat ($datafile))[7];
317    my @r = trap { ($cgi, $form) = post_data ($datafile, $uploaddir); };
318    is ($trap->stderr, '',
319        'Upload of params with no Content-Type is quiet');
320	is_deeply ($form->{foolots}, [qw/bar baz quux/],
321        'Upload of params with no Content-Type is correct');
322}
323
324# Special case where the file uploads appear not last
325sub post_data {
326	my ($datafile, $dir, $cgi, $as_array) = @_;
327	local *STDIN;
328	open STDIN, '<', $datafile
329		or die "Cannot open test file $datafile: $!";
330	binmode STDIN;
331	$cgi ||= CGI::Lite->new;
332	$cgi->set_platform ('DOS') if $^O eq 'MSWin32';
333	$cgi->set_directory ($dir);
334	if ($as_array) {
335		my %form = $cgi->parse_new_form_data;
336		close STDIN;
337		return ($cgi, \%form);
338	}
339	my $form = $cgi->parse_new_form_data;
340	close STDIN;
341	return ($cgi, $form);
342}
343
344sub warn_tail {
345	# If there's a size mismatch on the uploaded files, dump the end of
346	# the file here. Ideally this should never be called.
347	my $file = shift;
348	my $n    = 32;
349	open (my $in, '<', $file) or return warn "Cannot open $file for reading.  $!";
350	binmode $in;
351	local $/ = undef;
352	my $contents = <$in>;
353	close $file;
354	my $lastn = substr ($contents, 0 - $n);
355	foreach (split (//, $lastn, $n)) {
356		diag ($n-- . " chars from the end: " . ord ($_) . "\n");
357	}
358}
359