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