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