File Coverage

blib/lib/PAGI/Middleware/Cookie.pm
Criterion Covered Total %
statement 68 75 90.6
branch 12 22 54.5
condition 10 16 62.5
subroutine 15 16 93.7
pod 1 1 100.0
total 106 130 81.5


line stmt bran cond sub pod time code
1             package PAGI::Middleware::Cookie;
2             $PAGI::Middleware::Cookie::VERSION = '0.002000';
3 1     1   407662 use strict;
  1         4  
  1         86  
4 1     1   7 use warnings;
  1         3  
  1         116  
5 1     1   620 use parent 'PAGI::Middleware';
  1         522  
  1         7  
6 1     1   110 use Future::AsyncAwait;
  1         2  
  1         7  
7 1     1   591 use Cookie::Baker ();
  1         6863  
  1         1597  
8              
9             =head1 NAME
10              
11             PAGI::Middleware::Cookie - Cookie parsing middleware
12              
13             =head1 SYNOPSIS
14              
15             use PAGI::Middleware::Builder;
16              
17             my $app = builder {
18             enable 'Cookie';
19             $my_app;
20             };
21              
22             # In your app:
23             async sub app {
24             my ($scope, $receive, $send) = @_;
25              
26             my $cookies = $scope->{'pagi.cookies'};
27             my $session_id = $cookies->{session_id};
28             }
29              
30             =head1 DESCRIPTION
31              
32             PAGI::Middleware::Cookie parses the Cookie header and makes the parsed
33             cookies available in C<< $scope->{'pagi.cookies'} >> as a hashref.
34              
35             It also provides a helper for setting response cookies.
36              
37             =head1 CONFIGURATION
38              
39             =over 4
40              
41             =item * secret (optional)
42              
43             Secret key for signed cookies. Required for C/C.
44              
45             =back
46              
47             =cut
48              
49             sub _init {
50 2     2   6 my ($self, $config) = @_;
51              
52 2         10 $self->{secret} = $config->{secret};
53             }
54              
55             sub wrap {
56 2     2 1 26 my ($self, $app) = @_;
57              
58 2     2   68 return async sub {
59 2         5 my ($scope, $receive, $send) = @_;
60 2 50       10 if ($scope->{type} ne 'http') {
61 0         0 await $app->($scope, $receive, $send);
62 0         0 return;
63             }
64              
65             # Parse cookies from Cookie header
66 2   100     27 my $cookie_header = $self->_get_header($scope, 'cookie') // '';
67 2         8 my $cookies = $self->_parse_cookies($cookie_header);
68              
69             # Create cookie jar for setting response cookies
70 2         62 my @response_cookies;
71             my $cookie_jar = PAGI::Middleware::Cookie::Jar->new(
72             \@response_cookies,
73 1         6 sub { $self->_format_set_cookie(@_) },
74 2         28 );
75              
76             # Add cookies and jar to scope
77 2         16 my $new_scope = $self->modify_scope($scope, {
78             'pagi.cookies' => $cookies,
79             'pagi.cookie_jar' => $cookie_jar,
80             });
81              
82             # Wrap send to add Set-Cookie headers
83 4         486 my $wrapped_send = async sub {
84 4         10 my ($event) = @_;
85 4 100 100     22 if ($event->{type} eq 'http.response.start' && @response_cookies) {
86 1   50     4 my @headers = @{$event->{headers} // []};
  1         5  
87 1         3 for my $cookie (@response_cookies) {
88 1         6 push @headers, ['Set-Cookie', $cookie];
89             }
90             await $send->({
91             %$event,
92             headers => \@headers,
93 1         6 });
94             } else {
95 3         8 await $send->($event);
96             }
97 2         17 };
98              
99 2         8 await $app->($new_scope, $receive, $wrapped_send);
100 2         14 };
101             }
102              
103             sub _parse_cookies {
104 2     2   5 my ($self, $header) = @_;
105              
106 2 100 66     12 return {} unless defined $header && length $header;
107 1         5 return Cookie::Baker::crush_cookie($header);
108             }
109              
110             sub _format_set_cookie {
111 1     1   4 my ($self, $name, $value, %opts) = @_;
112              
113             my %cookie_opts = (
114             value => $value,
115 1   50     11 path => $opts{path} // '/',
116             );
117 1 50       4 $cookie_opts{domain} = $opts{domain} if defined $opts{domain};
118 1 50       8 $cookie_opts{expires} = $opts{expires} if defined $opts{expires};
119 1 50       5 $cookie_opts{'max-age'} = $opts{max_age} if defined $opts{max_age};
120 1 50       6 $cookie_opts{secure} = $opts{secure} if $opts{secure};
121 1 50       30 $cookie_opts{httponly} = $opts{httponly} if $opts{httponly};
122 1 50       7 $cookie_opts{samesite} = $opts{samesite} if defined $opts{samesite};
123 1         7 return Cookie::Baker::bake_cookie($name, \%cookie_opts);
124             }
125              
126             sub _get_header {
127 2     2   6 my ($self, $scope, $name) = @_;
128              
129 2         7 $name = lc($name);
130 2   50     3 for my $h (@{$scope->{headers} // []}) {
  2         9  
131 1 50       33 return $h->[1] if lc($h->[0]) eq $name;
132             }
133 1         7 return;
134             }
135              
136             # Simple cookie jar class for method-style access
137             package PAGI::Middleware::Cookie::Jar;
138             $PAGI::Middleware::Cookie::Jar::VERSION = '0.002000';
139 1     1   14 use strict;
  1         3  
  1         37  
140 1     1   5 use warnings;
  1         2  
  1         579  
141              
142             sub new {
143 2     2   6 my ($class, $cookies_ref, $formatter) = @_;
144              
145 2         9 return bless {
146             cookies => $cookies_ref,
147             formatter => $formatter,
148             }, $class;
149             }
150              
151             sub set {
152 1     1   16 my ($self, $name, $value, %opts) = @_;
153              
154 1         4 push @{$self->{cookies}}, $self->{formatter}->($name, $value, %opts);
  1         13  
155             }
156              
157             sub delete {
158 0     0     my ($self, $name, %opts) = @_;
159              
160             my %cookie_opts = (
161             value => '',
162             expires => 0, # Epoch 0 = expired
163 0   0       path => $opts{path} // '/',
164             );
165 0 0         $cookie_opts{domain} = $opts{domain} if defined $opts{domain};
166 0           push @{$self->{cookies}}, Cookie::Baker::bake_cookie($name, \%cookie_opts);
  0            
167             }
168              
169             package PAGI::Middleware::Cookie;
170              
171             1;
172              
173             __END__