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