File Coverage

blib/lib/Mojo/UserAgent/CookieJar.pm
Criterion Covered Total %
statement 100 100 100.0
branch 58 58 100.0
condition 32 34 94.1
subroutine 16 16 100.0
pod 9 9 100.0
total 215 217 99.0


line stmt bran cond sub pod time code
1             package Mojo::UserAgent::CookieJar;
2 58     58   1017 use Mojo::Base -base;
  58         134  
  58         434  
3              
4 58     58   24426 use Mojo::Cookie::Request;
  58         231  
  58         629  
5 58     58   456 use Mojo::File qw(path);
  58         141  
  58         3612  
6 58     58   24806 use Mojo::Path;
  58         249  
  58         519  
7 58     58   508 use Scalar::Util qw(looks_like_number);
  58         136  
  58         149976  
8              
9             has [qw(file ignore)];
10             has max_cookie_size => 4096;
11              
12             my $COMMENT = "# Netscape HTTP Cookie File\n# This file was generated by Mojolicious! Edit at your own risk.\n\n";
13              
14             sub add {
15 150     150 1 414 my ($self, @cookies) = @_;
16              
17 150         555 my $size = $self->max_cookie_size;
18 150         394 for my $cookie (@cookies) {
19              
20             # Convert max age to expires
21 158         431 my $age = $cookie->max_age;
22 158 100       647 $cookie->expires($age <= 0 ? 0 : $age + time) if looks_like_number $age;
    100          
23              
24             # Check cookie size
25 158 100 100     483 next if length($cookie->value // '') > $size;
26              
27             # Replace cookie
28 157 100 100     457 next unless my $domain = lc($cookie->domain // '');
29 156 100       369 next unless my $path = $cookie->path;
30 154 100 100     404 next unless length(my $name = $cookie->name // '');
31 153   100     718 my $jar = $self->{jar}{$domain} //= [];
32 153         456 @$jar = (grep({ _compare($_, $path, $name, $domain) } @$jar), $cookie);
  461         876  
33             }
34              
35 150         523 return $self;
36             }
37              
38             sub all {
39 27     27 1 93 my $jar = shift->{jar};
40 27         196 return [map { @{$jar->{$_}} } sort keys %$jar];
  21         33  
  21         171  
41             }
42              
43             sub collect {
44 1031     1031 1 3059 my ($self, $tx) = @_;
45              
46 1031         3480 my $url = $tx->req->url;
47 1031         2307 for my $cookie (@{$tx->res->cookies}) {
  1031         3275  
48              
49             # Validate domain
50 144         651 my $host = lc $url->ihost;
51 144 100       488 $cookie->domain($host)->host_only(1) unless $cookie->domain;
52 144         398 my $domain = lc $cookie->domain;
53 144 100       545 if (my $cb = $self->ignore) { next if $cb->($cookie) }
  50 100       143  
54 135 100 100     853 next if $host ne $domain && ($host !~ /\Q.$domain\E$/ || $host =~ /\.\d+$/);
      100        
55              
56             # Validate path
57 128   66     373 my $path = $cookie->path // $url->path->to_dir->to_abs_string;
58 128         606 $path = Mojo::Path->new($path)->trailing_slash(0)->to_abs_string;
59 128 100       3778 next unless _path($path, $url->path->to_abs_string);
60 125         508 $self->add($cookie->path($path));
61             }
62             }
63              
64             sub empty {
65 7     7 1 19 my $self = shift;
66 7         74 delete $self->{jar};
67 7         25 return $self;
68             }
69              
70             sub find {
71 198     198 1 533 my ($self, $url) = @_;
72              
73 198         425 my @found;
74 198         646 my $domain = my $host = lc $url->ihost;
75 198         669 my $path = $url->path->to_abs_string;
76 198         745 while ($domain) {
77 734 100       2351 next unless my $old = $self->{jar}{$domain};
78              
79             # Grab cookies
80 201         631 my $new = $self->{jar}{$domain} = [];
81 201         627 for my $cookie (@$old) {
82 502 100 100     1833 next if $cookie->host_only && $host ne $cookie->domain;
83              
84             # Check if cookie has expired
85 501 100       1505 if (defined(my $expires = $cookie->expires)) { next if time > $expires }
  275 100       835  
86 497         2178 push @$new, $cookie;
87              
88             # Taste cookie
89 497 100 100     1342 next if $cookie->secure && $url->protocol ne 'https';
90 496 100       1348 next unless _path($cookie->path, $path);
91 362         1253 my $name = $cookie->name;
92 362         1093 my $value = $cookie->value;
93 362         1632 push @found, Mojo::Cookie::Request->new(name => $name, value => $value);
94             }
95             }
96              
97             # Remove another part
98 734         3074 continue { $domain =~ s/^[^.]*\.*// }
99              
100 198         1335 return \@found;
101             }
102              
103             sub load {
104 6     6 1 22 my $self = shift;
105              
106 6         36 my $file = $self->file;
107 6 100 100     212 return $self unless $file && -r $file;
108              
109 4         22 for my $line (split "\n", path($file)->slurp) {
110              
111             # Prefix used by curl for HttpOnly cookies
112 20 100       46 my $httponly = $line =~ s/^#HttpOnly_// ? 1 : 0;
113 20 100       48 next if $line =~ /^#/;
114              
115 11         56 my @values = split "\t", $line;
116 11 100       26 next if @values != 7;
117              
118 8 100       117 $self->add(Mojo::Cookie::Response->new({
    100          
    100          
119             domain => $values[0] =~ s/^\.//r,
120             host_only => $values[1] eq 'FALSE' ? 1 : 0,
121             path => $values[2],
122             secure => $values[3] eq 'FALSE' ? 0 : 1,
123             expires => $values[4] eq '0' ? undef : $values[4],
124             name => $values[5],
125             value => $values[6],
126             httponly => $httponly
127             }));
128             }
129              
130 4         24 return $self;
131             }
132              
133             sub prepare {
134 1037     1037 1 3152 my ($self, $tx) = @_;
135 1037 100       1972 return unless keys %{$self->{jar}};
  1037         9508  
136 177         624 my $req = $tx->req;
137 177         396 $req->cookies(@{$self->find($req->url)});
  177         577  
138             }
139              
140             sub save {
141 4     4 1 16 my $self = shift;
142 4 100       17 return $self unless my $file = $self->file;
143              
144 3         18 my $final = path($file);
145 3         26 my $tmp = path("$file.$$");
146 3         288 $tmp->spew($COMMENT . $self->to_string)->move_to($final);
147              
148 3         13 return $self;
149             }
150              
151             sub to_string {
152 5     5 1 13 my $self = shift;
153              
154 5         9 my @lines;
155 5         7 for my $cookie (@{$self->all}) {
  5         17  
156 5 100 100     13 my $line = [
    100          
157             $cookie->domain, $cookie->host_only ? 'FALSE' : 'TRUE',
158             $cookie->path, $cookie->secure ? 'TRUE' : 'FALSE',
159             $cookie->expires // 0, $cookie->name,
160             $cookie->value
161             ];
162 5         26 push @lines, join "\t", @$line;
163             }
164              
165 5         32 return join "\n", @lines, '';
166             }
167              
168             sub _compare {
169 461     461   870 my ($cookie, $path, $name, $domain) = @_;
170 461   66     862 return $cookie->path ne $path || $cookie->name ne $name || $cookie->domain ne $domain;
171             }
172              
173 624 100 100 624   3484 sub _path { $_[0] eq '/' || $_[0] eq $_[1] || index($_[1], "$_[0]/") == 0 }
174              
175             1;
176              
177             =encoding utf8
178              
179             =head1 NAME
180              
181             Mojo::UserAgent::CookieJar - Cookie jar for HTTP user agents
182              
183             =head1 SYNOPSIS
184              
185             use Mojo::UserAgent::CookieJar;
186              
187             # Add response cookies
188             my $jar = Mojo::UserAgent::CookieJar->new;
189             $jar->add(
190             Mojo::Cookie::Response->new(
191             name => 'foo',
192             value => 'bar',
193             domain => 'localhost',
194             path => '/test'
195             )
196             );
197              
198             # Find request cookies
199             for my $cookie (@{$jar->find(Mojo::URL->new('http://localhost/test'))}) {
200             say $cookie->name;
201             say $cookie->value;
202             }
203              
204             =head1 DESCRIPTION
205              
206             L is a minimalistic and relaxed cookie jar used by L, based on L
207             6265|https://tools.ietf.org/html/rfc6265>.
208              
209             =head1 ATTRIBUTES
210              
211             L implements the following attributes.
212              
213             =head2 file
214              
215             my $file = $jar->file;
216             $jar = $jar->file('/home/sri/cookies.txt');
217              
218             File to L cookies from and L cookies to in Netscape format.
219              
220             # Save cookies to file
221             $jar->file('cookies.txt')->save;
222              
223             # Empty cookie jar and load cookies from file
224             $jar->file('cookies.txt')->empty->load;
225              
226             =head2 ignore
227              
228             my $ignore = $jar->ignore;
229             $jar = $jar->ignore(sub {...});
230              
231             A callback used to decide if a cookie should be ignored by L.
232              
233             # Ignore all cookies
234             $jar->ignore(sub { 1 });
235              
236             # Ignore cookies for domains "com", "net" and "org"
237             $jar->ignore(sub ($cookie) {
238             return undef unless my $domain = $cookie->domain;
239             return $domain eq 'com' || $domain eq 'net' || $domain eq 'org';
240             });
241              
242             =head2 max_cookie_size
243              
244             my $size = $jar->max_cookie_size;
245             $jar = $jar->max_cookie_size(4096);
246              
247             Maximum cookie size in bytes, defaults to C<4096> (4KiB).
248              
249             =head1 METHODS
250              
251             L inherits all methods from L and implements the following new ones.
252              
253             =head2 add
254              
255             $jar = $jar->add(@cookies);
256              
257             Add multiple L objects to the jar.
258              
259             =head2 all
260              
261             my $cookies = $jar->all;
262              
263             Return all L objects that are currently stored in the jar.
264              
265             # Names of all cookies
266             say $_->name for @{$jar->all};
267              
268             =head2 collect
269              
270             $jar->collect(Mojo::Transaction::HTTP->new);
271              
272             Collect response cookies from transaction.
273              
274             =head2 empty
275              
276             $jar = $jar->empty;
277              
278             Empty the jar.
279              
280             =head2 find
281              
282             my $cookies = $jar->find(Mojo::URL->new);
283              
284             Find L objects in the jar for L object.
285              
286             # Names of all cookies found
287             say $_->name for @{$jar->find(Mojo::URL->new('http://example.com/foo'))};
288              
289             =head2 load
290              
291             $jar = $jar->load;
292              
293             Load cookies from L.
294              
295             =head2 prepare
296              
297             $jar->prepare(Mojo::Transaction::HTTP->new);
298              
299             Prepare request cookies for transaction.
300              
301             =head2 save
302              
303             $jar = $jar->save;
304              
305             Save cookies to L.
306              
307             =head2 to_string
308              
309             my $string = $jar->to_string;
310              
311             Stringify cookies in Netscape format.
312              
313              
314             =head1 SEE ALSO
315              
316             L, L, L.
317              
318             =cut