From: Gavin Carr
Date: Fri, 31 Aug 2007 01:47:10 +0000 (+0000)
Subject: Add Frank Hecker plugins to general.
X-Git-Url: https://git.stderr.nl/gitweb?a=commitdiff_plain;h=fca3198611f04b221a988c7bf2df19cb4a08402b;p=matthijs%2Fupstream%2Fblosxom-plugins.git
Add Frank Hecker plugins to general.
---
diff --git a/general/canonicaluri b/general/canonicaluri
new file mode 100644
index 0000000..f15a012
--- /dev/null
+++ b/general/canonicaluri
@@ -0,0 +1,264 @@
+# Blosxom Plugin: canonicaluri
+# Author(s): Frank Hecker
+# 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 $uri.\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 , 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 function to get the URI path,
+and the C 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
index 0000000..33df8dc
--- /dev/null
+++ b/general/extensionless
@@ -0,0 +1,279 @@
+# Blosxom Plugin: extensionless
+# Author(s): Frank Hecker
+# 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 , 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 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 (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 in preference to displaying
+a file C.)
+
+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 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 exists in the Blosxom data directory
+(and does not conflict with a category C) 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. 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 and attempted to parse C<123bar> and C 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
index 0000000..8989659
--- /dev/null
+++ b/general/feedback
@@ -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 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 .
+$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 () {
+ 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 =
+ "";
+ } 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 =
+ ""
+ . "Trackbacks are closed for this story.
";
+ } 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 ? "$name" : $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 = '';
+ } else {
+ $response = '';
+ }
+
+ 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 .
+ # 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 = ""
+ . "0";
+ } else {
+ $response = ""
+ . "1"
+ . "$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 = '' . $comment . '
';
+ $comment =~ s!\r\r!
!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 = "\"$title\"";
+ } else {
+ $title = $default_title;
+ $title_link = "$title";
+ }
+
+ 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 = '
' . $excerpt . '
';
+ $excerpt =~ s!\r\r!!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*\#/ } ;
+ 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
+html trackback \n$feedback::blog_name mentioned this post in $feedback::title_link$feedback::excerpt eq="">.
?>$feedback::excerpt ne="">:\n
$feedback::excerpt
?>
+html commentform \n
+html trackbackinfo URL for TrackBack pings: $blosxom::url$blosxom::path/$blosxom::fn.$feedback::trackback_flavour
\n
+general comment \n
+general trackback \n$feedback::blog_name mentioned this post in $feedback::title_link$feedback::excerpt eq="">.
?>$feedback::excerpt ne="">:\n
$feedback::excerpt
?>
+general commentform \n
+general trackbackinfo URL for TrackBack pings: $blosxom::url$blosxom::path/$blosxom::fn.$feedback::trackback_flavour
\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); 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), 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 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 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 then the feedback
+directory C<$fb_dir> is set to C.)
+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 then use C 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). 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; 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; 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). 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);
+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). 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 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). 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). 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). 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
index 0000000..995a4a9
--- /dev/null
+++ b/general/lastmodified2
@@ -0,0 +1,611 @@
+# Blosxom Plugin: lastmodified2
+# Author(s): Frank Hecker
+# (based on work by Bob Schumaker )
+# 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 '', ;
+ 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, C, C, and/or
+C HTTP headers in the response and responding appropriately
+to an C and/or C header in the
+request. Also generates a C header to support HTTP 1.0
+persistent connections.
+
+=head1 VERSION
+
+0.10
+
+=head1 AUTHOR
+
+Frank Hecker , http://www.hecker.org/ (based on
+work by Bob Schumaker, , 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, C, and C subroutines. It
+needs to run after any other plugin whose C subroutine changes
+the list of entries included in the response; otherwise the
+C date may be computed incorrectly. It needs to run
+after any other plugin whose C 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 subroutine changes the output
+for the page; otherwise the C value (and the C
+and C 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 http://www.hecker.org/
+
+Based on the original lastmodified plugin by Bob Schumaker
+ 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
index 0000000..9c0f9ad
--- /dev/null
+++ b/general/noslashredir
@@ -0,0 +1,173 @@
+# Blosxom Plugin: noslashredir
+# Author(s): Frank Hecker
+# 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 $uri.\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 , 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
index 0000000..ab2e659
--- /dev/null
+++ b/general/slashredir
@@ -0,0 +1,224 @@
+# Blosxom Plugin: slashredir
+# Author(s): Frank Hecker
+# 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 $uri.\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 , 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.
$feedback::name_link wrote at $feedback::date:
\n