File Coverage

blib/lib/URI/Query.pm
Criterion Covered Total %
statement 134 138 97.1
branch 40 54 74.0
condition 20 24 83.3
subroutine 24 24 100.0
pod 12 13 92.3
total 230 253 90.9


line stmt bran cond sub pod time code
1             #
2             # Class providing URI query string manipulation
3             #
4              
5             package URI::Query;
6              
7 8     8   65609 use 5.00503;
  8         26  
8 8     8   40 use strict;
  8         13  
  8         203  
9              
10 8     8   5857 use URI::Escape qw(uri_escape_utf8 uri_unescape);
  8         12085  
  8         524  
11 8     8   46 use Carp;
  8         14  
  8         1065  
12              
13             use overload
14             '""' => \&stringify,
15 2     2   211 'eq' => sub { $_[0]->stringify eq $_[1]->stringify },
16 8     8   4619 'ne' => sub { $_[0]->stringify ne $_[1]->stringify };
  8     2   3261  
  8         90  
  2         201  
17              
18 8     8   619 use vars q($VERSION);
  8         15  
  8         13469  
19             $VERSION = '0.11';
20              
21             # -------------------------------------------------------------------------
22             # Remove all occurrences of the given parameters
23             sub strip
24             {
25 4     4 1 9 my $self = shift;
26 4         10 foreach (@_) {
27 7 100       35 delete $self->{qq}->{$_} and $self->{changed}++;
28             }
29 4         24 $self;
30             }
31              
32             # Remove all parameters except those given
33             sub strip_except
34             {
35 3     3 1 7 my $self = shift;
36 3         7 my %keep = map { $_ => 1 } @_;
  9         23  
37 3         6 foreach (keys %{$self->{qq}}) {
  3         10  
38 13 100       35 next if $keep{$_};
39 5 50       27 delete $self->{qq}->{$_} and $self->{changed}++;
40             }
41 3         11 $self;
42             }
43              
44             # Remove all empty/undefined parameters
45             sub strip_null
46             {
47 2     2 1 5 my $self = shift;
48 2         4 foreach (keys %{$self->{qq}}) {
  2         8  
49 9 100       11 next if @{$self->{qq}->{$_}};
  9         40  
50 3 50       12 delete $self->{qq}->{$_} and $self->{changed}++;
51             }
52 2         12 $self;
53             }
54              
55             # Remove all parameters matching $re
56             sub strip_like
57             {
58 3     3 1 5 my $self = shift;
59 3 50       12 my $re = shift or croak "Missing regex param to strip_like";
60 3 50 33     19 croak "Invalid param '$re' to strip_like - must be regex" if ! ref $re || ref $re ne 'Regexp';
61 3 50       7 croak "Too many params to strip_like - only one permitted" if @_;
62              
63 3         4 foreach (keys %{$self->{qq}}) {
  3         10  
64 10 100       44 next if $_ !~ $re;
65 3 50       13 delete $self->{qq}->{$_} and $self->{changed}++;
66             }
67              
68 3         14 $self;
69             }
70              
71             # Replace all occurrences of the given parameters
72             sub replace
73             {
74 2     2 1 23 my $self = shift;
75 2         7 my %arg = @_;
76 2         6 for my $key (keys %arg) {
77 5         11 $self->{qq}->{$key} = [];
78 5 100       17 if (ref $arg{$key} eq 'ARRAY') {
79 1         3 push @{$self->{qq}->{$key}}, $_ foreach @{$arg{$key}};
  1         4  
  3         9  
80             }
81             else {
82 4         5 push @{$self->{qq}->{$key}}, $arg{$key};
  4         10  
83             }
84 5         12 $self->{changed}++;
85             }
86 2         11 $self;
87             }
88              
89             # Return the stringified qq hash
90             sub stringify
91             {
92 83     83 1 2932 my $self = shift;
93 83   100     453 my $sep = shift || $self->{sep} || '&';
94 83         135 my @out = ();
95 83         97 for my $key (sort keys %{$self->{qq}}) {
  83         348  
96 233         3558 for my $value (@{$self->{qq}->{$key}}) {
  233         506  
97 337         3006 push @out, sprintf("%s=%s", uri_escape_utf8($key), uri_escape_utf8($value));
98             }
99             }
100 83         1916 join $sep, @out;
101             }
102              
103             sub revert
104             {
105 4     4 1 507 my $self = shift;
106             # Revert qq to the qq_orig hashref
107 4         11 $self->{qq} = $self->_deepcopy($self->{qq_orig});
108 4         39 $self->{changed} = 0;
109 4         9 $self;
110             }
111              
112             sub has_changed {
113 9     9 1 16 my $self = shift;
114 9 100       41 $self->{changed} > 0 ? 1 : 0;
115             }
116              
117             # -------------------------------------------------------------------------
118             # Convenience methods
119              
120             # Return the current qq hash(ref) with one-elt arrays flattened
121             sub hash
122             {
123 4     4 1 7392 my $self = shift;
124 4         6 my %qq = %{$self->{qq}};
  4         19  
125             # Flatten one element arrays
126 4         18 for (sort keys %qq) {
127 10 100       28 $qq{$_} = $qq{$_}->[0] if @{$qq{$_}} == 1;
  10         45  
128             }
129 4 50       42 return wantarray ? %qq : \%qq;
130             }
131              
132             # Return the current qq hash(ref) with all elements as arrayrefs
133             sub hash_arrayref
134             {
135 1     1 1 16525 my $self = shift;
136 1         3 my %qq = %{$self->{qq}};
  1         7  
137             # (Don't flatten one element arrays)
138 1 50       9 return wantarray ? %qq : \%qq;
139             }
140              
141             # Return the current query as a string of html hidden input tags
142             sub hidden
143             {
144 1     1 1 2939 my $self = shift;
145 1         4 my $str = '';
146 1         2 for my $key (sort keys %{$self->{qq}}) {
  1         8  
147 4         6 for my $value (@{$self->{qq}->{$key}}) {
  4         11  
148 6         23 $str .= qq(\n);
149             }
150             }
151 1         5 return $str;
152             }
153              
154             # -------------------------------------------------------------------------
155             # Set the output separator to use by default
156             sub separator
157             {
158 1     1 1 471 my $self = shift;
159 1         4 $self->{sep} = shift;
160             }
161              
162             # Deep copy routine, originally swiped from a Randal Schwartz column
163             sub _deepcopy
164             {
165 220     220   296 my ($self, $this) = @_;
166 220 100       566 if (! ref $this) {
    100          
    50          
    0          
    0          
167 110         450 return $this;
168             } elsif (ref $this eq "ARRAY") {
169 81         204 return [map $self->_deepcopy($_), @$this];
170             } elsif (ref $this eq "HASH") {
171 29         142 return {map { $_ => $self->_deepcopy($this->{$_}) } keys %$this};
  81         209  
172             } elsif (ref $this eq "CODE") {
173 0         0 return $this;
174             } elsif (sprintf $this) {
175             # Object! As a last resort, try copying the stringification value
176 0         0 return sprintf $this;
177             } else {
178 0         0 die "what type is $_? (" . ref($this) . ")";
179             }
180             }
181              
182             # Parse query string, storing as hash (qq) of key => arrayref pairs
183             sub _parse_qs
184             {
185 14     14   22 my $self = shift;
186 14         21 my $qs = shift;
187 14         90 for (split /[&;]/, $qs) {
188 64         150 my ($key, $value) = map { uri_unescape($_) } split /=/, $_, 2;
  128         650  
189 64   100     903 $self->{qq}->{$key} ||= [];
190 64 100 66     334 push @{$self->{qq}->{$key}}, $value if defined $value && $value ne '';
  63         193  
191             }
192             $self
193 14         44 }
194              
195             # Process arrayref arguments into hash (qq) of key => arrayref pairs
196             sub _init_from_arrayref
197             {
198 6     6   12 my ($self, $arrayref) = @_;
199 6         20 while (@$arrayref) {
200 24         176 my $key = shift @$arrayref;
201 24         33 my $value = shift @$arrayref;
202 24         57 my $key_unesc = uri_unescape($key);
203              
204 24   100     228 $self->{qq}->{$key_unesc} ||= [];
205 24 100 100     116 if (defined $value && $value ne '') {
206 22         32 my @values;
207 22 100       46 if (! ref $value) {
    50          
208 20         43 @values = split "\0", $value;
209             }
210             elsif (ref $value eq 'ARRAY') {
211 2         5 @values = @$value;
212             }
213             else {
214 0         0 die "Invalid value found: $value. Not string or arrayref!";
215             }
216 22         28 push @{$self->{qq}->{$key_unesc}}, map { uri_unescape($_) } @values;
  22         58  
  26         80  
217             }
218             }
219             }
220              
221             # Constructor - either new($qs) where $qs is a scalar query string or a
222             # a hashref of key => value pairs, or new(key => val, key => val);
223             # In the array form, keys can repeat, and/or values can be arrayrefs.
224             sub new
225             {
226 25     25 0 4683 my $class = shift;
227 25         85 my $self = bless { qq => {} }, $class;
228 25 100 100     255 if (@_ == 1 && ! ref $_[0] && $_[0]) {
    100 66        
    100 100        
229 14         46 $self->_parse_qs($_[0]);
230             }
231             elsif (@_ == 1 && ref $_[0] eq 'HASH') {
232 3         6 $self->_init_from_arrayref([ %{$_[0]} ]);
  3         27  
233             }
234             elsif (scalar(@_) % 2 == 0) {
235 3         12 $self->_init_from_arrayref(\@_);
236             }
237              
238             # Clone the qq hashref to allow reversion
239 25         125 $self->{qq_orig} = $self->_deepcopy($self->{qq});
240              
241             # Changed flag
242 25         61 $self->{changed} = 0;
243              
244 25         112 return $self;
245             }
246             # -------------------------------------------------------------------------
247              
248             1;
249              
250             =head1 NAME
251              
252             URI::Query - class providing URI query string manipulation
253              
254             =head1 SYNOPSIS
255              
256             # Constructor - using a GET query string
257             $qq = URI::Query->new($query_string);
258             # OR Constructor - using a hashref of key => value parameters
259             $qq = URI::Query->new($cgi->Vars);
260             # OR Constructor - using an array of successive keys and values
261             $qq = URI::Query->new(@params);
262              
263             # Revert back to the initial constructor state (to do it all again)
264             $qq->revert;
265              
266             # Remove all occurrences of the given parameters
267             $qq->strip('page', 'next');
268              
269             # Remove all parameters except the given ones
270             $qq->strip_except('pagesize', 'order');
271              
272             # Remove all empty/undefined parameters
273             $qq->strip_null;
274              
275             # Replace all occurrences of the given parameters
276             $qq->replace(page => $page, foo => 'bar');
277              
278             # Set the argument separator to use for output (default: unescaped '&')
279             $qq->separator(';');
280              
281             # Output the current query string
282             print "$qq"; # OR $qq->stringify;
283             # Stringify with explicit argument separator
284             $qq->stringify(';');
285              
286             # Get a flattened hash/hashref of the current parameters
287             # (single item parameters as scalars, multiples as an arrayref)
288             my %qq = $qq->hash;
289              
290             # Get a non-flattened hash/hashref of the current parameters
291             # (parameter => arrayref of values)
292             my %qq = $qq->hash_arrayref;
293              
294             # Get the current query string as a set of hidden input tags
295             print $qq->hidden;
296              
297             # Check whether the query has changed since construction
298             if ($qq->has_changed) {
299             print "changed version: $qq\n";
300             }
301              
302              
303             =head1 DESCRIPTION
304              
305             URI::Query provides simple URI query string manipulation, allowing you
306             to create and manipulate URI query strings from GET and POST requests in
307             web applications. This is primarily useful for creating links where you
308             wish to preserve some subset of the parameters to the current request,
309             and potentially add or replace others. Given a query string this is
310             doable with regexes, of course, but making sure you get the anchoring
311             and escaping right is tedious and error-prone - this module is simpler.
312              
313             =head2 CONSTRUCTOR
314              
315             URI::Query objects can be constructed from scalar query strings
316             ('foo=1&bar=2&bar=3'), from a hashref which has parameters as keys, and
317             values either as scalars or arrayrefs of scalars (to handle the case of
318             parameters with multiple values e.g. { foo => '1', bar => [ '2', '3' ] }),
319             or arrays composed of successive parameters-value pairs
320             e.g. ('foo', '1', 'bar', '2', 'bar', '3'). For instance:
321              
322             # Constructor - using a GET query string
323             $qq = URI::Query->new($query_string);
324              
325             # Constructor - using an array of successive keys and values
326             $qq = URI::Query->new(@params);
327              
328             # Constructor - using a hashref of key => value parameters,
329             # where values are either scalars or arrayrefs of scalars
330             $qq = URI::Query->new($cgi->Vars);
331              
332             URI::Query also handles L-style hashrefs, where multiple
333             values are packed into a single string, separated by the "\0" (null)
334             character.
335              
336             All keys and values are URI unescaped at construction time, and are
337             stored and referenced unescaped. So a query string like:
338              
339             group=prod%2Cinfra%2Ctest&op%3Aset=x%3Dy
340              
341             is stored as:
342              
343             'group' => 'prod,infra,test'
344             'op:set' => 'x=y'
345              
346             You should always use the unescaped/normal variants in methods i.e.
347              
348             $qq->replace('op:set' => 'x=z');
349              
350             NOT:
351              
352             $qq->replace('op%3Aset' => 'x%3Dz');
353              
354              
355             =head2 MODIFIER METHODS
356              
357             All modifier methods change the state of the URI::Query object in some
358             way, and return $self, so they can be used in chained style e.g.
359              
360             $qq->revert->strip('foo')->replace(bar => 123);
361              
362             Note that URI::Query stashes a copy of the parameter set that existed
363             at construction time, so that any changes made by these methods can be
364             rolled back using 'revert()'. So you don't (usually) need to keep
365             multiple copies around to handle incompatible changes.
366              
367             =over 4
368              
369             =item revert()
370              
371             Revert the current parameter set back to that originally given at
372             construction time i.e. discard all changes made since construction.
373              
374             =item strip($param1, $param2, ...)
375              
376             Remove all occurrences of the given parameters and their values from
377             the current parameter set.
378              
379             =item strip_except($param1, $param2, ...)
380              
381             Remove all parameters EXCEPT those given from the current parameter
382             set.
383              
384             =item strip_null()
385              
386             Remove all parameters that have a value of undef from the current
387             parameter set.
388              
389             =item replace($param1 => $value1, $param2, $value2, ...)
390              
391             Replace the values of the given parameters in the current parameter set
392             with these new ones. Parameter names must be scalars, but values can be
393             either scalars or arrayrefs of scalars, when multiple values are desired.
394              
395             Note that 'replace' can also be used to add or append, since there's
396             no requirement that the parameters already exist in the current parameter
397             set.
398              
399             =item strip_like($regex)
400              
401             Remove all parameters whose names match the given (qr-quoted) regex e.g.
402              
403             $qq->strip_like(qr/^utm/)
404              
405             Does NOT match against parameter values.
406              
407             =item separator($separator)
408              
409             Set the argument separator to use for output. Default: '&'.
410              
411             =back
412              
413             =head2 ACCESSOR METHODS
414              
415             =over 4
416              
417             =item has_changed()
418              
419             If the query is actually changed by any of the modifier methods (strip,
420             strip_except, strip_null, strip_like, or replace) it sets an internal
421             changed flag which can be access by:
422              
423             $qq->has_changed
424              
425             revert() resets the has_changed flag to false.
426              
427             =back
428              
429             =head2 OUTPUT METHODS
430              
431             =over 4
432              
433             =item "$qq", stringify(), stringify($separator)
434              
435             Return the current parameter set as a conventional param=value query
436             string, using $separator as the separator if given. e.g.
437              
438             foo=1&bar=2&bar=3
439              
440             Note that all parameters and values are URI escaped by stringify(), so
441             that query-string reserved characters do not occur within elements. For
442             instance, a parameter set of:
443              
444             'group' => 'prod,infra,test'
445             'op:set' => 'x=y'
446              
447             will be stringified as:
448              
449             group=prod%2Cinfra%2Ctest&op%3Aset=x%3Dy
450              
451             =item hash()
452              
453             Return a hash (in list context) or hashref (in scalar context) of the
454             current parameter set. Single-item parameters have scalar values, while
455             while multiple-item parameters have arrayref values e.g.
456              
457             {
458             foo => 1,
459             bar => [ 2, 3 ],
460             }
461              
462             =item hash_arrayref()
463              
464             Return a hash (in list context) or hashref (in scalar context) of the
465             current parameter set. All values are returned as arrayrefs, including
466             those with single values e.g.
467              
468             {
469             foo => [ 1 ],
470             bar => [ 2, 3 ],
471             }
472              
473             =item hidden()
474              
475             Returns the current parameter set as a concatenated string of hidden
476             input tags, one per parameter-value e.g.
477              
478            
479            
480            
481              
482             =back
483              
484             =head1 BUGS AND CAVEATS
485              
486             Please report bugs and/or feature requests to
487             C, or through
488             the web interface at
489             L.
490              
491             Should allow unescaping of input to be turned off, for situations in
492             which it's already been done. Please let me know if you find you
493             actually need this.
494              
495             I don't think it makes sense on the output side though, since you need
496             to understand the structure of the query to escape elements correctly.
497              
498              
499             =head1 PATCHES
500              
501             URI::Query code lives at L.
502             Patches / pull requests welcome!
503              
504              
505             =head1 AUTHOR
506              
507             Gavin Carr
508              
509              
510             =head1 COPYRIGHT
511              
512             Copyright 2004-2015, Gavin Carr.
513              
514             This program is free software. You may copy or redistribute it under the
515             same terms as perl itself.
516              
517             =cut
518              
519             =for Pod::Coverage new
520              
521             # vim:sw=4:et