File Coverage

blib/lib/HTTP/Headers/Fast.pm
Criterion Covered Total %
statement 280 302 92.7
branch 147 190 77.3
condition 50 73 68.4
subroutine 46 47 97.8
pod 3 29 10.3
total 526 641 82.0


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