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);
25 if (exists $wikistate{editpage} && exists $wikistate{editpage}{previews}) {
26 # Expire old preview files after one hour.
27 my $expire=time - (60 * 60);
30 foreach my $file (@{$wikistate{editpage}{previews}}) {
31 my $mtime=(stat("$config{destdir}/$file"))[9];
32 if (defined $mtime && $mtime <= $expire) {
33 # Avoid deleting a preview that was later saved.
35 foreach my $page (keys %renderedfiles) {
36 if (grep { $_ eq $file } @{$renderedfiles{$page}}) {
41 debug(sprintf(gettext("removing old preview %s"), $file));
42 IkiWiki::prune("$config{destdir}/$file", $config{destdir});
45 elsif (defined $mtime) {
46 push @previews, $file;
49 $wikistate{editpage}{previews}=\@previews;
53 # Back to ikiwiki namespace for the rest, this code is very much
54 # internal to ikiwiki even though it's separated into a plugin,
55 # and other plugins use the function below.
58 sub cgi_editpage ($$) {
62 my $do=$q->param('do');
63 return unless $do eq 'create' || $do eq 'edit';
67 my @fields=qw(do rcsinfo subpage from page type editcontent
68 editmessage subscribe);
69 my @buttons=("Save Page", "Preview", "Cancel");
70 eval q{use CGI::FormBuilder};
72 my $form = CGI::FormBuilder->new(
76 required => [qw{editcontent}],
79 action => IkiWiki::cgiurl(),
82 template => { template("editpage.tmpl") },
85 decode_form_utf8($form);
86 run_hooks(formbuilder_setup => sub {
87 shift->(form => $form, cgi => $q, session => $session,
88 buttons => \@buttons);
90 decode_form_utf8($form);
92 # This untaint is safe because we check file_pruned and
94 my ($page)=$form->field('page')=~/$config{wiki_file_regexp}/;
95 if (! defined $page) {
96 error(gettext("bad page name"));
98 $page=possibly_foolish_untaint($page);
99 my $absolute=($page =~ s#^/+##); # absolute name used to force location
100 if (! defined $page || ! length $page ||
101 file_pruned($page)) {
102 error(gettext("bad page name"));
105 my $baseurl = urlto($page);
108 if (defined $form->field('from')) {
109 ($from)=$form->field('from')=~/$config{wiki_file_regexp}/;
114 if (exists $pagesources{$page} && $form->field("do") ne "create") {
115 $file=$pagesources{$page};
116 $type=pagetype($file);
117 if (! defined $type || $type=~/^_/) {
118 error(sprintf(gettext("%s is not an editable page"), $page));
120 if (! $form->submitted) {
121 $form->field(name => "rcsinfo",
122 value => rcs_prepedit($file), force => 1);
124 $form->field(name => "editcontent", validate => '/.*/');
127 $type=$form->param('type');
128 if (defined $type && length $type && $hooks{htmlize}{$type}) {
129 $type=possibly_foolish_untaint($type);
131 elsif (defined $from && exists $pagesources{$from}) {
132 # favor the type of linking page
133 $type=pagetype($pagesources{$from});
135 $type=$config{default_pageext}
136 if ! defined $type || $type=~/^_/; # not internal type
137 $file=newpagefile($page, $type);
138 if (! $form->submitted) {
139 $form->field(name => "rcsinfo", value => "", force => 1);
141 $form->field(name => "editcontent", validate => '/.+/');
144 $form->field(name => "do", type => 'hidden');
145 $form->field(name => "sid", type => "hidden", value => $session->id,
147 $form->field(name => "from", type => 'hidden');
148 $form->field(name => "rcsinfo", type => 'hidden');
149 $form->field(name => "subpage", type => 'hidden');
150 $form->field(name => "page", value => $page, force => 1);
151 $form->field(name => "type", value => $type, force => 1);
152 $form->field(name => "editmessage", type => "text", size => 80);
153 $form->field(name => "editcontent", type => "textarea", rows => 20,
155 $form->tmpl_param("can_commit", $config{rcs});
156 $form->tmpl_param("helponformattinglink",
157 htmllink($page, $page, "ikiwiki/formatting",
159 linktext => "FormattingHelp"));
161 my $cansubscribe=IkiWiki::Plugin::notifyemail->can("subscribe")
162 && IkiWiki::Plugin::comments->can("import")
163 && defined $session->param('name');
165 $form->field(name => "subscribe", type => "checkbox",
166 options => [gettext("email comments to me")]);
169 $form->field(name => "subscribe", type => 'hidden');
173 if ($form->submitted eq "Cancel") {
174 if ($form->field("do") eq "create" && defined $from) {
175 redirect($q, urlto($from));
177 elsif ($form->field("do") eq "create") {
178 redirect($q, baseurl(undef));
181 redirect($q, $baseurl);
185 elsif ($form->submitted eq "Preview") {
188 my $new=not exists $pagesources{$page};
189 # temporarily record its type
190 $pagesources{$page}=$page.".".$type if $new;
191 my %wasrendered=map { $_ => 1 } @{$renderedfiles{$page}};
193 my $content=$form->field('editcontent');
195 run_hooks(editcontent => sub {
203 my $preview=htmlize($page, $page, $type,
204 linkify($page, $page,
205 preprocess($page, $page,
206 filter($page, $page, $content), 0, 1)));
207 run_hooks(format => sub {
213 $form->tmpl_param("page_preview", $preview);
215 # Previewing may have created files on disk.
216 # Keep a list of these to be deleted later.
217 my %previews = map { $_ => 1 } @{$wikistate{editpage}{previews}};
218 foreach my $f (@{$renderedfiles{$page}}) {
219 $previews{$f}=1 unless $wasrendered{$f};
222 # Throw out any other state changes made during previewing,
223 # and save the previews list.
225 @{$wikistate{editpage}{previews}} = keys %previews;
228 elsif ($form->submitted eq "Save Page") {
229 $form->tmpl_param("page_preview", "");
232 if ($form->submitted ne "Save Page" || ! $form->validate) {
233 if ($form->field("do") eq "create") {
236 if (! defined $from || ! length $from ||
237 $from ne $form->field('from') ||
238 file_pruned($from) ||
241 @page_locs=$best_loc=$page;
242 unshift @page_locs, lc($page)
243 if ! $form->submitted && lc($page) ne $page;
245 elsif (lc $page eq lc $config{discussionpage}) {
246 @page_locs=$best_loc="$from/".lc($page);
252 if ((defined $form->field('subpage') &&
253 length $form->field('subpage'))) {
254 $best_loc="$from/$page";
257 $best_loc=$dir.$page;
260 my $mixedcase=lc($page) ne $page;
262 push @page_locs, $dir.lc($page) if $mixedcase;
263 push @page_locs, $dir.$page;
264 push @page_locs, $from."/".lc($page) if $mixedcase;
265 push @page_locs, $from."/".$page;
266 while (length $dir) {
268 push @page_locs, $dir.lc($page) if $mixedcase;
269 push @page_locs, $dir.$page;
272 my $userpage=IkiWiki::userpage($page);
273 push @page_locs, $userpage
274 if ! grep { $_ eq $userpage } @page_locs;
278 ! exists $pagecase{lc $_}
281 # hmm, someone else made the page in the
284 # let them go ahead with the edit
285 # and resolve the conflict at save
290 redirect($q, $baseurl);
295 my @editable_locs = grep {
296 check_canedit($_, $q, $session, 1)
298 if (! @editable_locs) {
299 # now let it throw an error, or prompt for
301 map { check_canedit($_, $q, $session) }
302 ($best_loc, @page_locs);
306 if (exists $hooks{htmlize}) {
307 foreach my $key (grep { !/^_/ } keys %{$hooks{htmlize}}) {
308 push @page_types, [$key, $hooks{htmlize}{$key}{longname} || $key]
309 unless $hooks{htmlize}{$key}{nocreate};
312 @page_types=sort @page_types;
314 $form->tmpl_param("page_select", 1);
315 $form->field(name => "page", type => 'select',
316 options => [ map { [ $_, pagetitle($_, 1) ] } @editable_locs ],
318 $form->field(name => "type", type => 'select',
319 options => \@page_types);
320 $form->title(sprintf(gettext("creating %s"), pagetitle(basename($page))));
323 elsif ($form->field("do") eq "edit") {
324 check_canedit($page, $q, $session);
325 if (! defined $form->field('editcontent') ||
326 ! length $form->field('editcontent')) {
328 if (exists $pagesources{$page}) {
329 $content=readfile(srcfile($pagesources{$page}));
330 $content=~s/\n/\r\n/g;
332 $form->field(name => "editcontent", value => $content,
335 $form->tmpl_param("page_select", 0);
336 $form->field(name => "page", type => 'hidden');
337 $form->field(name => "type", type => 'hidden');
338 $form->title(sprintf(gettext("editing %s"), pagetitle(basename($page))));
341 showform($form, \@buttons, $session, $q, page => $page);
345 check_canedit($page, $q, $session);
346 checksessionexpiry($q, $session);
348 my $exists=-e "$config{srcdir}/$file";
350 if ($form->field("do") ne "create" && ! $exists &&
351 ! defined srcfile($file, 1)) {
352 $form->tmpl_param("message", template("editpagegone.tmpl")->output);
353 $form->field(name => "do", value => "create", force => 1);
354 $form->tmpl_param("page_select", 0);
355 $form->field(name => "page", type => 'hidden');
356 $form->field(name => "type", type => 'hidden');
357 $form->title(sprintf(gettext("editing %s"), $page));
358 showform($form, \@buttons, $session, $q,
362 elsif ($form->field("do") eq "create" && $exists) {
363 $form->tmpl_param("message", template("editcreationconflict.tmpl")->output);
364 $form->field(name => "do", value => "edit", force => 1);
365 $form->tmpl_param("page_select", 0);
366 $form->field(name => "page", type => 'hidden');
367 $form->field(name => "type", type => 'hidden');
368 $form->title(sprintf(gettext("editing %s"), $page));
369 $form->field("editcontent",
370 value => readfile("$config{srcdir}/$file").
371 "\n\n\n".$form->field("editcontent"),
373 showform($form, \@buttons, $session, $q,
379 if (defined $form->field('editmessage') &&
380 length $form->field('editmessage')) {
381 $message=$form->field('editmessage');
384 my $content=$form->field('editcontent');
385 check_content(content => $content, page => $page,
386 cgi => $q, session => $session,
387 subject => $message);
388 run_hooks(editcontent => sub {
396 $content=~s/\r\n/\n/g;
398 $content.="\n" if $content !~ /\n$/;
400 $config{cgi}=0; # avoid cgi error message
401 eval { writefile($file, $config{srcdir}, $content) };
404 # save $@ in case a called function clobbers it
406 $form->field(name => "rcsinfo", value => rcs_prepedit($file),
408 my $mtemplate=template("editfailedsave.tmpl");
409 $mtemplate->param(error_message => $error);
410 $form->tmpl_param("message", $mtemplate->output);
411 $form->field("editcontent", value => $content, force => 1);
412 $form->tmpl_param("page_select", 0);
413 $form->field(name => "page", type => 'hidden');
414 $form->field(name => "type", type => 'hidden');
415 $form->title(sprintf(gettext("editing %s"), $page));
416 showform($form, \@buttons, $session, $q,
427 # Prevent deadlock with post-commit hook by
428 # signaling to it that it should not try to
430 disable_commit_hook();
431 $conflict=rcs_commit(
434 token => $form->field("rcsinfo"),
437 enable_commit_hook();
441 # Refresh even if there was a conflict, since other changes
442 # may have been committed while the post-commit hook was
444 require IkiWiki::Render;
448 if (defined $conflict) {
449 $form->field(name => "rcsinfo", value => rcs_prepedit($file),
451 $form->tmpl_param("message", template("editconflict.tmpl")->output);
452 $form->field("editcontent", value => $conflict, force => 1);
453 $form->field("do", "edit", force => 1);
454 $form->tmpl_param("page_select", 0);
455 $form->field(name => "page", type => 'hidden');
456 $form->field(name => "type", type => 'hidden');
457 $form->title(sprintf(gettext("editing %s"), $page));
458 showform($form, \@buttons, $session, $q,
462 # The trailing question mark tries to avoid broken
463 # caches and get the most recent version of the page.
464 redirect($q, $baseurl."?updated");
467 if ($cansubscribe && length $form->field("subscribe")) {
468 my $subspec="comment($page)";
469 IkiWiki::Plugin::notifyemail::subscribe(
470 $session->param('name'), $subspec);