File Coverage

blib/lib/LWPx/UserAgent/Cached.pm
Criterion Covered Total %
statement 72 77 93.5
branch 18 32 56.2
condition 2 15 13.3
subroutine 16 17 94.1
pod 0 2 0.0
total 108 143 75.5


line stmt bran cond sub pod time code
1             package LWPx::UserAgent::Cached;
2              
3             # ABSTRACT: Subclass of LWP::UserAgent that caches HTTP GET requests
4              
5 7     7   969392 use strict;
  7         72  
  7         207  
6 7     7   38 use warnings;
  7         18  
  7         200  
7 7     7   4160 use utf8;
  7         108  
  7         33  
8             our $VERSION = '0.011';
9              
10             ## no critic (Bangs::ProhibitCommentedOutCode)
11              
12             #pod =head1 SYNOPSIS
13             #pod
14             #pod use LWPx::UserAgent::Cached;
15             #pod use CHI;
16             #pod
17             #pod my $ua = LWPx::UserAgent::Cached->new(
18             #pod cache => CHI->new(
19             #pod driver => 'File', root_dir => '/tmp/cache', expires_in => '1d',
20             #pod ),
21             #pod );
22             #pod $ua->get('http://www.perl.org/');
23             #pod
24             #pod =head1 DESCRIPTION
25             #pod
26             #pod This module borrows the caching logic from
27             #pod L|WWW::Mechanize::Cached> but
28             #pod without inheriting from
29             #pod L|WWW::Mechanize>; instead it is just
30             #pod a direct subclass of
31             #pod L|LWP::UserAgent>.
32             #pod
33             #pod =head2 HTTP/1.1 cache operation
34             #pod
35             #pod Full HTTP/1.1 cache compliance is a work in progress. As of version 0.006 we
36             #pod have limited support for HTTP/1.1 C/C headers, as well as
37             #pod C and C C directives (both on request and
38             #pod response) and the C request header.
39             #pod
40             #pod =head1 SEE ALSO
41             #pod
42             #pod =over
43             #pod
44             #pod =item L|LWP::UserAgent>
45             #pod
46             #pod Parent of this class.
47             #pod
48             #pod =item L|WWW::Mechanize::Cached>
49             #pod
50             #pod Inspiration for this class.
51             #pod
52             #pod =back
53             #pod
54             #pod =cut
55              
56 7     7   3815 use CHI;
  7         560782  
  7         295  
57 7     7   2554 use HTTP::Status qw(HTTP_OK HTTP_MOVED_PERMANENTLY HTTP_NOT_MODIFIED);
  7         24292  
  7         908  
58 7     7   54 use List::Util 1.33 'any';
  7         148  
  7         436  
59 7     7   52 use Moo 1.004005;
  7         151  
  7         55  
60 7     7   6709 use Types::Standard qw(Bool HasMethods HashRef InstanceOf Maybe);
  7         563656  
  7         83  
61 7     7   12873 use namespace::clean;
  7         68683  
  7         47  
