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