Fixed run-on sentence in COPYRIGHT and s/program/library/
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Engine.pm
index 423cf41..d66bb5d 100644 (file)
@@ -1,22 +1,28 @@
 package Catalyst::Engine;
 
-use strict;
-use base 'Class::Accessor::Fast';
-use CGI::Cookie;
-use Data::Dumper;
+use Moose;
+with 'MooseX::Emulate::Class::Accessor::Fast';
+
+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 namespace::clean -except => 'meta';
+
+has env => (is => 'rw');
+
 # input position and length
-__PACKAGE__->mk_accessors(qw/read_position read_length/);
+has read_length => (is => 'rw');
+has read_position => (is => 'rw');
 
-# Stringify to class
-use overload '""' => sub { return ref shift }, fallback => 1;
+has _prepared_write => (is => 'rw');
 
 # Amount of data to read from input on each pass
-our $CHUNKSIZE = 4096;
+our $CHUNKSIZE = 64 * 1024;
 
 =head1 NAME
 
@@ -30,9 +36,6 @@ See L<Catalyst>.
 
 =head1 METHODS
 
-=head2 $self->finalize_output
-
-<obsolete>, see finalize_body
 
 =head2 $self->finalize_body($c)
 
@@ -42,93 +45,103 @@ Finalize body.  Prints the response output.
 
 sub finalize_body {
     my ( $self, $c ) = @_;
-    if ( ref $c->response->body && $c->response->body->can('read') ) {
-        while ( !$c->response->body->eof() ) {
-            $c->response->body->read( my $buffer, $CHUNKSIZE );
+    my $body = $c->response->body;
+    no warnings 'uninitialized';
+    if ( blessed($body) && $body->can('read') or ref($body) eq 'GLOB' ) {
+        while ( !eof $body ) {
+            read $body, my ($buffer), $CHUNKSIZE;
             last unless $self->write( $c, $buffer );
         }
-        $c->response->body->close();
+        close $body;
     }
     else {
-        $self->write( $c, $c->response->body );
+        $self->write( $c, $body );
     }
 }
 
 =head2 $self->finalize_cookies($c)
 
+Create CGI::Simple::Cookie objects from $c->res->cookies, and set them as
+response headers.
+
 =cut
 
 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
+    my $response = $c->response;
+
+    foreach my $name (keys %{ $response->cookies }) {
+
+        my $val = $response->cookies->{$name};
+
+        my $cookie = (
+            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,
+                -httponly => $val->{httponly} || 0,
+            )
         );
 
         push @cookies, $cookie->as_string;
     }
 
     for my $cookie (@cookies) {
-        $c->res->headers->push_header( 'Set-Cookie' => $cookie );
+        $response->headers->push_header( 'Set-Cookie' => $cookie );
     }
 }
 
 =head2 $self->finalize_error($c)
 
+Output an appropriate error message. Called if there's an error in $c
+after the dispatch has finished. Will output debug messages if Catalyst
+is in debug mode, or a `please come back later` message otherwise.
+
 =cut
 
 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($_)
               . '</code></p>'
         } @{ $c->error };
         $error ||= 'No output';
-        $error = "<pre>$error</pre>";
+        $error = qq{<pre wrap="">$error</pre>};
         $title = $name = "$name on Catalyst $Catalyst::VERSION";
         $name  = "<h1>$name</h1>";
 
         # Don't show context in the dump
-        delete $c->req->{_context};
-        delete $c->res->{_context};
+        $c->req->_clear_context;
+        $c->res->_clear_context;
 
         # Don't show body parser in the dump
-        delete $c->req->{_body};
-
-        # 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;
+        $c->req->_clear_body;
 
         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">
-    <pre>%s</pre>
+    <pre wrap="">%s</pre>
 </div>
 EOF
             $i++;
@@ -141,11 +154,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 = '';
@@ -175,13 +192,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;
@@ -189,30 +206,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;
@@ -233,6 +246,15 @@ EOF
             font-size: medium;
             font-weight: normal;
         }
+        /* from http://users.tkk.fi/~tkarvine/linux/doc/pre-wrap/pre-wrap-css3-mozilla-opera-ie.html */
+        /* Browser specific (not valid) styles to make preformatted text wrap */
+        pre { 
+            white-space: pre-wrap;       /* css-3 */
+            white-space: -moz-pre-wrap;  /* Mozilla, since 1999 */
+            white-space: -pre-wrap;      /* Opera 4-6 */
+            white-space: -o-pre-wrap;    /* Opera 7 */
+            word-wrap: break-word;       /* Internet Explorer 5.5+ */
+        }
     </style>
 </head>
 <body>
@@ -254,6 +276,8 @@ EOF
 
 =head2 $self->finalize_headers($c)
 
+Abstract method, allows engines to write headers to response
+
 =cut
 
 sub finalize_headers { }
