From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

use Mojo::Base -base;
use Mojo::File qw(path);
use Scalar::Util qw(looks_like_number);
has [qw(file ignore)];
has max_cookie_size => 4096;
my $COMMENT = "# Netscape HTTP Cookie File\n# This file was generated by Mojolicious! Edit at your own risk.\n\n";
sub add {
my ($self, @cookies) = @_;
my $size = $self->max_cookie_size;
for my $cookie (@cookies) {
# Convert max age to expires
my $age = $cookie->max_age;
$cookie->expires($age <= 0 ? 0 : $age + time) if looks_like_number $age;
# Check cookie size
next if length($cookie->value // '') > $size;
# Replace cookie
next unless my $domain = lc($cookie->domain // '');
next unless my $path = $cookie->path;
next unless length(my $name = $cookie->name // '');
my $jar = $self->{jar}{$domain} //= [];
@$jar = (grep({ _compare($_, $path, $name, $domain) } @$jar), $cookie);
}
return $self;
}
sub all {
my $jar = shift->{jar};
return [map { @{$jar->{$_}} } sort keys %$jar];
}
sub collect {
my ($self, $tx) = @_;
my $url = $tx->req->url;
for my $cookie (@{$tx->res->cookies}) {
# Validate domain
my $host = lc $url->ihost;
$cookie->domain($host)->host_only(1) unless $cookie->domain;
my $domain = lc $cookie->domain;
if (my $cb = $self->ignore) { next if $cb->($cookie) }
next if $host ne $domain && ($host !~ /\Q.$domain\E$/ || $host =~ /\.\d+$/);
# Validate path
my $path = $cookie->path // $url->path->to_dir->to_abs_string;
$path = Mojo::Path->new($path)->trailing_slash(0)->to_abs_string;
next unless _path($path, $url->path->to_abs_string);
$self->add($cookie->path($path));
}
}
sub empty {
my $self = shift;
delete $self->{jar};
return $self;
}
sub find {
my ($self, $url) = @_;
my @found;
my $domain = my $host = lc $url->ihost;
my $path = $url->path->to_abs_string;
while ($domain) {
next unless my $old = $self->{jar}{$domain};
# Grab cookies
my $new = $self->{jar}{$domain} = [];
for my $cookie (@$old) {
next if $cookie->host_only && $host ne $cookie->domain;
# Check if cookie has expired
if (defined(my $expires = $cookie->expires)) { next if time > $expires }
push @$new, $cookie;
# Taste cookie
next if $cookie->secure && $url->protocol ne 'https';
next unless _path($cookie->path, $path);
my $name = $cookie->name;
my $value = $cookie->value;
push @found, Mojo::Cookie::Request->new(name => $name, value => $value);
}
}
# Remove another part
continue { $domain =~ s/^[^.]*\.*// }
return \@found;
}
sub load {
my $self = shift;
my $file = $self->file;
return $self unless $file && -r $file;
for my $line (split "\n", path($file)->slurp) {
# Prefix used by curl for HttpOnly cookies
my $httponly = $line =~ s/^#HttpOnly_// ? 1 : 0;
next if $line =~ /^#/;
my @values = split "\t", $line;
next if @values != 7;
$self->add(Mojo::Cookie::Response->new({
domain => $values[0] =~ s/^\.//r,
host_only => $values[1] eq 'FALSE' ? 1 : 0,
path => $values[2],
secure => $values[3] eq 'FALSE' ? 0 : 1,
expires => $values[4] eq '0' ? undef : $values[4],
name => $values[5],
value => $values[6],
httponly => $httponly
}));
}
return $self;
}
sub prepare {
my ($self, $tx) = @_;
return unless keys %{$self->{jar}};
my $req = $tx->req;
$req->cookies(@{$self->find($req->url)});
}
sub save {
my $self = shift;
return $self unless my $file = $self->file;
my $final = path($file);
my $tmp = path("$file.$$");
$tmp->spew($COMMENT . $self->to_string)->move_to($final);
return $self;
}
sub to_string {
my $self = shift;
my @lines;
for my $cookie (@{$self->all}) {
my $line = [
$cookie->domain, $cookie->host_only ? 'FALSE' : 'TRUE',
$cookie->path, $cookie->secure ? 'TRUE' : 'FALSE',
$cookie->expires // 0, $cookie->name,
$cookie->value
];
push @lines, join "\t", @$line;
}
return join "\n", @lines, '';
}
sub _compare {
my ($cookie, $path, $name, $domain) = @_;
return $cookie->path ne $path || $cookie->name ne $name || $cookie->domain ne $domain;
}
sub _path { $_[0] eq '/' || $_[0] eq $_[1] || index($_[1], "$_[0]/") == 0 }
1;
=encoding utf8
=head1 NAME
Mojo::UserAgent::CookieJar - Cookie jar for HTTP user agents
=head1 SYNOPSIS
use Mojo::UserAgent::CookieJar;
# Add response cookies
my $jar = Mojo::UserAgent::CookieJar->new;
$jar->add(
Mojo::Cookie::Response->new(
name => 'foo',
value => 'bar',
domain => 'localhost',
path => '/test'
)
);
# Find request cookies
for my $cookie (@{$jar->find(Mojo::URL->new('http://localhost/test'))}) {
say $cookie->name;
say $cookie->value;
}
=head1 DESCRIPTION
L<Mojo::UserAgent::CookieJar> is a minimalistic and relaxed cookie jar used by L<Mojo::UserAgent>, based on L<RFC
=head1 ATTRIBUTES
L<Mojo::UserAgent::CookieJar> implements the following attributes.
=head2 file
my $file = $jar->file;
$jar = $jar->file('/home/sri/cookies.txt');
File to L</"load"> cookies from and L</"save"> cookies to in Netscape format. Note that this attribute is
B<EXPERIMENTAL> and might change without warning!
# Save cookies to file
$jar->file('cookies.txt')->save;
# Empty cookie jar and load cookies from file
$jar->file('cookies.txt')->empty->load;
=head2 ignore
my $ignore = $jar->ignore;
$jar = $jar->ignore(sub {...});
A callback used to decide if a cookie should be ignored by L</"collect">.
# Ignore all cookies
$jar->ignore(sub { 1 });
# Ignore cookies for domains "com", "net" and "org"
$jar->ignore(sub ($cookie) {
return undef unless my $domain = $cookie->domain;
return $domain eq 'com' || $domain eq 'net' || $domain eq 'org';
});
=head2 max_cookie_size
my $size = $jar->max_cookie_size;
$jar = $jar->max_cookie_size(4096);
Maximum cookie size in bytes, defaults to C<4096> (4KiB).
=head1 METHODS
L<Mojo::UserAgent::CookieJar> inherits all methods from L<Mojo::Base> and implements the following new ones.
=head2 add
$jar = $jar->add(@cookies);
Add multiple L<Mojo::Cookie::Response> objects to the jar.
=head2 all
my $cookies = $jar->all;
Return all L<Mojo::Cookie::Response> objects that are currently stored in the jar.
# Names of all cookies
say $_->name for @{$jar->all};
=head2 collect
$jar->collect(Mojo::Transaction::HTTP->new);
Collect response cookies from transaction.
=head2 empty
$jar = $jar->empty;
Empty the jar.
=head2 find
my $cookies = $jar->find(Mojo::URL->new);
Find L<Mojo::Cookie::Request> objects in the jar for L<Mojo::URL> object.
# Names of all cookies found
say $_->name for @{$jar->find(Mojo::URL->new('http://example.com/foo'))};
=head2 load
$jar = $jar->load;
Load cookies from L</"file">. Note that this method is B<EXPERIMENTAL> and might change without warning!
=head2 prepare
$jar->prepare(Mojo::Transaction::HTTP->new);
Prepare request cookies for transaction.
=head2 save
$jar = $jar->save;
Save cookies to L</"file">. Note that this method is B<EXPERIMENTAL> and might change without warning!
=head2 to_string
my $string = $jar->to_string;
Stringify cookies in Netscape format. Note that this method is B<EXPERIMENTAL> and might change without warning!
=head1 SEE ALSO
L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.
=cut