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   143498 use strict;
  6         34  
  6         192  
4 6     6   34 use warnings;
  6         25  
  6         161  
5 6     6   3061 use HTTP::MultiPartParser;
  6         11358  
  6         211  
6 6     6   4597 use File::Temp qw/tempfile/;
  6         97675  
  6         409  
7 6     6   51 use Carp qw//;
  6         13  
  6         117  
8 6     6   33 use Fcntl ":seek";
  6         15  
  6         7895  
9              
10             #
11             # copy from https://gist.github.com/chansen/7163968
12             #
13             sub extract_form_data {
14 176     176 0 320 local $_ = shift;
15             # Fast exit for common form-data disposition
16 176 100       1208 if (/\A form-data; \s name="((?:[^"]|\\")*)" (?: ;\s filename="((?:[^"]|\\")*)" )? \z/x) {
17 148         622 return ($1, $2);
18             }
19              
20             # disposition type must be form-data
21             s/\A \s* form-data \s* ; //xi
22 28 100       152 or return;
23              
24 26         62 my (%p, $k, $v);
25 26         65 while (length) {
26 49         193 s/ ^ \s+ //x;
27 49         511 s/ \s+ $ //x;
28              
29             # skip empty parameters and unknown tokens
30 49 100       153 next if s/^ [^\s"=;]* \s* ; //x;
31              
32             # parameter name (token)
33 46 100       175 s/^ ([^\s"=;]+) \s* = \s* //x
34             or return;
35 44         122 $k = lc $1;
36             # quoted parameter value
37 44 100       229 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     159 if ($k eq 'name' || $k eq 'filename') {
48 36 100       89 return () if exists $p{$k};
49 34         105 $p{$k} = $v;
50             }
51             }
52 21 100       112 return exists $p{name} ? @p{qw(name filename)} : ();
53             }
54              
55             sub new {
56 63     63 0 87620 my ($class, $env, $opts) = @_;
57              
58 63         171 my $self = bless { }, $class;
59              
60 63         126 my @uploads;
61             my @params;
62              
63 63 50       187 unless (defined $env->{CONTENT_TYPE}) {
64 0         0 Carp::croak("Missing CONTENT_TYPE in PSGI env");
65             }
66 63 50       428 unless ( $env->{CONTENT_TYPE} =~ /boundary=\"?([^\";]+)\"?/ ) {
67 0         0 Carp::croak("Invalid boundary in content_type: $env->{CONTENT_TYPE}");
68             }
69 63         179 my $boundary = $1;
70              
71              
72 63         90 my $part;
73             my $parser = HTTP::MultiPartParser->new(
74             boundary => $boundary,
75             on_header => sub {
76 176     176   6161 my ($headers) = @_;
77              
78 176         284 my $disposition;
79 176         340 foreach (@$headers) {
80 176 50       674 if (/\A Content-Disposition: [\x09\x20]* (.*)/xi) {
81 176         515 $disposition = $1;
82 176         294 last;
83             }
84             }
85              
86 176 50       357 (defined $disposition)
87             or die q/Content-Disposition header is missing in part/;
88              
89 176         365 my ($disposition_name, $disposition_filename) = extract_form_data($disposition);
90 176 100       479 defined $disposition_name
91             or die q/Parameter 'name' is missing from Content-Disposition header/;
92              
93 168         1231 $part = {
94             name => $disposition_name,
95             headers => $headers,
96             };
97              
98 168 100       518 if ( defined $disposition_filename ) {
99 88         195 $part->{filename} = $disposition_filename;
100 88   66     372 $self->{tempdir} ||= do {
101 42         275 my $dir = File::Temp->newdir('XXXXX', TMPDIR => 1, CLEANUP => 1);
102             # Temporary dirs will remove after the request.
103 42         16687 push @{$env->{'http.entity.parser.multipart.tempdir'}}, $dir;
  42         158  
104 42         345 $dir;
105              
106             };
107 88         756 my ($tempfh, $tempname) = tempfile(UNLINK => 0, DIR => $self->{tempdir});
108 88         26988 $part->{fh} = $tempfh;
109 88         360 $part->{tempname} = $tempname;
110             }
111             },
112             on_body => sub {
113 234     234   4242 my ($chunk, $final) = @_;
114              
115 234         403 my $fh = $part->{fh};
116 234 100       478 if ($fh) {
117 138 50       792 print $fh $chunk
118             or die qq/Could not write to file handle: '$!'/;
119 138 100 100     625 if ($final && $part->{filename} ne "" ) { # compatible with HTTP::Body
120 71 50       2123 seek($fh, 0, SEEK_SET)
121             or die qq/Could not rewind file handle: '$!'/;
122              
123 140         901 my @headers = map { split(/\s*:\s*/, $_, 2) }
124 71         213 @{$part->{headers}};
  71         241  
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         1434 };
132             }
133             } else {
134 96         196 $part->{data} .= $chunk;
135 96 100       222 if ($final) {
136 80         268 push @params, $part->{name}, $part->{data};
137             }
138             }
139             },
140 63 50       797 $opts->{on_error} ? (on_error => $opts->{on_error}) : (),
141             );
142              
143 63         5078 $self->{parser} = $parser;
144 63         136 $self->{params} = \@params;
145 63         124 $self->{uploads} = \@uploads;
146              
147 63         199 return $self;
148             }
149              
150             sub add {
151 6987     6987 0 113879 my $self = shift;
152 6987 50       16108 $self->{parser}->parse($_[0]) if defined $_[0];
153             }
154              
155             sub finalize {
156 55     55 0 1339 my $self = shift;
157 55         202 (delete $self->{parser})->finish();
158 53         3418 return ($self->{params}, $self->{uploads});
159             }
160              
161              
162             1;
163              
164             __END__