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