File Coverage

lib/Pcore/HTTP/Cookies.pm
Criterion Covered Total %
statement 108 130 83.0
branch 48 90 53.3
condition 7 12 58.3
subroutine 6 7 85.7
pod 0 4 0.0
total 169 243 69.5


line stmt bran cond sub pod time code
1             package Pcore::HTTP::Cookies;
2              
3 1     1   45341 use Pcore -class;
  1         2  
  1         9  
4              
5             has cookies => ( is => 'ro', isa => HashRef, default => sub { {} } );
6              
7 0     0 0 0 sub clear ($self) {
  0         0  
  0         0  
8 0         0 $self->{cookies} = {};
9              
10 0         0 return;
11             }
12              
13             # COOKIES LIMITATIONS:
14             # http://browsercookielimits.squawky.net/
15             #
16             # RFC 2965 - http://www.ietf.org/rfc/rfc2965.txt;
17             # To get a good understanding of cookies read this - http://www.quirksmode.org/js/cookies.html;
18             # Cookies are stored as a single string containing name, value, expiry etc.;
19             # Size limits apply to the entire cookie, not just its value;
20             # If you use characters only in the ASCII range, each character takes 1 byte, so you can typically store 4096 characters;
21             # In UTF-8 some characters are more than 1 byte, hence you can not store as many characters in the same amount of bytes;
22             # The ';' character is reserved as a separator. Do not use it in the key or value;
23             # jQuery Cookie plugin stores the cookie using encodeURIComponent. Hence ÿ is stored as %C3%BF, 6 characters. This works well, as other you would lose the ';' character;
24             # You cannot delete cookies with a key that hits the size limit and has a small value. The method to delete a cookie is to set its expiry value, but when the key is large there is not enough room left to do this. Hence I have not tested limitations around key size;
25             # It appears that some browsers limit by bytes, while others limit the number of characters;
26              
27 22     22 0 3374 sub parse_cookies ( $self, $url, $set_cookie_header ) {
  22         32  
  22         54  
  22         27  
  22         24  
28 22 50       408 $url = P->uri($url) if !ref $url;
29              
30 22         52 COOKIE: for ( $set_cookie_header->@* ) {
31 16         59 my ( $kvp, @attrs ) = split /;/sm;
32              
33 16 50       37 next if !defined $kvp;
34              
35             # trim
36 16         39 $kvp =~ s/\A\s+//sm;
37 16         38 $kvp =~ s/\s+\z//sm;
38              
39 16 50       38 next if $kvp eq q[];
40              
41 16         46 my $origin_domain_name = $url->host->name;
42              
43 16         305 my $cookie = {
44             domain => $origin_domain_name,
45             path => $url->path->to_string,
46             expires => 0,
47             httponly => 0,
48             secure => 0,
49             };
50              
51             # parse and set key and value
52 16 100       42 if ( ( my $idx = index $kvp, q[=] ) != -1 ) {
53 10         24 $cookie->{name} = substr $kvp, 0, $idx;
54              
55 10         25 $cookie->{val} = substr $kvp, $idx + 1;
56             }
57             else {
58 6         15 $cookie->{name} = $kvp;
59              
60 6         12 $cookie->{val} = q[];
61             }
62              
63             # parse attributes
64 16         28 for my $attr (@attrs) {
65              
66             # trim
67 20         40 $attr =~ s/\A\s+//sm;
68 20         41 $attr =~ s/\s+\z//sm;
69              
70 20 50       33 next if $attr eq q[];
71              
72 20         30 my ( $k, $v );
73              
74 20 50       36 if ( ( my $idx = index $attr, q[=] ) != -1 ) {
75 20         36 $k = lc substr $attr, 0, $idx;
76              
77 20         35 $v = substr $attr, $idx + 1;
78             }
79             else {
80 0         0 $k = lc $attr;
81              
82 0         0 $v = q[];
83             }
84              
85 20 100       46 if ( $k eq 'domain' ) {
    100          
    50          
    0          
    0          
    0          
86 16 100       30 if ( $v ne q[] ) {
87              
88             # http://bayou.io/draft/cookie.domain.html
89             # origin domain - domain from the request
90             # cover domain - domain from cookie attribute
91              
92             # if a cookie's origin domain is an IP, the cover domain must be null
93 14 50       239 next COOKIE if $url->host->is_ip;
94              
95             # parse cover domain
96 14         213 my $cover_domain = P->host($v);
97              
98             # a cover domain must not be a IP address
99 14 50       210 next COOKIE if $cover_domain->is_ip;
100              
101 14         37 my $cover_domain_name = $cover_domain->name;
102              
103             # if the origin domain is the same domain
104 14 100       32 if ( $cover_domain_name eq $origin_domain_name ) {
105              
106             # permit a public suffix domain to specify itself as the cover domain
107             # ignore cover domain, if cover domain is pub. suffix
108 7 100       111 next if $cover_domain->is_pub_suffix;
109             }
110             else {
111             # the cover domain must not be a TLD, a public suffix, or a parent of a public suffix
112 7 100       125 next COOKIE if $cover_domain->is_pub_suffix;
113              
114             # the cover domain must cover (be a parent) the origin domain
115 4 100       20 if ( ( my $idx = index $origin_domain_name, q[.] . $cover_domain_name ) > 0 ) {
116 2 50       9 next COOKIE if length($origin_domain_name) != 1 + $idx + length $cover_domain_name;
117             }
118             else {
119 2         13 next COOKIE;
120             }
121             }
122              
123             # accept cover domain cookie
124 6         57 $cookie->{domain} = q[.] . $cover_domain_name;
125             }
126             }
127             elsif ( $k eq 'path' ) {
128 3 50       9 if ( $v ne q[] ) {
129 0         0 $cookie->{path} = $v;
130             }
131             }
132             elsif ( $k eq 'expires' ) {
133 1 50       8 if ( $v ne q[] ) {
134 1 50       8 if ( !$cookie->{expires} ) { # do not process expires attribute, if expires is already set by expires or max-age
135 1 50       2 if ( my $expires = eval { P->date->parse($v) } ) {
  1         26  
136 1         19 $cookie->{expires} = $expires->epoch;
137             }
138             else {
139             # ignore cookie if expires value is invalid
140 0         0 next COOKIE;
141             }
142             }
143             }
144             }
145             elsif ( $k eq 'max-age' ) {
146 0 0       0 if ( $v ne q[] ) {
147 0 0       0 if ( $v =~ /\A\d+\z/sm ) {
148 0         0 $cookie->{expires} = time + $v;
149             }
150             else {
151             # ignore cookie if max-age value is invalid
152 0         0 next COOKIE;
153             }
154             }
155             }
156             elsif ( $k eq 'httponly' ) {
157 0         0 $cookie->{httponly} = 1;
158             }
159             elsif ( $k eq 'secure' ) {
160 0         0 $cookie->{secure} = 1;
161             }
162             }
163              
164 11 100 66     38 if ( $cookie->{expires} && $cookie->{expires} < time ) {
165 1         9 $self->remove_cookie( $cookie->{domain}, $cookie->{path}, $cookie->{name} );
166             }
167             else {
168 10         50 $self->{cookies}->{ $cookie->{domain} }->{ $cookie->{path} }->{ $cookie->{name} } = $cookie;
169             }
170             }
171              
172 22         124 return;
173             }
174              
175 12     12 0 137 sub get_cookies ( $self, $url ) {
  12         17  
  12         16  
  12         14  
176 12     12   11 state $match_path = sub ( $url_path, $cookie_path ) {
  12         15  
  12         17  
  12         17  
177 12 50       54 return 1 if $cookie_path eq $url_path;
178              
179 0 0       0 return 1 if $cookie_path eq q[/];
180              
181 0 0       0 if ( $url_path =~ /\A\Q$cookie_path\E(.*)/sm ) {
182 0         0 my $rest = $1;
183              
184 0 0       0 return 1 if substr( $cookie_path, -1, 1 ) eq q[/];
185              
186 0 0       0 return 1 if substr( $rest, 0, 1 ) eq q[/];
187             }
188              
189 0         0 return;
190 12         22 };
191              
192 12     12   13 state $match_domain = sub ( $self, $domain, $domain_cookies, $url ) {
  12         14  
  12         14  
  12         13  
  12         16  
  12         36  
193 12         13 my $cookies;
194              
195 12         18 my $time = time;
196              
197 12         33 for my $cookie_path ( keys $domain_cookies->%* ) {
198 12 50       30 if ( $match_path->( $url->path, $cookie_path ) ) {
199 12         53 for my $cookie ( values $domain_cookies->{$cookie_path}->%* ) {
200 14 50 33     36 if ( $cookie->{expires} && $cookie->{expires} < $time ) {
201              
202             # remove expired cookie
203 0         0 $self->remove_cookie( $domain, $cookie_path, $cookie->{name} );
204             }
205             else {
206 14 50 33     29 next if $cookie->{secure} && !$url->is_secure;
207              
208 14         47 push $cookies->@*, $cookie->{name} . q[=] . $cookie->{val};
209             }
210             }
211             }
212             }
213              
214 12         35 return $cookies;
215 12         21 };
216              
217 12 50       220 $url = P->uri($url) if !ref $url;
218              
219 12         16 my $cookies;
220              
221             # origin cookie
222 12         42 my $origin_domain_name = $url->host->name;
223              
224 12 100       34 if ( my $origin_cookies = $self->{cookies}->{$origin_domain_name} ) {
225 6 50       16 if ( my $match_cookies = $match_domain->( $self, $origin_domain_name, $origin_cookies, $url ) ) {
226 6         13 push $cookies->@*, $match_cookies->@*;
227             }
228             }
229              
230             # cover cookies
231             # http://bayou.io/draft/cookie.domain.html#Coverage_Model
232 12 50       238 if ( !$url->host->is_ip ) {
233 12         53 my @labels = split /[.]/sm, $url->host->name;
234              
235 12         18 my $origin = 1;
236              
237 12         26 while ( @labels > 1 ) {
238 25         433 my $domain = P->host( join q[.], @labels );
239              
240 25         64 my $cover_domain_name = q[.] . $domain->name;
241              
242 25 100       60 if ( my $cover_cookies = $self->{cookies}->{$cover_domain_name} ) {
243 6 50       15 if ( my $match_cookies = $match_domain->( $self, $cover_domain_name, $cover_cookies, $url ) ) {
244 6         13 push $cookies->@*, $match_cookies->@*;
245             }
246             }
247              
248 25 100 100     377 last if $domain->is_pub_suffix && !$origin;
249              
250 23         39 $origin = 0;
251              
252 23         76 shift @labels;
253             }
254             }
255              
256 12         56 return $cookies;
257             }
258              
259 1     1 0 3 sub remove_cookie ( $self, $domain, $path, $name ) {
  1         3  
  1         3  
  1         3  
  1         19  
  1         3  
260 1 50       12 if ( delete $self->{cookies}->{$domain}->{$path}->{$name} ) {
261 1 50       12 delete $self->{cookies}->{$domain}->{$path} if !keys $self->{cookies}->{$domain}->{$path}->%*;
262              
263 1 50       8 delete $self->{cookies}->{$domain} if !keys $self->{cookies}->{$domain}->%*;
264             }
265              
266 1         267 return;
267             }
268              
269             1;
270             ## -----SOURCE FILTER LOG BEGIN-----
271             ##
272             ## PerlCritic profile "pcore-script" policy violations:
273             ## +------+----------------------+----------------------------------------------------------------------------------------------------------------+
274             ## | Sev. | Lines | Policy |
275             ## |======+======================+================================================================================================================|
276             ## | 3 | | Subroutines::ProhibitExcessComplexity |
277             ## | | 27 | * Subroutine "parse_cookies" with high complexity score (38) |
278             ## | | 175 | * Subroutine "get_cookies" with high complexity score (23) |
279             ## |------+----------------------+----------------------------------------------------------------------------------------------------------------|
280             ## | 3 | 115, 135 | ControlStructures::ProhibitDeepNests - Code structure is deeply nested |
281             ## +------+----------------------+----------------------------------------------------------------------------------------------------------------+
282             ##
283             ## -----SOURCE FILTER LOG END-----
284             __END__
285             =pod
286              
287             =encoding utf8
288              
289             =head1 NAME
290              
291             Pcore::HTTP::Cookies
292              
293             =head1 SYNOPSIS
294              
295             =head1 DESCRIPTION
296              
297             =cut