]> mj.ucw.cz Git - libucw.git/blob - ucw/perl/UCW/CGI.pm
UCW::CGI: Implemented UTF-8 mode (compatible with `use utf8')
[libucw.git] / ucw / perl / UCW / CGI.pm
1 #       Poor Man's CGI Module for Perl
2 #
3 #       (c) 2002--2010 Martin Mares <mj@ucw.cz>
4 #       Slightly modified by Tomas Valla <tom@ucw.cz>
5 #
6 #       This software may be freely distributed and used according to the terms
7 #       of the GNU Lesser General Public License.
8
9 # FIXME:
10 # - respond with proper HTTP error codes
11 # - if we get invalid parameters, generate HTTP error or redirect
12
13 package UCW::CGI;
14
15 # First of all, set up error handling, so that even errors during parsing
16 # will be reported properly.
17
18 # Variables to be set by the calling module:
19 #       $UCW::CGI::error_mail           mail address of the script admin (optional)
20 #                                       (this one has to be set in the BEGIN block!)
21 #       $UCW::CGI::error_hook           function to be called for reporting errors
22
23 my $error_reported;
24 my $exit_code;
25 my $debug = 0;
26
27 sub report_bug($)
28 {
29         if (!defined $error_reported) {
30                 $error_reported = 1;
31                 print STDERR $_[0];
32                 if (defined($UCW::CGI::error_hook)) {
33                         &$UCW::CGI::error_hook($_[0]);
34                 } else {
35                         print "Content-type: text/plain\n\n";
36                         print "Internal bug:\n";
37                         print $_[0], "\n";
38                         print "Please notify $UCW::CGI::error_mail\n" if defined $UCW::CGI::error_mail;
39                 }
40         }
41         die;
42 }
43
44 BEGIN {
45         $SIG{__DIE__} = sub { report_bug($_[0]); };
46         $SIG{__WARN__} = sub { report_bug("WARNING: " . $_[0]); };
47         $exit_code = 0;
48 }
49
50 END {
51         $? = $exit_code;
52 }
53
54 use strict;
55 use warnings;
56
57 require Exporter;
58 our $VERSION = 1.0;
59 our @ISA = qw(Exporter);
60 our @EXPORT = qw(&html_escape &url_escape &url_deescape &url_param_escape &url_param_deescape &self_ref &self_form &http_get);
61 our @EXPORT_OK = qw();
62
63 our $utf8_mode = 0;
64
65 ### Escaping ###
66
67 sub url_escape($) {
68         my $x = shift @_;
69         utf8::encode($x) if $utf8_mode;
70         $x =~ s/([^-\$_.!*'(),0-9A-Za-z\x80-\xff])/"%".unpack('H2',$1)/ge;
71         utf8::decode($x) if $utf8_mode;
72         return $x;
73 }
74
75 sub url_deescape($) {
76         my $x = shift @_;
77         utf8::encode($x) if $utf8_mode;
78         $x =~ s/%(..)/pack("H2",$1)/ge;
79         utf8::decode($x) if $utf8_mode;
80         return $x;
81 }
82
83 sub url_param_escape($) {
84         my $x = shift @_;
85         $x = url_escape($x);
86         $x =~ s/%20/+/g;
87         return $x;
88 }
89
90 sub url_param_deescape($) {
91         my $x = shift @_;
92         $x =~ s/\+/ /g;
93         return url_deescape($x);
94 }
95
96 sub html_escape($) {
97         my $x = shift @_;
98         $x =~ s/&/&amp;/g;
99         $x =~ s/</&lt;/g;
100         $x =~ s/>/&gt;/g;
101         $x =~ s/"/&quot;/g;
102         $x =~ s/'/&#39;/g;
103         return $x;
104 }
105
106 ### Analysing RFC 822 Style Headers ###
107
108 sub rfc822_prepare($) {
109         my $x = shift @_;
110         # Convert all %'s and backslash escapes to %xx escapes
111         $x =~ s/%/%25/g;
112         $x =~ s/\\(.)/"%".unpack("H2",$1)/ge;
113         # Remove all comments, beware, they can be nested (unterminated comments are closed at EOL automatically)
114         while ($x =~ s/^(("[^"]*"|[^"(])*(\([^)]*)*)(\([^()]*(\)|$))/$1 /) { }
115         # Remove quotes and escape dangerous characters inside (again closing at the end automatically)
116         $x =~ s{"([^"]*)("|$)}{my $z=$1; $z =~ s/([^0-9a-zA-Z%_-])/"%".unpack("H2",$1)/ge; $z;}ge;
117         # All control characters are properly escaped, tokens are clearly visible.
118         # Finally remove all unnecessary spaces.
119         $x =~ s/\s+/ /g;
120         $x =~ s/(^ | $)//g;
121         $x =~ s{\s*([()<>@,;:\\"/\[\]?=])\s*}{$1}g;
122         return $x;
123 }
124
125 sub rfc822_deescape($) {
126         my $x = shift @_;
127         return url_deescape($x);
128 }
129
130 ### Reading of HTTP headers ###
131
132 sub http_get($) {
133         my $h = shift @_;
134         $h =~ tr/a-z-/A-Z_/;
135         return $ENV{"HTTP_$h"} || $ENV{"$h"};
136 }
137
138 ### Parsing of Arguments ###
139
140 my $main_arg_table;
141 my %raw_args;
142
143 sub parse_raw_args_ll($$) {
144         my ($arg, $s) = @_;
145         $s =~ s/\r\n/\n/g;
146         $s =~ s/\r/\n/g;
147         utf8::decode($s) if $utf8_mode;
148         push @{$raw_args{$arg}}, $s;
149 }
150
151 sub parse_raw_args($) {
152         my ($s) = @_;
153         $s =~ s/\s+//;
154         for $_ (split /[&:]/, $s) {
155                 (/^([^=]+)=(.*)$/) or next;
156                 my $arg = $1;
157                 $_ = $2;
158                 s/\+/ /g;
159                 s/%(..)/pack("H2",$1)/eg;
160                 parse_raw_args_ll($arg, $_);
161         }
162 }
163
164 sub parse_multipart_form_data();
165
166 sub init_args() {
167         if (!defined $ENV{"GATEWAY_INTERFACE"}) {
168                 print STDERR "Must be called as a CGI script.\n";
169                 $exit_code = 1;
170                 exit;
171         }
172
173         my $method = $ENV{"REQUEST_METHOD"};
174         if (my $qs = $ENV{"QUERY_STRING"}) {
175                 parse_raw_args($qs);
176         }
177         if ($method eq "GET") {
178         } elsif ($method eq "POST") {
179                 if ($ENV{"CONTENT_TYPE"} =~ /^application\/x-www-form-urlencoded\b/i) {
180                         while (<STDIN>) {
181                                 chomp;
182                                 parse_raw_args($_);
183                         }
184                 } elsif ($ENV{"CONTENT_TYPE"} =~ /^multipart\/form-data\b/i) {
185                         parse_multipart_form_data();
186                 } else {
187                         die "Unknown content type for POST data";
188                 }
189         } else {
190                 die "Unknown request method";
191         }
192 }
193
194 sub parse_args($) {                     # CAVEAT: attached files must be defined in the main arg table
195         my $args = shift @_;
196         if (!$main_arg_table) {
197                 $main_arg_table = $args;
198                 init_args();
199         }
200
201         for my $a (values %$args) {
202                 my $r = ref($a->{'var'});
203                 defined($a->{'default'}) or $a->{'default'}="";
204                 if ($r eq 'SCALAR') {
205                         ${$a->{'var'}} = $a->{'default'};
206                 } elsif ($r eq 'ARRAY') {
207                         @{$a->{'var'}} = ();
208                 }
209         }
210
211         for my $arg (keys %$args) {
212                 my $a = $args->{$arg};
213                 defined($raw_args{$arg}) or next;
214                 for (@{$raw_args{$arg}}) {
215                         $a->{'multiline'} or s/(\n|\t)/ /g;
216                         s/^\s+//;
217                         s/\s+$//;
218                         if (my $rx = $a->{'check'}) {
219                                 if (!/^$rx$/) { $_ = $a->{'default'}; }
220                         }
221
222                         my $v = $a->{'var'};
223                         my $r = ref($v);
224                         if ($r eq 'SCALAR') {
225                                 $$v = $_;
226                         } elsif ($r eq 'ARRAY') {
227                                 push @$v, $_;
228                         }
229                 }
230         }
231 }
232
233 ### Parsing Multipart Form Data ###
234
235 my $boundary;
236 my $boundary_len;
237 my $mp_buffer;
238 my $mp_buffer_i;
239 my $mp_buffer_boundary;
240 my $mp_eof;
241
242 sub refill_mp_data($) {
243         my ($more) = @_;
244         if ($mp_buffer_boundary >= $mp_buffer_i) {
245                 return $mp_buffer_boundary - $mp_buffer_i;
246         } elsif ($mp_buffer_i + $more <= length($mp_buffer) - $boundary_len) {
247                 return $more;
248         } else {
249                 if ($mp_buffer_i) {
250                         $mp_buffer = substr($mp_buffer, $mp_buffer_i);
251                         $mp_buffer_i = 0;
252                 }
253                 while ($mp_buffer_i + $more > length($mp_buffer) - $boundary_len) {
254                         last if $mp_eof;
255                         my $data;
256                         my $n = read(STDIN, $data, 2048);
257                         if ($n > 0) {
258                                 $mp_buffer .= $data;
259                         } else {
260                                 $mp_eof = 1;
261                         }
262                 }
263                 $mp_buffer_boundary = index($mp_buffer, $boundary, $mp_buffer_i);
264                 if ($mp_buffer_boundary >= 0) {
265                         return $mp_buffer_boundary;
266                 } elsif ($mp_eof) {
267                         return length($mp_buffer);
268                 } else {
269                         return length($mp_buffer) - $boundary_len;
270                 }
271         }
272 }
273
274 sub get_mp_line($) {
275         my ($allow_empty) = @_;
276         my $n = refill_mp_data(1024);
277         my $i = index($mp_buffer, "\r\n", $mp_buffer_i);
278         if ($i >= $mp_buffer_i && $i < $mp_buffer_i + $n - 1) {
279                 my $s = substr($mp_buffer, $mp_buffer_i, $i - $mp_buffer_i);
280                 $mp_buffer_i = $i + 2;
281                 return $s;
282         } elsif ($allow_empty) {
283                 if ($n) {                                                       # An incomplete line
284                         my $s = substr($mp_buffer, $mp_buffer_i, $n);
285                         $mp_buffer_i += $n;
286                         return $s;
287                 } else {                                                        # No more lines
288                         return undef;
289                 }
290         } else {
291                 die "Premature end of multipart POST data";
292         }
293 }
294
295 sub skip_mp_boundary() {
296         if ($mp_buffer_boundary != $mp_buffer_i) {
297                 die "Premature end of multipart POST data";
298         }
299         $mp_buffer_boundary = -1;
300         $mp_buffer_i += 2;
301         my $b = get_mp_line(0);
302         print STDERR "SEP $b\n" if $debug;
303         $mp_buffer_boundary = index($mp_buffer, $boundary, $mp_buffer_i);
304         if (substr("\r\n$b", 0, $boundary_len) eq "$boundary--") {
305                 return 0;
306         } else {
307                 return 1;
308         }
309 }
310
311 sub parse_mp_header() {
312         my $h = {};
313         my $last;
314         while ((my $l = get_mp_line(0)) ne "") {
315                 print STDERR "HH $l\n" if $debug;
316                 if (my ($name, $value) = ($l =~ /([A-Za-z0-9-]+)\s*:\s*(.*)/)) {
317                         $name =~ tr/A-Z/a-z/;
318                         $h->{$name} = $value;
319                         $last = $name;
320                 } elsif ($l =~ /^\s+/ && $last) {
321                         $h->{$last} .= $l;
322                 } else {
323                         $last = undef;
324                 }
325         }
326         foreach my $n (keys %$h) {
327                 $h->{$n} = rfc822_prepare($h->{$n});
328                 print STDERR "H $n: $h->{$n}\n" if $debug;
329         }
330         return (keys %$h) ? $h : undef;
331 }
332
333 sub parse_multipart_form_data() {
334         # First of all, find the boundary string
335         my $ct = rfc822_prepare($ENV{"CONTENT_TYPE"});
336         if (!(($boundary) = ($ct =~ /^.*;boundary=([^; ]+)/))) {
337                 die "Multipart content with no boundary string received";
338         }
339         $boundary = rfc822_deescape($boundary);
340         print STDERR "BOUNDARY IS $boundary\n" if $debug;
341
342         # BUG: IE 3.01 on Macintosh forgets to add the "--" at the start of the boundary string
343         # as the MIME specs preach. Workaround borrowed from CGI.pm in Perl distribution.
344         my $agent = http_get("User-agent") || "";
345         $boundary = "--$boundary" unless $agent =~ /MSIE\s+3\.0[12];\s*Mac/;
346         $boundary = "\r\n$boundary";
347         $boundary_len = length($boundary) + 2;
348
349         # Check upload size in advance
350         if (my $size = http_get("Content-Length")) {
351                 my $max_allowed = 0;
352                 foreach my $a (values %$main_arg_table) {
353                         $max_allowed += $a->{"maxsize"} || 65536;
354                 }
355                 if ($size > $max_allowed) {
356                         die "Maximum form data length exceeded";
357                 }
358         }
359
360         # Initialize our buffering mechanism and part splitter
361         $mp_buffer = "\r\n";
362         $mp_buffer_i = 0;
363         $mp_buffer_boundary = -1;
364         $mp_eof = 0;
365
366         # Skip garbage before the 1st part
367         while (my $i = refill_mp_data(256)) { $mp_buffer_i += $i; }
368         skip_mp_boundary() || return;
369
370         # Process individual parts
371         do { PART: {
372                 print STDERR "NEXT PART\n" if $debug;
373                 my $h = parse_mp_header();
374                 my ($field, $cdisp, $a);
375                 if ($h &&
376                     ($cdisp = $h->{"content-disposition"}) &&
377                     $cdisp =~ /^form-data/ &&
378                     (($field) = ($cdisp =~ /;name=([^;]+)/)) &&
379                     ($a = $main_arg_table->{"$field"})) {
380                         print STDERR "FIELD $field\n" if $debug;
381                         if (defined $h->{"content-transfer-encoding"}) { die "Unexpected Content-Transfer-Encoding"; }
382                         if (defined $a->{"var"}) {
383                                 while (defined (my $l = get_mp_line(1))) {
384                                         print STDERR "VALUE $l\n" if $debug;
385                                         parse_raw_args_ll($field, $l);
386                                 }
387                                 next PART;
388                         } elsif (defined $a->{"file"}) {
389                                 require File::Temp;
390                                 require IO::Handle;
391                                 my $max_size = $a->{"maxsize"} || 1048576;
392                                 my @tmpargs = (undef, UNLINK => 1);
393                                 push @tmpargs, DIR => $a->{"tmpdir"} if defined $a->{"tmpdir"};
394                                 my ($fh, $fn) = File::Temp::tempfile(@tmpargs);
395                                 print STDERR "FILE UPLOAD to $fn\n" if $debug;
396                                 ${$a->{"file"}} = $fn;
397                                 ${$a->{"fh"}} = $fh if defined $a->{"fh"};
398                                 my $total_size = 0;
399                                 while (my $i = refill_mp_data(4096)) {
400                                         print $fh substr($mp_buffer, $mp_buffer_i, $i);
401                                         $mp_buffer_i += $i;
402                                         $total_size += $i;
403                                         if ($total_size > $max_size) { die "Uploaded file too long"; }
404                                 }
405                                 $fh->flush();   # Don't close the handle, the file would disappear otherwise
406                                 next PART;
407                         }
408                 }
409                 print STDERR "SKIPPING\n" if $debug;
410                 while (my $i = refill_mp_data(256)) { $mp_buffer_i += $i; }
411         } } while (skip_mp_boundary());
412 }
413
414 ### Generating Self-ref URL's ###
415
416 sub make_out_args(@) {          # Usage: make_out_args([arg_table, ...] name => value, ...)
417         my @arg_tables = ( $main_arg_table );
418         while (@_ && ref($_[0]) eq 'HASH') {
419                 push @arg_tables, shift @_;
420         }
421         my %overrides = @_;
422         my $out = {};
423         for my $table (@arg_tables) {
424                 for my $name (keys %$table) {
425                         my $arg = $table->{$name};
426                         defined($arg->{'var'}) || next;
427                         defined($arg->{'pass'}) && !$arg->{'pass'} && !exists $overrides{$name} && next;
428                         defined $arg->{'default'} or $arg->{'default'} = "";
429                         my $value;
430                         if (!defined($value = $overrides{$name})) {
431                                 if (exists $overrides{$name}) {
432                                         $value = $arg->{'default'};
433                                 } else {
434                                         $value = ${$arg->{'var'}};
435                                         defined $value or $value = $arg->{'default'};
436                                 }
437                         }
438                         if ($value ne $arg->{'default'}) {
439                                 $out->{$name} = $value;
440                         }
441                 }
442         }
443         return $out;
444 }
445
446 sub self_ref(@) {
447         my $out = make_out_args(@_);
448         return "?" . join(':', map { "$_=" . url_param_escape($out->{$_}) } sort keys %$out);
449 }
450
451 sub self_form(@) {
452         my $out = make_out_args(@_);
453         return join('', map { "<input type=hidden name=$_ value='" . html_escape($out->{$_}) . "'>\n" } sort keys %$out);
454 }
455
456 ### Cookies
457
458 sub set_cookie($$@) {
459         #
460         # Unfortunately, the support for the new cookie standard (RFC 2965) among
461         # web browsers is still very scarce, so we are still using the old Netscape
462         # specification.
463         #
464         # Usage: set_cookie(name, value, option => value...), where options are:
465         #
466         #       max-age         maximal age in seconds
467         #       domain          domain name scope
468         #       path            path name scope
469         #       secure          if present, cookie applies only to SSL connections
470         #                       (in this case, the value should be undefined)
471         #       discard         if present with any value, the cookie is discarded
472         #
473
474         my $key = shift @_;
475         my $value = shift @_;
476         my %other = @_;
477         if (exists $other{'discard'}) {
478                 delete $other{'discard'};
479                 $other{'max-age'} = 0;
480         }
481         if (defined(my $age = $other{'max-age'})) {
482                 delete $other{'max-age'};
483                 my $exp = ($age ? (time + $age) : 0);
484                 # Avoid problems with locales
485                 my ($S,$M,$H,$d,$m,$y,$wd) = gmtime $exp;
486                 my @wdays = ( 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' );
487                 my @mons = ( 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' );
488                 $other{'expires'} = sprintf("%s, %02d-%s-%d %02d:%02d:%02d GMT",
489                         $wdays[$wd], $d, $mons[$m], $y+1900, $H, $M, $S);
490         }
491
492         print "Set-Cookie: $key=", url_escape($value);
493         foreach my $k (keys %other) {
494                 print "; $k";
495                 print "=", $other{$k} if defined $other{$k};
496         }
497         print "\n";
498 }
499
500 sub parse_cookies() {
501         my $h = http_get("Cookie") or return ();
502         my @cook = ();
503         foreach my $x (split /;\s*/, $h) {
504                 my ($k,$v) = split /=/, $x;
505                 $v = url_deescape($v) if defined $v;
506                 push @cook, $k => $v;
507         }
508         return @cook;
509 }
510
511 1;  # OK