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