File Coverage

blib/lib/HTTP/Entity/Parser/MultiPart.pm
Criterion Covered Total %
statement 87 89 97.7
branch 36 44 81.8
condition 8 9 88.8
subroutine 12 12 100.0
pod 0 4 0.0
total 143 158 90.5


line stmt bran cond sub pod time code
1             package HTTP::Entity::Parser::MultiPart;
2              
3 6     6   141375 use strict;
  6         33  
  6         217  
4 6     6   40 use warnings;
  6         25  
  6         153  
5 6     6   2918 use HTTP::MultiPartParser;
  6         11094  
  6         203  
6 6     6   4481 use File::Temp qw/tempfile/;
  6         95362  
  6         453  
7 6     6   49 use Carp qw//;
  6         14  
  6         115  
8 6     6   31 use Fcntl ":seek";
  6         13  
  6         7741  
9              
10             #
11             # copy from https://gist.github.com/chansen/7163968
12             #
13             sub extract_form_data {
14 176     176 0 318 local $_ = shift;
15             # Fast exit for common form-data disposition
16 176 100       1207 if (/\A form-data; \s name="((?:[^"]|\\")*)" (?: ;\s filename="((?:[^"]|\\")*)" )? \z/x) {
17 148         605 return ($1, $2);
18             }
19              
20             # disposition type must be form-data
21             s/\A \s* form-data \s* ; //xi
22 28 100       147 or return;
23              
24 26         60 my (%p, $k, $v);
25 26         60 while (length) {
26 49         154 s/ ^ \s+ //x;
27 49         132 s/ \s+ $ //x;
28              
29             # skip empty parameters and unknown tokens
30 49 100       134 next if s/^ [^\s"=;]* \s* ; //x;
31              
32             # parameter name (token)
33 46 100       178 s/^ ([^\s"=;]+) \s* = \s* //x
34             or return;
35 44         109 $k = lc $1;
36             # quoted parameter value
37 44 100       207 if (s/^ "((?:[^"]|\\")*)" \s* (?: ; | $) //x) {
    100          
38 28         58 $v = $1;
39             }
40             # unquoted parameter value (token)
41             elsif (s/^ ([^\s";]*) \s* (?: ; | $) //x) {
42 15         31 $v = $1;
43             }
44             else {
45 1         5 return;
46             }
47 43 100 100     147 if ($k eq 'name' || $k eq 'filename') {
48 36 100       84 return () if exists $p{$k};
49 34         113 $p{$k} = $v;
50             }
51             }
52 21 100       107 return exists $p{name} ? @p{qw(name filename)} : ();
53             }
54              
55             sub new {
56 63     63 0 84789 my ($class, $env, $opts) = @_;
57              
58 63         153 my $self = bless { }, $class;
59              
60 63         152 my @uploads;
61             my @params;
62              
63 63 50       203 unless (defined $env->{CONTENT_TYPE}) {
64 0         0 Carp::croak("Missing CONTENT_TYPE in PSGI env");
65             }
66 63 50       389 unless ( $env->{CONTENT_TYPE} =~ /boundary=\"?([^\";]+)\"?/ ) {
67 0         0 Carp::croak("Invalid boundary in content_type: $env->{CONTENT_TYPE}");
68             }
69 63         181 my $boundary = $1;
70              
71              
72 63         87 my $part;
73             my $parser = HTTP::MultiPartParser->new(
74             boundary => $boundary,
75             on_header => sub {
76 176     176   6151 my ($headers) = @_;
77              
78 176         265 my $disposition;
79 176         325 foreach (@$headers) {
80 176 50       629 if (/\A Content-Disposition: [\x09\x20]* (.*)/xi) {
81 176         512 $disposition = $1;
82 176         336 last;
83             }
84             }
85              
86 176 50       384 (defined $disposition)
87             or die q/Content-Disposition header is missing in part/;
88              
89 176         558 my ($disposition_name, $disposition_filename) = extract_form_data($disposition);
90 176 100       446 defined $disposition_name
91             or die q/Parameter 'name' is missing from Content-Disposition header/;
92              
93 168         1206 $part = {
94             name => $disposition_name,
95             headers => $headers,
96             };
97              
98 168 100       512 if ( defined $disposition_filename ) {
99 88         235 $part->{filename} = $disposition_filename;
100 88   66     376 $self->{tempdir} ||= do {
101 42         238 my $dir = File::Temp->newdir('XXXXX', TMPDIR => 1, CLEANUP => 1);
102             # Temporary dirs will remove after the request.
103 42         16390 push @{$env->{'http.entity.parser.multipart.tempdir'}}, $dir;
  42         147  
104 42         321 $dir;
105              
106             };
107 88         737 my ($tempfh, $tempname) = tempfile(UNLINK => 0, DIR => $self->{tempdir});
108 88         26028 $part->{fh} = $tempfh;
109 88         423 $part->{tempname} = $tempname;
110             }
111             },
112             on_body => sub {
113 234     234   4127 my ($chunk, $final) = @_;
114              
115 234         418 my $fh = $part->{fh};
116 234 100       454 if ($fh) {
117 138 50       741 print $fh $chunk
118             or die qq/Could not write to file handle: '$!'/;
119 138 100 100     614 if ($final && $part->{filename} ne "" ) { # compatible with HTTP::Body
120 71 50       1987 seek($fh, 0, SEEK_SET)
121             or die qq/Could not rewind file handle: '$!'/;
122              
123 140         909 my @headers = map { split(/\s*:\s*/, $_, 2) }
124 71         207 @{$part->{headers}};
  71         266  
125             push @uploads, $part->{name}, {
126             name => $part->{name},
127             headers => \@headers,
128             size => -s $part->{fh},
129             filename => $part->{filename},
130             tempname => $part->{tempname},
131 71         1351 };
132             }
133             } else {
134 96         245 $part->{data} .= $chunk;
135 96 100       202 if ($final) {
136 80         257 push @params, $part->{name}, $part->{data};
137             }
138             }
139             },
140 63 50       693 $opts->{on_error} ? (on_error => $opts->{on_error}) : (),
141             );
142              
143 63         4803 $self->{parser} = $parser;
144 63         124 $self->{params} = \@params;
145 63         111 $self->{uploads} = \@uploads;
146              
147 63         195 return $self;
148             }
149              
150             sub add {
151 6987     6987 0 114416 my $self = shift;
152 6987 50       16520 $self->{parser}->parse($_[0]) if defined $_[0];
153             }
154              
155             sub finalize {
156 55     55 0 1294 my $self = shift;
157 55         209 (delete $self->{parser})->finish();
158 53         3441 return ($self->{params}, $self->{uploads});
159             }
160              
161              
162             1;
163              
164             __END__