2 package IkiWiki::Plugin::editpage;
7 use open qw{:utf8 :std};
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);
24 if (exists $wikistate{editpage} && exists $wikistate{editpage}{previews}) {
25 # Expire old preview files after one hour.
26 my $expire=time - (60 * 60);
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.
34 foreach my $page (keys %renderedfiles) {
35 if (grep { $_ eq $file } @{$renderedfiles{$page}}) {
40 debug(sprintf(gettext("removing old preview %s"), $file));
41 IkiWiki::prune("$config{destdir}/$file");
44 elsif (defined $mtime) {
45 push @previews, $file;
48 $wikistate{editpage}{previews}=\@previews;
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.
57 sub check_canedit ($$$;$) {
64 run_hooks(canedit => sub {
65 return if defined $canedit;
66 my $ret=shift->($page, $q, $session);
71 elsif (ref $ret eq 'CODE') {
72 $ret->() unless $nonfatal;
75 elsif (defined $ret) {
76 error($ret) unless $nonfatal;
81 return defined $canedit ? $canedit : 1;
84 sub check_content (@) {
87 return 1 if ! exists $hooks{checkcontent}; # optimisation
89 if (exists $pagesources{$params{page}}) {
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{$_};
96 $params{diff}=join("\n", @diff);
100 run_hooks(checkcontent => sub {
101 return if defined $ok;
102 my $ret=shift->(%params);
107 elsif (ref $ret eq 'CODE') {
108 $ret->() unless $params{nonfatal};
111 elsif (defined $ret) {
112 error($ret) unless $params{nonfatal};
118 return defined $ok ? $ok : 1;
121 sub cgi_editpage ($$) {
125 my $do=$q->param('do');
126 return unless $do eq 'create' || $do eq 'edit';
130 my @fields=qw(do rcsinfo subpage from page type editcontent comments);
131 my @buttons=("Save Page", "Preview", "Cancel");
132 eval q{use CGI::FormBuilder};
134 my $form = CGI::FormBuilder->new(
138 required => [qw{editcontent}],
141 action => $config{cgiurl},
144 template => scalar template_params("editpage.tmpl"),
147 decode_form_utf8($form);
148 run_hooks(formbuilder_setup => sub {
149 shift->(form => $form, cgi => $q, session => $session,
150 buttons => \@buttons);
152 decode_form_utf8($form);
154 # This untaint is safe because we check file_pruned and
156 my ($page)=$form->field('page')=~/$config{wiki_file_regexp}/;
157 $page=possibly_foolish_untaint($page);
158 my $absolute=($page =~ s#^/+##);
159 if (! defined $page || ! length $page ||
160 file_pruned($page, $config{srcdir})) {
161 error(gettext("bad page name"));
164 my $baseurl = urlto($page, undef, 1);
167 if (defined $form->field('from')) {
168 ($from)=$form->field('from')=~/$config{wiki_file_regexp}/;
173 if (exists $pagesources{$page} && $form->field("do") ne "create") {
174 $file=$pagesources{$page};
175 $type=pagetype($file);
176 if (! defined $type || $type=~/^_/) {
177 error(sprintf(gettext("%s is not an editable page"), $page));
179 if (! $form->submitted) {
180 $form->field(name => "rcsinfo",
181 value => rcs_prepedit($file), force => 1);
183 $form->field(name => "editcontent", validate => '/.*/');
186 $type=$form->param('type');
187 if (defined $type && length $type && $hooks{htmlize}{$type}) {
188 $type=possibly_foolish_untaint($type);
190 elsif (defined $from && exists $pagesources{$from}) {
191 # favor the type of linking page
192 $type=pagetype($pagesources{$from});
194 $type=$config{default_pageext} unless defined $type;
195 $file=newpagefile($page, $type);
196 if (! $form->submitted) {
197 $form->field(name => "rcsinfo", value => "", force => 1);
199 $form->field(name => "editcontent", validate => '/.+/');
202 $form->field(name => "do", type => 'hidden');
203 $form->field(name => "sid", type => "hidden", value => $session->id,
205 $form->field(name => "from", type => 'hidden');
206 $form->field(name => "rcsinfo", type => 'hidden');
207 $form->field(name => "subpage", type => 'hidden');
208 $form->field(name => "page", value => $page, force => 1);
209 $form->field(name => "type", value => $type, force => 1);
210 $form->field(name => "comments", type => "text", size => 80);
211 $form->field(name => "editcontent", type => "textarea", rows => 20,
213 $form->tmpl_param("can_commit", $config{rcs});
214 $form->tmpl_param("indexlink", indexlink());
215 $form->tmpl_param("helponformattinglink",
216 htmllink($page, $page, "ikiwiki/formatting",
218 linktext => "FormattingHelp"));
220 if ($form->submitted eq "Cancel") {
221 if ($form->field("do") eq "create" && defined $from) {
222 redirect($q, urlto($from, undef, 1));
224 elsif ($form->field("do") eq "create") {
225 redirect($q, $config{url});
228 redirect($q, urlto($page, undef, 1));
232 elsif ($form->submitted eq "Preview") {
233 my $new=not exists $pagesources{$page};
235 # temporarily record its type
236 $pagesources{$page}=$page.".".$type;
238 my %wasrendered=map { $_ => 1 } @{$renderedfiles{$page}};
240 my $content=$form->field('editcontent');
242 run_hooks(editcontent => sub {
250 my $preview=htmlize($page, $page, $type,
251 linkify($page, $page,
252 preprocess($page, $page,
253 filter($page, $page, $content), 0, 1)));
254 run_hooks(format => sub {
260 $form->tmpl_param("page_preview", $preview);
263 delete $pagesources{$page};
266 # Previewing may have created files on disk.
267 # Keep a list of these to be deleted later.
268 my %previews = map { $_ => 1 } @{$wikistate{editpage}{previews}};
269 foreach my $f (@{$renderedfiles{$page}}) {
270 $previews{$f}=1 unless $wasrendered{$f};
272 @{$wikistate{editpage}{previews}} = keys %previews;
273 $renderedfiles{$page}=[keys %wasrendered];
276 elsif ($form->submitted eq "Save Page") {
277 $form->tmpl_param("page_preview", "");
280 if ($form->submitted ne "Save Page" || ! $form->validate) {
281 if ($form->field("do") eq "create") {
284 if (! defined $from || ! length $from ||
285 $from ne $form->field('from') ||
286 file_pruned($from, $config{srcdir}) ||
290 @page_locs=$best_loc=$page;
296 if ((defined $form->field('subpage') && length $form->field('subpage')) ||
297 $page eq gettext('discussion')) {
298 $best_loc="$from/$page";
301 $best_loc=$dir.$page;
304 push @page_locs, $dir.$page;
305 push @page_locs, "$from/$page";
306 while (length $dir) {
308 push @page_locs, $dir.$page;
311 push @page_locs, "$config{userdir}/$page"
312 if length $config{userdir};
316 ! exists $pagecase{lc $_}
319 # hmm, someone else made the page in the
321 if ($form->submitted eq "Preview") {
322 # let them go ahead with the edit
323 # and resolve the conflict at save
328 redirect($q, urlto($page, undef, 1));
333 my @editable_locs = grep {
334 check_canedit($_, $q, $session, 1)
336 if (! @editable_locs) {
337 # let it throw an error this time
338 map { check_canedit($_, $q, $session) } @page_locs;
342 if (exists $hooks{htmlize}) {
343 @page_types=grep { !/^_/ }
344 keys %{$hooks{htmlize}};
347 $form->tmpl_param("page_select", 1);
348 $form->field(name => "page", type => 'select',
349 options => [ map { [ $_, pagetitle($_, 1) ] } @editable_locs ],
351 $form->field(name => "type", type => 'select',
352 options => \@page_types);
353 $form->title(sprintf(gettext("creating %s"), pagetitle($page)));
356 elsif ($form->field("do") eq "edit") {
357 check_canedit($page, $q, $session);
358 if (! defined $form->field('editcontent') ||
359 ! length $form->field('editcontent')) {
361 if (exists $pagesources{$page}) {
362 $content=readfile(srcfile($pagesources{$page}));
363 $content=~s/\n/\r\n/g;
365 $form->field(name => "editcontent", value => $content,
368 $form->tmpl_param("page_select", 0);
369 $form->field(name => "page", type => 'hidden');
370 $form->field(name => "type", type => 'hidden');
371 $form->title(sprintf(gettext("editing %s"), pagetitle($page)));
374 showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
378 check_canedit($page, $q, $session);
379 checksessionexpiry($q, $session, $q->param('sid'));
381 my $exists=-e "$config{srcdir}/$file";
383 if ($form->field("do") ne "create" && ! $exists &&
384 ! defined srcfile($file, 1)) {
385 $form->tmpl_param("message", template("editpagegone.tmpl")->output);
386 $form->field(name => "do", value => "create", force => 1);
387 $form->tmpl_param("page_select", 0);
388 $form->field(name => "page", type => 'hidden');
389 $form->field(name => "type", type => 'hidden');
390 $form->title(sprintf(gettext("editing %s"), $page));
391 showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
394 elsif ($form->field("do") eq "create" && $exists) {
395 $form->tmpl_param("message", template("editcreationconflict.tmpl")->output);
396 $form->field(name => "do", value => "edit", force => 1);
397 $form->tmpl_param("page_select", 0);
398 $form->field(name => "page", type => 'hidden');
399 $form->field(name => "type", type => 'hidden');
400 $form->title(sprintf(gettext("editing %s"), $page));
401 $form->field("editcontent",
402 value => readfile("$config{srcdir}/$file").
403 "\n\n\n".$form->field("editcontent"),
405 showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
410 if (defined $form->field('comments') &&
411 length $form->field('comments')) {
412 $message=$form->field('comments');
415 my $content=$form->field('editcontent');
416 check_content(content => $content, page => $page,
417 cgi => $q, session => $session,
418 subject => $message);
419 run_hooks(editcontent => sub {
427 $content=~s/\r\n/\n/g;
429 $content.="\n" if $content !~ /\n$/;
431 $config{cgi}=0; # avoid cgi error message
432 eval { writefile($file, $config{srcdir}, $content) };
435 $form->field(name => "rcsinfo", value => rcs_prepedit($file),
437 my $mtemplate=template("editfailedsave.tmpl");
438 $mtemplate->param(error_message => $@);
439 $form->tmpl_param("message", $mtemplate->output);
440 $form->field("editcontent", value => $content, force => 1);
441 $form->tmpl_param("page_select", 0);
442 $form->field(name => "page", type => 'hidden');
443 $form->field(name => "type", type => 'hidden');
444 $form->title(sprintf(gettext("editing %s"), $page));
445 showform($form, \@buttons, $session, $q,
446 forcebaseurl => $baseurl);
456 # Prevent deadlock with post-commit hook by
457 # signaling to it that it should not try to
459 disable_commit_hook();
460 $conflict=rcs_commit($file, $message,
461 $form->field("rcsinfo"),
462 $session->param("name"), $ENV{REMOTE_ADDR});
463 enable_commit_hook();
467 # Refresh even if there was a conflict, since other changes
468 # may have been committed while the post-commit hook was
470 require IkiWiki::Render;
474 if (defined $conflict) {
475 $form->field(name => "rcsinfo", value => rcs_prepedit($file),
477 $form->tmpl_param("message", template("editconflict.tmpl")->output);
478 $form->field("editcontent", value => $conflict, force => 1);
479 $form->field("do", "edit", force => 1);
480 $form->tmpl_param("page_select", 0);
481 $form->field(name => "page", type => 'hidden');
482 $form->field(name => "type", type => 'hidden');
483 $form->title(sprintf(gettext("editing %s"), $page));
484 showform($form, \@buttons, $session, $q,
485 forcebaseurl => $baseurl);
488 # The trailing question mark tries to avoid broken
489 # caches and get the most recent version of the page.
490 redirect($q, urlto($page, undef, 1)."?updated");