File Coverage

blib/lib/Cookie/Jar.pm
Criterion Covered Total %
statement 513 1134 45.2
branch 178 678 26.2
condition 121 597 20.2
subroutine 49 73 67.1
pod 41 41 100.0
total 902 2523 35.7


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## Cookies API for Server & Client - ~/lib/Cookie/Jar.pm
3             ## Version v0.3.3
4             ## Copyright(c) 2025 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2019/10/08
7             ## Modified 2025/07/19
8             ## You can use, copy, modify and redistribute this package and associated
9             ## files under the same terms as Perl itself.
10             ##----------------------------------------------------------------------------
11             package Cookie::Jar;
12             BEGIN
13 0         0 {
14 2     2   4816 use strict;
  2         5  
  2         81  
15 2     2   9 use warnings;
  2         4  
  2         142  
16 2     2   13 use warnings::register;
  2         3  
  2         119  
17 2     2   10 use parent qw( Module::Generic );
  2         3  
  2         15  
18 2     2   176 use vars qw( $VERSION $COOKIES_DEBUG $MOD_PERL $MOD_PERL_VERSION );
  2         4  
  2         530  
19 2     2   7 our( $MOD_PERL, $MOD_PERL_VERSION );
20 2 50 33     12 if( exists( $ENV{MOD_PERL} )
21             &&
22             ( $MOD_PERL = $ENV{MOD_PERL} =~ /^mod_perl\/(\d+\.[\d\.]+)/ ) )
23             {
24 0         0 $MOD_PERL_VERSION = $1;
25 0         0 select( ( select( STDOUT ), $| = 1 )[0] );
26 0         0 require Apache2::Const;
27 0         0 Apache2::Const->import( compile => qw( :common :http OK DECLINED ) );
28 0         0 require APR::Pool;
29 0         0 require APR::Table;
30 0         0 require Apache2::RequestUtil;
31 0         0 require APR::Request::Apache2;
32 0         0 require APR::Request::Cookie;
33             }
34 2     2   28 use Cookie;
  2         3  
  2         13  
35 2     2   1135 use Cookie::Domain;
  2         4  
  2         20  
36 2     2   529 use DateTime;
  2         4  
  2         40  
37 2     2   7 use JSON;
  2         3  
  2         19  
38 2     2   1626 use Module::Generic::HeaderValue;
  2         8343  
  2         22  
39 2     2   661 use Scalar::Util;
  2         5  
  2         109  
40 2     2   12 use URI::Escape ();
  2         4  
  2         94  
41 2         7 our $VERSION = 'v0.3.3';
42             # This flag to allow extensive debug message to be enabled
43 2         45 our $COOKIES_DEBUG = 0;
44 2     2   13 use constant CRYPTX_VERSION => '0.074';
  2         5  
  2         225  
45             };
46              
47 2     2   11 use strict;
  2         4  
  2         48  
48 2     2   10 use warnings;
  2         2  
  2         3337  
49              
50             sub init
51             {
52 8     8 1 396996 my $self = shift( @_ );
53             # Apache2::RequestRec object
54 8         17 my $req;
55 8 50 66     106 $req = shift( @_ ) if( @_ && ( @_ % 2 ) );
56             # For decryption and encryption
57 8         109 $self->{algo} = undef;
58             # If a cookie file is provided, yes, we'll automatically load and save from and to it.
59 8         27 $self->{autosave} = 1;
60             # For decryption and encryption
61 8         39 $self->{encrypt} = 0;
62 8         75 $self->{file} = '';
63 8         55 $self->{host} = '';
64             # For decryption and encryption
65 8         24 $self->{iv} = undef;
66             # For decryption and encryption
67 8         46 $self->{secret} = undef;
68             # Cookie file type; can also be 'lwp' or 'netscape'
69 8         31 $self->{type} = 'json';
70 8         25 $self->{_init_strict_use_sub} = 1;
71 8         70 $self->SUPER::init( @_ );
72 8 50       1333 $self->{request} = $req if( $req );
73             # Repository of all objects
74 8         31 $self->{_cookies} = [];
75             # Index by host, path, name
76 8         25 $self->{_index} = {};
77 8         42 my $file = $self->file;
78 8 0 33     6671 if( $file && $file->exists && !$file->is_empty )
      33        
79             {
80 0         0 my $encrypt = $self->encrypt;
81 0         0 my $type = $self->type;
82 0         0 my $type2sub =
83             {
84             json => \&load,
85             lwp => \&load_as_lwp,
86             netscape => \&load_as_netscape,
87             };
88 0 0       0 return( $self->error( "Unknown cookie jar type '$type'. This can be either json, lwp or netscape" ) ) if( !CORE::exists( $type2sub->{ $type } ) );
89 0         0 my $loader = $type2sub->{ $type };
90            
91 0 0       0 if( $encrypt )
92             {
93 0 0       0 $loader->( $self, $file,
94             algo => $self->algo,
95             key => $self->secret,
96             ) || return( $self->pass_error );
97             }
98             else
99             {
100 0 0       0 $loader->( $self, $file ) || return( $self->pass_error );
101             }
102             }
103 8         24 return( $self );
104             }
105              
106             sub add
107             {
108 16     16 1 87 my $self = shift( @_ );
109 16         32 my $this;
110 16 100       83 if( scalar( @_ ) == 1 )
    50          
111             {
112 13         45 $this = shift( @_ );
113             }
114             elsif( scalar( @_ ) )
115             {
116 3         21 $this = $self->_get_args_as_hash( @_ );
117             }
118             else
119             {
120 0         0 return( $self->error( "No data was provided to add a cookie in the repository." ) );
121             }
122            
123 16 100 33     4013 if( ref( $this ) eq 'HASH' )
    100 33        
    50          
124             {
125 3         34 $this = $self->make( $this );
126 3 50       16 return( $self->pass_error ) if( !defined( $this ) );
127             }
128             # A string ?
129             elsif( !ref( $this ) )
130             {
131 3   50     18 my $hv = Module::Generic::HeaderValue->new_from_header( $this, decode => 1, debug => $self->debug ) ||
132             return( $self->error( Module::Generic::HeaderValue->error ) );
133 3         152863 my $ref = {};
134 3         23 $ref->{name} = $hv->value->first;
135 3         5865 $ref->{value} = $hv->value->second;
136             $hv->params->foreach(sub
137             {
138 15     15   3740 my( $n, $v ) = @_;
139 15         385 $ref->{ $n } = $v;
140 15         41 return(1);
141 3         3351 });
142 3 50       70 $ref->{secure} = 1 if( CORE::exists( $ref->{secure} ) );
143             # In case those were provided too in the cookie line
144 3 50       23 $ref->{samesite} = 1 if( CORE::exists( $ref->{samesite} ) );
145 3 50       15 $ref->{httponly} = 1 if( CORE::exists( $ref->{httponly} ) );
146 3         44 $this = $self->make( %$ref );
147 3 50       57 return( $self->pass_error ) if( !defined( $this ) );
148             }
149             elsif( !$self->_is_object( $this ) ||
150             ( $self->_is_object( $this ) && !$this->isa( 'Cookie' ) ) )
151             {
152 0         0 return( $self->error( "I was expecting an hash reference or a Cookie object, but instead I got '$this'." ) );
153             }
154 16         3688 my $ref = $self->_cookies;
155 16         16851 my $idx = $self->_index;
156 16 50       19633 $this->name or return( $self->error( "No cookie name was set in this cookie." ) );
157 16   50     14161 my $key = $self->key( $this ) || return( $self->pass_error );
158 16         533 $ref->push( $this );
159 16 50       259 $idx->{ $key } = [] if( !CORE::exists( $idx->{ $key } ) );
160 16         1283 push( @{$idx->{ $key }}, $this );
  16         118  
161 16         574 return( $this );
162             }
163              
164 0     0 1 0 sub add_cookie_header { return( shift->add_request_header( @_ ) ); }
165              
166             sub add_request_header
167             {
168 5     5 1 12031 my $self = shift( @_ );
169 5   50     28 my $req = shift( @_ ) || return( $self->error( "No request object was provided." ) );
170 5 50       23 return( $self->error( "Request object provided is not an object." ) ) if( !Scalar::Util::blessed( $req ) );
171 5 50 33     85 return( $self->error( "Request object provided does not support the uri or header methods." ) ) if( !$req->can( 'uri' ) || !$req->can( 'header' ) );
172 5   50     235 my $uri = $req->uri || return( $self->error( "No uri set in the request object." ) );
173 5         183 my $scheme = $uri->scheme;
174 5 50       224 unless( $scheme =~ /^https?\z/ )
175             {
176 0         0 return( '' );
177             }
178 5         25 my( $host, $port, $path );
179 5 50       28 if( $host = $req->header( 'Host' ) )
180             {
181 5         542 $host =~ s/:(\d+)$//;
182 5         15 $host = lc( $host );
183 5         28 $port = $1;
184             }
185             else
186             {
187 0         0 $host = lc( $uri->host );
188             }
189 5 50       46 my $is_secure = ( $scheme eq 'https' ? 1 : 0 );
190             # URI::URL method
191 5 50       41 if( $uri->can( 'epath' ) )
192             {
193 0         0 $path = $uri->epath;
194             }
195             else
196             {
197             # URI::_generic method
198 5         40 $path = $uri->path;
199             }
200 5 50       115 $path = '/' unless( CORE::length( $path ) );
201 5 50 33     77 $port = $uri->port if( !defined( $port ) || !CORE::length( $port ) );
202             # my $now = time();
203 5         358 my $now = DateTime->now;
204 5 50       3064 $path = $self->_normalize_path( $path ) if( CORE::index( $path, '%' ) != -1 );
205 5         18 my $root;
206 5 50       30 if( $self->_is_ip( $host ) )
207             {
208 0         0 $root = $host;
209             }
210             else
211             {
212 5   50     9530 my $dom = Cookie::Domain->new || return( $self->pass_error( Cookie::Domain->error ) );
213 5         47732 my $res = $dom->stat( $host );
214 5 50       4475 return( $self->pass_error( $dom->error ) ) if( !defined( $res ) );
215 5 50 33     30 if( !CORE::length( $res ) || ( $res && !$res->domain->length ) )
      33        
216             {
217 0         0 return( $self->error( "No root domain found for host \"$host\"." ) );
218             }
219 5         186025 $root = $res->domain;
220             }
221             # rfc6265, section 5.4
222             # "Either:
223             # The cookie's host-only-flag is true and the canonicalized request-host is identical to the cookie's domain.
224             # Or:
225             # The cookie's host-only-flag is false and the canonicalized request-host domain-matches the cookie's domain."
226             # Meaning, $host is, for example, www.example.or.jp and cookie domain was not set and defaulted to example.or.jp, then it matches; or
227             # cookie domain was explicitly set to www.example.or.jp and matches www.example.or.jp
228             # <https://datatracker.ietf.org/doc/html/rfc6265#section-5.4>
229             # cookie values for the "Cookie" header
230 5         67247 my @values = ();
231 5         1164 my @ok_cookies = ();
232             # Get all cookies for the canonicalised request-host and its sub domains, then we check each one found according to rfc6265 algorithm as stated above
233 5         121 my $cookies = $self->get_by_domain( $root, with_subdomain => 1 );
234             # Ref: rfc6265, section 5.4
235             # <https://datatracker.ietf.org/doc/html/rfc6265#section-5.4>
236 5         5236 foreach my $c ( @$cookies )
237             {
238 12 0 33     159 unless( $c->host_only && $root eq $c->domain ||
      0        
      33        
239             !$c->host_only && $host eq $c->domain )
240             {
241 0         0 next;
242             }
243 12 100 33     11571 if( index( $path, $c->path ) != 0 )
    50 100        
    100 33        
    50          
244             {
245 1         1070 next;
246             }
247             elsif( !$is_secure && $c->secure )
248             {
249 0         0 next;
250             }
251             # elsif( $c->expires && $c->expires->epoch < $now )
252             elsif( $c->expires && $c->expires < $now )
253             {
254 1         1666 next;
255             }
256             elsif( $c->port && $c->port != $port )
257             {
258 0         0 next;
259             }
260 10         18172 push( @ok_cookies, $c );
261             }
262            
263             # sort cookies by path and by creation date.
264             # Ref: rfc6265, section 5.4.2:
265             # "Cookies with longer paths are listed before cookies with shorter paths."
266             # "Among cookies that have equal-length path fields, cookies with earlier creation-times are listed before cookies with later creation-times."
267             # <https://datatracker.ietf.org/doc/html/rfc6265#section-5.4>
268             # The OR here actually means AND, since the <=> comparison returns false when 2 elements are equal
269             # So when 2 path are the same, we differentiate them by their creation date
270 5 50       32 foreach my $c ( sort{ $b->path->length <=> $a->path->length || $a->created_on <=> $b->created_on } @ok_cookies )
  5         1348  
271             {
272 10         131649 push( @values, $c->as_string({ is_request => 1 }) );
273             # rfc6265, section 5.4.3
274             # <https://datatracker.ietf.org/doc/html/rfc6265#section-5.4>
275             # "Update the last-access-time of each cookie in the cookie-list to the current date and time."
276 10         114 $c->accessed_on( time() );
277             }
278              
279 5 50       32201 if( @values )
280             {
281 5 50       19146 if( my $old = $req->header( 'Cookie' ) )
282             {
283 0         0 unshift( @values, $old );
284             }
285 5         787 $req->header( Cookie => join( '; ', @values ) );
286             }
287 5         418 return( $req );
288             }
289              
290             sub add_response_header
291             {
292 0     0 1 0 my $self = shift( @_ );
293 0         0 my $resp = shift( @_ );
294 0         0 my $r = $self->request;
295 0 0       0 if( $resp )
296             {
297 0 0       0 return( $self->error( "Request object provided is not an object." ) ) if( !$self->_is_object( $resp ) );
298 0 0       0 return( $self->error( "Request object provided does not support the header methods." ) ) if( !$resp->can( 'header' ) );
299             }
300 0         0 my @values = ();
301 0         0 my $ref = $self->_cookies;
302 0         0 foreach my $c ( sort{ $a->path->length <=> $b->path->length } @$ref )
  0         0  
303             {
304 0         0 $c->debug( $self->debug );
305 0 0       0 if( $c->discard )
306             {
307 0         0 next;
308             }
309            
310 0 0       0 if( $resp )
    0          
311             {
312 0         0 $resp->headers->push_header( 'Set-Cookie' => "$c" );
313             }
314             elsif( $r )
315             {
316             # APR::Table
317             # We use 'add' and not 'set'
318 0         0 $r->err_headers_out->add( 'Set-Cookie' => "$c" );
319             }
320             else
321             {
322 0         0 push( @values, "Set-Cookie: $c" );
323             }
324             }
325 0 0       0 if( @values )
326             {
327 0 0       0 return( wantarray() ? @values : join( "\015\012", @values ) );
328             }
329             # We return our object only if a response object or an Apache2::RequestRec was set
330             # because otherwise if the user is expecting the cookie as a returned string,
331             # we do not want to return our object instead when there is no cookie to return.
332 0 0 0     0 return( $self ) if( $r || $resp );
333 0         0 return( '' );
334             }
335              
336             # NOTE: the algorithm used, if any, to decrypt or encrypt the cookie jar file
337 0     0 1 0 sub algo { return( shift->_set_get_scalar( 'algo', @_ ) ); }
338              
339 8     8 1 52 sub autosave { return( shift->_set_get_boolean( 'autosave', @_ ) ); }
340              
341             sub delete
342             {
343 1     1 1 3 my $self = shift( @_ );
344 2     2   17 no overloading;
  2         2  
  2         10855  
345 1         6 my $ref = $self->_cookies;
346 1         853 my $idx = $self->_index;
347 1 50 33     925 if( scalar( @_ ) == 1 && $self->_is_a( $_[0], 'Cookie' ) )
348             {
349 1         48 my $c = shift( @_ );
350 1         6 my $addr = Scalar::Util::refaddr( $c );
351 1         7 my $removed = $self->new_array;
352 1         871 for( my $i = 0; $i < scalar( @$ref ); $i++ )
353             {
354 3         24 my $this = $ref->[$i];
355 3 100       18 if( Scalar::Util::refaddr( $this ) eq $addr )
356             {
357 1         11 my $key = $self->key( $this );
358 1 50       33 if( CORE::exists( $idx->{ $key } ) )
359             {
360             # if( !$self->_is_array( $idx->{ $key } ) )
361 1 50       40 if( Scalar::Util::reftype( $idx->{ $key } ) ne 'ARRAY' )
362             {
363 0   0     0 return( $self->error( "I was expecting an array for key '$key', but got '", overload::StrVal( $idx->{ $key } // 'undef' ), "' (", ref( $idx->{ $key } ), ")" ) );
364             }
365 1         49 for( my $j = 0; $j < scalar( @{$idx->{ $key }} ); $j++ )
  2         9  
366             {
367 1 50       31 if( Scalar::Util::refaddr( $idx->{ $key }->[$j] ) eq $addr )
368             {
369 1         59 CORE::splice( @{$idx->{ $key }}, $j, 1 );
  1         8  
370 1         33 $j--;
371             }
372             }
373             # Cleanup
374 1 50       25 CORE::delete( $idx->{ $key } ) if( scalar( @{$idx->{ $key }} ) == 0 );
  1         9  
375             }
376 1         96 CORE::splice( @$ref, $i, 1 );
377 1         8 $i--;
378 1         8 $removed->push( $c );
379             }
380             }
381 1         11 return( $removed );
382             }
383             else
384             {
385 0         0 my( $name, $host, $path ) = @_;
386 0   0     0 $host ||= $self->host || '';
      0        
387 0   0     0 $path //= '';
388 0 0 0     0 return( $self->error( "No cookie object provided nor any cookie name either." ) ) if( !defined( $name ) || !CORE::length( "$name" ) );
389 0         0 my $key = $self->key( $name => $host, $path );
390 0         0 my $removed = $self->new_array;
391 0 0       0 return( $removed ) if( !CORE::exists( $idx->{ $key } ) );
392 0 0 0     0 return( $self->error( "I was expecting an array for key '$key', but got '", overload::StrVal( $idx->{ $key } // 'undef' ), "'" ) ) if( !$self->_is_array( $idx->{ $key } ) );
393 0         0 $removed->push( @{$idx->{ $key }} );
  0         0  
394 0         0 foreach my $c ( @$removed )
395             {
396 0 0 0     0 next if( !ref( $c ) || !$self->_is_a( $c, 'Cookie' ) );
397 0         0 my $addr = Scalar::Util::refaddr( $c );
398 0         0 for( my $i = 0; $i < scalar( @$ref ); $i++ )
399             {
400 0 0       0 if( Scalar::Util::refaddr( $ref->[$i] ) eq $addr )
401             {
402 0         0 CORE::splice( @$ref, $i, 1 );
403 0         0 last;
404             }
405             }
406             }
407             # Remove cookie and return the previous entry
408 0         0 CORE::delete( $idx->{ $key } );
409 0         0 return( $removed );
410             }
411             }
412              
413             sub do
414             {
415 2     2 1 3359 my $self = shift( @_ );
416 2   50     13 my $code = shift( @_ ) || return( $self->error( "No callback code was provided." ) );
417 2 50       21 return( $self->error( "Callback code provided is not a code." ) ) if( ref( $code ) ne 'CODE' );
418 2         20 my $ref = $self->_cookies->clone;
419 2         2102 my $all = $self->new_array;
420 2         1483 foreach my $c ( @$ref )
421             {
422 6 50 33     72 next if( !ref( $c ) || !$self->_is_a( $c, 'Cookie' ) );
423             # try-catch
424 6         255 local $@;
425             eval
426 6         15 {
427 6         15 local $_ = $c;
428 6         29 my $rv = $code->( $c );
429 6 50       1249079 if( !defined( $rv ) )
    50          
430             {
431 0         0 last;
432             }
433             elsif( $rv )
434             {
435 6         62 $all->push( $c );
436             }
437             };
438 6 50       93 if( $@ )
439             {
440 0         0 return( $self->error( "An unexpected error occurred while calling code reference on cookie named \"", $ref->{ $c }->name, "\": $@" ) );
441             }
442             }
443 2         12 return( $all );
444             }
445              
446             # NOTE: Should we decrypt or encrypt the cookie jar file?
447 0     0 1 0 sub encrypt { return( shift->_set_get_boolean( 'encrypt', @_ ) ); }
448              
449             sub exists
450             {
451 0     0 1 0 my $self = shift( @_ );
452 0         0 my( $name, $host, $path ) = @_;
453 0   0     0 $host ||= $self->host || '';
      0        
454 0   0     0 $path //= '';
455 0 0 0     0 return( $self->error( "No cookie name was provided to check if it exists." ) ) if( !defined( $name ) || !CORE::length( $name ) );
456 0         0 my $c = $self->get( $name => $host, $path );
457 0 0       0 return( defined( $c ) ? 1 : 0 );
458             }
459              
460             # From http client point of view
461             sub extract
462             {
463 4     4 1 867 my $self = shift( @_ );
464 4   50     17 my $resp = shift( @_ ) || return( $self->error( "No response object was provided." ) );
465 4 50       16 return( $self->error( "Response object provided is not an object." ) ) if( !Scalar::Util::blessed( $resp ) );
466 4         35 my $uri;
467 4 50 0     25 if( $self->_is_a( $resp, 'HTTP::Response' ) )
    0          
468             {
469 4         190 my $req = $resp->request;
470 4 50       63 return( $self->error( "No HTTP::Request object is set in this HTTP::Response." ) ) if( !$resp->request );
471 4         46 $uri = $resp->request->uri;
472             }
473             elsif( $resp->can( 'uri' ) && $resp->can( 'header' ) )
474             {
475 0         0 $uri = $resp->uri;
476             }
477             else
478             {
479 0         0 return( $self->error( "Response object provided does not support the uri or scheme methods and is not a class or subclass of HTTP::Response either." ) );
480             }
481 4   50     115 my $all = Module::Generic::HeaderValue->new_from_multi( [$resp->header( 'Set-Cookie' )], debug => $self->debug, decode => 1 ) ||
482             return( $self->pass_error( Module::Generic::HeaderValue->error ) );
483 4 50       85267 return( $resp ) unless( $all->length );
484 4 50       160990 $uri || return( $self->error( "No uri set in the response object." ) );
485 4         10673 my( $host, $port, $path );
486 4 50 33     60 if( $host = $resp->header( 'Host' ) ||
487             ( $resp->request && ( $host = $resp->request->header( 'Host' ) ) ) )
488             {
489 4         759 $host =~ s/:(\d+)$//;
490 4         12 $host = lc( $host );
491 4         27 $port = $1;
492             }
493             else
494             {
495 0         0 $host = lc( $uri->host );
496             }
497            
498             # URI::URL method
499 4 50       37 if( $uri->can( 'epath' ) )
500             {
501 0         0 $path = $uri->epath;
502             }
503             else
504             {
505             # URI::_generic method
506 4         34 $path = $uri->path;
507             }
508 4 50       101 $path = '/' unless( CORE::length( $path ) );
509 4 50 33     59 $port = $uri->port if( !defined( $port ) || !CORE::length( $port ) );
510 4         350 my $root;
511 4 50       51 if( $self->_is_ip( $host ) )
512             {
513 0         0 $root = $host;
514             }
515             else
516             {
517 4   50     11039 my $dom = Cookie::Domain->new || return( $self->pass_error( Cookie::Domain->error ) );
518 4         37334 my $res = $dom->stat( $host );
519 4 50       5316 if( !defined( $res ) )
520             {
521 0         0 return( $self->pass_error( $dom->error ) );
522             }
523             # Possibly empty
524 4 50       144 $root = $res ? $res->domain : '';
525             }
526            
527 4         55043 foreach my $o ( @$all )
528             {
529 4         1831 my( $name, $value ) = $o->value->list;
530 4   50     6909 my $c = Cookie->new( name => $name, value => $value ) ||
531             return( $self->pass_error( Cookie->error ) );
532 4 100       74 if( CORE::length( $o->param( 'expires' ) ) )
    50          
533             {
534 2         1923 my $dt = $self->_parse_timestamp( $o->param( 'expire' ) );
535 2 50       1967 if( $dt )
536             {
537 0         0 $c->expires( $dt );
538             }
539             else
540             {
541 2         11 $c->expires( $o->param( 'expires' ) );
542             }
543             }
544             elsif( CORE::length( $o->param( 'max-age' ) ) )
545             {
546 0         0 $c->max_age( $o->param( 'max-age' ) );
547             }
548            
549 4 50       7217 if( $o->param( 'domain' ) )
550             {
551             # rfc6265, section 5.2.3:
552             # "If the first character of the attribute-value string is %x2E ("."): Let cookie-domain be the attribute-value without the leading %x2E (".") character."
553             # Ref: <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3>
554 0         0 my $c_dom = $o->param( 'domain' );
555             # Remove leading dot as per rfc specifications
556 0         0 $c_dom =~ s/^\.//g;
557             # "Convert the cookie-domain to lower case."
558 0         0 $c_dom = lc( $c_dom );
559             # Check the domain name is legitimate, i.e. sent from a host that has authority
560             # "The user agent will reject cookies unless the Domain attribute specifies a scope for the cookie that would include the origin server. For example, the user agent will accept a cookie with a Domain attribute of "example.com" or of "foo.example.com" from foo.example.com, but the user agent will not accept a cookie with a Domain attribute of "bar.example.com" or of "baz.foo.example.com"."
561             # <https://tools.ietf.org/html/rfc6265#section-4.1.2.3>
562 0 0 0     0 if( CORE::length( $c_dom ) >= CORE::length( $root ) &&
      0        
563             ( $c_dom eq $host || $host =~ /\.$c_dom$/ ) )
564             {
565 0         0 $c->domain( $c_dom );
566             }
567             else
568             {
569 0         0 next;
570             }
571             }
572             # "If omitted, defaults to the host of the current document URL, not including subdomains."
573             # <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie>
574             else
575             {
576 4 50       3385 if( $root )
577             {
578 4         139 $c->domain( $root );
579 4         7656 $c->implicit(1);
580             }
581             else
582             {
583             }
584             }
585            
586             # rfc6265: "If the server omits the Path attribute, the user agent will use the "directory" of the request-uri's path component as the default value."
587 4 100 66     4344 if( defined( $o->param( 'path' ) ) && CORE::length( $o->param( 'path' ) ) )
588             {
589 3         4826 $c->path( $o->param( 'path' ) );
590             }
591             else
592             {
593 1         878 my $frag = $self->new_array( [split( /\//, $path )] );
594             # Not perfect
595 1 50 33     826 if( $path eq '/' || substr( $path, -1, 1 ) eq '/' )
596             {
597 1         393 $c->path( $path );
598             }
599             else
600             {
601 0         0 $frag->pop;
602 0         0 $c->path( $frag->join( '/' )->scalar );
603             }
604             }
605 4 50       7723 $c->port( $port ) if( defined( $port ) );
606 4 50       155742 $c->http_only(1) if( $o->param( 'httponly' ) );
607 4 50       3869 $c->secure(1) if( $o->param( 'secure' ) );
608 4 50       3339 $c->same_site(1) if( $o->param( 'samesite' ) );
609            
610 4         3384 my @old = $self->get({ name => $c->name, host => $c->domain, path => $c->path });
611 4 100       47 if( scalar( @old ) )
612             {
613 1 50       13 $c->created_on( $old[0]->created_on ) if( $old[0]->created_on );
614             # $self->replace( $c );
615 1         1420 for( @old )
616             {
617 1         4 my $arr;
618             $arr = $self->delete( $_ ) || do
619 1   33     29 {
620             # Error trying to remove cookie
621             };
622             }
623             }
624 4 50       291 $self->message( 3, "Adding cookie name '", $c->name, "'." ) if( $COOKIES_DEBUG );
625 4 50       82 $self->add( $c ) || return( $self->pass_error );
626             }
627 4         4136 return( $self );
628             }
629              
630 0     0 1 0 sub extract_cookies { return( shift->extract( @_ ) ); }
631              
632             sub extract_one
633             {
634 1     1 1 35 my $self = shift( @_ );
635 1         6 my $str = shift( @_ );
636 1         14 my $opts = $self->_get_args_as_hash( @_ );
637 1   50     1790 $opts->{path} //= '/';
638 1 50       13 return( $self->error( "No cookie data was provided." ) ) if( !length( "$str" ) );
639            
640 1         7 my( $host, $root );
641 1 50 33     16 if( defined( $opts->{host} ) && CORE::length( $opts->{host} ) )
642             {
643 1         3 $host = $opts->{host};
644 1 50       12 if( $self->_is_ip( $host ) )
645             {
646 0         0 $root = $host;
647             }
648             else
649             {
650 1   50     2126 my $dom = Cookie::Domain->new || return( $self->pass_error( Cookie::Domain->error ) );
651 1         12005 my $res = $dom->stat( $host );
652 1 50       1401 if( !defined( $res ) )
653             {
654 0         0 return( $self->pass_error( $dom->error ) );
655             }
656             # Possibly empty
657 1 50       13 $root = $res ? $res->domain : '';
658             }
659             }
660              
661 1   50     16057 my $o = Module::Generic::HeaderValue->new_from_header( "$str" ) ||
662             return( $self->pass_error( Module::Generic::HeaderValue->error ) );
663 1         75050 my( $name, $value ) = $o->value->list;
664 1   50     2299 my $c = Cookie->new( name => $name, value => $value ) ||
665             return( $self->pass_error( Cookie->error ) );
666 1 50       12 if( CORE::length( $o->param( 'expires' ) ) )
    0          
667             {
668 1         1026 my $dt = $self->_parse_timestamp( $o->param( 'expire' ) );
669 1 50       1102 if( $dt )
670             {
671 0         0 $c->expires( $dt );
672             }
673             else
674             {
675 1         11 $c->expires( $o->param( 'expires' ) );
676             }
677             }
678             elsif( CORE::length( $o->param( 'max-age' ) ) )
679             {
680 0         0 $c->max_age( $o->param( 'max-age' ) );
681             }
682            
683 1 50       1198 if( $o->param( 'domain' ) )
684             {
685             # rfc6265, section 5.2.3:
686             # "If the first character of the attribute-value string is %x2E ("."): Let cookie-domain be the attribute-value without the leading %x2E (".") character."
687             # Ref: <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3>
688 0         0 my $c_dom = $o->param( 'domain' );
689             # Remove leading dot as per rfc specifications
690 0         0 $c_dom =~ s/^\.//g;
691             # "Convert the cookie-domain to lower case."
692 0         0 $c_dom = lc( $c_dom );
693 0         0 $c->domain( $c_dom );
694             }
695             # "If omitted, defaults to the host of the current document URL, not including subdomains."
696             # <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie>
697             else
698             {
699 1 50       987 if( $root )
700             {
701 1         34 $c->domain( $root );
702 1         2217 $c->implicit(1);
703             }
704             else
705             {
706             }
707             }
708            
709             # rfc6265: "If the server omits the Path attribute, the user agent will use the "directory" of the request-uri's path component as the default value."
710 1 50 33     1139 if( defined( $o->param( 'path' ) ) && CORE::length( $o->param( 'path' ) ) )
711             {
712 1         1964 $c->path( $o->param( 'path' ) );
713             }
714             else
715             {
716 0         0 my $frag = $self->new_array( [split( /\//, $opts->{path} )] );
717             # Not perfect
718 0 0 0     0 if( $opts->{path} eq '/' || substr( $opts->{path}, -1, 1 ) eq '/' )
719             {
720 0         0 $c->path( $opts->{path} );
721             }
722             else
723             {
724 0         0 $frag->pop;
725 0         0 $c->path( $frag->join( '/' )->scalar );
726             }
727             }
728 1 50 33     2147 $c->port( $opts->{port} ) if( defined( $opts->{port} ) && $self->_is_integer( $opts->{port} ) );
729 1 50       41309 $c->http_only(1) if( $o->param( 'httponly' ) );
730 1 50       663 $c->secure(1) if( $o->param( 'secure' ) );
731 1 50       990 $c->same_site(1) if( $o->param( 'samesite' ) );
732 1         768 return( $c );
733             }
734              
735             # From server point of view
736             sub fetch
737             {
738 1     1 1 3518 my $self = shift( @_ );
739 1         38 my $opts = $self->_get_args_as_hash( @_ );
740 1   50     829 $opts->{string} //= '';
741 1 50       19 $opts->{store} = 1 if( !CORE::exists( $opts->{store} ) );
742 1   50     12 my $host = $opts->{host} || $self->host || '';
743 1         1193 my $cookie_header;
744 1         11 my $r = $self->request;
745 1         77 my $cookies = [];
746 1 50 33     15 if( $r )
    50 33        
    0          
747             {
748             # try-catch
749 0         0 local $@;
750             eval
751 0         0 {
752 0         0 my $pool = $r->pool;
753             # my $o = APR::Request::Apache2->handle( $r->pool );
754 0         0 my $o = APR::Request::Apache2->handle( $r );
755 0 0       0 if( $o->jar_status =~ /^(?:Missing input data|Success)$/ )
756             {
757             # all cookie names in order of appearance in the Cookie request header
758 0         0 my @all = $o->jar;
759 0         0 foreach my $cookie_name ( @all )
760             {
761 0         0 my @values = $o->jar( $cookie_name );
762 0         0 foreach my $v ( @values )
763             {
764             # And of course, Apache/modperl does not uri decode the cookie value...
765 0         0 $v = URI::Escape::uri_unescape( $v );
766 0         0 my $c = $self->make( name => $cookie_name, value => $v );
767 0         0 push( @$cookies, $c );
768             }
769             }
770             }
771             else
772             {
773             # Malformed cookie found:
774             }
775             };
776 0 0       0 if( $@ )
777             {
778             # An error occurred while trying to get cookies using APR::Request::Apache2, reverting to Cookie header.
779             }
780 0         0 $cookie_header = $r->headers_in->get( 'Cookie' );
781             }
782             elsif( $opts->{request} && $self->_is_object( $opts->{request} ) && $opts->{request}->can( 'header' ) )
783             {
784 1         34 $cookie_header = $opts->{request}->header( 'Cookie' );
785             }
786             elsif( CORE::length( $opts->{string} ) )
787             {
788 0         0 $cookie_header = $opts->{string};
789             }
790             else
791             {
792 0   0     0 $cookie_header = $ENV{HTTP_COOKIE} // '';
793             }
794 1 50       47 if( !scalar( @$cookies ) )
795             {
796 1         10 my $ref = $self->parse( $cookie_header );
797 1         5 foreach my $def ( @$ref )
798             {
799 3   50     22 my $c = $self->make( name => $def->{name}, value => $def->{value} ) ||
800             return( $self->pass_error );
801 3         42 push( @$cookies, $c );
802             }
803             }
804             # We are called in void context like $jar->fetch which means we fetch the cookies and add them to our stack internally
805 1 50       301 if( $opts->{store} )
806             {
807 1         5 foreach my $c ( @$cookies )
808             {
809 3 50       279 $self->add( $c ) || return( $self->pass_error );
810             }
811             }
812 1         14 return( $self->new_array( $cookies ) );
813             }
814              
815             # NOTE: the location of the cookie jar file
816 16     16 1 148 sub file { return( shift->_set_get_file( 'file', @_ ) ); }
817              
818             sub get
819             {
820 8     8 1 8423 my $self = shift( @_ );
821             # If called on the server side, $host and $path would likely be undefined
822             # my( $name, $host, $path ) = @_;
823 8         44 my( $name, $host, $path );
824 8 50 66     180 if( scalar( @_ ) == 1 && $self->_is_a( $_[0], 'Cookie' ) )
    100 66        
    50 33        
825             {
826 0         0 my $c = shift( @_ );
827 0         0 $name = $c->name;
828 0         0 $host = $c->host;
829 0         0 $path = $c->path;
830             }
831             elsif( scalar( @_ ) == 1 && ref( $_[0] ) eq 'HASH' )
832             {
833 4         174 my $this = shift( @_ );
834 4         29 ( $name, $host, $path ) = @$this{qw( name host path )};
835             }
836             elsif( scalar( @_ ) > 0 && scalar( @_ ) <= 3 )
837             {
838 4         22 ( $name, $host, $path ) = @_;
839             }
840             else
841             {
842 0         0 return( $self->error( "Error calling get: I was expecting either a Cookie object, or a list or hash reference of parameters." ) );
843             }
844 8 50 33     128 return( $self->error( "No cookie name was provided to get its object." ) ) if( !defined( $name ) || !CORE::length( $name ) );
845 8   0     167 $host //= $self->host || '';
      33        
846 8   100     73 $path //= '';
847 8         98 my $ref = $self->_cookies;
848 8         8971 my $idx = $self->_index;
849 8         9386 my $key = $self->key( $name => $host, $path );
850             # Return immediately if we found a perfect match
851 8 50       273 if( CORE::exists( $idx->{ $key } ) )
852             {
853 0 0       0 return( wantarray() ? @{$idx->{ $key }} : $idx->{ $key }->[0] );
  0         0  
854             }
855             # If it does not exist, we check each of our cookie to see if it is a higher level cookie.
856             # For example, $host is www.example.org and our cookie key host part is example.org
857             # In this case, example.org would match, because the cookie would apply also to sub domains.
858 8         322 my @found = ();
859 8         63 foreach my $c ( @$ref )
860             {
861 18         231 my $c_name = $c->name;
862 18         18143 my $c_host = $c->domain;
863 18         18227 my $c_path = $c->path;
864            
865 18 100       18089 next unless( $c_name eq $name );
866            
867              
868 5 50 33     136 if( !defined( $host ) || !CORE::length( $host ) )
869             {
870 0         0 push( @found, $c );
871 0         0 next;
872             }
873            
874 5 50 33     74 if( defined( $c_host ) &&
      66        
875             ( $host eq $c_host || index( reverse( $host ), reverse( ".${c_host}" ) ) == 0 ) )
876             {
877 4 100 66     102 if( defined( $path ) && CORE::length( "$path" ) )
878             {
879 1 50       11 if( index( $path, $c_path ) == 0 )
880             {
881 1         20 push( @found, $c );
882             }
883             }
884             else
885             {
886 3         50 push( @found, $c );
887             }
888             }
889             }
890            
891 8 100       119 if( scalar( @found ) )
892             {
893 4 100       34 return( wantarray() ? @found : $found[0] );
894             }
895            
896             # Ultimately, check if there is a cookie entry with just the cookie name and no host
897             # which happens for cookies repository on server side
898 4 100       25 if( CORE::exists( $idx->{ $name } ) )
899             {
900 1 50       82 return( wantarray() ? @{$idx->{ $name }} : $idx->{ $name }->[0] );
  0         0  
901             }
902 3         166 return;
903             }
904              
905             sub get_by_domain
906             {
907 5     5 1 15 my $self = shift( @_ );
908 5         23 my $host = shift( @_ );
909 5         32 my $opts = $self->_get_args_as_hash( @_ );
910 5         5854 $opts->{with_subdomain} = 0;
911 5 50       65 $opts->{sort} = 1 if( !CORE::exists( $opts->{sort} ) );
912 5         29 my $all = $self->new_array;
913 5 50 33     3451 return( $all ) if( !defined( $host ) || !CORE::length( $host ) );
914 5         105 $host = lc( $host );
915 5         115 my $ref = $self->_cookies;
916 5         3824 foreach my $c ( @$ref )
917             {
918 12         258 my $dom = $c->domain;
919 12 50 0     9728 $all->push( $c ) if( $dom eq $host || ( $opts->{with_subdomain} && $host =~ /\.$dom$/ ) );
      33        
920             }
921 5         137 my $new = [];
922 5 50       42 if( $opts->{sort} )
923             {
924 5         70 $new = [sort{ $a->path cmp $b->path } @$all];
  10         5342  
925             }
926             else
927             {
928 0         0 $new = [sort{ $b->path cmp $a->path } @$all];
  0         0  
929             }
930 5         3674 return( $self->new_array( $new ) );
931             }
932              
933 3     3 1 26 sub host { return( shift->_set_get_scalar_as_object( 'host', @_ ) ); }
934              
935 0     0 1 0 sub iv { return( shift->_initialisation_vector( @_ ) ); }
936              
937             sub key
938             {
939 25     25 1 108 my $self = shift( @_ );
940 25         93 my( $name, $host, $path );
941 25 100 66     260 if( scalar( @_ ) == 1 && $self->_is_a( $_[0], 'Cookie' ) )
942             {
943 17         771 my $c = shift( @_ );
944 17         107 $name = $c->name;
945 17         15054 $host = $c->domain;
946 17         17484 $path = $c->path;
947             }
948             else
949             {
950 8         81 ( $name, $host, $path ) = @_;
951 8 50 0     123 return( $self->error( "Received cookie object '", overload::StrVal( $name // 'undef' ), "' along with cookie host '$host' and path '$path' while I was expecting cookie name, host and path. If you want to call key() with a cookie object, pass it with no other argument." ) ) if( ref( $name ) && $self->_is_a( $name, ref( $self ) ) );
      66        
952             }
953 25 50       17791 return( $self->error( "No cookie name was provided to get its key." ) ) if( !CORE::length( $name ) );
954 25 100 66     472 return( join( ';', $host, $path, $name ) ) if( defined( $host ) && CORE::length( $host ) );
955 3         18 return( $name );
956             }
957              
958 0     0 1 0 sub length { return( shift->repo->length ); }
959              
960             # Load cookie data from json cookie file
961             sub load
962             {
963 1     1 1 15 my $self = shift( @_ );
964 1   50     7 my $file = shift( @_ ) || return( $self->error( "No filename was provided." ) );
965 1         30 my $opts = $self->_get_args_as_hash( @_ );
966 1   50     16 $opts->{host} //= '';
967 1   50     14 $opts->{decrypt} //= 0;
968 1   50     8 $opts->{algo} //= '';
969             # Initialisation Vector for encryption
970             # Re-use it if it was previously set
971 1   50     12 $opts->{iv} //= $self->_initialisation_vector->scalar || '';
      33        
972 1   50     1212 my $host = $opts->{host} || $self->host || '';
973 1   50     1163 my $f = $self->new_file( $file ) || return( $self->pass_error );
974 1         135766 my $json = $f->load;
975 1 50       9637 return( $self->pass_error( $f->error ) ) if( !defined( $json ) );
976             # No need to go further
977 1 50       35 if( !CORE::length( $json ) )
978             {
979 0         0 return( $self );
980             }
981            
982 1 50       19 if( $opts->{decrypt} )
983             {
984 0         0 my $key = $opts->{key};
985 0         0 my $algo = $opts->{algo};
986 0 0 0     0 return( $self->error( "Cookies file encryption was enabled, but no key was set to decrypt it." ) ) if( !defined( $key ) || !CORE::length( "$key" ) );
987 0 0 0     0 return( $self->error( "Cookies file encryption was enabled, but no algorithm was set to decrypt it." ) ) if( !defined( $algo ) || !CORE::length( "$algo" ) );
988 0 0       0 $self->_load_class( 'Crypt::Misc', { version => CRYPTX_VERSION } ) || return( $self->pass_error );
989 0   0     0 my $p = $self->_encrypt_objects( @$opts{qw( key algo iv )} ) || return( $self->pass_error );
990             # try-catch
991 0         0 local $@;
992             eval
993 0         0 {
994 0         0 my $crypt = $p->{crypt};
995 0         0 my $bin = Crypt::Misc::decode_b64( "$json" );
996 0         0 $json = $crypt->decrypt( "$bin", @$p{qw( key iv )} );
997             };
998 0 0       0 if( $@ )
999             {
1000 0         0 return( $self->error( "An error occurred while trying to decrypt cookies file \"$file\": $@" ) );
1001             }
1002             }
1003            
1004 1         84 my $j = JSON->new->relaxed->utf8;
1005 1         5 my $hash;
1006             # try-catch
1007 1         2 local $@;
1008             $hash = eval
1009 1         2 {
1010 1         122 $j->decode( $json );
1011             };
1012 1 50       11 if( $@ )
1013             {
1014 0         0 return( $self->error( "Unable to decode ", CORE::length( $json ), " bytes of json data to perl: $@" ) );
1015             }
1016 1 50       19 if( ref( $hash ) ne 'HASH' )
1017             {
1018 0         0 return( $self->error( "Data retrieved from json cookie file \"$file\" does not contain an hash as expected, but instead I got '$hash'." ) );
1019             }
1020 1         4 my $last_update = CORE::delete( $hash->{last_update} );
1021 1         2 my $repo = CORE::delete( $hash->{cookies} );
1022 1 50       6 return( $self->error( "I was expecting the JSON cookies properties to be an array, but instead I got '$repo'" ) ) if( ref( $repo ) ne 'ARRAY' );
1023 1         7 foreach my $def ( @$repo )
1024             {
1025 3 50 33     62 if( !CORE::exists( $def->{name} ) ||
    50 33        
1026             !CORE::exists( $def->{value} ) )
1027             {
1028 0         0 next;
1029             }
1030             elsif( !defined( $def->{name} ) || !CORE::length( $def->{name} ) )
1031             {
1032             next:
1033 0         0 }
1034             my $c = $self->make( $def ) || do
1035 3   33     23 {
1036             next;
1037             };
1038 3 50       31 $self->add( $c ) || return( $self->pass_error );
1039             }
1040 1         53 return( $self );
1041             }
1042              
1043             sub load_as_lwp
1044             {
1045 1     1 1 11520 my $self = shift( @_ );
1046 1   50     9 my $file = shift( @_ ) || return( $self->error( "No filename was provided." ) );
1047 1         14 my $opts = $self->_get_args_as_hash( @_ );
1048 1   50     19 $opts->{decrypt} //= 0;
1049 1   50     22 $opts->{algo} //= '';
1050             # Initialisation Vector for encryption
1051             # Re-use it if it was previously set
1052 1   50     24 $opts->{iv} //= $self->_initialisation_vector->scalar || '';
      33        
1053 1         1327 my $f = $self->new_file( $file );
1054 1   50     167984 my $host = $opts->{host} || $self->host || '';
1055 1 50       1166 $f->open( '<', { binmode => ( $opts->{decrypt} ? 'raw' : 'utf-8' ) }) || return( $self->pass_error( $f->error ) );
    50          
1056             my $code = sub
1057             {
1058 4 100   4   8501 if( /^Set-Cookie3:[[:blank:]\h]*(.*?)$/ )
1059             {
1060 3         29 my $c = $self->add( $1 );
1061             }
1062             else
1063             {
1064             # Line does not match regep.
1065             }
1066 1         6158 };
1067            
1068 1 50       14 if( $opts->{decrypt} )
1069             {
1070 0         0 my $raw = $f->load;
1071 0         0 $f->close;
1072 0         0 my $key = $opts->{key};
1073 0         0 my $algo = $opts->{algo};
1074 0 0 0     0 return( $self->error( "Cookies file encryption was enabled, but no key was set to decrypt it." ) ) if( !defined( $key ) || !CORE::length( "$key" ) );
1075 0 0 0     0 return( $self->error( "Cookies file encryption was enabled, but no algorithm was set to decrypt it." ) ) if( !defined( $algo ) || !CORE::length( "$algo" ) );
1076 0 0       0 $self->_load_class( 'Crypt::Misc', { version => CRYPTX_VERSION } ) || return( $self->pass_error );
1077 0   0     0 my $p = $self->_encrypt_objects( @$opts{qw( key algo iv )} ) || return( $self->pass_error );
1078             # try-catch
1079 0         0 local $@;
1080             my $data = eval
1081 0         0 {
1082 0         0 my $crypt = $p->{crypt};
1083 0         0 my $bin = Crypt::Misc::decode_b64( "$raw" );
1084 0         0 $crypt->decrypt( "$bin", @$p{qw( key iv )} );
1085             };
1086 0 0       0 if( $@ )
1087             {
1088 0         0 return( $self->error( "An error occurred while trying to decrypt cookies file \"$file\": $@" ) );
1089             }
1090 0         0 my $scalar = $self->new_scalar( \$data );
1091 0   0     0 my $io = $scalar->open || return( $self->pass_error( $! ) );
1092 0 0       0 $io->line( $code, chomp => 1, auto_next => 1 ) || return( $self->pass_error( $f->error ) );
1093 0         0 $io->close;
1094             }
1095             else
1096             {
1097 1 50       36 $f->line( $code, chomp => 1, auto_next => 1 ) || return( $self->pass_error( $f->error ) );
1098 1         1799 $f->close;
1099             }
1100 1         6354 return( $self );
1101             }
1102              
1103             sub load_as_mozilla
1104             {
1105 0     0 1 0 my $self = shift( @_ );
1106 0   0     0 my $file = shift( @_ ) || return( $self->error( "No filename was provided." ) );
1107 0   0     0 my $f = $self->new_file( $file ) || return( $self->pass_error );
1108 0         0 my $opts = $self->_get_args_as_hash( @_ );
1109 0   0     0 $opts->{use_dbi} //= 0;
1110 0   0     0 $opts->{sqlite} //= '';
1111             # First, we copy the file, because Firefox locks it
1112 0         0 my $tmpfile = $self->new_tempfile( extension => 'sqlite' );
1113 0   0     0 my $sqldb = $f->copy( $tmpfile ) || return( $self->pass_error );
1114             # Now, try to load DBI and DBD::SQLite
1115 0         0 my $dbi_error;
1116             my $sqlite_bin;
1117 0         0 my $cookies = [];
1118 0 0 0     0 my $requires_dbi = ( CORE::exists( $opts->{use_dbi} ) && defined( $opts->{use_dbi} ) && $opts->{use_dbi} ) ? 1 : 0;
1119 0         0 require version;
1120 0         0 my $sql = <<EOT;
1121             SELECT
1122             name
1123             ,value
1124             ,host AS "domain"
1125             ,path
1126             ,expiry AS "expires"
1127             ,isSecure AS "secure"
1128             ,sameSite AS "same_site"
1129             ,isHttpOnly AS "http_only"
1130             ,CAST( ( lastAccessed / 1000000 ) AS "INTEGER" ) AS "accessed"
1131             ,CAST( ( creationTime / 1000000 ) AS "INTEGER" ) AS "created"
1132             FROM moz_cookies
1133             EOT
1134              
1135             # If the user explicitly required the use of DBI/DBD::SQLite; or
1136             # the user has not explicitly required the use of DBI/DBD::SQLite nor of sqlite3 binary
1137 0 0 0     0 if( $requires_dbi ||
      0        
1138             ( !$opts->{use_dbi} && !$opts->{sqlite} ) )
1139             {
1140 0         0 local $@;
1141             eval
1142 0         0 {
1143 0         0 require DBI;
1144 0         0 require DBD::SQLite;
1145             };
1146 0 0       0 $dbi_error = $@ if( $@ );
1147             # User explicitly required the use of DBI/DBD::SQLite, but it failed, so we return an error
1148 0 0 0     0 if( defined( $dbi_error ) && exists( $opts->{use_dbi} ) && defined( $opts->{use_dbi} ) && $opts->{use_dbi} )
    0 0        
      0        
1149             {
1150 0         0 return( $self->error( "Unable to load either DBI or DBD::SQLite: $@" ) );
1151             }
1152             elsif( !defined( $dbi_error ) )
1153             {
1154             # As of Firefox 106.0.5 (2022-11-06), the cookie table structure is:
1155             # CREATE TABLE moz_cookies(
1156             # id INTEGER PRIMARY KEY,
1157             # originAttributes TEXT NOT NULL DEFAULT '',
1158             # name TEXT,
1159             # value TEXT,
1160             # host TEXT,
1161             # path TEXT,
1162             # expiry INTEGER,
1163             # lastAccessed INTEGER,
1164             # creationTime INTEGER,
1165             # isSecure INTEGER,
1166             # isHttpOnly INTEGER,
1167             # inBrowserElement INTEGER DEFAULT 0,
1168             # sameSite INTEGER DEFAULT 0,
1169             # rawSameSite INTEGER DEFAULT 0,
1170             # schemeMap INTEGER DEFAULT 0,
1171             # CONSTRAINT moz_uniqueid UNIQUE(name, host, path, originAttributes)
1172             # );
1173             # 'expiry' is a unix timestamp
1174             # 'lastAccessed' and 'creationTime' are in microseconds
1175             # try-catch
1176 0         0 local $@;
1177             eval
1178 0         0 {
1179 0   0     0 my $dbh = DBI->connect( "dbi:SQLite:dbname=${sqldb}", '', '', { RaiseError => 1 } ) ||
1180             die( "Unable to connect to SQLite database file ${sqldb}: ", $DBI::errstr );
1181 0   0     0 my $tbl_check = $dbh->table_info( undef, undef, 'moz_cookies', 'TABLE' ) ||
1182             die( "Error checking for existence of table 'moz_cookies' in SQLite database file ${sqldb}: ", $dbh->errstr );
1183 0 0       0 $tbl_check->execute || die( "Error executing query to check existence of table 'moz_cookies': ", $tbl_check->errstr );
1184 0         0 my $found = $tbl_check->fetchrow;
1185 0         0 $tbl_check->finish;
1186 0 0       0 if( !$found )
1187             {
1188 0         0 die( "No table 'moz_cookies' found in SQLite database ${sqldb}" );
1189             }
1190 0   0     0 my $sth = $dbh->prepare( $sql ) ||
1191             die( "Error preparing the sql query to get all mozilla cookies from database ${sqldb}: ", $dbh->errstr, "\nSQL query was: ${sql}" );
1192 0 0       0 $sth->execute() ||
1193             die( "Error executing sql query to get all mozilla cookies from database ${sqldb}: ", $sth->errstr, "\nSQL query was: ${sql}" );
1194 0         0 $cookies = $sth->fetchall_arrayref;
1195 0         0 $sth->finish;
1196 0         0 $dbh->disconnect;
1197 0         0 $sqldb->remove;
1198             };
1199 0 0       0 if( $@ )
1200             {
1201 0 0       0 if( $requires_dbi )
1202             {
1203 0         0 return( $self->error( "Error trying to get mozilla cookies from SQLite database ${sqldb} using DBI: $@" ) );
1204             }
1205             else
1206             {
1207 0 0       0 warn( "Non fatal error occurred while trying to get mozilla cookies from SQLite database ${sqldb} using DBI: $@\n" ) if( $self->_warnings_is_enabled );
1208             }
1209             }
1210             }
1211             }
1212            
1213             # If there is no cookies found yet; and
1214             # the user did not require exclusively the use of DBI, but required the use of sqlite3 binary
1215             # the user did not require the use of DBI nor the use of sqlite3 binary
1216 0 0 0     0 if( !scalar( @$cookies ) && !$requires_dbi )
1217             {
1218             # If the user required specific sqlite3 binary
1219 0 0 0     0 if( exists( $opts->{sqlite} ) && defined( $opts->{sqlite} ) && CORE::length( $opts->{sqlite} ) )
      0        
1220             {
1221 0 0       0 if( !-e( $opts->{sqlite} ) )
    0          
1222             {
1223 0         0 return( $self->error( "sqlite3 binary path provided \"$opts->{sqlite}\" does not exist." ) );
1224             }
1225             elsif( !-x( $opts->{sqlite} ) )
1226             {
1227 0         0 return( $self->error( "sqlite3 binary path provided \"$opts->{sqlite}\" is not executable by user id $>" ) );
1228             }
1229 0         0 $sqlite_bin = $opts->{sqlite};
1230             }
1231             else
1232             {
1233 0         0 require File::Which;
1234 0         0 my $bin = File::Which::which( 'sqlite3' );
1235 0 0       0 if( !defined( $bin ) )
1236             {
1237 0         0 return( $self->error( "DBI and/or DBD::SQLite modules are not installed and I could not find thr sqlite3 binary anywhere." ) );
1238             }
1239 0         0 $sqlite_bin = $bin;
1240             }
1241            
1242 0         0 $sql =~ s/\n/ /gs;
1243 0 0       0 open( my $fh, '-|', $sqlite_bin, '-noheader', '-separator', '|', "${sqldb}", $sql ) ||
1244             return( $self->error( "Failed to execute sqlite3 binary with sql query to get all mozilla cookies from database ${sqldb}: $!" ) );
1245             # $cookies = [map{ [split( /\|/, $_ )] } <$fh>];
1246 0         0 while( defined( $_ = <$fh> ) )
1247             {
1248 0         0 chomp;
1249 0         0 push( @$cookies, [split( /\|/, $_ )] );
1250             }
1251 0         0 close( $fh );
1252             }
1253              
1254 0         0 foreach my $ref ( @$cookies )
1255             {
1256 0         0 my( $name, $value, $domain, $path, $expires, $secure, $same_site, $http_only, $accessed, $created ) = @$ref;
1257 0 0       0 $self->add({
1258             name => $name,
1259             value => $value,
1260             domain => $domain,
1261             path => $path,
1262             expires => $expires,
1263             secure => $secure,
1264             http_only => $http_only,
1265             same_site => $same_site,
1266             accessed_on => $accessed,
1267             created_on => $created,
1268             }) || return( $self->pass_error );
1269             }
1270 0         0 return( $self );
1271             }
1272              
1273             sub load_as_netscape
1274             {
1275 0     0 1 0 my $self = shift( @_ );
1276 0   0     0 my $file = shift( @_ ) || return( $self->error( "No filename was provided." ) );
1277 0   0     0 my $f = $self->new_file( $file ) || return( $self->pass_error );
1278 0         0 my $opts = $self->_get_args_as_hash( @_ );
1279 0   0     0 my $host = $opts->{host} || $self->host || '';
1280 0 0       0 $f->open || return( $self->pass_error( $f->error ) );
1281             $f->line(sub
1282             {
1283 0     0   0 my( $domain, $sub_too, $path, $secure, $expires, $name, $value ) = split( /\t/, $_ );
1284 0 0       0 $secure = ( lc( $secure ) eq 'true' ? 1 : 0 );
1285             # rfc6265 makes obsolete domains prepended with a dot.
1286 0 0       0 $domain = substr( $domain, 1 ) if( substr( $domain, 1, 1 ) eq '.' );
1287 0         0 $self->add({
1288             name => $name,
1289             value => $value,
1290             domain => $domain,
1291             path => $path,
1292             expires => $expires,
1293             secure => $secure,
1294             });
1295 0 0       0 }, chomp => 1, auto_next => 1 ) || return( $self->pass_error( $f->error ) );
1296 0         0 return( $self );
1297             }
1298              
1299             sub make
1300             {
1301 15     15 1 15845 my $self = shift( @_ );
1302 15         100 my $opts = $self->_get_args_as_hash( @_ );
1303 2     2   23 no overloading;
  2         4  
  2         12763  
1304 15 50       16555 return( $self->error( "Cookie name was not provided." ) ) if( !$opts->{name} );
1305 15         81 $opts->{debug} = $self->debug;
1306 15         417 my $c = Cookie->new( debug => $self->debug );
1307 15 50       127 return( $self->pass_error( Cookie->error ) ) if( !defined( $c ) );
1308 15 50       104 $c->apply( $opts ) || return( $self->pass_error( $c->error ) );
1309 15         168 return( $c );
1310             }
1311              
1312             sub merge
1313             {
1314 0     0 1 0 my $self = shift( @_ );
1315 0   0     0 my $jar = shift( @_ ) || return( $self->error( "No Cookie::Jar object was provided to merge." ) );
1316 0         0 my $opts = $self->_get_args_as_hash( @_ );
1317 0 0 0     0 return( $self->error( "Cookie::Jar object provided (", overload::StrVal( $jar // 'undef' ), ") is not a Cookie::Jar object." ) ) if( !$self->_is_a( $jar, 'Cookie::Jar' ) );
1318             # We require the do method on purpose, because the scan method is from the old HTTP::Cookies api which does not send an object, but a list of cookie property value
1319 0 0       0 return( $self->error( "Cookie::Jar object provided does not have a method \"do\"." ) ) if( !$jar->can( 'do' ) );
1320 0   0     0 $opts->{overwrite} //= 0;
1321 0   0     0 $opts->{host} //= $self->host || '';
      0        
1322 0   0     0 $opts->{die} //= 0;
1323 0         0 my $n = 0;
1324 0         0 my $error;
1325             $jar->do(sub
1326             {
1327             # Skip the rest if we already found an error
1328 0 0   0   0 return if( defined( $error ) );
1329 0         0 my $c = shift( @_ );
1330 0 0       0 if( $self->_is_object( $c ) )
1331             {
1332 0 0 0     0 if( $self->_is_a( $c, 'Cookie' ) )
    0 0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
1333             {
1334 0 0       0 if( $opts->{overwrite} )
1335             {
1336 0         0 $self->replace( $c );
1337             }
1338             else
1339             {
1340 0         0 $self->add( $c );
1341             }
1342 0         0 $n++;
1343             }
1344             elsif( $c->can( 'name' ) &&
1345             $c->can( 'value' ) &&
1346             $c->can( 'domain' ) &&
1347             $c->can( 'path' ) &&
1348             $c->can( 'expires' ) &&
1349             $c->can( 'max_age' ) &&
1350             $c->can( 'port' ) &&
1351             $c->can( 'secure' ) &&
1352             $c->can( 'same_site' ) &&
1353             $c->can( 'http_only' ) )
1354             {
1355 0         0 my $new = $jar->make(
1356             name => $c->name,
1357             value => $c->value,
1358             domain => $c->domain,
1359             path => $c->path,
1360             expires => $c->expires,
1361             max_age => $c->max_age,
1362             http_only => $c->http_only,
1363             same_site => $c->same_site,
1364             secure => $c->secure,
1365             );
1366 0 0       0 if( !defined( $new ) )
1367             {
1368 0         0 $error = $jar->error;
1369 0 0       0 die( $error ) if( $opts->{die} );
1370             }
1371             else
1372             {
1373 0 0       0 if( $opts->{overwrite} )
1374             {
1375 0         0 $self->replace( $new );
1376             }
1377             else
1378             {
1379 0         0 $self->add( $new );
1380             }
1381 0         0 $n++;
1382             }
1383             }
1384             else
1385             {
1386 0   0     0 $error = "Cookie object received (" . overload::StrVal( $c // 'undef' ) . ") is not a Cookie object and does not support the methods name, value, domain, path, port, expires, max_age, secure, same_site and http_only";
1387 0 0       0 die( $error ) if( $opts->{die} );
1388             }
1389             }
1390 0         0 });
1391 0 0       0 return( $self->error( $error ) ) if( defined( $error ) );
1392 0         0 return( $self );
1393             }
1394              
1395             # Swell:
1396             # "if the Cookie header field contains two cookies with the same name (e.g., that were set with different Path or Domain attributes), servers SHOULD NOT rely upon the order in which these cookies appear in the header field."
1397             # <https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.2>
1398             sub parse
1399             {
1400 23     23 1 55931 my $self = shift( @_ );
1401 23         41 my $raw = shift( @_ );
1402 23         125 my $ref = $self->new_array;
1403 23 100 100     15716 return( $ref ) unless( defined( $raw ) && CORE::length( $raw ) );
1404 21         314 my @pairs = grep( /=/, split( /; ?/, $raw ) );
1405 21         62 foreach my $pair ( @pairs )
1406             {
1407             # Remove leading and trailing whitespaces
1408 60         887 $pair =~ s/^[[:blank:]\h]+|[[:blank:]\h]+$//g;
1409 60         177 my( $k, $v ) = split( '=', $pair, 2 );
1410 60         154 $k = URI::Escape::uri_unescape( $k );
1411 60 50       439 $v = '' unless( defined( $v ) );
1412 60         110 $v =~ s/\A"(.*)"\z/$1/;
1413 60         94 $v = URI::Escape::uri_unescape( $v );
1414 60         691 $ref->push( { name => $k, value => $v } );
1415             }
1416 21         250 return( $ref );
1417             }
1418              
1419             sub purge
1420             {
1421 0     0 1 0 my $self = shift( @_ );
1422 0         0 my $ref = $self->_cookies;
1423 0         0 my $removed = $self->new_array;
1424 0         0 for( my $i = 0; $i < scalar( @$ref ); $i++ )
1425             {
1426 0         0 my $c = $ref->[$i];
1427 0 0       0 if( $c->is_expired )
1428             {
1429 0 0       0 $self->delete( $c ) || return( $self->pass_error );
1430 0         0 $removed->push( $c );
1431             }
1432             }
1433 0         0 return( $removed );
1434             }
1435              
1436 8     8 1 147974 sub repo { return( shift->_set_get_array_as_object( '_cookies', @_ ) ); }
1437              
1438             sub replace
1439             {
1440 0     0 1 0 my $self = shift( @_ );
1441 0         0 my( $c, $old ) = @_;
1442 0         0 my $idx = $self->_index;
1443 0         0 my $ref = $self->_cookies;
1444 0 0       0 return( $self->error( "No cookie object was provided." ) ) if( !defined( $c ) );
1445 0 0       0 return( $self->error( "Cookie object provided is not a Cookie object." ) ) if( !$self->_is_a( $c, 'Cookie' ) );
1446 0         0 my $replaced = $self->new_array;
1447 0 0       0 if( defined( $old ) )
1448             {
1449 0 0       0 return( $self->error( "Old cookie object to be replaced is not a Cookie object." ) ) if( !$self->_is_a( $old, 'Cookie' ) );
1450 0 0 0     0 if( $c->name ne $old->name ||
      0        
1451             $c->domain ne $old->domain ||
1452             $c->path ne $old->path )
1453             {
1454 0         0 return( $self->error( "New cookie name '", $c->name, "' with host '", $c->domain, "' and path '", $c->path, "' does not match old cookie name '", $old->name, "' with host '", $old->host, "' and path '", $old->path, "'" ) );
1455             }
1456 0   0     0 my $key = $self->key( $old ) || return( $self->pass_error );
1457 0         0 my $addr = Scalar::Util::refaddr( $old );
1458 0 0       0 if( CORE::exists( $idx->{ $key } ) )
1459             {
1460 0         0 for( my $i = 0; $i < scalar( @{$idx->{ $key }} ); $i++ )
  0         0  
1461             {
1462 0 0       0 if( Scalar::Util::refaddr( $idx->{ $key }->[$i] ) eq $addr )
1463             {
1464 0         0 $idx->{ $key }->[$i] = $c;
1465 0         0 last;
1466             }
1467             }
1468             }
1469 0         0 for( my $i = 0; $i < scalar( @$ref ); $i++ )
1470             {
1471 0 0       0 if( Scalar::Util::refaddr( $ref->[$i] ) eq $addr )
1472             {
1473 0         0 $replaced->push( $ref->[$i] );
1474 0         0 $ref->[$i] = $c;
1475 0         0 last;
1476             }
1477             }
1478             }
1479             else
1480             {
1481 0   0     0 my $key = $self->key( $c ) || return( $self->pass_error );
1482 0 0       0 $replaced->push( CORE::exists( $idx->{ $key } ) ? @{$idx->{ $key }} : () );
  0         0  
1483 0         0 foreach my $old ( @$replaced )
1484             {
1485 0         0 my $addr = Scalar::Util::refaddr( $old );
1486 0         0 for( my $j = 0; $j < scalar( @$ref ); $j++ )
1487             {
1488 0 0       0 if( Scalar::Util::refaddr( $ref->[$j] ) eq $addr )
1489             {
1490 0         0 CORE::splice( @$ref, $j, 1 );
1491 0         0 $j--;
1492 0         0 last;
1493             }
1494             }
1495             }
1496 0         0 $idx->{ $key } = [ $c ];
1497             }
1498 0         0 return( $replaced );
1499             }
1500              
1501 5     5 1 37 sub request { return( shift->_set_get_object_without_init( 'request', 'Apache2::RequestRec', @_ ) ); }
1502              
1503             sub save
1504             {
1505 1     1 1 484716 my $self = shift( @_ );
1506 1   50     10 my $file = shift( @_ ) || return( $self->error( "No filename was provided." ) );
1507 1         19 my $opts = $self->_get_args_as_hash( @_ );
1508 1   50     24 $opts->{encrypt} //= 0;
1509 1   50     13 $opts->{algo} //= '';
1510             # Initialisation Vector for encryption
1511             # Re-use it if it was previously set
1512 1   50     18 $opts->{iv} //= $self->_initialisation_vector->scalar || '';
      33        
1513 1   50     1308 $opts->{format} //= '';
1514 1 50       6 return( $self->save_as_lwp( $opts ) ) if( $opts->{format} eq 'lwp' );
1515 1         6 my $all = [];
1516 1         12 my $ref = $self->_cookies;
1517 1         606 foreach my $c ( @$ref )
1518             {
1519 3         46 push( @$all, $c->as_hash );
1520             }
1521 1         2 my $tz;
1522             # DateTime::TimeZone::Local will die ungracefully if the local timezeon is not set with the error:
1523             # "Cannot determine local time zone"
1524             # try-catch
1525 1         2 local $@;
1526             eval
1527 1         9 {
1528 1         9 $tz = DateTime::TimeZone->new( name => 'local' );
1529             };
1530 1 50       648 if( $@ )
1531             {
1532 1         9 $tz = DateTime::TimeZone->new( name => 'UTC' );
1533             }
1534 1         68 my $today = DateTime->now( time_zone => $tz );
1535 1         371 my $dt_fmt = DateTime::Format::Strptime->new(
1536             pattern => '%FT%T%z',
1537             # Unnecessary
1538             # locale => 'en_GB',
1539             time_zone => $tz->name,
1540             );
1541 1         1484 $today->set_formatter( $dt_fmt );
1542 1         83 my $data = { cookies => $all, updated_on => "$today" };
1543            
1544 1   50     891 my $f = $self->new_file( $file ) || return( $self->pass_error );
1545 1         188781 my $j = JSON->new->allow_nonref->pretty->canonical->convert_blessed;
1546 1         7 my $json;
1547             # try-catch
1548             $json = eval
1549 1         10 {
1550 1         133 $j->encode( $data );
1551             };
1552 1 50       2486 if( $@ )
1553             {
1554 0         0 return( $self->error( "Unable to encode data to json: $@" ) );
1555             }
1556              
1557 1 50       44 $f->open( '>', { binmode => ( $opts->{encrypt} ? 'raw' : 'utf8' ) }) ||
    50          
1558             return( $self->pass_error( $f->error ) );
1559 1 50       59482 if( $opts->{encrypt} )
1560             {
1561 0 0       0 $self->_load_class( 'Crypt::Misc', { version => CRYPTX_VERSION } ) || return( $self->pass_error );
1562 0   0     0 my $p = $self->_encrypt_objects( @$opts{qw( key algo iv )} ) || return( $self->pass_error );
1563 0         0 my $crypt = $p->{crypt};
1564             # $value = Crypt::Misc::encode_b64( $crypt->encrypt( "$value", $p->{key}, $p->{iv} ) );
1565 0         0 my $encrypted = $crypt->encrypt( "$json", @$p{qw( key iv )} );
1566 0         0 my $b64 = Crypt::Misc::encode_b64( $encrypted );
1567 0         0 $f->unload( $b64 );
1568             }
1569             else
1570             {
1571 1         24 $f->unload( $json );
1572             }
1573 1         3558 $f->close;
1574 1         12628 return( $self );
1575             }
1576              
1577             sub save_as_lwp
1578             {
1579 1     1 1 406301 my $self = shift( @_ );
1580 1   50     19 my $file = shift( @_ ) || return( $self->error( "No filename was provided." ) );
1581 1         29 my $opts = $self->_get_args_as_hash( @_ );
1582 1   50     47 $opts->{encrypt} //= 0;
1583 1   50     22 $opts->{algo} //= '';
1584             # Initialisation Vector for encryption
1585             # Re-use it if it was previously set
1586 1   50     29 $opts->{iv} //= $self->_initialisation_vector->scalar || '';
      33        
1587 1   50     731 $opts->{skip_discard} //= 0;
1588 1   50     11 $opts->{skip_expired} //= 0;
1589 1 50       5 return( $self->error( "No file to write cookies was specified." ) ) if( !$file );
1590 1   50     16 my $f = $self->new_file( $file ) || return( $self->pass_error );
1591            
1592 1         128470 my $raw = '';
1593 1         21 my $p = {};
1594 1 50       8 if( $opts->{encrypt} )
1595             {
1596 0 0       0 $self->_load_class( 'Crypt::Misc', { version => CRYPTX_VERSION } ) || return( $self->pass_error );
1597 0   0     0 $p = $self->_encrypt_objects( @$opts{qw( key algo iv )} ) || return( $self->pass_error );
1598             }
1599            
1600 1   50     30 my $io = $f->open( '>', { binmode => ( $opts->{encrypt} ? 'raw' : 'utf-8' ) }) ||
1601             return( $self->error( "Unable to write cookies to file \"$file\": ", $f->error ) );
1602 1 50       43428 if( $opts->{encrypt} )
1603             {
1604 0         0 $raw = "#LWP-Cookies-1.0\n";
1605             }
1606             else
1607             {
1608 1 50       19 $io->print( "#LWP-Cookies-1.0\n" ) || return( $self->error( "Unable to write to cookie file \"$file\": $!" ) );
1609             }
1610 1         408 my $now = DateTime->now;
1611             $self->scan(sub
1612             {
1613 3     3   7 my $c = shift( @_ );
1614 3 0 33     33 return(1) if( $c->discard && $opts->{skip_discard} );
1615 3 0 33     2163 return(1) if( $c->expires && $c->expires < $now && $opts->{skip_expired} );
      33        
1616 3         2504 my $vals = $c->as_hash;
1617 3 50       29 $vals->{path_spec} = 1 if( CORE::length( $vals->{path} ) );
1618             # In HTTP::Cookies logic, version 1 is rfc2109, version 2 is rfc6265
1619 3         45 $vals->{version} = 2;
1620 3         48 my $hv = Module::Generic::HeaderValue->new( [CORE::delete( @$vals{qw( name value )} )] );
1621 3         2685 $hv->param( path => sprintf( '"%s"', $vals->{path} ) );
1622 3         5588 $hv->param( domain => $vals->{domain} );
1623 3 50 33     1839 $hv->param( port => $vals->{port} ) if( defined( $vals->{port} ) && CORE::length( $vals->{port} ) );
1624 3 50 33     39 $hv->param( path_spec => undef() ) if( defined( $vals->{path_spec} ) && $vals->{path_spec} );
1625 3 50 33     2142 $hv->param( secure => undef() ) if( defined( $vals->{secure} ) && $vals->{secure} );
1626 3 50 33     1880 $hv->param( expires => sprintf( '"%s"', "$vals->{expires}" ) ) if( defined( $vals->{secure} ) && $vals->{expires} );
1627 3 0 33     12 $hv->param( discard => undef() ) if( defined( $vals->{discard} ) && $vals->{discard} );
1628 3 50 33     21 if( defined( $vals->{comment} ) && CORE::length( $vals->{comment} ) )
1629             {
1630 0         0 $vals->{comment} =~ s/(?<!\\)\"/\\\"/g;
1631 0         0 $hv->param( comment => sprintf( '"%s"', $vals->{comment} ) );
1632             }
1633 3 50 33     18 $hv->param( commentURL => $vals->{commentURL} ) if( defined( $vals->{commentURL} ) && CORE::length( $vals->{commentURL} ) );
1634 3         20 $hv->param( version => $vals->{version} );
1635 3 50       1850 if( $opts->{encrypt} )
1636             {
1637 0         0 $raw .= 'Set-Cookie3: ' . $hv->as_string . "\n";
1638             }
1639             else
1640             {
1641 3 50       34 $io->print( 'Set-Cookie3: ', $hv->as_string, "\n" ) || return( $self->error( "Unable to write to cookie file \"$file\": $!" ) );
1642             }
1643 1         857 });
1644 1 50       538 if( $opts->{encrypt} )
1645             {
1646 0         0 my $crypt = $p->{crypt};
1647 0         0 my $encrypted = $crypt->encrypt( "$raw", @$p{qw( key iv )} );
1648 0         0 my $b64 = Crypt::Misc::encode_b64( $encrypted );
1649 0         0 $io->print( $b64 );
1650             }
1651 1         9 $io->close;
1652 1         2722 return( $self );
1653             }
1654              
1655             sub save_as_mozilla
1656             {
1657 0     0 1 0 my $self = shift( @_ );
1658 0   0     0 my $file = shift( @_ ) || return( $self->error( "No database file to write cookies was specified." ) );
1659 0         0 my $opts = $self->_get_args_as_hash( @_ );
1660 0   0     0 $opts->{log_sql} //= '';
1661 0   0     0 $opts->{overwrite} //= 0;
1662 0   0     0 $opts->{rollback} //= 0;
1663 0   0     0 $opts->{skip_discard} //= 0;
1664 0   0     0 $opts->{skip_expired} //= 0;
1665 0   0     0 $opts->{sqlite} //= '';
1666 0   0     0 $opts->{use_dbi} //= 0;
1667 0   0     0 my $sqldb = $self->new_file( $file ) || return( $self->pass_error );
1668 0         0 my $dbi_error;
1669             my $sqlite_bin;
1670 0 0 0     0 my $requires_dbi = ( CORE::exists( $opts->{use_dbi} ) && defined( $opts->{use_dbi} ) && $opts->{use_dbi} ) ? 1 : 0;
1671 0         0 require version;
1672 0         0 my $db_file_exists = $sqldb->exists;
1673 0         0 my $table_moz_cookies_exists = 0;
1674             # As of Firefox 106.0.5 (2022-11-06), the cookie table structure is:
1675             # 'expiry' is a unix timestamp
1676             # 'lastAccessed' and 'creationTime' are in microseconds
1677 0         0 my $create_table_sql = <<EOT;
1678             CREATE TABLE moz_cookies(
1679             id INTEGER PRIMARY KEY,
1680             originAttributes TEXT NOT NULL DEFAULT '',
1681             name TEXT,
1682             value TEXT,
1683             host TEXT,
1684             path TEXT,
1685             expiry INTEGER,
1686             lastAccessed INTEGER,
1687             creationTime INTEGER,
1688             isSecure INTEGER,
1689             isHttpOnly INTEGER,
1690             inBrowserElement INTEGER DEFAULT 0,
1691             sameSite INTEGER DEFAULT 0,
1692             rawSameSite INTEGER DEFAULT 0,
1693             schemeMap INTEGER DEFAULT 0,
1694             CONSTRAINT moz_uniqueid UNIQUE(name, host, path, originAttributes)
1695             )
1696             EOT
1697 0         0 my $core_fields =
1698             {
1699             name => { type => 'TEXT', constant => 'SQL_VARCHAR' },
1700             value => { type => 'TEXT', constant => 'SQL_VARCHAR' },
1701             host => { type => 'TEXT', constant => 'SQL_VARCHAR' },
1702             path => { type => 'TEXT', constant => 'SQL_VARCHAR' },
1703             expiry => { type => 'INTEGER', constant => 'SQL_INTEGER' },
1704             isSecure => { type => 'INTEGER', constant => 'SQL_INTEGER' },
1705             sameSite => { type => 'INTEGER', constant => 'SQL_INTEGER' },
1706             isHttpOnly => { type => 'INTEGER', constant => 'SQL_INTEGER' },
1707             lastAccessed => { type => 'INTEGER', constant => 'SQL_INTEGER' },
1708             creationTime => { type => 'INTEGER', constant => 'SQL_INTEGER' },
1709             };
1710             # To hold the cookies data to be saved
1711 0         0 my $cookies = [];
1712 0         0 my $now = DateTime->now;
1713 0         0 my $can_do_upsert = 0;
1714             my $get_cookies = sub
1715             {
1716 0     0   0 my $c = shift( @_ );
1717 0 0 0     0 return(1) if( $c->discard && $opts->{skip_discard} );
1718 0 0 0     0 return(1) if( $c->expires && $c->expires < $now && $opts->{skip_expired} );
      0        
1719             # Offset 0 is the value, offset 1 is the data type for DBI and offset 2 is the field name used for the sqlite3 binary method
1720             my $row =
1721             [
1722             [$c->name->scalar, $core_fields->{name}->{constant}, 'name'],
1723             [$c->value->scalar, $core_fields->{value}->{constant}, 'value'],
1724             [$c->domain->scalar, $core_fields->{host}->{constant}, 'host'],
1725             [$c->path->scalar, $core_fields->{path}->{constant}, 'path'],
1726             [( $c->expires ? $c->expires->epoch : undef ), $core_fields->{expiry}->{constant}, 'expiry'],
1727             [( $c->secure ? 1 : 0 ), $core_fields->{isSecure}->{constant}, 'isSecure'],
1728             [( $c->same_site->lc eq 'strict' ? 1 : 0 ), $core_fields->{sameSite}->{constant}, 'sameSite'],
1729             [( $c->http_only ? 1 : 0 ), $core_fields->{isHttpOnly}->{constant}, 'isHttpOnly'],
1730             [( $c->accessed_on ? ( $c->accessed_on->epoch * 1000000 ) : undef ), $core_fields->{lastAccessed}->{constant}, 'lastAccessed'],
1731 0 0       0 [( $c->created_on ? ( $c->created_on->epoch * 1000000 ) : undef ), $core_fields->{creationTime}->{constant}, 'creationTime'],
    0          
    0          
    0          
    0          
    0          
1732             ];
1733 0 0       0 if( $can_do_upsert )
1734             {
1735 0         0 push( @$row, [$c->value->scalar, $core_fields->{value}->{constant}, 'value'] );
1736 0 0       0 push( @$row, [( $c->expires ? $c->expires->epoch : undef ), $core_fields->{expiry}->{constant}, 'expiry'] );
1737 0 0       0 push( @$row, [( $c->secure ? 1 : 0 ), $core_fields->{isSecure}->{constant}, 'isSecure'] );
1738 0 0       0 push( @$row, [( $c->same_site->lc eq 'strict' ? 1 : 0 ), $core_fields->{sameSite}->{constant}, 'sameSite'] );
1739 0 0       0 push( @$row, [( $c->http_only ? 1 : 0 ), $core_fields->{isHttpOnly}->{constant}, 'isHttpOnly'] );
1740 0 0       0 push( @$row, [( $c->accessed_on ? ( $c->accessed_on->epoch * 1000000 ) : undef ), $core_fields->{lastAccessed}->{constant}, 'lastAccessed'] );
1741 0 0       0 push( @$row, [( $c->created_on ? ( $c->created_on->epoch * 1000000 ) : undef ), $core_fields->{creationTime}->{constant}, 'creationTime'] );
1742             }
1743 0         0 push( @$cookies, $row );
1744 0         0 };
1745              
1746             # From SQLite version 3.24.0
1747             # update if there is a constraint violation on 'moz_uniqueid', i.e. name, host, path, originAttributes
1748 0         0 my $upsert_sql = <<EOT;
1749             INSERT INTO moz_cookies (name, value, host, path, expiry, isSecure, sameSite, isHttpOnly, lastAccessed, creationTime)
1750             VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1751             ON CONFLICT(name, host, path, originAttributes)
1752             DO UPDATE SET value = ?, expiry = ?, isSecure = ?, sameSite = ?, isHttpOnly = ?, lastAccessed = ?, creationTime = ?
1753             EOT
1754 0         0 my $insert_ignore_sql = <<EOT;
1755             INSERT OR IGNORE INTO moz_cookies (name, value, host, path, expiry, isSecure, sameSite, isHttpOnly, lastAccessed, creationTime)
1756             VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1757             EOT
1758 0         0 my $insert_replace_sql = <<EOT;
1759             INSERT OR REPLACE INTO moz_cookies (name, value, host, path, expiry, isSecure, sameSite, isHttpOnly, lastAccessed, creationTime)
1760             VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1761             EOT
1762            
1763             # Required version for upsert
1764 0         0 my $req_v = version->parse( '3.24.0' );
1765 0         0 my $log_file;
1766 0 0       0 if( $opts->{log_sql} )
1767             {
1768 0   0     0 $log_file = $self->new_file( $opts->{log_sql} ) ||
1769             return( $self->pass_error );
1770             }
1771              
1772             # If the user explicitly required the use of DBI/DBD::SQLite; or
1773             # the user has not explicitly required the use of DBI/DBD::SQLite nor of sqlite3 binary
1774 0 0 0     0 if( $requires_dbi ||
      0        
1775             ( !$opts->{use_dbi} && !$opts->{sqlite} ) )
1776             {
1777             eval
1778 0         0 {
1779 0         0 require DBI;
1780 0         0 require DBD::SQLite;
1781             };
1782 0 0       0 $dbi_error = $@ if( $@ );
1783             # User explicitly required the use of DBI/DBD::SQLite, but it failed, so we return an error
1784 0 0 0     0 if( defined( $dbi_error ) && exists( $opts->{use_dbi} ) && defined( $opts->{use_dbi} ) && $opts->{use_dbi} )
    0 0        
      0        
1785             {
1786 0         0 return( $self->error( "Unable to load either DBI or DBD::SQLite: $@" ) );
1787             }
1788             elsif( !defined( $dbi_error ) )
1789             {
1790 0         0 foreach my $f ( keys( %$core_fields ) )
1791             {
1792             my $code = DBI->can( $core_fields->{ $f }->{constant} ) ||
1793 0   0     0 die( "Invalid data type '", $core_fields->{ $f }->{constant}, "' for DBI." );
1794 0         0 $core_fields->{ $f }->{constant} = $code->();
1795             }
1796            
1797             # try-catch;
1798 0         0 local $@;
1799 0         0 my $err;
1800             eval
1801 0         0 {
1802 0   0     0 my $dbh = DBI->connect( "dbi:SQLite:dbname=${sqldb}", '', '', { RaiseError => 1, AutoCommit => 1 } ) ||
1803             die( "Unable to connect to SQLite database file ${sqldb}: ", $DBI::errstr );
1804 0 0       0 if( $opts->{log_sql} )
1805             {
1806 0 0       0 if( !$log_file->open( '>>', { binmode => 'utf-8', autoflush => 1 } ) )
1807             {
1808 0         0 $err = $log_file->error;
1809 0         0 return;
1810             }
1811             $dbh->sqlite_trace(sub
1812             {
1813 0     0   0 my $sql = shift( @_ );
1814 0         0 $log_file->print( $sql, "\n" );
1815 0         0 });
1816             }
1817 0         0 my $rv;
1818 0         0 my $version_sql = q{SELECT sqlite_version()};
1819 0   0     0 my $version_sth = $dbh->prepare( $version_sql ) ||
1820             die( "Errror preparing sql query to get the SQLite driver version: ", $dbh->errstr, "\nSQL query was ${version_sql}" );
1821 0   0     0 $rv = $version_sth->execute() ||
1822             die( "Errror executing sql query to get the SQLite driver version: ", $version_sth->errstr, "\nSQL query was ${version_sql}" );
1823 0         0 my $sqlite_version = $version_sth->fetchrow;
1824 0         0 $version_sth->finish;
1825 0         0 my $sql_v = version->parse( $sqlite_version );
1826            
1827 0 0       0 if( $db_file_exists )
1828             {
1829             # my $tbl_check = $dbh->table_info( undef, undef, 'moz_cookies', 'TABLE' ) ||
1830 0   0     0 my $tbl_check = $dbh->prepare( q{SELECT name FROM sqlite_master WHERE type IN ('table') AND name IS 'moz_cookies'} ) ||
1831             die( "Error preparing sql query to check for existence of table 'moz_cookies' in SQLite database file ${sqldb}: ", $dbh->errstr );
1832 0   0     0 $rv = $tbl_check->execute || die( "Error executing query to check existence of table 'moz_cookies': ", $tbl_check->errstr );
1833 0         0 $table_moz_cookies_exists = $tbl_check->fetchrow;
1834 0         0 $tbl_check->finish;
1835 0 0       0 if( $table_moz_cookies_exists )
1836             {
1837             # Drop the table altogether
1838 0 0       0 if( $opts->{overwrite} )
1839             {
1840 0         0 my $drop_sql = q{DROP TABLE moz_cookies};
1841 0   0     0 my $drop_sth = $dbh->prepare( $drop_sql ) ||
1842             die( "Error preparing query to drop existing table moz_cookies in SQLIte database file ${sqldb}: ", $dbh->errstr, "\nSQL query was ${$drop_sql}" );
1843 0   0     0 $rv = $drop_sth->execute() ||
1844             die( "Error executing query to drop existing table moz_cookies in SQLIte database file ${sqldb}: ", $drop_sth->errstr, "\nSQL query was ${$drop_sql}" );
1845 0         0 $drop_sth->finish;
1846 0         0 $table_moz_cookies_exists = 0;
1847             }
1848             else
1849             {
1850             # PRAGMA table_info() returns cid, name, type, notnull, dflt_value, pk
1851 0         0 my $tbl_info_sql = q{PRAGMA TABLE_INFO(moz_cookies)};
1852 0   0     0 my $tbl_info_sth = $dbh->prepare( $tbl_info_sql ) ||
1853             die( "Error while trying to prepare query to get the existing table 'moz_cookies' information: ", $dbh->errstr, "\nSQL query is: ${tbl_info_sql}" );
1854 0   0     0 $rv = $tbl_info_sth->execute ||
1855             die( "Error while trying to execute query to get the existing table 'moz_cookies' information: ", $tbl_info_sth->errstr, "\nSQL query is: ${tbl_info_sql}" );
1856 0         0 my $all = $tbl_info_sth->fetchall_arrayref( {} );
1857 0         0 $tbl_info_sth->finish;
1858             # Check existing table field for missing fields
1859 0         0 my $fields = {};
1860 0         0 foreach my $this ( @$all )
1861             {
1862 0         0 $fields->{ $this->{name} } = $this;
1863             }
1864 0         0 my $missing = [];
1865 0         0 my $bad_datatype = [];
1866 0         0 foreach my $f ( keys( %$core_fields ) )
1867             {
1868 0 0       0 if( !CORE::exists( $fields->{ $f } ) )
    0          
1869             {
1870 0         0 push( @$missing, $f );
1871             }
1872             elsif( $core_fields->{ $f }->{type} ne uc( $fields->{ $f }->{type} ) )
1873             {
1874 0         0 push( @$bad_datatype, $f );
1875             }
1876             }
1877 0 0 0     0 if( scalar( @$missing ) || scalar( @$bad_datatype ) )
1878             {
1879 0         0 $self->error( sprintf( "Found an existing SQLite database file ${sqldb} with a table 'moz_cookies', but found %d missing fields (%s) and %d fields with inappropriate data type (%s)", scalar( @$missing ), join( ', ', @$missing ), scalar( @$bad_datatype ), join( ', ', @$bad_datatype ) ) );
1880 0         0 $err = $self->error;
1881 0         0 return;
1882             }
1883             }
1884             }
1885             }
1886            
1887 0         0 my $errors = [];
1888 0         0 my $insert_sth;
1889             # Create the table if it does not exist
1890 0 0       0 if( !$table_moz_cookies_exists )
1891             {
1892 0   0     0 my $create_table_sth = $dbh->prepare( $create_table_sql ) ||
1893             die( "Error preparing query to create table moz_cookies in SQLite database file ${sqldb}: ", $dbh->errstr, "\nSQL query was: ${create_table_sql}" );
1894 0   0     0 my $rv = $create_table_sth->execute() ||
1895             die( "Error executing query to create table moz_cookies in SQLite database file ${sqldb}: ", $create_table_sth->errstr, "\nSQL query was: ${create_table_sql}" );
1896 0         0 $create_table_sth->finish;
1897 0   0     0 $insert_sth = $dbh->prepare( $insert_ignore_sql ) ||
1898             die( "Error preparing the sql query to add/ignore cookies to 'moz_cookies' in SQLite database file ${sqldb}: ", $dbh->errstr, "\nSQL query was: ${insert_ignore_sql}" );
1899             }
1900             # or update the data
1901             else
1902             {
1903 0 0       0 $can_do_upsert = ( $sql_v >= $req_v ) ? 1 : 0;
1904             # if version is greater or equal to 3.24.0 we can do upsert, otherwise we do insert replace
1905 0 0       0 if( $can_do_upsert )
1906             {
1907 0   0     0 $insert_sth = $dbh->prepare( $upsert_sql ) ||
1908             die( "Error preparing the sql query to add cookies to 'moz_cookies' in SQLite database file ${sqldb}: ", $dbh->errstr, "\nSQL query was: ${upsert_sql}" );
1909             }
1910             else
1911             {
1912 0   0     0 $insert_sth = $dbh->prepare( $insert_replace_sql ) ||
1913             die( "Error preparing the sql query to add/replace cookies to 'moz_cookies' in SQLite database file ${sqldb}: ", $dbh->errstr, "\nSQL query was: ${insert_replace_sql}" );
1914             }
1915             }
1916            
1917             # NOTE: call to scan() must be after setting $can_do_upsert
1918 0         0 $self->scan( $get_cookies );
1919            
1920 0 0       0 if( $opts->{rollback} )
1921             {
1922 0         0 $dbh->begin_work;
1923             }
1924            
1925 0         0 foreach my $c ( @$cookies )
1926             {
1927             eval
1928 0         0 {
1929 0         0 for( my $i = 0; $i < scalar( @$c ); $i++ )
1930             {
1931 0 0       0 $insert_sth->bind_param( $i + 1, $c->[$i]->[0], $c->[$i]->[1] ) ||
1932             die( "Error binding parameter No. ", ( $i + 1 ), " with value '", $c->[$i]->[0], "': ", $insert_sth->errstr );
1933             }
1934 0   0     0 $rv = $insert_sth->execute() ||
1935             die( "Failed to execute query to insert cookie '", $c->name->scalar, "' -> ", $insert_sth->errstr, "\nQuery was ${insert_ignore_sql}" );
1936             };
1937 0 0       0 if( $@ )
1938             {
1939             # offset 0 -> name, offset 2 -> domain
1940 0         0 push( @$errors, [$c->[0], $c->[2], $@] );
1941 0 0       0 if( $opts->{rollback} )
1942             {
1943 0         0 $dbh->rollback;
1944 0         0 last;
1945             }
1946             }
1947             }
1948 0         0 $insert_sth->finish;
1949 0         0 $dbh->disconnect;
1950             };
1951 0 0       0 if( $@ )
1952             {
1953 0 0       0 if( $requires_dbi )
1954             {
1955 0         0 return( $self->error( "Error trying to save mozilla cookies to SQLite database ${sqldb} using DBI: $@" ) );
1956             }
1957             else
1958             {
1959 0         0 $dbi_error = $@;
1960 0 0       0 warn( "Non fatal error occurred while trying to save mozilla cookies to SQLite database ${sqldb} using DBI: $@\n" ) if( $self->_warnings_is_enabled );
1961             }
1962             }
1963 0 0       0 return( $self->pass_error( $err ) ) if( defined( $err ) );
1964             }
1965             }
1966            
1967             # If the user did not require exclusively the use of DBI, but required the use of sqlite3 binary
1968             # the user did not require the use of DBI nor the use of sqlite3 binary
1969 0 0 0     0 if( ( defined( $dbi_error ) && !$requires_dbi ) ||
      0        
      0        
      0        
1970             ( exists( $opts->{sqlite} ) && defined( $opts->{sqlite} ) && CORE::length( $opts->{sqlite} ) ) )
1971             {
1972             # If the user required specific sqlite3 binary
1973 0 0 0     0 if( exists( $opts->{sqlite} ) && defined( $opts->{sqlite} ) && CORE::length( $opts->{sqlite} ) )
      0        
1974             {
1975 0 0       0 if( !-e( $opts->{sqlite} ) )
    0          
1976             {
1977 0         0 return( $self->error( "sqlite3 binary path provided \"$opts->{sqlite}\" does not exist." ) );
1978             }
1979             elsif( !-x( $opts->{sqlite} ) )
1980             {
1981 0         0 return( $self->error( "sqlite3 binary path provided \"$opts->{sqlite}\" is not executable by user id $>" ) );
1982             }
1983 0         0 $sqlite_bin = $opts->{sqlite};
1984             }
1985             else
1986             {
1987 0         0 require File::Which;
1988 0         0 my $bin = File::Which::which( 'sqlite3' );
1989 0 0       0 if( !defined( $bin ) )
1990             {
1991 0         0 return( $self->error( "DBI and/or DBD::SQLite modules are not installed and I could not find thr sqlite3 binary anywhere." ) );
1992             }
1993 0         0 $sqlite_bin = $bin;
1994             }
1995            
1996 0         0 my $fh;
1997             # Get SQLite version
1998             # open( $fh, '-|', $sqlite_bin, "SELECT sqlite_version()" ) ||
1999 0 0       0 open( $fh, '-|', $sqlite_bin, "--version" ) ||
2000             return( $self->error( "Failed to execute sqlite3 binary ${sqlite_bin} to get its version number: $!" ) );
2001 0         0 my $sqlite_version = <$fh>;
2002 0         0 my $sql_v;
2003 0 0       0 if( defined( $sqlite_version ) )
2004             {
2005 0         0 chomp( $sqlite_version );
2006 0         0 $sqlite_version = [split( /[[:blank:]\h]+/, $sqlite_version )]->[0];
2007 0         0 $sql_v = version->parse( $sqlite_version );
2008 0         0 close( $fh );
2009             }
2010            
2011             # Check if table moz_cookies exists
2012 0 0       0 open( $fh, '-|', $sqlite_bin, "${sqldb}", "SELECT name FROM sqlite_master WHERE type IN ('table') AND name IS 'moz_cookies'" ) ||
2013             return( $self->error( "Failed to execute sqlite3 binary ${sqlite_bin} to check if table moz_cookies exists: $!" ) );
2014             # chomp( $table_moz_cookies_exists = <$fh> );
2015 0         0 $table_moz_cookies_exists = <$fh>;
2016 0   0     0 $table_moz_cookies_exists //= '';
2017 0         0 chomp( $table_moz_cookies_exists );
2018 0         0 close( $fh );
2019            
2020             # Now, get the data to save
2021 0 0       0 open( $fh, '|-', $sqlite_bin, '--bail', "${sqldb}" ) ||
2022             return( $self->error( "Failed to execute sqlite3 binary ${sqlite_bin} to save all mozilla cookies to database ${sqldb}: $!" ) );
2023 0         0 $fh->autoflush;
2024 0 0       0 if( $opts->{log_sql} )
2025             {
2026 0 0       0 print( $fh ".trace ${log_file}\n" ) ||
2027             return( $self->error( "Failed to print sqlite command to enable logging to file ${log_file}: $!" ) );
2028             }
2029 0 0 0     0 if( $table_moz_cookies_exists && $opts->{overwrite} )
2030             {
2031 0 0       0 print( $fh "DROP TABLE IF EXISTS moz_cookies;\n" ) ||
2032             return( $self->error( "Failed to print sql commands to sqlite3 binary ${sqlite_bin} to save all mozilla cookies to database ${sqldb}: $!" ) );
2033 0         0 $table_moz_cookies_exists = 0;
2034             }
2035            
2036 0 0       0 if( $opts->{rollback} )
2037             {
2038 0 0       0 print( $fh "BEGIN TRANSACTION;\n" ) ||
2039             return( $self->error( "Failed to print sql commands to sqlite3 binary ${sqlite_bin} to save all mozilla cookies to database ${sqldb}: $!" ) );
2040             }
2041 0         0 my $template;
2042 0 0       0 if( !$table_moz_cookies_exists )
2043             {
2044 0         0 chomp( $create_table_sql );
2045 0 0       0 print( $fh "${create_table_sql};\n" ) ||
2046             return( $self->error( "Failed to print sql commands to sqlite3 binary ${sqlite_bin} to save all mozilla cookies to database ${sqldb}: $!" ) );
2047 0         0 $template = $insert_ignore_sql;
2048             }
2049             else
2050             {
2051 0 0 0     0 $can_do_upsert = ( defined( $sql_v ) && $sql_v >= $req_v ) ? 1 : 0;
2052             # if version is greater or equal to 3.24.0 we can do upsert, otherwise we do insert replace
2053 0 0       0 if( $can_do_upsert )
2054             {
2055 0         0 $template = $upsert_sql;
2056             }
2057             else
2058             {
2059 0         0 $template = $insert_replace_sql;
2060             }
2061             }
2062 0         0 chomp( $template );
2063             # This stores the data in $cookies array reference
2064             # NOTE: call to scan() must be after setting $can_do_upsert
2065 0         0 $self->scan( $get_cookies );
2066 0         0 my $row = $cookies->[0];
2067 0         0 foreach my $ref ( @$row )
2068             {
2069 0 0       0 if( $core_fields->{ $ref->[2] }->{constant} eq 'SQL_INTEGER' )
2070             {
2071 0         0 $template =~ s/\?/%s/;
2072             }
2073             else
2074             {
2075 0         0 $template =~ s/\?/'%s'/;
2076             }
2077             }
2078            
2079 0         0 foreach my $row ( @$cookies )
2080             {
2081 0         0 my $sql = sprintf( $template, map( $_->[0], @$row ) );
2082             print( $fh "${sql};\n" ) || do
2083 0 0       0 {
2084 0         0 my $err = $!;
2085 0 0       0 if( $opts->{rollback} )
2086             {
2087 0         0 print( $fh "ROLLBACK TRANSACTION;\n" );
2088             }
2089 0         0 return( $self->error( "Failed to print sql commands to sqlite3 binary ${sqlite_bin} to save all mozilla cookies to database ${sqldb}: ${err}" ) );
2090             };
2091             }
2092            
2093 0 0       0 if( $opts->{rollback} )
2094             {
2095 0 0       0 print( $fh "END TRANSACTION;\n" ) ||
2096             return( $self->error( "Failed to print sql commands to sqlite3 binary ${sqlite_bin} to save all mozilla cookies to database ${sqldb}: $!" ) );
2097             }
2098 0         0 close( $fh );
2099             }
2100              
2101 0 0 0     0 if( $opts->{log_sql} &&
      0        
2102             defined( $log_file ) &&
2103             $log_file->opened )
2104             {
2105 0         0 $log_file->close;
2106             }
2107 0         0 return( $self );
2108             }
2109              
2110             sub save_as_netscape
2111             {
2112 0     0 1 0 my $self = shift( @_ );
2113 0         0 my $opts = $self->_get_args_as_hash( @_ );
2114 0   0     0 $opts->{file} //= '';
2115 0   0     0 $opts->{skip_discard} //= 0;
2116 0   0     0 $opts->{skip_expired} //= 0;
2117 0 0       0 return( $self->error( "No file to write cookies was specified." ) ) if( !$opts->{file} );
2118 0   0     0 my $f = $self->new_file( $opts->{file} ) || return( $self->pass_error );
2119 0   0     0 my $io = $f->open( '>', { binmode => 'utf-8' }) ||
2120             return( $self->error( "Unable to write cookies to file \"$opts->{file}\": ", $f->error ) );
2121 0         0 $io->print( "# Netscape HTTP Cookie File:\n" );
2122 0         0 my $now = DateTime->now;
2123             $self->scan(sub
2124             {
2125 0     0   0 my $c = shift( @_ );
2126 0 0 0     0 return(1) if( $c->discard && $opts->{skip_discard} );
2127 0 0 0     0 return(1) if( $c->expires && $c->expires < $now && $opts->{skip_expired} );
      0        
2128 0         0 my @temp = ( $c->domain );
2129 0 0       0 push( @temp, $c->domain->substr( 1, 1 ) eq '.' ? 'TRUE' : 'FALSE' );
2130 0         0 push( @temp, $c->path );
2131 0 0       0 push( @temp, $c->secure ? 'TRUE' : 'FALSE' );
2132 0         0 push( @temp, $c->expires->epoch );
2133 0         0 push( @temp, $c->name );
2134 0         0 push( @temp, $c->value );
2135 0         0 $io->print( join( "\t", @temp ), "\n" );
2136 0         0 });
2137 0         0 $io->close;
2138 0         0 return( $self );
2139             }
2140              
2141             # For backward compatibility with HTTP::Cookies
2142 1     1 1 24 sub scan { return( shift->do( @_ ) ); }
2143              
2144             # NOTE: the secret key to be used to decrypt or encrypt the cookie jar file
2145 0     0 1 0 sub secret { return( shift->_set_get_scalar( 'secret', @_ ) ); }
2146              
2147             sub set
2148             {
2149 4     4 1 63 my $self = shift( @_ );
2150 4         9 my $c = shift( @_ );
2151 4         23 my $opts = $self->_get_args_as_hash( @_ );
2152 4 50       4578 return( $self->error( "No cookie name was provided to set." ) ) if( !$c->name->length );
2153 4 50       153567 return( $self->error( "Cookie value should be an object." ) ) if( !Scalar::Util::blessed( $c ) );
2154 4 50       9591 return( $self->error( "Cookie object does not have any as_string method." ) ) if( !$c->can( 'as_string' ) );
2155 4   50     19 $opts->{response} //= '';
2156 4         46 my $r = $self->request;
2157 4 50 33     488 if( $r )
    50 33        
2158             {
2159 0         0 $r->err_headers_out->add( 'Set-Cookie', $c->as_string );
2160             }
2161             elsif( $opts->{response} && $self->_is_object( $opts->{response} ) && $opts->{response}->can( 'header' ) )
2162             {
2163 4         201 $opts->{response}->header( 'Set-Cookie' => $c->as_string );
2164             }
2165             else
2166             {
2167 0         0 return( "Set-Cookie: " . $c->as_string );
2168             }
2169 4         536 return( $self );
2170             }
2171              
2172             # NOTE: cookie jar file type, e.g.: json, lwp or netscape
2173 0     0 1 0 sub type { return( shift->_set_get_scalar( 'type', @_ ) ); }
2174              
2175 33     33   302 sub _cookies { return( shift->_set_get_array_as_object( '_cookies', @_ ) ); }
2176              
2177             sub _encrypt_objects
2178             {
2179 0     0   0 my $self = shift( @_ );
2180 0         0 my( $key, $algo, $iv ) = @_;
2181 0 0 0     0 return( $self->error( "Key provided is empty!" ) ) if( !defined( $key ) || !CORE::length( "$key" ) );
2182 0 0 0     0 return( $self->error( "No algorithm was provided to encrypt cookie value. You can choose any <NAME> for which there exists Crypt::Cipher::<NAME>" ) ) if( !defined( $algo ) || !CORE::length( "$algo" ) );
2183 0 0       0 $self->_load_class( 'Crypt::Mode::CBC', { version => CRYPTX_VERSION } ) || return( $self->pass_error );
2184 0 0       0 $self->_load_class( 'Bytes::Random::Secure' ) || return( $self->pass_error );
2185             # try-catch
2186 0         0 local $@;
2187             my $crypt = eval
2188 0         0 {
2189 0         0 Crypt::Mode::CBC->new( "$algo" );
2190             };
2191 0 0       0 if( $@ )
2192             {
2193 0         0 return( $self->error( "Error getting the encryption objects for algorithm \"$algo\": $@" ) );
2194             }
2195 0 0       0 $crypt or return( $self->error( "Unable to create a Crypt::Mode::CBC object." ) );
2196              
2197 0         0 my $class = "Crypt::Cipher::${algo}";
2198 0 0       0 $self->_load_class( $class ) || return( $self->pass_error );
2199            
2200 0         0 my( $key_len, $block_len );
2201             eval
2202 0         0 {
2203 0         0 $key_len = $class->keysize;
2204 0         0 $block_len = $class->blocksize;
2205             };
2206 0 0       0 if( $@ )
2207             {
2208 0         0 return( $self->error( "Error getting the encryption key and block size for algorithm \"$algo\": $@" ) );
2209             }
2210 0 0       0 return( $self->error( "The size of the key provided (", CORE::length( $key ), ") does not match the minimum key size required for this algorithm \"$algo\" (${key_len})." ) ) if( CORE::length( $key ) < $key_len );
2211             # Generate an "IV", i.e. Initialisation Vector based on the required block size
2212 0 0 0     0 if( defined( $iv ) && CORE::length( "$iv" ) )
2213             {
2214 0 0       0 if( CORE::length( $iv ) != $block_len )
2215             {
2216 0         0 return( $self->error( "The Initialisation Vector provided for cookie encryption has a length (", CORE::length( $iv ), ") which does not match the algorithm ($algo) size requirement ($block_len). Please refer to the Cookie::Jar package documentation." ) );
2217             }
2218             }
2219             else
2220             {
2221             $iv = eval
2222 0         0 {
2223 0         0 Bytes::Random::Secure::random_bytes( $block_len );
2224             };
2225 0 0       0 if( $@ )
2226             {
2227 0         0 return( $self->error( "Error trying to get $block_len secure random bytes: $@" ) );
2228             }
2229             # Save it for decryption
2230 0         0 $self->_initialisation_vector( $iv );
2231             }
2232 0         0 my $key_pack = pack( 'H' x $key_len, $key );
2233 0         0 my $iv_pack = pack( 'H' x $block_len, $iv );
2234 0         0 return({ 'crypt' => $crypt, key => $key_pack, iv => $iv_pack });
2235             }
2236              
2237 25     25   216 sub _index { return( shift->_set_get_hash_as_mix_object( '_index', @_ ) ); }
2238              
2239             # For cookies file encryption
2240 4     4   28 sub _initialisation_vector { return( shift->_set_get_scalar_as_object( '_initialisation_vector', @_ ) ); }
2241              
2242             sub _normalize_path # so that plain string compare can be used
2243             {
2244 0     0   0 my $self = shift( @_ );
2245 0         0 my $str = shift( @_ );
2246 0         0 my $x;
2247 0         0 $str =~ s{
2248             %([0-9a-fA-F][0-9a-fA-F])
2249             }
2250             {
2251 0         0 $x = uc( $1 );
2252 0 0 0     0 $x eq '2F' || $x eq '25' ? "%$x" : pack( 'C', hex( $x ) );
2253             }egx;
2254 0         0 $str =~ s/([\0-\x20\x7f-\xff])/sprintf( '%%%02X', ord( $1 ) )/eg;
  0         0  
2255 0         0 return( $str );
2256             }
2257              
2258             sub DESTROY
2259             {
2260 8     8   49387 my $self = shift( @_ );
2261 8         60 my $file = $self->file;
2262 8 50 33     6187 if( $self->autosave && $file )
2263             {
2264 0           my $encrypt = $self->encrypt;
2265 0           my $type = $self->type;
2266 0           my $type2sub =
2267             {
2268             json => \&save,
2269             lwp => \&save_as_lwp,
2270             mozilla => \&save_as_mozilla,
2271             netscape => \&save_as_netscape,
2272             };
2273 0 0         if( !CORE::exists( $type2sub->{ $type } ) )
2274             {
2275 0 0         warn( "Unknown cookie jar type '$type'. This can be either json, lwp or netscape\n" ) if( $self->_warnings_is_enabled );
2276 0           return;
2277             }
2278            
2279 0           my $unloader = $type2sub->{ $type };
2280            
2281 0 0         if( $encrypt )
2282             {
2283             $unloader->( $self, $file,
2284             algo => $self->algo,
2285             key => $self->secret,
2286             ) || do
2287 0 0         {
2288 0 0         warn( $self->error, "\n" ) if( $self->_warnings_is_enabled );
2289             };
2290             }
2291             else
2292             {
2293             $unloader->( $self, $file ) || do
2294 0 0         {
2295 0 0         warn( $self->error, "\n" ) if( $self->_warnings_is_enabled );
2296             };
2297             }
2298             }
2299             };
2300              
2301             1;
2302             # NOTE: POD
2303             __END__
2304              
2305             =encoding utf8
2306              
2307             =head1 NAME
2308              
2309             Cookie::Jar - Cookie Jar Class for Server & Client
2310              
2311             =head1 SYNOPSIS
2312              
2313             use Cookie::Jar;
2314             my $jar = Cookie::Jar->new( request => $r ) ||
2315             die( "An error occurred while trying to get the cookie jar:", Cookie::Jar->error );
2316             # set the default host
2317             $jar->host( 'www.example.com' );
2318             $jar->fetch;
2319             # or using a HTTP::Request object
2320             # Retrieve cookies from Cookie header sent from client
2321             $jar->fetch( request => $http_request );
2322             if( $jar->exists( 'my-cookie' ) )
2323             {
2324             # do something
2325             }
2326             # get the cookie
2327             my $sid = $jar->get( 'my-cookie' );
2328             # get all cookies
2329             my @all = $jar->get( 'my-cookie', 'example.com', '/' );
2330             # set a new Set-Cookie header
2331             $jar->set( 'my-cookie' => $cookie_object );
2332             # Remove cookie from jar
2333             $jar->delete( 'my-cookie' );
2334             # or using the object itself:
2335             $jar->delete( $cookie_object );
2336              
2337             # Create and add cookie to jar
2338             $jar->add(
2339             name => 'session',
2340             value => 'lang=en-GB',
2341             path => '/',
2342             secure => 1,
2343             same_site => 'Lax',
2344             ) || die( $jar->error );
2345             # or add an existing cookie
2346             $jar->add( $some_cookie_object );
2347              
2348             my $c = $jar->make({
2349             name => 'my-cookie',
2350             domain => 'example.com',
2351             value => 'sid1234567',
2352             path => '/',
2353             expires => '+10D',
2354             # or alternatively
2355             maxage => 864000
2356             # to make it exclusively accessible by regular http request and not ajax
2357             http_only => 1,
2358             # should it be used under ssl only?
2359             secure => 1,
2360             });
2361              
2362             # Add the Set-Cookie headers
2363             $jar->add_response_header;
2364             # Alternatively, using a HTTP::Response object or equivalent
2365             $jar->add_response_header( $http_response );
2366             $jar->delete( 'some_cookie' );
2367             $jar->do(sub
2368             {
2369             # cookie object is available as $_ or as first argument in @_
2370             });
2371              
2372             # For client side
2373             # Takes a HTTP::Response object or equivalent
2374             # Extract cookies from Set-Cookie headers received from server
2375             $jar->extract( $http_response );
2376             # get by domain; by default sort it
2377             my $all = $jar->get_by_domain( 'example.com' );
2378             # Reverse sort
2379             $all = $jar->get_by_domain( 'example.com', sort => 0 );
2380              
2381             # Save cookies repository as json
2382             $jar->save( '/some/where/mycookies.json' ) || die( $jar->error );
2383             # Load cookies into jar
2384             $jar->load( '/some/where/mycookies.json' ) || die( $jar->error );
2385              
2386             # Save encrypted
2387             $jar->save( '/some/where/mycookies.json',
2388             {
2389             encrypt => 1,
2390             key => $key,
2391             iv => $iv,
2392             algo => 'AES',
2393             }) || die( $jar->error );
2394             # Load cookies from encrypted file
2395             $jar->load( '/some/where/mycookies.json',
2396             {
2397             decrypt => 1,
2398             key => $key,
2399             iv => $iv,
2400             algo => 'AES'
2401             }) || die( $jar->error );
2402              
2403             # Merge repository
2404             $jar->merge( $jar2 ) || die( $jar->error );
2405            
2406             # For autosave
2407             my $jar = Cookie::Jar->new(
2408             file => '/some/where/cookies.json',
2409             # True by default
2410             autosave => 1,
2411             encrypt => 1,
2412             secret => 'My big secret',
2413             algo => 'AES',
2414             ) || die( Cookie::Jar->error );
2415              
2416             say "There are ", $jar->length, " cookies in the repository.";
2417            
2418             # Take a string from a Set-Cookie header and get a Cookie object
2419             my $c = $jar->extract_one( $cookie_string );
2420              
2421             =head1 VERSION
2422              
2423             v0.3.3
2424              
2425             =head1 DESCRIPTION
2426              
2427             This is a module to handle L<cookies|Cookie>, according to the latest standard as set by L<rfc6265|https://datatracker.ietf.org/doc/html/rfc6265>, both by the http server and the client. Most modules out there are either antiquated, i.e. they do not support latest cookie L<rfc6265|https://datatracker.ietf.org/doc/html/rfc6265>, or they focus only on http client side.
2428              
2429             For example, Apache2::Cookie does not work well in decoding cookies, and L<Cookie::Baker> C<Set-Cookie> timestamp format is wrong. They use Mon-09-Jan 2020 12:17:30 GMT where it should be, as per rfc 6265 Mon, 09 Jan 2020 12:17:30 GMT
2430              
2431             Also L<APR::Request::Cookie> and L<Apache2::Cookie> which is a wrapper around L<APR::Request::Cookie> return a cookie object that returns the value of the cookie upon stringification instead of the full C<Set-Cookie> parameters. Clearly they designed it with a bias leaned toward collecting cookies from the browser.
2432              
2433             This module supports modperl and uses a L<Apache2::RequestRec> if provided, or can use package objects that implement similar interface as L<HTTP::Request> and L<HTTP::Response>, or if none of those above are available or provided, this module returns its results as a string.
2434              
2435             This module is also compatible with L<LWP::UserAgent>, so you can use like this:
2436              
2437             use LWP::UserAgent;
2438             use Cookie::Jar;
2439            
2440             my $ua = LWP::UserAgent->new(
2441             cookie_jar => Cookie::Jar->new
2442             );
2443              
2444             It is also compatible with L<HTTP::Promise>, such as:
2445              
2446             use HTTP::Promise;
2447             my $ua = HTTP::Promise->new( cookie_jar => Cookie::Jar->new );
2448              
2449             This module does not die upon error, but instead sets an L<error|Module::Generic/error> and returns C<undef> in scalar context or an empty list in list context, so you should always check the return value of a method.
2450              
2451             =head1 METHODS
2452              
2453             =head2 new
2454              
2455             This instantiates a new package object and accepts the following options:
2456              
2457             =over 4
2458              
2459             =item * C<request>
2460              
2461             This is an optional parameter to provide a L<Apache2::RequestRec> object. When provided, it will be used in various methods to get or set cookies from or onto http headers.
2462              
2463             package MyApacheHandler;
2464             use Apache2::Request ();
2465             use Cookie::Jar;
2466            
2467             sub handler : method
2468             {
2469             my( $class, $r ) = @_;
2470             my $jar = Cookie::Jar->new( $r );
2471             # Load cookies;
2472             $jar->fetch;
2473             $r->log_error( "$class: Found ", $jar->repo->length, " cookies." );
2474             $jar->add(
2475             name => 'session',
2476             value => 'lang=en-GB',
2477             path => '/',
2478             secure => 1,
2479             same_site => 'Lax',
2480             );
2481             # Will use Apache2::RequestRec object to set the Set-Cookie headers
2482             $jar->add_response_header || do
2483             {
2484             $r->log_reason( "Unable to add Set-Cookie to response header: ", $jar->error );
2485             return( Apache2::Const::HTTP_INTERNAL_SERVER_ERROR );
2486             };
2487             # Do some more computing
2488             return( Apache2::Const::OK );
2489             }
2490              
2491             =item * C<debug>
2492              
2493             Optional. If set with a positive integer, this will activate verbose debugging message
2494              
2495             =back
2496              
2497             =head2 add
2498              
2499             Provided with an hash or hash reference of cookie parameters (see L<Cookie>) and this will create a new L<cookie|Cookie> and add it to the cookie repository.
2500              
2501             Alternatively, you can also provide directly an existing L<cookie object|Cookie>
2502              
2503             my $c = $jar->add( $cookie_object ) || die( $jar->error );
2504              
2505             =head2 add_cookie_header
2506              
2507             This is an alias for L</add_request_header> for backward compatibility with L<HTTP::Cookies>
2508              
2509             =head2 add_request_header
2510              
2511             Provided with a request object, such as, but not limited to L<HTTP::Request> and this will add all relevant cookies in the repository into the C<Cookie> C<HTTP> request header. The object method needs to have the C<header> method in order to get, or set the C<Cookie> or C<Set-Cookie> headers and the C<uri> method.
2512              
2513             As long as the object provided supports the C<uri> and C<header> method, you can provide any class of object you want.
2514              
2515             Please refer to the L<rfc6265|https://datatracker.ietf.org/doc/html/rfc6265> for more information on the applicable rule when adding cookies to the outgoing request header.
2516              
2517             Basically, it will add, for a given domain, first all cookies whose path is longest and at path equivalent, the cookie creation date is used, with the earliest first. Cookies who have expired are not sent, and there can be cookies bearing the same name for the same domain in different paths.
2518              
2519             =head2 add_response_header
2520              
2521             # Adding cookie to the repository
2522             $jar->add(
2523             name => 'session',
2524             value => 'lang=en-GB',
2525             path => '/',
2526             secure => 1,
2527             same_site => 'Lax',
2528             ) || die( $jar->error );
2529             # then placing it onto the response header
2530             $jar->add_response_header;
2531              
2532             This is the alter ego to L</add_request_header>, in that it performs the equivalent function, but for the server side.
2533              
2534             You can optionally provide, as unique argument, an object, such as but not limited to, L<HTTP::Response>, as long as that class supports the C<header> method
2535              
2536             Alternatively, if an L<Apache object|Apache2::RequestRec> has been set upon object instantiation or later using the L</request> method, then it will be used to set the outgoing C<Set-Cookie> headers (there is one for every cookie sent).
2537              
2538             If no response, nor Apache2 object were set, then this will simply return a list of C<Set-Cookie> in list context, or a string of possibly multiline C<Set-Cookie> headers, or an empty string if there is no cookie found to be sent.
2539              
2540             Be careful not to do the following:
2541              
2542             # get cookies sent by the HTTP client
2543             $jar->fetch || die( $jar->error );
2544             # set the response headers with the cookies from our repository
2545             $jar->add_response_header;
2546              
2547             Why? Well, because L</fetch> retrieves the cookies sent by the HTTP client and store them into the repository. However, cookies sent by the HTTP client only contain the cookie name and value, such as:
2548              
2549             GET /my/path/ HTTP/1.1
2550             Host: www.example.org
2551             Cookie: session_token=eyJleHAiOjE2MzYwNzEwMzksImFsZyI6IkhTMjU2In0.eyJqdGkiOiJkMDg2Zjk0OS1mYWJmLTRiMzgtOTE1ZC1hMDJkNzM0Y2ZmNzAiLCJmaXJzdF9uYW1lIjoiSm9obiIsImlhdCI6MTYzNTk4NDYzOSwiYXpwIjoiNGQ0YWFiYWQtYmJiMy00ODgwLThlM2ItNTA0OWMwZTczNjBlIiwiaXNzIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20iLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwibGFzdF9uYW1lIjoiRG9lIiwic3ViIjoiYXV0aHxlNzg5OTgyMi0wYzlkLTQyODctYjc4Ni02NTE3MjkyYTVlODIiLCJjbGllbnRfaWQiOiJiZTI3N2VkYi01MDgzLTRjMWEtYTM4MC03Y2ZhMTc5YzA2ZWQiLCJleHAiOjE2MzYwNzEwMzksImF1ZCI6IjRkNGFhYmFkLWJiYjMtNDg4MC04ZTNiLTUwNDljMGU3MzYwZSJ9.VSiSkGIh41xXIVKn9B6qGjfzcLlnJAZ9jGOPVgXASp0; csrf_token=9849724969dbcffd48c074b894c8fbda14610dc0ae62fac0f78b2aa091216e0b.1635825594; site_prefs=lang%3Den-GB
2552              
2553             As you can see, 3 cookies were sent: C<session_token>, C<csrf_token> and C<site_prefs>
2554              
2555             So, when L</fetch> creates an object for each one and store them, those cookies have no C<path> value and no other attribute, and when L</add_response_header> is then called, it stringifies the cookies and create a C<Set-Cookie> header for each one, but only with their value and no other attribute.
2556              
2557             The HTTP client, when receiving those cookies will derive the missing cookie path to be C</my/path>, i.e. the current URI path, and will create a duplicate cookie from the previously stored cookie with the same name for that host, but that had the path set to C</>
2558              
2559             So you can create a repository and use it to store the cookies sent by the HTTP client using L</fetch>, but in preparation of the server response, either use a separate repository with, for example, C<< my $jar_out = Cookie::Jar->new >> or use L</set> which will not add the cookie to the repository, but rather only set the C<Set-Cookie> header for that cookie.
2560              
2561             # Add Set-Cookie header for that cookie, but do not add cookie to repository
2562             $jar->set( $cookie_object );
2563              
2564             =head2 algo
2565              
2566             String. Sets or gets the algorithm to use when loading or saving the cookie jar.
2567              
2568             =head2 autosave
2569              
2570             Boolean. Sets or gets the boolean value for automatically saving the cookie jar to the given file specified with L</file>
2571              
2572             =head2 delete
2573              
2574             Given a cookie name, an optional host and optional path or a L<Cookie> object, and this will remove it from the cookie repository.
2575              
2576             It returns an L<array object|Module::Generic::Array> upon success, or L<perlfunc/undef> and sets an L<error|Module::Generic/error>. Note that the array object may be empty.
2577              
2578             However, this will NOT remove it from the web browser by sending a Set-Cookie header. For that, you might want to look at the L<Cookie/elapse> method.
2579              
2580             It returns an L<array object|Module::Generic::Array> of cookie objects removed.
2581              
2582             my $arr = $jar->delete( 'my-cookie' );
2583             # alternatively
2584             my $arr = $jar->delete( 'my-cookie' => 'www.example.org' );
2585             # or
2586             my $arr = $jar->delete( $my_cookie_object );
2587             printf( "%d cookie(s) removed.\n", $arr->length );
2588             print( "Cookie value removed was: ", $arr->first->value, "\n" );
2589              
2590             If you are interested in telling the HTTP client to remove all your cookies, you can set the C<Clear-Site-Data> header:
2591              
2592             Clear-Site-Data: "cookies"
2593              
2594             You can instruct the HTTP client to remove other data like local storage:
2595              
2596             Clear-Site-Data: "cookies", "cache", "storage", "executionContexts"
2597              
2598             Although this is widely supported, there is no guarantee the HTTP client will actually comply with this request.
2599              
2600             See L<Mozilla documentation|https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data> for more information.
2601              
2602             =head2 do
2603              
2604             Provided with an anonymous code or reference to a subroutine, and this will call that code for every cookie in the repository, passing it the cookie object as the sole argument. Also, that cookie object is accessible using C<$_>.
2605              
2606             If the code return C<undef>, it will end the loop, and if the code returns true, this will have the current cookie object added to an L<array object|Module::Generic::Array> returned upon completion of the loop.
2607              
2608             my $found = $jar->do(sub
2609             {
2610             # Part of the path
2611             if( index( $path, $_->path ) == 0 )
2612             {
2613             return(1);
2614             }
2615             return(0);
2616             });
2617             print( "Found cookies: ", $found->map(sub{$_->name})->join( ',' ), "\n" );
2618              
2619             =head2 encrypt
2620              
2621             Boolean. Sets or gets the boolean value for whether to encrypt or not the cookie jar when saving it, or whether to decrypt it when loading cookies from it.
2622              
2623             This defaults to false.
2624              
2625             =head2 exists
2626              
2627             Given a cookie name, this will check if it exists.
2628              
2629             It returns 1 if it does, or 0 if it does not.
2630              
2631             =head2 extract
2632              
2633             Provided with a response object, such as, but not limited to L<HTTP::Response>, and this will retrieve any cookie sent from the remote server, parse them and add their respective to the repository.
2634              
2635             As per the L<rfc6265, section 5.3.11 specifications|https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> if there are duplicate cookies for the same domain and path, only the last one will be retained.
2636              
2637             If the cookie received does not contain any C<Domain> specification, then, in line with rfc6265 specifications, it will take the root of the current domain as the default domain value. Since finding out what is the root for a domain name is a non-trivial exercise, this method relies on L<Cookie::Domain>.
2638              
2639             =head2 extract_cookies
2640              
2641             This is an alias for L</extract> for backward compatibility with L<HTTP::Cookies>
2642              
2643             =head2 extract_one
2644              
2645             This method takes a cookie string, which can be found in the C<Set-Cookie> header, parse it, and returns a L<Cookie> object if successful, or sets an L<error|Module::Generic/error> and return C<undef> or an empty list depending on the context.
2646              
2647             It also takes an hash or hash reference of options.
2648              
2649             The following options are supported:
2650              
2651             =over 4
2652              
2653             =item * C<host>
2654              
2655             If provided, it will be used to find out the host's root domain, and to set the cookie object C<domain> property if none is specified in the cookie string.
2656              
2657             =item * C<path>
2658              
2659             If provided, it will be used to set the cookie object C<path> property.
2660              
2661             =item * C<port>
2662              
2663             If provided, it will be used to set the cookie object C<port> property.
2664              
2665             =back
2666              
2667             =head2 fetch
2668              
2669             This method does the equivalent of L</extract>, but for the server.
2670              
2671             It retrieves all possible cookies from the HTTP request received from the web browser.
2672              
2673             It takes an optional hash or hash reference of parameters, such as C<host>. If it is not provided, the value set with L</host> is used instead.
2674              
2675             If the parameter C<request> containing an HTTP request object, such as, but not limited to L<HTTP::Request>, is provided, it will use it to get the C<Cookie> header value. The object method needs to have the C<header> method in order to get, or set the C<Cookie> or C<Set-Cookie> headers.
2676              
2677             Alternatively, if a value for L</request> has been set, it will use it to get the C<Cookie> header value from Apache modperl.
2678              
2679             You can also provide the C<Cookie> string to parse by providing the C<string> option to this method.
2680              
2681             $jar->fetch( string => q{foo=bar; site_prefs=lang%3Den-GB} ) ||
2682             die( $jar->error );
2683              
2684             Ultimately, if none of those are available, it will use the environment variable C<HTTP_COOKIE>
2685              
2686             If the option C<store> is true (by default it is true), this method will add the fetched cookies to the L<repository|/repo>.
2687              
2688             It returns an hash reference of cookie key => L<cookie object|Cookie>
2689              
2690             A cookie key is made of the host (possibly empty), the path and the cookie name separated by C<;>
2691              
2692             # Cookies added to the repository
2693             $jar->fetch || die( $jar->error );
2694             # Cookies returned, but NOT added to the repository
2695             my $cookies = $jar->fetch || die( $jar->error );
2696              
2697             =head2 file
2698              
2699             Sets or gets the file path to the cookie jar file.
2700              
2701             If provided upon instantiation, and if the file exists on the filesystem and is not empty, C<Cookie::Jar> will load all the cookies from it.
2702              
2703             If L</autosave> is set to a true, C<Cookie::Jar> will automatically save all cookies to the specified cookie jar file, possibly encrypting it if L</algo> and L</secret> are set.
2704              
2705             =head2 get
2706              
2707             Given a cookie name, an optional host and an optional path, this will retrieve its corresponding L<cookie object|Cookie> and return it.
2708              
2709             If not found, it will try to return a value with just the cookie name.
2710              
2711             If nothing is found, this will return and empty list in list context or C<undef> in scalar context.
2712              
2713             You can C<get> multiple cookie object and this method will return a list in list context and the first cookie object found in scalar context.
2714              
2715             # Wrong, an undefined returned value here only means there is no such cookie
2716             my $c = $jar->get( 'my-cookie' );
2717             die( $jar->error ) if( !defined( $c ) );
2718             # Correct
2719             my $c = $jar->get( 'my-cookie' ) || die( "No cookie my-cookie found\n" );
2720             # Possibly get multiple cookie object for the same name
2721             my @cookies = $jar->get( 'my_same_name' ) || die( "No cookies my_same_name found\n" );
2722             # or
2723             my @cookies = $jar->get( 'my_same_name' => 'www.example.org', '/private' ) || die( "No cookies my_same_name found\n" );
2724              
2725             =head2 get_by_domain
2726              
2727             Provided with a host and an optional hash or hash reference of parameters, and this returns an L<array object|Module::Generic::Array> of L<cookie objects|Cookie> matching the domain specified.
2728              
2729             If a C<sort> parameter has been provided and its value is true, this will sort the cookies by path alphabetically. If the sort value exists, but is false, this will sort the cookies by path but in a reverse alphabetical order.
2730              
2731             By default, the cookies are sorted.
2732              
2733             =head2 host
2734              
2735             Sets or gets the default host. This is especially useful for cookies repository used on the server side.
2736              
2737             =head2 key
2738              
2739             Provided with a cookie name and an optional host and this returns a key used to add an entry in the hash repository.
2740              
2741             If no host is provided, the key is just the cookie, otherwise the resulting key is the cookie name and host separated just by C<;>
2742              
2743             You should not need to use this method as it is used internally only.
2744              
2745             =head2 length
2746              
2747             Read-only. Returns the size of the Cookie repository as a L<number object|Module::Generic::Number>
2748              
2749             =head2 load
2750              
2751             $jar->load( '/home/joe/cookies.json' ) || die( $jar->error );
2752              
2753             # or loading cookies from encrypted file
2754             $jar->load( '/home/joe/cookies_encrypted.json',
2755             {
2756             decrypt => 1,
2757             key => $key,
2758             iv => $iv,
2759             algo => 'AES'
2760             }) || die( $jar->error );
2761              
2762             Give a json cookie file, and an hash or hash reference of options, and this will load its data into the repository. If there are duplicates (same cookie name and host), the latest one added takes precedence, as per the rfc6265 specifications.
2763              
2764             Supported options are:
2765              
2766             =over 4
2767              
2768             =item * C<algo> string
2769              
2770             Algorithm to use to decrypt the cookie file.
2771              
2772             It can be any of L<AES|Crypt::Cipher::AES>, L<Anubis|Crypt::Cipher::Anubis>, L<Blowfish|Crypt::Cipher::Blowfish>, L<CAST5|Crypt::Cipher::CAST5>, L<Camellia|Crypt::Cipher::Camellia>, L<DES|Crypt::Cipher::DES>, L<DES_EDE|Crypt::Cipher::DES_EDE>, L<KASUMI|Crypt::Cipher::KASUMI>, L<Khazad|Crypt::Cipher::Khazad>, L<MULTI2|Crypt::Cipher::MULTI2>, L<Noekeon|Crypt::Cipher::Noekeon>, L<RC2|Crypt::Cipher::RC2>, L<RC5|Crypt::Cipher::RC5>, L<RC6|Crypt::Cipher::RC6>, L<SAFERP|Crypt::Cipher::SAFERP>, L<SAFER_K128|Crypt::Cipher::SAFER_K128>, L<SAFER_K64|Crypt::Cipher::SAFER_K64>, L<SAFER_SK128|Crypt::Cipher::SAFER_SK128>, L<SAFER_SK64|Crypt::Cipher::SAFER_SK64>, L<SEED|Crypt::Cipher::SEED>, L<Skipjack|Crypt::Cipher::Skipjack>, L<Twofish|Crypt::Cipher::Twofish>, L<XTEA|Crypt::Cipher::XTEA>, L<IDEA|Crypt::Cipher::IDEA>, L<Serpent|Crypt::Cipher::Serpent> or simply any <NAME> for which there exists Crypt::Cipher::<NAME>
2773              
2774             =item * C<decrypt> boolean
2775              
2776             Must be set to true to enable decryption.
2777              
2778             =item * C<iv> string
2779              
2780             Set the L<Initialisation Vector|https://en.wikipedia.org/wiki/Initialization_vector> used for file encryption and decryption. This must be the same value used for encryption. See L</save>
2781              
2782             =item * C<key> string
2783              
2784             Set the encryption key used to decrypt the cookies file.
2785              
2786             The key must be the same one used to encrypt the file. See L</save>
2787              
2788             =back
2789              
2790             L</load> returns the current object upon success and C<undef> and sets an L<error|Module::Generic/error> upon error.
2791              
2792             =head2 load_as_lwp
2793              
2794             $jar->load_as_lwp( '/home/joe/cookies_lwp.txt' ) ||
2795             die( "Unable to load cookies from file: ", $jar->error );
2796              
2797             # or loading an encrypted file
2798             $jar->load_as_lwp( '/home/joe/cookies_encrypted_lwp.txt',
2799             {
2800             encrypt => 1,
2801             key => $key,
2802             iv => $iv,
2803             algo => 'AES',
2804             }) || die( $jar->error );
2805              
2806             Given a file path to an LWP-style cookie file (see below a snapshot of what it looks like), and an hash or hash reference of options, and this method will read the cookies from the file and add them to our repository, possibly overwriting previous cookies with the same name and domain name.
2807              
2808             The supported options are the same as for L</load>
2809              
2810             LWP-style cookie files are ancient, and barely used anymore, but no matter; if you need to load cookies from such file, it looks like this:
2811              
2812             #LWP-Cookies-1.0
2813             Set-Cookie3: cookie1=value1; domain=example.com; path=; path_spec; secure; version=2
2814             Set-Cookie3: cookie2=value2; domain=api.example.com; path=; path_spec; secure; version=2
2815             Set-Cookie3: cookie3=value3; domain=img.example.com; path=; path_spec; secure; version=2
2816              
2817             It returns the current object upon success, or C<undef> and sets an L<error|Module::Generic/error> upon error.
2818              
2819             =head2 load_as_mozilla
2820              
2821             $jar->load_as_mozilla( '/home/joe/cookies.sqlite' ) ||
2822             die( "Unable to load cookies from mozilla cookies.sqlite file: ", $jar->error );
2823              
2824             Given a file path to a mozilla SQLite database file, and an hash or hash reference of options, and this method will attempt to read the cookies from the SQLite database file and add them to our repository, possibly overwriting previous cookies with the same name and domain name.
2825              
2826             To read the SQLite database file, this will try first to load L<DBI> and L<DBD::SQLite> and use them if they are available, otherwise it will resort to using the C<sqlite3> binary if it can find it, using L<File::Which/which>
2827              
2828             If none of those 2 methods succeeded, it will return C<undef> with an L<error|Module::Generic/error>
2829              
2830             Note that contrary to other loading method, this method does not support encryption.
2831              
2832             It returns the current object upon success, or C<undef> and sets an L<error|Module::Generic/error> upon error.
2833              
2834             Supported options are:
2835              
2836             =over 4
2837              
2838             =item * C<use_dbi>
2839              
2840             Boolean. If true, this will require the use of L<DBI> and L<DBD::SQLite> and if it cannot load them, it will return an error without trying to alternatively use the C<sqlite3> binary. Default to false.
2841              
2842             =item * C<sqlite>
2843              
2844             String. The file path to a C<sqlite3> binary. If the file path does not exist, or is lacking sufficient permission, this will return an error.
2845              
2846             If it is not provided, and using L<DBI> and L<DBD::SQLite> failed, it will try to find the C<sqlite3> using L<File::Which/which>
2847              
2848             =back
2849              
2850             =head2 load_as_netscape
2851              
2852             $jar->save_as_netscape( '/home/joe/cookies_netscape.txt' ) ||
2853             die( "Unable to save cookies file: ", $jar->error );
2854              
2855             # or saving as an encrypted file
2856             $jar->save_as_netscape( '/home/joe/cookies_encrypted_netscape.txt',
2857             {
2858             encrypt => 1,
2859             key => $key,
2860             iv => $iv,
2861             algo => 'AES',
2862             }) || die( $jar->error );
2863              
2864             Given a file path to a Netscape-style cookie file, and this method will read cookies from the file and add them to our repository, possibly overwriting previous cookies with the same name and domain name.
2865              
2866             It returns the current object upon success, or C<undef> and sets an L<error|Module::Generic/error> upon error.
2867              
2868             =head2 make
2869              
2870             Provided with some parameters and this will instantiate a new L<Cookie> object with those parameters and return the new object.
2871              
2872             This does not add the newly created cookie object to the cookies repository.
2873              
2874             For a list of supported parameters, refer to the L<Cookie documentation|Cookie>
2875              
2876             # Make an encrypted cookie
2877             use Bytes::Random::Secure ();
2878             my $c = $jar->make(
2879             name => 'session',
2880             value => $secret_value,
2881             path => '/',
2882             secure => 1,
2883             http_only => 1,
2884             same_site => 'Lax',
2885             key => Bytes::Random::Secure::random_bytes(32),
2886             algo => $algo,
2887             encrypt => 1,
2888             ) || die( $jar->error );
2889             # or as an hash reference of parameters
2890             my $c = $jar->make({
2891             name => 'session',
2892             value => $secret_value,
2893             path => '/',
2894             secure => 1,
2895             http_only => 1,
2896             same_site => 'Lax',
2897             key => Bytes::Random::Secure::random_bytes(32),
2898             algo => $algo,
2899             encrypt => 1,
2900             }) || die( $jar->error );
2901              
2902             =head2 merge
2903              
2904             Provided with another L<Cookie::Jar> object, or at least an object that supports the L</do> method, which takes an anonymous code as argument, and that calls that code passing it each cookie object found in the alternate repository, and this method will add all those cookies in the alternate repository into the current repository.
2905              
2906             $jar->merge( $other_jar ) || die( $jar->error );
2907              
2908             If the cookie objects passed to the anonymous code in this method, are not L<Cookie> object, then at least they must support the methods C<name>, C<value>, C<domain>, C<path>, C<port>, C<secure>, C<max_age>, C<secure>, C<same_site> and , C<http_only>
2909              
2910             This method also takes an hash or hash reference of options:
2911              
2912             =over 4
2913              
2914             =item * C<die> boolean
2915              
2916             If true, the anonymous code passed to the C<do> method called, will die upon error. Default to false.
2917              
2918             By default, if an error occurs, C<undef> is returned and the L<error|Module::Generic/error> is set.
2919              
2920             =item * C<overwrite> boolean
2921              
2922             If true, when an existing cookie is found it will be overwritten by the new one. Default to false.
2923              
2924             =back
2925              
2926             use Nice::Try;
2927             try
2928             {
2929             $jar->merge( $other_jar, die => 1, overwrite => 1 );
2930             }
2931             catch( $e )
2932             {
2933             die( "Failed to merge cookies repository: $e\n" );
2934             }
2935              
2936             Upon success this will return the current object, and if there was an error, this returns L<perlfunc/undef> and sets an L<error|Module::Generic/error>
2937              
2938             =head2 parse
2939              
2940             This method is used by L</fetch> to parse cookies sent by HTTP client. Parsing is much simpler than for HTTP client receiving cookies from server.
2941              
2942             It takes the raw C<Cookie> string sent by the HTTP client, and returns an hash reference (possibly empty) of cookie name to cookie value pairs.
2943              
2944             my $cookies = $jar->parse( 'foo=bar; site_prefs=lang%3Den-GB' );
2945             # You can safely do as well:
2946             my $cookies = $jar->parse( '' );
2947              
2948             =head2 purge
2949              
2950             Thise takes no argument and will remove from the repository all cookies that have expired. A cookie that has expired is a L<Cookie> that has its C<expires> property set and whose value is in the past.
2951              
2952             This returns an L<array object|Module::Generic::Array> of all the cookies thus removed.
2953              
2954             my $all = $jar->purge;
2955             printf( "Cookie(s) removed were: %s\n", $all->map(sub{ $_->name })->join( ',' ) );
2956             # or
2957             printf( "%d cookie(s) removed from our repository.\n", $jar->purge->length );
2958              
2959             =head2 replace
2960              
2961             Provided with a L<Cookie> object, and an optional other L<Cookie> object, and this method will replace the former cookie provided in the second parameter with the new one provided in the first parameter.
2962              
2963             If only one parameter is provided, the cookies to be replaced will be derived from the replacement cookie's properties, namely: C<name>, C<domain> and C<path>
2964              
2965             It returns an L<array object|Module::Generic::Array> of cookie objects replaced upon success, or C<undef> and set an L<error|Module::Generic/error> upon error.
2966              
2967             =head2 repo
2968              
2969             Set or get the L<array object|Module::Generic::Array> used as the cookie jar repository.
2970              
2971             printf( "%d cookies found\n", $jar->repo->length );
2972              
2973             =head2 request
2974              
2975             Set or get the L<Apache2::RequestRec> object. This object is used to set the C<Set-Cookie> header within modperl.
2976              
2977             =head2 save
2978              
2979             $jar->save( '/home/joe/cookies.json' ) ||
2980             die( "Failed to save cookies: ", $jar->error );
2981              
2982             # or saving the cookies file encrypted
2983             $jar->save( '/home/joe/cookies_encrypted.json',
2984             {
2985             encrypt => 1,
2986             key => $key,
2987             iv => $iv,
2988             algo => 'AES',
2989             }) || die( $jar->error );
2990              
2991             Provided with a file, and an hash or hash reference of options, and this will save the repository of cookies as json data.
2992              
2993             The hash saved to file contains 2 top properties: C<updated_on> containing the last update date and C<cookies> containing an hash of cookie name to cookie properties pairs.
2994              
2995             It returns the current object. If an error occurred, it will return C<undef> and set an L<error|Module::Generic/error>
2996              
2997             Supported options are:
2998              
2999             =over 4
3000              
3001             =item * C<algo> string
3002              
3003             Algorithm to use to encrypt the cookie file.
3004              
3005             It can be any of L<AES|Crypt::Cipher::AES>, L<Anubis|Crypt::Cipher::Anubis>, L<Blowfish|Crypt::Cipher::Blowfish>, L<CAST5|Crypt::Cipher::CAST5>, L<Camellia|Crypt::Cipher::Camellia>, L<DES|Crypt::Cipher::DES>, L<DES_EDE|Crypt::Cipher::DES_EDE>, L<KASUMI|Crypt::Cipher::KASUMI>, L<Khazad|Crypt::Cipher::Khazad>, L<MULTI2|Crypt::Cipher::MULTI2>, L<Noekeon|Crypt::Cipher::Noekeon>, L<RC2|Crypt::Cipher::RC2>, L<RC5|Crypt::Cipher::RC5>, L<RC6|Crypt::Cipher::RC6>, L<SAFERP|Crypt::Cipher::SAFERP>, L<SAFER_K128|Crypt::Cipher::SAFER_K128>, L<SAFER_K64|Crypt::Cipher::SAFER_K64>, L<SAFER_SK128|Crypt::Cipher::SAFER_SK128>, L<SAFER_SK64|Crypt::Cipher::SAFER_SK64>, L<SEED|Crypt::Cipher::SEED>, L<Skipjack|Crypt::Cipher::Skipjack>, L<Twofish|Crypt::Cipher::Twofish>, L<XTEA|Crypt::Cipher::XTEA>, L<IDEA|Crypt::Cipher::IDEA>, L<Serpent|Crypt::Cipher::Serpent> or simply any <NAME> for which there exists Crypt::Cipher::<NAME>
3006              
3007             =item * C<encrypt> boolean
3008              
3009             Must be set to true to enable encryption.
3010              
3011             =item * C<iv> string
3012              
3013             Set the L<Initialisation Vector|https://en.wikipedia.org/wiki/Initialization_vector> used for file encryption. If you do not provide one, it will be automatically generated. If you want to provide your own, make sure the size meets the encryption algorithm size requirement. You also need to keep this to decrypt the cookies file.
3014              
3015             To find the right size for the Initialisation Vector, for example for algorithm C<AES>, you could do:
3016              
3017             perl -MCrypt::Cipher::AES -lE 'say Crypt::Cipher::AES->blocksize'
3018              
3019             which would yield C<16>
3020              
3021             =item * C<key> string
3022              
3023             Set the encryption key used to encrypt the cookies file.
3024              
3025             The key must be the same one used to decrypt the file and must have a size big enough to satisfy the encryption algorithm requirement, which you can check with, say for C<AES>:
3026              
3027             perl -MCrypt::Cipher::AES -lE 'say Crypt::Cipher::AES->keysize'
3028              
3029             In this case, it will yield C<32>. Replace above C<AES>, by whatever algorithm you have chosen.
3030              
3031             perl -MCrypt::Cipher::Blowfish -lE 'say Crypt::Cipher::Blowfish->keysize'
3032              
3033             would yield C<56> for C<Blowfish>
3034              
3035             You can use L<Bytes::Random::Secure/random_bytes> to generate a random key:
3036              
3037             # will generate a 32 bytes-long key
3038             my $key = Bytes::Random::Secure::random_bytes(32);
3039              
3040             =back
3041              
3042             When encrypting the cookies file, this method will encode the encrypted data in base64 before saving it to file.
3043              
3044             =head2 save_as_lwp
3045              
3046             $jar->save_as_lwp( '/home/joe/cookies_lwp.txt' ) ||
3047             die( "Unable to save cookies file: ", $jar->error );
3048              
3049             # or saving as an encrypted file
3050             $jar->save_as_lwp( '/home/joe/cookies_encrypted_lwp.txt',
3051             {
3052             encrypt => 1,
3053             key => $key,
3054             iv => $iv,
3055             algo => 'AES',
3056             }) || die( $jar->error );
3057              
3058             Provided with a file, and an hash or hash reference of options, and this save the cookies repository as a LWP-style data.
3059              
3060             The supported options are the same as for L</save>
3061              
3062             It returns the current object. If an error occurred, it will return C<undef> and set an L<error|Module::Generic/error>
3063              
3064             =head2 save_as_mozilla
3065              
3066             $jar->save_as_mozilla( '/home/joe/cookies.sqlite' ) ||
3067             die( "Unable to save cookies as mozilla SQLite database: ", $jar->error );
3068              
3069             # or
3070             $jar->save_as_mozilla( '/home/joe/cookies.sqlite',
3071             {
3072             # force use of DBI/DBD::SQLite
3073             use_dbi => 1,
3074             # or specify the path of the sqlite3 binary
3075             # sqlite => '/some/where/sqlite3',
3076             # Enable logging of SQL queries maybe?
3077             # log_sql => '/some/where/sql.log',
3078             # Overwrite previous data
3079             overwrite => 1,
3080             # abort if an error occurred
3081             rollback => 1,
3082             }) || die( "Unable to save cookies as mozilla SQLite database: ", $jar->error );
3083              
3084             Provided with a file path to a SQLite database and this saves the cookies repository as a mozilla SQLite database.
3085              
3086             The structure of the L<mozilla SQLite database|http://kb.mozillazine.org/Cookies> is:
3087              
3088             CREATE TABLE moz_cookies(
3089             id INTEGER PRIMARY KEY,
3090             originAttributes TEXT NOT NULL DEFAULT '',
3091             name TEXT,
3092             value TEXT,
3093             host TEXT,
3094             path TEXT,
3095             expiry INTEGER,
3096             lastAccessed INTEGER,
3097             creationTime INTEGER,
3098             isSecure INTEGER,
3099             isHttpOnly INTEGER,
3100             inBrowserElement INTEGER DEFAULT 0,
3101             sameSite INTEGER DEFAULT 0,
3102             rawSameSite INTEGER DEFAULT 0,
3103             schemeMap INTEGER DEFAULT 0,
3104             CONSTRAINT moz_uniqueid UNIQUE(name, host, path, originAttributes)
3105             );
3106              
3107             This method will attempt loading L<DBI> and L<DBD::SQLite>, and if it fails, it will alternatively try to use the C<sqlite3> binary.
3108              
3109             Note that, contrary to other save methods, this method does not allow encrypting the SQLite database.
3110              
3111             It returns the current object. If an error occurred, it will return C<undef> and set an L<error|Module::Generic/error>
3112              
3113             Supported options are:
3114              
3115             =over 4
3116              
3117             =item * C<log_sql>
3118              
3119             String. This specifies a file name that will be opened in append mode and to which the SQL statements issued will be logged.
3120              
3121             =item * C<overwrite>
3122              
3123             Boolean. If true, this will overwrite any existing data if the specified SQLite database file already exists.
3124              
3125             And if false, this will issue sql queries to perform L<upsert|https://www.sqlite.org/lang_UPSERT.html> if the SQLite version is greater or equal to C<3.24.0> (2018-06-04), or otherwise it will issue L<INSERT OR REPLACE|https://www.sqlite.org/lang_insert.html> queries.
3126              
3127             Default false.
3128              
3129             =item * C<rollback>
3130              
3131             Boolean. If true, this will cancel, i.e. rollback, any change mad to the SQLite database upon error, otherwise, any change made will be kept up to the point of when the error occurred. Default to false.
3132              
3133             =item * C<skip_discard>
3134              
3135             Boolean. If true, this will not save cookies that have been marked as being discarded, such as session cookies. Default false.
3136              
3137             =item * C<skip_expired>
3138              
3139             Boolean. If true, this will not save the cookies that have already expired. Default false.
3140              
3141             =item * C<sqlite>
3142              
3143             String. The file path to a C<sqlite3> binary. If the file path does not exist, or is lacking sufficient permission, this will return an error.
3144              
3145             If it is not provided, and using L<DBI> and L<DBD::SQLite> failed, it will try to find the C<sqlite3> using L<File::Which/which>
3146              
3147             =item * C<use_dbi>
3148              
3149             Boolean. Requires the use of L<DBI> and L<DBD::SQLite> and it will return an error if those are not installed.
3150              
3151             If you want to let this method try also to use C<sqlite3> binary if necessary, then do not set this option.
3152              
3153             =back
3154              
3155             =head2 save_as_netscape
3156              
3157             Provided with a file and this saves the cookies repository as a Netscape-style data.
3158              
3159             It returns the current object. If an error occurred, it will return C<undef> and set an L<error|Module::Generic/error>
3160              
3161             =head2 scan
3162              
3163             This is an alias for L</do>
3164              
3165             =head2 secret
3166              
3167             String. Sets or gets the secret string to use for decrypting or encrypting the cookie jar. This is used in conjonction with L</file>, L</encrypt> and L</algo>
3168              
3169             =head2 set
3170              
3171             Given a cookie object, and an optional hash or hash reference of parameters, and this will add the cookie to the outgoing HTTP headers using the C<Set-Cookie> HTTP header. To do so, it uses the L<Apache2::RequestRec> value set in L</request>, if any, or a L<HTTP::Response> compatible response object provided with the C<response> parameter.
3172              
3173             $jar->set( $c, response => $http_response_object ) ||
3174             die( $jar->error );
3175              
3176             Ultimately if none of those two are provided it returns the C<Set-Cookie> header as a string.
3177              
3178             # Returns something like:
3179             # Set-Cookie: my-cookie=somevalue
3180             print( STDOUT $jar->set( $c ), "\015\012" );
3181              
3182             Unless the latter, this method returns the current object.
3183              
3184             =head2 type
3185              
3186             String. Sets or gets the cookie jar file format type. The supported formats are: C<json>, C<lwp> and C<netscape>
3187              
3188             =head1 IMPORTING COOKIES
3189              
3190             To import cookies, you can either use the methods L<scan|HTTP::Cookies/scan> from L<HTTP::Cookies>, such as:
3191              
3192             use Cookie::Jar;
3193             use HTTP::Cookies;
3194             my $jar = Cookie::Jar->new;
3195             my $old = HTTP::Cookies->new;
3196             $old->load( '/home/joe/old_cookies_file.txt' );
3197             my @keys = qw( version key val path domain port path_spec secure expires discard hash );
3198             $old->scan(sub
3199             {
3200             my @values = @_;
3201             my $ref = {};
3202             @$ref{ @keys } = @values;
3203             my $c = Cookie->new;
3204             $c->apply( $ref ) || die( $c->error );
3205             $jar->add( $c );
3206             });
3207             printf( "%d cookies now in our repository.\n", $jar->repo->length );
3208              
3209             or you could also load a cookie file. L<Cookie::Jar> supports L<LWP> format and old Netscape format:
3210              
3211             $jar->load_as_lwp( '/home/joe/lwp_cookies.txt' );
3212             $jar->load_as_netscape( '/home/joe/netscape_cookies.txt' );
3213              
3214             And of course, if you are using L<Cookie::Jar> json cookies file, you can import them with:
3215              
3216             $jar->load( '/home/joe/cookies.json' );
3217              
3218             =head1 ENCRYPTION
3219              
3220             This package supports encryption and decryption of cookies file, and also the cookies values themselve.
3221              
3222             See methods L</save> and L</load> for encryption options and the L<Cookie> package for options to encrypt or sign cookies value.
3223              
3224             =head1 INSTALLATION
3225              
3226             As usual, to install this module, you can do:
3227              
3228             perl Makefile.PL
3229             make
3230             make test
3231             sudo make install
3232              
3233             If you have Apache/modperl2 installed, this will also prepare the Makefile and run test under modperl.
3234              
3235             The Makefile.PL tries hard to find your Apache configuration, but you can give it a hand by specifying some command line parameters. See L<Apache::TestMM> for available parameters or you can type on the command line:
3236              
3237             perl -MApache::TestConfig -le 'Apache::TestConfig::usage()'
3238              
3239             For example:
3240              
3241             perl Makefile.PL -apxs /usr/bin/apxs -port 1234
3242             # which will also set the path to httpd_conf, otherwise
3243             perl Makefile.PL -httpd_conf /etc/apache2/apache2.conf
3244              
3245             # then
3246             make
3247             make test
3248             sudo make install
3249              
3250             See also L<modperl testing documentation|https://perl.apache.org/docs/general/testing/testing.html>
3251              
3252             But, if for some reason, you do not want to perform the mod_perl tests, you can use C<NO_MOD_PERL=1> when calling C<perl Makefile.PL>, such as:
3253              
3254             NO_MOD_PERL=1 perl Makefile.PL
3255             make
3256             make test
3257             sudo make install
3258              
3259             =head1 AUTHOR
3260              
3261             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
3262              
3263             =head1 SEE ALSO
3264              
3265             L<Cookie>, L<Cookie::Domain>, L<Apache2::Cookies>, L<APR::Request::Cookie>, L<Cookie::Baker>
3266              
3267             L<Latest tentative version of the cookie standard|https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-09>
3268              
3269             L<Mozilla documentation on Set-Cookie|https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie>
3270              
3271             L<Information on double submit cookies|https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie>
3272              
3273             =head1 COPYRIGHT & LICENSE
3274              
3275             Copyright (c) 2019-2019 DEGUEST Pte. Ltd.
3276              
3277             You can use, copy, modify and redistribute this package and associated
3278             files under the same terms as Perl itself.
3279              
3280             =cut