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