]> sipb.mit.edu Git - ikiwiki.git/blob - IkiWiki/Plugin/attachment.pm
load attachment javascript into template the clean way
[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 3.00;
7
8 sub import {
9         add_underlay("javascript");
10         add_underlay("attachment");
11         hook(type => "getsetup", id => "attachment", call => \&getsetup);
12         hook(type => "checkconfig", id => "attachment", call => \&checkconfig);
13         hook(type => "formbuilder_setup", id => "attachment", call => \&formbuilder_setup);
14         hook(type => "formbuilder", id => "attachment", call => \&formbuilder, last => 1);
15         IkiWiki::loadplugin("filecheck");
16 }
17
18 sub getsetup () {
19         return
20                 plugin => {
21                         safe => 1,
22                         rebuild => 0,
23                         section => "web",
24                 },
25                 allowed_attachments => {
26                         type => "pagespec",
27                         example => "virusfree() and mimetype(image/*) and maxsize(50kb)",
28                         description => "enhanced PageSpec specifying what attachments are allowed",
29                         link => "ikiwiki/PageSpec/attachment",
30                         safe => 1,
31                         rebuild => 0,
32                 },
33                 virus_checker => {
34                         type => "string",
35                         example => "clamdscan -",
36                         description => "virus checker program (reads STDIN, returns nonzero if virus found)",
37                         safe => 0, # executed
38                         rebuild => 0,
39                 },
40 }
41
42 sub check_canattach ($$;$) {
43         my $session=shift;
44         my $dest=shift; # where it's going to be put, under the srcdir
45         my $file=shift; # the path to the attachment currently
46
47         # Don't allow an attachment to be uploaded with the same name as an
48         # existing page.
49         if (exists $IkiWiki::pagesources{$dest} &&
50             $IkiWiki::pagesources{$dest} ne $dest) {
51                 error(sprintf(gettext("there is already a page named %s"), $dest));
52         }
53
54         # Use a special pagespec to test that the attachment is valid.
55         my $allowed=1;
56         if (defined $config{allowed_attachments} &&
57             length $config{allowed_attachments}) {
58                 $allowed=pagespec_match($dest,
59                         $config{allowed_attachments},
60                         file => $file,
61                         user => $session->param("name"),
62                         ip => $session->remote_addr(),
63                 );
64         }
65
66         if (! $allowed) {
67                 error(gettext("prohibited by allowed_attachments")." ($allowed)");
68         }
69         else {
70                 return 1;
71         }
72 }
73
74 sub checkconfig () {
75         $config{cgi_disable_uploads}=0;
76 }
77
78 sub formbuilder_setup (@) {
79         my %params=@_;
80         my $form=$params{form};
81         my $q=$params{cgi};
82
83         if (defined $form->field("do") && ($form->field("do") eq "edit" ||
84             $form->field("do") eq "create")) {
85                 # Add attachment field, set type to multipart.
86                 $form->enctype(&CGI::MULTIPART);
87                 $form->field(name => 'attachment', type => 'file');
88                 # These buttons are not put in the usual place, so
89                 # are not added to the normal formbuilder button list.
90                 $form->tmpl_param("field-upload" => '<input name="_submit" type="submit" value="Upload Attachment" />');
91                 $form->tmpl_param("field-link" => '<input name="_submit" type="submit" value="Insert Links" />');
92
93                 # Add all the javascript used by the attachments interface.
94                 require IkiWiki::Plugin::toggle;
95                 my $js=IkiWiki::Plugin::toggle::include_javascript($params{page});
96                 $js.='<link rel="stylesheet" href="'.urlto("ikiwiki/jquery-ui.css", $params{page}).' id="theme">\n';
97                 my @jsfiles=qw{jquery.min jquery-ui.min
98                         jquery.tmpl.min jquery.iframe-transport
99                         jquery.fileupload jquery.fileupload-ui
100                 };
101                 foreach my $file (@jsfiles) {
102                         $js.='<script src="'.urlto("ikiwiki/$file.js", $params{page}).
103                              '" type="text/javascript" charset="utf-8"></script>'."\n";
104                 }
105                 $form->tmpl_param("javascript" => $js);
106
107                 # Start with the attachments interface toggled invisible,
108                 # but if it was used, keep it open.
109                 if ($form->submitted ne "Upload Attachment" &&
110                     (! defined $q->param("attachment_select") ||
111                     ! length $q->param("attachment_select"))) {
112                         $form->tmpl_param("attachments-class" => "toggleable");
113                 }
114                 else {
115                         $form->tmpl_param("attachments-class" => "toggleable-open");
116                 }
117                 
118                 # Save attachments in holding area before previewing so
119                 # they can be seen in the preview.
120                 if ($form->submitted eq "Preview") {
121                         attachments_save($form, $params{session});
122                 }
123         }
124 }
125
126 sub formbuilder (@) {
127         my %params=@_;
128         my $form=$params{form};
129         my $q=$params{cgi};
130
131         return if ! defined $form->field("do") || ($form->field("do") ne "edit" && $form->field("do") ne "create") ;
132
133         my $filename=Encode::decode_utf8($q->param('attachment'));
134         if (defined $filename && length $filename) {
135                 attachment_store($filename, $form, $q, $params{session});
136         }
137
138         if ($form->submitted eq "Save Page") {
139                 attachments_save($form, $params{session});
140         }
141
142         if ($form->submitted eq "Insert Links") {
143                 my $page=quotemeta(Encode::decode_utf8($q->param("page")));
144                 my $add="";
145                 foreach my $f ($q->param("attachment_select")) {
146                         $f=Encode::decode_utf8($f);
147                         $f=~s/^$page\///;
148                         if (IkiWiki::isinlinableimage($f) &&
149                             UNIVERSAL::can("IkiWiki::Plugin::img", "import")) {
150                                 $add.='[[!img '.$f.' align="right" size="" alt=""]]';
151                         }
152                         else {
153                                 $add.="[[$f]]";
154                         }
155                         $add.="\n";
156                 }
157                 $form->field(name => 'editcontent',
158                         value => $form->field('editcontent')."\n\n".$add,
159                         force => 1) if length $add;
160         }
161         
162         # Generate the attachment list only after having added any new
163         # attachments.
164         $form->tmpl_param("attachment_list" => [attachment_list($form->field('page'))]);
165 }
166
167 sub attachment_holding_location {
168         my $page=attachment_location(shift);
169
170         my $dir=$config{wikistatedir}."/attachments/".
171                 IkiWiki::possibly_foolish_untaint(linkpage($page));
172         $dir=~s/\/$//;
173         return $dir;
174 }
175
176 sub is_held_attachment {
177         my $attachment=shift;
178
179         my $f=attachment_holding_location($attachment);
180         if (-f $f) {
181                 return $f
182         }
183         else {
184                 return undef;
185         }
186 }
187
188 # Stores the attachment in a holding area, not yet in the wiki proper.
189 sub attachment_store {
190         my $filename=shift;
191         my $form=shift;
192         my $q=shift;
193         my $session=shift;
194         
195         # This is an (apparently undocumented) way to get the name
196         # of the temp file that CGI writes the upload to.
197         my $tempfile=$q->tmpFileName($filename);
198         if (! defined $tempfile || ! length $tempfile) {
199                 # perl 5.8 needs an alternative, awful method
200                 if ($q =~ /HASH/ && exists $q->{'.tmpfiles'}) {
201                         foreach my $key (keys(%{$q->{'.tmpfiles'}})) {
202                                 $tempfile=$q->tmpFileName(\$key);
203                                 last if defined $tempfile && length $tempfile;
204                         }
205                 }
206                 if (! defined $tempfile || ! length $tempfile) {
207                         error("CGI::tmpFileName failed to return the uploaded file name");
208                 }
209         }
210
211         $filename=IkiWiki::basename($filename);
212         $filename=~s/.*\\+(.+)/$1/; # hello, windows
213         $filename=IkiWiki::possibly_foolish_untaint(linkpage($filename));
214         
215         # Check that the user is allowed to edit the attachment.
216         my $final_filename=
217                 linkpage(IkiWiki::possibly_foolish_untaint(
218                         attachment_location($form->field('page')))).
219                 $filename;
220         if (IkiWiki::file_pruned($final_filename)) {
221                 error(gettext("bad attachment filename"));
222         }
223         IkiWiki::check_canedit($final_filename, $q, $session);
224         # And that the attachment itself is acceptable.
225         check_canattach($session, $final_filename, $tempfile);
226
227         # Move the attachment into holding directory.
228         # Try to use a fast rename; fall back to copying.
229         my $dest=attachment_holding_location($form->field('page'));
230         IkiWiki::prep_writefile($filename, $dest);
231         unlink($dest."/".$filename);
232         if (rename($tempfile, $dest."/".$filename)) {
233                 # The temp file has tight permissions; loosen up.
234                 chmod(0666 & ~umask, $dest."/".$filename);
235         }
236         else {
237                 my $fh=$q->upload('attachment');
238                 if (! defined $fh || ! ref $fh) {
239                         # needed by old CGI versions
240                         $fh=$q->param('attachment');
241                         if (! defined $fh || ! ref $fh) {
242                                 # even that doesn't always work,
243                                 # fall back to opening the tempfile
244                                 $fh=undef;
245                                 open($fh, "<", $tempfile) || error("failed to open \"$tempfile\": $!");
246                         }
247                 }
248                 binmode($fh);
249                 require IkiWiki::Render; 
250                 writefile($filename, $dest, undef, 1, sub {
251                         IkiWiki::fast_file_copy($tempfile, $filename, $fh, @_);
252                 });
253         }
254         
255         # Return JSON response for the jquery file upload widget.
256         eval q{use JSON};
257         error $@ if $@;
258         print "Content-type: application/json\n\n";
259         my $size=-s $dest."/".$filename;
260         print to_json([
261                 {
262                         name => $filename,
263                         size => $size,
264                         humansize => IkiWiki::Plugin::filecheck::humansize($size),
265                         stored_msg => stored_msg(),
266                         
267                 }
268         ]);
269         exit 0;
270 }
271
272 # Save all stored attachments for a page.
273 sub attachments_save {
274         my $form=shift;
275         my $session=shift;
276
277         # Move attachments out of holding directory.
278         my @attachments;
279         my $dir=attachment_holding_location($form->field('page'));
280         foreach my $filename (glob("$dir/*")) {
281                 next unless -f $filename;
282                 my $dest=$config{srcdir}."/".
283                         linkpage(IkiWiki::possibly_foolish_untaint(
284                                 attachment_location($form->field('page')))).
285                         IkiWiki::basename($filename);
286                 unlink($dest);
287                 rename($filename, $dest);
288                 push @attachments, $dest;
289         }
290         return unless @attachments;
291         require IkiWiki::Render;
292         IkiWiki::prune($dir);
293
294         # Check the attachments in and trigger a wiki refresh.
295         if ($config{rcs}) {
296                 IkiWiki::rcs_add($_) foreach @attachments;
297                 IkiWiki::disable_commit_hook();
298                 IkiWiki::rcs_commit_staged(
299                         message => gettext("attachment upload"),
300                         session => $session,
301                 );
302                 IkiWiki::enable_commit_hook();
303                 IkiWiki::rcs_update();
304         }
305         IkiWiki::refresh();
306         IkiWiki::saveindex();
307 }
308
309 sub attachment_location ($) {
310         my $page=shift;
311         
312         # Put the attachment in a subdir of the page it's attached
313         # to, unless that page is an "index" page.
314         $page=~s/(^|\/)index//;
315         $page.="/" if length $page;
316         
317         return $page;
318 }
319
320 sub attachment_list ($) {
321         my $page=shift;
322         my $loc=attachment_location($page);
323
324         my $std=sub {
325                 my $file=shift;
326                 my $mtime=shift;
327                 my $date=shift;
328                 my $size=shift;
329
330                 name => $file,
331                 size => IkiWiki::Plugin::filecheck::humansize($size),
332                 mtime => $date,
333                 mtime_raw => $mtime,
334         };
335
336         # attachments already in the wiki
337         my %attachments;
338         foreach my $f (values %pagesources) {
339                 if (! defined pagetype($f) &&
340                     $f=~m/^\Q$loc\E[^\/]+$/) {
341                         $attachments{$f}={
342                                 $std->($f, $IkiWiki::pagemtime{$f}, displaytime($IkiWiki::pagemtime{$f}), (stat($f))[7]),
343                                 link => htmllink($page, $page, $f, noimageinline => 1),
344                         };
345                 }
346         }
347         
348         # attachments in holding directory
349         my $dir=attachment_holding_location($page);
350         my $heldmsg=gettext("this attachment is not yet saved");
351         foreach my $file (glob("$dir/*")) {
352                 next unless -f $file;
353                 my $base=IkiWiki::basename($file);
354                 my $f=$loc.$base;
355                 $attachments{$f}={
356                         $std->($f, (stat($file))[9], stored_msg(), (stat(_))[7]),
357                         link => $base,
358                 }
359         }
360
361         # Sort newer attachments to the end of the list.
362         return sort { $a->{mtime_raw} <=> $b->{mtime_raw} || $a->{link} cmp $b->{link} }
363                 values %attachments;
364 }
365
366 sub stored_msg {
367         gettext("just uploaded");
368 }
369
370 1