Don't run the moose controller test if Moose isn't available
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Engine.pm
index 0c8929a..18addd4 100644 (file)
@@ -2,12 +2,14 @@ package Catalyst::Engine;
 
 use strict;
 use base 'Class::Accessor::Fast';
-use CGI::Cookie;
-use Data::Dumper;
+use CGI::Simple::Cookie;
+use Data::Dump qw/dump/;
+use Errno 'EWOULDBLOCK';
 use HTML::Entities;
 use HTTP::Body;
 use HTTP::Headers;
 use URI::QueryParam;
+use Scalar::Util ();
 
 # input position and length
 __PACKAGE__->mk_accessors(qw/read_position read_length/);
@@ -16,7 +18,7 @@ __PACKAGE__->mk_accessors(qw/read_position read_length/);
 use overload '""' => sub { return ref shift }, fallback => 1;
 
 # Amount of data to read from input on each pass
-our $CHUNKSIZE = 4096;
+our $CHUNKSIZE = 64 * 1024;
 
 =head1 NAME
 
@@ -40,9 +42,10 @@ Finalize body.  Prints the response output.
 sub finalize_body {
     my ( $self, $c ) = @_;
     my $body = $c->response->body;
-    if ( ref $body && ($body->can('read') || ref($body) eq 'GLOB') ) {
+    no warnings 'uninitialized';
+    if ( Scalar::Util::blessed($body) && $body->can('read') or ref($body) eq 'GLOB' ) {
         while ( !eof $body ) {
-            read $body, my $buffer, $CHUNKSIZE;
+            read $body, my ($buffer), $CHUNKSIZE;
             last unless $self->write( $c, $buffer );
         }
         close $body;
@@ -54,7 +57,8 @@ sub finalize_body {
 
 =head2 $self->finalize_cookies($c)
 
-Create CGI::Cookies from $c->res->cookies, and set them as response headers.
+Create CGI::Simple::Cookie objects from $c->res->cookies, and set them as
+response headers.
 
 =cut
 
@@ -62,15 +66,22 @@ sub finalize_cookies {
     my ( $self, $c ) = @_;
 
     my @cookies;
-    while ( my ( $name, $cookie ) = each %{ $c->response->cookies } ) {
-
-        my $cookie = CGI::Cookie->new(
-            -name    => $name,
-            -value   => $cookie->{value},
-            -expires => $cookie->{expires},
-            -domain  => $cookie->{domain},
-            -path    => $cookie->{path},
-            -secure  => $cookie->{secure} || 0
+
+    foreach my $name ( keys %{ $c->response->cookies } ) {
+
+        my $val = $c->response->cookies->{$name};
+
+        my $cookie = (
+            Scalar::Util::blessed($val)
+            ? $val
+            : CGI::Simple::Cookie->new(
+                -name    => $name,
+                -value   => $val->{value},
+                -expires => $val->{expires},
+                -domain  => $val->{domain},
+                -path    => $val->{path},
+                -secure  => $val->{secure} || 0
+            )
         );
 
         push @cookies, $cookie->as_string;
@@ -93,13 +104,12 @@ sub finalize_error {
     my ( $self, $c ) = @_;
 
     $c->res->content_type('text/html; charset=utf-8');
-    my $name = $c->config->{name} || 'Catalyst Application';
+    my $name = $c->config->{name} || join(' ', split('::', ref $c));
 
     my ( $title, $error, $infos );
     if ( $c->debug ) {
 
         # For pretty dumps
-        local $Data::Dumper::Terse = 1;
         $error = join '', map {
                 '<p><code class="error">'
               . encode_entities($_)
@@ -120,15 +130,11 @@ sub finalize_error {
         # Don't show response header state in dump
         delete $c->res->{_finalized_headers};
 
-        my $req   = encode_entities Dumper $c->req;
-        my $res   = encode_entities Dumper $c->res;
-        my $stash = encode_entities Dumper $c->stash;
-
         my @infos;
         my $i = 0;
         for my $dump ( $c->dump_these ) {
             my $name  = $dump->[0];
-            my $value = encode_entities( Dumper $dump->[1] );
+            my $value = encode_entities( dump( $dump->[1] ));
             push @infos, sprintf <<"EOF", $name, $value;
 <h2><a href="#" onclick="toggleDump('dump_$i'); return false">%s</a></h2>
 <div id="dump_$i">
@@ -145,11 +151,15 @@ EOF
         $infos = <<"";
 <pre>
 (en) Please come back later
+(fr) SVP veuillez revenir plus tard
 (de) Bitte versuchen sie es spaeter nocheinmal
 (at) Konnten's bitt'schoen spaeter nochmal reinschauen
 (no) Vennligst prov igjen senere
 (dk) Venligst prov igen senere
 (pl) Prosze sprobowac pozniej
+(pt) Por favor volte mais tarde
+(ru) Попробуйте еще раз позже
+(ua) Спробуйте ще раз пізніше
 </pre>
 
         $name = '';
@@ -179,13 +189,13 @@ EOF
         body {
             font-family: "Bitstream Vera Sans", "Trebuchet MS", Verdana,
                          Tahoma, Arial, helvetica, sans-serif;
-            color: #ddd;
+            color: #333;
             background-color: #eee;
             margin: 0px;
             padding: 0px;
         }
         :link, :link:hover, :visited, :visited:hover {
-            color: #ddd;
+            color: #000;
         }
         div.box {
             position: relative;
@@ -193,30 +203,26 @@ EOF
             border: 1px solid #aaa;
             padding: 4px;
             margin: 10px;
-            -moz-border-radius: 10px;
         }
         div.error {
-            background-color: #977;
+            background-color: #cce;
             border: 1px solid #755;
             padding: 8px;
             margin: 4px;
             margin-bottom: 10px;
-            -moz-border-radius: 10px;
         }
         div.infos {
-            background-color: #797;
+            background-color: #eee;
             border: 1px solid #575;
             padding: 8px;
             margin: 4px;
             margin-bottom: 10px;
-            -moz-border-radius: 10px;
         }
         div.name {
-            background-color: #779;
+            background-color: #cce;
             border: 1px solid #557;
             padding: 8px;
             margin: 4px;
-            -moz-border-radius: 10px;
         }
         code.error {
             display: block;
@@ -277,11 +283,7 @@ sub finalize_headers { }
 
 =cut
 
-sub finalize_read {
-    my ( $self, $c ) = @_;
-
-    undef $self->{_prepared_read};
-}
+sub finalize_read { }
 
 =head2 $self->finalize_uploads($c)
 
@@ -311,26 +313,30 @@ sets up the L<Catalyst::Request> object body using L<HTTP::Body>
 sub prepare_body {
     my ( $self, $c ) = @_;
 
-    $self->read_length( $c->request->header('Content-Length') || 0 );
-    my $type = $c->request->header('Content-Type');
-
-    unless ( $c->request->{_body} ) {
-        $c->request->{_body} = HTTP::Body->new( $type, $self->read_length );
-        $c->request->{_body}->{tmpdir} = $c->config->{uploadtmp} if exists $c->config->{uploadtmp};
-    }
-
-    if ( $self->read_length > 0 ) {
+    if ( my $length = $self->read_length ) {
+        unless ( $c->request->{_body} ) {
+            my $type = $c->request->header('Content-Type');
+            $c->request->{_body} = HTTP::Body->new( $type, $length );
+            $c->request->{_body}->tmpdir( $c->config->{uploadtmp} )
+              if exists $c->config->{uploadtmp};
+        }
+        
         while ( my $buffer = $self->read($c) ) {
             $c->prepare_body_chunk($buffer);
         }
 
         # paranoia against wrong Content-Length header
-        my $remaining = $self->read_length - $self->read_position;
-        if ($remaining > 0) {
+        my $remaining = $length - $self->read_position;
+        if ( $remaining > 0 ) {
             $self->finalize_read($c);
-            Catalyst::Exception->throw("Wrong Content-Length value: ". $self->read_length);
+            Catalyst::Exception->throw(
+                "Wrong Content-Length value: $length" );
         }
     }
+    else {
+        # Defined but will cause all body code to be skipped
+        $c->request->{_body} = 0;
+    }
 }
 
 =head2 $self->prepare_body_chunk($c)
@@ -353,6 +359,9 @@ Sets up parameters from body.
 
 sub prepare_body_parameters {
     my ( $self, $c ) = @_;
+    
+    return unless $c->request->{_body};
+    
     $c->request->body_parameters( $c->request->{_body}->param );
 }
 
@@ -366,7 +375,7 @@ sub prepare_connection { }
 
 =head2 $self->prepare_cookies($c)
 
-Parse cookies from header. Sets a L<CGI::Cookie> object.
+Parse cookies from header. Sets a L<CGI::Simple::Cookie> object.
 
 =cut
 
@@ -374,7 +383,7 @@ sub prepare_cookies {
     my ( $self, $c ) = @_;
 
     if ( my $header = $c->request->header('Cookie') ) {
-        $c->req->cookies( { CGI::Cookie->parse($header) } );
+        $c->req->cookies( { CGI::Simple::Cookie->parse($header) } );
     }
 }
 
@@ -394,13 +403,15 @@ sub prepare_parameters {
     my ( $self, $c ) = @_;
 
     # We copy, no references
-    while ( my ( $name, $param ) = each %{ $c->request->query_parameters } ) {
+    foreach my $name ( keys %{ $c->request->query_parameters } ) {
+        my $param = $c->request->query_parameters->{$name};
         $param = ref $param eq 'ARRAY' ? [ @{$param} ] : $param;
         $c->request->parameters->{$name} = $param;
     }
 
     # Merge query and body parameters
-    while ( my ( $name, $param ) = each %{ $c->request->body_parameters } ) {
+    foreach my $name ( keys %{ $c->request->body_parameters } ) {
+        my $param = $c->request->body_parameters->{$name};
         $param = ref $param eq 'ARRAY' ? [ @{$param} ] : $param;
         if ( my $old_param = $c->request->parameters->{$name} ) {
             if ( ref $old_param eq 'ARRAY' ) {
@@ -431,16 +442,43 @@ process the query string and extract query parameters.
 
 sub prepare_query_parameters {
     my ( $self, $c, $query_string ) = @_;
+    
+    # Check for keywords (no = signs)
+    # (yes, index() is faster than a regex :))
+    if ( index( $query_string, '=' ) < 0 ) {
+        $c->request->query_keywords( $self->unescape_uri($query_string) );
+        return;
+    }
+
+    my %query;
 
     # replace semi-colons
     $query_string =~ s/;/&/g;
-
-    my $u = URI->new( '', 'http' );
-    $u->query($query_string);
-    for my $key ( $u->query_param ) {
-        my @vals = $u->query_param($key);
-        $c->request->query_parameters->{$key} = @vals > 1 ? [@vals] : $vals[0];
+    
+    my @params = grep { length $_ } split /&/, $query_string;
+
+    for my $item ( @params ) {
+        
+        my ($param, $value) 
+            = map { $self->unescape_uri($_) }
+              split( /=/, $item, 2 );
+          
+        $param = $self->unescape_uri($item) unless defined $param;
+        
+        if ( exists $query{$param} ) {
+            if ( ref $query{$param} ) {
+                push @{ $query{$param} }, $value;
+            }
+            else {
+                $query{$param} = [ $query{$param}, $value ];
+            }
+        }
+        else {
+            $query{$param} = $value;
+        }
     }
+
+    $c->request->query_parameters( \%query );
 }
 
 =head2 $self->prepare_read($c)
@@ -452,8 +490,11 @@ prepare to read from the engine.
 sub prepare_read {
     my ( $self, $c ) = @_;
 
-    # Reset the read position
+    # Initialize the read position
     $self->read_position(0);
+    
+    # Initialize the amount of data we think we need to read
+    $self->read_length( $c->request->header('Content-Length') || 0 );
 }
 
 =head2 $self->prepare_request(@arguments)
@@ -470,6 +511,9 @@ sub prepare_request { }
 
 sub prepare_uploads {
     my ( $self, $c ) = @_;
+    
+    return unless $c->request->{_body};
+    
     my $uploads = $c->request->{_body}->upload;
     for my $name ( keys %$uploads ) {
         my $files = $uploads->{$name};
@@ -488,8 +532,20 @@ sub prepare_uploads {
 
         # support access to the filename as a normal param
         my @filenames = map { $_->{filename} } @uploads;
-        $c->request->parameters->{$name} =
-          @filenames > 1 ? \@filenames : $filenames[0];
+        # append, if there's already params with this name
+        if (exists $c->request->parameters->{$name}) {
+            if (ref $c->request->parameters->{$name} eq 'ARRAY') {
+                push @{ $c->request->parameters->{$name} }, @filenames;
+            }
+            else {
+                $c->request->parameters->{$name} = 
+                    [ $c->request->parameters->{$name}, @filenames ];
+            }
+        }
+        else {
+            $c->request->parameters->{$name} =
+                @filenames > 1 ? \@filenames : $filenames[0];
+        }
     }
 }
 
@@ -508,11 +564,6 @@ sub prepare_write { }
 sub read {
     my ( $self, $c, $maxlength ) = @_;
 
-    unless ( $self->{_prepared_read} ) {
-        $self->prepare_read($c);
-        $self->{_prepared_read} = 1;
-    }
-
     my $remaining = $self->read_length - $self->read_position;
     $maxlength ||= $CHUNKSIZE;
 
@@ -562,7 +613,7 @@ sub run { }
 
 =head2 $self->write($c, $buffer)
 
-Writes the buffer to the client. Can only be called once for a request.
+Writes the buffer to the client.
 
 =cut
 
@@ -573,8 +624,47 @@ sub write {
         $self->prepare_write($c);
         $self->{_prepared_write} = 1;
     }
+    
+    my $len   = length($buffer);
+    my $wrote = syswrite STDOUT, $buffer;
+    
+    if ( !defined $wrote && $! == EWOULDBLOCK ) {
+        # Unable to write on the first try, will retry in the loop below
+        $wrote = 0;
+    }
+    
+    if ( defined $wrote && $wrote < $len ) {
+        # We didn't write the whole buffer
+        while (1) {
+            my $ret = syswrite STDOUT, $buffer, $CHUNKSIZE, $wrote;
+            if ( defined $ret ) {
+                $wrote += $ret;
+            }
+            else {
+                next if $! == EWOULDBLOCK;
+                return;
+            }
+            
+            last if $wrote >= $len;
+        }
+    }
+    
+    return $wrote;
+}
+
+=head2 $self->unescape_uri($uri)
+
+Unescapes a given URI using the most efficient method available.  Engines such
+as Apache may implement this using Apache's C-based modules, for example.
 
-    print STDOUT $buffer;
+=cut
+
+sub unescape_uri {
+    my ( $self, $str ) = @_;
+
+    $str =~ s/(?:%([0-9A-Fa-f]{2})|\+)/defined $1 ? chr(hex($1)) : ' '/eg;
+
+    return $str;
 }
 
 =head2 $self->finalize_output
@@ -583,9 +673,7 @@ sub write {
 
 =head1 AUTHORS
 
-Sebastian Riedel, <sri@cpan.org>
-
-Andy Grundman, <andy@hybridized.org>
+Catalyst Contributors, see Catalyst.pm
 
 =head1 COPYRIGHT