package Plack::Response; use strict; use warnings; our $VERSION = '0.9927'; $VERSION = eval $VERSION; use Plack::Util::Accessor qw(body status); use Carp (); use Scalar::Util (); use HTTP::Headers; use URI::Escape (); sub code { shift->status(@_) } sub content { shift->body(@_) } sub new { my($class, $rc, $headers, $content) = @_; my $self = bless {}, $class; $self->status($rc) if defined $rc; $self->headers($headers) if defined $headers; $self->body($content) if defined $content; $self; } sub headers { my $self = shift; if (@_) { my $headers = shift; if (ref $headers eq 'ARRAY') { Carp::carp("Odd number of headers") if @$headers % 2 != 0; $headers = HTTP::Headers->new(@$headers); } elsif (ref $headers eq 'HASH') { $headers = HTTP::Headers->new(%$headers); } return $self->{headers} = $headers; } else { return $self->{headers} ||= HTTP::Headers->new(); } } sub cookies { my $self = shift; if (@_) { $self->{cookies} = shift; } else { return $self->{cookies} ||= +{ }; } } sub header { shift->headers->header(@_) } # shortcut sub content_length { shift->headers->content_length(@_); } sub content_type { shift->headers->content_type(@_); } sub content_encoding { shift->headers->content_encoding(@_); } sub location { shift->headers->header('Location' => @_); } sub redirect { my $self = shift; if (@_) { my $url = shift; my $status = shift || 302; $self->location($url); $self->status($status); } return $self->location; } sub finalize { my $self = shift; Carp::croak "missing status" unless $self->status(); $self->_finalize_cookies(); return [ $self->status, +[ map { my $k = $_; map { ( $k => $_ ) } $self->headers->header($_); } $self->headers->header_field_names ], $self->_body, ]; } sub _body { my $self = shift; my $body = $self->body; $body = [] unless defined $body; if (!ref $body or Scalar::Util::blessed($body) && overload::Method($body, q(""))) { return [ $body ]; } else { return $body; } } sub _finalize_cookies { my $self = shift; while (my($name, $val) = each %{$self->cookies}) { my $cookie = $self->_bake_cookie($name, $val); $self->headers->push_header( 'Set-Cookie' => $cookie ); } } sub _bake_cookie { my($self, $name, $val) = @_; return '' unless defined $val; $val = { value => $val } unless ref $val eq 'HASH'; my @cookie = ( URI::Escape::uri_escape($name) . "=" . URI::Escape::uri_escape($val->{value}) ); push @cookie, "domain=" . $val->{domain} if $val->{domain}; push @cookie, "path=" . $val->{path} if $val->{path}; push @cookie, "expires=" . $self->_date($val->{expires}) if $val->{expires}; push @cookie, "secure" if $val->{secure}; push @cookie, "HttpOnly" if $val->{httponly}; return join "; ", @cookie; } my @MON = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec ); my @WDAY = qw( Sun Mon Tue Wed Thu Fri Sat ); sub _date { my($self, $expires) = @_; if ($expires =~ /^\d+$/) { # all numbers -> epoch date # (cookies use '-' as date separator, HTTP uses ' ') my($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($expires); $year += 1900; return sprintf("%s, %02d-%s-%04d %02d:%02d:%02d GMT", $WDAY[$wday], $mday, $MON[$mon], $year, $hour, $min, $sec); } return $expires; } 1; __END__ =head1 NAME Plack::Response - Portable HTTP Response object for PSGI response =head1 SYNOPSIS use Plack::Response; sub psgi_handler { my $env = shift; my $res = Plack::Response->new(200); $res->content_type('text/html'); $res->body("Hello World"); return $res->finalize; } =head1 DESCRIPTION Plack::Response allows you a way to create PSGI response array ref through a simple API. =head1 METHODS =over 4 =item new $res = Plack::Response->new; $res = Plack::Response->new($status); $res = Plack::Response->new($status, $headers); $res = Plack::Response->new($status, $headers, $body); Creates a new Plack::Response object. =item status $res->status(200); $status = $res->status; Sets and gets HTTP status code. C<code> is an alias. =item headers $headers = $res->headers; $res->headers([ 'Content-Type' => 'text/html' ]); $res->headers({ 'Content-Type' => 'text/html' }); $res->headers( HTTP::Headers->new ); Sets and gets HTTP headers of the response. Setter can take either an array ref, a hash ref or L<HTTP::Headers> object containing a list of headers. =item body $res->body($body_str); $res->body([ "Hello", "World" ]); $res->body($io); Gets and sets HTTP response body. Setter can take either a string, an array ref, or an IO::Handle-like object. C<content> is an alias. =item header $res->header('X-Foo' => 'bar'); my $val = $res->header('X-Foo'); Shortcut for C<< $res->headers->header >>. =item content_type, content_length, content_encoding $res->content_type('text/plain'); $res->content_length(123); $res->content_encoding('gzip'); Shortcut for the equivalent get/set methods in C<< $res->headers >>. =item redirect $res->redirect($url); $res->redirect($url, 301); Sets redirect URL with an optional status code, which defaults to 302. =item location Gets and sets C<Location> header. =item cookies $res->cookies->{foo} = 123; $res->cookies->{foo} = { value => '123' }; Returns a hash reference containing cookies to be set in the response. The keys of the hash are the cookies' names, and their corresponding values are a plain string (for C<value> with everything else defaults) or a hash reference that can contain keys such as C<value>, C<domain>, C<expires>, C<path>, C<httponly>, C<secure>. C<expires> can take a string or an integer (as an epoch time) and B<does not> convert string formats such as C<+3M>. $res->cookies->{foo} = { value => 'test', path => "/", domain => '.example.com', expires => time + 24 * 60 * 60, }; =back =head1 AUTHOR Tokuhiro Matsuno Tatsuhiko Miyagawa =head1 SEE ALSO L<Plack::Request> =cut