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   215429 use strict;
  4         34  
  4         119  
3              
4 4     4   20 use warnings;
  4         9  
  4         93  
5 4     4   19 no warnings;
  4         8  
  4         179  
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   33 use base qw( HTTP::Cookies );
  4         10  
  4         2133  
83 4     4   46345 use vars qw( $VERSION $SQLITE );
  4         9  
  4         182  
84              
85 4     4   25 use Carp qw(carp);
  4         7  
  4         186  
86              
87 4     4   23 use constant TRUE => 'TRUE';
  4         9  
  4         208  
88 4     4   24 use constant FALSE => 'FALSE';
  4         8  
  4         2513  
89              
90             $VERSION = '2.036';
91             $SQLITE = 'sqlite3';
92              
93              
94             sub _load_ff3 {
95 3     3   12 my ($self, $file) = @_;
96 3         6 my $cookies;
97 3         8 my $query = 'SELECT host, path, name, value, isSecure, expiry '
98             . ' FROM moz_cookies';
99             eval {
100 3         24 require DBI;
101 3         40 my $dbh = DBI->connect('dbi:SQLite:dbname=' . $file, '', '',
102             {RaiseError => 1}
103             );
104 3         4178 $cookies = $dbh->selectall_arrayref($query);
105 3         1340 $dbh->disconnect();
106 3         117 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     7 or do {
115 0         0 carp "neither DBI nor pipe to sqlite3 worked ($@), install either one";
116 0         0 return;
117             };
118              
119 3         14 for my $cookie ( @$cookies )
120             {
121 15         385 my( $domain, $path, $key, $val, $secure, $expires ) = @$cookie;
122              
123 15         38 $self->set_cookie( undef, $key, $val, $path, $domain, undef,
124             0, $secure, $expires - _now(), 0 );
125             }
126              
127 3         88 return 1;
128             }
129              
130             sub load {
131 5     5 1 63454 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       40 return $self->_load_ff3($file) if $file =~ m{\.sqlite}i;
139              
140 2         4 local $_;
141 2         10 local $/ = "\n"; # make sure we got standard record separator
142              
143 2         4 my $fh;
144 2 50       119 unless( open $fh, '<:utf8', $file ) {
145 0         0 carp "Could not open file [$file]: $!";
146 0         0 return;
147             }
148              
149 2         47 my $magic = <$fh>;
150              
151 2 50       38 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         19 while( <$fh> ) {
158 18 100       310 next if /^\s*\#/;
159 12 100       40 next if /^\s*$/;
160 10         39 tr/\n\r//d;
161              
162 10         71 my( $domain, $bool1, $path, $secure, $expires, $key, $val )
163             = split /\t/;
164              
165 10         26 $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         31 $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         19 1;
177             }
178              
179 0         0 BEGIN {
180 4 50   4   3352 my $EPOCH_OFFSET = $^O eq "MacOS" ? 21600 : 0; # difference from Unix epoch
181 65     65   219 sub _epoch_offset { $EPOCH_OFFSET }
182             }
183              
184 45     45   96 sub _now { time() - _epoch_offset() };
185              
186             sub _scansub_maker { # Encapsulate checks logic during cookie scan
187 4     4   15 my ($self, $coresub) = @_;
188              
189             return sub {
190 20     20   391 my( $version, $key, $val, $path, $domain, $port,
191             $path_spec, $secure, $expires, $discard, $rest ) = @_;
192              
193 20 50 33     58 return if $discard && not $self->{ignore_discard};
194              
195 20 50       54 $expires = $expires ? $expires - _epoch_offset() : 0;
196 20 50 33     71 return if defined $expires && _now() > $expires;
197              
198 20         50 return $coresub->($domain, $path, $key, $val, $secure, $expires);
199 4         46 };
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         5 my $fnames = join ', ', @fnames;
207              
208             eval {
209 1         7 require DBI;
210 1         7 my $dbh = DBI->connect('dbi:SQLite:dbname=' . $file, '', '',
211             {RaiseError => 1, AutoCommit => 0});
212              
213 1         413 $dbh->do('DROP TABLE IF EXISTS moz_cookies;');
214              
215 1         539 $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         224 my $pholds = join ', ', ('?') x @fnames;
  1         6  
222 1         8 my $sth = $dbh->prepare(
223             "INSERT INTO moz_cookies($fnames) VALUES ($pholds)");
224             $self->scan($self->_scansub_maker(
225             sub {
226 5     5   11 my( $domain, $path, $key, $val, $secure, $expires ) = @_;
227 5 50       10 $secure = $secure ? 1 : 0;
228 5         63 $sth->execute($domain, $path, $key, $val, $secure, $expires);
229             }
230             )
231 1         64 );
232 1         21 $sth->finish();
233             }
234              
235 1         12698 $dbh->commit();
236 1         92 $dbh->disconnect();
237 1         53 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     2 or do {
277 0         0 carp "neither DBI nor pipe to sqlite3 worked ($@), install either one";
278 0         0 return;
279             };
280              
281 1         9 return 1;
282             }
283              
284             sub save {
285 4     4 1 2127 my( $self, $file ) = @_;
286              
287 4   0     16 $file ||= $self->{'file'} || do {
      33        
288             carp "save() did not get a filename!";
289             return;
290             };
291              
292 4 100       30 return $self->_save_ff3($file) if $file =~ m{\. sqlite}imsx;
293              
294 3         7 local $_;
295              
296 3         22 my $fh;
297 3 50       347 unless( open $fh, '>:utf8', $file ) {
298 0         0 carp "Could not open file [$file]: $!";
299 0         0 return;
300             }
301              
302 3         45 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   46 my( $domain, $path, $key, $val, $secure, $expires ) = @_;
313 15 50       39 $secure = $secure ? TRUE : FALSE;
314 15 50       64 my $bool = $domain =~ /^\./ ? TRUE : FALSE;
315 15         104 print $fh join( "\t", $domain, $bool, $path, $secure,
316             $expires, $key, $val ), "\n";
317             }
318             )
319 3         39 );
320              
321 3         175 close $fh;
322              
323 3         28 1;
324             }
325              
326             1;