File Coverage

blib/lib/HTTP/Headers/Fast.pm
Criterion Covered Total %
statement 282 304 92.7
branch 147 190 77.3
condition 53 73 72.6
subroutine 47 48 97.9
pod 3 30 10.0
total 532 645 82.4


line stmt bran cond sub pod time code
1             package HTTP::Headers::Fast;
2 8     8   291687 use strict;
  8         48  
  8         195  
3 8     8   35 use warnings;
  8         13  
  8         172  
4 8     8   149 use 5.00800;
  8         21  
5 8     8   32 use Carp ();
  8         15  
  8         28588  
6              
7             our $VERSION = '0.22';
8              
9             our $TRANSLATE_UNDERSCORE = 1;
10              
11             # "Good Practice" order of HTTP message headers:
12             # - General-Headers
13             # - Request-Headers
14             # - Response-Headers
15             # - Entity-Headers
16              
17             # yappo says "Readonly sucks".
18             my $OP_GET = 0;
19             my $OP_SET = 1;
20             my $OP_INIT = 2;
21             my $OP_PUSH = 3;
22              
23             my @general_headers = qw(
24             Cache-Control Connection Date Pragma Trailer Transfer-Encoding Upgrade
25             Via Warning
26             );
27              
28             my @request_headers = qw(
29             Accept Accept-Charset Accept-Encoding Accept-Language
30             Authorization Expect From Host
31             If-Match If-Modified-Since If-None-Match If-Range If-Unmodified-Since
32             Max-Forwards Proxy-Authorization Range Referer TE User-Agent
33             );
34              
35             my @response_headers = qw(
36             Accept-Ranges Age ETag Location Proxy-Authenticate Retry-After Server
37             Vary WWW-Authenticate
38             );
39              
40             my @entity_headers = qw(
41             Allow Content-Encoding Content-Language Content-Length Content-Location
42             Content-MD5 Content-Range Content-Type Expires Last-Modified
43             );
44              
45             my %entity_header = map { lc($_) => 1 } @entity_headers;
46              
47             my @header_order =
48             ( @general_headers, @request_headers, @response_headers, @entity_headers, );
49              
50             # Make alternative representations of @header_order. This is used
51             # for sorting and case matching.
52             my %header_order;
53             our %standard_case;
54              
55             {
56             my $i = 0;
57             for (@header_order) {
58             my $lc = lc $_;
59             $header_order{$lc} = ++$i;
60             $standard_case{$lc} = $_;
61             }
62             }
63              
64             sub new {
65 29     29 0 9981 my ($class) = shift;
66 29         57 my $self = bless {}, $class;
67 29 100       103 $self->header(@_) if @_; # set up initial headers
68 29         94 $self;
69             }
70              
71             sub isa {
72 5     5 0 659 my ($self, $klass) = @_;
73 5   33     11 my $proto = ref $self || $self;
74 5 100 100     30 return ($proto eq $klass || $klass eq 'HTTP::Headers') ? 1 : 0;
75             }
76              
77             sub header {
78 68     68 0 711 my $self = shift;
79 68 100       276 Carp::croak('Usage: $h->header($field, ...)') unless @_;
80 67         81 my (@old);
81              
82 67 100       138 if (@_ == 1) {
    100          
83 30         49 @old = $self->_header_get(@_);
84             } elsif( @_ == 2 ) {
85 21         64 @old = $self->_header_set(@_);
86             } else {
87 16         21 my %seen;
88 16         31 while (@_) {
89 56         69 my $field = shift;
90 56 100       128 if ( $seen{ lc $field }++ ) {
91 7         13 @old = $self->_header_push($field, shift);
92             } else {
93 49         72 @old = $self->_header_set($field, shift);
94             }
95             }
96             }
97 67 100       136 return @old if wantarray;
98 59 100       197 return $old[0] if @old <= 1;
99 8         37 join( ", ", @old );
100             }
101              
102             sub clear {
103 5     5 0 245 my $self = shift;
104 5         15 %$self = ();
105             }
106              
107             sub push_header {
108 6     6 0 58 my $self = shift;
109              
110 6 50       13 if (@_ == 2) {
111 6         13 my ($field, $val) = @_;
112 6 100       19 $field = _standardize_field_name($field) unless $field =~ /^:/;
113              
114 6         10 my $h = $self->{$field};
115 6 100       16 if (!defined $h) {
    100          
116 1         3 $h = [];
117 1         3 $self->{$field} = $h;
118             } elsif (ref $h ne 'ARRAY') {
119 3         6 $h = [ $h ];
120 3         4 $self->{$field} = $h;
121             }
122            
123 6 100       20 push @$h, ref $val ne 'ARRAY' ? $val : @$val;
124             } else {
125 0         0 while ( my ($field, $val) = splice( @_, 0, 2 ) ) {
126 0 0       0 $field = _standardize_field_name($field) unless $field =~ /^:/;
127              
128 0         0 my $h = $self->{$field};
129 0 0       0 if (!defined $h) {
    0          
130 0         0 $h = [];
131 0         0 $self->{$field} = $h;
132             } elsif (ref $h ne 'ARRAY') {
133 0         0 $h = [ $h ];
134 0         0 $self->{$field} = $h;
135             }
136            
137 0 0       0 push @$h, ref $val ne 'ARRAY' ? $val : @$val;
138             }
139             }
140 6         21 return ();
141             }
142              
143             sub init_header {
144 5 100   5 0 79 Carp::croak('Usage: $h->init_header($field, $val)') if @_ != 3;
145 4         8 shift->_header( @_, $OP_INIT );
146             }
147              
148             sub remove_header {
149 14     14 0 126 my ( $self, @fields ) = @_;
150 14         20 my $field;
151             my @values;
152 14         25 for my $field (@fields) {
153 21 100 66     79 $field =~ tr/_/-/ if $field !~ /^:/ && $TRANSLATE_UNDERSCORE;
154 21         40 my $v = delete $self->{ lc $field };
155 21 100       64 push( @values, ref($v) eq 'ARRAY' ? @$v : $v ) if defined $v;
    100          
156             }
157 14         50 return @values;
158             }
159              
160             sub remove_content_headers {
161 3     3 0 6 my $self = shift;
162 3 100       9 unless ( defined(wantarray) ) {
163              
164             # fast branch that does not create return object
165 1   100     18 delete @$self{ grep $entity_header{$_} || /^content-/, keys %$self };
166 1         3 return;
167             }
168              
169 2         6 my $c = ref($self)->new;
170 2   100     30 for my $f ( grep $entity_header{$_} || /^content-/, keys %$self ) {
171 10         19 $c->{$f} = delete $self->{$f};
172             }
173 2         6 $c;
174             }
175              
176             my %field_name;
177             sub _standardize_field_name {
178 148     148   191 my $field = shift;
179              
180 148 100       274 $field =~ tr/_/-/ if $TRANSLATE_UNDERSCORE;
181 148 100       299 if (my $cache = $field_name{$field}) {
182 74         135 return $cache;
183             }
184              
185 74         86 my $old = $field;
186 74         99 $field = lc $field;
187 74 100       144 unless ( defined $standard_case{$field} ) {
188             # generate a %standard_case entry for this field
189 34         221 $old =~ s/\b(\w)/\u$1/g;
190 34         78 $standard_case{$field} = $old;
191             }
192 74         141 $field_name{$old} = $field;
193 74         265 return $field;
194             }
195              
196             sub _header_get {
197 41     41   64 my ($self, $field, $skip_standardize) = @_;
198              
199 41 100 100     134 $field = _standardize_field_name($field) unless $skip_standardize || $field =~ /^:/;
200              
201 41         58 my $h = $self->{$field};
202 41 100       114 return (ref($h) eq 'ARRAY') ? @$h : ( defined($h) ? ($h) : () );
    100          
203             }
204              
205             sub _header_set {
206 85     85   201 my ($self, $field, $val) = @_;
207              
208 85 100       196 $field = _standardize_field_name($field) unless $field =~ /^:/;
209              
210 85         125 my $h = $self->{$field};
211 85 100       185 my @old = ref($h) eq 'ARRAY' ? @$h : ( defined($h) ? ($h) : () );
    100          
212 85 100       110 if ( defined($val) ) {
213 84 50 66     163 if (ref $val eq 'ARRAY' && scalar(@$val) == 1) {
214 0         0 $val = $val->[0];
215             }
216 84         121 $self->{$field} = $val;
217             } else {
218 1         2 delete $self->{$field};
219             }
220 85         189 return @old;
221             }
222              
223             sub _header_push {
224 7     7   22 my ($self, $field, $val) = @_;
225              
226 7 50       29 $field = _standardize_field_name($field) unless $field =~ /^:/;
227              
228 7         11 my $h = $self->{$field};
229 7 100       20 if (ref($h) eq 'ARRAY') {
    50          
230 3         6 my @old = @$h;
231 3 50       8 push @$h, ref $val ne 'ARRAY' ? $val : @$val;
232 3         11 return @old;
233             } elsif (defined $h) {
234 4 50       13 $self->{$field} = [$h, ref $val ne 'ARRAY' ? $val : @$val ];
235 4         13 return ($h);
236             } else {
237 0 0       0 $self->{$field} = ref $val ne 'ARRAY' ? $val : @$val;
238 0         0 return ();
239             }
240             }
241              
242             sub _header {
243 24     24   41 my ($self, $field, $val, $op) = @_;
244              
245 24 50       64 $field = _standardize_field_name($field) unless $field =~ /^:/;
246              
247 24 100 100     78 $op ||= defined($val) ? $OP_SET : $OP_GET;
248              
249 24         36 my $h = $self->{$field};
250 24 100       58 my @old = ref($h) eq 'ARRAY' ? @$h : ( defined($h) ? ($h) : () );
    50          
251              
252 24 100 100     58 unless ( $op == $OP_GET || ( $op == $OP_INIT && @old ) ) {
      100        
253 8 50       13 if ( defined($val) ) {
    0          
254 8 50       22 my @new = ( $op == $OP_PUSH ) ? @old : ();
255 8 100       14 if ( ref($val) ne 'ARRAY' ) {
256 7         36 push( @new, $val );
257             }
258             else {
259 1         2 push( @new, @$val );
260             }
261 8 100       22 $self->{$field} = @new > 1 ? \@new : $new[0];
262             }
263             elsif ( $op != $OP_PUSH ) {
264 0         0 delete $self->{$field};
265             }
266             }
267 24         57 @old;
268             }
269              
270             sub _sorted_field_names {
271 45     45   53 my $self = shift;
272             return [ sort {
273 45 50 100     171 ( $header_order{$a} || 999 ) <=> ( $header_order{$b} || 999 )
  224   100     702  
274             || $a cmp $b
275             } keys %$self ];
276             }
277              
278             sub header_field_names {
279 8     8 0 146 my $self = shift;
280 8 100 66     14 return map $standard_case{$_} || $_, @{ $self->_sorted_field_names }
  6         12  
281             if wantarray;
282 2         7 return keys %$self;
283             }
284              
285             sub scan {
286 4     4 0 221 my ( $self, $sub ) = @_;
287 4         5 for my $key (@{ $self->_sorted_field_names }) {
  4         8  
288 12 50       36 next if substr($key, 0, 1) eq '_';
289 12         17 my $vals = $self->{$key};
290 12 100       20 if ( ref($vals) eq 'ARRAY' ) {
291 3         6 for my $val (@$vals) {
292 7   33     20 $sub->( $standard_case{$key} || $key, $val );
293             }
294             }
295             else {
296 9   33     19 $sub->( $standard_case{$key} || $key, $vals );
297             }
298             }
299             }
300              
301             sub _process_newline {
302 10     10   15 local $_ = shift;
303 10         13 my $endl = shift;
304             # must handle header values with embedded newlines with care
305 10         26 s/\s+$//; # trailing newlines and space must go
306 10         26 s/\n(\x0d?\n)+/\n/g; # no empty lines
307 10         30 s/\n([^\040\t])/\n $1/g; # intial space for continuation
308 10         23 s/\n/$endl/g; # substitute with requested line ending
309 10         19 $_;
310             }
311              
312             sub _as_string {
313 28     28   53 my ($self, $endl, $fieldnames) = @_;
314              
315 28         33 my @result;
316 28         44 for my $key ( @$fieldnames ) {
317 114 50       189 next if index($key, '_') == 0;
318 114         154 my $vals = $self->{$key};
319 114 100       185 if ( ref($vals) eq 'ARRAY' ) {
320 7         15 for my $val (@$vals) {
321 16   66     37 my $field = $standard_case{$key} || $key;
322 16         24 $field =~ s/^://;
323 16 50       28 if ( index($val, "\n") >= 0 ) {
324 0         0 $val = _process_newline($val, $endl);
325             }
326 16         32 push @result, $field . ': ' . $val;
327             }
328             } else {
329 107   66     168 my $field = $standard_case{$key} || $key;
330 107         133 $field =~ s/^://;
331 107 100       164 if ( index($vals, "\n") >= 0 ) {
332 10         14 $vals = _process_newline($vals, $endl);
333             }
334 107         207 push @result, $field . ': ' . $vals;
335             }
336             }
337              
338 28         148 join( $endl, @result, '' );
339             }
340              
341             sub as_string {
342 27     27 0 367 my ( $self, $endl ) = @_;
343 27 100       53 $endl = "\n" unless defined $endl;
344 27         47 $self->_as_string($endl, $self->_sorted_field_names);
345             }
346              
347             sub as_string_without_sort {
348 1     1 1 4 my ( $self, $endl ) = @_;
349 1 50       3 $endl = "\n" unless defined $endl;
350 1         3 $self->_as_string($endl, [keys(%$self)]);
351             }
352              
353              
354             sub _psgi_flatten {
355 12     12   20 my ($self, $keys) = @_;
356 12         16 my @headers;
357 12         15 for my $key ( @{$keys} ) {
  12         20  
358 19 50       44 next if substr($key, 0, 1) eq '_';
359 19         29 my $vals = $self->{$key};
360 19 100       29 if ( ref($vals) eq 'ARRAY' ) {
361 3         4 for my $val (@$vals) {
362 8         13 $val =~ s/\015\012[\040|\011]+/chr(32)/ge; # replace LWS with a single SP
  0         0  
363 8         18 $val =~ s/\015|\012//g; # remove CR and LF since the char is invalid here
364 8   33     29 push @headers, $standard_case{$key} || $key, $val;
365             }
366             }
367             else {
368 16         40 $vals =~ s/\015\012[\040|\011]+/chr(32)/ge; # replace LWS with a single SP
  4         12  
369 16         53 $vals =~ s/\015|\012//g; # remove CR and LF since the char is invalid here
370 16   33     327 push @headers, $standard_case{$key} || $key, $vals;
371             }
372             }
373 12         93 return \@headers;
374             }
375              
376             sub psgi_flatten {
377 8     8 1 33 $_[0]->_psgi_flatten($_[0]->_sorted_field_names);
378             }
379              
380              
381             sub psgi_flatten_without_sort {
382 4     4 1 9 $_[0]->_psgi_flatten([keys %{$_[0]}]);
  4         15  
383             }
384              
385             {
386             my $storable_required;
387             sub clone {
388 12 100   12 0 331 unless ($storable_required) {
389 1         553 require Storable;
390 1         2613 $storable_required++;
391             }
392 12         497 goto &Storable::dclone;
393             }
394             }
395              
396             sub _date_header {
397 16     16   482 require HTTP::Date;
398 16         3609 my ( $self, $header, $time ) = @_;
399 16         18 my $old;
400 16 100       28 if ( defined $time ) {
401 5         11 ($old) = $self->_header_set( $header, HTTP::Date::time2str($time) );
402             } else {
403 11         22 ($old) = $self->_header_get($header, 1);
404             }
405 16 100       33 $old =~ s/;.*// if defined($old);
406 16         32 HTTP::Date::str2time($old);
407             }
408              
409 3     3 0 93 sub date { shift->_date_header( 'date', @_ ); }
410 3     3 0 362 sub expires { shift->_date_header( 'expires', @_ ); }
411 4     4 0 342 sub if_modified_since { shift->_date_header( 'if-modified-since', @_ ); }
412 3     3 0 305 sub if_unmodified_since { shift->_date_header( 'if-unmodified-since', @_ ); }
413 3     3 0 265 sub last_modified { shift->_date_header( 'last-modified', @_ ); }
414              
415             # This is used as a private LWP extension. The Client-Date header is
416             # added as a timestamp to a response when it has been received.
417 0     0 0 0 sub client_date { shift->_date_header( 'client-date', @_ ); }
418              
419             # The retry_after field is dual format (can also be a expressed as
420             # number of seconds from now), so we don't provide an easy way to
421             # access it until we have know how both these interfaces can be
422             # addressed. One possibility is to return a negative value for
423             # relative seconds and a positive value for epoch based time values.
424             #sub retry_after { shift->_date_header('Retry-After', @_); }
425              
426             sub content_type {
427 29     29 0 88 my $self = shift;
428 29         39 my $ct = $self->{'content-type'};
429 29 100       62 $self->{'content-type'} = shift if @_;
430 29 50       47 $ct = $ct->[0] if ref($ct) eq 'ARRAY';
431 29 100 66     83 return '' unless defined($ct) && length($ct);
432 24         59 my @ct = split( /;\s*/, $ct, 2 );
433 24         38 for ( $ct[0] ) {
434 24         45 s/\s+//g;
435 24         42 $_ = lc($_);
436             }
437 24 100       81 wantarray ? @ct : $ct[0];
438             }
439              
440             sub content_type_charset {
441 2     2 0 8 my $self = shift;
442 2         7 my $h = $self->{'content-type'};
443 2 50       6 $h = $h->[0] if ref($h);
444 2 100       6 $h = "" unless defined $h;
445 2         4 my @v = _split_header_words($h);
446 2 100       5 if (@v) {
447 1         2 my($ct, undef, %ct_param) = @{$v[0]};
  1         10  
448 1         5 my $charset = $ct_param{charset};
449 1 50       3 if ($ct) {
450 1         2 $ct = lc($ct);
451 1         3 $ct =~ s/\s+//;
452             }
453 1 50       4 if ($charset) {
454 1         3 $charset = uc($charset);
455 1         3 $charset =~ s/^\s+//; $charset =~ s/\s+\z//;
  1         2  
456 1 50       3 undef($charset) if $charset eq "";
457             }
458 1 50       3 return $ct, $charset if wantarray;
459 1         9 return $charset;
460             }
461 1 50       5 return undef, undef if wantarray;
462 1         4 return undef;
463             }
464              
465             sub _split_header_words
466             {
467 2     2   6 my(@val) = @_;
468 2         3 my @res;
469 2         5 for (@val) {
470 2         3 my @cur;
471 2         5 while (length) {
472 3 100 33     22 if (s/^\s*(=*[^\s=;,]+)//) { # 'token' or parameter 'attribute'
    50          
    50          
473 2         6 push(@cur, $1);
474             # a quoted value
475 2 100       12 if (s/^\s*=\s*\"([^\"\\]*(?:\\.[^\"\\]*)*)\"//) {
    50          
476 1         3 my $val = $1;
477 1         3 $val =~ s/\\(.)/$1/g;
478 1         596 push(@cur, $val);
479             # some unquoted value
480             }
481             elsif (s/^\s*=\s*([^;,\s]*)//) {
482 0         0 my $val = $1;
483 0         0 $val =~ s/\s+$//;
484 0         0 push(@cur, $val);
485             # no value, a lone token
486             }
487             else {
488 1         2 push(@cur, undef);
489             }
490             }
491             elsif (s/^\s*,//) {
492 0 0       0 push(@res, [@cur]) if @cur;
493 0         0 @cur = ();
494             }
495             elsif (s/^\s*;// || s/^\s+//) {
496             # continue
497             }
498             else {
499 0         0 die "This should not happen: '$_'";
500             }
501             }
502 2 100       10 push(@res, \@cur) if @cur;
503             }
504              
505 2         4 for my $arr (@res) {
506 1         14 for (my $i = @$arr - 2; $i >= 0; $i -= 2) {
507 2         8 $arr->[$i] = lc($arr->[$i]);
508             }
509             }
510 2         6 return @res;
511             }
512              
513             sub content_is_text {
514 3     3 0 7 my $self = shift;
515 3         4 return $self->content_type =~ m,^text/,;
516             }
517              
518             sub content_is_html {
519 3     3 0 5 my $self = shift;
520 3   100     5 return $self->content_type eq 'text/html' || $self->content_is_xhtml;
521             }
522              
523             sub content_is_xhtml {
524 5     5 0 7 my $ct = shift->content_type;
525 5   66     26 return $ct eq "application/xhtml+xml"
526             || $ct eq "application/vnd.wap.xhtml+xml";
527             }
528              
529             sub content_is_xml {
530 3     3 0 7 my $ct = shift->content_type;
531 3 50       7 return 1 if $ct eq "text/xml";
532 3 50       6 return 1 if $ct eq "application/xml";
533 3 100       10 return 1 if $ct =~ /\+xml$/;
534 2         7 return 0;
535             }
536              
537             sub referer {
538 8     8 0 6018 my $self = shift;
539 8 100 100     79 if ( @_ && $_[0] =~ /#/ ) {
540              
541             # Strip fragment per RFC 2616, section 14.36.
542 2         10 my $uri = shift;
543 2 100       6 if ( ref($uri) ) {
544 1         5 $uri = $uri->clone;
545 1         12 $uri->fragment(undef);
546             }
547             else {
548 1         5 $uri =~ s/\#.*//;
549             }
550 2         18 unshift @_, $uri;
551             }
552 8         20 ( $self->_header( 'Referer', @_ ) )[0];
553             }
554             *referrer = \&referer; # on tchrist's request
555              
556             for my $key (qw/content-length content-language content-encoding title user-agent server from warnings www-authenticate authorization proxy-authenticate proxy-authorization/) {
557 8     8   66 no strict 'refs';
  8         19  
  8         2991  
558             (my $meth = $key) =~ s/-/_/g;
559             *{$meth} = sub {
560 24     24   216 my $self = shift;
561 24 100       44 if (@_) {
562 10         22 ( $self->_header_set( $key, @_ ) )[0]
563             } else {
564 14         21 my $h = $self->{$key};
565 14 50       46 (ref($h) eq 'ARRAY') ? $h->[0] : $h;
566             }
567             };
568             }
569              
570 7     7 0 156 sub authorization_basic { shift->_basic_auth( "Authorization", @_ ) }
571             sub proxy_authorization_basic {
572 2     2 0 52 shift->_basic_auth( "Proxy-Authorization", @_ );
573             }
574              
575             sub _basic_auth {
576 9     9   442 require MIME::Base64;
577 9         567 my ( $self, $h, $user, $passwd ) = @_;
578 9         16 my ($old) = $self->_header($h);
579 9 100       18 if ( defined $user ) {
580 4 100       154 Carp::croak("Basic authorization user name can't contain ':'")
581             if $user =~ /:/;
582 3 100       6 $passwd = '' unless defined $passwd;
583 3         18 $self->_header(
584             $h => 'Basic ' . MIME::Base64::encode( "$user:$passwd", '' ) );
585             }
586 8 100 66     40 if ( defined $old && $old =~ s/^\s*Basic\s+// ) {
587 5         14 my $val = MIME::Base64::decode($old);
588 5 100       15 return $val unless wantarray;
589 3         30 return split( /:/, $val, 2 );
590             }
591 3         10 return;
592             }
593              
594             1;
595             __END__