Fix a few tests to work properly on remote servers
[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
fbcc39ad 282sub finalize_read {
283 my ( $self, $c ) = @_;
4f5ebacd 284
fbcc39ad 285 undef $self->{_prepared_read};
fc7ec1d9 286}
287
b5ecfcf0 288=head2 $self->finalize_uploads($c)
fc7ec1d9 289
4ab87e27 290Clean up after uploads, deleting temp files.
291
fc7ec1d9 292=cut
293
fbcc39ad 294sub finalize_uploads {
295 my ( $self, $c ) = @_;
99fe1710 296
fbcc39ad 297 if ( keys %{ $c->request->uploads } ) {
298 for my $key ( keys %{ $c->request->uploads } ) {
299 my $upload = $c->request->uploads->{$key};
300 unlink map { $_->tempname }
301 grep { -e $_->tempname }
302 ref $upload eq 'ARRAY' ? @{$upload} : ($upload);
c85ff642 303 }
c85ff642 304 }
fc7ec1d9 305}
306
b5ecfcf0 307=head2 $self->prepare_body($c)
fc7ec1d9 308
4ab87e27 309sets up the L<Catalyst::Request> object body using L<HTTP::Body>
310
fc7ec1d9 311=cut
312
fbcc39ad 313sub prepare_body {
314 my ( $self, $c ) = @_;
847e3257 315
316 my $length = $c->request->header('Content-Length') || 0;
99fe1710 317
847e3257 318 $self->read_length( $length );
99fe1710 319
847e3257 320 if ( $length > 0 ) {
321 unless ( $c->request->{_body} ) {
322 my $type = $c->request->header('Content-Type');
323 $c->request->{_body} = HTTP::Body->new( $type, $length );
324 $c->request->{_body}->{tmpdir} = $c->config->{uploadtmp}
325 if exists $c->config->{uploadtmp};
326 }
327
4f5ebacd 328 while ( my $buffer = $self->read($c) ) {
329 $c->prepare_body_chunk($buffer);
fbcc39ad 330 }
fdb3773e 331
332 # paranoia against wrong Content-Length header
847e3257 333 my $remaining = $length - $self->read_position;
34d28dfd 334 if ( $remaining > 0 ) {
fdb3773e 335 $self->finalize_read($c);
34d28dfd 336 Catalyst::Exception->throw(
847e3257 337 "Wrong Content-Length value: $length" );
fdb3773e 338 }
fc7ec1d9 339 }
847e3257 340 else {
341 # Defined but will cause all body code to be skipped
342 $c->request->{_body} = 0;
343 }
fc7ec1d9 344}
345
b5ecfcf0 346=head2 $self->prepare_body_chunk($c)
4bd82c41 347
4ab87e27 348Add a chunk to the request body.
349
4bd82c41 350=cut
351
352sub prepare_body_chunk {
353 my ( $self, $c, $chunk ) = @_;
4f5ebacd 354
355 $c->request->{_body}->add($chunk);
4bd82c41 356}
357
b5ecfcf0 358=head2 $self->prepare_body_parameters($c)
06e1b616 359
4ab87e27 360Sets up parameters from body.
361
06e1b616 362=cut
363
fbcc39ad 364sub prepare_body_parameters {
365 my ( $self, $c ) = @_;
847e3257 366
367 return unless $c->request->{_body};
368
fbcc39ad 369 $c->request->body_parameters( $c->request->{_body}->param );
370}
0556eb49 371
b5ecfcf0 372=head2 $self->prepare_connection($c)
0556eb49 373
4ab87e27 374Abstract method implemented in engines.
375
0556eb49 376=cut
377
378sub prepare_connection { }
379
b5ecfcf0 380=head2 $self->prepare_cookies($c)
fc7ec1d9 381
fa32ac82 382Parse cookies from header. Sets a L<CGI::Simple::Cookie> object.
4ab87e27 383
fc7ec1d9 384=cut
385
6dc87a0f 386sub prepare_cookies {
fbcc39ad 387 my ( $self, $c ) = @_;
6dc87a0f 388
389 if ( my $header = $c->request->header('Cookie') ) {
fa32ac82 390 $c->req->cookies( { CGI::Simple::Cookie->parse($header) } );
6dc87a0f 391 }
392}
fc7ec1d9 393
b5ecfcf0 394=head2 $self->prepare_headers($c)
fc7ec1d9 395
396=cut
397
398sub prepare_headers { }
399
b5ecfcf0 400=head2 $self->prepare_parameters($c)
fc7ec1d9 401
4ab87e27 402sets up parameters from query and post parameters.
403
fc7ec1d9 404=cut
405
fbcc39ad 406sub prepare_parameters {
407 my ( $self, $c ) = @_;
fc7ec1d9 408
fbcc39ad 409 # We copy, no references
c82ed742 410 foreach my $name ( keys %{ $c->request->query_parameters } ) {
411 my $param = $c->request->query_parameters->{$name};
fbcc39ad 412 $param = ref $param eq 'ARRAY' ? [ @{$param} ] : $param;
413 $c->request->parameters->{$name} = $param;
414 }
fc7ec1d9 415
fbcc39ad 416 # Merge query and body parameters
c82ed742 417 foreach my $name ( keys %{ $c->request->body_parameters } ) {
418 my $param = $c->request->body_parameters->{$name};
fbcc39ad 419 $param = ref $param eq 'ARRAY' ? [ @{$param} ] : $param;
420 if ( my $old_param = $c->request->parameters->{$name} ) {
421 if ( ref $old_param eq 'ARRAY' ) {
422 push @{ $c->request->parameters->{$name} },
423 ref $param eq 'ARRAY' ? @$param : $param;
424 }
425 else { $c->request->parameters->{$name} = [ $old_param, $param ] }
426 }
427 else { $c->request->parameters->{$name} = $param }
428 }
429}
430
b5ecfcf0 431=head2 $self->prepare_path($c)
fc7ec1d9 432
4ab87e27 433abstract method, implemented by engines.
434
fc7ec1d9 435=cut
436
437sub prepare_path { }
438
b5ecfcf0 439=head2 $self->prepare_request($c)
fc7ec1d9 440
b5ecfcf0 441=head2 $self->prepare_query_parameters($c)
fc7ec1d9 442
4ab87e27 443process the query string and extract query parameters.
444
fc7ec1d9 445=cut
446
e0616220 447sub prepare_query_parameters {
448 my ( $self, $c, $query_string ) = @_;
933ba403 449
3b4d1251 450 # Check for keywords (no = signs)
451 # (yes, index() is faster than a regex :))
933ba403 452 if ( index( $query_string, '=' ) < 0 ) {
3b4d1251 453 $c->request->query_keywords( $self->unescape_uri($query_string) );
933ba403 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 {
8c7d83e1 645 my ( $self, $str ) = @_;
933ba403 646
8c7d83e1 647 $str =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
648 $str =~ s/\+/ /g;
933ba403 649
8c7d83e1 650 return $str;
933ba403 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;