File Coverage

blib/lib/LWPx/UserAgent/Cached.pm
Criterion Covered Total %
statement 72 77 93.5
branch 17 30 56.6
condition 2 15 13.3
subroutine 16 17 94.1
pod 0 2 0.0
total 107 141 75.8


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   909629 use strict;
  7         66  
  7         202  
6 7     7   38 use warnings;
  7         15  
  7         203  
7 7     7   4087 use utf8;
  7         101  
  7         34  
8             our $VERSION = '0.009';
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   3811 use CHI;
  7         551546  
  7         282  
57 7     7   2383 use HTTP::Status qw(HTTP_OK HTTP_MOVED_PERMANENTLY HTTP_NOT_MODIFIED);
  7         22973  
  7         834  
58 7     7   55 use List::Util 1.33 'any';
  7         134  
  7         520  
59 7     7   49 use Moo 1.004005;
  7         135  
  7         50  
60 7     7   6328 use Types::Standard qw(Bool HasMethods HashRef InstanceOf Maybe);
  7         559890  
  7         123  
61 7     7   12692 use namespace::clean;
  7         67834  
  7         50  
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 =head1 HANDLERS
149             #pod
150             #pod This module works by adding C, C and
151             #pod C L
152             #pod that run on successful HTTP C requests.
153             #pod If you need to modify or remove these handlers you may use LWP::UserAgent's
154             #pod L.
155             #pod
156             #pod =for Pod::Coverage BUILD
157             #pod
158             #pod =cut
159              
160             sub BUILD {
161 13     13 0 27304 my $self = shift;
162              
163 13         94 $self->add_handler( request_send => \&_get_cache, ( m_method => 'GET' ) );
164 13         531 $self->add_handler(
165             response_done => \&_set_cache,
166             ( m_method => 'GET', m_code => 2 ),
167             );
168 13         491 $self->add_handler(
169             response_header => \&_get_not_modified,
170             ( m_method => 'GET', m_code => HTTP_NOT_MODIFIED ),
171             );
172              
173 13         325 return;
174             }
175              
176             # load from cache on each GET request
177             sub _get_cache {
178 57     57   806540 my ( $request, $self ) = @_;
179 57         1658 $self->_set_is_cached(0);
180              
181 57         1908 my $clone = $request->clone;
182 57 50       10897 if ( not $self->ref_in_cache_key ) { $clone->header( Referer => undef ) }
  57         628  
183 57 100       3163 return if $self->_no_cache_header_directives($request);
184              
185 51 100       1114 return if not my $response = $self->cache->get( $clone->as_string );
186             return
187 21 50 33     2596 if $response->code < HTTP_OK
188             or $response->code > HTTP_MOVED_PERMANENTLY;
189              
190 21 100       555 if ( $response->header('etag') ) {
191 1         61 $clone->header( if_none_match => $response->header('etag') );
192 1         112 $response = $self->request($clone);
193             }
194 21 50       1480 return if $self->_no_cache_header_directives($response);
195              
196 21         494 $self->_set_is_cached(1);
197 21         606 return $response;
198             }
199              
200             sub _get_not_modified {
201 1     1   4430 my ( $response, $self ) = @_;
202 1         29 $self->_set_is_cached(0);
203              
204 1         29 my $request = $response->request->clone;
205 1         181 $request->remove_header(qw(if_modified_since if_none_match));
206              
207 1         57 my $cached_response = $self->cache->get( $request->as_string );
208 1         214 $response->content( $cached_response->decoded_content );
209              
210 1         218 $self->_set_is_cached(1);
211 1         31 return;
212             }
213              
214             # save to cache after successful GET
215             sub _set_cache {
216 41     41   475286 my ( $response, $self ) = @_;
217 41 50       134 return if not $response;
218              
219 41 50 33     110 if (not($response->header('client-transfer-encoding')
220 0     0   0 and any { 'chunked' eq $_ }
221             $response->header('client-transfer-encoding')
222             )
223             )
224             {
225 41         2283 for ( $response->header('size') ) {
226             return
227 0 0 0     0 if not defined and $self->cache_undef_content_length;
228             return
229 0 0 0     0 if 0 == $_
230             and not $self->cache_zero_content_length;
231             return
232 0 0 0     0 if $_ != length $response->content
233             and not $self->cache_mismatch_content_length;
234             }
235             }
236              
237 41         1857 for my $message ( $response, $response->request ) {
238 78 100       576 return if $self->_no_cache_header_directives($message);
239             }
240              
241 31         147 $response->decode;
242 31         1319 $response->remove_content_headers;
243 31         1704 $self->cache->set( $response->request->as_string => $response );
244 31         12509 return;
245             }
246              
247             sub _no_cache_header_directives {
248 156     156   350 my ( $self, $message ) = @_;
249 156         343 for my $header_name (qw(pragma cache_control)) {
250 308 100       6397 if ( my @directives = $message->header($header_name) ) {
251 16 50   16   758 return 1 if any {/\A no- (?: cache | store ) /xms} @directives;
  16         257  
252             }
253             }
254 140         5391 return;
255             }
256              
257             #pod =for Pod::Coverage FOREIGNBUILDARGS
258             #pod
259             #pod =cut
260              
261             ## no critic (Subroutines::RequireArgUnpacking)
262             sub FOREIGNBUILDARGS {
263 13     13 0 234316 shift;
264 13 50       274 return 'HASH' eq ref $_[0] ? %{ $_[0] } : @_;
  0            
265             }
266              
267             1;
268              
269             __END__