add user and ip support to attachment pagespecs
[ikiwiki] / IkiWiki / Plugin / attachment.pm
1 #!/usr/bin/perl
2 package IkiWiki::Plugin::attachment;
3
4 use warnings;
5 use strict;
6 use IkiWiki 2.00;
7
8 sub import { #{{{
9         hook(type => "checkconfig", id => "attachment", call => \&checkconfig);
10         hook(type => "formbuilder_setup", id => "attachment", call => \&formbuilder_setup);
11         hook(type => "formbuilder", id => "attachment", call => \&formbuilder);
12 } # }}}
13
14 sub checkconfig () { #{{{
15         $config{cgi_disable_uploads}=0;
16 } #}}}
17
18 sub formbuilder_setup (@) { #{{{
19         my %params=@_;
20         my $form=$params{form};
21         my $q=$params{cgi};
22
23         if ($form->field("do") eq "edit") {
24                 $form->field(name => 'attachment', type => 'file');
25                 # These buttons are not put in the usual place, so
26                 # are not added to the normal formbuilder button list.
27                 $form->tmpl_param("field-upload" => '<input name="_submit" type="submit" value="Upload Attachment" />');
28                 $form->tmpl_param("field-link" => '<input name="_submit" type="submit" value="Insert Links" />');
29
30                 # Add the javascript from the toggle plugin;
31                 # the attachments interface uses it to toggle visibility.
32                 require IkiWiki::Plugin::toggle;
33                 $form->tmpl_param("javascript" => $IkiWiki::Plugin::toggle::javascript);
34                 # Start with the attachments interface toggled invisible,
35                 # but if it was used, keep it open.
36                 if ($form->submitted ne "Upload Attachment" &&
37                     ! length $q->param("attachment_select")) {
38                         $form->tmpl_param("attachments-class" => "toggleable");
39                 }
40                 else {
41                         $form->tmpl_param("attachments-class" => "toggleable-open");
42                 }
43         }
44         elsif ($form->title eq "preferences") {
45                 my $session=$params{session};
46                 my $user_name=$session->param("name");
47
48                 $form->field(name => "allowed_attachments", size => 50,
49                         fieldset => "admin",
50                         comment => "(".htmllink("", "", "ikiwiki/PageSpec", noimageinline => 1).")");
51                 if (! IkiWiki::is_admin($user_name)) {
52                         $form->field(name => "allowed_attachments", type => "hidden");
53                 }
54                 if (! $form->submitted) {
55                         $form->field(name => "allowed_attachments", force => 1,
56                                 value => IkiWiki::userinfo_get($user_name, "allowed_attachments"));
57                 }
58                 if ($form->submitted && $form->submitted eq 'Save Preferences') {
59                         if (defined $form->field("allowed_attachments")) {
60                                 IkiWiki::userinfo_set($user_name, "allowed_attachments",
61                                 $form->field("allowed_attachments")) ||
62                                         error("failed to set allowed_attachments");
63                         }
64                 }
65         }
66 } #}}}
67
68 sub formbuilder (@) { #{{{
69         my %params=@_;
70         my $form=$params{form};
71         my $q=$params{cgi};
72
73         return if $form->field("do") ne "edit";
74
75         my $filename=$q->param('attachment');
76         if (defined $filename && length $filename &&
77             ($form->submitted eq "Upload Attachment" || $form->submitted eq "Save Page")) {
78                 my $session=$params{session};
79                 
80                 # This is an (apparently undocumented) way to get the name
81                 # of the temp file that CGI writes the upload to.
82                 my $tempfile=$q->tmpFileName($filename);
83                 
84                 $filename=IkiWiki::titlepage(
85                         IkiWiki::possibly_foolish_untaint(
86                                 attachment_location($form->field('page')).
87                                 IkiWiki::basename($filename)));
88                 if (IkiWiki::file_pruned($filename, $config{srcdir})) {
89                         error(gettext("bad attachment filename"));
90                 }
91                 
92                 # Check that the user is allowed to edit a page with the
93                 # name of the attachment.
94                 IkiWiki::check_canedit($filename, $q, $session, 1);
95                 
96                 # Use a special pagespec to test that the attachment is valid.
97                 my $allowed=1;
98                 foreach my $admin (@{$config{adminuser}}) {
99                         my $allowed_attachments=IkiWiki::userinfo_get($admin, "allowed_attachments");
100                         if (defined $allowed_attachments &&
101                             length $allowed_attachments) {
102                                 $allowed=pagespec_match($filename,
103                                         $allowed_attachments,
104                                         file => $tempfile,
105                                         user => $session->param("name"),
106                                         ip => $ENV{REMOTE_ADDR},
107                                 );
108                                 last if $allowed;
109                         }
110                 }
111                 if (! $allowed) {
112                         error(gettext("attachment rejected")." ($allowed)");
113                 }
114
115                 # Needed for fast_file_copy and for rendering below.
116                 require IkiWiki::Render;
117
118                 # Move the attachment into place.
119                 # Try to use a fast rename; fall back to copying.
120                 IkiWiki::prep_writefile($filename, $config{srcdir});
121                 unlink($config{srcdir}."/".$filename);
122                 if (rename($tempfile, $config{srcdir}."/".$filename)) {
123                         # The temp file has tight permissions; loosen up.
124                         chmod(0666 & ~umask, $config{srcdir}."/".$filename);
125                 }
126                 else {
127                         my $fh=$q->upload('attachment');
128                         if (! defined $fh || ! ref $fh) {
129                                 error("failed to get filehandle");
130                         }
131                         binmode($fh);
132                         writefile($filename, $config{srcdir}, undef, 1, sub {
133                                 IkiWiki::fast_file_copy($tempfile, $filename, $fh, @_);
134                         });
135                 }
136
137                 # Check the attachment in and trigger a wiki refresh.
138                 if ($config{rcs}) {
139                         IkiWiki::rcs_add($filename);
140                         IkiWiki::disable_commit_hook();
141                         IkiWiki::rcs_commit($filename, gettext("attachment upload"),
142                                 IkiWiki::rcs_prepedit($filename),
143                                 $session->param("name"), $ENV{REMOTE_ADDR});
144                         IkiWiki::enable_commit_hook();
145                         IkiWiki::rcs_update();
146                 }
147                 IkiWiki::refresh();
148                 IkiWiki::saveindex();
149         }
150         elsif ($form->submitted eq "Insert Links") {
151                 my $add="";
152                 foreach my $f ($q->param("attachment_select")) {
153                         $add.="[[$f]]\n";
154                 }
155                 $form->field(name => 'editcontent',
156                         value => $form->field('editcontent')."\n\n".$add,
157                         force => 1) if length $add;
158         }
159         
160         # Generate the attachment list only after having added any new
161         # attachments.
162         $form->tmpl_param("attachment_list" => [attachment_list($form->field('page'))]);
163 } # }}}
164
165 sub attachment_location ($) {
166         my $page=shift;
167         
168         # Put the attachment in a subdir of the page it's attached
169         # to, unless that page is an "index" page.
170         $page=~s/(^|\/)index//;
171         $page.="/" if length $page;
172         
173         return $page;
174 }
175
176 sub attachment_list ($) {
177         my $page=shift;
178         my $loc=attachment_location($page);
179
180         my @ret;
181         foreach my $f (values %pagesources) {
182                 if (! defined IkiWiki::pagetype($f) &&
183                     $f=~m/^\Q$loc\E[^\/]+$/ &&
184                     -e "$config{srcdir}/$f") {
185                         push @ret, {
186                                 "field-select" => '<input type="checkbox" name="attachment_select" value="'.$f.'" />',
187                                 link => htmllink($page, $page, $f, noimageinline => 1),
188                                 size => humansize((stat(_))[7]),
189                                 mtime => displaytime($IkiWiki::pagemtime{$f}),
190                                 mtime_raw => $IkiWiki::pagemtime{$f},
191                         };
192                 }
193         }
194
195         # Sort newer attachments to the top of the list, so a newly-added
196         # attachment appears just before the form used to add it.
197         return sort { $b->{mtime_raw} <=> $a->{mtime_raw} || $a->{link} cmp $b->{link} } @ret;
198 }
199
200 my %units=(             # size in bytes
201         B               => 1,
202         byte            => 1,
203         KB              => 2 ** 10,
204         kilobyte        => 2 ** 10,
205         K               => 2 ** 10,
206         KB              => 2 ** 10,
207         kilobyte        => 2 ** 10,
208         M               => 2 ** 20,
209         MB              => 2 ** 20,
210         megabyte        => 2 ** 20,
211         G               => 2 ** 30,
212         GB              => 2 ** 30,
213         gigabyte        => 2 ** 30,
214         T               => 2 ** 40,
215         TB              => 2 ** 40,
216         terabyte        => 2 ** 40,
217         P               => 2 ** 50,
218         PB              => 2 ** 50,
219         petabyte        => 2 ** 50,
220         E               => 2 ** 60,
221         EB              => 2 ** 60,
222         exabyte         => 2 ** 60,
223         Z               => 2 ** 70,
224         ZB              => 2 ** 70,
225         zettabyte       => 2 ** 70,
226         Y               => 2 ** 80,
227         YB              => 2 ** 80,
228         yottabyte       => 2 ** 80,
229         # ikiwiki, if you find you need larger data quantities, either modify
230         # yourself to add them, or travel back in time to 2008 and kill me.
231         #   -- Joey
232 );
233
234 sub parsesize ($) { #{{{
235         my $size=shift;
236
237         no warnings;
238         my $base=$size+0; # force to number
239         use warnings;
240         foreach my $unit (sort keys %units) {
241                 if ($size=~/[0-9\s]\Q$unit\E$/i) {
242                         return $base * $units{$unit};
243                 }
244         }
245         return $base;
246 } #}}}
247
248 sub humansize ($) { #{{{
249         my $size=shift;
250
251         foreach my $unit (reverse sort { $units{$a} <=> $units{$b} || $b cmp $a } keys %units) {
252                 if ($size / $units{$unit} > 0.25) {
253                         return (int($size / $units{$unit} * 10)/10).$unit;
254                 }
255         }
256         return $size; # near zero, or negative
257 } #}}}
258
259 package IkiWiki::PageSpec;
260
261 sub match_maxsize ($$;@) { #{{{
262         shift;
263         my $maxsize=eval{IkiWiki::Plugin::attachment::parsesize(shift)};
264         if ($@) {
265                 return IkiWiki::FailReason->new("unable to parse maxsize (or number too large)");
266         }
267
268         my %params=@_;
269         if (! exists $params{file}) {
270                 return IkiWiki::FailReason->new("no file specified");
271         }
272
273         if (-s $params{file} > $maxsize) {
274                 return IkiWiki::FailReason->new("file too large (".(-s $params{file})." >  $maxsize)");
275         }
276         else {
277                 return IkiWiki::SuccessReason->new("file not too large");
278         }
279 } #}}}
280
281 sub match_minsize ($$;@) { #{{{
282         shift;
283         my $minsize=eval{IkiWiki::Plugin::attachment::parsesize(shift)};
284         if ($@) {
285                 return IkiWiki::FailReason->new("unable to parse minsize (or number too large)");
286         }
287
288         my %params=@_;
289         if (! exists $params{file}) {
290                 return IkiWiki::FailReason->new("no file specified");
291         }
292
293         if (-s $params{file} < $minsize) {
294                 return IkiWiki::FailReason->new("file too small");
295         }
296         else {
297                 return IkiWiki::SuccessReason->new("file not too small");
298         }
299 } #}}}
300
301 sub match_ispage ($$;@) { #{{{
302         my $filename=shift;
303
304         if (defined IkiWiki::pagetype($filename)) {
305                 return IkiWiki::SuccessReason->new("file is a wiki page");
306         }
307         else {
308                 return IkiWiki::FailReason->new("file is not a wiki page");
309         }
310 } #}}}
311
312 sub match_user ($$;@) { #{{{
313         shift;
314         my $user=shift;
315         my %params=@_;
316         
317         if (! exists $params{user}) {
318                 return IkiWiki::FailReason->new("no user specified");
319         }
320
321         if (defined $params{user} && lc $params{user} eq lc $user) {
322                 return IkiWiki::SuccessReason->new("user is $user");
323         }
324         else {
325                 return IkiWiki::FailReason->new("user is $params{user}, not $user");
326         }
327 } #}}}
328
329 sub match_ip ($$;@) { #{{{
330         shift;
331         my $ip=shift;
332         my %params=@_;
333         
334         if (! exists $params{ip}) {
335                 return IkiWiki::FailReason->new("no IP specified");
336         }
337
338         if (defined $params{ip} && lc $params{ip} eq lc $ip) {
339                 return IkiWiki::SuccessReason->new("IP is $ip");
340         }
341         else {
342                 return IkiWiki::FailReason->new("IP is $params{ip}, not $ip");
343         }
344 } #}}}
345
346 1