• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

briandfoy / http-cookies-mozilla / 16927816716

15 Jul 2025 03:32PM UTC coverage: 59.633% (-0.6%) from 60.185%
16927816716

push

github

briandfoy
Updated GitHub workflows from https://github.com/briandfoy/github_workflows
272977a0f

* release.yml upgraded: 20250515.001 -> 20250713.001

65 of 109 relevant lines covered (59.63%)

15.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

59.63
/lib/HTTP/Cookies/Mozilla.pm
1
use v5.8;
18✔
2
package HTTP::Cookies::Mozilla;
3
use strict;
18✔
4

5
use warnings;
18✔
6
no warnings;
18✔
7

8
=encoding utf8
9

10
=head1 NAME
11

12
HTTP::Cookies::Mozilla - Cookie storage and management for Mozilla
13

14
=head1 SYNOPSIS
15

16
        use HTTP::Cookies::Mozilla;
17

18
        # see "SQLite access notes"
19
        my $file = ...; # Firefox profile dir / cookies.sqlite
20
        my $cookie_jar = HTTP::Cookies::Mozilla->new( file => $file );
21

22
        # otherwise same as HTTP::Cookies
23

24
=head1 DESCRIPTION
25

26
This package overrides the C<load()> and C<save()> methods of
27
L<HTTP::Cookies> so it can work with Mozilla cookie files. These might
28
be stored in the user profile directory as F<cookies>. On macOS,for
29
instance, that's F<~/Application Support/Firefox/*/cookies.sqlite>.
30

31
This module should be able to work with all Mozilla derived browsers
32
(FireBird, Camino, et alia).
33

34
Note that as of FireFox, version 3, the cookie file format changed
35
from plain text files to SQLite databases, so you will need to have
36
either L<DBI>/L<DBD::SQLite>, or the B<sqlite3> executable somewhere
37
in the path. Neither one has been put as explicit dependency, anyway,
38
so you'll get an exception if you try to use this module with a new
39
style file but without having any of them:
40

41
   neither DBI nor pipe to sqlite3 worked (%s), install either one
42

43
If your command-line B<sqlite3> is not in the C<$ENV{PATH}>, you can
44
set C<$HTTP::Cookies::Mozilla::SQLITE> to point to the actual program
45
to be used, e.g.:
46

47
   use HTTP::Cookies::Mozilla;
48
   $HTTP::Cookies::Mozilla::SQLITE = '/path/to/sqlite3';
49

50
Usage of the external program is supported under perl 5.8 onwards only,
51
because previous perl versions do not support L<perlfunc/open> with
52
more than three arguments, which are safer. If you are still sticking
53
to perl 5.6, you'll have to install L<DBI>/L<DBD::SQLite> to make
54
FireFox 3 cookies work.
55

56
=head2 SQLite access notes
57

58
SQLite allows a connection to lock the database for exclusive use, and
59
Firefox does this. If a browser is running, it probably has an
60
exclusive lock on the database file.
61

62
If you want to add cookies that the browser will see, you need to add
63
the cookies to the file that the browser would use. You can't add to
64
that while the browser is running because the database is locked, but
65
if you copy the file and try to replace it, you can miss updates that
66
the browser makes when it closes. You have to coordinate that yourself.
67

68
If you just want to read it, you may have to copy the file to another
69
location then use that.
70

71
=head2 Privacy settings
72

73
Firefox has a setting to erase all cookie and session data on quit.
74
With this set, all of your cookies will disappear, even if the expiry
75
times are in the future. Look in settings under "Privacy & Security"
76

77
=head2 Cookie data
78

79
Firefox tracks more information than L<HTTP::Cookies> tracks. So far
80
this module tracks host, path, name, value, and expiry because these are
81
the columns common among the different modules:
82

83
=over 4
84

85
=item * id (no support) - primary key row
86

87
=item * originAttributes (no support) - something about containers
88

89
=item * name (supported)
90

91
=item * value (supported)
92

