]> sipb.mit.edu Git - ikiwiki.git/blob - IkiWiki/Plugin/editpage.pm
blogspam: New plugin, adding spam filtering for page editing / comment posting using...
[ikiwiki.git] / IkiWiki / Plugin / editpage.pm
1 #!/usr/bin/perl
2 package IkiWiki::Plugin::editpage;
3
4 use warnings;
5 use strict;
6 use IkiWiki;
7 use open qw{:utf8 :std};
8
9 sub import {
10         hook(type => "getsetup", id => "editpage", call => \&getsetup);
11         hook(type => "refresh", id => "editpage", call => \&refresh);
12         hook(type => "sessioncgi", id => "editpage", call => \&IkiWiki::cgi_editpage);
13 }
14
15 sub getsetup () {
16         return
17                 plugin => {
18                         safe => 1,
19                         rebuild => 1,
20                 },
21 }
22
23 sub refresh () {
24         if (exists $wikistate{editpage} && exists $wikistate{editpage}{previews}) {
25                 # Expire old preview files after one hour.
26                 my $expire=time - (60 * 60);
27
28                 my @previews;
29                 foreach my $file (@{$wikistate{editpage}{previews}}) {
30                         my $mtime=(stat("$config{destdir}/$file"))[9];
31                         if (defined $mtime && $mtime <= $expire) {
32                                 # Avoid deleting a preview that was later saved.
33                                 my $delete=1;
34                                 foreach my $page (keys %renderedfiles) {
35                                         if (grep { $_ eq $file } @{$renderedfiles{$page}}) {
36                                                 $delete=0;
37                                         }
38                                 }
39                                 if ($delete) {
40                                         debug(sprintf(gettext("removing old preview %s"), $file));
41                                         IkiWiki::prune("$config{destdir}/$file");
42                                 }
43                         }
44                         elsif (defined $mtime) {
45                                 push @previews, $file;
46                         }
47                 }
48                 $wikistate{editpage}{previews}=\@previews;
49         }
50 }
51
52 # Back to ikiwiki namespace for the rest, this code is very much
53 # internal to ikiwiki even though it's separated into a plugin,
54 # and other plugins use the functions below.
55 package IkiWiki;
56
57 sub check_canedit ($$$;$) {
58         my $page=shift;
59         my $q=shift;
60         my $session=shift;
61         my $nonfatal=shift;
62         
63         my $canedit;
64         run_hooks(canedit => sub {
65                 return if defined $canedit;
66                 my $ret=shift->($page, $q, $session);
67                 if (defined $ret) {
68                         if ($ret eq "") {
69                                 $canedit=1;
70                         }
71                         elsif (ref $ret eq 'CODE') {
72                                 $ret->() unless $nonfatal;
73                                 $canedit=0;
74                         }
75                         elsif (defined $ret) {
76                                 error($ret) unless $nonfatal;
77                                 $canedit=0;
78                         }
79                 }
80         });
81         return defined $canedit ? $canedit : 1;
82 }
83
84 sub check_content (@) {
85         my %params=@_;
86         
87         return 1 if ! exists $hooks{checkcontent}; # optimisation
88
89         if (exists $pagesources{$params{page}}) {
90                 my @diff;
91                 my %old=map { $_ => 1 }
92                         split("\n", readfile(srcfile($pagesources{$params{page}})));
93                 foreach my $line (split("\n", $params{content})) {
94                         push @diff, $line if ! exists $old{$_};
95                 }
96                 $params{content}=join("\n", @diff);
97         }
98
99         my $ok;
100         run_hooks(checkcontent => sub {
101                 return if defined $ok;
102                 my $ret=shift->(%params);
103                 if (defined $ret) {
104                         if ($ret eq "") {
105                                 $ok=1;
106                         }
107                         elsif (ref $ret eq 'CODE') {
108                                 $ret->();
109                                 $ok=0;
110                         }
111                         elsif (defined $ret) {
112                                 error($ret);
113                         }
114                 }
115
116         });
117         return defined $ok ? $ok : 1;
118 }
119
120 sub cgi_editpage ($$) {
121         my $q=shift;
122         my $session=shift;
123         
124         my $do=$q->param('do');
125         return unless $do eq 'create' || $do eq 'edit';
126
127         decode_cgi_utf8($q);
128
129         my @fields=qw(do rcsinfo subpage from page type editcontent comments);
130         my @buttons=("Save Page", "Preview", "Cancel");
131         eval q{use CGI::FormBuilder};
132         error($@) if $@;
133         my $form = CGI::FormBuilder->new(
134                 fields => \@fields,
135                 charset => "utf-8",
136                 method => 'POST',
137                 required => [qw{editcontent}],
138                 javascript => 0,
139                 params => $q,
140                 action => $config{cgiurl},
141                 header => 0,
142                 table => 0,
143                 template => scalar template_params("editpage.tmpl"),
144         );
145         
146         decode_form_utf8($form);
147         run_hooks(formbuilder_setup => sub {
148                 shift->(form => $form, cgi => $q, session => $session,
149                         buttons => \@buttons);
150         });
151         decode_form_utf8($form);
152         
153         # This untaint is safe because we check file_pruned and
154         # wiki_file_regexp.
155         my ($page)=$form->field('page')=~/$config{wiki_file_regexp}/;
156         $page=possibly_foolish_untaint($page);
157         my $absolute=($page =~ s#^/+##);
158         if (! defined $page || ! length $page ||
159             file_pruned($page, $config{srcdir})) {
160                 error(gettext("bad page name"));
161         }
162
163         my $baseurl = urlto($page, undef, 1);
164
165         my $from;
166         if (defined $form->field('from')) {
167                 ($from)=$form->field('from')=~/$config{wiki_file_regexp}/;
168         }
169         
170         my $file;
171         my $type;
172         if (exists $pagesources{$page} && $form->field("do") ne "create") {
173                 $file=$pagesources{$page};
174                 $type=pagetype($file);
175                 if (! defined $type || $type=~/^_/) {
176                         error(sprintf(gettext("%s is not an editable page"), $page));
177                 }
178                 if (! $form->submitted) {
179                         $form->field(name => "rcsinfo",
180                                 value => rcs_prepedit($file), force => 1);
181                 }
182                 $form->field(name => "editcontent", validate => '/.*/');
183         }
184         else {
185                 $type=$form->param('type');
186                 if (defined $type && length $type && $hooks{htmlize}{$type}) {
187                         $type=possibly_foolish_untaint($type);
188                 }
189                 elsif (defined $from && exists $pagesources{$from}) {
190                         # favor the type of linking page
191                         $type=pagetype($pagesources{$from});
192                 }
193                 $type=$config{default_pageext} unless defined $type;
194                 $file=newpagefile($page, $type);
195                 if (! $form->submitted) {
196                         $form->field(name => "rcsinfo", value => "", force => 1);
197                 }
198                 $form->field(name => "editcontent", validate => '/.+/');
199         }
200
201         $form->field(name => "do", type => 'hidden');
202         $form->field(name => "sid", type => "hidden", value => $session->id,
203                 force => 1);
204         $form->field(name => "from", type => 'hidden');
205         $form->field(name => "rcsinfo", type => 'hidden');
206         $form->field(name => "subpage", type => 'hidden');
207         $form->field(name => "page", value => $page, force => 1);
208         $form->field(name => "type", value => $type, force => 1);
209         $form->field(name => "comments", type => "text", size => 80);
210         $form->field(name => "editcontent", type => "textarea", rows => 20,
211                 cols => 80);
212         $form->tmpl_param("can_commit", $config{rcs});
213         $form->tmpl_param("indexlink", indexlink());
214         $form->tmpl_param("helponformattinglink",
215                 htmllink($page, $page, "ikiwiki/formatting",
216                         noimageinline => 1,
217                         linktext => "FormattingHelp"));
218         
219         if ($form->submitted eq "Cancel") {
220                 if ($form->field("do") eq "create" && defined $from) {
221                         redirect($q, urlto($from, undef, 1));
222                 }
223                 elsif ($form->field("do") eq "create") {
224                         redirect($q, $config{url});
225                 }
226                 else {
227                         redirect($q, urlto($page, undef, 1));
228                 }
229                 exit;
230         }
231         elsif ($form->submitted eq "Preview") {
232                 my $new=not exists $pagesources{$page};
233                 if ($new) {
234                         # temporarily record its type
235                         $pagesources{$page}=$page.".".$type;
236                 }
237                 my %wasrendered=map { $_ => 1 } @{$renderedfiles{$page}};
238
239                 my $content=$form->field('editcontent');
240
241                 run_hooks(editcontent => sub {
242                         $content=shift->(
243                                 content => $content,
244                                 page => $page,
245                                 cgi => $q,
246                                 session => $session,
247                         );
248                 });
249                 my $preview=htmlize($page, $page, $type,
250                         linkify($page, $page,
251                         preprocess($page, $page,
252                         filter($page, $page, $content), 0, 1)));
253                 run_hooks(format => sub {
254                         $preview=shift->(
255                                 page => $page,
256                                 content => $preview,
257                         );
258                 });
259                 $form->tmpl_param("page_preview", $preview);
260                 
261                 if ($new) {
262                         delete $pagesources{$page};
263                 }
264
265                 # Previewing may have created files on disk.
266                 # Keep a list of these to be deleted later.
267                 my %previews = map { $_ => 1 } @{$wikistate{editpage}{previews}};
268                 foreach my $f (@{$renderedfiles{$page}}) {
269                         $previews{$f}=1 unless $wasrendered{$f};
270                 }
271                 @{$wikistate{editpage}{previews}} = keys %previews;
272                 $renderedfiles{$page}=[keys %wasrendered];
273                 saveindex();
274         }
275         elsif ($form->submitted eq "Save Page") {
276                 $form->tmpl_param("page_preview", "");
277         }
278         
279         if ($form->submitted ne "Save Page" || ! $form->validate) {
280                 if ($form->field("do") eq "create") {
281                         my @page_locs;
282                         my $best_loc;
283                         if (! defined $from || ! length $from ||
284                             $from ne $form->field('from') ||
285                             file_pruned($from, $config{srcdir}) ||
286                             $from=~/^\// || 
287                             $absolute ||
288                             $form->submitted) {
289                                 @page_locs=$best_loc=$page;
290                         }
291                         else {
292                                 my $dir=$from."/";
293                                 $dir=~s![^/]+/+$!!;
294                                 
295                                 if ((defined $form->field('subpage') && length $form->field('subpage')) ||
296                                     $page eq gettext('discussion')) {
297                                         $best_loc="$from/$page";
298                                 }
299                                 else {
300                                         $best_loc=$dir.$page;
301                                 }
302                                 
303                                 push @page_locs, $dir.$page;
304                                 push @page_locs, "$from/$page";
305                                 while (length $dir) {
306                                         $dir=~s![^/]+/+$!!;
307                                         push @page_locs, $dir.$page;
308                                 }
309                         
310                                 push @page_locs, "$config{userdir}/$page"
311                                         if length $config{userdir};
312                         }
313
314                         @page_locs = grep {
315                                 ! exists $pagecase{lc $_}
316                         } @page_locs;
317                         if (! @page_locs) {
318                                 # hmm, someone else made the page in the
319                                 # meantime?
320                                 if ($form->submitted eq "Preview") {
321                                         # let them go ahead with the edit
322                                         # and resolve the conflict at save
323                                         # time
324                                         @page_locs=$page;
325                                 }
326                                 else {
327                                         redirect($q, urlto($page, undef, 1));
328                                         exit;
329                                 }
330                         }
331
332                         my @editable_locs = grep {
333                                 check_canedit($_, $q, $session, 1)
334                         } @page_locs;
335                         if (! @editable_locs) {
336                                 # let it throw an error this time
337                                 map { check_canedit($_, $q, $session) } @page_locs;
338                         }
339                         
340                         my @page_types;
341                         if (exists $hooks{htmlize}) {
342                                 @page_types=grep { !/^_/ }
343                                         keys %{$hooks{htmlize}};
344                         }
345                         
346                         $form->tmpl_param("page_select", 1);
347                         $form->field(name => "page", type => 'select',
348                                 options => [ map { [ $_, pagetitle($_, 1) ] } @editable_locs ],
349                                 value => $best_loc);
350                         $form->field(name => "type", type => 'select',
351                                 options => \@page_types);
352                         $form->title(sprintf(gettext("creating %s"), pagetitle($page)));
353                         
354                 }
355                 elsif ($form->field("do") eq "edit") {
356                         check_canedit($page, $q, $session);
357                         if (! defined $form->field('editcontent') || 
358                             ! length $form->field('editcontent')) {
359                                 my $content="";
360                                 if (exists $pagesources{$page}) {
361                                         $content=readfile(srcfile($pagesources{$page}));
362                                         $content=~s/\n/\r\n/g;
363                                 }
364                                 $form->field(name => "editcontent", value => $content,
365                                         force => 1);
366                         }
367                         $form->tmpl_param("page_select", 0);
368                         $form->field(name => "page", type => 'hidden');
369                         $form->field(name => "type", type => 'hidden');
370                         $form->title(sprintf(gettext("editing %s"), pagetitle($page)));
371                 }
372                 
373                 showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
374         }
375         else {
376                 # save page
377                 check_canedit($page, $q, $session);
378                 checksessionexpiry($q, $session, $q->param('sid'));
379
380                 my $exists=-e "$config{srcdir}/$file";
381
382                 if ($form->field("do") ne "create" && ! $exists &&
383                     ! defined srcfile($file, 1)) {
384                         $form->tmpl_param("message", template("editpagegone.tmpl")->output);
385                         $form->field(name => "do", value => "create", force => 1);
386                         $form->tmpl_param("page_select", 0);
387                         $form->field(name => "page", type => 'hidden');
388                         $form->field(name => "type", type => 'hidden');
389                         $form->title(sprintf(gettext("editing %s"), $page));
390                         showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
391                         exit;
392                 }
393                 elsif ($form->field("do") eq "create" && $exists) {
394                         $form->tmpl_param("message", template("editcreationconflict.tmpl")->output);
395                         $form->field(name => "do", value => "edit", force => 1);
396                         $form->tmpl_param("page_select", 0);
397                         $form->field(name => "page", type => 'hidden');
398                         $form->field(name => "type", type => 'hidden');
399                         $form->title(sprintf(gettext("editing %s"), $page));
400                         $form->field("editcontent", 
401                                 value => readfile("$config{srcdir}/$file").
402                                          "\n\n\n".$form->field("editcontent"),
403                                 force => 1);
404                         showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
405                         exit;
406                 }
407                         
408                 my $message="";
409                 if (defined $form->field('comments') &&
410                     length $form->field('comments')) {
411                         $message=$form->field('comments');
412                 }
413                 
414                 my $content=$form->field('editcontent');
415                 check_content(content => $content, page => $page,
416                         cgi => $q, session => $session,
417                         subject => $message);
418                 run_hooks(editcontent => sub {
419                         $content=shift->(
420                                 content => $content,
421                                 page => $page,
422                                 cgi => $q,
423                                 session => $session,
424                         );
425                 });
426                 $content=~s/\r\n/\n/g;
427                 $content=~s/\r/\n/g;
428                 $content.="\n" if $content !~ /\n$/;
429
430                 $config{cgi}=0; # avoid cgi error message
431                 eval { writefile($file, $config{srcdir}, $content) };
432                 $config{cgi}=1;
433                 if ($@) {
434                         $form->field(name => "rcsinfo", value => rcs_prepedit($file),
435                                 force => 1);
436                         my $mtemplate=template("editfailedsave.tmpl");
437                         $mtemplate->param(error_message => $@);
438                         $form->tmpl_param("message", $mtemplate->output);
439                         $form->field("editcontent", value => $content, force => 1);
440                         $form->tmpl_param("page_select", 0);
441                         $form->field(name => "page", type => 'hidden');
442                         $form->field(name => "type", type => 'hidden');
443                         $form->title(sprintf(gettext("editing %s"), $page));
444                         showform($form, \@buttons, $session, $q,
445                                 forcebaseurl => $baseurl);
446                         exit;
447                 }
448                 
449                 my $conflict;
450                 if ($config{rcs}) {
451                         if (! $exists) {
452                                 rcs_add($file);
453                         }
454
455                         # Prevent deadlock with post-commit hook by
456                         # signaling to it that it should not try to
457                         # do anything.
458                         disable_commit_hook();
459                         $conflict=rcs_commit($file, $message,
460                                 $form->field("rcsinfo"),
461                                 $session->param("name"), $ENV{REMOTE_ADDR});
462                         enable_commit_hook();
463                         rcs_update();
464                 }
465                 
466                 # Refresh even if there was a conflict, since other changes
467                 # may have been committed while the post-commit hook was
468                 # disabled.
469                 require IkiWiki::Render;
470                 refresh();
471                 saveindex();
472
473                 if (defined $conflict) {
474                         $form->field(name => "rcsinfo", value => rcs_prepedit($file),
475                                 force => 1);
476                         $form->tmpl_param("message", template("editconflict.tmpl")->output);
477                         $form->field("editcontent", value => $conflict, force => 1);
478                         $form->field("do", "edit", force => 1);
479                         $form->tmpl_param("page_select", 0);
480                         $form->field(name => "page", type => 'hidden');
481                         $form->field(name => "type", type => 'hidden');
482                         $form->title(sprintf(gettext("editing %s"), $page));
483                         showform($form, \@buttons, $session, $q,
484                                 forcebaseurl => $baseurl);
485                 }
486                 else {
487                         # The trailing question mark tries to avoid broken
488                         # caches and get the most recent version of the page.
489                         redirect($q, urlto($page, undef, 1)."?updated");
490                 }
491         }
492
493         exit;
494 }
495
496 1