summaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/comments.pm
blob: 73f8d6140c7f2455ffd539153f09712438d40195 (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 2.00;
  10. use constant PREVIEW => "Preview";
  11. use constant POST_COMMENT => "Post comment";
  12. use constant CANCEL => "Cancel";
  13. sub import { #{{{
  14. hook(type => "checkconfig", id => 'comments', call => \&checkconfig);
  15. hook(type => "getsetup", id => 'comments', call => \&getsetup);
  16. hook(type => "sessioncgi", id => 'comment', call => \&sessioncgi);
  17. hook(type => "htmlize", id => "_comment", call => \&htmlize);
  18. hook(type => "pagetemplate", id => "comments", call => \&pagetemplate);
  19. hook(type => "cgi", id => "comments", call => \&linkcgi);
  20. IkiWiki::loadplugin("inline");
  21. IkiWiki::loadplugin("mdwn");
  22. } # }}}
  23. sub htmlize { # {{{
  24. eval q{use IkiWiki::Plugin::mdwn};
  25. error($@) if ($@);
  26. return IkiWiki::Plugin::mdwn::htmlize(@_)
  27. } # }}}
  28. sub getsetup () { #{{{
  29. return
  30. plugin => {
  31. safe => 1,
  32. rebuild => 1,
  33. },
  34. # Pages where comments are shown, but new comments are not
  35. # allowed, will show "Comments are closed".
  36. comments_shown_pagespec => {
  37. type => 'pagespec',
  38. example => 'blog/*',
  39. default => '',
  40. description => 'PageSpec for pages where comments will be shown inline',
  41. link => 'ikiwiki/PageSpec',
  42. safe => 1,
  43. rebuild => 1,
  44. },
  45. comments_open_pagespec => {
  46. type => 'pagespec',
  47. example => 'blog/* and created_after(close_old_comments)',
  48. default => '',
  49. description => 'PageSpec for pages where new comments can be posted',
  50. link => 'ikiwiki/PageSpec',
  51. safe => 1,
  52. rebuild => 1,
  53. },
  54. comments_pagename => {
  55. type => 'string',
  56. example => 'comment_',
  57. default => 'comment_',
  58. description => 'Base name for comments, e.g. "comment_" for pages like "sandbox/comment_12"',
  59. safe => 0, # manual page moving will required
  60. rebuild => undef,
  61. },
  62. comments_allowdirectives => {
  63. type => 'boolean',
  64. default => 0,
  65. example => 0,
  66. description => 'Allow directives in newly posted comments?',
  67. safe => 1,
  68. rebuild => 0,
  69. },
  70. comments_commit => {
  71. type => 'boolean',
  72. example => 1,
  73. default => 1,
  74. description => 'commit comments to the VCS',
  75. # old uncommitted comments are likely to cause
  76. # confusion if this is changed
  77. safe => 0,
  78. rebuild => 0,
  79. },
  80. } #}}}
  81. sub checkconfig () { #{{{
  82. $config{comments_commit} = 1 unless defined $config{comments_commit};
  83. $config{comments_pagename} = 'comment_'
  84. unless defined $config{comments_pagename};
  85. } #}}}
  86. # FIXME: logic taken from editpage, should be common code?
  87. sub getcgiuser ($) { # {{{
  88. my $session = shift;
  89. my $user = $session->param('name');
  90. $user = $ENV{REMOTE_ADDR} unless defined $user;
  91. debug("getcgiuser() -> $user");
  92. return $user;
  93. } # }}}
  94. # This is exactly the same as recentchanges_link :-(
  95. sub linkcgi ($) { #{{{
  96. my $cgi=shift;
  97. if (defined $cgi->param('do') && $cgi->param('do') eq "commenter") {
  98. my $page=decode_utf8($cgi->param("page"));
  99. if (!defined $page) {
  100. error("missing page parameter");
  101. }
  102. IkiWiki::loadindex();
  103. my $link=bestlink("", $page);
  104. if (! length $link) {
  105. print "Content-type: text/html\n\n";
  106. print IkiWiki::misctemplate(gettext(gettext("missing page")),
  107. "<p>".
  108. sprintf(gettext("The page %s does not exist."),
  109. htmllink("", "", $page)).
  110. "</p>");
  111. }
  112. else {
  113. IkiWiki::redirect($cgi, urlto($link, undef, 1));
  114. }
  115. exit;
  116. }
  117. }
  118. # FIXME: basically the same logic as recentchanges
  119. # returns (author URL, pretty-printed version)
  120. sub linkuser ($) { # {{{
  121. my $user = shift;
  122. my $oiduser = eval { IkiWiki::openiduser($user) };
  123. if (defined $oiduser) {
  124. return ($user, $oiduser);
  125. }
  126. # FIXME: it'd be good to avoid having such a link for anonymous
  127. # posts
  128. else {
  129. return (IkiWiki::cgiurl(
  130. do => 'commenter',
  131. page => (length $config{userdir}
  132. ? "$config{userdir}/"
  133. : "")
  134. ).$user, $user);
  135. }
  136. } # }}}
  137. # Mostly cargo-culted from IkiWiki::plugin::editpage
  138. sub sessioncgi ($$) { #{{{
  139. my $cgi=shift;
  140. my $session=shift;
  141. my $do = $cgi->param('do');
  142. return unless $do eq 'comment';
  143. IkiWiki::decode_cgi_utf8($cgi);
  144. eval q{use CGI::FormBuilder};
  145. error($@) if $@;
  146. my @buttons = (POST_COMMENT, PREVIEW, CANCEL);
  147. my $form = CGI::FormBuilder->new(
  148. fields => [qw{do sid page subject body}],
  149. charset => 'utf-8',
  150. method => 'POST',
  151. required => [qw{body}],
  152. javascript => 0,
  153. params => $cgi,
  154. action => $config{cgiurl},
  155. header => 0,
  156. table => 0,
  157. template => scalar IkiWiki::template_params('comments_form.tmpl'),
  158. # wtf does this do in editpage?
  159. wikiname => $config{wikiname},
  160. );
  161. IkiWiki::decode_form_utf8($form);
  162. IkiWiki::run_hooks(formbuilder_setup => sub {
  163. shift->(title => "comment", form => $form, cgi => $cgi,
  164. session => $session, buttons => \@buttons);
  165. });
  166. IkiWiki::decode_form_utf8($form);
  167. $form->field(name => 'do', type => 'hidden');
  168. $form->field(name => 'sid', type => 'hidden', value => $session->id,
  169. force => 1);
  170. $form->field(name => 'page', type => 'hidden');
  171. $form->field(name => 'subject', type => 'text', size => 72);
  172. $form->field(name => 'body', type => 'textarea', rows => 5,
  173. cols => 80);
  174. # The untaint is OK (as in editpage) because we're about to pass
  175. # it to file_pruned anyway
  176. my $page = $form->field('page');
  177. $page = IkiWiki::possibly_foolish_untaint($page);
  178. if (!defined $page || !length $page ||
  179. IkiWiki::file_pruned($page, $config{srcdir})) {
  180. error(gettext("bad page name"));
  181. }
  182. my $allow_directives = $config{comments_allowdirectives};
  183. my $commit_comments = $config{comments_commit};
  184. my $comments_pagename = $config{comments_pagename};
  185. # FIXME: is this right? Or should we be using the candidate subpage
  186. # (whatever that might mean) as the base URL?
  187. my $baseurl = urlto($page, undef, 1);
  188. $form->title(sprintf(gettext("commenting on %s"),
  189. IkiWiki::pagetitle($page)));
  190. $form->tmpl_param('helponformattinglink',
  191. htmllink($page, $page, 'ikiwiki/formatting',
  192. noimageinline => 1,
  193. linktext => 'FormattingHelp'),
  194. allowdirectives => $allow_directives);
  195. if ($form->submitted eq CANCEL) {
  196. # bounce back to the page they wanted to comment on, and exit.
  197. # CANCEL need not be considered in future
  198. IkiWiki::redirect($cgi, urlto($page, undef, 1));
  199. exit;
  200. }
  201. if (not exists $pagesources{$page}) {
  202. error(sprintf(gettext(
  203. "page '%s' doesn't exist, so you can't comment"),
  204. $page));
  205. }
  206. if (not pagespec_match($page, $config{comments_open_pagespec},
  207. location => $page)) {
  208. error(sprintf(gettext(
  209. "comments on page '%s' are closed"),
  210. $page));
  211. }
  212. IkiWiki::check_canedit($page . "[postcomment]", $cgi, $session);
  213. my ($authorurl, $author) = linkuser(getcgiuser($session));
  214. my $body = $form->field('body') || '';
  215. $body =~ s/\r\n/\n/g;
  216. $body =~ s/\r/\n/g;
  217. $body .= "\n" if $body !~ /\n$/;
  218. unless ($allow_directives) {
  219. # don't allow new-style directives at all
  220. $body =~ s/(^|[^\\])\[\[!/$1&#91;&#91;!/g;
  221. # don't allow [[ unless it begins an old-style
  222. # wikilink, if prefix_directives is off
  223. $body =~ s/(^|[^\\])\[\[(?![^\n\s\]+]\]\])/$1&#91;&#91;!/g
  224. unless $config{prefix_directives};
  225. }
  226. # FIXME: check that the wiki is locked right now, because
  227. # if it's not, there are mad race conditions!
  228. # FIXME: rather a simplistic way to make the comments...
  229. my $i = 0;
  230. my $file;
  231. my $location;
  232. do {
  233. $i++;
  234. $location = "$page/${comments_pagename}${i}";
  235. } while (-e "$config{srcdir}/$location._comment");
  236. my $anchor = "${comments_pagename}${i}";
  237. IkiWiki::run_hooks(sanitize => sub {
  238. $body=shift->(
  239. page => $location,
  240. destpage => $location,
  241. content => $body,
  242. );
  243. });
  244. # In this template, the [[!meta]] directives should stay at the end,
  245. # so that they will override anything the user specifies. (For
  246. # instance, [[!meta author="I can fake the author"]]...)
  247. my $content_tmpl = template('comments_comment.tmpl');
  248. $content_tmpl->param(author => $author);
  249. $content_tmpl->param(authorurl => $authorurl);
  250. $content_tmpl->param(subject => $form->field('subject'));
  251. $content_tmpl->param(body => $body);
  252. $content_tmpl->param(anchor => "$anchor");
  253. $content_tmpl->param(permalink => "$baseurl#$anchor");
  254. $content_tmpl->param(date => IkiWiki::formattime(time, "%X %x"));
  255. my $content = $content_tmpl->output;
  256. # This is essentially a simplified version of editpage:
  257. # - the user does not control the page that's created, only the parent
  258. # - it's always a create operation, never an edit
  259. # - this means that conflicts should never happen
  260. # - this means that if they do, rocks fall and everyone dies
  261. if ($form->submitted eq PREVIEW) {
  262. my $preview = IkiWiki::htmlize($location, $page, 'mdwn',
  263. IkiWiki::linkify($page, $page,
  264. IkiWiki::preprocess($page, $page,
  265. IkiWiki::filter($location,
  266. $page, $content),
  267. 0, 1)));
  268. IkiWiki::run_hooks(format => sub {
  269. $preview = shift->(page => $page,
  270. content => $preview);
  271. });
  272. my $template = template("comments_display.tmpl");
  273. $template->param(content => $preview);
  274. $template->param(title => $form->field('subject'));
  275. $template->param(ctime => displaytime(time));
  276. $template->param(author => $author);
  277. $template->param(authorurl => $authorurl);
  278. $form->tmpl_param(page_preview => $template->output);
  279. }
  280. else {
  281. $form->tmpl_param(page_preview => "");
  282. }
  283. if ($form->submitted eq POST_COMMENT && $form->validate) {
  284. my $file = "$location._comment";
  285. IkiWiki::checksessionexpiry($session, $cgi->param('sid'));
  286. # FIXME: could probably do some sort of graceful retry
  287. # on error? Would require significant unwinding though
  288. writefile($file, $config{srcdir}, $content);
  289. my $conflict;
  290. if ($config{rcs} and $commit_comments) {
  291. my $message = gettext("Added a comment");
  292. if (defined $form->field('subject') &&
  293. length $form->field('subject')) {
  294. $message = sprintf(
  295. gettext("Added a comment: %s"),
  296. $form->field('subject'));
  297. }
  298. IkiWiki::rcs_add($file);
  299. IkiWiki::disable_commit_hook();
  300. $conflict = IkiWiki::rcs_commit_staged($message,
  301. $session->param('name'), $ENV{REMOTE_ADDR});
  302. IkiWiki::enable_commit_hook();
  303. IkiWiki::rcs_update();
  304. }
  305. # Now we need a refresh
  306. require IkiWiki::Render;
  307. IkiWiki::refresh();
  308. IkiWiki::saveindex();
  309. # this should never happen, unless a committer deliberately
  310. # breaks it or something
  311. error($conflict) if defined $conflict;
  312. # Bounce back to where we were, but defeat broken caches
  313. my $anticache = "?updated=$page/${comments_pagename}${i}";
  314. IkiWiki::redirect($cgi, urlto($page, undef, 1).$anticache);
  315. }
  316. else {
  317. IkiWiki::showform ($form, \@buttons, $session, $cgi,
  318. forcebaseurl => $baseurl);
  319. }
  320. exit;
  321. } #}}}
  322. sub pagetemplate (@) { #{{{
  323. my %params = @_;
  324. my $page = $params{page};
  325. my $template = $params{template};
  326. if ($template->query(name => 'comments')) {
  327. my $comments = undef;
  328. my $comments_pagename = $config{comments_pagename};
  329. my $open = 0;
  330. my $shown = pagespec_match($page,
  331. $config{comments_shown_pagespec},
  332. location => $page);
  333. if (pagespec_match($page, "*/${comments_pagename}*",
  334. location => $page)) {
  335. $shown = 0;
  336. $open = 0;
  337. }
  338. if (length $config{cgiurl}) {
  339. $open = pagespec_match($page,
  340. $config{comments_open_pagespec},
  341. location => $page);
  342. }
  343. if ($shown) {
  344. eval q{use IkiWiki::Plugin::inline};
  345. error($@) if $@;
  346. my @args = (
  347. pages => "internal($page/${comments_pagename}*)",
  348. template => 'comments_display',
  349. show => 0,
  350. reverse => 'yes',
  351. page => $page,
  352. destpage => $params{destpage},
  353. );
  354. $comments = IkiWiki::preprocess_inline(@args);
  355. }
  356. if (defined $comments && length $comments) {
  357. $template->param(comments => $comments);
  358. }
  359. if ($open) {
  360. my $commenturl = IkiWiki::cgiurl(do => 'comment',
  361. page => $page);
  362. $template->param(commenturl => $commenturl);
  363. }
  364. }
  365. } # }}}
  366. package IkiWiki::PageSpec;
  367. sub match_postcomment ($$;@) {
  368. my $page = shift;
  369. my $glob = shift;
  370. unless ($page =~ s/\[postcomment\]$//) {
  371. return IkiWiki::FailReason->new("not posting a comment");
  372. }
  373. return match_glob($page, $glob);
  374. }
  375. 1