Fix various doc typoes.
[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
0f56bbcf 132 $c->req->_clear_body;
fbcc39ad 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
b31c0f2e 162(ru) Попробуйте еще раз позже
163(ua) Спробуйте ще раз пізніше
969647fd 164</pre>
165
166 $name = '';
167 }
e060fe05 168 $c->res->body( <<"" );
7299a7b4 169<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
170 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
171<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
969647fd 172<head>
7299a7b4 173 <meta http-equiv="Content-Language" content="en" />
174 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
969647fd 175 <title>$title</title>
7299a7b4 176 <script type="text/javascript">
c6ef5e69 177 <!--
178 function toggleDump (dumpElement) {
7299a7b4 179 var e = document.getElementById( dumpElement );
180 if (e.style.display == "none") {
181 e.style.display = "";
c6ef5e69 182 }
183 else {
7299a7b4 184 e.style.display = "none";
c6ef5e69 185 }
186 }
187 -->
188 </script>
969647fd 189 <style type="text/css">
190 body {
191 font-family: "Bitstream Vera Sans", "Trebuchet MS", Verdana,
192 Tahoma, Arial, helvetica, sans-serif;
34d28dfd 193 color: #333;
969647fd 194 background-color: #eee;
195 margin: 0px;
196 padding: 0px;
197 }
c6ef5e69 198 :link, :link:hover, :visited, :visited:hover {
34d28dfd 199 color: #000;
c6ef5e69 200 }
969647fd 201 div.box {
9619f23c 202 position: relative;
969647fd 203 background-color: #ccc;
204 border: 1px solid #aaa;
205 padding: 4px;
206 margin: 10px;
969647fd 207 }
208 div.error {
34d28dfd 209 background-color: #cce;
969647fd 210 border: 1px solid #755;
211 padding: 8px;
212 margin: 4px;
213 margin-bottom: 10px;
969647fd 214 }
215 div.infos {
34d28dfd 216 background-color: #eee;
969647fd 217 border: 1px solid #575;
218 padding: 8px;
219 margin: 4px;
220 margin-bottom: 10px;
969647fd 221 }
222 div.name {
34d28dfd 223 background-color: #cce;
969647fd 224 border: 1px solid #557;
225 padding: 8px;
226 margin: 4px;
969647fd 227 }
7f8e0078 228 code.error {
229 display: block;
230 margin: 1em 0;
231 overflow: auto;
7f8e0078 232 }
9619f23c 233 div.name h1, div.error p {
234 margin: 0;
235 }
236 h2 {
237 margin-top: 0;
238 margin-bottom: 10px;
239 font-size: medium;
240 font-weight: bold;
241 text-decoration: underline;
242 }
243 h1 {
244 font-size: medium;
245 font-weight: normal;
246 }
2666dd3b 247 /* from http://users.tkk.fi/~tkarvine/linux/doc/pre-wrap/pre-wrap-css3-mozilla-opera-ie.html */
248 /* Browser specific (not valid) styles to make preformatted text wrap */
ac5c933b 249 pre {
2666dd3b 250 white-space: pre-wrap; /* css-3 */
251 white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
252 white-space: -pre-wrap; /* Opera 4-6 */
253 white-space: -o-pre-wrap; /* Opera 7 */
254 word-wrap: break-word; /* Internet Explorer 5.5+ */
255 }
969647fd 256 </style>
257</head>
258<body>
259 <div class="box">
260 <div class="error">$error</div>
261 <div class="infos">$infos</div>
262 <div class="name">$name</div>
263 </div>
264</body>
265</html>
266
d82cc9ae 267
268 # Trick IE
269 $c->res->{body} .= ( ' ' x 512 );
270
271 # Return 500
33117422 272 $c->res->status(500);
969647fd 273}
274
b5ecfcf0 275=head2 $self->finalize_headers($c)
fc7ec1d9 276
4ab87e27 277Abstract method, allows engines to write headers to response
278
fc7ec1d9 279=cut
280
281sub finalize_headers { }
282
b5ecfcf0 283=head2 $self->finalize_read($c)
fc7ec1d9 284
285=cut
286
878b821c 287sub finalize_read { }
fc7ec1d9 288
b5ecfcf0 289=head2 $self->finalize_uploads($c)
fc7ec1d9 290
4ab87e27 291Clean up after uploads, deleting temp files.
292
fc7ec1d9 293=cut
294
fbcc39ad 295sub finalize_uploads {
296 my ( $self, $c ) = @_;
99fe1710 297
7fa2c9c1 298 my $request = $c->request;
91772de9 299 foreach my $key (keys %{ $request->uploads }) {
300 my $upload = $request->uploads->{$key};
7fa2c9c1 301 unlink grep { -e $_ } map { $_->tempname }
302 (ref $upload eq 'ARRAY' ? @{$upload} : ($upload));
c85ff642 303 }
7fa2c9c1 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 ) {
7fa2c9c1 317 my $request = $c->request;
0f56bbcf 318 unless ( $request->_body ) {
7fa2c9c1 319 my $type = $request->header('Content-Type');
0f56bbcf 320 $request->_body(HTTP::Body->new( $type, $length ));
321 $request->_body->tmpdir( $c->config->{uploadtmp} )
847e3257 322 if exists $c->config->{uploadtmp};
323 }
ac5c933b 324
4f5ebacd 325 while ( my $buffer = $self->read($c) ) {
326 $c->prepare_body_chunk($buffer);
fbcc39ad 327 }
fdb3773e 328
329 # paranoia against wrong Content-Length header
847e3257 330 my $remaining = $length - $self->read_position;
34d28dfd 331 if ( $remaining > 0 ) {
fdb3773e 332 $self->finalize_read($c);
34d28dfd 333 Catalyst::Exception->throw(
847e3257 334 "Wrong Content-Length value: $length" );
fdb3773e 335 }
fc7ec1d9 336 }
847e3257 337 else {
338 # Defined but will cause all body code to be skipped
0f56bbcf 339 $c->request->_body(0);
847e3257 340 }
fc7ec1d9 341}
342
b5ecfcf0 343=head2 $self->prepare_body_chunk($c)
4bd82c41 344
4ab87e27 345Add a chunk to the request body.
346
4bd82c41 347=cut
348
349sub prepare_body_chunk {
350 my ( $self, $c, $chunk ) = @_;
4f5ebacd 351
0f56bbcf 352 $c->request->_body->add($chunk);
4bd82c41 353}
354
b5ecfcf0 355=head2 $self->prepare_body_parameters($c)
06e1b616 356
ac5c933b 357Sets up parameters from body.
4ab87e27 358
06e1b616 359=cut
360
fbcc39ad 361sub prepare_body_parameters {
362 my ( $self, $c ) = @_;
ac5c933b 363
0f56bbcf 364 return unless $c->request->_body;
ac5c933b 365
0f56bbcf 366 $c->request->body_parameters( $c->request->_body->param );
fbcc39ad 367}
0556eb49 368
b5ecfcf0 369=head2 $self->prepare_connection($c)
0556eb49 370
4ab87e27 371Abstract method implemented in engines.
372
0556eb49 373=cut
374
375sub prepare_connection { }
376
b5ecfcf0 377=head2 $self->prepare_cookies($c)
fc7ec1d9 378
fa32ac82 379Parse cookies from header. Sets a L<CGI::Simple::Cookie> object.
4ab87e27 380
fc7ec1d9 381=cut
382
6dc87a0f 383sub prepare_cookies {
fbcc39ad 384 my ( $self, $c ) = @_;
6dc87a0f 385
386 if ( my $header = $c->request->header('Cookie') ) {
fa32ac82 387 $c->req->cookies( { CGI::Simple::Cookie->parse($header) } );
6dc87a0f 388 }
389}
fc7ec1d9 390
b5ecfcf0 391=head2 $self->prepare_headers($c)
fc7ec1d9 392
393=cut
394
395sub prepare_headers { }
396
b5ecfcf0 397=head2 $self->prepare_parameters($c)
fc7ec1d9 398
4ab87e27 399sets up parameters from query and post parameters.
400
fc7ec1d9 401=cut
402
fbcc39ad 403sub prepare_parameters {
404 my ( $self, $c ) = @_;
fc7ec1d9 405
7fa2c9c1 406 my $request = $c->request;
407 my $parameters = $request->parameters;
408 my $body_parameters = $request->body_parameters;
409 my $query_parameters = $request->query_parameters;
fbcc39ad 410 # We copy, no references
91772de9 411 foreach my $name (keys %$query_parameters) {
412 my $param = $query_parameters->{$name};
7fa2c9c1 413 $parameters->{$name} = ref $param eq 'ARRAY' ? [ @$param ] : $param;
fbcc39ad 414 }
fc7ec1d9 415
fbcc39ad 416 # Merge query and body parameters
91772de9 417 foreach my $name (keys %$body_parameters) {
418 my $param = $body_parameters->{$name};
7fa2c9c1 419 my @values = ref $param eq 'ARRAY' ? @$param : ($param);
420 if ( my $existing = $parameters->{$name} ) {
421 unshift(@values, (ref $existing eq 'ARRAY' ? @$existing : $existing));
fbcc39ad 422 }
7fa2c9c1 423 $parameters->{$name} = @values > 1 ? \@values : $values[0];
fbcc39ad 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 ) = @_;
ac5c933b 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;
ac5c933b 457
2f381252 458 my @params = grep { length $_ } split /&/, $query_string;
e0616220 459
933ba403 460 for my $item ( @params ) {
ac5c933b 461
462 my ($param, $value)
933ba403 463 = map { $self->unescape_uri($_) }
e5542b70 464 split( /=/, $item, 2 );
ac5c933b 465
933ba403 466 $param = $self->unescape_uri($item) unless defined $param;
ac5c933b 467
933ba403 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);
ac5c933b 495
878b821c 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 ) = @_;
7fa2c9c1 514
515 my $request = $c->request;
0f56bbcf 516 return unless $request->_body;
7fa2c9c1 517
0f56bbcf 518 my $uploads = $request->_body->upload;
7fa2c9c1 519 my $parameters = $request->parameters;
91772de9 520 foreach my $name (keys %$uploads) {
521 my $files = $uploads->{$name};
fbcc39ad 522 my @uploads;
7fa2c9c1 523 for my $upload (ref $files eq 'ARRAY' ? @$files : ($files)) {
524 my $headers = HTTP::Headers->new( %{ $upload->{headers} } );
525 my $u = Catalyst::Request::Upload->new
526 (
527 size => $upload->{size},
528 type => $headers->content_type,
529 headers => $headers,
530 tempname => $upload->{tempname},
531 filename => $upload->{filename},
532 );
fbcc39ad 533 push @uploads, $u;
534 }
7fa2c9c1 535 $request->uploads->{$name} = @uploads > 1 ? \@uploads : $uploads[0];
f4a57de4 536
c4bed79a 537 # support access to the filename as a normal param
538 my @filenames = map { $_->{filename} } @uploads;
a7e05d9d 539 # append, if there's already params with this name
7fa2c9c1 540 if (exists $parameters->{$name}) {
541 if (ref $parameters->{$name} eq 'ARRAY') {
542 push @{ $parameters->{$name} }, @filenames;
a7e05d9d 543 }
544 else {
7fa2c9c1 545 $parameters->{$name} = [ $parameters->{$name}, @filenames ];
a7e05d9d 546 }
547 }
548 else {
7fa2c9c1 549 $parameters->{$name} = @filenames > 1 ? \@filenames : $filenames[0];
a7e05d9d 550 }
fbcc39ad 551 }
552}
553
b5ecfcf0 554=head2 $self->prepare_write($c)
c9afa5fc 555
4ab87e27 556Abstract method. Implemented by the engines.
557
c9afa5fc 558=cut
559
fbcc39ad 560sub prepare_write { }
561
b5ecfcf0 562=head2 $self->read($c, [$maxlength])
fbcc39ad 563
564=cut
565
566sub read {
567 my ( $self, $c, $maxlength ) = @_;
4f5ebacd 568
fbcc39ad 569 my $remaining = $self->read_length - $self->read_position;
4bd82c41 570 $maxlength ||= $CHUNKSIZE;
4f5ebacd 571
fbcc39ad 572 # Are we done reading?
573 if ( $remaining <= 0 ) {
4f5ebacd 574 $self->finalize_read($c);
fbcc39ad 575 return;
576 }
c9afa5fc 577
fbcc39ad 578 my $readlen = ( $remaining > $maxlength ) ? $maxlength : $remaining;
579 my $rc = $self->read_chunk( $c, my $buffer, $readlen );
580 if ( defined $rc ) {
581 $self->read_position( $self->read_position + $rc );
582 return $buffer;
583 }
584 else {
4f5ebacd 585 Catalyst::Exception->throw(
586 message => "Unknown error reading input: $!" );
fbcc39ad 587 }
588}
fc7ec1d9 589
b5ecfcf0 590=head2 $self->read_chunk($c, $buffer, $length)
23f9d934 591
10011c19 592Each engine implements read_chunk as its preferred way of reading a chunk
fbcc39ad 593of data.
fc7ec1d9 594
fbcc39ad 595=cut
61b1e958 596
fbcc39ad 597sub read_chunk { }
61b1e958 598
b5ecfcf0 599=head2 $self->read_length
ca39d576 600
fbcc39ad 601The length of input data to be read. This is obtained from the Content-Length
602header.
fc7ec1d9 603
b5ecfcf0 604=head2 $self->read_position
fc7ec1d9 605
fbcc39ad 606The amount of input data that has already been read.
63b763c5 607
b5ecfcf0 608=head2 $self->run($c)
63b763c5 609
4ab87e27 610Start the engine. Implemented by the various engine classes.
611
fbcc39ad 612=cut
fc7ec1d9 613
fbcc39ad 614sub run { }
fc7ec1d9 615
b5ecfcf0 616=head2 $self->write($c, $buffer)
fc7ec1d9 617
e512dd24 618Writes the buffer to the client.
4ab87e27 619
fc7ec1d9 620=cut
621
fbcc39ad 622sub write {
623 my ( $self, $c, $buffer ) = @_;
4f5ebacd 624
02570318 625 unless ( $self->_prepared_write ) {
4f5ebacd 626 $self->prepare_write($c);
02570318 627 $self->_prepared_write(1);
fc7ec1d9 628 }
ac5c933b 629
094a0974 630 return 0 if !defined $buffer;
631
d04b2ffd 632 my $len = length($buffer);
633 my $wrote = syswrite STDOUT, $buffer;
ac5c933b 634
d04b2ffd 635 if ( !defined $wrote && $! == EWOULDBLOCK ) {
636 # Unable to write on the first try, will retry in the loop below
637 $wrote = 0;
638 }
ac5c933b 639
d04b2ffd 640 if ( defined $wrote && $wrote < $len ) {
641 # We didn't write the whole buffer
642 while (1) {
643 my $ret = syswrite STDOUT, $buffer, $CHUNKSIZE, $wrote;
644 if ( defined $ret ) {
645 $wrote += $ret;
646 }
647 else {
648 next if $! == EWOULDBLOCK;
649 return;
650 }
ac5c933b 651
d04b2ffd 652 last if $wrote >= $len;
e2b0ddd3 653 }
e512dd24 654 }
ac5c933b 655
e512dd24 656 return $wrote;
fc7ec1d9 657}
658
933ba403 659=head2 $self->unescape_uri($uri)
660
6a44fe01 661Unescapes a given URI using the most efficient method available. Engines such
662as Apache may implement this using Apache's C-based modules, for example.
933ba403 663
664=cut
665
666sub unescape_uri {
8c7d83e1 667 my ( $self, $str ) = @_;
7d22a537 668
669 $str =~ s/(?:%([0-9A-Fa-f]{2})|\+)/defined $1 ? chr(hex($1)) : ' '/eg;
670
8c7d83e1 671 return $str;
933ba403 672}
34d28dfd 673
4ab87e27 674=head2 $self->finalize_output
675
676<obsolete>, see finalize_body
677
fbcc39ad 678=head1 AUTHORS
679
2f381252 680Catalyst Contributors, see Catalyst.pm
fc7ec1d9 681
682=head1 COPYRIGHT
683
684This program is free software, you can redistribute it and/or modify it under
685the same terms as Perl itself.
686
687=cut
688
6891;