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