File Coverage

blib/lib/HTTP/Rollup.pm
Criterion Covered Total %
statement 57 111 51.3
branch 15 56 26.7
condition 3 11 27.2
subroutine 8 12 66.6
pod 2 2 100.0
total 85 192 44.2


line stmt bran cond sub pod time code
1             package HTTP::Rollup;
2              
3             require 5.005;
4              
5 3     2   13562 use strict;
  2         86  
  2         88  
6 3     2   2476 use CGI::Util qw( unescape );
  3         14451  
  3         204  
7 3     2   30 use Exporter;
  3         470  
  3         326  
8              
9 3     2   257 use vars qw($VERSION @ISA @EXPORT_OK);
  3         12  
  3         3721  
10              
11             @ISA = qw(Exporter);
12             @EXPORT_OK = qw(RollupQueryString);
13              
14             $VERSION = '0.8';
15              
16             my $DEFAULT_DELIMITER = "&";
17              
18             # Turn on special checking for Doug MacEachern's modperl
19             my $MOD_PERL = 0;
20             if (exists $ENV{MOD_PERL}) {
21             if ($ENV{MOD_PERL_API_VERSION} == 2) {
22             $MOD_PERL = 2;
23             require Apache2::RequestUtil;
24             require APR::Table;
25             } else {
26             $MOD_PERL = 1;
27             require Apache;
28             }
29             }
30              
31             =head1 NAME
32              
33             HTTP::Rollup - translate an HTTP query string to a hierarchical structure
34              
35             =head1 SYNOPSIS
36              
37             use HTTP::Rollup qw(RollupQueryString);
38              
39             my $rollup = new HTTP::Rollup;
40              
41             my $hashref = $rollup->RollupQueryString($query_string);
42              
43             =head1 DESCRIPTION
44              
45             Given input text of the format:
46              
47             employee.name.first=Jane
48             employee.name.last=Smith
49             employee.address=123%20Main%20St.
50             employee.city=New%20York
51             id=444
52             phone=(212)123-4567
53             phone=(212)555-1212
54             @fax=(212)999-8877
55              
56             Construct an output data structure like this:
57              
58             $hashref = {
59             employee => {
60             name => {
61             "first" => "Jane",
62             "last" => "Smith",
63             },
64             address => "123 Main St.",
65             city => "New York"
66             },
67             phone => [
68             "(212)123-4567",
69             "(212)555-1212"
70             ],
71             fax => [
72             "(212)999-8877"
73             ],
74             id => 444
75             };
76              
77             This is intended as a drop-in replacement for the HTTP query string
78             parsing implemented in CGI.pm, adding the ability to assemble a nested
79             data structure (CGI.pm constructs purely flat structures).
80              
81             e.g. given the sample input above, CGI.pm would produce:
82              
83             $hashref = {
84             "employee.name.first" => [ "Jason" ],
85             "employee.name.last" => [ "Smith" ],
86             "employee.name.address" => [ "123 Main St." ],
87             "employee.name.city" => [ "New York" ],
88             "phone" => [ "(212)123-4567", "(212)555-1212" ],
89             "@fax"=> [ "(212)999-8877" ],
90             "id" => [ 444 ]
91             };
92              
93             If no $query_string parameter is provided, HTTP::Rollup will attempt to find
94             the input in the same manner used by CGI.pm (the internal _query_string
95             function is pretty much cloned from CGI.pm).
96              
97             HTTP::Rollup runs under both CGI or mod_perl contexts, and from the
98             command line (reads from @ARGV or stdin).
99              
100             =head1 FEATURES
101              
102             =over
103              
104             =item *
105              
106             Data nesting using dot notation
107              
108             =item *
109              
110             Recognizes a list if there is more than one value with the same name
111              
112             =item *
113              
114             Lists can be forced with a leading @-sign, to allow for lists that could
115             have just one element (eliminating ambiguity between scalar and single-
116             element list). The @ will be stripped.
117              
118             =back
119              
120             =head1 FUNCTIONS
121              
122             =item new([ FORCE_LIST => 1 ], [ DELIM => ";" ])
123              
124             The FORCE_LIST switch causes CGI.pm-style behavior, as above,
125             for backward compatibility.
126              
127 1     1   42948 The DELIM option specifies the input field delimiter. This is not
  1         1466  
  1         8  
128 1     1   893 auto-detected. Default is the standard ampersand, though semicolon has
  1         3  
  1         76  