@@ -262,84 +286,107 @@ sub finalize_headers { }
 
 =cut
 
-sub finalize_read {
-    my ( $self, $c ) = @_;
-
-    undef $self->{_prepared_read};
-}
+sub finalize_read { }
 
 =head2 $self->finalize_uploads($c)
 
+Clean up after uploads, deleting temp files.
+
 =cut
 
 sub finalize_uploads {
     my ( $self, $c ) = @_;
 
-    if ( keys %{ $c->request->uploads } ) {
-        for my $key ( keys %{ $c->request->uploads } ) {
-            my $upload = $c->request->uploads->{$key};
-            unlink map { $_->tempname }
-              grep     { -e $_->tempname }
-              ref $upload eq 'ARRAY' ? @{$upload} : ($upload);
-        }
+    my $request = $c->request;
+    foreach my $key (keys %{ $request->uploads }) {
+        my $upload = $request->uploads->{$key};
+        unlink grep { -e $_ } map { $_->tempname }
+          (ref $upload eq 'ARRAY' ? @{$upload} : ($upload));
     }
+
 }
 
 =head2 $self->prepare_body($c)
 
+sets up the L<Catalyst::Request> object body using L<HTTP::Body>
+
 =cut
 
 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 );
-    }
-
-    if ( $self->read_length > 0 ) {
+    if ( my $length = $self->read_length ) {
+        my $request = $c->request;
+        unless ( $request->_body ) {
+            my $type = $request->header('Content-Type');
+            $request->_body(HTTP::Body->new( $type, $length ));
+            $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 = $length - $self->read_position;
+        if ( $remaining > 0 ) {
+            $self->finalize_read($c);
+            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)
 
+Add a chunk to the request body.
+
 =cut
 
 sub prepare_body_chunk {
     my ( $self, $c, $chunk ) = @_;
 
-    $c->request->{_body}->add($chunk);
+    $c->request->_body->add($chunk);
 }
 
 =head2 $self->prepare_body_parameters($c)
 
+Sets up parameters from body. 
+
 =cut
 
 sub prepare_body_parameters {
     my ( $self, $c ) = @_;
-    $c->request->body_parameters( $c->request->{_body}->param );
+    
+    return unless $c->request->_body;
+    
+    $c->request->body_parameters( $c->request->_body->param );
 }
 
 =head2 $self->prepare_connection($c)
 
+Abstract method implemented in engines.
+
 =cut
 
 sub prepare_connection { }
 
 =head2 $self->prepare_cookies($c)
 
+Parse cookies from header. Sets a L<CGI::Simple::Cookie> object.
+
 =cut
 
 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) } );
     }
 }
 
@@ -351,33 +398,38 @@ sub prepare_headers { }
 
 =head2 $self->prepare_parameters($c)
 
