]> git.vanrenterghem.biz Git - git.ikiwiki.info.git/blob - IkiWiki/Plugin/rename.pm
yes, not committing the setup file to the same VCS is a security thing
[git.ikiwiki.info.git] / IkiWiki / Plugin / rename.pm
1 #!/usr/bin/perl
2 package IkiWiki::Plugin::rename;
4 use warnings;
5 use strict;
6 use IkiWiki 3.00;
8 sub import {
9         hook(type => "getsetup", id => "rename", call => \&getsetup);
10         hook(type => "formbuilder_setup", id => "rename", call => \&formbuilder_setup);
11         hook(type => "formbuilder", id => "rename", call => \&formbuilder);
12         hook(type => "sessioncgi", id => "rename", call => \&sessioncgi);
13         hook(type => "rename", id => "rename", call => \&rename_subpages);
14 }
16 sub getsetup () {
17         return 
18                 plugin => {
19                         safe => 1,
20                         rebuild => 0,
21                         section => "web",
22                 },
23 }
25 sub check_canrename ($$$$$$) {
26         my $src=shift;
27         my $srcfile=shift;
28         my $dest=shift;
29         my $destfile=shift;
30         my $q=shift;
31         my $session=shift;
33         my $attachment=! defined pagetype($pagesources{$src});
35         # Must be a known source file.
36         if (! exists $pagesources{$src}) {
37                 error(sprintf(gettext("%s does not exist"),
38                         htmllink("", "", $src, noimageinline => 1)));
39         }
40         
41         # Must exist on disk, and be a regular file.
42         if (! -e "$config{srcdir}/$srcfile") {
43                 error(sprintf(gettext("%s is not in the srcdir, so it cannot be renamed"), $srcfile));
44         }
45         elsif (-l "$config{srcdir}/$srcfile" && ! -f _) {
46                 error(sprintf(gettext("%s is not a file"), $srcfile));
47         }
49         # Must be editable.
50         IkiWiki::check_canedit($src, $q, $session);
51         if ($attachment) {
52                 if (IkiWiki::Plugin::attachment->can("check_canattach")) {
53                         IkiWiki::Plugin::attachment::check_canattach($session, $src, "$config{srcdir}/$srcfile");
54                 }
55                 else {
56                         error("renaming of attachments is not allowed");
57                 }
58         }
59         
60         # Dest checks can be omitted by passing undef.
61         if (defined $dest) {
62                 if ($srcfile eq $destfile) {
63                         error(gettext("no change to the file name was specified"));
64                 }
66                 # Must be a legal filename.
67                 if (IkiWiki::file_pruned($destfile)) {
68                         error(sprintf(gettext("illegal name")));
69                 }
71                 # Must not be a known source file.
72                 if ($src ne $dest && exists $pagesources{$dest}) {
73                         error(sprintf(gettext("%s already exists"),
74                                 htmllink("", "", $dest, noimageinline => 1)));
75                 }
76         
77                 # Must not exist on disk already.
78                 if (-l "$config{srcdir}/$destfile" || -e _) {
79                         error(sprintf(gettext("%s already exists on disk"), $destfile));
80                 }
81         
82                 # Must be editable.
83                 IkiWiki::check_canedit($dest, $q, $session);
84                 if ($attachment) {
85                         # Note that $srcfile is used here, not $destfile,
86                         # because it wants the current file, to check it.
87                         IkiWiki::Plugin::attachment::check_canattach($session, $dest, "$config{srcdir}/$srcfile");
88                 }
89         }
91         my $canrename;
92         IkiWiki::run_hooks(canrename => sub {
93                 return if defined $canrename;
94                 my $ret=shift->(cgi => $q, session => $session,
95                         src => $src, srcfile => $srcfile,
96                         dest => $dest, destfile => $destfile);
97                 if (defined $ret) {
98                         if ($ret eq "") {
99                                 $canrename=1;
100                         }
101                         elsif (ref $ret eq 'CODE') {
102                                 $ret->();
103                                 $canrename=0;
104                         }
105                         elsif (defined $ret) {
106                                 error($ret);
107                                 $canrename=0;
108                         }
109                 }
110         });
111         return defined $canrename ? $canrename : 1;
114 sub rename_form ($$$) {
115         my $q=shift;
116         my $session=shift;
117         my $page=shift;
119         eval q{use CGI::FormBuilder};
120         error($@) if $@;
121         my $f = CGI::FormBuilder->new(
122                 name => "rename",
123                 title => sprintf(gettext("rename %s"), pagetitle($page)),
124                 header => 0,
125                 charset => "utf-8",
126                 method => 'POST',
127                 javascript => 0,
128                 params => $q,
129                 action => IkiWiki::cgiurl(),
130                 stylesheet => 1,
131                 fields => [qw{do page new_name attachment}],
132         );
133         
134         $f->field(name => "do", type => "hidden", value => "rename", force => 1);
135         $f->field(name => "sid", type => "hidden", value => $session->id,
136                 force => 1);
137         $f->field(name => "page", type => "hidden", value => $page, force => 1);
138         $f->field(name => "new_name", value => pagetitle($page, 1), size => 60);
139         if (!$q->param("attachment")) {
140                 # insert the standard extensions
141                 my @page_types;
142                 if (exists $IkiWiki::hooks{htmlize}) {
143                         foreach my $key (grep { !/^_/ } keys %{$IkiWiki::hooks{htmlize}}) {
144                                 push @page_types, [$key, $IkiWiki::hooks{htmlize}{$key}{longname} || $key]
145                                         unless $IkiWiki::hooks{htmlize}{$key}{nocreate};
146                         }
147                 }
148                 @page_types=sort @page_types;
149         
150                 # make sure the current extension is in the list
151                 my ($ext) = $pagesources{$page}=~/\.([^.]+)$/;
152                 if (! $IkiWiki::hooks{htmlize}{$ext}) {
153                         unshift(@page_types, [$ext, $ext]);
154                 }
155         
156                 $f->field(name => "type", type => 'select',
157                         options => \@page_types,
158                         value => $ext, force => 1);
159                 
160                 foreach my $p (keys %pagesources) {
161                         if ($pagesources{$p}=~m/^\Q$page\E\//) {
162                                 $f->field(name => "subpages",
163                                         label => "",
164                                         type => "checkbox",
165                                         options => [ [ 1 => gettext("Also rename SubPages and attachments") ] ],
166                                         value => 1,
167                                         force => 1);
168                                 last;
169                         }
170                 }
171         }
172         $f->field(name => "attachment", type => "hidden");
174         return $f, ["Rename", "Cancel"];
177 sub rename_start ($$$$) {
178         my $q=shift;
179         my $session=shift;
180         my $attachment=shift;
181         my $page=shift;
183         # Special case for renaming held attachments; normal checks
184         # don't apply.
185         my $held=$attachment &&
186                 IkiWiki::Plugin::attachment->can("is_held_attachment") &&
187                 IkiWiki::Plugin::attachment::is_held_attachment($page);
188         if (! $held) {
189                 check_canrename($page, $pagesources{$page}, undef, undef,
190                         $q, $session);
191         }
193         # Save current form state to allow returning to it later
194         # without losing any edits.
195         # (But don't save what button was submitted, to avoid
196         # looping back to here.)
197         # Note: "_submit" is CGI::FormBuilder internals.
198         $q->param(-name => "_submit", -value => "");
199         $session->param(postrename => scalar $q->Vars);
200         IkiWiki::cgi_savesession($session);
201         
202         if (defined $attachment) {
203                 $q->param(-name => "attachment", -value => $attachment);
204         }
205         my ($f, $buttons)=rename_form($q, $session, $page);
206         IkiWiki::showform($f, $buttons, $session, $q);
207         exit 0;
210 sub postrename ($$$;$$) {
211         my $cgi=shift;
212         my $session=shift;
213         my $src=shift;
214         my $dest=shift;
215         my $attachment=shift;
217         # Load saved form state and return to edit page, using stored old
218         # cgi state. Or, if the rename was not started on the edit page, 
219         # return to the renamed page.
220         my $postrename=$session->param("postrename");
221         if (! defined $postrename) {
222                 IkiWiki::redirect($cgi, urlto(defined $dest ? $dest : $src));
223                 exit;
224         }
225         my $oldcgi=CGI->new($postrename);
226         $session->clear("postrename");
227         IkiWiki::cgi_savesession($session);
229         if (defined $dest) {
230                 if (! $attachment) {
231                         # They renamed the page they were editing. This requires
232                         # fixups to the edit form state.
233                         # Tweak the edit form to be editing the new page.
234                         $oldcgi->param("page", $dest);
235                 }
237                 # Update edit form content to fix any links present
238                 # on it.
239                 $oldcgi->param("editcontent",
240                         renamepage_hook($dest, $src, $dest,
241                                 scalar $oldcgi->param("editcontent")));
243                 # Get a new edit token; old was likely invalidated.
244                 $oldcgi->param("rcsinfo",
245                         IkiWiki::rcs_prepedit($pagesources{$dest}));
246         }
248         IkiWiki::cgi_editpage($oldcgi, $session);
251 sub formbuilder (@) {
252         my %params=@_;
253         my $form=$params{form};
255         if (defined $form->field("do") && ($form->field("do") eq "edit" ||
256             $form->field("do") eq "create")) {
257                 IkiWiki::decode_form_utf8($form);
258                 my $q=$params{cgi};
259                 my $session=$params{session};
261                 if ($form->submitted eq "Rename" && $form->field("do") eq "edit") {
262                         rename_start($q, $session, 0, $form->field("page"));
263                 }
264                 elsif ($form->submitted eq "Rename Attachment") {
265                         my @selected=map { Encode::decode_utf8($_) } $q->param("attachment_select");
266                         if (@selected > 1) {
267                                 error(gettext("Only one attachment can be renamed at a time."));
268                         }
269                         elsif (! @selected) {
270                                 error(gettext("Please select the attachment to rename."))
271                         }
272                         rename_start($q, $session, 1, $selected[0]);
273                 }
274         }
277 my $renamesummary;
279 sub formbuilder_setup (@) {
280         my %params=@_;
281         my $form=$params{form};
282         my $q=$params{cgi};
284         if (defined $form->field("do") && ($form->field("do") eq "edit" ||
285             $form->field("do") eq "create")) {
286                 # Rename button for the page, and also for attachments.
287                 push @{$params{buttons}}, "Rename" if $form->field("do") eq "edit";
288                 $form->tmpl_param("field-rename" => '<input name="_submit" type="submit" value="Rename Attachment" />');
290                 if (defined $renamesummary) {
291                         $form->tmpl_param(message => $renamesummary);
292                 }
293         }
296 sub sessioncgi ($$) {
297         my $q=shift;
299         if ($q->param("do") eq 'rename') {
300                 my $session=shift;
301                 my ($form, $buttons)=rename_form($q, $session, Encode::decode_utf8(scalar $q->param("page")));
302                 IkiWiki::decode_form_utf8($form);
303                 my $src=$form->field("page");
305                 if ($form->submitted eq 'Cancel') {
306                         postrename($q, $session, $src);
307                 }
308                 elsif ($form->submitted eq 'Rename' && $form->validate) {
309                         IkiWiki::checksessionexpiry($q, $session);
311                         # These untaints are safe because of the checks
312                         # performed in check_canrename later.
313                         my $srcfile=IkiWiki::possibly_foolish_untaint($pagesources{$src})
314                                 if exists $pagesources{$src};
315                         my $dest=IkiWiki::possibly_foolish_untaint(titlepage($form->field("new_name")));
316                         my $destfile=$dest;
317                         if (! $q->param("attachment")) {
318                                 my $type=$q->param('type');
319                                 if (defined $type && length $type && $IkiWiki::hooks{htmlize}{$type}) {
320                                         $type=IkiWiki::possibly_foolish_untaint($type);
321                                 }
322                                 else {
323                                         my ($ext)=$srcfile=~/\.([^.]+)$/;
324                                         $type=$ext;
325                                 }
326                                 
327                                 $destfile=newpagefile($dest, $type);
328                         }
329                 
330                         # Special case for renaming held attachments.
331                         my $held=$q->param("attachment") &&
332                                 IkiWiki::Plugin::attachment->can("is_held_attachment") &&
333                                 IkiWiki::Plugin::attachment::is_held_attachment($src);
334                         if ($held) {
335                                 rename($held, IkiWiki::Plugin::attachment::attachment_holding_location($dest));
336                                 postrename($q, $session, $src, $dest, scalar $q->param("attachment"))
337                                         unless defined $srcfile;
338                         }
339                         
340                         # Queue of rename actions to perfom.
341                         my @torename;
342                         push @torename, {
343                                 src => $src,
344                                 srcfile => $srcfile,
345                                 dest => $dest,
346                                 destfile => $destfile,
347                                 required => 1,
348                         };
350                         @torename=rename_hook(
351                                 torename => \@torename,
352                                 done => {},
353                                 cgi => $q,
354                                 session => $session,
355                         );
357                         require IkiWiki::Render;
358                         IkiWiki::disable_commit_hook() if $config{rcs};
359                         my %origpagesources=%pagesources;
361                         # First file renaming.
362                         foreach my $rename (@torename) {
363                                 if ($rename->{required}) {
364                                         do_rename($rename, $q, $session);
365                                 }
366                                 else {
367                                         eval {do_rename($rename, $q, $session)};
368                                         if ($@) {
369                                                 $rename->{error}=$@;
370                                                 next;
371                                         }
372                                 }
374                                 # Temporarily tweak pagesources to point to
375                                 # the renamed file, in case fixlinks needs
376                                 # to edit it.
377                                 $pagesources{$rename->{src}}=$rename->{destfile};
378                         }
379                         IkiWiki::rcs_commit_staged(
380                                 message => sprintf(gettext("rename %s to %s"), $srcfile, $destfile),
381                                 session => $session,
382                         ) if $config{rcs};
384                         # Then link fixups.
385                         foreach my $rename (@torename) {
386                                 next if $rename->{src} eq $rename->{dest};
387                                 next if $rename->{error};
388                                 foreach my $p (fixlinks($rename, $session)) {
389                                         # map old page names to new
390                                         foreach my $r (@torename) {
391                                                 next if $rename->{error};
392                                                 if ($r->{src} eq $p) {
393                                                         $p=$r->{dest};
394                                                         last;
395                                                 }
396                                         }
397                                         push @{$rename->{fixedlinks}}, $p;
398                                 }
399                         }
401                         # Then refresh.
402                         %pagesources=%origpagesources;
403                         if ($config{rcs}) {
404                                 IkiWiki::enable_commit_hook();
405                                 IkiWiki::rcs_update();
406                         }
407                         IkiWiki::refresh();
408                         IkiWiki::saveindex();
410                         # Find pages with remaining, broken links.
411                         foreach my $rename (@torename) {
412                                 next if $rename->{src} eq $rename->{dest};
413                                 
414                                 foreach my $page (keys %links) {
415                                         my $broken=0;
416                                         foreach my $link (@{$links{$page}}) {
417                                                 my $bestlink=bestlink($page, $link);
418                                                 if ($bestlink eq $rename->{src}) {
419                                                         push @{$rename->{brokenlinks}}, $page;
420                                                         last;
421                                                 }
422                                         }
423                                 }
424                         }
426                         # Generate a summary, that will be shown at the top
427                         # of the edit template.
428                         $renamesummary="";
429                         foreach my $rename (@torename) {
430                                 my $template=template("renamesummary.tmpl");
431                                 $template->param(src => $rename->{srcfile});
432                                 $template->param(dest => $rename->{destfile});
433                                 $template->param(error => $rename->{error});
434                                 if ($rename->{src} ne $rename->{dest}) {
435                                         $template->param(brokenlinks_checked => 1);
436                                         $template->param(brokenlinks => linklist($rename->{dest}, $rename->{brokenlinks}));
437                                         $template->param(fixedlinks => linklist($rename->{dest}, $rename->{fixedlinks}));
438                                 }
439                                 $renamesummary.=$template->output;
440                         }
442                         postrename($q, $session, $src, $dest, scalar $q->param("attachment"));
443                 }
444                 else {
445                         IkiWiki::showform($form, $buttons, $session, $q);
446                 }
448                 exit 0;
449         }
452 # Add subpages to the list of pages to be renamed, if needed.
453 sub rename_subpages (@) {
454         my %params = @_;
456         my %torename = %{$params{torename}};
457         my $q = $params{cgi};
458         my $src = $torename{src};
459         my $srcfile = $torename{src};
460         my $dest = $torename{dest};
461         my $destfile = $torename{dest};
463         return () unless ($q->param("subpages") && $src ne $dest);
465         my @ret;
466         foreach my $p (keys %pagesources) {
467                 next unless $pagesources{$p}=~m/^\Q$src\E\//;
468                 # If indexpages is enabled, the srcfile should not be confused
469                 # with a subpage.
470                 next if $pagesources{$p} eq $srcfile;
472                 my $d=$pagesources{$p};
473                 $d=~s/^\Q$src\E\//$dest\//;
474                 push @ret, {
475                         src => $p,
476                         srcfile => $pagesources{$p},
477                         dest => pagename($d),
478                         destfile => $d,
479                         required => 0,
480                 };
481         }
482         return @ret;
485 sub linklist {
486         # generates a list of links in a form suitable for FormBuilder
487         my $dest=shift;
488         my $list=shift;
489         # converts a list of pages into a list of links
490         # in a form suitable for FormBuilder.
492         [map {
493                 {
494                         page => htmllink($dest, $dest, $_,
495                                         noimageinline => 1,
496                                         linktext => pagetitle($_),
497                                 )
498                 }
499         } @{$list}]
502 sub renamepage_hook ($$$$) {
503         my ($page, $src, $dest, $content)=@_;
505         IkiWiki::run_hooks(renamepage => sub {
506                 $content=shift->(
507                         page => $page,
508                         oldpage => $src,
509                         newpage => $dest,
510                         content => $content,
511                 );
512         });
514         return $content;
517 sub rename_hook {
518         my %params = @_;
520         my @torename=@{$params{torename}};
521         my %done=%{$params{done}};
522         my $q=$params{cgi};
523         my $session=$params{session};
525         return () unless @torename;
527         my @nextset;
528         foreach my $torename (@torename) {
529                 unless (exists $done{$torename->{src}} && $done{$torename->{src}}) {
530                         IkiWiki::run_hooks(rename => sub {
531                                 push @nextset, shift->(
532                                         torename => $torename,
533                                         cgi => $q,
534                                         session => $session,
535                                 );
536                         });
537                         $done{$torename->{src}}=1;
538                 }
539         }
541         push @torename, rename_hook(
542                 torename => \@nextset,
543                 done => \%done,
544                 cgi => $q,
545                 session => $session,
546         );
548         # dedup
549         my %seen;
550         return grep { ! $seen{$_->{src}}++ } @torename;
553 sub do_rename ($$$) {
554         my $rename=shift;
555         my $q=shift;
556         my $session=shift;
558         # First, check if this rename is allowed.
559         check_canrename($rename->{src},
560                 $rename->{srcfile},
561                 $rename->{dest},
562                 $rename->{destfile},
563                 $q, $session);
565         # Ensure that the dest directory exists and is ok.
566         IkiWiki::prep_writefile($rename->{destfile}, $config{srcdir});
568         if ($config{rcs}) {
569                 IkiWiki::rcs_rename($rename->{srcfile}, $rename->{destfile});
570         }
571         else {
572                 if (! rename($config{srcdir}."/".$rename->{srcfile},
573                              $config{srcdir}."/".$rename->{destfile})) {
574                         error("rename: $!");
575                 }
576         }
580 sub fixlinks ($$$) {
581         my $rename=shift;
582         my $session=shift;
584         my @fixedlinks;
586         foreach my $page (keys %links) {
587                 my $needfix=0;
588                 foreach my $link (@{$links{$page}}) {
589                         my $bestlink=bestlink($page, $link);
590                         if ($bestlink eq $rename->{src}) {
591                                 $needfix=1;
592                                 last;
593                         }
594                 }
595                 if ($needfix) {
596                         my $file=$pagesources{$page};
597                         next unless -e $config{srcdir}."/".$file;
598                         my $oldcontent=readfile($config{srcdir}."/".$file);
599                         my $content=renamepage_hook($page, $rename->{src}, $rename->{dest}, $oldcontent);
600                         if ($oldcontent ne $content) {
601                                 my $token=IkiWiki::rcs_prepedit($file);
602                                 eval { writefile($file, $config{srcdir}, $content) };
603                                 next if $@;
604                                 my $conflict=IkiWiki::rcs_commit(
605                                         file => $file,
606                                         message => sprintf(gettext("update for rename of %s to %s"), $rename->{srcfile}, $rename->{destfile}),
607                                         token => $token,
608                                         session => $session,
609                                 );
610                                 push @fixedlinks, $page if ! defined $conflict;
611                         }
612                 }
613         }
615         return @fixedlinks;