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 = + "

" + . "Comments are closed for this story.

"; + } 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 = '

Thanks for the comment!

'; + } else { + $response = '

' . $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

$feedback::name_link wrote at $feedback::date:

\n
$feedback::comment
+html trackback \n

$feedback::blog_name mentioned this post in $feedback::title_link.

:

\n
$feedback::excerpt
+html commentform \n
\n\n\n\n\n
Name:
URL (optional):
Comments:
+html trackbackinfo

URL for TrackBack pings: $blosxom::url$blosxom::path/$blosxom::fn.$feedback::trackback_flavour

\n +general comment \n

$feedback::name_link wrote at $feedback::date:

\n
$feedback::comment
+general trackback \n

$feedback::blog_name mentioned this post in $feedback::title_link.

:

\n
$feedback::excerpt
+general commentform \n
\n\n\n\n\n
Name:
URL (optional):
Comments:
+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.