+sets up parameters from query and post parameters.
+
 =cut
 
 sub prepare_parameters {
     my ( $self, $c ) = @_;
 
+    my $request = $c->request;
+    my $parameters = $request->parameters;
+    my $body_parameters = $request->body_parameters;
+    my $query_parameters = $request->query_parameters;
     # We copy, no references
-    while ( my ( $name, $param ) = each %{ $c->request->query_parameters } ) {
-        $param = ref $param eq 'ARRAY' ? [ @{$param} ] : $param;
-        $c->request->parameters->{$name} = $param;
+    foreach my $name (keys %$query_parameters) {
+        my $param = $query_parameters->{$name};
+        $parameters->{$name} = ref $param eq 'ARRAY' ? [ @$param ] : $param;
     }
 
     # Merge query and body parameters
-    while ( my ( $name, $param ) = each %{ $c->request->body_parameters } ) {
-        $param = ref $param eq 'ARRAY' ? [ @{$param} ] : $param;
-        if ( my $old_param = $c->request->parameters->{$name} ) {
-            if ( ref $old_param eq 'ARRAY' ) {
-                push @{ $c->request->parameters->{$name} },
-                  ref $param eq 'ARRAY' ? @$param : $param;
-            }
-            else { $c->request->parameters->{$name} = [ $old_param, $param ] }
+    foreach my $name (keys %$body_parameters) {
+        my $param = $body_parameters->{$name};
+        my @values = ref $param eq 'ARRAY' ? @$param : ($param);
+        if ( my $existing = $parameters->{$name} ) {
+          unshift(@values, (ref $existing eq 'ARRAY' ? @$existing : $existing));
         }
-        else { $c->request->parameters->{$name} = $param }
+        $parameters->{$name} = @values > 1 ? \@values : $values[0];
     }
 }
 
 =head2 $self->prepare_path($c)
 
+abstract method, implemented by engines.
+
 =cut
 
 sub prepare_path { }
@@ -386,35 +438,71 @@ sub prepare_path { }
 
 =head2 $self->prepare_query_parameters($c)
 
+process the query string and extract query parameters.
+
 =cut
 
 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)
 
+prepare to read from the engine.
+
 =cut
 
 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)
 
+Populate the context object from the request object.
+
 =cut
 
 sub prepare_request { }
@@ -425,31 +513,50 @@ sub prepare_request { }
 
 sub prepare_uploads {
     my ( $self, $c ) = @_;
-    my $uploads = $c->request->{_body}->upload;
-    for my $name ( keys %$uploads ) {
+
+    my $request = $c->request;
+    return unless $request->_body;
+
+    my $uploads = $request->_body->upload;
+    my $parameters = $request->parameters;
+    foreach my $name (keys %$uploads) {
         my $files = $uploads->{$name};
-        $files = ref $files eq 'ARRAY' ? $files : [$files];
         my @uploads;
-        for my $upload (@$files) {
-            my $u = Catalyst::Request::Upload->new;
-            $u->headers( HTTP::Headers->new( %{ $upload->{headers} } ) );
-            $u->type( $u->headers->content_type );
-            $u->tempname( $upload->{tempname} );
-            $u->size( $upload->{size} );
-            $u->filename( $upload->{filename} );
+        for my $upload (ref $files eq 'ARRAY' ? @$files : ($files)) {
+            my $headers = HTTP::Headers->new( %{ $upload->{headers} } );
+            my $u = Catalyst::Request::Upload->new
+              (
+               size => $upload->{size},
+               type => $headers->content_type,
+               headers => $headers,
+               tempname => $upload->{tempname},
+               filename => $upload->{filename},
+              );
             push @uploads, $u;
         }
-        $c->request->uploads->{$name} = @uploads > 1 ? \@uploads : $uploads[0];
+        $request->uploads->{$name} = @uploads > 1 ? \@uploads : $uploads[0];
 
         # 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 $parameters->{$name}) {
+            if (ref $parameters->{$name} eq 'ARRAY') {
+                push @{ $parameters->{$name} }, @filenames;
+            }
+            else {
+                $parameters->{$name} = [ $parameters->{$name}, @filenames ];
+            }
+        }
+        else {
+            $parameters->{$name} = @filenames > 1 ? \@filenames : $filenames[0];
+        }
     }
 }
 
 =head2 $self->prepare_write($c)
 
+Abstract method. Implemented by the engines.
+
 =cut
 
 sub prepare_write { }
@@ -461,11 +568,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;
 
@@ -489,7 +591,7 @@ sub read {
 
 =head2 $self->read_chunk($c, $buffer, $length)
 
-Each engine inplements read_chunk as its preferred way of reading a chunk
+Each engine implements read_chunk as its preferred way of reading a chunk
 of data.
 
 =cut
@@ -507,34 +609,93 @@ The amount of input data that has already been read.
 
 =head2 $self->run($c)
 
+Start the engine. Implemented by the various engine classes.
+
 =cut
 
 sub run { }
 
 =head2 $self->write($c, $buffer)
 
+Writes the buffer to the client.
+
 =cut
 
 sub write {
     my ( $self, $c, $buffer ) = @_;
 
-    unless ( $self->{_prepared_write} ) {
+    unless ( $self->_prepared_write ) {
         $self->prepare_write($c);
-        $self->{_prepared_write} = 1;
+        $self->_prepared_write(1);
+    }
+    
+    return 0 if !defined $buffer;
+    
+    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.
+
+=cut
+
+sub unescape_uri {
+    my ( $self, $str ) = @_;
 
-    print STDOUT $buffer;
+    $str =~ s/(?:%([0-9A-Fa-f]{2})|\+)/defined $1 ? chr(hex($1)) : ' '/eg;
+
+    return $str;
 }
 
-=head1 AUTHORS
+=head2 $self->finalize_output
+
+<obsolete>, see finalize_body
 
-Sebastian Riedel, <sri@cpan.org>
+=head2 $self->env
+
+Hash containing enviroment variables including many special variables inserted
+by WWW server - like SERVER_*, REMOTE_*, HTTP_* ...
+
+Before accesing enviroment variables consider whether the same information is
+not directly available via Catalyst objects $c->request, $c->engine ...
+
+BEWARE: If you really need to access some enviroment variable from your Catalyst
+application you should use $c->engine->env->{VARNAME} instead of $ENV{VARNAME},
+as in some enviroments the %ENV hash does not contain what you would expect.
+
+=head1 AUTHORS
 
-Andy Grundman, <andy@hybridized.org>
+Catalyst Contributors, see Catalyst.pm
 
 =head1 COPYRIGHT
 
-This program is free software, you can redistribute it and/or modify it under
+This library is free software. You can redistribute it and/or modify it under
 the same terms as Perl itself.
 
 =cut