129             been proposed as a replacement to avoid conflict with the ampersand used
130             for character entities.
131              
132             Specifying "\n" for the delimiter is helpful for parsing parameters on stdin.
133              
134             =item RollupQueryString()
135              
136             Workhorse function.
137              
138             =begin testing
139              
140             use lib "./blib/lib";
141             use HTTP::Rollup qw(RollupQueryString);
142             use Data::Dumper;
143              
144             my $s1 = "one=abc&two=def&three=ghi";
145             my $r1 = new HTTP::Rollup;
146             my $hr = $r1->RollupQueryString($s1); # default delimiter
147             ok ($hr->{one} eq "abc");
148             ok ($hr->{two} eq "def");
149             ok ($hr->{three} eq "ghi");
150              
151             my $string = <<_END_;
152             employee.name.first=Jane
153             employee.name.last=Smith
154             employee.address=123%20Main%20St.
155             employee.city=New%20York
156             id=444
157             phone=(212)123-4567
158             phone=(212)555-1212
159             \@fax=(212)999-8877
160             _END_
161              
162             my $r2 = new HTTP::Rollup(DELIM => "\n");
163             my $hashref = $r2->RollupQueryString($string);
164             ok($hashref->{employee}->{name}->{first} eq "Jane",
165             "2-nested scalar");
166             ok($hashref->{employee}->{city} eq "New York",
167             "1-nested scalar, with unescape");
168             ok($hashref->{id} eq "444",
169             "top-level scalar");
170             ok($hashref->{phone}->[1] eq "(212)555-1212",
171             "auto-list");
172             ok($hashref->{fax}->[0] eq "(212)999-8877",
173             "\@-list");
174              
175             my $string2 = "employee.name.first=Jane;employee.name.last=Smith;employee.address=123%20Main%20St.;employee.city=New%York;id=444;phone=(212)123-4567;phone=(212)555-1212;\@fax=(212)999-8877";
176              
177             my $r3 = new HTTP::Rollup(DELIM => ";");
178             $hashref = $r3->RollupQueryString($string2);
179             ok($hashref->{employee}->{name}->{first} eq "Jane",
180             "nested scalar");
181             ok($hashref->{id} eq "444",
182             "top-level scalar");
183             ok($hashref->{phone}->[1] eq "(212)555-1212",
184             "auto-list");
185             ok($hashref->{fax}->[0] eq "(212)999-8877",
186             "\@-list");
187              
188             my $r4 = new HTTP::Rollup(FORCE_LIST => 1, DELIM => "\n");
189             my $hashref2 = $r4->RollupQueryString($string);
190             ok($hashref2->{'employee.name.first'}->[0] eq "Jane",
191             "nested scalar");
192             ok($hashref2->{id}->[0] eq "444",
193             "top-level scalar");
194             ok($hashref2->{phone}->[1] eq "(212)555-1212",
195             "auto-list");
196             ok($hashref2->{'@fax'}->[0] eq "(212)999-8877",
197             "\@-list");
198              
199             =end testing
200              
201             =cut
202              
203             my %legal_parameters = (
204             FORCE_LIST => 1,
205             DELIM => 1,
206             );
207             sub new {
208 6     5 1 23839 my $cl = shift;
209 6   33     13826 my $class = ref($cl) || $cl;
210 6         3275 my %params = @_;
211              
212 5         226 my $self = {};
213 5         255 bless $self, $class;
214              
215 5         284 for my $param (keys %params) {
216 5 50       20 if ($legal_parameters{$param}) {
217 5         16 $self->{$param} = $params{$param};
218             } else {
219 1         9 print STDERR __PACKAGE__, ": illegal config parameter $param\n";
220             }
221             }
222              
223 5         6896 return $self;
224             }
225              
226             sub RollupQueryString {
227 5     4 1 354 my $self = shift;
228 5         484 my $input = shift;
229              
230 5   66     279 my $delimiter = $self->{DELIM} || $DEFAULT_DELIMITER;
231              
232 5 50       17 if (!defined $input) {
233 1         11 $input = _query_string();
234             }
235              
236 5         303 my $root = {};
237              
238 5 50       281 return $root if !$input;
239              
240             # query strings are name-value pairs delimited by & or by newline or semicolon
241 5         327 foreach my $nvp (split(/$delimiter/, $input)) {
242 28 50       295 last if $nvp eq "="; # sometimes appears as query string terminator
243              
244 28         202 PARSE:
245             my ($name, $value) = split /=/, $nvp;
246 27         66 my @levels = split /\./, $name;
247 27         65 $value = CGI::Util::unescape($value);
248              
249 27 100       352 if ($self->{FORCE_LIST}) {
250             # always use a list, for CGI.pm-style behavior
251 8 100       19 if (ref $root->{$name}) {
252             # there's already a list there
253 1         2 push @{$root->{$name}}, $value;
  1         2  
254             } else {
255 7         19 $root->{$name} = [ $value ];
256             }
257 8         17 next;
258             }
259              
260             TRAVERSE:
261 19         23 my $node = $root;
262 19         19 my $leaf;
263 19         50 for ($leaf = shift @levels;
264             scalar(@levels) >= 1;
265             $leaf = shift @levels) {
266 12 100       34 $node->{$leaf} = {}
267             unless defined $node->{$leaf}; # vivify
268 12         34 $node = $node->{$leaf};
269             }
270              
271             SAVE:
272 19 50       69 if (ref $node->{$leaf}) {
    100          
    100          
273             # there's already a list there
274 0         0 $leaf =~ s/^@//;
275 0         0 push @{$node->{$leaf}}, $value;
  0         0  
276             } elsif (defined $node->{$leaf}) {
277             # scalar now, convert to a list
278 2         8 $node->{$leaf} = [ $node->{$leaf}, $value ];
279             } elsif ($leaf =~ /^\@/) {
280             # leading @ forces list
281 2         6 $leaf =~ s/^@//;
282 2         10 $node->{$leaf} = [ $value ];
283             } else {
284 15         63 $node->{$leaf} = $value;
285             }
286             }
287              
288 4         36 return $root;
289             }
290              
291              
292             # Most of the following was copied from CGI.pm (some version <2.8).
293             # Frozen here to avoid breakage on CGI changes, and to allow local
294             # alterations (e.g. support for PUT).
295              
296             sub _query_string {
297 0     0   0 my $meth = $ENV{'REQUEST_METHOD'};
298 0         0 my $query_string;
299              
300 0 0       0 if (!defined $meth) {
301             # no REQUEST_METHOD, so must be command-line usage
302              
303 0         0 return _read_from_cmdline();
304             }
305              
306 0 0       0 if ($meth =~ /^(GET|HEAD)$/o) {
307 0 0       0 if ($MOD_PERL == 1) {
    0          
308 0         0 return Apache->request->args;
309             } elsif ($MOD_PERL ==2) {
310 0         0 return Apache2::RequestUtil->request->args;
311             } else {
312             # CGI mode, not mod_perl
313 0   0     0 return $ENV{QUERY_STRING} || $ENV{REDIRECT_QUERY_STRING};
314             }
315             }
316              
317             # this is a POST
318              
319 0   0     0 my $content_length = $ENV{CONTENT_LENGTH} || 0;
320              
321 0 0       0 _read_from_client(\*STDIN,
322             \$query_string,
323             $content_length,
324             0)
325             if $content_length > 0;
326              
327             # Have our cake and eat it too! (see CGI.pm)
328             # Append query string contents to the POST data.
329 0 0       0 if ($ENV{QUERY_STRING}) {
330 0 0       0 $query_string .= (length($query_string) ? '&' : '') . $ENV{QUERY_STRING};
331             }
332 0         0 return $query_string;
333             }
334              
335             sub _read_from_client {
336 0     0   0 my($fh, $buff, $len, $offset) = @_;
337 0         0 local $^W=0; # prevent a warning
338 0 0       0 return undef unless defined($fh);
339 0         0 return read($fh, $$buff, $len, $offset);
340             }
341              
342             # Note: multiple parameters on cmdline are always linked with ampersand;
343             # so better not change DELIM for this input style.
344              
345             sub _read_from_cmdline {
346 0     0   0 my($input,@words);
347 0         0 my($query_string);
348              
349 0 0       0 if (@ARGV) {
350 0         0 @words = @ARGV;
351             } else {
352 0         0 my @lines;
353 0         0 chomp(@lines = ); # remove newlines
354 0         0 $input = join(" ",@lines);
355 0         0 @words = _shellwords($input);
356             }
357 0         0 foreach (@words) {
358 0         0 s/\\=/%3D/g;
359 0         0 s/\\&/%26/g;
360             }
361              
362 0 0       0 if ("@words"=~/=/) {
363 0         0 $query_string = join('&',@words);
364             } else {
365 0         0 $query_string = join('+',@words);
366             }
367              
368 0         0 return $query_string;
369             }
370              
371             # Taken from shellwords.pl in the Perl 5.6 distribution.
372             #
373             # Usage:
374             # @words = &shellwords($line);
375             # or
376             # @words = &shellwords(@lines);
377             # or
378             # @words = &shellwords; # defaults to $_ (and clobbers it)
379              
380             sub _shellwords {
381 0 0   0   0 local ($_) = join('', @_) if @_;
382 0         0 my (@words, $snippet, $field);
383              
384 0         0 s/^\s+//;
385 0 0       0 if ($_ ne '') {
386 0         0 $field = '';
387 0         0 for (;;) {
388 0 0       0 if (s/^"(([^"\\]|\\.)*)"//) {
    0          
    0          
    0          
    0          
    0          
389 0         0 ($snippet = $1) =~ s#\\(.)#$1#g;
390             }
391             elsif (/^"/) {
392 0         0 die "Unmatched double quote: $_\n";
393             }
394             elsif (s/^'(([^'\\]|\\.)*)'//) {
395 0         0 ($snippet = $1) =~ s#\\(.)#$1#g;
396             }
397             elsif (/^'/) {
398 0         0 die "Unmatched single quote: $_\n";
399             }
400             elsif (s/^\\(.)//) {
401 0         0 $snippet = $1;
402             }
403             elsif (s/^([^\s\\'"]+)//) {
404 0         0 $snippet = $1;
405             }
406             else {
407 0         0 s/^\s+//;
408 0         0 last;
409             }
410 0         0 $field .= $snippet;
411             }
412 0         0 push(@words, $field);
413             }
414 0         0 @words;
415             }
416              
417             1;
418              
419             =head1 AUTHOR
420              
421             Jason W. May
422              
423             =head1 COPYRIGHT
424              
425             Copyright (C) 2002-2005 Jason W. May. All rights reserved.
426             This module is free software; you can redistribute it and/or
427             modify it under the same terms as Perl itself.
428              
429             =cut