File Coverage

blib/lib/Web/Dispatch/ParamParser.pm
Criterion Covered Total %
statement 69 73 94.5
branch 20 26 76.9
condition 19 34 55.8
subroutine 9 9 100.0
pod 0 4 0.0
total 117 146 80.1


line stmt bran cond sub pod time code
1             package Web::Dispatch::ParamParser;
2              
3 6     6   20564 use strict;
  6         11  
  6         271  
4 6     6   27 use warnings FATAL => 'all';
  6         12  
  6         311  
5              
6 6     6   3829 use Encode 'decode_utf8';
  6         62255  
  6         7590  
7              
8             sub UNPACKED_QUERY () { __PACKAGE__.'.unpacked_query' }
9             sub UNPACKED_BODY () { __PACKAGE__.'.unpacked_body' }
10             sub UNPACKED_BODY_OBJECT () { __PACKAGE__.'.unpacked_body_object' }
11             sub UNPACKED_UPLOADS () { __PACKAGE__.'.unpacked_uploads' }
12             sub ORIG_ENV () { 'Web::Dispatch.original_env' }
13              
14             sub get_unpacked_query_from {
15 101   66 101 0 516 return ($_[0]->{+ORIG_ENV}||$_[0])->{+UNPACKED_QUERY} ||= do {
      33        
16             _unpack_params($_[0]->{QUERY_STRING})
17 101         226 };
18             }
19              
20             sub get_unpacked_body_from {
21 26   66 26 0 102 return ($_[0]->{+ORIG_ENV}||$_[0])->{+UNPACKED_BODY} ||= do {
      66        
22 13   100     52 my $ct = lc($_[0]->{CONTENT_TYPE}||'');
23 13 100       61 if (!$_[0]->{CONTENT_LENGTH}) {
    100          
    50          
24             {}
25 2         9 } elsif (index($ct, 'application/x-www-form-urlencoded') >= 0) {
26 6         54 $_[0]->{'psgi.input'}->read(my $buf, $_[0]->{CONTENT_LENGTH});
27 6         57 _unpack_params($buf);
28             } elsif (index($ct, 'multipart/form-data') >= 0) {
29 5         15 my $p = get_unpacked_body_object_from($_[0])->param;
30             # forcible arrayification (functional, $p does not belong to us,
31             # do NOT replace this with a side-effect ridden "simpler" version)
32             +{
33             map +(ref($p->{$_}) eq 'ARRAY'
34             ? ($_ => $p->{$_})
35 5 100       73 : ($_ => [ $p->{$_} ])
36             ), keys %$p
37             };
38             } else {
39             {}
40 0         0 }
41             };
42             }
43              
44             sub get_unpacked_body_object_from {
45             # we may have no object at all - so use a single element arrayref for ||=
46 12   33 12 0 49 return (($_[0]->{+ORIG_ENV}||$_[0])->{+UNPACKED_BODY_OBJECT} ||= do {
      66        
47 8 100 50     34 if (!$_[0]->{CONTENT_LENGTH}) {
    100          
48 2         13 [ undef ]
49             } elsif (index(lc($_[0]->{CONTENT_TYPE}||''),'multipart/form-data')==-1) {
50 2         7 [ undef ]
51             } else {
52 4         7 [ _make_http_body($_[0]) ]
53             }
54             })->[0];
55             }
56              
57             sub get_unpacked_uploads_from {
58 7   33 7 0 19 $_[0]->{+UNPACKED_UPLOADS} ||= do {
59 7         447 require Web::Dispatch::Upload; require HTTP::Headers;
  7         18  
60 7         12 my ($final, $reason) = (
61             {}, "field %s exists with value %s but body was not multipart/form-data"
62             );
63 7 100       12 if (my $body = get_unpacked_body_object_from($_[0])) {
64 3         11 my $u = $body->upload;
65 3         13 $reason = "field %s exists with value %s but was not an upload";
66 3         8 foreach my $k (keys %$u) {
67 1 50       4 foreach my $v (ref($u->{$k}) eq 'ARRAY' ? @{$u->{$k}} : $u->{$k}) {
  0         0  
68 1   50     7 push(@{$final->{$k}||=[]}, Web::Dispatch::Upload->new(
69 1         7 %{$v},
70             headers => HTTP::Headers->new($v->{headers})
71 1         2 ));
72             }
73             }
74             }
75 7         64 my $b = get_unpacked_body_from($_[0]);
76 7         16 foreach my $k (keys %$b) {
77 5 50       12 next if $final->{$k};
78 5         6 foreach my $v (@{$b->{$k}}) {
  5         9  
79 5 50       9 next unless $v;
80 5   50     2 push(@{$final->{$k}||=[]}, Web::Dispatch::NotAnUpload->new(
  5         55  
81             filename => $v,
82             reason => sprintf($reason, $k, $v)
83             ));
84             }
85             }
86 7         25 $final;
87             };
88             }
89              
90             {
91             # shamelessly stolen from HTTP::Body::UrlEncoded by Christian Hansen
92              
93             my $DECODE = qr/%([0-9a-fA-F]{2})/;
94              
95             my %hex_chr;
96              
97             foreach my $num ( 0 .. 255 ) {
98             my $h = sprintf "%02X", $num;
99             $hex_chr{ lc $h } = $hex_chr{ uc $h } = chr $num;
100             }
101              
102             sub _unpack_params {
103 108     108   128 my %unpack;
104 108         501 (my $params = $_[0]) =~ s/\+/ /g;
105 108         134 my ($name, $value);
106 108         1106 foreach my $pair (split(/[&;](?:\s+)?/, $params)) {
107 853 100       2775 $value = 1 unless (($name, $value) = split(/=/, $pair, 2)) == 2;
108              
109 853         17122 s/$DECODE/$hex_chr{$1}/gs for ($name, $value);
110 853         1957 $_ = decode_utf8 $_ for ($name, $value);
111              
112 853   100     44684 push(@{$unpack{$name}||=[]}, $value);
  853         4488  
113             }
114 108         747 \%unpack;
115             }
116             }
117              
118             {
119             # shamelessly stolen from Plack::Request by miyagawa
120              
121             sub _make_http_body {
122              
123             # Can't actually do this yet, since Plack::Request deletes the
124             # header structure out of the uploads in its copy of the body.
125             # I suspect I need to supply miyagawa with a failing test.
126              
127             #if (my $plack_body = $_[0]->{'plack.request.http.body'}) {
128             # # Plack already constructed one; probably wasteful to do it again
129             # return $plack_body;
130             #}
131              
132 4     4   433 require HTTP::Body;
133 4         14838 my $body = HTTP::Body->new(@{$_[0]}{qw(CONTENT_TYPE CONTENT_LENGTH)});
  4         21  
134 4         295 $body->cleanup(1);
135 4         15 my $spin = 0;
136 4         7 my $input = $_[0]->{'psgi.input'};
137 4         6 my $cl = $_[0]->{CONTENT_LENGTH};
138 4         8 while ($cl) {
139 4 50       22 $input->read(my $chunk, $cl < 8192 ? $cl : 8192);
140 4         24 my $read = length $chunk;
141 4         4 $cl -= $read;
142 4         10 $body->add($chunk);
143              
144 4 50 33     1447 if ($read == 0 && $spin++ > 2000) {
145 0         0 require Carp;
146 0         0 Carp::croak("Bad Content-Length: maybe client disconnect? ($cl bytes remaining)");
147             }
148             }
149 4         26 return $body;
150             }
151             }
152              
153             1;