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