1 # Blosxom Plugin: lastmodified2
2 # Author(s): Frank Hecker <hecker@hecker.org>
3 # (based on work by Bob Schumaker <cobblers@pobox.com>)
5 # Documentation: See the bottom of this file or type: perldoc lastmodified2
13 use POSIX qw! strftime !;
15 # Use the Digest:MD5 module if available, the older MD5 module if not.
21 if (eval "require Digest::MD5") {
22 Digest::MD5->import();
25 elsif (eval "require MD5") {
31 # --- Package variables -----
33 my $current_time = time(); # Use consistent value of current time.
34 my $last_modified_time = 0;
39 # --- Output variables -----
41 our $latest_rfc822 = '';
42 our $latest_iso8601 = '';
44 our $others_rfc822 = '';
45 our $others_iso8601 = '';
48 our $now_iso8601 = '';
50 our $story_rfc822 = '';
51 our $story_iso8601 = '';
53 # --- Configurable variables -----
55 my $generate_etag = 1; # generate ETag header?
57 my $generate_mod = 1; # generate Last-modified header?
59 my $strong = 0; # do strong validation?
61 my $val_cache = "validator.cache"; # where to cache last-modified values
62 # and MD5 digests (in state directory)
64 my $generate_expires = 0; # generate Expires header?
66 my $generate_cache = 0; # generate Cache-control header?
68 my $freshness_time = 3000; # number of seconds pages are fresh
69 # (0 = do not cache, max is 1 year)
71 my $generate_length = 1; # generate Content-length header?
73 my $use_others = 0; # consult %others for weak validation
76 my $export_dates = 1; # set $latest_rfc822, etc., for
77 # compatibility with lastmodified
79 my $debug = 0; # set > 0 for debug output
81 # --------------------------------
84 # Do any initial processing, and decide whether to activate the plugin.
87 warn "lastmodified2: start\n" if $debug > 1;
89 # Don't activate this plugin if we are doing static page generation.
91 return 0 if $blosxom::static_or_dynamic eq 'static';
93 # If we can't do MD5 then we don't do strong validation.
95 if ($strong && !($use_digest || $use_just_md5)) {
98 warn "lastmodified2: MD5 not available, forcing weak validation\n"
102 # Limit freshness time to maximum of one year, must be non-negative.
104 $freshness_time > 365*24*3600 and $freshness_time = 365*24*3600;
105 $freshness_time < 0 and $freshness_time = 0;
108 warn "lastmodified2: \$generate_etag = $generate_etag\n";
109 warn "lastmodified2: \$generate_mod = $generate_mod\n";
110 warn "lastmodified2: \$strong = $strong\n";
111 warn "lastmodified2: \$generate_cache = $generate_cache\n";
112 warn "lastmodified2: \$generate_expires = $generate_expires\n";
113 warn "lastmodified2: \$freshness_time = $freshness_time\n";
114 warn "lastmodified2: \$generate_length = $generate_length\n";
117 # If we are using Last-modified as a strong validator then read
118 # in the cached last-modified values and MD5 digests.
120 if ($generate_mod && $strong &&
121 open CACHE, "<$blosxom::plugin_state_dir/$val_cache" ) {
123 warn "lastmodified2: loading cached validators\n" if $debug > 0;
125 my $index = join '', <CACHE>;
129 $index =~ m!\$VAR1 = \{!
130 and eval($index) and !$@ and %validator = %$VAR1;
133 # Convert current time to RFC 822 and ISO 8601 formats for others' use.
135 if ($export_dates && $current_time) {
136 $now_rfc822 = HTTP::Date::time2str($current_time);
137 $now_iso8601 = iso8601($current_time);
144 # We check the list of entries to be displayed and determine the modification
145 # time of the most recent entry.
148 my ($pkg, $files, $others) = @_;
150 warn "lastmodified2: filter\n" if $debug > 1;
152 # We can skip all this unless we're doing weak validation and/or we're
153 # setting the *_rfc822 and *_iso8601 variables for others to use.
155 return 1 unless $export_dates ||
156 (($generate_etag || $generate_mod) && !$strong);
158 # Find the latest date/time modified for the entries to be displayed.
160 $last_modified_time = 0;
161 for (values %$files) {
162 $_ > $last_modified_time and $last_modified_time = $_;
165 warn "lastmodified2: \$last_modified_time = " .
166 $last_modified_time . " (entries)\n" if $debug > 0;
168 # Convert last modified time to RFC 822 and ISO 8601 formats for others.
170 if ($export_dates && $last_modified_time) {
171 $latest_rfc822 = HTTP::Date::time2str($last_modified_time);
172 $latest_iso8601 = iso8601($last_modified_time);
175 # Optionally look at other files as well (DEPRECATED).
178 my $others_last_modified_time = 0;
179 for (values %$others) {
180 $_ > $others_last_modified_time
181 and $others_last_modified_time = $_;
184 if ($export_dates && $others_last_modified_time) {
185 $others_rfc822 = HTTP::Date::time2str($others_last_modified_time);
186 $others_iso8601 = iso8601($others_last_modified_time);
189 warn "lastmodified2: \$others_last_modified_time = " .
190 $others_last_modified_time . " (others)\n" if $debug > 0;
192 $others_last_modified_time > $last_modified_time
193 and $last_modified_time = $others_last_modified_time;
196 # If we're doing weak validation then create an etag based on the latest
197 # date/time modified and mark it as weak (i.e., by prefixing it with 'W/').
199 if ($generate_etag && !$strong) {
200 $etag = 'W/"' . $last_modified_time . '"';
202 warn "lastmodified2: \$etag = $etag\n" if $debug > 0;
209 # Skip story processing and generate configured headers now on a conditional
210 # GET request for which we don't need to return a full response.
213 warn "lastmodified2: skip\n" if $debug > 1;
215 # If we are doing strong validation then we can't skip story processing
216 # because we need all output in order to generate the proper etag and/or
217 # last-modified value.
219 return 0 unless ($generate_etag || $generate_mod) && !$strong;
221 # Otherwise we can check here whether we can send a 304 or not.
223 my $send_304 = check_for_304();
225 # If we don't need to return a full response on a conditional GET then
226 # set the HTTP status to 304 and generate headers as configured.
227 # (We have to do this here because the last subroutine won't be executed
228 # if we skip story processing.)
230 add_headers($send_304) if $send_304;
236 # Set variables with story date/time in RFC 822 and ISO 8601 formats.
239 my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
241 warn "lastmodified2: story (\$path = $path, \$filename = $filename)\n"
248 $blosxom::files{"$blosxom::datadir$path/$filename.$blosxom::file_extension"};
250 warn "lastmodified2: \$timestamp = $timestamp\n" if $debug > 0;
252 $story_rfc822 = $timestamp ? HTTP::Date::time2str($timestamp) : '';
253 $story_iso8601 = $timestamp ? iso8601($timestamp) : '';
260 # Do conditional GET checks if we couldn't do them before (i.e., we are
261 # doing strong validation and couldn't skip story processing) and output
262 # any configured headers plus a 304 status if appropriate.
265 warn "lastmodified2: last\n" if $debug > 1;
267 # If some other plugin has set the HTTP status to a non-OK value then we
268 # don't attempt to do anything here, since it would probably be wrong.
270 return 1 if $blosxom::header->{'Status'} &&
271 $blosxom::header->{'Status'} !~ m!^200 !;
273 # If we are using ETag and/or Last-modified as a strong validator then
274 # we generate an entity tag from the MD5 message digest of the complete
275 # output. (We use the base-64 representation if possible because it is
276 # more compact than hex and hence saves a few bytes of bandwidth.)
278 if (($generate_etag || $generate_mod) && $strong) {
280 $use_digest ? Digest::MD5::md5_base64($blosxom::output)
281 : MD5->hex_hash($blosxom::output);
282 $etag = '"' . $md5_digest . '"';
284 warn "lastmodified2: \$etag = $etag\n" if $debug > 0;
287 # If we are using Last-modified as a strong validator then we look up
288 # the cached MD5 digest for this URI, compare it to the current digest,
289 # and use the cached last-modified value if they match. Otherwise we set
290 # the last-modified value to just prior to the current time.
292 my $cache_tag = cache_tag();
293 my $update_cache = 0;
295 if ($generate_mod && $strong) {
296 if ($validator{$cache_tag} &&
297 $md5_digest eq $validator{$cache_tag}{'md5'}) {
298 $last_modified_time = $validator{$cache_tag}{'last-modified'};
300 $last_modified_time = $current_time - 5;
301 $validator{$cache_tag}{'last-modified'} = $last_modified_time;
302 $validator{$cache_tag}{'md5'} = $md5_digest;
306 warn "lastmodified2: \$last_modified_time = $last_modified_time\n"
311 # Do conditional GET checks and output configured headers plus status.
313 my $send_304 = check_for_304();
314 add_headers($send_304);
316 # Update the validator cache if we need to. To minimize race conditions
317 # we write the cache as a temporary file and then rename it.
320 warn "lastmodified2: updating validator cache\n" if $debug > 0;
322 my $tmp_cache = "$val_cache-$$-$current_time";
324 if (open CACHE, ">$blosxom::plugin_state_dir/$tmp_cache") {
325 print CACHE Dumper \%validator;
328 warn "lastmodified2: renaming $tmp_cache to $val_cache\n"
331 rename("$blosxom::plugin_state_dir/$tmp_cache",
332 "$blosxom::plugin_state_dir/$val_cache")
333 or warn "couldn't rename $blosxom::plugin_state_dir/$tmp_cache: $!\n";
335 warn "couldn't > $blosxom::plugin_state_dir/$tmp_cache: $!\n";
343 # Check If-none-match and/or If-modified-since headers and return true if
344 # we can send a 304 (not modified) response instead of a normal response.
347 my $etag_send_304 = 0;
348 my $mod_send_304 = 0;
349 my $etag_request = 0;
353 warn "lastmodified2: check_for_304\n" if $debug > 1;
355 # For a conditional GET using the If-none-match header, compare the
356 # ETag value(s) in the header with the ETag value generated for the page,
357 # set $etag_send_304 true if we don't need to send a full response,
358 # and note that an etag value was included in the request.
360 if ($ENV{'HTTP_IF_NONE_MATCH'}) {
362 if ($generate_etag) {
363 my @inm_etags = split '\s*,\s*', $ENV{'HTTP_IF_NONE_MATCH'};
367 warn "lastmodified2: \$inm_etag = |" . $_ . "|\n";
372 $etag eq $_ and $etag_send_304 = 1 and last;
377 # For a conditional GET using the If-modified-since header, compare the
378 # time in the header with the time any entry on the page was last modified,
379 # set $mod_send_304 true if we don't need to send a full response, and
380 # also note that a last-modified value was included in the request.
382 if ($ENV{'HTTP_IF_MODIFIED_SINCE'}) {
386 HTTP::Date::str2time($ENV{'HTTP_IF_MODIFIED_SINCE'});
388 warn "lastmodified2: \$ims_time = " . $ims_time . "\n"
391 $mod_send_304 = 1 if $last_modified_time <= $ims_time;
395 # If the request includes both If-none-match and If-modified-since then
396 # we don't send a 304 response unless both tests agree it should be sent,
397 # per section 13.3.4 of the HTTP 1.1 specification.
399 if ($etag_request && $mod_request) {
400 $send_304 = $etag_send_304 && $mod_send_304;
402 $send_304 = $etag_send_304 || $mod_send_304;
405 warn "lastmodified2: \$send_304 = " . $send_304 .
406 " \$etag_send_304 = " . $etag_send_304 .
407 " \$mod_send_304 = " . $mod_send_304 . "\n"
414 # Set status and add additional header(s) depending on the type of response.
419 warn "lastmodified2: add_headers (\$send_304 = $send_304)\n"
422 # Set HTTP status and truncate output if we are sending a 304 response.
425 $blosxom::header->{'Status'} = "304 Not Modified";
426 $blosxom::output = "";
428 warn "lastmodified2: Status: " .
429 $blosxom::header->{'Status'} . "\n" if $debug > 0;
432 # For the rules on what headers to generate for a 304 response, see
433 # section 10.3.5 of the HTTP 1.1 protocol specification.
435 # Last-modified is not returned on a 304 response.
437 if ($generate_mod && !$send_304) {
438 $blosxom::header->{'Last-modified'} =
439 HTTP::Date::time2str($last_modified_time);
441 warn "lastmodified2: Last-modified: " .
442 $blosxom::header->{'Last-modified'} . "\n" if $debug > 0;
445 # If we send ETag on a 200 response then we send it on a 304 as well.
447 if ($generate_etag) {
448 $blosxom::header->{'ETag'} = $etag;
450 warn "lastmodified2: ETag: " .
451 $blosxom::header->{'ETag'} . "\n" if $debug > 0;
454 # We send Expires for a 304 since its value is updated for each request.
456 if ($generate_expires) {
457 $blosxom::header->{'Expires'} = $freshness_time ?
458 HTTP::Date::time2str($current_time + $freshness_time) :
459 HTTP::Date::time2str($current_time - 60);
461 warn "lastmodified2: Expires: " .
462 $blosxom::header->{'Expires'} . "\n" if $debug > 0;
465 # We send Cache-control for a 304 response for consistency with Expires.
467 if ($generate_cache) {
468 $blosxom::header->{'Cache-control'} =
469 $freshness_time ? "max-age=" . $freshness_time
472 warn "lastmodified2: Cache-control: " .
473 $blosxom::header->{'Cache-control'} . "\n" if $debug > 0;
476 # Content-length is not returned on a 304 response.
478 if ($generate_length && !$send_304) {
479 $blosxom::header->{'Content-length'} = length($blosxom::output);
481 warn "lastmodified2: Content-length: " .
482 $blosxom::header->{'Content-length'} . "\n" if $debug > 0;
487 # Generate a tag to look up the cached last-modified value and MD5 digest
491 # Start with the original URI from the request.
493 my $tag = $ENV{REQUEST_URI} || "";
495 # Add an "/index.flavour" for uniqueness unless it's already present.
497 unless ($tag =~ m!/index\.!) {
498 $tag .= '/' unless ($tag =~ m!/$!);
499 $tag .= "index.$blosxom::flavour";
506 # Convert time to ISO 8601 format (including time zone offset).
507 # (Format is YYYY-MM-DDThh:mm:ssTZD per http://www.w3.org/TR/NOTE-datetime)
510 my ($timestamp) = @_;
511 my $tz_offset = strftime("%z", localtime());
512 $tz_offset = substr($tz_offset, 0, 3) . ":" . substr($tz_offset, 3, 5);
513 return strftime("%Y-%m-%dT%T", localtime($timestamp)) . $tz_offset;
523 Blosxom Plug-in: lastmodified2
527 Enables caching and validation of dynamically-generated Blosxom pages
528 by generating C<ETag>, C<Last-modified>, C<Cache-control>, and/or
529 C<Expires> HTTP headers in the response and responding appropriately
530 to an C<If-none-match> and/or C<If-modified-since> header in the
531 request. Also generates a C<Content-length> header to support HTTP 1.0
532 persistent connections.
540 Frank Hecker <hecker@hecker.org>, http://www.hecker.org/ (based on
541 work by Bob Schumaker, <cobblers@pobox.com>, http://www.cobblers.net/blog/)
545 This plugin enables caching and validation of dynamically-generated
546 Blosxom pages by web browsers, web proxies, feed aggregators, and
547 other clients by generating various cache-related HTTP headers in the
548 response and supporting conditional GET requests, as described
549 below. This can reduce excess network traffic and server load caused
550 by requests for RSS or Atom feeds or for web pages for popular entries
553 =head1 INSTALLATION AND CONFIGURATION
555 Copy this plugin into your Blosxom plugin directory. You should not
556 normally need to rename the plugin; however see the discussion below.
558 Configurable variables specify how the plugin handles validation
559 (C<$generate_etag>, C<$generate_mod>, and C<$strong>), caching
560 (C<$generate_cache>, C<$generate_expires>, and C<$freshness_time>) and
561 whether or not to generate any other recommended headers
562 (C<$generate_length>). The plugin supports the variable C<$use_others>
563 as used in the lastmodified plugin; however use of this is deprecated
564 (use strong validation instead). The variable C<$export_dates>
565 specifies whether to export date/time variables C<$latest_rfc822>,
566 etc., for compatibility with the lastmodified plugin.
568 You can set the variable C<$debug> to 1 or greater to produce
569 additional information useful in debugging the operation of the
570 plugin; the debug output is sent to your web server's error log.
572 This plugin supplies C<filter>, C<skip>, and C<last> subroutines. It
573 needs to run after any other plugin whose C<filter> subroutine changes
574 the list of entries included in the response; otherwise the
575 C<Last-modified> date may be computed incorrectly. It needs to run
576 after any other plugin whose C<skip> subroutine does redirection
577 (e.g., the canonicaluri plugin) or otherwise conditionally sets the
578 HTTP status to any value other than 200. Finally, this plugin needs to
579 run after any other plugin whose C<last> subroutine changes the output
580 for the page; otherwise the C<Content-length> value (and the C<ETag>
581 and C<Last-modified> values, if you are using strong validation) may
582 be computed incorrectly. If you are encountering problems in any of
583 these regards then you can force the plugin to run after other plugins
584 by renaming it to, e.g., 99lastmodified2.
588 Blosxom Home/Docs/Licensing: http://www.blosxom.com/
590 Blosxom Plugin Docs: http://www.blosxom.com/documentation/users/plugins.html
592 lastmodified plugin: http://www.cobblers.net/blog/dev/blosxom/
594 more on the lastmodified2 plugin: http://www.hecker.org/blosxom/lastmodified2
598 Frank Hecker <hecker@hecker.org> http://www.hecker.org/
600 Based on the original lastmodified plugin by Bob Schumaker
601 <cobblers@pobox.com> http://www.cobblers.net/blog
605 This source code is submitted to the public domain. Feel free to use
606 and modify it. If you like, a comment in your modified source
607 attributing credit to myself, Bob Schumaker, and any other
608 contributors for our work would be appreciated.
610 THIS SOFTWARE IS PROVIDED AS IS AND WITHOUT ANY WARRANTY OF ANY KIND.
611 USE AT YOUR OWN RISK!