X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FCatalyst%2FRequest.pm;h=87ce66a22119de587f0602f97a7b600ab2dc5927;hb=e72a3cd6e12d8b9594dcfdddf13c1fbabdffb900;hp=dce7a052691c6a57be7f123f95b6e0e0b1e907bb;hpb=d7f189232be6991bad0861a3d847d224475677df;p=catagits%2FCatalyst-Runtime.git diff --git a/lib/Catalyst/Request.pm b/lib/Catalyst/Request.pm index dce7a05..87ce66a 100644 --- a/lib/Catalyst/Request.pm +++ b/lib/Catalyst/Request.pm @@ -7,6 +7,9 @@ use URI::http; use URI::https; use URI::QueryParam; use HTTP::Headers; +use Stream::Buffered; +use Hash::MultiValue; +use Scalar::Util; use Moose; @@ -14,7 +17,7 @@ use namespace::clean -except => 'meta'; with 'MooseX::Emulate::Class::Accessor::Fast'; -has env => (is => 'ro', writer => '_set_env'); +has env => (is => 'ro', writer => '_set_env', predicate => 'has_env'); # XXX Deprecated crap here - warn? has action => (is => 'rw'); # XXX: Deprecated in docs ages ago (2006), deprecated with warning in 5.8000 due @@ -91,6 +94,48 @@ has _log => ( required => 1, ); +has io_fh => ( + is=>'ro', + predicate=>'has_io_fh', + lazy=>1, + builder=>'_build_io_fh'); + +sub _build_io_fh { + my $self = shift; + return $self->env->{'psgix.io'} + || ( + $self->env->{'net.async.http.server.req'} && + $self->env->{'net.async.http.server.req'}->stream) ## Until I can make ioasync cabal see the value of supportin psgix.io (jnap) + || die "Your Server does not support psgix.io"; +}; + +has data_handlers => ( is=>'ro', isa=>'HashRef', default=>sub { +{} } ); + +has body_data => ( + is=>'ro', + lazy=>1, + builder=>'_build_body_data'); + +sub _build_body_data { + my ($self) = @_; + my $content_type = $self->content_type; + my ($match) = grep { $content_type =~/$_/i } + keys(%{$self->data_handlers}); + + if($match) { + my $fh = $self->body; + local $_ = $fh; + return $self->data_handlers->{$match}->($fh, $self); + } else { + return undef; + } +} + +has _use_hash_multivalue => ( + is=>'ro', + required=>1, + default=> sub {0}); + # Amount of data to read from input on each pass our $CHUNKSIZE = 64 * 1024; @@ -159,13 +204,23 @@ sub prepare_parameters { return $self->parameters; } - - sub _build_parameters { my ( $self ) = @_; my $parameters = {}; my $body_parameters = $self->body_parameters; my $query_parameters = $self->query_parameters; + + ## setup for downstream plack + $self->env->{'plack.request.merged'} ||= do { + my $query = $self->env->{'plack.request.query'} || Hash::MultiValue->new; + my $body = $self->env->{'plack.request.body'} || Hash::MultiValue->new; + Hash::MultiValue->new($query->flatten, $body->flatten); + }; + + if($self->_use_hash_multivalue) { + return $self->env->{'plack.request.merged'}->clone; # We want a copy, in case your App is evil + } + # We copy, no references foreach my $name (keys %$query_parameters) { my $param = $query_parameters->{$name}; @@ -192,30 +247,78 @@ has _uploadtmp => ( sub prepare_body { my ( $self ) = @_; - if ( my $length = $self->_read_length ) { - unless ( $self->_body ) { - my $type = $self->header('Content-Type'); - $self->_body(HTTP::Body->new( $type, $length )); - $self->_body->cleanup(1); # Make extra sure! - $self->_body->tmpdir( $self->_uploadtmp ) - if $self->_has_uploadtmp; - } + # If previously applied middleware created the HTTP::Body object, then we + # just use that one. - # Check for definedness as you could read '0' - while ( defined ( my $buffer = $self->read() ) ) { - $self->prepare_body_chunk($buffer); - } + if(my $plack_body = $self->env->{'plack.request.http.body'}) { + $self->_body($plack_body); + $self->_body->cleanup(1); + return; + } - # paranoia against wrong Content-Length header - my $remaining = $length - $self->_read_position; - if ( $remaining > 0 ) { - Catalyst::Exception->throw( - "Wrong Content-Length value: $length" ); - } + # Define PSGI ENV placeholders, or for empty should there be no content + # body (typical in HEAD or GET). Looks like from Plack::Request that + # middleware would probably expect to see this, even if empty + + $self->env->{'plack.request.body'} = Hash::MultiValue->new; + $self->env->{'plack.request.upload'} = Hash::MultiValue->new; + + # If there is nothing to read, set body to naught and return. This + # will cause all body code to be skipped + + return $self->_body(0) unless my $length = $self->_read_length; + + # Unless the body has already been set, create it. Not sure about this + # code, how else might it be set, but this was existing logic. + + unless ($self->_body) { + my $type = $self->header('Content-Type'); + $self->_body(HTTP::Body->new( $type, $length )); + $self->_body->cleanup(1); + + # JNAP: I'm not sure this is doing what we expect, but it also doesn't + # seem to be hurting (seems ->_has_uploadtmp is true more than I would + # expect. + + $self->_body->tmpdir( $self->_uploadtmp ) + if $self->_has_uploadtmp; } - else { - # Defined but will cause all body code to be skipped - $self->_body(0); + + # Ok if we get this far, we have to read psgi.input into the new body + # object. Lets play nice with any plack app or other downstream, so + # we create a buffer unless one exists. + + my $stream_buffer; + if ($self->env->{'psgix.input.buffered'}) { + # Be paranoid about previous psgi middleware or apps that read the + # input but didn't return the buffer to the start. + $self->env->{'psgi.input'}->seek(0, 0); + } else { + $stream_buffer = Stream::Buffered->new($length); + } + + # Check for definedness as you could read '0' + while ( defined ( my $chunk = $self->read() ) ) { + $self->prepare_body_chunk($chunk); + $stream_buffer->print($chunk) if $stream_buffer; + } + + # Ok, we read the body. Lets play nice for any PSGI app down the pipe + + if ($stream_buffer) { + $self->env->{'psgix.input.buffered'} = 1; + $self->env->{'psgi.input'} = $stream_buffer->rewind; + } else { + $self->env->{'psgi.input'}->seek(0, 0); # Reset the buffer for downstream middleware or apps + } + + $self->env->{'plack.request.http.body'} = $self->_body; + $self->env->{'plack.request.body'} = Hash::MultiValue->from_mixed($self->_body->param); + + # paranoia against wrong Content-Length header + my $remaining = $length - $self->_read_position; + if ( $remaining > 0 ) { + Catalyst::Exception->throw("Wrong Content-Length value: $length" ); } } @@ -231,7 +334,9 @@ sub prepare_body_parameters { $self->prepare_body if ! $self->_has_body; return {} unless $self->_body; - return $self->_body->param; + return $self->_use_hash_multivalue ? + $self->env->{'plack.request.body'}->clone : + $self->_body->param; } sub prepare_connection { @@ -281,7 +386,7 @@ has _body => ( # and provide a custom reader.. sub body { my $self = shift; - $self->prepare_body unless ! $self->_has_body; + $self->prepare_body unless $self->_has_body; croak 'body is a reader' if scalar @_; return blessed $self->_body ? $self->_body->body : $self->_body; } @@ -319,6 +424,7 @@ Catalyst::Request - provides information about the current client request $req->args; $req->base; $req->body; + $req->body_data; $req->body_parameters; $req->content_encoding; $req->content_length; @@ -401,6 +507,14 @@ Returns the message body of the request, as returned by L: a string, unless Content-Type is C, C, or C, in which case a L object is returned. +=head2 $req->body_data + +Returns a Perl representation of POST/PUT body data that is not classic HTML +form data, such as JSON, XML, etc. By default, Catalyst will parse incoming +data of the type 'application/json' and return access to that data via this +method. You may define addition data_handlers via a global configuration +setting. See L for more information. + =head2 $req->body_parameters Returns a reference to a hash containing body (POST) parameters. Values can @@ -846,6 +960,12 @@ Returns the value of the C environment variable. Shortcut to $req->headers->user_agent. Returns the user agent (browser) version string. +=head2 $req->io_fh + +Returns a psgix.io bidirectional socket, if your server supports one. Used for +when you want to jailbreak out of PSGI and handle bidirectional client server +communication manually, such as when you are using cometd or websockets. + =head1 SETUP METHODS You should never need to call these yourself in application code,