Fix locating t/lib
[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/;
d04b2ffd 7use Errno 'EWOULDBLOCK';
fc7ec1d9 8use HTML::Entities;
fbcc39ad 9use HTTP::Body;
fc7ec1d9 10use HTTP::Headers;
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
af29be6e 160(pt) Por favor volte mais tarde
969647fd 161</pre>
162
163 $name = '';
164 }
e060fe05 165 $c->res->body( <<"" );
7299a7b4 166<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
167 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
168<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
969647fd 169<head>
7299a7b4 170 <meta http-equiv="Content-Language" content="en" />
171 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
969647fd 172 <title>$title</title>
7299a7b4 173 <script type="text/javascript">
c6ef5e69 174 <!--
175 function toggleDump (dumpElement) {
7299a7b4 176 var e = document.getElementById( dumpElement );
177 if (e.style.display == "none") {
178 e.style.display = "";
c6ef5e69 179 }
180 else {
7299a7b4 181 e.style.display = "none";
c6ef5e69 182 }
183 }
184 -->
185 </script>
969647fd 186 <style type="text/css">
187 body {
188 font-family: "Bitstream Vera Sans", "Trebuchet MS", Verdana,
189 Tahoma, Arial, helvetica, sans-serif;
34d28dfd 190 color: #333;
969647fd 191 background-color: #eee;
192 margin: 0px;
193 padding: 0px;
194 }
c6ef5e69 195 :link, :link:hover, :visited, :visited:hover {
34d28dfd 196 color: #000;
c6ef5e69 197 }
969647fd 198 div.box {
9619f23c 199 position: relative;
969647fd 200 background-color: #ccc;
201 border: 1px solid #aaa;
202 padding: 4px;
203 margin: 10px;
969647fd 204 }
205 div.error {
34d28dfd 206 background-color: #cce;
969647fd 207 border: 1px solid #755;
208 padding: 8px;
209 margin: 4px;
210 margin-bottom: 10px;
969647fd 211 }
212 div.infos {
34d28dfd 213 background-color: #eee;
969647fd 214 border: 1px solid #575;
215 padding: 8px;
216 margin: 4px;
217 margin-bottom: 10px;
969647fd 218 }
219 div.name {
34d28dfd 220 background-color: #cce;
969647fd 221 border: 1px solid #557;
222 padding: 8px;
223 margin: 4px;
969647fd 224 }
7f8e0078 225 code.error {
226 display: block;
227 margin: 1em 0;
228 overflow: auto;
7f8e0078 229 }
9619f23c 230 div.name h1, div.error p {
231 margin: 0;
232 }
233 h2 {
234 margin-top: 0;
235 margin-bottom: 10px;
236 font-size: medium;
237 font-weight: bold;
238 text-decoration: underline;
239 }
240 h1 {
241 font-size: medium;
242 font-weight: normal;
243 }
2666dd3b 244 /* from http://users.tkk.fi/~tkarvine/linux/doc/pre-wrap/pre-wrap-css3-mozilla-opera-ie.html */
245 /* Browser specific (not valid) styles to make preformatted text wrap */
246 pre {
247 white-space: pre-wrap; /* css-3 */
248 white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
249 white-space: -pre-wrap; /* Opera 4-6 */
250 white-space: -o-pre-wrap; /* Opera 7 */
251 word-wrap: break-word; /* Internet Explorer 5.5+ */
252 }
969647fd 253 </style>
254</head>
255<body>
256 <div class="box">
257 <div class="error">$error</div>
258 <div class="infos">$infos</div>
259 <div class="name">$name</div>
260 </div>
261</body>
262</html>
263
d82cc9ae 264
265 # Trick IE
266 $c->res->{body} .= ( ' ' x 512 );
267
268 # Return 500
33117422 269 $c->res->status(500);
969647fd 270}
271
b5ecfcf0 272=head2 $self->finalize_headers($c)
fc7ec1d9 273
4ab87e27 274Abstract method, allows engines to write headers to response
275
fc7ec1d9 276=cut
277
278sub finalize_headers { }
279
b5ecfcf0 280=head2 $self->finalize_read($c)
fc7ec1d9 281
282=cut
283
878b821c 284sub finalize_read { }
fc7ec1d9 285
b5ecfcf0 286=head2 $self->finalize_uploads($c)
fc7ec1d9 287
4ab87e27 288Clean up after uploads, deleting temp files.
289
fc7ec1d9 290=cut
291
fbcc39ad 292sub finalize_uploads {
293 my ( $self, $c ) = @_;
99fe1710 294
fbcc39ad 295 if ( keys %{ $c->request->uploads } ) {
296 for my $key ( keys %{ $c->request->uploads } ) {
297 my $upload = $c->request->uploads->{$key};
298 unlink map { $_->tempname }
299 grep { -e $_->tempname }
300 ref $upload eq 'ARRAY' ? @{$upload} : ($upload);
c85ff642 301 }
c85ff642 302 }
fc7ec1d9 303}
304
b5ecfcf0 305=head2 $self->prepare_body($c)
fc7ec1d9 306
4ab87e27 307sets up the L<Catalyst::Request> object body using L<HTTP::Body>
308
fc7ec1d9 309=cut
310
fbcc39ad 311sub prepare_body {
312 my ( $self, $c ) = @_;
99fe1710 313
878b821c 314 if ( my $length = $self->read_length ) {
847e3257 315 unless ( $c->request->{_body} ) {
316 my $type = $c->request->header('Content-Type');
317 $c->request->{_body} = HTTP::Body->new( $type, $length );
3c402610 318 $c->request->{_body}->tmpdir( $c->config->{uploadtmp} )
847e3257 319 if exists $c->config->{uploadtmp};
320 }
321
4f5ebacd 322 while ( my $buffer = $self->read($c) ) {
323 $c->prepare_body_chunk($buffer);
fbcc39ad 324 }
fdb3773e 325
326 # paranoia against wrong Content-Length header
847e3257 327 my $remaining = $length - $self->read_position;
34d28dfd 328 if ( $remaining > 0 ) {
fdb3773e 329 $self->finalize_read($c);
34d28dfd 330 Catalyst::Exception->throw(
847e3257 331 "Wrong Content-Length value: $length" );
fdb3773e 332 }
fc7ec1d9 333 }
847e3257 334 else {
335 # Defined but will cause all body code to be skipped
336 $c->request->{_body} = 0;
337 }
fc7ec1d9 338}
339
b5ecfcf0 340=head2 $self->prepare_body_chunk($c)
4bd82c41 341
4ab87e27 342Add a chunk to the request body.
343
4bd82c41 344=cut
345
346sub prepare_body_chunk {
347 my ( $self, $c, $chunk ) = @_;
4f5ebacd 348
349 $c->request->{_body}->add($chunk);
4bd82c41 350}
351
b5ecfcf0 352=head2 $self->prepare_body_parameters($c)
06e1b616 353
4ab87e27 354Sets up parameters from body.
355
06e1b616 356=cut
357
fbcc39ad 358sub prepare_body_parameters {
359 my ( $self, $c ) = @_;
847e3257 360
361 return unless $c->request->{_body};
362
fbcc39ad 363 $c->request->body_parameters( $c->request->{_body}->param );
364}
0556eb49 365
b5ecfcf0 366=head2 $self->prepare_connection($c)
0556eb49 367
4ab87e27 368Abstract method implemented in engines.
369
0556eb49 370=cut
371
372sub prepare_connection { }
373
b5ecfcf0 374=head2 $self->prepare_cookies($c)
fc7ec1d9 375
fa32ac82 376Parse cookies from header. Sets a L<CGI::Simple::Cookie> object.
4ab87e27 377
fc7ec1d9 378=cut
379
6dc87a0f 380sub prepare_cookies {
fbcc39ad 381 my ( $self, $c ) = @_;
6dc87a0f 382
383 if ( my $header = $c->request->header('Cookie') ) {
fa32ac82 384 $c->req->cookies( { CGI::Simple::Cookie->parse($header) } );
6dc87a0f 385 }
386}
fc7ec1d9 387
b5ecfcf0 388=head2 $self->prepare_headers($c)
fc7ec1d9 389
390=cut
391
392sub prepare_headers { }
393
b5ecfcf0 394=head2 $self->prepare_parameters($c)
fc7ec1d9 395
4ab87e27 396sets up parameters from query and post parameters.
397
fc7ec1d9 398=cut
399
fbcc39ad 400sub prepare_parameters {
401 my ( $self, $c ) = @_;
fc7ec1d9 402
fbcc39ad 403 # We copy, no references
c82ed742 404 foreach my $name ( keys %{ $c->request->query_parameters } ) {
405 my $param = $c->request->query_parameters->{$name};
fbcc39ad 406 $param = ref $param eq 'ARRAY' ? [ @{$param} ] : $param;
407 $c->request->parameters->{$name} = $param;
408 }
fc7ec1d9 409
fbcc39ad 410 # Merge query and body parameters
c82ed742 411 foreach my $name ( keys %{ $c->request->body_parameters } ) {
412 my $param = $c->request->body_parameters->{$name};
fbcc39ad 413 $param = ref $param eq 'ARRAY' ? [ @{$param} ] : $param;
414 if ( my $old_param = $c->request->parameters->{$name} ) {
415 if ( ref $old_param eq 'ARRAY' ) {
416 push @{ $c->request->parameters->{$name} },
417 ref $param eq 'ARRAY' ? @$param : $param;
418 }
419 else { $c->request->parameters->{$name} = [ $old_param, $param ] }
420 }
421 else { $c->request->parameters->{$name} = $param }
422 }
423}
424
b5ecfcf0 425=head2 $self->prepare_path($c)
fc7ec1d9 426
4ab87e27 427abstract method, implemented by engines.
428
fc7ec1d9 429=cut
430
431sub prepare_path { }
432
b5ecfcf0 433=head2 $self->prepare_request($c)
fc7ec1d9 434
b5ecfcf0 435=head2 $self->prepare_query_parameters($c)
fc7ec1d9 436
4ab87e27 437process the query string and extract query parameters.
438
fc7ec1d9 439=cut
440
e0616220 441sub prepare_query_parameters {
442 my ( $self, $c, $query_string ) = @_;
933ba403 443
3b4d1251 444 # Check for keywords (no = signs)
445 # (yes, index() is faster than a regex :))
933ba403 446 if ( index( $query_string, '=' ) < 0 ) {
3b4d1251 447 $c->request->query_keywords( $self->unescape_uri($query_string) );
933ba403 448 return;
449 }
450
451 my %query;
e0616220 452
453 # replace semi-colons
454 $query_string =~ s/;/&/g;
933ba403 455
8e9cc26f 456 my @params = grep { length $_ } split /&/, $query_string;
e0616220 457
933ba403 458 for my $item ( @params ) {
459
460 my ($param, $value)
461 = map { $self->unescape_uri($_) }
e5542b70 462 split( /=/, $item, 2 );
933ba403 463
464 $param = $self->unescape_uri($item) unless defined $param;
465
466 if ( exists $query{$param} ) {
467 if ( ref $query{$param} ) {
468 push @{ $query{$param} }, $value;
469 }
470 else {
471 $query{$param} = [ $query{$param}, $value ];
472 }
473 }
474 else {
475 $query{$param} = $value;
476 }
e0616220 477 }
933ba403 478
479 $c->request->query_parameters( \%query );
e0616220 480}
fbcc39ad 481
b5ecfcf0 482=head2 $self->prepare_read($c)
fbcc39ad 483
4ab87e27 484prepare to read from the engine.
485
fbcc39ad 486=cut
fc7ec1d9 487
fbcc39ad 488sub prepare_read {
489 my ( $self, $c ) = @_;
4f5ebacd 490
878b821c 491 # Initialize the read position
4f5ebacd 492 $self->read_position(0);
878b821c 493
494 # Initialize the amount of data we think we need to read
495 $self->read_length( $c->request->header('Content-Length') || 0 );
fbcc39ad 496}
fc7ec1d9 497
b5ecfcf0 498=head2 $self->prepare_request(@arguments)
fc7ec1d9 499
4ab87e27 500Populate the context object from the request object.
501
fc7ec1d9 502=cut
503
fbcc39ad 504sub prepare_request { }
fc7ec1d9 505
b5ecfcf0 506=head2 $self->prepare_uploads($c)
c9afa5fc 507
fbcc39ad 508=cut
509
510sub prepare_uploads {
511 my ( $self, $c ) = @_;
847e3257 512
513 return unless $c->request->{_body};
514
fbcc39ad 515 my $uploads = $c->request->{_body}->upload;
516 for my $name ( keys %$uploads ) {
517 my $files = $uploads->{$name};
518 $files = ref $files eq 'ARRAY' ? $files : [$files];
519 my @uploads;
520 for my $upload (@$files) {
521 my $u = Catalyst::Request::Upload->new;
522 $u->headers( HTTP::Headers->new( %{ $upload->{headers} } ) );
523 $u->type( $u->headers->content_type );
524 $u->tempname( $upload->{tempname} );
525 $u->size( $upload->{size} );
526 $u->filename( $upload->{filename} );
527 push @uploads, $u;
528 }
529 $c->request->uploads->{$name} = @uploads > 1 ? \@uploads : $uploads[0];
f4a57de4 530
c4bed79a 531 # support access to the filename as a normal param
532 my @filenames = map { $_->{filename} } @uploads;
a7e05d9d 533 # append, if there's already params with this name
534 if (exists $c->request->parameters->{$name}) {
535 if (ref $c->request->parameters->{$name} eq 'ARRAY') {
536 push @{ $c->request->parameters->{$name} }, @filenames;
537 }
538 else {
539 $c->request->parameters->{$name} =
540 [ $c->request->parameters->{$name}, @filenames ];
541 }
542 }
543 else {
544 $c->request->parameters->{$name} =
545 @filenames > 1 ? \@filenames : $filenames[0];
546 }
fbcc39ad 547 }
548}
549
b5ecfcf0 550=head2 $self->prepare_write($c)
c9afa5fc 551
4ab87e27 552Abstract method. Implemented by the engines.
553
c9afa5fc 554=cut
555
fbcc39ad 556sub prepare_write { }
557
b5ecfcf0 558=head2 $self->read($c, [$maxlength])
fbcc39ad 559
560=cut
561
562sub read {
563 my ( $self, $c, $maxlength ) = @_;
4f5ebacd 564
fbcc39ad 565 my $remaining = $self->read_length - $self->read_position;
4bd82c41 566 $maxlength ||= $CHUNKSIZE;
4f5ebacd 567
fbcc39ad 568 # Are we done reading?
569 if ( $remaining <= 0 ) {
4f5ebacd 570 $self->finalize_read($c);
fbcc39ad 571 return;
572 }
c9afa5fc 573
fbcc39ad 574 my $readlen = ( $remaining > $maxlength ) ? $maxlength : $remaining;
575 my $rc = $self->read_chunk( $c, my $buffer, $readlen );
576 if ( defined $rc ) {
577 $self->read_position( $self->read_position + $rc );
578 return $buffer;
579 }
580 else {
4f5ebacd 581 Catalyst::Exception->throw(
582 message => "Unknown error reading input: $!" );
fbcc39ad 583 }
584}
fc7ec1d9 585
b5ecfcf0 586=head2 $self->read_chunk($c, $buffer, $length)
23f9d934 587
fbcc39ad 588Each engine inplements read_chunk as its preferred way of reading a chunk
589of data.
fc7ec1d9 590
fbcc39ad 591=cut
61b1e958 592
fbcc39ad 593sub read_chunk { }
61b1e958 594
b5ecfcf0 595=head2 $self->read_length
ca39d576 596
fbcc39ad 597The length of input data to be read. This is obtained from the Content-Length
598header.
fc7ec1d9 599
b5ecfcf0 600=head2 $self->read_position
fc7ec1d9 601
fbcc39ad 602The amount of input data that has already been read.
63b763c5 603
b5ecfcf0 604=head2 $self->run($c)
63b763c5 605
4ab87e27 606Start the engine. Implemented by the various engine classes.
607
fbcc39ad 608=cut
fc7ec1d9 609
fbcc39ad 610sub run { }
fc7ec1d9 611
b5ecfcf0 612=head2 $self->write($c, $buffer)
fc7ec1d9 613
e512dd24 614Writes the buffer to the client.
4ab87e27 615
fc7ec1d9 616=cut
617
fbcc39ad 618sub write {
619 my ( $self, $c, $buffer ) = @_;
4f5ebacd 620
fbcc39ad 621 unless ( $self->{_prepared_write} ) {
4f5ebacd 622 $self->prepare_write($c);
fbcc39ad 623 $self->{_prepared_write} = 1;
fc7ec1d9 624 }
e512dd24 625
d04b2ffd 626 my $len = length($buffer);
627 my $wrote = syswrite STDOUT, $buffer;
e512dd24 628
d04b2ffd 629 if ( !defined $wrote && $! == EWOULDBLOCK ) {
630 # Unable to write on the first try, will retry in the loop below
631 $wrote = 0;
632 }
4d4d6635 633
d04b2ffd 634 if ( defined $wrote && $wrote < $len ) {
635 # We didn't write the whole buffer
636 while (1) {
637 my $ret = syswrite STDOUT, $buffer, $CHUNKSIZE, $wrote;
638 if ( defined $ret ) {
639 $wrote += $ret;
640 }
641 else {
642 next if $! == EWOULDBLOCK;
643 return;
644 }
645
646 last if $wrote >= $len;
e2b0ddd3 647 }
e512dd24 648 }
649
650 return $wrote;
fc7ec1d9 651}
652
933ba403 653=head2 $self->unescape_uri($uri)
654
6a44fe01 655Unescapes a given URI using the most efficient method available. Engines such
656as Apache may implement this using Apache's C-based modules, for example.
933ba403 657
658=cut
659
660sub unescape_uri {
8c7d83e1 661 my ( $self, $str ) = @_;
7d22a537 662
663 $str =~ s/(?:%([0-9A-Fa-f]{2})|\+)/defined $1 ? chr(hex($1)) : ' '/eg;
664
8c7d83e1 665 return $str;
933ba403 666}
34d28dfd 667
4ab87e27 668=head2 $self->finalize_output
669
670<obsolete>, see finalize_body
671
fbcc39ad 672=head1 AUTHORS
673
0bf7ab71 674Catalyst Contributors, see Catalyst.pm
fc7ec1d9 675
676=head1 COPYRIGHT
677
678This program is free software, you can redistribute it and/or modify it under
679the same terms as Perl itself.
680
681=cut
682
6831;