File Coverage

blib/lib/HTTP/Cookies/Mozilla.pm
Criterion Covered Total %
statement 99 130 76.1
branch 20 38 52.6
condition 7 24 29.1
subroutine 19 20 95.0
pod 2 2 100.0
total 147 214 68.6


line stmt bran cond sub pod time code
1             package HTTP::Cookies::Mozilla;
2 4     4   219751 use strict;
  4         37  
  4         123  
3              
4 4     4   21 use warnings;
  4         8  
  4         98  
5 4     4   19 no warnings;
  4         7  
  4         239  
6              
7             =encoding utf8
8              
9             =head1 NAME
10              
11             HTTP::Cookies::Mozilla - Cookie storage and management for Mozilla
12              
13             =head1 SYNOPSIS
14              
15             use HTTP::Cookies::Mozilla;
16              
17             my $file = ...; # Firefox profile dir / cookies.sqlite
18             my $cookie_jar = HTTP::Cookies::Mozilla->new( file => $file );
19              
20             # otherwise same as HTTP::Cookies
21              
22             =head1 DESCRIPTION
23              
24             This package overrides the C and C methods of
25             HTTP::Cookies so it can work with Mozilla cookie files. These might
26             be stored in the user profile directory as F. On macOS,for
27             instance, that's F<~/Application Support/Firefox/*/cookies.sqlite>.
28              
29             This module should be able to work with all Mozilla derived browsers
30             (FireBird, Camino, et alia).
31              
32             Note that as of FireFox, version 3, the cookie file format changed
33             from plain text files to SQLite databases, so you will need to have
34             either L/L, or the B executable somewhere
35             in the path. Neither one has been put as explicit dependency, anyway,
36             so you'll get an exception if you try to use this module with a new
37             style file but without having any of them:
38              
39             neither DBI nor pipe to sqlite3 worked (%s), install either one
40              
41             If your command-line B is not in the C<$ENV{PATH}>, you can
42             set C<$HTTP::Cookies::Mozilla::SQLITE> to point to the actual program
43             to be used, e.g.:
44              
45             use HTTP::Cookies::Mozilla;
46             $HTTP::Cookies::Mozilla::SQLITE = '/path/to/sqlite3';
47              
48             Usage of the external program is supported under perl 5.8 onwards only,
49             because previous perl versions do not support L with
50             more than three arguments, which are safer. If you are still sticking
51             to perl 5.6, you'll have to install L/L to make
52             FireFox 3 cookies work.
53              
54             See L.
55              
56             =head1 SOURCE AVAILABILITY
57              
58             The source is in GitHub:
59              
60             https://github.com/briandfoy/http-cookies-mozilla
61              
62             =head1 AUTHOR
63              
64             Derived from Gisle Aas's HTTP::Cookies::Netscape package with very
65             few material changes.
66              
67             Flavio Poletti added the SQLite support.
68              
69             Maintained by brian d foy, C<< >>
70              
71             =head1 COPYRIGHT AND LICENSE
72              
73             Parts Copyright 1997-1999 Gisle Aas.
74              
75             Other parts Copyright 2018-2019 by brian d foy, C<< >>
76              
77             This library is free software; you can redistribute it and/or modify
78             it under the terms of the Artistic License 2.0.
79              
80             =cut
81              
82 4     4   30 use base qw( HTTP::Cookies );
  4         8  
  4         2240  
83 4     4   46742 use vars qw( $VERSION $SQLITE );
  4         10  
  4         185  
84              
85 4     4   23 use Carp qw(carp);
  4         9  
  4         205  
86              
87 4     4   25 use constant TRUE => 'TRUE';
  4         10  
  4         226  
88 4     4   25 use constant FALSE => 'FALSE';
  4         8  
  4         2476  
