Renaming all the attributes, as making them _private breaks multiple plugins. See...
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Engine.pm
CommitLineData
fc7ec1d9 1package Catalyst::Engine;
2
7fa2c9c1 3use Moose;
4with 'MooseX::Emulate::Class::Accessor::Fast';
5
fa32ac82 6use CGI::Simple::Cookie;
f63c03e4 7use Data::Dump qw/dump/;
d04b2ffd 8use Errno 'EWOULDBLOCK';
fc7ec1d9 9use HTML::Entities;
fbcc39ad 10use HTTP::Body;
fc7ec1d9 11use HTTP::Headers;
e0616220 12use URI::QueryParam;
2832cb5d 13use Scalar::Util ();
fbcc39ad 14
15# input position and length
7fa2c9c1 16has read_length => (is => 'rw');
17has read_position => (is => 'rw');
fbcc39ad 18
02570318 19has _prepared_write => (is => 'rw');
20
0fc2d522 21no Moose;
22
4bd82c41 23# Amount of data to read from input on each pass
4bb8bd62 24our $CHUNKSIZE = 64 * 1024;
4bd82c41 25
fc7ec1d9 26=head1 NAME
27
28Catalyst::Engine - The Catalyst Engine
29
30=head1 SYNOPSIS
31
32See L<Catalyst>.
33
34=head1 DESCRIPTION
35
23f9d934 36=head1 METHODS
fc7ec1d9 37
cd3bb248 38
b5ecfcf0 39=head2 $self->finalize_body($c)
06e1b616 40
fbcc39ad 41Finalize body. Prints the response output.
06e1b616 42
43=cut
44
fbcc39ad 45sub finalize_body {
46 my ( $self, $c ) = @_;
7257e9db 47 my $body = $c->response->body;
f9b6d612 48 no warnings 'uninitialized';
49 if ( Scalar::Util::blessed($body) && $body->can('read') or ref($body) eq 'GLOB' ) {
7257e9db 50 while ( !eof $body ) {
4c423abf 51 read $body, my ($buffer), $CHUNKSIZE;
6484fba0 52 last unless $self->write( $c, $buffer );
f4a57de4 53 }
7257e9db 54 close $body;
f4a57de4 55 }
56 else {
7257e9db 57 $self->write( $c, $body );
f4a57de4 58 }
fbcc39ad 59}
6dc87a0f 60
b5ecfcf0 61=head2 $self->finalize_cookies($c)
6dc87a0f 62
fa32ac82 63Create CGI::Simple::Cookie objects from $c->res->cookies, and set them as
64response headers.
4ab87e27 65
6dc87a0f 66=cut
67
68sub finalize_cookies {
fbcc39ad 69 my ( $self, $c ) = @_;
6dc87a0f 70
fbcc39ad 71 my @cookies;
7fa2c9c1 72 my $response = $c->response;
c82ed742 73
91772de9 74 foreach my $name (keys %{ $response->cookies }) {
75
76 my $val = $response->cookies->{$name};
fbcc39ad 77
2832cb5d 78 my $cookie = (
79 Scalar::Util::blessed($val)
80 ? $val
81 : CGI::Simple::Cookie->new(
82 -name => $name,
83 -value => $val->{value},
84 -expires => $val->{expires},
85 -domain => $val->{domain},
86 -path => $val->{path},
87 -secure => $val->{secure} || 0
88 )
6dc87a0f 89 );
90
fbcc39ad 91 push @cookies, $cookie->as_string;
6dc87a0f 92 }
6dc87a0f 93
b39840da 94 for my $cookie (@cookies) {
7fa2c9c1 95 $response->headers->push_header( 'Set-Cookie' => $cookie );
fbcc39ad 96 }
97}
969647fd 98
b5ecfcf0 99=head2 $self->finalize_error($c)
969647fd 100
6e5b548e 101Output an appropriate error message. Called if there's an error in $c
4ab87e27 102after the dispatch has finished. Will output debug messages if Catalyst
103is in debug mode, or a `please come back later` message otherwise.
104
969647fd 105=cut
106
107sub finalize_error {
fbcc39ad 108 my ( $self, $c ) = @_;
969647fd 109
7299a7b4 110 $c->res->content_type('text/html; charset=utf-8');
34d28dfd 111 my $name = $c->config->{name} || join(' ', split('::', ref $c));
969647fd 112
113 my ( $title, $error, $infos );
114 if ( $c->debug ) {
62d9b030 115
116 # For pretty dumps
b5ecfcf0 117 $error = join '', map {
118 '<p><code class="error">'
119 . encode_entities($_)
120 . '</code></p>'
121 } @{ $c->error };
969647fd 122 $error ||= 'No output';
2666dd3b 123 $error = qq{<pre wrap="">$error</pre>};
969647fd 124 $title = $name = "$name on Catalyst $Catalyst::VERSION";
d82cc9ae 125 $name = "<h1>$name</h1>";
fbcc39ad 126
127 # Don't show context in the dump
02570318 128 $c->req->_clear_context;
129 $c->res->_clear_context;
fbcc39ad 130
131 # Don't show body parser in the dump
132 delete $c->req->{_body};
133
c6ef5e69 134 my @infos;
135 my $i = 0;
c6ef5e69 136 for my $dump ( $c->dump_these ) {
c6ef5e69 137 my $name = $dump->[0];
f63c03e4 138 my $value = encode_entities( dump( $dump->[1] ));
c6ef5e69 139 push @infos, sprintf <<"EOF", $name, $value;
9619f23c 140<h2><a href="#" onclick="toggleDump('dump_$i'); return false">%s</a></h2>
c6ef5e69 141<div id="dump_$i">
2666dd3b 142 <pre wrap="">%s</pre>
c6ef5e69 143</div>
144EOF
145 $i++;
146 }
147 $infos = join "\n", @infos;
969647fd 148 }
149 else {
150 $title = $name;
151 $error = '';
152 $infos = <<"";
153<pre>
154(en) Please come back later
0c2b4ac0 155(fr) SVP veuillez revenir plus tard
969647fd 156(de) Bitte versuchen sie es spaeter nocheinmal
d82cc9ae 157(at) Konnten's bitt'schoen spaeter nochmal reinschauen
969647fd 158(no) Vennligst prov igjen senere
d82cc9ae 159(dk) Venligst prov igen senere
160(pl) Prosze sprobowac pozniej
2f381252 161(pt) Por favor volte mais tarde
969647fd 162</pre>
163
164 $name = '';
165 }
e060fe05 166 $c->res->body( <<"" );
7299a7b4 167<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
168 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
169<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
969647fd 170<head>
7299a7b4 171 <meta http-equiv="Content-Language" content="en" />
172 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
969647fd 173 <title>$title</title>
7299a7b4 174 <script type="text/javascript">
c6ef5e69 175 <!--
176 function toggleDump (dumpElement) {
7299a7b4 177 var e = document.getElementById( dumpElement );
178 if (e.style.display == "none") {
179 e.style.display = "";
c6ef5e69 180 }
181 else {
7299a7b4 182 e.style.display = "none";
c6ef5e69 183 }
184 }
185 -->
186 </script>
969647fd 187 <style type="text/css">
188 body {
189 font-family: "Bitstream Vera Sans", "Trebuchet MS", Verdana,
190 Tahoma, Arial, helvetica, sans-serif;
34d28dfd 191 color: #333;
969647fd 192 background-color: #eee;
193 margin: 0px;
194 padding: 0px;
195 }
c6ef5e69 196 :link, :link:hover, :visited, :visited:hover {
34d28dfd 197 color: #000;
c6ef5e69 198 }
969647fd 199 div.box {
9619f23c 200 position: relative;
969647fd 201 background-color: #ccc;
202 border: 1px solid #aaa;
203 padding: 4px;
204 margin: 10px;
969647fd 205 }
206 div.error {
34d28dfd 207 background-color: #cce;
969647fd 208 border: 1px solid #755;
209 padding: 8px;
210 margin: 4px;
211 margin-bottom: 10px;
969647fd 212 }
213 div.infos {
34d28dfd 214 background-color: #eee;
969647fd 215 border: 1px solid #575;
216 padding: 8px;
217 margin: 4px;
218 margin-bottom: 10px;
969647fd 219 }
220 div.name {
34d28dfd 221 background-color: #cce;
969647fd 222 border: 1px solid #557;
223 padding: 8px;
224 margin: 4px;
969647fd 225 }
7f8e0078 226 code.error {
227 display: block;
228 margin: 1em 0;
229 overflow: auto;
7f8e0078 230 }
9619f23c 231 div.name h1, div.error p {
232 margin: 0;
233 }
234 h2 {
235 margin-top: 0;
236 margin-bottom: 10px;
237 font-size: medium;
238 font-weight: bold;
239 text-decoration: underline;
240 }
241 h1 {
242 font-size: medium;
243 font-weight: normal;
244 }
2666dd3b 245 /* from http://users.tkk.fi/~tkarvine/linux/doc/pre-wrap/pre-wrap-css3-mozilla-opera-ie.html */
246 /* Browser specific (not valid) styles to make preformatted text wrap */
ac5c933b 247 pre {
2666dd3b 248 white-space: pre-wrap; /* css-3 */
249 white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
250 white-space: -pre-wrap; /* Opera 4-6 */
251 white-space: -o-pre-wrap; /* Opera 7 */
252 word-wrap: break-word; /* Internet Explorer 5.5+ */
253 }
969647fd 254 </style>
255</head>
256<body>
257 <div class="box">
258 <div class="error">$error</div>
259 <div class="infos">$infos</div>
260 <div class="name">$name</div>
261 </div>
262</body>
263</html>
264
d82cc9ae 265
266 # Trick IE
267 $c->res->{body} .= ( ' ' x 512 );
268
269 # Return 500
33117422 270 $c->res->status(500);
969647fd 271}
272
b5ecfcf0 273=head2 $self->finalize_headers($c)
fc7ec1d9 274
4ab87e27 275Abstract method, allows engines to write headers to response
276
fc7ec1d9 277=cut
278
279sub finalize_headers { }
280
b5ecfcf0 281=head2 $self->finalize_read($c)
fc7ec1d9 282
283=cut
284
878b821c 285sub finalize_read { }
fc7ec1d9 286
b5ecfcf0 287=head2 $self->finalize_uploads($c)
fc7ec1d9 288
4ab87e27 289Clean up after uploads, deleting temp files.
290
fc7ec1d9 291=cut
292
fbcc39ad 293sub finalize_uploads {
294 my ( $self, $c ) = @_;
99fe1710 295
7fa2c9c1 296 my $request = $c->request;
91772de9 297 foreach my $key (keys %{ $request->uploads }) {
298 my $upload = $request->uploads->{$key};
7fa2c9c1 299 unlink grep { -e $_ } map { $_->tempname }
300 (ref $upload eq 'ARRAY' ? @{$upload} : ($upload));
c85ff642 301 }
7fa2c9c1 302
fc7ec1d9 303}
304
b5ecfcf0 305=head2 $self->prepare_body($c)
fc7ec1d9 306
4ab87e27 307sets up the L<Catalyst::Request> object body using L<HTTP::Body>
308
fc7ec1d9 309=cut
310
fbcc39ad 311sub prepare_body {
312 my ( $self, $c ) = @_;
99fe1710 313
878b821c 314 if ( my $length = $self->read_length ) {
7fa2c9c1 315 my $request = $c->request;
316 unless ( $request->{_body} ) {
317 my $type = $request->header('Content-Type');
318 $request->{_body} = HTTP::Body->new( $type, $length );
2f381252 319 $request->{_body}->tmpdir( $c->config->{uploadtmp} )
847e3257 320 if exists $c->config->{uploadtmp};
321 }
ac5c933b 322
4f5ebacd 323 while ( my $buffer = $self->read($c) ) {
324 $c->prepare_body_chunk($buffer);
fbcc39ad 325 }
fdb3773e 326
327 # paranoia against wrong Content-Length header
847e3257 328 my $remaining = $length - $self->read_position;
34d28dfd 329 if ( $remaining > 0 ) {
fdb3773e 330 $self->finalize_read($c);
34d28dfd 331 Catalyst::Exception->throw(
847e3257 332 "Wrong Content-Length value: $length" );
fdb3773e 333 }
fc7ec1d9 334 }
847e3257 335 else {
336 # Defined but will cause all body code to be skipped
337 $c->request->{_body} = 0;
338 }
fc7ec1d9 339}
340
b5ecfcf0 341=head2 $self->prepare_body_chunk($c)
4bd82c41 342
4ab87e27 343Add a chunk to the request body.
344
4bd82c41 345=cut
346
347sub prepare_body_chunk {
348 my ( $self, $c, $chunk ) = @_;
4f5ebacd 349
350 $c->request->{_body}->add($chunk);
4bd82c41 351}
352
b5ecfcf0 353=head2 $self->prepare_body_parameters($c)
06e1b616 354
ac5c933b 355Sets up parameters from body.
4ab87e27 356
06e1b616 357=cut
358
fbcc39ad 359sub prepare_body_parameters {
360 my ( $self, $c ) = @_;
ac5c933b 361
847e3257 362 return unless $c->request->{_body};
ac5c933b 363
fbcc39ad 364 $c->request->body_parameters( $c->request->{_body}->param );
365}
0556eb49 366
b5ecfcf0 367=head2 $self->prepare_connection($c)
0556eb49 368
4ab87e27 369Abstract method implemented in engines.
370
0556eb49 371=cut
372
373sub prepare_connection { }
374
b5ecfcf0 375=head2 $self->prepare_cookies($c)
fc7ec1d9 376
fa32ac82 377Parse cookies from header. Sets a L<CGI::Simple::Cookie> object.
4ab87e27 378
fc7ec1d9 379=cut
380
6dc87a0f 381sub prepare_cookies {
fbcc39ad 382 my ( $self, $c ) = @_;
6dc87a0f 383
384 if ( my $header = $c->request->header('Cookie') ) {
fa32ac82 385 $c->req->cookies( { CGI::Simple::Cookie->parse($header) } );
6dc87a0f 386 }
387}
fc7ec1d9 388
b5ecfcf0 389=head2 $self->prepare_headers($c)
fc7ec1d9 390
391=cut
392
393sub prepare_headers { }
394
b5ecfcf0 395=head2 $self->prepare_parameters($c)
fc7ec1d9 396
4ab87e27 397sets up parameters from query and post parameters.
398
fc7ec1d9 399=cut
400
fbcc39ad 401sub prepare_parameters {
402 my ( $self, $c ) = @_;
fc7ec1d9 403
7fa2c9c1 404 my $request = $c->request;
405 my $parameters = $request->parameters;
406 my $body_parameters = $request->body_parameters;
407 my $query_parameters = $request->query_parameters;
fbcc39ad 408 # We copy, no references
91772de9 409 foreach my $name (keys %$query_parameters) {
410 my $param = $query_parameters->{$name};
7fa2c9c1 411 $parameters->{$name} = ref $param eq 'ARRAY' ? [ @$param ] : $param;
fbcc39ad 412 }
fc7ec1d9 413
fbcc39ad 414 # Merge query and body parameters
91772de9 415 foreach my $name (keys %$body_parameters) {
416 my $param = $body_parameters->{$name};
7fa2c9c1 417 my @values = ref $param eq 'ARRAY' ? @$param : ($param);
418 if ( my $existing = $parameters->{$name} ) {
419 unshift(@values, (ref $existing eq 'ARRAY' ? @$existing : $existing));
fbcc39ad 420 }
7fa2c9c1 421 $parameters->{$name} = @values > 1 ? \@values : $values[0];
fbcc39ad 422 }
423}
424
b5ecfcf0 425=head2 $self->prepare_path($c)
fc7ec1d9 426
4ab87e27 427abstract method, implemented by engines.
428
fc7ec1d9 429=cut
430
431sub prepare_path { }
432
b5ecfcf0 433=head2 $self->prepare_request($c)
fc7ec1d9 434
b5ecfcf0 435=head2 $self->prepare_query_parameters($c)
fc7ec1d9 436
4ab87e27 437process the query string and extract query parameters.
438
fc7ec1d9 439=cut
440
e0616220 441sub prepare_query_parameters {
442 my ( $self, $c, $query_string ) = @_;
ac5c933b 443
3b4d1251 444 # Check for keywords (no = signs)
445 # (yes, index() is faster than a regex :))
933ba403 446 if ( index( $query_string, '=' ) < 0 ) {
3b4d1251 447 $c->request->query_keywords( $self->unescape_uri($query_string) );
933ba403 448 return;
449 }
450
451 my %query;
e0616220 452
453 # replace semi-colons
454 $query_string =~ s/;/&/g;
ac5c933b 455
2f381252 456 my @params = grep { length $_ } split /&/, $query_string;
e0616220 457
933ba403 458 for my $item ( @params ) {
ac5c933b 459
460 my ($param, $value)
933ba403 461 = map { $self->unescape_uri($_) }
e5542b70 462 split( /=/, $item, 2 );
ac5c933b 463
933ba403 464 $param = $self->unescape_uri($item) unless defined $param;
ac5c933b 465
933ba403 466 if ( exists $query{$param} ) {
467 if ( ref $query{$param} ) {
468 push @{ $query{$param} }, $value;
469 }
470 else {
471 $query{$param} = [ $query{$param}, $value ];
472 }
473 }
474 else {
475 $query{$param} = $value;
476 }
e0616220 477 }
933ba403 478
479 $c->request->query_parameters( \%query );
e0616220 480}
fbcc39ad 481
b5ecfcf0 482=head2 $self->prepare_read($c)
fbcc39ad 483
4ab87e27 484prepare to read from the engine.
485
fbcc39ad 486=cut
fc7ec1d9 487
fbcc39ad 488sub prepare_read {
489 my ( $self, $c ) = @_;
4f5ebacd 490
878b821c 491 # Initialize the read position
4f5ebacd 492 $self->read_position(0);
ac5c933b 493
878b821c 494 # Initialize the amount of data we think we need to read
495 $self->read_length( $c->request->header('Content-Length') || 0 );
fbcc39ad 496}
fc7ec1d9 497
b5ecfcf0 498=head2 $self->prepare_request(@arguments)
fc7ec1d9 499
4ab87e27 500Populate the context object from the request object.
501
fc7ec1d9 502=cut
503
fbcc39ad 504sub prepare_request { }
fc7ec1d9 505
b5ecfcf0 506=head2 $self->prepare_uploads($c)
c9afa5fc 507
fbcc39ad 508=cut
509
510sub prepare_uploads {
511 my ( $self, $c ) = @_;
7fa2c9c1 512
513 my $request = $c->request;
514 return unless $request->{_body};
515
516 my $uploads = $request->{_body}->upload;
517 my $parameters = $request->parameters;
91772de9 518 foreach my $name (keys %$uploads) {
519 my $files = $uploads->{$name};
fbcc39ad 520 my @uploads;
7fa2c9c1 521 for my $upload (ref $files eq 'ARRAY' ? @$files : ($files)) {
522 my $headers = HTTP::Headers->new( %{ $upload->{headers} } );
523 my $u = Catalyst::Request::Upload->new
524 (
525 size => $upload->{size},
526 type => $headers->content_type,
527 headers => $headers,
528 tempname => $upload->{tempname},
529 filename => $upload->{filename},
530 );
fbcc39ad 531 push @uploads, $u;
532 }
7fa2c9c1 533 $request->uploads->{$name} = @uploads > 1 ? \@uploads : $uploads[0];
f4a57de4 534
c4bed79a 535 # support access to the filename as a normal param
536 my @filenames = map { $_->{filename} } @uploads;
a7e05d9d 537 # append, if there's already params with this name
7fa2c9c1 538 if (exists $parameters->{$name}) {
539 if (ref $parameters->{$name} eq 'ARRAY') {
540 push @{ $parameters->{$name} }, @filenames;
a7e05d9d 541 }
542 else {
7fa2c9c1 543 $parameters->{$name} = [ $parameters->{$name}, @filenames ];
a7e05d9d 544 }
545 }
546 else {
7fa2c9c1 547 $parameters->{$name} = @filenames > 1 ? \@filenames : $filenames[0];
a7e05d9d 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
02570318 623 unless ( $self->_prepared_write ) {
4f5ebacd 624 $self->prepare_write($c);
02570318 625 $self->_prepared_write(1);
fc7ec1d9 626 }
ac5c933b 627
094a0974 628 return 0 if !defined $buffer;
629
d04b2ffd 630 my $len = length($buffer);
631 my $wrote = syswrite STDOUT, $buffer;
ac5c933b 632
d04b2ffd 633 if ( !defined $wrote && $! == EWOULDBLOCK ) {
634 # Unable to write on the first try, will retry in the loop below
635 $wrote = 0;
636 }
ac5c933b 637
d04b2ffd 638 if ( defined $wrote && $wrote < $len ) {
639 # We didn't write the whole buffer
640 while (1) {
641 my $ret = syswrite STDOUT, $buffer, $CHUNKSIZE, $wrote;
642 if ( defined $ret ) {
643 $wrote += $ret;
644 }
645 else {
646 next if $! == EWOULDBLOCK;
647 return;
648 }
ac5c933b 649
d04b2ffd 650 last if $wrote >= $len;
e2b0ddd3 651 }
e512dd24 652 }
ac5c933b 653
e512dd24 654 return $wrote;
fc7ec1d9 655}
656
933ba403 657=head2 $self->unescape_uri($uri)
658
6a44fe01 659Unescapes a given URI using the most efficient method available. Engines such
660as Apache may implement this using Apache's C-based modules, for example.
933ba403 661
662=cut
663
664sub unescape_uri {
8c7d83e1 665 my ( $self, $str ) = @_;
7d22a537 666
667 $str =~ s/(?:%([0-9A-Fa-f]{2})|\+)/defined $1 ? chr(hex($1)) : ' '/eg;
668
8c7d83e1 669 return $str;
933ba403 670}
34d28dfd 671
4ab87e27 672=head2 $self->finalize_output
673
674<obsolete>, see finalize_body
675
fbcc39ad 676=head1 AUTHORS
677
2f381252 678Catalyst Contributors, see Catalyst.pm
fc7ec1d9 679
680=head1 COPYRIGHT
681
682This program is free software, you can redistribute it and/or modify it under
683the same terms as Perl itself.
684
685=cut
686
6871;