File Coverage

blib/lib/FCGI/Buffer.pm
Criterion Covered Total %
statement 376 834 45.0
branch 172 518 33.2
condition 81 272 29.7
subroutine 27 29 93.1
pod 5 5 100.0
total 661 1658 39.8


line stmt bran cond sub pod time code
1             package FCGI::Buffer;
2              
3 12     12   3809518 use strict;
  12         85  
  12         380  
4 12     12   67 use warnings;
  12         21  
  12         340  
5              
6             # FIXME: save_to treats ?arg1=a&arg2=b and ?arg2=b&arg1=a as different
7             # FIXME: save_to treats /cgi-bin/foo.fcgi and /cgi-bin2/foo.fcgi as the same
8              
9 12     12   68 use Digest::MD5;
  12         19  
  12         463  
10 12     12   70 use File::Path;
  12         24  
  12         643  
11 12     12   70 use File::Spec;
  12         34  
  12         309  
12 12     12   5136 use IO::String;
  12         33288  
  12         385  
13 12     12   6072 use CGI::Info;
  12         759577  
  12         831  
14 12     12   147 use Carp;
  12         27  
  12         898  
15 12     12   6106 use HTTP::Date;
  12         45659  
  12         799  
16 12     12   20047 use DBI;
  12         229024  
  12         1276  
17              
18             =head1 NAME
19              
20             FCGI::Buffer - Verify, Cache and Optimise FCGI Output
21              
22             =head1 VERSION
23              
24             Version 0.17
25              
26             =cut
27              
28             our $VERSION = '0.17';
29              
30             =head1 SYNOPSIS
31              
32             FCGI::Buffer verifies the HTML that you produce by passing it through
33             C<HTML::Lint>.
34              
35             FCGI::Buffer optimises FCGI programs by reducing, filtering and compressing
36             output to speed up the transmission and by nearly seamlessly making use of
37             client and server caches.
38              
39             To make use of client caches, that is to say to reduce needless calls
40             to your server asking for the same data:
41              
42             use FCGI;
43             use FCGI::Buffer;
44             # ...
45             my $request = FCGI::Request();
46             while($request->FCGI::Accept() >= 0) {
47             my $buffer = FCGI::Buffer->new();
48             $buffer->init(
49             optimise_content => 1,
50             lint_content => 0,
51             );
52             # ...
53             }
54              
55             To also make use of server caches, that is to say to save regenerating
56             output when different clients ask you for the same data, you will need
57             to create a cache.
58             But that's simple:
59              
60             use FCGI;
61             use CHI;
62             use FCGI::Buffer;
63              
64             # ...
65             my $request = FCGI::Request();
66             while($request->FCGI::Accept() >= 0) {
67             my $buffer = FCGI::Buffer->new();
68             $buffer->init(
69             optimise_content => 1,
70             lint_content => 0,
71             cache => CHI->new(driver => 'File')
72             );
73             if($buffer->is_cached()) {
74             # Nothing has changed - use the version in the cache
75             $request->Finish();
76             next;
77             # ...
78             }
79             }
80              
81             To temporarily prevent the use of server-side caches, for example whilst
82             debugging before publishing a code change, set the NO_CACHE environment variable
83             to any non-zero value.
84             This will also stop ETag being added to the header.
85             If you get errors about Wide characters in print it means that you've
86             forgotten to emit pure HTML on non-ascii characters.
87             See L<HTML::Entities>.
88             As a hack work around you could also remove accents and the like by using
89             L<Text::Unidecode>,
90             which works well but isn't really what you want.
91              
92             =head1 SUBROUTINES/METHODS
93              
94             =cut
95              
96 12     12   130 use constant MIN_GZIP_LEN => 32;
  12         30  
  12         130860  
