root/lang/perl/HTTP-Headers-Fast/trunk/lib/HTTP/Headers/Fast.pm @ 23203

Revision 23203, 12.3 kB (checked in by tokuhirom, 5 years ago)

$OP_PUSH_H is no longer needed

Line 
1package HTTP::Headers::Fast;
2use strict;
3use warnings;
4use 5.00800;
5our $VERSION = '0.01';
6use Carp ();
7use base qw/HTTP::Headers/;
8
9our $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".
18my $OP_GET    = 0;
19my $OP_SET    = 1;
20my $OP_INIT   = 2;
21
22my @general_headers = qw(
23  Cache-Control Connection Date Pragma Trailer Transfer-Encoding Upgrade
24  Via Warning
25);
26
27my @request_headers = qw(
28  Accept Accept-Charset Accept-Encoding Accept-Language
29  Authorization Expect From Host
30  If-Match If-Modified-Since If-None-Match If-Range If-Unmodified-Since
31  Max-Forwards Proxy-Authorization Range Referer TE User-Agent
32);
33
34my @response_headers = qw(
35  Accept-Ranges Age ETag Location Proxy-Authenticate Retry-After Server
36  Vary WWW-Authenticate
37);
38
39my @entity_headers = qw(
40  Allow Content-Encoding Content-Language Content-Length Content-Location
41  Content-MD5 Content-Range Content-Type Expires Last-Modified
42);
43
44my %entity_header = map { lc($_) => 1 } @entity_headers;
45
46my @header_order =
47  ( @general_headers, @request_headers, @response_headers, @entity_headers, );
48
49# Make alternative representations of @header_order.  This is used
50# for sorting and case matching.
51my %header_order;
52my %standard_case;
53
54{
55    my $i = 0;
56    for (@header_order) {
57        my $lc = lc $_;
58        $header_order{$lc}  = ++$i;
59        $standard_case{$lc} = $_;
60    }
61}
62
63sub new {
64    my ($class) = shift;
65    my $self = bless {}, $class;
66    $self->header(@_) if @_;    # set up initial headers
67    $self;
68}
69
70sub header {
71    my $self = shift;
72    Carp::croak('Usage: $h->header($field, ...)') unless @_;
73    my (@old);
74    my %seen;
75    while (@_) {
76        my $field = shift;
77        my $method;
78        if (@_) {
79            if ( $seen{ lc $field }++ ) {
80                $method = '_header_push';
81            } else {
82                $method = '_header_set';
83            }
84        } else {
85            $method = '_header_get';
86        }
87       
88        @old = $self->$method($field, shift);
89    }
90    return @old    if wantarray;
91    return $old[0] if @old <= 1;
92    join( ", ", @old );
93}
94
95sub clear {
96    my $self = shift;
97    %$self = ();
98}
99
100sub push_header {
101    my $self = shift;
102
103    if (@_ == 2) {
104        $self->_header_push_no_return( @_ );
105    } else {
106        while (@_) {
107            $self->_header_push_no_return( splice( @_, 0, 2 ) );
108        }
109    }
110}
111
112sub init_header {
113    Carp::croak('Usage: $h->init_header($field, $val)') if @_ != 3;
114    shift->_header( @_, $OP_INIT );
115}
116
117sub remove_header {
118    my ( $self, @fields ) = @_;
119    my $field;
120    my @values;
121    for my $field (@fields) {
122        $field =~ tr/_/-/ if $field !~ /^:/ && $TRANSLATE_UNDERSCORE;
123        my $v = delete $self->{ lc $field };
124        push( @values, ref($v) eq 'ARRAY' ? @$v : $v ) if defined $v;
125    }
126    return @values;
127}
128
129sub remove_content_headers {
130    my $self = shift;
131    unless ( defined(wantarray) ) {
132
133        # fast branch that does not create return object
134        delete @$self{ grep $entity_header{$_} || /^content-/, keys %$self };
135        return;
136    }
137
138    my $c = ref($self)->new;
139    for my $f ( grep $entity_header{$_} || /^content-/, keys %$self ) {
140        $c->{$f} = delete $self->{$f};
141    }
142    $c;
143}
144
145sub _standardize_field_name {
146    my $field = shift;
147    $field =~ tr/_/-/ if $TRANSLATE_UNDERSCORE;
148    my $old = $field;
149    $field = lc $field;
150    unless ( defined $standard_case{$field} ) {
151        # generate a %standard_case entry for this field
152        $old =~ s/\b(\w)/\u$1/g;
153        $standard_case{$field} = $old;
154    }
155    return $field;
156}
157
158sub _header_get {
159    my ($self, $field) = @_;
160
161    $field = _standardize_field_name($field) unless $field =~ /^:/;
162
163    my $h = $self->{$field};
164    my @old = ref($h) eq 'ARRAY' ? @$h : ( defined($h) ? ($h) : () );
165    return @old;
166}
167
168sub _header_set {
169    my ($self, $field, $val) = @_;
170
171    $field = _standardize_field_name($field) unless $field =~ /^:/;
172
173    my $h = $self->{$field};
174    my @old = ref($h) eq 'ARRAY' ? @$h : ( defined($h) ? ($h) : () );
175    if ( defined($val) ) {
176        my @new;
177        if ( ref($val) ne 'ARRAY' ) {
178            push( @new, $val );
179        }
180        else {
181            push( @new, @$val );
182        }
183        $self->{$field} = @new > 1 ? \@new : $new[0];
184    } else {
185        delete $self->{$field};
186    }
187    return @old;
188}
189
190sub _header_push_no_return {
191    my ($self, $field, $val) = @_;
192
193    $field = _standardize_field_name($field) unless $field =~ /^:/;
194
195    my $h = $self->{$field};
196    if (ref($h) eq 'ARRAY') {
197        push @$h, ref $val ne 'ARRAY' ? $val : @$val;
198    } elsif (defined $h) {
199        $self->{$field} = [$h, ref $val ne 'ARRAY' ? $val : @$val ];
200    } else {
201        $self->{$field} = ref $val ne 'ARRAY' ? $val : @$val;
202    }
203}
204
205sub _header_push {
206    my ($self, $field, $val) = @_;
207
208    $field = _standardize_field_name($field) unless $field =~ /^:/;
209
210    my $h = $self->{$field};
211    if (ref($h) eq 'ARRAY') {
212        my @old = @$h;
213        push @$h, ref $val ne 'ARRAY' ? $val : @$val;
214        return @old;
215    } elsif (defined $h) {
216        $self->{$field} = [$h, ref $val ne 'ARRAY' ? $val : @$val ];
217        return ($h);
218    } else {
219        $self->{$field} = ref $val ne 'ARRAY' ? $val : @$val;
220        return ();
221    }
222}
223
224sub _header {
225    my ($self, $field, $val, $op) = @_;
226
227    $field = _standardize_field_name($field) unless $field =~ /^:/;
228
229    $op ||= defined($val) ? $OP_SET : $OP_GET;
230
231    my $h = $self->{$field};
232    my @old = ref($h) eq 'ARRAY' ? @$h : ( defined($h) ? ($h) : () );
233
234    unless ( $op == $OP_GET || ( $op == $OP_INIT && @old ) ) {
235        if ( defined($val) ) {
236            my @new = ( $op == $OP_PUSH ) ? @old : ();
237            if ( ref($val) ne 'ARRAY' ) {
238                push( @new, $val );
239            }
240            else {
241                push( @new, @$val );
242            }
243            $self->{$field} = @new > 1 ? \@new : $new[0];
244        }
245        elsif ( $op != $OP_PUSH ) {
246            delete $self->{$field};
247        }
248    }
249    @old;
250}
251
252sub _sorted_field_names {
253    my $self = shift;
254    return sort {
255        ( $header_order{$a} || 999 ) <=> ( $header_order{$b} || 999 )
256          || $a cmp $b
257    } keys %$self;
258}
259
260sub header_field_names {
261    my $self = shift;
262    return map $standard_case{$_} || $_, $self->_sorted_field_names
263      if wantarray;
264    return keys %$self;
265}
266
267sub scan {
268    my ( $self, $sub ) = @_;
269    my @sorted = $self->_sorted_field_names;
270    for my $key ( @sorted ) {
271        next if index($key, '_') == 0;
272        my $vals = $self->{$key};
273        if ( ref($vals) eq 'ARRAY' ) {
274            for my $val (@$vals) {
275                $sub->( $standard_case{$key} || $key, $val );
276            }
277        }
278        else {
279            $sub->( $standard_case{$key} || $key, $vals );
280        }
281    }
282}
283
284sub as_string {
285    my ( $self, $endl ) = @_;
286    $endl = "\n" unless defined $endl;
287
288    my @result = ();
289    my $process_newline = sub {
290        local $_ = shift;
291        # must handle header values with embedded newlines with care
292        s/\s+$//;        # trailing newlines and space must go
293        s/\n\n+/\n/g;    # no empty lines
294        s/\n([^\040\t])/\n $1/g; # intial space for continuation
295        s/\n/$endl/g;    # substitute with requested line ending
296        $_;
297    };
298    my @sorted = $self->_sorted_field_names;
299    for my $key ( @sorted ) {
300        next if index($key, '_') == 0;
301        my $vals = $self->{$key};
302        if ( ref($vals) eq 'ARRAY' ) {
303            for my $val (@$vals) {
304                my $field = $standard_case{$key} || $key;
305                $field =~ s/^://;
306                if ( index($val, "\n") >= 0 ) {
307                    $val = $process_newline->($val);
308                }
309                push @result, $field . ': ' . $val;
310            }
311        } else {
312            my $field = $standard_case{$key} || $key;
313            $field =~ s/^://;
314            if ( index($vals, "\n") >= 0 ) {
315                $vals = $process_newline->($vals);
316            }
317            push @result, $field . ': ' . $vals;
318        }
319    }
320
321    join( $endl, @result, '' );
322}
323
324if ( eval { require Storable; 1 } ) {
325    *clone = \&Storable::dclone;
326}
327else {
328    *clone = sub {
329        my $self  = shift;
330        my $clone = new HTTP::Headers::Fast;
331        $self->scan( sub { $clone->push_header(@_); } );
332        $clone;
333    };
334}
335
336sub _date_header {
337    require HTTP::Date;
338    my ( $self, $header, $time ) = @_;
339    my ($old) = $self->_header_get($header);
340    if ( defined $time ) {
341        $self->_header_set( $header, HTTP::Date::time2str($time) );
342    }
343    $old =~ s/;.*// if defined($old);
344    HTTP::Date::str2time($old);
345}
346
347sub date                { shift->_date_header( 'Date',                @_ ); }
348sub expires             { shift->_date_header( 'Expires',             @_ ); }
349sub if_modified_since   { shift->_date_header( 'If-Modified-Since',   @_ ); }
350sub if_unmodified_since { shift->_date_header( 'If-Unmodified-Since', @_ ); }
351sub last_modified       { shift->_date_header( 'Last-Modified',       @_ ); }
352
353# This is used as a private LWP extension.  The Client-Date header is
354# added as a timestamp to a response when it has been received.
355sub client_date { shift->_date_header( 'Client-Date', @_ ); }
356
357# The retry_after field is dual format (can also be a expressed as
358# number of seconds from now), so we don't provide an easy way to
359# access it until we have know how both these interfaces can be
360# addressed.  One possibility is to return a negative value for
361# relative seconds and a positive value for epoch based time values.
362#sub retry_after       { shift->_date_header('Retry-After',       @_); }
363
364sub content_type {
365    my $self = shift;
366    my $ct   = $self->{'content-type'};
367    $self->{'content-type'} = shift if @_;
368    $ct = $ct->[0] if ref($ct) eq 'ARRAY';
369    return '' unless defined($ct) && length($ct);
370    my @ct = split( /;\s*/, $ct, 2 );
371    for ( $ct[0] ) {
372        s/\s+//g;
373        $_ = lc($_);
374    }
375    wantarray ? @ct : $ct[0];
376}
377
378sub content_is_html {
379    my $self = shift;
380    return $self->content_type eq 'text/html' || $self->content_is_xhtml;
381}
382
383sub content_is_xhtml {
384    my $ct = shift->content_type;
385    return $ct eq "application/xhtml+xml"
386      || $ct   eq "application/vnd.wap.xhtml+xml";
387}
388
389sub content_is_xml {
390    my $ct = shift->content_type;
391    return 1 if $ct eq "text/xml";
392    return 1 if $ct eq "application/xml";
393    return 1 if $ct =~ /\+xml$/;
394    return 0;
395}
396
397sub referer {
398    my $self = shift;
399    if ( @_ && $_[0] =~ /#/ ) {
400
401        # Strip fragment per RFC 2616, section 14.36.
402        my $uri = shift;
403        if ( ref($uri) ) {
404            $uri = $uri->clone;
405            $uri->fragment(undef);
406        }
407        else {
408            $uri =~ s/\#.*//;
409        }
410        unshift @_, $uri;
411    }
412    ( $self->_header( 'Referer', @_ ) )[0];
413}
414*referrer = \&referer;    # on tchrist's request
415
416for my $key (qw/content-length content-language content-encoding title user-agent server from warnings www-authenticate authorization proxy-authenticate proxy-authorization/) {
417    no strict 'refs';
418    (my $meth = $key) =~ s/-/_/g;
419    *{$meth} = sub {
420        my $self = shift;
421        if (@_) {
422            ( $self->_header_set( $key, @_ ) )[0]
423        } else {
424            ( $self->_header_get($key) )[0];
425        }
426    };
427}
428
429sub authorization_basic { shift->_basic_auth( "Authorization", @_ ) }
430sub proxy_authorization_basic {
431    shift->_basic_auth( "Proxy-Authorization", @_ );
432}
433
434sub _basic_auth {
435    require MIME::Base64;
436    my ( $self, $h, $user, $passwd ) = @_;
437    my ($old) = $self->_header($h);
438    if ( defined $user ) {
439        Carp::croak("Basic authorization user name can't contain ':'")
440          if $user =~ /:/;
441        $passwd = '' unless defined $passwd;
442        $self->_header(
443            $h => 'Basic ' . MIME::Base64::encode( "$user:$passwd", '' ) );
444    }
445    if ( defined $old && $old =~ s/^\s*Basic\s+// ) {
446        my $val = MIME::Base64::decode($old);
447        return $val unless wantarray;
448        return split( /:/, $val, 2 );
449    }
450    return;
451}
452
4531;
454__END__
455
456=encoding utf8
457
458=head1 NAME
459
460HTTP::Headers::Fast -
461
462=head1 SYNOPSIS
463
464  use HTTP::Headers::Fast;
465
466=head1 DESCRIPTION
467
468HTTP::Headers::Fast is
469
470=head1 AUTHOR
471
472Tokuhiro Matsuno E<lt>tokuhirom@gmail.comE<gt>
473
474=head1 SEE ALSO
475
476=head1 LICENSE
477
478This library is free software; you can redistribute it and/or modify
479it under the same terms as Perl itself.
480
481=cut
Note: See TracBrowser for help on using the browser.