File Coverage

inc/Plack/Request.pm
Criterion Covered Total %
statement 41 187 21.9
branch 1 52 1.9
condition 1 30 3.3
subroutine 15 53 28.3
pod 37 38 97.3
total 95 360 26.3


line stmt bran cond sub pod time code
1             #line 1
2 3     3   1120 package Plack::Request;
  3         15  
  3         142  
3 3     3   13 use strict;
  3         5  
  3         71  
4 3     3   55 use warnings;
  3         9  
  3         147  
5             use 5.008_001;
6             our $VERSION = '1.0030';
7 3     3   2639  
  3         42060  
  3         111  
8 3     3   30 use HTTP::Headers;
  3         4  
  3         60  
9 3     3   3148 use Carp ();
  3         8751  
  3         83  
10 3     3   2659 use Hash::MultiValue;
  3         144834  
  3         102  
11             use HTTP::Body;
12 3     3   2420  
  3         1437  
  3         92  
13 3     3   2591 use Plack::Request::Upload;
  3         19402  
  3         91  
14 3     3   26 use Stream::Buffered;
  3         5  
  3         78  
15 3     3   18 use URI;
  3         5  
  3         8090  
16             use URI::Escape ();
17              
18 5     5 1 6735 sub new {
19 5 50 33     37 my($class, $env) = @_;
20             Carp::croak(q{$env is required})
21             unless defined $env && ref($env) eq 'HASH';
22 5         22  
23             bless { env => $env }, $class;
24             }
25 6     6 1 24  
26             sub env { $_[0]->{env} }
27 0     0 1 0  
28 0     0 1 0 sub address { $_[0]->env->{REMOTE_ADDR} }
29 0     0 1 0 sub remote_host { $_[0]->env->{REMOTE_HOST} }
30 0     0 1 0 sub protocol { $_[0]->env->{SERVER_PROTOCOL} }
31 0     0 0 0 sub method { $_[0]->env->{REQUEST_METHOD} }
32 0     0 1 0 sub port { $_[0]->env->{SERVER_PORT} }
33 0     0 1 0 sub user { $_[0]->env->{REMOTE_USER} }
34 6     6 1 19 sub request_uri { $_[0]->env->{REQUEST_URI} }
35 0 0   0 1 0 sub path_info { $_[0]->env->{PATH_INFO} }
36 0     0 1 0 sub path { $_[0]->env->{PATH_INFO} || '/' }
37 0     0 1 0 sub script_name { $_[0]->env->{SCRIPT_NAME} }
38 0     0 1 0 sub scheme { $_[0]->env->{'psgi.url_scheme'} }
39 0     0 1 0 sub secure { $_[0]->scheme eq 'https' }
40 0     0 1 0 sub body { $_[0]->env->{'psgi.input'} }
41             sub input { $_[0]->env->{'psgi.input'} }
42 0     0 1 0  
43 0     0 1 0 sub content_length { $_[0]->env->{CONTENT_LENGTH} }
44             sub content_type { $_[0]->env->{CONTENT_TYPE} }
45 0     0 1 0  
46 0     0 1 0 sub session { $_[0]->env->{'psgix.session'} }
47 0     0 1 0 sub session_options { $_[0]->env->{'psgix.session.options'} }
48             sub logger { $_[0]->env->{'psgix.logger'} }
49              
50 0     0 1 0 sub cookies {
51             my $self = shift;
52 0 0       0  
53             return {} unless $self->env->{HTTP_COOKIE};
54              
55 0 0 0     0 # HTTP_COOKIE hasn't changed: reuse the parsed cookie
56             if ( $self->env->{'plack.cookie.parsed'}
57 0         0 && $self->env->{'plack.cookie.string'} eq $self->env->{HTTP_COOKIE}) {
58             return $self->env->{'plack.cookie.parsed'};
59             }
60 0         0  
61             $self->env->{'plack.cookie.string'} = $self->env->{HTTP_COOKIE};
62 0         0  
63 0         0 my %results;
64 0         0 my @pairs = grep m/=/, split "[;,] ?", $self->env->{'plack.cookie.string'};
65             for my $pair ( @pairs ) {
66 0         0 # trim leading trailing whitespace
  0         0  
67             $pair =~ s/^\s+//; $pair =~ s/\s+$//;
68 0         0  
69             my ($key, $value) = map URI::Escape::uri_unescape($_), split( "=", $pair, 2 );
70              
71 0 0       0 # Take the first one like CGI.pm or rack do
72             $results{$key} = $value unless exists $results{$key};
73             }
74 0         0  
75             $self->env->{'plack.cookie.parsed'} = \%results;
76             }
77              
78 0     0 1 0 sub query_parameters {
79 0   0     0 my $self = shift;
80             $self->env->{'plack.request.query'} ||= $self->_parse_query;
81             }
82              
83 0     0   0 sub _parse_query {
84             my $self = shift;
85 0         0  
86 0         0 my @query;
87 0 0       0 my $query_string = $self->env->{QUERY_STRING};
88 0 0       0 if (defined $query_string) {
89             if ($query_string =~ /=/) {
90 0         0 # Handle ?foo=bar&bar=foo type of query
91 0 0       0 @query =
  0         0  
92 0         0 map { s/\+/ /g; URI::Escape::uri_unescape($_) }
93             map { /=/ ? split(/=/, $_, 2) : ($_ => '')}
94             split(/[&;]/, $query_string);
95             } else {
96 0         0 # Handle ...?dog+bones type of query
97 0         0 @query =
98             map { (URI::Escape::uri_unescape($_), '') }
99             split(/\+/, $query_string, -1);
100             }
101             }
102 0         0  
103             Hash::MultiValue->new(@query);
104             }
105              
106 0     0 1 0 sub content {
107             my $self = shift;
108 0 0       0  
109 0         0 unless ($self->env->{'psgix.input.buffered'}) {
110             $self->_parse_request_body;
111             }
112 0 0       0  
113 0 0       0 my $fh = $self->input or return '';
114             my $cl = $self->env->{CONTENT_LENGTH} or return '';
115 0         0  
116 0         0 $fh->seek(0, 0); # just in case middleware/apps read it without seeking back
117 0         0 $fh->read(my($content), $cl, 0);
118             $fh->seek(0, 0);
119 0         0  
120             return $content;
121             }
122 0     0 1 0  
123             sub raw_body { $_[0]->content }
124              
125             # XXX you can mutate headers with ->headers but it's not written through to the env
126              
127 0     0 1 0 sub headers {
128 0 0       0 my $self = shift;
129 0         0 if (!defined $self->{headers}) {
130 0         0 my $env = $self->env;
131             $self->{headers} = HTTP::Headers->new(
132 0         0 map {
133 0         0 (my $field = $_) =~ s/^HTTPS?_//;
134             ( $field => $env->{$_} );
135 0         0 }
136             grep { /^(?:HTTP|CONTENT)/i } keys %$env
137             );
138 0         0 }
139             $self->{headers};
140             }
141 0     0 1 0  
142 0     0 1 0 sub content_encoding { shift->headers->content_encoding(@_) }
143 0     0 1 0 sub header { shift->headers->header(@_) }
144 0     0 1 0 sub referer { shift->headers->referer(@_) }
145             sub user_agent { shift->headers->user_agent(@_) }
146              
147 0     0 1 0 sub body_parameters {
148             my $self = shift;
149 0 0       0  
150 0         0 unless ($self->env->{'plack.request.body'}) {
151             $self->_parse_request_body;
152             }
153 0         0  
154             return $self->env->{'plack.request.body'};
155             }
156              
157             # contains body + query
158 0     0 1 0 sub parameters {
159             my $self = shift;
160 0   0     0  
161 0         0 $self->env->{'plack.request.merged'} ||= do {
162 0         0 my $query = $self->query_parameters;
163 0         0 my $body = $self->body_parameters;
164             Hash::MultiValue->new($query->flatten, $body->flatten);
165             };
166             }
167              
168 0     0 1 0 sub uploads {
169             my $self = shift;
170 0 0       0  
171 0         0 if ($self->env->{'plack.request.upload'}) {
172             return $self->env->{'plack.request.upload'};
173             }
174 0         0  
175 0         0 $self->_parse_request_body;
176             return $self->env->{'plack.request.upload'};
177             }
178              
179 0     0 1 0 sub param {
180             my $self = shift;
181 0 0       0  
  0         0  
182             return keys %{ $self->parameters } if @_ == 0;
183 0         0  
184 0 0       0 my $key = shift;
185 0         0 return $self->parameters->{$key} unless wantarray;
186             return $self->parameters->get_all($key);
187             }
188              
189 0     0 1 0 sub upload {
190             my $self = shift;
191 0 0       0  
  0         0  
192             return keys %{ $self->uploads } if @_ == 0;
193 0         0  
194 0 0       0 my $key = shift;
195 0         0 return $self->uploads->{$key} unless wantarray;
196             return $self->uploads->get_all($key);
197             }
198              
199 0     0 1 0 sub uri {
200             my $self = shift;
201 0         0  
202             my $base = $self->_uri_base;
203              
204             # We have to escape back PATH_INFO in case they include stuff like
205             # ? or # so that the URI parser won't be tricked. However we should
206             # preserve '/' since encoding them into %2f doesn't make sense.
207             # This means when a request like /foo%2fbar comes in, we recognize
208             # it as /foo/bar which is not ideal, but that's how the PSGI PATH_INFO
209             # spec goes and we can't do anything about it. See PSGI::FAQ for details.
210              
211 0         0 # See RFC 3986 before modifying.
212             my $path_escape_class = q{^/;:@&=A-Za-z0-9\$_.+!*'(),-};
213 0   0     0  
214 0 0 0     0 my $path = URI::Escape::uri_escape($self->env->{PATH_INFO} || '', $path_escape_class);
215             $path .= '?' . $self->env->{QUERY_STRING}
216             if defined $self->env->{QUERY_STRING} && $self->env->{QUERY_STRING} ne '';
217 0 0       0  
218             $base =~ s!/$!! if $path =~ m!^/!;
219 0         0  
220             return URI->new($base . $path)->canonical;
221             }
222              
223 0     0 1 0 sub base {
224 0         0 my $self = shift;
225             URI->new($self->_uri_base)->canonical;
226             }
227              
228 0     0   0 sub _uri_base {
229             my $self = shift;
230 0         0  
231             my $env = $self->env;
232 0   0     0  
      0        
      0        
233             my $uri = ($env->{'psgi.url_scheme'} || "http") .
234             "://" .
235             ($env->{HTTP_HOST} || (($env->{SERVER_NAME} || "") . ":" . ($env->{SERVER_PORT} || 80))) .
236             ($env->{SCRIPT_NAME} || '/');
237 0         0  
238             return $uri;
239             }
240              
241 2     2 1 59 sub new_response {
242 2         1819 my $self = shift;
243 2         4555 require Plack::Response;
244             Plack::Response->new(@_);
245             }
246              
247 0     0     sub _parse_request_body {
248             my $self = shift;
249 0            
250 0           my $ct = $self->env->{CONTENT_TYPE};
251 0 0 0       my $cl = $self->env->{CONTENT_LENGTH};
252             if (!$ct && !$cl) {
253 0           # No Content-Type nor Content-Length -> GET/HEAD
254 0           $self->env->{'plack.request.body'} = Hash::MultiValue->new;
255 0           $self->env->{'plack.request.upload'} = Hash::MultiValue->new;
256             return;
257             }
258 0            
259             my $body = HTTP::Body->new($ct, $cl);
260              
261             # HTTP::Body will create temporary files in case there was an
262             # upload. Those temporary files can be cleaned up by telling
263             # HTTP::Body to do so. It will run the cleanup when the request
264             # env is destroyed. That the object will not go out of scope by
265 0           # the end of this sub we will store a reference here.
266 0           $self->env->{'plack.request.http.body'} = $body;
267             $body->cleanup(1);
268 0            
269             my $input = $self->input;
270 0            
271 0 0         my $buffer;
272             if ($self->env->{'psgix.input.buffered'}) {
273 0           # Just in case if input is read by middleware/apps beforehand
274             $input->seek(0, 0);
275 0           } else {
276             $buffer = Stream::Buffered->new($cl);
277             }
278 0            
279 0           my $spin = 0;
280 0 0         while ($cl) {
281 0           $input->read(my $chunk, $cl < 8192 ? $cl : 8192);
282 0           my $read = length $chunk;
283 0           $cl -= $read;
284 0 0         $body->add($chunk);
285             $buffer->print($chunk) if $buffer;
286 0 0 0        
287 0           if ($read == 0 && $spin++ > 2000) {
288             Carp::croak "Bad Content-Length: maybe client disconnect? ($cl bytes remaining)";
289             }
290             }
291 0 0          
292 0           if ($buffer) {
293 0           $self->env->{'psgix.input.buffered'} = 1;
294             $self->env->{'psgi.input'} = $buffer->rewind;
295 0           } else {
296             $input->seek(0, 0);
297             }
298 0            
299             $self->env->{'plack.request.body'} = Hash::MultiValue->from_mixed($body->param);
300 0            
301 0           my @uploads = Hash::MultiValue->from_mixed($body->upload)->flatten;
302 0           my @obj;
303 0           while (my($k, $v) = splice @uploads, 0, 2) {
304             push @obj, $k, $self->_make_upload($v);
305             }
306 0            
307             $self->env->{'plack.request.upload'} = Hash::MultiValue->new(@obj);
308 0            
309             1;
310             }
311              
312 0     0     sub _make_upload {
313 0           my($self, $upload) = @_;
314 0           my %copy = %$upload;
  0            
315 0           $copy{headers} = HTTP::Headers->new(%{$upload->{headers}});
316             Plack::Request::Upload->new(%copy);
317             }
318              
319             1;
320             __END__