93
=item * host (supported)
94

95
=item * path (supported)
96

97
=item * expiry (supported)
98

99
=item * lastAccessed (no support)
100

101
=item * creationTime (no support)
102

103
=item * isSecure (supported)
104

105
=item * isHttpOnly (no support)
106

107
=item * isBrowserElement (no support)
108

109
=item * sameSite (no support)
110

111
=item * rawSameSite (no support)
112

113
=item * rawSameSite (no support)
114

115
=back
116

117
=head1 SEE ALSO
118

119
=over 4
120

121
=item * L<HTTP::Cookies>.
122

123
=back
124

125
=head1 SOURCE AVAILABILITY
126

127
The source is in GitHub:
128

129
        https://github.com/briandfoy/http-cookies-mozilla
130

131
=head1 AUTHOR
132

133
Derived from Gisle Aas's HTTP::Cookies::Netscape package with very
134
few material changes.
135

136
Flavio Poletti added the SQLite support.
137

138
Maintained by brian d foy, C<< <briandfoy@pobox.com> >>
139

140
=head1 COPYRIGHT AND LICENSE
141

142
Parts Copyright 1997-1999 Gisle Aas.
143

144
Other parts Copyright 2018-2025 by brian d foy, C<< <briandfoy@pobox.com> >>
145

146
This library is free software; you can redistribute it and/or modify
147
it under the terms of the Artistic License 2.0.
148

149
=cut
150

151
use base qw( HTTP::Cookies );
18✔
152
use vars qw( $VERSION $SQLITE );
18✔
153

154
use Carp qw(carp);
18✔
155
use DBD::SQLite::Constants qw/:file_open/;
18✔
156

157
use constant TRUE  => 'TRUE';
18✔
158
use constant FALSE => 'FALSE';
18✔
159

160
$VERSION = '3.002';
161
$SQLITE = 'sqlite3';
162

163
sub _load_ff3 {
164
        my ($self, $file) = @_;
6✔
165
        my $cookies;
6✔
166
        my $query = 'SELECT host, path, name, value, isSecure, expiry FROM moz_cookies';
6✔
167

168
        eval {
169
                require DBI;
6✔
170
                my $dbh = DBI->connect('dbi:SQLite:dbname=' . $file, '', '', { RaiseError => 1, } );
6✔
171

172
                $cookies = $dbh->selectall_arrayref($query);
6✔
173
                $dbh->disconnect();
6✔
174
                1;
6✔
175
                }
176
        or eval {
177
                open my $fh, '-|', $SQLITE, $file, $query or die $!;
×
178
                $cookies = [ map { [ split /\|/ ] } <$fh> ];
×
179
                1;
×
180
                }
181
        or do {
6✔
182
                carp "neither DBI nor pipe to sqlite3 worked ($@), install either one";
×
183
                return;
×
184
                };
185

186
        for my $cookie ( @$cookies ) {
6✔
187
                my( $domain, $path, $key, $val, $secure, $expires ) = @$cookie;
36✔
188

189
                $self->set_cookie( undef, $key, $val, $path, $domain, undef,
36✔
190
                   0, $secure, $expires - _now(), 0 );
191
                }
192

193
        return 1;
6✔
194
}
195

