X-Git-Url: https://git.stderr.nl/gitweb?a=blobdiff_plain;f=general%2Ffeedback;fp=general%2Ffeedback;h=8989659bedd482ddd4bfce36d0450d191217edad;hb=fca3198611f04b221a988c7bf2df19cb4a08402b;hp=0000000000000000000000000000000000000000;hpb=e7287283e68baf7fef5a415a05d1168f9dc7ea98;p=matthijs%2Fupstream%2Fblosxom-plugins.git 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.