summaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/comments.pm
blob: 29dc06f32c8907f37a178ef6710c7ca5f54a05fe (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 => "getsetup", id => 'comments', call => \&getsetup);
  15. hook(type => "preprocess", id => 'comments', call => \&preprocess);
  16. hook(type => "sessioncgi", id => 'comment', call => \&sessioncgi);
  17. hook(type => "htmlize", id => "_comment", call => \&htmlize);
  18. IkiWiki::loadplugin("inline");
  19. IkiWiki::loadplugin("mdwn");
  20. } # }}}
  21. sub htmlize { # {{{
  22. eval { use IkiWiki::Plugin::mdwn; };
  23. error($@) if ($@);
  24. return IkiWiki::Plugin::mdwn::htmlize(@_)
  25. } # }}}
  26. sub getsetup () { #{{{
  27. return
  28. plugin => {
  29. safe => 1,
  30. rebuild => undef,
  31. },
  32. } #}}}
  33. # Somewhat based on IkiWiki::Plugin::inline blog posting support
  34. sub preprocess (@) { #{{{
  35. my %params=@_;
  36. unless (length $config{cgiurl}) {
  37. error(gettext("[[!comments plugin requires CGI enabled]]"));
  38. }
  39. my $page = $params{page};
  40. $pagestate{$page}{comments}{comments} = defined $params{closed}
  41. ? (not IkiWiki::yesno($params{closed}))
  42. : 1;
  43. $pagestate{$page}{comments}{allowhtml} = IkiWiki::yesno($params{allowhtml});
  44. $pagestate{$page}{comments}{allowdirectives} = IkiWiki::yesno($params{allowdirectives});
  45. $pagestate{$page}{comments}{commit} = defined $params{commit}
  46. ? IkiWiki::yesno($params{commit})
  47. : 1;
  48. my $formtemplate = IkiWiki::template("comments_embed.tmpl",
  49. blind_cache => 1);
  50. $formtemplate->param(cgiurl => $config{cgiurl});
  51. $formtemplate->param(page => $params{page});
  52. if (not $pagestate{$page}{comments}{comments}) {
  53. $formtemplate->param("disabled" =>
  54. gettext('comments are closed'));
  55. }
  56. elsif ($params{preview}) {
  57. $formtemplate->param("disabled" =>
  58. gettext('not available during Preview'));
  59. }
  60. debug("page $params{page} => destpage $params{destpage}");
  61. my $posts = '';
  62. unless (defined $params{inline} && !IkiWiki::yesno($params{inline})) {
  63. eval { use IkiWiki::Plugin::inline; };
  64. error($@) if ($@);
  65. my @args = (
  66. pages => "internal($params{page}/_comment_*)",
  67. template => "comments_display",
  68. show => 0,
  69. reverse => "yes",
  70. # special stuff passed through
  71. page => $params{page},
  72. destpage => $params{destpage},
  73. preview => $params{preview},
  74. );
  75. push @args, atom => $params{atom} if defined $params{atom};
  76. push @args, rss => $params{rss} if defined $params{rss};
  77. push @args, feeds => $params{feeds} if defined $params{feeds};
  78. push @args, feedshow => $params{feedshow} if defined $params{feedshow};
  79. push @args, timeformat => $params{timeformat} if defined $params{timeformat};
  80. push @args, feedonly => $params{feedonly} if defined $params{feedonly};
  81. $posts = "\n" . IkiWiki::preprocess_inline(@args);
  82. }
  83. return $formtemplate->output . $posts;
  84. } # }}}
  85. # FIXME: logic taken from editpage, should be common code?
  86. sub getcgiuser ($) { # {{{
  87. my $session = shift;
  88. my $user = $session->param('name');
  89. $user = $ENV{REMOTE_ADDR} unless defined $user;
  90. debug("getcgiuser() -> $user");
  91. return $user;
  92. } # }}}
  93. # FIXME: logic adapted from recentchanges, should be common code?
  94. sub linkuser ($) { # {{{
  95. my $user = shift;
  96. my $oiduser = eval { IkiWiki::openiduser($user) };
  97. if (defined $oiduser) {
  98. return ($user, $oiduser);
  99. }
  100. else {
  101. my $page = bestlink('', (length $config{userdir}
  102. ? "$config{userdir}/"
  103. : "").$user);
  104. return (urlto($page, undef, 1), $user);
  105. }
  106. } # }}}
  107. # FIXME: taken from IkiWiki::Plugin::editpage, should be common?
  108. sub checksessionexpiry ($$) { # {{{
  109. my $session = shift;
  110. my $sid = shift;
  111. if (defined $session->param("name")) {
  112. if (! defined $sid || $sid ne $session->id) {
  113. error(gettext("Your login session has expired."));
  114. }
  115. }
  116. } # }}}
  117. # Mostly cargo-culted from IkiWiki::plugin::editpage
  118. sub sessioncgi ($$) { #{{{
  119. my $cgi=shift;
  120. my $session=shift;
  121. my $do = $cgi->param('do');
  122. return unless $do eq 'comment';
  123. IkiWiki::decode_cgi_utf8($cgi);
  124. eval q{use CGI::FormBuilder};
  125. error($@) if $@;
  126. my @buttons = (POST_COMMENT, PREVIEW, CANCEL);
  127. my $form = CGI::FormBuilder->new(
  128. fields => [qw{do sid page subject body}],
  129. charset => 'utf-8',
  130. method => 'POST',
  131. required => [qw{body}],
  132. javascript => 0,
  133. params => $cgi,
  134. action => $config{cgiurl},
  135. header => 0,
  136. table => 0,
  137. template => scalar IkiWiki::template_params('comments_form.tmpl'),
  138. # wtf does this do in editpage?
  139. wikiname => $config{wikiname},
  140. );
  141. IkiWiki::decode_form_utf8($form);
  142. IkiWiki::run_hooks(formbuilder_setup => sub {
  143. shift->(title => "comment", form => $form, cgi => $cgi,
  144. session => $session, buttons => \@buttons);
  145. });
  146. IkiWiki::decode_form_utf8($form);
  147. $form->field(name => 'do', type => 'hidden');
  148. $form->field(name => 'sid', type => 'hidden', value => $session->id,
  149. force => 1);
  150. $form->field(name => 'page', type => 'hidden');
  151. $form->field(name => 'subject', type => 'text', size => 72);
  152. $form->field(name => 'body', type => 'textarea', rows => 5,
  153. cols => 80);
  154. # The untaint is OK (as in editpage) because we're about to pass
  155. # it to file_pruned anyway
  156. my $page = $form->field('page');
  157. $page = IkiWiki::possibly_foolish_untaint($page);
  158. if (!defined $page || !length $page ||
  159. IkiWiki::file_pruned($page, $config{srcdir})) {
  160. error(gettext("bad page name"));
  161. }
  162. my $allow_directives = $pagestate{$page}{comments}{allowdirectives};
  163. my $allow_html = $pagestate{$page}{comments}{allowdirectives};
  164. my $commit_comments = defined $pagestate{$page}{comments}{commit}
  165. ? $pagestate{$page}{comments}{commit}
  166. : 1;
  167. # FIXME: is this right? Or should we be using the candidate subpage
  168. # (whatever that might mean) as the base URL?
  169. my $baseurl = urlto($page, undef, 1);
  170. $form->title(sprintf(gettext("commenting on %s"),
  171. IkiWiki::pagetitle($page)));
  172. $form->tmpl_param('helponformattinglink',
  173. htmllink($page, $page, 'ikiwiki/formatting',
  174. noimageinline => 1,
  175. linktext => 'FormattingHelp'),
  176. allowhtml => $allow_html,
  177. allowdirectives => $allow_directives);
  178. if (not exists $pagesources{$page}) {
  179. error(sprintf(gettext(
  180. "page '%s' doesn't exist, so you can't comment"),
  181. $page));
  182. }
  183. if (not $pagestate{$page}{comments}{comments}) {
  184. error(sprintf(gettext(
  185. "comments are not enabled on page '%s'"),
  186. $page));
  187. }
  188. if ($form->submitted eq CANCEL) {
  189. # bounce back to the page they wanted to comment on, and exit.
  190. # CANCEL need not be considered in future
  191. IkiWiki::redirect($cgi, urlto($page, undef, 1));
  192. exit;
  193. }
  194. IkiWiki::check_canedit($page . "[postcomment]", $cgi, $session);
  195. my ($authorurl, $author) = linkuser(getcgiuser($session));
  196. my $body = $form->field('body') || '';
  197. $body =~ s/\r\n/\n/g;
  198. $body =~ s/\r/\n/g;
  199. $body .= "\n" if $body !~ /\n$/;
  200. unless ($allow_directives) {
  201. # don't allow new-style directives at all
  202. $body =~ s/(^|[^\\])\[\[!/$1\\[[!/g;
  203. # don't allow [[ unless it begins an old-style
  204. # wikilink, if prefix_directives is off
  205. $body =~ s/(^|[^\\])\[\[(?![^\n\s\]+]\]\])/$1\\[[!/g
  206. unless $config{prefix_directives};
  207. }
  208. unless ($allow_html) {
  209. $body =~ s/&(\w|#)/&amp;$1/g;
  210. $body =~ s/</&lt;/g;
  211. $body =~ s/>/&gt;/g;
  212. }
  213. IkiWiki::run_hooks(sanitize => sub {
  214. # $fake is a possible location for this comment. We don't
  215. # know yet what the comment number *actually* is.
  216. my $fake = "$page/_comment_1";
  217. $body=shift->(
  218. page => $fake,
  219. destpage => $fake,
  220. content => $body,
  221. );
  222. });
  223. # In this template, the [[!meta]] directives should stay at the end,
  224. # so that they will override anything the user specifies. (For
  225. # instance, [[!meta author="I can fake the author"]]...)
  226. my $content_tmpl = template('comments_comment.tmpl');
  227. $content_tmpl->param(author => $author);
  228. $content_tmpl->param(authorurl => $authorurl);
  229. $content_tmpl->param(subject => $form->field('subject'));
  230. $content_tmpl->param(body => $body);
  231. my $content = $content_tmpl->output;
  232. # This is essentially a simplified version of editpage:
  233. # - the user does not control the page that's created, only the parent
  234. # - it's always a create operation, never an edit
  235. # - this means that conflicts should never happen
  236. # - this means that if they do, rocks fall and everyone dies
  237. if ($form->submitted eq PREVIEW) {
  238. # $fake is a possible location for this comment. We don't
  239. # know yet what the comment number *actually* is.
  240. my $fake = "$page/_comment_1";
  241. my $preview = IkiWiki::htmlize($fake, $page, 'mdwn',
  242. IkiWiki::linkify($page, $page,
  243. IkiWiki::preprocess($page, $page,
  244. IkiWiki::filter($fake, $page,
  245. $content),
  246. 0, 1)));
  247. IkiWiki::run_hooks(format => sub {
  248. $preview = shift->(page => $page,
  249. content => $preview);
  250. });
  251. my $template = template("comments_display.tmpl");
  252. $template->param(content => $preview);
  253. $template->param(title => $form->field('subject'));
  254. $template->param(ctime => displaytime(time));
  255. $template->param(author => $author);
  256. $template->param(authorurl => $authorurl);
  257. $form->tmpl_param(page_preview => $template->output);
  258. }
  259. else {
  260. $form->tmpl_param(page_preview => "");
  261. }
  262. if ($form->submitted eq POST_COMMENT && $form->validate) {
  263. # Let's get posting. We don't check_canedit here because
  264. # that somewhat defeats the point of this plugin.
  265. checksessionexpiry($session, $cgi->param('sid'));
  266. # FIXME: check that the wiki is locked right now, because
  267. # if it's not, there are mad race conditions!
  268. # FIXME: rather a simplistic way to make the comments...
  269. my $i = 0;
  270. my $file;
  271. do {
  272. $i++;
  273. $file = "$page/_comment_${i}._comment";
  274. } while (-e "$config{srcdir}/$file");
  275. # FIXME: could probably do some sort of graceful retry
  276. # if I could be bothered
  277. writefile($file, $config{srcdir}, $content);
  278. my $conflict;
  279. if ($config{rcs} and $commit_comments) {
  280. my $message = gettext("Added a comment");
  281. if (defined $form->field('subject') &&
  282. length $form->field('subject')) {
  283. $message .= ": ".$form->field('subject');
  284. }
  285. IkiWiki::rcs_add($file);
  286. IkiWiki::disable_commit_hook();
  287. $conflict = IkiWiki::rcs_commit_staged($message,
  288. $session->param('name'), $ENV{REMOTE_ADDR});
  289. IkiWiki::enable_commit_hook();
  290. IkiWiki::rcs_update();
  291. }
  292. # Now we need a refresh
  293. require IkiWiki::Render;
  294. IkiWiki::refresh();
  295. IkiWiki::saveindex();
  296. # this should never happen, unless a committer deliberately
  297. # breaks it or something
  298. error($conflict) if defined $conflict;
  299. # Bounce back to where we were, but defeat broken caches
  300. my $anticache = "?updated=$page/_comment_$i";
  301. IkiWiki::redirect($cgi, urlto($page, undef, 1).$anticache);
  302. }
  303. else {
  304. IkiWiki::showform ($form, \@buttons, $session, $cgi,
  305. forcebaseurl => $baseurl);
  306. }
  307. exit;
  308. } #}}}
  309. package IkiWiki::PageSpec;
  310. sub match_postcomment ($$;@) {
  311. my $page = shift;
  312. my $glob = shift;
  313. unless ($page =~ s/\[postcomment\]$//) {
  314. return IkiWiki::FailReason->new("not posting a comment");
  315. }
  316. return match_glob($page, $glob);
  317. }
  318. 1