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