196
sub load {
197
        my( $self, $file ) = @_;
18✔
198

199
        $file ||= $self->{'file'} || do {
18✔
200
                carp "load() did not get a filename!";
201
                return;
202
                };
203

204
        return $self->_load_ff3($file) if $file =~ m{\.sqlite}i;
18✔
205

206
        local $_;
12✔
207
        local $/ = "\n";  # make sure we got standard record separator
12✔
208

209
        my $fh;
12✔
210
        unless( open $fh, '<:utf8', $file ) {
12✔
211
                carp "Could not open file [$file]: $!";
×
212
                return;
×
213
                }
214

215
        my $magic = <$fh>;
12✔
216

217
        unless( $magic =~ /^\# HTTP Cookie File/ ) {
12✔
218
                carp "$file does not look like a Mozilla cookies file";
×
219
                close $fh;
×
220
                return;
×
221
                }
222

223
        while( <$fh> ) {
12✔
224
                next if /^\s*\#/;
120✔
225
                next if /^\s*$/;
84✔
226
                tr/\n\r//d;
72✔
227

228
                my( $domain, $bool1, $path, $secure, $expires, $key, $val )
72✔
229
                   = split /\t/;
230

231
                $secure = ( $secure eq TRUE );
72✔
232

233
                # The cookie format is an absolute time in epoch seconds, so
234
                # we subtract the current time (with appropriate offsets) to
235
                # get the max_age for the second-to-last argument.
236
                $self->set_cookie( undef, $key, $val, $path, $domain, undef,
72✔
237
                    0, $secure, $expires - _now(), 0 );
238
                }
239

240
        close $fh;
12✔
241

242
        1;
12✔
243
        }
244

245
BEGIN {
×
246
        my $EPOCH_OFFSET = $^O eq "MacOS" ? 21600 : 0;  # difference from Unix epoch
18✔
247
        sub _epoch_offset { $EPOCH_OFFSET }
180✔
248
        }
249

250
sub _now { time() - _epoch_offset() };
144✔
251

252
sub _scansub_maker {  # Encapsulate checks logic during cookie scan
253
        my ($self, $coresub) = @_;
6✔
254

255
        return sub {
256
                my( $version, $key, $val, $path, $domain, $port,
36✔
257
                    $path_spec, $secure, $expires, $discard, $rest ) = @_;
258

259
                return if $discard && not $self->{ignore_discard};
36✔
260

261
                $expires = $expires ? $expires - _epoch_offset() : 0;
36✔
262
                return if defined $expires && _now() > $expires;
36✔
263

264
                return $coresub->($domain, $path, $key, $val, $secure, $expires);
36✔
265
                };
6✔
266
        }
267

