# Blosxom Plugin: page_caching -*-cperl-*- # Author(s): Jason Blevins (http://jblevins.org) # Version: 2007-12-05 # Last Modified: December 6, 2007 13:11 EST # Documentation: See the bottom of this file or type: perldoc config package page_caching; use strict; use File::stat; use Time::localtime; use CGI qw/:standard/; # --- Configurable variables ----- # A file containing a list of files to cache. my $cache_file = 'cache'; # Time before refreshing the cache, in seconds. my $cache_time = 3000; # Default: 50 minutes # A list of plugins to disable when the cache file is being generated my @disable = qw( gzip ); # Set this to a strictly positive value to enable debugging. my $debug = 0; # -------------------------------- my $want_cache; # Is this a page we should be caching? my $do_cache; # Should we update the cache? my @cache_pages = (); # An array of the lines in $cache_file my $page_cache_file; # Individual page cache filename my $cache_dir = $blosxom::plugin_state_dir . "/page_caching"; my $debug_file = '/tmp/page_caching_debug'; # This subroutine checks to see whether the plugin should be loaded and if # so, whether the caching is wanted for the requested page. If caching is # not wanted, if we were called in static mode, or if there are CGI # parameters present (e.g., a find query), then there is no need to start. # # The fact that the plugin does not start when CGI parameters are present # allows for dynamic behavior (feedback, searches, image galleries, etc.) to # continue as usual. The only drawback is that if, say, a reader submits a # comment, that comment can be previewed and will be processed when # submitted, but it will not appear on the story page until the cache is # updated. However, this is not really an issue if you moderate your # comments. sub start { # Don't start in static mode (doesn't make sense). return 0 if $blosxom::static_or_dynamic eq 'static'; open DEBUG, ">> $debug_file" if $debug > 0; my $cache_ext = (-e "$blosxom::datadir/${cache_file}.${blosxom::flavour}") ? ".${blosxom::flavour}" : ""; my $cache_fn = "$blosxom::datadir/$cache_file$cache_ext"; print DEBUG "cache file: $cache_fn\n" if $debug > 0; open(CACHE, "< $cache_fn") or 1; while () { chomp; push(@cache_pages, $_) if $_; print DEBUG "caching requested for $_\n" if $debug > 0; } close(CACHE); # If $cache_dir does not yet exist, create it. unless (-e $cache_dir) { return 0 unless (make_cache_dir($cache_dir)); } # Determine the story filename (or category path) my $filename = $blosxom::path_info; $filename =~ s/\.$blosxom::flavour$/\.$blosxom::file_extension/; print DEBUG "filename: $filename\n" if $debug > 0; # Should we cache this page? $want_cache = 0; foreach my $entry (@cache_pages) { print DEBUG "cache file entry: $entry\n" if $debug > 0; $want_cache = 1 if ($filename =~ m/^$entry/); } # Don't use cache if CGI parameters are present my @param = param(); print DEBUG "param: @param\n" if $debug > 0; $want_cache = 0 if ($#param > 0); print DEBUG "want_cache: $want_cache\n" if $debug > 0; close(DEBUG) if $debug > 0; # Don't start unless this page is in the list of pages to cache $want_cache > 0 ? return 1 : return 0; } # This subroutine should only be called if caching is wanted for the # requested page. We first test to see whether the cached version exists and # whether it has expired. If it does not exist or it has expired, the page # is generated as normal but we set the $do_cache flag so that it will be # cached in the end() subroutine. If the cache file exists and it is still # fresh, we put the cached page body, minus any headers (e.g., Content-Type), # in $blosxom::output and ask Blosxom to skip any further processing. sub skip { open DEBUG, ">> $debug_file" if $debug > 0; # First, assume we need to update the cache $do_cache = 0; # Determine the full cache file name. $page_cache_file = $cache_dir . "/" . $blosxom::path_info; # If this is not a story page, then we need to append # index.$blosxom::flavour to the filename. unless ($blosxom::path_info =~ /$blosxom::flavour$/) { $page_cache_file .= "/" unless $page_cache_file =~ m!/$!; $page_cache_file .= "index.$blosxom::flavour"; } print DEBUG "page_cache_file: $page_cache_file\n" if $debug > 0; # Should we update the cache or return the current version? if (!-e $page_cache_file) { # If a cached version does not exist, we need to create it. $do_cache = 1; print DEBUG "cache file does not exist!\n" if $debug > 0; } elsif (stat($page_cache_file)->mtime + $cache_time < time) { # The cache file is older than $cache_time so update it. print DEBUG "cache file is old!\n" if $debug > 0; $do_cache = 1; } else { # The cache file is fresh, we can just return it. print DEBUG "cache file is fresh!\n" if $debug > 0; $do_cache = 0; } # We know what to do, now act accordingly. if ($do_cache > 0) { # Update cache. Disable plugins in @disable and don't skip. print DEBUG "The cache needs to be updated.\n" if $debug > 0; disable_plugins(@disable); close(DEBUG) if $debug > 0; return 0; } else { # Return the cached version. # Slurp the cached page open PAGECACHE, "< $page_cache_file"; my @lines = ; print DEBUG "Read $#lines lines from cache file.\n" if $debug > 0; close PAGECACHE; $blosxom::output = join('', @lines); # Do any required processing of headers here. We need to do this # because last() will not be called since we are skipping. set_headers(); close(DEBUG) if $debug > 0; return 1; } } # Here, if the cache needs to be updated (i.e., $do_cache > 0), then we take # $blosxom::output and store it in the page's cache file. We have to make # sure to strip any HTTP headers first. sub end { open DEBUG, ">> $debug_file" if $debug > 0; print DEBUG "end called, do_cache = $do_cache\n" if $debug > 0; if ($do_cache > 0) { # Create a new subdirectory if needed my $path = $page_cache_file; print DEBUG "path: $path\n" if $debug > 0; $path =~ s!^(?:(.*)/)?(.*)!$1!; # Remove the cache_dir prefix and any leading / $path =~ s!^$cache_dir!!; $path =~ s!^/!!; print DEBUG "path: $path\n" if $debug > 0; # Create the appropriate cache subdirectory if it doesn't exist make_cache_subdir($path); # Strip HTTP headers from the output and cache the page my $page_body = $blosxom::output; my $in_header = 1; open PAGECACHE, "> $page_cache_file"; foreach ( split /\n/, $page_body ) { if ($in_header == 1 and /^\s*$/) { $in_header = 0; next; } next if $in_header; print PAGECACHE "$_\n"; } close PAGECACHE; } close DEBUG if $debug > 0; } # Create a directory to hold cached files, and make it writeable. sub make_cache_dir { my $dir = shift; open DEBUG, ">> $debug_file" if $debug > 0; my $mkdir_r = mkdir("$dir", 0755); if ($debug > 0) { if ($mkdir_r) { print DEBUG "page_caching: $dir created.\n"; } else { print DEBUG "page_caching: Could create $dir.\n"; } } $mkdir_r or return 0; my $chmod_r = chmod 0755, $dir; if ($debug > 0) { if ($chmod_r) { print DEBUG "page_caching: $dir set to 0755 permissions.\n"; } else { print DEBUG "page_caching: Could not set permissions on $dir.\n"; } } $chmod_r or return 0; print DEBUG "page_caching: page_caching is enabled!\n" if $debug > 0; close DEBUG if $debug > 0; return 1; } # Create subdirectories if needed sub make_cache_subdir { my $dir = shift; my $p = ''; open DEBUG, ">> $debug_file" if $debug > 0; print DEBUG "make_cache_subdir called with path: $dir\n" if $debug > 0; return 1 if !defined($dir) or $dir eq ''; foreach (('', split /\//, $dir)) { $p .= "/$_"; $p =~ s!^/!!; print DEBUG "make_cache_subdir looping over path $p\n" if $debug > 0; return 0 unless (-d "$cache_dir/$p" or mkdir "$cache_dir/$p", 0755); } close DEBUG if $debug > 0; return 1; } # A custom subroutine to modify headers for cached pages sub set_headers { # Set Content-Type header if necessary if ($blosxom::plugins{'xhtmlmime'} and ($xhtmlmime::mime eq 'html')) { $blosxom::header->{'Content-Type'} = "text/html; charset=$xhtmlmime::charset"; } else { $blosxom::header->{'Content-Type'} = "application/xhtml+xml; charset=$xhtmlmime::charset"; } } # Disable plugins sub disable_plugins { foreach my $plugin (shift) { $blosxom::plugins{$plugin} = 0; } } 1; =head1 NAME Blosxom Plug-in: page_caching =head1 SYNOPSIS Purpose: Caches selected pages for a specified time interval. Once a page is loaded, within this time interval if the page is requested again, the cached version is returned and Blosxom's skip() method is called to halt all further processing of the story. =head1 VERSION 2007-12-05 =head1 AUTHOR Jason Blevins http://jblevins.org =head1 CONFIGURATION C<$cache_file> name of a file containing pages to cache. Defaults to C. =head1 EXAMPLE Your C should look like this: meta/ software/blosxom.txt with one entry per line. Regular expressions should work as well. =head1 LICENSE Copyright 2007 (C) Jason Blevins (This license is the same as Blosxom's) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.