factor out a userpage function
[ikiwiki.git] / IkiWiki / Plugin / comments.pm
1 #!/usr/bin/perl
2 # Copyright © 2006-2008 Joey Hess <joey@ikiwiki.info>
3 # Copyright © 2008 Simon McVittie <http://smcv.pseudorandom.co.uk/>
4 # Licensed under the GNU GPL, version 2, or any later version published by the
5 # Free Software Foundation
6 package IkiWiki::Plugin::comments;
7
8 use warnings;
9 use strict;
10 use IkiWiki 3.00;
11 use Encode;
12 use POSIX qw(strftime);
13
14 use constant PREVIEW => "Preview";
15 use constant POST_COMMENT => "Post comment";
16 use constant CANCEL => "Cancel";
17
18 my $postcomment;
19 my %commentstate;
20
21 sub import {
22         hook(type => "checkconfig", id => 'comments',  call => \&checkconfig);
23         hook(type => "getsetup", id => 'comments',  call => \&getsetup);
24         hook(type => "preprocess", id => 'comment', call => \&preprocess);
25         # here for backwards compatability with old comments
26         hook(type => "preprocess", id => '_comment', call => \&preprocess);
27         hook(type => "sessioncgi", id => 'comment', call => \&sessioncgi);
28         hook(type => "htmlize", id => "_comment", call => \&htmlize);
29         hook(type => "pagetemplate", id => "comments", call => \&pagetemplate);
30         hook(type => "formbuilder_setup", id => "comments", call => \&formbuilder_setup);
31         # Load goto to fix up user page links for logged-in commenters
32         IkiWiki::loadplugin("goto");
33         IkiWiki::loadplugin("inline");
34 }
35
36 sub getsetup () {
37         return
38                 plugin => {
39                         safe => 1,
40                         rebuild => 1,
41                 },
42                 comments_pagespec => {
43                         type => 'pagespec',
44                         example => 'blog/* and !*/Discussion',
45                         description => 'PageSpec of pages where comments are allowed',
46                         link => 'ikiwiki/PageSpec',
47                         safe => 1,
48                         rebuild => 1,
49                 },
50                 comments_closed_pagespec => {
51                         type => 'pagespec',
52                         example => 'blog/controversial or blog/flamewar',
53                         description => 'PageSpec of pages where posting new comments is not allowed',
54                         link => 'ikiwiki/PageSpec',
55                         safe => 1,
56                         rebuild => 1,
57                 },
58                 comments_pagename => {
59                         type => 'string',
60                         default => 'comment_',
61                         description => 'Base name for comments, e.g. "comment_" for pages like "sandbox/comment_12"',
62                         safe => 0, # manual page moving required
63                         rebuild => undef,
64                 },
65                 comments_allowdirectives => {
66                         type => 'boolean',
67                         example => 0,
68                         description => 'Interpret directives in comments?',
69                         safe => 1,
70                         rebuild => 0,
71                 },
72                 comments_allowauthor => {
73                         type => 'boolean',
74                         example => 0,
75                         description => 'Allow anonymous commenters to set an author name?',
76                         safe => 1,
77                         rebuild => 0,
78                 },
79                 comments_commit => {
80                         type => 'boolean',
81                         example => 1,
82                         description => 'commit comments to the VCS',
83                         # old uncommitted comments are likely to cause
84                         # confusion if this is changed
85                         safe => 0,
86                         rebuild => 0,
87                 },
88 }
89
90 sub checkconfig () {
91         $config{comments_commit} = 1
92                 unless defined $config{comments_commit};
93         $config{comments_pagespec} = ''
94                 unless defined $config{comments_pagespec};
95         $config{comments_closed_pagespec} = ''
96                 unless defined $config{comments_closed_pagespec};
97         $config{comments_pagename} = 'comment_'
98                 unless defined $config{comments_pagename};
99 }
100
101 sub htmlize {
102         my %params = @_;
103         return $params{content};
104 }
105
106 # FIXME: copied verbatim from meta
107 sub safeurl ($) {
108         my $url=shift;
109         if (exists $IkiWiki::Plugin::htmlscrubber::{safe_url_regexp} &&
110             defined $IkiWiki::Plugin::htmlscrubber::safe_url_regexp) {
111                 return $url=~/$IkiWiki::Plugin::htmlscrubber::safe_url_regexp/;
112         }
113         else {
114                 return 1;
115         }
116 }
117
118 sub preprocess {
119         my %params = @_;
120         my $page = $params{page};
121
122         my $format = $params{format};
123         if (defined $format && ! exists $IkiWiki::hooks{htmlize}{$format}) {
124                 error(sprintf(gettext("unsupported page format %s"), $format));
125         }
126
127         my $content = $params{content};
128         if (! defined $content) {
129                 error(gettext("comment must have content"));
130         }
131         $content =~ s/\\"/"/g;
132
133         $content = IkiWiki::filter($page, $params{destpage}, $content);
134
135         if ($config{comments_allowdirectives}) {
136                 $content = IkiWiki::preprocess($page, $params{destpage},
137                         $content);
138         }
139
140         # no need to bother with htmlize if it's just HTML
141         $content = IkiWiki::htmlize($page, $params{destpage}, $format, $content)
142                 if defined $format;
143
144         IkiWiki::run_hooks(sanitize => sub {
145                 $content = shift->(
146                         page => $page,
147                         destpage => $params{destpage},
148                         content => $content,
149                 );
150         });
151
152         # set metadata, possibly overriding [[!meta]] directives from the
153         # comment itself
154
155         my $commentuser;
156         my $commentip;
157         my $commentauthor;
158         my $commentauthorurl;
159         my $commentopenid;
160         if (defined $params{username}) {
161                 $commentuser = $params{username};
162
163                 my $oiduser = eval { IkiWiki::openiduser($commentuser) };
164
165                 if (defined $oiduser) {
166                         # looks like an OpenID
167                         $commentauthorurl = $commentuser;
168                         $commentauthor = $oiduser;
169                         $commentopenid = $commentuser;
170                 }
171                 else {
172                         $commentauthorurl = IkiWiki::cgiurl(
173                                 do => 'goto',
174                                 page => IkiWiki::userpage($commentuser)
175                         );
176
177                         $commentauthor = $commentuser;
178                 }
179         }
180         else {
181                 if (defined $params{ip}) {
182                         $commentip = $params{ip};
183                 }
184                 $commentauthor = gettext("Anonymous");
185         }
186
187         $commentstate{$page}{commentuser} = $commentuser;
188         $commentstate{$page}{commentopenid} = $commentopenid;
189         $commentstate{$page}{commentip} = $commentip;
190         $commentstate{$page}{commentauthor} = $commentauthor;
191         $commentstate{$page}{commentauthorurl} = $commentauthorurl;
192         if (! defined $pagestate{$page}{meta}{author}) {
193                 $pagestate{$page}{meta}{author} = $commentauthor;
194         }
195         if (! defined $pagestate{$page}{meta}{authorurl}) {
196                 $pagestate{$page}{meta}{authorurl} = $commentauthorurl;
197         }
198
199         if ($config{comments_allowauthor}) {
200                 if (defined $params{claimedauthor}) {
201                         $pagestate{$page}{meta}{author} = $params{claimedauthor};
202                 }
203
204                 if (defined $params{url}) {
205                         my $url=$params{url};
206
207                         eval q{use URI::Heuristic}; 
208                         if (! $@) {
209                                 $url=URI::Heuristic::uf_uristr($url);
210                         }
211
212                         if (safeurl($url)) {
213                                 $pagestate{$page}{meta}{authorurl} = $url;
214                         }
215                 }
216         }
217         else {
218                 $pagestate{$page}{meta}{author} = $commentauthor;
219                 $pagestate{$page}{meta}{authorurl} = $commentauthorurl;
220         }
221
222         if (defined $params{subject}) {
223                 $pagestate{$page}{meta}{title} = $params{subject};
224         }
225
226         if ($params{page} =~ m/\/\Q$config{comments_pagename}\E\d+_/) {
227                 $pagestate{$page}{meta}{permalink} = urlto(IkiWiki::dirname($params{page}), undef, 1).
228                         "#".page_to_id($params{page});
229         }
230
231         eval q{use Date::Parse};
232         if (! $@) {
233                 my $time = str2time($params{date});
234                 $IkiWiki::pagectime{$page} = $time if defined $time;
235         }
236
237         return $content;
238 }
239
240 sub sessioncgi ($$) {
241         my $cgi=shift;
242         my $session=shift;
243
244         my $do = $cgi->param('do');
245         if ($do eq 'comment') {
246                 editcomment($cgi, $session);
247         }
248         elsif ($do eq 'commentmoderation') {
249                 commentmoderation($cgi, $session);
250         }
251 }
252
253 # Mostly cargo-culted from IkiWiki::plugin::editpage
254 sub editcomment ($$) {
255         my $cgi=shift;
256         my $session=shift;
257
258         IkiWiki::decode_cgi_utf8($cgi);
259
260         eval q{use CGI::FormBuilder};
261         error($@) if $@;
262
263         my @buttons = (POST_COMMENT, PREVIEW, CANCEL);
264         my $form = CGI::FormBuilder->new(
265                 fields => [qw{do sid page subject editcontent type author url}],
266                 charset => 'utf-8',
267                 method => 'POST',
268                 required => [qw{editcontent}],
269                 javascript => 0,
270                 params => $cgi,
271                 action => $config{cgiurl},
272                 header => 0,
273                 table => 0,
274                 template => scalar IkiWiki::template_params('editcomment.tmpl'),
275         );
276
277         IkiWiki::decode_form_utf8($form);
278         IkiWiki::run_hooks(formbuilder_setup => sub {
279                         shift->(title => "comment", form => $form, cgi => $cgi,
280                                 session => $session, buttons => \@buttons);
281                 });
282         IkiWiki::decode_form_utf8($form);
283
284         my $type = $form->param('type');
285         if (defined $type && length $type && $IkiWiki::hooks{htmlize}{$type}) {
286                 $type = IkiWiki::possibly_foolish_untaint($type);
287         }
288         else {
289                 $type = $config{default_pageext};
290         }
291
292
293         my @page_types;
294         if (exists $IkiWiki::hooks{htmlize}) {
295                 foreach my $key (grep { !/^_/ } keys %{$IkiWiki::hooks{htmlize}}) {
296                         push @page_types, [$key, $IkiWiki::hooks{htmlize}{$key}{longname} || $key];
297                 }
298         }
299         @page_types=sort @page_types;
300
301         $form->field(name => 'do', type => 'hidden');
302         $form->field(name => 'sid', type => 'hidden', value => $session->id,
303                 force => 1);
304         $form->field(name => 'page', type => 'hidden');
305         $form->field(name => 'subject', type => 'text', size => 72);
306         $form->field(name => 'editcontent', type => 'textarea', rows => 10);
307         $form->field(name => "type", value => $type, force => 1,
308                 type => 'select', options => \@page_types);
309
310         $form->tmpl_param(username => $session->param('name'));
311
312         if ($config{comments_allowauthor} and
313             ! defined $session->param('name')) {
314                 $form->tmpl_param(allowauthor => 1);
315                 $form->field(name => 'author', type => 'text', size => '40');
316                 $form->field(name => 'url', type => 'text', size => '40');
317         }
318         else {
319                 $form->tmpl_param(allowauthor => 0);
320                 $form->field(name => 'author', type => 'hidden', value => '',
321                         force => 1);
322                 $form->field(name => 'url', type => 'hidden', value => '',
323                         force => 1);
324         }
325
326         if (! defined $session->param('name')) {
327                 # Make signinurl work and return here.
328                 $form->tmpl_param(signinurl => IkiWiki::cgiurl(do => 'signin'));
329                 $session->param(postsignin => $ENV{QUERY_STRING});
330                 IkiWiki::cgi_savesession($session);
331         }
332
333         # The untaint is OK (as in editpage) because we're about to pass
334         # it to file_pruned anyway
335         my $page = $form->field('page');
336         $page = IkiWiki::possibly_foolish_untaint($page);
337         if (! defined $page || ! length $page ||
338                 IkiWiki::file_pruned($page, $config{srcdir})) {
339                 error(gettext("bad page name"));
340         }
341
342         my $baseurl = urlto($page, undef, 1);
343
344         $form->title(sprintf(gettext("commenting on %s"),
345                         IkiWiki::pagetitle($page)));
346
347         $form->tmpl_param('helponformattinglink',
348                 htmllink($page, $page, 'ikiwiki/formatting',
349                         noimageinline => 1,
350                         linktext => 'FormattingHelp'),
351                         allowdirectives => $config{allow_directives});
352
353         if ($form->submitted eq CANCEL) {
354                 # bounce back to the page they wanted to comment on, and exit.
355                 # CANCEL need not be considered in future
356                 IkiWiki::redirect($cgi, urlto($page, undef, 1));
357                 exit;
358         }
359
360         if (not exists $pagesources{$page}) {
361                 error(sprintf(gettext(
362                         "page '%s' doesn't exist, so you can't comment"),
363                         $page));
364         }
365
366         if (pagespec_match($page, $config{comments_closed_pagespec},
367                 location => $page)) {
368                 error(sprintf(gettext(
369                         "comments on page '%s' are closed"),
370                         $page));
371         }
372
373         # Set a flag to indicate that we're posting a comment,
374         # so that postcomment() can tell it should match.
375         $postcomment=1;
376         IkiWiki::check_canedit($page, $cgi, $session);
377         $postcomment=0;
378
379         my $content = "[[!comment format=$type\n";
380
381         # FIXME: handling of double quotes probably wrong?
382         if (defined $session->param('name')) {
383                 my $username = $session->param('name');
384                 $username =~ s/"/&quot;/g;
385                 $content .= " username=\"$username\"\n";
386         }
387         elsif (defined $ENV{REMOTE_ADDR}) {
388                 my $ip = $ENV{REMOTE_ADDR};
389                 if ($ip =~ m/^([.0-9]+)$/) {
390                         $content .= " ip=\"$1\"\n";
391                 }
392         }
393
394         if ($config{comments_allowauthor}) {
395                 my $author = $form->field('author');
396                 if (defined $author && length $author) {
397                         $author =~ s/"/&quot;/g;
398                         $content .= " claimedauthor=\"$author\"\n";
399                 }
400                 my $url = $form->field('url');
401                 if (defined $url && length $url) {
402                         $url =~ s/"/&quot;/g;
403                         $content .= " url=\"$url\"\n";
404                 }
405         }
406
407         my $subject = $form->field('subject');
408         if (defined $subject && length $subject) {
409                 $subject =~ s/"/&quot;/g;
410         }
411         else {
412                 $subject = "comment ".(num_comments($page, $config{srcdir}) + 1);
413         }
414         $content .= " subject=\"$subject\"\n";
415
416         $content .= " date=\"" . decode_utf8(strftime('%Y-%m-%dT%H:%M:%SZ', gmtime)) . "\"\n";
417
418         my $editcontent = $form->field('editcontent') || '';
419         $editcontent =~ s/\r\n/\n/g;
420         $editcontent =~ s/\r/\n/g;
421         $editcontent =~ s/"/\\"/g;
422         $content .= " content=\"\"\"\n$editcontent\n\"\"\"]]\n";
423
424         my $location=unique_comment_location($page, $content, $config{srcdir});
425
426         # This is essentially a simplified version of editpage:
427         # - the user does not control the page that's created, only the parent
428         # - it's always a create operation, never an edit
429         # - this means that conflicts should never happen
430         # - this means that if they do, rocks fall and everyone dies
431
432         if ($form->submitted eq PREVIEW) {
433                 my $preview=previewcomment($content, $location, $page, time);
434                 IkiWiki::run_hooks(format => sub {
435                         $preview = shift->(page => $page,
436                                 content => $preview);
437                 });
438                 $form->tmpl_param(page_preview => $preview);
439         }
440         else {
441                 $form->tmpl_param(page_preview => "");
442         }
443
444         if ($form->submitted eq POST_COMMENT && $form->validate) {
445                 IkiWiki::checksessionexpiry($cgi, $session);
446                 
447                 $postcomment=1;
448                 my $ok=IkiWiki::check_content(content => $form->field('editcontent'),
449                         subject => $form->field('subject'),
450                         $config{comments_allowauthor} ? (
451                                 author => $form->field('author'),
452                                 url => $form->field('url'),
453                         ) : (),
454                         page => $location,
455                         cgi => $cgi,
456                         session => $session,
457                         nonfatal => 1,
458                 );
459                 $postcomment=0;
460
461                 if (! $ok) {
462                         my $penddir=$config{wikistatedir}."/comments_pending";
463                         $location=unique_comment_location($page, $content, $penddir);
464                         writefile("$location._comment", $penddir, $content);
465                         IkiWiki::printheader($session);
466                         print IkiWiki::misctemplate(gettext(gettext("comment stored for moderation")),
467                                 "<p>".
468                                 gettext("Your comment will be posted after moderator review").
469                                 "</p>");
470                         exit;
471                 }
472
473                 # FIXME: could probably do some sort of graceful retry
474                 # on error? Would require significant unwinding though
475                 my $file = "$location._comment";
476                 writefile($file, $config{srcdir}, $content);
477
478                 my $conflict;
479
480                 if ($config{rcs} and $config{comments_commit}) {
481                         my $message = gettext("Added a comment");
482                         if (defined $form->field('subject') &&
483                                 length $form->field('subject')) {
484                                 $message = sprintf(
485                                         gettext("Added a comment: %s"),
486                                         $form->field('subject'));
487                         }
488
489                         IkiWiki::rcs_add($file);
490                         IkiWiki::disable_commit_hook();
491                         $conflict = IkiWiki::rcs_commit_staged($message,
492                                 $session->param('name'), $ENV{REMOTE_ADDR});
493                         IkiWiki::enable_commit_hook();
494                         IkiWiki::rcs_update();
495                 }
496
497                 # Now we need a refresh
498                 require IkiWiki::Render;
499                 IkiWiki::refresh();
500                 IkiWiki::saveindex();
501
502                 # this should never happen, unless a committer deliberately
503                 # breaks it or something
504                 error($conflict) if defined $conflict;
505
506                 # Jump to the new comment on the page.
507                 # The trailing question mark tries to avoid broken
508                 # caches and get the most recent version of the page.
509                 IkiWiki::redirect($cgi, urlto($page, undef, 1).
510                         "?updated#".page_to_id($location));
511
512         }
513         else {
514                 IkiWiki::showform ($form, \@buttons, $session, $cgi,
515                         forcebaseurl => $baseurl);
516         }
517
518         exit;
519 }
520
521 sub commentmoderation ($$) {
522         my $cgi=shift;
523         my $session=shift;
524
525         IkiWiki::needsignin($cgi, $session);
526         if (! IkiWiki::is_admin($session->param("name"))) {
527                 error(gettext("you are not logged in as an admin"));
528         }
529
530         IkiWiki::decode_cgi_utf8($cgi);
531         
532         if (defined $cgi->param('sid')) {
533                 IkiWiki::checksessionexpiry($cgi, $session);
534
535                 my $rejectalldefer=$cgi->param('rejectalldefer');
536
537                 my %vars=$cgi->Vars;
538                 my $added=0;
539                 foreach my $id (keys %vars) {
540                         if ($id =~ /(.*)\Q._comment\E$/) {
541                                 my $action=$cgi->param($id);
542                                 next if $action eq 'Defer' && ! $rejectalldefer;
543
544                                 # Make sure that the id is of a legal
545                                 # pending comment before untainting.
546                                 my ($f)= $id =~ /$config{wiki_file_regexp}/;
547                                 if (! defined $f || ! length $f ||
548                                     IkiWiki::file_pruned($f, $config{srcdir})) {
549                                         error("illegal file");
550                                 }
551
552                                 my $page=IkiWiki::possibly_foolish_untaint(IkiWiki::dirname($1));
553                                 my $file="$config{wikistatedir}/comments_pending/".
554                                         IkiWiki::possibly_foolish_untaint($id);
555
556                                 if ($action eq 'Accept') {
557                                         my $content=eval { readfile($file) };
558                                         next if $@; # file vanished since form was displayed
559                                         my $dest=unique_comment_location($page, $content, $config{srcdir})."._comment";
560                                         writefile($dest, $config{srcdir}, $content);
561                                         if ($config{rcs} and $config{comments_commit}) {
562                                                 IkiWiki::rcs_add($dest);
563                                         }
564                                         $added++;
565                                 }
566
567                                 # This removes empty subdirs, so the
568                                 # .ikiwiki/comments_pending dir will
569                                 # go away when all are moderated.
570                                 require IkiWiki::Render;
571                                 IkiWiki::prune($file);
572                         }
573                 }
574
575                 if ($added) {
576                         my $conflict;
577                         if ($config{rcs} and $config{comments_commit}) {
578                                 my $message = gettext("Comment moderation");
579                                 IkiWiki::disable_commit_hook();
580                                 $conflict=IkiWiki::rcs_commit_staged($message,
581                                         $session->param('name'), $ENV{REMOTE_ADDR});
582                                 IkiWiki::enable_commit_hook();
583                                 IkiWiki::rcs_update();
584                         }
585                 
586                         # Now we need a refresh
587                         require IkiWiki::Render;
588                         IkiWiki::refresh();
589                         IkiWiki::saveindex();
590                 
591                         error($conflict) if defined $conflict;
592                 }
593         }
594
595         my @comments=map {
596                 my ($id, $ctime)=@{$_};
597                 my $file="$config{wikistatedir}/comments_pending/$id";
598                 my $content=readfile($file);
599                 my $preview=previewcomment($content, $id,
600                         IkiWiki::dirname($_), $ctime);
601                 {
602                         id => $id,
603                         view => $preview,
604                 } 
605         } sort { $b->[1] <=> $a->[1] } comments_pending();
606
607         my $template=template("commentmoderation.tmpl");
608         $template->param(
609                 sid => $session->id,
610                 comments => \@comments,
611         );
612         IkiWiki::printheader($session);
613         my $out=$template->output;
614         IkiWiki::run_hooks(format => sub {
615                 $out = shift->(page => "", content => $out);
616         });
617         print IkiWiki::misctemplate(gettext("comment moderation"), $out);
618         exit;
619 }
620
621 sub formbuilder_setup (@) {
622         my %params=@_;
623
624         my $form=$params{form};
625         if ($form->title eq "preferences" &&
626             IkiWiki::is_admin($params{session}->param("name"))) {
627                 push @{$params{buttons}}, "Comment Moderation";
628                 if ($form->submitted && $form->submitted eq "Comment Moderation") {
629                         commentmoderation($params{cgi}, $params{session});
630                 }
631         }
632 }
633
634 sub comments_pending () {
635         my $dir="$config{wikistatedir}/comments_pending/";
636         return unless -d $dir;
637
638         my @ret;
639         eval q{use File::Find};
640         error($@) if $@;
641         find({
642                 no_chdir => 1,
643                 wanted => sub {
644                         $_=decode_utf8($_);
645                         if (IkiWiki::file_pruned($_, $dir)) {
646                                 $File::Find::prune=1;
647                         }
648                         elsif (! -l $_ && ! -d _) {
649                                 $File::Find::prune=0;
650                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
651                                 if (defined $f && $f =~ /\Q._comment\E$/) {
652                                         my $ctime=(stat($f))[10];
653                                         $f=~s/^\Q$dir\E\/?//;
654                                         push @ret, [$f, $ctime];
655                                 }
656                         }
657                 }
658         }, $dir);
659
660         return @ret;
661 }
662
663 sub previewcomment ($$$) {
664         my $content=shift;
665         my $location=shift;
666         my $page=shift;
667         my $time=shift;
668
669         my $preview = IkiWiki::htmlize($location, $page, '_comment',
670                         IkiWiki::linkify($location, $page,
671                         IkiWiki::preprocess($location, $page,
672                         IkiWiki::filter($location, $page, $content), 0, 1)));
673
674         my $template = template("comment.tmpl");
675         $template->param(content => $preview);
676         $template->param(ctime => displaytime($time));
677
678         IkiWiki::run_hooks(pagetemplate => sub {
679                 shift->(page => $location,
680                         destpage => $page,
681                         template => $template);
682         });
683
684         $template->param(have_actions => 0);
685
686         return $template->output;
687 }
688
689 sub commentsshown ($) {
690         my $page=shift;
691
692         return ! pagespec_match($page, "internal(*/$config{comments_pagename}*)",
693                                 location => $page) &&
694                pagespec_match($page, $config{comments_pagespec},
695                               location => $page);
696 }
697
698 sub commentsopen ($) {
699         my $page = shift;
700
701         return length $config{cgiurl} > 0 &&
702                (! length $config{comments_closed_pagespec} ||
703                 ! pagespec_match($page, $config{comments_closed_pagespec},
704                                  location => $page));
705 }
706
707 sub pagetemplate (@) {
708         my %params = @_;
709
710         my $page = $params{page};
711         my $template = $params{template};
712         my $shown = ($template->query(name => 'commentslink') ||
713                      $template->query(name => 'commentsurl') ||
714                      $template->query(name => 'atomcommentsurl') ||
715                      $template->query(name => 'comments')) &&
716                     commentsshown($page);
717
718         if ($template->query(name => 'comments')) {
719                 my $comments = undef;
720                 if ($shown) {
721                         $comments = IkiWiki::preprocess_inline(
722                                 pages => "internal($page/$config{comments_pagename}*)",
723                                 template => 'comment',
724                                 show => 0,
725                                 reverse => 'yes',
726                                 page => $page,
727                                 destpage => $params{destpage},
728                                 feedfile => 'comments',
729                                 emptyfeeds => 'no',
730                         );
731                 }
732
733                 if (defined $comments && length $comments) {
734                         $template->param(comments => $comments);
735                 }
736
737                 if ($shown && commentsopen($page)) {
738                         my $addcommenturl = IkiWiki::cgiurl(do => 'comment',
739                                 page => $page);
740                         $template->param(addcommenturl => $addcommenturl);
741                 }
742         }
743
744         if ($template->query(name => 'commentsurl')) {
745                 if ($shown) {
746                         $template->param(commentsurl =>
747                                 urlto($page, undef, 1).'#comments');
748                 }
749         }
750
751         if ($template->query(name => 'atomcommentsurl') && $config{usedirs}) {
752                 if ($shown) {
753                         # This will 404 until there are some comments, but I
754                         # think that's probably OK...
755                         $template->param(atomcommentsurl =>
756                                 urlto($page, undef, 1).'comments.atom');
757                 }
758         }
759
760         if ($template->query(name => 'commentslink')) {
761                 # XXX Would be nice to say how many comments there are in
762                 # the link. But, to update the number, blog pages
763                 # would have to update whenever comments of any inlines
764                 # page are added, which is not currently done.
765                 if ($shown) {
766                         $template->param(commentslink =>
767                                 htmllink($page, $params{destpage}, $page,
768                                         linktext => gettext("Comments"),
769                                         anchor => "comments",
770                                         noimageinline => 1));
771                 }
772         }
773
774         # everything below this point is only relevant to the comments
775         # themselves
776         if (!exists $commentstate{$page}) {
777                 return;
778         }
779         
780         if ($template->query(name => 'commentid')) {
781                 $template->param(commentid => page_to_id($page));
782         }
783
784         if ($template->query(name => 'commentuser')) {
785                 $template->param(commentuser =>
786                         $commentstate{$page}{commentuser});
787         }
788
789         if ($template->query(name => 'commentopenid')) {
790                 $template->param(commentopenid =>
791                         $commentstate{$page}{commentopenid});
792         }
793
794         if ($template->query(name => 'commentip')) {
795                 $template->param(commentip =>
796                         $commentstate{$page}{commentip});
797         }
798
799         if ($template->query(name => 'commentauthor')) {
800                 $template->param(commentauthor =>
801                         $commentstate{$page}{commentauthor});
802         }
803
804         if ($template->query(name => 'commentauthorurl')) {
805                 $template->param(commentauthorurl =>
806                         $commentstate{$page}{commentauthorurl});
807         }
808
809         if ($template->query(name => 'removeurl') &&
810             IkiWiki::Plugin::remove->can("check_canremove") &&
811             length $config{cgiurl}) {
812                 $template->param(removeurl => IkiWiki::cgiurl(do => 'remove',
813                         page => $page));
814                 $template->param(have_actions => 1);
815         }
816 }
817
818 sub num_comments ($$) {
819         my $page=shift;
820         my $dir=shift;
821
822         my @comments=glob("$dir/$page/$config{comments_pagename}*._comment");
823         return @comments;
824 }
825
826 sub unique_comment_location ($$$) {
827         my $page=shift;
828
829         eval q{use Digest::MD5 'md5_hex'};
830         error($@) if $@;
831         my $content_md5=md5_hex(shift);
832
833         my $dir=shift;
834
835         my $location;
836         my $i = num_comments($page, $dir);
837         do {
838                 $i++;
839                 $location = "$page/$config{comments_pagename}${i}_${content_md5}";
840         } while (-e "$dir/$location._comment");
841
842         return $location;
843 }
844
845 sub page_to_id ($) {
846         # Converts a comment page name into a unique, legal html id
847         # addtibute value, that can be used as an anchor to link to the
848         # comment.
849         my $page=shift;
850
851         eval q{use Digest::MD5 'md5_hex'};
852         error($@) if $@;
853
854         return "comment-".md5_hex($page);
855 }
856         
857 package IkiWiki::PageSpec;
858
859 sub match_postcomment ($$;@) {
860         my $page = shift;
861         my $glob = shift;
862
863         if (! $postcomment) {
864                 return IkiWiki::FailReason->new("not posting a comment");
865         }
866         return match_glob($page, $glob);
867 }
868
869 1