File Coverage

blib/lib/Mojo/Content/MultiPart.pm
Criterion Covered Total %
statement 97 97 100.0
branch 43 46 93.4
condition 15 17 88.2
subroutine 13 13 100.0
pod 7 7 100.0
total 175 180 97.2


line stmt bran cond sub pod time code
1             package Mojo::Content::MultiPart;
2 61     61   1058 use Mojo::Base 'Mojo::Content';
  61         201  
  61         461  
3              
4 61     61   894 use Mojo::Util qw(b64_encode);
  61         134  
  61         119698  
5              
6             has parts => sub { [] };
7              
8             sub body_contains {
9 39     39 1 118 my ($self, $chunk) = @_;
10 39   100     67 ($_->headers_contain($chunk) or $_->body_contains($chunk)) and return 1 for @{$self->parts};
  39   100     133  
11 25         111 return undef;
12             }
13              
14             sub body_size {
15 24     24 1 62 my $self = shift;
16              
17             # Check for existing Content-Length header
18 24 100       98 if (my $len = $self->headers->content_length) { return $len }
  2         12  
19              
20             # Calculate length of whole body
21 22         92 my $len = my $boundary_len = length($self->build_boundary) + 6;
22 22         72 $len += $_->header_size + $_->body_size + $boundary_len for @{$self->parts};
  22         114  
23              
24 22         99 return $len;
25             }
26              
27             sub build_boundary {
28 49     49 1 98 my $self = shift;
29              
30             # Check for existing boundary
31 49         87 my $boundary;
32 49 100       161 return $boundary if defined($boundary = $self->boundary);
33              
34             # Generate and check boundary
35 18         84 my $size = 1;
36 18         48 do {
37 18         354 $boundary = b64_encode join('', map chr(rand 256), 1 .. $size++ * 3);
38 18         177 $boundary =~ s/\W/X/g;
39             } while $self->body_contains($boundary);
40              
41             # Add boundary to Content-Type header
42 18         84 my $headers = $self->headers;
43 18         57 my ($before, $after) = ('multipart/mixed', '');
44 18 100 100     73 ($before, $after) = ($1, $2) if ($headers->content_type // '') =~ m!^(.*multipart/[^;]+)(.*)$!;
45 18         117 $headers->content_type("$before; boundary=$boundary$after");
46              
47 18         98 return $boundary;
48             }
49              
50             sub clone {
51 2     2 1 12 my $self = shift;
52 2 50       17 return undef unless my $clone = $self->SUPER::clone();
53 2         18 return $clone->parts($self->parts);
54             }
55              
56             sub get_body_chunk {
57 184     184 1 361 my ($self, $offset) = @_;
58              
59             # Body generator
60 184 50       495 return $self->generate_body_chunk($offset) if $self->is_dynamic;
61              
62             # First boundary
63 184   66     599 my $boundary = $self->{boundary} //= $self->build_boundary;
64 184         356 my $boundary_len = length($boundary) + 6;
65 184         301 my $len = $boundary_len - 2;
66 184 100       508 return substr "--$boundary\x0d\x0a", $offset if $len > $offset;
67              
68             # Skip parts that have already been processed
69 162         280 my $start = 0;
70 162 100 100     695 ($len, $start) = ($self->{last_len}, $self->{last_part} + 1) if $self->{offset} && $offset > $self->{offset};
71              
72             # Prepare content part by part
73 162         438 my $parts = $self->parts;
74 162         551 for (my $i = $start; $i < @$parts; $i++) {
75 192         353 my $part = $parts->[$i];
76              
77             # Headers
78 192         496 my $header_len = $part->header_size;
79 192 100       983 return $part->get_header_chunk($offset - $len) if ($len + $header_len) > $offset;
80 146         628 $len += $header_len;
81              
82             # Content
83 146         451 my $content_len = $part->body_size;
84 146 100       471 return $part->get_body_chunk($offset - $len) if ($len + $content_len) > $offset;
85 101         198 $len += $content_len;
86              
87             # Boundary
88 101 100       262 if ($#$parts == $i) {
89 47         144 $boundary .= '--';
90 47         93 $boundary_len += 2;
91             }
92 101 100       501 return substr "\x0d\x0a--$boundary\x0d\x0a", $offset - $len if ($len + $boundary_len) > $offset;
93 52         119 $len += $boundary_len;
94              
95 52         114 @{$self}{qw(last_len last_part offset)} = ($len, $i, $offset);
  52         361  
96             }
97             }
98              
99 155     155 1 680 sub is_multipart {1}
100              
101             sub new {
102 81     81 1 3004 my $self = shift->SUPER::new(@_);
103 81         454 $self->on(read => \&_read);
104 81         296 return $self;
105             }
106              
107             sub _parse_multipart_body {
108 276     276   578 my ($self, $boundary) = @_;
109              
110             # Whole part in buffer
111 276         885 my $pos = index $self->{multipart}, "\x0d\x0a--$boundary";
112 276 100       679 if ($pos < 0) {
113 177         362 my $len = length($self->{multipart}) - (length($boundary) + 8);
114 177 100       480 return undef unless $len > 0;
115              
116             # Store chunk
117 119         544 my $chunk = substr $self->{multipart}, 0, $len, '';
118 119         372 $self->parts->[-1] = $self->parts->[-1]->parse($chunk);
119 119         437 return undef;
120             }
121              
122             # Store chunk
123 99         329 my $chunk = substr $self->{multipart}, 0, $pos, '';
124 99         282 $self->parts->[-1] = $self->parts->[-1]->parse($chunk);
125 99         627 return !!($self->{multi_state} = 'multipart_boundary');
126             }
127              
128             sub _parse_multipart_boundary {
129 144     144   317 my ($self, $boundary) = @_;
130              
131             # Boundary begins
132 144 100       617 if ((index $self->{multipart}, "\x0d\x0a--$boundary\x0d\x0a") == 0) {
133 100         260 substr $self->{multipart}, 0, length($boundary) + 6, '';
134              
135             # New part
136 100         329 my $part = Mojo::Content::Single->new(relaxed => 1);
137 100         432 $self->emit(part => $part);
138 100         178 push @{$self->parts}, $part;
  100         327  
139 100         562 return !!($self->{multi_state} = 'multipart_body');
140             }
141              
142             # Boundary ends
143 44         191 my $end = "\x0d\x0a--$boundary--";
144 44 100       184 if ((index $self->{multipart}, $end) == 0) {
145 38         132 substr $self->{multipart}, 0, length $end, '';
146 38         117 $self->{multi_state} = 'finished';
147             }
148              
149 44         130 return undef;
150             }
151              
152             sub _parse_multipart_preamble {
153 57     57   162 my ($self, $boundary) = @_;
154              
155             # No boundary yet
156 57 100       334 return undef if (my $pos = index $self->{multipart}, "--$boundary") < 0;
157              
158             # Replace preamble with carriage return and line feed
159 40         145 substr $self->{multipart}, 0, $pos, "\x0d\x0a";
160              
161             # Parse boundary
162 40         251 return !!($self->{multi_state} = 'multipart_boundary');
163             }
164              
165             sub _read {
166 240     240   542 my ($self, $chunk) = @_;
167              
168 240         1328 $self->{multipart} .= $chunk;
169 240         636 my $boundary = $self->boundary;
170 240   100     1355 until (($self->{multi_state} //= 'multipart_preamble') eq 'finished') {
171              
172             # Preamble
173 477 100       1568 if ($self->{multi_state} eq 'multipart_preamble') { last unless $self->_parse_multipart_preamble($boundary) }
  57 100       202  
    100          
    50          
174              
175             # Boundary
176 144 100       391 elsif ($self->{multi_state} eq 'multipart_boundary') { last unless $self->_parse_multipart_boundary($boundary) }
177              
178             # Body
179 276 100       702 elsif ($self->{multi_state} eq 'multipart_body') { last unless $self->_parse_multipart_body($boundary) }
180             }
181              
182             # Check buffer size
183 240 100 50     957 @$self{qw(state limit)} = ('finished', 1) if length($self->{multipart} // '') > $self->max_buffer_size;
184             }
185              
186             1;
187              
188             =encoding utf8
189              
190             =head1 NAME
191              
192             Mojo::Content::MultiPart - HTTP multipart content
193              
194             =head1 SYNOPSIS
195              
196             use Mojo::Content::MultiPart;
197              
198             my $multi = Mojo::Content::MultiPart->new;
199             $multi->parse('Content-Type: multipart/mixed; boundary=---foobar');
200             my $single = $multi->parts->[4];
201              
202             =head1 DESCRIPTION
203              
204             L is a container for HTTP multipart content, based on L
205             7230|https://tools.ietf.org/html/rfc7230>, L and L
206             2388|https://tools.ietf.org/html/rfc2388>.
207              
208             =head1 EVENTS
209              
210             L inherits all events from L and can emit the following new ones.
211              
212             =head2 part
213              
214             $multi->on(part => sub ($multi, $single) {...});
215              
216             Emitted when a new L part starts.
217              
218             $multi->on(part => sub ($multi, $single) {
219             return unless $single->headers->content_disposition =~ /name="([^"]+)"/;
220             say "Field: $1";
221             });
222              
223             =head1 ATTRIBUTES
224              
225             L inherits all attributes from L and implements the following new ones.
226              
227             =head2 parts
228              
229             my $parts = $multi->parts;
230             $multi = $multi->parts([Mojo::Content::Single->new]);
231              
232             Content parts embedded in this multipart content, usually L objects.
233              
234             =head1 METHODS
235              
236             L inherits all methods from L and implements the following new ones.
237              
238             =head2 body_contains
239              
240             my $bool = $multi->body_contains('foobarbaz');
241              
242             Check if content parts contain a specific string.
243              
244             =head2 body_size
245              
246             my $size = $multi->body_size;
247              
248             Content size in bytes.
249              
250             =head2 build_boundary
251              
252             my $boundary = $multi->build_boundary;
253              
254             Generate a suitable boundary for content and add it to C header.
255              
256             =head2 clone
257              
258             my $clone = $multi->clone;
259              
260             Return a new L object cloned from this content if possible, otherwise return C.
261              
262             =head2 get_body_chunk
263              
264             my $bytes = $multi->get_body_chunk(0);
265              
266             Get a chunk of content starting from a specific position. Note that it might not be possible to get the same chunk
267             twice if content was generated dynamically.
268              
269             =head2 is_multipart
270              
271             my $bool = $multi->is_multipart;
272              
273             True, this is a L object.
274              
275             =head2 new
276              
277             my $multi = Mojo::Content::MultiPart->new;
278             my $multi
279             = Mojo::Content::MultiPart->new(parts => [Mojo::Content::Single->new]);
280             my $multi
281             = Mojo::Content::MultiPart->new({parts => [Mojo::Content::Single->new]});
282              
283             Construct a new L object and subscribe to event L with default content
284             parser.
285              
286             =head1 SEE ALSO
287              
288             L, L, L.
289              
290             =cut