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