97              
98             =head2 new
99              
100             Create an FCGI::Buffer object. Do one of these for each FCGI::Accept.
101              
102             =cut
103              
104             # FIXME: Call init() on any arguments that are given
105             sub new {
106 39     39 1 175702 my $proto = shift;
107 39   66     228 my $class = ref($proto) || $proto;
108              
109             # Use FCGI::Buffer->new(), not FCGI::Buffer::new()
110 39 100       111 if(!defined($class)) {
111 1         19 carp(__PACKAGE__, ' use ->new() not ::new() to instantiate');
112 1         274 return;
113             }
114              
115 38         224 my $buf = IO::String->new();
116              
117 38         1992 my $rc = {
118             buf => $buf,
119             old_buf => select($buf),
120             generate_304 => 1,
121             generate_last_modified => 1,
122             compress_content => 1,
123             optimise_content => 0,
124             lint_content => 0,
125             };
126             # $rc->{o} = ();
127              
128 38 100 100     256 if($ENV{'SERVER_PROTOCOL'} &&
      100        
129             (($ENV{'SERVER_PROTOCOL'} eq 'HTTP/1.1') || ($ENV{'SERVER_PROTOCOL'} eq 'HTTP/2.0'))) {
130 20         64 $rc->{generate_etag} = 1;
131             } else {
132 18         61 $rc->{generate_etag} = 0;
133             }
134              
135 38         175 return bless $rc, $class;
136             }
137              
138             sub DESTROY {
139 37 50 33 37   6174 if(defined($^V) && ($^V ge 'v5.14.0')) {
140 37 50       152 return if ${^GLOBAL_PHASE} eq 'DESTRUCT'; # >= 5.14.0 only
141             }
142 37         126 my $self = shift;
143              
144 37 100       133 if($self->{'logger'}) {
145 1         7 $self->{'logger'}->info('In DESTROY');
146             }
147 37         132 select($self->{old_buf});
148 37 50 33     174 if((!defined($self->{buf})) || (!defined($self->{buf}->getpos()))) {
149             # Unlikely
150 0 0       0 if($self->{'logger'}) {
151 0         0 $self->{'logger'}->info('Nothing to send');
152             }
153 0         0 return;
154             }
155 37         492 my $pos = $self->{buf}->getpos();
156 37         346 $self->{buf}->setpos(0);
157 37         565 my $buf;
158 37         135 read($self->{buf}, $buf, $pos);
159 37         733 my $headers;
160 37         355 ($headers, $self->{body}) = split /\r?\n\r?\n/, $buf, 2;
161              
162 37 100       124 if($self->{'logger'}) {
163 1 50       6 if($ENV{'HTTP_IF_NONE_MATCH'}) {
164 0         0 $self->{logger}->debug("HTTP_IF_NONE_MATCH: $ENV{HTTP_IF_NONE_MATCH}");
165             }
166 1 50       5 if($ENV{'HTTP_IF_MODIFIED_SINCE'}) {
167 0         0 $self->{logger}->debug("HTTP_IF_MODIFIED_SINCE: $ENV{HTTP_IF_MODIFIED_SINCE}");
168             }
169 1         15 $self->{logger}->debug("Generate_etag = $self->{generate_etag}",
170             "Generate_304 = $self->{generate_304}",
171             "Generate_last_modified = $self->{generate_last_modified}");
172             }
173 37 100 66     118 unless($headers || $self->is_cached()) {
174 3 50       12 if($self->{'logger'}) {
175 0         0 $self->{'logger'}->debug('There was no output');
176             }
177 3         21 return;
178             }
179 34 100 100     160 if($ENV{'REQUEST_METHOD'} && ($ENV{'REQUEST_METHOD'} eq 'HEAD')) {
180 1         5 $self->{send_body} = 0;
181             } else {
182 33         73 $self->{send_body} = 1;
183             }
184              
185 34 50       78 if($headers) {
186 34         86 $self->_set_content_type($headers);
187             }
188              
189 34 100 66     195 if(defined($self->{body}) && ($self->{body} eq '')) {
    100          
190             # E.g. if header of Location is given with no body, for
191             # redirection
192 2         9 delete $self->{body};
193 2 50       11 if($self->{cache}) {
194             # Don't try to retrieve it below from the cache
195 0         0 $self->{send_body} = 0;
196             }
197             } elsif(defined($self->{content_type})) {
198 29         49 my @content_type = @{$self->{content_type}};
  29         84  
199 29 50 33     292 if(defined($content_type[0]) && (lc($content_type[0]) eq 'text') && (lc($content_type[1]) =~ /^html/) && defined($self->{body})) {
      33        
      33        
200 29 100       108 if($self->{optimise_content}) {
201             # require HTML::Clean;
202 25         1250 require HTML::Packer; # Overkill using HTML::Clean and HTML::Packer...
203              
204 25 50       23050 if($self->{'logger'}) {
205 0         0 $self->{'logger'}->trace('Packer');
206             }
207              
208 25         42 my $oldlength = length($self->{body});
209 25         39 my $newlength;
210              
211 25 100       67 if($self->{optimise_content} == 1) {
212 24         74 $self->_optimise_content();
213             } else {
214 1         2 while(1) {
215 3         11 $self->_optimise_content();
216 3         4 $newlength = length($self->{body});
217 3 100       10 last if ($newlength >= $oldlength);
218 2         4 $oldlength = $newlength;
219             }
220             }
221              
222             # If we're on http://www.example.com and have a link
223             # to http://www.example.com/foo/bar.htm, change the
224             # link to /foo/bar.htm - there's no need to include
225             # the site name in the link
226 25 100       72 unless(defined($self->{info})) {
227 22 50       52 if($self->{cache}) {
228 0         0 $self->{info} = CGI::Info->new({ cache => $self->{cache} });
229             } else {
230 22         132 $self->{info} = CGI::Info->new();
231             }
232             }
233              
234 25         547 my $href = $self->{info}->host_name();
235 25         9867 my $protocol = $self->{info}->protocol();
236              
237 25 100       256 unless($protocol) {
238 10         19 $protocol = 'http';
239             }
240              
241 25         206 $self->{body} =~ s/<a\s+?href="$protocol:\/\/$href"/<a href="\/"/gim;
242 25         137 $self->{body} =~ s/<a\s+?href="$protocol:\/\/$href/<a href="/gim;
243 25         109 $self->{body} =~ s/<a\s+?href="$protocol:\/\//<a href="\/\//gim;
244              
245             # If we're in "/cgi-bin/foo.cgi?arg1=a" replace
246             # "/cgi-bin/foo.cgi?arg2=b" with "?arg2=b"
247              
248 25 100       76 if(my $script_name = $ENV{'SCRIPT_NAME'}) {
249 18 50       57 if($script_name =~ /^\//) {
250 18         103 $self->{body} =~ s/<a\s+?href="$script_name(\?.+)?"/<a href="$1"/gim;
251             }
252             }
253              
254             # TODO use URI->path_segments to change links in
255             # /aa/bb/cc/dd.htm which point to /aa/bb/ff.htm to
256             # ../ff.htm
257              
258             # TODO: <img border=0 src=...>
259 25         114 $self->{body} =~ s/<img\s+?src="$protocol:\/\/$href"/<img src="\/"/gim;
260 25         96 $self->{body} =~ s/<img\s+?src="$protocol:\/\/$href/<img src="/gim;
261              
262             # Don't use HTML::Clean because of RT402
263             # my $h = new HTML::Clean(\$self->{body});
264             # # $h->compat();
265             # $h->strip();
266             # my $ref = $h->data();
267              
268             # Don't always do javascript 'best' since it's confused
269             # by the common <!-- HIDE technique.
270 25         87 my $options = {
271             remove_comments => 1,
272             remove_newlines => 0,
273             do_stylesheet => 'minify'
274             };
275 25 100       71 if($self->{optimise_content} >= 2) {
276 1         2 $options->{do_javascript} = 'best';
277 1         16 $self->{body} =~ s/(<script.*?>)\s*<!--/$1/gi;
278 1         4 $self->{body} =~ s/\/\/-->\s*<\/script>/<\/script>/gi;
279 1         18 $self->{body} =~ s/(<script.*?>)\s+/$1/gi;
280             }
281 25         136 $self->{body} = HTML::Packer->init()->minify(\$self->{body}, $options);
282 25 100       321355 if($self->{optimise_content} >= 2) {
283             # Change document.write("a"); document.write("b")
284             # into document.write("a"+"b");
285 1         3 while(1) {
286 1         26 $self->{body} =~ s/<script\s*?type\s*?=\s*?"text\/javascript"\s*?>(.*?)document\.write\((.+?)\);\s*?document\.write\((.+?)\)/<script type="text\/JavaScript">${1}document.write($2+$3)/igs;
287 1         3 $newlength = length($self->{body});
288 1 50       7 last if ($newlength >= $oldlength);
289 0         0 $oldlength = $newlength;
290             }
291             }
292             }
293 29 100       132 if($self->{lint_content}) {
294 3         20 require HTML::Lint;
295 3         23 HTML::Lint->import;
296              
297 3 50       11 if($self->{'logger'}) {
298 0         0 $self->{'logger'}->trace('Lint');
299             }
300 3         13 my $lint = HTML::Lint->new();
301 3         56 $lint->parse($self->{body});
302 3         4275 $lint->eof();
303              
304 3 100       356 if($lint->errors) {
305 1         13 $headers = 'Status: 500 Internal Server Error';
306 1         3 @{$self->{o}} = ('Content-type: text/plain');
  1         3  
307 1         3 $self->{body} = '';
308 1         4 foreach my $error ($lint->errors) {
309 3         22 my $errtext = $error->where() . ': ' . $error->errtext() . "\n";
310 3         172 warn($errtext);
311 3         22 $self->{body} .= $errtext;
312             }
313             }
314             }
315             }
316             }
317              
318 34 100 66     281 if(defined($headers) && ($headers =~ /^Status: (\d+)/m)) {
    100          
319 1         6 $self->{status} = $1;
320             } elsif(defined($self->{info})) {
321 25         122 $self->{status} = $self->{info}->status();
322             } else {
323 8         20 $self->{status} = 200;
324             }
325              
326 34 100       422 if($self->{'logger'}) {
327 1         10 $self->{'logger'}->debug("Initial status = $self->{status}");
328             }
329              
330             # Generate the eTag before compressing, since the compressed data
331             # includes the mtime field which changes thus causing a different
332             # Etag to be generated
333 34 100 100     209 if($ENV{'SERVER_PROTOCOL'} &&
      100        
      100        
      66        
334             (($ENV{'SERVER_PROTOCOL'} eq 'HTTP/1.1') || ($ENV{'SERVER_PROTOCOL'} eq 'HTTP/2.0')) &&
335             $self->{generate_etag} && defined($self->{body})) {
336 11         38 $self->_generate_etag();
337              
338 11 100 100     48 if($ENV{'HTTP_IF_NONE_MATCH'} && $self->{generate_304} && ($self->{status} == 200)) {
      66        
339 2         9 $self->_check_if_none_match();
340             }
341             }
342              
343 34         52 my $dbh;
344 34 50       101 if(my $save_to = $self->{save_to}) {
345 0         0 my $sqlite_file = File::Spec->catfile($save_to->{directory}, 'fcgi.buffer.sql');
346 0 0       0 if($self->{logger}) {
347 0         0 $self->{logger}->debug("save_to sqlite file: $sqlite_file");
348             }
349 0 0       0 if(!-r $sqlite_file) {
350 0 0       0 if(!-d $save_to->{directory}) {
351 0         0 mkdir $save_to->{directory};
352             }
353 0         0 $dbh = DBI->connect("dbi:SQLite:dbname=$sqlite_file", undef, undef);
354 0 0       0 if($self->{save_to}->{create_table}) {
355 0         0 $dbh->prepare('CREATE TABLE fcgi_buffer(key char PRIMARY KEY, language char, browser_type char, path char UNIQUE NOT NULL, uri char NOT NULL, creation timestamp NOT NULL)')->execute();
356             }
357             } else {
358 0         0 $dbh = DBI->connect("dbi:SQLite:dbname=$sqlite_file", undef, undef);
359             }
360             }
361              
362 34         99 my $encoding = $self->_should_gzip();
363 34         74 my $unzipped_body = $self->{body};
364              
365 34 100       91 if(defined($unzipped_body)) {
366 32 50       82 my $range = $ENV{'Range'} ? $ENV{'Range'} : $ENV{'HTTP_RANGE'};
367              
368 32 100 66     83 if($range && !$self->{cache}) {
369             # TODO: Partials
370 3 50       20 if($range =~ /^bytes=(\d*)-(\d*)/) {
371 3 100 100     25 if($1 && $2) {
    100          
    50          
372 1         7 $self->{body} = substr($self->{body}, $1, $2-$1);
373             } elsif($1) {
374 1         5 $self->{body} = substr($self->{body}, $1);
375             } elsif($2) {
376 1         6 $self->{body} = substr($self->{body}, 0, $2);
377             }
378 3         7 $unzipped_body = $self->{body};
379 3         7 $self->{'status'} = 206;
380             }
381             }
382 32         463 $self->_compress({ encoding => $encoding });
383             }
384              
385 34 50       163 if($self->{cache}) {
    100          
386 0         0 require Storable;
387              
388 0         0 my $cache_hash;
389 0         0 my $key = $self->_generate_key();
390              
391             # Cache unzipped version
392 0 0       0 if(!defined($self->{body})) {
393 0 0       0 if($self->{send_body}) {
394 0         0 $self->{cobject} = $self->{cache}->get_object($key);
395 0 0       0 if(defined($self->{cobject})) {
396 0         0 $cache_hash = Storable::thaw($self->{cobject}->value());
397 0         0 $headers = $cache_hash->{'headers'};
398 0         0 $self->_set_content_type($headers);
399 0         0 @{$self->{o}} = ("X-FCGI-Buffer-$VERSION: Hit");
  0         0  
400 0 0       0 if($self->{info}) {
401 0         0 my $host_name = $self->{info}->host_name();
402 0         0 push @{$self->{o}}, "X-Cache: HIT from $host_name";
  0         0  
403 0         0 push @{$self->{o}}, "X-Cache-Lookup: HIT from $host_name";
  0         0  
404             } else {
405 0         0 push @{$self->{o}}, 'X-Cache: HIT';
  0         0  
406 0         0 push @{$self->{o}}, 'X-Cache-Lookup: HIT';
  0         0  
407             }
408             } else {
409 0         0 carp( __PACKAGE__, ": error retrieving data for key $key");
410             }
411             }
412              
413             # Nothing has been output yet, so we can check if it's
414             # OK to send 304 if possible
415 0 0 0     0 if($self->{send_body} && $ENV{'SERVER_PROTOCOL'} &&
      0        
      0        
      0        
      0        
416             (($ENV{'SERVER_PROTOCOL'} eq 'HTTP/1.1') || ($ENV{'SERVER_PROTOCOL'} eq 'HTTP/2.0')) &&
417             $self->{generate_304} && ($self->{status} == 200)) {
418 0 0       0 if($ENV{'HTTP_IF_MODIFIED_SINCE'}) {
419             $self->_check_modified_since({
420             since => $ENV{'HTTP_IF_MODIFIED_SINCE'},
421             modified => $self->{cobject}->created_at()
422 0         0 });
423             }
424             }
425 0 0 0     0 if($self->{send_body} && ($self->{status} == 200)) {
426 0         0 $self->{body} = $cache_hash->{'body'};
427 0 0       0 if($dbh) {
428 0         0 my $changes = $self->_save_to($self->{body}, $dbh);
429 0 0 0     0 if($changes && (my $ttl = $self->{save_to}->{ttl})) {
430 0         0 push @{$self->{o}}, 'Expires: ' . HTTP::Date::time2str(time + $ttl);
  0         0  
431             }
432             }
433 0 0       0 if(!defined($self->{body})) {
434             # Panic
435 0         0 $headers = 'Status: 500 Internal Server Error';
436 0         0 @{$self->{o}} = ('Content-type: text/plain');
  0         0  
437 0         0 $self->{body} = "Can't retrieve body for key $key, cache_hash contains:\n";
438 0         0 foreach my $k (keys %{$cache_hash}) {
  0         0  
439 0         0 $self->{body} .= "\t$k\n";
440             }
441              
442 0 0       0 if($dbh) {
443 0         0 my $query = "SELECT DISTINCT path FROM fcgi_buffer WHERE key = '$key'";
444 0         0 my $sth = $dbh->prepare($query);
445 0 0       0 if($self->{logger}) {
446 0         0 $self->{logger}->debug($query);
447             }
448 0 0 0     0 if($sth->execute() && (my $href = $sth->fetchrow_hashref())) {
449 0 0       0 if(my $path = $href->{'path'}) {
450 0         0 unlink($path);
451             }
452             }
453 0         0 $query = "DELETE FROM fcgi_buffer WHERE key = '$key'";
454 0         0 $dbh->do($query);
455 0 0       0 if($self->{logger}) {
456 0         0 $self->{logger}->debug($query);
457             }
458             }
459              
460 0         0 $self->{cache}->remove($key);
461              
462 0 0       0 if($self->{logger}) {
463 0         0 $self->{logger}->error("Can't retrieve body for key $key");
464             } else {
465 0         0 carp "Can't retrieve body for key $key";
466             }
467 0         0 warn($self->{body});
468 0         0 $self->{send_body} = 0;
469 0         0 $self->{status} = 500;
470             }
471             }
472 0 0 0     0 if($self->{send_body} && $ENV{'SERVER_PROTOCOL'} &&
      0        
      0        
      0        
473             (($ENV{'SERVER_PROTOCOL'} eq 'HTTP/1.1') || ($ENV{'SERVER_PROTOCOL'} eq 'HTTP/2.0')) &&
474             ($self->{status} == 200)) {
475 0 0 0     0 if($ENV{'HTTP_IF_NONE_MATCH'} && $self->{generate_etag}) {
476 0 0       0 if(!defined($self->{etag})) {
477 0         0 $self->_generate_etag();
478             }
479 0         0 $self->_check_if_none_match();
480             }
481             }
482 0 0       0 if($self->{status} == 200) {
483 0         0 $encoding = $self->_should_gzip();
484 0 0       0 if($self->{send_body}) {
485 0 0 0     0 if($self->{generate_etag} && !defined($self->{etag}) && ((!defined($headers)) || ($headers !~ /^ETag: /m))) {
      0        
      0        
486 0         0 $self->_generate_etag();
487             }
488 0         0 $self->_compress({ encoding => $encoding });
489             }
490             }
491 0         0 my $cannot_304 = !$self->{generate_304};
492 0 0       0 unless($self->{etag}) {
493 0 0 0     0 if(defined($headers) && ($headers =~ /^ETag: "([a-z0-9]{32})"/m)) {
494 0         0 $self->{etag} = $1;
495             } else {
496 0         0 $self->{etag} = $cache_hash->{'etag'};
497             }
498             }
499 0 0 0     0 if($ENV{'HTTP_IF_NONE_MATCH'} && $self->{send_body} && ($self->{status} != 304) && $self->{generate_304}) {
      0        
      0        
500 0 0       0 if(!$self->_check_if_none_match()) {
501 0         0 $cannot_304 = 1;
502             }
503             }
504 0 0       0 if($self->{cobject}) {
505 0 0 0     0 if($ENV{'HTTP_IF_MODIFIED_SINCE'} && ($self->{status} != 304) && !$cannot_304) {
      0        
506             $self->_check_modified_since({
507             since => $ENV{'HTTP_IF_MODIFIED_SINCE'},
508             modified => $self->{cobject}->created_at()
509 0         0 });
510             }
511 0 0 0     0 if(($self->{status} == 200) && $self->{generate_last_modified}) {
512 0 0       0 if($self->{logger}) {
513 0         0 $self->{logger}->debug('Set Last-Modified to ', HTTP::Date::time2str($self->{cobject}->created_at()));
514             }
515 0         0 push @{$self->{o}}, 'Last-Modified: ' . HTTP::Date::time2str($self->{cobject}->created_at());
  0         0  
516             }
517             }
518             } else {
519             # Not in the server side cache
520 0 0       0 if($self->{status} == 200) {
521 0         0 my $changes = $self->_save_to($unzipped_body, $dbh);
522              
523 0 0       0 unless($self->{cache_age}) {
524             # It would be great if CHI::set()
525             # allowed the time to be 'lru' for least
526             # recently used.
527 0         0 $self->{cache_age} = '10 minutes';
528             }
529 0         0 $cache_hash->{'body'} = $unzipped_body;
530 0 0 0     0 if($changes && $encoding) {
531 0         0 $self->_compress({ encoding => $encoding });
532             }
533 0 0 0     0 if($self->{o} && scalar(@{$self->{o}})) {
  0 0 0     0  
534             # Remember, we're storing the UNzipped
535             # version in the cache
536 0         0 my $c;
537 0 0 0     0 if(defined($headers) && length($headers)) {
538 0         0 $c = "$headers\r\n" . join("\r\n", @{$self->{o}});
  0         0  
539             } else {
540 0         0 $c = join("\r\n", @{$self->{o}});
  0         0  
541             }
542 0         0 $c =~ s/^Content-Encoding: .+$//mg;
543 0         0 $c =~ s/^Vary: Accept-Encoding.*\r?$//mg;
544 0         0 $c =~ s/\n+/\n/gs;
545 0 0       0 if(length($c)) {
546 0         0 $cache_hash->{'headers'} = $c;
547             }
548             } elsif(defined($headers) && length($headers)) {
549 0         0 $headers =~ s/^Content-Encoding: .+$//mg;
550 0         0 $headers =~ s/^Vary: Accept-Encoding.*\r?$//mg;
551 0         0 $headers =~ s/\n+/\n/gs;
552 0 0       0 if(length($headers)) {
553 0         0 $cache_hash->{'headers'} = $headers;
554             }
555             }
556 0 0       0 if($self->{generate_etag}) {
557 0 0       0 if(!defined($self->{etag})) {
558 0         0 $self->_generate_etag();
559             }
560 0         0 $cache_hash->{'etag'} = $self->{etag};
561             }
562             # TODO: Support the Expires header
563             # if($headers !~ /^Expires: /m))) {
564             # }
565 0 0       0 if($self->{logger}) {
566 0         0 $self->{logger}->debug("Store $key in the cache, age = ", $self->{cache_age}, ' ', length($cache_hash->{'body'}), ' bytes');
567             }
568 0         0 $self->{cache}->set($key, Storable::freeze($cache_hash), $self->{cache_age});
569              
570             # Create a static page with the information and link to that in the output
571             # HTML
572 0 0 0     0 if($dbh && $self->{info} && $self->{save_to} && (my $request_uri = $ENV{'REQUEST_URI'})) {
      0        
      0        
573 0         0 my $query = "SELECT DISTINCT creation FROM fcgi_buffer WHERE key = ?";
574 0 0       0 if($self->{logger}) {
575 0         0 $self->{logger}->debug("$query: $key");
576             }
577 0         0 my $sth = $dbh->prepare($query);
578 0         0 $sth->execute($key);
579 0 0       0 if(my $href = $sth->fetchrow_hashref()) {
580 0 0       0 if(my $ttl = $self->{save_to}->{ttl}) {
581 0         0 push @{$self->{o}}, 'Expires: ' .
582 0         0 HTTP::Date::time2str($href->{'creation'} + $ttl);
583             }
584             } else {
585 0         0 my $dir = $self->{save_to}->{directory};
586 0         0 my $browser_type = $self->{info}->browser_type();
587 0         0 my $language;
588 0 0       0 if($self->{'lingua'}) {
589 0         0 $language = $self->{lingua}->language();
590 0 0       0 if($language =~ /([\w\s]+)/i) {
591 0         0 $language = $1; # Untaint
592             }
593             } else {
594 0         0 $language = 'default';
595             }
596 0         0 my $bdir = File::Spec->catfile($dir, $browser_type);
597 0 0       0 if($bdir =~ /^([\/\\])(.+)$/) {
598 0         0 $bdir = "$1$2"; # Untaint
599             }
600 0         0 my $ldir = File::Spec->catfile($bdir, $language);
601 0         0 my $sdir = File::Spec->catfile($ldir, $self->{info}->script_name());
602 0 0       0 if($self->{logger}) {
603 0         0 $self->{logger}->debug("Create paths to $sdir");
604             }
605 0         0 File::Path::make_path($sdir);
606 0         0 my $file = $self->{info}->as_string();
607 0         0 $file =~ tr/\//_/;
608 0         0 my $path = File::Spec->catfile($sdir, "$file.html");
609 0 0       0 if($path =~ /^(.+)$/) {
610 0         0 $path = $1; # Untaint
611 0         0 $path =~ tr/[\|;]/_/;
612             }
613 0 0       0 if(open(my $fout, '>', $path)) {
614 0         0 my $u = $request_uri;
615 0         0 $u =~ s/\?/\\?/g;
616 0         0 $u =~ s/\)/\\)/g;
617 0         0 my $copy = $unzipped_body;
618 0         0 my $changes = ($copy =~ s/<a\s+href="$u"/<a href="$path"/gi);
619              
620             # handle <a href="?arg3=4">Call self with different args</a>
621 0         0 my $script_name = $ENV{'SCRIPT_NAME'};
622 0         0 $copy =~ s/<a\s+href="(\?.+?)"/<a href="$script_name$1"/gi;
623              
624             # Avoid Wide character
625 0 0       0 unless($self->{_encode_loaded}) {
626 0         0 require Encode;
627 0         0 $self->{_encode_loaded} = 1;
628             }
629 0         0 print $fout Encode::encode_utf8($copy);
630 0         0 close $fout;
631             # Do INSERT OR REPLACE in case another program has
632             # got in first,
633 0         0 $query = "INSERT OR REPLACE INTO fcgi_buffer(key, language, browser_type, path, uri, creation) VALUES('$key', '$language', '$browser_type', '$path', '$request_uri', strftime('\%s','now'))";
634 0 0       0 if($self->{logger}) {
635 0         0 $self->{logger}->debug($query);
636             }
637 0         0 $dbh->prepare($query)->execute();
638              
639 0 0 0     0 if($changes && (my $ttl = $self->{save_to}->{ttl})) {
640 0         0 push @{$self->{o}}, 'Expires: ' . HTTP::Date::time2str(time + $ttl);
  0         0  
641             }
642             } else {
643 0 0       0 if($self->{logger}) {
644 0         0 $self->{logger}->warn("Can't create $path");
645             }
646             }
647             }
648             }
649 0 0       0 if($self->{generate_last_modified}) {
650 0         0 $self->{cobject} = $self->{cache}->get_object($key);
651 0 0       0 if(defined($self->{cobject})) {
652 0         0 push @{$self->{o}}, 'Last-Modified: ' . HTTP::Date::time2str($self->{cobject}->created_at());
  0         0  
653             } else {
654 0         0 push @{$self->{o}}, 'Last-Modified: ' . HTTP::Date::time2str(time);
  0         0  
655             }
656             }
657             }
658 0 0       0 if($self->{info}) {
659 0         0 my $host_name = $self->{info}->host_name();
660 0 0       0 if(defined($self->{x_cache})) {
661 0         0 push @{$self->{o}}, 'X-Cache: ' . $self->{x_cache} . " from $host_name";
  0         0  
662             } else {
663 0         0 push @{$self->{o}}, "X-Cache: MISS from $host_name";
  0         0  
664             }
665 0         0 push @{$self->{o}}, "X-Cache-Lookup: MISS from $host_name";
  0         0  
666             } else {
667 0 0       0 if(defined($self->{x_cache})) {
668 0         0 push @{$self->{o}}, 'X-Cache: ' . $self->{x_cache};
  0         0  
669             } else {
670 0         0 push @{$self->{o}}, 'X-Cache: MISS';
  0         0  
671             }
672 0         0 push @{$self->{o}}, 'X-Cache-Lookup: MISS';
  0         0  
673             }
674 0         0 push @{$self->{o}}, "X-FCGI-Buffer-$VERSION: Miss";
  0         0  
675             }
676             # We don't need it any more, so give Perl a chance to
677             # tidy it up seeing as we're in the destructor
678 0         0 delete $self->{cache};
679             } elsif($self->{info}) {
680 26         88 my $host_name = $self->{info}->host_name();
681 26         257 push @{$self->{o}}, ("X-Cache: MISS from $host_name", "X-Cache-Lookup: MISS from $host_name");
  26         124  
682 26 50       66 if($self->{generate_last_modified}) {
683 26 100       61 if(my $age = $self->_my_age()) {
684 8         16 push @{$self->{o}}, 'Last-Modified: ' . HTTP::Date::time2str($age);
  8         51  
685             }
686             }
687 26 50 66     282 if($ENV{'HTTP_IF_MODIFIED_SINCE'} && ($self->{status} != 304) && $self->{generate_304}) {
      66        
688             $self->_check_modified_since({
689 3         11 since => $ENV{'HTTP_IF_MODIFIED_SINCE'},
690             modified => $self->_my_age()
691             });
692             }
693 26 50 33     88 if($self->_save_to($unzipped_body, $dbh) && $encoding) {
694 0         0 $self->_compress({ encoding => $encoding });
695             }
696             } else {
697 8         15 push @{$self->{o}}, ('X-Cache: MISS', 'X-Cache-Lookup: MISS');
  8         29  
698             }
699              
700             # if($self->{generate_etag} && ((!defined($headers)) || ($headers !~ /^ETag: /m))) {
701             # if((!defined($self->{etag})) &&
702             # (($self->{status} == 200) || $self->{status} == 304) &&
703             # $self->{body} && (!$ENV{'NO_CACHE'}) &&
704             # !$self->is_cached()) {
705             # unless($self->{_encode_loaded}) {
706             # require Encode;
707             # $self->{_encode_loaded} = 1;
708             # }
709             # $self->{etag} = '"' . Digest::MD5->new->add(Encode::encode_utf8($unzipped_body))->hexdigest() . '"';
710             # }
711             # if(defined($self->{etag})) {
712             # push @{$self->{o}}, "ETag: $self->{etag}";
713             # if($self->{logger}) {
714             # $self->{logger}->debug("Set ETag to $self->{etag}");
715             # }
716             # } else {
717             # open(my $fout, '>>', '/tmp/FCGI-bug');
718             # print $fout "BUG: ETag not generated, status $self->{status}:\n",
719             # "$headers\n",
720             # 'x' x 40, "\n",
721             # # defined($self->{body}) ? $self->{body} : "body is empty\n",
722             # defined($unzipped_body) ? "$unzipped_body\n" : "body is empty\n",
723             # 'x' x 40,
724             # "\n";
725             # print $fout "ENV:\n";
726             # while(my ($key, $value) = each %ENV) {
727             # print $fout "$key = $value\n";
728             # }
729             # close $fout;
730             # $self->{logger}->warn("BUG: ETag not generated, status $self->{status}");
731             # }
732             # }
733 34 100 33     168 if($self->{generate_etag} && ((!defined($headers)) || ($headers !~ /^ETag: /m))) {
      66        
734 11 50 0     30 if(defined($self->{etag})) {
    0 0        
      0        
      0        
      0        
735 11         20 push @{$self->{o}}, "ETag: $self->{etag}";
  11         35  
736 11 50       31 if($self->{logger}) {
737 0         0 $self->{logger}->debug("Set ETag to $self->{etag}");
738             }
739             } elsif($self->{logger} && (($self->{status} == 200) || $self->{status} == 304) && $self->{body} && (!$ENV{'NO_CACHE'}) && !$self->is_cached()) {
740             # $self->{logger}->warn("BUG: ETag not generated, status $self->{status}");
741             # open(my $fout, '>>', '/tmp/FCGI-bug');
742             # print $fout "BUG: ETag not generated, status $self->{status}:\n",
743             # $headers,
744             # 'x' x 40,
745             # defined($self->{body}) ? $self->{body} : "body is empty\n",
746             # 'x' x 40,
747             # "\n";
748             # print $fout "ENV:\n";
749             # while(my ($key, $value) = each %ENV) {
750             # print $fout "$key = $value\n;
751             # }
752             # print $fout 'x' x 40, "\n";
753             # close $fout;
754             # $self->{logger}->warn("BUG: ETag not generated, status $self->{status}");
755             }
756             }
757              
758 34         58 my $body_length;
759 34 100       83 if(defined($self->{body})) {
760 32 100       105 if(utf8::is_utf8($self->{body})) {
761 1         6 utf8::encode($self->{body});
762             }
763 32         56 $body_length = length($self->{body});
764             } else {
765 2         7 $body_length = 0;
766             }
767              
768 34 50 33     150 if(defined($headers) && length($headers)) {
769             # Put the original headers first, then those generated within
770             # FCGI::Buffer
771 34         54 unshift @{$self->{o}}, split(/\r\n/, $headers);
  34         190  
772 34 100 100     170 if($self->{body} && $self->{send_body}) {
773 30 50       48 unless(grep(/^Content-Length: \d/, @{$self->{o}})) {
  30         148  
774 30         49 push @{$self->{o}}, "Content-Length: $body_length";
  30         108  
775             }
776             }
777 34 100       55 unless(grep(/^Status: \d/, @{$self->{o}})) {
  34         153  
778 31         1702 require HTTP::Status;
779 31         26567 HTTP::Status->import();
780              
781 31         83 push @{$self->{o}}, 'Status: ' . $self->{status} . ' ' . HTTP::Status::status_message($self->{status});
  31         179  
782             }
783             } else {
784 0         0 push @{$self->{o}}, "X-FCGI-Buffer-$VERSION: No headers";
  0         0  
785             }
786              
787 34 100 100     283 if($body_length && $self->{send_body}) {
788 30         55 push @{$self->{o}}, ('', $self->{body});
  30         66  
789             }
790              
791             # XXXXXXXXXXXXXXXXXXXXXXX
792 34 50       58 if(0) {
793             # This code helps to debug Wide character prints
794             my $wideCharWarningsIssued = 0;
795             my $widemess;
796             $SIG{__WARN__} = sub {
797 0     0   0 $wideCharWarningsIssued += "@_" =~ /Wide character in .../;
798 0         0 $widemess = "@_";
799 0 0       0 if($self->{logger}) {
800 0         0 $self->{logger}->fatal($widemess);
801 0         0 my $i = 1;
802 0         0 $self->{logger}->trace('Stack Trace');
803 0         0 while((my @call_details = (caller($i++)))) {
804 0         0 $self->{logger}->trace($call_details[1], ':', $call_details[2], ' in function ', $call_details[3]);
805             }
806             }
807 0         0 CORE::warn(@_); # call the builtin warn as usual
808             };
809              
810             if(scalar @{$self->{o}}) {
811             print join("\r\n", @{$self->{o}});
812             if($wideCharWarningsIssued) {
813             my $mess = join("\r\n", @{$self->{o}});
814             $mess =~ /[^\x00-\xFF]/;
815             open(my $fout, '>>', '/tmp/NJH');
816             print $fout "$widemess:\n",
817             $mess,
818             'x' x 40,
819             "\n";
820             close $fout;
821             }
822             }
823 0         0 } elsif(scalar @{$self->{o}}) {
  34         86  
824 34         61 print join("\r\n", @{$self->{o}});
  34         1728  
825             }
826             # XXXXXXXXXXXXXXXXXXXXXXX
827              
828 34 100 100     713 if((!$self->{send_body}) || !defined($self->{body})) {
829 4         100 print "\r\n\r\n";
830             }
831             }
832              
833             sub _generate_etag {
834 11     11   16 my $self = shift;
835              
836 11 50       28 return if defined($self->{'etag'});
837              
838 11 50       27 if(!defined($self->{_encode_loaded})) {
839             # encode to avoid "Wide character in subroutine entry"
840 11         65 require Encode;
841 11         30 $self->{_encode_loaded} = 1;
842             }
843 11         152 $self->{etag} = '"' . Digest::MD5->new->add(Encode::encode_utf8($self->{body}))->hexdigest() . '"';
844 11 50       71 if($self->{'logger'}) {
845 0         0 $self->{'logger'}->debug('Etag = ', $self->{'etag'});
846             }
847             }
848              
849             sub _check_modified_since {
850 3     3   6 my $self = shift;
851              
852 3 50       11 if($self->{logger}) {
853 0         0 $self->{logger}->trace('In _check_modified_since');
854             }
855              
856 3 50       10 if(!$self->{generate_304}) {
857 0         0 return;
858             }
859 3         6 my $params = shift;
860              
861 3 50       11 if(!defined($$params{since})) {
862 0         0 return;
863             }
864 3         12 my $s = HTTP::Date::str2time($$params{since});
865 3 100       317 if(!defined($s)) {
866 1 50       4 if($self->{logger}) {
867 0         0 $self->{logger}->info("$$params{since} is not a valid date");
868             }
869 1         5 return;
870             }
871              
872 2         8 my $age = $self->_my_age();
873 2 50       7 if(!defined($age)) {
874 2 50       8 if($self->{logger}) {
875 0         0 $self->{logger}->info("Can't determine my age");
876             }
877 2         6 return;
878             }
879 0 0       0 if($age > $s) {
880 0 0       0 if($self->{logger}) {
881 0         0 $self->{logger}->debug('_check_modified_since: script has been modified');
882             }
883             # Script has been updated so it may produce different output
884 0         0 return;
885             }
886              
887 0 0       0 if($self->{logger}) {
888 0         0 $self->{logger}->debug("_check_modified_since: Compare $$params{modified} with $s");
889             }
890 0 0       0 if($$params{modified} <= $s) {
891 0         0 push @{$self->{o}}, 'Status: 304 Not Modified';
  0         0  
892 0         0 $self->{status} = 304;
893 0         0 $self->{send_body} = 0;
894 0 0       0 if($self->{logger}) {
895 0         0 $self->{logger}->debug('Set status to 304');
896             }
897             }
898             }
899              
900             # Reduce output, e.g. remove superfluous white-space.
901             sub _optimise_content {
902 27     27   45 my $self = shift;
903              
904             # FIXME: regex bad, HTML parser good
905             # Regexp::List - wow!
906 27         1125 $self->{body} =~ s/(((\s+|\r)\n|\n(\s+|\+)))/\n/g;
907             # $self->{body} =~ s/\r\n/\n/gs;
908             # $self->{body} =~ s/\s+\n/\n/gs;
909             # $self->{body} =~ s/\n+/\n/gs;
910             # $self->{body} =~ s/\n\s+|\s+\n/\n/g;
911 27         108 $self->{body} =~ s/\<\/div\>\s+\<div/\<\/div\>\<div/gis;
912             # $self->{body} =~ s/\<\/p\>\s\<\/div/\<\/p\>\<\/div/gis;
913             # $self->{body} =~ s/\<div\>\s+/\<div\>/gis; # Remove spaces after <div>
914 27         8881 $self->{body} =~ s/(<div>\s+|\s+<div>)/<div>/gis;
915 27         98 $self->{body} =~ s/\s+<\/div\>/\<\/div\>/gis; # Remove spaces before </div>
916 27         518 $self->{body} =~ s/\s+\<p\>|\<p\>\s+/\<p\>/im; # TODO <p class=
917 27         520 $self->{body} =~ s/\s+\<\/p\>|\<\/p\>\s+/\<\/p\>/gis;
918 27         85 $self->{body} =~ s/<html>\s+<head>/<html><head>/is;
919 27         133 $self->{body} =~ s/\s*<\/head>\s+<body>\s*/<\/head><body>/is;
920 27         74 $self->{body} =~ s/<html>\s+<body>/<html><body>/is;
921 27         72 $self->{body} =~ s/<body>\s+/<body>/is;
922 27         87 $self->{body} =~ s/\s+\<\/html/\<\/html/is;
923 27         87 $self->{body} =~ s/\s+\<\/body/\<\/body/is;
924 27         148 $self->{body} =~ s/\s(\<.+?\>\s\<.+?\>)/$1/;
925             # $self->{body} =~ s/(\<.+?\>\s\<.+?\>)\s/$1/g;
926 27         74 $self->{body} =~ s/\<p\>\s/\<p\>/gi;
927 27         63 $self->{body} =~ s/\<\/p\>\s\<p\>/\<\/p\>\<p\>/gi;
928 27         63 $self->{body} =~ s/\<\/tr\>\s\<tr\>/\<\/tr\>\<tr\>/gi;
929 27         97 $self->{body} =~ s/\<\/td\>\s\<\/tr\>/\<\/td\>\<\/tr\>/gi;
930 27         113 $self->{body} =~ s/\<\/td\>\s*\<td\>/\<\/td\>\<td\>/gis;
931 27         71 $self->{body} =~ s/\<\/tr\>\s\<\/table\>/\<\/tr\>\<\/table\>/gi;
932 27         84 $self->{body} =~ s/\<br\s?\/?\>\s?\<p\>/\<p\>/gi;
933 27         62 $self->{body} =~ s/\<br\>\s+/\<br\>/gi;
934 27         88 $self->{body} =~ s/\s+\<br/\<br/gi;
935 27         60 $self->{body} =~ s/\<br\s?\/\>\s/\<br \/\>/gi;
936 27         138 $self->{body} =~ s/[ \t]+/ /gs; # Remove duplicate space, don't use \s+ it breaks JavaScript
937 27         85 $self->{body} =~ s/\s\<p\>/\<p\>/gi;
938 27         90 $self->{body} =~ s/\s\<script/\<script/gi;
939 27         453 $self->{body} =~ s/(<script>\s|\s<script>)/<script>/gis;
940 27         415 $self->{body} =~ s/(<\/script>\s|\s<\/script>)/<\/script>/gis;
941 27         76 $self->{body} =~ s/\<td\>\s/\<td\>/gi;
942 27         102 $self->{body} =~ s/\s+\<a\shref="(.+?)"\>\s?/ <a href="$1">/gis;
943 27         117 $self->{body} =~ s/\s?<a\shref=\s"(.+?)"\>/ <a href="$1">/gis;
944 27         79 $self->{body} =~ s/\s+<\/a\>\s+/<\/a> /gis;
945 27         692 $self->{body} =~ s/(\s?<hr>\s+|\s+<hr>\s?)/<hr>/gis;
946             # $self->{body} =~ s/\s<hr>/<hr>/gis;
947             # $self->{body} =~ s/<hr>\s/<hr>/gis;
948 27         64 $self->{body} =~ s/<\/li>\s+<li>/<\/li><li>/gis;
949 27         57 $self->{body} =~ s/<\/li>\s+<\/ul>/<\/li><\/ul>/gis;
950 27         57 $self->{body} =~ s/<ul>\s+<li>/<ul><li>/gis;
951 27         92 $self->{body} =~ s/\s+<\/li>/<\/li>/gis;
952 27         63 $self->{body} =~ s/\<\/option\>\s+\<option/\<\/option\>\<option/gis;
953 27         77 $self->{body} =~ s/<title>\s*(.+?)\s*<\/title>/<title>$1<\/title>/is;
954 27         91 $self->{body} =~ s/<\/center>\s+<center>/ /gis;
955             }
956              
957             # Create a key for the cache
958             sub _generate_key {
959 0     0   0 my $self = shift;
960 0 0       0 if($self->{cache_key}) {
961 0         0 return $self->{cache_key};
962             }
963 0 0       0 unless(defined($self->{info})) {
964 0         0 $self->{info} = CGI::Info->new({ cache => $self->{cache} });
965             }
966              
967 0         0 my $key = $self->{info}->browser_type() . '::' . $self->{info}->domain_name() . '::' . $self->{info}->script_name() . '::' . $self->{info}->as_string();
968              
969 0 0       0 if($self->{lingua}) {
970 0         0 $key .= '::' . $self->{lingua}->language();
971             }
972 0 0       0 if($ENV{'HTTP_COOKIE'}) {
973             # Different states of the client are stored in different caches
974             # Don't put different Google Analytics in different caches, and anyway they
975             # would be wrong
976 0         0 foreach my $cookie(split(/;/, $ENV{'HTTP_COOKIE'})) {
977 0 0       0 unless($cookie =~ /^__utm[abcz]/) {
978 0         0 $key .= "::$cookie";
979             }
980             }
981             }
982              
983             # Honour the Vary headers
984 0         0 my $headers = $self->{'headers'};
985 0 0 0     0 if($headers && ($headers =~ /^Vary: .*$/m)) {
986 0 0       0 if(defined($self->{logger})) {
987 0         0 $self->{logger}->debug('Found Vary header');
988             }
989 0         0 foreach my $h1(split(/\r?\n/, $headers)) {
990 0         0 my ($h1_name, $h1_value) = split /\:\s*/, $h1, 2;
991 0 0       0 if(lc($h1_name) eq 'vary') {
992 0         0 foreach my $h2(split(/\r?\n/, $headers)) {
993 0         0 my ($h2_name, $h2_value) = split /\:\s*/, $h2, 2;
994 0 0       0 if($h2_name eq $h1_value) {
995 0         0 $key .= "::$h2_value";
996 0         0 last;
997             }
998             }
999             }
1000             }
1001             }
1002 0         0 $key =~ s/\//::/g;
1003 0         0 $key =~ s/::::/::/g;
1004 0         0 $key =~ s/::$//;
1005 0 0       0 if(defined($self->{logger})) {
1006 0         0 $self->{logger}->trace("Returning $key");
1007             }
1008 0         0 $self->{cache_key} = $key;
1009 0         0 return $key;
1010             }
1011              
1012             =head2 init
1013              
1014             Set various options and override default values.
1015              
1016             # Put this toward the top of your program before you do anything
1017             # By default, generate_tag, generate_304 and compress_content are ON,
1018             # optimise_content and lint_content are OFF. Set optimise_content to 2 to
1019             # do aggressive JavaScript optimisations which may fail.
1020             use FCGI::Buffer;
1021              
1022             my $buffer = FCGI::Buffer->new()->init({
1023             generate_etag => 1, # make good use of client's cache
1024             generate_last_modified => 1, # more use of client's cache
1025             compress_content => 1, # if gzip the output
1026             optimise_content => 0, # optimise your program's HTML, CSS and JavaScript
1027             cache => CHI->new(driver => 'File'), # cache requests
1028             cache_key => 'string', # key for the cache
1029             cache_age => '10 minutes', # how long to store responses in the cache
1030             logger => $self->{logger},
1031             lint_content => 0, # Pass through HTML::Lint
1032             generate_304 => 1, # When appropriate, generate 304: Not modified
1033             save_to => { directory => '/var/www/htdocs/save_to', ttl => 600, create_table => 1 },
1034             info => CGI::Info->new(),
1035             lingua => CGI::Lingua->new(),
1036             });
1037              
1038             If no cache_key is given, one will be generated which may not be unique.
1039             The cache_key should be a unique value dependent upon the values set by the
1040             browser.
1041              
1042             The cache object will be an object that understands get_object(),
1043             set(), remove() and created_at() messages, such as an L<CHI> object. It is
1044             used as a server-side cache to reduce the need to rerun database accesses.
1045              
1046             Items stay in the server-side cache by default for 10 minutes.
1047             This can be overridden by the cache_control HTTP header in the request, and
1048             the default can be changed by the cache_age argument to init().
1049              
1050             Save_to is feature which stores output of dynamic pages to your
1051             htdocs tree and replaces future links that point to that page with static links
1052             to avoid going through CGI at all.
1053             Ttl is set to the number of seconds that the static pages are deemed to
1054             be live for, the default is 10 minutes.
1055             If set to 0, the page is live forever.
1056             To enable save_to, a info and lingua arguments must also be given.
1057             It works best when cache is also given.
1058             Only use where output is guaranteed to be the same with a given set of arguments
1059             (the same criteria for enabling generate_304).
1060             You can turn it off on a case by case basis thus:
1061              
1062             my $params = CGI::Info->new()->params();
1063             if($params->{'send_private_email'}) {
1064             $buffer->init('save_to' => undef);
1065             }
1066              
1067             Info is an optional argument to give information about the FCGI environment, e.g.
1068             a L<CGI::Info> object.
1069              
1070             Logger will be an object that understands debug() such as an L<Log::Log4perl>
1071             object.
1072              
1073             To generate a last_modified header, you must give a cache object.
1074              
1075             Init allows a reference of the options to be passed. So both of these work:
1076             use FCGI::Buffer;
1077             #...
1078             my $buffer = FCGI::Buffer->new();
1079             $b->init(generate_etag => 1);
1080             $b->init({ generate_etag => 1, info => CGI::Info->new() });
1081              
1082             Generally speaking, passing by reference is better since it copies less on to
1083             the stack.
1084              
1085             If you give a cache to init() then later give cache => undef,
1086             the server side cache is no longer used.
1087             This is useful when you find an error condition when creating your HTML
1088             and decide that you no longer wish to store the output in the cache.
1089              
1090             =cut
1091              
1092             sub init {
1093 32     32 1 8320 my $self = shift;
1094 32 100       119 my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  22         87  
1095              
1096             # Safe options - can be called at any time
1097 32 100       111 if(defined($params{generate_etag})) {
1098 9         22 $self->{generate_etag} = $params{generate_etag};
1099             }
1100 32 50       80 if(defined($params{generate_last_modified})) {
1101 0         0 $self->{generate_last_modified} = $params{generate_last_modified};
1102             }
1103 32 100       74 if(defined($params{compress_content})) {
1104 1         4 $self->{compress_content} = $params{compress_content};
1105             }
1106 32 100       79 if(defined($params{optimise_content})) {
1107 31         65 $self->{optimise_content} = $params{optimise_content};
1108             }
1109 32 100       80 if(defined($params{lint_content})) {
1110 4         7 $self->{lint_content} = $params{lint_content};
1111             }
1112 32 100       72 if(defined($params{logger})) {
1113 1         4 $self->{logger} = $params{logger};
1114             }
1115 32 50       74 if(defined($params{lingua})) {
1116 0         0 $self->{lingua} = $params{lingua};
1117             }
1118              
1119 32 100 66     147 if(defined($params{save_to}) && $self->can_cache()) {
    50 33        
1120 1 50       15 if(my $dir = $params{'save_to'}->{'directory'}) {
1121 1 50       20 if(! -d $dir) {
1122 1         12 mkdir $dir;
1123 1 50       17 if(! -d $dir) {
1124 1         8 Carp::carp("$dir isn't a directory");
1125 1         369 return;
1126             }
1127             }
1128 0 0       0 if(! -w $dir) {
1129 0         0 Carp::carp("$dir isn't writeable");
1130 0         0 return;
1131             }
1132             }
1133 0         0 $self->{save_to} = $params{save_to};
1134 0 0       0 if(!exists($params{save_to}->{'ttl'})) {
1135 0         0 $self->{save_to}->{'ttl'} = 600;
1136             }
1137             } elsif(exists($params{'save_to'}) && !defined($params{'save_to'})) {
1138 0         0 delete $self->{'save_to'};
1139             }
1140 31 100       63 if(defined($params{generate_304})) {
1141 3         5 $self->{generate_304} = $params{generate_304};
1142             }
1143 31 100 66     96 if(defined($params{info}) && (!defined($self->{info}))) {
1144 4         11 $self->{info} = $params{info};
1145             }
1146              
1147             # Unsafe options - must be called before output has been started
1148 31         114 my $pos = $self->{buf}->getpos;
1149 31 50       351 if($pos > 0) {
1150 0 0       0 if(defined($self->{logger})) {
1151 0         0 my @call_details = caller(0);
1152 0         0 $self->{logger}->warn("Too late to call init, $pos characters have been printed, caller line $call_details[2] of $call_details[1]");
1153             } else {
1154             # Must do Carp::carp instead of carp for Test::Carp
1155 0         0 Carp::carp "Too late to call init, $pos characters have been printed";
1156             }
1157             }
1158 31 50 33     96 if(exists($params{cache}) && $self->can_cache()) {
1159 0 0       0 if(defined($ENV{'HTTP_CACHE_CONTROL'})) {
1160 0         0 my $control = $ENV{'HTTP_CACHE_CONTROL'};
1161 0 0       0 if(defined($self->{logger})) {
1162 0         0 $self->{logger}->debug("cache_control = $control");
1163             }
1164 0 0       0 if($control =~ /^max-age\s*=\s*(\d+)$/) {
1165             # There is an argument not to do this
1166             # since one client will affect others
1167 0         0 $self->{cache_age} = "$1 seconds";
1168 0 0       0 if(defined($self->{logger})) {
1169 0         0 $self->{logger}->debug("cache_age = $self->{cache_age}");
1170             }
1171             }
1172             }
1173 0   0     0 $self->{cache_age} ||= $params{cache_age};
1174 0 0 0     0 if((!defined($params{cache})) && defined($self->{cache})) {
1175 0 0       0 if(defined($self->{logger})) {
1176 0 0       0 if($self->{cache_key}) {
1177 0         0 $self->{logger}->debug('disabling cache ', $self->{cache_key});
1178             } else {
1179 0         0 $self->{logger}->debug('disabling cache');
1180             }
1181             }
1182 0         0 delete $self->{cache};
1183             } else {
1184 0         0 $self->{cache} = $params{cache};
1185             }
1186 0 0       0 if(defined($params{cache_key})) {
1187 0         0 $self->{cache_key} = $params{cache_key};
1188             }
1189             }
1190              
1191 31         111 return $self;
1192             }
1193              
1194             sub import {
1195             # my $class = shift;
1196 11     11   190 shift;
1197              
1198 11 50       2189 return unless scalar(@_);
1199              
1200 0         0 init(@_);
1201             }
1202              
1203             =head2 set_options
1204              
1205             Synonym for init, kept for historical reasons.
1206              
1207             =cut
1208              
1209             sub set_options {
1210 11     11 1 7146 my $self = shift;
1211 11 100       48 my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  8         39  
1212              
1213 11         39 $self->init(\%params);
1214             }
1215              
1216             =head2 can_cache
1217              
1218             Returns true if the server is allowed to store the results locally.
1219             This is the value of X-Cache in the returned header.
1220              
1221             =cut
1222              
1223             sub can_cache {
1224 9     9 1 2770 my $self = shift;
1225              
1226 9 100       47 if(defined($self->{x_cache})) {
1227 1         5 return ($self->{x_cache} eq 'HIT');
1228             }
1229 8 50 33     69 if(defined($ENV{'NO_CACHE'}) || defined($ENV{'NO_STORE'})) {
1230 0         0 $self->{x_cache} = 'MISS';
1231 0         0 return 0;
1232             }
1233 8 50       32 if(defined($ENV{'HTTP_CACHE_CONTROL'})) {
1234 0         0 my $control = $ENV{'HTTP_CACHE_CONTROL'};
1235 0 0       0 if(defined($self->{logger})) {
1236 0         0 $self->{logger}->debug("cache_control = $control");
1237             }
1238             # TODO: check Authorization header not present
1239 0 0 0     0 if(($control eq 'no-store') ||
      0        
      0        
1240             ($control eq 'no-cache') ||
1241             ($control eq 'max-age=0') ||
1242             ($control eq 'private')) {
1243 0         0 $self->{x_cache} = 'MISS';
1244 0         0 return 0;
1245             }
1246             }
1247 8         22 $self->{x_cache} = 'HIT';
1248 8         38 return 1;
1249             }
1250              
1251             =head2 is_cached
1252              
1253             Returns true if the output is cached. If it is then it means that all of the
1254             expensive routines in the FCGI script can be by-passed because we already have
1255             the result stored in the cache.
1256              
1257             # Put this toward the top of your program before you do anything
1258              
1259             # Example key generation - use whatever you want as something
1260             # unique for this call, so that subsequent calls with the same
1261             # values match something in the cache
1262             use CGI::Info;
1263             use CGI::Lingua;
1264             use FCGI::Buffer;
1265              
1266             my $i = CGI::Info->new();
1267             my $l = CGI::Lingua->new(supported => ['en']);
1268              
1269             # To use server side caching you must give the cache argument, however
1270             # the cache_key argument is optional - if you don't give one then one will
1271             # be generated for you
1272             my $buffer = FCGI::Buffer->new();
1273             if($buffer->can_cache()) {
1274             $buffer->init(
1275             cache => CHI->new(driver => 'File'),
1276             cache_key => $i->domain_name() . '/' . $i->script_name() . '/' . $i->as_string() . '/' . $l->language()
1277             );
1278             if($buffer->is_cached()) {
1279             # Output will be retrieved from the cache and sent automatically
1280             exit;
1281             }
1282             }
1283             # Not in the cache, so now do our expensive computing to generate the
1284             # results
1285             print "Content-type: text/html\n";
1286             # ...
1287              
1288             =cut
1289              
1290             sub is_cached {
1291 12     12 1 1485 my $self = shift;
1292              
1293 12 50       61 unless($self->{cache}) {
1294 12 50       40 if($self->{logger}) {
1295 0         0 $self->{logger}->debug("is_cached: cache hasn't been enabled");
1296 0         0 my $i = 0;
1297 0         0 while((my @call_details = (caller($i++)))) {
1298 0         0 $self->{logger}->debug($call_details[1], ':', $call_details[2], ' calling function ', $call_details[3]);
1299             }
1300             }
1301 12         66 return 0;
1302             }
1303              
1304 0         0 my $key = $self->_generate_key();
1305              
1306 0 0       0 if($self->{logger}) {
1307 0         0 $self->{logger}->debug("is_cached: looking for key = $key");
1308             }
1309 0         0 $self->{cobject} = $self->{cache}->get_object($key);
1310 0 0       0 unless($self->{cobject}) {
1311 0 0       0 if($self->{logger}) {
1312 0         0 $self->{logger}->debug('not found in cache');
1313             }
1314 0         0 return 0;
1315             }
1316 0 0       0 unless($self->{cobject}->value($key)) {
1317 0 0       0 if($self->{logger}) {
1318 0         0 $self->{logger}->warn('is_cached: object is in the cache but not the data');
1319             }
1320 0         0 delete $self->{cobject};
1321 0         0 return 0;
1322             }
1323              
1324             # If the script has changed, don't use the cache since we may produce
1325             # different output
1326 0         0 my $age = $self->_my_age();
1327 0 0       0 unless(defined($age)) {
1328 0 0       0 if($self->{logger}) {
1329 0         0 $self->{logger}->debug("Can't determine script's age");
1330             }
1331             # Can't determine the age. Play it safe an assume we're not
1332             # cached
1333 0         0 delete $self->{cobject};
1334 0         0 return 0;
1335             }
1336 0 0       0 if($age > $self->{cobject}->created_at()) {
1337             # Script has been updated so it may produce different output
1338 0 0       0 if($self->{logger}) {
1339 0         0 $self->{logger}->debug('Script has been updated');
1340             }
1341 0         0 delete $self->{cobject};
1342             # Nothing will be in date and all new searches would miss
1343             # anyway, so may as well clear it all
1344             # FIXME: RT104471
1345             # $self->{cache}->clear();
1346 0         0 return 0;
1347             }
1348 0 0       0 if($self->{logger}) {
1349 0         0 $self->{logger}->debug('Script is in the cache');
1350             }
1351 0         0 return 1;
1352             }
1353              
1354             sub _my_age {
1355 31     31   46 my $self = shift;
1356              
1357 31 50       69 if($self->{script_mtime}) {
1358 0         0 return $self->{script_mtime};
1359             }
1360 31 50       75 unless(defined($self->{info})) {
1361 0 0       0 if($self->{cache}) {
1362 0         0 $self->{info} = CGI::Info->new({ cache => $self->{cache} });
1363             } else {
1364 0         0 $self->{info} = CGI::Info->new();
1365             }
1366             }
1367              
1368 31         89 my $path = $self->{info}->script_path();
1369 31 50       5071 unless(defined($path)) {
1370 0         0 return;
1371             }
1372              
1373 31         441 my @statb = stat($path);
1374 31         122 $self->{script_mtime} = $statb[9];
1375 31         141 return $self->{script_mtime};
1376             }
1377              
1378             sub _should_gzip {
1379 34     34   61 my $self = shift;
1380              
1381 34 100 100     234 if($self->{compress_content} && ($ENV{'HTTP_ACCEPT_ENCODING'} || $ENV{'HTTP_TE'})) {
      100        
1382 2 50       8 if(defined($self->{content_type})) {
1383 2         5 my @content_type = @{$self->{content_type}};
  2         7  
1384 2 50       9 if($content_type[0] ne 'text') {
1385 0         0 return '';
1386             }
1387             }
1388 2 100       11 my $accept = lc($ENV{'HTTP_ACCEPT_ENCODING'} ? $ENV{'HTTP_ACCEPT_ENCODING'} : $ENV{'HTTP_TE'});
1389 2         16 foreach my $method(split(/,\s?/, $accept)) {
1390 2 50 66     14 if(($method eq 'gzip') || ($method eq 'x-gzip') || ($method eq 'br')) {
      66        
1391 2         8 return $method;
1392             }
1393             }
1394             }
1395              
1396 32         81 return '';
1397             }
1398              
1399             sub _set_content_type {
1400 34     34   59 my $self = shift;
1401 34         58 my $headers = shift;
1402              
1403 34         122 foreach my $header (split(/\r?\n/, $headers)) {
1404 34         162 my ($header_name, $header_value) = split /\:\s*/, $header, 2;
1405 34 100       133 if (lc($header_name) eq 'content-type') {
1406 31         98 my @content_type = split /\//, $header_value, 2;
1407 31         96 $self->{content_type} = \@content_type;
1408 31         96 return;
1409             }
1410             }
1411             }
1412              
1413             sub _compress()
1414             {
1415 32     32   58 my $self = shift;
1416 32 50       97 my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  32         124  
1417              
1418 32         69 my $encoding = $params{encoding};
1419              
1420 32 100 66     100 if((length($encoding) == 0) || (length($self->{body}) < MIN_GZIP_LEN)) {
1421 30         84 return;
1422             }
1423              
1424 2 100       9 if($encoding eq 'gzip') {
    50          
1425 1         1835 require Compress::Zlib;
1426 1         67158 Compress::Zlib->import;
1427              
1428             # Avoid 'Wide character in memGzip'
1429 1 50       13 unless($self->{_encode_loaded}) {
1430 1         571 require Encode;
1431 1         10341 $self->{_encode_loaded} = 1;
1432             }
1433 1         9 my $nbody = Compress::Zlib::memGzip(\Encode::encode_utf8($self->{body}));
1434 1 50       1084 if(length($nbody) < length($self->{body})) {
1435 0         0 $self->{body} = $nbody;
1436 0 0       0 unless(grep(/^Content-Encoding: gzip/, @{$self->{o}})) {
  0         0  
1437 0         0 push @{$self->{o}}, 'Content-Encoding: gzip';
  0         0  
1438             }
1439 0 0       0 unless(grep(/^Vary: Accept-Encoding/, @{$self->{o}})) {
  0         0  
1440 0         0 push @{$self->{o}}, 'Vary: Accept-Encoding';
  0         0  
1441             }
1442             }
1443             } elsif($encoding eq 'br') {
1444 1         425 require IO::Compress::Brotli;
1445 1         1526 IO::Compress::Brotli->import();
1446              
1447 1 50       11 unless($self->{_encode_loaded}) {
1448 0         0 require Encode;
1449 0         0 $self->{_encode_loaded} = 1;
1450             }
1451 1         2153 my $nbody = IO::Compress::Brotli::bro(Encode::encode_utf8($self->{body}));
1452 1 50       11 if(length($nbody) < length($self->{body})) {
1453 1         3 $self->{body} = $nbody;
1454 1 50       3 unless(grep(/^Content-Encoding: br/, @{$self->{o}})) {
  1         6  
1455 1         2 push @{$self->{o}}, 'Content-Encoding: br';
  1         4  
1456             }
1457 1 50       3 unless(grep(/^Vary: Accept-Encoding/, @{$self->{o}})) {
  1         6  
1458 1         2 push @{$self->{o}}, 'Vary: Accept-Encoding';
  1         6  
1459             }
1460             }
1461             }
1462             }
1463              
1464             sub _check_if_none_match {
1465 2     2   3 my $self = shift;
1466              
1467 2 50       17 if($self->{logger}) {
1468 0         0 $self->{logger}->debug("Compare $ENV{HTTP_IF_NONE_MATCH} with $self->{etag}");
1469             }
1470 2 50       8 if($ENV{'HTTP_IF_NONE_MATCH'} eq $self->{etag}) {
1471 2         4 push @{$self->{o}}, 'Status: 304 Not Modified';
  2         9  
1472 2         4 $self->{send_body} = 0;
1473 2         5 $self->{status} = 304;
1474 2 50       6 if($self->{logger}) {
1475 0         0 $self->{logger}->debug('Set status to 304');
1476             }
1477 2         5 return 1;
1478             }
1479 0 0 0     0 if($self->{cache} && $self->{logger} && $self->{logger}->is_debug()) {
      0        
1480 0         0 my $cached_copy = $self->{cache}->get($self->_generate_key());
1481              
1482 0 0 0     0 if($cached_copy && $self->{body}) {
1483 0         0 require Text::Diff;
1484 0         0 Text::Diff->import();
1485              
1486 0         0 $cached_copy = Storable::thaw($cached_copy)->{body};
1487 0         0 my $diffs = diff(\$self->{body}, \$cached_copy);
1488 0         0 $self->{logger}->debug('diffs: ', $diffs);
1489             } else {
1490 0         0 $self->{logger}->debug('Nothing to compare');
1491             }
1492             }
1493 0         0 return 0;
1494             }
1495              
1496             # replace dynamic links with static links
1497             sub _save_to {
1498 26     26   68 my ($self, $unzipped_body, $dbh) = @_;
1499              
1500 26 0 33     109 return 0 unless($dbh && $self->{info} && (my $request_uri = $ENV{'REQUEST_URI'}));
      0        
1501 0 0         return 0 if(!defined($unzipped_body));
1502              
1503 0           my $query;
1504 0           my $copy = $unzipped_body;
1505 0           my $changes = 0;
1506 0           my $creation;
1507             my %seen_links;
1508 0           while($unzipped_body =~ /<a\shref="(.+?)"/gis) {
1509 0           my $link = $1;
1510 0 0         next if($seen_links{$link}); # Already updated in the copy
1511 0           $seen_links{$link} = 1;
1512 0           $link =~ tr/[\|;]/_/;
1513              
1514 0           my $search_uri = $link;
1515 0 0         if($search_uri =~ /^\?/) {
1516             # CGI script has links to itself
1517             # $search_uri = "${request_uri}${link}";
1518 0           my $r = $request_uri;
1519 0           $r =~ s/\?.*$//;
1520 0           $search_uri = "${r}$link";
1521             } else {
1522 0 0         next if($link =~ /^https?:\/\//); # FIXME: skips full URLs to ourself
1523             # Though optimise_content fixes that
1524 0 0         next if($link =~ /.html?$/);
1525 0 0         next if($link =~ /.jpg$/);
1526 0 0         next if($link =~ /.gif$/);
1527             }
1528 0 0         if($self->{save_to}->{ttl}) {
1529 0           $query = "SELECT DISTINCT path, creation FROM fcgi_buffer WHERE uri = ? AND language = ? AND browser_type = ? AND creation >= strftime('\%s','now') - " . $self->{save_to}->{ttl};
1530             } else {
1531 0           $query = "SELECT DISTINCT path, creation FROM fcgi_buffer WHERE uri = ? AND language = ? AND browser_type = ?";
1532             }
1533 0 0         if($self->{logger}) {
1534 0           $self->{logger}->debug("$query: $search_uri, ", $self->{lingua}->language(), ', ', $self->{info}->browser_type());
1535             }
1536 0 0         if(defined(my $sth = $dbh->prepare($query))) {
    0          
1537 0           $sth->execute($search_uri, $self->{lingua}->language(), $self->{info}->browser_type());
1538 0 0         if(my $href = $sth->fetchrow_hashref()) {
1539 0 0         if(my $path = $href->{'path'}) {
1540 0 0         if(-r $path) {
1541 0 0         if($self->{logger}) {
1542 0           $self->{logger}->debug("Changing links from $link to $path");
1543             }
1544 0           $link =~ s/\?/\\?/g;
1545 0           my $rootdir = $self->{info}->rootdir();
1546 0           $path = substr($path, length($rootdir));
1547 0           $changes += ($copy =~ s/<a\s+href="$link">/<a href="$path">/gis);
1548             # Find the first link that will expire and use that
1549 0 0 0       if((!defined($creation)) || ($href->{'creation'} < $creation)) {
1550 0           $creation = $href->{'creation'};
1551             }
1552             } else {
1553 0           $query = "DELETE FROM fcgi_buffer WHERE path = ?";
1554 0           $dbh->prepare($query)->execute($path);
1555 0 0         if($self->{logger}) {
1556 0           $self->{logger}->warn("Remove entry for non-existant file $path");
1557             }
1558             }
1559             }
1560             }
1561             } elsif($self->{logger}) {
1562 0           $self->{logger}->warn("failed to prepare '$query'");
1563             }
1564             }
1565 0           my $expiration = 0;
1566 0 0 0       if(defined($creation) && (my $ttl = $self->{save_to}->{ttl})) {
1567 0           $expiration = $creation + $ttl;
1568             }
1569 0 0 0       if($changes && (($expiration == 0) || ($expiration >= time))) {
    0 0        
      0        
1570 0 0         if($self->{logger}) {
1571             # $self->{logger}->debug("$changes links now point to static pages");
1572 0 0         if($changes == 1) {
1573 0 0         if($self->{'save_to'}->{'ttl'}) {
1574 0           $self->{logger}->info('1 link now points to a static page for ', $expiration - time, 's');
1575             } else {
1576 0           $self->{logger}->info('1 link now points to a static page');
1577             }
1578             } else {
1579 0           $self->{logger}->info("$changes links now point to static pages");
1580             }
1581             }
1582 0           $unzipped_body = $copy;
1583 0           $self->{'body'} = $unzipped_body;
1584 0 0         if(my $ttl = $self->{save_to}->{ttl}) {
1585 0           push @{$self->{o}}, 'Expires: ' . HTTP::Date::time2str($creation + $ttl);
  0            
1586             }
1587             } elsif($expiration && ($expiration < time)) {
1588             # Delete the save_to files
1589 0 0         if($self->{save_to}->{ttl}) {
1590 0           $query = "SELECT path FROM fcgi_buffer WHERE creation < strftime('\%s','now') - " . $self->{save_to}->{ttl};
1591             } else {
1592 0           $query = 'SELECT path FROM fcgi_buffer'; # Hmm, I suspect this is overkill
1593             }
1594 0           my $sth = $dbh->prepare($query);
1595 0           $sth->execute();
1596 0           while(my $href = $sth->fetchrow_hashref()) {
1597 0 0         if(my $path = $href->{'path'}) {
1598 0 0         if($self->{logger}) {
1599 0           $self->{logger}->debug("unlink $path");
1600             }
1601 0           unlink $path;
1602             }
1603             }
1604 0 0         if($self->{save_to}->{ttl}) {
1605 0           $query = "DELETE FROM fcgi_buffer WHERE creation < strftime('\%s','now') - " . $self->{save_to}->{ttl};
1606             } else {
1607 0           $query = 'DELETE FROM fcgi_buffer'; # Hmm, I suspect this is overkill
1608             }
1609 0 0         if($self->{logger}) {
1610 0           $self->{logger}->debug($query);
1611             }
1612 0           $dbh->prepare($query)->execute();
1613             # } else {
1614             # Old code
1615             # if($self->{save_to}->{ttl}) {
1616             # $query = "SELECT DISTINCT path, creation FROM fcgi_buffer WHERE key = '$key' AND creation >= strftime('\%s','now') - " . $self->{save_to}->{ttl};
1617             # } else {
1618             # $query = "SELECT DISTINCT path, creation FROM fcgi_buffer WHERE key = '$key'";
1619             # }
1620             # my $sth = $dbh->prepare($query);
1621             # $sth->execute();
1622             # my $href = $sth->fetchrow_hashref();
1623             # if(my $path = $href->{'path'}) {
1624             # # FIXME: don't do this if we've passed the TTL, and if we are clean
1625             # # up the database and remove the static page
1626             # $request_uri =~ s/\?/\\?/g;
1627             # if(($unzipped_body =~ s/<a href="$request_uri"/<a href="$path"/gi) > 0) {
1628             # $self->{'body'} = $unzipped_body;
1629             # if(my $ttl = $self->{save_to}->{ttl}) {
1630             # push @{$self->{o}}, 'Expires: ' . HTTP::Date::time2str($href->{creation} + $ttl);
1631             # }
1632             # }
1633             # }
1634             }
1635 0           return $changes;
1636             }
1637              
1638             =head1 AUTHOR
1639              
1640             Nigel Horne, C<< <njh at bandsman.co.uk> >>
1641              
1642             =head1 BUGS
1643              
1644             FCGI::Buffer should be safe even in scripts which produce lots of different
1645             output, e.g. e-commerce situations.
1646             On such pages, however, I strongly urge to setting generate_304 to 0 and
1647             sending the HTTP header "Cache-Control: no-cache".
1648              
1649             When using L<Template>, ensure that you don't use it to output to STDOUT,
1650             instead you will need to capture into a variable and print that.
1651             For example:
1652              
1653             my $output;
1654             $template->process($input, $vars, \$output) || ($output = $template->error());
1655             print $output;
1656              
1657             Can produce buggy JavaScript if you use the <!-- HIDING technique.
1658             This is a bug in L<JavaScript::Packer>, not FCGI::Buffer.
1659              
1660             Mod_deflate can confuse this when compressing output.
1661             Ensure that deflation is off for .pl files:
1662              
1663             SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|pl)$ no-gzip dont-vary
1664              
1665             If you request compressed output then uncompressed output (or vice
1666             versa) on input that produces the same output, the status will be 304.
1667             The letter of the spec says that's wrong, so I'm noting it here, but
1668             in practice you should not see this happen or have any difficulties
1669             because of it.
1670              
1671             FCGI::Buffer has not been tested against FastCGI.
1672              
1673             I advise adding FCGI::Buffer as the last use statement so that it is
1674             cleared up first. In particular it should be loaded after
1675             L<Log::Log4Perl>, if you're using that, so that any messages it
1676             produces are printed after the HTTP headers have been sent by
1677             FCGI::Buffer;
1678              
1679             Save_to doesn't understand links in JavaScript, which means that if you use self-calling
1680             CGIs which are loaded as a static page they may point to the wrong place.
1681             The workaround is to avoid self-calling CGIs in JavaScript
1682              
1683             Please report any bugs or feature requests to C<bug-fcgi-buffer at rt.cpan.org>,
1684             or through the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=FCGI-Buffer>.
1685             I will be notified, and then you'll automatically be notified of progress on
1686             your bug as I make changes.
1687              
1688             The lint operation only works on HTML4, because of a restriction in L<HTML::Lint>.
1689              
1690             =head1 SEE ALSO
1691              
1692             CGI::Buffer, HTML::Packer, HTML::Lint
1693              
1694             =head1 SUPPORT
1695              
1696             You can find documentation for this module with the perldoc command.
1697              
1698             perldoc FCGI::Buffer
1699              
1700             You can also look for information at:
1701              
1702             =over 4
1703              
1704             =item * RT: CPAN's request tracker
1705              
1706             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=FCGI-Buffer>
1707              
1708             =item * CPAN Ratings
1709              
1710             L<http://cpanratings.perl.org/d/FCGI-Buffer>
1711              
1712             =item * Search CPAN
1713              
1714             L<http://search.cpan.org/dist/FCGI-Buffer/>
1715              
1716             =back
1717              
1718             =head1 ACKNOWLEDGEMENTS
1719              
1720             The inspiration and code for some of this is cgi_buffer by Mark
1721             Nottingham: L<https://www.mnot.net/blog/2003/04/24/etags>.
1722              
1723             =head1 LICENSE AND COPYRIGHT
1724              
1725             The licence for cgi_buffer is:
1726              
1727             "(c) 2000 Copyright Mark Nottingham <mnot@pobox.com>
1728              
1729             This software may be freely distributed, modified and used,
1730             provided that this copyright notice remain intact.
1731              
1732             This software is provided 'as is' without warranty of any kind."
1733              
1734             The rest of the program is Copyright 2015-2022 Nigel Horne,
1735             and is released under the following licence: GPL2
1736              
1737             =cut
1738              
1739             1; # End of FCGI::Buffer