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