X-Git-Url: http://git.vanrenterghem.biz/git.ikiwiki.info.git/blobdiff_plain/14cb2b0869496105915892b509016b9e46991ee7..47d179802d5f78365fa7077ae64de35f2892a1e4:/IkiWiki/Plugin/attachment.pm?ds=inline diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index 48cd954d9..01d0d2478 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -18,9 +18,31 @@ sub checkconfig () { #{{{ sub formbuilder_setup (@) { #{{{ my %params=@_; my $form=$params{form}; + my $q=$params{cgi}; - if ($form->field("do") eq "edit") { + if (defined $form->field("do") && $form->field("do") eq "edit") { + # Add attachment field, set type to multipart. + $form->enctype(&CGI::MULTIPART); $form->field(name => 'attachment', type => 'file'); + # These buttons are not put in the usual place, so + # are not added to the normal formbuilder button list. + $form->tmpl_param("field-upload" => ''); + $form->tmpl_param("field-link" => ''); + + # Add the javascript from the toggle plugin; + # the attachments interface uses it to toggle visibility. + require IkiWiki::Plugin::toggle; + $form->tmpl_param("javascript" => $IkiWiki::Plugin::toggle::javascript); + # Start with the attachments interface toggled invisible, + # but if it was used, keep it open. + if ($form->submitted ne "Upload Attachment" && + (! defined $q->param("attachment_select") || + ! length $q->param("attachment_select"))) { + $form->tmpl_param("attachments-class" => "toggleable"); + } + else { + $form->tmpl_param("attachments-class" => "toggleable-open"); + } } elsif ($form->title eq "preferences") { my $session=$params{session}; @@ -28,7 +50,13 @@ sub formbuilder_setup (@) { #{{{ $form->field(name => "allowed_attachments", size => 50, fieldset => "admin", - comment => "(".htmllink("", "", "ikiwiki/PageSpec", noimageinline => 1).")"); + comment => "(". + htmllink("", "", + "ikiwiki/PageSpec/attachment", + noimageinline => 1, + linktext => "Enhanced PageSpec", + ).")" + ); if (! IkiWiki::is_admin($user_name)) { $form->field(name => "allowed_attachments", type => "hidden"); } @@ -49,32 +77,35 @@ sub formbuilder_setup (@) { #{{{ sub formbuilder (@) { #{{{ my %params=@_; my $form=$params{form}; + my $q=$params{cgi}; - return if $form->field("do") ne "edit"; + return if ! defined $form->field("do") || $form->field("do") ne "edit"; - if ($form->submitted eq "Upload" || $form->submitted eq "Save Page") { - my $q=$params{cgi}; + my $filename=$q->param('attachment'); + if (defined $filename && length $filename && + ($form->submitted eq "Upload Attachment" || $form->submitted eq "Save Page")) { my $session=$params{session}; - - my $filename=$q->param('attachment'); - if (! defined $filename || ! length $filename) { - # no file, so do nothing - return; - } # This is an (apparently undocumented) way to get the name # of the temp file that CGI writes the upload to. my $tempfile=$q->tmpFileName($filename); - - # Put the attachment in a subdir of the page it's attached - # to, unless that page is an "index" page. - my $page=$form->field('page'); - $page=~s/(^|\/)index//; - $filename=(length $page ? $page."/" : "").IkiWiki::basename($filename); - - # To untaint the filename, escape any hazardous characters, - # and make sure it isn't pruned. - $filename=IkiWiki::titlepage(IkiWiki::possibly_foolish_untaint($filename)); + if (! defined $tempfile || ! length $tempfile) { + # perl 5.8 needs an alternative, awful method + if ($q =~ /HASH/ && exists $q->{'.tmpfiles'}) { + foreach my $key (keys(%{$q->{'.tmpfiles'}})) { + $tempfile=$q->tmpFileName(\$key); + last if defined $tempfile && length $tempfile; + } + } + if (! defined $tempfile || ! length $tempfile) { + error("CGI::tmpFileName failed to return the uploaded file name"); + } + } + + $filename=IkiWiki::titlepage( + IkiWiki::possibly_foolish_untaint( + attachment_location($form->field('page')). + IkiWiki::basename($filename))); if (IkiWiki::file_pruned($filename, $config{srcdir})) { error(gettext("bad attachment filename")); } @@ -91,7 +122,10 @@ sub formbuilder (@) { #{{{ length $allowed_attachments) { $allowed=pagespec_match($filename, $allowed_attachments, - file => $tempfile); + file => $tempfile, + user => $session->param("name"), + ip => $ENV{REMOTE_ADDR}, + ); last if $allowed; } } @@ -106,10 +140,21 @@ sub formbuilder (@) { #{{{ # Try to use a fast rename; fall back to copying. IkiWiki::prep_writefile($filename, $config{srcdir}); unlink($config{srcdir}."/".$filename); - if (! rename($tempfile, $config{srcdir}."/".$filename)) { + if (rename($tempfile, $config{srcdir}."/".$filename)) { + # The temp file has tight permissions; loosen up. + chmod(0666 & ~umask, $config{srcdir}."/".$filename); + } + else { my $fh=$q->upload('attachment'); if (! defined $fh || ! ref $fh) { - error("failed to get filehandle"); + # needed by old CGI versions + $fh=$q->param('attachment'); + if (! defined $fh || ! ref $fh) { + # even that doesn't always work, + # fall back to opening the tempfile + $fh=undef; + open($fh, "<", $tempfile) || error("failed to open \"$tempfile\": $!"); + } } binmode($fh); writefile($filename, $config{srcdir}, undef, 1, sub { @@ -130,34 +175,120 @@ sub formbuilder (@) { #{{{ IkiWiki::refresh(); IkiWiki::saveindex(); } + elsif ($form->submitted eq "Insert Links") { + my $add=""; + foreach my $f ($q->param("attachment_select")) { + $add.="[[$f]]\n"; + } + $form->field(name => 'editcontent', + value => $form->field('editcontent')."\n\n".$add, + force => 1) if length $add; + } + + # Generate the attachment list only after having added any new + # attachments. + $form->tmpl_param("attachment_list" => [attachment_list($form->field('page'))]); } # }}} -package IkiWiki::PageSpec; +sub attachment_location ($) { + my $page=shift; + + # Put the attachment in a subdir of the page it's attached + # to, unless that page is an "index" page. + $page=~s/(^|\/)index//; + $page.="/" if length $page; + + return $page; +} + +sub attachment_list ($) { + my $page=shift; + my $loc=attachment_location($page); + + my @ret; + foreach my $f (values %pagesources) { + if (! defined IkiWiki::pagetype($f) && + $f=~m/^\Q$loc\E[^\/]+$/ && + -e "$config{srcdir}/$f") { + push @ret, { + "field-select" => '', + link => htmllink($page, $page, $f, noimageinline => 1), + size => humansize((stat(_))[7]), + mtime => displaytime($IkiWiki::pagemtime{$f}), + mtime_raw => $IkiWiki::pagemtime{$f}, + }; + } + } + + # Sort newer attachments to the top of the list, so a newly-added + # attachment appears just before the form used to add it. + return sort { $b->{mtime_raw} <=> $a->{mtime_raw} || $a->{link} cmp $b->{link} } @ret; +} + +my %units=( # size in bytes + B => 1, + byte => 1, + KB => 2 ** 10, + kilobyte => 2 ** 10, + K => 2 ** 10, + KB => 2 ** 10, + kilobyte => 2 ** 10, + M => 2 ** 20, + MB => 2 ** 20, + megabyte => 2 ** 20, + G => 2 ** 30, + GB => 2 ** 30, + gigabyte => 2 ** 30, + T => 2 ** 40, + TB => 2 ** 40, + terabyte => 2 ** 40, + P => 2 ** 50, + PB => 2 ** 50, + petabyte => 2 ** 50, + E => 2 ** 60, + EB => 2 ** 60, + exabyte => 2 ** 60, + Z => 2 ** 70, + ZB => 2 ** 70, + zettabyte => 2 ** 70, + Y => 2 ** 80, + YB => 2 ** 80, + yottabyte => 2 ** 80, + # ikiwiki, if you find you need larger data quantities, either modify + # yourself to add them, or travel back in time to 2008 and kill me. + # -- Joey +); sub parsesize ($) { #{{{ my $size=shift; + no warnings; my $base=$size+0; # force to number use warnings; - my $multiple=1; - if ($size=~/kb?$/i) { - $multiple=2**10; - } - elsif ($size=~/mb?$/i) { - $multiple=2**20; - } - elsif ($size=~/gb?$/i) { - $multiple=2**30; + foreach my $unit (sort keys %units) { + if ($size=~/[0-9\s]\Q$unit\E$/i) { + return $base * $units{$unit}; + } } - elsif ($size=~/tb?$/i) { - $multiple=2**40; + return $base; +} #}}} + +sub humansize ($) { #{{{ + my $size=shift; + + foreach my $unit (reverse sort { $units{$a} <=> $units{$b} || $b cmp $a } keys %units) { + if ($size / $units{$unit} > 0.25) { + return (int($size / $units{$unit} * 10)/10).$unit; + } } - return $base * $multiple; + return $size; # near zero, or negative } #}}} +package IkiWiki::PageSpec; + sub match_maxsize ($$;@) { #{{{ shift; - my $maxsize=eval{parsesize(shift)}; + my $maxsize=eval{IkiWiki::Plugin::attachment::parsesize(shift)}; if ($@) { return IkiWiki::FailReason->new("unable to parse maxsize (or number too large)"); } @@ -168,7 +299,7 @@ sub match_maxsize ($$;@) { #{{{ } if (-s $params{file} > $maxsize) { - return IkiWiki::FailReason->new("file too large"); + return IkiWiki::FailReason->new("file too large (".(-s $params{file})." > $maxsize)"); } else { return IkiWiki::SuccessReason->new("file not too large"); @@ -177,7 +308,7 @@ sub match_maxsize ($$;@) { #{{{ sub match_minsize ($$;@) { #{{{ shift; - my $minsize=eval{parsesize(shift)}; + my $minsize=eval{IkiWiki::Plugin::attachment::parsesize(shift)}; if ($@) { return IkiWiki::FailReason->new("unable to parse minsize (or number too large)"); } @@ -195,6 +326,76 @@ sub match_minsize ($$;@) { #{{{ } } #}}} +sub match_mimetype ($$;@) { #{{{ + shift; + my $wanted=shift; + + my %params=@_; + if (! exists $params{file}) { + return IkiWiki::FailReason->new("no file specified"); + } + + # Use ::magic to get the mime type, the idea is to only trust + # data obtained by examining the actual file contents. + eval q{use File::MimeInfo::Magic}; + if ($@) { + return IkiWiki::FailReason->new("failed to load File::MimeInfo::Magic ($@); cannot check MIME type"); + } + my $mimetype=File::MimeInfo::Magic::magic($params{file}); + if (! defined $mimetype) { + $mimetype="unknown"; + } + + my $regexp=IkiWiki::glob2re($wanted); + if ($mimetype!~/^$regexp$/i) { + return IkiWiki::FailReason->new("file MIME type is $mimetype, not $wanted"); + } + else { + return IkiWiki::SuccessReason->new("file MIME type is $mimetype"); + } +} #}}} + +sub match_virusfree ($$;@) { #{{{ + shift; + my $wanted=shift; + + my %params=@_; + if (! exists $params{file}) { + return IkiWiki::FailReason->new("no file specified"); + } + + if (! exists $IkiWiki::config{virus_checker} || + ! length $IkiWiki::config{virus_checker}) { + return IkiWiki::FailReason->new("no virus_checker configured"); + } + + # The file needs to be fed into the virus checker on stdin, + # because the file is not world-readable, and if clamdscan is + # used, clamd would fail to read it. + eval q{use IPC::Open2}; + error($@) if $@; + open (IN, "<", $params{file}) || return IkiWiki::FailReason->new("failed to read file"); + binmode(IN); + my $sigpipe=0; + $SIG{PIPE} = sub { $sigpipe=1 }; + my $pid=open2(\*CHECKER_OUT, "<&IN", $IkiWiki::config{virus_checker}); + my $reason=; + chomp $reason; + 1 while (); + close(CHECKER_OUT); + waitpid $pid, 0; + $SIG{PIPE}="DEFAULT"; + if ($sigpipe || $?) { + if (! length $reason) { + $reason="virus checker $IkiWiki::config{virus_checker}; failed with no output"; + } + return IkiWiki::FailReason->new("file seems to contain a virus ($reason)"); + } + else { + return IkiWiki::SuccessReason->new("file seems virusfree ($reason)"); + } +} #}}} + sub match_ispage ($$;@) { #{{{ my $filename=shift; @@ -206,4 +407,38 @@ sub match_ispage ($$;@) { #{{{ } } #}}} +sub match_user ($$;@) { #{{{ + shift; + my $user=shift; + my %params=@_; + + if (! exists $params{user}) { + return IkiWiki::FailReason->new("no user specified"); + } + + if (defined $params{user} && lc $params{user} eq lc $user) { + return IkiWiki::SuccessReason->new("user is $user"); + } + else { + return IkiWiki::FailReason->new("user is $params{user}, not $user"); + } +} #}}} + +sub match_ip ($$;@) { #{{{ + shift; + my $ip=shift; + my %params=@_; + + if (! exists $params{ip}) { + return IkiWiki::FailReason->new("no IP specified"); + } + + if (defined $params{ip} && lc $params{ip} eq lc $ip) { + return IkiWiki::SuccessReason->new("IP is $ip"); + } + else { + return IkiWiki::FailReason->new("IP is $params{ip}, not $ip"); + } +} #}}} + 1