268
sub _save_ff3 {
269
        my ($self, $file) = @_;
×
270

271
        my @fnames = qw( host path name value isSecure expiry );
×
272
        my $fnames = join ', ', @fnames;
×
273

274
        eval {
275
                require DBI;
×
276
                my $dbh = DBI->connect('dbi:SQLite:dbname=' . $file, '', '',
×
277
                   {RaiseError => 1, AutoCommit => 0});
278

279
                $dbh->do('DROP TABLE IF EXISTS moz_cookies;');
×
280

281
                $dbh->do(<<'SQL');
×
282
CREATE TABLE moz_cookies (
283
        id INTEGER PRIMARY KEY,
284
        originAttributes TEXT NOT NULL DEFAULT '',
285
        name TEXT,
286
        value TEXT,
287
        host TEXT,
288
        path TEXT,
289
        expiry INTEGER,
290
        lastAccessed INTEGER,
291
        creationTime INTEGER,
292
        isSecure INTEGER,
293
        isHttpOnly INTEGER,
294
        inBrowserElement INTEGER DEFAULT 0,
295
        sameSite INTEGER DEFAULT 0,
296
        rawSameSite INTEGER DEFAULT 0,
297
        schemeMap INTEGER DEFAULT 0,
298
        isPartitionedAttributeSet INTEGER DEFAULT 0,
299
CONSTRAINT moz_uniqueid UNIQUE (
300
        name,
301
        host,
302
        path,
303
        originAttributes
304
))
305
SQL
306
                { # restrict scope for $sth
307
                my $pholds = join ', ', ('?') x @fnames;
×
308
                my $sth = $dbh->prepare(
×
309
                    "INSERT INTO moz_cookies($fnames) VALUES ($pholds)");
310
                $self->scan($self->_scansub_maker(
311
                        sub {
312
                                my( $domain, $path, $key, $val, $secure, $expires ) = @_;
×
313
                                $secure = $secure ? 1 : 0;
×
314
                                $sth->execute($domain, $path, $key, $val, $secure, $expires);
×
315
                                }
316
                                )
317
                        );
×
318
                $sth->finish();
×
319
                }
320

321
                $dbh->commit();
×
322
                $dbh->disconnect();
×
323
                1;
×
324
                }
325
        or eval {
326
                open my $fh, '|-', $SQLITE, $file or die $!;
×
327
                print {$fh} <<'INCIPIT';
×
328
BEGIN TRANSACTION;
329

330
DROP TABLE IF EXISTS moz_cookies;
331
CREATE TABLE moz_cookies (
332
        id INTEGER PRIMARY KEY,
333
        originAttributes TEXT NOT NULL DEFAULT '',
334
        name TEXT,
335
        value TEXT,
336
        host TEXT,
337
        path TEXT,
338
        expiry INTEGER,
339
        lastAccessed INTEGER,
340
        creationTime INTEGER,
341
        isSecure INTEGER,
342
        isHttpOnly INTEGER,
343
        inBrowserElement INTEGER DEFAULT 0,
344
        sameSite INTEGER DEFAULT 0,
345
        rawSameSite INTEGER DEFAULT 0,
346
        schemeMap INTEGER DEFAULT 0,
347
        isPartitionedAttributeSet INTEGER DEFAULT 0,
348
CONSTRAINT moz_uniqueid UNIQUE (
349
        name,
350
        host,
351
        path,
352
        originAttributes
353
));
354
INCIPIT
355

356
                $self->scan( $self->_scansub_maker(
357
                        sub {
358
                                my( $domain, $path, $key, $val, $secure, $expires ) = @_;
×
359
                                $secure = $secure ? 1 : 0;
×
360
                                my $values = join ', ',
361
                                        map {  # Encode all params as hex, a bit overkill
362
                                        my $hex = unpack 'H*', $_;
×
363
                                        "X'$hex'";
×
364
                                        } ( $domain, $path, $key, $val, $secure, $expires );
365
                                print {$fh}
×
366
                                        "INSERT INTO moz_cookies( $fnames ) VALUES ( $values );\n";
367
                                }
368
                        )
369
                );
×
370

371
                print {$fh} <<'EPILOGUE';
×
372

373
UPDATE moz_cookies SET lastAccessed = id;
374
END TRANSACTION;
375

376
EPILOGUE
377
        1;
×
378
        }
379
        or do {
×
380
                carp "neither DBI nor pipe to sqlite3 worked ($@), install either one";
×
381
                return;
×
382
        };
383

384
        return 1;
×
385
}
386

387
sub save {
388
        my( $self, $file ) = @_;
6✔
389

390
        $file ||= $self->{'file'} || do {
6✔
391
                carp "save() did not get a filename!";
392
                return;
393
                };
394

395
        return $self->_save_ff3($file) if $file =~ m{\. sqlite}imsx;
6✔
396

397
        local $_;
6✔
398

399
        my $fh;
6✔
400
        unless( open $fh, '>:utf8', $file ) {
6✔
401
                carp "Could not open file [$file]: $!";
×
402
                return;
×
403
                }
404

405
        print $fh <<'EOT';
6✔
406
# HTTP Cookie File
407
# http://www.netscape.com/newsref/std/cookie_spec.html
408
# This is a generated file!  Do not edit.
409
# To delete cookies, use the Cookie Manager.
410

411
EOT
412

413
        $self->scan($self->_scansub_maker(
414
                sub {
415
                        my( $domain, $path, $key, $val, $secure, $expires ) = @_;
36✔
416
                        $secure = $secure ? TRUE : FALSE;
36✔
417
                        my $bool = $domain =~ /^\./ ? TRUE : FALSE;
36✔
418
                        print $fh join( "\t", $domain, $bool, $path, $secure,
36✔
419
                                $expires, $key, $val ), "\n";
420
                        }
421
                        )
422
                );
6✔
423

424
        close $fh;
6✔
425

426
        1;
6✔
427
        }
428

429
1;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc