File Coverage

blib/lib/Cookie/Jar.pm
Criterion Covered Total %
statement 511 1133 45.1
branch 177 676 26.1
condition 121 597 20.2
subroutine 49 73 67.1
pod 41 41 100.0
total 899 2520 35.6


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