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