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