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 60     60   954 use Mojo::Base 'Mojo::Content';
  60         2549  
  60         500  
3              
4 60     60   483 use Mojo::Util qw(b64_encode);
  60         209  
  60         113981  
5              
6             has parts => sub { [] };
7              
8             sub body_contains {
9 39     39 1 119 my ($self, $chunk) = @_;
10 39   100     68 ($_->headers_contain($chunk) or $_->body_contains($chunk)) and return 1 for @{$self->parts};
  39   100     119  
11 25         109 return undef;
12             }
13              
14             sub body_size {
15 24     24 1 58 my $self = shift;
16              
17             # Check for existing Content-Length header
18 24 100       83 if (my $len = $self->headers->content_length) { return $len }
  2         15  
19              
20             # Calculate length of whole body
21 22         94 my $len = my $boundary_len = length($self->build_boundary) + 6;
22 22         59 $len += $_->header_size + $_->body_size + $boundary_len for @{$self->parts};
  22         88  
23              
24 22         92 return $len;
25             }
26              
27             sub build_boundary {
28 49     49 1 121 my $self = shift;
29              
30             # Check for existing boundary
31 49         121 my $boundary;
32 49 100       186 return $boundary if defined($boundary = $self->boundary);
33              
34             # Generate and check boundary
35 18         57 my $size = 1;
36 18         47 do {
37 18         267 $boundary = b64_encode join('', map chr(rand 256), 1 .. $size++ * 3);
38 18         187 $boundary =~ s/\W/X/g;
39             } while $self->body_contains($boundary);
40              
41             # Add boundary to Content-Type header
42 18         80 my $headers = $self->headers;
43 18         72 my ($before, $after) = ('multipart/mixed', '');
44 18 100 100     76 ($before, $after) = ($1, $2) if ($headers->content_type // '') =~ m!^(.*multipart/[^;]+)(.*)$!;
45 18         139 $headers->content_type("$before; boundary=$boundary$after");
46              
47 18         90 return $boundary;
48             }
49              
50             sub clone {
51 2     2 1 6 my $self = shift;
52 2 50       25 return undef unless my $clone = $self->SUPER::clone();
53 2         7 return $clone->parts($self->parts);
54             }
55              
56             sub get_body_chunk {
57 184     184 1 364 my ($self, $offset) = @_;
58              
59             # Body generator
60 184 50       479 return $self->generate_body_chunk($offset) if $self->is_dynamic;
61              
62             # First boundary
63 184   66     595 my $boundary = $self->{boundary} //= $self->build_boundary;
64 184         388 my $boundary_len = length($boundary) + 6;
65 184         361 my $len = $boundary_len - 2;
66 184 100       506 return substr "--$boundary\x0d\x0a", $offset if $len > $offset;
67              
68             # Skip parts that have already been processed
69 162         298 my $start = 0;
70 162 100 100     615 ($len, $start) = ($self->{last_len}, $self->{last_part} + 1) if $self->{offset} && $offset > $self->{offset};
71              
72             # Prepare content part by part
73 162         397 my $parts = $self->parts;
74 162         511 for (my $i = $start; $i < @$parts; $i++) {
75 192         359 my $part = $parts->[$i];
76              
77             # Headers
78 192         450 my $header_len = $part->header_size;
79 192 100       543 return $part->get_header_chunk($offset - $len) if ($len + $header_len) > $offset;
80 146         228 $len += $header_len;
81              
82             # Content
83 146         411 my $content_len = $part->body_size;
84 146 100       458 return $part->get_body_chunk($offset - $len) if ($len + $content_len) > $offset;
85 101         172 $len += $content_len;
86              
87             # Boundary
88 101 100       251 if ($#$parts == $i) {
89 47         106 $boundary .= '--';
90 47         101 $boundary_len += 2;
91             }
92 101 100       418 return substr "\x0d\x0a--$boundary\x0d\x0a", $offset - $len if ($len + $boundary_len) > $offset;
93 52         100 $len += $boundary_len;
94              
95 52         115 @{$self}{qw(last_len last_part offset)} = ($len, $i, $offset);
  52         337  
96             }
97             }
98              
99 155     155 1 752 sub is_multipart {1}
100              
101             sub new {
102 81     81 1 2995 my $self = shift->SUPER::new(@_);
103 81         479 $self->on(read => \&_read);
104 81         286 return $self;
105             }
106              
107             sub _parse_multipart_body {
108 275     275   556 my ($self, $boundary) = @_;
109              
110             # Whole part in buffer
111 275         930 my $pos = index $self->{multipart}, "\x0d\x0a--$boundary";
112 275 100       630 if ($pos < 0) {
113 176         345 my $len = length($self->{multipart}) - (length($boundary) + 8);
114 176 100       499 return undef unless $len > 0;
115              
116             # Store chunk
117 116         696 my $chunk = substr $self->{multipart}, 0, $len, '';
118 116         309 $self->parts->[-1] = $self->parts->[-1]->parse($chunk);
119 116         379 return undef;
120             }
121              
122             # Store chunk
123 99         313 my $chunk = substr $self->{multipart}, 0, $pos, '';
124 99         326 $self->parts->[-1] = $self->parts->[-1]->parse($chunk);
125 99         580 return !!($self->{multi_state} = 'multipart_boundary');
126             }
127              
128             sub _parse_multipart_boundary {
129 144     144   314 my ($self, $boundary) = @_;
130              
131             # Boundary begins
132 144 100       556 if ((index $self->{multipart}, "\x0d\x0a--$boundary\x0d\x0a") == 0) {
133 100         276 substr $self->{multipart}, 0, length($boundary) + 6, '';
134              
135             # New part
136 100         322 my $part = Mojo::Content::Single->new(relaxed => 1);
137 100         375 $self->emit(part => $part);
138 100         161 push @{$self->parts}, $part;
  100         285  
139 100         521 return !!($self->{multi_state} = 'multipart_body');
140             }
141              
142             # Boundary ends
143 44         142 my $end = "\x0d\x0a--$boundary--";
144 44 100       180 if ((index $self->{multipart}, $end) == 0) {
145 38         105 substr $self->{multipart}, 0, length $end, '';
146 38         83 $self->{multi_state} = 'finished';
147             }
148              
149 44         136 return undef;
150             }
151              
152             sub _parse_multipart_preamble {
153 57     57   143 my ($self, $boundary) = @_;
154              
155             # No boundary yet
156 57 100       313 return undef if (my $pos = index $self->{multipart}, "--$boundary") < 0;
157              
158             # Replace preamble with carriage return and line feed
159 40         109 substr $self->{multipart}, 0, $pos, "\x0d\x0a";
160              
161             # Parse boundary
162 40         211 return !!($self->{multi_state} = 'multipart_boundary');
163             }
164              
165             sub _read {
166 239     239   481 my ($self, $chunk) = @_;
167              
168 239         1281 $self->{multipart} .= $chunk;
169 239         630 my $boundary = $self->boundary;
170 239   100     1218 until (($self->{multi_state} //= 'multipart_preamble') eq 'finished') {
171              
172             # Preamble
173 476 100       1574 if ($self->{multi_state} eq 'multipart_preamble') { last unless $self->_parse_multipart_preamble($boundary) }
  57 100       173  
    100          
    50          
174              
175             # Boundary
176 144 100       364 elsif ($self->{multi_state} eq 'multipart_boundary') { last unless $self->_parse_multipart_boundary($boundary) }
177              
178             # Body
179 275 100       651 elsif ($self->{multi_state} eq 'multipart_body') { last unless $self->_parse_multipart_body($boundary) }
180             }
181              
182             # Check buffer size
183 239 100 50     920 @$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