use HTTP::Body;
use HTTP::Headers;
use URI::QueryParam;
-use Moose::Util::TypeConstraints;
use Plack::Loader;
+use Catalyst::EngineLoader;
+use Encode ();
+use utf8;
use namespace::clean -except => 'meta';
-has env => (is => 'ro', writer => '_set_env', clearer => '_clear_env');
-
-# input position and length
-has read_length => (is => 'rw');
-has read_position => (is => 'rw');
-
-has _prepared_write => (is => 'rw');
-
-has _response_cb => (
- is => 'ro',
- isa => 'CodeRef',
- writer => '_set_response_cb',
- clearer => '_clear_response_cb',
-);
-
-has _writer => (
- is => 'ro',
- isa => duck_type([qw(write close)]),
- writer => '_set_writer',
- clearer => '_clear_writer',
-);
-
# Amount of data to read from input on each pass
our $CHUNKSIZE = 64 * 1024;
+# XXX - this is only here for compat, do not use!
+has env => ( is => 'rw', writer => '_set_env' );
+my $WARN_ABOUT_ENV = 0;
+around env => sub {
+ my ($orig, $self, @args) = @_;
+ if(@args) {
+ warn "env as a writer is deprecated, you probably need to upgrade Catalyst::Engine::PSGI"
+ unless $WARN_ABOUT_ENV++;
+ return $self->_set_env(@args);
+ }
+ return $self->$orig;
+};
+
+# XXX - Only here for Engine::PSGI compat
+sub prepare_connection {
+ my ($self, $ctx) = @_;
+ $ctx->request->prepare_connection;
+}
+
=head1 NAME
Catalyst::Engine - The Catalyst Engine
$self->write( $c, $body );
}
- $self->_writer->close;
- $self->_clear_writer;
- $self->_clear_env;
+ my $res = $c->response;
+ $res->_writer->close;
+ $res->_clear_writer;
return;
}
-httponly => $val->{httponly} || 0,
)
);
+ if (!defined $cookie) {
+ $c->log->warn("undef passed in '$name' cookie value - not setting cookie")
+ if $c->debug;
+ next;
+ }
push @cookies, $cookie->as_string;
}
$c->res->content_type('text/html; charset=utf-8');
my $name = ref($c)->config->{name} || join(' ', split('::', ref $c));
+
+ # Prevent Catalyst::Plugin::Unicode::Encoding from running.
+ # This is a little nasty, but it's the best way to be clean whether or
+ # not the user has an encoding plugin.
+
+ if ($c->can('encoding')) {
+ $c->{encoding} = '';
+ }
my ( $title, $error, $infos );
if ( $c->debug ) {
</body>
</html>
-
- # Trick IE
+ # Trick IE. Old versions of IE would display their own error page instead
+ # of ours if we'd give it less than 512 bytes.
$c->res->{body} .= ( ' ' x 512 );
+ $c->res->{body} = Encode::encode("UTF-8", $c->res->{body});
+
# Return 500
$c->res->status(500);
}
=head2 $self->finalize_headers($c)
-Abstract method, allows engines to write headers to response
+Allows engines to write headers to response
=cut
sub finalize_headers {
my ($self, $ctx) = @_;
- my @headers;
- $ctx->response->headers->scan(sub { push @headers, @_ });
-
- $self->_set_writer($self->_response_cb->([ $ctx->response->status, \@headers ]));
- $self->_clear_response_cb;
-
+ $ctx->response->finalize_headers;
return;
}
-=head2 $self->finalize_read($c)
-
-=cut
-
-sub finalize_read { }
-
=head2 $self->finalize_uploads($c)
Clean up after uploads, deleting temp files.
sub finalize_uploads {
my ( $self, $c ) = @_;
+ # N.B. This code is theoretically entirely unneeded due to ->cleanup(1)
+ # on the HTTP::Body object.
my $request = $c->request;
foreach my $key (keys %{ $request->uploads }) {
my $upload = $request->uploads->{$key};
sub prepare_body {
my ( $self, $c ) = @_;
- my $appclass = ref($c) || $c;
- 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( $appclass->config->{uploadtmp} )
- if exists $appclass->config->{uploadtmp};
- }
-
- # Check for definedness as you could read '0'
- while ( defined ( 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);
- }
+ $c->request->prepare_body;
}
=head2 $self->prepare_body_chunk($c)
=cut
+# XXX - Can this be deleted?
sub prepare_body_chunk {
my ( $self, $c, $chunk ) = @_;
- $c->request->_body->add($chunk);
+ $c->request->prepare_body_chunk($chunk);
}
=head2 $self->prepare_body_parameters($c)
sub prepare_body_parameters {
my ( $self, $c ) = @_;
- 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 {
- my ($self, $ctx) = @_;
-
- my $env = $self->env;
- my $request = $ctx->request;
-
- $request->address( $env->{REMOTE_ADDR} );
- $request->hostname( $env->{REMOTE_HOST} )
- if exists $env->{REMOTE_HOST};
- $request->protocol( $env->{SERVER_PROTOCOL} );
- $request->remote_user( $env->{REMOTE_USER} );
- $request->method( $env->{REQUEST_METHOD} );
- $request->secure( $env->{'psgi.url_scheme'} eq 'https' ? 1 : 0 );
-
- return;
-}
-
-=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::Simple::Cookie->parse($header) } );
- }
-}
-
-=head2 $self->prepare_headers($c)
-
-=cut
-
-sub prepare_headers {
- my ($self, $ctx) = @_;
-
- my $env = $self->env;
- my $headers = $ctx->request->headers;
-
- for my $header (keys %{ $env }) {
- next unless $header =~ /^(HTTP|CONTENT|COOKIE)/i;
- (my $field = $header) =~ s/^HTTPS?_//;
- $field =~ tr/_/-/;
- $headers->header($field => $env->{$header});
- }
+ $c->request->prepare_body_parameters;
}
=head2 $self->prepare_parameters($c)
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
- foreach my $name (keys %$query_parameters) {
- my $param = $query_parameters->{$name};
- $parameters->{$name} = ref $param eq 'ARRAY' ? [ @$param ] : $param;
- }
-
- # Merge query and body parameters
- 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));
- }
- $parameters->{$name} = @values > 1 ? \@values : $values[0];
- }
+ $c->request->parameters;
}
=head2 $self->prepare_path($c)
sub prepare_path {
my ($self, $ctx) = @_;
- my $env = $self->env;
+ my $env = $ctx->request->env;
my $scheme = $ctx->request->secure ? 'https' : 'http';
my $host = $env->{HTTP_HOST} || $env->{SERVER_NAME};
my $base_path = $env->{SCRIPT_NAME} || "/";
# set the request URI
- my $req_uri = $env->{REQUEST_URI};
- $req_uri =~ s/\?.*$//;
- my $path = $req_uri;
- $path =~ s{^/+}{};
+ my $path;
+ if (!$ctx->config->{use_request_uri_for_path}) {
+ my $path_info = $env->{PATH_INFO};
+ if ( exists $env->{REDIRECT_URL} ) {
+ $base_path = $env->{REDIRECT_URL};
+ $base_path =~ s/\Q$path_info\E$//;
+ }
+ $path = $base_path . $path_info;
+ $path =~ s{^/+}{};
+ $path =~ s/([^$URI::uric])/$URI::Escape::escapes{$1}/go;
+ $path =~ s/\?/%3F/g; # STUPID STUPID SPECIAL CASE
+ }
+ else {
+ my $req_uri = $env->{REQUEST_URI};
+ $req_uri =~ s/\?.*$//;
+ $path = $req_uri;
+ $path =~ s{^/+}{};
+ }
# Using URI directly is way too slow, so we construct the URLs manually
my $uri_class = "URI::$scheme";
my $query = $env->{QUERY_STRING} ? '?' . $env->{QUERY_STRING} : '';
my $uri = $scheme . '://' . $host . '/' . $path . $query;
- $ctx->request->uri( bless \$uri, $uri_class );
+ $ctx->request->uri( (bless \$uri, $uri_class)->canonical );
# set the base URI
# base must end in a slash
sub prepare_query_parameters {
my ($self, $c) = @_;
- my $query_string = exists $self->env->{QUERY_STRING}
- ? $self->env->{QUERY_STRING}
+ my $env = $c->request->env;
+ my $query_string = exists $env->{QUERY_STRING}
+ ? $env->{QUERY_STRING}
: '';
# Check for keywords (no = signs)
$query{$param} = $value;
}
}
-
$c->request->query_parameters( \%query );
}
=head2 $self->prepare_read($c)
-prepare to read from the engine.
+Prepare to read by initializing the Content-Length from headers.
=cut
sub prepare_read {
my ( $self, $c ) = @_;
- # 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 );
+ $c->request->_read_length;
}
=head2 $self->prepare_request(@arguments)
sub prepare_request {
my ($self, $ctx, %args) = @_;
- $self->_set_env($args{env});
+ $ctx->request->_set_env($args{env});
+ $self->_set_env($args{env}); # Nasty back compat!
+ $ctx->response->_set_response_cb($args{response_cb});
}
=head2 $self->prepare_uploads($c)
my $u = Catalyst::Request::Upload->new
(
size => $upload->{size},
- type => $headers->content_type,
+ type => scalar $headers->content_type,
headers => $headers,
tempname => $upload->{tempname},
filename => $upload->{filename},
}
}
-=head2 $self->prepare_write($c)
+=head2 $self->write($c, $buffer)
-Abstract method. Implemented by the engines.
+Writes the buffer to the client.
=cut
-sub prepare_write { }
+sub write {
+ my ( $self, $c, $buffer ) = @_;
+
+ $c->response->write($buffer);
+}
=head2 $self->read($c, [$maxlength])
sub read {
my ( $self, $c, $maxlength ) = @_;
- my $remaining = $self->read_length - $self->read_position;
- $maxlength ||= $CHUNKSIZE;
-
- # Are we done reading?
- if ( $remaining <= 0 ) {
- $self->finalize_read($c);
- return;
- }
-
- my $readlen = ( $remaining > $maxlength ) ? $maxlength : $remaining;
- my $rc = $self->read_chunk( $c, my $buffer, $readlen );
- if ( defined $rc ) {
- if (0 == $rc) { # Nothing more to read even though Content-Length
- # said there should be.
- $self->finalize_read;
- return;
- }
- $self->read_position( $self->read_position + $rc );
- return $buffer;
- }
- else {
- Catalyst::Exception->throw(
- message => "Unknown error reading input: $!" );
- }
+ $c->request->read($maxlength);
}
-=head2 $self->read_chunk($c, $buffer, $length)
+=head2 $self->read_chunk($c, \$buffer, $length)
Each engine implements read_chunk as its preferred way of reading a chunk
of data. Returns the number of bytes read. A return of 0 indicates that
sub read_chunk {
my ($self, $ctx) = (shift, shift);
- return $self->env->{'psgi.input'}->read(@_);
+ return $ctx->request->read_chunk(@_);
}
-=head2 $self->read_length
+=head2 $self->run($app, $server)
-The length of input data to be read. This is obtained from the Content-Length
-header.
+Start the engine. Builds a PSGI application and calls the
+run method on the server passed in, which then causes the
+engine to loop, handling requests..
-=head2 $self->read_position
+=cut
-The amount of input data that has already been read.
+sub run {
+ my ($self, $app, $psgi, @args) = @_;
+ # @args left here rather than just a $options, $server for back compat with the
+ # old style scripts which send a few args, then a hashref
+
+ # They should never actually be used in the normal case as the Plack engine is
+ # passed in got all the 'standard' args via the loader in the script already.
+
+ # FIXME - we should stash the options in an attribute so that custom args
+ # like Gitalist's --git_dir are possible to get from the app without stupid tricks.
+ my $server = pop @args if (scalar @args && blessed $args[-1]);
+ my $options = pop @args if (scalar @args && ref($args[-1]) eq 'HASH');
+ # Back compat hack for applications with old (non Catalyst::Script) scripts to work in FCGI.
+ if (scalar @args && !ref($args[0])) {
+ if (my $listen = shift @args) {
+ $options->{listen} ||= [$listen];
+ }
+ }
+ if (! $server ) {
+ $server = Catalyst::EngineLoader->new(application_name => ref($self))->auto(%$options);
+ # We're not being called from a script, so auto detect what backend to
+ # run on. This should never happen, as mod_perl never calls ->run,
+ # instead the $app->handle method is called per request.
+ $app->log->warn("Not supplied a Plack engine, falling back to engine auto-loader (are your scripts ancient?)")
+ }
+ $app->run_options($options);
+ $server->run($psgi, $options);
+}
-=head2 $self->run($c)
+=head2 build_psgi_app ($app, @args)
-Start the engine. Implemented by the various engine classes.
+Builds and returns a PSGI application closure, wrapping it in the reverse proxy
+middleware if the using_frontend_proxy config setting is set.
=cut
-sub run {
+sub build_psgi_app {
my ($self, $app, @args) = @_;
- Carp::cluck("Run");
- # FIXME - Do something sensible with the options we're passed
- $self->_run_psgi_app($self->_build_psgi_app($app, @args), @args);
-}
-sub _build_psgi_app {
- my ($self, $app, @args) = @_;
return sub {
my ($env) = @_;
return sub {
my ($respond) = @_;
- $self->_set_response_cb($respond);
- $app->handle_request(env => $env);
+ $app->handle_request(env => $env, response_cb => $respond);
};
};
}
-sub _run_psgi_app {
- my ($self, $psgi_app, @args);
- # FIXME - Need to be able to specify engine and pass options..
- Plack::Loader->auto()->run($psgi_app);
-}
-
-=head2 $self->write($c, $buffer)
-
-Writes the buffer to the client.
-
-=cut
-
-sub write {
- my ( $self, $c, $buffer ) = @_;
-
- unless ( $self->_prepared_write ) {
- $self->prepare_write($c);
- $self->_prepared_write(1);
- }
-
- return 0 if !defined $buffer;
-
- my $len = length($buffer);
- $self->_writer->write($buffer);
-
- return $len;
-}
-
=head2 $self->unescape_uri($uri)
Unescapes a given URI using the most efficient method available. Engines such
=head2 $self->env
-Hash containing enviroment variables including many special variables inserted
+Hash containing environment variables including many special variables inserted
by WWW server - like SERVER_*, REMOTE_*, HTTP_* ...
-Before accesing enviroment variables consider whether the same information is
+Before accessing environment 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
+BEWARE: If you really need to access some environment 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.
+as in some environments the %ENV hash does not contain what you would expect.
=head1 AUTHORS