use Moose;
use HTTP::Headers;
+use Moose::Util::TypeConstraints;
+use namespace::autoclean;
with 'MooseX::Emulate::Class::Accessor::Fast';
-has cookies => (is => 'rw', default => sub { {} });
-has body => (is => 'rw', default => undef);
-sub has_body { defined($_[0]->body) }
-
-has location => (is => 'rw');
-has status => (is => 'rw', default => 200);
-has finalized_headers => (is => 'rw', default => 0);
-has headers => (
- is => 'rw',
- isa => 'HTTP::Headers',
- handles => [qw(content_encoding content_length content_type header)],
- default => sub { HTTP::Headers->new() },
- required => 1,
- lazy => 1,
+has _response_cb => (
+ is => 'ro',
+ isa => 'CodeRef',
+ writer => '_set_response_cb',
+ clearer => '_clear_response_cb',
+ predicate => '_has_response_cb',
+);
+
+subtype 'Catalyst::Engine::Types::Writer', as duck_type( [qw(write close)] );
+
+has _writer => (
+ is => 'ro',
+ isa => 'Catalyst::Engine::Types::Writer'
+ , #Pointless since we control how this is built
+ #writer => '_set_writer', Now that its lazy I think this is safe to remove
+ clearer => '_clear_writer',
+ predicate => '_has_writer',
+ lazy => 1,
+ builder => '_build_writer',
+);
+
+sub _build_writer {
+ my $self = shift;
+
+ ## These two lines are probably crap now...
+ $self->_context->finalize_headers
+ unless $self->finalized_headers;
+
+ my @headers;
+ $self->headers->scan( sub { push @headers, @_ } );
+
+ my $writer = $self->_response_cb->( [ $self->status, \@headers ] );
+ $self->_clear_response_cb;
+
+ return $writer;
+}
+
+has write_fh => (
+ is => 'ro',
+ predicate => '_has_write_fh',
+ lazy => 1,
+ builder => '_build_write_fh',
+);
+
+sub _build_write_fh { shift->_writer }
+
+sub DEMOLISH {
+ my $self = shift;
+ return if $self->_has_write_fh;
+ if ( $self->_has_writer ) {
+ $self->_writer->close;
+ }
+}
+
+has cookies => ( is => 'rw', default => sub { {} } );
+has body => ( is => 'rw', default => undef );
+sub has_body { defined( $_[0]->body ) }
+
+has location => ( is => 'rw', writer => '_set_location' );
+has status => ( is => 'rw', default => 200 );
+has finalized_headers => ( is => 'rw', default => 0 );
+has headers => (
+ is => 'rw',
+ isa => 'HTTP::Headers',
+ handles => [qw(content_encoding content_length content_type header)],
+ default => sub { HTTP::Headers->new() },
+ required => 1,
+ lazy => 1,
);
has _context => (
- is => 'rw',
- weak_ref => 1,
- handles => ['write'],
- clearer => '_clear_context',
+ is => 'rw',
+ weak_ref => 1,
+ clearer => '_clear_context',
);
+before [
+ qw(status headers content_encoding content_length content_type header)]
+ => sub {
+ my $self = shift;
+
+ $self->_context->log->warn(
+ "Useless setting a header value after finalize_headers called."
+ . " Not what you want." )
+ if ( $self->finalized_headers && @_ );
+ };
+
sub output { shift->body(@_) }
-sub code { shift->status(@_) }
+sub code { shift->status(@_) }
+
+sub write {
+ my ( $self, $buffer ) = @_;
+
+ # Finalize headers if someone manually writes output
+ $self->_context->finalize_headers unless $self->finalized_headers;
+
+ $buffer = q[] unless defined $buffer;
+
+ my $len = length($buffer);
+ $self->_writer->write($buffer);
-no Moose;
+ return $len;
+}
+
+sub finalize_headers {
+ my ($self) = @_;
+ return;
+}
+
+sub from_psgi_response {
+ my ( $self, $psgi_res ) = @_;
+ if ( ref $psgi_res eq 'ARRAY' ) {
+ my ( $status, $headers, $body ) = @$psgi_res;
+ $self->status($status);
+ $self->headers( HTTP::Headers->new(@$headers) );
+ $self->body($body);
+ } elsif ( ref $psgi_res eq 'CODE' ) {
+ $psgi_res->(
+ sub {
+ my $response = shift;
+ my ( $status, $headers, $maybe_body ) = @$response;
+ $self->status($status);
+ $self->headers( HTTP::Headers->new(@$headers) );
+ if ( defined $maybe_body ) {
+ $self->body($maybe_body);
+ } else {
+ return $self->write_fh;
+ }
+ }
+ );
+ } else {
+ die
+ "You can't set a Catalyst response from that, expect a valid PSGI response";
+ }
+}
=head1 NAME
in the same fashion), or a filehandle GLOB. Catalyst
will write it piece by piece into the response.
+When using a L<IO::Handle> type of object and no content length has been
+already set in the response headers Catalyst will make a reasonable attempt
+to determine the size of the Handle. Depending on the implementation of your
+handle object, setting the content length may fail. If it is at all possible
+for you to determine the content length of your handle object,
+it is recommended that you set the content length in the response headers
+yourself, which will be respected and sent by Catalyst in the response.
+
=head2 $res->has_body
Predicate which returns true when a body has been set.
want to C< return > or C<< $c->detach() >> to interrupt the normal
processing flow if you want the redirect to occur straight away.
+B<Note:> do not give a relative URL as $url, i.e: one that is not fully
+qualified (= C<http://...>, etc.) or that starts with a slash
+(= C</path/here>). While it may work, it is not guaranteed to do the right
+thing and is not a standard behaviour. You may opt to use uri_for() or
+uri_for_action() instead.
+
=cut
sub redirect {
if (@_) {
my $location = shift;
- my $status = shift || 302;
+ my $status = shift || 302;
$self->location($location);
$self->status($status);
+
}
return $self->location;
}
+around '_set_location' => sub {
+ my $orig = shift;
+ my $self = shift;
+
+ if (@_) {
+
+ my $location = shift;
+
+ if ( $location =~ m/[\n\r]/ ) { # check for header injection
+
+ die "blocking header injection";
+
+ } else {
+
+ $self->$orig($location);
+
+ }
+
+ } else {
+
+ $self->$orig();
+
+ }
+
+};
+
=head2 $res->location
Sets or returns the HTTP 'Location'.
Writes $data to the output stream.
-=head2 meta
+=head2 $res->write_fh
-Provided by Moose
+Returns a PSGI $writer object that has two methods, write and close. You can
+close over this object for asynchronous and nonblocking applications. For
+example (assuming you are using a supporting server, like L<Twiggy>
+
+ package AsyncExample::Controller::Root;
+
+ use Moose;
+
+ BEGIN { extends 'Catalyst::Controller' }
+
+ sub prepare_cb {
+ my $write_fh = pop;
+ return sub {
+ my $message = shift;
+ $write_fh->write("Finishing: $message\n");
+ $write_fh->close;
+ };
+ }
+
+ sub anyevent :Local :Args(0) {
+ my ($self, $c) = @_;
+ my $cb = $self->prepare_cb($c->res->write_fh);
+
+ my $watcher;
+ $watcher = AnyEvent->timer(
+ after => 5,
+ cb => sub {
+ $cb->(scalar localtime);
+ undef $watcher; # cancel circular-ref
+ });
+ }
=head2 $res->print( @data )
Prints @data to the output stream, separated by $,. This lets you pass
the response object to functions that want to write to an L<IO::Handle>.
+=head2 $self->finalize_headers($c)
+
+Writes headers to response if not already written
+
+=head2 from_psgi_response
+
+Given a PSGI response (either three element ARRAY reference OR coderef expecting
+a $responder) set the response from it.
+
+Properly supports streaming and delayed response and / or async IO if running
+under an expected event loop.
+
+Example:
+
+ package MyApp::Web::Controller::Test;
+
+ use base 'Catalyst::Controller';
+ use Plack::App::Directory;
+
+
+ my $app = Plack::App::Directory->new({ root => "/path/to/htdocs" })
+ ->to_app;
+
+ sub myaction :Local Args {
+ my ($self, $c) = @_;
+ $c->res->from_psgi_response($app->($c->req->env));
+ }
+
+Please note this does not attempt to map or nest your PSGI application under
+the Controller and Action namespace or path.
+
+=head2 DEMOLISH
+
+Ensures that the response is flushed and closed at the end of the
+request.
+
+=head2 meta
+
+Provided by Moose
+
=cut
sub print {