62             extends 'LWP::UserAgent';
63              
64             #pod =attr cache
65             #pod
66             #pod Settable at construction, defaults to using
67             #pod L|CHI::Driver::RawMemory> with
68             #pod an instance-specific hash datastore and a namespace with the current
69             #pod package name. You can use your own caching object here as long as it has
70             #pod C and C methods.
71             #pod
72             #pod =cut
73              
74             has cache => (
75             is => 'lazy',
76             isa => HasMethods [qw(get set)],
77             default => sub {
78             CHI->new(
79             serializer => 'Sereal',
80             driver => 'RawMemory',
81             datastore => $_[0]->_cache_datastore,
82             namespace => __PACKAGE__,
83             );
84             },
85             );
86             has _cache_datastore =>
87             ( is => 'lazy', isa => HashRef, default => sub { {} } );
88              
89             #pod =attr is_cached
90             #pod
91             #pod Read-only accessor that indicates if the current request is cached or not.
92             #pod
93             #pod =cut
94              
95             has is_cached =>
96             ( is => 'rwp', isa => Maybe [Bool], init_arg => undef, default => undef );
97              
98             #pod =attr cache_undef_content_length
99             #pod
100             #pod Settable at construction or anytime thereafter, indicates whether we should
101             #pod cache content even if the HTTP C header is missing or
102             #pod undefined. Defaults to false.
103             #pod
104             #pod =cut
105              
106             has cache_undef_content_length => ( is => 'rw', isa => Bool, default => 0 );
107              
108             #pod =attr cache_zero_content_length
109             #pod
110             #pod Settable at construction or anytime thereafter, indicates whether we should
111             #pod cache content even if the HTTP C header is zero. Defaults to
112             #pod false.
113             #pod
114             #pod =cut
115              
116             has cache_zero_content_length => ( is => 'rw', isa => Bool, default => 0 );
117              
118             #pod =attr cache_mismatch_content_length
119             #pod
120             #pod Settable at construction or anytime thereafter, indicates whether we should
121             #pod cache content even if the length of the data does not match the HTTP
122             #pod C header. Defaults to true.
123             #pod
124             #pod =cut
125              
126             has cache_mismatch_content_length =>
127             ( is => 'rw', isa => Bool, default => 1 );
128              
129             #pod =attr ref_in_cache_key
130             #pod
131             #pod Settable at construction or anytime thereafter, indicates whether we should
132             #pod store the HTTP referrer in the cache key. Defaults to false.
133             #pod
134             #pod =cut
135              
136             has ref_in_cache_key => ( is => 'rw', isa => Bool, default => 0 );
137              
138             #pod =attr positive_cache
139             #pod
140             #pod Settable at construction or anytime thereafter, indicates whether we should
141             #pod only cache positive responses (HTTP response codes from C<200> to C<300>
142             #pod inclusive) or cache everything. Defaults to true.
143             #pod
144             #pod =cut
145              
146             has positive_cache => ( is => 'rw', isa => Bool, default => 1 );
147              
148             #pod =attr ignore_headers
149             #pod
150             #pod Settable at construction or anytime thereafter, indicates whether we should
151             #pod ignore C, C, and
152             #pod C HTTP headers when deciding whether to cache a response.
153             #pod Defaults to false.
154             #pod
155             #pod B This option is potentially dangerous, as it ignores the
156             #pod explicit instructions from the server and thus can lead to returning stale
157             #pod content.
158             #pod
159             #pod =cut
160              
161             has ignore_headers => ( is => 'rw', isa => Bool, default => 0 );
162              
163             #pod =head1 HANDLERS
164             #pod
165             #pod This module works by adding C, C and
166             #pod C L
167             #pod that run on successful HTTP C requests.
168             #pod If you need to modify or remove these handlers you may use LWP::UserAgent's
169             #pod L.
170             #pod
171             #pod =for Pod::Coverage BUILD
172             #pod
173             #pod =cut
174              
175             sub BUILD {
176 13     13 0 28970 my $self = shift;
177              
178 13         104 $self->add_handler( request_send => \&_get_cache, ( m_method => 'GET' ) );
179 13         573 $self->add_handler(
180             response_done => \&_set_cache,
181             ( m_method => 'GET', m_code => 2 ),
182             );
183 13         518 $self->add_handler(
184             response_header => \&_get_not_modified,
185             ( m_method => 'GET', m_code => HTTP_NOT_MODIFIED ),
186             );
187              
188 13         326 return;
189             }
190              
191             # load from cache on each GET request
192             sub _get_cache {
193 57     57   815577 my ( $request, $self ) = @_;
194 57         1660 $self->_set_is_cached(0);
195              
196 57         2042 my $clone = $request->clone;
197 57 50       12402 if ( not $self->ref_in_cache_key ) { $clone->header( Referer => undef ) }
  57         711  
198 57 100       3153 return if $self->_no_cache_header_directives($request);
199              
200 51 100       1182 return if not my $response = $self->cache->get( $clone->as_string );
201             return
202 21 50 33     2205 if $response->code < HTTP_OK
203             or $response->code > HTTP_MOVED_PERMANENTLY;
204              
205 21 100       486 if ( $response->header('etag') ) {
206 1         50 $clone->header( if_none_match => $response->header('etag') );
207 1         92 $response = $self->request($clone);
208             }
209 21 50       1768 return if $self->_no_cache_header_directives($response);
210              
211 21         450 $self->_set_is_cached(1);
212 21         580 return $response;
213             }
214              
215             sub _get_not_modified {
216 1     1   6106 my ( $response, $self ) = @_;
217 1         45 $self->_set_is_cached(0);
218              
219 1         38 my $request = $response->request->clone;
220 1         237 $request->remove_header(qw(if_modified_since if_none_match));
221              
222 1         74 my $cached_response = $self->cache->get( $request->as_string );
223 1         289 $response->content( $cached_response->decoded_content );
224              
225 1         2619 $self->_set_is_cached(1);
226 1         39 return;
227             }
228              
229             # save to cache after successful GET
230             sub _set_cache {
231 41     41   492053 my ( $response, $self ) = @_;
232 41 50       128 return if not $response;
233              
234 41 50 33     147 if (not($response->header('client-transfer-encoding')
235 0     0   0 and any { 'chunked' eq $_ }
236             $response->header('client-transfer-encoding')
237             )
238             )
239             {
240 41         2520 for ( $response->header('size') ) {
241             return
242 0 0 0     0 if not defined and $self->cache_undef_content_length;
243             return
244 0 0 0     0 if 0 == $_
245             and not $self->cache_zero_content_length;
246             return
247 0 0 0     0 if $_ != length $response->content
248             and not $self->cache_mismatch_content_length;
249             }
250             }
251              
252 41         2102 for my $message ( $response, $response->request ) {
253 78 100       542 return if $self->_no_cache_header_directives($message);
254             }
255              
256 31         110 $response->decode;
257 31         1839 $self->cache->set( $response->request->as_string => $response );
258 31         16532 return;
259             }
260              
261             sub _no_cache_header_directives {
262 156     156   350 my ( $self, $message ) = @_;
263 156 50       3348 return if $self->ignore_headers;
264              
265 156         1199 for my $header_name (qw(pragma cache_control)) {
266 308 100       6786 if ( my @directives = $message->header($header_name) ) {
267 16 50   16   743 return 1 if any {/\A no- (?: cache | store ) /xms} @directives;
  16         228  
268             }
269             }
270 140         5509 return;
271             }
272              
273             #pod =for Pod::Coverage FOREIGNBUILDARGS
274             #pod
275             #pod =cut
276              
277             ## no critic (Subroutines::RequireArgUnpacking)
278             sub FOREIGNBUILDARGS {
279 13     13 0 257108 shift;
280 13 50       350 return 'HASH' eq ref $_[0] ? %{ $_[0] } : @_;
  0            
281             }
282              
283             1;
284              
285             __END__