Revert the c->req->keywords change, this is a feature and should wait until 5.8
[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
94b8f5de 451 # Make sure query has params
933ba403 452 if ( index( $query_string, '=' ) < 0 ) {
933ba403 453 return;
454 }
455
456 my %query;
e0616220 457
458 # replace semi-colons
459 $query_string =~ s/;/&/g;
933ba403 460
461 my @params = split /&/, $query_string;
e0616220 462
933ba403 463 for my $item ( @params ) {
464
465 my ($param, $value)
466 = map { $self->unescape_uri($_) }
467 split( /=/, $item );
468
469 $param = $self->unescape_uri($item) unless defined $param;
470
471 if ( exists $query{$param} ) {
472 if ( ref $query{$param} ) {
473 push @{ $query{$param} }, $value;
474 }
475 else {
476 $query{$param} = [ $query{$param}, $value ];
477 }
478 }
479 else {
480 $query{$param} = $value;
481 }
e0616220 482 }
933ba403 483
484 $c->request->query_parameters( \%query );
e0616220 485}
fbcc39ad 486
b5ecfcf0 487=head2 $self->prepare_read($c)
fbcc39ad 488
4ab87e27 489prepare to read from the engine.
490
fbcc39ad 491=cut
fc7ec1d9 492
fbcc39ad 493sub prepare_read {
494 my ( $self, $c ) = @_;
4f5ebacd 495
fbcc39ad 496 # Reset the read position
4f5ebacd 497 $self->read_position(0);
fbcc39ad 498}
fc7ec1d9 499
b5ecfcf0 500=head2 $self->prepare_request(@arguments)
fc7ec1d9 501
4ab87e27 502Populate the context object from the request object.
503
fc7ec1d9 504=cut
505
fbcc39ad 506sub prepare_request { }
fc7ec1d9 507
b5ecfcf0 508=head2 $self->prepare_uploads($c)
c9afa5fc 509
fbcc39ad 510=cut
511
512sub prepare_uploads {
513 my ( $self, $c ) = @_;
847e3257 514
515 return unless $c->request->{_body};
516
fbcc39ad 517 my $uploads = $c->request->{_body}->upload;
518 for my $name ( keys %$uploads ) {
519 my $files = $uploads->{$name};
520 $files = ref $files eq 'ARRAY' ? $files : [$files];
521 my @uploads;
522 for my $upload (@$files) {
523 my $u = Catalyst::Request::Upload->new;
524 $u->headers( HTTP::Headers->new( %{ $upload->{headers} } ) );
525 $u->type( $u->headers->content_type );
526 $u->tempname( $upload->{tempname} );
527 $u->size( $upload->{size} );
528 $u->filename( $upload->{filename} );
529 push @uploads, $u;
530 }
531 $c->request->uploads->{$name} = @uploads > 1 ? \@uploads : $uploads[0];
f4a57de4 532
c4bed79a 533 # support access to the filename as a normal param
534 my @filenames = map { $_->{filename} } @uploads;
a7e05d9d 535 # append, if there's already params with this name
536 if (exists $c->request->parameters->{$name}) {
537 if (ref $c->request->parameters->{$name} eq 'ARRAY') {
538 push @{ $c->request->parameters->{$name} }, @filenames;
539 }
540 else {
541 $c->request->parameters->{$name} =
542 [ $c->request->parameters->{$name}, @filenames ];
543 }
544 }
545 else {
546 $c->request->parameters->{$name} =
547 @filenames > 1 ? \@filenames : $filenames[0];
548 }
fbcc39ad 549 }
550}
551
b5ecfcf0 552=head2 $self->prepare_write($c)
c9afa5fc 553
4ab87e27 554Abstract method. Implemented by the engines.
555
c9afa5fc 556=cut
557
fbcc39ad 558sub prepare_write { }
559
b5ecfcf0 560=head2 $self->read($c, [$maxlength])
fbcc39ad 561
562=cut
563
564sub read {
565 my ( $self, $c, $maxlength ) = @_;
4f5ebacd 566
fbcc39ad 567 unless ( $self->{_prepared_read} ) {
4f5ebacd 568 $self->prepare_read($c);
fbcc39ad 569 $self->{_prepared_read} = 1;
570 }
4f5ebacd 571
fbcc39ad 572 my $remaining = $self->read_length - $self->read_position;
4bd82c41 573 $maxlength ||= $CHUNKSIZE;
4f5ebacd 574
fbcc39ad 575 # Are we done reading?
576 if ( $remaining <= 0 ) {
4f5ebacd 577 $self->finalize_read($c);
fbcc39ad 578 return;
579 }
c9afa5fc 580
fbcc39ad 581 my $readlen = ( $remaining > $maxlength ) ? $maxlength : $remaining;
582 my $rc = $self->read_chunk( $c, my $buffer, $readlen );
583 if ( defined $rc ) {
584 $self->read_position( $self->read_position + $rc );
585 return $buffer;
586 }
587 else {
4f5ebacd 588 Catalyst::Exception->throw(
589 message => "Unknown error reading input: $!" );
fbcc39ad 590 }
591}
fc7ec1d9 592
b5ecfcf0 593=head2 $self->read_chunk($c, $buffer, $length)
23f9d934 594
fbcc39ad 595Each engine inplements read_chunk as its preferred way of reading a chunk
596of data.
fc7ec1d9 597
fbcc39ad 598=cut
61b1e958 599
fbcc39ad 600sub read_chunk { }
61b1e958 601
b5ecfcf0 602=head2 $self->read_length
ca39d576 603
fbcc39ad 604The length of input data to be read. This is obtained from the Content-Length
605header.
fc7ec1d9 606
b5ecfcf0 607=head2 $self->read_position
fc7ec1d9 608
fbcc39ad 609The amount of input data that has already been read.
63b763c5 610
b5ecfcf0 611=head2 $self->run($c)
63b763c5 612
4ab87e27 613Start the engine. Implemented by the various engine classes.
614
fbcc39ad 615=cut
fc7ec1d9 616
fbcc39ad 617sub run { }
fc7ec1d9 618
b5ecfcf0 619=head2 $self->write($c, $buffer)
fc7ec1d9 620
4ab87e27 621Writes the buffer to the client. Can only be called once for a request.
622
fc7ec1d9 623=cut
624
fbcc39ad 625sub write {
626 my ( $self, $c, $buffer ) = @_;
4f5ebacd 627
fbcc39ad 628 unless ( $self->{_prepared_write} ) {
4f5ebacd 629 $self->prepare_write($c);
fbcc39ad 630 $self->{_prepared_write} = 1;
fc7ec1d9 631 }
4f5ebacd 632
633 print STDOUT $buffer;
fc7ec1d9 634}
635
933ba403 636=head2 $self->unescape_uri($uri)
637
6a44fe01 638Unescapes a given URI using the most efficient method available. Engines such
639as Apache may implement this using Apache's C-based modules, for example.
933ba403 640
641=cut
642
643sub unescape_uri {
644 my $self = shift;
645
646 my $e = URI::Escape::uri_unescape(@_);
647 $e =~ s/\+/ /g;
648
649 return $e;
650}
34d28dfd 651
4ab87e27 652=head2 $self->finalize_output
653
654<obsolete>, see finalize_body
655
fbcc39ad 656=head1 AUTHORS
657
658Sebastian Riedel, <sri@cpan.org>
fc7ec1d9 659
fbcc39ad 660Andy Grundman, <andy@hybridized.org>
fc7ec1d9 661
662=head1 COPYRIGHT
663
664This program is free software, you can redistribute it and/or modify it under
665the same terms as Perl itself.
666
667=cut
668
6691;