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