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