prepared for 5.30 release.
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Engine.pm
CommitLineData
fc7ec1d9 1package Catalyst::Engine;
2
3use strict;
424b2705 4use base qw/Class::Data::Inheritable Class::Accessor::Fast/;
d70195d8 5use attributes ();
fc7ec1d9 6use UNIVERSAL::require;
6dc87a0f 7use CGI::Cookie;
fc7ec1d9 8use Data::Dumper;
9use HTML::Entities;
10use HTTP::Headers;
11use Time::HiRes qw/gettimeofday tv_interval/;
0f7ecc53 12use Text::ASCIITable;
a2f2cde9 13use Catalyst::Exception;
fc7ec1d9 14use Catalyst::Request;
146554c5 15use Catalyst::Request::Upload;
fc7ec1d9 16use Catalyst::Response;
a268a011 17use Catalyst::Utils;
fc7ec1d9 18
99fe1710 19# For pretty dumps
fc7ec1d9 20$Data::Dumper::Terse = 1;
21
1abd6db7 22__PACKAGE__->mk_classdata('components');
6ef62eb2 23__PACKAGE__->mk_accessors(qw/counter depth request response state/);
fc7ec1d9 24
fc7ec1d9 25*comp = \&component;
26*req = \&request;
27*res = \&response;
28
06e1b616 29# For backwards compatibility
30*finalize_output = \&finalize_body;
31
99fe1710 32# For statistics
e88fa058 33our $COUNT = 1;
34our $START = time;
35our $RECURSION = 1000;
6ef62eb2 36our $DETACH = "catalyst_detach\n";
fc7ec1d9 37
38=head1 NAME
39
40Catalyst::Engine - The Catalyst Engine
41
42=head1 SYNOPSIS
43
44See L<Catalyst>.
45
46=head1 DESCRIPTION
47
23f9d934 48=head1 METHODS
fc7ec1d9 49
23f9d934 50=over 4
51
23f9d934 52=item $c->benchmark($coderef)
fc7ec1d9 53
54Takes a coderef with arguments and returns elapsed time as float.
55
56 my ( $elapsed, $status ) = $c->benchmark( sub { return 1 } );
57 $c->log->info( sprintf "Processing took %f seconds", $elapsed );
58
59=cut
60
61sub benchmark {
62 my $c = shift;
63 my $code = shift;
64 my $time = [gettimeofday];
65 my @return = &$code(@_);
66 my $elapsed = tv_interval $time;
67 return wantarray ? ( $elapsed, @return ) : $elapsed;
68}
69
23f9d934 70=item $c->comp($name)
71
72=item $c->component($name)
fc7ec1d9 73
74Get a component object by name.
75
76 $c->comp('MyApp::Model::MyModel')->do_stuff;
77
78Regex search for a component.
79
80 $c->comp('mymodel')->do_stuff;
81
82=cut
83
84sub component {
3245f607 85 my $c = shift;
99fe1710 86
e88fa058 87 if (@_) {
99fe1710 88
3245f607 89 my $name = shift;
90
91 if ( my $component = $c->components->{$name} ) {
92 return $component;
93 }
94
95 else {
96 for my $component ( keys %{ $c->components } ) {
97 return $c->components->{$component} if $component =~ /$name/i;
98 }
fc7ec1d9 99 }
100 }
99fe1710 101
3245f607 102 return sort keys %{ $c->components };
fc7ec1d9 103}
104
e88fa058 105=item $c->counter
106
107Returns a hashref containing coderefs and execution counts.
108(Needed for deep recursion detection)
109
6ef62eb2 110=item $c->depth
111
112Returns the actual forward depth.
113
a554cc3b 114=item $c->error
23f9d934 115
a554cc3b 116=item $c->error($error, ...)
23f9d934 117
a554cc3b 118=item $c->error($arrayref)
fc7ec1d9 119
a554cc3b 120Returns an arrayref containing error messages.
fc7ec1d9 121
a554cc3b 122 my @error = @{ $c->error };
fc7ec1d9 123
124Add a new error.
125
a554cc3b 126 $c->error('Something bad happened');
fc7ec1d9 127
128=cut
129
a554cc3b 130sub error {
fc7ec1d9 131 my $c = shift;
a554cc3b 132 my $error = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
133 push @{ $c->{error} }, @$error;
134 return $c->{error};
fc7ec1d9 135}
136
6dc87a0f 137=item $c->execute($class, $coderef)
138
139Execute a coderef in given class and catch exceptions.
140Errors are available via $c->error.
141
142=cut
143
144sub execute {
145 my ( $c, $class, $code ) = @_;
91571b7b 146 $class = $c->components->{$class} || $class;
6dc87a0f 147 $c->state(0);
39de91b0 148 my $callsub = ( caller(1) )[3];
99fe1710 149
e88fa058 150 my $action = '';
151 if ( $c->debug ) {
152 $action = $c->actions->{reverse}->{"$code"};
153 $action = "/$action" unless $action =~ /\-\>/;
154 $c->counter->{"$code"}++;
155
156 if ( $c->counter->{"$code"} > $RECURSION ) {
157 my $error = qq/Deep recursion detected in "$action"/;
158 $c->log->error($error);
159 $c->error($error);
160 $c->state(0);
161 return $c->state;
162 }
163
164 $action = "-> $action" if $callsub =~ /forward$/;
165 }
166
6ef62eb2 167 $c->{depth}++;
6dc87a0f 168 eval {
169 if ( $c->debug )
170 {
6ef62eb2 171 my ( $elapsed, @state ) =
172 $c->benchmark( $code, $class, $c, @{ $c->req->args } );
0f7ecc53 173 push @{ $c->{stats} }, [ $action, sprintf( '%fs', $elapsed ) ];
6dc87a0f 174 $c->state(@state);
175 }
3ceed047 176 else { $c->state( &$code( $class, $c, @{ $c->req->args } ) || 0 ) }
6dc87a0f 177 };
6ef62eb2 178 $c->{depth}--;
99fe1710 179
6dc87a0f 180 if ( my $error = $@ ) {
b9ffe28b 181
6ef62eb2 182 if ( $error eq $DETACH ) { die $DETACH if $c->{depth} > 1 }
183 else {
184 unless ( ref $error ) {
185 chomp $error;
186 $error = qq/Caught exception "$error"/;
187 }
b9ffe28b 188
6ef62eb2 189 $c->log->error($error);
190 $c->error($error);
191 $c->state(0);
192 }
6dc87a0f 193 }
194 return $c->state;
195}
196
23f9d934 197=item $c->finalize
fc7ec1d9 198
ca39d576 199Finalize request.
fc7ec1d9 200
201=cut
202
203sub finalize {
204 my $c = shift;
23f9d934 205
6dc87a0f 206 $c->finalize_cookies;
207
49490aab 208 if ( my $location = $c->response->redirect ) {
23f9d934 209 $c->log->debug(qq/Redirecting to "$location"/) if $c->debug;
6dc87a0f 210 $c->response->header( Location => $location );
e7c0c583 211 $c->response->status(302) if $c->response->status !~ /^3\d\d$/;
6dc87a0f 212 }
213
969647fd 214 if ( $#{ $c->error } >= 0 ) {
215 $c->finalize_error;
23f9d934 216 }
6ef62eb2 217
d290eee8 218 if ( !$c->response->body && $c->response->status == 200 ) {
969647fd 219 $c->finalize_error;
220 }
fc7ec1d9 221
d7945f32 222 if ( $c->response->body && !$c->response->content_length ) {
d290eee8 223 $c->response->content_length( bytes::length( $c->response->body ) );
224 }
6ef62eb2 225
d290eee8 226 if ( $c->response->status =~ /^(1\d\d|[23]04)$/ ) {
227 $c->response->headers->remove_header("Content-Length");
228 $c->response->body('');
229 }
6ef62eb2 230
d290eee8 231 if ( $c->request->method eq 'HEAD' ) {
232 $c->response->body('');
fc7ec1d9 233 }
969647fd 234
fc7ec1d9 235 my $status = $c->finalize_headers;
06e1b616 236 $c->finalize_body;
fc7ec1d9 237 return $status;
238}
239
cd3bb248 240=item $c->finalize_output
241
242<obsolete>, see finalize_body
243
06e1b616 244=item $c->finalize_body
245
246Finalize body.
247
248=cut
249
250sub finalize_body { }
251
6dc87a0f 252=item $c->finalize_cookies
253
254Finalize cookies.
255
256=cut
257
258sub finalize_cookies {
259 my $c = shift;
260
261 while ( my ( $name, $cookie ) = each %{ $c->response->cookies } ) {
262 my $cookie = CGI::Cookie->new(
263 -name => $name,
264 -value => $cookie->{value},
265 -expires => $cookie->{expires},
266 -domain => $cookie->{domain},
267 -path => $cookie->{path},
268 -secure => $cookie->{secure} || 0
269 );
270
271 $c->res->headers->push_header( 'Set-Cookie' => $cookie->as_string );
272 }
273}
274
969647fd 275=item $c->finalize_error
276
ca39d576 277Finalize error.
969647fd 278
279=cut
280
281sub finalize_error {
282 my $c = shift;
283
284 $c->res->headers->content_type('text/html');
285 my $name = $c->config->{name} || 'Catalyst Application';
286
287 my ( $title, $error, $infos );
288 if ( $c->debug ) {
7f8e0078 289 $error = join '', map { '<code class="error">' . encode_entities($_) . '</code>' } @{ $c->error };
969647fd 290 $error ||= 'No output';
291 $title = $name = "$name on Catalyst $Catalyst::VERSION";
292 my $req = encode_entities Dumper $c->req;
293 my $res = encode_entities Dumper $c->res;
294 my $stash = encode_entities Dumper $c->stash;
295 $infos = <<"";
296<br/>
297<b><u>Request</u></b><br/>
298<pre>$req</pre>
299<b><u>Response</u></b><br/>
300<pre>$res</pre>
301<b><u>Stash</u></b><br/>
302<pre>$stash</pre>
303
304 }
305 else {
306 $title = $name;
307 $error = '';
308 $infos = <<"";
309<pre>
310(en) Please come back later
311(de) Bitte versuchen sie es spaeter nocheinmal
312(nl) Gelieve te komen later terug
313(no) Vennligst prov igjen senere
314(fr) Veuillez revenir plus tard
315(es) Vuelto por favor mas adelante
316(pt) Voltado por favor mais tarde
317(it) Ritornato prego più successivamente
318</pre>
319
320 $name = '';
321 }
e060fe05 322 $c->res->body( <<"" );
969647fd 323<html>
324<head>
325 <title>$title</title>
326 <style type="text/css">
327 body {
328 font-family: "Bitstream Vera Sans", "Trebuchet MS", Verdana,
329 Tahoma, Arial, helvetica, sans-serif;
330 color: #ddd;
331 background-color: #eee;
332 margin: 0px;
333 padding: 0px;
334 }
335 div.box {
336 background-color: #ccc;
337 border: 1px solid #aaa;
338 padding: 4px;
339 margin: 10px;
340 -moz-border-radius: 10px;
341 }
342 div.error {
343 background-color: #977;
344 border: 1px solid #755;
345 padding: 8px;
346 margin: 4px;
347 margin-bottom: 10px;
348 -moz-border-radius: 10px;
349 }
350 div.infos {
351 background-color: #797;
352 border: 1px solid #575;
353 padding: 8px;
354 margin: 4px;
355 margin-bottom: 10px;
356 -moz-border-radius: 10px;
357 }
358 div.name {
359 background-color: #779;
360 border: 1px solid #557;
361 padding: 8px;
362 margin: 4px;
363 -moz-border-radius: 10px;
364 }
7f8e0078 365 code.error {
366 display: block;
367 margin: 1em 0;
368 overflow: auto;
369 white-space: pre;
370 }
969647fd 371 </style>
372</head>
373<body>
374 <div class="box">
375 <div class="error">$error</div>
376 <div class="infos">$infos</div>
377 <div class="name">$name</div>
378 </div>
379</body>
380</html>
381
382}
383
23f9d934 384=item $c->finalize_headers
fc7ec1d9 385
ca39d576 386Finalize headers.
fc7ec1d9 387
388=cut
389
390sub finalize_headers { }
391
e2fd5b5f 392=item $c->handler( $class, @arguments )
fc7ec1d9 393
ca39d576 394Handles the request.
fc7ec1d9 395
396=cut
397
6dc87a0f 398sub handler {
e2fd5b5f 399 my ( $class, @arguments ) = @_;
fc7ec1d9 400
401 # Always expect worst case!
402 my $status = -1;
403 eval {
d41516b2 404 my @stats = ();
99fe1710 405
fc7ec1d9 406 my $handler = sub {
e2fd5b5f 407 my $c = $class->prepare(@arguments);
d41516b2 408 $c->{stats} = \@stats;
63b763c5 409 $c->dispatch;
fc7ec1d9 410 return $c->finalize;
411 };
99fe1710 412
fc7ec1d9 413 if ( $class->debug ) {
414 my $elapsed;
415 ( $elapsed, $status ) = $class->benchmark($handler);
416 $elapsed = sprintf '%f', $elapsed;
e88fa058 417 my $av = sprintf '%.3f',
418 ( $elapsed == 0 ? '??' : ( 1 / $elapsed ) );
0f7ecc53 419 my $t = Text::ASCIITable->new;
420 $t->setCols( 'Action', 'Time' );
3f36a3a3 421 $t->setColWidth( 'Action', 64, 1 );
422 $t->setColWidth( 'Time', 9, 1 );
0822f9a4 423
cd677e12 424 for my $stat (@stats) { $t->addRow( $stat->[0], $stat->[1] ) }
db0d17e8 425 $class->log->info( "Request took ${elapsed}s ($av/s)\n" . $t->draw );
fc7ec1d9 426 }
427 else { $status = &$handler }
99fe1710 428
fc7ec1d9 429 };
99fe1710 430
fc7ec1d9 431 if ( my $error = $@ ) {
432 chomp $error;
433 $class->log->error(qq/Caught exception in engine "$error"/);
434 }
99fe1710 435
fc7ec1d9 436 $COUNT++;
437 return $status;
438}
439
e2fd5b5f 440=item $c->prepare(@arguments)
fc7ec1d9 441
a554cc3b 442Turns the engine-specific request( Apache, CGI ... )
443into a Catalyst context .
fc7ec1d9 444
445=cut
446
447sub prepare {
e2fd5b5f 448 my ( $class, @arguments ) = @_;
99fe1710 449
fc7ec1d9 450 my $c = bless {
e88fa058 451 counter => {},
6ef62eb2 452 depth => 0,
fc7ec1d9 453 request => Catalyst::Request->new(
454 {
455 arguments => [],
456 cookies => {},
457 headers => HTTP::Headers->new,
458 parameters => {},
bfde09a2 459 secure => 0,
fc7ec1d9 460 snippets => [],
461 uploads => {}
462 }
463 ),
464 response => Catalyst::Response->new(
bfde09a2 465 {
03222156 466 body => '',
bfde09a2 467 cookies => {},
d290eee8 468 headers => HTTP::Headers->new( 'Content-Length' => 0 ),
03222156 469 status => 200
bfde09a2 470 }
fc7ec1d9 471 ),
b768faa3 472 stash => {},
473 state => 0
fc7ec1d9 474 }, $class;
99fe1710 475
fc7ec1d9 476 if ( $c->debug ) {
477 my $secs = time - $START || 1;
478 my $av = sprintf '%.3f', $COUNT / $secs;
1a0250cb 479 $c->log->debug('**********************************');
fc7ec1d9 480 $c->log->debug("* Request $COUNT ($av/s) [$$]");
1a0250cb 481 $c->log->debug('**********************************');
fc7ec1d9 482 $c->res->headers->header( 'X-Catalyst' => $Catalyst::VERSION );
483 }
99fe1710 484
e2fd5b5f 485 $c->prepare_request(@arguments);
bfde09a2 486 $c->prepare_connection;
ac733264 487 $c->prepare_headers;
1a80619d 488 $c->prepare_cookies;
bfde09a2 489 $c->prepare_path;
06e1b616 490 $c->prepare_action;
99fe1710 491
6ef62eb2 492 my $method = $c->req->method || '';
493 my $path = $c->req->path || '';
494 my $address = $c->req->address || '';
06e1b616 495
b4ca0ee8 496 $c->log->debug(qq/"$method" request for "$path" from $address/)
0556eb49 497 if $c->debug;
99fe1710 498
06e1b616 499 if ( $c->request->method eq 'POST' and $c->request->content_length ) {
500
501 if ( $c->req->content_type eq 'application/x-www-form-urlencoded' ) {
502 $c->prepare_parameters;
503 }
504 elsif ( $c->req->content_type eq 'multipart/form-data' ) {
505 $c->prepare_parameters;
506 $c->prepare_uploads;
507 }
508 else {
509 $c->prepare_body;
510 }
511 }
512
513 if ( $c->request->method eq 'GET' ) {
514 $c->prepare_parameters;
515 }
c85ff642 516
517 if ( $c->debug && keys %{ $c->req->params } ) {
0f7ecc53 518 my $t = Text::ASCIITable->new;
519 $t->setCols( 'Key', 'Value' );
0822f9a4 520 $t->setColWidth( 'Key', 37, 1 );
521 $t->setColWidth( 'Value', 36, 1 );
f78172f1 522 for my $key ( sort keys %{ $c->req->params } ) {
6d1ab915 523 my $param = $c->req->params->{$key};
524 my $value = defined($param) ? $param : '';
cd677e12 525 $t->addRow( $key, $value );
c85ff642 526 }
f45789b1 527 $c->log->debug( "Parameters are:\n" . $t->draw );
c85ff642 528 }
99fe1710 529
fc7ec1d9 530 return $c;
531}
532
23f9d934 533=item $c->prepare_action
fc7ec1d9 534
ca39d576 535Prepare action.
fc7ec1d9 536
537=cut
538
539sub prepare_action {
540 my $c = shift;
541 my $path = $c->req->path;
542 my @path = split /\//, $c->req->path;
543 $c->req->args( \my @args );
99fe1710 544
fc7ec1d9 545 while (@path) {
7833fdfc 546 $path = join '/', @path;
0169d3a8 547 if ( my $result = ${ $c->get_action($path) }[0] ) {
fc7ec1d9 548
549 # It's a regex
550 if ($#$result) {
7e5adedd 551 my $match = $result->[1];
552 my @snippets = @{ $result->[2] };
81f6fc50 553 $c->log->debug(
554 qq/Requested action is "$path" and matched "$match"/)
fc7ec1d9 555 if $c->debug;
556 $c->log->debug(
557 'Snippets are "' . join( ' ', @snippets ) . '"' )
558 if ( $c->debug && @snippets );
559 $c->req->action($match);
560 $c->req->snippets( \@snippets );
561 }
99fe1710 562
fc7ec1d9 563 else {
564 $c->req->action($path);
81f6fc50 565 $c->log->debug(qq/Requested action is "$path"/) if $c->debug;
fc7ec1d9 566 }
99fe1710 567
fc7ec1d9 568 $c->req->match($path);
fc7ec1d9 569 last;
570 }
571 unshift @args, pop @path;
572 }
99fe1710 573
fc7ec1d9 574 unless ( $c->req->action ) {
ac733264 575 $c->req->action('default');
87e67021 576 $c->req->match('');
fc7ec1d9 577 }
99fe1710 578
5783a9a5 579 $c->log->debug( 'Arguments are "' . join( '/', @args ) . '"' )
580 if ( $c->debug && @args );
fc7ec1d9 581}
582
06e1b616 583=item $c->prepare_body
584
585Prepare message body.
586
587=cut
588
589sub prepare_body { }
590
c9afa5fc 591=item $c->prepare_connection
0556eb49 592
ca39d576 593Prepare connection.
0556eb49 594
595=cut
596
597sub prepare_connection { }
598
c9afa5fc 599=item $c->prepare_cookies
fc7ec1d9 600
ca39d576 601Prepare cookies.
fc7ec1d9 602
603=cut
604
6dc87a0f 605sub prepare_cookies {
606 my $c = shift;
607
608 if ( my $header = $c->request->header('Cookie') ) {
609 $c->req->cookies( { CGI::Cookie->parse($header) } );
610 }
611}
fc7ec1d9 612
23f9d934 613=item $c->prepare_headers
fc7ec1d9 614
ca39d576 615Prepare headers.
fc7ec1d9 616
617=cut
618
619sub prepare_headers { }
620
23f9d934 621=item $c->prepare_parameters
fc7ec1d9 622
ca39d576 623Prepare parameters.
fc7ec1d9 624
625=cut
626
627sub prepare_parameters { }
628
23f9d934 629=item $c->prepare_path
fc7ec1d9 630
ca39d576 631Prepare path and base.
fc7ec1d9 632
633=cut
634
635sub prepare_path { }
636
23f9d934 637=item $c->prepare_request
fc7ec1d9 638
ca39d576 639Prepare the engine request.
fc7ec1d9 640
641=cut
642
643sub prepare_request { }
644
23f9d934 645=item $c->prepare_uploads
fc7ec1d9 646
ca39d576 647Prepare uploads.
fc7ec1d9 648
649=cut
650
651sub prepare_uploads { }
652
c9afa5fc 653=item $c->run
654
ca39d576 655Starts the engine.
c9afa5fc 656
657=cut
658
659sub run { }
660
61b1e958 661=item $c->request
fc7ec1d9 662
ca39d576 663=item $c->req
23f9d934 664
ca39d576 665Returns a C<Catalyst::Request> object.
fc7ec1d9 666
ca39d576 667 my $req = $c->req;
61b1e958 668
669=item $c->response
670
ca39d576 671=item $c->res
672
fc7ec1d9 673Returns a C<Catalyst::Response> object.
674
675 my $res = $c->res;
676
63b763c5 677=item $c->state
678
679Contains the return value of the last executed action.
680
23f9d934 681=item $c->stash
fc7ec1d9 682
ca39d576 683Returns a hashref containing all your data.
fc7ec1d9 684
685 $c->stash->{foo} ||= 'yada';
686 print $c->stash->{foo};
687
688=cut
689
690sub stash {
691 my $self = shift;
e88fa058 692 if (@_) {
c19e2f4a 693 my $stash = @_ > 1 ? {@_} : $_[0];
fc7ec1d9 694 while ( my ( $key, $val ) = each %$stash ) {
695 $self->{stash}->{$key} = $val;
696 }
697 }
698 return $self->{stash};
699}
700
23f9d934 701=back
702
fc7ec1d9 703=head1 AUTHOR
704
705Sebastian Riedel, C<sri@cpan.org>
706
707=head1 COPYRIGHT
708
709This program is free software, you can redistribute it and/or modify it under
710the same terms as Perl itself.
711
712=cut
713
7141;