Fixed a bug where c->read didn't work properly, and added some tests for parse_on_dem...
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Engine.pm
CommitLineData
fc7ec1d9 1package Catalyst::Engine;
2
3use strict;
fbcc39ad 4use base 'Class::Accessor::Fast';
fa32ac82 5use CGI::Simple::Cookie;
f63c03e4 6use Data::Dump qw/dump/;
fc7ec1d9 7use HTML::Entities;
fbcc39ad 8use HTTP::Body;
fc7ec1d9 9use HTTP::Headers;
e0616220 10use URI::QueryParam;
2832cb5d 11use Scalar::Util ();
fbcc39ad 12
13# input position and length
4f5ebacd 14__PACKAGE__->mk_accessors(qw/read_position read_length/);
fbcc39ad 15
16# Stringify to class
17use overload '""' => sub { return ref shift }, fallback => 1;
fc7ec1d9 18
4bd82c41 19# Amount of data to read from input on each pass
4bb8bd62 20our $CHUNKSIZE = 64 * 1024;
4bd82c41 21
fc7ec1d9 22=head1 NAME
23
24Catalyst::Engine - The Catalyst Engine
25
26=head1 SYNOPSIS
27
28See L<Catalyst>.
29
30=head1 DESCRIPTION
31
23f9d934 32=head1 METHODS
fc7ec1d9 33
cd3bb248 34
b5ecfcf0 35=head2 $self->finalize_body($c)
06e1b616 36
fbcc39ad 37Finalize body. Prints the response output.
06e1b616 38
39=cut
40
fbcc39ad 41sub finalize_body {
42 my ( $self, $c ) = @_;
7257e9db 43 my $body = $c->response->body;
f9b6d612 44 no warnings 'uninitialized';
45 if ( Scalar::Util::blessed($body) && $body->can('read') or ref($body) eq 'GLOB' ) {
7257e9db 46 while ( !eof $body ) {
4c423abf 47 read $body, my ($buffer), $CHUNKSIZE;
6484fba0 48 last unless $self->write( $c, $buffer );
f4a57de4 49 }
7257e9db 50 close $body;
f4a57de4 51 }
52 else {
7257e9db 53 $self->write( $c, $body );
f4a57de4 54 }
fbcc39ad 55}
6dc87a0f 56
b5ecfcf0 57=head2 $self->finalize_cookies($c)
6dc87a0f 58
fa32ac82 59Create CGI::Simple::Cookie objects from $c->res->cookies, and set them as
60response headers.
4ab87e27 61
6dc87a0f 62=cut
63
64sub finalize_cookies {
fbcc39ad 65 my ( $self, $c ) = @_;
6dc87a0f 66
fbcc39ad 67 my @cookies;
c82ed742 68
69 foreach my $name ( keys %{ $c->response->cookies } ) {
70
71 my $val = $c->response->cookies->{$name};
fbcc39ad 72
2832cb5d 73 my $cookie = (
74 Scalar::Util::blessed($val)
75 ? $val
76 : CGI::Simple::Cookie->new(
77 -name => $name,
78 -value => $val->{value},
79 -expires => $val->{expires},
80 -domain => $val->{domain},
81 -path => $val->{path},
82 -secure => $val->{secure} || 0
83 )
6dc87a0f 84 );
85
fbcc39ad 86 push @cookies, $cookie->as_string;
6dc87a0f 87 }
6dc87a0f 88
b39840da 89 for my $cookie (@cookies) {
90 $c->res->headers->push_header( 'Set-Cookie' => $cookie );
fbcc39ad 91 }
92}
969647fd 93
b5ecfcf0 94=head2 $self->finalize_error($c)
969647fd 95
4ab87e27 96Output an apropriate error message, called if there's an error in $c
97after the dispatch has finished. Will output debug messages if Catalyst
98is in debug mode, or a `please come back later` message otherwise.
99
969647fd 100=cut
101
102sub finalize_error {
fbcc39ad 103 my ( $self, $c ) = @_;
969647fd 104
7299a7b4 105 $c->res->content_type('text/html; charset=utf-8');
34d28dfd 106 my $name = $c->config->{name} || join(' ', split('::', ref $c));
969647fd 107
108 my ( $title, $error, $infos );
109 if ( $c->debug ) {
62d9b030 110
111 # For pretty dumps
b5ecfcf0 112 $error = join '', map {
113 '<p><code class="error">'
114 . encode_entities($_)
115 . '</code></p>'
116 } @{ $c->error };
969647fd 117 $error ||= 'No output';
2666dd3b 118 $error = qq{<pre wrap="">$error</pre>};
969647fd 119 $title = $name = "$name on Catalyst $Catalyst::VERSION";
d82cc9ae 120 $name = "<h1>$name</h1>";
fbcc39ad 121
122 # Don't show context in the dump
123 delete $c->req->{_context};
124 delete $c->res->{_context};
125
126 # Don't show body parser in the dump
127 delete $c->req->{_body};
128
129 # Don't show response header state in dump
130 delete $c->res->{_finalized_headers};
131
c6ef5e69 132 my @infos;
133 my $i = 0;
c6ef5e69 134 for my $dump ( $c->dump_these ) {
c6ef5e69 135 my $name = $dump->[0];
f63c03e4 136 my $value = encode_entities( dump( $dump->[1] ));
c6ef5e69 137 push @infos, sprintf <<"EOF", $name, $value;
9619f23c 138<h2><a href="#" onclick="toggleDump('dump_$i'); return false">%s</a></h2>
c6ef5e69 139<div id="dump_$i">
2666dd3b 140 <pre wrap="">%s</pre>
c6ef5e69 141</div>
142EOF
143 $i++;
144 }
145 $infos = join "\n", @infos;
969647fd 146 }
147 else {
148 $title = $name;
149 $error = '';
150 $infos = <<"";
151<pre>
152(en) Please come back later
0c2b4ac0 153(fr) SVP veuillez revenir plus tard
969647fd 154(de) Bitte versuchen sie es spaeter nocheinmal
d82cc9ae 155(at) Konnten's bitt'schoen spaeter nochmal reinschauen
969647fd 156(no) Vennligst prov igjen senere
d82cc9ae 157(dk) Venligst prov igen senere
158(pl) Prosze sprobowac pozniej
969647fd 159</pre>
160
161 $name = '';
162 }
e060fe05 163 $c->res->body( <<"" );
7299a7b4 164<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
165 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
166<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
969647fd 167<head>
7299a7b4 168 <meta http-equiv="Content-Language" content="en" />
169 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
969647fd 170 <title>$title</title>
7299a7b4 171 <script type="text/javascript">
c6ef5e69 172 <!--
173 function toggleDump (dumpElement) {
7299a7b4 174 var e = document.getElementById( dumpElement );
175 if (e.style.display == "none") {
176 e.style.display = "";
c6ef5e69 177 }
178 else {
7299a7b4 179 e.style.display = "none";
c6ef5e69 180 }
181 }
182 -->
183 </script>
969647fd 184 <style type="text/css">
185 body {
186 font-family: "Bitstream Vera Sans", "Trebuchet MS", Verdana,
187 Tahoma, Arial, helvetica, sans-serif;
34d28dfd 188 color: #333;
969647fd 189 background-color: #eee;
190 margin: 0px;
191 padding: 0px;
192 }
c6ef5e69 193 :link, :link:hover, :visited, :visited:hover {
34d28dfd 194 color: #000;
c6ef5e69 195 }
969647fd 196 div.box {
9619f23c 197 position: relative;
969647fd 198 background-color: #ccc;
199 border: 1px solid #aaa;
200 padding: 4px;
201 margin: 10px;
969647fd 202 }
203 div.error {
34d28dfd 204 background-color: #cce;
969647fd 205 border: 1px solid #755;
206 padding: 8px;
207 margin: 4px;
208 margin-bottom: 10px;
969647fd 209 }
210 div.infos {
34d28dfd 211 background-color: #eee;
969647fd 212 border: 1px solid #575;
213 padding: 8px;
214 margin: 4px;
215 margin-bottom: 10px;
969647fd 216 }
217 div.name {
34d28dfd 218 background-color: #cce;
969647fd 219 border: 1px solid #557;
220 padding: 8px;
221 margin: 4px;
969647fd 222 }
7f8e0078 223 code.error {
224 display: block;
225 margin: 1em 0;
226 overflow: auto;
7f8e0078 227 }
9619f23c 228 div.name h1, div.error p {
229 margin: 0;
230 }
231 h2 {
232 margin-top: 0;
233 margin-bottom: 10px;
234 font-size: medium;
235 font-weight: bold;
236 text-decoration: underline;
237 }
238 h1 {
239 font-size: medium;
240 font-weight: normal;
241 }
2666dd3b 242 /* from http://users.tkk.fi/~tkarvine/linux/doc/pre-wrap/pre-wrap-css3-mozilla-opera-ie.html */
243 /* Browser specific (not valid) styles to make preformatted text wrap */
244 pre {
245 white-space: pre-wrap; /* css-3 */
246 white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
247 white-space: -pre-wrap; /* Opera 4-6 */
248 white-space: -o-pre-wrap; /* Opera 7 */
249 word-wrap: break-word; /* Internet Explorer 5.5+ */
250 }
969647fd 251 </style>
252</head>
253<body>
254 <div class="box">
255 <div class="error">$error</div>
256 <div class="infos">$infos</div>
257 <div class="name">$name</div>
258 </div>
259</body>
260</html>
261
d82cc9ae 262
263 # Trick IE
264 $c->res->{body} .= ( ' ' x 512 );
265
266 # Return 500
33117422 267 $c->res->status(500);
969647fd 268}
269
b5ecfcf0 270=head2 $self->finalize_headers($c)
fc7ec1d9 271
4ab87e27 272Abstract method, allows engines to write headers to response
273
fc7ec1d9 274=cut
275
276sub finalize_headers { }
277
b5ecfcf0 278=head2 $self->finalize_read($c)
fc7ec1d9 279
280=cut
281
878b821c 282sub finalize_read { }
fc7ec1d9 283
b5ecfcf0 284=head2 $self->finalize_uploads($c)
fc7ec1d9 285
4ab87e27 286Clean up after uploads, deleting temp files.
287
fc7ec1d9 288=cut
289
fbcc39ad 290sub finalize_uploads {
291 my ( $self, $c ) = @_;
99fe1710 292
fbcc39ad 293 if ( keys %{ $c->request->uploads } ) {
294 for my $key ( keys %{ $c->request->uploads } ) {
295 my $upload = $c->request->uploads->{$key};
296 unlink map { $_->tempname }
297 grep { -e $_->tempname }
298 ref $upload eq 'ARRAY' ? @{$upload} : ($upload);
c85ff642 299 }
c85ff642 300 }
fc7ec1d9 301}
302
b5ecfcf0 303=head2 $self->prepare_body($c)
fc7ec1d9 304
4ab87e27 305sets up the L<Catalyst::Request> object body using L<HTTP::Body>
306
fc7ec1d9 307=cut
308
fbcc39ad 309sub prepare_body {
310 my ( $self, $c ) = @_;
99fe1710 311
878b821c 312 if ( my $length = $self->read_length ) {
847e3257 313 unless ( $c->request->{_body} ) {
314 my $type = $c->request->header('Content-Type');
315 $c->request->{_body} = HTTP::Body->new( $type, $length );
316 $c->request->{_body}->{tmpdir} = $c->config->{uploadtmp}
317 if exists $c->config->{uploadtmp};
318 }
319
4f5ebacd 320 while ( my $buffer = $self->read($c) ) {
321 $c->prepare_body_chunk($buffer);
fbcc39ad 322 }
fdb3773e 323
324 # paranoia against wrong Content-Length header
847e3257 325 my $remaining = $length - $self->read_position;
34d28dfd 326 if ( $remaining > 0 ) {
fdb3773e 327 $self->finalize_read($c);
34d28dfd 328 Catalyst::Exception->throw(
847e3257 329 "Wrong Content-Length value: $length" );
fdb3773e 330 }
fc7ec1d9 331 }
847e3257 332 else {
333 # Defined but will cause all body code to be skipped
334 $c->request->{_body} = 0;
335 }
fc7ec1d9 336}
337
b5ecfcf0 338=head2 $self->prepare_body_chunk($c)
4bd82c41 339
4ab87e27 340Add a chunk to the request body.
341
4bd82c41 342=cut
343
344sub prepare_body_chunk {
345 my ( $self, $c, $chunk ) = @_;
4f5ebacd 346
347 $c->request->{_body}->add($chunk);
4bd82c41 348}
349
b5ecfcf0 350=head2 $self->prepare_body_parameters($c)
06e1b616 351
4ab87e27 352Sets up parameters from body.
353
06e1b616 354=cut
355
fbcc39ad 356sub prepare_body_parameters {
357 my ( $self, $c ) = @_;
847e3257 358
359 return unless $c->request->{_body};
360
fbcc39ad 361 $c->request->body_parameters( $c->request->{_body}->param );
362}
0556eb49 363
b5ecfcf0 364=head2 $self->prepare_connection($c)
0556eb49 365
4ab87e27 366Abstract method implemented in engines.
367
0556eb49 368=cut
369
370sub prepare_connection { }
371
b5ecfcf0 372=head2 $self->prepare_cookies($c)
fc7ec1d9 373
fa32ac82 374Parse cookies from header. Sets a L<CGI::Simple::Cookie> object.
4ab87e27 375
fc7ec1d9 376=cut
377
6dc87a0f 378sub prepare_cookies {
fbcc39ad 379 my ( $self, $c ) = @_;
6dc87a0f 380
381 if ( my $header = $c->request->header('Cookie') ) {
fa32ac82 382 $c->req->cookies( { CGI::Simple::Cookie->parse($header) } );
6dc87a0f 383 }
384}
fc7ec1d9 385
b5ecfcf0 386=head2 $self->prepare_headers($c)
fc7ec1d9 387
388=cut
389
390sub prepare_headers { }
391
b5ecfcf0 392=head2 $self->prepare_parameters($c)
fc7ec1d9 393
4ab87e27 394sets up parameters from query and post parameters.
395
fc7ec1d9 396=cut
397
fbcc39ad 398sub prepare_parameters {
399 my ( $self, $c ) = @_;
fc7ec1d9 400
fbcc39ad 401 # We copy, no references
c82ed742 402 foreach my $name ( keys %{ $c->request->query_parameters } ) {
403 my $param = $c->request->query_parameters->{$name};
fbcc39ad 404 $param = ref $param eq 'ARRAY' ? [ @{$param} ] : $param;
405 $c->request->parameters->{$name} = $param;
406 }
fc7ec1d9 407
fbcc39ad 408 # Merge query and body parameters
c82ed742 409 foreach my $name ( keys %{ $c->request->body_parameters } ) {
410 my $param = $c->request->body_parameters->{$name};
fbcc39ad 411 $param = ref $param eq 'ARRAY' ? [ @{$param} ] : $param;
412 if ( my $old_param = $c->request->parameters->{$name} ) {
413 if ( ref $old_param eq 'ARRAY' ) {
414 push @{ $c->request->parameters->{$name} },
415 ref $param eq 'ARRAY' ? @$param : $param;
416 }
417 else { $c->request->parameters->{$name} = [ $old_param, $param ] }
418 }
419 else { $c->request->parameters->{$name} = $param }
420 }
421}
422
b5ecfcf0 423=head2 $self->prepare_path($c)
fc7ec1d9 424
4ab87e27 425abstract method, implemented by engines.
426
fc7ec1d9 427=cut
428
429sub prepare_path { }
430
b5ecfcf0 431=head2 $self->prepare_request($c)
fc7ec1d9 432
b5ecfcf0 433=head2 $self->prepare_query_parameters($c)
fc7ec1d9 434
4ab87e27 435process the query string and extract query parameters.
436
fc7ec1d9 437=cut
438
e0616220 439sub prepare_query_parameters {
440 my ( $self, $c, $query_string ) = @_;
933ba403 441
3b4d1251 442 # Check for keywords (no = signs)
443 # (yes, index() is faster than a regex :))
933ba403 444 if ( index( $query_string, '=' ) < 0 ) {
3b4d1251 445 $c->request->query_keywords( $self->unescape_uri($query_string) );
933ba403 446 return;
447 }
448
449 my %query;
e0616220 450
451 # replace semi-colons
452 $query_string =~ s/;/&/g;
933ba403 453
454 my @params = split /&/, $query_string;
e0616220 455
933ba403 456 for my $item ( @params ) {
457
458 my ($param, $value)
459 = map { $self->unescape_uri($_) }
460 split( /=/, $item );
461
462 $param = $self->unescape_uri($item) unless defined $param;
463
464 if ( exists $query{$param} ) {
465 if ( ref $query{$param} ) {
466 push @{ $query{$param} }, $value;
467 }
468 else {
469 $query{$param} = [ $query{$param}, $value ];
470 }
471 }
472 else {
473 $query{$param} = $value;
474 }
e0616220 475 }
933ba403 476
477 $c->request->query_parameters( \%query );
e0616220 478}
fbcc39ad 479
b5ecfcf0 480=head2 $self->prepare_read($c)
fbcc39ad 481
4ab87e27 482prepare to read from the engine.
483
fbcc39ad 484=cut
fc7ec1d9 485
fbcc39ad 486sub prepare_read {
487 my ( $self, $c ) = @_;
4f5ebacd 488
878b821c 489 # Initialize the read position
4f5ebacd 490 $self->read_position(0);
878b821c 491
492 # Initialize the amount of data we think we need to read
493 $self->read_length( $c->request->header('Content-Length') || 0 );
fbcc39ad 494}
fc7ec1d9 495
b5ecfcf0 496=head2 $self->prepare_request(@arguments)
fc7ec1d9 497
4ab87e27 498Populate the context object from the request object.
499
fc7ec1d9 500=cut
501
fbcc39ad 502sub prepare_request { }
fc7ec1d9 503
b5ecfcf0 504=head2 $self->prepare_uploads($c)
c9afa5fc 505
fbcc39ad 506=cut
507
508sub prepare_uploads {
509 my ( $self, $c ) = @_;
847e3257 510
511 return unless $c->request->{_body};
512
fbcc39ad 513 my $uploads = $c->request->{_body}->upload;
514 for my $name ( keys %$uploads ) {
515 my $files = $uploads->{$name};
516 $files = ref $files eq 'ARRAY' ? $files : [$files];
517 my @uploads;
518 for my $upload (@$files) {
519 my $u = Catalyst::Request::Upload->new;
520 $u->headers( HTTP::Headers->new( %{ $upload->{headers} } ) );
521 $u->type( $u->headers->content_type );
522 $u->tempname( $upload->{tempname} );
523 $u->size( $upload->{size} );
524 $u->filename( $upload->{filename} );
525 push @uploads, $u;
526 }
527 $c->request->uploads->{$name} = @uploads > 1 ? \@uploads : $uploads[0];
f4a57de4 528
c4bed79a 529 # support access to the filename as a normal param
530 my @filenames = map { $_->{filename} } @uploads;
a7e05d9d 531 # append, if there's already params with this name
532 if (exists $c->request->parameters->{$name}) {
533 if (ref $c->request->parameters->{$name} eq 'ARRAY') {
534 push @{ $c->request->parameters->{$name} }, @filenames;
535 }
536 else {
537 $c->request->parameters->{$name} =
538 [ $c->request->parameters->{$name}, @filenames ];
539 }
540 }
541 else {
542 $c->request->parameters->{$name} =
543 @filenames > 1 ? \@filenames : $filenames[0];
544 }
fbcc39ad 545 }
546}
547
b5ecfcf0 548=head2 $self->prepare_write($c)
c9afa5fc 549
4ab87e27 550Abstract method. Implemented by the engines.
551
c9afa5fc 552=cut
553
fbcc39ad 554sub prepare_write { }
555
b5ecfcf0 556=head2 $self->read($c, [$maxlength])
fbcc39ad 557
558=cut
559
560sub read {
561 my ( $self, $c, $maxlength ) = @_;
4f5ebacd 562
fbcc39ad 563 my $remaining = $self->read_length - $self->read_position;
4bd82c41 564 $maxlength ||= $CHUNKSIZE;
4f5ebacd 565
fbcc39ad 566 # Are we done reading?
567 if ( $remaining <= 0 ) {
4f5ebacd 568 $self->finalize_read($c);
fbcc39ad 569 return;
570 }
c9afa5fc 571
fbcc39ad 572 my $readlen = ( $remaining > $maxlength ) ? $maxlength : $remaining;
573 my $rc = $self->read_chunk( $c, my $buffer, $readlen );
574 if ( defined $rc ) {
575 $self->read_position( $self->read_position + $rc );
576 return $buffer;
577 }
578 else {
4f5ebacd 579 Catalyst::Exception->throw(
580 message => "Unknown error reading input: $!" );
fbcc39ad 581 }
582}
fc7ec1d9 583
b5ecfcf0 584=head2 $self->read_chunk($c, $buffer, $length)
23f9d934 585
fbcc39ad 586Each engine inplements read_chunk as its preferred way of reading a chunk
587of data.
fc7ec1d9 588
fbcc39ad 589=cut
61b1e958 590
fbcc39ad 591sub read_chunk { }
61b1e958 592
b5ecfcf0 593=head2 $self->read_length
ca39d576 594
fbcc39ad 595The length of input data to be read. This is obtained from the Content-Length
596header.
fc7ec1d9 597
b5ecfcf0 598=head2 $self->read_position
fc7ec1d9 599
fbcc39ad 600The amount of input data that has already been read.
63b763c5 601
b5ecfcf0 602=head2 $self->run($c)
63b763c5 603
4ab87e27 604Start the engine. Implemented by the various engine classes.
605
fbcc39ad 606=cut
fc7ec1d9 607
fbcc39ad 608sub run { }
fc7ec1d9 609
b5ecfcf0 610=head2 $self->write($c, $buffer)
fc7ec1d9 611
4ab87e27 612Writes the buffer to the client. Can only be called once for a request.
613
fc7ec1d9 614=cut
615
fbcc39ad 616sub write {
617 my ( $self, $c, $buffer ) = @_;
4f5ebacd 618
fbcc39ad 619 unless ( $self->{_prepared_write} ) {
4f5ebacd 620 $self->prepare_write($c);
fbcc39ad 621 $self->{_prepared_write} = 1;
fc7ec1d9 622 }
4f5ebacd 623
624 print STDOUT $buffer;
fc7ec1d9 625}
626
933ba403 627=head2 $self->unescape_uri($uri)
628
6a44fe01 629Unescapes a given URI using the most efficient method available. Engines such
630as Apache may implement this using Apache's C-based modules, for example.
933ba403 631
632=cut
633
634sub unescape_uri {
8c7d83e1 635 my ( $self, $str ) = @_;
933ba403 636
8c7d83e1 637 $str =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
638 $str =~ s/\+/ /g;
933ba403 639
8c7d83e1 640 return $str;
933ba403 641}
34d28dfd 642
4ab87e27 643=head2 $self->finalize_output
644
645<obsolete>, see finalize_body
646
fbcc39ad 647=head1 AUTHORS
648
649Sebastian Riedel, <sri@cpan.org>
fc7ec1d9 650
fbcc39ad 651Andy Grundman, <andy@hybridized.org>
fc7ec1d9 652
653=head1 COPYRIGHT
654
655This program is free software, you can redistribute it and/or modify it under
656the same terms as Perl itself.
657
658=cut
659
6601;