Merge branch 'tova'

master
Joey Hess 2008-07-02 16:38:13 -04:00
commit c987aee47a
17 changed files with 525 additions and 46 deletions

View File

@ -88,6 +88,7 @@ sub defaultconfig () { #{{{
account_creation_password => "",
prefix_directives => 0,
hardlink => 0,
cgi_disable_uploads => 1,
} #}}}
sub checkconfig () { #{{{

View File

@ -281,7 +281,6 @@ sub cgi_editpage ($$) { #{{{
eval q{use CGI::FormBuilder};
error($@) if $@;
my $form = CGI::FormBuilder->new(
title => "editpage",
fields => \@fields,
charset => "utf-8",
method => 'POST',
@ -304,7 +303,7 @@ sub cgi_editpage ($$) { #{{{
# This untaint is safe because titlepage removes any problematic
# characters.
my ($page)=$form->field('page');
my $page=$form->field('page');
$page=titlepage(possibly_foolish_untaint($page));
if (! defined $page || ! length $page ||
file_pruned($page, $config{srcdir}) || $page=~/^\//) {
@ -667,10 +666,11 @@ sub cgi (;$$) { #{{{
my $q=shift;
my $session=shift;
eval q{use CGI};
error($@) if $@;
$CGI::DISABLE_UPLOADS=$config{cgi_disable_uploads};
if (! $q) {
eval q{use CGI};
error($@) if $@;
binmode(STDIN);
$q=CGI->new;
binmode(STDIN, ":utf8");

View File

@ -0,0 +1,346 @@
#!/usr/bin/perl
package IkiWiki::Plugin::attachment;
use warnings;
use strict;
use IkiWiki 2.00;
sub import { #{{{
hook(type => "checkconfig", id => "attachment", call => \&checkconfig);
hook(type => "formbuilder_setup", id => "attachment", call => \&formbuilder_setup);
hook(type => "formbuilder", id => "attachment", call => \&formbuilder);
} # }}}
sub checkconfig () { #{{{
$config{cgi_disable_uploads}=0;
} #}}}
sub formbuilder_setup (@) { #{{{
my %params=@_;
my $form=$params{form};
my $q=$params{cgi};
if ($form->field("do") eq "edit") {
$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" => '<input name="_submit" type="submit" value="Upload Attachment" />');
$form->tmpl_param("field-link" => '<input name="_submit" type="submit" value="Insert Links" />');
# 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" &&
! 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};
my $user_name=$session->param("name");
$form->field(name => "allowed_attachments", size => 50,
fieldset => "admin",
comment => "(".htmllink("", "", "ikiwiki/PageSpec", noimageinline => 1).")");
if (! IkiWiki::is_admin($user_name)) {
$form->field(name => "allowed_attachments", type => "hidden");
}
if (! $form->submitted) {
$form->field(name => "allowed_attachments", force => 1,
value => IkiWiki::userinfo_get($user_name, "allowed_attachments"));
}
if ($form->submitted && $form->submitted eq 'Save Preferences') {
if (defined $form->field("allowed_attachments")) {
IkiWiki::userinfo_set($user_name, "allowed_attachments",
$form->field("allowed_attachments")) ||
error("failed to set allowed_attachments");
}
}
}
} #}}}
sub formbuilder (@) { #{{{
my %params=@_;
my $form=$params{form};
my $q=$params{cgi};
return if $form->field("do") ne "edit";
my $filename=$q->param('attachment');
if (defined $filename && length $filename &&
($form->submitted eq "Upload Attachment" || $form->submitted eq "Save Page")) {
my $session=$params{session};
# 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);
$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"));
}
# Check that the user is allowed to edit a page with the
# name of the attachment.
IkiWiki::check_canedit($filename, $q, $session, 1);
# Use a special pagespec to test that the attachment is valid.
my $allowed=1;
foreach my $admin (@{$config{adminuser}}) {
my $allowed_attachments=IkiWiki::userinfo_get($admin, "allowed_attachments");
if (defined $allowed_attachments &&
length $allowed_attachments) {
$allowed=pagespec_match($filename,
$allowed_attachments,
file => $tempfile,
user => $session->param("name"),
ip => $ENV{REMOTE_ADDR},
);
last if $allowed;
}
}
if (! $allowed) {
error(gettext("attachment rejected")." ($allowed)");
}
# Needed for fast_file_copy and for rendering below.
require IkiWiki::Render;
# Move the attachment into place.
# 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)) {
# 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");
}
binmode($fh);
writefile($filename, $config{srcdir}, undef, 1, sub {
IkiWiki::fast_file_copy($tempfile, $filename, $fh, @_);
});
}
# Check the attachment in and trigger a wiki refresh.
if ($config{rcs}) {
IkiWiki::rcs_add($filename);
IkiWiki::disable_commit_hook();
IkiWiki::rcs_commit($filename, gettext("attachment upload"),
IkiWiki::rcs_prepedit($filename),
$session->param("name"), $ENV{REMOTE_ADDR});
IkiWiki::enable_commit_hook();
IkiWiki::rcs_update();
}
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'))]);
} # }}}
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" => '<input type="checkbox" name="attachment_select" value="'.$f.'" />',
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;
foreach my $unit (sort keys %units) {
if ($size=~/[0-9\s]\Q$unit\E$/i) {
return $base * $units{$unit};
}
}
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 $size; # near zero, or negative
} #}}}
package IkiWiki::PageSpec;
sub match_maxsize ($$;@) { #{{{
shift;
my $maxsize=eval{IkiWiki::Plugin::attachment::parsesize(shift)};
if ($@) {
return IkiWiki::FailReason->new("unable to parse maxsize (or number too large)");
}
my %params=@_;
if (! exists $params{file}) {
return IkiWiki::FailReason->new("no file specified");
}
if (-s $params{file} > $maxsize) {
return IkiWiki::FailReason->new("file too large (".(-s $params{file})." > $maxsize)");
}
else {
return IkiWiki::SuccessReason->new("file not too large");
}
} #}}}
sub match_minsize ($$;@) { #{{{
shift;
my $minsize=eval{IkiWiki::Plugin::attachment::parsesize(shift)};
if ($@) {
return IkiWiki::FailReason->new("unable to parse minsize (or number too large)");
}
my %params=@_;
if (! exists $params{file}) {
return IkiWiki::FailReason->new("no file specified");
}
if (-s $params{file} < $minsize) {
return IkiWiki::FailReason->new("file too small");
}
else {
return IkiWiki::SuccessReason->new("file not too small");
}
} #}}}
sub match_ispage ($$;@) { #{{{
my $filename=shift;
if (defined IkiWiki::pagetype($filename)) {
return IkiWiki::SuccessReason->new("file is a wiki page");
}
else {
return IkiWiki::FailReason->new("file is not a wiki page");
}
} #}}}
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

View File

@ -48,8 +48,7 @@ sub formbuilder_setup { #{{{
my $form=$params{form};
my $page=$form->field("page");
return if $form->title ne "editpage"
|| $form->field("do") ne "edit";
return if $form->field("do") ne "edit";
$page = IkiWiki::titlepage(IkiWiki::possibly_foolish_untaint($page));
return unless exists $pagesources{$page};

View File

@ -40,11 +40,11 @@ sub formbuilder_setup (@) { #{{{
my %params=@_;
my $form=$params{form};
my $session=$params{session};
my $cgi=$params{cgi};
my $user_name=$session->param("name");
if ($form->title eq "preferences") {
my $session=$params{session};
my $cgi=$params{cgi};
my $user_name=$session->param("name");
$form->field(name => "locked_pages", size => 50,
fieldset => "admin",
comment => "(".htmllink("", "", "ikiwiki/PageSpec", noimageinline => 1).")");

View File

@ -9,7 +9,7 @@ use IkiWiki 2.00;
# of css to hide toggleables, to avoid any flashing on page load. The css
# is only emitted after the javascript tests that it's going to be able to
# show the toggleables.
my $javascript=<<'EOF';
our $javascript=<<'EOF';
<script type="text/javascript">
<!--
if (document.getElementById && document.getElementsByTagName && document.createTextNode) {
@ -21,7 +21,8 @@ function inittoggle() {
var as = getElementsByClass('toggle');
for (var i = 0; i < as.length; i++) {
var id = as[i].href.match(/#(\w.+)/)[1];
document.getElementById(id).style.display="none";
if (document.getElementById(id).className == "toggleable")
document.getElementById(id).style.display="none";
as[i].onclick = function() {
toggle(this);
return false;
@ -80,17 +81,11 @@ sub preprocess_toggle (@) { #{{{
my %params=(id => "default", text => "more", @_);
my $id=genid($params{page}, $params{id});
if (! $params{preview}) {
return "<a class=\"toggle\" href=\"#$id\">$params{text}</a>";
}
else {
return "$params{text} ".
gettext("(not toggleable in preview mode)");
}
return "<a class=\"toggle\" href=\"#$id\">$params{text}</a>";
} # }}}
sub preprocess_toggleable (@) { #{{{
my %params=(id => "default", text => "", @_);
my %params=(id => "default", text => "", open => "no", @_);
# Preprocess the text to expand any preprocessor directives
# embedded inside it.
@ -98,23 +93,24 @@ sub preprocess_toggleable (@) { #{{{
IkiWiki::filter($params{page}, $params{destpage}, $params{text}));
my $id=genid($params{page}, $params{id});
my $class=(lc($params{open}) ne "yes") ? "toggleable" : "toggleable-open";
# Should really be a postprocessor directive, oh well. Work around
# markdown's dislike of markdown inside a <div> with various funky
# whitespace.
my ($indent)=$params{text}=~/( +)$/;
$indent="" unless defined $indent;
return "<div class=\"toggleable\" id=\"$id\"></div>\n\n$params{text}\n$indent<div class=\"toggleableend\"></div>";
return "<div class=\"$class\" id=\"$id\"></div>\n\n$params{text}\n$indent<div class=\"toggleableend\"></div>";
} # }}}
sub format (@) { #{{{
my %params=@_;
if ($params{content}=~s!(<div class="toggleable" id="[^"]+">)</div>!$1!g) {
if ($params{content}=~s!(<div class="toggleable(?:-open)?" id="[^"]+">)</div>!$1!g) {
$params{content}=~s/<div class="toggleableend">//g;
if (! ($params{content}=~s!^<\/body>!$javascript</body>!m)) {
if (! ($params{content}=~s!^<body>!<body>$javascript!m)) {
# no </body> tag, probably in preview mode
$params{content}.=$javascript;
$params{content}=$javascript.$params{content};
}
}
return $params{content};

View File

@ -180,6 +180,30 @@ sub scan ($) { #{{{
}
} #}}}
sub fast_file_copy (@) { #{{{
my $srcfile=shift;
my $destfile=shift;
my $srcfd=shift;
my $destfd=shift;
my $cleanup=shift;
my $blksize = 16384;
my ($len, $buf, $written);
while ($len = sysread $srcfd, $buf, $blksize) {
if (! defined $len) {
next if $! =~ /^Interrupted/;
error("failed to read $srcfile: $!", $cleanup);
}
my $offset = 0;
while ($len) {
defined($written = syswrite $destfd, $buf, $len, $offset)
or error("failed to write $destfile: $!", $cleanup);
$len -= $written;
$offset += $written;
}
}
}
sub render ($) { #{{{
my $file=shift;
@ -215,24 +239,7 @@ sub render ($) { #{{{
my $srcfd=readfile($srcfile, 1, 1);
writefile($file, $config{destdir}, undef, 1, sub {
my $destfd=shift;
my $cleanup=shift;
my $blksize = 16384;
my ($len, $buf, $written);
while ($len = sysread $srcfd, $buf, $blksize) {
if (! defined $len) {
next if $! =~ /^Interrupted/;
error("failed to read $srcfile: $!", $cleanup);
}
my $offset = 0;
while ($len) {
defined($written = syswrite $destfd, $buf, $len, $offset)
or error("failed to write $file: $!", $cleanup);
$len -= $written;
$offset += $written;
}
}
fast_file_copy($srcfile, $file, $srcfd, @_);
});
}
} #}}}

13
debian/changelog vendored
View File

@ -1,3 +1,16 @@
ikiwiki (2.52) UNRELEASED; urgency=low
* attachment: New plugin for uploading and managing attachments.
(Sponsored by The TOVA Company.)
* If attachments are not enabled, configure CGI.pm to disable file
uploads by default. (An anti-DOS measure.)
* toggle: Add support for toggles that are open by default.
* toggle: Fix to work in preview mode.
* toggle: Add javascript to top of page, not to end. This avoids flicker
since closed toggles will not be displayed as the page is loading.
-- Joey Hess <joeyh@debian.org> Mon, 30 Jun 2008 19:56:28 -0400
ikiwiki (2.51) unstable; urgency=low
* Improve toplevel parentlink to link directly to index.html when usedirs is

View File

@ -0,0 +1,64 @@
[[template id=plugin name=conditional core=1 author="[[Joey]]"]]
[[tag type/useful]]
This plugin allows files to be uploaded to the wiki over the web.
For each page `foo`, files in the subdirectory `foo/` are treated as
attachments of that page. Attachments can be uploaded and managed as
part of the interface for editing a page.
Warning: Do not enable this plugin on publically editable wikis, unless you
take care to lock down the types and sizes of files that can be uploaded.
Bear in mind that if you let anyone upload a particular kind of file
("*.mp3" files, say), then someone can abuse your wiki in at least three ways:
1. By uploading many mp3 files, wasting your disk space.
2. By uploading mp3 files that attempt to exploit security holes
in web browsers or other players.
3. By uploading files that claim to be mp3 files, but are really some
other kind of file. Some web browsers may display a `foo.mp3` that
contains html as a web page; including running any malicious javascript
embedded in that page.
To provide a way to combat these abuses, the wiki admin can specify a
[[ikiwiki/PageSpec]] on their preferences page, to control what types of
attachments can be uploaded, and by whom. The regular [[ikiwiki/PageSpec]]
syntax is expanded with additional tests.
For example, to limit arbitrary files to 50 kilobytes, but allow
larger mp3 files to be uploaded by joey, a test like this could be
used:
(user(joey) and *.mp3 and maxsize(15mb)) or (!ispage() and maxsize(50kb))
The following additional tests are available:
* maxsize(size)
Tests whether the attachment is no larger than the specified size.
The size defaults to being in bytes, but "kb", "mb", "gb" etc can be
used to specify the units.
* minsize(size)
Tests whether the attachment is no smaller than the specified size.
* ispage()
Tests whether the attachment will be treated by ikiwiki as a wiki page.
(Ie, if it has an extension of ".mdwn", or of any other enabled page
format).
So, if you don't want to allow wiki pages to be uploaded as attachments,
use `!ispage()` ; if you only want to allow wiki pages to be uploaded
as attachments, use `ispage()`.
* user(username)
Tests whether the attachment is being uploaded by a user with the
specified username. If openid is enabled, an openid can also be put here.
* ip(address)
Tests whether the attacment is being uploaded from the specified IP
address.

View File

@ -0,0 +1,18 @@
I found this posted to todo list, moved here: --[[Joey]]
> First pass at an attachments plugin. See [[plugins/contrib/attach]] for
> details/docs. Here's the [diff](http://pastebin.com/f4d889b65), and
> here's some [technical notes](http://pastebin.com/f584b9d9d). There are
> still various things I want to fix and tweak, but it works reasonably for
> me as is.
I guess I missed this when the plugin page was posted last September, and
since the [[soc]] stuff wasn't updated, I didn't realize this was Ben's soc
work. Which is more or less why I didn't look at it.
This plugin would need quite a lot of work to finish up, I do think it was
taking the right approach, sorry I never followed up on it.
In the meantime, I've written an attachment plugin that does most of the
same stuff, and behaves closer to how I originally sketched [[todo/fileupload]]
as working.

View File

@ -28,3 +28,6 @@ each other, but can be located anywhere on the page. There can also be
mutiple toggles that all toggle a single togglable.
The id has a default value of "default", so can be omitted in simple cases.
If you'd like a toggleable to be displayed by default, and toggle to
hidden, then pass a parameter "open=true" when setting up the toggleable.

View File

@ -11,7 +11,7 @@ accepted, and the following projects were worked on:
(See [[todo/latex]])
* Implement File Upload Functionality and Image Gallery Creation
by Ben Coffey
(See [[todo/fileupload/soc-proposal]])
(See [[todo/fileupload/soc-proposal]] and [[plugins/contrib/attach]])
* Wiki WYSIWYG Editor
by [[TaylorKillian]]
(See [[todo/wikiwyg]])

View File

@ -0,0 +1,16 @@
Stuff the [[plugins/attachment]] plugin is currently missing, that might be
nice to add:
* `mimetype()` pagespecs. (Using a mime type sniffer.)
* Virus scanning.
* Add a progress bar for attachment uploads (needs AJAX stuff..)
* Maybe optimise the "Insert Links" button with javascript, so, if
javascript is available, the link is inserted at the current cursor
position in the page edit form, without actually reposting the form.
(Falling back to the current reposting of the form if javascript is not
available of course.)
* Set `$CGI::POST_MAX` to some sane value (ie, larger than the largest
configured `maxsize()` in the pagespec, or if none is configured,
something reasonable. Just as a belt-and-suspenders DOS prevention.
* Only allow attachments to be added to a given list of pages.
Maybe a pagespec like `parent(patches/*)`

View File

@ -1 +0,0 @@
First pass at an attachments plugin. See [[plugins/contrib/attach]] for details/docs. Here's the [diff](http://pastebin.com/f4d889b65), and here's some [technical notes](http://pastebin.com/f584b9d9d). There are still various things I want to fix and tweak, but it works reasonably for me as is.

View File

@ -60,4 +60,4 @@ pagespec lock like the above prevents an edit or upload from happening,
ikiwiki could display a reasonable message to the user, indicating what
they've done wrong.)
[[tag soc]]
[[tag soc done]]

View File

@ -2,3 +2,5 @@ It would be nice if one could set the initial state of the toggleable area.
--[[[rdennis]]
[[tag plugins/toggle]]
[[done]]

View File

@ -1,3 +1,4 @@
<TMPL_VAR JAVASCRIPT>
<TMPL_IF NAME="PAGE_CONFLICT">
<p>
<b>Your changes conflict with other changes made to the page.</b>
@ -60,6 +61,20 @@ Optional comment about this change:<br />
</TMPL_IF>
<TMPL_VAR FORM-SUBMIT>
<TMPL_VAR HELPONFORMATTINGLINK>
<a class="toggle" href="#attachments">Attachments</a>
<TMPL_IF NAME="FIELD-ATTACHMENT">
<div class="<TMPL_VAR ATTACHMENTS-CLASS>" id="attachments">
<table>
<tr><td colspan="5"><TMPL_VAR FIELD-ATTACHMENT><TMPL_VAR FIELD-UPLOAD></td></tr>
<TMPL_LOOP NAME="ATTACHMENT_LIST">
<tr><td><TMPL_VAR FIELD-SELECT><TMPL_VAR LINK></td><td><TMPL_VAR SIZE></td><td><TMPL_VAR MTIME></td></tr>
</TMPL_LOOP>
<TMPL_IF NAME="ATTACHMENT_LIST">
<tr><td colspan="2"><TMPL_VAR FIELD-LINK><TMPL_VAR FIELD-DELETE><TMPL_VAR FIELD-RENAME></td></tr>
</TMPL_IF>
</table>
</div>
</TMPL_IF>
<TMPL_VAR FORM-END>
<TMPL_IF NAME="PAGE_PREVIEW">