89              
90             $VERSION = '2.035';
91             $SQLITE = 'sqlite3';
92              
93              
94             sub _load_ff3 {
95 3     3   10 my ($self, $file) = @_;
96 3         7 my $cookies;
97 3         6 my $query = 'SELECT host, path, name, value, isSecure, expiry '
98             . ' FROM moz_cookies';
99             eval {
100 3         21 require DBI;
101 3         42 my $dbh = DBI->connect('dbi:SQLite:dbname=' . $file, '', '',
102             {RaiseError => 1}
103             );
104 3         4190 $cookies = $dbh->selectall_arrayref($query);
105 3         1221 $dbh->disconnect();
106 3         112 1;
107             }
108             or eval {
109 0         0 require 5.008_000; # for >3 arguments open, which is safer
110 0 0       0 open my $fh, '-|', $SQLITE, $file, $query or die $!;
111 0         0 $cookies = [ map { [ split /\|/ ] } <$fh> ];
  0         0  
112 0         0 1;
113             }
114 3 50 33     8 or do {
115 0         0 carp "neither DBI nor pipe to sqlite3 worked ($@), install either one";
116 0         0 return;
117             };
118              
119 3         11 for my $cookie ( @$cookies )
120             {
121 15         343 my( $domain, $path, $key, $val, $secure, $expires ) = @$cookie;
122              
123 15         41 $self->set_cookie( undef, $key, $val, $path, $domain, undef,
124             0, $secure, $expires - _now(), 0 );
125             }
126              
127 3         77 return 1;
128             }
129              
130             sub load {
131 5     5 1 64628 my( $self, $file ) = @_;
132              
133 5   33     52 $file ||= $self->{'file'} || do {
      33        
134             carp "load() did not get a filename!";
135             return;
136             };
137              
138 5 100       41 return $self->_load_ff3($file) if $file =~ m{\.sqlite}i;
139              
140 2         5 local $_;
141 2         10 local $/ = "\n"; # make sure we got standard record separator
142              
143 2         4 my $fh;
144 2 50       87 unless( open $fh, '<:utf8', $file ) {
145 0         0 carp "Could not open file [$file]: $!";
146 0         0 return;
147             }
148              
149 2         39 my $magic = <$fh>;
150              
151 2 50       40 unless( $magic =~ /^\# HTTP Cookie File/ ) {
152 0         0 carp "$file does not look like a Mozilla cookies file";
153 0         0 close $fh;
154 0         0 return;
155             }
156              
157 2         27 while( <$fh> ) {
158 18 100       322 next if /^\s*\#/;
159 12 100       44 next if /^\s*$/;
160 10         38 tr/\n\r//d;
161              
162 10         93 my( $domain, $bool1, $path, $secure, $expires, $key, $val )
163             = split /\t/;
164              
165 10         28 $secure = ( $secure eq TRUE );
166              
167             # The cookie format is an absolute time in epoch seconds, so
168             # we subtract the current time (with appropriate offsets) to
169             # get the max_age for the second-to-last argument.
170 10         62 $self->set_cookie( undef, $key, $val, $path, $domain, undef,
171             0, $secure, $expires - _now(), 0 );
172             }
173              
174 2         80 close $fh;
175              
176 2         18 1;
177             }
178              
179 0         0 BEGIN {
180 4 50   4   3371 my $EPOCH_OFFSET = $^O eq "MacOS" ? 21600 : 0; # difference from Unix epoch
181 65     65   196 sub _epoch_offset { $EPOCH_OFFSET }
182             }
183              
184 45     45   89 sub _now { time() - _epoch_offset() };
185              
186             sub _scansub_maker { # Encapsulate checks logic during cookie scan
187 4     4   12 my ($self, $coresub) = @_;
188              
189             return sub {
190 20     20   326 my( $version, $key, $val, $path, $domain, $port,
191             $path_spec, $secure, $expires, $discard, $rest ) = @_;
192              
193 20 50 33     47 return if $discard && not $self->{ignore_discard};
194              
195 20 50       46 $expires = $expires ? $expires - _epoch_offset() : 0;
196 20 50 33     56 return if defined $expires && _now() > $expires;
197              
198 20         39 return $coresub->($domain, $path, $key, $val, $secure, $expires);
199 4         39 };
200             }
201              
202             sub _save_ff3 {
203 1     1   3 my ($self, $file) = @_;
204              
205 1         4 my @fnames = qw( host path name value isSecure expiry );
206 1         4 my $fnames = join ', ', @fnames;
207              
208             eval {
209 1         6 require DBI;
210 1         8 my $dbh = DBI->connect('dbi:SQLite:dbname=' . $file, '', '',
211             {RaiseError => 1, AutoCommit => 0});
212              
213 1         412 $dbh->do('DROP TABLE IF EXISTS moz_cookies;');
214              
215 1         557 $dbh->do('CREATE TABLE moz_cookies '
216             . ' (id INTEGER PRIMARY KEY, name TEXT, value TEXT, host TEXT,'
217             . ' path TEXT,expiry INTEGER, lastAccessed INTEGER, '
218             . ' isSecure INTEGER, isHttpOnly INTEGER);');
219              
220             { # restrict scope for $sth
221 1         237 my $pholds = join ', ', ('?') x @fnames;
  1         6  
222 1         9 my $sth = $dbh->prepare(
223             "INSERT INTO moz_cookies($fnames) VALUES ($pholds)");
224             $self->scan($self->_scansub_maker(
225             sub {
226 5     5   12 my( $domain, $path, $key, $val, $secure, $expires ) = @_;
227 5 50       10 $secure = $secure ? 1 : 0;
228 5         65 $sth->execute($domain, $path, $key, $val, $secure, $expires);
229             }
230             )
231 1         65 );
232 1         21 $sth->finish();
233             }
234              
235 1         11696 $dbh->commit();
236 1         64 $dbh->disconnect();
237 1         41 1;
238             }
239             or eval {
240 0 0       0 open my $fh, '|-', $SQLITE, $file or die $!;
241 0         0 print {$fh} <<'INCIPIT';
  0         0  
242              
243             BEGIN TRANSACTION;
244              
245             DROP TABLE IF EXISTS moz_cookies;
246             CREATE TABLE moz_cookies
247             (id INTEGER PRIMARY KEY, name TEXT, value TEXT, host TEXT,
248             path TEXT,expiry INTEGER, lastAccessed INTEGER,
249             isSecure INTEGER, isHttpOnly INTEGER);
250              
251             INCIPIT
252              
253             $self->scan( $self->_scansub_maker(
254             sub {
255 0     0   0 my( $domain, $path, $key, $val, $secure, $expires ) = @_;
256 0 0       0 $secure = $secure ? 1 : 0;
257             my $values = join ', ',
258             map { # Encode all params as hex, a bit overkill
259 0         0 my $hex = unpack 'H*', $_;
  0         0  
260 0         0 "X'$hex'";
261             } ( $domain, $path, $key, $val, $secure, $expires );
262 0         0 print {$fh}
  0         0  
263             "INSERT INTO moz_cookies( $fnames ) VALUES ( $values );\n";
264             }
265             )
266 0         0 );
267              
268 0         0 print {$fh} <<'EPILOGUE';
  0         0  
269              
270             UPDATE moz_cookies SET lastAccessed = id;
271             END TRANSACTION;
272              
273             EPILOGUE
274 0         0 1;
275             }
276 1 50 33     3 or do {
277 0         0 carp "neither DBI nor pipe to sqlite3 worked ($@), install either one";
278 0         0 return;
279             };
280              
281 1         7 return 1;
282             }
283              
284             sub save {
285 4     4 1 1840 my( $self, $file ) = @_;
286              
287 4   0     15 $file ||= $self->{'file'} || do {
      33        
288             carp "save() did not get a filename!";
289             return;
290             };
291              
292 4 100       27 return $self->_save_ff3($file) if $file =~ m{\. sqlite}imsx;
293              
294 3         6 local $_;
295              
296 3         7 my $fh;
297 3 50       271 unless( open $fh, '>:utf8', $file ) {
298 0         0 carp "Could not open file [$file]: $!";
299 0         0 return;
300             }
301              
302 3         34 print $fh <<'EOT';
303             # HTTP Cookie File
304             # http://www.netscape.com/newsref/std/cookie_spec.html
305             # This is a generated file! Do not edit.
306             # To delete cookies, use the Cookie Manager.
307              
308             EOT
309              
310             $self->scan($self->_scansub_maker(
311             sub {
312 15     15   33 my( $domain, $path, $key, $val, $secure, $expires ) = @_;
313 15 50       35 $secure = $secure ? TRUE : FALSE;
314 15 50       48 my $bool = $domain =~ /^\./ ? TRUE : FALSE;
315 15         81 print $fh join( "\t", $domain, $bool, $path, $secure,
316             $expires, $key, $val ), "\n";
317             }
318             )
319 3         30 );
320              
321 3         165 close $fh;
322              
323 3         25 1;
324             }
325              
326             1;