File Coverage

blib/lib/HTTP/Cookies/Opera.pm
Criterion Covered Total %
statement 135 137 98.5
branch 52 68 76.4
condition 20 46 43.4
subroutine 12 12 100.0
pod 2 2 100.0
total 221 265 83.4


line stmt bran cond sub pod time code
1             package HTTP::Cookies::Opera;
2              
3 3     3   182592 use strict;
  3         10  
  3         213  
4 3     3   18 use warnings;
  3         5  
  3         1087  
5              
6 3     3   8373 use parent qw(HTTP::Cookies);
  3         2073  
  3         18  
7 3     3   105060 use Carp qw(croak);
  3         10  
  3         409  
8              
9             our $VERSION = '0.08';
10             $VERSION = eval $VERSION;
11              
12 3     3   17 use constant DEBUG => !! $ENV{HTTP_COOKIES_OPERA_DEBUG};
  3         7  
  3         412  
13 3     3   18 use constant FILE_VER => 1;
  3         6  
  3         148  
14 3     3   18 use constant APP_VER => 2;
  3         6  
  3         215  
15 3     3   20 use constant TAG_LEN => 1;
  3         6  
  3         152  
16 3     3   16 use constant LEN_LEN => 2;
  3         4  
  3         8015  
17              
18             sub load {
19 3     3 1 129 my ($self, $file) = @_;
20 3 50 33     33 $file ||= $self->{file} or return;
21              
22 3 50       541 open my $fh, '<', $file or die "$file: $!";
23 3         13 binmode $fh;
24 3 50       197 12 == read($fh, my $header, 12) or croak 'bad file header';
25 3         24 my ($file_ver, $app_ver, $tag_len, $len_len) = unpack 'NNnn', $header;
26              
27 3 50 33     125 croak 'unexpected file format'
      33        
      33        
28             unless FILE_VER == $file_ver >> 12 and APP_VER == $app_ver >> 12
29             and TAG_LEN == $tag_len and LEN_LEN == $len_len;
30              
31 3         7 my (@domain_parts, @path_parts, %cookie);
32              
33 3         14 while (TAG_LEN == read $fh, my $tag, TAG_LEN) {
34 329         680 $tag = unpack 'C', $tag;
35 329         687 DEBUG and printf "tag: %#x\n", $tag;
36              
37             # End of domain component.
38 329 100       2263 if (0x84 == $tag) {
    100          
    100          
    100          
39 21         273 pop @domain_parts;
40             }
41             # End of path component.
42             elsif (0x85 == $tag) {
43 27         34 pop @path_parts;
44              
45             # Add last constructed cookie as this path will have no more.
46 27         74 $self->_add_cookie(\%cookie);
47             }
48 3         9 elsif (0x99 == $tag) { $cookie{secure} = 1 }
49             elsif (0x3 == $tag) {
50             # Add previous cookie now that it is fully constructed.
51 35         134 $self->_add_cookie(\%cookie);
52              
53             # Reset cookie for new record.
54 35         2223 %cookie = (
55             domain => join('.', reverse @domain_parts),
56             path => '/' . join('/', @path_parts),
57             );
58             }
59              
60             # Record is a flag and contains no payload.
61 329 100       3479 next if 0x80 & $tag;
62              
63 242 50       4205 LEN_LEN == read $fh, my $len, LEN_LEN or croak 'bad file';
64              
65             # Tags have unique ids among top-level domain/path/cookie records as
66             # well as the payload records, so simplify parsing by treating the
67             # payload records as top-level records during the next iteration.
68 242 100       919 next if 0x3 >= $tag;
69              
70 180         708 $len = unpack 'n', $len;
71 180         173 DEBUG and printf " len: %d\n", $len;
72 180 50       507 $len == read $fh, my $payload, $len or croak 'bad file';
73              
74 180 100       727 if (0x1e == $tag) { push @domain_parts, $payload }
  18 100       37  
    100          
    100          
    100          
    50          
75 9         56 elsif (0x1d == $tag) { push @path_parts, $payload }
76 35         164 elsif (0x10 == $tag) { $cookie{key} = $payload }
77 35         68 elsif (0x11 == $tag) { $cookie{val} = $payload }
78             elsif (0x12 == $tag) {
79             # Time is stored in 8 bytes for Opera >=10, 4 bytes for <10.
80 35 50       86 $payload = unpack 8 == $len ? 'x4N' : 'N', $payload;
81 35         157 $cookie{maxage} = $payload - time;
82 35         224 DEBUG and $payload = scalar localtime $payload;
83             }
84             elsif (0x1a == $tag) {
85             # Version- not yet seen.
86             }
87              
88 180         633 DEBUG and printf " payload: %s\n", $payload;
89             }
90              
91 3         53 close $fh;
92              
93 3         27 return 1;
94             }
95              
96             sub _add_cookie {
97 62     62   4325 my ($self, $cookie) = @_;
98              
99 62 100       201 return unless exists $cookie->{key};
100              
101 56         276 $self->set_cookie(
102             undef, @$cookie{qw(key val path domain)}, undef, undef,
103             @$cookie{qw(secure maxage)}, undef, undef
104             );
105             }
106              
107             sub save {
108 1     1 1 12 my ($self, $file) = @_;
109 1 50 33     7 $file ||= $self->{file} or return;
110              
111 1 50       176 open my $fh, '>', $file or die "$file: $!";
112 1         4 binmode $fh;
113              
114 1         16 print $fh pack 'NNnn', FILE_VER << 12, APP_VER << 12, TAG_LEN, LEN_LEN;
115              
116             # Cannot call scan() as it iterates over the domains in lexical order,
117             # but Opera requires the cookies to be stored in a hierarchy of domain
118             # components (i.e. com -> opera -> www).
119 9 50       17 my @domains = sort { $a->[0] cmp $b->[0] } map {
  5         22  
120             # Do not split IP addresses into components.
121 1         5 my @parts = /^(?:\d+\.){3}\d+$/ ? ($_) : reverse split '\.';
122 5         21 [ join('.', @parts), $_, \@parts ]
123 1         3 } keys %{$self->{COOKIES}};
124              
125             # Add an empty domain field to close the last open domain record.
126 1         3 push @domains, [];
127              
128 1         2 my @prev_domain;
129 1         4 for my $aref (@domains) {
130 6         10 my ($sort_key, $domain, $parts) = @$aref;
131              
132             # Opera does not support cross-subdomain cookies.
133             #
134             # TODO: if a domain cookie and a cross-subdomain cookie both exist
135             # for the same key, which should take precedence?
136 6 100 66     35 my $is_cross = $parts && length $parts->[-1] ? 0 : pop @$parts || 1;
      50        
137              
138             # Close domain component records for previous domain.
139 6         16 for (my $i = @prev_domain - 1; 0 <= $i; $i--) {
140 11         15 my $prev = $prev_domain[$i];
141 11 100 100     69 if (length $prev and $prev ne ($parts->[$i] || '')) {
      66        
142 6         2 DEBUG and print " closing: $prev\n";
143 6         6 pop @prev_domain;
144 6         18 print $fh pack 'C', 0x84;
145             }
146             }
147              
148 6 100       13 last unless $domain;
149 5         6 DEBUG and print "domain: $domain\n";
150              
151             # Open domain component records for next domain.
152 5         13 for (my $i = @prev_domain; $i < @$parts; $i++) {
153 6         9 my $curr = $parts->[$i];
154 6 50 50     115 if (length $curr and $curr ne ($prev_domain[$i] || '')) {
      33        
155 6         5 DEBUG and print " opening: $curr\n";
156 6         10 push @prev_domain, $curr;
157 6         16 print $fh pack 'Cn', 0x1, 3 + length($curr);
158 6         9 print $fh pack 'Cn', 0x1e, length($curr);
159 6         6 print $fh $curr;
160 6 100       23 print $fh pack 'C', 0x85 if $i < @$parts - 1;
161             }
162             }
163              
164 5         7 my @paths = sort keys %{$self->{COOKIES}{$domain}};
  5         18  
165              
166             # Add an empty path field to close the last open path record.
167 5         7 push @paths, '';
168              
169 5         6 my @prev_path;
170 5         6 for my $path (@paths) {
171 12         26 my @parts = split '/', $path;
172 12         14 shift @parts;
173              
174             # Close path component records for previous path.
175 12         31 for (my $i = @prev_path - 1; 0 <= $i; $i--) {
176 3         4 my $prev = $prev_path[$i];
177 3 50 50     155 if (length $prev and $prev ne ($parts[$i] || '')) {
      33        
178 3         3 DEBUG and print " closing: $prev\n";
179 3         4 print $fh pack 'C', 0x85;
180 3         8 pop @prev_path;
181             }
182             }
183              
184 12 100       24 last unless $path;
185 7         5 DEBUG and print " path: $path\n";
186              
187             # Open path component records for next path.
188 7         19 for (my $i = @prev_path; $i < @parts; $i++) {
189 3         4 my $curr = $parts[$i];
190 3 50 50     23 if (length $curr and $curr ne ($prev_path[$i] || '')) {
      33        
191 3         42 DEBUG and print " opening: $curr\n";
192 3         8 print $fh pack 'Cn', 0x2, 3 + length($curr);
193 3         6 print $fh pack 'Cn', 0x1d, length($curr);
194 3         2 print $fh $curr;
195 3         9 push @prev_path, $curr;
196             }
197             }
198              
199 7         13 my $href = $self->{COOKIES}{$domain}{$path};
200 7         25 while (my ($key, $aref) = each %$href) {
201             my (
202 11         91 $version, $val, $port, $path_spec, $secure, $expires,
203             $discard, $rest
204             ) = @$aref;
205              
206 11 50 33     25 next if $discard and not $self->{ignore_discard};
207 11 50 33     109 if (defined $expires and time > $expires) {
208 0         0 DEBUG and print " expired cookie: $key\n";
209 0         0 next;
210             }
211              
212 11         121 DEBUG and print " cookie: $key -> $val\n";
213 11         24 print $fh pack 'Cn', 0x3,
214             17 + length($key) + length($val) + !! $secure;
215 11         22 print $fh pack('Cn', 0x10, length($key)), $key;
216 11         14 print $fh pack('Cn', 0x11, length($val)), $val;
217 11         23 print $fh pack 'Cnx4N', 0x12, 8, $expires;
218 11 100       60 print $fh pack 'C', 0x99 if $secure;
219             }
220             }
221              
222 5         12 print $fh pack 'C', 0x85;
223             }
224              
225 1         2 print $fh pack 'C', 0x84;
226 1         78 close $fh;
227              
228 1         74 return 1;
229             }
230              
231              
232             1;
233              
234             __END__