File Coverage

blib/lib/REST/Utils.pm
Criterion Covered Total %
statement 106 106 100.0
branch 39 40 97.5
condition 40 42 95.2
subroutine 19 19 100.0
pod 9 9 100.0
total 213 216 98.6


line stmt bran cond sub pod time code
1              
2             =head1 NAME
3              
4             REST::Utils - Utility functions for REST applications
5              
6             =head1 SYNOPSIS
7              
8             use REST::Utils qw( :all );
9              
10             =cut
11              
12             package REST::Utils;
13              
14 5     5   907766 use base qw( Exporter );
  5         14  
  5         445  
15 5     5   27 use warnings;
  5         11  
  5         153  
16 5     5   26 use strict;
  5         13  
  5         193  
17 5     5   28 use Carp qw( croak );
  5         10  
  5         333  
18 5     5   25 use Scalar::Util qw( looks_like_number );
  5         8  
  5         512  
19 5     5   27 use constant NOT_FIT => -1;
  5         24  
  5         349  
20 5     5   24 use constant HUNDRED_PERCENT => 100;
  5         14  
  5         227  
21 5     5   23 use constant TEN_PERCENT => 10;
  5         10  
  5         369  
22 5     5   26 use constant POST_UNLIMITED => -1;
  5         20  
  5         8891  
