little things in Catalyst.pm
[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
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
878b821c 283sub finalize_read { }
fc7ec1d9 284
b5ecfcf0 285=head2 $self->finalize_uploads($c)
fc7ec1d9 286
4ab87e27 287Clean up after uploads, deleting temp files.
288
fc7ec1d9 289=cut
290
fbcc39ad 291sub finalize_uploads {
292 my ( $self, $c ) = @_;
99fe1710 293
fbcc39ad 294 if ( keys %{ $c->request->uploads } ) {
295 for my $key ( keys %{ $c->request->uploads } ) {
296 my $upload = $c->request->uploads->{$key};
297 unlink map { $_->tempname }
298 grep { -e $_->tempname }
299 ref $upload eq 'ARRAY' ? @{$upload} : ($upload);
c85ff642 300 }
c85ff642 301 }
fc7ec1d9 302}
303
b5ecfcf0 304=head2 $self->prepare_body($c)
fc7ec1d9 305
4ab87e27 306sets up the L<Catalyst::Request> object body using L<HTTP::Body>
307
fc7ec1d9 308=cut
309
fbcc39ad 310sub prepare_body {
311 my ( $self, $c ) = @_;
99fe1710 312
878b821c 313 if ( my $length = $self->read_length ) {
847e3257 314 unless ( $c->request->{_body} ) {
315 my $type = $c->request->header('Content-Type');
316 $c->request->{_body} = HTTP::Body->new( $type, $length );
317 $c->request->{_body}->{tmpdir} = $c->config->{uploadtmp}
318 if exists $c->config->{uploadtmp};
319 }
320
4f5ebacd 321 while ( my $buffer = $self->read($c) ) {
322 $c->prepare_body_chunk($buffer);
fbcc39ad 323 }
fdb3773e 324
325 # paranoia against wrong Content-Length header
847e3257 326 my $remaining = $length - $self->read_position;
34d28dfd 327 if ( $remaining > 0 ) {
fdb3773e 328 $self->finalize_read($c);
34d28dfd 329 Catalyst::Exception->throw(
847e3257 330 "Wrong Content-Length value: $length" );
fdb3773e 331 }
fc7ec1d9 332 }
847e3257 333 else {
334 # Defined but will cause all body code to be skipped
335 $c->request->{_body} = 0;
336 }
fc7ec1d9 337}
338
b5ecfcf0 339=head2 $self->prepare_body_chunk($c)
4bd82c41 340
4ab87e27 341Add a chunk to the request body.
342
4bd82c41 343=cut
344
345sub prepare_body_chunk {
346 my ( $self, $c, $chunk ) = @_;
4f5ebacd 347
348 $c->request->{_body}->add($chunk);
4bd82c41 349}
350
b5ecfcf0 351=head2 $self->prepare_body_parameters($c)
06e1b616 352
4ab87e27 353Sets up parameters from body.
354
06e1b616 355=cut
356
fbcc39ad 357sub prepare_body_parameters {
358 my ( $self, $c ) = @_;
847e3257 359
360 return unless $c->request->{_body};
361
fbcc39ad 362 $c->request->body_parameters( $c->request->{_body}->param );
363}
0556eb49 364
b5ecfcf0 365=head2 $self->prepare_connection($c)
0556eb49 366
4ab87e27 367Abstract method implemented in engines.
368
0556eb49 369=cut
370
371sub prepare_connection { }
372
b5ecfcf0 373=head2 $self->prepare_cookies($c)
fc7ec1d9 374
fa32ac82 375Parse cookies from header. Sets a L<CGI::Simple::Cookie> object.
4ab87e27 376
fc7ec1d9 377=cut
378
6dc87a0f 379sub prepare_cookies {
fbcc39ad 380 my ( $self, $c ) = @_;
6dc87a0f 381
382 if ( my $header = $c->request->header('Cookie') ) {
fa32ac82 383 $c->req->cookies( { CGI::Simple::Cookie->parse($header) } );
6dc87a0f 384 }
385}
fc7ec1d9 386
b5ecfcf0 387=head2 $self->prepare_headers($c)
fc7ec1d9 388
389=cut
390
391sub prepare_headers { }
392
b5ecfcf0 393=head2 $self->prepare_parameters($c)
fc7ec1d9 394
4ab87e27 395sets up parameters from query and post parameters.
396
fc7ec1d9 397=cut
398
fbcc39ad 399sub prepare_parameters {
400 my ( $self, $c ) = @_;
fc7ec1d9 401
fbcc39ad 402 # We copy, no references
c82ed742 403 foreach my $name ( keys %{ $c->request->query_parameters } ) {
404 my $param = $c->request->query_parameters->{$name};
fbcc39ad 405 $param = ref $param eq 'ARRAY' ? [ @{$param} ] : $param;
406 $c->request->parameters->{$name} = $param;
407 }
fc7ec1d9 408
fbcc39ad 409 # Merge query and body parameters
c82ed742 410 foreach my $name ( keys %{ $c->request->body_parameters } ) {
411 my $param = $c->request->body_parameters->{$name};
fbcc39ad 412 $param = ref $param eq 'ARRAY' ? [ @{$param} ] : $param;
413 if ( my $old_param = $c->request->parameters->{$name} ) {
414 if ( ref $old_param eq 'ARRAY' ) {
415 push @{ $c->request->parameters->{$name} },
416 ref $param eq 'ARRAY' ? @$param : $param;
417 }
418 else { $c->request->parameters->{$name} = [ $old_param, $param ] }
419 }
420 else { $c->request->parameters->{$name} = $param }
421 }
422}
423
b5ecfcf0 424=head2 $self->prepare_path($c)
fc7ec1d9 425
4ab87e27 426abstract method, implemented by engines.
427
fc7ec1d9 428=cut
429
430sub prepare_path { }
431
b5ecfcf0 432=head2 $self->prepare_request($c)
fc7ec1d9 433
b5ecfcf0 434=head2 $self->prepare_query_parameters($c)
fc7ec1d9 435
4ab87e27 436process the query string and extract query parameters.
437
fc7ec1d9 438=cut
439
e0616220 440sub prepare_query_parameters {
441 my ( $self, $c, $query_string ) = @_;
933ba403 442
3b4d1251 443 # Check for keywords (no = signs)
444 # (yes, index() is faster than a regex :))
933ba403 445 if ( index( $query_string, '=' ) < 0 ) {
3b4d1251 446 $c->request->query_keywords( $self->unescape_uri($query_string) );
933ba403 447 return;
448 }
449
450 my %query;
e0616220 451
452 # replace semi-colons
453 $query_string =~ s/;/&/g;
933ba403 454
455 my @params = split /&/, $query_string;
e0616220 456
933ba403 457 for my $item ( @params ) {
458
459 my ($param, $value)
460 = map { $self->unescape_uri($_) }
e5542b70 461 split( /=/, $item, 2 );
933ba403 462
463 $param = $self->unescape_uri($item) unless defined $param;
464
465 if ( exists $query{$param} ) {
466 if ( ref $query{$param} ) {
467 push @{ $query{$param} }, $value;
468 }
469 else {
470 $query{$param} = [ $query{$param}, $value ];
471 }
472 }
473 else {
474 $query{$param} = $value;
475 }
e0616220 476 }
933ba403 477
478 $c->request->query_parameters( \%query );
e0616220 479}
fbcc39ad 480
b5ecfcf0 481=head2 $self->prepare_read($c)
fbcc39ad 482
4ab87e27 483prepare to read from the engine.
484
fbcc39ad 485=cut
fc7ec1d9 486
fbcc39ad 487sub prepare_read {
488 my ( $self, $c ) = @_;
4f5ebacd 489
878b821c 490 # Initialize the read position
4f5ebacd 491 $self->read_position(0);
878b821c 492
493 # Initialize the amount of data we think we need to read
494 $self->read_length( $c->request->header('Content-Length') || 0 );
fbcc39ad 495}
fc7ec1d9 496
b5ecfcf0 497=head2 $self->prepare_request(@arguments)
fc7ec1d9 498
4ab87e27 499Populate the context object from the request object.
500
fc7ec1d9 501=cut
502
fbcc39ad 503sub prepare_request { }
fc7ec1d9 504
b5ecfcf0 505=head2 $self->prepare_uploads($c)
c9afa5fc 506
fbcc39ad 507=cut
508
509sub prepare_uploads {
510 my ( $self, $c ) = @_;
847e3257 511
512 return unless $c->request->{_body};
513
fbcc39ad 514 my $uploads = $c->request->{_body}->upload;
515 for my $name ( keys %$uploads ) {
516 my $files = $uploads->{$name};
517 $files = ref $files eq 'ARRAY' ? $files : [$files];
518 my @uploads;
519 for my $upload (@$files) {
520 my $u = Catalyst::Request::Upload->new;
521 $u->headers( HTTP::Headers->new( %{ $upload->{headers} } ) );
522 $u->type( $u->headers->content_type );
523 $u->tempname( $upload->{tempname} );
524 $u->size( $upload->{size} );
525 $u->filename( $upload->{filename} );
526 push @uploads, $u;
527 }
528 $c->request->uploads->{$name} = @uploads > 1 ? \@uploads : $uploads[0];
f4a57de4 529
c4bed79a 530 # support access to the filename as a normal param
531 my @filenames = map { $_->{filename} } @uploads;
a7e05d9d 532 # append, if there's already params with this name
533 if (exists $c->request->parameters->{$name}) {
534 if (ref $c->request->parameters->{$name} eq 'ARRAY') {
535 push @{ $c->request->parameters->{$name} }, @filenames;
536 }
537 else {
538 $c->request->parameters->{$name} =
539 [ $c->request->parameters->{$name}, @filenames ];
540 }
541 }
542 else {
543 $c->request->parameters->{$name} =
544 @filenames > 1 ? \@filenames : $filenames[0];
545 }
fbcc39ad 546 }
547}
548
b5ecfcf0 549=head2 $self->prepare_write($c)
c9afa5fc 550
4ab87e27 551Abstract method. Implemented by the engines.
552
c9afa5fc 553=cut
554
fbcc39ad 555sub prepare_write { }
556
b5ecfcf0 557=head2 $self->read($c, [$maxlength])
fbcc39ad 558
559=cut
560
561sub read {
562 my ( $self, $c, $maxlength ) = @_;
4f5ebacd 563
fbcc39ad 564 my $remaining = $self->read_length - $self->read_position;
4bd82c41 565 $maxlength ||= $CHUNKSIZE;
4f5ebacd 566
fbcc39ad 567 # Are we done reading?
568 if ( $remaining <= 0 ) {
4f5ebacd 569 $self->finalize_read($c);
fbcc39ad 570 return;
571 }
c9afa5fc 572
fbcc39ad 573 my $readlen = ( $remaining > $maxlength ) ? $maxlength : $remaining;
574 my $rc = $self->read_chunk( $c, my $buffer, $readlen );
575 if ( defined $rc ) {
576 $self->read_position( $self->read_position + $rc );
577 return $buffer;
578 }
579 else {
4f5ebacd 580 Catalyst::Exception->throw(
581 message => "Unknown error reading input: $!" );
fbcc39ad 582 }
583}
fc7ec1d9 584
b5ecfcf0 585=head2 $self->read_chunk($c, $buffer, $length)
23f9d934 586
fbcc39ad 587Each engine inplements read_chunk as its preferred way of reading a chunk
588of data.
fc7ec1d9 589
fbcc39ad 590=cut
61b1e958 591
fbcc39ad 592sub read_chunk { }
61b1e958 593
b5ecfcf0 594=head2 $self->read_length
ca39d576 595
fbcc39ad 596The length of input data to be read. This is obtained from the Content-Length
597header.
fc7ec1d9 598
b5ecfcf0 599=head2 $self->read_position
fc7ec1d9 600
fbcc39ad 601The amount of input data that has already been read.
63b763c5 602
b5ecfcf0 603=head2 $self->run($c)
63b763c5 604
4ab87e27 605Start the engine. Implemented by the various engine classes.
606
fbcc39ad 607=cut
fc7ec1d9 608
fbcc39ad 609sub run { }
fc7ec1d9 610
b5ecfcf0 611=head2 $self->write($c, $buffer)
fc7ec1d9 612
e512dd24 613Writes the buffer to the client.
4ab87e27 614
fc7ec1d9 615=cut
616
fbcc39ad 617sub write {
618 my ( $self, $c, $buffer ) = @_;
4f5ebacd 619
fbcc39ad 620 unless ( $self->{_prepared_write} ) {
4f5ebacd 621 $self->prepare_write($c);
fbcc39ad 622 $self->{_prepared_write} = 1;
fc7ec1d9 623 }
e512dd24 624
d04b2ffd 625 my $len = length($buffer);
626 my $wrote = syswrite STDOUT, $buffer;
e512dd24 627
d04b2ffd 628 if ( !defined $wrote && $! == EWOULDBLOCK ) {
629 # Unable to write on the first try, will retry in the loop below
630 $wrote = 0;
631 }
4d4d6635 632
d04b2ffd 633 if ( defined $wrote && $wrote < $len ) {
634 # We didn't write the whole buffer
635 while (1) {
636 my $ret = syswrite STDOUT, $buffer, $CHUNKSIZE, $wrote;
637 if ( defined $ret ) {
638 $wrote += $ret;
639 }
640 else {
641 next if $! == EWOULDBLOCK;
642 return;
643 }
644
645 last if $wrote >= $len;
e2b0ddd3 646 }
e512dd24 647 }
648
649 return $wrote;
fc7ec1d9 650}
651
933ba403 652=head2 $self->unescape_uri($uri)
653
6a44fe01 654Unescapes a given URI using the most efficient method available. Engines such
655as Apache may implement this using Apache's C-based modules, for example.
933ba403 656
657=cut
658
659sub unescape_uri {
8c7d83e1 660 my ( $self, $str ) = @_;
7d22a537 661
662 $str =~ s/(?:%([0-9A-Fa-f]{2})|\+)/defined $1 ? chr(hex($1)) : ' '/eg;
663
8c7d83e1 664 return $str;
933ba403 665}
34d28dfd 666
4ab87e27 667=head2 $self->finalize_output
668
669<obsolete>, see finalize_body
670
fbcc39ad 671=head1 AUTHORS
672
673Sebastian Riedel, <sri@cpan.org>
fc7ec1d9 674
fbcc39ad 675Andy Grundman, <andy@hybridized.org>
fc7ec1d9 676
677=head1 COPYRIGHT
678
679This program is free software, you can redistribute it and/or modify it under
680the same terms as Perl itself.
681
682=cut
683
6841;