tagging: Allow using titles in for related stories.
[matthijs/upstream/blosxom-plugins.git] / general / feedback
1 # Blosxom Plug-in: feedback
2 # Author: Frank Hecker (http://www.hecker.org/)
3 #
4 # Version 0.23
5
6 package feedback;
7
8 use warnings;
9
10
11 # --- Configurable variables ---
12
13 # --- You *must* set the following variables properly for your blog ---
14
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";
21
22
23 # --- Set the following variables according to your preferences ---
24
25 # Are comments and TrackBacks allowed? Set to zero to disable either or both.
26 my $allow_comments = 1;
27 my $allow_trackbacks = 1;
28
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
33
34 # Do Akismet checking of comments and/or TrackBacks for spam.
35 my $akismet_comments = 0;
36 my $akismet_trackbacks = 0;
37
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 = '';
41
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;
47
48 # Where can I find the local copy of the MT-Blacklist file?
49 my $blacklist_file = "$blosxom::plugin_state_dir/blacklist.txt";
50
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;
57
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';
61
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";
66
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';
70
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;
74
75
76 # --- You should not normally need to change the following variables ---
77
78 # What flavour should I consider an incoming TrackBack ping?
79 $trackback_flavour = "trackback";
80
81 # What file extension should I use for saved comments and TrackBacks?
82 my $fb_extension = "wb";
83
84 # What fields are used in the comments form?
85 my @comment_fields = qw! name url comment !;
86
87 # What fields are used by TrackBacks?
88 my @trackback_fields = qw! blog_name url title excerpt !;
89
90 # Limit all fields to this length or less (just in case).
91 my $max_param_length = 10000;
92
93
94 # --- Variables for use in flavour templates (e.g., as $feedback::foo) ---
95
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
107
108 # Field values for previewed comments, used in the commentform template.
109 $name_preview = '';
110 $comment_preview = '';
111 $url_preview = '';
112
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 = '';
117
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 = '';
122
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 = '';
128
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
134
135 # Previewed comment for a particular story, for use in the story
136 # template for an individual story page.
137 $preview = '';
138
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 = '';
144
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 = '';
150
151
152 # --- External modules required ---
153
154 use CGI qw/:standard/;
155 use FileHandle;
156 use URI;
157 use URI::Escape;
158
159
160 # --- Global variables (used in interpolation) ---
161
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 !;
167
168
169 # --- Private static variables ---
170
171 # Spam blacklist array.
172 my @blacklist_entries = ();
173
174 # File handle for use in reading/writing the feedback file, etc.
175 my $fh = new FileHandle;
176
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 = '';
181
182 # Whether comments or TrackBacks are closed for a given story.
183 my $closed_comments = 0;
184 my $closed_trackbacks = 0;
185
186
187 # --- Plug-in initialization ---
188
189 # Strip potentially confounding final slash from feedback directory path.
190 $fb_dir =~ s!/$!!;
191
192 # Strip potentially confounding initial period from file extension.
193 $fb_extension =~ s!^\.!!;
194
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;
206 }
207
208 # Moderation implies notification.
209 $notify_comments = 1 if $moderate_comments;
210 $notify_trackbacks = 1 if $moderate_trackbacks;
211
212
213 # --- Plug-in subroutines ---
214
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     }
224
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     }
232
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     }
237
238     return 1;
239 }
240
241
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) = @_;
245
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.
251
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     }
259
260     # $trackback_period works the same way as $comment_period.
261
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     }
269
270     return 1;
271 }
272
273
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.
277
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;
283
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.
297
298     # Make $path empty if at top level, preceded by a single slash otherwise.
299     !defined($path) and $path = "";
300     $path =~ s!^/*!!; $path &&= "/$path";
301
302     # Set feedback path and filename for this story.
303     $fb_path = $path;
304     $fb_fn = $filename . '.' . $fb_extension;
305
306     # Determine whether this is an individual story page or not.
307     $is_story_page =
308         $blosxom::path_info =~ m!^(.*/)?(.+)\.(.+)$! ? 1 : 0;
309
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.
318
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     }
332
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.
336
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;
346
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     }
352
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.)
356
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     }
371
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.
375
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     }
390
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.
394
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;
401
402     return 1;
403 }
404
405
406 # --- Helper subroutines ---
407
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;
412
413 sub process_submission {
414     my $submission_type = '';
415     my $status_msg = '';
416
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.
420
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).
428
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.
434
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'));
452
453         if ($moderate_param) {
454             # We have two possible cases: moderator approval or rejection,
455             # distinguished based on the value of the 'moderate' parameter.
456
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     }
473
474     return $submission_type, $status_msg;
475 }
476
477
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.
485
486 sub get_feedback {
487     my $path = shift;
488     my ($comments, $comments_count, $trackbacks, $trackbacks_count);
489
490     $comments = $trackbacks = '';
491     $comments_count = $trackbacks_count = 0;
492
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');
497
498     my $trackback_template =
499         &$blosxom::template($path, 'trackback', $blosxom::flavour)
500         || &$blosxom::template($path, 'trackback', 'general');
501
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.
505
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'});
516
517                     my $cmt = $comment_template;
518                     $cmt = &$blosxom::interpolate($cmt);
519
520                     $comments .= $cmt;
521                     $comments_count++;
522                 } else {
523
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'});
529
530                     my $trackback = $trackback_template;
531                     $trackback = &$blosxom::interpolate($trackback);
532
533                     $trackbacks .= $trackback;
534                     $trackbacks_count++;
535                 }
536                 %param = ();
537             }
538         }
539     }
540
541     return ($comments, $comments_count, $trackbacks, $trackbacks_count);
542 }
543
544
545 # Retrieve comments and TrackBacks for a story and (just) count them.
546
547 sub count_feedback {
548     my $comments_count = 0;
549     my $trackbacks_count = 0;
550
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.
554
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     }
569
570     return ($comments_count, $trackbacks_count);
571 }
572
573
574 # Format a previewed comment according to the appropriate preview template
575 # for the story (based on the story's path).
576
577 sub get_preview {
578     my $path = shift;
579     my $preview = '';
580
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');
585
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);
591
592     $preview = &$blosxom::interpolate($comment_template);
593
594     return $preview;
595 }
596
597
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;
605
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;
611
612     warn "feedback: feedback is enabled!\n";
613     return 1;
614 }
615
616
617 # Create subdirectories of feedback directory as necessary.
618 sub mk_feedback_subdir {
619     my $dir = shift;
620     my $p = '';
621
622     return 1 if !defined($dir) or $dir eq '';
623
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     }
630
631     return 1;
632 }
633
634
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;
642
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     }
654
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;
663
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     }
678
679     # Filter out the "good" fields from the CGI parameters.
680     my %params = copy_params(\@fields);
681
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     }
693
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.
698
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     }
711
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).
717
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;
723
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     }
729
730     return $status_msg;
731 }
732
733
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;
739
740     foreach my $key (@$fields_ref) {
741         my $value = substr(param($key), 0, $max_param_length) || "";
742
743         # Eliminate leading and trailing whitespace, use carriage returns
744         # as line delimiters, and collapse multiple blank lines into one.
745
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;
750
751         $params{$key} = $value;
752     }
753
754     return %params;
755 }
756
757
758 # Send notification or moderation email to blog owner.
759 sub send_notification {
760     my ($params_ref, $moderate, $fb_item) = @_;
761
762     unless ($address && $smtp_server) {
763         warn "feedback: No address or SMTP server for notifications\n";
764         return 0;
765     }
766
767     my $message = "New feedback for your post \"$blosxom::title\" ("
768         . $blosxom::path_info . "):\n\n";
769
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     }
786
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;
792
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";
796
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     }
801
802     # Load Net::SMTP module only now that it's needed.
803     require Net::SMTP; Net::SMTP->import;
804
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;
817
818     return 1;
819 }
820
821
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.)
826
827 sub format_date {
828     my $date_value = shift;
829
830     if ($date_value =~ m!^\d+$!) {
831         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
832             localtime($date_value);
833         $year += 1900;
834
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     }
841 }
842
843
844 # Format the name used in comments.
845 sub format_name {
846     my ($name, $url) = @_;
847
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     }
856
857     # Link to a URL if one was provided.
858     my $name_link =
859         $url ? "<a href=\"$url\" rel=\"nofollow\">$name</a>" : $name ;
860
861     return $name, $name_link;
862 }
863
864
865 # Format the comment response message.
866 sub format_cmt_response {
867     my $response = shift;
868
869     # Clean up the response.
870     $response =~ s/^\s+//;
871     $response =~ s/\s+$//;
872
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     }
880
881     return $response;
882 }
883
884
885 # Format the TrackBack response message.
886 sub format_tb_response {
887     my $response = shift;
888
889     # Clean up the response.
890     $response =~ s/^\s+//;
891     $response =~ s/\s+$//;
892
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.
897
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     }
906
907     return $response;
908 }
909
910
911 # Format the comment itself.
912 sub format_comment {
913     my $comment = shift;
914
915     # TODO: Support other comment formats such as Textile.
916
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     }
927
928     return $comment;
929 }
930
931
932 # Format the blog name used in TrackBacks.
933 sub format_blog_name {
934     my $blog_name = shift;
935
936     $blog_name or $blog_name = $default_blog_name;
937
938     return $blog_name;
939 }
940
941
942 # Format the title used in TrackBacks.
943 sub format_title {
944     my ($title, $url) = @_;
945     my $title_link;
946
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     }
954
955     return $title, $title_link;
956 }
957
958
959 # Format the TrackBack excerpt.
960 sub format_excerpt {
961     my $excerpt = shift;
962
963     # TODO: Truncate excerpts at some reasonable length.
964
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     }
970
971     return $excerpt;
972 }
973
974
975 # Read in the MT-Blacklist file.
976 sub read_blacklist {
977
978     # No need to do anything if we've already read in the blacklist file.
979     return 1 if @blacklist_entries;
980
981     # Try to find the blacklist file and open it.
982     open BLACKLIST, "$blacklist_file"
983         or die "Can't read '$blacklist_file', $!\n";
984
985     my @lines = grep {! /^\s*\#/ } <BLACKLIST>;
986     close BLACKLIST;
987     die "No blacklists?\n" unless @lines;
988
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;
996
997     return 1;
998 }
999
1000
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) = @_;
1004
1005     # Perform a series of spam tests. If any show positive then reject.
1006
1007     # Does the host part of the URL reference an IP address?
1008     return 1 if uses_ipaddr($$params_ref{'url'});
1009
1010     # Does the comment or TrackBack match against the Akismet service?
1011     return 1 if $akismet && matches_akismet($params_ref, $is_comment);
1012
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));
1017
1018     # TODO: Add other useful spam checks.
1019
1020     # Got by all the tests, so assume it's not spam.
1021     return 0;
1022 }
1023
1024
1025 # Check host part of URL to see if it is an IP address.
1026 sub uses_ipaddr {
1027     my $uri = shift;
1028
1029     return 0 unless $uri;
1030
1031     # Construct URI object.
1032     my $u = URI->new($uri);
1033
1034     # Return if this not actually a URI (i.e., it's an email address).
1035     return 0 unless defined($u->scheme);
1036
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     }
1043
1044     return 0;
1045 }
1046
1047
1048 # Check comment or TrackBack against the Akismet online service.
1049 sub matches_akismet {
1050     my ($params_ref, $is_comment) = @_;
1051
1052     # Load Net:Akismet module only now that it's needed.
1053     require Net::Akismet; Net::Akismet->import;
1054
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     }
1062
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.
1065
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     }
1079
1080     # Is it spam?
1081     return 1 if $akismet->check(%fields) eq 'true';
1082
1083     # Apparently not.
1084     return 0;
1085 }
1086
1087
1088 # Check comment or TrackBack against the MT-Blacklist file (deprecated).
1089 sub matches_blacklist {
1090     my $params_string = shift;
1091
1092     # Read in the blacklist file.
1093     read_blacklist();
1094
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     }
1100
1101     return 0;
1102 }
1103
1104
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.
1109
1110 sub save_feedback {
1111     my ($params_ref, $moderate) = @_;
1112     my $fb_item = '';
1113     my $feedback_fn = '';
1114     my $status_msg = '';
1115
1116     # Clear values used to prefill commentform.
1117     $name_preview = $url_preview = $comment_preview = '';
1118
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     }
1124
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     }
1133
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     }
1140
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};
1144
1145         # Eliminate leading and trailing whitespace, use carriage returns
1146         # as line delimiters, and collapse multiple blank lines into one.
1147
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;
1152
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         }
1159
1160         print $fh "$key: $value\n";
1161     }
1162
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";
1166
1167     # End the entry and close the file.
1168     print $fh "-----\n";
1169     $fh->close();
1170
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     }
1181 }
1182
1183
1184 # Generate random alphanumeric string of the specified length.
1185 sub rand_alphanum {
1186     my $size = shift;
1187     return '' if $size <= 0;
1188
1189     my @alphanumeric = ('a'..'z', 'A'..'Z', 0..9);
1190     return join '', map $alphanumeric[rand @alphanumeric], 0..$size;
1191 }
1192
1193
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;
1199
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};
1203
1204         # Eliminate leading and trailing whitespace, use carriage returns
1205         # as line delimiters, and collapse multiple blank lines into one.
1206
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;
1211
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         }
1218
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     }
1227
1228     # Save the date/time (in seconds) as well.
1229     $date_preview = time();
1230
1231     # Set response to indicate success.
1232     $status_msg .=
1233         "Please review your previewed comment below and submit it when "
1234         . "you are ready.";
1235
1236     return $status_msg;
1237 }
1238
1239
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 = '';
1245
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     }
1255
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     }
1263
1264     my @new_feedback = ();
1265     while (<$fh>) {
1266         push @new_feedback, $_;
1267     }
1268     $fh->close();
1269
1270     # Attempt to open the story's feedback file and append to it.
1271     # TODO: Try to make this more resistant to race conditions.
1272
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     }
1279
1280     foreach my $line (@new_feedback) {
1281         print $fh $line;
1282     }
1283
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";
1290
1291     # Set response to indicate successful approval.
1292     $status_msg = "Feedback '$item' approved by moderator. ";
1293
1294     return $status_msg;
1295 }
1296
1297
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;
1303
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     }
1313
1314     # TODO: Optionally report comment or TrackBack to Akismet as spam.
1315
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";
1321
1322     # Set response to indicate successful rejection.
1323     $status_msg = "Feedback '$item' rejected by moderator.";
1324
1325     return $status_msg;
1326 }
1327
1328
1329 # Sanitize a query parameter to remove unexpected characters.
1330 sub sanitize_param
1331 {
1332     my $param = shift || '';
1333
1334     # Allow only alphanumeric, underscore, dash, and period.
1335     $param and $param =~ s/[^-.\w]/_/go;
1336
1337     return $param;
1338 }
1339
1340
1341 # Sanitize a URI.
1342 sub sanitize_uri {
1343     my $uri = shift;
1344
1345     # Construct URI object.
1346     my $u = URI->new($uri);
1347
1348     # If it's not a URI then assume it's an email address.
1349     $u->scheme('mailto') unless defined($u->scheme);
1350
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     }
1384
1385     return $uri;
1386 }
1387
1388 # The following is taken from the Mail::RFC822::Address module, for
1389 # sites that don't have that module loaded.
1390 my $rfc822re;
1391
1392 # Preloaded methods go here.
1393 my $lwsp = "(?:(?:\\r\\n)?[ \\t])";
1394
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.
1400
1401     my $specials = '()<>@,;:\\\\".\\[\\]';
1402     my $controls = '\\000-\\031';
1403
1404     my $dtext = "[^\\[\\]\\r\\\\]";
1405     my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$lwsp*";
1406
1407     my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$lwsp)*\"$lwsp*";
1408
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)*";
1414
1415     my $sub_domain = "(?:$atom|$domain_literal)";
1416     my $domain = "$sub_domain(?:\\.$lwsp*$sub_domain)*";
1417
1418     my $addr_spec = "$localpart\@$lwsp*$domain";
1419
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)";
1424
1425     my $group = "$phrase:$lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
1426     my $address = "(?:$mailbox|$group)";
1427
1428     return "$lwsp*$address";
1429 }
1430
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.
1436
1437     while ($s =~ s/^((?:[^"\\]|\\.)*
1438                     (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)
1439                     \((?:[^()\\]|\\.)*\)/$1 /osx) {}
1440     return $s;
1441 }
1442
1443 #   valid: returns true if the parameter is an RFC822 valid address
1444 #
1445 sub valid ($) {
1446     my $s = strip_comments(shift);
1447
1448     if (!$rfc822re) {
1449         $rfc822re = make_rfc822re();
1450     }
1451
1452     return $s =~ m/^$rfc822re$/so;
1453 }
1454
1455
1456 1;
1457
1458
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__
1475
1476 =head1 NAME
1477
1478 Blosxom Plug-in: feedback
1479
1480 =head1 SYNOPSIS
1481
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.
1487
1488 Comments and TrackBack pings for a particular story are kept in
1489 C<$fb_dir/$path/$filename.wb>.
1490
1491 =head1 QUICK START
1492
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.)
1501
1502 Note that by default all comments and TrackBacks are allowed, with no
1503 spam checking, moderation, or notification.
1504
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.
1509
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).
1517
1518 =head1 CONFIGURATION
1519
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).
1530
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.)
1536
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.
1544
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.)
1554
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.)
1565
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').
1574
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.
1582
1583 =head1 FLAVOUR TEMPLATE VARIABLES
1584
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.
1593
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.
1600
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).
1609
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.
1620
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.
1628
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.
1636
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.
1642
1643 =head1 COMMENT AND TRACKBACK TEMPLATES
1644
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).
1654
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:
1659
1660 =over
1661
1662 =item
1663
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.
1676
1677 =item
1678
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>.
1690
1691 =back
1692
1693 The feedback plug-in also uses the following templates:
1694
1695 =over
1696
1697 =item
1698
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.
1713
1714 =item
1715
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.
1721
1722 =back
1723
1724 =head1 SECURITY
1725
1726 This plug-in has at least the following security-related issues, which
1727 we attempt to address as described:
1728
1729 =over
1730
1731 =item
1732
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).
1737
1738 =item
1739
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.
1749
1750 =item
1751
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).
1766
1767 =back
1768
1769 =head1 VERSION
1770
1771 0.23
1772
1773 =head1 AUTHOR
1774
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.
1779
1780 This plugin is now maintained by the Blosxom Sourceforge Team,
1781 <blosxom-devel@lists.sourceforge.net>.
1782
1783 =head1 SEE ALSO
1784
1785 More on the feedback plug-in: http://www.hecker.org/blosxom/feedback
1786
1787 Blosxom Home/Docs/Licensing: http://blosxom.sourceforge.net/
1788
1789 Blosxom Plugin Docs: http://blosxom.sourceforge.net/documentation/users/plugins.html
1790
1791 =head1 BUGS
1792
1793 None known; please send bug reports and feedback to the Blosxom
1794 development mailing list <blosxom-devel@lists.sourceforge.net>.
1795
1796 =head1 LICENSE
1797
1798 The feedback plug-in
1799 Copyright 2003-2006 Frank Hecker, Rael Dornfest, Fletcher T. Penney,
1800                     Doug Alcorn, Kevin Scaldeferri, and others
1801
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:
1808
1809 The above copyright notice and this permission notice shall be included
1810 in all copies or substantial portions of the Software.
1811
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.