23              
24             =head1 VERSION
25              
26             This document describes REST::Utils Version 0.6
27              
28             =cut
29              
30             our $VERSION = '0.6';
31              
32             =head1 DESCRIPTION
33              
34             This module contains some functions that are useful for implementing REST
35             applications.
36              
37             =cut
38              
39             our @EXPORT_OK = qw/ best_match get_body fitness_and_quality_parsed
40             media_type parse_media_range parse_mime_type quality quality_parsed
41             request_method /;
42              
43             our %EXPORT_TAGS = ( 'all' => [@EXPORT_OK] );
44              
45             =head2 FUNCTIONS
46              
47             The following functions are available. None of them are exported by default.
48             You can give the tag :all to the C statement to import all
49             the functions at once.
50              
51             =head3 best_match(\@supported, $header)
52              
53             Takes an arrayref of supported MIME types and finds the best match for
54             all the media-ranges listed in $header. The value of $header must be a
55             string that conforms to the format of the HTTP Accept: header. The
56             value of @supported is a list of MIME types. If no type can be matched,
57             C is returned.
58              
59             Example:
60              
61             print best_match(['application/xbel+xml', 'text/xml'],
62             'text/*;q=0.5,*/*; q=0.1');
63             # text/xml
64              
65             =cut
66              
67             sub best_match {
68 20     20 1 424 my ( $supported, $header ) = @_;
69 20         71 my @parsed_header = map { [ parse_media_range($_) ] } split /,/msx, $header;
  34         69  
70 36 50       149 my @weighted_matches =
71 49         118 sort { $a->[0][0] <=> $b->[0][0] || $a->[0][1] <=> $b->[0][1] }
72 20         30 map { [ [ fitness_and_quality_parsed( $_, @parsed_header ) ], $_ ] }
73 20         40 @{$supported};
74 20 100       151 return $weighted_matches[-1][0][1] ? $weighted_matches[-1][1] : undef;
75             }
76              
77             =head3 get_body($cgi)
78              
79             This function takes a L or compatible object as its first parameter.
80              
81             It will retrieve the body of an HTTP request regardless of the request method.
82              
83             If the body is larger than L.pms' POST_MAX variable allows or if
84             C<$ENV{CONTENT_LENGTH}> reports a bigger size than is actually available,
85             get_body() will return undef.
86              
87             If there is no body or if C<$ENV{CONTENT_LENGTH}> is undefined, it will
88             return an empty string.
89              
90             Otherwise it will return a scalar containing the body as a sequence of bytes
91             up to the size of C<$ENV{CONTENT_LENGTH}>
92              
93             It is up to you to turn the bytes returned by get_body() into something
94             useful.
95              
96             =cut
97              
98             # bits of this taken from derby http://www.perlmonks.org/?node_id=609632
99             sub get_body {
100 10     10 1 144688 my ($cgi) = @_;
101              
102 10         29 my $content = undef;
103 10         36 my $method = request_method($cgi);
104              
105 10   100     50 my $len = $ENV{CONTENT_LENGTH} || 0;
106              
107 10 100 100     55 if ( $CGI::POST_MAX != POST_UNLIMITED && $len > $CGI::POST_MAX ) {
108 2         5 return;
109             }
110              
111 8 100       27 if ( defined $cgi->param('POSTDATA') ) {
    100          
112 2         44 $content = $cgi->param('POSTDATA');
113             }
114             elsif ( defined $cgi->param('PUTDATA') ) {
115 1         42 $content = $cgi->param('PUTDATA');
116             }
117             else {
118              
119             # we may not get all the data we want with a single read on large
120             # POSTs as it may not be here yet! Credit Jason Luther for patch
121             # CGI.pm < 2.99 suffers from same bug -- derby
122 5         287 while ( sysread STDIN, ( my $buffer ), $len ) {
123 1         71 $content .= $buffer;
124             }
125             }
126              
127             # To make sure it is not undef at this point.
128 8 100       95 return defined $content ? $content : q{};
129             }
130              
131             =head3 fitness_and_quality_parsed($mime_type, @parsed_ranges)
132              
133             Find the best match for a given mime-type against a list of media_ranges that
134             have already been parsed by parse_media_range(). Returns a list of the fitness
135             value and the value of the 'q' quality parameter of the best match, or (-1, 0)
136             if no match was found. Just as for quality_parsed(), @parsed_ranges must be a
137             list of parsed media ranges.
138              
139             =cut
140              
141             sub fitness_and_quality_parsed {
142 56     56 1 104 my ( $mime_type, @parsed_ranges ) = @_;
143 56         89 my $best_fitness = NOT_FIT;
144 56         60 my $best_fit_q = 0;
145 56         80 my ( $target_type, $target_subtype, $target_params ) =
146             parse_media_range($mime_type);
147 56 100       74 while ( my ( $type, $subtype, $params ) = @{ shift @parsed_ranges || [] } )
  173         685  
148             {
149 117 100       173 $subtype = defined $subtype ? $subtype : q{};
150 117 100 100     781 if (
      100        
      66        
151             ( $type eq $target_type || $type eq q{*} || $target_type eq q{*} )
152             && ( $subtype eq $target_subtype
153             || $subtype eq q{*}
154             || $target_subtype eq q{*} )
155             )
156             {
157 77 100 100     290 my $param_matches = scalar grep {
158 62         132 $_ ne 'q'
159             && exists $params->{$_}
160             && $target_params->{$_} eq $params->{$_}
161             }
162 62         61 keys %{$target_params};
163 62 100       123 my $fitness =
164             $type eq $target_type
165             ? HUNDRED_PERCENT
166             : 0;
167 62 100       103 $fitness +=
168             $subtype eq $target_subtype
169             ? TEN_PERCENT
170             : 0;
171 62         62 $fitness += $param_matches;
172 62 100       124 if ( $fitness > $best_fitness ) {
173 40         38 $best_fitness = $fitness;
174 40         88 $best_fit_q = $params->{q};
175             }
176             }
177             }
178              
179 56         348 return $best_fitness, $best_fit_q;
180             }
181              
182             =head3 media_type($cgi, \@types)
183              
184             This function takes a L or compatible object as its first parameter
185             and a reference to a list of MIME media types as the second parameter. It
186             returns the member of the list most preferred by the requestor.
187              
188             Example:
189              
190             my $preferred = media_type($cgi, ['text/html', 'text/plain', '*/*']);
191              
192             If the incoming request is a C or C, the function will return
193             the member of the C listref which is most preferred based on the
194             C HTTP headers sent by the requestor. If the requestor wants a
195             type which is not on the list, the function will return C. (HINT:
196             you can specify ' */*' to match every MIME media type.)
197              
198             For C or C requests, the function will compare the MIME media
199             type in the C HTTP header provided by the requestor with
200             the list and return that type if it matches a member of the list or
201             C if it doesn't.
202              
203             For other HTTP requests (such as C) this function will always return
204             an empty string.
205              
206             =cut
207              
208             sub media_type {
209 10     10 1 197030 my ( $cgi, $types ) = @_;
210              
211             # Get the preferred MIME media type. Other HTTP verbs than the ones below
212             # (and DELETE) are not covered. Should they be?
213 10         37 my $req = request_method($cgi);
214 10         20 my $media_type = undef;
215 10 100 100     72 if ( $req eq 'GET' || $req eq 'HEAD' ) {
    100 100        
216 4         89 $media_type = best_match( $types, $cgi->http('accept') );
217             }
218             elsif ( $req eq 'POST' || $req eq 'PUT' ) {
219 5         96 $media_type = best_match( $types, $cgi->content_type );
220             }
221             else {
222 1         3 $media_type = q{};
223             }
224              
225 10         33 return $media_type;
226             }
227              
228             =head3 parse_media_range($range)
229              
230             Carves up a media range and returns a list of the C<($type, $subtype,\%params)>
231             where %params is a hash of all the parameters for the media range.
232              
233             For example, the media range 'application/*;q=0.5' would get
234             parsed into:
235              
236             ('application', '*', { q => 0.5 })
237              
238             In addition this function also guarantees that there is a value for 'q' in the
239             %params hash, filling it in with a proper default if necessary.
240              
241             =cut
242              
243             sub parse_media_range {
244 129     129 1 185 my ($range) = @_;
245 129         205 my ( $type, $subtype, $params ) = parse_mime_type($range);
246              
247 129 100 100     1030 if ( !exists $params->{q}
      100        
      100        
248             || !$params->{q}
249             || !looks_like_number( $params->{q} )
250             || $params->{q} > 1
251             || $params->{q} < 0 )
252             {
253 82         149 $params->{q} = 1;
254             }
255 129         533 return $type, $subtype, $params;
256             }
257              
258             =head3 parse_mime_type($mime_type)
259              
260             Carves up a MIME type and returns a list of the ($type, $subtype,
261             \%params) where %params is a hash of all the parameters for the media range.
262              
263             For example, the media range 'application/xhtml;q=0.5' would get parsed into:
264              
265             ('application', 'xhtml', { q => 0.5 })
266              
267             =cut
268              
269             sub parse_mime_type {
270 129     129 1 151 my ($mime_type) = @_;
271              
272 129         313 my @parts = split /;/msx, $mime_type;
273 138         234 my %params =
274 129         279 map { _strip($_) } map { split /=/msx, $_, 2 } @parts[ 1 .. $#parts ];
  69         171  
275 129         271 my $full_type = _strip( $parts[0] );
276              
277             # Java URLConnection class sends an Accept header that includes a single
278             # "*" Turn it into a legal wildcard.
279              
280 129 100       295 if ( $full_type eq q{*} ) {
281 1         3 $full_type = q{*/*};
282             }
283 129         287 my ( $type, $subtype ) = split m{/}msx, $full_type;
284 129         211 return _strip($type), _strip($subtype), \%params;
285             }
286              
287             =head3 quality($mime_type, $ranges)
288              
289             Returns the quality 'q' of a MIME type when compared against the
290             media-ranges in $ranges. For example:
291              
292             print quality('text/html', 'text/*;q=0.3, text/html;q=0.7, text/html;level
293             # 0.7
294              
295             =cut
296              
297             sub quality {
298 6     6 1 32 my ( $mime_type, $ranges ) = @_;
299 6         21 my @parsed_ranges = map { [ parse_media_range($_) ] } split /,/msx, $ranges;
  30         47  
300 6         16 return quality_parsed( $mime_type, @parsed_ranges );
301             }
302              
303             =head3 quality_parsed($mime_type, @parsed_ranges)
304              
305             Find the best match for a given MIME type against a list of media_ranges
306             that have already been parsed by parse_media_range(). Returns the 'q'
307             quality parameter of the best match, 0 if no match was found. This
308             function behaves the same as quality() except that @parsed_ranges must
309             be a list of parsed media ranges.
310              
311             =cut
312              
313             sub quality_parsed {
314 6     6 1 12 my (@args) = @_;
315              
316 6         16 return ( fitness_and_quality_parsed(@args) )[1];
317             }
318              
319             =head3 request_method($cgi)
320              
321             This function returns the query's HTTP request method.
322              
323             Example 1:
324              
325             my $method = request_method($cgi);
326            
327             This function takes a L or compatible object as its first parameter.
328              
329             Because many web sites don't allow the full set of HTTP methods needed
330             for REST, you can "tunnel" methods through C or C requests in
331             the following ways:
332              
333             In the query with the C<_method> parameter. This will work even with C
334             requests where parameters are usually passed in the request body.
335              
336             Example 2:
337              
338             http://localhost/index.cgi?_method=DELETE
339              
340             Or with the C HTTP header.
341              
342             Example 3:
343              
344             X-HTTP-METHOD-OVERRIDE: PUT
345            
346             if more than one of these are present, the HTTP header will override the query
347             parameter, which will override the "real" method.
348              
349             Any method can be tunneled through a C request. Only C and C
350             can be tunneled through a C request. You cannot tunnel through a
351             C, C, C, or any other request. If an invalid tunnel is
352             attempted, it will be ignored.
353              
354             =cut
355              
356             sub request_method {
357 46     46 1 207477 my ($cgi) = @_;
358              
359 46   100     1029 my $real_method = uc( $cgi->request_method() || q{} );
360 46   100     7991 my $tunnel_method =
361             uc( $cgi->http('X-HTTP-Method-Override')
362             || $cgi->url_param('_method')
363             || $cgi->param('_method')
364             || q{} )
365             || undef;
366              
367 46 100       7434 return $real_method if !defined $tunnel_method;
368              
369             # POST can tunnel any method.
370 27 100       113 return $tunnel_method if $real_method eq 'POST';
371              
372             # GET can only tunnel GET/HEAD
373 11 100 100     94 if ( $real_method eq 'GET'
      66        
374             && ( $tunnel_method eq 'GET' || $tunnel_method eq 'HEAD' ) )
375             {
376 4         16 return $tunnel_method;
377             }
378              
379 7         24 return $real_method;
380             }
381              
382             # utility function
383             sub _strip {
384 525     525   605 my $s = shift;
385 525         1443 $s =~ s/^\s*//msx;
386 525         1669 $s =~ s/\s*$//msx;
387 525         1357 return $s;
388             }
389              
390             =head1 SUPPORT
391              
392             You can find documentation for this module with the perldoc command.
393              
394             perldoc Rest::Utils
395            
396             You can also look for information at:
397              
398             =over 4
399              
400             =item * RT: CPAN's request tracker
401              
402             L
403              
404             =item * AnnoCPAN: Annotated CPAN documentation
405              
406             L
407              
408             =item * CPAN Ratings
409              
410             L
411              
412             =item * Search CPAN
413              
414             L
415              
416             =back
417              
418             =head1 BUGS
419              
420             There are no known problems with this module.
421              
422             Please report any bugs or feature requests to
423             C, or through the web interface at
424             L.
425             I will be notified, and then you'll automatically be notified of progress on
426             your bug as I make changes.
427              
428             =head1 THANKS
429              
430             MIME type parsing code borrowed from MIMEParser.pm by:
431             Joe Gregorio C<< joe at bitworking.org >>
432             Stanis Trendelenburg C<< stanis.trendelenburg at gmail.com >>
433             (L)
434              
435             =head1 AUTHOR
436              
437             Jaldhar H. Vyas, C<< >>
438              
439             =head1 LICENSE AND COPYRIGHT
440              
441             Copyright (c) 2012 Consolidated Braincells Inc. All rights reserved.
442              
443             This distribution is free software; you can redistribute it and/or modify it
444             under the terms of either:
445              
446             a) the GNU General Public License as published by the Free Software
447             Foundation; either version 2, or (at your option) any later version, or
448              
449             b) the Artistic License version 2.0.
450              
451             The full text of the license can be found in the LICENSE file included
452             with this distribution.
453              
454             =cut
455              
456             1; # End of REST::Utils
457              
458             __END__