]> git.vanrenterghem.biz Git - git.ikiwiki.info.git/blob - IkiWiki/Plugin/remove.pm
Add automated test for using the CGI with git, including CVE-2016-10026
[git.ikiwiki.info.git] / IkiWiki / Plugin / remove.pm
1 #!/usr/bin/perl
2 package IkiWiki::Plugin::remove;
4 use warnings;
5 use strict;
6 use IkiWiki 3.00;
8 sub import {
9         hook(type => "getsetup", id => "remove", call => \&getsetup);
10         hook(type => "formbuilder_setup", id => "remove", call => \&formbuilder_setup);
11         hook(type => "formbuilder", id => "remove", call => \&formbuilder);
12         hook(type => "sessioncgi", id => "remove", call => \&sessioncgi);
14 }
16 sub getsetup () {
17         return 
18                 plugin => {
19                         safe => 1,
20                         rebuild => 0,
21                         section => "web",
22                 },
23 }
25 sub allowed_dirs {
26         return grep { defined $_ } (
27                 $config{srcdir},
28                 $IkiWiki::Plugin::transient::transientdir,
29         );
30 }
32 sub check_canremove ($$$) {
33         my $page=shift;
34         my $q=shift;
35         my $session=shift;
37         # Must be a known source file.
38         if (! exists $pagesources{$page}) {
39                 error(sprintf(gettext("%s does not exist"),
40                         htmllink("", "", $page, noimageinline => 1)));
41         }
43         # Must exist in either the srcdir or a suitable underlay (e.g.
44         # transient underlay), and be a regular file.
45         my $file=$pagesources{$page};
46         my $dir;
48         foreach my $srcdir (allowed_dirs()) {
49                 if (-e "$srcdir/$file") {
50                         $dir = $srcdir;
51                         last;
52                 }
53         }
55         if (! defined $dir) {
56                 error(sprintf(gettext("%s is not in the srcdir, so it cannot be deleted"), $file));
57         }
58         elsif (-l "$dir/$file" && ! -f _) {
59                 error(sprintf(gettext("%s is not a file"), $file));
60         }
61         
62         # If a user can't upload an attachment, don't let them delete it.
63         # This is sorta overkill, but better safe than sorry.
64         if (! defined pagetype($pagesources{$page})) {
65                 if (IkiWiki::Plugin::attachment->can("check_canattach")) {
66                         IkiWiki::Plugin::attachment::check_canattach($session, $page, "$dir/$file");
67                 }
68                 else {
69                         error("removal of attachments is not allowed");
70                 }
71         }
73         my $canremove;
74         IkiWiki::run_hooks(canremove => sub {
75                 return if defined $canremove;
76                 my $ret=shift->(page => $page, cgi => $q, session => $session);
77                 if (defined $ret) {
78                         if ($ret eq "") {
79                                 $canremove=1;
80                         }
81                         elsif (ref $ret eq 'CODE') {
82                                 $ret->();
83                                 $canremove=0;
84                         }
85                         elsif (defined $ret) {
86                                 error($ret);
87                                 $canremove=0;
88                         }
89                 }
90         });
91         return defined $canremove ? $canremove : 1;
92 }
94 sub formbuilder_setup (@) {
95         my %params=@_;
96         my $form=$params{form};
97         my $q=$params{cgi};
99         if (defined $form->field("do") && ($form->field("do") eq "edit" ||
100             $form->field("do") eq "create")) {
101                 # Removal button for the page, and also for attachments.
102                 push @{$params{buttons}}, "Remove" if $form->field("do") eq "edit";
103                 $form->tmpl_param("field-remove" => '<input name="_submit" type="submit" value="Remove Attachments" />');
104         }
107 sub confirmation_form ($$) {
108         my $q=shift;
109         my $session=shift;
111         eval q{use CGI::FormBuilder};
112         error($@) if $@;
113         my $f = CGI::FormBuilder->new(
114                 name => "remove",
115                 header => 0,
116                 charset => "utf-8",
117                 method => 'POST',
118                 javascript => 0,
119                 params => $q,
120                 action => IkiWiki::cgiurl(),
121                 stylesheet => 1,
122                 fields => [qw{do page}],
123         );
124         
125         $f->field(name => "sid", type => "hidden", value => $session->id,
126                 force => 1);
127         $f->field(name => "do", type => "hidden", value => "remove", force => 1);
129         return $f, ["Remove", "Cancel"];
132 sub removal_confirm ($$@) {
133         my $q=shift;
134         my $session=shift;
135         my $attachment=shift;
136         my @pages=@_;
137                 
138         # Special case for unsaved attachments.
139         foreach my $page (@pages) {
140                 if ($attachment && IkiWiki::Plugin::attachment->can("is_held_attachment")) {
141                         my $f=IkiWiki::Plugin::attachment::is_held_attachment($page);
142                         if (defined $f) {
143                                 require IkiWiki::Render;
144                                 IkiWiki::prune($f, "$config{wikistatedir}/attachments");
145                         }
146                 }
147         }
148         @pages=grep { exists $pagesources{$_} } @pages;
149         return unless @pages;
151         foreach my $page (@pages) {
152                 IkiWiki::check_canedit($page, $q, $session);
153                 check_canremove($page, $q, $session);
154         }
156         # Save current form state to allow returning to it later
157         # without losing any edits.
158         # (But don't save what button was submitted, to avoid
159         # looping back to here.)
160         # Note: "_submit" is CGI::FormBuilder internals.
161         $q->param(-name => "_submit", -value => "");
162         $session->param(postremove => scalar $q->Vars);
163         IkiWiki::cgi_savesession($session);
164         
165         my ($f, $buttons)=confirmation_form($q, $session);
166         $f->title(sprintf(gettext("confirm removal of %s"),
167                 join(", ", map { pagetitle($_) } @pages)));
168         $f->field(name => "page", type => "hidden", value => \@pages, force => 1);
169         if (defined $attachment) {
170                 $f->field(name => "attachment", type => "hidden",
171                         value => $attachment, force => 1);
172         }
174         IkiWiki::showform($f, $buttons, $session, $q);
175         exit 0;
178 sub postremove ($) {
179         my $session=shift;
181         # Load saved form state and return to edit form.
182         my $postremove=CGI->new($session->param("postremove"));
183         $session->clear("postremove");
184         IkiWiki::cgi_savesession($session);
185         IkiWiki::cgi($postremove, $session);
188 sub formbuilder (@) {
189         my %params=@_;
190         my $form=$params{form};
192         if (defined $form->field("do") && ($form->field("do") eq "edit" ||
193             $form->field("do") eq "create")) {
194                 my $q=$params{cgi};
195                 my $session=$params{session};
197                 if ($form->submitted eq "Remove" && $form->field("do") eq "edit") {
198                         removal_confirm($q, $session, 0, $form->field("page"));
199                 }
200                 elsif ($form->submitted eq "Remove Attachments") {
201                         my @selected=map { Encode::decode_utf8($_) } $q->param("attachment_select");
202                         if (! @selected) {
203                                 error(gettext("Please select the attachments to remove."));
204                         }
205                         removal_confirm($q, $session, 1, @selected);
206                 }
207         }
210 sub sessioncgi ($$) {
211         my $q=shift;
213         if ($q->param("do") eq 'remove') {
214                 my $session=shift;
215                 my ($form, $buttons)=confirmation_form($q, $session);
216                 IkiWiki::decode_form_utf8($form);
218                 if ($form->submitted eq 'Cancel') {
219                         postremove($session);
220                 }
221                 elsif ($form->submitted eq 'Remove' && $form->validate) {
222                         IkiWiki::checksessionexpiry($q, $session, $q->param('sid'));
224                         my @pages=$form->field("page");
225                         
226                         # Validate removal by checking that the page exists,
227                         # and that the user is allowed to edit(/remove) it.
228                         my @files;
229                         foreach my $page (@pages) {
230                                 IkiWiki::check_canedit($page, $q, $session);
231                                 check_canremove($page, $q, $session);
232                                 
233                                 # This untaint is safe because of the
234                                 # checks performed above, which verify the
235                                 # page is a normal file, etc.
236                                 push @files, IkiWiki::possibly_foolish_untaint($pagesources{$page});
237                         }
239                         # Do removal, and update the wiki.
240                         require IkiWiki::Render;
241                         if ($config{rcs}) {
242                                 IkiWiki::disable_commit_hook();
243                         }
244                         my $rcs_removed = 1;
246                         foreach my $file (@files) {
247                                 foreach my $srcdir (allowed_dirs()) {
248                                         if (-e "$srcdir/$file") {
249                                                 if ($srcdir eq $config{srcdir} && $config{rcs}) {
250                                                         IkiWiki::rcs_remove($file);
251                                                         $rcs_removed = 1;
252                                                 }
253                                                 else {
254                                                         IkiWiki::prune("$srcdir/$file", $srcdir);
255                                                 }
256                                         }
257                                 }
258                         }
260                         if ($config{rcs}) {
261                                 if ($rcs_removed) {
262                                         IkiWiki::rcs_commit_staged(
263                                                 message => gettext("removed"),
264                                                 session => $session,
265                                         );
266                                 }
267                                 IkiWiki::enable_commit_hook();
268                                 IkiWiki::rcs_update();
269                         }
271                         IkiWiki::refresh();
272                         IkiWiki::saveindex();
274                         if ($q->param("attachment")) {
275                                 # Attachments were deleted, so redirect
276                                 # back to the edit form.
277                                 postremove($session);
278                         }
279                         else {
280                                 # The page is gone, so redirect to parent
281                                 # of the page.
282                                 my $parent=IkiWiki::dirname($pages[0]);
283                                 if (! exists $pagesources{$parent}) {
284                                         $parent="index";
285                                 }
286                                 IkiWiki::redirect($q, urlto($parent));
287                         }
288                 }
289                 else {
290                         removal_confirm($q, $session, 0, $form->field("page"));
291                 }
293                 exit 0;
294         }