package Plack::Request; use strict; use warnings; use 5.008_001; our $VERSION = "0.09"; use HTTP::Headers; use URI::QueryParam; use Carp (); use Socket qw[AF_INET inet_aton]; # for _build_hostname use Plack::Request::Upload; use URI; sub new { my($class, $env) = @_; Carp::confess(q{$env is required}) unless defined $env && ref($env) eq 'HASH'; bless { env => $env, }, $class; } sub env { $_[0]->{env} } sub address { $_[0]->env->{REMOTE_ADDR} } sub protocol { $_[0]->env->{SERVER_PROTOCOL} } sub method { $_[0]->env->{REQUEST_METHOD} } sub port { $_[0]->env->{SERVER_PORT} } sub user { $_[0]->env->{REMOTE_USER} } sub request_uri { $_[0]->env->{REQUEST_URI} } sub url_scheme { $_[0]->env->{'psgi.url_scheme'} } sub session { $_[0]->env->{'plack.session'} } sub secure { $_[0]->url_scheme eq 'https'; } # we need better cookie lib? # http://mark.stosberg.com/blog/2008/12/cookie-handling-in-titanium-catalyst-and-mojo.html sub cookies { my $self = shift; if (defined $_[0]) { unless (ref($_[0]) eq 'HASH') { Carp::confess "Attribute (cookies) does not pass the type constraint because: Validation failed for 'HashRef' failed with value $_[0]"; } $self->{cookies} = $_[0]; } elsif (!defined $self->{cookies}) { require CGI::Simple::Cookie; if (my $header = $self->header('Cookie')) { $self->{cookies} = { CGI::Simple::Cookie->parse($header) }; } else { $self->{cookies} = {}; } } $self->{cookies}; } sub query_parameters { my $self = shift; if (defined $_[0]) { unless (ref($_[0]) eq 'HASH') { Carp::confess "Attribute (query_parameters) does not pass the type constraint because: Validation failed for 'HashRef' failed with value $_[0]"; } $self->{query_parameters} = $_[0]; } elsif (!defined $self->{query_parameters}) { $self->{query_parameters} = $self->uri->query_form_hash; } $self->{query_parameters}; } sub _body_parser { my $self = shift; unless (defined $self->{_body_parser}) { require Plack::Request::BodyParser; $self->{_body_parser} = Plack::Request::BodyParser->new( $self->env ); } $self->{_body_parser}; } sub raw_body { my $self = shift; if (!defined($self->{raw_body})) { $self->{raw_body} ||= $self->_body_parser->raw_body($self); } $self->{raw_body}; } sub headers { my $self = shift; if (!defined $self->{headers}) { my $env = $self->env; $self->{headers} = HTTP::Headers->new( map { (my $field = $_) =~ s/^HTTPS?_//; ( $field => $env->{$_} ); } grep { /^(?:HTTP|CONTENT|COOKIE)/i } keys %$env ); } $self->{headers}; } # shortcut sub content_encoding { shift->headers->content_encoding(@_) } sub content_length { shift->headers->content_length(@_) } sub content_type { shift->headers->content_type(@_) } sub header { shift->headers->header(@_) } sub referer { shift->headers->referer(@_) } sub user_agent { shift->headers->user_agent(@_) } sub hostname { my $self = shift; if (defined $_[0]) { $self->{hostname} = $_[0]; } elsif (!defined $self->{hostname}) { $self->{hostname} = $self->env->{REMOTE_HOST} || $self->_resolve_hostname; } $self->{hostname}; } sub _resolve_hostname { my ( $self, ) = @_; gethostbyaddr( inet_aton( $self->address ), AF_INET ); } # for win32 hacks BEGIN { if ($^O eq 'MSWin32') { no warnings 'redefine'; *_build_hostname = sub { my ( $self, ) = @_; my $address = $self->address; return 'localhost' if $address eq '127.0.0.1'; return gethostbyaddr( inet_aton( $address ), AF_INET ); }; } } # TODO: This attribute should be private. I will remove deps for HTTP::Body sub _http_body { my $self = shift; if (!defined $self->{_http_body}) { $self->{_http_body} = $self->_body_parser->http_body(); } $self->{_http_body}; } sub body_parameters { my $self = shift; if (@_ || defined $self->{_http_body} || $self->method eq 'POST') { return $self->_http_body->param(@_); } else { return {}; } } sub body { shift->_http_body->body(@_) } # contains body_params and query_params sub parameters { my $self = shift; if (defined $_[0]) { unless (ref($_[0]) eq 'HASH') { Carp::confess "Attribute (parameters) does not pass the type constraint because: Validation failed for 'HashRef' failed with value $_[0]"; } $self->{parameters} = $_[0]; } elsif (!defined $self->{parameters}) { $self->{parameters} = $self->_build_parameters; } $self->{parameters}; } sub _build_parameters { my $self = shift; my $query = $self->query_parameters; my $body = $self->body_parameters; my %merged; foreach my $hash ( $query, $body ) { foreach my $name ( keys %$hash ) { my $param = $hash->{$name}; push( @{ $merged{$name} ||= [] }, ( ref $param ? @$param : $param ) ); } } foreach my $param ( values %merged ) { $param = $param->[0] if @$param == 1; } return \%merged; } sub uploads { my $self = shift; if (defined $_[0]) { unless (ref($_[0]) eq 'HASH') { Carp::confess "Attribute (uploads) does not pass the type constraint because: Validation failed for 'HashRef' failed with value $_[0]"; } $self->{uploads} = $_[0]; } elsif (!defined $self->{uploads}) { $self->{uploads} = $self->_build_uploads; } $self->{uploads}; } sub _build_uploads { my $self = shift; my $uploads = $self->_http_body->upload; my %uploads; for my $name (keys %{ $uploads }) { my $files = $uploads->{$name}; $files = ref $files eq 'ARRAY' ? $files : [$files]; my @uploads; for my $upload (@{ $files }) { my $headers = HTTP::Headers->new( %{ $upload->{headers} } ); push( @uploads, Plack::Request::Upload->new( headers => $headers, tempname => $upload->{tempname}, size => $upload->{size}, filename => $upload->{filename}, ) ); } $uploads{$name} = @uploads > 1 ? \@uploads : $uploads[0]; # support access to the filename as a normal param my @filenames = map { $_->{filename} } @uploads; $self->parameters->{$name} = @filenames > 1 ? \@filenames : $filenames[0]; } return \%uploads; } # aliases sub body_params { shift->body_parameters(@_) } sub input { shift->body(@_) } sub params { shift->parameters(@_) } sub query_params { shift->query_parameters(@_) } sub path_info { shift->env->{PATH_INFO} } sub script_name { shift->env->{SCRIPT_NAME} } sub cookie { my $self = shift; return keys %{ $self->cookies } if @_ == 0; if (@_ == 1) { my $name = shift; return undef unless exists $self->cookies->{$name}; ## no critic. return $self->cookies->{$name}; } return; } sub param { my $self = shift; return keys %{ $self->parameters } if @_ == 0; if (@_ == 1) { my $param = shift; return wantarray ? () : undef unless exists $self->parameters->{$param}; if ( ref $self->parameters->{$param} eq 'ARRAY' ) { return (wantarray) ? @{ $self->parameters->{$param} } : $self->parameters->{$param}->[0]; } else { return (wantarray) ? ( $self->parameters->{$param} ) : $self->parameters->{$param}; } } else { my $field = shift; $self->parameters->{$field} = [@_]; } } sub upload { my $self = shift; return keys %{ $self->uploads } if @_ == 0; if (@_ == 1) { my $upload = shift; return wantarray ? () : undef unless exists $self->uploads->{$upload}; if (ref $self->uploads->{$upload} eq 'ARRAY') { return (wantarray) ? @{ $self->uploads->{$upload} } : $self->uploads->{$upload}->[0]; } else { return (wantarray) ? ( $self->uploads->{$upload} ) : $self->uploads->{$upload}; } } else { while ( my($field, $upload) = splice(@_, 0, 2) ) { if ( exists $self->uploads->{$field} ) { for ( $self->uploads->{$field} ) { $_ = [$_] unless ref($_) eq "ARRAY"; push(@{ $_ }, $upload); } } else { $self->uploads->{$field} = $upload; } } } } sub raw_uri { my $self = shift; my $env = $self->env; my $scheme = $env->{'psgi.url_scheme'} || "http"; # Host header should contain port number as well my $host = $env->{HTTP_HOST} || do { my $port = $env->{SERVER_PORT} || 80; my $is_std_port = ($scheme eq 'http' && $port == 80) || ($scheme eq 'https' && $port == 443); $env->{SERVER_NAME} . ($is_std_port ? "" : ":$port"); }; my $uri = "$scheme\://$host" . $env->{REQUEST_URI}; return URI->new($uri); } sub base { my $self = shift; my $uri = $self->raw_uri; $uri->path_query($self->env->{SCRIPT_NAME} || "/"); return $uri; } sub uri { my $self = shift; if (defined $_[0]) { unless (eval { $_[0]->isa('URI') }) { Carp::confess "Attribute (uri) does not pass the type constraint because: Validation failed for 'URI' failed with value $_[0]"; } $self->{uri} = $_[0]; } elsif (!defined $self->{uri}) { $self->{uri} = $self->_build_uri; } $self->{uri}; } sub _build_uri { my($self, ) = @_; my $env = $self->env; my $base_path = $env->{SCRIPT_NAME} || '/'; my $path = $base_path . ($env->{PATH_INFO} || ''); $path =~ s{^/+}{}; my $uri = ($env->{'psgi.url_scheme'} || "http") . "://" . ($env->{HTTP_HOST} || (($env->{SERVER_NAME} || "") . ":" . ($env->{SERVER_PORT} || 80))) . "/" . ($path || "") . ($env->{QUERY_STRING} ? "?$env->{QUERY_STRING}" : ""); # sanitize the URI return URI->new($uri)->canonical; } sub path { shift->uri->path(@_) } sub new_response { my $self = shift; require Plack::Response; Plack::Response->new(@_); } sub content { my ( $self, @args ) = @_; if ( @args ) { Carp::croak "The HTTP::Request method 'content' is unsupported when used as a writer, use Plack::RequestBuilder"; } else { return $self->raw_body; } } 1; __END__ =head1 NAME Plack::Request - Portable HTTP request object from PSGI env hash =head1 SYNOPSIS use Plack::Request; sub psgi_handler { my $env = shift; my $req = Plack::Request->new($env); my $res = $req->new_response(200); $res->content_type('text/html'); $res->body("Hello World"); return $res->finalize; } =head1 DESCRIPTION L provides a consistent API for request objects across web server environments. =head1 CAVEAT Note that this module is intended to be used by web application framework developers rather than application developers (end users). Writing your web application directly using Plack::Request is certainly possible but not recommended: it's like doing so with mod_perl's Apache::Request: yet too low level. If you're writing a web application, not a framework, then you're encouraged to use one of the web application frameworks that support PSGI, or use L if you want to write a micro web server application. Also, even if you're a framework developer, you probably want to handle Cookies and file uploads in your own way: Plack::Request gives you a simple API to deal with these things but ultimately you probably want to implement those in your own code. =head1 METHODS =head2 new Plack::Request->new( $psgi_env ); =head1 ATTRIBUTES =over 4 =item address Returns the IP address of the client. =item cookies Returns a reference to a hash containing the cookies =item method Contains the request method (C, C, C, etc). =item protocol Returns the protocol (HTTP/1.0 or HTTP/1.1) used for the current request. =item request_uri Returns the request uri (like $ENV{REQUEST_URI}) =item query_parameters Returns a reference to a hash containing query string (GET) parameters. Values can be either a scalar or an arrayref containing scalars. =item secure Returns true or false, indicating whether the connection is secure (https). =item uri Returns a URI object for the current request. Stringifies to the URI text. =item user Returns REMOTE_USER. =item raw_body Returns string containing body(POST). =item headers Returns an L object containing the headers for the current request. =item hostname Returns the hostname of the client. =item parameters Returns a reference to a hash containing GET and POST parameters. Values can be either a scalar or an arrayref containing scalars. =item uploads Returns a reference to a hash containing uploads. Values can be either a L object, or an arrayref of L objects. =item content_encoding Shortcut to $req->headers->content_encoding. =item content_length Shortcut to $req->headers->content_length. =item content_type Shortcut to $req->headers->content_type. =item header Shortcut to $req->headers->header. =item referer Shortcut to $req->headers->referer. =item user_agent Shortcut to $req->headers->user_agent. =item cookie A convenient method to access $req->cookies. $cookie = $req->cookie('name'); @cookies = $req->cookie; =item param Returns GET and POST parameters with a CGI.pm-compatible param method. This is an alternative method for accessing parameters in $req->parameters. $value = $req->param( 'foo' ); @values = $req->param( 'foo' ); @params = $req->param; Like L, and B earlier versions of Catalyst, passing multiple arguments to this method, like this: $req->param( 'foo', 'bar', 'gorch', 'quxx' ); will set the parameter C to the multiple values C, C and C. Previously this would have added C as another value to C (creating it if it didn't exist before), and C as another value for C. =item path Returns the path, i.e. the part of the URI after $req->base, for the current request. =item upload A convenient method to access $req->uploads. $upload = $req->upload('field'); @uploads = $req->upload('field'); @fields = $req->upload; for my $upload ( $req->upload('field') ) { print $upload->filename; } =item new_response my $res = $req->new_response; Creates a new L by default. Handy to remove dependency on L in your code for easy subclassing and duck typing in web application frameworks, as well as overriding Response generation in middlewares. =back =head1 AUTHORS Kazuhiro Osawa Tokuhiro Matsuno =head1 SEE ALSO L L, L =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut