File Coverage

blib/lib/Dancer2/Plugin/ConditionalCaching.pm
Criterion Covered Total %
statement 46 144 31.9
branch 6 104 5.7
condition 14 66 21.2
subroutine 9 12 75.0
pod n/a
total 75 326 23.0


line stmt bran cond sub pod time code
1 3     3   1274359 use strictures 2;
  3         18  
  3         125  
2              
3             package Dancer2::Plugin::ConditionalCaching;
4              
5             # ABSTRACT: RFC7234 Caching
6              
7 3     3   1781 use Dancer2::Plugin;
  3         6103  
  3         17  
8 3     3   1363 use HTTP::Headers::Fancy;
  3         3  
  3         67  
9 3     3   12 use HTTP::Date qw(time2str str2time);
  3         4  
  3         145  
10 3     3   1325 use HTTP::Exception;
  3         1542  
  3         16  
11 3     3   126836 use Scalar::Util qw(blessed);
  3         7  
  3         196  
12 3     3   15 use Time::HiRes qw(time);
  3         5  
  3         21  
13 3     3   1841 use Math::Round qw(round);
  3         2576  
  3         3505  
14              
15             our $VERSION = '0.001'; # VERSION
16              
17             sub _instanceof {
18 0     0   0 my ( $obj, @cls ) = @_;
19 0 0       0 return 0 unless blessed $obj;
20 0         0 foreach my $cls (@cls) {
21 0 0       0 $cls = blessed $cls if blessed $cls;
22 0 0       0 return 1 if $obj->isa($cls);
23             }
24 0         0 return 0;
25             }
26              
27             sub _cmp_weak {
28 0     0   0 my ( $a, $b, $w ) = @_;
29 0 0       0 return unless defined $a;
30 0 0       0 return unless defined $b;
31 0   0     0 $w //= 0;
32 0 0       0 if ($w) {
33 0 0       0 if ( ref $b ) {
34 0         0 return $a eq $$b;
35             }
36             else {
37 0         0 return $a eq $b;
38             }
39             }
40             else {
41 0 0       0 return 0 if ref $b;
42 0         0 return $a eq $b;
43             }
44             }
45              
46             register caching => sub {
47 16     16   385017 my $dsl = shift;
48 16         77 my %args = @_;
49              
50 16         97 my $get_or_head = $dsl->request->method =~ m{^(?:get|head)$}i;
51              
52 16   50     605 my $dry = delete $args{dry} // 0;
53              
54 16   50     69 my $force = delete $args{force} // 0;
55 16   50     65 my $throw = delete $args{throw} // 0;
56 16   50     64 my $check = delete $args{check} // 1;
57              
58 16         32 my $etag = delete $args{etag};
59 16         35 my $weak = !!delete $args{weak};
60              
61 16         30 my $changed = delete $args{changed};
62 16         23 my $expires = delete $args{expires};
63              
64 16   100     59 my $no_cache = ( ( delete $args{cache} // 1 ) == 0 );
65 16   100     60 my $no_store = ( ( delete $args{store} // 1 ) == 0 );
66              
67 16   100     59 my $private = ( delete $args{private} // 0 );
68 16   100     56 my $public = ( delete $args{public} // 0 );
69              
70 16 50 66     43 if ( $private and $public ) {
71 0         0 warn "Cache-Control: private and public are mutually exclusive";
72             }
73 16 100       70 if ($no_cache) {
74 1 50       3 if ($no_store) {
75 0         0 warn "Cache-Control: no-cache and no-store are mutually exclusive";
76             }
77 1 50       3 if ($private) {
78 0         0 warn "Cache-Control: no-cache and private are useless together";
79             }
80 1 50       3 if ($private) {
81 0         0 warn "Cache-Control: no-cache and private are useless together";
82             }
83             }
84              
85 16         49 my %reqh = HTTP::Headers::Fancy::decode_hash( $dsl->request->headers );
86              
87 16         4321 my %req_cc = HTTP::Headers::Fancy::split_field_hash( $reqh{CacheControl} );
88              
89 0           my ( $age, $fresh );
90              
91 0 0         $age = time - $changed if defined $changed;
92 0 0         $fresh = $expires - time if defined $expires;
93              
94 0 0 0       if ( defined $age and $age < 0 ) {
95 0           warn("Age: 'changed' points to a timestamp in the future");
96 0           $age = 0;
97             }
98              
99 0           my %resp_cc = (
100             ( NoCache => undef ) x !!$no_cache,
101             ( NoStore => undef ) x !!$no_store,
102             ( Private => undef ) x !!$private,
103             ( Public => undef ) x !!$public,
104             MustRevalidate => undef,
105             NoTransform => undef,
106             );
107              
108 0 0         if ($check) {
109 0 0 0       if ( $req_cc{MaxAge} and defined $age ) {
110 0 0         if ( $age > $req_cc{MaxAge} ) {
111 0           $force = 1;
112             }
113             }
114 0 0 0       if ( $req_cc{MinFresh} and defined $fresh ) {
115 0 0         if ( $fresh < $req_cc{MinFresh} ) {
116 0           $force = 1;
117             }
118             }
119             }
120              
121             my $builder = sub {
122 0 0   0     return unless defined $args{builder};
123 0           my $sub;
124 0 0         if ( ref $args{builder} eq 'CODE' ) {
125 0           $sub = delete $args{builder};
126             }
127             else {
128 0           my $data = delete $args{builder};
129 0           $sub = sub { return $data };
  0            
130             }
131 0           my %subargs = ( %req_cc, Force => $force, );
132 0           return $sub->(%subargs);
133 0           };
134              
135 0           my %resph;
136              
137 0 0         if ( keys %resp_cc ) {
138 0           $resph{CacheControl} = HTTP::Headers::Fancy::build_field_hash(%resp_cc);
139             }
140 0 0         if ( defined $age ) {
141 0           $resph{Age} = round($age);
142             }
143 0 0         if ( defined $expires ) {
144 0           $resph{Expires} = time2str( round($expires) );
145             }
146 0 0         if ( defined $changed ) {
147 0           $resph{LastModified} = time2str( round($changed) );
148             }
149 0 0         if ( defined $etag ) {
150 0 0         if ($weak) {
151 0           $resph{Etag} = 'W/"' . $etag . '"';
152             }
153             else {
154 0           $resph{Etag} = '"' . $etag . '"';
155             }
156             }
157              
158 0 0 0       if ( $get_or_head and !$dry ) {
159 0           $dsl->response->header( HTTP::Headers::Fancy::encode_hash(%resph) );
160             }
161              
162 0 0         unless ($check) {
163 0           return $builder->();
164             }
165              
166 0   0       my $if_match = ( exists $reqh{IfMatch} and defined $reqh{IfMatch} );
167 0   0       my $if_match_any =
168             ( $if_match and ( $reqh{IfMatch} =~ qr{^ \s* \* \s* $}xsi ) );
169 0 0         my @if_match =
170             $if_match_any
171             ? ()
172             : HTTP::Headers::Fancy::split_field_list( $reqh{IfMatch} );
173              
174 0   0       my $if_none_match =
175             ( exists $reqh{IfNoneMatch} and defined $reqh{IfNoneMatch} );
176 0   0       my $if_none_match_any =
177             ( $if_none_match and ( $reqh{IfNoneMatch} =~ qr{^ \s* \* \s* $}xsi ) );
178 0 0         my @if_none_match =
179             $if_none_match_any
180             ? ()
181             : HTTP::Headers::Fancy::split_field_list( $reqh{IfNoneMatch} );
182              
183 0           my $if_modified_since = str2time( $reqh{IfModifiedSince} );
184 0           my $if_unmodified_since = str2time( $reqh{IfUnmodifiedSince} );
185              
186 0 0 0       if ($if_match) {
    0          
187 0   0       my $xa = ( !!$if_match_any and !!$etag );
188 0           my $xb = scalar grep { _cmp_weak( $etag, $_, $weak ) } @if_match;
  0            
189 0 0 0       unless ( $xa or $xb ) {
190 0 0         HTTP::Exception->throw(412) if $throw;
191 0 0         return 412 if $dry;
192 0           $dsl->send_error( 'Precondition Failed', 412 );
193             }
194             }
195             elsif ( $if_unmodified_since and defined $changed ) {
196 0 0         unless ( $if_unmodified_since > $changed ) {
197 0 0         HTTP::Exception->throw(412) if $throw;
198 0 0         return 412 if $dry;
199 0           $dsl->send_error( 'Precondition Failed', 412 );
200             }
201             }
202 0 0 0       if ($if_none_match) {
    0 0        
203 0   0       my $xa = ( !!$if_none_match_any and !!$etag );
204 0           my $xb = scalar grep { _cmp_weak( $etag, $_, $weak ) } @if_none_match;
  0            
205 0 0 0       if ( $xa or $xb ) {
206 0 0         if ($get_or_head) {
207 0 0         HTTP::Exception->throw(304) if $throw;
208 0 0         return 304 if $dry;
209 0           $dsl->send_error( 'Not Modfied', 304 );
210             }
211             else {
212 0 0         HTTP::Exception->throw(412) if $throw;
213 0 0         return 412 if $dry;
214 0           $dsl->send_error( 'Precondition Failed', 412 );
215             }
216             }
217             }
218             elsif ( $get_or_head and $if_modified_since and defined $changed ) {
219 0 0         if ( $if_modified_since > $changed ) {
220 0 0         HTTP::Exception->throw(304) if $throw;
221 0 0         return 304 if $dry;
222 0           $dsl->send_error( 'Not Modfied', 304 );
223             }
224             }
225              
226 0           return $builder->();
227             };
228              
229             register_plugin;
230              
231             1;
232              
233             __END__