1.08
[catagits/HTTP-Body.git] / lib / HTTP / Body.pm
CommitLineData
32b29b79 1package HTTP::Body;
2
3use strict;
4
348fdd5a 5use Carp qw[ ];
32b29b79 6
7e2df1d9 7our $TYPES = {
4f5db602 8 'application/octet-stream' => 'HTTP::Body::OctetStream',
9 'application/x-www-form-urlencoded' => 'HTTP::Body::UrlEncoded',
5940e4c7 10 'multipart/form-data' => 'HTTP::Body::MultiPart',
11 'multipart/related' => 'HTTP::Body::XFormsMultipart',
12 'application/xml' => 'HTTP::Body::XForms'
32b29b79 13};
14
b018320d 15require HTTP::Body::OctetStream;
16require HTTP::Body::UrlEncoded;
17require HTTP::Body::MultiPart;
5940e4c7 18require HTTP::Body::XFormsMultipart;
19require HTTP::Body::XForms;
b018320d 20
0a66fd23 21use HTTP::Headers;
22use HTTP::Message;
23
aac7ca02 24=head1 NAME
25
26HTTP::Body - HTTP Body Parser
27
28=head1 SYNOPSIS
29
30 use HTTP::Body;
17c3e9b3 31
32 sub handler : method {
33 my ( $class, $r ) = @_;
34
35 my $content_type = $r->headers_in->get('Content-Type');
36 my $content_length = $r->headers_in->get('Content-Length');
37
38 my $body = HTTP::Body->new( $content_type, $content_length );
39 my $length = $content_length;
40
41 while ( $length ) {
42
43 $r->read( my $buffer, ( $length < 8192 ) ? $length : 8192 );
44
45 $length -= length($buffer);
46
47 $body->add($buffer);
48 }
49
50 my $uploads = $body->upload; # hashref
51 my $params = $body->param; # hashref
52 my $body = $body->body; # IO::Handle
53 }
aac7ca02 54
55=head1 DESCRIPTION
56
6215b02b 57HTTP::Body parses chunks of HTTP POST data and supports
58application/octet-stream, application/x-www-form-urlencoded, and
59multipart/form-data.
60
0a66fd23 61Chunked bodies are supported by not passing a length value to new().
62
6215b02b 63It is currently used by L<Catalyst> to parse POST bodies.
aac7ca02 64
1ced50e0 65=head1 NOTES
66
67When parsing multipart bodies, temporary files are created to store any
68uploaded files. You must delete these temporary files yourself after
b1da105b 69processing them, or set $body->cleanup(1) to automatically delete them
70at DESTROY-time.
1ced50e0 71
aac7ca02 72=head1 METHODS
73
6153c112 74=over 4
75
76=item new
77
78Constructor. Takes content type and content length as parameters,
79returns a L<HTTP::Body> object.
aac7ca02 80
81=cut
82
32b29b79 83sub new {
84 my ( $class, $content_type, $content_length ) = @_;
85
0a66fd23 86 unless ( @_ >= 2 ) {
87 Carp::croak( $class, '->new( $content_type, [ $content_length ] )' );
32b29b79 88 }
7e2df1d9 89
27ee4e94 90 my $type;
91 foreach my $supported ( keys %{$TYPES} ) {
92 if ( index( lc($content_type), $supported ) >= 0 ) {
93 $type = $supported;
94 }
95 }
96
7e2df1d9 97 my $body = $TYPES->{ $type || 'application/octet-stream' };
98
32b29b79 99 my $self = {
b1da105b 100 cleanup => 0,
32b29b79 101 buffer => '',
0a66fd23 102 chunk_buffer => '',
44761c00 103 body => undef,
0a66fd23 104 chunked => !defined $content_length,
105 content_length => defined $content_length ? $content_length : -1,
32b29b79 106 content_type => $content_type,
58050177 107 length => 0,
7e2df1d9 108 param => {},
109 state => 'buffering',
3debb7c0 110 upload => {},
111 tmpdir => File::Spec->tmpdir(),
32b29b79 112 };
113
114 bless( $self, $body );
7e2df1d9 115
32b29b79 116 return $self->init;
117}
118
b1da105b 119sub DESTROY {
120 my $self = shift;
121
122 if ( $self->{cleanup} ) {
123 my @temps = ();
124 for my $upload ( values %{ $self->{upload} } ) {
125 push @temps, map { $_->{tempname} || () }
126 ( ref $upload eq 'ARRAY' ? @{$upload} : $upload );
127 }
128
129 unlink map { $_ } grep { -e $_ } @temps;
130 }
131}
132
aac7ca02 133=item add
134
4deaf0f0 135Add string to internal buffer. Will call spin unless done. returns
6153c112 136length before adding self.
137
aac7ca02 138=cut
139
32b29b79 140sub add {
58050177 141 my $self = shift;
304dca13 142
0a66fd23 143 if ( $self->{chunked} ) {
144 $self->{chunk_buffer} .= $_[0];
145
146 while ( $self->{chunk_buffer} =~ m/^([\da-fA-F]+).*\x0D\x0A/ ) {
147 my $chunk_len = hex($1);
148
149 if ( $chunk_len == 0 ) {
150 # Strip chunk len
151 $self->{chunk_buffer} =~ s/^([\da-fA-F]+).*\x0D\x0A//;
152
153 # End of data, there may be trailing headers
154 if ( my ($headers) = $self->{chunk_buffer} =~ m/(.*)\x0D\x0A/s ) {
155 if ( my $message = HTTP::Message->parse( $headers ) ) {
156 $self->{trailing_headers} = $message->headers;
157 }
158 }
159
160 $self->{chunk_buffer} = '';
161
162 # Set content_length equal to the amount of data we read,
163 # so the spin methods can finish up.
164 $self->{content_length} = $self->{length};
165 }
166 else {
167 # Make sure we have the whole chunk in the buffer (+CRLF)
168 if ( length( $self->{chunk_buffer} ) >= $chunk_len ) {
169 # Strip chunk len
170 $self->{chunk_buffer} =~ s/^([\da-fA-F]+).*\x0D\x0A//;
171
172 # Pull chunk data out of chunk buffer into real buffer
173 $self->{buffer} .= substr $self->{chunk_buffer}, 0, $chunk_len, '';
174
175 # Strip remaining CRLF
176 $self->{chunk_buffer} =~ s/^\x0D\x0A//;
177
178 $self->{length} += $chunk_len;
179 }
180 else {
181 # Not enough data for this chunk, wait for more calls to add()
182 return;
183 }
184 }
185
186 unless ( $self->{state} eq 'done' ) {
187 $self->spin;
188 }
189 }
190
191 return;
192 }
193
304dca13 194 my $cl = $self->content_length;
7e2df1d9 195
58050177 196 if ( defined $_[0] ) {
7e2df1d9 197 $self->{length} += length( $_[0] );
304dca13 198
199 # Don't allow buffer data to exceed content-length
200 if ( $self->{length} > $cl ) {
201 $_[0] = substr $_[0], 0, $cl - $self->{length};
202 $self->{length} = $cl;
203 }
204
205 $self->{buffer} .= $_[0];
58050177 206 }
aac7ca02 207
7e2df1d9 208 unless ( $self->state eq 'done' ) {
209 $self->spin;
210 }
211
304dca13 212 return ( $self->length - $cl );
32b29b79 213}
214
aac7ca02 215=item body
216
6153c112 217accessor for the body.
218
aac7ca02 219=cut
220
32b29b79 221sub body {
222 my $self = shift;
223 $self->{body} = shift if @_;
224 return $self->{body};
225}
226
0a66fd23 227=item chunked
aac7ca02 228
0a66fd23 229Returns 1 if the request is chunked.
6153c112 230
aac7ca02 231=cut
232
0a66fd23 233sub chunked {
234 return shift->{chunked};
58050177 235}
236
b1da105b 237=item cleanup
238
239Set to 1 to enable automatic deletion of temporary files at DESTROY-time.
240
241=cut
242
243sub cleanup {
244 my $self = shift;
245 $self->{cleanup} = shift if @_;
246 return $self->{cleanup};
247}
248
aac7ca02 249=item content_length
250
0a66fd23 251Returns the content-length for the body data if known.
252Returns -1 if the request is chunked.
6153c112 253
aac7ca02 254=cut
255
32b29b79 256sub content_length {
257 return shift->{content_length};
258}
259
aac7ca02 260=item content_type
261
0a66fd23 262Returns the content-type of the body data.
6153c112 263
aac7ca02 264=cut
265
32b29b79 266sub content_type {
267 return shift->{content_type};
268}
269
aac7ca02 270=item init
271
6153c112 272return self.
273
aac7ca02 274=cut
275
58050177 276sub init {
277 return $_[0];
278}
279
aac7ca02 280=item length
281
0a66fd23 282Returns the total length of data we expect to read if known.
283In the case of a chunked request, returns the amount of data
284read so far.
6153c112 285
aac7ca02 286=cut
287
58050177 288sub length {
289 return shift->{length};
290}
291
0a66fd23 292=item trailing_headers
293
294If a chunked request body had trailing headers, trailing_headers will
295return an HTTP::Headers object populated with those headers.
296
297=cut
298
299sub trailing_headers {
300 return shift->{trailing_headers};
301}
302
aac7ca02 303=item spin
304
6153c112 305Abstract method to spin the io handle.
306
aac7ca02 307=cut
308
58050177 309sub spin {
310 Carp::croak('Define abstract method spin() in implementation');
311}
312
aac7ca02 313=item state
314
0a66fd23 315Returns the current state of the parser.
6153c112 316
aac7ca02 317=cut
318
7e2df1d9 319sub state {
320 my $self = shift;
321 $self->{state} = shift if @_;
aac7ca02 322 return $self->{state};
323}
324
aac7ca02 325=item param
326
0a66fd23 327Get/set body parameters.
6153c112 328
aac7ca02 329=cut
330
32b29b79 331sub param {
332 my $self = shift;
333
334 if ( @_ == 2 ) {
335
336 my ( $name, $value ) = @_;
337
338 if ( exists $self->{param}->{$name} ) {
339 for ( $self->{param}->{$name} ) {
340 $_ = [$_] unless ref($_) eq "ARRAY";
341 push( @$_, $value );
342 }
343 }
344 else {
345 $self->{param}->{$name} = $value;
346 }
347 }
348
349 return $self->{param};
350}
351
aac7ca02 352=item upload
353
0a66fd23 354Get/set file uploads.
355
aac7ca02 356=cut
357
32b29b79 358sub upload {
359 my $self = shift;
360
361 if ( @_ == 2 ) {
362
363 my ( $name, $upload ) = @_;
364
365 if ( exists $self->{upload}->{$name} ) {
366 for ( $self->{upload}->{$name} ) {
367 $_ = [$_] unless ref($_) eq "ARRAY";
368 push( @$_, $upload );
369 }
370 }
371 else {
372 $self->{upload}->{$name} = $upload;
373 }
374 }
375
376 return $self->{upload};
377}
378
3debb7c0 379=item tmpdir
380
381Specify a different path for temporary files. Defaults to the system temporary path.
382
383=cut
384
385sub tmpdir {
386 my $self = shift;
387 $self->{tmpdir} = shift if @_;
388 return $self->{tmpdir};
389}
390
aac7ca02 391=back
392
393=head1 AUTHOR
394
f994d0c8 395Christian Hansen, C<chansen@cpan.org>
17c3e9b3 396
397Sebastian Riedel, C<sri@cpan.org>
aac7ca02 398
0a66fd23 399Andy Grundman, C<andy@hybridized.org>
400
aac7ca02 401=head1 LICENSE
402
17c3e9b3 403This library is free software. You can redistribute it and/or modify
aac7ca02 404it under the same terms as perl itself.
405
406=cut
407
32b29b79 4081;