Update perldocs in general plugins to point to current website/mailing list.
[blosxom-plugins.git] / general / feedback
blob800819f524bdc81831492b8a127fba75741799aa
1 # Blosxom Plug-in: feedback
2 # Author: Frank Hecker (http://www.hecker.org/)
4 # Version 0.23
6 package feedback;
8 use warnings;
11 # --- Configurable variables ---
13 # --- You *must* set the following variables properly for your blog ---
15 # Where should I keep the feedback hierarchy?
16 # (By default it goes in the Blosxom state directory. However you may
17 # prefer it to go in the same directory as the Blosxom data directory.
18 # If so, delete the following line and uncomment the line following it.)
19 $fb_dir = "$blosxom::plugin_state_dir/feedback";
20 # $fb_dir = "$blosxom::datadir/../feedback";
23 # --- Set the following variables according to your preferences ---
25 # Are comments and TrackBacks allowed? Set to zero to disable either or both.
26 my $allow_comments = 1;
27 my $allow_trackbacks = 1;
29 # Don't allow comments/TrackBacks if story is older than this (in seconds).
30 # (Set to zero to keep story open for comments/TrackBacks forever.)
31 my $comment_period = 90 * 86400;        # 90 days
32 my $trackback_period = 90 * 86400;      # 90 days
34 # Do Akismet checking of comments and/or TrackBacks for spam.
35 my $akismet_comments = 0;
36 my $akismet_trackbacks = 0;
38 # WordPress API key for use with Akismet.
39 # (Register at <http://wordpress.com/> to get your own API key.)
40 my $wordpress_api_key = '';
42 # Do MT-blacklist checking of comments and/or TrackBacks for spam.
43 # NOTE: The MT-Blacklist file is no longer maintained; we suggest using
44 # Akismet instead.
45 my $blacklist_comments = 0;
46 my $blacklist_trackbacks = 0;
48 # Where can I find the local copy of the MT-Blacklist file?
49 my $blacklist_file = "$blosxom::plugin_state_dir/blacklist.txt";
51 # Send an email message to notify the blog owner of new comments and/or
52 # TrackBacks and (optionally) request approval of new comments/TrackBacks.
53 my $notify_comments = 0;
54 my $notify_trackbacks = 0;
55 my $moderate_comments = 1;
56 my $moderate_trackbacks = 1;
58 # Email address and SMTP server used for notifications and moderation requests.
59 my $address = 'jdoe@example.com';
60 my $smtp_server = 'smtp.example.com';
62 # Default values for fields not submitted with the comment or TrackBack ping.
63 my $default_name = "Someone";
64 my $default_blog_name = "An unnamed blog";
65 my $default_title = "an article";
67 # The formatting used for comments, i.e., how they are translated to (X)HTML.
68 # Valid choices at present are 'none', 'plaintext' and 'markdown'.
69 my $comment_format = 'plaintext';
71 # Should we accept and display commenter's email addresses? (The default is
72 # to support http/https URLs only; this may be the only option in future.)
73 my $allow_mailto = 0;
76 # --- You should not normally need to change the following variables ---
78 # What flavour should I consider an incoming TrackBack ping?
79 $trackback_flavour = "trackback";
81 # What file extension should I use for saved comments and TrackBacks?
82 my $fb_extension = "wb";
84 # What fields are used in the comments form?
85 my @comment_fields = qw! name url comment !;
87 # What fields are used by TrackBacks?
88 my @trackback_fields = qw! blog_name url title excerpt !;
90 # Limit all fields to this length or less (just in case).
91 my $max_param_length = 10000;
94 # --- Variables for use in flavour templates (e.g., as $feedback::foo) ---
96 # Comment and TrackBack fields, for use in the comment, preview, and
97 # trackback templates.
98 $name = '';
99 $name_link = '';                        # Combines name and link for email/URL
100 $date = '';
101 $comment = '';
102 $blog_name = '';
103 $title = '';
104 $title_link = '';                       # Combines title and link to article
105 $excerpt = '';
106 $url = '';                              # Also used in $name_link, $title_link
108 # Field values for previewed comments, used in the commentform template.
109 $name_preview = '';
110 $comment_preview = '';
111 $url_preview = '';
113 # Message displayed in response to a comment submission (e.g., to display
114 # an error message), for use in the story or foot templates. The response is
115 # formatted for use in HTML/XHTML content.
116 $comment_response = '';
118 # XML message displayed in response to a TrackBack ping (e.g., to display
119 # an error message or indicate success), per the TrackBack Technical
120 # Specification <http://www.sixapart.com/pronet/docs/trackback_spec>.
121 $trackback_response = '';
123 # All comments and TrackBacks for a particular story, for use in the story
124 # template for an individual story page. Also includes content from the
125 # comments_head/comments_foot and trackbacks_head/trackbacks_foot templates.
126 $comments = '';
127 $trackbacks = '';
129 # Counts of comments and TrackBacks for a story, for use in the story
130 # template (e.g., for index and archive pages).
131 $comments_count = 0;
132 $trackbacks_count = 0;
133 $count = 0;                             # total of both
135 # Previewed comment for a particular story, for use in the story
136 # template for an individual story page.
137 $preview = '';
139 # Default comment submission form, for use in the foot template (for an
140 # individual story page). The plug-in sets this value to null if comments
141 # are disabled or in cases where the page is not for an individual story
142 # or the story is older than the allowed comment period.
143 $commentform = '';
145 # TrackBack discovery information, for use in the foot template (for
146 # an individual story page). The code sets this value to null if TrackBacks
147 # are disabled or in cases where the page is not for an individual story
148 # or the story is older than the allowed TrackBack period.
149 $trackbackinfo = '';
152 # --- External modules required ---
154 use CGI qw/:standard/;
155 use FileHandle;
156 use URI;
157 use URI::Escape;
160 # --- Global variables (used in interpolation) ---
162 use vars qw! $fb_dir $trackback_flavour $name $name_link $date $comment
163     $blog_name $title $name_preview $comment_preview $url_preview
164     $comment_response $trackback_response $comments $trackbacks
165     $comments_count $trackbacks_count $count $preview $commentform
166     $trackbackinfo !;
169 # --- Private static variables ---
171 # Spam blacklist array.
172 my @blacklist_entries = ();
174 # File handle for use in reading/writing the feedback file, etc.
175 my $fh = new FileHandle;
177 # Path and filename for the main feedback file for a story, and item name
178 # used in contructing filenames for files containing moderated items.
179 my $fb_path = '';
180 my $fb_fn = '';
182 # Whether comments or TrackBacks are closed for a given story.
183 my $closed_comments = 0;
184 my $closed_trackbacks = 0;
187 # --- Plug-in initialization ---
189 # Strip potentially confounding final slash from feedback directory path.
190 $fb_dir =~ s!/$!!;
192 # Strip potentially confounding initial period from file extension.
193 $fb_extension =~ s!^\.!!;
195 # Initialize the default templates; use $blosxom::template so we can leverage
196 # the Blosxom template subroutine (whether default or replaced by a plug-in).
197 my %template = ();
198 while (<DATA>) {
199     last if /^(__END__)?$/;
200     # TODO: Fix this to correctly handle empty flavours (i.e., no $txt).
201     my ($ct, $comp, $txt) = /^(\S+)\s(\S+)(?:\s(.*))?$/;
202 #   my ($ct, $comp, $txt) = /^(\S+)\s(\S+)\s(.*)$/;
203     $txt = '' unless defined($txt);
204     $txt =~ s/\\n/\n/mg;
205     $blosxom::template{$ct}{$comp} = $txt;
208 # Moderation implies notification.
209 $notify_comments = 1 if $moderate_comments;
210 $notify_trackbacks = 1 if $moderate_trackbacks;
213 # --- Plug-in subroutines ---
215 # Create feedback directory if needed.
216 sub start {
217     # The $fb_dir variable must be set to activate feedback.
218     unless ($fb_dir) {
219         warn "feedback: " .
220             "The \$fb_dir configurable variable is not set; "
221             . "please set it to enable comments or TrackBacks.\n";
222         return 0;
223     }
225     # The value of $fb_dir must be a writeable directory.
226     if (-e $fb_dir && !(-d $fb_dir && -w $fb_dir)) {
227         warn "feedback: The feedback directory '$fb_dir' "
228              . "must be a writeable directory; please rename or remove it "
229              . "and Blosxom will create it properly for you.\n";
230         return 0;
231     }
233     # The $fb_dir does not yet exist, so Blosxom will create it.
234     unless (-e $fb_dir)  {
235         return 0 unless (mk_feedback_dir($fb_dir));
236     }
238     return 1;
242 # Decide whether to close comments and TrackBacks for a story.
243 sub date {
244     my ($pkg, $file, $date_ref, $mtime, $dw, $mo, $mo_num, $da, $ti, $yr) = @_;
246     # A positive value of $comment_period represents the time in seconds
247     # during which posting comments or TrackBacks is allowed after a
248     # story has been published. (Note that updating a story has the effect
249     # of reopening the feedback period.) A zero or negative value for
250     # $comment_period means that posting feedback is always allowed.
252     if ($comment_period <= 0) {
253         $closed_comments = 0;
254     } elsif ($allow_comments && (time - $mtime) > $comment_period) {
255         $closed_comments = 1;
256     } else {
257         $closed_comments = 0;
258     }
260     # $trackback_period works the same way as $comment_period.
262     if ($trackback_period <= 0) {
263         $closed_trackbacks = 0;
264     } elsif ($allow_trackbacks && (time - $mtime) > $trackback_period) {
265         $closed_trackbacks = 1;
266     } else {
267         $closed_trackbacks = 0;
268     }
270     return 1;
274 # Parse posted TrackBacks and comments and take action as appropriate.
275 # Retrieve comments and TrackBacks and format according to the templates.
276 # Display a comment form and/or TrackBack URL as appropriate.
278 sub story {
279     my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
280     my $submission_type;
281     my $status_msg;
282     my $is_story_page;
284     # We have five possible tasks in this subroutine:
285     #
286     #   * handle submitted TrackBack pings or comments (or related requests)
287     #   * display previously-submitted TrackBacks or comments
288     #   * display a comment being previewed
289     #   * display a form for entering a comment (or editing a previewed one)
290     #   * display information about submitting TrackBacks
291     #
292     # Exactly what we do depends whether we are rendering dynamically or
293     # statically and on the type of request (GET, HEAD, or POST) (when
294     # dynamically rendering),  the Blosxom flavour, the parameters associated
295     # with the request, the age of the story, and the way the feedback
296     # plug-in itself is configured.
298     # Make $path empty if at top level, preceded by a single slash otherwise.
299     !defined($path) and $path = "";
300     $path =~ s!^/*!!; $path &&= "/$path";
302     # Set feedback path and filename for this story.
303     $fb_path = $path;
304     $fb_fn = $filename . '.' . $fb_extension;
306     # Determine whether this is an individual story page or not.
307     $is_story_page =
308         $blosxom::path_info =~ m!^(.*/)?(.+)\.(.+)$! ? 1 : 0;
310     # For dynamic rendering of an individual story page *only*, check to
311     # see if this is a feedback-related request, take action, and formulate
312     # a response.
313     #
314     # We have five possible cases: TrackBack ping, comment preview, comment
315     # post, moderator approval, and moderator rejection. These are
316     # distinguished based on the type of request (POST vs. GET/HEAD),
317     # the flavour (for TrackBack pings only), and the request parameters.
319     $submission_type = $comment_response = $trackback_response = '';
320     if ($blosxom::static_or_dynamic eq 'dynamic' && $is_story_page) {
321         ($submission_type, $status_msg) = process_submission();
322         if ($submission_type eq 'trackback') {
323             $trackback_response = format_tb_response($status_msg);
324             return 1;                   # All done.
325         } elsif ($submission_type eq 'comment'
326                  || $submission_type eq 'preview'
327                  || $submission_type eq 'approve'
328                  || $submission_type eq 'reject') {
329             $comment_response = format_cmt_response($status_msg);
330         }
331     }
333     # Display previously-submitted comments and TrackBacks for this story.
334     # For index and and archive pages we just display counts of the comments
335     # and TrackBacks.
337     $comments = $trackbacks = '';
338     $comments_count = $trackbacks_count = 0;
339     if ($is_story_page) {
340         ($comments, $comments_count, $trackbacks, $trackbacks_count) =
341             get_feedback($path);
342     } else {
343         ($comments_count, $trackbacks_count) = count_feedback();
344     }
345     $count = $comments_count + $trackbacks_count;
347     # If we are previewing a comment then format the comment for display.
348     $preview = '';
349     if ($submission_type eq 'preview') {
350         $preview = get_preview($path);
351     }
353     # Display a form for comment submission, if we are on an individual
354     # story page and comments are (still) allowed. (If we are previewing
355     # a comment then the form will be pre-filled as appropriate.)
357     $commentform = '';
358     if ($is_story_page && $allow_comments) {
359         if ($closed_comments) {
360             $commentform =
361                 "<p class=\"commentform\">"
362                 . "Comments are closed for this story.</p>";
363         } else {
364             # Get the commentform template and interpolate variables in it.
365             $commentform =
366                 &$blosxom::template($path,'commentform',$blosxom::flavour)
367                 || &$blosxom::template($path,'commentform','general');
368             $commentform = &$blosxom::interpolate($commentform);
369         }
370     }
372     # Display information on submitting TrackPack pings (including code for
373     # TrackBack autodiscovery), if we are on an individual story page and
374     # TrackBacks are (still) allowed.
376     $trackbackinfo = '';
377     if ($is_story_page && $allow_trackbacks) {
378         if ($closed_trackbacks) {
379             $trackbackinfo =
380                 "<p class=\"trackbackinfo\">"
381                 . "Trackbacks are closed for this story.</p>";
382         } else {
383             # Get the trackbackinfo template and interpolate variables in it.
384             $trackbackinfo =
385                 &$blosxom::template($path,'trackbackinfo',$blosxom::flavour)
386                 || &$blosxom::template($path,'trackbackinfo','general');
387             $trackbackinfo = &$blosxom::interpolate($trackbackinfo);
388         }
389     }
391     # For interpolate_fancy to work properly when deciding whether to include
392     # certain content or not, the associated variables must be undefined if
393     # there is no actual content to be displayed.
395     $comment_response  =~ m!^\s*$! and $comment_response = undef;
396     $comments =~ m!^\s*$! and $comments = undef;
397     $trackbacks =~ m!^\s*$! and $trackbacks = undef;
398     $preview =~ m!^\s*$! and $preview = undef;
399     $commentform =~ m!^\s*$! and $commentform = undef;
400     $trackbackinfo =~ m!^\s*$! and $trackbackinfo = undef;
402     return 1;
406 # --- Helper subroutines ---
408 # Process a submitted HTTP request and take whatever action is appropriate.
409 # Returns the type of submission: 'trackback', 'comment', 'preview',
410 # 'approve', 'reject', or null for a request not related to feedback.
411 # Also sets $comment_response and $trackback_response;
413 sub process_submission {
414     my $submission_type = '';
415     my $status_msg = '';
417     if (request_method() eq 'POST') {
418         # We have two possible cases: a TrackBack ping (identified by
419         # the flavour extension) or a submitted comment.
421         if ($blosxom::flavour eq $trackback_flavour) {
422             $status_msg = handle_feedback('trackback');
423             $submission_type = 'trackback';
424         } else {
425             # Comment posts may or may not use a particular flavour
426             # extension, so we check for the value of the 'plugin'
427             # hidden field (from the comment form).
429             my $plugin_param = sanitize_param(param('plugin'));
430             if ($plugin_param eq 'writeback') {
431                 # Comment previews are distinguished from comment posts
432                 # by the value of the 'submit' parameter associated with
433                 # the 'Post' and 'Preview' form buttons.
435                 my $submit_param = sanitize_param(param('submit'));
436                 $status_msg = '';
437                 if ($submit_param eq 'Preview') {
438                     $status_msg = handle_feedback('preview');
439                     $submission_type = 'preview';
440                 } elsif ($submit_param eq 'Post') {
441                     $status_msg = handle_feedback('comment');
442                     $submission_type = 'comment';
443                 } else {
444                     $status_msg = "The submit parameter must have the value "
445                         . "'Preview' or 'Post'";
446                 }
447             }
448         }
449     } elsif (request_method() eq 'GET' || request_method() eq 'HEAD') {
450         my $moderate_param = sanitize_param(param('moderate'));
451         my $feedback_param = sanitize_param(param('feedback'));
453         if ($moderate_param) {
454             # We have two possible cases: moderator approval or rejection,
455             # distinguished based on the value of the 'moderate' parameter.
457             if (!$feedback_param) {
458                 $status_msg =
459                     "You must provide a 'feedback' parameter and item.";
460             } elsif ($moderate_param eq 'approve') {
461                 $status_msg = approve_feedback($feedback_param);
462                 $submission_type = 'approve';
463             } elsif ($moderate_param eq 'reject') {
464                 $status_msg = reject_feedback($feedback_param);
465                 $submission_type = 'reject';
466             } else {
467                 $status_msg =
468                     "'moderate' parameter must "
469                     . "have the value 'approve' or 'reject'.";
470             }
471         }
472     }
474     return $submission_type, $status_msg;
478 # Retrieve comments and TrackBacks for a story and format them according
479 # to the appropriate templates for the story (based on the story's path).
480 # For comments we use the comment template for each individual comment,
481 # along with the optional comments_head and comments_foot templates (before
482 # and after the comments proper). For TrackBacks we use the corresponding
483 # trackback template for each TrackBack, together with the optional
484 # trackbacks_head and trackbacks_foot templates.
486 sub get_feedback {
487     my $path = shift;
488     my ($comments, $comments_count, $trackbacks, $trackbacks_count);
490     $comments = $trackbacks = '';
491     $comments_count = $trackbacks_count = 0;
493     # Retrieve the templates for individual comments and TrackBacks.
494     my $comment_template =
495         &$blosxom::template($path, 'comment', $blosxom::flavour)
496         || &$blosxom::template($path, 'comment', 'general');
498     my $trackback_template =
499         &$blosxom::template($path, 'trackback', $blosxom::flavour)
500         || &$blosxom::template($path, 'trackback', 'general');
502     # Open the feedback file (if it exists) and read any comments or
503     # TrackBacks. Note that we can distinguish comments from TrackBacks
504     # because comments have a 'comment' field and TrackBacks don't.
506     my %param = ();
507     if ($fh->open("$fb_dir$fb_path/$fb_fn")) {
508         foreach my $line (<$fh>) {
509             $line =~ /^(.+?): (.*)$/ and $param{$1} = $2;
510             if ( $line =~ /^-----$/ ) {
511                 if ($param{'comment'}) {
512                     $comment = format_comment($param{'comment'});
513                     $date = format_date($param{'date'});
514                     ($name, $name_link) =
515                         format_name($param{'name'}, $param{'url'});
517                     my $cmt = $comment_template;
518                     $cmt = &$blosxom::interpolate($cmt);
520                     $comments .= $cmt;
521                     $comments_count++;
522                 } else {
524                     $blog_name = format_blog_name($param{'blog_name'});
525                     $excerpt = format_excerpt($param{'excerpt'});
526                     $date = format_date($param{'date'});
527                     ($title, $title_link) =
528                         format_title($param{'title'}, $param{'url'});
530                     my $trackback = $trackback_template;
531                     $trackback = &$blosxom::interpolate($trackback);
533                     $trackbacks .= $trackback;
534                     $trackbacks_count++;
535                 }
536                 %param = ();
537             }
538         }
539     }
541     return ($comments, $comments_count, $trackbacks, $trackbacks_count);
545 # Retrieve comments and TrackBacks for a story and (just) count them.
547 sub count_feedback {
548     my $comments_count = 0;
549     my $trackbacks_count = 0;
551     # Open the feedback file (if it exists) and count any comments or
552     # TrackBacks. Note that we can distinguish comments from TrackBacks
553     # because comments have a 'comment' field and TrackBacks don't.
555     my %param = ();
556     if ($fh->open("$fb_dir$fb_path/$fb_fn")) {
557         foreach my $line (<$fh>) {
558             $line =~ /^(.+?): (.*)$/ and $param{$1} = $2;
559             if ( $line =~ /^-----$/ ) {
560                 if ($param{'comment'}) {
561                     $comments_count++;
562                 } else {
563                     $trackbacks_count++;
564                 }
565                 %param = ();
566             }
567         }
568     }
570     return ($comments_count, $trackbacks_count);
574 # Format a previewed comment according to the appropriate preview template
575 # for the story (based on the story's path).
577 sub get_preview {
578     my $path = shift;
579     my $preview = '';
581     # Retrieve the comment template (also used for previewed comments).
582     my $comment_template =
583         &$blosxom::template($path, 'comment', $blosxom::flavour)
584         || &$blosxom::template($path, 'comment', 'general');
586     # Format the previewed comment using the submitted values.
587     $comment = format_comment($comment_preview);
588     $date = format_date($date_preview);
589     ($name, $name_link) =
590         format_name($name_preview, $url_preview);
592     $preview = &$blosxom::interpolate($comment_template);
594     return $preview;
598 # Create top-level directory to hold feedback files, and make it writeable.
599 sub mk_feedback_dir {
600     my $mkdir_r = mkdir("$fb_dir", 0755);
601     warn $mkdir_r
602         ? "feedback: $fb_dir created.\n"
603         : "feedback: Could not create $fb_dir.\n";
604     $mkdir_r or return 0;
606     my $chmod_r = chmod 0755, $fb_dir;
607     warn $chmod_r
608         ? "feedback: $fb_dir set to 0755 permissions.\n"
609         : "feedback: Could not set permissions on $fb_dir.\n";
610     $chmod_r or return 0;
612     warn "feedback: feedback is enabled!\n";
613     return 1;
617 # Create subdirectories of feedback directory as necessary.
618 sub mk_feedback_subdir {
619     my $dir = shift;
620     my $p = '';
622     return 1 if !defined($dir) or $dir eq '';
624     foreach (('', split /\//, $dir)) {
625         $p .= "/$_";
626         $p =~ s!^/!!;
627         return 0
628             unless (-d "$fb_dir/$p" or mkdir "$fb_dir/$p", 0755);
629     }
631     return 1;
635 # Process a submitted comment or TrackBack.
636 sub handle_feedback {
637     my $feedback_type = shift;
638     my $status_msg = '';
639     my $is_comment;
640     my $is_preview;
641     my $fb_item;
643     # Set up to handle either a comment, preview, or TrackBack as requested.
644     if ($feedback_type eq 'comment') {
645         $is_comment = 1;
646         $is_preview = 0;
647     } elsif ($feedback_type eq 'preview') {
648         $is_comment = 1;
649         $is_preview = 1;
650     } else {
651         $is_comment = 0;
652         $is_preview = 0;
653     }
655     my $allow = $is_comment ? $allow_comments : $allow_trackbacks;
656     my $closed = $is_comment ? $closed_comments : $closed_trackbacks;
657     my $period = $is_comment ? $comment_period : $trackback_period;
658     my $akismet = $is_comment ? $akismet_comments : $akismet_trackbacks;
659     my $blacklist = $is_comment ? $blacklist_comments : $blacklist_trackbacks;
660     my $notify = $is_comment ? $notify_comments : $notify_trackbacks;
661     my $moderate = $is_comment ? $moderate_comments : $moderate_trackbacks;
662     my @fields = $is_comment ? @comment_fields : @trackback_fields;
664     # Reject request if feedback is not (still) allowed.
665     unless ($allow && !$closed) {
666         if ($closed) {
667             $status_msg =
668                 "This story is older than " . ($period/86400) . " days. "
669                 . ($is_comment ? "Comments" : "TrackBacks")
670                 . " have now been closed.";
671         } else {
672             $status_msg =
673                 ($is_comment ? "Comments" : "TrackBacks")
674                 . " are not enabled for this site.";
675         }
676         return $status_msg;
677     }
679     # Filter out the "good" fields from the CGI parameters.
680     my %params = copy_params(\@fields);
682     # Comments must have (at least) a comment parameter, and TrackBacks a URL.
683     if ($is_comment) {
684         unless ($params{'comment'}) {
685             $status_msg =
686                 "You didn't enter anything in the comment field.";
687             return $status_msg;
688         }
689     } elsif (!$params{'url'}) {
690         $status_msg = "No URL specified for the TrackBack";
691         return 0;
692     }
694     # Check feedback to see if it's spam.
695     if (is_spam(\%params, $is_comment, $akismet, $blacklist)) {
696         # If we are previewing a comment then we allow the poster a
697         # chance to revise the comment; otherwise we just reject it.
699         if ($is_preview) {
700             $status_msg =
701                 "Your comment appears to be spam and will be rejected "
702                 . "unless it is revised. ";
703         } else {
704             $status_msg =
705                 "Your feedback was rejected because it appears to be spam; "
706                 . "please contact the site administrator if you believe that "
707                 . "your feedback was rejected in error.";
708             return $status_msg;
709         }
710     }
712     # If we are previewing a comment then just save the fields for later
713     # use in the previewed comment and (as prefilled values) in the comment
714     # form. Otherwise attempt to save the new feedback information, either
715     # into the permanent feedback file for this story (if no moderation) or
716     # into a temporary file (for later moderation).
718     if ($is_preview) {
719         $status_msg .= save_preview(\%params);
720     } else {
721         ($fb_item, $status_msg) = save_feedback(\%params, $moderate);
722         return $status_msg unless $fb_item;
724         # Send a moderation message or notify blog owner of the new feedback.
725         if ($moderate || $notify) {
726             send_notification(\%params, $moderate, $fb_item);
727         }
728     }
730     return $status_msg;
734 # Make a "safe" copy of the CGI parameters based on the expected
735 # field names associated with either a comment or TrackBack.
736 sub copy_params {
737     my $fields_ref = shift;
738     my %params;
740     foreach my $key (@$fields_ref) {
741         my $value = substr(param($key), 0, $max_param_length) || "";
743         # Eliminate leading and trailing whitespace, use carriage returns
744         # as line delimiters, and collapse multiple blank lines into one.
746         $value =~ s/^\s+//;
747         $value =~ s/\s+$//;
748         $value =~ s/\r?\n\r?/\r/mg;
749         $value =~ s/\r\r\r*/\r\r/mg;
751         $params{$key} = $value;
752     }
754     return %params;
758 # Send notification or moderation email to blog owner.
759 sub send_notification {
760     my ($params_ref, $moderate, $fb_item) = @_;
762     unless ($address && $smtp_server) {
763         warn "feedback: No address or SMTP server for notifications\n";
764         return 0;
765     }
767     my $message = "New feedback for your post \"$blosxom::title\" ("
768         . $blosxom::path_info . "):\n\n";
770     if ($$params_ref{'comment'}) {
771         $message .= "Name     : " . $$params_ref{'name'} . "\n";
772         $message .= "Email/URL: " . $$params_ref{'url'} . "\n";
773         $message .= "Comment  :\n";
774         my $comment = $$params_ref{'comment'};
775         $comment =~ s!\r!\n!g;
776         $message .= $comment . "\n";
777     } else {
778         $message .= "Blog name: " . $$params_ref{'blog_name'} . "\n";
779         $message .= "Article  : " . $$params_ref{'title'} . "\n";
780         $message .= "URL      : " . $$params_ref{'url'} . "\n";
781         $message .= "Excerpt  :\n";
782         my $excerpt = $$params_ref{'excerpt'};
783         $excerpt =~ s!\r!\n!g;
784         $message .= $excerpt . "\n";
785     }
787     if ($moderate) {
788         # For TrackBacks use the default flavour for the approve/reject URI.
789         my $moderate_flavour = $blosxom::flavour;
790         $moderate_flavour eq $trackback_flavour
791             and $moderate_flavour = $blosxom::default_flavour;
793         $message .= "\n\nTo approve this feedback, please click on the URL\n"
794             . "$blosxom::url$blosxom::path/$blosxom::fn.$moderate_flavour"
795             . "?moderate=approve;feedback=" . uri_escape($fb_item) . "\n";
797         $message .= "\nTo reject this feedback, please click on the URL\n"
798             . "$blosxom::url$blosxom::path/$blosxom::fn.$moderate_flavour"
799             . "?moderate=reject;feedback=" . uri_escape($fb_item) . "\n";
800     }
802     # Load Net::SMTP module only now that it's needed.
803     require Net::SMTP; Net::SMTP->import;
805     my $smtp = Net::SMTP->new($smtp_server);
806     $smtp->mail($address);
807     $smtp->to($address);
808     $smtp->data();
809     $smtp->datasend("To: $address\n");
810     $smtp->datasend("From: $address\n");
811     $smtp->datasend("Subject: [$blosxom::blog_title] Feedback: "
812                     . "\"$blosxom::title\"\n");
813     $smtp->datasend("\n\n");
814     $smtp->datasend($message);
815     $smtp->dataend();
816     $smtp->quit;
818     return 1;
822 # Format the date used in comments and TrackBacks. If the argument is a
823 # number then it is considered to be a date/time in seconds since the
824 # (Perl) epoch; otherwise we assume that the date is already formatted.
825 # (This may allow the feedback plug-in to use legacy writeback files.)
827 sub format_date {
828     my $date_value = shift;
830     if ($date_value =~ m!^\d+$!) {
831         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
832             localtime($date_value);
833         $year += 1900;
835         # Modify the following to match your preference.
836         return sprintf("%4d-%02d-%02d %02d:%02d",
837                        $year, $mon+1, $mday, $hour, $min);
838     } else {
839         return $date_value;
840     }
844 # Format the name used in comments.
845 sub format_name {
846     my ($name, $url) = @_;
848     # If the user didn't supply a name, try to use something sensible.
849     unless ($name) {
850         if ($url =~ m/^mailto:/) {
851             $name = substr($url, 7);
852         } else {
853             $name = $default_name;
854         }
855     }
857     # Link to a URL if one was provided.
858     my $name_link =
859         $url ? "<a href=\"$url\" rel=\"nofollow\">$name</a>" : $name ;
861     return $name, $name_link;
865 # Format the comment response message.
866 sub format_cmt_response {
867     my $response = shift;
869     # Clean up the response.
870     $response =~ s/^\s+//;
871     $response =~ s/\s+$//;
873     # Convert the response into a special type of paragraph.
874     # NOTE: A value 'OK' for $response indicates a successful comment.
875     if ($response eq 'OK') {
876         $response = '<p class="comment-response">Thanks for the comment!</p>';
877     } else {
878         $response = '<p class="comment-response">' . $response . '</p>';
879     }
881     return $response;
885 # Format the TrackBack response message.
886 sub format_tb_response {
887     my $response = shift;
889     # Clean up the response.
890     $response =~ s/^\s+//;
891     $response =~ s/\s+$//;
893     # Convert the response into an XML message per the TrackBack Technical
894     # Specification <http://www.sixapart.com/pronet/docs/trackback_spec>.
895     # NOTE: A value 'OK' for $response indicates a successful TrackBack;
896     # note that this value is *not* used as part of the TrackBack response.
898     if ($response eq 'OK') {
899         $response = "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>"
900             . "<response><error>0</error></response>";
901     } else {
902         $response = "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>"
903             . "<response><error>1</error>"
904             . "<message>$response</message></response>";
905     }
907     return $response;
911 # Format the comment itself.
912 sub format_comment {
913     my $comment = shift;
915     # TODO: Support other comment formats such as Textile.
917     if ($comment_format eq 'none') {
918         # A no-op, assumes formatting will be added in the template.
919     } elsif ($comment_format eq 'plaintext') {
920         # Simply convert the comment into a series of paragraphs.
921         $comment = '<p>' . $comment . '</p>';
922         $comment =~ s!\r\r!</p><p>!mg;
923     } elsif ($comment_format eq 'markdown'
924              && $blosxom::plugins{'Markdown'} > 0) {
925         $comment = &Markdown::Markdown($comment);
926     }
928     return $comment;
932 # Format the blog name used in TrackBacks.
933 sub format_blog_name {
934     my $blog_name = shift;
936     $blog_name or $blog_name = $default_blog_name;
938     return $blog_name;
942 # Format the title used in TrackBacks.
943 sub format_title {
944     my ($title, $url) = @_;
945     my $title_link;
947     # Link to article, quoting the title if one was supplied.
948     if ($title) {
949         $title_link = "\"<a href=\"$url\" rel=\"nofollow\">$title</a>\"";
950     } else {
951         $title = $default_title;
952         $title_link = "<a href=\"$url\" rel=\"nofollow\">$title</a>";
953     }
955     return $title, $title_link;
959 # Format the TrackBack excerpt.
960 sub format_excerpt {
961     my $excerpt = shift;
963     # TODO: Truncate excerpts at some reasonable length.
965     # Simply convert the excerpt into a series of paragraphs.
966     if ($excerpt) {
967         $excerpt = '<p>' . $excerpt . '</p>';
968         $excerpt =~ s!\r\r!</p><p>!mg;
969     }
971     return $excerpt;
975 # Read in the MT-Blacklist file.
976 sub read_blacklist {
978     # No need to do anything if we've already read in the blacklist file.
979     return 1 if @blacklist_entries;
981     # Try to find the blacklist file and open it.
982     open BLACKLIST, "$blacklist_file"
983         or die "Can't read '$blacklist_file', $!\n";
985     my @lines = grep {! /^\s*\#/ } <BLACKLIST>;
986     close BLACKLIST;
987     die "No blacklists?\n" unless @lines;
989     foreach my $line (@lines) {
990         $line =~ s/^\s*//;
991         $line =~ s/\s*[^\\]\#.*//;
992         next unless $line;
993         push @blacklist_entries, $line;
994     }
995     die "No entries in blacklist file?\n" unless @blacklist_entries;
997     return 1;
1001 # Do spam tests on comment or TrackBack; returns 1 if spam, 0 if OK.
1002 sub is_spam {
1003     my ($params_ref, $is_comment, $akismet, $blacklist) = @_;
1005     # Perform a series of spam tests. If any show positive then reject.
1007     # Does the host part of the URL reference an IP address?
1008     return 1 if uses_ipaddr($$params_ref{'url'});
1010     # Does the comment or TrackBack match against the Akismet service?
1011     return 1 if $akismet && matches_akismet($params_ref, $is_comment);
1013     # Does the comment or TrackBack match against the MT-Blacklist file
1014     # (deprecated)?
1015     return 1
1016         if $blacklist && matches_blacklist((join "\n", values %$params_ref));
1018     # TODO: Add other useful spam checks.
1020     # Got by all the tests, so assume it's not spam.
1021     return 0;
1025 # Check host part of URL to see if it is an IP address.
1026 sub uses_ipaddr {
1027     my $uri = shift;
1029     return 0 unless $uri;
1031     # Construct URI object.
1032     my $u = URI->new($uri);
1034     # Return if this not actually a URI (i.e., it's an email address).
1035     return 0 unless defined($u->scheme);
1037     # Check for an IPv4 or IPv6 address on http/https URLs.
1038     if ($u->scheme eq 'http' || $u->scheme eq 'https') {
1039         if ($u->authority =~ m!^\[?\d!) {
1040             return 1;
1041         }
1042     }
1044     return 0;
1048 # Check comment or TrackBack against the Akismet online service.
1049 sub matches_akismet {
1050     my ($params_ref, $is_comment) = @_;
1052     # Load Net:Akismet module only now that it's needed.
1053     require Net::Akismet; Net::Akismet->import;
1055     # Attempt to connect to the Askimet service.
1056     my $akismet = Net::Akismet->new(KEY => $wordpress_api_key,
1057                                     URL => $blosxom::url);
1058     unless ($akismet) {
1059         warn "feedback: Akismet key verification failed\n";
1060         return 0;
1061     }
1063     # Set up fields to be verified. Note that we do not use the REFERRER,
1064     # PERMALINK, or COMMENT_AUTHOR_EMAIL fields supported by Akismet.
1066     my %fields = (USER_IP => $ENV{'REMOTE_ADDR'});
1067     if ($is_comment) {
1068         $fields{COMMENT_TYPE} = 'comment';
1069         $fields{COMMENT_CONTENT} = $$params_ref{'comment'};
1070         $fields{COMMENT_AUTHOR} = $$params_ref{'name'};
1071         $fields{COMMENT_AUTHOR_URL} = $$params_ref{'url'};
1072     } else {
1073         $fields{COMMENT_TYPE} = 'trackback';
1074         $fields{COMMENT_CONTENT} =
1075             $$params_ref{'title'} . "\n" . $$params_ref{'excerpt'};
1076         $fields{COMMENT_AUTHOR} = $$params_ref{'blog_name'};
1077         $fields{COMMENT_AUTHOR_URL} = $$params_ref{'url'};
1078     }
1080     # Is it spam?
1081     return 1 if $akismet->check(%fields) eq 'true';
1083     # Apparently not.
1084     return 0;
1088 # Check comment or TrackBack against the MT-Blacklist file (deprecated).
1089 sub matches_blacklist {
1090     my $params_string = shift;
1092     # Read in the blacklist file.
1093     read_blacklist();
1095     # Check each blacklist entry against the comment or TrackBack.
1096     foreach my $spam (@blacklist_entries) {
1097         chomp($spam);
1098         return 1 if $params_string =~ /$spam/;
1099     }
1101     return 0;
1105 # Save comment or TrackBack to disk. If moderating, returns the (randomly-
1106 # generated) id of the item saved for later approval or rejection (plus
1107 # a status message). If not moderating returns the name of the feedback
1108 # file in which the item was saved instead of the id. Returns null on errors.
1110 sub save_feedback {
1111     my ($params_ref, $moderate) = @_;
1112     my $fb_item = '';
1113     my $feedback_fn = '';
1114     my $status_msg = '';
1116     # Clear values used to prefill commentform.
1117     $name_preview = $url_preview = $comment_preview = '';
1119     # Create a new directory if needed to contain the feedback file.
1120     unless (mk_feedback_subdir($fb_path)) {
1121         $status_msg = 'Could not save comment or TrackBack.';
1122         return '', $status_msg;
1123     }
1125     # Save into the main feedback file or a temporary file, depending on
1126     # whether feedback is being moderated or not.
1127     if ($moderate) {
1128         $fb_item = rand_alphanum(8);
1129         $feedback_fn = $fb_item . '-' . $fb_fn;
1130     } else {
1131         $feedback_fn = $fb_fn;
1132     }
1134     # Attempt to open the file and append to it.
1135     unless ($fh->open(">> $fb_dir$fb_path/$feedback_fn")) {
1136         warn "couldn't >> $fb_dir$fb_path/$feedback_fn\n";
1137         $status_msg = 'Could not save comment or TrackBack.';
1138         return '', $status_msg;
1139     }
1141     # Write each parameter out as a line in the file.
1142     foreach my $key (sort keys %$params_ref) {
1143         my $value = $$params_ref{$key};
1145         # Eliminate leading and trailing whitespace, use carriage returns
1146         # as line delimiters, and collapse multiple blank lines into one.
1148         $value =~ s/^\s+//;
1149         $value =~ s/\s+$//;
1150         $value =~ s/\r?\n\r?/\r/mg;
1151         $value =~ s/\r\r\r*/\r\r/mg;
1153         # Ensure URL and other fields are sanitized.
1154         if ($key eq 'url') {
1155             $value = sanitize_uri($value);
1156         } else {
1157             $value = escapeHTML($value);
1158         }
1160         print $fh "$key: $value\n";
1161     }
1163     # Save the date/time (in seconds) and IP address as well.
1164     print $fh "date: " . time() ."\n";
1165     print $fh "ip: " . $ENV{'REMOTE_ADDR'} . "\n";
1167     # End the entry and close the file.
1168     print $fh "-----\n";
1169     $fh->close();
1171     # Set responses to indicate success.
1172     if ($moderate) {
1173         $status_msg =
1174             "Your feedback has been submitted for a moderator's approval; "
1175             . "it may take 24 hours or more to appear on the site.";
1176         return $fb_item, $status_msg;
1177     } else {
1178         $status_msg = 'OK';
1179         return $feedback_fn, $status_msg;
1180     }
1184 # Generate random alphanumeric string of the specified length.
1185 sub rand_alphanum {
1186     my $size = shift;
1187     return '' if $size <= 0;
1189     my @alphanumeric = ('a'..'z', 'A'..'Z', 0..9);
1190     return join '', map $alphanumeric[rand @alphanumeric], 0..$size;
1194 # Save previewed comment for later viewing (on the same page).
1195 # Sets $status_msg with an appropriate message.
1196 sub save_preview {
1197     my $params_ref = shift;
1198     my $status_msg;
1200     # Save each parameter for later use in the preview template.
1201     foreach my $key (sort keys %$params_ref) {
1202         my $value = $$params_ref{$key};
1204         # Eliminate leading and trailing whitespace, use carriage returns
1205         # as line delimiters, and collapse multiple blank lines into one.
1207         $value =~ s/^\s+//;
1208         $value =~ s/\s+$//;
1209         $value =~ s/\r?\n\r?/\r/mg;
1210         $value =~ s/\r\r\r*/\r\r/mg;
1212         # Ensure URL and other fields are sanitized.
1213         if ($key eq 'url') {
1214             $value = sanitize_uri($value);
1215         } else {
1216             $value = escapeHTML($value);
1217         }
1219         if ($key eq 'name') {
1220             $name_preview = $value;
1221         } elsif ($key eq 'url') {
1222             $url_preview = $value;
1223         } elsif ($key eq 'comment') {
1224             $comment_preview = $value;
1225         }
1226     }
1228     # Save the date/time (in seconds) as well.
1229     $date_preview = time();
1231     # Set response to indicate success.
1232     $status_msg .=
1233         "Please review your previewed comment below and submit it when "
1234         . "you are ready.";
1236     return $status_msg;
1240 # Approve a moderated comment or TrackBack (add it to feedback file).
1241 sub approve_feedback {
1242     my $item = shift;
1243     my $item_fn;
1244     my $status_msg = '';
1246     # Construct filename containing item to be approved, checking the
1247     # item name against the proper format from save_feedback().
1248     if ($item =~ m!^[a-zA-Z0-9]{8}!) {
1249         $item_fn = $item . "-" . $fb_fn;
1250     } else {
1251         $status_msg =
1252             "The item name to be approved was not in the proper format.";
1253         return $status_msg;
1254     }
1256     # Read lines from file containing the approved comment or TrackBack.
1257     unless ($fh->open("$fb_dir$fb_path/$item_fn")) {
1258         warn "feedback: couldn't < $fb_dir$fb_path/$item_fn\n";
1259         $status_msg =
1260             "There was a problem approving the comment or TrackBack.";
1261         return $status_msg;
1262     }
1264     my @new_feedback = ();
1265     while (<$fh>) {
1266         push @new_feedback, $_;
1267     }
1268     $fh->close();
1270     # Attempt to open the story's feedback file and append to it.
1271     # TODO: Try to make this more resistant to race conditions.
1273     unless ($fh->open(">> $fb_dir$fb_path/$fb_fn")) {
1274         warn "couldn't >> $fb_dir$fb_path/$fb_fn\n";
1275         $status_msg =
1276             "There was a problem approving the comment or TrackBack.";
1277         return $status_msg;
1278     }
1280     foreach my $line (@new_feedback) {
1281         print $fh $line;
1282     }
1284     # Close the feedback file, delete the file with the approved item.
1285     $fh->close();
1286     chdir("$fb_dir$fb_path")
1287         or warn "feedback: Couldn't cd to $fb_dir$fb_path\n";
1288     unlink($item_fn)
1289         or warn "feedback: Couldn't delete $item_fn\n";
1291     # Set response to indicate successful approval.
1292     $status_msg = "Feedback '$item' approved by moderator. ";
1294     return $status_msg;
1298 # Reject a moderated comment or TrackBack (delete the temporary file).
1299 sub reject_feedback {
1300     my $item = shift;
1301     my $item_fn;
1302     my $status_msg;
1304     # Construct filename containing item to be rejected, checking the
1305     # item name against the proper format from save_feedback().
1306     if ($item =~ m!^[a-zA-Z0-9]{8}!) {
1307         $item_fn = $item . "-" . $fb_fn;
1308     } else {
1309         $status_msg =
1310             "The item name to be rejected was not in the proper format.";
1311         return $status_msg;
1312     }
1314     # TODO: Optionally report comment or TrackBack to Akismet as spam.
1316     # Delete the file with the rejected item.
1317     chdir("$fb_dir$fb_path")
1318         or warn "feedback: Couldn't cd to '$fb_dir$fb_path'\n";
1319     unlink($item_fn)
1320         or warn "feedback: Couldn't delete '$item_fn'\n";
1322     # Set response to indicate successful rejection.
1323     $status_msg = "Feedback '$item' rejected by moderator.";
1325     return $status_msg;
1329 # Sanitize a query parameter to remove unexpected characters.
1330 sub sanitize_param
1332     my $param = shift || '';
1334     # Allow only alphanumeric, underscore, dash, and period.
1335     $param and $param =~ s/[^-.\w]/_/go;
1337     return $param;
1341 # Sanitize a URI.
1342 sub sanitize_uri {
1343     my $uri = shift;
1345     # Construct URI object.
1346     my $u = URI->new($uri);
1348     # If it's not a URI then assume it's an email address.
1349     $u->scheme('mailto') unless defined($u->scheme);
1351     # We check email addresses (if allowed) separately from web addresses.
1352     if ($allow_mailto && $u->scheme eq 'mailto') {
1353         # Make sure this is a valid RFC 822 address.
1354         if (valid($u->opaque)) {
1355             $uri = $u->canonical;
1356         } else {
1357             $status_msg = "You submitted an invalid email address. ";
1358             $uri = '';
1359         }
1360     } elsif ($u->scheme eq 'http' || $u->scheme eq 'https') {
1361         if ($u->authority =~ m!^.*@!) {
1362             $status_msg =
1363                 "Userids and passwords are not permitted in the URL field. ";
1364             $uri = '';
1365         } elsif ($u->authority =~ m!^\d! || $u->authority =~ m!^\[\d!) {
1366             $status_msg =
1367                 "IP addresses are not permitted in the URL field. ";
1368             $uri = '';
1369         } else {
1370             $uri = $u->canonical;
1371         }
1372     } else {
1373         $status_msg =
1374             "You specified an invalid scheme in the URL field; ";
1375         if ($allow_mailto) {
1376             $status_msg .=
1377                 "the only allowed schemes are 'http', 'https', and 'mailto'. ";
1378         } else {
1379             $status_msg .=
1380                 "the only allowed schemes are 'http' and 'https'. ";
1381         }
1382         $uri = '';
1383     }
1385     return $uri;
1388 # The following is taken from the Mail::RFC822::Address module, for
1389 # sites that don't have that module loaded.
1390 my $rfc822re;
1392 # Preloaded methods go here.
1393 my $lwsp = "(?:(?:\\r\\n)?[ \\t])";
1395 sub make_rfc822re {
1396 #   Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
1397 #   comment.  We must allow for lwsp (or comments) after each of these.
1398 #   This regexp will only work on addresses which have had comments stripped
1399 #   and replaced with lwsp.
1401     my $specials = '()<>@,;:\\\\".\\[\\]';
1402     my $controls = '\\000-\\031';
1404     my $dtext = "[^\\[\\]\\r\\\\]";
1405     my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$lwsp*";
1407     my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$lwsp)*\"$lwsp*";
1409 #   Use zero-width assertion to spot the limit of an atom.  A simple
1410 #   $lwsp* causes the regexp engine to hang occasionally.
1411     my $atom = "[^$specials $controls]+(?:$lwsp+|\\Z|(?=[\\[\"$specials]))";
1412     my $word = "(?:$atom|$quoted_string)";
1413     my $localpart = "$word(?:\\.$lwsp*$word)*";
1415     my $sub_domain = "(?:$atom|$domain_literal)";
1416     my $domain = "$sub_domain(?:\\.$lwsp*$sub_domain)*";
1418     my $addr_spec = "$localpart\@$lwsp*$domain";
1420     my $phrase = "$word*";
1421     my $route = "(?:\@$domain(?:,\@$lwsp*$domain)*:$lwsp*)";
1422     my $route_addr = "\\<$lwsp*$route?$addr_spec\\>$lwsp*";
1423     my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
1425     my $group = "$phrase:$lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
1426     my $address = "(?:$mailbox|$group)";
1428     return "$lwsp*$address";
1431 sub strip_comments {
1432     my $s = shift;
1433 #   Recursively remove comments, and replace with a single space.  The simpler
1434 #   regexps in the Email Addressing FAQ are imperfect - they will miss escaped
1435 #   chars in atoms, for example.
1437     while ($s =~ s/^((?:[^"\\]|\\.)*
1438                     (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)
1439                     \((?:[^()\\]|\\.)*\)/$1 /osx) {}
1440     return $s;
1443 #   valid: returns true if the parameter is an RFC822 valid address
1445 sub valid ($) {
1446     my $s = strip_comments(shift);
1448     if (!$rfc822re) {
1449         $rfc822re = make_rfc822re();
1450     }
1452     return $s =~ m/^$rfc822re$/so;
1459 # Default feedback templates.
1460 __DATA__
1461 html comment \n<div class="comment"><p>$feedback::name_link wrote at $feedback::date:</p>\n<blockquote>$feedback::comment</blockquote></div>
1462 html trackback \n<div class="trackback"><p>$feedback::blog_name mentioned this post in $feedback::title_link<?$feedback::excerpt eq="">.</p></?><?$feedback::excerpt ne="">:</p>\n<blockquote>$feedback::excerpt</blockquote></?></div>
1463 html commentform \n<form method="POST" action="$blosxom::url$blosxom::path/$blosxom::fn.$blosxom::flavour">\n<table><tr><td>Name:</td><td><input name="name" size="35" value="$feedback::name_preview"></td></tr>\n<tr><td>URL (optional):</td><td><input name="url" size="35" value="$feedback::url_preview"></td></tr>\n<tr><td>Comments:</td><td><textarea name="comment" rows="5" cols="60">$feedback::comment_preview</textarea></td></tr>\n<tr><td><input type="hidden" name="plugin" value="writeback"><input type="submit" name="submit" value="Preview"></td><td><input type="submit" name="submit" value="Post"></td></tr>\n</table></form>
1464 html trackbackinfo <p>URL for TrackBack pings: <code>$blosxom::url$blosxom::path/$blosxom::fn.$feedback::trackback_flavour</code></p>\n<!--\n<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/">\n<rdf:Description rdf:about="$blosxom::url$blosxom::path/$blosxom::fn.$blosxom::flavour" dc:identifier="$blosxom::url$blosxom::path/$blosxom::fn.$blosxom::flavour" dc:title="$blosxom::title" trackback:ping="$blosxom::url$blosxom::path/$blosxom::fn.$feedback::trackback_flavour" />\n</rdf:RDF>\n-->
1465 general comment \n<div class="comment"><p>$feedback::name_link wrote at $feedback::date:</p>\n<blockquote>$feedback::comment</blockquote></div>
1466 general trackback \n<div class="trackback"><p>$feedback::blog_name mentioned this post in $feedback::title_link<?$feedback::excerpt eq="">.</p></?><?$feedback::excerpt ne="">:</p>\n<blockquote>$feedback::excerpt</blockquote></?></div>
1467 general commentform \n<form method="POST" action="$blosxom::url$blosxom::path/$blosxom::fn.$blosxom::flavour">\n<table><tr><td>Name:</td><td><input name="name" size="35" value="$feedback::name_preview"></td></tr>\n<tr><td>URL (optional):</td><td><input name="url" size="35" value="$feedback::url_preview"></td></tr>\n<tr><td>Comments:</td><td><textarea name="comment" rows="5" cols="60">$feedback::comment_preview</textarea></td></tr>\n<tr><td><input type="hidden" name="plugin" value="writeback"><input type="submit" name="submit" value="Preview"></td><td><input type="submit" name="submit" value="Post"></td></tr>\n</table></form>
1468 general trackbackinfo <p>URL for TrackBack pings: <code>$blosxom::url$blosxom::path/$blosxom::fn.$feedback::trackback_flavour</code></p>\n<!--\n<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/">\n<rdf:Description rdf:about="$blosxom::url$blosxom::path/$blosxom::fn.$blosxom::flavour" dc:identifier="$blosxom::url$blosxom::path/$blosxom::fn.$blosxom::flavour" dc:title="$blosxom::title" trackback:ping="$blosxom::url$blosxom::path/$blosxom::fn.$feedback::trackback_flavour" />\n</rdf:RDF>\n-->
1469 trackback content_type application/xml
1470 trackback head
1471 trackback story $feedback::trackback_response
1472 trackback date
1473 trackback foot
1474 __END__
1476 =head1 NAME
1478 Blosxom Plug-in: feedback
1480 =head1 SYNOPSIS
1482 Provides comments and TrackBacks
1483 (C<http://www.movabletype.org/trackback/>); also supports comment and
1484 TrackBack moderation and spam filtering using Akismet and/or
1485 MT-Blacklist (deprecated). Inspired by the original writeback plug-in
1486 and the various enhanced versions of it.
1488 Comments and TrackBack pings for a particular story are kept in
1489 C<$fb_dir/$path/$filename.wb>.
1491 =head1 QUICK START
1493 Drop this feedback plug-in file into your plug-ins directory (whatever
1494 you set as C<$plugin_dir> in C<blosxom.cgi>), and modify the file to
1495 set the configurable variable C<$fb_dir>. You must also modify the
1496 variable C<$wordpress_api_key> if you are using the Akismet spam
1497 blacklist service, the variable C<$blacklist_file> if you are using
1498 the MT-Blacklist file (deprecated), and the variables C<$address> and
1499 C<$smtp_server> if you want feedback notification or moderation. (See
1500 below for more information on these optional features.)
1502 Note that by default all comments and TrackBacks are allowed, with no
1503 spam checking, moderation, or notification.
1505 Modify your story template (e.g., C<story.html> in your Blosxom data
1506 directory) to include the variables C<$feedback::comments> and
1507 C<$feedback::trackbacks> at the points where you'd like comments and
1508 trackbacks to be inserted.
1510 Modify your story template or foot template (e.g., C<foot.html> in
1511 your Blosxom data directory) to include the variables
1512 C<$feedback::comment_response>, C<$feedback::preview>,
1513 C<$feedback::commentform> and C<$feedback::trackbackinfo> at the
1514 points where you'd like to insert the response to a submitted comment,
1515 the previewed comment (if any), the comment submission form and the
1516 TrackBack information (including TrackBack auto-discovery code).
1518 =head1 CONFIGURATION
1520 By default C<$fb_dir> is set to put the feedback directory and its
1521 contents in the plug-in state directory. (For example, if
1522 C<$plugin_state_dir> is C</foo/blosxom/state> then the feedback
1523 directory C<$fb_dir> is set to C</foo/blosxom/state/feedback>.)
1524 However a better approach may be to keep the feedback directory at the
1525 same level as C<$datadir>. (For example, if C<$datadir> is
1526 C</foo/blosxom/data> then use C</foo/blosxom/feedback> for the
1527 feedback directory.)  This helps ensure that you don't accidentally
1528 delete previously-submitted comments and TrackBacks (e.g., if you
1529 clean out the plug-in state directory).
1531 Once C<$fb_dir> is set, the next time you visit your site the feedback
1532 plug-in will perform some checks, creating the directory C<$fb_dir>
1533 and setting appropriate permissions on the directory if it doesn't
1534 already exist.  (Check your web server error log for details of what's
1535 happening behind the scenes.)
1537 Set the variables C<$allow_comments> and C<$allow_trackbacks> to
1538 enable or disable comments and/or TrackBacks; by default the plug-in
1539 allows both comments and TrackBacks to be submitted. The variables
1540 C<$comment_period> and C<$trackback_period> specify the amount of time
1541 after a story is published (or updated) during which comments or
1542 TrackBacks may be submitted (90 days by default); set these variables
1543 to zero to allow submission of feedback at any time after publication.
1545 Set the variables C<$akismet_comments> and C<$akismet_trackbacks> to
1546 enable or disable checking of comments and/or TrackBacks against the
1547 Akismet spam blacklist service (C<http://www.akismet.com>). If Akismet
1548 checking is enabled then you must also set C<$wordpress_api_key> to
1549 your personal WordPress API key, which is required to connect to the
1550 Akismet service. (You can obtain a WordPress API key by registering
1551 for a free blog at C<http://www.wordpress.com>; as a side effect of
1552 registering you will get an API key that you can then use on any of
1553 your blogs, whether they're hosted at wordpress.com or not.)
1555 Set the variables C<$blacklist_comments> and C<$blacklist_trackbacks>
1556 to enable or disable checking of comments and/or TrackBacks against
1557 the MT-Blacklist file. If blacklist checking is enabled then you must
1558 also set C<$blacklist_file> to a valid value. (Note that in the past
1559 you could get a copy of the MT-Blacklist file from
1560 C<http://www.jayallen.org/comment_spam/blacklist.txt>; however that
1561 URL is no longer active and no one is currently maintaining the
1562 MT-Blacklist file. We are therefore deprecating use of the
1563 MT-Blacklist file, except for people who already have a copy of the
1564 file and are currently using it; we suggest using Akismet instead.)
1566 Set the variables C<$notify_comments> and C<$notify_trackbacks> to
1567 enable or disable sending an email message to you each time a new
1568 comment and/or TrackBack is submitted. If notification is enabled then
1569 you must set C<$address> and C<$smtp_server> to valid values.
1570 Typically you would set C<$address> to your own email address (e.g.,
1571 'jdoe@example.com') and C<$smtp_server> to the fully-qualified domain
1572 name of the SMTP server you normally use to send outbound mail from
1573 your email account (e.g., 'smtp.example.com').
1575 Set the variables C<$moderate_comments> and C<$moderate_trackbacks> to
1576 enable or disable moderation of comments and/or TrackBacks; moderation
1577 is done by sending you an email message with the submitted comment or
1578 TrackBack and links on which you can click to approve or reject the
1579 comment or TrackBack. If moderation is enabled then you must set
1580 C<$address> and C<$smtp_server> to valid values; see the discussion of
1581 notification above for more information.
1583 =head1 FLAVOUR TEMPLATE VARIABLES
1585 Unlike Rael Dornfest's original writeback plug-in, this plug-in does
1586 not require or assume that you will be using a special Blosxom flavour
1587 (e.g., the 'writeback' flavour) in order to display comments with
1588 stories. Instead you can display comments and/or TrackBacks with any
1589 flavour whatsoever (except the 'trackback' flavour, which is reserved
1590 for use with TrackBack pings). Also unlike the original writeback
1591 plug-in, this plug-in separates display of comments from display of
1592 TrackBacks and allows them to be formatted in different ways.
1594 Insert the variables C<$feedback::comments> and/or
1595 C<$feedback::trackbacks> into the story template for the flavour or
1596 flavours for which you wish comments and/or TrackBacks to be displayed
1597 (e.g., C<story.html>). Note that the plug-in will automatically set
1598 these variables to undefined values unless the page being displayed is
1599 for an individual story.
1601 Insert the variables C<$feedback::comments_count> and/or
1602 C<$feedback::trackbacks_count> into the story templates where you wish
1603 to display a count of the comments and/or TrackBacks for a particular
1604 story. Note that these variables are available on all pages, including
1605 index and archive pages. As an alternative you can use the variable
1606 C<$feedback::count> to display the combined total number of comments
1607 and TrackBacks (analogous to the variable C<$writeback::count> in the
1608 original writeback plug-in).
1610 Insert the variables C<$feedback::commentform> and
1611 C<$feedback::trackbackinfo> into your story or foot template for the
1612 flavour or flavours for which you want to enable submission of
1613 comments and/or TrackBacks (e.g., C<foot.html>);
1614 C<$feedback::commentform> is an HTML form for comment submission,
1615 while C<$feedback::trackbackinfo> displays the URL for TrackBack pings
1616 and also includes RDF code to support auto-discovery of the TrackBack
1617 ping URL. Note that the plug-in sets C<$feedback::commentform> and
1618 C<$feedback::trackbackinfo> to be undefined unless the page being
1619 displayed is for an individual story.
1621 The plug-in also sets C<$feedback::commentform> and/or
1622 C<$feedback::trackbackinfo> to be undefined if comments and/or
1623 TrackBacks have been disabled globally (i.e., using C<$allow_comments>
1624 or C<$allow_trackbacks>). However if comments or TrackBacks are closed
1625 because the story is older than the time set using C<$comment_period>
1626 or C<$trackback_period> then the plug-in sets C<$feedback::commentform>
1627 or C<$feedback::trackbackinfo> to display an appropriate message.
1629 Insert the variable C<$feedback::comment_response> into your story or
1630 foot template to display a message indicating the results of
1631 submitting or moderating a comment. Note that
1632 C<$feedback::comment_response> has an undefined value unless the
1633 displayed page is in response to a POST request containing a comment
1634 submission (i.e., using the 'Post' or 'Preview' buttons) or a GET
1635 request containing a moderator approval or rejection.
1637 Insert the variable C<$feedback::preview> into your story or foot
1638 template at the point at which you'd like a previewed comment to be
1639 displayed. Note that C<$feedback::preview> will be undefined except on
1640 an individual story page displayed in response to a comment submission
1641 using the 'Preview' button.
1643 =head1 COMMENT AND TRACKBACK TEMPLATES
1645 This plug-in uses a number of flavour templates to format comments and
1646 TrackBacks; the plug-in contains a full set of default templates for
1647 use with the 'html' flavour, as well as a full set of 'general'
1648 templates used as a default for other flavours. You can also supply
1649 your own comment and TrackBack templates in the same way that you can
1650 define other Blosxom templates, by putting appropriately-named
1651 template files into the Blosxom data directory (or one or more of its
1652 subdirectories, if you want different templates for different
1653 categories).
1655 The templates used for displaying comments and TrackBacks are
1656 analogous to the story template used for displaying stories; the
1657 templates are used for each and every comment or TrackBack displayed
1658 on a page:
1660 =over
1662 =item
1664 comment template (e.g., C<comment.html>). This template contains the
1665 content to be displayed for each comment (analogous to the writeback
1666 template used in the original writeback plug-in). Within this template
1667 you can use the variables C<$feedback::name> (name of the comment
1668 submitter), C<$feedback::url> (URL containing the comment submitter's
1669 email address or web site), C<$feedback::date> (date/time the comment
1670 was submitted), and C<$feedback::comment> (the comment itself). You
1671 can also use the variable C<$feedback::name_link>, which combines
1672 C<feedback::name> and C<$feedback::url> to create an (X)HTML link if
1673 the commenter supplied a URL, and otherwise is the same as
1674 C<$feedback::name>. Note that this template is also used for previewed
1675 comments.
1677 =item
1679 trackback template (e.g., C<trackback.html>). This template contains
1680 the content to be displayed for each TrackBack (analogous to the
1681 writeback template used in the original writeback plug-in). Within
1682 this template you can use the variables C<$feedback::blog_name> (name
1683 of the blog submitting the TrackBack), C<$feedback::title> (title of
1684 the blog post making the TrackBack), C<$feedback::url> (URL for the
1685 blog post making the TrackBack), C<$feedback::date> (date/time the
1686 TrackBack was submitted), and C<$feedback::excerpt> (an excerpt from
1687 the blog post making the TrackBack). You can also use the variable
1688 C<$feedback::title_link>, which combines C<$feedback::title> and
1689 C<$feedback::url> and is analogous to C<$feedback::name_link>.
1691 =back
1693 The feedback plug-in also uses the following templates:
1695 =over
1697 =item
1699 commentform template (e.g., C<commentform.html>). This template
1700 provides a form for submitting a comment. The default template
1701 contains a form containing fields for the submitter's name, email
1702 address or URL, and the comment itself; submitting the form initiates
1703 a POST request to the same URL (and Blosxom flavour) used in
1704 displaying the page on which the form appears. If you define your own
1705 commentform template note that the plug-in requires the presence of a
1706 'plugin' hidden form variable with the value set to 'writeback'; this
1707 tells the plug-in that it should handle the incoming data from the POST
1708 request rather than leaving it for another plug-in. Also note that in
1709 order to support both comment posting and previewing the form has two
1710 buttons, both with name 'submit' and with values 'Post' and 'Preview'
1711 respectively; if you change these names and values then you must
1712 change the plug-in's code.
1714 =item
1716 trackbackinfo template (e.g., C<trackbackinfo.html>). This template
1717 provides information for how to go about submitting a TrackBack. The
1718 default template provides both a displayed reference to the TrackBack
1719 ping URL and non-displayed RDF code by which other systems can
1720 auto-discover the TrackBack ping URL.
1722 =back
1724 =head1 SECURITY
1726 This plug-in has at least the following security-related issues, which
1727 we attempt to address as described:
1729 =over
1731 =item
1733 The plug-in handles POST and GET requests with included parameters of
1734 potentially arbitrary length. To help minimize the possibility of
1735 problems (e.g., buffer overruns) the plug-in truncates all parameters
1736 to a maximum length (currently 10,000 bytes).
1738 =item
1740 People can submit arbitrary content as part of a submitted comment or
1741 TrackBack ping, with that content then being displayed as part of the
1742 page viewed by other users. To help minimize the possibility of
1743 attacks involving injection of arbitrary page content, the plug-in
1744 "sanitizes" any submitted HTML/XHTML content by converting the '<'
1745 character and other problematic characters (including '>' and the
1746 double quote character) to the corresponding HTML/XHTML character
1747 entities. The plug-in also sanitizes submitted URLs by URL-encoding
1748 characters that are not permitted in a URL.
1750 =item
1752 When using moderation, comments or TrackBacks are approved (or
1753 rejected) by invoking a GET (or HEAD) request using the URL of the
1754 story to which the comment or TrackBack applies, with the URL having
1755 some additional parameters to signal whether the comment should be
1756 approved or rejected. Since the feedback plug-in does not track (much
1757 less validate) the source of the moderation request, in theory
1758 spammers could approve their own comments or TrackBacks simply by
1759 following up their feedback submission with a GET request of the
1760 proper form. To minimize the possibility of this happening we generate
1761 a random eight-character alphanumeric key for each submitted comment
1762 or TrackBack, and require that that key be supplied in the approval or
1763 rejection request. This provides reasonable protection assuming that a
1764 spammer is not intercepting and reading your personal email (since the
1765 key is included in the moderation email message).
1767 =back
1769 =head1 VERSION
1771 0.23
1773 =head1 AUTHOR
1775 This plug-in was created by Frank Hecker, hecker@hecker.org; it was
1776 based on and inspired by the original writeback plug-in by Rael
1777 Dornfest together with modifications made by Fletcher T. Penney, Doug
1778 Alcorn, Kevin Scaldeferri, and others.
1780 This plugin is now maintained by the Blosxom Sourceforge Team,
1781 <blosxom-devel@lists.sourceforge.net>.
1783 =head1 SEE ALSO
1785 More on the feedback plug-in: http://www.hecker.org/blosxom/feedback
1787 Blosxom Home/Docs/Licensing: http://blosxom.sourceforge.net/
1789 Blosxom Plugin Docs: http://blosxom.sourceforge.net/documentation/users/plugins.html
1791 =head1 BUGS
1793 None known; please send bug reports and feedback to the Blosxom
1794 development mailing list <blosxom-devel@lists.sourceforge.net>.
1796 =head1 LICENSE
1798 The feedback plug-in
1799 Copyright 2003-2006 Frank Hecker, Rael Dornfest, Fletcher T. Penney,
1800                     Doug Alcorn, Kevin Scaldeferri, and others
1802 Permission is hereby granted, free of charge, to any person obtaining a
1803 copy of this software and associated documentation files (the "Software"),
1804 to deal in the Software without restriction, including without limitation
1805 the rights to use, copy, modify, merge, publish, distribute, sublicense,
1806 and/or sell copies of the Software, and to permit persons to whom the
1807 Software is furnished to do so, subject to the following conditions:
1809 The above copyright notice and this permission notice shall be included
1810 in all copies or substantial portions of the Software.
1812 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1813 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1814 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1815 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1816 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1817 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1818 OTHER DEALINGS IN THE SOFTWARE.