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   142553 use strict;
  6         31  
  6         188  
4 6     6   33 use warnings;
  6         28  
  6         154  
5 6     6   2905 use HTTP::MultiPartParser;
  6         11138  
  6         247  
6 6     6   4658 use File::Temp qw/tempfile/;
  6         96824  
  6         399  
7 6     6   64 use Carp qw//;
  6         13  
  6         114  
8 6     6   29 use Fcntl ":seek";
  6         23  
  6         7920  
9              
10             #
11             # copy from https://gist.github.com/chansen/7163968
12             #
13             sub extract_form_data {
14 176     176 0 340 local $_ = shift;
15             # Fast exit for common form-data disposition
16 176 100       1258 if (/\A form-data; \s name="((?:[^"]|\\")*)" (?: ;\s filename="((?:[^"]|\\")*)" )? \z/x) {
17 148         620 return ($1, $2);
18             }
19              
20             # disposition type must be form-data
21             s/\A \s* form-data \s* ; //xi
22 28 100       156 or return;
23              
24 26         56 my (%p, $k, $v);
25 26         57 while (length) {
26 49         137 s/ ^ \s+ //x;
27 49         134 s/ \s+ $ //x;
28              
29             # skip empty parameters and unknown tokens
30 49 100       137 next if s/^ [^\s"=;]* \s* ; //x;
31              
32             # parameter name (token)
33 46 100       185 s/^ ([^\s"=;]+) \s* = \s* //x
34             or return;
35 44         115 $k = lc $1;
36             # quoted parameter value
37 44 100       206 if (s/^ "((?:[^"]|\\")*)" \s* (?: ; | $) //x) {
    100          
38 28         55 $v = $1;
39             }
40             # unquoted parameter value (token)
41             elsif (s/^ ([^\s";]*) \s* (?: ; | $) //x) {
42 15         29 $v = $1;
43             }
44             else {
45 1         6 return;
46             }
47 43 100 100     152 if ($k eq 'name' || $k eq 'filename') {
48 36 100       85 return () if exists $p{$k};
49 34         100 $p{$k} = $v;
50             }
51             }
52 21 100       117 return exists $p{name} ? @p{qw(name filename)} : ();
53             }
54              
55             sub new {
56 63     63 0 86588 my ($class, $env, $opts) = @_;
57              
58 63         172 my $self = bless { }, $class;
59              
60 63         122 my @uploads;
61             my @params;
62              
63 63 50       169 unless (defined $env->{CONTENT_TYPE}) {
64 0         0 Carp::croak("Missing CONTENT_TYPE in PSGI env");
65             }
66 63 50       390 unless ( $env->{CONTENT_TYPE} =~ /boundary=\"?([^\";]+)\"?/ ) {
67 0         0 Carp::croak("Invalid boundary in content_type: $env->{CONTENT_TYPE}");
68             }
69 63         182 my $boundary = $1;
70              
71              
72 63         101 my $part;
73             my $parser = HTTP::MultiPartParser->new(
74             boundary => $boundary,
75             on_header => sub {
76 176     176   6093 my ($headers) = @_;
77              
78 176         281 my $disposition;
79 176         373 foreach (@$headers) {
80 176 50       662 if (/\A Content-Disposition: [\x09\x20]* (.*)/xi) {
81 176         513 $disposition = $1;
82 176         332 last;
83             }
84             }
85              
86 176 50       381 (defined $disposition)
87             or die q/Content-Disposition header is missing in part/;
88              
89 176         367 my ($disposition_name, $disposition_filename) = extract_form_data($disposition);
90 176 100       442 defined $disposition_name
91             or die q/Parameter 'name' is missing from Content-Disposition header/;
92              
93 168         1234 $part = {
94             name => $disposition_name,
95             headers => $headers,
96             };
97              
98 168 100       515 if ( defined $disposition_filename ) {
99 88         194 $part->{filename} = $disposition_filename;
100 88   66     382 $self->{tempdir} ||= do {
101 42         250 my $dir = File::Temp->newdir('XXXXX', TMPDIR => 1, CLEANUP => 1);
102             # Temporary dirs will remove after the request.
103 42         16251 push @{$env->{'http.entity.parser.multipart.tempdir'}}, $dir;
  42         145  
104 42         353 $dir;
105              
106             };
107 88         731 my ($tempfh, $tempname) = tempfile(UNLINK => 0, DIR => $self->{tempdir});
108 88         26728 $part->{fh} = $tempfh;
109 88         359 $part->{tempname} = $tempname;
110             }
111             },
112             on_body => sub {
113 234     234   4609 my ($chunk, $final) = @_;
114              
115 234         396 my $fh = $part->{fh};
116 234 100       481 if ($fh) {
117 138 50       781 print $fh $chunk
118             or die qq/Could not write to file handle: '$!'/;
119 138 100 100     635 if ($final && $part->{filename} ne "" ) { # compatible with HTTP::Body
120 71 50       1992 seek($fh, 0, SEEK_SET)
121             or die qq/Could not rewind file handle: '$!'/;
122              
123 140         908 my @headers = map { split(/\s*:\s*/, $_, 2) }
124 71         224 @{$part->{headers}};
  71         249  
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         1471 };
132             }
133             } else {
134 96         206 $part->{data} .= $chunk;
135 96 100       207 if ($final) {
136 80         278 push @params, $part->{name}, $part->{data};
137             }
138             }
139             },
140 63 50       681 $opts->{on_error} ? (on_error => $opts->{on_error}) : (),
141             );
142              
143 63         4904 $self->{parser} = $parser;
144 63         136 $self->{params} = \@params;
145 63         110 $self->{uploads} = \@uploads;
146              
147 63         198 return $self;
148             }
149              
150             sub add {
151 6987     6987 0 114023 my $self = shift;
152 6987 50       15961 $self->{parser}->parse($_[0]) if defined $_[0];
153             }
154              
155             sub finalize {
156 55     55 0 1292 my $self = shift;
157 55         195 (delete $self->{parser})->finish();
158 53         3424 return ($self->{params}, $self->{uploads});
159             }
160              
161              
162             1;
163              
164             __END__