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