use HTTP::Body;
use HTTP::Headers;
use URI::QueryParam;
+use Plack::Loader;
+use Catalyst::EngineLoader;
+use Encode ();
+use utf8;
use namespace::clean -except => 'meta';
-has env => (is => 'rw');
-
-# input position and length
-has read_length => (is => 'rw');
-has read_position => (is => 'rw');
-
-has _prepared_write => (is => 'rw');
-
# 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
else {
$self->write( $c, $body );
}
+
+ my $res = $c->response;
+ $res->_writer->close;
+ $res->_clear_writer;
+
+ return;
}
=head2 $self->finalize_cookies($c)
-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;
}
=cut
+sub _dump_error_page_element {
+ my ($self, $i, $element) = @_;
+ my ($name, $val) = @{ $element };
+
+ # This is fugly, but the metaclass is _HUGE_ and demands waaay too much
+ # scrolling. Suggestions for more pleasant ways to do this welcome.
+ local $val->{'__MOP__'} = "Stringified: "
+ . $val->{'__MOP__'} if ref $val eq 'HASH' && exists $val->{'__MOP__'};
+
+ my $text = encode_entities( dump( $val ));
+ sprintf <<"EOF", $name, $text;
+<h2><a href="#" onclick="toggleDump('dump_$i'); return false">%s</a></h2>
+<div id="dump_$i">
+ <pre wrap="">%s</pre>
+</div>
+EOF
+}
+
sub finalize_error {
my ( $self, $c ) = @_;
$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 ) {
$name = "<h1>$name</h1>";
# Don't show context in the dump
- $c->req->_clear_context;
$c->res->_clear_context;
# Don't show body parser in the dump
my @infos;
my $i = 0;
for my $dump ( $c->dump_these ) {
- my $name = $dump->[0];
- 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 wrap="">%s</pre>
-</div>
-EOF
+ push @infos, $self->_dump_error_page_element($i, $dump);
$i++;
}
$infos = join "\n", @infos;
</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 { }
-
-=head2 $self->finalize_read($c)
-
-=cut
+sub finalize_headers {
+ my ($self, $ctx) = @_;
-sub finalize_read { }
+ $ctx->finalize_headers unless $ctx->response->finalized_headers;
+ return;
+}
=head2 $self->finalize_uploads($c)
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 );
+ $c->request->prepare_body_parameters;
}
-=head2 $self->prepare_connection($c)
-
-Abstract method implemented in engines.
-
-=cut
-
-sub prepare_connection { }
-
-=head2 $self->prepare_cookies($c)
+=head2 $self->prepare_parameters($c)
-Parse cookies from header. Sets a L<CGI::Simple::Cookie> object.
+Sets up parameters from query and post parameters.
+If parameters have already been set up will clear
+existing parameters and set up again.
=cut
-sub prepare_cookies {
+sub prepare_parameters {
my ( $self, $c ) = @_;
- if ( my $header = $c->request->header('Cookie') ) {
- $c->req->cookies( { CGI::Simple::Cookie->parse($header) } );
- }
+ $c->request->_clear_parameters;
+ return $c->request->parameters;
}
-=head2 $self->prepare_headers($c)
+=head2 $self->prepare_path($c)
+
+abstract method, implemented by engines.
=cut
-sub prepare_headers { }
+sub prepare_path {
+ my ($self, $ctx) = @_;
-=head2 $self->prepare_parameters($c)
+ my $env = $ctx->request->env;
-sets up parameters from query and post parameters.
+ my $scheme = $ctx->request->secure ? 'https' : 'http';
+ my $host = $env->{HTTP_HOST} || $env->{SERVER_NAME};
+ my $port = $env->{SERVER_PORT} || 80;
+ my $base_path = $env->{SCRIPT_NAME} || "/";
-=cut
+ # set the request URI
+ 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{^/+}{};
+ }
-sub prepare_parameters {
- my ( $self, $c ) = @_;
+ # Using URI directly is way too slow, so we construct the URLs manually
+ my $uri_class = "URI::$scheme";
- 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;
- }
+ # HTTP_HOST will include the port even if it's 80/443
+ $host =~ s/:(?:80|443)$//;
- # 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];
+ if ($port !~ /^(?:80|443)$/ && $host !~ /:/) {
+ $host .= ":$port";
}
-}
-=head2 $self->prepare_path($c)
+ my $query = $env->{QUERY_STRING} ? '?' . $env->{QUERY_STRING} : '';
+ my $uri = $scheme . '://' . $host . '/' . $path . $query;
-abstract method, implemented by engines.
+ $ctx->request->uri( (bless \$uri, $uri_class)->canonical );
-=cut
+ # set the base URI
+ # base must end in a slash
+ $base_path .= '/' unless $base_path =~ m{/$};
+
+ my $base_uri = $scheme . '://' . $host . $base_path;
-sub prepare_path { }
+ $ctx->request->base( bless \$base_uri, $uri_class );
+
+ return;
+}
=head2 $self->prepare_request($c)
=cut
sub prepare_query_parameters {
- my ( $self, $c, $query_string ) = @_;
+ my ($self, $c) = @_;
+
+ my $env = $c->request->env;
+ my $query_string = exists $env->{QUERY_STRING}
+ ? $env->{QUERY_STRING}
+ : '';
# Check for keywords (no = signs)
# (yes, index() is faster than a regex :))
$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)
=cut
-sub prepare_request { }
+sub prepare_request {
+ my ($self, $ctx, %args) = @_;
+ $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. FIXME - Warn in the log here?
- $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
=cut
-sub read_chunk { }
-
-=head2 $self->read_length
-
-The length of input data to be read. This is obtained from the Content-Length
-header.
-
-=head2 $self->read_position
-
-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 { }
+sub read_chunk {
+ my ($self, $ctx) = (shift, shift);
+ return $ctx->request->read_chunk(@_);
+}
-=head2 $self->write($c, $buffer)
+=head2 $self->run($app, $server)
-Writes the buffer to the client.
+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..
=cut
-sub write {
- my ( $self, $c, $buffer ) = @_;
-
- unless ( $self->_prepared_write ) {
- $self->prepare_write($c);
- $self->_prepared_write(1);
+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);
+}
- return 0 if !defined $buffer;
+=head2 build_psgi_app ($app, @args)
- my $len = length($buffer);
- my $wrote = syswrite STDOUT, $buffer;
+Builds and returns a PSGI application closure. (Raw, not wrapped in middleware)
- if ( !defined $wrote && $! == EWOULDBLOCK ) {
- # Unable to write on the first try, will retry in the loop below
- $wrote = 0;
- }
+=cut
- 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;
- }
+sub build_psgi_app {
+ my ($self, $app, @args) = @_;
- last if $wrote >= $len;
- }
- }
+ return sub {
+ my ($env) = @_;
- return $wrote;
+ return sub {
+ my ($respond) = @_;
+ confess("Did not get a response callback for writer, cannot continiue") unless $respond;
+ $app->handle_request(env => $env, response_cb => $respond);
+ };
+ };
}
=head2 $self->unescape_uri($uri)
=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