Start making the path handling like it is in trunk. Merge extra tests, add using...
[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;
44d28c7d 13use Moose::Util::TypeConstraints;
a1791811 14use Plack::Loader;
c2f4a965 15use Plack::Middleware::Conditional;
16use Plack::Middleware::ReverseProxy;
fbcc39ad 17
d495753a 18use namespace::clean -except => 'meta';
19
44d28c7d 20has env => (is => 'ro', writer => '_set_env', clearer => '_clear_env');
a50e5b46 21
fbcc39ad 22# input position and length
7fa2c9c1 23has read_length => (is => 'rw');
24has read_position => (is => 'rw');
fbcc39ad 25
02570318 26has _prepared_write => (is => 'rw');
27
44d28c7d 28has _response_cb => (
eebe046f 29 is => 'ro',
30 isa => 'CodeRef',
31 writer => '_set_response_cb',
32 clearer => '_clear_response_cb',
44d28c7d 33);
34
35has _writer => (
eebe046f 36 is => 'ro',
37 isa => duck_type([qw(write close)]),
38 writer => '_set_writer',
39 clearer => '_clear_writer',
44d28c7d 40);
41
4bd82c41 42# Amount of data to read from input on each pass
4bb8bd62 43our $CHUNKSIZE = 64 * 1024;
4bd82c41 44
fc7ec1d9 45=head1 NAME
46
47Catalyst::Engine - The Catalyst Engine
48
49=head1 SYNOPSIS
50
51See L<Catalyst>.
52
53=head1 DESCRIPTION
54
23f9d934 55=head1 METHODS
fc7ec1d9 56
cd3bb248 57
b5ecfcf0 58=head2 $self->finalize_body($c)
06e1b616 59
fbcc39ad 60Finalize body. Prints the response output.
06e1b616 61
62=cut
63
fbcc39ad 64sub finalize_body {
65 my ( $self, $c ) = @_;
7257e9db 66 my $body = $c->response->body;
f9b6d612 67 no warnings 'uninitialized';
7e95ba12 68 if ( blessed($body) && $body->can('read') or ref($body) eq 'GLOB' ) {
be1c9503 69 my $got;
70 do {
1235b30f 71 $got = read $body, my ($buffer), $CHUNKSIZE;
3a64ecc9 72 $got = 0 unless $self->write( $c, $buffer );
be1c9503 73 } while $got > 0;
74
7257e9db 75 close $body;
f4a57de4 76 }
77 else {
7257e9db 78 $self->write( $c, $body );
f4a57de4 79 }
ca3023fc 80
81 $self->_writer->close;
eebe046f 82 $self->_clear_writer;
030674d0 83 $self->_clear_env;
84
ca3023fc 85 return;
fbcc39ad 86}
6dc87a0f 87
b5ecfcf0 88=head2 $self->finalize_cookies($c)
6dc87a0f 89
fa32ac82 90Create CGI::Simple::Cookie objects from $c->res->cookies, and set them as
91response headers.
4ab87e27 92
6dc87a0f 93=cut
94
95sub finalize_cookies {
fbcc39ad 96 my ( $self, $c ) = @_;
6dc87a0f 97
fbcc39ad 98 my @cookies;
7fa2c9c1 99 my $response = $c->response;
c82ed742 100
91772de9 101 foreach my $name (keys %{ $response->cookies }) {
102
103 my $val = $response->cookies->{$name};
fbcc39ad 104
2832cb5d 105 my $cookie = (
7e95ba12 106 blessed($val)
2832cb5d 107 ? $val
108 : CGI::Simple::Cookie->new(
109 -name => $name,
110 -value => $val->{value},
111 -expires => $val->{expires},
112 -domain => $val->{domain},
113 -path => $val->{path},
b21bc468 114 -secure => $val->{secure} || 0,
115 -httponly => $val->{httponly} || 0,
2832cb5d 116 )
6dc87a0f 117 );
118
fbcc39ad 119 push @cookies, $cookie->as_string;
6dc87a0f 120 }
6dc87a0f 121
b39840da 122 for my $cookie (@cookies) {
7fa2c9c1 123 $response->headers->push_header( 'Set-Cookie' => $cookie );
fbcc39ad 124 }
125}
969647fd 126
b5ecfcf0 127=head2 $self->finalize_error($c)
969647fd 128
6e5b548e 129Output an appropriate error message. Called if there's an error in $c
4ab87e27 130after the dispatch has finished. Will output debug messages if Catalyst
131is in debug mode, or a `please come back later` message otherwise.
132
969647fd 133=cut
134
c96cdcef 135sub _dump_error_page_element {
136 my ($self, $i, $element) = @_;
137 my ($name, $val) = @{ $element };
138
139 # This is fugly, but the metaclass is _HUGE_ and demands waaay too much
140 # scrolling. Suggestions for more pleasant ways to do this welcome.
141 local $val->{'__MOP__'} = "Stringified: "
1565e158 142 . $val->{'__MOP__'} if ref $val eq 'HASH' && exists $val->{'__MOP__'};
c96cdcef 143
144 my $text = encode_entities( dump( $val ));
145 sprintf <<"EOF", $name, $text;
146<h2><a href="#" onclick="toggleDump('dump_$i'); return false">%s</a></h2>
147<div id="dump_$i">
148 <pre wrap="">%s</pre>
149</div>
150EOF
151}
152
969647fd 153sub finalize_error {
fbcc39ad 154 my ( $self, $c ) = @_;
969647fd 155
7299a7b4 156 $c->res->content_type('text/html; charset=utf-8');
df960201 157 my $name = ref($c)->config->{name} || join(' ', split('::', ref $c));
969647fd 158
159 my ( $title, $error, $infos );
160 if ( $c->debug ) {
62d9b030 161
162 # For pretty dumps
b5ecfcf0 163 $error = join '', map {
164 '<p><code class="error">'
165 . encode_entities($_)
166 . '</code></p>'
167 } @{ $c->error };
969647fd 168 $error ||= 'No output';
2666dd3b 169 $error = qq{<pre wrap="">$error</pre>};
969647fd 170 $title = $name = "$name on Catalyst $Catalyst::VERSION";
d82cc9ae 171 $name = "<h1>$name</h1>";
fbcc39ad 172
173 # Don't show context in the dump
02570318 174 $c->req->_clear_context;
175 $c->res->_clear_context;
fbcc39ad 176
177 # Don't show body parser in the dump
0f56bbcf 178 $c->req->_clear_body;
fbcc39ad 179
c6ef5e69 180 my @infos;
181 my $i = 0;
c6ef5e69 182 for my $dump ( $c->dump_these ) {
c96cdcef 183 push @infos, $self->_dump_error_page_element($i, $dump);
c6ef5e69 184 $i++;
185 }
186 $infos = join "\n", @infos;
969647fd 187 }
188 else {
189 $title = $name;
190 $error = '';
191 $infos = <<"";
192<pre>
193(en) Please come back later
0c2b4ac0 194(fr) SVP veuillez revenir plus tard
969647fd 195(de) Bitte versuchen sie es spaeter nocheinmal
d82cc9ae 196(at) Konnten's bitt'schoen spaeter nochmal reinschauen
969647fd 197(no) Vennligst prov igjen senere
d82cc9ae 198(dk) Venligst prov igen senere
199(pl) Prosze sprobowac pozniej
2f381252 200(pt) Por favor volte mais tarde
b31c0f2e 201(ru) Попробуйте еще раз позже
202(ua) Спробуйте ще раз пізніше
969647fd 203</pre>
204
205 $name = '';
206 }
e060fe05 207 $c->res->body( <<"" );
7299a7b4 208<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
209 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
210<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
969647fd 211<head>
7299a7b4 212 <meta http-equiv="Content-Language" content="en" />
213 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
969647fd 214 <title>$title</title>
7299a7b4 215 <script type="text/javascript">
c6ef5e69 216 <!--
217 function toggleDump (dumpElement) {
7299a7b4 218 var e = document.getElementById( dumpElement );
219 if (e.style.display == "none") {
220 e.style.display = "";
c6ef5e69 221 }
222 else {
7299a7b4 223 e.style.display = "none";
c6ef5e69 224 }
225 }
226 -->
227 </script>
969647fd 228 <style type="text/css">
229 body {
230 font-family: "Bitstream Vera Sans", "Trebuchet MS", Verdana,
231 Tahoma, Arial, helvetica, sans-serif;
34d28dfd 232 color: #333;
969647fd 233 background-color: #eee;
234 margin: 0px;
235 padding: 0px;
236 }
c6ef5e69 237 :link, :link:hover, :visited, :visited:hover {
34d28dfd 238 color: #000;
c6ef5e69 239 }
969647fd 240 div.box {
9619f23c 241 position: relative;
969647fd 242 background-color: #ccc;
243 border: 1px solid #aaa;
244 padding: 4px;
245 margin: 10px;
969647fd 246 }
247 div.error {
34d28dfd 248 background-color: #cce;
969647fd 249 border: 1px solid #755;
250 padding: 8px;
251 margin: 4px;
252 margin-bottom: 10px;
969647fd 253 }
254 div.infos {
34d28dfd 255 background-color: #eee;
969647fd 256 border: 1px solid #575;
257 padding: 8px;
258 margin: 4px;
259 margin-bottom: 10px;
969647fd 260 }
261 div.name {
34d28dfd 262 background-color: #cce;
969647fd 263 border: 1px solid #557;
264 padding: 8px;
265 margin: 4px;
969647fd 266 }
7f8e0078 267 code.error {
268 display: block;
269 margin: 1em 0;
270 overflow: auto;
7f8e0078 271 }
9619f23c 272 div.name h1, div.error p {
273 margin: 0;
274 }
275 h2 {
276 margin-top: 0;
277 margin-bottom: 10px;
278 font-size: medium;
279 font-weight: bold;
280 text-decoration: underline;
281 }
282 h1 {
283 font-size: medium;
284 font-weight: normal;
285 }
2666dd3b 286 /* from http://users.tkk.fi/~tkarvine/linux/doc/pre-wrap/pre-wrap-css3-mozilla-opera-ie.html */
287 /* Browser specific (not valid) styles to make preformatted text wrap */
b0ad47c1 288 pre {
2666dd3b 289 white-space: pre-wrap; /* css-3 */
290 white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
291 white-space: -pre-wrap; /* Opera 4-6 */
292 white-space: -o-pre-wrap; /* Opera 7 */
293 word-wrap: break-word; /* Internet Explorer 5.5+ */
294 }
969647fd 295 </style>
296</head>
297<body>
298 <div class="box">
299 <div class="error">$error</div>
300 <div class="infos">$infos</div>
301 <div class="name">$name</div>
302 </div>
303</body>
304</html>
305
d82cc9ae 306
4b66aa19 307 # Trick IE. Old versions of IE would display their own error page instead
308 # of ours if we'd give it less than 512 bytes.
d82cc9ae 309 $c->res->{body} .= ( ' ' x 512 );
310
311 # Return 500
33117422 312 $c->res->status(500);
969647fd 313}
314
b5ecfcf0 315=head2 $self->finalize_headers($c)
fc7ec1d9 316
4ab87e27 317Abstract method, allows engines to write headers to response
318
fc7ec1d9 319=cut
320
44d28c7d 321sub finalize_headers {
322 my ($self, $ctx) = @_;
323
324 my @headers;
325 $ctx->response->headers->scan(sub { push @headers, @_ });
326
327 $self->_set_writer($self->_response_cb->([ $ctx->response->status, \@headers ]));
eebe046f 328 $self->_clear_response_cb;
44d28c7d 329
330 return;
331}
fc7ec1d9 332
b5ecfcf0 333=head2 $self->finalize_read($c)
fc7ec1d9 334
335=cut
336
878b821c 337sub finalize_read { }
fc7ec1d9 338
b5ecfcf0 339=head2 $self->finalize_uploads($c)
fc7ec1d9 340
4ab87e27 341Clean up after uploads, deleting temp files.
342
fc7ec1d9 343=cut
344
fbcc39ad 345sub finalize_uploads {
346 my ( $self, $c ) = @_;
99fe1710 347
671123ba 348 # N.B. This code is theoretically entirely unneeded due to ->cleanup(1)
349 # on the HTTP::Body object.
7fa2c9c1 350 my $request = $c->request;
91772de9 351 foreach my $key (keys %{ $request->uploads }) {
352 my $upload = $request->uploads->{$key};
7fa2c9c1 353 unlink grep { -e $_ } map { $_->tempname }
354 (ref $upload eq 'ARRAY' ? @{$upload} : ($upload));
c85ff642 355 }
7fa2c9c1 356
fc7ec1d9 357}
358
b5ecfcf0 359=head2 $self->prepare_body($c)
fc7ec1d9 360
4ab87e27 361sets up the L<Catalyst::Request> object body using L<HTTP::Body>
362
fc7ec1d9 363=cut
364
fbcc39ad 365sub prepare_body {
366 my ( $self, $c ) = @_;
99fe1710 367
df960201 368 my $appclass = ref($c) || $c;
878b821c 369 if ( my $length = $self->read_length ) {
7fa2c9c1 370 my $request = $c->request;
0f56bbcf 371 unless ( $request->_body ) {
7fa2c9c1 372 my $type = $request->header('Content-Type');
0f56bbcf 373 $request->_body(HTTP::Body->new( $type, $length ));
671123ba 374 $request->_body->cleanup(1); # Make extra sure!
df960201 375 $request->_body->tmpdir( $appclass->config->{uploadtmp} )
376 if exists $appclass->config->{uploadtmp};
847e3257 377 }
b0ad47c1 378
ea72fece 379 # Check for definedness as you could read '0'
380 while ( defined ( my $buffer = $self->read($c) ) ) {
4f5ebacd 381 $c->prepare_body_chunk($buffer);
fbcc39ad 382 }
fdb3773e 383
384 # paranoia against wrong Content-Length header
847e3257 385 my $remaining = $length - $self->read_position;
34d28dfd 386 if ( $remaining > 0 ) {
fdb3773e 387 $self->finalize_read($c);
34d28dfd 388 Catalyst::Exception->throw(
847e3257 389 "Wrong Content-Length value: $length" );
fdb3773e 390 }
fc7ec1d9 391 }
847e3257 392 else {
393 # Defined but will cause all body code to be skipped
0f56bbcf 394 $c->request->_body(0);
847e3257 395 }
fc7ec1d9 396}
397
b5ecfcf0 398=head2 $self->prepare_body_chunk($c)
4bd82c41 399
4ab87e27 400Add a chunk to the request body.
401
4bd82c41 402=cut
403
404sub prepare_body_chunk {
405 my ( $self, $c, $chunk ) = @_;
4f5ebacd 406
0f56bbcf 407 $c->request->_body->add($chunk);
4bd82c41 408}
409
b5ecfcf0 410=head2 $self->prepare_body_parameters($c)
06e1b616 411
b0ad47c1 412Sets up parameters from body.
4ab87e27 413
06e1b616 414=cut
415
fbcc39ad 416sub prepare_body_parameters {
417 my ( $self, $c ) = @_;
b0ad47c1 418
0f56bbcf 419 return unless $c->request->_body;
b0ad47c1 420
0f56bbcf 421 $c->request->body_parameters( $c->request->_body->param );
fbcc39ad 422}
0556eb49 423
b5ecfcf0 424=head2 $self->prepare_connection($c)
0556eb49 425
4ab87e27 426Abstract method implemented in engines.
427
0556eb49 428=cut
429
44d28c7d 430sub prepare_connection {
431 my ($self, $ctx) = @_;
432
433 my $env = $self->env;
434 my $request = $ctx->request;
435
436 $request->address( $env->{REMOTE_ADDR} );
437 $request->hostname( $env->{REMOTE_HOST} )
438 if exists $env->{REMOTE_HOST};
439 $request->protocol( $env->{SERVER_PROTOCOL} );
440 $request->remote_user( $env->{REMOTE_USER} );
441 $request->method( $env->{REQUEST_METHOD} );
c9de76f0 442 $request->secure( $env->{'psgi.url_scheme'} eq 'https' ? 1 : 0 );
44d28c7d 443
444 return;
445}
0556eb49 446
b5ecfcf0 447=head2 $self->prepare_cookies($c)
fc7ec1d9 448
fa32ac82 449Parse cookies from header. Sets a L<CGI::Simple::Cookie> object.
4ab87e27 450
fc7ec1d9 451=cut
452
6dc87a0f 453sub prepare_cookies {
fbcc39ad 454 my ( $self, $c ) = @_;
6dc87a0f 455
456 if ( my $header = $c->request->header('Cookie') ) {
fa32ac82 457 $c->req->cookies( { CGI::Simple::Cookie->parse($header) } );
6dc87a0f 458 }
459}
fc7ec1d9 460
b5ecfcf0 461=head2 $self->prepare_headers($c)
fc7ec1d9 462
463=cut
464
44d28c7d 465sub prepare_headers {
466 my ($self, $ctx) = @_;
467
468 my $env = $self->env;
469 my $headers = $ctx->request->headers;
470
471 for my $header (keys %{ $env }) {
472 next unless $header =~ /^(HTTP|CONTENT|COOKIE)/i;
473 (my $field = $header) =~ s/^HTTPS?_//;
474 $field =~ tr/_/-/;
475 $headers->header($field => $env->{$header});
476 }
477}
fc7ec1d9 478
b5ecfcf0 479=head2 $self->prepare_parameters($c)
fc7ec1d9 480
4ab87e27 481sets up parameters from query and post parameters.
482
fc7ec1d9 483=cut
484
fbcc39ad 485sub prepare_parameters {
486 my ( $self, $c ) = @_;
fc7ec1d9 487
7fa2c9c1 488 my $request = $c->request;
489 my $parameters = $request->parameters;
490 my $body_parameters = $request->body_parameters;
491 my $query_parameters = $request->query_parameters;
fbcc39ad 492 # We copy, no references
91772de9 493 foreach my $name (keys %$query_parameters) {
494 my $param = $query_parameters->{$name};
7fa2c9c1 495 $parameters->{$name} = ref $param eq 'ARRAY' ? [ @$param ] : $param;
fbcc39ad 496 }
fc7ec1d9 497
fbcc39ad 498 # Merge query and body parameters
91772de9 499 foreach my $name (keys %$body_parameters) {
500 my $param = $body_parameters->{$name};
7fa2c9c1 501 my @values = ref $param eq 'ARRAY' ? @$param : ($param);
502 if ( my $existing = $parameters->{$name} ) {
503 unshift(@values, (ref $existing eq 'ARRAY' ? @$existing : $existing));
fbcc39ad 504 }
7fa2c9c1 505 $parameters->{$name} = @values > 1 ? \@values : $values[0];
fbcc39ad 506 }
507}
508
b5ecfcf0 509=head2 $self->prepare_path($c)
fc7ec1d9 510
4ab87e27 511abstract method, implemented by engines.
512
fc7ec1d9 513=cut
514
44d28c7d 515sub prepare_path {
516 my ($self, $ctx) = @_;
517
518 my $env = $self->env;
519
520 my $scheme = $ctx->request->secure ? 'https' : 'http';
521 my $host = $env->{HTTP_HOST} || $env->{SERVER_NAME};
522 my $port = $env->{SERVER_PORT} || 80;
523 my $base_path = $env->{SCRIPT_NAME} || "/";
524
525 # set the request URI
661de072 526 my $path;
527 if (!$ctx->config->{use_request_uri_for_path}) {
528 $path = $base_path . $env->{PATH_INFO};
529 $path =~ s{^/+}{};
530 $path =~ s/([^$URI::uric])/$URI::Escape::escapes{$1}/go;
531 $path =~ s/\?/%3F/g; # STUPID STUPID SPECIAL CASE
532 }
533 else {
534 my $req_uri = $env->{REQUEST_URI};
535 $req_uri =~ s/\?.*$//;
536 $path = $req_uri;
537 $path =~ s{^/+}{};
538 }
44d28c7d 539
540 # Using URI directly is way too slow, so we construct the URLs manually
541 my $uri_class = "URI::$scheme";
542
543 # HTTP_HOST will include the port even if it's 80/443
544 $host =~ s/:(?:80|443)$//;
545
546 if ($port !~ /^(?:80|443)$/ && $host !~ /:/) {
547 $host .= ":$port";
548 }
549
44d28c7d 550 my $query = $env->{QUERY_STRING} ? '?' . $env->{QUERY_STRING} : '';
551 my $uri = $scheme . '://' . $host . '/' . $path . $query;
552
4ee03d72 553 $ctx->request->uri( (bless \$uri, $uri_class)->canonical );
44d28c7d 554
555 # set the base URI
556 # base must end in a slash
557 $base_path .= '/' unless $base_path =~ m{/$};
558
559 my $base_uri = $scheme . '://' . $host . $base_path;
560
561 $ctx->request->base( bless \$base_uri, $uri_class );
562
563 return;
564}
fc7ec1d9 565
b5ecfcf0 566=head2 $self->prepare_request($c)
fc7ec1d9 567
b5ecfcf0 568=head2 $self->prepare_query_parameters($c)
fc7ec1d9 569
4ab87e27 570process the query string and extract query parameters.
571
fc7ec1d9 572=cut
573
e0616220 574sub prepare_query_parameters {
44d28c7d 575 my ($self, $c) = @_;
576
577 my $query_string = exists $self->env->{QUERY_STRING}
578 ? $self->env->{QUERY_STRING}
579 : '';
b0ad47c1 580
3b4d1251 581 # Check for keywords (no = signs)
582 # (yes, index() is faster than a regex :))
933ba403 583 if ( index( $query_string, '=' ) < 0 ) {
3b4d1251 584 $c->request->query_keywords( $self->unescape_uri($query_string) );
933ba403 585 return;
586 }
587
588 my %query;
e0616220 589
590 # replace semi-colons
591 $query_string =~ s/;/&/g;
b0ad47c1 592
2f381252 593 my @params = grep { length $_ } split /&/, $query_string;
e0616220 594
933ba403 595 for my $item ( @params ) {
b0ad47c1 596
597 my ($param, $value)
933ba403 598 = map { $self->unescape_uri($_) }
e5542b70 599 split( /=/, $item, 2 );
b0ad47c1 600
933ba403 601 $param = $self->unescape_uri($item) unless defined $param;
b0ad47c1 602
933ba403 603 if ( exists $query{$param} ) {
604 if ( ref $query{$param} ) {
605 push @{ $query{$param} }, $value;
606 }
607 else {
608 $query{$param} = [ $query{$param}, $value ];
609 }
610 }
611 else {
612 $query{$param} = $value;
613 }
e0616220 614 }
933ba403 615
616 $c->request->query_parameters( \%query );
e0616220 617}
fbcc39ad 618
b5ecfcf0 619=head2 $self->prepare_read($c)
fbcc39ad 620
4ab87e27 621prepare to read from the engine.
622
fbcc39ad 623=cut
fc7ec1d9 624
fbcc39ad 625sub prepare_read {
626 my ( $self, $c ) = @_;
4f5ebacd 627
878b821c 628 # Initialize the read position
4f5ebacd 629 $self->read_position(0);
b0ad47c1 630
878b821c 631 # Initialize the amount of data we think we need to read
632 $self->read_length( $c->request->header('Content-Length') || 0 );
fbcc39ad 633}
fc7ec1d9 634
b5ecfcf0 635=head2 $self->prepare_request(@arguments)
fc7ec1d9 636
4ab87e27 637Populate the context object from the request object.
638
fc7ec1d9 639=cut
640
44d28c7d 641sub prepare_request {
642 my ($self, $ctx, %args) = @_;
643 $self->_set_env($args{env});
644}
fc7ec1d9 645
b5ecfcf0 646=head2 $self->prepare_uploads($c)
c9afa5fc 647
fbcc39ad 648=cut
649
650sub prepare_uploads {
651 my ( $self, $c ) = @_;
7fa2c9c1 652
653 my $request = $c->request;
0f56bbcf 654 return unless $request->_body;
7fa2c9c1 655
0f56bbcf 656 my $uploads = $request->_body->upload;
7fa2c9c1 657 my $parameters = $request->parameters;
91772de9 658 foreach my $name (keys %$uploads) {
659 my $files = $uploads->{$name};
fbcc39ad 660 my @uploads;
7fa2c9c1 661 for my $upload (ref $files eq 'ARRAY' ? @$files : ($files)) {
662 my $headers = HTTP::Headers->new( %{ $upload->{headers} } );
663 my $u = Catalyst::Request::Upload->new
664 (
665 size => $upload->{size},
a160c98d 666 type => scalar $headers->content_type,
7fa2c9c1 667 headers => $headers,
668 tempname => $upload->{tempname},
669 filename => $upload->{filename},
670 );
fbcc39ad 671 push @uploads, $u;
672 }
7fa2c9c1 673 $request->uploads->{$name} = @uploads > 1 ? \@uploads : $uploads[0];
f4a57de4 674
c4bed79a 675 # support access to the filename as a normal param
676 my @filenames = map { $_->{filename} } @uploads;
a7e05d9d 677 # append, if there's already params with this name
7fa2c9c1 678 if (exists $parameters->{$name}) {
679 if (ref $parameters->{$name} eq 'ARRAY') {
680 push @{ $parameters->{$name} }, @filenames;
a7e05d9d 681 }
682 else {
7fa2c9c1 683 $parameters->{$name} = [ $parameters->{$name}, @filenames ];
a7e05d9d 684 }
685 }
686 else {
7fa2c9c1 687 $parameters->{$name} = @filenames > 1 ? \@filenames : $filenames[0];
a7e05d9d 688 }
fbcc39ad 689 }
690}
691
b5ecfcf0 692=head2 $self->prepare_write($c)
c9afa5fc 693
4ab87e27 694Abstract method. Implemented by the engines.
695
c9afa5fc 696=cut
697
fbcc39ad 698sub prepare_write { }
699
b5ecfcf0 700=head2 $self->read($c, [$maxlength])
fbcc39ad 701
ea72fece 702Reads from the input stream by calling C<< $self->read_chunk >>.
703
704Maintains the read_length and read_position counters as data is read.
705
fbcc39ad 706=cut
707
708sub read {
709 my ( $self, $c, $maxlength ) = @_;
4f5ebacd 710
fbcc39ad 711 my $remaining = $self->read_length - $self->read_position;
4bd82c41 712 $maxlength ||= $CHUNKSIZE;
4f5ebacd 713
fbcc39ad 714 # Are we done reading?
715 if ( $remaining <= 0 ) {
4f5ebacd 716 $self->finalize_read($c);
fbcc39ad 717 return;
718 }
c9afa5fc 719
fbcc39ad 720 my $readlen = ( $remaining > $maxlength ) ? $maxlength : $remaining;
721 my $rc = $self->read_chunk( $c, my $buffer, $readlen );
722 if ( defined $rc ) {
ea72fece 723 if (0 == $rc) { # Nothing more to read even though Content-Length
9e1f645b 724 # said there should be.
ea72fece 725 $self->finalize_read;
726 return;
727 }
fbcc39ad 728 $self->read_position( $self->read_position + $rc );
729 return $buffer;
730 }
731 else {
4f5ebacd 732 Catalyst::Exception->throw(
733 message => "Unknown error reading input: $!" );
fbcc39ad 734 }
735}
fc7ec1d9 736
b5ecfcf0 737=head2 $self->read_chunk($c, $buffer, $length)
23f9d934 738
10011c19 739Each engine implements read_chunk as its preferred way of reading a chunk
ea72fece 740of data. Returns the number of bytes read. A return of 0 indicates that
741there is no more data to be read.
fc7ec1d9 742
fbcc39ad 743=cut
61b1e958 744
e6b46d80 745sub read_chunk {
ce7abbda 746 my ($self, $ctx) = (shift, shift);
e6b46d80 747 return $self->env->{'psgi.input'}->read(@_);
748}
61b1e958 749
b5ecfcf0 750=head2 $self->read_length
ca39d576 751
fbcc39ad 752The length of input data to be read. This is obtained from the Content-Length
753header.
fc7ec1d9 754
b5ecfcf0 755=head2 $self->read_position
fc7ec1d9 756
fbcc39ad 757The amount of input data that has already been read.
63b763c5 758
9560b708 759=head2 $self->run($app, $server)
63b763c5 760
9560b708 761Start the engine. Builds a PSGI application and calls the
762run method on the server passed in..
4ab87e27 763
fbcc39ad 764=cut
fc7ec1d9 765
44d28c7d 766sub run {
4b0f97fc 767 my ($self, $app, @args) = @_;
768 my $server = pop @args if blessed $args[-1];
9560b708 769 $server ||= Plack::Loader->auto(); # We're not being called from a script,
29bb04ad 770 # so auto detect what backend to run on.
771 # This does *NOT* cover mod_perl.
a1791811 772 # FIXME - Do something sensible with the options we're passed
4b0f97fc 773 my $psgi = $self->build_psgi_app($app, @args);
774 $server->run($psgi);
a1791811 775}
44d28c7d 776
9560b708 777=head2 build_psgi_app ($app, @args)
778
779Builds and returns a PSGI application closure, wrapping it in the reverse proxy
780middleware if the using_frontend_proxy config setting is set.
781
782=cut
783
22a5833d 784sub build_psgi_app {
a1791811 785 my ($self, $app, @args) = @_;
c2f4a965 786
787 my $psgi_app = sub {
44d28c7d 788 my ($env) = @_;
789
790 return sub {
791 my ($respond) = @_;
792 $self->_set_response_cb($respond);
793 $app->handle_request(env => $env);
794 };
795 };
c2f4a965 796
797 $psgi_app = Plack::Middleware::Conditional->wrap(
798 $psgi_app,
799 condition => sub {
800 my ($env) = @_;
801 return if $app->config->{ignore_frontend_proxy};
802 return $env->{REMOTE_ADDR} eq '127.0.0.1' || $app->config->{using_frontend_proxy};
803 },
804 builder => sub { Plack::Middleware::ReverseProxy->wrap($_[0]) },
805 );
806
807 return $psgi_app;
44d28c7d 808}
fc7ec1d9 809
b5ecfcf0 810=head2 $self->write($c, $buffer)
fc7ec1d9 811
e512dd24 812Writes the buffer to the client.
4ab87e27 813
fc7ec1d9 814=cut
815
fbcc39ad 816sub write {
817 my ( $self, $c, $buffer ) = @_;
4f5ebacd 818
02570318 819 unless ( $self->_prepared_write ) {
4f5ebacd 820 $self->prepare_write($c);
02570318 821 $self->_prepared_write(1);
fc7ec1d9 822 }
b0ad47c1 823
094a0974 824 return 0 if !defined $buffer;
b0ad47c1 825
44d28c7d 826 my $len = length($buffer);
827 $self->_writer->write($buffer);
b0ad47c1 828
44d28c7d 829 return $len;
fc7ec1d9 830}
831
933ba403 832=head2 $self->unescape_uri($uri)
833
6a44fe01 834Unescapes a given URI using the most efficient method available. Engines such
835as Apache may implement this using Apache's C-based modules, for example.
933ba403 836
837=cut
838
839sub unescape_uri {
8c7d83e1 840 my ( $self, $str ) = @_;
7d22a537 841
842 $str =~ s/(?:%([0-9A-Fa-f]{2})|\+)/defined $1 ? chr(hex($1)) : ' '/eg;
843
8c7d83e1 844 return $str;
933ba403 845}
34d28dfd 846
4ab87e27 847=head2 $self->finalize_output
848
849<obsolete>, see finalize_body
850
0c76ec45 851=head2 $self->env
852
6356febf 853Hash containing environment variables including many special variables inserted
0c76ec45 854by WWW server - like SERVER_*, REMOTE_*, HTTP_* ...
855
6356febf 856Before accessing environment variables consider whether the same information is
0c76ec45 857not directly available via Catalyst objects $c->request, $c->engine ...
858
6356febf 859BEWARE: If you really need to access some environment variable from your Catalyst
0c76ec45 860application you should use $c->engine->env->{VARNAME} instead of $ENV{VARNAME},
861as in some enviroments the %ENV hash does not contain what you would expect.
862
fbcc39ad 863=head1 AUTHORS
864
2f381252 865Catalyst Contributors, see Catalyst.pm
fc7ec1d9 866
867=head1 COPYRIGHT
868
536bee89 869This library is free software. You can redistribute it and/or modify it under
fc7ec1d9 870the same terms as Perl itself.
871
872=cut
873
8741;