Add Frank Hecker plugins to general.
authorGavin Carr <gonzai@users.sourceforge.net>
Fri, 31 Aug 2007 01:47:10 +0000 (01:47 +0000)
committerGavin Carr <gonzai@users.sourceforge.net>
Fri, 31 Aug 2007 01:47:10 +0000 (01:47 +0000)
general/canonicaluri [new file with mode: 0644]
general/extensionless [new file with mode: 0644]
general/feedback [new file with mode: 0644]
general/lastmodified2 [new file with mode: 0644]
general/noslashredir [new file with mode: 0644]
general/slashredir [new file with mode: 0644]

diff --git a/general/canonicaluri b/general/canonicaluri
new file mode 100644 (file)
index 0000000..f15a012
--- /dev/null
@@ -0,0 +1,264 @@
+# Blosxom Plugin: canonicaluri
+# Author(s): Frank Hecker <hecker@hecker.org>
+# Version: 0.6
+# Documentation: See the bottom of this file or type: perldoc canonicaluri
+
+package canonicaluri;
+
+use strict;
+
+use CGI qw/:standard :netscape/; 
+
+
+# --- Configurable variables -----
+
+my $debug = 0;                          # set to 1 to print debug messages
+
+# --------------------------------
+
+
+use vars qw!$redirecting!;              # 1 if we are redirecting, 0 if not
+
+
+sub start {
+    warn "canonicaluri: start\n" if $debug > 1;
+
+    $redirecting = 0;
+
+    # Activate this plugin only when doing dynamic page generation.
+    return $blosxom::static_or_dynamic eq 'dynamic' ? 1 : 0;
+}
+
+
+sub filter {
+    my ($pkg, $files_ref) = @_;
+
+    warn "canonicaluri: filter\n" if $debug > 1;
+
+    warn "canonicaluri: \$blosxom::path_info: '" . $blosxom::path_info . "'\n"
+        if $debug > 0;
+    warn "canonicaluri: path_info(): '" . path_info() . "'\n"
+        if $debug > 0;
+
+    # We need a copy of the original PATH_INFO, prior to Blosxom having
+    # parsed it, because we need to see the original URI passed into
+    # Blosxom, including whether the URI had an index.* component, trailing
+    # slash, or file extension.
+
+    my $path = path_info();
+
+    # If the requested URI has an explicit "index.*" component where the
+    # the file extension corresponds to the default flavour (typically
+    #  "html") then we rewrite the URI to strip the "index.*" component
+    # (and any spurious slashes after it) and leave a single trailing slash.
+
+    $path =~ s!/index\.$blosxom::default_flavour/*$!/!;
+
+    warn "canonicaluri: \$path (after index.html check): '" . $path . "'\n"
+       if $debug > 0;
+
+    # If the requested URI has an explicit "index.*" component for any
+    # flavour other than the default then we rewrite the URI if necessary
+    # to remove any trailing slashes.
+
+    $path =~ s!/(index\.[^./]*)/*$!/$1!;
+
+    warn "canonicaluri: \$path (after index.foo check): '" . $path . "'\n"
+       if $debug > 0;
+
+    # If the URI has a trailing slash but corresponds to an individual entry
+    # then we rewrite the URI to remove the trailing slash. On the other hand
+    # if the URI does not have a trailing slash but corresponds to a category
+    # or archive page then we rewrite the URI to add the trailing slash
+    # unless an "index.*" component is present in the URI.
+
+    if ($path =~ m!/$!) {               # trailing slash, remove if need be
+       $blosxom::path_info =~ m!\.! and $path =~ s!/+$!!;
+    } else {                            # no trailing slash, add if needed
+       $blosxom::path_info !~ m!\.!
+           and $path !~ m!/index.[^./]*$!
+           and $path =~ s!$!/!;
+    }
+
+    warn "canonicaluri: \$path (after slash check): '" . $path . "'\n"
+       if $debug > 0;
+
+    # If the URI has a flavour extension corresponding to the default flavour
+    # (typically "html") then we rewrite the URI to remove the extension.
+
+    $path =~ s!\.$blosxom::default_flavour!!;
+
+    warn "canonicaluri: \$path (after extension check): '" . $path . "'\n"
+       if $debug > 0;
+
+    # If we have rewritten the URI then we force a redirect to the new URI.
+
+    $path ne path_info() and redirect($path);
+
+    1;
+}
+
+
+sub skip {
+    warn "canonicaluri: skip\n" if $debug > 1;
+
+    return $redirecting;                # skip story generation if redirecting
+}
+
+
+sub redirect {
+    my ($path) = @_;
+
+    my $uri = "$blosxom::url$path";
+    $uri .= "?" . $ENV{QUERY_STRING} if $ENV{QUERY_STRING};
+
+    warn "canonicaluri: redirecting to '$uri'\n"
+       if $debug > 0;
+
+    $blosxom::output = qq!Redirecting to <a href="$uri">$uri</a>.\n!;
+    print "Status: 301\n";
+    print "Location: $uri\n";
+    $redirecting = 1;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Blosxom plugin: canonicaluri
+
+=head1 SYNOPSIS
+
+Have Blosxom force a redirect if a URI is not in canonical form with
+respect to index.* component, trailing slash, or flavour extension.
+
+=head1 VERSION
+
+0.6
+
+=head1 AUTHOR
+
+Frank Hecker <hecker@hecker.org>, http://www.hecker.org/
+
+=head1 DESCRIPTION
+
+This plugin checks to see whether the requested URI is in the
+canonical form for the type of page being requested, and if necessary
+does a browser redirect to the canonical form of the URI. The
+canonical forms are defined as follows:
+
+=over
+
+=item * URIs for the blog root, categories, and date-based archives
+should not have an "index.*" component if the flavour being requested
+is the default flavour, and if an "index.*" component is not present
+then the URI should have one (and only one) trailing slash.
+
+=item * URIs for individual entry pages should not have a trailing
+slash and also should not have a flavour extension if the flavour
+being requested is the default flavour (typically "html").
+
+=back
+
+For example, if you request the URIs
+
+  http://www.example.com/blog/foo
+
+or
+
+  http://www.example.com/blog/foo/index.html
+
+where "foo" is a category and "html" is the default flavour, this
+plugin will force a redirect to the canonical URI
+
+  http://www.example.com/blog/foo/
+
+Similarly, if you request the URIs
+
+  http://www.example.com/blog/foo/
+
+or
+
+  http://www.example.com/blog/foo.html
+
+where "foo" is an individual entry and "html" is the default flavour,
+this plugin will force a redirect to the canonical URI
+
+  http://www.example.com/blog/foo
+
+Note that using this plugin makes the most sense if you are also using
+URI rewriting rules to hide use of "/cgi-bin/blosxom.cgi" and support
+URIs similar to those traditionally used to access normal directories
+and files. This plugin should also be used in conjunction with the
+extensionless plugin in order to recognize extensionless URIs for
+individual entries. (See also below.)
+
+This plugin was inspired by the redirect plugin by Fletcher T. Penny
+http://www.blosxom.com/plugins/general/redirect.htm and adapts a bit
+of its code.
+
+=head1 INSTALLATION AND CONFIGURATION
+
+Copy this plugin into your Blosxom plugin directory. You do not
+normally need to rename the plugin; however see the discussion below.
+
+You can change the value of the variable C<$debug> to 1 if you need to
+debug the plugin's operation; the plugin will print to the web
+server's error log the original path component of the URI and the new
+URI if redirection is to be done.
+
+This plugin supplies a filter and skip subroutine and can normally
+coexist with other plugins with filter subroutines. However this
+plugin needs to be run after the extensionless plugin, since it needs
+the flavour extension on C<$blosxom::path_info> (provided by
+extensionless if not already present) to distinguish between
+individual entry pages and other pages.
+
+Finally, note that this plugin is sensitive to the exact URI rewriting
+rules you might have configured (e.g., in the Apache httpd.conf
+configuration file or in a .htaccess file). In particular, when
+rewriting URIs to add the name of the Blosxom CGI script (e.g.,
+"/cgi-bin/blosxom.cgi") you need to ensure that such rules preserve
+any trailing slash on the end of the URI and pass it on to Blosxom.
+
+=head1 SEE ALSO
+
+Blosxom Home/Docs/Licensing: http://www.blosxom.com/
+
+Blosxom Plugin Docs: http://www.blosxom.com/documentation/users/plugins.html
+
+=head1 BUGS
+
+This plugin depends on the Apache URI rewriting rules to enforce the
+restriction that a URI should never have more than one trailing
+slash. The plugin as presently written can't handle this case properly
+because it depends on the C<path_info()> function to get the URI path,
+and the C<path_info()> value has already been stripped of any excess
+trailing slashes that might have been present in the original URI.
+
+Please address other bug reports and comments to the Blosxom mailing list:
+http://www.yahoogroups.com/group/blosxom
+
+=head1 LICENSE
+
+canonicaluri Blosxom plugin Copyright 2004 Frank Hecker
+
+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.
diff --git a/general/extensionless b/general/extensionless
new file mode 100644 (file)
index 0000000..33df8dc
--- /dev/null
@@ -0,0 +1,279 @@
+# Blosxom Plugin: extensionless
+# Author(s): Frank Hecker <hecker@hecker.org>
+# Version: 0.4
+# Documentation: See the bottom of this file or type: perldoc extensionless
+
+package extensionless;
+
+use strict;
+
+use CGI qw/:standard :netscape/; 
+
+
+# --- Configurable variables -----
+
+my $hide_category = 0;                  # set to 1 to have an extensionless
+                                        # entry URI hide a category of the
+                                        # same name as the entry
+
+my $hide_year = 0;                      # set to 1 to have an extensionless
+                                        # URI with 4-digit entry name hide an
+                                        # archive page for a year
+
+my $debug = 0;                          # set to 1 to print debug messages
+
+# --------------------------------
+
+sub start {
+    warn "extensionless: \$path_info: '$blosxom::path_info' (before)\n"
+        if $debug > 0;
+
+    # Recover the original path passed to Blosxom, in order to properly
+    # handle extensionless URIs where the entry name starts with a digit
+    # and was parsed by Blosxom as a date reference, not as part of the path.
+
+    my $path_info = path_info() || param('path');
+    $path_info =~ s!(^/*)|(/*$)!!g;
+
+    # Check for the following situation:
+    #
+    #   * the requested URI does not include a file extension
+    #   * the requested URI does not *exactly* name a category OR
+    #     we want the URI to always resolve to an entry if one exists
+    #   * the requested URI does not appear to refer to an archive for a year
+    #     OR we want the URI to always resolve to an entry if one exists
+    #   * there is an entry corresponding to the URI
+    #
+    # If the above conditions hold then we update the URI (more specifically,
+    # the path component of the URI) to include the flavour's file extension.
+    # (The flavour used will be the default flavour, a flavour specified
+    # using the flav parameter, or a flavour set by another plugin.)
+    #
+    # We also set $path_info_yr to be undefined in order to prevent Blosxom
+    # and other plugins (e.g., storystate) from considering this an archive
+    # page reference in the case where the entry name starts with a digit.
+
+    if ($blosxom::path_info !~ m!\.!
+       and (! -d "$blosxom::datadir/$path_info" || $hide_category)
+       and ($path_info !~ m!(/19[0-9]{2}$)|(/2[0-9]{3}$)! || $hide_year)
+       and (-r "$blosxom::datadir/$path_info.$blosxom::file_extension")) {
+       $blosxom::path_info = "$path_info.$blosxom::flavour";
+       $blosxom::path_info_yr = undef;
+    }
+
+    warn "extensionless: \$path_info: '$blosxom::path_info' (after)\n"
+       if $debug > 0;
+
+    1;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Blosxom plugin: extensionless
+
+=head1 SYNOPSIS
+
+Make Blosxom recognize extensionless URIs corresponding to individual
+entries. See http://www.w3.org/Provider/Style/URI for motivation.
+
+=head1 VERSION
+
+0.4
+
+=head1 AUTHOR
+
+Frank Hecker <hecker@hecker.org>, http://www.hecker.org/
+
+=head1 DESCRIPTION
+
+This plugin allows Blosxom to recognize extensionless URIs for
+entries, as recommended by http://www.w3.org/Provider/Style/URI and
+elsewhere.  In other words, if you are requesting a page for an
+individual entry then you can omit the file extension that would
+normally be required for specifying the flavour, unless the extension
+is required to disambiguate the URI in the case where the entry has
+the same name as a Blosxom category or looks like a 4-digit year
+reference.
+
+For example, if you have an entry C<foo/bar.txt> in your Blosxom data
+directory then by using this plugin you can use a URI like
+
+  http://www.example.com/blog/foo/bar
+
+to request the entry rather than a URI like
+
+  http://www.example.com/blog/foo/bar.html
+
+unless there is an existing category C<foo/bar> (i.e., there is a
+subdirectory of that name in the Blosxom data directory). If there is
+such a name conflict then the plugin will cause Blosxom to display the
+category and not the individual entry.
+
+(You can change this behavior using a configurable variable as
+described below. The plugin's default behavior matches the behavior of
+Apache when using the MultiViews option, where Apache will display the
+index for an existing directory C<foo/bar> in preference to displaying
+a file C<foo/bar.html>.)
+
+You can also use extensionless URIs like
+
+  http://www.example.com/blog/foo/1st-post
+
+where the entry name starts with a digit, as long as the entry name
+doesn't look like a 4-digit year (e.g., "2004") used as part of a
+request for an archive page for that year. (Again, you can set a
+configurable variable to override the default behavior and have entry
+names that look like year references.)
+
+When an entry is requested using an extensionless URI the existing
+value of the Blosxom C<$flavour> variable will determine the flavour
+used for the entry. Normally this value will be either that specified
+by the C<flav> parameter (if it is present) or the default flavour
+configured into Blosxom; however another plugin could override this
+value if desired. For example, a separate plugin could do content
+negotiation (e.g., using the HTTP Accept header) to determine what
+flavour should be served for an entry. (However see the discussion
+below regarding plugin execution order.)
+
+Thus assuming that C<foo/bar.txt> exists in the Blosxom data directory
+(and does not conflict with a category C<foo/bar>) and the default
+flavour is "html" then the following URIs are all equivalent:
+
+  http://www.example.com/blog/foo/bar
+  http://www.example.com/blog/foo/bar.html
+  http://www.example.com/blog/foo/bar?flav=html
+
+=head1 INSTALLATION AND CONFIGURATION
+
+Copy this plugin into your Blosxom plugin directory. You do not
+normally need to rename the plugin; however see the discussion below
+regarding plugin execution order.
+
+Change the value of the configurable variable C<$hide_category> to 1
+if you want an extensionless URI to display an entry instead of a
+category of the same name. As described above, the default behavior is
+that in the event of a name conflict the category is displayed in
+preference to the entry, so that displaying the entry in that case
+requires explicitly adding a file extension to the URI. If you change
+the plugin's behavior from the default then the category can be
+displayed only by using a URI explicitly referencing an index page
+(e.g., C<.../foo/index.html>) or by referencing an abbreviation for
+the category.
+
+Change the value of the configurable variable C<$hide_year> to 1 if
+you want an extensionless URI to display an entry in the case where
+the URI otherwise appears to be a reference to an archive page for a
+given year, for example, with the URI
+
+  http://www.example.com/blog/elections/2004
+
+where an entry C<2004.txt> exists under the category C<elections>. As
+described above, the default behavior is that in the event of a
+conflict the archive page is displayed in preference to the entry, so
+that displaying the entry in that case requires explicitly adding a
+file extension to the URI. If you change the plugin's behavior from
+the default then the archive page for that year can be displayed only
+by usng a URI explicitly referencing an index page (e.g.,
+C<.../elections/2004/index.html>).
+
+You can also change the value of the variable C<$debug> to 1 if you
+need to debug the plugin's operation; the plugin will print to the web
+server's error log the original path component of the URI and the
+value after any modification is done.
+
+This plugin supplies only a start subroutine and can normally coexist
+with other plugins with start subroutines; however if the other
+plugins' start subroutines depend on the standard interpretation of
+the Blosxom variable C<$path_info> (i.e., that requests for individual
+entries have a period '.' in C<$path_info>) then you should rename
+this plugin (e.g., to "00extensionless") to ensure that it is loaded
+first and can modify C<$path_info> to match the standard
+interpretation before other plugins reference its value.
+
+Also, if you have a plugin that overrides the value of the Blosxom
+C<$flavour> variable then you should ensure that that plugin's code
+runs prior to this plugin's start subroutine being executed.
+
+=head1 ACKNOWLEDGEMENTS
+
+This plugin was inspired by the cooluri plugin by Rob Hague
+http://www.blosxom.com/plugins/link/cooluri.htm and the cooluri2
+plugin by Mark Ivey http://www.blosxom.com/plugins/link/cooluri2.htm
+but has much less ambitious goals; the code is correspondingly much
+simpler.
+
+Thanks also go to Stu MacKenzie, who contributed a patch to fix a
+major bug preventing the use of extensionless URIs having entry names
+beginning with digits.
+
+=head1 SEE ALSO
+
+Blosxom Home/Docs/Licensing: http://www.blosxom.com/
+
+Blosxom Plugin Docs: http://www.blosxom.com/documentation/users/plugins.html
+
+=head1 BUGS
+
+In order to check whether a file extension was originally included in
+the URI we check the value of C<$blosxom::path_info>. As noted in a
+comment in the code, this value is not the actual URI path in the case
+where the URI contains a component starting with a digit; however
+checking C<$blosxom::path_info> works for the URIs of interest to us
+and (unlike checking our local C<$path_info> value) does not produce
+potentially spurious results on invalid Blosxom URIs like
+
+  http://www.example.com/blog/foo/123bar/baz.html
+
+(Unlike entry names, Blosxom category names can never start with a
+digit, and allowing them to do so is beyond the scope of this
+plugin. For this example URI Blosxom would have set C<$path_info> to
+C<foo> and attempted to parse C<123bar> and C<baz.html> as
+C<$path_info_yr> and C<$path_info_mo> respectively.)
+
+Using an entry name that looks like a year reference is problematic
+because of potential ambiguity regarding what actually is a year
+reference and what is not. The code as presently written takes a
+relatively (but not completely) conservative approach: values from
+1900 through 2999 are considered to be year references, and by default
+will hide references to entries of the same name (sans extension).
+
+Also note that versions of this plugin prior to 0.4 are not
+compatible with Blosxom versions 2.0.1 and higher; in prior versions
+this plugin executed its code in the filter subroutine, and in Blosxom
+2.0.1 the execution of plugins' filter subroutines was moved in a way
+that broke this plugin. This problem was fixed in version 0.4 of this
+plugin by moving processing to the start subroutine.
+
+Finally note that this plugin assumes that you are storing Blosxom
+entries in the standard manner, i.e., as text files in the file system
+within the Blosxom data directory. It will not work if you are using
+other plugins like svn-backend that use different storage mechanisms.
+
+Please address other bug reports and comments to the Blosxom mailing
+list: http://www.yahoogroups.com/group/blosxom
+
+=head1 LICENSE
+
+extensionless Blosxom plugin Copyright 2004-2006 Frank Hecker
+
+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.
diff --git a/general/feedback b/general/feedback
new file mode 100644 (file)
index 0000000..8989659
--- /dev/null
@@ -0,0 +1,1815 @@
+# Blosxom Plug-in: feedback
+# Author: Frank Hecker (http://www.hecker.org/)
+#
+# Version 0.23
+
+package feedback;
+
+use warnings;
+
+
+# --- Configurable variables ---
+
+# --- You *must* set the following variables properly for your blog ---
+
+# Where should I keep the feedback hierarchy?
+# (By default it goes in the Blosxom state directory. However you may
+# prefer it to go in the same directory as the Blosxom data directory.
+# If so, delete the following line and uncomment the line following it.)
+$fb_dir = "$blosxom::plugin_state_dir/feedback";
+# $fb_dir = "$blosxom::datadir/../feedback";
+
+
+# --- Set the following variables according to your preferences ---
+
+# Are comments and TrackBacks allowed? Set to zero to disable either or both.
+my $allow_comments = 1;
+my $allow_trackbacks = 1;
+
+# Don't allow comments/TrackBacks if story is older than this (in seconds).
+# (Set to zero to keep story open for comments/TrackBacks forever.)
+my $comment_period = 90 * 86400;        # 90 days
+my $trackback_period = 90 * 86400;      # 90 days
+
+# Do Akismet checking of comments and/or TrackBacks for spam.
+my $akismet_comments = 0;
+my $akismet_trackbacks = 0;
+
+# WordPress API key for use with Akismet.
+# (Register at <http://wordpress.com/> to get your own API key.)
+my $wordpress_api_key = '';
+
+# Do MT-blacklist checking of comments and/or TrackBacks for spam.
+# NOTE: The MT-Blacklist file is no longer maintained; we suggest using
+# Akismet instead.
+my $blacklist_comments = 0;
+my $blacklist_trackbacks = 0;
+
+# Where can I find the local copy of the MT-Blacklist file?
+my $blacklist_file = "$blosxom::plugin_state_dir/blacklist.txt";
+
+# Send an email message to notify the blog owner of new comments and/or
+# TrackBacks and (optionally) request approval of new comments/TrackBacks.
+my $notify_comments = 0;
+my $notify_trackbacks = 0;
+my $moderate_comments = 1;
+my $moderate_trackbacks = 1;
+
+# Email address and SMTP server used for notifications and moderation requests.
+my $address = 'jdoe@example.com';
+my $smtp_server = 'smtp.example.com';
+
+# Default values for fields not submitted with the comment or TrackBack ping.
+my $default_name = "Someone";
+my $default_blog_name = "An unnamed blog";
+my $default_title = "an article";
+
+# The formatting used for comments, i.e., how they are translated to (X)HTML.
+# Valid choices at present are 'none', 'plaintext' and 'markdown'.
+my $comment_format = 'plaintext';
+
+# Should we accept and display commenter's email addresses? (The default is
+# to support http/https URLs only; this may be the only option in future.)
+my $allow_mailto = 0;
+
+
+# --- You should not normally need to change the following variables ---
+
+# What flavour should I consider an incoming TrackBack ping?
+$trackback_flavour = "trackback";
+
+# What file extension should I use for saved comments and TrackBacks?
+my $fb_extension = "wb";
+
+# What fields are used in the comments form?
+my @comment_fields = qw! name url comment !;
+
+# What fields are used by TrackBacks?
+my @trackback_fields = qw! blog_name url title excerpt !;
+
+# Limit all fields to this length or less (just in case).
+my $max_param_length = 10000;
+
+
+# --- Variables for use in flavour templates (e.g., as $feedback::foo) ---
+
+# Comment and TrackBack fields, for use in the comment, preview, and
+# trackback templates.
+$name = '';
+$name_link = '';                        # Combines name and link for email/URL
+$date = '';
+$comment = '';
+$blog_name = '';
+$title = '';
+$title_link = '';                       # Combines title and link to article
+$excerpt = '';
+$url = '';                              # Also used in $name_link, $title_link
+
+# Field values for previewed comments, used in the commentform template.
+$name_preview = '';
+$comment_preview = '';
+$url_preview = '';
+
+# Message displayed in response to a comment submission (e.g., to display
+# an error message), for use in the story or foot templates. The response is
+# formatted for use in HTML/XHTML content.
+$comment_response = '';
+
+# XML message displayed in response to a TrackBack ping (e.g., to display
+# an error message or indicate success), per the TrackBack Technical
+# Specification <http://www.sixapart.com/pronet/docs/trackback_spec>.
+$trackback_response = '';
+
+# All comments and TrackBacks for a particular story, for use in the story
+# template for an individual story page. Also includes content from the
+# comments_head/comments_foot and trackbacks_head/trackbacks_foot templates.
+$comments = '';
+$trackbacks = '';
+
+# Counts of comments and TrackBacks for a story, for use in the story
+# template (e.g., for index and archive pages).
+$comments_count = 0;
+$trackbacks_count = 0;
+$count = 0;                             # total of both
+
+# Previewed comment for a particular story, for use in the story
+# template for an individual story page.
+$preview = '';
+
+# Default comment submission form, for use in the foot template (for an
+# individual story page). The plug-in sets this value to null if comments
+# are disabled or in cases where the page is not for an individual story
+# or the story is older than the allowed comment period.
+$commentform = '';
+
+# TrackBack discovery information, for use in the foot template (for
+# an individual story page). The code sets this value to null if TrackBacks
+# are disabled or in cases where the page is not for an individual story
+# or the story is older than the allowed TrackBack period.
+$trackbackinfo = '';
+
+
+# --- External modules required ---
+
+use CGI qw/:standard/;
+use FileHandle;
+use URI;
+use URI::Escape;
+
+
+# --- Global variables (used in interpolation) ---
+
+use vars qw! $fb_dir $trackback_flavour $name $name_link $date $comment
+    $blog_name $title $name_preview $comment_preview $url_preview
+    $comment_response $trackback_response $comments $trackbacks
+    $comments_count $trackbacks_count $count $preview $commentform
+    $trackbackinfo !;
+
+
+# --- Private static variables ---
+
+# Spam blacklist array.
+my @blacklist_entries = ();
+
+# File handle for use in reading/writing the feedback file, etc.
+my $fh = new FileHandle;
+
+# Path and filename for the main feedback file for a story, and item name
+# used in contructing filenames for files containing moderated items.
+my $fb_path = '';
+my $fb_fn = '';
+
+# Whether comments or TrackBacks are closed for a given story.
+my $closed_comments = 0;
+my $closed_trackbacks = 0;
+
+
+# --- Plug-in initialization ---
+
+# Strip potentially confounding final slash from feedback directory path.
+$fb_dir =~ s!/$!!;
+
+# Strip potentially confounding initial period from file extension.
+$fb_extension =~ s!^\.!!;
+
+# Initialize the default templates; use $blosxom::template so we can leverage
+# the Blosxom template subroutine (whether default or replaced by a plug-in).
+my %template = ();
+while (<DATA>) {
+    last if /^(__END__)?$/;
+    # TODO: Fix this to correctly handle empty flavours (i.e., no $txt).
+    my ($ct, $comp, $txt) = /^(\S+)\s(\S+)(?:\s(.*))?$/;
+#   my ($ct, $comp, $txt) = /^(\S+)\s(\S+)\s(.*)$/;
+    $txt = '' unless defined($txt);
+    $txt =~ s/\\n/\n/mg;
+    $blosxom::template{$ct}{$comp} = $txt;
+}
+
+# Moderation implies notification.
+$notify_comments = 1 if $moderate_comments;
+$notify_trackbacks = 1 if $moderate_trackbacks;
+
+
+# --- Plug-in subroutines ---
+
+# Create feedback directory if needed.
+sub start {
+    # The $fb_dir variable must be set to activate feedback.
+    unless ($fb_dir) {
+        warn "feedback: " .
+                   "The \$fb_dir configurable variable is not set; "
+            . "please set it to enable comments or TrackBacks.\n";
+        return 0;
+    }
+
+    # The value of $fb_dir must be a writeable directory.
+    if (-e $fb_dir && !(-d $fb_dir && -w $fb_dir)) {
+        warn "feedback: The feedback directory '$fb_dir' "
+             . "must be a writeable directory; please rename or remove it "
+             . "and Blosxom will create it properly for you.\n";
+        return 0;
+    }
+
+    # The $fb_dir does not yet exist, so Blosxom will create it.
+    unless (-e $fb_dir)  {
+        return 0 unless (mk_feedback_dir($fb_dir));
+    }
+
+    return 1;
+}
+
+
+# Decide whether to close comments and TrackBacks for a story.
+sub date {
+    my ($pkg, $file, $date_ref, $mtime, $dw, $mo, $mo_num, $da, $ti, $yr) = @_;
+
+    # A positive value of $comment_period represents the time in seconds
+    # during which posting comments or TrackBacks is allowed after a
+    # story has been published. (Note that updating a story has the effect
+    # of reopening the feedback period.) A zero or negative value for
+    # $comment_period means that posting feedback is always allowed.
+
+    if ($comment_period <= 0) {
+       $closed_comments = 0;
+    } elsif ($allow_comments && (time - $mtime) > $comment_period) {
+       $closed_comments = 1;
+    } else {
+       $closed_comments = 0;
+    }
+
+    # $trackback_period works the same way as $comment_period.
+
+    if ($trackback_period <= 0) {
+       $closed_trackbacks = 0;
+    } elsif ($allow_trackbacks && (time - $mtime) > $trackback_period) {
+       $closed_trackbacks = 1;
+    } else {
+       $closed_trackbacks = 0;
+    }
+
+    return 1;
+}
+
+
+# Parse posted TrackBacks and comments and take action as appropriate.
+# Retrieve comments and TrackBacks and format according to the templates.
+# Display a comment form and/or TrackBack URL as appropriate.
+
+sub story {
+    my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
+    my $submission_type;
+    my $status_msg;
+    my $is_story_page;
+
+    # We have five possible tasks in this subroutine:
+    #
+    #   * handle submitted TrackBack pings or comments (or related requests)
+    #   * display previously-submitted TrackBacks or comments
+    #   * display a comment being previewed
+    #   * display a form for entering a comment (or editing a previewed one)
+    #   * display information about submitting TrackBacks
+    #
+    # Exactly what we do depends whether we are rendering dynamically or
+    # statically and on the type of request (GET, HEAD, or POST) (when
+    # dynamically rendering),  the Blosxom flavour, the parameters associated
+    # with the request, the age of the story, and the way the feedback
+    # plug-in itself is configured.
+
+    # Make $path empty if at top level, preceded by a single slash otherwise.
+    !defined($path) and $path = "";
+    $path =~ s!^/*!!; $path &&= "/$path";
+
+    # Set feedback path and filename for this story.
+    $fb_path = $path;
+    $fb_fn = $filename . '.' . $fb_extension;
+
+    # Determine whether this is an individual story page or not.
+    $is_story_page =
+       $blosxom::path_info =~ m!^(.*/)?(.+)\.(.+)$! ? 1 : 0;
+
+    # For dynamic rendering of an individual story page *only*, check to
+    # see if this is a feedback-related request, take action, and formulate
+    # a response.
+    #
+    # We have five possible cases: TrackBack ping, comment preview, comment
+    # post, moderator approval, and moderator rejection. These are
+    # distinguished based on the type of request (POST vs. GET/HEAD),
+    # the flavour (for TrackBack pings only), and the request parameters.
+
+    $submission_type = $comment_response = $trackback_response = '';
+    if ($blosxom::static_or_dynamic eq 'dynamic' && $is_story_page) {
+       ($submission_type, $status_msg) = process_submission();
+       if ($submission_type eq 'trackback') {
+           $trackback_response = format_tb_response($status_msg);
+           return 1;                   # All done.
+       } elsif ($submission_type eq 'comment'
+                || $submission_type eq 'preview'
+                || $submission_type eq 'approve'
+                || $submission_type eq 'reject') {
+           $comment_response = format_cmt_response($status_msg);
+       }
+    }
+
+    # Display previously-submitted comments and TrackBacks for this story.
+    # For index and and archive pages we just display counts of the comments
+    # and TrackBacks.
+
+    $comments = $trackbacks = '';
+    $comments_count = $trackbacks_count = 0;
+    if ($is_story_page) {
+       ($comments, $comments_count, $trackbacks, $trackbacks_count) =
+           get_feedback($path);
+    } else {
+       ($comments_count, $trackbacks_count) = count_feedback();
+    }
+    $count = $comments_count + $trackbacks_count;
+
+    # If we are previewing a comment then format the comment for display.
+    $preview = '';
+    if ($submission_type eq 'preview') {
+       $preview = get_preview($path);
+    }
+
+    # Display a form for comment submission, if we are on an individual
+    # story page and comments are (still) allowed. (If we are previewing
+    # a comment then the form will be pre-filled as appropriate.)
+
+    $commentform = '';
+    if ($is_story_page && $allow_comments) {
+       if ($closed_comments) {
+           $commentform =
+               "<p class=\"commentform\">"
+               . "Comments are closed for this story.</p>";
+       } else {
+           # Get the commentform template and interpolate variables in it.
+           $commentform =
+               &$blosxom::template($path,'commentform',$blosxom::flavour)
+               || &$blosxom::template($path,'commentform','general');
+           $commentform = &$blosxom::interpolate($commentform);
+       }
+    }
+
+    # Display information on submitting TrackPack pings (including code for
+    # TrackBack autodiscovery), if we are on an individual story page and
+    # TrackBacks are (still) allowed.
+
+    $trackbackinfo = '';
+    if ($is_story_page && $allow_trackbacks) {
+       if ($closed_trackbacks) {
+           $trackbackinfo =
+               "<p class=\"trackbackinfo\">"
+               . "Trackbacks are closed for this story.</p>";
+       } else {
+           # Get the trackbackinfo template and interpolate variables in it.
+           $trackbackinfo =
+               &$blosxom::template($path,'trackbackinfo',$blosxom::flavour)
+               || &$blosxom::template($path,'trackbackinfo','general');
+           $trackbackinfo = &$blosxom::interpolate($trackbackinfo);
+       }
+    }
+
+    # For interpolate_fancy to work properly when deciding whether to include
+    # certain content or not, the associated variables must be undefined if
+    # there is no actual content to be displayed.
+
+    $comment_response  =~ m!^\s*$! and $comment_response = undef;
+    $comments =~ m!^\s*$! and $comments = undef;
+    $trackbacks =~ m!^\s*$! and $trackbacks = undef;
+    $preview =~ m!^\s*$! and $preview = undef;
+    $commentform =~ m!^\s*$! and $commentform = undef;
+    $trackbackinfo =~ m!^\s*$! and $trackbackinfo = undef;
+
+    return 1;
+}
+
+
+# --- Helper subroutines ---
+
+# Process a submitted HTTP request and take whatever action is appropriate.
+# Returns the type of submission: 'trackback', 'comment', 'preview',
+# 'approve', 'reject', or null for a request not related to feedback.
+# Also sets $comment_response and $trackback_response;
+
+sub process_submission {
+    my $submission_type = '';
+    my $status_msg = '';
+
+    if (request_method() eq 'POST') {
+       # We have two possible cases: a TrackBack ping (identified by
+       # the flavour extension) or a submitted comment.
+
+       if ($blosxom::flavour eq $trackback_flavour) {
+           $status_msg = handle_feedback('trackback');
+           $submission_type = 'trackback';
+       } else {
+           # Comment posts may or may not use a particular flavour
+           # extension, so we check for the value of the 'plugin'
+           # hidden field (from the comment form).
+
+           my $plugin_param = sanitize_param(param('plugin'));
+           if ($plugin_param eq 'writeback') {
+               # Comment previews are distinguished from comment posts
+               # by the value of the 'submit' parameter associated with
+               # the 'Post' and 'Preview' form buttons.
+
+               my $submit_param = sanitize_param(param('submit'));
+               $status_msg = '';
+               if ($submit_param eq 'Preview') {
+                   $status_msg = handle_feedback('preview');
+                   $submission_type = 'preview';
+               } elsif ($submit_param eq 'Post') {
+                   $status_msg = handle_feedback('comment');
+                   $submission_type = 'comment';
+               } else {
+                   $status_msg = "The submit parameter must have the value "
+                       . "'Preview' or 'Post'";
+               }
+           }
+       }
+    } elsif (request_method() eq 'GET' || request_method() eq 'HEAD') {
+       my $moderate_param = sanitize_param(param('moderate'));
+       my $feedback_param = sanitize_param(param('feedback'));
+
+       if ($moderate_param) {
+           # We have two possible cases: moderator approval or rejection,
+           # distinguished based on the value of the 'moderate' parameter.
+
+           if (!$feedback_param) {
+               $status_msg =
+                   "You must provide a 'feedback' parameter and item.";
+           } elsif ($moderate_param eq 'approve') {
+               $status_msg = approve_feedback($feedback_param);
+               $submission_type = 'approve';
+           } elsif ($moderate_param eq 'reject') {
+               $status_msg = reject_feedback($feedback_param);
+               $submission_type = 'reject';
+           } else {
+               $status_msg =
+                   "'moderate' parameter must "
+                   . "have the value 'approve' or 'reject'.";
+           }
+       }
+    }
+
+    return $submission_type, $status_msg;
+}
+
+
+# Retrieve comments and TrackBacks for a story and format them according
+# to the appropriate templates for the story (based on the story's path).
+# For comments we use the comment template for each individual comment,
+# along with the optional comments_head and comments_foot templates (before
+# and after the comments proper). For TrackBacks we use the corresponding
+# trackback template for each TrackBack, together with the optional
+# trackbacks_head and trackbacks_foot templates.
+
+sub get_feedback {
+    my $path = shift;
+    my ($comments, $comments_count, $trackbacks, $trackbacks_count);
+
+    $comments = $trackbacks = '';
+    $comments_count = $trackbacks_count = 0;
+
+    # Retrieve the templates for individual comments and TrackBacks.
+    my $comment_template =
+       &$blosxom::template($path, 'comment', $blosxom::flavour)
+       || &$blosxom::template($path, 'comment', 'general');
+
+    my $trackback_template =
+       &$blosxom::template($path, 'trackback', $blosxom::flavour)
+       || &$blosxom::template($path, 'trackback', 'general');
+
+    # Open the feedback file (if it exists) and read any comments or
+    # TrackBacks. Note that we can distinguish comments from TrackBacks
+    # because comments have a 'comment' field and TrackBacks don't.
+
+    my %param = ();
+    if ($fh->open("$fb_dir$fb_path/$fb_fn")) {
+       foreach my $line (<$fh>) {
+           $line =~ /^(.+?): (.*)$/ and $param{$1} = $2;
+           if ( $line =~ /^-----$/ ) {
+               if ($param{'comment'}) {
+                   $comment = format_comment($param{'comment'});
+                   $date = format_date($param{'date'});
+                   ($name, $name_link) =
+                       format_name($param{'name'}, $param{'url'});
+
+                   my $cmt = $comment_template;
+                    $cmt = &$blosxom::interpolate($cmt);
+
+                   $comments .= $cmt;
+                   $comments_count++;
+               } else {
+
+                   $blog_name = format_blog_name($param{'blog_name'});
+                   $excerpt = format_excerpt($param{'excerpt'});
+                   $date = format_date($param{'date'});
+                   ($title, $title_link) =
+                        format_title($param{'title'}, $param{'url'});
+
+                   my $trackback = $trackback_template;
+                    $trackback = &$blosxom::interpolate($trackback);
+
+                   $trackbacks .= $trackback;
+                   $trackbacks_count++;
+               }
+               %param = ();
+           }
+       }
+    }
+
+    return ($comments, $comments_count, $trackbacks, $trackbacks_count);
+}
+
+
+# Retrieve comments and TrackBacks for a story and (just) count them.
+
+sub count_feedback {
+    my $comments_count = 0;
+    my $trackbacks_count = 0;
+
+    # Open the feedback file (if it exists) and count any comments or
+    # TrackBacks. Note that we can distinguish comments from TrackBacks
+    # because comments have a 'comment' field and TrackBacks don't.
+
+    my %param = ();
+    if ($fh->open("$fb_dir$fb_path/$fb_fn")) {
+       foreach my $line (<$fh>) {
+           $line =~ /^(.+?): (.*)$/ and $param{$1} = $2;
+           if ( $line =~ /^-----$/ ) {
+               if ($param{'comment'}) {
+                   $comments_count++;
+               } else {
+                   $trackbacks_count++;
+               }
+               %param = ();
+           }
+       }
+    }
+
+    return ($comments_count, $trackbacks_count);
+}
+
+
+# Format a previewed comment according to the appropriate preview template
+# for the story (based on the story's path).
+
+sub get_preview {
+    my $path = shift;
+    my $preview = '';
+
+    # Retrieve the comment template (also used for previewed comments).
+    my $comment_template =
+       &$blosxom::template($path, 'comment', $blosxom::flavour)
+       || &$blosxom::template($path, 'comment', 'general');
+
+    # Format the previewed comment using the submitted values.
+    $comment = format_comment($comment_preview);
+    $date = format_date($date_preview);
+    ($name, $name_link) =
+       format_name($name_preview, $url_preview);
+
+    $preview = &$blosxom::interpolate($comment_template);
+
+    return $preview;
+}
+
+
+# Create top-level directory to hold feedback files, and make it writeable.
+sub mk_feedback_dir {
+    my $mkdir_r = mkdir("$fb_dir", 0755);
+    warn $mkdir_r
+        ? "feedback: $fb_dir created.\n"
+        : "feedback: Could not create $fb_dir.\n";
+    $mkdir_r or return 0;
+
+    my $chmod_r = chmod 0755, $fb_dir;
+    warn $chmod_r
+        ? "feedback: $fb_dir set to 0755 permissions.\n"
+        : "feedback: Could not set permissions on $fb_dir.\n";
+    $chmod_r or return 0;
+
+    warn "feedback: feedback is enabled!\n";
+    return 1;
+}
+
+
+# Create subdirectories of feedback directory as necessary.
+sub mk_feedback_subdir {
+    my $dir = shift;
+    my $p = '';
+
+    return 1 if !defined($dir) or $dir eq '';
+
+    foreach (('', split /\//, $dir)) {
+        $p .= "/$_";
+        $p =~ s!^/!!;
+        return 0
+           unless (-d "$fb_dir/$p" or mkdir "$fb_dir/$p", 0755);
+    }
+
+    return 1;
+}
+
+
+# Process a submitted comment or TrackBack.
+sub handle_feedback {
+    my $feedback_type = shift;
+    my $status_msg = '';
+    my $is_comment;
+    my $is_preview;
+    my $fb_item;
+
+    # Set up to handle either a comment, preview, or TrackBack as requested.
+    if ($feedback_type eq 'comment') {
+       $is_comment = 1;
+       $is_preview = 0;
+    } elsif ($feedback_type eq 'preview') {
+       $is_comment = 1;
+       $is_preview = 1;
+    } else {
+       $is_comment = 0;
+       $is_preview = 0;
+    }
+
+    my $allow = $is_comment ? $allow_comments : $allow_trackbacks;
+    my $closed = $is_comment ? $closed_comments : $closed_trackbacks;
+    my $period = $is_comment ? $comment_period : $trackback_period;
+    my $akismet = $is_comment ? $akismet_comments : $akismet_trackbacks;
+    my $blacklist = $is_comment ? $blacklist_comments : $blacklist_trackbacks;
+    my $notify = $is_comment ? $notify_comments : $notify_trackbacks;
+    my $moderate = $is_comment ? $moderate_comments : $moderate_trackbacks;
+    my @fields = $is_comment ? @comment_fields : @trackback_fields;
+
+    # Reject request if feedback is not (still) allowed.
+    unless ($allow && !$closed) {
+       if ($closed) {
+           $status_msg =
+               "This story is older than " . ($period/86400) . " days. "
+               . ($is_comment ? "Comments" : "TrackBacks")
+               . " have now been closed.";
+       } else {
+           $status_msg =
+               ($is_comment ? "Comments" : "TrackBacks")
+               . " are not enabled for this site.";
+       }
+       return $status_msg;
+    }
+
+    # Filter out the "good" fields from the CGI parameters.
+    my %params = copy_params(\@fields);
+
+    # Comments must have (at least) a comment parameter, and TrackBacks a URL.
+    if ($is_comment) {
+       unless ($params{'comment'}) {
+           $status_msg =
+               "You didn't enter anything in the comment field.";
+           return $status_msg;
+       }
+    } elsif (!$params{'url'}) {
+       $status_msg = "No URL specified for the TrackBack";
+       return 0;
+    }
+
+    # Check feedback to see if it's spam.
+    if (is_spam(\%params, $is_comment, $akismet, $blacklist)) {
+       # If we are previewing a comment then we allow the poster a
+       # chance to revise the comment; otherwise we just reject it.
+
+       if ($is_preview) {
+           $status_msg =
+               "Your comment appears to be spam and will be rejected "
+               . "unless it is revised. ";
+       } else {
+           $status_msg =
+               "Your feedback was rejected because it appears to be spam; "
+               . "please contact the site administrator if you believe that "
+               . "your feedback was rejected in error.";
+           return $status_msg;
+       }
+    }
+
+    # If we are previewing a comment then just save the fields for later
+    # use in the previewed comment and (as prefilled values) in the comment
+    # form. Otherwise attempt to save the new feedback information, either
+    # into the permanent feedback file for this story (if no moderation) or
+    # into a temporary file (for later moderation).
+
+    if ($is_preview) {
+        $status_msg .= save_preview(\%params);
+    } else {
+       ($fb_item, $status_msg) = save_feedback(\%params, $moderate);
+       return $status_msg unless $fb_item;
+
+       # Send a moderation message or notify blog owner of the new feedback.
+       if ($moderate || $notify) {
+           send_notification(\%params, $moderate, $fb_item);
+       }
+    }
+
+    return $status_msg;
+}
+
+
+# Make a "safe" copy of the CGI parameters based on the expected
+# field names associated with either a comment or TrackBack.
+sub copy_params {
+    my $fields_ref = shift;
+    my %params;
+
+    foreach my $key (@$fields_ref) {
+        my $value = substr(param($key), 0, $max_param_length) || "";
+
+       # Eliminate leading and trailing whitespace, use carriage returns
+       # as line delimiters, and collapse multiple blank lines into one.
+
+       $value =~ s/^\s+//;
+       $value =~ s/\s+$//;
+       $value =~ s/\r?\n\r?/\r/mg;
+       $value =~ s/\r\r\r*/\r\r/mg;
+
+        $params{$key} = $value;
+    }
+
+    return %params;
+}
+
+
+# Send notification or moderation email to blog owner.
+sub send_notification {
+    my ($params_ref, $moderate, $fb_item) = @_;
+
+    unless ($address && $smtp_server) {
+       warn "feedback: No address or SMTP server for notifications\n";
+       return 0;
+    }
+
+    my $message = "New feedback for your post \"$blosxom::title\" ("
+       . $blosxom::path_info . "):\n\n";
+
+    if ($$params_ref{'comment'}) {
+       $message .= "Name     : " . $$params_ref{'name'} . "\n";
+       $message .= "Email/URL: " . $$params_ref{'url'} . "\n";
+       $message .= "Comment  :\n";
+       my $comment = $$params_ref{'comment'};
+       $comment =~ s!\r!\n!g;
+       $message .= $comment . "\n";
+    } else {
+       $message .= "Blog name: " . $$params_ref{'blog_name'} . "\n";
+       $message .= "Article  : " . $$params_ref{'title'} . "\n";
+       $message .= "URL      : " . $$params_ref{'url'} . "\n";
+       $message .= "Excerpt  :\n";
+       my $excerpt = $$params_ref{'excerpt'};
+       $excerpt =~ s!\r!\n!g;
+       $message .= $excerpt . "\n";
+    }
+
+    if ($moderate) {
+       # For TrackBacks use the default flavour for the approve/reject URI.
+       my $moderate_flavour = $blosxom::flavour;
+       $moderate_flavour eq $trackback_flavour
+           and $moderate_flavour = $blosxom::default_flavour;
+
+       $message .= "\n\nTo approve this feedback, please click on the URL\n"
+           . "$blosxom::url$blosxom::path/$blosxom::fn.$moderate_flavour"
+           . "?moderate=approve;feedback=" . uri_escape($fb_item) . "\n";
+
+       $message .= "\nTo reject this feedback, please click on the URL\n"
+           . "$blosxom::url$blosxom::path/$blosxom::fn.$moderate_flavour"
+           . "?moderate=reject;feedback=" . uri_escape($fb_item) . "\n";
+    }
+
+    # Load Net::SMTP module only now that it's needed.
+    require Net::SMTP; Net::SMTP->import;
+
+    my $smtp = Net::SMTP->new($smtp_server);
+    $smtp->mail($address);
+    $smtp->to($address);
+    $smtp->data();
+    $smtp->datasend("To: $address\n");
+    $smtp->datasend("From: $address\n");
+    $smtp->datasend("Subject: [$blosxom::blog_title] Feedback: "
+                   . "\"$blosxom::title\"\n");
+    $smtp->datasend("\n\n");
+    $smtp->datasend($message);
+    $smtp->dataend();
+    $smtp->quit;
+
+    return 1;
+}
+
+
+# Format the date used in comments and TrackBacks. If the argument is a
+# number then it is considered to be a date/time in seconds since the
+# (Perl) epoch; otherwise we assume that the date is already formatted.
+# (This may allow the feedback plug-in to use legacy writeback files.)
+
+sub format_date {
+    my $date_value = shift;
+
+    if ($date_value =~ m!^\d+$!) {
+       my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
+           localtime($date_value);
+       $year += 1900;
+
+       # Modify the following to match your preference.
+       return sprintf("%4d-%02d-%02d %02d:%02d",
+                      $year, $mon+1, $mday, $hour, $min);
+    } else {
+       return $date_value;
+    }
+}
+
+
+# Format the name used in comments.
+sub format_name {
+    my ($name, $url) = @_;
+
+    # If the user didn't supply a name, try to use something sensible.
+    unless ($name) {
+       if ($url =~ m/^mailto:/) {
+           $name = substr($url, 7);
+       } else {
+           $name = $default_name;
+       }
+    }
+
+    # Link to a URL if one was provided.
+    my $name_link =
+       $url ? "<a href=\"$url\" rel=\"nofollow\">$name</a>" : $name ;
+
+    return $name, $name_link;
+}
+
+
+# Format the comment response message.
+sub format_cmt_response {
+    my $response = shift;
+
+    # Clean up the response.
+    $response =~ s/^\s+//;
+    $response =~ s/\s+$//;
+
+    # Convert the response into a special type of paragraph.
+    # NOTE: A value 'OK' for $response indicates a successful comment.
+    if ($response eq 'OK') {
+       $response = '<p class="comment-response">Thanks for the comment!</p>';
+    } else {
+       $response = '<p class="comment-response">' . $response . '</p>';
+    }
+
+    return $response;
+}
+
+
+# Format the TrackBack response message.
+sub format_tb_response {
+    my $response = shift;
+
+    # Clean up the response.
+    $response =~ s/^\s+//;
+    $response =~ s/\s+$//;
+
+    # Convert the response into an XML message per the TrackBack Technical
+    # Specification <http://www.sixapart.com/pronet/docs/trackback_spec>.
+    # NOTE: A value 'OK' for $response indicates a successful TrackBack;
+    # note that this value is *not* used as part of the TrackBack response.
+
+    if ($response eq 'OK') {
+       $response = "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>"
+           . "<response><error>0</error></response>";
+    } else {
+       $response = "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>"
+           . "<response><error>1</error>"
+           . "<message>$response</message></response>";
+    }
+
+    return $response;
+}
+
+
+# Format the comment itself.
+sub format_comment {
+    my $comment = shift;
+
+    # TODO: Support other comment formats such as Textile.
+
+    if ($comment_format eq 'none') {
+       # A no-op, assumes formatting will be added in the template.
+    } elsif ($comment_format eq 'plaintext') {
+       # Simply convert the comment into a series of paragraphs.
+       $comment = '<p>' . $comment . '</p>';
+       $comment =~ s!\r\r!</p><p>!mg;
+    } elsif ($comment_format eq 'markdown'
+            && $blosxom::plugins{'Markdown'} > 0) {
+       $comment = &Markdown::Markdown($comment);
+    }
+
+    return $comment;
+}
+
+
+# Format the blog name used in TrackBacks.
+sub format_blog_name {
+    my $blog_name = shift;
+
+    $blog_name or $blog_name = $default_blog_name;
+
+    return $blog_name;
+}
+
+
+# Format the title used in TrackBacks.
+sub format_title {
+    my ($title, $url) = @_;
+    my $title_link;
+
+    # Link to article, quoting the title if one was supplied.
+    if ($title) {
+       $title_link = "\"<a href=\"$url\" rel=\"nofollow\">$title</a>\"";
+    } else {
+       $title = $default_title;
+       $title_link = "<a href=\"$url\" rel=\"nofollow\">$title</a>";
+    }
+
+    return $title, $title_link;
+}
+
+
+# Format the TrackBack excerpt.
+sub format_excerpt {
+    my $excerpt = shift;
+
+    # TODO: Truncate excerpts at some reasonable length.
+
+    # Simply convert the excerpt into a series of paragraphs.
+    if ($excerpt) {
+       $excerpt = '<p>' . $excerpt . '</p>';
+       $excerpt =~ s!\r\r!</p><p>!mg;
+    }
+
+    return $excerpt;
+}
+
+
+# Read in the MT-Blacklist file.
+sub read_blacklist {
+
+    # No need to do anything if we've already read in the blacklist file.
+    return 1 if @blacklist_entries;
+
+    # Try to find the blacklist file and open it.
+    open BLACKLIST, "$blacklist_file"
+       or die "Can't read '$blacklist_file', $!\n";
+
+    my @lines = grep {! /^\s*\#/ } <BLACKLIST>;
+    close BLACKLIST;
+    die "No blacklists?\n" unless @lines;
+
+    foreach my $line (@lines) {
+       $line =~ s/^\s*//;
+       $line =~ s/\s*[^\\]\#.*//;
+       next unless $line;
+       push @blacklist_entries, $line;
+    }
+    die "No entries in blacklist file?\n" unless @blacklist_entries;
+
+    return 1;
+}
+
+
+# Do spam tests on comment or TrackBack; returns 1 if spam, 0 if OK.
+sub is_spam {
+    my ($params_ref, $is_comment, $akismet, $blacklist) = @_;
+
+    # Perform a series of spam tests. If any show positive then reject.
+
+    # Does the host part of the URL reference an IP address?
+    return 1 if uses_ipaddr($$params_ref{'url'});
+
+    # Does the comment or TrackBack match against the Akismet service?
+    return 1 if $akismet && matches_akismet($params_ref, $is_comment);
+
+    # Does the comment or TrackBack match against the MT-Blacklist file
+    # (deprecated)?
+    return 1
+       if $blacklist && matches_blacklist((join "\n", values %$params_ref));
+
+    # TODO: Add other useful spam checks.
+
+    # Got by all the tests, so assume it's not spam.
+    return 0;
+}
+
+
+# Check host part of URL to see if it is an IP address.
+sub uses_ipaddr {
+    my $uri = shift;
+
+    return 0 unless $uri;
+
+    # Construct URI object.
+    my $u = URI->new($uri);
+
+    # Return if this not actually a URI (i.e., it's an email address).
+    return 0 unless defined($u->scheme);
+
+    # Check for an IPv4 or IPv6 address on http/https URLs.
+    if ($u->scheme eq 'http' || $u->scheme eq 'https') {
+        if ($u->authority =~ m!^\[?\d!) {
+           return 1;
+       }
+    }
+
+    return 0;
+}
+
+
+# Check comment or TrackBack against the Akismet online service.
+sub matches_akismet {
+    my ($params_ref, $is_comment) = @_;
+
+    # Load Net:Akismet module only now that it's needed.
+    require Net::Akismet; Net::Akismet->import;
+
+    # Attempt to connect to the Askimet service.
+    my $akismet = Net::Akismet->new(KEY => $wordpress_api_key,
+                                   URL => $blosxom::url);
+    unless ($akismet) {
+       warn "feedback: Akismet key verification failed\n";
+       return 0;
+    }
+
+    # Set up fields to be verified. Note that we do not use the REFERRER,
+    # PERMALINK, or COMMENT_AUTHOR_EMAIL fields supported by Akismet.
+
+    my %fields = (USER_IP => $ENV{'REMOTE_ADDR'});
+    if ($is_comment) {
+       $fields{COMMENT_TYPE} = 'comment';
+       $fields{COMMENT_CONTENT} = $$params_ref{'comment'};
+       $fields{COMMENT_AUTHOR} = $$params_ref{'name'};
+       $fields{COMMENT_AUTHOR_URL} = $$params_ref{'url'};
+    } else {
+       $fields{COMMENT_TYPE} = 'trackback';
+       $fields{COMMENT_CONTENT} =
+           $$params_ref{'title'} . "\n" . $$params_ref{'excerpt'};
+       $fields{COMMENT_AUTHOR} = $$params_ref{'blog_name'};
+       $fields{COMMENT_AUTHOR_URL} = $$params_ref{'url'};
+    }
+
+    # Is it spam?
+    return 1 if $akismet->check(%fields) eq 'true';
+
+    # Apparently not.
+    return 0;
+}
+
+
+# Check comment or TrackBack against the MT-Blacklist file (deprecated).
+sub matches_blacklist {
+    my $params_string = shift;
+
+    # Read in the blacklist file.
+    read_blacklist();
+
+    # Check each blacklist entry against the comment or TrackBack.
+    foreach my $spam (@blacklist_entries) {
+       chomp($spam);
+       return 1 if $params_string =~ /$spam/;
+    }
+
+    return 0;
+}
+
+
+# Save comment or TrackBack to disk. If moderating, returns the (randomly-
+# generated) id of the item saved for later approval or rejection (plus
+# a status message). If not moderating returns the name of the feedback
+# file in which the item was saved instead of the id. Returns null on errors.
+
+sub save_feedback {
+    my ($params_ref, $moderate) = @_;
+    my $fb_item = '';
+    my $feedback_fn = '';
+    my $status_msg = '';
+
+    # Clear values used to prefill commentform.
+    $name_preview = $url_preview = $comment_preview = '';
+
+    # Create a new directory if needed to contain the feedback file.
+    unless (mk_feedback_subdir($fb_path)) {
+       $status_msg = 'Could not save comment or TrackBack.';
+       return '', $status_msg;
+    }
+
+    # Save into the main feedback file or a temporary file, depending on
+    # whether feedback is being moderated or not.
+    if ($moderate) {
+       $fb_item = rand_alphanum(8);
+       $feedback_fn = $fb_item . '-' . $fb_fn;
+    } else {
+       $feedback_fn = $fb_fn;
+    }
+
+    # Attempt to open the file and append to it.
+    unless ($fh->open(">> $fb_dir$fb_path/$feedback_fn")) {
+        warn "couldn't >> $fb_dir$fb_path/$feedback_fn\n";
+       $status_msg = 'Could not save comment or TrackBack.';
+       return '', $status_msg;
+    }
+
+    # Write each parameter out as a line in the file.
+    foreach my $key (sort keys %$params_ref) {
+       my $value = $$params_ref{$key};
+
+       # Eliminate leading and trailing whitespace, use carriage returns
+       # as line delimiters, and collapse multiple blank lines into one.
+
+       $value =~ s/^\s+//;
+       $value =~ s/\s+$//;
+       $value =~ s/\r?\n\r?/\r/mg;
+       $value =~ s/\r\r\r*/\r\r/mg;
+
+       # Ensure URL and other fields are sanitized.
+       if ($key eq 'url') {
+           $value = sanitize_uri($value);
+       } else {
+           $value = escapeHTML($value);
+       }
+
+       print $fh "$key: $value\n";
+    }
+
+    # Save the date/time (in seconds) and IP address as well.
+    print $fh "date: " . time() ."\n";
+    print $fh "ip: " . $ENV{'REMOTE_ADDR'} . "\n";
+
+    # End the entry and close the file.
+    print $fh "-----\n";
+    $fh->close();
+
+    # Set responses to indicate success.
+    if ($moderate) {
+       $status_msg =
+           "Your feedback has been submitted for a moderator's approval; "
+           . "it may take 24 hours or more to appear on the site.";
+       return $fb_item, $status_msg;
+    } else {
+       $status_msg = 'OK';
+       return $feedback_fn, $status_msg;
+    }
+}
+
+
+# Generate random alphanumeric string of the specified length.
+sub rand_alphanum {
+    my $size = shift;
+    return '' if $size <= 0;
+
+    my @alphanumeric = ('a'..'z', 'A'..'Z', 0..9);
+    return join '', map $alphanumeric[rand @alphanumeric], 0..$size;
+}
+
+
+# Save previewed comment for later viewing (on the same page).
+# Sets $status_msg with an appropriate message.
+sub save_preview {
+    my $params_ref = shift;
+    my $status_msg;
+
+    # Save each parameter for later use in the preview template.
+    foreach my $key (sort keys %$params_ref) {
+       my $value = $$params_ref{$key};
+
+       # Eliminate leading and trailing whitespace, use carriage returns
+       # as line delimiters, and collapse multiple blank lines into one.
+
+       $value =~ s/^\s+//;
+       $value =~ s/\s+$//;
+       $value =~ s/\r?\n\r?/\r/mg;
+       $value =~ s/\r\r\r*/\r\r/mg;
+
+       # Ensure URL and other fields are sanitized.
+       if ($key eq 'url') {
+           $value = sanitize_uri($value);
+       } else {
+           $value = escapeHTML($value);
+       }
+
+       if ($key eq 'name') {
+           $name_preview = $value;
+       } elsif ($key eq 'url') {
+           $url_preview = $value;
+       } elsif ($key eq 'comment') {
+           $comment_preview = $value;
+       }
+    }
+
+    # Save the date/time (in seconds) as well.
+    $date_preview = time();
+
+    # Set response to indicate success.
+    $status_msg .=
+       "Please review your previewed comment below and submit it when "
+       . "you are ready.";
+
+    return $status_msg;
+}
+
+
+# Approve a moderated comment or TrackBack (add it to feedback file).
+sub approve_feedback {
+    my $item = shift;
+    my $item_fn;
+    my $status_msg = '';
+
+    # Construct filename containing item to be approved, checking the
+    # item name against the proper format from save_feedback().
+    if ($item =~ m!^[a-zA-Z0-9]{8}!) {
+       $item_fn = $item . "-" . $fb_fn;
+    } else {
+       $status_msg =
+           "The item name to be approved was not in the proper format.";
+       return $status_msg;
+    }
+
+    # Read lines from file containing the approved comment or TrackBack.
+    unless ($fh->open("$fb_dir$fb_path/$item_fn")) {
+        warn "feedback: couldn't < $fb_dir$fb_path/$item_fn\n";
+        $status_msg =
+           "There was a problem approving the comment or TrackBack.";
+       return $status_msg;
+    }
+
+    my @new_feedback = ();
+    while (<$fh>) {
+       push @new_feedback, $_;
+    }
+    $fh->close();
+
+    # Attempt to open the story's feedback file and append to it.
+    # TODO: Try to make this more resistant to race conditions.
+
+    unless ($fh->open(">> $fb_dir$fb_path/$fb_fn")) {
+        warn "couldn't >> $fb_dir$fb_path/$fb_fn\n";
+        $status_msg =
+           "There was a problem approving the comment or TrackBack.";
+       return $status_msg;
+    }
+
+    foreach my $line (@new_feedback) {
+       print $fh $line;
+    }
+
+    # Close the feedback file, delete the file with the approved item.
+    $fh->close();
+    chdir("$fb_dir$fb_path")
+       or warn "feedback: Couldn't cd to $fb_dir$fb_path\n";
+    unlink($item_fn)
+       or warn "feedback: Couldn't delete $item_fn\n";
+
+    # Set response to indicate successful approval.
+    $status_msg = "Feedback '$item' approved by moderator. ";
+
+    return $status_msg;
+}
+
+
+# Reject a moderated comment or TrackBack (delete the temporary file).
+sub reject_feedback {
+    my $item = shift;
+    my $item_fn;
+    my $status_msg;
+
+    # Construct filename containing item to be rejected, checking the
+    # item name against the proper format from save_feedback().
+    if ($item =~ m!^[a-zA-Z0-9]{8}!) {
+       $item_fn = $item . "-" . $fb_fn;
+    } else {
+       $status_msg =
+           "The item name to be rejected was not in the proper format.";
+       return $status_msg;
+    }
+
+    # TODO: Optionally report comment or TrackBack to Akismet as spam.
+
+    # Delete the file with the rejected item.
+    chdir("$fb_dir$fb_path")
+       or warn "feedback: Couldn't cd to '$fb_dir$fb_path'\n";
+    unlink($item_fn)
+       or warn "feedback: Couldn't delete '$item_fn'\n";
+
+    # Set response to indicate successful rejection.
+    $status_msg = "Feedback '$item' rejected by moderator.";
+
+    return $status_msg;
+}
+
+
+# Sanitize a query parameter to remove unexpected characters.
+sub sanitize_param
+{
+    my $param = shift || '';
+
+    # Allow only alphanumeric, underscore, dash, and period.
+    $param and $param =~ s/[^-.\w]/_/go;
+
+    return $param;
+}
+
+
+# Sanitize a URI.
+sub sanitize_uri {
+    my $uri = shift;
+
+    # Construct URI object.
+    my $u = URI->new($uri);
+
+    # If it's not a URI then assume it's an email address.
+    $u->scheme('mailto') unless defined($u->scheme);
+
+    # We check email addresses (if allowed) separately from web addresses.
+    if ($allow_mailto && $u->scheme eq 'mailto') {
+       # Make sure this is a valid RFC 822 address.
+       if (valid($u->opaque)) {
+           $uri = $u->canonical;
+       } else {
+           $status_msg = "You submitted an invalid email address. ";
+           $uri = '';
+       }
+    } elsif ($u->scheme eq 'http' || $u->scheme eq 'https') {
+       if ($u->authority =~ m!^.*@!) {
+           $status_msg =
+               "Userids and passwords are not permitted in the URL field. ";
+           $uri = '';
+       } elsif ($u->authority =~ m!^\d! || $u->authority =~ m!^\[\d!) {
+           $status_msg =
+               "IP addresses are not permitted in the URL field. ";
+           $uri = '';
+       } else {
+           $uri = $u->canonical;
+       }
+    } else {
+       $status_msg =
+           "You specified an invalid scheme in the URL field; ";
+       if ($allow_mailto) {
+           $status_msg .=
+               "the only allowed schemes are 'http', 'https', and 'mailto'. ";
+       } else {
+           $status_msg .=
+               "the only allowed schemes are 'http' and 'https'. ";
+       }
+       $uri = '';
+    }
+
+    return $uri;
+}
+
+# The following is taken from the Mail::RFC822::Address module, for
+# sites that don't have that module loaded.
+my $rfc822re;
+
+# Preloaded methods go here.
+my $lwsp = "(?:(?:\\r\\n)?[ \\t])";
+
+sub make_rfc822re {
+#   Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
+#   comment.  We must allow for lwsp (or comments) after each of these.
+#   This regexp will only work on addresses which have had comments stripped
+#   and replaced with lwsp.
+
+    my $specials = '()<>@,;:\\\\".\\[\\]';
+    my $controls = '\\000-\\031';
+
+    my $dtext = "[^\\[\\]\\r\\\\]";
+    my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$lwsp*";
+
+    my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$lwsp)*\"$lwsp*";
+
+#   Use zero-width assertion to spot the limit of an atom.  A simple
+#   $lwsp* causes the regexp engine to hang occasionally.
+    my $atom = "[^$specials $controls]+(?:$lwsp+|\\Z|(?=[\\[\"$specials]))";
+    my $word = "(?:$atom|$quoted_string)";
+    my $localpart = "$word(?:\\.$lwsp*$word)*";
+
+    my $sub_domain = "(?:$atom|$domain_literal)";
+    my $domain = "$sub_domain(?:\\.$lwsp*$sub_domain)*";
+
+    my $addr_spec = "$localpart\@$lwsp*$domain";
+
+    my $phrase = "$word*";
+    my $route = "(?:\@$domain(?:,\@$lwsp*$domain)*:$lwsp*)";
+    my $route_addr = "\\<$lwsp*$route?$addr_spec\\>$lwsp*";
+    my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
+
+    my $group = "$phrase:$lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
+    my $address = "(?:$mailbox|$group)";
+
+    return "$lwsp*$address";
+}
+
+sub strip_comments {
+    my $s = shift;
+#   Recursively remove comments, and replace with a single space.  The simpler
+#   regexps in the Email Addressing FAQ are imperfect - they will miss escaped
+#   chars in atoms, for example.
+
+    while ($s =~ s/^((?:[^"\\]|\\.)*
+                    (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)
+                    \((?:[^()\\]|\\.)*\)/$1 /osx) {}
+    return $s;
+}
+
+#   valid: returns true if the parameter is an RFC822 valid address
+#
+sub valid ($) {
+    my $s = strip_comments(shift);
+
+    if (!$rfc822re) {
+        $rfc822re = make_rfc822re();
+    }
+
+    return $s =~ m/^$rfc822re$/so;
+}
+
+
+1;
+
+
+# Default feedback templates.
+__DATA__
+html comment \n<div class="comment"><p>$feedback::name_link wrote at $feedback::date:</p>\n<blockquote>$feedback::comment</blockquote></div>
+html trackback \n<div class="trackback"><p>$feedback::blog_name mentioned this post in $feedback::title_link<?$feedback::excerpt eq="">.</p></?><?$feedback::excerpt ne="">:</p>\n<blockquote>$feedback::excerpt</blockquote></?></div>
+html commentform \n<form method="POST" action="$blosxom::url$blosxom::path/$blosxom::fn.$blosxom::flavour">\n<table><tr><td>Name:</td><td><input name="name" size="35" value="$feedback::name_preview"></td></tr>\n<tr><td>URL (optional):</td><td><input name="url" size="35" value="$feedback::url_preview"></td></tr>\n<tr><td>Comments:</td><td><textarea name="comment" rows="5" cols="60">$feedback::comment_preview</textarea></td></tr>\n<tr><td><input type="hidden" name="plugin" value="writeback"><input type="submit" name="submit" value="Preview"></td><td><input type="submit" name="submit" value="Post"></td></tr>\n</table></form>
+html trackbackinfo <p>URL for TrackBack pings: <code>$blosxom::url$blosxom::path/$blosxom::fn.$feedback::trackback_flavour</code></p>\n<!--\n<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/">\n<rdf:Description rdf:about="$blosxom::url$blosxom::path/$blosxom::fn.$blosxom::flavour" dc:identifier="$blosxom::url$blosxom::path/$blosxom::fn.$blosxom::flavour" dc:title="$blosxom::title" trackback:ping="$blosxom::url$blosxom::path/$blosxom::fn.$feedback::trackback_flavour" />\n</rdf:RDF>\n-->
+general comment \n<div class="comment"><p>$feedback::name_link wrote at $feedback::date:</p>\n<blockquote>$feedback::comment</blockquote></div>
+general trackback \n<div class="trackback"><p>$feedback::blog_name mentioned this post in $feedback::title_link<?$feedback::excerpt eq="">.</p></?><?$feedback::excerpt ne="">:</p>\n<blockquote>$feedback::excerpt</blockquote></?></div>
+general commentform \n<form method="POST" action="$blosxom::url$blosxom::path/$blosxom::fn.$blosxom::flavour">\n<table><tr><td>Name:</td><td><input name="name" size="35" value="$feedback::name_preview"></td></tr>\n<tr><td>URL (optional):</td><td><input name="url" size="35" value="$feedback::url_preview"></td></tr>\n<tr><td>Comments:</td><td><textarea name="comment" rows="5" cols="60">$feedback::comment_preview</textarea></td></tr>\n<tr><td><input type="hidden" name="plugin" value="writeback"><input type="submit" name="submit" value="Preview"></td><td><input type="submit" name="submit" value="Post"></td></tr>\n</table></form>
+general trackbackinfo <p>URL for TrackBack pings: <code>$blosxom::url$blosxom::path/$blosxom::fn.$feedback::trackback_flavour</code></p>\n<!--\n<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/">\n<rdf:Description rdf:about="$blosxom::url$blosxom::path/$blosxom::fn.$blosxom::flavour" dc:identifier="$blosxom::url$blosxom::path/$blosxom::fn.$blosxom::flavour" dc:title="$blosxom::title" trackback:ping="$blosxom::url$blosxom::path/$blosxom::fn.$feedback::trackback_flavour" />\n</rdf:RDF>\n-->
+trackback content_type application/xml
+trackback head
+trackback story $feedback::trackback_response
+trackback date
+trackback foot
+__END__
+
+=head1 NAME
+
+Blosxom Plug-in: feedback
+
+=head1 SYNOPSIS
+
+Provides comments and TrackBacks
+(C<http://www.movabletype.org/trackback/>); also supports comment and
+TrackBack moderation and spam filtering using Akismet and/or
+MT-Blacklist (deprecated). Inspired by the original writeback plug-in
+and the various enhanced versions of it.
+
+Comments and TrackBack pings for a particular story are kept in
+C<$fb_dir/$path/$filename.wb>.
+
+=head1 QUICK START
+
+Drop this feedback plug-in file into your plug-ins directory (whatever
+you set as C<$plugin_dir> in C<blosxom.cgi>), and modify the file to
+set the configurable variable C<$fb_dir>. You must also modify the
+variable C<$wordpress_api_key> if you are using the Akismet spam
+blacklist service, the variable C<$blacklist_file> if you are using
+the MT-Blacklist file (deprecated), and the variables C<$address> and
+C<$smtp_server> if you want feedback notification or moderation. (See
+below for more information on these optional features.)
+
+Note that by default all comments and TrackBacks are allowed, with no
+spam checking, moderation, or notification.
+
+Modify your story template (e.g., C<story.html> in your Blosxom data
+directory) to include the variables C<$feedback::comments> and
+C<$feedback::trackbacks> at the points where you'd like comments and
+trackbacks to be inserted.
+
+Modify your story template or foot template (e.g., C<foot.html> in
+your Blosxom data directory) to include the variables
+C<$feedback::comment_response>, C<$feedback::preview>,
+C<$feedback::commentform> and C<$feedback::trackbackinfo> at the
+points where you'd like to insert the response to a submitted comment,
+the previewed comment (if any), the comment submission form and the
+TrackBack information (including TrackBack auto-discovery code).
+
+=head1 CONFIGURATION
+
+By default C<$fb_dir> is set to put the feedback directory and its
+contents in the plug-in state directory. (For example, if
+C<$plugin_state_dir> is C</foo/blosxom/state> then the feedback
+directory C<$fb_dir> is set to C</foo/blosxom/state/feedback>.)
+However a better approach may be to keep the feedback directory at the
+same level as C<$datadir>. (For example, if C<$datadir> is
+C</foo/blosxom/data> then use C</foo/blosxom/feedback> for the
+feedback directory.)  This helps ensure that you don't accidentally
+delete previously-submitted comments and TrackBacks (e.g., if you
+clean out the plug-in state directory).
+
+Once C<$fb_dir> is set, the next time you visit your site the feedback
+plug-in will perform some checks, creating the directory C<$fb_dir>
+and setting appropriate permissions on the directory if it doesn't
+already exist.  (Check your web server error log for details of what's
+happening behind the scenes.)
+
+Set the variables C<$allow_comments> and C<$allow_trackbacks> to
+enable or disable comments and/or TrackBacks; by default the plug-in
+allows both comments and TrackBacks to be submitted. The variables
+C<$comment_period> and C<$trackback_period> specify the amount of time
+after a story is published (or updated) during which comments or
+TrackBacks may be submitted (90 days by default); set these variables
+to zero to allow submission of feedback at any time after publication.
+
+Set the variables C<$akismet_comments> and C<$akismet_trackbacks> to
+enable or disable checking of comments and/or TrackBacks against the
+Akismet spam blacklist service (C<http://www.akismet.com>). If Akismet
+checking is enabled then you must also set C<$wordpress_api_key> to
+your personal WordPress API key, which is required to connect to the
+Akismet service. (You can obtain a WordPress API key by registering
+for a free blog at C<http://www.wordpress.com>; as a side effect of
+registering you will get an API key that you can then use on any of
+your blogs, whether they're hosted at wordpress.com or not.)
+
+Set the variables C<$blacklist_comments> and C<$blacklist_trackbacks>
+to enable or disable checking of comments and/or TrackBacks against
+the MT-Blacklist file. If blacklist checking is enabled then you must
+also set C<$blacklist_file> to a valid value. (Note that in the past
+you could get a copy of the MT-Blacklist file from
+C<http://www.jayallen.org/comment_spam/blacklist.txt>; however that
+URL is no longer active and no one is currently maintaining the
+MT-Blacklist file. We are therefore deprecating use of the
+MT-Blacklist file, except for people who already have a copy of the
+file and are currently using it; we suggest using Akismet instead.)
+
+Set the variables C<$notify_comments> and C<$notify_trackbacks> to
+enable or disable sending an email message to you each time a new
+comment and/or TrackBack is submitted. If notification is enabled then
+you must set C<$address> and C<$smtp_server> to valid values.
+Typically you would set C<$address> to your own email address (e.g.,
+'jdoe@example.com') and C<$smtp_server> to the fully-qualified domain
+name of the SMTP server you normally use to send outbound mail from
+your email account (e.g., 'smtp.example.com').
+
+Set the variables C<$moderate_comments> and C<$moderate_trackbacks> to
+enable or disable moderation of comments and/or TrackBacks; moderation
+is done by sending you an email message with the submitted comment or
+TrackBack and links on which you can click to approve or reject the
+comment or TrackBack. If moderation is enabled then you must set
+C<$address> and C<$smtp_server> to valid values; see the discussion of
+notification above for more information.
+
+=head1 FLAVOUR TEMPLATE VARIABLES
+
+Unlike Rael Dornfest's original writeback plug-in, this plug-in does
+not require or assume that you will be using a special Blosxom flavour
+(e.g., the 'writeback' flavour) in order to display comments with
+stories. Instead you can display comments and/or TrackBacks with any
+flavour whatsoever (except the 'trackback' flavour, which is reserved
+for use with TrackBack pings). Also unlike the original writeback
+plug-in, this plug-in separates display of comments from display of
+TrackBacks and allows them to be formatted in different ways.
+
+Insert the variables C<$feedback::comments> and/or
+C<$feedback::trackbacks> into the story template for the flavour or
+flavours for which you wish comments and/or TrackBacks to be displayed
+(e.g., C<story.html>). Note that the plug-in will automatically set
+these variables to undefined values unless the page being displayed is
+for an individual story.
+
+Insert the variables C<$feedback::comments_count> and/or
+C<$feedback::trackbacks_count> into the story templates where you wish
+to display a count of the comments and/or TrackBacks for a particular
+story. Note that these variables are available on all pages, including
+index and archive pages. As an alternative you can use the variable
+C<$feedback::count> to display the combined total number of comments
+and TrackBacks (analogous to the variable C<$writeback::count> in the
+original writeback plug-in).
+
+Insert the variables C<$feedback::commentform> and
+C<$feedback::trackbackinfo> into your story or foot template for the
+flavour or flavours for which you want to enable submission of
+comments and/or TrackBacks (e.g., C<foot.html>);
+C<$feedback::commentform> is an HTML form for comment submission,
+while C<$feedback::trackbackinfo> displays the URL for TrackBack pings
+and also includes RDF code to support auto-discovery of the TrackBack
+ping URL. Note that the plug-in sets C<$feedback::commentform> and
+C<$feedback::trackbackinfo> to be undefined unless the page being
+displayed is for an individual story.
+
+The plug-in also sets C<$feedback::commentform> and/or
+C<$feedback::trackbackinfo> to be undefined if comments and/or
+TrackBacks have been disabled globally (i.e., using C<$allow_comments>
+or C<$allow_trackbacks>). However if comments or TrackBacks are closed
+because the story is older than the time set using C<$comment_period>
+or C<$trackback_period> then the plug-in sets C<$feedback::commentform>
+or C<$feedback::trackbackinfo> to display an appropriate message.
+
+Insert the variable C<$feedback::comment_response> into your story or
+foot template to display a message indicating the results of
+submitting or moderating a comment. Note that
+C<$feedback::comment_response> has an undefined value unless the
+displayed page is in response to a POST request containing a comment
+submission (i.e., using the 'Post' or 'Preview' buttons) or a GET
+request containing a moderator approval or rejection.
+
+Insert the variable C<$feedback::preview> into your story or foot
+template at the point at which you'd like a previewed comment to be
+displayed. Note that C<$feedback::preview> will be undefined except on
+an individual story page displayed in response to a comment submission
+using the 'Preview' button.
+
+=head1 COMMENT AND TRACKBACK TEMPLATES
+
+This plug-in uses a number of flavour templates to format comments and
+TrackBacks; the plug-in contains a full set of default templates for
+use with the 'html' flavour, as well as a full set of 'general'
+templates used as a default for other flavours. You can also supply
+your own comment and TrackBack templates in the same way that you can
+define other Blosxom templates, by putting appropriately-named
+template files into the Blosxom data directory (or one or more of its
+subdirectories, if you want different templates for different
+categories).
+
+The templates used for displaying comments and TrackBacks are
+analogous to the story template used for displaying stories; the
+templates are used for each and every comment or TrackBack displayed
+on a page:
+
+=over
+
+=item
+
+comment template (e.g., C<comment.html>). This template contains the
+content to be displayed for each comment (analogous to the writeback
+template used in the original writeback plug-in). Within this template
+you can use the variables C<$feedback::name> (name of the comment
+submitter), C<$feedback::url> (URL containing the comment submitter's
+email address or web site), C<$feedback::date> (date/time the comment
+was submitted), and C<$feedback::comment> (the comment itself). You
+can also use the variable C<$feedback::name_link>, which combines
+C<feedback::name> and C<$feedback::url> to create an (X)HTML link if
+the commenter supplied a URL, and otherwise is the same as
+C<$feedback::name>. Note that this template is also used for previewed
+comments.
+
+=item
+
+trackback template (e.g., C<trackback.html>). This template contains
+the content to be displayed for each TrackBack (analogous to the
+writeback template used in the original writeback plug-in). Within
+this template you can use the variables C<$feedback::blog_name> (name
+of the blog submitting the TrackBack), C<$feedback::title> (title of
+the blog post making the TrackBack), C<$feedback::url> (URL for the
+blog post making the TrackBack), C<$feedback::date> (date/time the
+TrackBack was submitted), and C<$feedback::excerpt> (an excerpt from
+the blog post making the TrackBack). You can also use the variable
+C<$feedback::title_link>, which combines C<$feedback::title> and
+C<$feedback::url> and is analogous to C<$feedback::name_link>.
+
+=back
+
+The feedback plug-in also uses the following templates:
+
+=over
+
+=item
+
+commentform template (e.g., C<commentform.html>). This template
+provides a form for submitting a comment. The default template
+contains a form containing fields for the submitter's name, email
+address or URL, and the comment itself; submitting the form initiates
+a POST request to the same URL (and Blosxom flavour) used in
+displaying the page on which the form appears. If you define your own
+commentform template note that the plug-in requires the presence of a
+'plugin' hidden form variable with the value set to 'writeback'; this
+tells the plug-in that it should handle the incoming data from the POST
+request rather than leaving it for another plug-in. Also note that in
+order to support both comment posting and previewing the form has two
+buttons, both with name 'submit' and with values 'Post' and 'Preview'
+respectively; if you change these names and values then you must
+change the plug-in's code.
+
+=item
+
+trackbackinfo template (e.g., C<trackbackinfo.html>). This template
+provides information for how to go about submitting a TrackBack. The
+default template provides both a displayed reference to the TrackBack
+ping URL and non-displayed RDF code by which other systems can
+auto-discover the TrackBack ping URL.
+
+=back
+
+=head1 SECURITY
+
+This plug-in has at least the following security-related issues, which
+we attempt to address as described:
+
+=over
+
+=item
+
+The plug-in handles POST and GET requests with included parameters of
+potentially arbitrary length. To help minimize the possibility of
+problems (e.g., buffer overruns) the plug-in truncates all parameters
+to a maximum length (currently 10,000 bytes).
+
+=item
+
+People can submit arbitrary content as part of a submitted comment or
+TrackBack ping, with that content then being displayed as part of the
+page viewed by other users. To help minimize the possibility of
+attacks involving injection of arbitrary page content, the plug-in
+"sanitizes" any submitted HTML/XHTML content by converting the '<'
+character and other problematic characters (including '>' and the
+double quote character) to the corresponding HTML/XHTML character
+entities. The plug-in also sanitizes submitted URLs by URL-encoding
+characters that are not permitted in a URL.
+
+=item
+
+When using moderation, comments or TrackBacks are approved (or
+rejected) by invoking a GET (or HEAD) request using the URL of the
+story to which the comment or TrackBack applies, with the URL having
+some additional parameters to signal whether the comment should be
+approved or rejected. Since the feedback plug-in does not track (much
+less validate) the source of the moderation request, in theory
+spammers could approve their own comments or TrackBacks simply by
+following up their feedback submission with a GET request of the
+proper form. To minimize the possibility of this happening we generate
+a random eight-character alphanumeric key for each submitted comment
+or TrackBack, and require that that key be supplied in the approval or
+rejection request. This provides reasonable protection assuming that a
+spammer is not intercepting and reading your personal email (since the
+key is included in the moderation email message).
+
+=back
+
+=head1 VERSION
+
+0.23
+
+=head1 AUTHOR
+
+This plug-in was created by Frank Hecker, hecker@hecker.org; it was
+based on and inspired by the original writeback plug-in by Rael
+Dornfest together with modifications made by Fletcher T. Penney, Doug
+Alcorn, Kevin Scaldeferri, and others.
+
+=head1 SEE ALSO
+
+More on the feedback plug-in: http://www.hecker.org/blosxom/feedback
+
+Blosxom Home/Docs/Licensing: http://www.blosxom.com/
+
+Blosxom Plug-in Docs: http://www.blosxom.com/plugin.shtml
+
+=head1 BUGS
+
+Address bug reports and comments to the Blosxom mailing list
+[http://www.yahoogroups.com/group/blosxom].
+
+=head1 LICENSE
+
+The feedback plug-in
+Copyright 2003-2006 Frank Hecker, Rael Dornfest, Fletcher T. Penney,
+                    Doug Alcorn, Kevin Scaldeferri, and others
+
+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.
diff --git a/general/lastmodified2 b/general/lastmodified2
new file mode 100644 (file)
index 0000000..995a4a9
--- /dev/null
@@ -0,0 +1,611 @@
+# Blosxom Plugin: lastmodified2
+# Author(s): Frank Hecker <hecker@hecker.org>
+# (based on work by Bob Schumaker <cobblers@pobox.com>)
+# Version: 0.10
+# Documentation: See the bottom of this file or type: perldoc lastmodified2
+
+package lastmodified2;
+
+use strict;
+
+use HTTP::Date;
+use Data::Dumper;
+use POSIX qw! strftime !;
+
+# Use the Digest:MD5 module if available, the older MD5 module if not.
+
+my $use_digest;
+my $use_just_md5;
+
+BEGIN {
+    if (eval "require Digest::MD5") {
+       Digest::MD5->import();
+       $use_digest = 1;
+    }
+    elsif (eval "require MD5") {
+       MD5->import();
+       $use_just_md5 = 1;
+    }
+}
+
+# --- Package variables -----
+
+my $current_time = time();              # Use consistent value of current time.
+my $last_modified_time = 0;
+my $etag = "";
+my $md5_digest = "";
+my %validator;
+
+# --- Output variables -----
+
+our $latest_rfc822 = '';
+our $latest_iso8601 = '';
+
+our $others_rfc822 = '';
+our $others_iso8601 = '';
+
+our $now_rfc822 = '';
+our $now_iso8601 = '';
+
+our $story_rfc822 = '';
+our $story_iso8601 = '';
+
+# --- Configurable variables -----
+
+my $generate_etag = 1;                  # generate ETag header?
+
+my $generate_mod = 1;                   # generate Last-modified header?
+
+my $strong = 0;                         # do strong validation?
+
+my $val_cache = "validator.cache";      # where to cache last-modified values
+                                       # and MD5 digests (in state directory)
+
+my $generate_expires = 0;               # generate Expires header?
+
+my $generate_cache = 0;                 # generate Cache-control header?
+
+my $freshness_time = 3000;              # number of seconds pages are fresh
+                                        # (0 = do not cache, max is 1 year)
+
+my $generate_length = 1;                # generate Content-length header?
+
+my $use_others = 0;                     # consult %others for weak validation
+                                        # (DEPRECATED)
+
+my $export_dates = 1;                   # set $latest_rfc822, etc., for
+                                        # compatibility with lastmodified
+
+my $debug = 0;                          # set > 0 for debug output
+
+# --------------------------------
+
+
+# Do any initial processing, and decide whether to activate the plugin.
+
+sub start {
+    warn "lastmodified2: start\n" if $debug > 1;
+
+    # Don't activate this plugin if we are doing static page generation.
+
+    return 0 if $blosxom::static_or_dynamic eq 'static';
+
+    # If we can't do MD5 then we don't do strong validation.
+
+    if ($strong && !($use_digest || $use_just_md5)) {
+       $strong = 0;
+
+       warn "lastmodified2: MD5 not available, forcing weak validation\n"
+           if $debug > 0;
+    }
+
+    # Limit freshness time to maximum of one year, must be non-negative.
+
+    $freshness_time > 365*24*3600 and $freshness_time = 365*24*3600;
+    $freshness_time < 0 and $freshness_time = 0;
+
+    if ($debug > 1) {
+       warn "lastmodified2: \$generate_etag = $generate_etag\n"; 
+       warn "lastmodified2: \$generate_mod = $generate_mod\n"; 
+       warn "lastmodified2: \$strong = $strong\n"; 
+       warn "lastmodified2: \$generate_cache = $generate_cache\n"; 
+       warn "lastmodified2: \$generate_expires = $generate_expires\n"; 
+       warn "lastmodified2: \$freshness_time = $freshness_time\n"; 
+       warn "lastmodified2: \$generate_length = $generate_length\n"; 
+    }
+
+    # If we are using Last-modified as a strong validator then read
+    # in the cached last-modified values and MD5 digests.
+
+    if ($generate_mod && $strong &&
+       open CACHE, "<$blosxom::plugin_state_dir/$val_cache" ) {
+
+       warn "lastmodified2: loading cached validators\n" if $debug > 0;
+
+       my $index = join '', <CACHE>;
+       close CACHE;
+
+       my $VAR1;
+       $index =~ m!\$VAR1 = \{!
+           and eval($index) and !$@ and %validator = %$VAR1;
+    }
+
+    # Convert current time to RFC 822 and ISO 8601 formats for others' use.
+
+    if ($export_dates && $current_time) {
+       $now_rfc822 = HTTP::Date::time2str($current_time);
+       $now_iso8601 = iso8601($current_time);
+    }
+
+    return 1;
+}
+
+
+# We check the list of entries to be displayed and determine the modification
+# time of the most recent entry.
+
+sub filter {
+    my ($pkg, $files, $others) = @_;
+
+    warn "lastmodified2: filter\n" if $debug > 1;
+
+    # We can skip all this unless we're doing weak validation and/or we're
+    # setting the *_rfc822 and *_iso8601 variables for others to use.
+
+    return 1 unless $export_dates ||
+       (($generate_etag || $generate_mod) && !$strong);
+
+    # Find the latest date/time modified for the entries to be displayed.
+
+    $last_modified_time = 0;
+    for (values %$files) {
+       $_ > $last_modified_time and $last_modified_time = $_;
+    }
+
+    warn "lastmodified2: \$last_modified_time = " .
+       $last_modified_time . " (entries)\n" if $debug > 0;
+
+    # Convert last modified time to RFC 822 and ISO 8601 formats for others.
+
+    if ($export_dates && $last_modified_time) {
+       $latest_rfc822 = HTTP::Date::time2str($last_modified_time);
+       $latest_iso8601 = iso8601($last_modified_time);
+    }
+
+    # Optionally look at other files as well (DEPRECATED).
+
+    if ($use_others) {
+       my $others_last_modified_time = 0;
+       for (values %$others) {
+           $_ > $others_last_modified_time
+               and $others_last_modified_time = $_;
+       }
+
+       if ($export_dates && $others_last_modified_time) {
+           $others_rfc822 = HTTP::Date::time2str($others_last_modified_time);
+           $others_iso8601 = iso8601($others_last_modified_time);
+       }
+
+       warn "lastmodified2: \$others_last_modified_time = " .
+           $others_last_modified_time . " (others)\n" if $debug > 0;
+
+       $others_last_modified_time > $last_modified_time
+           and $last_modified_time = $others_last_modified_time;
+    }
+
+    # If we're doing weak validation then create an etag based on the latest
+    # date/time modified and mark it as weak (i.e., by prefixing it with 'W/').
+
+    if ($generate_etag && !$strong) {
+       $etag = 'W/"' . $last_modified_time . '"';
+
+       warn "lastmodified2: \$etag = $etag\n" if $debug > 0;
+    }
+
+    return 1;
+}
+
+
+# Skip story processing and generate configured headers now on a conditional
+# GET request for which we don't need to return a full response.
+
+sub skip {
+    warn "lastmodified2: skip\n" if $debug > 1;
+
+    # If we are doing strong validation then we can't skip story processing
+    # because we need all output in order to generate the proper etag and/or
+    # last-modified value.
+
+    return 0 unless ($generate_etag || $generate_mod) && !$strong;
+
+    # Otherwise we can check here whether we can send a 304 or not.
+
+    my $send_304 = check_for_304();
+
+    # If we don't need to return a full response on a conditional GET then
+    # set the HTTP status to 304 and generate headers as configured.
+    # (We have to do this here because the last subroutine won't be executed
+    # if we skip story processing.)
+
+    add_headers($send_304) if $send_304;
+
+    return $send_304;
+}
+
+
+# Set variables with story date/time in RFC 822 and ISO 8601 formats.
+
+sub story {
+    my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
+
+    warn "lastmodified2: story (\$path = $path, \$filename = $filename)\n"
+       if $debug > 1;
+
+    if ($export_dates) {
+       $path ||= "";
+
+       my $timestamp =
+           $blosxom::files{"$blosxom::datadir$path/$filename.$blosxom::file_extension"};
+
+       warn "lastmodified2: \$timestamp = $timestamp\n" if $debug > 0;
+
+       $story_rfc822 = $timestamp ? HTTP::Date::time2str($timestamp) : '';
+       $story_iso8601 = $timestamp ? iso8601($timestamp) : '';
+    }
+
+    return 1;
+}
+
+
+# Do conditional GET checks if we couldn't do them before (i.e., we are
+# doing strong validation and couldn't skip story processing) and output
+# any configured headers plus a 304 status if appropriate.
+
+sub last {
+    warn "lastmodified2: last\n" if $debug > 1;
+
+    # If some other plugin has set the HTTP status to a non-OK value then we
+    # don't attempt to do anything here, since it would probably be wrong.
+
+    return 1 if $blosxom::header->{'Status'} &&
+       $blosxom::header->{'Status'} !~ m!^200 !;
+
+    # If we are using ETag and/or Last-modified as a strong validator then
+    # we generate an entity tag from the MD5 message digest of the complete
+    # output. (We use the base-64 representation if possible because it is
+    # more compact than hex and hence saves a few bytes of bandwidth.)
+
+    if (($generate_etag || $generate_mod) && $strong) {
+       $md5_digest =
+           $use_digest ? Digest::MD5::md5_base64($blosxom::output)
+                       : MD5->hex_hash($blosxom::output);
+       $etag = '"' . $md5_digest . '"';
+
+       warn "lastmodified2: \$etag = $etag\n" if $debug > 0;
+    }
+
+    # If we are using Last-modified as a strong validator then we look up
+    # the cached MD5 digest for this URI, compare it to the current digest,
+    # and use the cached last-modified value if they match. Otherwise we set
+    # the last-modified value to just prior to the current time.
+
+    my $cache_tag = cache_tag();
+    my $update_cache = 0;
+
+    if ($generate_mod && $strong) {
+       if ($validator{$cache_tag} &&
+           $md5_digest eq $validator{$cache_tag}{'md5'}) {
+           $last_modified_time = $validator{$cache_tag}{'last-modified'};
+       } else {
+           $last_modified_time = $current_time - 5;
+           $validator{$cache_tag}{'last-modified'} = $last_modified_time;
+           $validator{$cache_tag}{'md5'} = $md5_digest;
+           $update_cache = 1;
+       }
+
+       warn "lastmodified2: \$last_modified_time = $last_modified_time\n"
+           if $debug > 0;
+
+    }
+
+    # Do conditional GET checks and output configured headers plus status.
+
+    my $send_304 = check_for_304();
+    add_headers($send_304);
+
+    # Update the validator cache if we need to. To minimize race conditions
+    # we write the cache as a temporary file and then rename it.
+
+    if ($update_cache) {
+       warn "lastmodified2: updating validator cache\n" if $debug > 0;
+
+       my $tmp_cache = "$val_cache-$$-$current_time";
+
+       if (open CACHE, ">$blosxom::plugin_state_dir/$tmp_cache") {
+           print CACHE Dumper \%validator;
+           close CACHE;
+
+           warn "lastmodified2: renaming $tmp_cache to $val_cache\n"
+               if $debug > 1;
+
+           rename("$blosxom::plugin_state_dir/$tmp_cache",
+                  "$blosxom::plugin_state_dir/$val_cache")
+               or warn "couldn't rename $blosxom::plugin_state_dir/$tmp_cache: $!\n";
+       } else {
+           warn "couldn't > $blosxom::plugin_state_dir/$tmp_cache: $!\n";
+       }
+    }
+
+    1;
+}
+
+
+# Check If-none-match and/or If-modified-since headers and return true if
+# we can send a 304 (not modified) response instead of a normal response.
+
+sub check_for_304 {
+    my $etag_send_304 = 0;
+    my $mod_send_304 = 0;
+    my $etag_request = 0;
+    my $mod_request = 0;
+    my $send_304 = 0;
+
+    warn "lastmodified2: check_for_304\n" if $debug > 1;
+
+    # For a conditional GET using the If-none-match header, compare the
+    # ETag value(s) in the header with the ETag value generated for the page,
+    # set $etag_send_304 true if we don't need to send a full response,
+    # and note that an etag value was included in the request.
+
+    if ($ENV{'HTTP_IF_NONE_MATCH'}) {
+       $etag_request = 1;
+       if ($generate_etag) {
+           my @inm_etags = split '\s*,\s*', $ENV{'HTTP_IF_NONE_MATCH'};
+
+           if ($debug > 0) {
+               for (@inm_etags) {
+                   warn "lastmodified2: \$inm_etag = |" . $_ . "|\n";
+               }
+           }
+
+           for (@inm_etags) {
+               $etag eq $_ and $etag_send_304 = 1 and last;
+           }
+       }
+    }
+
+    # For a conditional GET using the If-modified-since header, compare the
+    # time in the header with the time any entry on the page was last modified,
+    # set $mod_send_304 true if we don't need to send a full response, and
+    # also note that a last-modified value was included in the request.
+
+    if ($ENV{'HTTP_IF_MODIFIED_SINCE'}) {
+       $mod_request = 1;
+       if ($generate_mod) {
+           my $ims_time =
+               HTTP::Date::str2time($ENV{'HTTP_IF_MODIFIED_SINCE'});
+
+           warn "lastmodified2: \$ims_time = " . $ims_time . "\n"
+               if $debug > 0;
+
+           $mod_send_304 = 1 if $last_modified_time <= $ims_time;
+       }
+    }
+
+    # If the request includes both If-none-match and If-modified-since then
+    # we don't send a 304 response unless both tests agree it should be sent,
+    # per section 13.3.4 of the HTTP 1.1 specification.
+
+    if ($etag_request && $mod_request) {
+       $send_304 = $etag_send_304 && $mod_send_304;
+    } else {
+       $send_304 = $etag_send_304 || $mod_send_304;
+    }
+
+    warn "lastmodified2: \$send_304 = " . $send_304 .
+           " \$etag_send_304 = " . $etag_send_304 .
+           " \$mod_send_304 = " . $mod_send_304 . "\n"
+       if $debug > 0;
+
+    return $send_304;
+}
+
+
+# Set status and add additional header(s) depending on the type of response.
+
+sub add_headers {
+    my ($send_304) = @_;
+
+    warn "lastmodified2: add_headers (\$send_304 = $send_304)\n"
+       if $debug > 1;
+
+    # Set HTTP status and truncate output if we are sending a 304 response.
+
+    if ($send_304) {
+       $blosxom::header->{'Status'} = "304 Not Modified";
+       $blosxom::output = "";
+
+       warn "lastmodified2: Status: " .
+           $blosxom::header->{'Status'} . "\n" if $debug > 0;
+    }
+
+    # For the rules on what headers to generate for a 304 response, see
+    # section 10.3.5 of the HTTP 1.1 protocol specification.
+
+    # Last-modified is not returned on a 304 response.
+
+    if ($generate_mod && !$send_304) {
+       $blosxom::header->{'Last-modified'} =
+           HTTP::Date::time2str($last_modified_time);
+
+       warn "lastmodified2: Last-modified: " .
+           $blosxom::header->{'Last-modified'} . "\n" if $debug > 0;
+    }
+
+    # If we send ETag on a 200 response then we send it on a 304 as well.
+
+    if ($generate_etag) {
+       $blosxom::header->{'ETag'} = $etag;
+
+       warn "lastmodified2: ETag: " .
+           $blosxom::header->{'ETag'} . "\n" if $debug > 0;
+    }
+
+    # We send Expires for a 304 since its value is updated for each request.
+
+    if ($generate_expires) {
+       $blosxom::header->{'Expires'} = $freshness_time ?
+           HTTP::Date::time2str($current_time + $freshness_time) :
+           HTTP::Date::time2str($current_time - 60);
+
+       warn "lastmodified2: Expires: " .
+           $blosxom::header->{'Expires'} . "\n" if $debug > 0;
+    }
+
+    # We send Cache-control for a 304 response for consistency with Expires.
+
+    if ($generate_cache) {
+       $blosxom::header->{'Cache-control'} =
+           $freshness_time ? "max-age=" . $freshness_time
+                           : "no-cache";
+
+       warn "lastmodified2: Cache-control: " .
+           $blosxom::header->{'Cache-control'} . "\n" if $debug > 0;
+    }
+
+    # Content-length is not returned on a 304 response.
+
+    if ($generate_length && !$send_304) {
+       $blosxom::header->{'Content-length'} = length($blosxom::output);
+
+       warn "lastmodified2: Content-length: " .
+           $blosxom::header->{'Content-length'} . "\n" if $debug > 0;
+    }
+}
+
+
+# Generate a tag to look up the cached last-modified value and MD5 digest
+# for this URI.
+
+sub cache_tag {
+    # Start with the original URI from the request.
+
+    my $tag = $ENV{REQUEST_URI} || "";
+
+    # Add an "/index.flavour" for uniqueness unless it's already present.
+
+    unless ($tag =~ m!/index\.!) {
+       $tag .= '/' unless ($tag =~ m!/$!);
+       $tag .= "index.$blosxom::flavour";
+    }
+
+    return $tag;
+}
+
+
+# Convert time to ISO 8601 format (including time zone offset).
+# (Format is YYYY-MM-DDThh:mm:ssTZD per http://www.w3.org/TR/NOTE-datetime)
+
+sub iso8601 {
+    my ($timestamp) = @_;
+    my $tz_offset = strftime("%z", localtime());
+    $tz_offset = substr($tz_offset, 0, 3) . ":" . substr($tz_offset, 3, 5);
+    return strftime("%Y-%m-%dT%T", localtime($timestamp)) . $tz_offset;
+}
+
+
+1;
+
+__END__
+
+=head1 NAME
+
+Blosxom Plug-in: lastmodified2
+
+=head1 SYNOPSIS
+
+Enables caching and validation of dynamically-generated Blosxom pages
+by generating C<ETag>, C<Last-modified>, C<Cache-control>, and/or
+C<Expires> HTTP headers in the response and responding appropriately
+to an C<If-none-match> and/or C<If-modified-since> header in the
+request. Also generates a C<Content-length> header to support HTTP 1.0
+persistent connections.
+
+=head1 VERSION
+
+0.10
+
+=head1 AUTHOR
+
+Frank Hecker <hecker@hecker.org>, http://www.hecker.org/ (based on
+work by Bob Schumaker, <cobblers@pobox.com>, http://www.cobblers.net/blog/)
+
+=head1 DESCRIPTION
+
+This plugin enables caching and validation of dynamically-generated
+Blosxom pages by web browsers, web proxies, feed aggregators, and
+other clients by generating various cache-related HTTP headers in the
+response and supporting conditional GET requests, as described
+below. This can reduce excess network traffic and server load caused
+by requests for RSS or Atom feeds or for web pages for popular entries
+or categories.
+
+=head1 INSTALLATION AND CONFIGURATION
+
+Copy this plugin into your Blosxom plugin directory. You should not
+normally need to rename the plugin; however see the discussion below.
+
+Configurable variables specify how the plugin handles validation
+(C<$generate_etag>, C<$generate_mod>, and C<$strong>), caching
+(C<$generate_cache>, C<$generate_expires>, and C<$freshness_time>) and
+whether or not to generate any other recommended headers
+(C<$generate_length>). The plugin supports the variable C<$use_others>
+as used in the lastmodified plugin; however use of this is deprecated
+(use strong validation instead). The variable C<$export_dates>
+specifies whether to export date/time variables C<$latest_rfc822>,
+etc., for compatibility with the lastmodified plugin.
+
+You can set the variable C<$debug> to 1 or greater to produce
+additional information useful in debugging the operation of the
+plugin; the debug output is sent to your web server's error log.
+
+This plugin supplies C<filter>, C<skip>, and C<last> subroutines. It
+needs to run after any other plugin whose C<filter> subroutine changes
+the list of entries included in the response; otherwise the
+C<Last-modified> date may be computed incorrectly. It needs to run
+after any other plugin whose C<skip> subroutine does redirection
+(e.g., the canonicaluri plugin) or otherwise conditionally sets the
+HTTP status to any value other than 200. Finally, this plugin needs to
+run after any other plugin whose C<last> subroutine changes the output
+for the page; otherwise the C<Content-length> value (and the C<ETag>
+and C<Last-modified> values, if you are using strong validation) may
+be computed incorrectly. If you are encountering problems in any of
+these regards then you can force the plugin to run after other plugins
+by renaming it to, e.g., 99lastmodified2.
+
+=head1 SEE ALSO
+
+Blosxom Home/Docs/Licensing: http://www.blosxom.com/
+
+Blosxom Plugin Docs: http://www.blosxom.com/documentation/users/plugins.html
+
+lastmodified plugin: http://www.cobblers.net/blog/dev/blosxom/
+
+more on the lastmodified2 plugin: http://www.hecker.org/blosxom/lastmodified2
+
+=head1 AUTHOR
+
+Frank Hecker <hecker@hecker.org> http://www.hecker.org/
+
+Based on the original lastmodified plugin by Bob Schumaker
+<cobblers@pobox.com> http://www.cobblers.net/blog
+
+=head1 LICENSE
+
+This source code is submitted to the public domain.  Feel free to use
+and modify it.  If you like, a comment in your modified source
+attributing credit to myself, Bob Schumaker, and any other
+contributors for our work would be appreciated.
+
+THIS SOFTWARE IS PROVIDED AS IS AND WITHOUT ANY WARRANTY OF ANY KIND.
+USE AT YOUR OWN RISK!
diff --git a/general/noslashredir b/general/noslashredir
new file mode 100644 (file)
index 0000000..9c0f9ad
--- /dev/null
@@ -0,0 +1,173 @@
+# Blosxom Plugin: noslashredir
+# Author(s): Frank Hecker <hecker@hecker.org>
+# Version: 0.1
+# Documentation: See the bottom of this file or type: perldoc noslashredir
+
+package noslashredir;
+
+use strict;
+
+use CGI qw/:standard :netscape/; 
+
+# --- Configurable variables -----
+
+my $debug = 0;                          # set to 1 to print debug messages
+
+# --------------------------------
+
+use vars qw!$redirecting!;              # 1 if we are redirecting, 0 if not
+
+sub start {
+    $redirecting = 0;
+    1;
+}
+
+sub filter {
+    my ($pkg, $files_ref) = @_;
+
+    warn "noslashredir: \$path_info: '" . $blosxom::path_info . "'\n"
+        if $debug > 0;
+    warn "noslashredir: path_info(): '" . path_info() . "'\n"
+        if $debug > 0;
+
+    # If the requested URI does not include a file extension (thus, it
+    # represents the blog root, a category, or a date-based archive page)
+    # and the requested URI does not end in a slash ('/') then add a slash
+    # to the URI and force a redirect.
+    #
+    # Note 1: We use $blosxom::path_info to check for the presence of a
+    # file extension because it may not have been present in the original
+    # URI but rather might have been added by the extensionless plugin.
+    #
+    # Note 2: We use path_info() to check for the presence of a trailing
+    # slash because the blosxom.cgi code strips trailing slashes from
+    # $blosxom::path_info and does not include the date components.
+
+    if ($blosxom::path_info !~ m!\.! && path_info() !~ m!/$!) {
+       my $uri = "$blosxom::url" . path_info() . "/";
+       $uri .= "?" . $ENV{QUERY_STRING} if $ENV{QUERY_STRING};
+
+       warn "noslashredir: redirecting to '$uri'\n"
+           if $debug > 0;
+
+       my $message =
+           qq!Trailing slash omitted, redirecting to <a href="$uri">$uri</a>.\n!;
+       $blosxom::output = $message;
+       print "Status: 301\n";
+       print "Location: $uri\n";
+       $redirecting = 1;
+       }
+
+    1;
+}
+
+sub skip {
+    return $redirecting;   # skip story generation if redirecting
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Blosxom plugin: noslashredir
+
+=head1 SYNOPSIS
+
+Have Blosxom force a redirect if a non-entry URI does not have a trailing
+slash.
+
+=head1 VERSION
+
+0.1
+
+=head1 AUTHOR
+
+Frank Hecker <hecker@hecker.org>, http://www.hecker.org/
+
+=head1 DESCRIPTION
+
+This plugin checks to see if the requested URI corresponds to a
+category or date-based archive page and forces a redirect if the URI
+omits a trailing slash; it assumes that you are using URI rewriting
+rules to hide use of "/cgi-bin/blosxom.cgi". (Otherwise using the
+plugin wouldn't make much sense.)
+
+For example, if you request the URI
+
+  http://www.example.com/blog/foo
+
+where "foo" is a category, this plugin will force a redirect to the
+URI
+
+  http://www.example.com/blog/foo/
+
+This plugin was inspired by the redirect plugin by Fletcher T. Penny
+http://www.blosxom.com/plugins/general/redirect.htm and adapts a bit
+of its code.
+
+=head1 INSTALLATION AND CONFIGURATION
+
+Copy this plugin into your Blosxom plugin directory. You do not
+normally need to rename the plugin; however see the discussion below.
+
+You can change the value of the variable C<$debug> to 1 if you need to
+debug the plugin's operation; the plugin will print to the web
+server's error log the original path component of the URI and the new
+URI if redirection is to be done.
+
+This plugin supplies only a filter and skip subroutine and can
+normally coexist with other plugins with filter subroutines. However
+this plugin needs to be run after the extensionless plugin, since it
+needs the file extension to distinguish between individual entry pages
+and other pages.
+
+Finally, note that this plugin is sensitive to the exact URI rewriting
+rules you might have configured (e.g., in an Apache configuration file
+or in a .htaccess file). In particular, when rewriting URIs to add the
+name of the Blosxom CGI script (e.g., "/cgi-bin/blosxom.cgi") you need
+to ensure that such rules preserve any trailing slash on the end of
+the URI and pass it on to Blosxom.
+
+=head1 SEE ALSO
+
+Blosxom Home/Docs/Licensing: http://www.blosxom.com/
+
+Blosxom Plugin Docs: http://www.blosxom.com/documentation/users/plugins.html
+
+=head1 BUGS
+
+At present the plugin forces a redirect for a date-based URI that
+specifies a particular day, for example
+
+  http://www.example.com/blog/2004/10/25
+
+This is arguably incorrect, since (unlike URIs specifying only the
+year or only the year and month) such a URI is not analogous to a
+directory.
+
+Address other bug reports and comments to the Blosxom mailing list:
+http://www.yahoogroups.com/group/blosxom
+
+=head1 LICENSE
+
+noslashredir Blosxom plugin Copyright 2004 Frank Hecker
+
+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.
diff --git a/general/slashredir b/general/slashredir
new file mode 100644 (file)
index 0000000..ab2e659
--- /dev/null
@@ -0,0 +1,224 @@
+# Blosxom Plugin: slashredir
+# Author(s): Frank Hecker <hecker@hecker.org>
+# Version: 0.4
+# Documentation: See the bottom of this file or type: perldoc slashredir
+
+package slashredir;
+
+use strict;
+
+use CGI qw/:standard :netscape/; 
+
+
+# --- Configurable variables -----
+
+my $debug = 1;                          # set to 1 to print debug messages
+
+# --------------------------------
+
+
+use vars qw!$redirecting!;              # 1 if we are redirecting, 0 if not
+
+
+sub start {
+    warn "slashredir: start\n" if $debug > 1;
+
+    $redirecting = 0;
+
+    # Activate this plugin only when doing dynamic page generation.
+    return $blosxom::static_or_dynamic eq 'dynamic' ? 1 : 0;
+}
+
+
+sub filter {
+    my ($pkg, $files_ref) = @_;
+
+    warn "slashredir: filter\n" if $debug > 1;
+
+    warn "slashredir: \$path_info: '" . $blosxom::path_info . "'\n"
+        if $debug > 0;
+    warn "slashredir: path_info(): '" . path_info() . "'\n"
+        if $debug > 0;
+
+    # We need a copy of the original PATH_INFO, prior to Blosxom having
+    # parsed it, because we need to see the trailing slashes stripped by
+    # Blosxom and also want the full path including date information.
+
+    my $path = path_info();
+
+    # We check to see if the URI is for an entry page and redirect if the URI
+    # has a trailing slash. Otherwise we check to see if a trailing slash
+    #  needs to be added or removed and redirect if needed.
+    #
+    # Note: We use $blosxom::path_info to check for the presence of a
+    # file extension because the extension may not have been present in the
+    # original URI but might have been added by the extensionless plugin.
+    # (That also implies that this plugin must run after extensionless runs.)
+
+    if ($blosxom::path_info =~ m!\.!) {
+       if ($path =~ m!/$!) {
+           $path =~ s!/+$!!;           # strip *all* trailing slashes
+           redirect($path, "Trailing slash(es) not needed");
+       }
+    } elsif ($path !~ m!/$!) {
+       $path .= '/';                   # add one trailing slash
+       redirect($path, "Adding trailing slash");
+    } elsif ($path =~ m!//+$!) {
+       $path =~ s!//+$!/!;             # remove redundant slash(es)
+       redirect($path, "Removing redundant trailing slash(es)");
+    }
+    1;
+}
+
+
+sub skip {
+    warn "slashredir: skip\n" if $debug > 1;
+
+    return $redirecting;                # skip story generation if redirecting
+}
+
+
+sub redirect {
+    my ($path, $error_msg) = @_;
+
+    my $uri = "$blosxom::url$path";
+    $uri .= "?" . $ENV{QUERY_STRING} if $ENV{QUERY_STRING};
+
+    warn "slashredir: redirecting to '$uri', '$error_msg'\n"
+       if $debug > 0;
+
+    my $redir_msg = qq!, redirecting to <a href="$uri">$uri</a>.\n!;
+    $blosxom::output = $error_msg . $redir_msg;
+    print "Status: 301\n";
+    print "Location: $uri\n";
+    $redirecting = 1;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Blosxom plugin: slashredir
+
+=head1 SYNOPSIS
+
+Have Blosxom force a redirect if a URI is not in canonical form with respect
+to trailing slashes.
+
+=head1 VERSION
+
+0.4
+
+=head1 AUTHOR
+
+Frank Hecker <hecker@hecker.org>, http://www.hecker.org/
+
+=head1 DESCRIPTION
+
+This plugin checks to see whether the requested URI has one or more
+trailing slashes and if necessary does a browser redirect to the
+canonical form of the URI. More specifically, URIs for the blog root,
+categories, and date-based archives should have one (and only one)
+trailing slash, while URIs for individual entry pages should not have
+a trailing slash.
+
+For example, if you request the URI
+
+  http://www.example.com/blog/foo
+
+where "foo" is a category, this plugin will force a redirect to the
+URI
+
+  http://www.example.com/blog/foo/
+
+This plugin essentially causes Blosxom to emulate the default behavior
+of the Apache web server. The underlying idea is that URIs for Blosxom
+pages should have trailing slashes if and only if there are (or could
+be) other URIs for other pages "underneath" the URI in question.
+
+Thus an individual entry page with a URI of the form
+".../entry.flavour" (or ".../entry" if using the extensionless plugin)
+should not have a trailing slash, because URIs of the form
+".../entry.html/foo" (or ".../entry/foo" with extensionless) don't and
+won't return anything meaningful. However a category page with a URI
+of the form ".../category" could refer to additional pages with URIs
+of the form ".../category/foo.html (or ".../category/foo/entry"), and
+hence the canocical form of the category's URI should be
+".../category/".
+
+Similarly, date-based archive pages with URIs like ".../2004" or
+".../2004/10" could have subsidiary URIs for months or days
+respectively; while days are not subdivided further, per-day archive
+pages could be requested in non-default flavours, e.g.,
+".../2004/10/14/index.rss" (as could per-year and per-month pages as
+well, of course). Hence for date-based archive pages the canonical
+form of the URI should also include a trailing slash if the default
+flavour is being requested.
+
+Note that using this plugin makes most sense if you are also using URI
+rewriting rules to hide use of "/cgi-bin/blosxom.cgi" and support URIs
+similar to those traditionally used to access normal directories and
+files. This plugin can be used in conjunction with the extensionless
+plugin, but does not assume or require its use. (See also below.)
+
+This plugin was inspired by the redirect plugin by Fletcher T. Penny
+http://www.blosxom.com/plugins/general/redirect.htm and adapts a bit
+of its code.
+
+=head1 INSTALLATION AND CONFIGURATION
+
+Copy this plugin into your Blosxom plugin directory. You do not
+normally need to rename the plugin; however see the discussion below.
+
+You can change the value of the variable C<$debug> to 1 if you need to
+debug the plugin's operation; the plugin will print to the web
+server's error log the original path component of the URI and the new
+URI if redirection is to be done.
+
+This plugin supplies a filter and skip subroutine and can normally
+coexist with other plugins with filter subroutines. However this
+plugin needs to be run after the extensionless plugin, since it needs
+the file extension (provided by extensionless if not already present)
+to distinguish between individual entry pages and other pages.
+
+Finally, note that this plugin is sensitive to the exact URI rewriting
+rules you might have configured (e.g., in an Apache configuration file
+or in a .htaccess file). In particular, when rewriting URIs to add the
+name of the Blosxom CGI script (e.g., "/cgi-bin/blosxom.cgi") you need
+to ensure that such rules preserve any trailing slash on the end of
+the URI and pass it on to Blosxom.
+
+=head1 SEE ALSO
+
+Blosxom Home/Docs/Licensing: http://www.blosxom.com/
+
+Blosxom Plugin Docs: http://www.blosxom.com/documentation/users/plugins.html
+
+=head1 BUGS
+
+Address other bug reports and comments to the Blosxom mailing list:
+http://www.yahoogroups.com/group/blosxom
+
+=head1 LICENSE
+
+slashredir Blosxom plugin Copyright 2004 Frank Hecker
+
+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.