line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package CGI::Buffer; |
2
|
|
|
|
|
|
|
|
3
|
6
|
|
|
6
|
|
2065073
|
use 5.14.0; # For IO::Compress::Brotli |
|
6
|
|
|
|
|
61
|
|
4
|
6
|
|
|
6
|
|
33
|
use strict; |
|
6
|
|
|
|
|
12
|
|
|
6
|
|
|
|
|
128
|
|
5
|
6
|
|
|
6
|
|
28
|
use warnings; |
|
6
|
|
|
|
|
12
|
|
|
6
|
|
|
|
|
157
|
|
6
|
|
|
|
|
|
|
|
7
|
6
|
|
|
6
|
|
30
|
use Digest::MD5; |
|
6
|
|
|
|
|
13
|
|
|
6
|
|
|
|
|
236
|
|
8
|
6
|
|
|
6
|
|
3005
|
use IO::String; |
|
6
|
|
|
|
|
20042
|
|
|
6
|
|
|
|
|
227
|
|
9
|
6
|
|
|
6
|
|
3756
|
use CGI::Info; |
|
6
|
|
|
|
|
456703
|
|
|
6
|
|
|
|
|
363
|
|
10
|
6
|
|
|
6
|
|
55
|
use Carp; |
|
6
|
|
|
|
|
13
|
|
|
6
|
|
|
|
|
337
|
|
11
|
6
|
|
|
6
|
|
2918
|
use HTTP::Date; |
|
6
|
|
|
|
|
23711
|
|
|
6
|
|
|
|
|
372
|
|
12
|
6
|
|
|
6
|
|
46
|
use Text::Diff; # For debugging |
|
6
|
|
|
|
|
17
|
|
|
6
|
|
|
|
|
417
|
|
13
|
|
|
|
|
|
|
|
14
|
|
|
|
|
|
|
=head1 NAME |
15
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
CGI::Buffer - Verify, Cache and Optimise CGI Output |
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
=head1 VERSION |
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
Version 0.84 |
21
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
=cut |
23
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
our $VERSION = '0.84'; |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
=head1 SYNOPSIS |
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
CGI::Buffer verifies the HTML that you produce by passing it through |
29
|
|
|
|
|
|
|
C<HTML::Lint>. |
30
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
CGI::Buffer optimises CGI programs by reducing, filtering and compressing |
32
|
|
|
|
|
|
|
output to speed up the transmission and by nearly seamlessly making use of |
33
|
|
|
|
|
|
|
client and server caches. |
34
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
To make use of client caches, that is to say to reduce needless calls |
36
|
|
|
|
|
|
|
to your server asking for the same data, all you need to do is to |
37
|
|
|
|
|
|
|
include the package, and it does the rest. |
38
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
use CGI::Buffer; |
40
|
|
|
|
|
|
|
# ... |
41
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
To also make use of server caches, that is to say to save regenerating |
43
|
|
|
|
|
|
|
output when different clients ask you for the same data, you will need |
44
|
|
|
|
|
|
|
to create a cache. |
45
|
|
|
|
|
|
|
But that's simple: |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
use CHI; |
48
|
|
|
|
|
|
|
use CGI::Buffer; |
49
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
# Put this at the top before you output anything |
51
|
|
|
|
|
|
|
CGI::Buffer::init( |
52
|
|
|
|
|
|
|
cache => CHI->new(driver => 'File') |
53
|
|
|
|
|
|
|
); |
54
|
|
|
|
|
|
|
if(CGI::Buffer::is_cached()) { |
55
|
|
|
|
|
|
|
# Nothing has changed - use the version in the cache |
56
|
|
|
|
|
|
|
exit; |
57
|
|
|
|
|
|
|
} |
58
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
# ... |
60
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
To temporarily prevent the use of server-side caches, for example whilst |
62
|
|
|
|
|
|
|
debugging before publishing a code change, set the NO_CACHE environment variable |
63
|
|
|
|
|
|
|
to any non-zero value. |
64
|
|
|
|
|
|
|
If you get errors about Wide characters in print it means that you've |
65
|
|
|
|
|
|
|
forgotten to emit pure HTML on non-ASCII characters. |
66
|
|
|
|
|
|
|
See L<HTML::Entities>. |
67
|
|
|
|
|
|
|
As a hack work around you could also remove accents and the like by using |
68
|
|
|
|
|
|
|
L<Text::Unidecode>, |
69
|
|
|
|
|
|
|
which works well but isn't really what you want. |
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
=head1 SUBROUTINES/METHODS |
72
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
=cut |
74
|
|
|
|
|
|
|
|
75
|
6
|
|
|
6
|
|
45
|
use constant MIN_GZIP_LEN => 32; |
|
6
|
|
|
|
|
15
|
|
|
6
|
|
|
|
|
1392
|
|
76
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
our $generate_etag = 1; |
78
|
|
|
|
|
|
|
our $generate_304 = 1; |
79
|
|
|
|
|
|
|
our $generate_last_modified = 1; |
80
|
|
|
|
|
|
|
our $compress_content = 1; |
81
|
|
|
|
|
|
|
our $optimise_content = 0; |
82
|
|
|
|
|
|
|
our $lint_content = 0; |
83
|
|
|
|
|
|
|
our $cache; |
84
|
|
|
|
|
|
|
our $cache_age; |
85
|
|
|
|
|
|
|
our $cache_key; |
86
|
|
|
|
|
|
|
our $info; |
87
|
|
|
|
|
|
|
our $logger; |
88
|
|
|
|
|
|
|
our $lingua; |
89
|
|
|
|
|
|
|
our $status; |
90
|
|
|
|
|
|
|
our $script_mtime; |
91
|
|
|
|
|
|
|
our $cobject; |
92
|
|
|
|
|
|
|
our($x_cache, $buf, $headers, $header, $body, @content_type, $etag, |
93
|
|
|
|
|
|
|
$send_body, @o, $encode_loaded); |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
BEGIN { |
96
|
|
|
|
|
|
|
# use Exporter(); |
97
|
|
|
|
|
|
|
|
98
|
6
|
|
|
6
|
|
65
|
$CGI::Buffer::buf = IO::String->new(); |
99
|
6
|
|
|
|
|
407
|
$CGI::Buffer::old_buf = select($CGI::Buffer::buf); |
100
|
|
|
|
|
|
|
|
101
|
6
|
50
|
33
|
|
|
45
|
if((!defined($ENV{'SERVER_PROTOCOL'})) || |
102
|
|
|
|
|
|
|
($ENV{'SERVER_PROTOCOL'} eq 'HTTP/1.0')) { |
103
|
6
|
|
|
|
|
46259
|
$generate_etag = 0; |
104
|
|
|
|
|
|
|
} |
105
|
|
|
|
|
|
|
} |
106
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
END { |
108
|
6
|
50
|
33
|
6
|
|
3667145
|
if(defined($^V) && ($^V ge 'v5.14.0')) { |
109
|
6
|
50
|
|
|
|
64
|
return if ${^GLOBAL_PHASE} eq 'DESTRUCT'; # >= 5.14.0 only |
110
|
|
|
|
|
|
|
} |
111
|
|
|
|
|
|
|
|
112
|
6
|
50
|
|
|
|
105
|
if($logger) { |
113
|
0
|
0
|
|
|
|
0
|
if($ENV{'HTTP_IF_NONE_MATCH'}) { |
114
|
0
|
|
|
|
|
0
|
$logger->debug("HTTP_IF_NONE_MATCH: $ENV{HTTP_IF_NONE_MATCH}"); |
115
|
|
|
|
|
|
|
} |
116
|
0
|
0
|
|
|
|
0
|
if($ENV{'HTTP_IF_MODIFIED_SINCE'}) { |
117
|
0
|
|
|
|
|
0
|
$logger->debug("HTTP_IF_MODIFIED_SINCE: $ENV{HTTP_IF_MODIFIED_SINCE}"); |
118
|
|
|
|
|
|
|
} |
119
|
0
|
|
|
|
|
0
|
$logger->debug("Generate_etag = $generate_etag, ", |
120
|
|
|
|
|
|
|
"Generate_304 = $generate_304, ", |
121
|
|
|
|
|
|
|
"Generate_last_modified = $generate_last_modified"); |
122
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
# This will cause everything to get flushed and prevent |
124
|
|
|
|
|
|
|
# outputs to the logger. We need to do that now since |
125
|
|
|
|
|
|
|
# if we leave it to Perl to delete later we may get |
126
|
|
|
|
|
|
|
# a message that Log4perl::init() hasn't been called |
127
|
0
|
|
|
|
|
0
|
$logger = undef; |
128
|
|
|
|
|
|
|
} |
129
|
6
|
|
|
|
|
45
|
select($CGI::Buffer::old_buf); |
130
|
6
|
|
|
|
|
92
|
my $pos = $CGI::Buffer::buf->getpos; |
131
|
6
|
|
|
|
|
168
|
$CGI::Buffer::buf->setpos(0); |
132
|
6
|
|
|
|
|
179
|
read($CGI::Buffer::buf, $buf, $pos); |
133
|
6
|
|
|
|
|
347
|
($headers, $body) = split /\r?\n\r?\n/, $buf, 2; |
134
|
|
|
|
|
|
|
|
135
|
6
|
50
|
33
|
|
|
80
|
unless($headers || is_cached()) { |
136
|
6
|
50
|
|
|
|
42
|
if($logger) { |
137
|
0
|
|
|
|
|
0
|
$logger->debug('There was no output'); |
138
|
|
|
|
|
|
|
} |
139
|
6
|
50
|
|
|
|
36
|
if(!defined($headers)) { |
140
|
6
|
|
|
|
|
3958
|
require HTTP::Status; |
141
|
6
|
|
|
|
|
34929
|
HTTP::Status->import(); |
142
|
|
|
|
|
|
|
|
143
|
6
|
50
|
|
|
|
51
|
if(!defined($status)) { |
144
|
6
|
|
|
|
|
27
|
$status = 200; |
145
|
|
|
|
|
|
|
} |
146
|
6
|
|
|
|
|
41
|
print "Status: $status ", |
147
|
|
|
|
|
|
|
HTTP::Status::status_message($status), |
148
|
|
|
|
|
|
|
"\n\n"; |
149
|
|
|
|
|
|
|
} |
150
|
6
|
|
|
|
|
707
|
return; |
151
|
|
|
|
|
|
|
} |
152
|
0
|
0
|
0
|
|
|
0
|
if($ENV{'REQUEST_METHOD'} && ($ENV{'REQUEST_METHOD'} eq 'HEAD')) { |
153
|
0
|
|
|
|
|
0
|
$send_body = 0; |
154
|
|
|
|
|
|
|
} else { |
155
|
0
|
|
|
|
|
0
|
$send_body = 1; |
156
|
|
|
|
|
|
|
} |
157
|
|
|
|
|
|
|
|
158
|
0
|
0
|
|
|
|
0
|
if($headers) { |
159
|
0
|
|
|
|
|
0
|
_set_content_type($headers); |
160
|
|
|
|
|
|
|
} |
161
|
|
|
|
|
|
|
|
162
|
0
|
0
|
0
|
|
|
0
|
if(defined($body) && ($body eq '')) { |
|
|
0
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
163
|
|
|
|
|
|
|
# E.g. if header of Location is given with no body, for |
164
|
|
|
|
|
|
|
# redirection |
165
|
0
|
|
|
|
|
0
|
$body = undef; |
166
|
0
|
0
|
|
|
|
0
|
if($cache) { |
167
|
|
|
|
|
|
|
# Don't try to retrieve it below from the cache |
168
|
0
|
|
|
|
|
0
|
$send_body = 0; |
169
|
|
|
|
|
|
|
} |
170
|
|
|
|
|
|
|
} elsif(defined($content_type[0]) && (lc($content_type[0]) eq 'text') && (lc($content_type[1]) =~ /^html/) && defined($body)) { |
171
|
0
|
0
|
|
|
|
0
|
if($optimise_content) { |
172
|
|
|
|
|
|
|
# require HTML::Clean; |
173
|
0
|
|
|
|
|
0
|
require HTML::Packer; # Overkill using HTML::Clean and HTML::Packer... |
174
|
|
|
|
|
|
|
|
175
|
0
|
0
|
|
|
|
0
|
if($logger) { |
176
|
0
|
|
|
|
|
0
|
$logger->trace('Packer'); |
177
|
|
|
|
|
|
|
} |
178
|
|
|
|
|
|
|
|
179
|
0
|
|
|
|
|
0
|
my $oldlength = length($body); |
180
|
0
|
|
|
|
|
0
|
my $newlength; |
181
|
|
|
|
|
|
|
|
182
|
0
|
0
|
|
|
|
0
|
if($optimise_content == 1) { |
183
|
0
|
|
|
|
|
0
|
_optimise_content(); |
184
|
|
|
|
|
|
|
} else { |
185
|
0
|
|
|
|
|
0
|
while(1) { |
186
|
0
|
|
|
|
|
0
|
_optimise_content(); |
187
|
0
|
|
|
|
|
0
|
$newlength = length($body); |
188
|
0
|
0
|
|
|
|
0
|
last if ($newlength >= $oldlength); |
189
|
0
|
|
|
|
|
0
|
$oldlength = $newlength; |
190
|
|
|
|
|
|
|
} |
191
|
|
|
|
|
|
|
} |
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
# If we're on http://www.example.com and have a link |
194
|
|
|
|
|
|
|
# to http://www.example.com/foo/bar.htm, change the |
195
|
|
|
|
|
|
|
# link to /foo/bar.htm - there's no need to include |
196
|
|
|
|
|
|
|
# the site name in the link |
197
|
0
|
0
|
|
|
|
0
|
unless(defined($info)) { |
198
|
0
|
0
|
|
|
|
0
|
if($cache) { |
199
|
0
|
|
|
|
|
0
|
$info = CGI::Info->new({ cache => $cache }); |
200
|
|
|
|
|
|
|
} else { |
201
|
0
|
|
|
|
|
0
|
$info = CGI::Info->new(); |
202
|
|
|
|
|
|
|
} |
203
|
|
|
|
|
|
|
} |
204
|
|
|
|
|
|
|
|
205
|
0
|
|
|
|
|
0
|
my $href = $info->host_name(); |
206
|
0
|
|
|
|
|
0
|
my $protocol = $info->protocol(); |
207
|
|
|
|
|
|
|
|
208
|
0
|
0
|
|
|
|
0
|
unless($protocol) { |
209
|
0
|
|
|
|
|
0
|
$protocol = 'http'; |
210
|
|
|
|
|
|
|
} |
211
|
|
|
|
|
|
|
|
212
|
0
|
|
|
|
|
0
|
$body =~ s/<a\s+?href="$protocol:\/\/$href"/<a href="\/"/gim; |
213
|
0
|
|
|
|
|
0
|
$body =~ s/<a\s+?href="$protocol:\/\/$href/<a href="/gim; |
214
|
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
# TODO use URI->path_segments to change links in |
216
|
|
|
|
|
|
|
# /aa/bb/cc/dd.htm which point to /aa/bb/ff.htm to |
217
|
|
|
|
|
|
|
# ../ff.htm |
218
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
# TODO: <img border=0 src=...> |
220
|
0
|
|
|
|
|
0
|
$body =~ s/<img\s+?src="$protocol:\/\/$href"/<img src="\/"/gim; |
221
|
0
|
|
|
|
|
0
|
$body =~ s/<img\s+?src="$protocol:\/\/$href/<img src="/gim; |
222
|
|
|
|
|
|
|
|
223
|
|
|
|
|
|
|
# Don't use HTML::Clean because of RT402 |
224
|
|
|
|
|
|
|
# my $h = new HTML::Clean(\$body); |
225
|
|
|
|
|
|
|
# # $h->compat(); |
226
|
|
|
|
|
|
|
# $h->strip(); |
227
|
|
|
|
|
|
|
# my $ref = $h->data(); |
228
|
|
|
|
|
|
|
|
229
|
|
|
|
|
|
|
# Don't always do javascript 'best' since it's confused |
230
|
|
|
|
|
|
|
# by the common <!-- HIDE technique. |
231
|
|
|
|
|
|
|
# See https://github.com/nevesenin/javascript-packer-perl/issues/1#issuecomment-4356790 |
232
|
0
|
|
|
|
|
0
|
my $options = { |
233
|
|
|
|
|
|
|
remove_comments => 1, |
234
|
|
|
|
|
|
|
remove_newlines => 0, |
235
|
|
|
|
|
|
|
do_stylesheet => 'minify' |
236
|
|
|
|
|
|
|
}; |
237
|
0
|
0
|
|
|
|
0
|
if($optimise_content >= 2) { |
238
|
0
|
|
|
|
|
0
|
$options->{do_javascript} = 'best'; |
239
|
0
|
|
|
|
|
0
|
$body =~ s/(<script.*?>)\s*<!--/$1/gi; |
240
|
0
|
|
|
|
|
0
|
$body =~ s/\/\/-->\s*<\/script>/<\/script>/gi; |
241
|
0
|
|
|
|
|
0
|
$body =~ s/(<script.*?>)\s+/$1/gi; |
242
|
|
|
|
|
|
|
} |
243
|
0
|
|
|
|
|
0
|
$body = HTML::Packer->init()->minify(\$body, $options); |
244
|
0
|
0
|
|
|
|
0
|
if($optimise_content >= 2) { |
245
|
|
|
|
|
|
|
# Change document.write("a"); document.write("b") |
246
|
|
|
|
|
|
|
# into document.write("a"+"b"); |
247
|
0
|
|
|
|
|
0
|
while(1) { |
248
|
0
|
|
|
|
|
0
|
$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; |
249
|
0
|
|
|
|
|
0
|
$newlength = length($body); |
250
|
0
|
0
|
|
|
|
0
|
last if ($newlength >= $oldlength); |
251
|
0
|
|
|
|
|
0
|
$oldlength = $newlength; |
252
|
|
|
|
|
|
|
} |
253
|
|
|
|
|
|
|
} |
254
|
|
|
|
|
|
|
} |
255
|
0
|
0
|
|
|
|
0
|
if($lint_content) { |
256
|
0
|
|
|
|
|
0
|
require HTML::Lint; |
257
|
0
|
|
|
|
|
0
|
HTML::Lint->import; |
258
|
|
|
|
|
|
|
|
259
|
0
|
0
|
|
|
|
0
|
if($logger) { |
260
|
0
|
|
|
|
|
0
|
$logger->trace('Lint'); |
261
|
|
|
|
|
|
|
} |
262
|
0
|
|
|
|
|
0
|
my $lint = HTML::Lint->new(); |
263
|
0
|
|
|
|
|
0
|
$lint->parse($body); |
264
|
0
|
|
|
|
|
0
|
$lint->eof(); |
265
|
|
|
|
|
|
|
|
266
|
0
|
0
|
|
|
|
0
|
if($lint->errors) { |
267
|
0
|
|
|
|
|
0
|
$headers = 'Status: 500 Internal Server Error'; |
268
|
0
|
|
|
|
|
0
|
@o = ('Content-type: text/plain'); |
269
|
0
|
|
|
|
|
0
|
$body = ''; |
270
|
0
|
|
|
|
|
0
|
foreach my $error ($lint->errors) { |
271
|
0
|
|
|
|
|
0
|
my $errtext = $error->where() . ': ' . $error->errtext() . "\n"; |
272
|
0
|
0
|
|
|
|
0
|
if($logger) { |
273
|
0
|
|
|
|
|
0
|
$logger->warn($errtext); |
274
|
|
|
|
|
|
|
} else { |
275
|
0
|
|
|
|
|
0
|
warn($errtext); |
276
|
|
|
|
|
|
|
} |
277
|
0
|
|
|
|
|
0
|
$body .= $errtext; |
278
|
|
|
|
|
|
|
} |
279
|
|
|
|
|
|
|
} |
280
|
|
|
|
|
|
|
} |
281
|
|
|
|
|
|
|
} |
282
|
|
|
|
|
|
|
|
283
|
0
|
0
|
0
|
|
|
0
|
if(defined($headers) && ($headers =~ /^Status: (\d+)/m)) { |
|
|
0
|
|
|
|
|
|
284
|
0
|
|
|
|
|
0
|
$status = $1; |
285
|
|
|
|
|
|
|
} elsif($info) { |
286
|
0
|
|
|
|
|
0
|
$status = $info->status(); |
287
|
|
|
|
|
|
|
} else { |
288
|
0
|
|
|
|
|
0
|
$status = 200; |
289
|
|
|
|
|
|
|
} |
290
|
|
|
|
|
|
|
|
291
|
0
|
0
|
|
|
|
0
|
if($logger) { |
292
|
0
|
|
|
|
|
0
|
$logger->debug("Initial status = $status"); |
293
|
|
|
|
|
|
|
} |
294
|
|
|
|
|
|
|
|
295
|
|
|
|
|
|
|
# Generate the eTag before compressing, since the compressed data |
296
|
|
|
|
|
|
|
# includes the mtime field which changes thus causing a different |
297
|
|
|
|
|
|
|
# Etag to be generated |
298
|
0
|
0
|
0
|
|
|
0
|
if($ENV{'SERVER_PROTOCOL'} && |
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
299
|
|
|
|
|
|
|
(($ENV{'SERVER_PROTOCOL'} eq 'HTTP/1.1') || ($ENV{'SERVER_PROTOCOL'} eq 'HTTP/2.0')) && |
300
|
|
|
|
|
|
|
$generate_etag && defined($body)) { |
301
|
|
|
|
|
|
|
# encode to avoid "Wide character in subroutine entry" |
302
|
0
|
|
|
|
|
0
|
require Encode; |
303
|
0
|
|
|
|
|
0
|
$encode_loaded = 1; |
304
|
0
|
|
|
|
|
0
|
$etag = '"' . Digest::MD5->new->add(Encode::encode_utf8($body))->hexdigest() . '"'; |
305
|
0
|
0
|
0
|
|
|
0
|
if($ENV{'HTTP_IF_NONE_MATCH'} && $generate_304 && ($status == 200)) { |
|
|
|
0
|
|
|
|
|
306
|
0
|
0
|
|
|
|
0
|
if($logger) { |
307
|
0
|
|
|
|
|
0
|
$logger->debug("Compare $ENV{HTTP_IF_NONE_MATCH} with $etag"); |
308
|
|
|
|
|
|
|
} |
309
|
0
|
0
|
|
|
|
0
|
if($ENV{'HTTP_IF_NONE_MATCH'} eq $etag) { |
|
|
0
|
|
|
|
|
|
310
|
0
|
|
|
|
|
0
|
push @o, "Status: 304 Not Modified"; |
311
|
0
|
|
|
|
|
0
|
$send_body = 0; |
312
|
0
|
|
|
|
|
0
|
$status = 304; |
313
|
0
|
0
|
|
|
|
0
|
if($logger) { |
314
|
0
|
|
|
|
|
0
|
$logger->debug('Set status to 304'); |
315
|
|
|
|
|
|
|
} |
316
|
|
|
|
|
|
|
} elsif($logger) { |
317
|
0
|
|
|
|
|
0
|
$logger->debug(diff(\$body, \$cache->get(_generate_key()))); |
318
|
|
|
|
|
|
|
} |
319
|
|
|
|
|
|
|
} |
320
|
|
|
|
|
|
|
} |
321
|
|
|
|
|
|
|
|
322
|
0
|
|
|
|
|
0
|
my $encoding = _should_gzip(); |
323
|
0
|
|
|
|
|
0
|
my $unzipped_body = $body; |
324
|
|
|
|
|
|
|
|
325
|
0
|
0
|
|
|
|
0
|
if(defined($unzipped_body)) { |
326
|
0
|
0
|
|
|
|
0
|
my $range = $ENV{'Range'} ? $ENV{'Range'} : $ENV{'HTTP_RANGE'}; |
327
|
|
|
|
|
|
|
|
328
|
0
|
0
|
0
|
|
|
0
|
if($range && !$cache) { |
329
|
|
|
|
|
|
|
# TODO: Partials |
330
|
0
|
0
|
|
|
|
0
|
if($range =~ /^bytes=(\d*)-(\d*)/) { |
331
|
0
|
0
|
0
|
|
|
0
|
if($1 && $2) { |
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
332
|
0
|
|
|
|
|
0
|
$body = substr($body, $1, $2-$1); |
333
|
|
|
|
|
|
|
} elsif($1) { |
334
|
0
|
|
|
|
|
0
|
$body = substr($body, $1); |
335
|
|
|
|
|
|
|
} elsif($2) { |
336
|
0
|
|
|
|
|
0
|
$body = substr($body, 0, $2); |
337
|
|
|
|
|
|
|
} |
338
|
0
|
|
|
|
|
0
|
$unzipped_body = $body; |
339
|
0
|
|
|
|
|
0
|
$status = 206; |
340
|
|
|
|
|
|
|
} |
341
|
|
|
|
|
|
|
} |
342
|
0
|
|
|
|
|
0
|
_compress({ encoding => $encoding }); |
343
|
|
|
|
|
|
|
} |
344
|
|
|
|
|
|
|
|
345
|
0
|
0
|
|
|
|
0
|
if($cache) { |
|
|
0
|
|
|
|
|
|
346
|
0
|
|
|
|
|
0
|
require Storable; |
347
|
|
|
|
|
|
|
|
348
|
0
|
|
|
|
|
0
|
my $cache_hash; |
349
|
0
|
|
|
|
|
0
|
my $key = _generate_key(); |
350
|
|
|
|
|
|
|
|
351
|
|
|
|
|
|
|
# Cache unzipped version |
352
|
0
|
0
|
|
|
|
0
|
if(!defined($body)) { |
353
|
0
|
0
|
|
|
|
0
|
if($send_body) { |
354
|
0
|
|
|
|
|
0
|
$cobject = $cache->get_object($key); |
355
|
0
|
0
|
|
|
|
0
|
if(defined($cobject)) { |
|
|
0
|
|
|
|
|
|
356
|
0
|
|
|
|
|
0
|
$cache_hash = Storable::thaw($cobject->value()); |
357
|
0
|
|
|
|
|
0
|
$headers = $cache_hash->{'headers'}; |
358
|
0
|
|
|
|
|
0
|
_set_content_type($headers); |
359
|
0
|
|
|
|
|
0
|
@o = ("X-CGI-Buffer-$VERSION: Hit"); |
360
|
0
|
0
|
|
|
|
0
|
if($info) { |
361
|
0
|
|
|
|
|
0
|
my $host_name = $info->host_name(); |
362
|
0
|
|
|
|
|
0
|
push @o, "X-Cache: HIT from $host_name"; |
363
|
0
|
|
|
|
|
0
|
push @o, "X-Cache-Lookup: HIT from $host_name"; |
364
|
|
|
|
|
|
|
} else { |
365
|
0
|
|
|
|
|
0
|
push @o, 'X-Cache: HIT'; |
366
|
0
|
|
|
|
|
0
|
push @o, 'X-Cache-Lookup: HIT'; |
367
|
|
|
|
|
|
|
} |
368
|
|
|
|
|
|
|
} elsif($logger) { |
369
|
0
|
|
|
|
|
0
|
$logger->warn("Error retrieving data for key $key"); |
370
|
|
|
|
|
|
|
} else { |
371
|
0
|
|
|
|
|
0
|
carp(__PACKAGE__, ": error retrieving data for key $key"); |
372
|
|
|
|
|
|
|
} |
373
|
|
|
|
|
|
|
} |
374
|
|
|
|
|
|
|
|
375
|
|
|
|
|
|
|
# Nothing has been output yet, so we can check if it's |
376
|
|
|
|
|
|
|
# OK to send 304 if possible |
377
|
0
|
0
|
0
|
|
|
0
|
if($send_body && $ENV{'SERVER_PROTOCOL'} && |
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
378
|
|
|
|
|
|
|
(($ENV{'SERVER_PROTOCOL'} eq 'HTTP/1.1') || ($ENV{'SERVER_PROTOCOL'} eq 'HTTP/2.0')) && |
379
|
|
|
|
|
|
|
$generate_304 && ($status == 200)) { |
380
|
0
|
0
|
|
|
|
0
|
if($ENV{'HTTP_IF_MODIFIED_SINCE'}) { |
381
|
|
|
|
|
|
|
_check_modified_since({ |
382
|
0
|
|
|
|
|
0
|
since => $ENV{'HTTP_IF_MODIFIED_SINCE'}, |
383
|
|
|
|
|
|
|
modified => $cobject->created_at() |
384
|
|
|
|
|
|
|
}); |
385
|
|
|
|
|
|
|
} |
386
|
|
|
|
|
|
|
} |
387
|
0
|
0
|
0
|
|
|
0
|
if($send_body && ($status == 200) && defined($cache_hash)) { |
|
|
|
0
|
|
|
|
|
388
|
0
|
|
|
|
|
0
|
$body = $cache_hash->{'body'}; |
389
|
0
|
0
|
|
|
|
0
|
if(!defined($body)) { |
390
|
|
|
|
|
|
|
# Panic |
391
|
0
|
|
|
|
|
0
|
$headers = 'Status: 500 Internal Server Error'; |
392
|
0
|
|
|
|
|
0
|
@o = ('Content-type: text/plain'); |
393
|
0
|
|
|
|
|
0
|
$body = "Can't retrieve body for key $key, cache_hash contains:\n"; |
394
|
0
|
|
|
|
|
0
|
foreach my $k (keys %{$cache_hash}) { |
|
0
|
|
|
|
|
0
|
|
395
|
0
|
|
|
|
|
0
|
$body .= "\t$k\n"; |
396
|
|
|
|
|
|
|
} |
397
|
0
|
|
|
|
|
0
|
$cache->remove($key); |
398
|
0
|
0
|
|
|
|
0
|
if($logger) { |
399
|
0
|
|
|
|
|
0
|
$logger->error("Can't retrieve body for key $key"); |
400
|
0
|
|
|
|
|
0
|
$logger->warn($body); |
401
|
|
|
|
|
|
|
} else { |
402
|
0
|
|
|
|
|
0
|
carp "Can't retrieve body for key $key"; |
403
|
0
|
|
|
|
|
0
|
warn($body); |
404
|
|
|
|
|
|
|
} |
405
|
0
|
|
|
|
|
0
|
$send_body = 0; |
406
|
0
|
|
|
|
|
0
|
$status = 500; |
407
|
|
|
|
|
|
|
} |
408
|
|
|
|
|
|
|
} |
409
|
0
|
0
|
0
|
|
|
0
|
if($send_body && $ENV{'SERVER_PROTOCOL'} && |
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
410
|
|
|
|
|
|
|
(($ENV{'SERVER_PROTOCOL'} eq 'HTTP/1.1') || ($ENV{'SERVER_PROTOCOL'} eq 'HTTP/2.0')) && |
411
|
|
|
|
|
|
|
($status == 200)) { |
412
|
0
|
0
|
|
|
|
0
|
if($ENV{'HTTP_IF_NONE_MATCH'}) { |
413
|
0
|
0
|
|
|
|
0
|
if(!defined($etag)) { |
414
|
0
|
0
|
|
|
|
0
|
unless($encode_loaded) { |
415
|
0
|
|
|
|
|
0
|
require Encode; |
416
|
0
|
|
|
|
|
0
|
$encode_loaded = 1; |
417
|
|
|
|
|
|
|
} |
418
|
0
|
|
|
|
|
0
|
$etag = '"' . Digest::MD5->new->add(Encode::encode_utf8($body))->hexdigest() . '"'; |
419
|
|
|
|
|
|
|
} |
420
|
0
|
0
|
0
|
|
|
0
|
if($logger && $generate_304) { |
421
|
0
|
|
|
|
|
0
|
$logger->debug("Compare etags $ENV{HTTP_IF_NONE_MATCH} and $etag"); |
422
|
|
|
|
|
|
|
} |
423
|
0
|
0
|
0
|
|
|
0
|
if(($ENV{'HTTP_IF_NONE_MATCH'} eq $etag) && $generate_304) { |
424
|
0
|
|
|
|
|
0
|
push @o, "Status: 304 Not Modified"; |
425
|
0
|
|
|
|
|
0
|
$status = 304; |
426
|
0
|
|
|
|
|
0
|
$send_body = 0; |
427
|
0
|
0
|
|
|
|
0
|
if($logger) { |
428
|
0
|
|
|
|
|
0
|
$logger->debug('Set status to 304'); |
429
|
|
|
|
|
|
|
} |
430
|
|
|
|
|
|
|
} |
431
|
|
|
|
|
|
|
} |
432
|
|
|
|
|
|
|
} |
433
|
0
|
0
|
|
|
|
0
|
if($status == 200) { |
434
|
0
|
|
|
|
|
0
|
$encoding = _should_gzip(); |
435
|
0
|
0
|
|
|
|
0
|
if($send_body) { |
436
|
0
|
0
|
0
|
|
|
0
|
if($generate_etag && !defined($etag) && defined($body) && ((!defined($headers)) || ($headers !~ /^ETag: /m))) { |
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
437
|
0
|
|
|
|
|
0
|
$etag = '"' . Digest::MD5->new->add(Encode::encode_utf8($body))->hexdigest() . '"'; |
438
|
|
|
|
|
|
|
} |
439
|
0
|
|
|
|
|
0
|
_compress({ encoding => $encoding }); |
440
|
|
|
|
|
|
|
} |
441
|
|
|
|
|
|
|
} |
442
|
0
|
|
|
|
|
0
|
my $cannot_304 = !$generate_304; |
443
|
0
|
0
|
|
|
|
0
|
unless($etag) { |
444
|
0
|
0
|
0
|
|
|
0
|
if(defined($headers) && ($headers =~ /^ETag: "([a-z0-9]{32})"/m)) { |
445
|
0
|
|
|
|
|
0
|
$etag = $1; |
446
|
|
|
|
|
|
|
} else { |
447
|
0
|
|
|
|
|
0
|
$etag = $cache_hash->{'etag'}; |
448
|
|
|
|
|
|
|
} |
449
|
|
|
|
|
|
|
} |
450
|
0
|
0
|
0
|
|
|
0
|
if($ENV{'HTTP_IF_NONE_MATCH'} && $send_body && ($status != 304) && $generate_304) { |
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
451
|
0
|
0
|
|
|
|
0
|
if($logger) { |
452
|
0
|
|
|
|
|
0
|
$logger->debug("Compare $ENV{HTTP_IF_NONE_MATCH} with $etag"); |
453
|
|
|
|
|
|
|
} |
454
|
0
|
0
|
0
|
|
|
0
|
if(defined($etag) && ($etag eq $ENV{'HTTP_IF_NONE_MATCH'}) && ($status == 200)) { |
|
|
|
0
|
|
|
|
|
455
|
0
|
|
|
|
|
0
|
push @o, "Status: 304 Not Modified"; |
456
|
0
|
|
|
|
|
0
|
$send_body = 0; |
457
|
0
|
|
|
|
|
0
|
$status = 304; |
458
|
0
|
0
|
|
|
|
0
|
if($logger) { |
459
|
0
|
|
|
|
|
0
|
$logger->debug('Set status to 304'); |
460
|
|
|
|
|
|
|
} |
461
|
|
|
|
|
|
|
} else { |
462
|
0
|
|
|
|
|
0
|
$cannot_304 = 1; |
463
|
|
|
|
|
|
|
} |
464
|
|
|
|
|
|
|
} |
465
|
0
|
0
|
|
|
|
0
|
if($cobject) { |
466
|
0
|
0
|
0
|
|
|
0
|
if($ENV{'HTTP_IF_MODIFIED_SINCE'} && ($status != 304) && !$cannot_304) { |
|
|
|
0
|
|
|
|
|
467
|
|
|
|
|
|
|
_check_modified_since({ |
468
|
0
|
|
|
|
|
0
|
since => $ENV{'HTTP_IF_MODIFIED_SINCE'}, |
469
|
|
|
|
|
|
|
modified => $cobject->created_at() |
470
|
|
|
|
|
|
|
}); |
471
|
|
|
|
|
|
|
} |
472
|
0
|
0
|
0
|
|
|
0
|
if(($status == 200) && $generate_last_modified) { |
473
|
0
|
0
|
|
|
|
0
|
if($logger) { |
474
|
0
|
|
|
|
|
0
|
$logger->debug('Set Last-Modified to ', HTTP::Date::time2str($cobject->created_at())); |
475
|
|
|
|
|
|
|
} |
476
|
0
|
|
|
|
|
0
|
push @o, "Last-Modified: " . HTTP::Date::time2str($cobject->created_at()); |
477
|
|
|
|
|
|
|
} |
478
|
|
|
|
|
|
|
} |
479
|
|
|
|
|
|
|
} else { |
480
|
|
|
|
|
|
|
# Not in the server side cache |
481
|
0
|
0
|
|
|
|
0
|
if($status == 200) { |
482
|
0
|
0
|
|
|
|
0
|
unless($cache_age) { |
483
|
|
|
|
|
|
|
# It would be great if CHI::set() |
484
|
|
|
|
|
|
|
# allowed the time to be 'lru' for least |
485
|
|
|
|
|
|
|
# recently used. |
486
|
0
|
|
|
|
|
0
|
$cache_age = '10 minutes'; |
487
|
|
|
|
|
|
|
} |
488
|
0
|
|
|
|
|
0
|
$cache_hash->{'body'} = $unzipped_body; |
489
|
0
|
0
|
0
|
|
|
0
|
if(@o && defined($o[0])) { |
|
|
0
|
0
|
|
|
|
|
490
|
|
|
|
|
|
|
# Remember, we're storing the UNzipped |
491
|
|
|
|
|
|
|
# version in the cache |
492
|
0
|
|
|
|
|
0
|
my $c; |
493
|
0
|
0
|
0
|
|
|
0
|
if(defined($headers) && length($headers)) { |
494
|
0
|
|
|
|
|
0
|
$c = $headers . "\r\n" . join("\r\n", @o); |
495
|
|
|
|
|
|
|
} else { |
496
|
0
|
|
|
|
|
0
|
$c = join("\r\n", @o); |
497
|
|
|
|
|
|
|
} |
498
|
0
|
|
|
|
|
0
|
$c =~ s/^Content-Encoding: .+$//mg; |
499
|
0
|
|
|
|
|
0
|
$c =~ s/^Vary: Accept-Encoding.*\r?$//mg; |
500
|
0
|
|
|
|
|
0
|
$c =~ s/\n+/\n/gs; |
501
|
0
|
0
|
|
|
|
0
|
if(length($c)) { |
502
|
0
|
|
|
|
|
0
|
$cache_hash->{'headers'} = $c; |
503
|
|
|
|
|
|
|
} |
504
|
|
|
|
|
|
|
} elsif(defined($headers) && length($headers)) { |
505
|
0
|
|
|
|
|
0
|
$headers =~ s/^Content-Encoding: .+$//mg; |
506
|
0
|
|
|
|
|
0
|
$headers =~ s/^Vary: Accept-Encoding.*\r?$//mg; |
507
|
0
|
|
|
|
|
0
|
$headers =~ s/\n+/\n/gs; |
508
|
0
|
0
|
|
|
|
0
|
if(length($headers)) { |
509
|
0
|
|
|
|
|
0
|
$cache_hash->{'headers'} = $headers; |
510
|
|
|
|
|
|
|
} |
511
|
|
|
|
|
|
|
} |
512
|
0
|
0
|
0
|
|
|
0
|
if($generate_etag && defined($etag)) { |
513
|
0
|
|
|
|
|
0
|
$cache_hash->{'etag'} = $etag; |
514
|
|
|
|
|
|
|
} |
515
|
|
|
|
|
|
|
# TODO: Support the Expires header |
516
|
|
|
|
|
|
|
# if($headers !~ /^Expires: /m))) { |
517
|
|
|
|
|
|
|
# } |
518
|
0
|
0
|
|
|
|
0
|
if($logger) { |
519
|
0
|
|
|
|
|
0
|
$logger->debug("Store $key in the cache, age = $cache_age ", length($cache_hash->{'body'}), ' bytes'); |
520
|
|
|
|
|
|
|
} |
521
|
0
|
|
|
|
|
0
|
$cache->set($key, Storable::freeze($cache_hash), $cache_age); |
522
|
0
|
0
|
|
|
|
0
|
if($generate_last_modified) { |
523
|
0
|
|
|
|
|
0
|
$cobject = $cache->get_object($key); |
524
|
0
|
0
|
|
|
|
0
|
if(defined($cobject)) { |
525
|
0
|
|
|
|
|
0
|
push @o, "Last-Modified: " . HTTP::Date::time2str($cobject->created_at()); |
526
|
|
|
|
|
|
|
} else { |
527
|
0
|
|
|
|
|
0
|
push @o, "Last-Modified: " . HTTP::Date::time2str(time); |
528
|
|
|
|
|
|
|
} |
529
|
|
|
|
|
|
|
} |
530
|
|
|
|
|
|
|
} |
531
|
0
|
0
|
|
|
|
0
|
if($info) { |
532
|
0
|
|
|
|
|
0
|
my $host_name = $info->host_name(); |
533
|
0
|
0
|
|
|
|
0
|
if(defined($x_cache)) { |
534
|
0
|
|
|
|
|
0
|
push @o, "X-Cache: $x_cache from $host_name"; |
535
|
|
|
|
|
|
|
} else { |
536
|
0
|
|
|
|
|
0
|
push @o, "X-Cache: MISS from $host_name"; |
537
|
|
|
|
|
|
|
} |
538
|
0
|
|
|
|
|
0
|
push @o, "X-Cache-Lookup: MISS from $host_name"; |
539
|
|
|
|
|
|
|
} else { |
540
|
0
|
0
|
|
|
|
0
|
if(defined($x_cache)) { |
541
|
0
|
|
|
|
|
0
|
push @o, "X-Cache: $x_cache"; |
542
|
|
|
|
|
|
|
} else { |
543
|
0
|
|
|
|
|
0
|
push @o, 'X-Cache: MISS'; |
544
|
|
|
|
|
|
|
} |
545
|
0
|
|
|
|
|
0
|
push @o, 'X-Cache-Lookup: MISS'; |
546
|
|
|
|
|
|
|
} |
547
|
0
|
|
|
|
|
0
|
push @o, "X-CGI-Buffer-$VERSION: Miss"; |
548
|
|
|
|
|
|
|
} |
549
|
|
|
|
|
|
|
# We don't need it any more, so give Perl a chance to |
550
|
|
|
|
|
|
|
# tidy it up seeing as we're in the destructor |
551
|
0
|
|
|
|
|
0
|
$cache = undef; |
552
|
|
|
|
|
|
|
} elsif($info) { |
553
|
0
|
|
|
|
|
0
|
my $host_name = $info->host_name(); |
554
|
0
|
|
|
|
|
0
|
push @o, ("X-Cache: MISS from $host_name", "X-Cache-Lookup: MISS from $host_name"); |
555
|
0
|
0
|
|
|
|
0
|
if($generate_last_modified) { |
556
|
0
|
0
|
|
|
|
0
|
if(my $age = _my_age()) { |
557
|
0
|
|
|
|
|
0
|
push @o, 'Last-Modified: ' . HTTP::Date::time2str($age); |
558
|
|
|
|
|
|
|
} |
559
|
|
|
|
|
|
|
} |
560
|
0
|
0
|
0
|
|
|
0
|
if($ENV{'HTTP_IF_MODIFIED_SINCE'} && ($status != 304) && $generate_304) { |
|
|
|
0
|
|
|
|
|
561
|
|
|
|
|
|
|
_check_modified_since({ |
562
|
0
|
|
|
|
|
0
|
since => $ENV{'HTTP_IF_MODIFIED_SINCE'}, |
563
|
|
|
|
|
|
|
modified => _my_age() |
564
|
|
|
|
|
|
|
}); |
565
|
|
|
|
|
|
|
} |
566
|
|
|
|
|
|
|
} else { |
567
|
0
|
|
|
|
|
0
|
push @o, ('X-Cache: MISS', 'X-Cache-Lookup: MISS'); |
568
|
|
|
|
|
|
|
} |
569
|
0
|
0
|
0
|
|
|
0
|
if($generate_etag && ((!defined($headers)) || ($headers !~ /^ETag: /m))) { |
|
|
|
0
|
|
|
|
|
570
|
0
|
0
|
0
|
|
|
0
|
if(defined($etag)) { |
|
|
0
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
571
|
0
|
|
|
|
|
0
|
push @o, "ETag: $etag"; |
572
|
0
|
0
|
|
|
|
0
|
if($logger) { |
573
|
0
|
|
|
|
|
0
|
$logger->debug("Set ETag to $etag"); |
574
|
|
|
|
|
|
|
} |
575
|
|
|
|
|
|
|
} elsif($logger && (($status == 200) || $status == 304) && $body && !is_cached()) { |
576
|
0
|
|
|
|
|
0
|
$logger->warn("BUG: ETag not generated, status $status"); |
577
|
|
|
|
|
|
|
} |
578
|
|
|
|
|
|
|
} |
579
|
|
|
|
|
|
|
|
580
|
0
|
|
|
|
|
0
|
my $body_length; |
581
|
0
|
0
|
|
|
|
0
|
if(defined($body)) { |
582
|
0
|
0
|
|
|
|
0
|
if(utf8::is_utf8($body)) { |
583
|
0
|
|
|
|
|
0
|
utf8::encode($body); |
584
|
|
|
|
|
|
|
} |
585
|
0
|
|
|
|
|
0
|
$body_length = length($body); |
586
|
|
|
|
|
|
|
} else { |
587
|
0
|
|
|
|
|
0
|
$body_length = 0; |
588
|
|
|
|
|
|
|
} |
589
|
|
|
|
|
|
|
|
590
|
0
|
0
|
0
|
|
|
0
|
if(defined($headers) && length($headers)) { |
591
|
|
|
|
|
|
|
# Put the original headers first, then those generated within |
592
|
|
|
|
|
|
|
# CGI::Buffer |
593
|
0
|
|
|
|
|
0
|
unshift @o, split(/\r\n/, $headers); |
594
|
0
|
0
|
0
|
|
|
0
|
if($body && $send_body) { |
595
|
0
|
0
|
|
|
|
0
|
if(scalar(grep(/^Content-Length: \d/, @o)) == 0) { |
596
|
0
|
|
|
|
|
0
|
push @o, "Content-Length: $body_length"; |
597
|
|
|
|
|
|
|
} |
598
|
|
|
|
|
|
|
} |
599
|
0
|
0
|
|
|
|
0
|
if(scalar(grep(/^Status: \d/, @o)) == 0) { |
600
|
0
|
|
|
|
|
0
|
require HTTP::Status; |
601
|
0
|
|
|
|
|
0
|
HTTP::Status->import(); |
602
|
|
|
|
|
|
|
|
603
|
0
|
|
|
|
|
0
|
push @o, "Status: $status " . HTTP::Status::status_message($status); |
604
|
|
|
|
|
|
|
} |
605
|
|
|
|
|
|
|
} else { |
606
|
0
|
|
|
|
|
0
|
push @o, "X-CGI-Buffer-$VERSION: No headers"; |
607
|
|
|
|
|
|
|
} |
608
|
|
|
|
|
|
|
|
609
|
0
|
0
|
0
|
|
|
0
|
if($body_length && $send_body) { |
610
|
0
|
|
|
|
|
0
|
push @o, ('', $body); |
611
|
|
|
|
|
|
|
} |
612
|
|
|
|
|
|
|
|
613
|
|
|
|
|
|
|
# XXXXXXXXXXXXXXXXXXXXXXX |
614
|
0
|
0
|
|
|
|
0
|
if(0) { |
615
|
|
|
|
|
|
|
# This code helps to debug Wide character prints |
616
|
|
|
|
|
|
|
my $wideCharWarningsIssued = 0; |
617
|
|
|
|
|
|
|
my $widemess; |
618
|
|
|
|
|
|
|
$SIG{__WARN__} = sub { |
619
|
|
|
|
|
|
|
$wideCharWarningsIssued += "@_" =~ /Wide character in .../; |
620
|
|
|
|
|
|
|
$widemess = "@_"; |
621
|
|
|
|
|
|
|
if($logger) { |
622
|
|
|
|
|
|
|
$logger->fatal($widemess); |
623
|
|
|
|
|
|
|
my $i = 1; |
624
|
|
|
|
|
|
|
$logger->trace('Stack Trace'); |
625
|
|
|
|
|
|
|
while((my @call_details = (caller($i++)))) { |
626
|
|
|
|
|
|
|
$logger->trace($call_details[1] . ':' . $call_details[2] . ' in function ' . $call_details[3]); |
627
|
|
|
|
|
|
|
} |
628
|
|
|
|
|
|
|
} |
629
|
|
|
|
|
|
|
CORE::warn(@_); # call the builtin warn as usual |
630
|
|
|
|
|
|
|
}; |
631
|
|
|
|
|
|
|
|
632
|
|
|
|
|
|
|
if(scalar @o) { |
633
|
|
|
|
|
|
|
print join("\r\n", @o); |
634
|
|
|
|
|
|
|
if($wideCharWarningsIssued) { |
635
|
|
|
|
|
|
|
my $mess = join("\r\n", @o); |
636
|
|
|
|
|
|
|
$mess =~ /[^\x00-\xFF]/; |
637
|
|
|
|
|
|
|
open(my $fout, '>>', '/tmp/NJH'); |
638
|
|
|
|
|
|
|
print $fout "$widemess:\n"; |
639
|
|
|
|
|
|
|
print $fout $mess; |
640
|
|
|
|
|
|
|
print $fout 'x' x 40, "\n"; |
641
|
|
|
|
|
|
|
close $fout; |
642
|
|
|
|
|
|
|
} |
643
|
|
|
|
|
|
|
} |
644
|
0
|
|
|
|
|
0
|
} elsif(scalar @o) { |
645
|
0
|
|
|
|
|
0
|
print join("\r\n", @o); |
646
|
|
|
|
|
|
|
} |
647
|
|
|
|
|
|
|
# XXXXXXXXXXXXXXXXXXXXXXX |
648
|
|
|
|
|
|
|
|
649
|
0
|
0
|
0
|
|
|
0
|
if((!$send_body) || !defined($body)) { |
650
|
0
|
|
|
|
|
0
|
print "\r\n\r\n"; |
651
|
|
|
|
|
|
|
} |
652
|
|
|
|
|
|
|
} |
653
|
|
|
|
|
|
|
|
654
|
|
|
|
|
|
|
sub _check_modified_since { |
655
|
0
|
0
|
|
0
|
|
0
|
if($logger) { |
656
|
0
|
|
|
|
|
0
|
$logger->trace('In _check_modified_since'); |
657
|
|
|
|
|
|
|
} |
658
|
|
|
|
|
|
|
|
659
|
0
|
0
|
|
|
|
0
|
if(!$generate_304) { |
660
|
0
|
|
|
|
|
0
|
return; |
661
|
|
|
|
|
|
|
} |
662
|
0
|
|
|
|
|
0
|
my $params = shift; |
663
|
|
|
|
|
|
|
|
664
|
0
|
0
|
|
|
|
0
|
if(!defined($$params{since})) { |
665
|
0
|
|
|
|
|
0
|
return; |
666
|
|
|
|
|
|
|
} |
667
|
0
|
|
|
|
|
0
|
my $s = HTTP::Date::str2time($$params{since}); |
668
|
0
|
0
|
|
|
|
0
|
if(!defined($s)) { |
669
|
|
|
|
|
|
|
# IF_MODIFIED_SINCE isn't a valid data |
670
|
0
|
|
|
|
|
0
|
return; |
671
|
|
|
|
|
|
|
} |
672
|
|
|
|
|
|
|
|
673
|
0
|
|
|
|
|
0
|
my $age = _my_age(); |
674
|
0
|
0
|
|
|
|
0
|
if(!defined($age)) { |
675
|
0
|
|
|
|
|
0
|
return; |
676
|
|
|
|
|
|
|
} |
677
|
0
|
0
|
|
|
|
0
|
if($age > $s) { |
678
|
0
|
0
|
|
|
|
0
|
if($logger) { |
679
|
0
|
|
|
|
|
0
|
$logger->debug('_check_modified_since: script has been modified'); |
680
|
|
|
|
|
|
|
} |
681
|
|
|
|
|
|
|
# Script has been updated so it may produce different output |
682
|
0
|
|
|
|
|
0
|
return; |
683
|
|
|
|
|
|
|
} |
684
|
|
|
|
|
|
|
|
685
|
0
|
0
|
|
|
|
0
|
if($logger) { |
686
|
0
|
|
|
|
|
0
|
$logger->debug("_check_modified_since: Compare $$params{modified} with $s"); |
687
|
|
|
|
|
|
|
} |
688
|
0
|
0
|
|
|
|
0
|
if($$params{modified} <= $s) { |
689
|
0
|
|
|
|
|
0
|
push @o, "Status: 304 Not Modified"; |
690
|
0
|
|
|
|
|
0
|
$status = 304; |
691
|
0
|
|
|
|
|
0
|
$send_body = 0; |
692
|
0
|
0
|
|
|
|
0
|
if($logger) { |
693
|
0
|
|
|
|
|
0
|
$logger->debug('Set status to 304'); |
694
|
|
|
|
|
|
|
} |
695
|
|
|
|
|
|
|
} |
696
|
|
|
|
|
|
|
} |
697
|
|
|
|
|
|
|
|
698
|
|
|
|
|
|
|
# Reduce output, e.g. remove superfluous white-space. |
699
|
|
|
|
|
|
|
sub _optimise_content { |
700
|
|
|
|
|
|
|
# FIXME: regex bad, HTML parser good |
701
|
|
|
|
|
|
|
# Regexp::List - wow! |
702
|
0
|
|
|
0
|
|
0
|
$body =~ s/(((\s+|\r)\n|\n(\s+|\+)))/\n/g; |
703
|
|
|
|
|
|
|
# $body =~ s/\r\n/\n/gs; |
704
|
|
|
|
|
|
|
# $body =~ s/\s+\n/\n/gs; |
705
|
|
|
|
|
|
|
# $body =~ s/\n+/\n/gs; |
706
|
|
|
|
|
|
|
# $body =~ s/\n\s+|\s+\n/\n/g; |
707
|
0
|
|
|
|
|
0
|
$body =~ s/\<\/div\>\s+\<div/\<\/div\>\<div/gis; |
708
|
|
|
|
|
|
|
# $body =~ s/\<\/p\>\s\<\/div/\<\/p\>\<\/div/gis; |
709
|
|
|
|
|
|
|
# $body =~ s/\<div\>\s+/\<div\>/gis; # Remove spaces after <div> |
710
|
0
|
|
|
|
|
0
|
$body =~ s/(<div>\s+|\s+<div>)/<div>/gis; |
711
|
0
|
|
|
|
|
0
|
$body =~ s/\s+<\/div\>/\<\/div\>/gis; # Remove spaces before </div> |
712
|
0
|
|
|
|
|
0
|
$body =~ s/\s+\<p\>|\<p\>\s+/\<p\>/im; # TODO <p class= |
713
|
0
|
|
|
|
|
0
|
$body =~ s/\s+\<\/p\>|\<\/p\>\s+/\<\/p\>/gis; |
714
|
0
|
|
|
|
|
0
|
$body =~ s/<html>\s+<head>/<html><head>/is; |
715
|
0
|
|
|
|
|
0
|
$body =~ s/\s*<\/head>\s+<body>\s*/<\/head><body>/is; |
716
|
0
|
|
|
|
|
0
|
$body =~ s/<html>\s+<body>/<html><body>/is; |
717
|
0
|
|
|
|
|
0
|
$body =~ s/<body>\s+/<body>/is; |
718
|
0
|
|
|
|
|
0
|
$body =~ s/\s+\<\/html/\<\/html/is; |
719
|
0
|
|
|
|
|
0
|
$body =~ s/\s+\<\/body/\<\/body/is; |
720
|
0
|
|
|
|
|
0
|
$body =~ s/\s(\<.+?\>\s\<.+?\>)/$1/; |
721
|
|
|
|
|
|
|
# $body =~ s/(\<.+?\>\s\<.+?\>)\s/$1/g; |
722
|
0
|
|
|
|
|
0
|
$body =~ s/\<p\>\s/\<p\>/gi; |
723
|
0
|
|
|
|
|
0
|
$body =~ s/\<\/p\>\s\<p\>/\<\/p\>\<p\>/gi; |
724
|
0
|
|
|
|
|
0
|
$body =~ s/\<\/tr\>\s\<tr\>/\<\/tr\>\<tr\>/gi; |
725
|
0
|
|
|
|
|
0
|
$body =~ s/\<\/td\>\s\<\/tr\>/\<\/td\>\<\/tr\>/gi; |
726
|
0
|
|
|
|
|
0
|
$body =~ s/\<\/td\>\s*\<td\>/\<\/td\>\<td\>/gis; |
727
|
0
|
|
|
|
|
0
|
$body =~ s/\<\/tr\>\s\<\/table\>/\<\/tr\>\<\/table\>/gi; |
728
|
0
|
|
|
|
|
0
|
$body =~ s/\<br\s?\/?\>\s?\<p\>/\<p\>/gi; |
729
|
0
|
|
|
|
|
0
|
$body =~ s/\<br\>\s/\<br\>/gi; |
730
|
0
|
|
|
|
|
0
|
$body =~ s/\s+\<br/\<br/gi; |
731
|
0
|
|
|
|
|
0
|
$body =~ s/\<br\s?\/\>\s/\<br \/\>/gi; |
732
|
0
|
|
|
|
|
0
|
$body =~ s/[ \t]+/ /gs; # Remove duplicate space, don't use \s+ it breaks JavaScript |
733
|
0
|
|
|
|
|
0
|
$body =~ s/\s\<p\>/\<p\>/gi; |
734
|
0
|
|
|
|
|
0
|
$body =~ s/\s\<script/\<script/gi; |
735
|
0
|
|
|
|
|
0
|
$body =~ s/(<script>\s|\s<script>)/<script>/gis; |
736
|
0
|
|
|
|
|
0
|
$body =~ s/(<\/script>\s|\s<\/script>)/<\/script>/gis; |
737
|
0
|
|
|
|
|
0
|
$body =~ s/\<td\>\s/\<td\>/gi; |
738
|
0
|
|
|
|
|
0
|
$body =~ s/\s+\<a\shref="(.+?)"\>\s?/ <a href="$1">/gis; |
739
|
0
|
|
|
|
|
0
|
$body =~ s/\s?<a\shref=\s"(.+?)"\>/ <a href="$1">/gis; |
740
|
0
|
|
|
|
|
0
|
$body =~ s/\s+<\/a\>\s+/<\/a> /gis; |
741
|
0
|
|
|
|
|
0
|
$body =~ s/(\s?<hr>\s+|\s+<hr>\s?)/<hr>/gis; |
742
|
|
|
|
|
|
|
# $body =~ s/\s<hr>/<hr>/gis; |
743
|
|
|
|
|
|
|
# $body =~ s/<hr>\s/<hr>/gis; |
744
|
0
|
|
|
|
|
0
|
$body =~ s/<\/li>\s+<li>/<\/li><li>/gis; |
745
|
0
|
|
|
|
|
0
|
$body =~ s/<\/li>\s+<\/ul>/<\/li><\/ul>/gis; |
746
|
0
|
|
|
|
|
0
|
$body =~ s/<ul>\s+<li>/<ul><li>/gis; |
747
|
0
|
|
|
|
|
0
|
$body =~ s/\s+<\/li>/<\/li>/gis; |
748
|
0
|
|
|
|
|
0
|
$body =~ s/\<\/option\>\s+\<option/\<\/option\>\<option/gis; |
749
|
0
|
|
|
|
|
0
|
$body =~ s/<title>\s*(.+?)\s*<\/title>/<title>$1<\/title>/is; |
750
|
0
|
|
|
|
|
0
|
$body =~ s/<\/center>\s+<center>/ /gis; |
751
|
|
|
|
|
|
|
} |
752
|
|
|
|
|
|
|
|
753
|
|
|
|
|
|
|
# Create a key for the cache |
754
|
|
|
|
|
|
|
sub _generate_key { |
755
|
0
|
0
|
|
0
|
|
0
|
if($cache_key) { |
756
|
0
|
|
|
|
|
0
|
return $cache_key; |
757
|
|
|
|
|
|
|
} |
758
|
0
|
0
|
|
|
|
0
|
unless(defined($info)) { |
759
|
0
|
|
|
|
|
0
|
$info = CGI::Info->new({ cache => $cache }); |
760
|
|
|
|
|
|
|
} |
761
|
|
|
|
|
|
|
|
762
|
0
|
|
|
|
|
0
|
my $key = $info->browser_type() . '::' . $info->domain_name() . '::' . $info->script_name() . '::' . $info->as_string(); |
763
|
0
|
0
|
|
|
|
0
|
if($lingua) { |
764
|
0
|
|
|
|
|
0
|
$key .= '::' . $lingua->language(); |
765
|
|
|
|
|
|
|
} |
766
|
0
|
0
|
|
|
|
0
|
if($ENV{'HTTP_COOKIE'}) { |
767
|
|
|
|
|
|
|
# Different states of the client are stored in different caches |
768
|
|
|
|
|
|
|
# Don't put different Google Analytics in different caches, and anyway they |
769
|
|
|
|
|
|
|
# would be wrong |
770
|
0
|
|
|
|
|
0
|
foreach my $cookie(split(/;/, $ENV{'HTTP_COOKIE'})) { |
771
|
0
|
0
|
|
|
|
0
|
unless($cookie =~ /^__utm[abcz]/) { |
772
|
0
|
|
|
|
|
0
|
$key .= "::$cookie"; |
773
|
|
|
|
|
|
|
} |
774
|
|
|
|
|
|
|
} |
775
|
|
|
|
|
|
|
} |
776
|
|
|
|
|
|
|
|
777
|
|
|
|
|
|
|
# Honour the Vary headers |
778
|
0
|
0
|
0
|
|
|
0
|
if($headers && ($headers =~ /^Vary: .*$/m)) { |
779
|
0
|
0
|
|
|
|
0
|
if(defined($logger)) { |
780
|
0
|
|
|
|
|
0
|
$logger->debug('Found Vary header'); |
781
|
|
|
|
|
|
|
} |
782
|
0
|
|
|
|
|
0
|
foreach my $h1(split(/\r?\n/, $headers)) { |
783
|
0
|
|
|
|
|
0
|
my ($h1_name, $h1_value) = split /\:\s*/, $h1, 2; |
784
|
0
|
0
|
|
|
|
0
|
if(lc($h1_name) eq 'vary') { |
785
|
0
|
|
|
|
|
0
|
foreach my $h2(split(/\r?\n/, $headers)) { |
786
|
0
|
|
|
|
|
0
|
my ($h2_name, $h2_value) = split /\:\s*/, $h2, 2; |
787
|
0
|
0
|
|
|
|
0
|
if($h2_name eq $h1_value) { |
788
|
0
|
|
|
|
|
0
|
$key .= '::' . $h2_value; |
789
|
0
|
|
|
|
|
0
|
last; |
790
|
|
|
|
|
|
|
} |
791
|
|
|
|
|
|
|
} |
792
|
|
|
|
|
|
|
} |
793
|
|
|
|
|
|
|
} |
794
|
|
|
|
|
|
|
} |
795
|
0
|
|
|
|
|
0
|
$key =~ s/\//::/g; |
796
|
0
|
|
|
|
|
0
|
$key =~ s/::::/::/g; |
797
|
0
|
|
|
|
|
0
|
$key =~ s/::$//; |
798
|
0
|
0
|
|
|
|
0
|
if(defined($logger)) { |
799
|
0
|
|
|
|
|
0
|
$logger->trace("Returning $key"); |
800
|
|
|
|
|
|
|
} |
801
|
0
|
|
|
|
|
0
|
$cache_key = $key; |
802
|
0
|
|
|
|
|
0
|
return $key; |
803
|
|
|
|
|
|
|
} |
804
|
|
|
|
|
|
|
|
805
|
|
|
|
|
|
|
=head2 init |
806
|
|
|
|
|
|
|
|
807
|
|
|
|
|
|
|
Set various options and override default values. |
808
|
|
|
|
|
|
|
|
809
|
|
|
|
|
|
|
# Put this toward the top of your program before you do anything |
810
|
|
|
|
|
|
|
# By default, generate_tag, generate_304 and compress_content are ON, |
811
|
|
|
|
|
|
|
# optimise_content and lint_content are OFF. Set optimise_content to 2 to |
812
|
|
|
|
|
|
|
# do aggressive JavaScript optimisations which may fail. |
813
|
|
|
|
|
|
|
use CGI::Buffer; |
814
|
|
|
|
|
|
|
CGI::Buffer::init( |
815
|
|
|
|
|
|
|
generate_etag => 1, # make good use of client's cache |
816
|
|
|
|
|
|
|
generate_last_modified => 1, # more use of client's cache |
817
|
|
|
|
|
|
|
compress_content => 1, # if gzip the output |
818
|
|
|
|
|
|
|
optimise_content => 0, # optimise your program's HTML, CSS and JavaScript |
819
|
|
|
|
|
|
|
cache => CHI->new(driver => 'File'), # cache requests |
820
|
|
|
|
|
|
|
cache_key => 'string', # key for the cache |
821
|
|
|
|
|
|
|
cache_age => '10 minutes', # how long to store responses in the cache |
822
|
|
|
|
|
|
|
logger => $logger, |
823
|
|
|
|
|
|
|
lint_content => 0, # Pass through HTML::Lint |
824
|
|
|
|
|
|
|
generate_304 => 1, # Generate 304: Not modified |
825
|
|
|
|
|
|
|
lingua => CGI::Lingua->new(), |
826
|
|
|
|
|
|
|
); |
827
|
|
|
|
|
|
|
|
828
|
|
|
|
|
|
|
If no cache_key is given, one will be generated which may not be unique. |
829
|
|
|
|
|
|
|
The cache_key should be a unique value dependent upon the values set by the |
830
|
|
|
|
|
|
|
browser. |
831
|
|
|
|
|
|
|
|
832
|
|
|
|
|
|
|
The cache object will be an object that understands get_object(), |
833
|
|
|
|
|
|
|
set(), remove() and created_at() messages, such as an L<CHI> object. It is |
834
|
|
|
|
|
|
|
used as a server-side cache to reduce the need to rerun database accesses. |
835
|
|
|
|
|
|
|
|
836
|
|
|
|
|
|
|
Items stay in the server-side cache by default for 10 minutes. |
837
|
|
|
|
|
|
|
This can be overridden by the cache_control HTTP header in the request, and |
838
|
|
|
|
|
|
|
the default can be changed by the cache_age argument to init(). |
839
|
|
|
|
|
|
|
|
840
|
|
|
|
|
|
|
Logger will be an object that understands debug() such as an L<Log::Log4perl> |
841
|
|
|
|
|
|
|
object. |
842
|
|
|
|
|
|
|
|
843
|
|
|
|
|
|
|
To generate a last_modified header, you must give a cache object. |
844
|
|
|
|
|
|
|
|
845
|
|
|
|
|
|
|
Init allows a reference of the options to be passed. So both of these work: |
846
|
|
|
|
|
|
|
use CGI::Buffer; |
847
|
|
|
|
|
|
|
#... |
848
|
|
|
|
|
|
|
CGI::Buffer::init(generate_etag => 1); |
849
|
|
|
|
|
|
|
CGI::Buffer::init({ generate_etag => 1, info => CGI::Info->new() }); |
850
|
|
|
|
|
|
|
|
851
|
|
|
|
|
|
|
Generally speaking, passing by reference is better since it copies less on to |
852
|
|
|
|
|
|
|
the stack. |
853
|
|
|
|
|
|
|
|
854
|
|
|
|
|
|
|
Alternatively you can give the options when loading the package: |
855
|
|
|
|
|
|
|
use CGI::Buffer { optimise_content => 1 }; |
856
|
|
|
|
|
|
|
|
857
|
|
|
|
|
|
|
=cut |
858
|
|
|
|
|
|
|
|
859
|
|
|
|
|
|
|
sub init { |
860
|
0
|
0
|
|
0
|
1
|
0
|
my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_; |
|
0
|
|
|
|
|
0
|
|
861
|
|
|
|
|
|
|
|
862
|
|
|
|
|
|
|
# Safe options - can be called at any time |
863
|
0
|
0
|
|
|
|
0
|
if(defined($params{generate_etag})) { |
864
|
0
|
|
|
|
|
0
|
$generate_etag = $params{generate_etag}; |
865
|
|
|
|
|
|
|
} |
866
|
0
|
0
|
|
|
|
0
|
if(defined($params{generate_last_modified})) { |
867
|
0
|
|
|
|
|
0
|
$generate_last_modified = $params{generate_last_modified}; |
868
|
|
|
|
|
|
|
} |
869
|
0
|
0
|
|
|
|
0
|
if(defined($params{compress_content})) { |
870
|
0
|
|
|
|
|
0
|
$compress_content = $params{compress_content}; |
871
|
|
|
|
|
|
|
} |
872
|
0
|
0
|
|
|
|
0
|
if(defined($params{optimise_content})) { |
873
|
0
|
|
|
|
|
0
|
$optimise_content = $params{optimise_content}; |
874
|
|
|
|
|
|
|
} |
875
|
0
|
0
|
|
|
|
0
|
if(defined($params{lint_content})) { |
876
|
0
|
|
|
|
|
0
|
$lint_content = $params{lint_content}; |
877
|
|
|
|
|
|
|
} |
878
|
0
|
0
|
|
|
|
0
|
if(defined($params{logger})) { |
879
|
0
|
|
|
|
|
0
|
$logger = $params{logger}; |
880
|
|
|
|
|
|
|
} |
881
|
0
|
0
|
|
|
|
0
|
if(defined($params{lingua})) { |
882
|
0
|
|
|
|
|
0
|
$lingua = $params{lingua}; |
883
|
|
|
|
|
|
|
} |
884
|
0
|
0
|
|
|
|
0
|
if(defined($params{generate_304})) { |
885
|
0
|
|
|
|
|
0
|
$generate_304 = $params{generate_304}; |
886
|
|
|
|
|
|
|
} |
887
|
0
|
0
|
0
|
|
|
0
|
if(defined($params{info}) && (!defined($info))) { |
888
|
0
|
|
|
|
|
0
|
$info = $params{info}; |
889
|
|
|
|
|
|
|
} |
890
|
|
|
|
|
|
|
|
891
|
|
|
|
|
|
|
# Unsafe options - must be called before output has been started |
892
|
0
|
|
|
|
|
0
|
my $pos = $CGI::Buffer::buf->getpos; |
893
|
0
|
0
|
|
|
|
0
|
if($pos > 0) { |
894
|
0
|
0
|
|
|
|
0
|
if(defined($logger)) { |
895
|
0
|
|
|
|
|
0
|
my @call_details = caller(0); |
896
|
0
|
|
|
|
|
0
|
$logger->warn("Too late to call init, $pos characters have been printed, caller line $call_details[2] of $call_details[1]"); |
897
|
|
|
|
|
|
|
} else { |
898
|
|
|
|
|
|
|
# Must do Carp::carp instead of carp for Test::Carp |
899
|
0
|
|
|
|
|
0
|
Carp::carp "Too late to call init, $pos characters have been printed"; |
900
|
|
|
|
|
|
|
} |
901
|
|
|
|
|
|
|
} |
902
|
0
|
0
|
0
|
|
|
0
|
if(defined($params{cache}) && can_cache()) { |
903
|
0
|
0
|
|
|
|
0
|
if(defined($ENV{'HTTP_CACHE_CONTROL'})) { |
904
|
0
|
|
|
|
|
0
|
my $control = $ENV{'HTTP_CACHE_CONTROL'}; |
905
|
0
|
0
|
|
|
|
0
|
if(defined($logger)) { |
906
|
0
|
|
|
|
|
0
|
$logger->debug("cache_control = $control"); |
907
|
|
|
|
|
|
|
} |
908
|
0
|
0
|
|
|
|
0
|
if($control =~ /^max-age\s*=\s*(\d+)$/) { |
909
|
|
|
|
|
|
|
# There is an argument not to do this |
910
|
|
|
|
|
|
|
# since one client will affect others |
911
|
0
|
|
|
|
|
0
|
$cache_age = "$1 seconds"; |
912
|
0
|
0
|
|
|
|
0
|
if(defined($logger)) { |
913
|
0
|
|
|
|
|
0
|
$logger->debug("cache_age = $cache_age"); |
914
|
|
|
|
|
|
|
} |
915
|
|
|
|
|
|
|
} |
916
|
|
|
|
|
|
|
} |
917
|
0
|
|
0
|
|
|
0
|
$cache_age ||= $params{cache_age}; |
918
|
|
|
|
|
|
|
|
919
|
0
|
0
|
0
|
|
|
0
|
if((!defined($params{cache})) && defined($cache)) { |
920
|
0
|
0
|
|
|
|
0
|
if(defined($logger)) { |
921
|
0
|
0
|
|
|
|
0
|
if($cache_key) { |
922
|
0
|
|
|
|
|
0
|
$logger->debug("disabling cache $cache_key"); |
923
|
|
|
|
|
|
|
} else { |
924
|
0
|
|
|
|
|
0
|
$logger->debug('disabling cache'); |
925
|
|
|
|
|
|
|
} |
926
|
|
|
|
|
|
|
} |
927
|
0
|
|
|
|
|
0
|
$cache = undef; |
928
|
|
|
|
|
|
|
} else { |
929
|
0
|
|
|
|
|
0
|
$cache = $params{cache}; |
930
|
|
|
|
|
|
|
} |
931
|
0
|
0
|
|
|
|
0
|
if(defined($params{cache_key})) { |
932
|
0
|
|
|
|
|
0
|
$cache_key = $params{cache_key}; |
933
|
|
|
|
|
|
|
} |
934
|
|
|
|
|
|
|
} |
935
|
|
|
|
|
|
|
} |
936
|
|
|
|
|
|
|
|
937
|
|
|
|
|
|
|
sub import { |
938
|
|
|
|
|
|
|
# my $class = shift; |
939
|
6
|
|
|
6
|
|
76
|
shift; |
940
|
|
|
|
|
|
|
|
941
|
6
|
50
|
|
|
|
88
|
return unless @_; |
942
|
|
|
|
|
|
|
|
943
|
0
|
|
|
|
|
0
|
init(@_); |
944
|
|
|
|
|
|
|
} |
945
|
|
|
|
|
|
|
|
946
|
|
|
|
|
|
|
=head2 set_options |
947
|
|
|
|
|
|
|
|
948
|
|
|
|
|
|
|
Synonym for init, kept for historical reasons. |
949
|
|
|
|
|
|
|
|
950
|
|
|
|
|
|
|
=cut |
951
|
|
|
|
|
|
|
|
952
|
|
|
|
|
|
|
sub set_options { |
953
|
0
|
0
|
|
0
|
1
|
0
|
my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_; |
|
0
|
|
|
|
|
0
|
|
954
|
|
|
|
|
|
|
|
955
|
0
|
|
|
|
|
0
|
init(\%params); |
956
|
|
|
|
|
|
|
} |
957
|
|
|
|
|
|
|
|
958
|
|
|
|
|
|
|
=head2 can_cache |
959
|
|
|
|
|
|
|
|
960
|
|
|
|
|
|
|
Returns true if the server is allowed to store the results locally. |
961
|
|
|
|
|
|
|
|
962
|
|
|
|
|
|
|
=cut |
963
|
|
|
|
|
|
|
|
964
|
|
|
|
|
|
|
sub can_cache { |
965
|
3
|
50
|
|
3
|
1
|
807
|
if(defined($x_cache)) { |
966
|
0
|
|
|
|
|
0
|
return ($x_cache eq 'HIT'); |
967
|
|
|
|
|
|
|
} |
968
|
|
|
|
|
|
|
|
969
|
3
|
50
|
33
|
|
|
28
|
if(defined($ENV{'NO_CACHE'}) || defined($ENV{'NO_STORE'})) { |
970
|
0
|
|
|
|
|
0
|
$x_cache = 'MISS'; |
971
|
0
|
|
|
|
|
0
|
return 0; |
972
|
|
|
|
|
|
|
} |
973
|
3
|
50
|
|
|
|
16
|
if(defined($ENV{'HTTP_CACHE_CONTROL'})) { |
974
|
0
|
|
|
|
|
0
|
my $control = $ENV{'HTTP_CACHE_CONTROL'}; |
975
|
0
|
0
|
|
|
|
0
|
if(defined($logger)) { |
976
|
0
|
|
|
|
|
0
|
$logger->debug("cache_control = $control"); |
977
|
|
|
|
|
|
|
} |
978
|
|
|
|
|
|
|
# TODO: check Authorization header not present |
979
|
0
|
0
|
0
|
|
|
0
|
if(($control eq 'no-store') || |
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
980
|
|
|
|
|
|
|
($control eq 'no-cache') || |
981
|
|
|
|
|
|
|
($control eq 'max-age=0') || |
982
|
|
|
|
|
|
|
($control eq 'private')) { |
983
|
0
|
|
|
|
|
0
|
$x_cache = 'MISS'; |
984
|
0
|
|
|
|
|
0
|
return 0; |
985
|
|
|
|
|
|
|
} |
986
|
|
|
|
|
|
|
} |
987
|
3
|
|
|
|
|
8
|
$x_cache = 'HIT'; |
988
|
3
|
|
|
|
|
16
|
return 1; |
989
|
|
|
|
|
|
|
} |
990
|
|
|
|
|
|
|
|
991
|
|
|
|
|
|
|
=head2 is_cached |
992
|
|
|
|
|
|
|
|
993
|
|
|
|
|
|
|
Returns true if the output is cached. If it is then it means that all of the |
994
|
|
|
|
|
|
|
expensive routines in the CGI script can be by-passed because we already have |
995
|
|
|
|
|
|
|
the result stored in the cache. |
996
|
|
|
|
|
|
|
|
997
|
|
|
|
|
|
|
# Put this toward the top of your program before you do anything |
998
|
|
|
|
|
|
|
|
999
|
|
|
|
|
|
|
# Example key generation - use whatever you want as something |
1000
|
|
|
|
|
|
|
# unique for this call, so that subsequent calls with the same |
1001
|
|
|
|
|
|
|
# values match something in the cache |
1002
|
|
|
|
|
|
|
use CGI::Info; |
1003
|
|
|
|
|
|
|
use CGI::Lingua; |
1004
|
|
|
|
|
|
|
use CGI::Buffer; |
1005
|
|
|
|
|
|
|
|
1006
|
|
|
|
|
|
|
my $i = CGI::Info->new(); |
1007
|
|
|
|
|
|
|
my $l = CGI::Lingua->new(supported => ['en']); |
1008
|
|
|
|
|
|
|
|
1009
|
|
|
|
|
|
|
# To use server side caching you must give the cache argument, however |
1010
|
|
|
|
|
|
|
# the cache_key argument is optional - if you don't give one then one will |
1011
|
|
|
|
|
|
|
# be generated for you |
1012
|
|
|
|
|
|
|
if(CGI::Buffer::can_cache()) { |
1013
|
|
|
|
|
|
|
CGI::Buffer::init( |
1014
|
|
|
|
|
|
|
cache => CHI->new(driver => 'File'), |
1015
|
|
|
|
|
|
|
cache_key => $i->domain_name() . '/' . $i->script_name() . '/' . $i->as_string() . '/' . $l->language() |
1016
|
|
|
|
|
|
|
); |
1017
|
|
|
|
|
|
|
if(CGI::Buffer::is_cached()) { |
1018
|
|
|
|
|
|
|
# Output will be retrieved from the cache and sent automatically |
1019
|
|
|
|
|
|
|
exit; |
1020
|
|
|
|
|
|
|
} |
1021
|
|
|
|
|
|
|
} |
1022
|
|
|
|
|
|
|
# Not in the cache, so now do our expensive computing to generate the |
1023
|
|
|
|
|
|
|
# results |
1024
|
|
|
|
|
|
|
print "Content-type: text/html\n"; |
1025
|
|
|
|
|
|
|
# ... |
1026
|
|
|
|
|
|
|
|
1027
|
|
|
|
|
|
|
=cut |
1028
|
|
|
|
|
|
|
|
1029
|
|
|
|
|
|
|
sub is_cached { |
1030
|
9
|
50
|
|
9
|
1
|
174
|
unless($cache) { |
1031
|
9
|
50
|
|
|
|
40
|
if($logger) { |
1032
|
0
|
|
|
|
|
0
|
$logger->debug("is_cached: cache hasn't been enabled"); |
1033
|
|
|
|
|
|
|
} |
1034
|
9
|
|
|
|
|
59
|
return 0; |
1035
|
|
|
|
|
|
|
} |
1036
|
|
|
|
|
|
|
|
1037
|
0
|
|
|
|
|
|
my $key = _generate_key(); |
1038
|
|
|
|
|
|
|
|
1039
|
0
|
0
|
|
|
|
|
if($logger) { |
1040
|
0
|
|
|
|
|
|
$logger->debug("is_cached: looking for key = $key"); |
1041
|
|
|
|
|
|
|
} |
1042
|
0
|
|
|
|
|
|
$cobject = $cache->get_object($key); |
1043
|
0
|
0
|
|
|
|
|
unless($cobject) { |
1044
|
0
|
0
|
|
|
|
|
if($logger) { |
1045
|
0
|
|
|
|
|
|
$logger->debug('not found in cache'); |
1046
|
|
|
|
|
|
|
} |
1047
|
0
|
|
|
|
|
|
return 0; |
1048
|
|
|
|
|
|
|
} |
1049
|
0
|
0
|
|
|
|
|
unless($cobject->value($key)) { |
1050
|
0
|
0
|
|
|
|
|
if($logger) { |
1051
|
0
|
|
|
|
|
|
$logger->warn('is_cached: object is in the cache but not the data'); |
1052
|
|
|
|
|
|
|
} |
1053
|
0
|
|
|
|
|
|
$cobject = undef; |
1054
|
0
|
|
|
|
|
|
return 0; |
1055
|
|
|
|
|
|
|
} |
1056
|
|
|
|
|
|
|
|
1057
|
|
|
|
|
|
|
# If the script has changed, don't use the cache since we may produce |
1058
|
|
|
|
|
|
|
# different output |
1059
|
0
|
|
|
|
|
|
my $age = _my_age(); |
1060
|
0
|
0
|
|
|
|
|
unless(defined($age)) { |
1061
|
0
|
0
|
|
|
|
|
if($logger) { |
1062
|
0
|
|
|
|
|
|
$logger->debug("Can't determine script's age"); |
1063
|
|
|
|
|
|
|
} |
1064
|
|
|
|
|
|
|
# Can't determine the age. Play it safe an assume we're not |
1065
|
|
|
|
|
|
|
# cached |
1066
|
0
|
|
|
|
|
|
$cobject = undef; |
1067
|
0
|
|
|
|
|
|
return 0; |
1068
|
|
|
|
|
|
|
} |
1069
|
0
|
0
|
|
|
|
|
if($age > $cobject->created_at()) { |
1070
|
|
|
|
|
|
|
# Script has been updated so it may produce different output |
1071
|
0
|
0
|
|
|
|
|
if($logger) { |
1072
|
0
|
|
|
|
|
|
$logger->debug('Script has been updated'); |
1073
|
|
|
|
|
|
|
} |
1074
|
0
|
|
|
|
|
|
$cobject = undef; |
1075
|
|
|
|
|
|
|
# Nothing will be in date and all new searches would miss |
1076
|
|
|
|
|
|
|
# anyway, so may as well clear it all |
1077
|
|
|
|
|
|
|
# FIXME: RT104471 |
1078
|
|
|
|
|
|
|
# $cache->clear(); |
1079
|
0
|
|
|
|
|
|
return 0; |
1080
|
|
|
|
|
|
|
} |
1081
|
0
|
0
|
|
|
|
|
if($logger) { |
1082
|
0
|
|
|
|
|
|
$logger->debug('Script is in the cache'); |
1083
|
|
|
|
|
|
|
} |
1084
|
0
|
|
|
|
|
|
return 1; |
1085
|
|
|
|
|
|
|
} |
1086
|
|
|
|
|
|
|
|
1087
|
|
|
|
|
|
|
sub _my_age { |
1088
|
0
|
0
|
|
0
|
|
|
if($script_mtime) { |
1089
|
0
|
|
|
|
|
|
return $script_mtime; |
1090
|
|
|
|
|
|
|
} |
1091
|
0
|
0
|
|
|
|
|
unless(defined($info)) { |
1092
|
0
|
0
|
|
|
|
|
if($cache) { |
1093
|
0
|
|
|
|
|
|
$info = CGI::Info->new({ cache => $cache }); |
1094
|
|
|
|
|
|
|
} else { |
1095
|
0
|
|
|
|
|
|
$info = CGI::Info->new(); |
1096
|
|
|
|
|
|
|
} |
1097
|
|
|
|
|
|
|
} |
1098
|
|
|
|
|
|
|
|
1099
|
0
|
|
|
|
|
|
my $path = $info->script_path(); |
1100
|
0
|
0
|
|
|
|
|
unless(defined($path)) { |
1101
|
0
|
|
|
|
|
|
return; |
1102
|
|
|
|
|
|
|
} |
1103
|
|
|
|
|
|
|
|
1104
|
0
|
|
|
|
|
|
my @statb = stat($path); |
1105
|
0
|
|
|
|
|
|
$script_mtime = $statb[9]; |
1106
|
0
|
|
|
|
|
|
return $script_mtime; |
1107
|
|
|
|
|
|
|
} |
1108
|
|
|
|
|
|
|
|
1109
|
|
|
|
|
|
|
sub _should_gzip |
1110
|
|
|
|
|
|
|
{ |
1111
|
0
|
0
|
0
|
0
|
|
|
if($compress_content && ($ENV{'HTTP_ACCEPT_ENCODING'} || $ENV{'HTTP_TE'})) { |
|
|
|
0
|
|
|
|
|
1112
|
0
|
0
|
|
|
|
|
if(scalar(@content_type)) { |
1113
|
0
|
0
|
|
|
|
|
if($content_type[0] ne 'text') { |
1114
|
0
|
|
|
|
|
|
return ''; |
1115
|
|
|
|
|
|
|
} |
1116
|
|
|
|
|
|
|
} |
1117
|
0
|
0
|
|
|
|
|
my $accept = lc($ENV{'HTTP_ACCEPT_ENCODING'} ? $ENV{'HTTP_ACCEPT_ENCODING'} : $ENV{'HTTP_TE'}); |
1118
|
0
|
|
|
|
|
|
foreach my $method(split(/,\s?/, $accept)) { |
1119
|
0
|
0
|
0
|
|
|
|
if(($method eq 'gzip') || ($method eq 'x-gzip') || ($method eq 'br')) { |
|
|
|
0
|
|
|
|
|
1120
|
0
|
|
|
|
|
|
return $method; |
1121
|
|
|
|
|
|
|
} |
1122
|
|
|
|
|
|
|
} |
1123
|
|
|
|
|
|
|
} |
1124
|
|
|
|
|
|
|
|
1125
|
0
|
|
|
|
|
|
return ''; |
1126
|
|
|
|
|
|
|
} |
1127
|
|
|
|
|
|
|
|
1128
|
|
|
|
|
|
|
sub _set_content_type |
1129
|
|
|
|
|
|
|
{ |
1130
|
0
|
|
|
0
|
|
|
my $headers = shift; |
1131
|
|
|
|
|
|
|
|
1132
|
0
|
|
|
|
|
|
foreach my $header (split(/\r?\n/, $headers)) { |
1133
|
0
|
|
|
|
|
|
my ($header_name, $header_value) = split /\:\s*/, $header, 2; |
1134
|
0
|
0
|
|
|
|
|
if (lc($header_name) eq 'content-type') { |
1135
|
0
|
|
|
|
|
|
@content_type = split /\//, $header_value, 2; |
1136
|
0
|
|
|
|
|
|
last; |
1137
|
|
|
|
|
|
|
} |
1138
|
|
|
|
|
|
|
} |
1139
|
|
|
|
|
|
|
} |
1140
|
|
|
|
|
|
|
|
1141
|
|
|
|
|
|
|
sub _compress { |
1142
|
0
|
0
|
|
0
|
|
|
my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_; |
|
0
|
|
|
|
|
|
|
1143
|
|
|
|
|
|
|
|
1144
|
0
|
0
|
|
|
|
|
return unless(defined($body)); |
1145
|
|
|
|
|
|
|
|
1146
|
0
|
|
|
|
|
|
my $encoding = $params{encoding}; |
1147
|
|
|
|
|
|
|
|
1148
|
0
|
0
|
0
|
|
|
|
if((length($encoding) == 0) || (length($body) < MIN_GZIP_LEN)) { |
1149
|
0
|
|
|
|
|
|
return; |
1150
|
|
|
|
|
|
|
} |
1151
|
|
|
|
|
|
|
|
1152
|
0
|
0
|
|
|
|
|
if($encoding eq 'gzip') { |
|
|
0
|
|
|
|
|
|
1153
|
0
|
|
|
|
|
|
require Compress::Zlib; |
1154
|
0
|
|
|
|
|
|
Compress::Zlib->import; |
1155
|
|
|
|
|
|
|
|
1156
|
|
|
|
|
|
|
# Avoid 'Wide character in memGzip' |
1157
|
0
|
0
|
|
|
|
|
unless($encode_loaded) { |
1158
|
0
|
|
|
|
|
|
require Encode; |
1159
|
0
|
|
|
|
|
|
$encode_loaded = 1; |
1160
|
|
|
|
|
|
|
} |
1161
|
0
|
|
|
|
|
|
my $nbody = Compress::Zlib::memGzip(\Encode::encode_utf8($body)); |
1162
|
0
|
0
|
|
|
|
|
if(length($nbody) < length($body)) { |
1163
|
0
|
|
|
|
|
|
$body = $nbody; |
1164
|
0
|
|
|
|
|
|
push @o, "Content-Encoding: $encoding"; |
1165
|
0
|
|
|
|
|
|
push @o, "Vary: Accept-Encoding"; |
1166
|
|
|
|
|
|
|
} |
1167
|
|
|
|
|
|
|
} elsif($encoding eq 'br') { |
1168
|
0
|
|
|
|
|
|
require IO::Compress::Brotli; |
1169
|
0
|
|
|
|
|
|
IO::Compress::Brotli->import(); |
1170
|
|
|
|
|
|
|
|
1171
|
|
|
|
|
|
|
# Avoid 'Wide character in memGzip' |
1172
|
0
|
0
|
|
|
|
|
unless($encode_loaded) { |
1173
|
0
|
|
|
|
|
|
require Encode; |
1174
|
0
|
|
|
|
|
|
$encode_loaded = 1; |
1175
|
|
|
|
|
|
|
} |
1176
|
0
|
|
|
|
|
|
my $nbody = IO::Compress::Brotli::bro(Encode::encode_utf8($body)); |
1177
|
0
|
0
|
|
|
|
|
if(length($nbody) < length($body)) { |
1178
|
0
|
|
|
|
|
|
$body = $nbody; |
1179
|
0
|
|
|
|
|
|
push @o, "Content-Encoding: $encoding"; |
1180
|
0
|
|
|
|
|
|
push @o, "Vary: Accept-Encoding"; |
1181
|
|
|
|
|
|
|
} |
1182
|
|
|
|
|
|
|
} |
1183
|
|
|
|
|
|
|
} |
1184
|
|
|
|
|
|
|
|
1185
|
|
|
|
|
|
|
=head1 AUTHOR |
1186
|
|
|
|
|
|
|
|
1187
|
|
|
|
|
|
|
Nigel Horne, C<< <njh at bandsman.co.uk> >> |
1188
|
|
|
|
|
|
|
|
1189
|
|
|
|
|
|
|
=head1 BUGS |
1190
|
|
|
|
|
|
|
|
1191
|
|
|
|
|
|
|
CGI::Buffer should be safe even in scripts which produce lots of different |
1192
|
|
|
|
|
|
|
output, e.g. e-commerce situations. |
1193
|
|
|
|
|
|
|
On such pages, however, I strongly urge to setting generate_304 to 0 and |
1194
|
|
|
|
|
|
|
sending the HTTP header "Cache-Control: no-cache". |
1195
|
|
|
|
|
|
|
|
1196
|
|
|
|
|
|
|
When using L<Template>, ensure that you don't use it to output to STDOUT, |
1197
|
|
|
|
|
|
|
instead you will need to capture into a variable and print that. |
1198
|
|
|
|
|
|
|
For example: |
1199
|
|
|
|
|
|
|
|
1200
|
|
|
|
|
|
|
my $output; |
1201
|
|
|
|
|
|
|
$template->process($input, $vars, \$output) || ($output = $template->error()); |
1202
|
|
|
|
|
|
|
print $output; |
1203
|
|
|
|
|
|
|
|
1204
|
|
|
|
|
|
|
Can produce buggy JavaScript if you use the <!-- HIDING technique. |
1205
|
|
|
|
|
|
|
This is a bug in L<JavaScript::Packer>, not CGI::Buffer. |
1206
|
|
|
|
|
|
|
See https://github.com/nevesenin/javascript-packer-perl/issues/1#issuecomment-4356790 |
1207
|
|
|
|
|
|
|
|
1208
|
|
|
|
|
|
|
Mod_deflate can confuse this when compressing output. |
1209
|
|
|
|
|
|
|
Ensure that deflation is off for .pl files: |
1210
|
|
|
|
|
|
|
|
1211
|
|
|
|
|
|
|
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|pl)$ no-gzip dont-vary |
1212
|
|
|
|
|
|
|
|
1213
|
|
|
|
|
|
|
If you request compressed output then uncompressed output (or vice |
1214
|
|
|
|
|
|
|
versa) on input that produces the same output, the status will be 304. |
1215
|
|
|
|
|
|
|
The letter of the spec says that's wrong, so I'm noting it here, but |
1216
|
|
|
|
|
|
|
in practice you should not see this happen or have any difficulties |
1217
|
|
|
|
|
|
|
because of it. |
1218
|
|
|
|
|
|
|
|
1219
|
|
|
|
|
|
|
CGI::Buffer is not compatible with FastCGI. |
1220
|
|
|
|
|
|
|
|
1221
|
|
|
|
|
|
|
I advise adding CGI::Buffer as the last use statement so that it is |
1222
|
|
|
|
|
|
|
cleared up first. In particular it should be loaded after |
1223
|
|
|
|
|
|
|
L<Log::Log4perl>, if you're using that, so that any messages it |
1224
|
|
|
|
|
|
|
produces are printed after the HTTP headers have been sent by |
1225
|
|
|
|
|
|
|
CGI::Buffer; |
1226
|
|
|
|
|
|
|
|
1227
|
|
|
|
|
|
|
CGI::Buffer is not compatible with FCGI, use L<FCGI::Buffer> instead. |
1228
|
|
|
|
|
|
|
|
1229
|
|
|
|
|
|
|
Please report any bugs or feature requests to C<bug-cgi-buffer at rt.cpan.org>, |
1230
|
|
|
|
|
|
|
or through the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=CGI-Buffer>. |
1231
|
|
|
|
|
|
|
I will be notified, and then you'll automatically be notified of progress on |
1232
|
|
|
|
|
|
|
your bug as I make changes. |
1233
|
|
|
|
|
|
|
|
1234
|
|
|
|
|
|
|
=head1 SEE ALSO |
1235
|
|
|
|
|
|
|
|
1236
|
|
|
|
|
|
|
L<HTML::Packer>, L<HTML::Lint> |
1237
|
|
|
|
|
|
|
|
1238
|
|
|
|
|
|
|
=head1 SUPPORT |
1239
|
|
|
|
|
|
|
|
1240
|
|
|
|
|
|
|
You can find documentation for this module with the perldoc command. |
1241
|
|
|
|
|
|
|
|
1242
|
|
|
|
|
|
|
perldoc CGI::Buffer |
1243
|
|
|
|
|
|
|
|
1244
|
|
|
|
|
|
|
You can also look for information at: |
1245
|
|
|
|
|
|
|
|
1246
|
|
|
|
|
|
|
=over 4 |
1247
|
|
|
|
|
|
|
|
1248
|
|
|
|
|
|
|
=item * MetaCPAN |
1249
|
|
|
|
|
|
|
|
1250
|
|
|
|
|
|
|
L<https://metacpan.org/release/CGI-Buffer> |
1251
|
|
|
|
|
|
|
|
1252
|
|
|
|
|
|
|
=item * RT: CPAN's request tracker |
1253
|
|
|
|
|
|
|
|
1254
|
|
|
|
|
|
|
L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=CGI-Buffer> |
1255
|
|
|
|
|
|
|
|
1256
|
|
|
|
|
|
|
=item * CPANTS |
1257
|
|
|
|
|
|
|
|
1258
|
|
|
|
|
|
|
L<http://cpants.cpanauthors.org/dist/CGI-Buffer> |
1259
|
|
|
|
|
|
|
|
1260
|
|
|
|
|
|
|
=item * CPAN Testers' Matrix |
1261
|
|
|
|
|
|
|
|
1262
|
|
|
|
|
|
|
L<http://matrix.cpantesters.org/?dist=CGI-Buffer> |
1263
|
|
|
|
|
|
|
|
1264
|
|
|
|
|
|
|
=item * CPAN Ratings |
1265
|
|
|
|
|
|
|
|
1266
|
|
|
|
|
|
|
L<http://cpanratings.perl.org/d/CGI-Buffer> |
1267
|
|
|
|
|
|
|
|
1268
|
|
|
|
|
|
|
=item * CPAN Testers Dependencies |
1269
|
|
|
|
|
|
|
|
1270
|
|
|
|
|
|
|
L<http://deps.cpantesters.org/?module=CGI::Buffer> |
1271
|
|
|
|
|
|
|
|
1272
|
|
|
|
|
|
|
=back |
1273
|
|
|
|
|
|
|
|
1274
|
|
|
|
|
|
|
=head1 ACKNOWLEDGEMENTS |
1275
|
|
|
|
|
|
|
|
1276
|
|
|
|
|
|
|
The inspiration and code for some of this is cgi_buffer by Mark |
1277
|
|
|
|
|
|
|
Nottingham: L<https://www.mnot.net/blog/2003/04/24/etags>. |
1278
|
|
|
|
|
|
|
|
1279
|
|
|
|
|
|
|
=head1 LICENSE AND COPYRIGHT |
1280
|
|
|
|
|
|
|
|
1281
|
|
|
|
|
|
|
The licence for cgi_buffer is: |
1282
|
|
|
|
|
|
|
|
1283
|
|
|
|
|
|
|
"(c) 2000 Copyright Mark Nottingham <mnot@pobox.com> |
1284
|
|
|
|
|
|
|
|
1285
|
|
|
|
|
|
|
This software may be freely distributed, modified and used, |
1286
|
|
|
|
|
|
|
provided that this copyright notice remain intact. |
1287
|
|
|
|
|
|
|
|
1288
|
|
|
|
|
|
|
This software is provided 'as is' without warranty of any kind." |
1289
|
|
|
|
|
|
|
|
1290
|
|
|
|
|
|
|
The rest of the program is Copyright 2011-2023 Nigel Horne, |
1291
|
|
|
|
|
|
|
and is released under the following licence: GPL2 |
1292
|
|
|
|
|
|
|
|
1293
|
|
|
|
|
|
|
=cut |
1294
|
|
|
|
|
|
|
|
1295
|
|
|
|
|
|
|
1; # End of CGI::Buffer |