File Coverage

blib/lib/PCGI.pm
Criterion Covered Total %
statement 24 430 5.5
branch 1 306 0.3
condition 1 75 1.3
subroutine 7 44 15.9
pod 15 15 100.0
total 48 870 5.5


line stmt bran cond sub pod time code
1             package PCGI;
2              
3 1     1   6041 use 5.005;
  1         3  
  1         38  
4 1     1   5 use strict;
  1         2  
  1         29  
5 1     1   6 use Exporter;
  1         13  
  1         37  
6              
7 1     1   895 use Stream::Reader 0.09;
  1         2412  
  1         982  
8              
9             our $VERSION = '0.28';
10              
11             our @ISA = qw( Exporter );
12             our %EXPORT_TAGS = ( all => [ qw( trim urlencode urldecode httpdate ) ] );
13             our @EXPORT_OK = ( @{$EXPORT_TAGS{all}} );
14             our @EXPORT = ();
15              
16             # Global/system variables
17              
18             our $CODE;
19             our $AUTOLOAD;
20             our $Shift;
21             our $CRLF;
22             our $TempMode;
23             our $RTag;
24             our $RType;
25             our $Powered;
26             our $RandChars;
27             our $MonStr;
28             our $DayStr;
29             our $Char2Hex;
30              
31             unless( $CRLF ) {
32              
33             # New line delimiter
34             $CRLF = "\r\n";
35              
36             # 'X-Powered-By' string
37             my $perlver;
38             if( $] >= 5.006 ) {
39             $perlver = sprintf( '%d.%d.%d', ( $] * 1e6 ) =~ /(\d)(\d{3})(\d{3})/ );
40             } else {
41             $perlver = $];
42             }
43             $Powered = "Perl/$perlver PCGI/$VERSION";
44              
45             # HTTP/1.1 header fields, ordered by rfc2616 (without 'Request header' group)
46             # Added fields: Status, Set-Cookie and X-Powered-By
47             $RTag = [
48             # General header
49             qw( Status Cache-Control Connection Date Pragma Trailer Transfer-Encoding Upgrade
50             Via Warning ),
51             # Response header
52             qw( Set-Cookie Accept-Ranges Age ETag Location Proxy-Authenticate Retry-After Server
53             Vary WWW-Authenticate ),
54             # Entity header
55             qw( X-Powered-By Allow Content-Encoding Content-Language Content-Length Content-Location
56             Content-MD5 Content-Range Content-Type Expires Last-Modified )
57             ];
58              
59             # HTTP header types:
60             # 1 - can be multiple
61             # 2 - can be multiple (multitags)
62             # 3 - must be unique
63             @$RType{ @$RTag } = (
64             qw( 3 1 1 3 1 1 3 1 1 1 ), qw( 2 1 1 1 3 1 1 3 1 1 ), qw( 3 1 1 1 3 1 1 1 3 3 3 )
65             );
66              
67             # Autoload code
68             $CODE = {
69              
70             # Constructor
71             new => <<'ENDC',
72             foreach( \*STDIN, \*STDOUT, \*STDERR ) {
73             binmode $_; # binmoding all standard streams
74             }
75             my $self = bless {
76             set => {
77             COOKIE => { EscNull => 1, IncEmpty => undef },
78             GET => { EscNull => 1, IncEmpty => undef },
79             MULTI => {
80             EscNull => 1,
81             IncEmpty => undef,
82             MaxFiles => 16,
83             MaxNameSize => 128,
84             MaxLoops => 256,
85             MaxSize => 33_554_432,
86             MaxValsSize => 2_097_152,
87             TempDir => undef
88             },
89             POST => {
90             EscNull => 1,
91             IncEmpty => undef,
92             MaxNameSize => 128,
93             MaxLoops => 256,
94             MaxSize => 2_097_152,
95             MaxValsSize => 2_097_152
96             }
97             },
98             data => { COOKIE => {}, FILE => {}, GET => {}, POST => {} },
99             flag => { COOKIE => undef, GET => undef, POST => undef },
100             header => {},
101             cookie => [],
102             temp => [],
103             errstr => '',
104             hsent => undef
105             } => shift;
106              
107             # Header pre-definition
108             $self->header(
109             X_Powered_By => $Powered,
110             Content_Type => 'text/html; charset=ISO-8859-1',
111             Date => httpdate(),
112             Connection => 'close'
113             );
114             return $self;
115             ENDC
116              
117             # Destructor
118             DESTROY => <<'ENDC',
119             my $self = shift;
120             # Attempt to delete temporary files
121             foreach( @{$self->{temp}} ) {
122             if( -e and !unlink and $^W ) { warn "Can't unlink temporary file $_: $!" }
123             }
124             ENDC
125              
126             # Public method
127             set => <<'ENDC',
128             my $self = shift;
129             $self->_set( shift, shift ) while @_;
130             ENDC
131              
132             # Private method: SELF->_set( NAME => { ... Params ... } )
133             # Changing specified by NAME group settings
134             _set => <<'ENDC',
135             my $self = shift;
136             my $name = shift;
137             my $attr = shift;
138              
139             if( !exists($self->{set}{$name}) ) {
140             _carp("Invalid name of group") if $^W;
141             } elsif( $self->{flag}{$name} ) {
142             _carp("Too late for changing settings of group '$name'") if $^W;
143             } elsif( ref($attr) ne 'HASH' ) {
144             _carp("New parameters should be at HASH array") if $^W;
145             } else {
146             foreach( keys %$attr ) {
147             if( !exists($self->{set}{$name}{$_}) ) {
148             _carp("Unknown parameter '$_'") if $^W;
149             } elsif( /IncEmpty|EscNull/ ) {
150             $self->{set}{$name}{$_} = $attr->{$_};
151             } elsif ( /TempDir/ ) {
152             if( !defined($attr->{$_}) or ( -d $attr->{$_} and -w $attr->{$_} ) ) {
153             $self->{set}{$name}{$_} = $attr->{$_};
154             } else {
155             _carp("Wrong temporary directory: $attr->{$_}. Will be used autodetect") if $^W;
156             $self->{set}{$name}{$_} = undef;
157             }
158             } else {
159             $self->{set}{$name}{$_} = ( defined($attr->{$_}) and $attr->{$_} >= 0 )?
160             $attr->{$_} : 2e9;
161             }
162             }
163             }
164             ENDC
165              
166             # Public method
167             header => <<'ENDC',
168             my $self = shift;
169              
170             if( $self->{hsent} ) {
171             _carp("Too late for use header() method. Header already sent") if $^W;
172             } else {
173             $self->_header( shift, shift ) while @_;
174             }
175             ENDC
176              
177             # Private method: SELF->_header( PARAM => VALUE )
178             # Checking and storing(or deleting) new HTTP header parameter
179             _header => <<'ENDC',
180             my $self = shift;
181             my $name = defined($Shift = shift)? lc($Shift) : return;
182             my $value = shift;
183             my $rtype = 1;
184              
185             # Checking name
186             $name =~ tr/_/-/;
187             NCHECK: {
188             foreach( @$RTag ) {
189             if( lc eq $name ) { $name = $_; $rtype = $RType->{$_}; last NCHECK }
190             }
191             $name =~ s/([^-]+)/ ucfirst($1) /eg;
192             }
193             # Storing parameter
194             if( defined $value ) {
195             $value = [ $value ] unless( ref($value) eq 'ARRAY' );
196             if( $rtype == 3 and @$value > 1 ) {
197             _carp("Parameter '$name' cannot have multiple value") if $^W;
198             $value = [ pop @$value ];
199             }
200             $self->{header}{$name} = $value;
201             } else {
202             delete $self->{header}{$name};
203             }
204             ENDC
205              
206             # Public method
207             sendheader => <<'ENDC',
208             my $self = shift;
209              
210             if( $self->{hsent}++ ) {
211             _carp("You can use a method sendheader() only once") if $^W;
212             } else {
213             $self->_sendheader();
214             }
215             ENDC
216              
217             # Private method: SELF->_sendheader()
218             # Formating and sending header
219             _sendheader => <<'ENDC',
220             my $self = shift;
221             my $header = '';
222              
223             # Appending cookie data
224             # and hecking some header parameter(s)
225             if( @{$self->{cookie}} ) {
226             push( @{$self->{header}{'Set-Cookie'}}, @{$self->{cookie}} );
227             }
228             if( exists $self->{header}{Location} ) {
229             $self->{header}{Status} = [ '302 Found' ] unless( exists $self->{header}{Status} );
230             }
231             # Preparing header
232             foreach my $name ( @$RTag, sort( keys %{$self->{header}} ) ) {
233             if( exists $self->{header}{$name} ) {
234             if( exists($RType->{$name}) and $RType->{$name} == 2 ) {
235             $header .= "$name: $_".$CRLF foreach( @{ delete $self->{header}{$name} } );
236             } else {
237             $header .= "$name: ".join( ', ', @{ delete $self->{header}{$name} } ).$CRLF;
238             }
239             }
240             }
241             $header .= $CRLF;
242             # Sending header by better way
243             if( exists $ENV{MOD_PERL} ) {
244             Apache->request->send_cgi_header($header);
245             } else {
246             print $header;
247             }
248             ENDC
249              
250             # Public method
251             setcookie => <<'ENDC',
252             my $self = shift;
253              
254             if( $self->{hsent} ) {
255             _carp("Too late for use setcookie() method. Header already sent") if $^W;
256             } else {
257             $self->_setcookie(@_);
258             }
259             ENDC
260              
261             # Private method: SELF->_setcookie( NAME => VALUE, { ... Params ... } )
262             # Checking and storing(or deleting) HTTP cookie data
263             _setcookie => <<'ENDC',
264             my $self = shift;
265             my $name = defined($Shift = shift)? $Shift : return;
266             my $value = shift;
267             my $attr = ( ref($Shift = shift) eq 'HASH' )? $Shift : {};
268             my $data = $name.'=';
269              
270             # Preparing cookie
271             if( defined $value ) {
272             $data .= $attr->{Raw}? $value : urlencode($value);
273             $data .= '; Expires='._httpdate( $attr->{Expires}, '-' ) if( defined $attr->{Expires} );
274             $data .= '; Path='.$attr->{Path} if( defined $attr->{Path} );
275             $data .= '; Domain='.$attr->{Domain} if( defined $attr->{Domain} );
276             $data .= '; Secure' if $attr->{Secure};
277             } else {
278             $data .= 'deleted; Expires='._httpdate( 1, '-' );
279             }
280             # Storing cookie
281             push( @{$self->{cookie}}, $data );
282             ENDC
283              
284             # Public method
285             env => <<'ENDC',
286             return(( exists($ENV{$_[1]}) and defined($ENV{$_[1]}) )? $ENV{$_[1]} : '' );
287             ENDC
288              
289             # Public method
290             errstr => <<'ENDC',
291             my $self = shift;
292             $self->_init_p() unless $self->{flag}{POST};
293             return $self->{errstr};
294             ENDC
295              
296             # Public method
297             GET => <<'ENDC',
298             my $self = shift;
299             $self->_init_g() unless $self->{flag}{GET};
300             return $self->_param( GET => @_ );
301             ENDC
302              
303             # Public method
304             COOKIE => <<'ENDC',
305             my $self = shift;
306             $self->_init_c() unless $self->{flag}{COOKIE};
307             return $self->_param( COOKIE => @_ );
308             ENDC
309              
310             # Public method
311             POST => <<'ENDC',
312             my $self = shift;
313             $self->_init_p() unless $self->{flag}{POST};
314             return $self->_param( POST => @_ );
315             ENDC
316              
317             # Public method
318             FILE => <<'ENDC',
319             my $self = shift;
320             $self->_init_p() unless $self->{flag}{POST};
321             return $self->_param( FILE => @_ );
322             ENDC
323              
324             # Private method: VALUES = _param( TYPE => NAME )
325             # Returns specific parameter(s)
326             _param => <<'ENDC',
327             my $self = shift;
328             my $type = shift;
329             my $name = shift;
330              
331             if( defined $name ) {
332             if( exists $self->{data}{$type}{$name} ) {
333             return wantarray? @{$self->{data}{$type}{$name}} : $self->{data}{$type}{$name}[0];
334             } else {
335             return;
336             }
337             } else {
338             return keys( %{$self->{data}{$type}} );
339             }
340             ENDC
341              
342             # Private method: SELF->_init_g()
343             # GET query parser
344             _init_g => <<'ENDC',
345             my $self = shift;
346              
347             if( !$self->{flag}{GET}++ ) {
348             # Preparing
349             my $query = $self->env('QUERY_STRING');
350             $query = $self->env('REDIRECT_QUERY_STRING') unless( defined $query );
351             # Processing
352             foreach( split( '[&;]+', $query ) ) {
353             my( $name, $value ) = ( split('='), ('')x2 );
354             if( length $name ) {
355             if( length($value) or $self->{set}{GET}{IncEmpty} ) {
356             if( $self->{set}{GET}{EscNull} ) {
357             tr/+/ / foreach( $name, $value ); # escaping null symbols
358             }
359             push( @{$self->{data}{GET}{ urldecode($name) }},
360             urldecode($value)
361             );
362             }
363             }
364             }
365             }
366             ENDC
367              
368             # Private method: SELF->_init_c()
369             # COOKIE query parser
370             _init_c => <<'ENDC',
371             my $self = shift;
372              
373             if( !$self->{flag}{COOKIE}++ ) {
374             # Preparing
375             my $query = $self->env('HTTP_COOKIE');
376             $query = $self->env('COOKIE') unless( defined $query );
377             # Processing
378             foreach( split( ';+', $query ) ) {
379             my( $name, $value ) = ( split('='), ('')x2 );
380             foreach( $name, $value ) {
381             $_ = trim($_); # removing unnecessary spaces
382             }
383             if( length $name ) {
384             if( length($value) or $self->{set}{COOKIE}{IncEmpty} ) {
385             if( $self->{set}{GET}{EscNull} ) {
386             tr/+/ / foreach( $name, $value ); # escaping null symbols
387             }
388             $self->{data}{COOKIE}{ urldecode($name) } = [
389             urldecode($value)
390             ];
391             }
392             }
393             }
394             }
395             ENDC
396              
397             _init_p => <<'ENDC',
398             my $self = shift;
399              
400             unless( $self->{flag}{POST}++ ) {
401             my $reason;
402             # Checking request
403             if( $self->env('REQUEST_METHOD') eq 'POST' ) {
404             # Checking Content-Length
405             my $length = $self->env('CONTENT_LENGTH');
406             unless( length $length ) {
407             $reason = 'Content-Length required';
408             } elsif( $length !~ /^\d+$/ ) { # it is possible
409             $reason = 'Invalid Content-Length';
410             } elsif( $length > 0 ) {
411             # Checking Content-Type
412             my $ctype = _parse_mheader( 'a: '.$self->env('CONTENT_TYPE') );
413             unless( exists $ctype->{a}{_MAIN_} ) {
414             $reason = 'Undefined Content-Type';
415             } elsif( $ctype->{a}{_MAIN_} eq 'application/x-www-form-urlencoded' ) {
416             # Simple query
417             if( $length > $self->{set}{POST}{MaxSize} ) {
418             $reason = 'Request entity too large';
419             } else {
420             $reason = $self->_init_p_simple($length);
421             }
422             } elsif( $ctype->{a}{_MAIN_} eq 'multipart/form-data' ) {
423             # Multipart query
424             if( $length > $self->{set}{MULTI}{MaxSize} ) {
425             $reason = 'Request entity too large';
426             } elsif( !exists($ctype->{a}{boundary}) or !length($ctype->{a}{boundary}) ) {
427             $reason = 'Undefined multipart boundary';
428             } else {
429             $reason = $self->_init_p_multipart( $length, $ctype->{a}{boundary} );
430             }
431             } else {
432             $reason = 'Unsupported Content-Type';
433             }
434             }
435             }
436             # Setting error if having reason
437             if( $reason ) {
438             $self->{errstr} = "400 Bad Request ($reason)";
439             # Truncating data
440             $self->{data}{POST} = {};
441             $self->{data}{FILE} = {};
442             }
443             }
444             ENDC
445              
446             # Private method: SELF->_init_p_simple()
447             # Simple POST query sub-parser
448             _init_p_simple => <<'ENDC',
449             my $self = shift;
450             my $stream = Stream::Reader->new( \*STDIN, { Limit => shift } );
451             my $loop = $self->{set}{POST}{MaxLoops};
452             my $mvsize = $self->{set}{POST}{MaxValsSize};
453             my $name;
454             my $value;
455             my $name_attr = {
456             Out => \$name, Mode => 'E',
457             Limit => $self->{set}{POST}{MaxNameSize} + 1
458             };
459              
460             while( $loop--> 0 ) {
461             unless( $stream->readto( '=', $name_attr ) ) {
462             return ''; # normal finish
463             } else {
464             $stream->readto( '&', {
465             Out => \$value, Limit => $mvsize + 1
466             });
467             if( length $name ) { # checking
468             if( length($name) > $self->{set}{POST}{MaxNameSize} ) {
469             return 'Found too large name of parameter';
470             } elsif( length($value) or $self->{set}{POST}{IncEmpty} ) {
471             if( ( $mvsize -= length $value ) < 0 ) {
472             return 'Summary values size is too large';
473             } else {
474             if( $self->{set}{POST}{EscNull} ) {
475             tr/+/ / foreach( $name, $value ); # escaping null symbols
476             }
477             push( @{$self->{data}{POST}{ urldecode($name) }},
478             urldecode($value)
479             );
480             }
481             }
482             }
483             }
484             }
485             return 'Too much elements';
486             ENDC
487              
488             # Private method: SELF->_init_p_multipart()
489             # Multipart POST query sub-parser
490             _init_p_multipart => <<'ENDC',
491             my $self = shift;
492             my $temp;
493             my $s;
494             my $r;
495              
496             # So strange local variables needs for more simple
497             # splitting this method on some parts
498             $s->{stream} = Stream::Reader->new( \*STDIN, { Limit => shift } );
499             $s->{bound} = '--'.shift;
500             $s->{fcount} = $self->{set}{MULTI}{MaxFiles};
501             $s->{loop} = $self->{set}{MULTI}{MaxLoops};
502             $s->{mvsize} = $self->{set}{MULTI}{MaxValsSize};
503             $s->{rewind} = 1;
504             $s->{header} = undef;
505              
506             # Main cycle
507             while( $s->{loop}--> 0 ) {
508             # Rewinding position after next found boundary. If rewinding was disabled,
509             # then only checking CRLF after boundary
510             if( $s->{rewind}++ and !$s->{stream}->readto( $s->{bound}, { Mode => 'E' } )) {
511             return 'Malformed multipart POST'; # could not found boundary
512             } elsif( !$s->{stream}->readsome( 2, { Out => \$temp } )) {
513             return 'Malformed multipart POST'; # no CRLF after boundary
514             } elsif( $temp eq '--' ) {
515             return ''; # normal finish
516             } elsif( $temp ne $CRLF ) {
517             return 'Malformed multipart POST'; # bad CRLF after boundary
518             } else {
519             # Reading and parsing multipart header.
520             # Doing that very cautiously
521             unless( $s->{stream}->readto(
522             $CRLF x2, { Out => \$temp, Limit => 8*1024, Mode => 'E' }
523             )) {
524             return 'Malformed multipart POST';
525             } elsif( $s->{stream}{Readed} != $s->{stream}{Stored} ) {
526             return 'Malformed multipart POST'; # malformed or too big header
527             } else {
528             $s->{header} = _parse_mheader($temp);
529             # Checking header
530             if( exists($s->{header}{content_disposition}{_MAIN_})
531             and $s->{header}{content_disposition}{_MAIN_} eq 'form-data'
532             and exists($s->{header}{content_disposition}{name})
533             and length($s->{header}{content_disposition}{name})
534             ) {
535             $s->{name} = $s->{header}{content_disposition}{name};
536             if( length($s->{name}) > $self->{set}{MULTI}{MaxNameSize} ) {
537             return 'Found too large name of parameter';
538             } else {
539             if( $self->{set}{MULTI}{EscNull} ) {
540             $s->{name} =~ tr/\0/ /; # escaping null symbols
541             }
542             # Let looking, what we have
543             if( exists $s->{header}{content_disposition}{filename} ) {
544             if( $s->{fcount} ) {
545             $r = $self->_init_p_multipart_file($s); # file transfer
546             return $r if $r;
547             }
548             } elsif( exists($s->{header}{content_type}{_MAIN_})
549             and $s->{header}{content_type}{_MAIN_} eq 'multipart/mixed'
550             ) {
551             if( $s->{fcount} ) {
552             $r = $self->_init_p_multipart_mixed($s); # many files transfer
553             return $r if $r;
554             }
555             } else {
556             $r = $self->_init_p_multipart_simple($s); # simple value
557             return $r if $r;
558             }
559             }
560             }
561             }
562             }
563             }
564             return 'Too much elements';
565             ENDC
566              
567             # Private method: BOOL = SELF->_init_p_multipart_simple( S )
568             # Simple value extraction
569             _init_p_multipart_simple => <<'ENDC',
570             my $self = shift;
571             my $s = shift;
572             my $value;
573              
574             # Reading data before next found boundary
575             unless( $s->{stream}->readto(
576             $CRLF.$s->{bound}, { Out => \$value, Limit => $s->{mvsize}, Mode => 'E' }
577             )) {
578             return 'Malformed multipart POST';
579             } elsif( $s->{stream}{Stored} != $s->{stream}{Readed} ) {
580             return 'Summary values size is too large';
581             } else {
582             $s->{rewind} = 0; # disabling rewind at next iteration
583             if( $s->{stream}{Stored} or $self->{set}{MULTI}{IncEmpty} ) {
584             $s->{mvsize} -= $s->{stream}{Stored};
585             if( $self->{set}{MULTI}{EscNull} ) {
586             $value =~ tr/\0/ /; # checking value
587             }
588             push( @{$self->{data}{POST}{$s->{name}}}, $value );
589             }
590             return '';
591             }
592             ENDC
593              
594             # Private method: BOOL = SELF->_init_p_multipart_file( S )
595             # File extraction
596             _init_p_multipart_file => <<'ENDC',
597             my $self = shift;
598             my $s = shift;
599             my $file = {
600             full => $s->{header}{content_disposition}{filename}
601             };
602              
603             # Correcting and checking filename, creating new temporary file
604             # and reading all data, before next found boundary, directly to temporary file
605             if( length $file->{full} ) {
606             $file->{base} = _basename( $file->{full} );
607             if( length $file->{base} ) {
608             my $handler;
609             if(( $handler, $file->{temp} ) = $self->_tempfile() ) {
610             unless( $s->{stream}->readto(
611             $CRLF.$s->{bound}, { Out => $handler, Mode => 'E' }
612             )) {
613             unless( close $handler ) {
614             warn("Can't close file $file->{temp}: $!") if $^W;
615             }
616             return 'Malformed multipart POST';
617             } else {
618             $s->{rewind} = 0; # disabling rewind at next iteration
619             unless( close $handler ) {
620             warn("Can't close file $file->{temp}: $!") if $^W;
621             } elsif( $s->{stream}{Stored} != $s->{stream}{Readed} ) {
622             warn("Possible writing error in file $file->{temp}") if $^W;
623             } else {
624             $file->{size} = $s->{stream}{Stored};
625             if( exists $s->{header}{content_type}{_MAIN_} ) {
626             $file->{mime} = $s->{header}{content_type}{_MAIN_};
627             } else {
628             $file->{mime} = '';
629             }
630             if( $self->{set}{MULTI}{EscNull} ) {
631             tr/+/ / foreach( @$file{qw( full base mime )} ); # escaping null symbols
632             }
633             push( @{$self->{data}{FILE}{$s->{name}}}, $file );
634             $s->{fcount}--;
635             }
636             }
637             }
638             }
639             }
640             return '';
641             ENDC
642              
643             # Private method: BOOL = SELF->_init_p_multipart_mixed( S )
644             # Many files extraction
645             _init_p_multipart_mixed => <<'ENDC',
646             my $self = shift;
647             my $s = shift;
648             my $r;
649             my $temp;
650              
651             unless( exists($s->{header}{content_type}{boundary})
652             and length($s->{header}{content_type}{boundary})
653             ) {
654             return 'Malformed multipart POST';
655             } else {
656             my $mbound = '--'.$s->{header}{content_type}{boundary};
657             my $mrewind = 1;
658             my $mheader;
659              
660             # Mixed sub-cycle
661             for( $s->{loop}++; $s->{loop}--> 0; ) {
662             unless( $s->{fcount} ) {
663             return ''; # limit for files
664             } else {
665             # Rewinding position after next found boundary. If rewinding was disabled,
666             # then only checking CRLF after boundary
667             if( $mrewind++ and !$s->{stream}->readto( $mbound, { Mode => 'E' } )) {
668             return 'Malformed multipart POST'; # could not found mixed boundary
669             } elsif( !$s->{stream}->readsome( 2, { Out => \$temp } )) {
670             return 'Malformed multipart POST'; # no CRLF after mixed boundary
671             } elsif( $temp eq '--' ) {
672             return ''; # normal finish
673             } elsif( $temp ne $CRLF ) {
674             return 'Malformed multipart POST'; # bad CRLF after mixed boundary
675             } else {
676             # Reading and parsing multipart/mixed header.
677             # Doing that very cautiously
678             unless( $s->{stream}->readto(
679             $CRLF x2, { Out => \$temp, Limit => 8*1024, Mode => 'E' }
680             )) {
681             return 'Malformed multipart POST';
682             } elsif( $s->{stream}{Readed} != $s->{stream}{Stored} ) {
683             return 'Malformed multipart POST'; # malformed or too big header
684             } else {
685             $mheader = _parse_mheader($temp);
686             # Checking multipart/mixed header
687             if( exists $mheader->{content_disposition}{_MAIN_} ) {
688             $temp = $mheader->{content_disposition}{_MAIN_};
689             if( ( $temp eq 'file' or $temp eq 'attachment' )
690             and exists($mheader->{content_disposition}{filename})
691             ) {
692             my $file = {
693             full => $mheader->{content_disposition}{filename}
694             };
695             # Correcting and checking filename, creating new temporary file
696             # and reading all data, before next found boundary, directly to temporary file
697             if( length $file->{full} ) {
698             $file->{base} = _basename( $file->{full} );
699             if( length $file->{base} ) {
700             my $handler;
701             if(( $handler, $file->{temp} ) = $self->_tempfile() ) {
702             unless( $s->{stream}->readto(
703             $CRLF.$mbound, { Out => $handler, Mode => 'E' }
704             )) {
705             unless( close $handler ) {
706             warn("Can't close file $file->{temp}: $!") if $^W;
707             }
708             return 'Malformed multipart POST';
709             } else {
710             $mrewind = 0; # disabling rewind at next iteration
711             unless( close $handler ) {
712             warn("Can't close file $file->{temp}: $!") if $^W;
713             } elsif( $s->{stream}{Stored} != $s->{stream}{Readed} ) {
714             warn("Possible writing error in file $file->{temp}") if $^W;
715             } else {
716             $file->{size} = $s->{stream}{Stored};
717             if( exists $mheader->{content_type}{_MAIN_} ) {
718             $file->{mime} = $mheader->{content_type}{_MAIN_};
719             } else {
720             $file->{mime} = '';
721             }
722             if( $self->{set}{MULTI}{EscNull} ) {
723             tr/+/ / foreach( @$file{qw( full base mime )} ); # escaping null symbols
724             }
725             push( @{$self->{data}{FILE}{$s->{name}}}, $file );
726             $s->{fcount}--;
727             }
728             }
729             }
730             }
731             }
732             }
733             }
734             }
735             }
736             }
737             }
738             return 'Too much elements';
739             }
740             ENDC
741              
742             # Private method: ( FILENAME, HANDLER ) = SELF->_tempfile()
743             # Temporary files generator.
744             # Note(only for author): need to remember about closing all temporary files manualy
745             _tempfile => <<'ENDC',
746             my $self = shift;
747             my $tempdir;
748              
749             # Preparing
750             if( !$TempMode ) {
751             require File::Spec;
752             require Fcntl;
753             $TempMode = Fcntl::O_CREAT()|Fcntl::O_WRONLY()|Fcntl::O_EXCL()|Fcntl::O_BINARY();
754             }
755             $tempdir = $self->{set}{MULTI}{TempDir};
756             $tempdir = File::Spec->tmpdir() unless( defined $tempdir );
757             # Processing
758             unless( -w $tempdir ) { # warn if bad directory
759             warn("Directory is not writable: $tempdir") if $^W;
760             } else {
761             foreach( 1 .. 3 ) {
762             my $fname = File::Spec->catfile( $tempdir, 'PCGI_'._randstr(32) );
763             sysopen( my $handler, $fname, $TempMode, 0600 );
764             if( fileno $handler ) {
765             push( @{$self->{temp}}, $fname );
766             return( $handler, $fname );
767             }
768             }
769             # Warn if can't
770             warn("Can't create file at directory: $tempdir") if $^W;
771             }
772             return;
773             ENDC
774              
775             # Public function
776             trim => <<'ENDC',
777             my $string = shift;
778              
779             $string =~ s/^\s+//s;
780             $string =~ s/\s+$//s;
781             return $string;
782             ENDC
783              
784             # Public function
785             urldecode => <<'ENDC',
786             my $string = shift;
787             no warnings;
788              
789             $string =~ tr/+/ /;
790             if( $] > 5.007 ) {
791             use bytes;
792             $string =~ s/%u([0-9a-fA-F]{4})/pack('U',hex($1))/eg;
793             } else {
794             my $dec;
795             $string =~ s/%u([0-9a-fA-F]{4})/
796             # Here utf-8 characters can have
797             # maximal length 3 bytes (4 hex simbols)
798             $dec = hex $1;
799             if( $dec < 0x80 ) {
800             chr $dec;
801             } elsif( $dec < 0x800 ) {
802             pack( 'c2', 0xc0|($dec>>6),0x80|($dec&0x3f) );
803             } else {
804             pack( 'c3', 0xe0|($dec>>12),0x80|(($dec>>6)&0x3f),0x80|($dec&0x3f) );
805             }
806             /egx;
807             }
808             $string =~ s/%([0-9a-fA-F]{2})/chr(hex $1)/eg;
809             return $string;
810             ENDC
811              
812             # Public function
813             urlencode => <<'ENDC',
814             my $string = shift;
815              
816             # Conformity symbols to their codes
817             if( !$Char2Hex ) {
818             foreach( 0 .. 255 ) {
819             $Char2Hex->{ chr() } = sprintf( '%%%02X', $_ );
820             }
821             }
822             # Encoding
823             $string =~ s/([^A-Za-z0-9\-_.!~*\'() ])/$Char2Hex->{$1}/g;
824             $string =~ tr/ /+/;
825             return $string;
826             ENDC
827              
828             # Public function
829             httpdate => <<'ENDC',
830             _httpdate( ( defined($Shift = shift)? $Shift : time ), ' ' );
831             ENDC
832              
833             # Private function: DATE = _httpdate( UTIME, SEPARATOR )
834             _httpdate => <<'ENDC',
835             my @time = gmtime(shift);
836             my $sep = shift;
837              
838             # Conformity numbers to months and day of weeks
839             if( !$MonStr ) {
840             $DayStr = [ qw( Sun Mon Tue Wed Thu Fri Sat ) ];
841             $MonStr = [ qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec ) ];
842             }
843             # Formating date
844             return sprintf( "%s, %02d${sep}%s${sep}%d %02d:%02d:%02d GMT",
845             $DayStr->[$time[6]], $time[3], $MonStr->[$time[4]], $time[5]+1900, $time[2], $time[1], $time[0]
846             );
847             ENDC
848              
849             # Private function: ARRAY = _parse_mheader(HEADER)
850             # Parsing multipart headers
851             _parse_mheader => <<'ENDC',
852             my $array = {};
853             my $group;
854             my $name;
855             my $value;
856              
857             # For beginning found the main pair.
858             # Main parameter must be exists and not empty
859             foreach my $line (
860             split( $CRLF, $_[0], 6 )
861             ) {
862             if( $line =~ s/^([^:]+):([^;,]+)[;,]?// ) {
863             $group = trim(lc $1);
864             $value = trim($2);
865             if( length($group) and length($value) ) {
866             $group =~ tr/-/_/;
867             $array->{$group}{_MAIN_} = $value;
868             # Reading other parameters
869             # For security check, cycle have maximum 4 iterations
870             foreach( 1 .. 4 ) {
871             if( $line =~ s/^([^=]+)=// ) {
872             $name = trim($1);
873             if( $name eq 'filename' ) {
874             $value = trim($line); $line = '';
875             } else {
876             $line =~ s/^([^;,]+)[;,]?//;
877             $value = trim($1);
878             }
879             if( length($name) and $name ne '_MAIN_' ) {
880             $value =~ s/^\"(.*)\"$/$1/s;
881             $array->{$group}{$name} = $value;
882             }
883             } else {
884             last; # no matches
885             }
886             }
887             }
888             }
889             }
890             return $array;
891             ENDC
892              
893             # Private function: BASENAME = _basename(PATH)
894             # Extracting file name from path. Very simple variant, but here more then enough
895             _basename => <<'ENDC',
896             return(( $_[0] =~ /([^\\\/\:]+)$/ )? $1 : '' );
897             ENDC
898              
899             # Private function: STRING = _randstr(LENGTH)
900             # Random strings generator. (Normal works only with little size)
901             _randstr => <<'ENDC',
902             my $length = shift;
903             my $string = '';
904              
905             # Characters for random generator
906             if( !$RandChars ) {
907             $RandChars = [ 'a'..'z', '0'..'9' ];
908             }
909             # Generating string
910             foreach( 1 .. $length ) {
911             $string .= $RandChars->[ rand( @$RandChars - 0.5 ) ];
912             }
913             return $string;
914             ENDC
915              
916             };
917             }
918              
919             # Compiling all under mod_perl
920             if( exists $ENV{MOD_PERL} ) {
921             _compile($_) foreach( keys %{$CODE} );
922             }
923              
924             # Standard function
925             sub AUTOLOAD {
926 0     0   0 my $name = substr(
927             $AUTOLOAD, rindex( $AUTOLOAD, ':' ) + 1
928             );
929 0 0       0 unless( _compile($name) ) {
930 0         0 _croak("Undefined subroutine &${AUTOLOAD} called");
931             } else {
932 0         0 goto &{$AUTOLOAD};
  0         0  
933             }
934             }
935              
936             # Private function: BOOL = _compile(NAME)
937             # Compiling, specified by NAME, subroutine from $CODE array
938             sub _compile {
939 34     34   42 my $name = shift;
940              
941 34 50       62 unless( exists $CODE->{$name} ) {
942 0         0 return undef;
943             } else {
944 1 0 0 1 1 11 eval "sub $name { $CODE->{$name} }";
  1 0 0 1 1 2  
  1 0 0 0 1 76  
  1 0 0 0 1 1330  
  1 0 0 0 1 34  
  1 0 0 0 1 5  
  34 0 0 0 1 6532  
  0 0 0 0 1    
  0 0 0 0 1    
  0 0 0 0 1    
  0 0 0 0 1    
  0 0 0 0 1    
  0 0 0 0 1    
  0 0 0 0 1    
  0 0 0 0 1    
  0 0 0 0      
  0 0 0 0      
  0 0 0 0      
  0 0 0 0      
  0 0 0 0      
  0 0 0 0      
  0 0 0 0      
  0 0 0 0      
  0 0 0 0      
  0 0   0      
  0 0   0      
  0 0   0      
  0 0   0      
  0 0   0      
  0 0   0      
  0 0   0      
  0 0   0      
  0 0   0      
  0 0   0      
  0 0   0      
  0 0   0      
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
945 34 0 33     311 if( $@ ne '' and $^W ) {
946 0         0 warn $@; # warnings enable
947             }
948 34         52 delete $CODE->{$name};
949 34         95 return 1;
950             }
951             }
952              
953             # Handling warnings
954             sub _carp {
955 0     0     require Carp; Carp::carp(shift);
  0            
956             }
957              
958             # Handling fatals
959             sub _croak {
960 0     0     require Carp; Carp::croak(shift);
  0            
961             }
962              
963             1;
964              
965             __END__