From 4c96fce144115c55ce089b8cc8fb4021ba69ba20 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 14 Jun 2011 13:06:59 -0400 Subject: [PATCH 01/31] refactor --- IkiWiki/Plugin/attachment.pm | 162 ++++++++++++++++++----------------- 1 file changed, 84 insertions(+), 78 deletions(-) diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index 647a671a5..c8cabe18c 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -116,84 +116,7 @@ sub formbuilder (@) { my $filename=Encode::decode_utf8($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); - 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::basename($filename); - $filename=~s/.*\\+(.+)/$1/; # hello, windows - - $filename=linkpage(IkiWiki::possibly_foolish_untaint( - attachment_location($form->field('page')). - $filename)); - if (IkiWiki::file_pruned($filename)) { - 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); - # And that the attachment itself is acceptable. - check_canattach($session, $filename, $tempfile); - - # 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) { - # 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 { - 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( - file => $filename, - message => gettext("attachment upload"), - token => IkiWiki::rcs_prepedit($filename), - session => $session, - ); - IkiWiki::enable_commit_hook(); - IkiWiki::rcs_update(); - } - IkiWiki::refresh(); - IkiWiki::saveindex(); + attachment_save($filename, $form, $q, $params{session}); } elsif ($form->submitted eq "Insert Links") { my $page=quotemeta(Encode::decode_utf8($q->param("page"))); @@ -220,6 +143,89 @@ sub formbuilder (@) { $form->tmpl_param("attachment_list" => [attachment_list($form->field('page'))]); } +sub attachment_save { + my $filename=shift; + my $form=shift; + my $q=shift; + my $session=shift; + + # 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); + 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::basename($filename); + $filename=~s/.*\\+(.+)/$1/; # hello, windows + $filename=linkpage(IkiWiki::possibly_foolish_untaint( + attachment_location($form->field('page')). + $filename)); + if (IkiWiki::file_pruned($filename)) { + 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); + # And that the attachment itself is acceptable. + check_canattach($session, $filename, $tempfile); + + # 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) { + # 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 { + 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( + file => $filename, + message => gettext("attachment upload"), + token => IkiWiki::rcs_prepedit($filename), + session => $session, + ); + IkiWiki::enable_commit_hook(); + IkiWiki::rcs_update(); + } + IkiWiki::refresh(); + IkiWiki::saveindex(); +} + sub attachment_location ($) { my $page=shift; From e08daac239a0a29b5b9d936d6ec4f9ae1c67bf49 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 14 Jun 2011 13:30:09 -0400 Subject: [PATCH 02/31] store attachments in holding area; commit to wiki on page save This makes uploading a lot of attachments somewhat faster, because the user does not need to wait for a long website refresh after each upload. Still probably somewhat slow, since ikiwiki has to run for each upload. More importantly, this opens the door for integration of things like the jquery file upload interface, which allow drag-n-drop and multiple file uploads to be queued and then ran. It uses rcs_commit_staged, which leaves out tla and mercurual which lack that, but since rename, remove, autoindex, etc also use that, I think it's fine for attachments to also depend on it. The attachment list is currently broken; it does not look in the holding area yet, and its links to the attached files won't work since they're not yet in the wiki. previewing is also currently broken. Work sponsored by TOVA. --- IkiWiki/Plugin/attachment.pm | 80 +++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 25 deletions(-) diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index c8cabe18c..b7ea1f312 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -116,9 +116,13 @@ sub formbuilder (@) { my $filename=Encode::decode_utf8($q->param('attachment')); if (defined $filename && length $filename && ($form->submitted eq "Upload Attachment" || $form->submitted eq "Save Page")) { - attachment_save($filename, $form, $q, $params{session}); + attachment_store($filename, $form, $q, $params{session}); } - elsif ($form->submitted eq "Insert Links") { + if ($form->submitted eq "Save Page") { + attachments_save($form, $params{session}); + } + + if ($form->submitted eq "Insert Links") { my $page=quotemeta(Encode::decode_utf8($q->param("page"))); my $add=""; foreach my $f ($q->param("attachment_select")) { @@ -143,7 +147,14 @@ sub formbuilder (@) { $form->tmpl_param("attachment_list" => [attachment_list($form->field('page'))]); } -sub attachment_save { +sub attachment_holding_dir { + my $page=shift; + + return $config{wikistatedir}."/attachments/$page"; +} + +# Stores the attachment in a holding area, not yet in the wiki proper. +sub attachment_store { my $filename=shift; my $form=shift; my $q=shift; @@ -167,29 +178,26 @@ sub attachment_save { $filename=IkiWiki::basename($filename); $filename=~s/.*\\+(.+)/$1/; # hello, windows - $filename=linkpage(IkiWiki::possibly_foolish_untaint( + + # Check that the user is allowed to edit the attachment. + my $final_filename=linkpage(IkiWiki::possibly_foolish_untaint( attachment_location($form->field('page')). - $filename)); - if (IkiWiki::file_pruned($filename)) { + $filename)); + if (IkiWiki::file_pruned($final_filename)) { 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); + IkiWiki::check_canedit($final_filename, $q, $session); # And that the attachment itself is acceptable. - check_canattach($session, $filename, $tempfile); + check_canattach($session, $final_filename, $tempfile); - # Needed for fast_file_copy and for rendering below. - require IkiWiki::Render; - - # Move the attachment into place. + # Move the attachment into holding directory. # 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)) { + my $dest=attachment_holding_dir($form->field('page')); + IkiWiki::prep_writefile($filename, $dest); + unlink($dest."/".$filename); + if (rename($tempfile, $dest."/".$filename)) { # The temp file has tight permissions; loosen up. - chmod(0666 & ~umask, $config{srcdir}."/".$filename); + chmod(0666 & ~umask, $dest."/".$filename); } else { my $fh=$q->upload('attachment'); @@ -204,19 +212,41 @@ sub attachment_save { } } binmode($fh); - writefile($filename, $config{srcdir}, undef, 1, sub { + # Needed for fast_file_copy. + require IkiWiki::Render; + writefile($filename, $dest, undef, 1, sub { IkiWiki::fast_file_copy($tempfile, $filename, $fh, @_); }); } +} - # Check the attachment in and trigger a wiki refresh. +# Save all stored attachments for a page. +sub attachments_save { + my $form=shift; + my $session=shift; + + # Move attachments out of holding directory. + my @attachments; + my $dir=attachment_holding_dir($form->field('page')); + foreach my $filename (glob("$dir/*")) { + next unless -f $filename; + my $dest=$config{srcdir}."/". + linkpage(IkiWiki::possibly_foolish_untaint( + attachment_location($form->field('page')). + $filename)); + unlink($dest); + rename($filename, $dest); + push @attachments, $dest; + } + return unless @attachments; + rmdir($dir); + + # Check the attachments in and trigger a wiki refresh. if ($config{rcs}) { - IkiWiki::rcs_add($filename); + IkiWiki::rcs_add($_) foreach @attachments; IkiWiki::disable_commit_hook(); - IkiWiki::rcs_commit( - file => $filename, + IkiWiki::rcs_commit_staged( message => gettext("attachment upload"), - token => IkiWiki::rcs_prepedit($filename), session => $session, ); IkiWiki::enable_commit_hook(); From 1d951583a6d3718ea5e44b8c52fb2acbe5a989e8 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 14 Jun 2011 13:38:37 -0400 Subject: [PATCH 03/31] store filename in holding dir in linkpage form Avoids any unpleasantness with .. or other special chars in the attachment filename. --- IkiWiki/Plugin/attachment.pm | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index b7ea1f312..f46388948 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -178,11 +178,13 @@ sub attachment_store { $filename=IkiWiki::basename($filename); $filename=~s/.*\\+(.+)/$1/; # hello, windows + $filename=IkiWiki::possibly_foolish_untaint(linkpage($filename)); # Check that the user is allowed to edit the attachment. - my $final_filename=linkpage(IkiWiki::possibly_foolish_untaint( - attachment_location($form->field('page')). - $filename)); + my $final_filename= + linkpage(IkiWiki::possibly_foolish_untaint( + attachment_location($form->field('page')))). + $filename; if (IkiWiki::file_pruned($final_filename)) { error(gettext("bad attachment filename")); } @@ -232,8 +234,8 @@ sub attachments_save { next unless -f $filename; my $dest=$config{srcdir}."/". linkpage(IkiWiki::possibly_foolish_untaint( - attachment_location($form->field('page')). - $filename)); + attachment_location($form->field('page')))). + $filename; unlink($dest); rename($filename, $dest); push @attachments, $dest; From 8619faaa8b01c4675be8d2c53d5d96f9c5d3fa16 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 14 Jun 2011 13:41:07 -0400 Subject: [PATCH 04/31] untaint and linkpage the page name used in attachment holding directory --- IkiWiki/Plugin/attachment.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index f46388948..f4bfbe98f 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -150,7 +150,8 @@ sub formbuilder (@) { sub attachment_holding_dir { my $page=shift; - return $config{wikistatedir}."/attachments/$page"; + return $config{wikistatedir}."/attachments/". + IkiWiki::possibly_foolish_untaint(linkpage($page)); } # Stores the attachment in a holding area, not yet in the wiki proper. From e2cb19ed9e107d89251655232649a6d2eb9c187b Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 14 Jun 2011 13:49:41 -0400 Subject: [PATCH 05/31] attachment list includes new attachments in holding area Note that it's possible for an attachment in the holding area to be older than an attachemnt in the wiki with the same name. I intentionally show the one in the holding area in this (unlikely) case, since saving the page will overwrite the wiki's file with the held attachment. It does not seem worth the bother of doing something more intelligent, since in this case two people have basically conflicted with one-another.. and both attachment contents will be stored in revision control in case it needs to be sorted out. I had to remove the hyperlink for attachments in the holding area, since they're not yet live on the web. This could be annoying/confusing. Added a moseover notice instead. --- IkiWiki/Plugin/attachment.pm | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index f4bfbe98f..7f88010b5 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -274,11 +274,12 @@ sub attachment_list ($) { my $page=shift; my $loc=attachment_location($page); - my @ret; + # attachments already in the wiki + my %attachments; foreach my $f (values %pagesources) { if (! defined pagetype($f) && $f=~m/^\Q$loc\E[^\/]+$/) { - push @ret, { + $attachments{$f}={ "field-select" => '', link => htmllink($page, $page, $f, noimageinline => 1), size => IkiWiki::Plugin::filecheck::humansize((stat($f))[7]), @@ -287,10 +288,25 @@ sub attachment_list ($) { }; } } + + # attachments in holding directory + my $dir=attachment_holding_dir($page); + foreach my $file (glob("$dir/*")) { + my $mtime=(stat($file))[9]; + my $f=IkiWiki::basename($file); + $attachments{$f}={ + "field-select" => '', + link => $f, # no link possible + size => IkiWiki::Plugin::filecheck::humansize((stat($file))[7]), + mtime => displaytime($mtime), + mtime_raw => $mtime, + } + } # 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; + return sort { $b->{mtime_raw} <=> $a->{mtime_raw} || $a->{link} cmp $b->{link} } + values %attachments; } 1 From d4d232740078d0cab15b4f78e797c0802b692e5c Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 14 Jun 2011 13:59:11 -0400 Subject: [PATCH 06/31] fixed saving attachments on page save Also saved on preview, but previewing is a bit broken, does not see the newly saved attachment yet. --- IkiWiki/Plugin/attachment.pm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index 7f88010b5..06b0c6c51 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -118,7 +118,7 @@ sub formbuilder (@) { ($form->submitted eq "Upload Attachment" || $form->submitted eq "Save Page")) { attachment_store($filename, $form, $q, $params{session}); } - if ($form->submitted eq "Save Page") { + if ($form->submitted eq "Save Page" || $form->submitted eq "Preview") { attachments_save($form, $params{session}); } @@ -236,7 +236,7 @@ sub attachments_save { my $dest=$config{srcdir}."/". linkpage(IkiWiki::possibly_foolish_untaint( attachment_location($form->field('page')))). - $filename; + IkiWiki::basename($filename); unlink($dest); rename($filename, $dest); push @attachments, $dest; @@ -291,12 +291,13 @@ sub attachment_list ($) { # attachments in holding directory my $dir=attachment_holding_dir($page); + my $heldmsg=gettext("this attachment is not yet saved"); foreach my $file (glob("$dir/*")) { my $mtime=(stat($file))[9]; my $f=IkiWiki::basename($file); $attachments{$f}={ "field-select" => '', - link => $f, # no link possible + link => "$f", size => IkiWiki::Plugin::filecheck::humansize((stat($file))[7]), mtime => displaytime($mtime), mtime_raw => $mtime, From c55a32d3e18e5ed72be0dd74461f33ee5e39ef12 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 14 Jun 2011 14:08:13 -0400 Subject: [PATCH 07/31] fixed previewing of attachments from holding area --- IkiWiki/Plugin/attachment.pm | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index 06b0c6c51..eb8846d06 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -103,6 +103,12 @@ sub formbuilder_setup (@) { else { $form->tmpl_param("attachments-class" => "toggleable-open"); } + + # Save attachments in holding area before previewing so + # they can be seen in the preview. + if ($form->submitted eq "Preview") { + attachments_save($form, $params{session}); + } } } @@ -118,7 +124,7 @@ sub formbuilder (@) { ($form->submitted eq "Upload Attachment" || $form->submitted eq "Save Page")) { attachment_store($filename, $form, $q, $params{session}); } - if ($form->submitted eq "Save Page" || $form->submitted eq "Preview") { + if ($form->submitted eq "Save Page") { attachments_save($form, $params{session}); } From 04b2a4d6762b33a199b18de4df3991ecba87db7d Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 14 Jun 2011 14:44:20 -0400 Subject: [PATCH 08/31] fix removal of helf attachments Left out confirmation of removal for held attachments because a) they're not in the wiki yet, so confirmation is a bit unnecessary b) it would be hard c) eases later integration of jquery file upload interface Also changed where attachments of index are held (to match where they're stored in the srcdir). Note that the attachment formbuilder hook was made to run last, so that the list of attachments is not generated before removal, in the fast path w/o confirm. --- IkiWiki/Plugin/attachment.pm | 25 ++++++++++++++++++++----- IkiWiki/Plugin/remove.pm | 15 +++++++++++---- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index eb8846d06..c6f8891c2 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -10,7 +10,7 @@ sub import { hook(type => "getsetup", id => "attachment", call => \&getsetup); hook(type => "checkconfig", id => "attachment", call => \&checkconfig); hook(type => "formbuilder_setup", id => "attachment", call => \&formbuilder_setup); - hook(type => "formbuilder", id => "attachment", call => \&formbuilder); + hook(type => "formbuilder", id => "attachment", call => \&formbuilder, last => 1); IkiWiki::loadplugin("filecheck"); } @@ -154,12 +154,27 @@ sub formbuilder (@) { } sub attachment_holding_dir { - my $page=shift; + my $page=attachment_location(shift); return $config{wikistatedir}."/attachments/". IkiWiki::possibly_foolish_untaint(linkpage($page)); } +sub remove_held_attachment { + my $attachment=shift; + + my $f=attachment_holding_dir($attachment); + $f=~s/\/$//; + if (-f $f) { + require IkiWiki::Render; + IkiWiki::prune($f); + return 1; + } + else { + return 0; + } +} + # Stores the attachment in a holding area, not yet in the wiki proper. sub attachment_store { my $filename=shift; @@ -221,8 +236,7 @@ sub attachment_store { } } binmode($fh); - # Needed for fast_file_copy. - require IkiWiki::Render; + require IkiWiki::Render; writefile($filename, $dest, undef, 1, sub { IkiWiki::fast_file_copy($tempfile, $filename, $fh, @_); }); @@ -248,7 +262,8 @@ sub attachments_save { push @attachments, $dest; } return unless @attachments; - rmdir($dir); + require IkiWiki::Render; + IkiWiki::prune($dir); # Check the attachments in and trigger a wiki refresh. if ($config{rcs}) { diff --git a/IkiWiki/Plugin/remove.pm b/IkiWiki/Plugin/remove.pm index bc481502a..5e5b83349 100644 --- a/IkiWiki/Plugin/remove.pm +++ b/IkiWiki/Plugin/remove.pm @@ -117,13 +117,20 @@ sub removal_confirm ($$@) { my $session=shift; my $attachment=shift; my @pages=@_; + + # Special case for unsaved attachments. + @pages=grep { + ! (IkiWiki::Plugin::attachment->can("remove_held_attachment") && + IkiWiki::Plugin::attachment::remove_held_attachment($_)) + } @pages; + return unless @pages; foreach my $page (@pages) { IkiWiki::check_canedit($page, $q, $session); check_canremove($page, $q, $session); } - # Save current form state to allow returning to it later + # Save current form state to allow returning to it later # without losing any edits. # (But don't save what button was submitted, to avoid # looping back to here.) @@ -178,10 +185,10 @@ sub formbuilder (@) { } sub sessioncgi ($$) { - my $q=shift; + my $q=shift; if ($q->param("do") eq 'remove') { - my $session=shift; + my $session=shift; my ($form, $buttons)=confirmation_form($q, $session); IkiWiki::decode_form_utf8($form); @@ -192,7 +199,7 @@ sub sessioncgi ($$) { IkiWiki::checksessionexpiry($q, $session, $q->param('sid')); my @pages=$form->field("page"); - + # Validate removal by checking that the page exists, # and that the user is allowed to edit(/remove) it. my @files; From a61849e95496027d07c38219d5cc47fe37b5aa25 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 14 Jun 2011 15:07:34 -0400 Subject: [PATCH 09/31] bugfix for attachments of non-index pages --- IkiWiki/Plugin/attachment.pm | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index c6f8891c2..ea40379ff 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -295,17 +295,25 @@ sub attachment_list ($) { my $page=shift; my $loc=attachment_location($page); + my $std=sub { + my $file=shift; + my $mtime=shift; + my $size=shift; + + "field-select" => '', + size => IkiWiki::Plugin::filecheck::humansize($size), + mtime => displaytime($mtime), + mtime_raw => $mtime, + }; + # attachments already in the wiki my %attachments; foreach my $f (values %pagesources) { if (! defined pagetype($f) && $f=~m/^\Q$loc\E[^\/]+$/) { $attachments{$f}={ - "field-select" => '', + $std->($f, $IkiWiki::pagemtime{$f}, (stat($f))[7]), link => htmllink($page, $page, $f, noimageinline => 1), - size => IkiWiki::Plugin::filecheck::humansize((stat($f))[7]), - mtime => displaytime($IkiWiki::pagemtime{$f}), - mtime_raw => $IkiWiki::pagemtime{$f}, }; } } @@ -315,13 +323,11 @@ sub attachment_list ($) { my $heldmsg=gettext("this attachment is not yet saved"); foreach my $file (glob("$dir/*")) { my $mtime=(stat($file))[9]; - my $f=IkiWiki::basename($file); + my $f=$file; + $f=~s/^\Q$dir\E\///; $attachments{$f}={ - "field-select" => '', + $std->($page."/".$f, (stat($file))[9], (stat($file))[7]), link => "$f", - size => IkiWiki::Plugin::filecheck::humansize((stat($file))[7]), - mtime => displaytime($mtime), - mtime_raw => $mtime, } } From 49e7bc253569c809ba5d4d9e52f8a38ae8c183f7 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 14 Jun 2011 15:19:23 -0400 Subject: [PATCH 10/31] bugfixes --- IkiWiki/Plugin/attachment.pm | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index ea40379ff..f97ab45a4 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -322,12 +322,13 @@ sub attachment_list ($) { my $dir=attachment_holding_dir($page); my $heldmsg=gettext("this attachment is not yet saved"); foreach my $file (glob("$dir/*")) { - my $mtime=(stat($file))[9]; - my $f=$file; - $f=~s/^\Q$dir\E\///; + next unless -f $file; + my $mtime=(stat(_))[9]; + my $base=IkiWiki::basename($file); + my $f=$loc.$base; $attachments{$f}={ - $std->($page."/".$f, (stat($file))[9], (stat($file))[7]), - link => "$f", + $std->($f, (stat($file))[9], (stat(_))[7]), + link => "$base", } } From d4254c7f97cacffe500c3a9b6c1d887ebd3687fb Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 14 Jun 2011 15:30:19 -0400 Subject: [PATCH 11/31] more generic interface --- IkiWiki/Plugin/attachment.pm | 8 +++----- IkiWiki/Plugin/remove.pm | 11 +++++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index f97ab45a4..f8a3a68c6 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -160,18 +160,16 @@ sub attachment_holding_dir { IkiWiki::possibly_foolish_untaint(linkpage($page)); } -sub remove_held_attachment { +sub is_held_attachment { my $attachment=shift; my $f=attachment_holding_dir($attachment); $f=~s/\/$//; if (-f $f) { - require IkiWiki::Render; - IkiWiki::prune($f); - return 1; + return $f } else { - return 0; + return undef; } } diff --git a/IkiWiki/Plugin/remove.pm b/IkiWiki/Plugin/remove.pm index 5e5b83349..e6f7e1dbd 100644 --- a/IkiWiki/Plugin/remove.pm +++ b/IkiWiki/Plugin/remove.pm @@ -120,8 +120,15 @@ sub removal_confirm ($$@) { # Special case for unsaved attachments. @pages=grep { - ! (IkiWiki::Plugin::attachment->can("remove_held_attachment") && - IkiWiki::Plugin::attachment::remove_held_attachment($_)) + if (IkiWiki::Plugin::attachment->can("is_held_attachment")) { + my $f=IkiWiki::Plugin::attachment::is_held_attachment($_); + if (defined $f) { + require IkiWiki::Render; + IkiWiki::prune($f); + 0; + } + } + 1; } @pages; return unless @pages; From 2e086b3263f33d3d42ddbcb451e9995c466929cd Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 14 Jun 2011 15:47:47 -0400 Subject: [PATCH 12/31] remove trailing slash from attachment_holding_dir If it's passed a filename, it should return the filename inside the holding dir. If passed a page, the directory sans slash. All code adds the slash. --- IkiWiki/Plugin/attachment.pm | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index f8a3a68c6..4bd08bf7b 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -153,18 +153,19 @@ sub formbuilder (@) { $form->tmpl_param("attachment_list" => [attachment_list($form->field('page'))]); } -sub attachment_holding_dir { +sub attachment_holding_location { my $page=attachment_location(shift); - return $config{wikistatedir}."/attachments/". + my $dir=$config{wikistatedir}."/attachments/". IkiWiki::possibly_foolish_untaint(linkpage($page)); + $dir=~s/\/$//; + return $dir; } sub is_held_attachment { my $attachment=shift; - my $f=attachment_holding_dir($attachment); - $f=~s/\/$//; + my $f=attachment_holding_location($attachment); if (-f $f) { return $f } @@ -214,7 +215,7 @@ sub attachment_store { # Move the attachment into holding directory. # Try to use a fast rename; fall back to copying. - my $dest=attachment_holding_dir($form->field('page')); + my $dest=attachment_holding_location($form->field('page')); IkiWiki::prep_writefile($filename, $dest); unlink($dest."/".$filename); if (rename($tempfile, $dest."/".$filename)) { @@ -248,7 +249,7 @@ sub attachments_save { # Move attachments out of holding directory. my @attachments; - my $dir=attachment_holding_dir($form->field('page')); + my $dir=attachment_holding_location($form->field('page')); foreach my $filename (glob("$dir/*")) { next unless -f $filename; my $dest=$config{srcdir}."/". @@ -317,7 +318,7 @@ sub attachment_list ($) { } # attachments in holding directory - my $dir=attachment_holding_dir($page); + my $dir=attachment_holding_location($page); my $heldmsg=gettext("this attachment is not yet saved"); foreach my $file (glob("$dir/*")) { next unless -f $file; From 176c7f3ff36a29c2fbfb3ba079186f8c4e883633 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 14 Jun 2011 16:02:09 -0400 Subject: [PATCH 13/31] implement renaming of held attachments This is somewhat suboptimal, it does not update links to the renamed file, or show a result message. --- IkiWiki/Plugin/rename.pm | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/IkiWiki/Plugin/rename.pm b/IkiWiki/Plugin/rename.pm index e871b815d..aa1081756 100644 --- a/IkiWiki/Plugin/rename.pm +++ b/IkiWiki/Plugin/rename.pm @@ -179,8 +179,15 @@ sub rename_start ($$$$) { my $attachment=shift; my $page=shift; - check_canrename($page, $pagesources{$page}, undef, undef, - $q, $session); + # Special case for renaming held attachments; normal checks + # don't apply. + my $held=$attachment && + IkiWiki::Plugin::attachment->can("is_held_attachment") && + IkiWiki::Plugin::attachment::is_held_attachment($page); + if (! defined $held) { + check_canrename($page, $pagesources{$page}, undef, undef, + $q, $session); + } # Save current form state to allow returning to it later # without losing any edits. @@ -291,13 +298,11 @@ sub sessioncgi ($$) { elsif ($form->submitted eq 'Rename' && $form->validate) { IkiWiki::checksessionexpiry($q, $session, $q->param('sid')); - # Queue of rename actions to perfom. - my @torename; - # These untaints are safe because of the checks # performed in check_canrename later. my $src=$form->field("page"); - my $srcfile=IkiWiki::possibly_foolish_untaint($pagesources{$src}); + my $srcfile=IkiWiki::possibly_foolish_untaint($pagesources{$src}) + if exists $pagesources{$src}; my $dest=IkiWiki::possibly_foolish_untaint(titlepage($form->field("new_name"))); my $destfile=$dest; if (! $q->param("attachment")) { @@ -312,6 +317,19 @@ sub sessioncgi ($$) { $destfile=newpagefile($dest, $type); } + + # Special case for renaming held attachments. + my $held=$q->param("attachment") && + IkiWiki::Plugin::attachment->can("is_held_attachment") && + IkiWiki::Plugin::attachment::is_held_attachment($src); + if (defined $held) { + rename($held, IkiWiki::Plugin::attachment::attachment_holding_location($dest)); + postrename($session, $src, $dest, $q->param("attachment")) + unless defined $srcfile; + } + + # Queue of rename actions to perfom. + my @torename; push @torename, { src => $src, srcfile => $srcfile, From b66261d08bb978a1c5b1fe02ef25b49b4cd9d9e6 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Wed, 15 Jun 2011 15:21:59 -0400 Subject: [PATCH 14/31] WIP --- IkiWiki/Plugin/attachment.pm | 43 +++++++++++++----- templates/editpage.tmpl | 85 +++++++++++++++++++++++++++++++++--- 2 files changed, 109 insertions(+), 19 deletions(-) diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index 4bd08bf7b..cb4f4dcd6 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -119,11 +119,11 @@ sub formbuilder (@) { return if ! defined $form->field("do") || ($form->field("do") ne "edit" && $form->field("do") ne "create") ; - my $filename=Encode::decode_utf8($q->param('attachment')); - if (defined $filename && length $filename && - ($form->submitted eq "Upload Attachment" || $form->submitted eq "Save Page")) { + my $filename=Encode::decode_utf8($q->param('attachments')); + if (defined $filename && length $filename) { attachment_store($filename, $form, $q, $params{session}); } + if ($form->submitted eq "Save Page") { attachments_save($form, $params{session}); } @@ -240,6 +240,22 @@ sub attachment_store { IkiWiki::fast_file_copy($tempfile, $filename, $fh, @_); }); } + + # Return JSON response for the jquery file upload widget. + eval q{use JSON}; + error $@ if $@; + print "Content-type: application/json\n\n"; + my $size=-s $dest."/".$filename; + print to_json([ + { + name => $filename, + size => $size, + humansize => IkiWiki::Plugin::filecheck::humansize($size), + stored_msg => stored_msg(), + + } + ]); + exit 0; } # Save all stored attachments for a page. @@ -297,11 +313,12 @@ sub attachment_list ($) { my $std=sub { my $file=shift; my $mtime=shift; + my $date=shift; my $size=shift; - "field-select" => '', + name => $file, size => IkiWiki::Plugin::filecheck::humansize($size), - mtime => displaytime($mtime), + mtime => $date, mtime_raw => $mtime, }; @@ -311,7 +328,7 @@ sub attachment_list ($) { if (! defined pagetype($f) && $f=~m/^\Q$loc\E[^\/]+$/) { $attachments{$f}={ - $std->($f, $IkiWiki::pagemtime{$f}, (stat($f))[7]), + $std->($f, $IkiWiki::pagemtime{$f}, displaytime($IkiWiki::pagemtime{$f}), (stat($f))[7]), link => htmllink($page, $page, $f, noimageinline => 1), }; } @@ -322,19 +339,21 @@ sub attachment_list ($) { my $heldmsg=gettext("this attachment is not yet saved"); foreach my $file (glob("$dir/*")) { next unless -f $file; - my $mtime=(stat(_))[9]; my $base=IkiWiki::basename($file); my $f=$loc.$base; $attachments{$f}={ - $std->($f, (stat($file))[9], (stat(_))[7]), - link => "$base", + $std->($f, (stat($file))[9], stored_msg(), (stat(_))[7]), + link => $base, } } - # 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} } + # Sort newer attachments to the end of the list. + return sort { $a->{mtime_raw} <=> $b->{mtime_raw} || $a->{link} cmp $b->{link} } values %attachments; } +sub stored_msg { + gettext("just uploaded"); +} + 1 diff --git a/templates/editpage.tmpl b/templates/editpage.tmpl index d6ad80614..6fe495aa8 100644 --- a/templates/editpage.tmpl +++ b/templates/editpage.tmpl @@ -1,3 +1,5 @@ + +
@@ -25,21 +27,90 @@ Attachments -
- + - - - + +
+
+
+ + + -
+ + + + + + + + + + + +
+
+ + +
" />
+ +
-
From 00c0677588920832bad40d10f7d0fee5312fb115 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Wed, 15 Jun 2011 17:46:08 -0400 Subject: [PATCH 15/31] fix removal of staged attachments --- IkiWiki/Plugin/remove.pm | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/IkiWiki/Plugin/remove.pm b/IkiWiki/Plugin/remove.pm index e6f7e1dbd..b94447020 100644 --- a/IkiWiki/Plugin/remove.pm +++ b/IkiWiki/Plugin/remove.pm @@ -119,17 +119,17 @@ sub removal_confirm ($$@) { my @pages=@_; # Special case for unsaved attachments. - @pages=grep { + foreach my $page (@pages) { if (IkiWiki::Plugin::attachment->can("is_held_attachment")) { - my $f=IkiWiki::Plugin::attachment::is_held_attachment($_); + my $f=IkiWiki::Plugin::attachment::is_held_attachment($page); if (defined $f) { + print STDERR "!! remove $f\n"; require IkiWiki::Render; IkiWiki::prune($f); - 0; } } - 1; - } @pages; + } + @pages=grep { exists $pagesources{$_} } @pages; return unless @pages; foreach my $page (@pages) { From f77452b7caa7346625882d20f396b7154cda6a54 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Wed, 15 Jun 2011 18:37:12 -0400 Subject: [PATCH 16/31] WIP --- IkiWiki/Plugin/attachment.pm | 2 +- templates/editpage.tmpl | 89 ++++++++++++------------------------ 2 files changed, 29 insertions(+), 62 deletions(-) diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index cb4f4dcd6..fdae8d1e0 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -119,7 +119,7 @@ sub formbuilder (@) { return if ! defined $form->field("do") || ($form->field("do") ne "edit" && $form->field("do") ne "create") ; - my $filename=Encode::decode_utf8($q->param('attachments')); + my $filename=Encode::decode_utf8($q->param('attachment')); if (defined $filename && length $filename) { attachment_store($filename, $form, $q, $params{session}); } diff --git a/templates/editpage.tmpl b/templates/editpage.tmpl index 6fe495aa8..b3098a240 100644 --- a/templates/editpage.tmpl +++ b/templates/editpage.tmpl @@ -27,69 +27,8 @@ Attachments -
-
- - - - - - -
- - @@ -99,6 +38,29 @@ + +
@@ -106,6 +68,11 @@ $(function () { $('#fileupload').fileupload(); }); // initialize upload widget
+ + + + +
From 9a4e7b6500ee564f9d2abefce656738db642efde Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Wed, 15 Jun 2011 18:41:50 -0400 Subject: [PATCH 17/31] add the blueimp jquery fileupload widget from http://aquantum-demo.appspot.com/file-upload git version 7f89121 removed some files ikiwiki does not need --- .../ikiwiki/jquery.fileupload-ui.css | 100 +++ .../ikiwiki/jquery.fileupload-ui.js | 642 ++++++++++++++++ .../attachments/ikiwiki/jquery.fileupload.js | 720 ++++++++++++++++++ .../ikiwiki/jquery.iframe-transport.js | 133 ++++ underlays/attachments/ikiwiki/pbar-ani.gif | Bin 0 -> 3323 bytes 5 files changed, 1595 insertions(+) create mode 100644 underlays/attachments/ikiwiki/jquery.fileupload-ui.css create mode 100644 underlays/attachments/ikiwiki/jquery.fileupload-ui.js create mode 100644 underlays/attachments/ikiwiki/jquery.fileupload.js create mode 100644 underlays/attachments/ikiwiki/jquery.iframe-transport.js create mode 100644 underlays/attachments/ikiwiki/pbar-ani.gif diff --git a/underlays/attachments/ikiwiki/jquery.fileupload-ui.css b/underlays/attachments/ikiwiki/jquery.fileupload-ui.css new file mode 100644 index 000000000..48224023f --- /dev/null +++ b/underlays/attachments/ikiwiki/jquery.fileupload-ui.css @@ -0,0 +1,100 @@ +@charset 'UTF-8'; +/* + * jQuery File Upload UI Plugin CSS 5.0.6 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://creativecommons.org/licenses/MIT/ + */ + +.fileupload-buttonbar .ui-button input { + position: absolute; + top: 0; + right: 0; + margin: 0; + border: solid transparent; + border-width: 0 0 100px 200px; + opacity: 0; + filter: alpha(opacity=0); + -o-transform: translate(250px, -50px) scale(1); + -moz-transform: translate(-300px, 0) scale(4); + direction: ltr; + cursor: pointer; +} + +.fileinput-button { + overflow: hidden; +} + +/* Fix for IE 6: */ +*html .fileinput-button { + padding: 2px 0; +} + +/* Fix for IE 7: */ +*+html .fileinput-button { + padding: 2px 0; +} + +.fileupload-buttonbar { + padding: 0.2em 0.4em; +} + +.fileupload-buttonbar .ui-button { + vertical-align: middle; +} + +.fileupload-content { + padding: 0.2em 0.4em; + border-top-width: 0; +} + +.fileupload-content .ui-progressbar { + width: 200px; + height: 20px; +} + +.fileupload-content .ui-progressbar-value { + background: url(pbar-ani.gif); +} + +.fileupload-content .fileupload-progressbar { + width: 400px; + margin: 10px 0; +} + +.files { + margin: 10px 0; + border-collapse: collapse; +} + +.files td { + padding: 5px; + border-spacing: 5px; +} + +.files img { + border: none; +} + +.files .name { + padding: 0 10px; +} + +.files .size { + padding: 0 10px 0 0; + text-align: right; + white-space: nowrap; +} + +.ui-state-disabled .ui-state-disabled { + opacity: 1; + filter: alpha(opacity=100); +} + +.ui-state-disabled input { + cursor: default; +} \ No newline at end of file diff --git a/underlays/attachments/ikiwiki/jquery.fileupload-ui.js b/underlays/attachments/ikiwiki/jquery.fileupload-ui.js new file mode 100644 index 000000000..46b78e299 --- /dev/null +++ b/underlays/attachments/ikiwiki/jquery.fileupload-ui.js @@ -0,0 +1,642 @@ +/* + * jQuery File Upload User Interface Plugin 5.0.13 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://creativecommons.org/licenses/MIT/ + */ + +/*jslint nomen: true, unparam: true, regexp: true */ +/*global window, document, URL, webkitURL, FileReader, jQuery */ + +(function ($) { + 'use strict'; + + // The UI version extends the basic fileupload widget and adds + // a complete user interface based on the given upload/download + // templates. + $.widget('blueimpUI.fileupload', $.blueimp.fileupload, { + + options: { + // By default, files added to the widget are uploaded as soon + // as the user clicks on the start buttons. To enable automatic + // uploads, set the following option to true: + autoUpload: false, + // The following option limits the number of files that are + // allowed to be uploaded using this widget: + maxNumberOfFiles: undefined, + // The maximum allowed file size: + maxFileSize: undefined, + // The minimum allowed file size: + minFileSize: 1, + // The regular expression for allowed file types, matches + // against either file type or file name: + acceptFileTypes: /.+$/i, + // The regular expression to define for which files a preview + // image is shown, matched against the file type: + previewFileTypes: /^image\/(gif|jpeg|png)$/, + // The maximum width of the preview images: + previewMaxWidth: 80, + // The maximum height of the preview images: + previewMaxHeight: 80, + // By default, preview images are displayed as canvas elements + // if supported by the browser. Set the following option to false + // to always display preview images as img elements: + previewAsCanvas: true, + // The file upload template that is given as first argument to the + // jQuery.tmpl method to render the file uploads: + uploadTemplate: $('#template-upload'), + // The file download template, that is given as first argument to the + // jQuery.tmpl method to render the file downloads: + downloadTemplate: $('#template-download'), + // The expected data type of the upload response, sets the dataType + // option of the $.ajax upload requests: + dataType: 'json', + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop or add API call). + // See the basic file upload widget for more information: + add: function (e, data) { + var that = $(this).data('fileupload'); + that._adjustMaxNumberOfFiles(-data.files.length); + data.isAdjusted = true; + data.isValidated = that._validate(data.files); + data.context = that._renderUpload(data.files) + .appendTo($(this).find('.files')).fadeIn(function () { + // Fix for IE7 and lower: + $(this).show(); + }).data('data', data); + if ((that.options.autoUpload || data.autoUpload) && + data.isValidated) { + data.jqXHR = data.submit(); + } + }, + // Callback for the start of each file upload request: + send: function (e, data) { + if (!data.isValidated) { + var that = $(this).data('fileupload'); + if (!data.isAdjusted) { + that._adjustMaxNumberOfFiles(-data.files.length); + } + if (!that._validate(data.files)) { + return false; + } + } + if (data.context && data.dataType && + data.dataType.substr(0, 6) === 'iframe') { + // Iframe Transport does not support progress events. + // In lack of an indeterminate progress bar, we set + // the progress to 100%, showing the full animated bar: + data.context.find('.ui-progressbar').progressbar( + 'value', + parseInt(100, 10) + ); + } + }, + // Callback for successful uploads: + done: function (e, data) { + var that = $(this).data('fileupload'); + if (data.context) { + data.context.each(function (index) { + var file = ($.isArray(data.result) && + data.result[index]) || {error: 'emptyResult'}; + if (file.error) { + that._adjustMaxNumberOfFiles(1); + } + $(this).fadeOut(function () { + that._renderDownload([file]) + .css('display', 'none') + .replaceAll(this) + .fadeIn(function () { + // Fix for IE7 and lower: + $(this).show(); + }); + }); + }); + } else { + that._renderDownload(data.result) + .css('display', 'none') + .appendTo($(this).find('.files')) + .fadeIn(function () { + // Fix for IE7 and lower: + $(this).show(); + }); + } + }, + // Callback for failed (abort or error) uploads: + fail: function (e, data) { + var that = $(this).data('fileupload'); + that._adjustMaxNumberOfFiles(data.files.length); + if (data.context) { + data.context.each(function (index) { + $(this).fadeOut(function () { + if (data.errorThrown !== 'abort') { + var file = data.files[index]; + file.error = file.error || data.errorThrown + || true; + that._renderDownload([file]) + .css('display', 'none') + .replaceAll(this) + .fadeIn(function () { + // Fix for IE7 and lower: + $(this).show(); + }); + } else { + data.context.remove(); + } + }); + }); + } else if (data.errorThrown !== 'abort') { + that._adjustMaxNumberOfFiles(-data.files.length); + data.context = that._renderUpload(data.files) + .css('display', 'none') + .appendTo($(this).find('.files')) + .fadeIn(function () { + // Fix for IE7 and lower: + $(this).show(); + }).data('data', data); + } + }, + // Callback for upload progress events: + progress: function (e, data) { + if (data.context) { + data.context.find('.ui-progressbar').progressbar( + 'value', + parseInt(data.loaded / data.total * 100, 10) + ); + } + }, + // Callback for global upload progress events: + progressall: function (e, data) { + $(this).find('.fileupload-progressbar').progressbar( + 'value', + parseInt(data.loaded / data.total * 100, 10) + ); + }, + // Callback for uploads start, equivalent to the global ajaxStart event: + start: function () { + $(this).find('.fileupload-progressbar') + .progressbar('value', 0).fadeIn(); + }, + // Callback for uploads stop, equivalent to the global ajaxStop event: + stop: function () { + $(this).find('.fileupload-progressbar').fadeOut(); + }, + // Callback for file deletion: + destroy: function (e, data) { + var that = $(this).data('fileupload'); + if (data.url) { + $.ajax(data) + .success(function () { + that._adjustMaxNumberOfFiles(1); + $(this).fadeOut(function () { + $(this).remove(); + }); + }); + } else { + data.context.fadeOut(function () { + $(this).remove(); + }); + } + } + }, + + // Scales the given image (img HTML element) + // using the given options. + // Returns a canvas object if the canvas option is true + // and the browser supports canvas, else the scaled image: + _scaleImage: function (img, options) { + options = options || {}; + var canvas = document.createElement('canvas'), + scale = Math.min( + (options.maxWidth || img.width) / img.width, + (options.maxHeight || img.height) / img.height + ); + if (scale >= 1) { + scale = Math.max( + (options.minWidth || img.width) / img.width, + (options.minHeight || img.height) / img.height + ); + } + img.width = parseInt(img.width * scale, 10); + img.height = parseInt(img.height * scale, 10); + if (!options.canvas || !canvas.getContext) { + return img; + } + canvas.width = img.width; + canvas.height = img.height; + canvas.getContext('2d') + .drawImage(img, 0, 0, img.width, img.height); + return canvas; + }, + + _createObjectURL: function (file) { + var undef = 'undefined', + urlAPI = (typeof window.createObjectURL !== undef && window) || + (typeof URL !== undef && URL) || + (typeof webkitURL !== undef && webkitURL); + return urlAPI ? urlAPI.createObjectURL(file) : false; + }, + + _revokeObjectURL: function (url) { + var undef = 'undefined', + urlAPI = (typeof window.revokeObjectURL !== undef && window) || + (typeof URL !== undef && URL) || + (typeof webkitURL !== undef && webkitURL); + return urlAPI ? urlAPI.revokeObjectURL(url) : false; + }, + + // Loads a given File object via FileReader interface, + // invokes the callback with a data url: + _loadFile: function (file, callback) { + if (typeof FileReader !== 'undefined' && + FileReader.prototype.readAsDataURL) { + var fileReader = new FileReader(); + fileReader.onload = function (e) { + callback(e.target.result); + }; + fileReader.readAsDataURL(file); + return true; + } + return false; + }, + + // Loads an image for a given File object. + // Invokes the callback with an img or optional canvas + // element (if supported by the browser) as parameter: + _loadImage: function (file, callback, options) { + var that = this, + url, + img; + if (!options || !options.fileTypes || + options.fileTypes.test(file.type)) { + url = this._createObjectURL(file); + img = $('').bind('load', function () { + $(this).unbind('load'); + that._revokeObjectURL(url); + callback(that._scaleImage(img[0], options)); + }).prop('src', url); + if (!url) { + this._loadFile(file, function (url) { + img.prop('src', url); + }); + } + } + }, + + // Link handler, that allows to download files + // by drag & drop of the links to the desktop: + _enableDragToDesktop: function () { + var link = $(this), + url = link.prop('href'), + name = decodeURIComponent(url.split('/').pop()) + .replace(/:/g, '-'), + type = 'application/octet-stream'; + link.bind('dragstart', function (e) { + try { + e.originalEvent.dataTransfer.setData( + 'DownloadURL', + [type, name, url].join(':') + ); + } catch (err) {} + }); + }, + + _adjustMaxNumberOfFiles: function (operand) { + if (typeof this.options.maxNumberOfFiles === 'number') { + this.options.maxNumberOfFiles += operand; + if (this.options.maxNumberOfFiles < 1) { + this._disableFileInputButton(); + } else { + this._enableFileInputButton(); + } + } + }, + + _formatFileSize: function (file) { + if (typeof file.size !== 'number') { + return ''; + } + if (file.size >= 1000000000) { + return (file.size / 1000000000).toFixed(2) + ' GB'; + } + if (file.size >= 1000000) { + return (file.size / 1000000).toFixed(2) + ' MB'; + } + return (file.size / 1000).toFixed(2) + ' KB'; + }, + + _hasError: function (file) { + if (file.error) { + return file.error; + } + // The number of added files is subtracted from + // maxNumberOfFiles before validation, so we check if + // maxNumberOfFiles is below 0 (instead of below 1): + if (this.options.maxNumberOfFiles < 0) { + return 'maxNumberOfFiles'; + } + // Files are accepted if either the file type or the file name + // matches against the acceptFileTypes regular expression, as + // only browsers with support for the File API report the type: + if (!(this.options.acceptFileTypes.test(file.type) || + this.options.acceptFileTypes.test(file.name))) { + return 'acceptFileTypes'; + } + if (this.options.maxFileSize && + file.size > this.options.maxFileSize) { + return 'maxFileSize'; + } + if (typeof file.size === 'number' && + file.size < this.options.minFileSize) { + return 'minFileSize'; + } + return null; + }, + + _validate: function (files) { + var that = this, + valid; + $.each(files, function (index, file) { + file.error = that._hasError(file); + valid = !file.error; + }); + return valid; + }, + + _uploadTemplateHelper: function (file) { + file.sizef = this._formatFileSize(file); + return file; + }, + + _renderUploadTemplate: function (files) { + var that = this; + return $.tmpl( + this.options.uploadTemplate, + $.map(files, function (file) { + return that._uploadTemplateHelper(file); + }) + ); + }, + + _renderUpload: function (files) { + var that = this, + options = this.options, + tmpl = this._renderUploadTemplate(files); + if (!(tmpl instanceof $)) { + return $(); + } + tmpl.css('display', 'none'); + // .slice(1).remove().end().first() removes all but the first + // element and selects only the first for the jQuery collection: + tmpl.find('.progress div').slice(1).remove().end().first() + .progressbar(); + tmpl.find('.start button').slice( + this.options.autoUpload ? 0 : 1 + ).remove().end().first() + .button({ + text: false, + icons: {primary: 'ui-icon-circle-arrow-e'} + }); + tmpl.find('.cancel button').slice(1).remove().end().first() + .button({ + text: false, + icons: {primary: 'ui-icon-cancel'} + }); + tmpl.find('.preview').each(function (index, node) { + that._loadImage( + files[index], + function (img) { + $(img).hide().appendTo(node).fadeIn(); + }, + { + maxWidth: options.previewMaxWidth, + maxHeight: options.previewMaxHeight, + fileTypes: options.previewFileTypes, + canvas: options.previewAsCanvas + } + ); + }); + return tmpl; + }, + + _downloadTemplateHelper: function (file) { + file.sizef = this._formatFileSize(file); + return file; + }, + + _renderDownloadTemplate: function (files) { + var that = this; + return $.tmpl( + this.options.downloadTemplate, + $.map(files, function (file) { + return that._downloadTemplateHelper(file); + }) + ); + }, + + _renderDownload: function (files) { + var tmpl = this._renderDownloadTemplate(files); + if (!(tmpl instanceof $)) { + return $(); + } + tmpl.css('display', 'none'); + tmpl.find('.delete button').button({ + text: false, + icons: {primary: 'ui-icon-trash'} + }); + tmpl.find('a').each(this._enableDragToDesktop); + return tmpl; + }, + + _startHandler: function (e) { + e.preventDefault(); + var tmpl = $(this).closest('.template-upload'), + data = tmpl.data('data'); + if (data && data.submit && !data.jqXHR) { + data.jqXHR = data.submit(); + $(this).fadeOut(); + } + }, + + _cancelHandler: function (e) { + e.preventDefault(); + var tmpl = $(this).closest('.template-upload'), + data = tmpl.data('data') || {}; + if (!data.jqXHR) { + data.errorThrown = 'abort'; + e.data.fileupload._trigger('fail', e, data); + } else { + data.jqXHR.abort(); + } + }, + + _deleteHandler: function (e) { + e.preventDefault(); + var button = $(this); + e.data.fileupload._trigger('destroy', e, { + context: button.closest('.template-download'), + url: button.attr('data-url'), + type: button.attr('data-type'), + dataType: e.data.fileupload.options.dataType + }); + }, + + _initEventHandlers: function () { + $.blueimp.fileupload.prototype._initEventHandlers.call(this); + var filesList = this.element.find('.files'), + eventData = {fileupload: this}; + filesList.find('.start button') + .live( + 'click.' + this.options.namespace, + eventData, + this._startHandler + ); + filesList.find('.cancel button') + .live( + 'click.' + this.options.namespace, + eventData, + this._cancelHandler + ); + filesList.find('.delete button') + .live( + 'click.' + this.options.namespace, + eventData, + this._deleteHandler + ); + }, + + _destroyEventHandlers: function () { + var filesList = this.element.find('.files'); + filesList.find('.start button') + .die('click.' + this.options.namespace); + filesList.find('.cancel button') + .die('click.' + this.options.namespace); + filesList.find('.delete button') + .die('click.' + this.options.namespace); + $.blueimp.fileupload.prototype._destroyEventHandlers.call(this); + }, + + _initFileUploadButtonBar: function () { + var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'), + filesList = this.element.find('.files'), + ns = this.options.namespace; + fileUploadButtonBar + .addClass('ui-widget-header ui-corner-top'); + this.element.find('.fileinput-button').each(function () { + var fileInput = $(this).find('input:file').detach(); + $(this).button({icons: {primary: 'ui-icon-plusthick'}}) + .append(fileInput); + }); + fileUploadButtonBar.find('.start') + .button({icons: {primary: 'ui-icon-circle-arrow-e'}}) + .bind('click.' + ns, function (e) { + e.preventDefault(); + filesList.find('.start button').click(); + }); + fileUploadButtonBar.find('.cancel') + .button({icons: {primary: 'ui-icon-cancel'}}) + .bind('click.' + ns, function (e) { + e.preventDefault(); + filesList.find('.cancel button').click(); + }); + fileUploadButtonBar.find('.delete') + .button({icons: {primary: 'ui-icon-trash'}}) + .bind('click.' + ns, function (e) { + e.preventDefault(); + filesList.find('.delete button').click(); + }); + }, + + _destroyFileUploadButtonBar: function () { + this.element.find('.fileupload-buttonbar') + .removeClass('ui-widget-header ui-corner-top'); + this.element.find('.fileinput-button').each(function () { + var fileInput = $(this).find('input:file').detach(); + $(this).button('destroy') + .append(fileInput); + }); + this.element.find('.fileupload-buttonbar button') + .unbind('click.' + this.options.namespace) + .button('destroy'); + }, + + _enableFileInputButton: function () { + this.element.find('.fileinput-button input:file:disabled') + .each(function () { + var fileInput = $(this), + button = fileInput.parent(); + fileInput.detach().prop('disabled', false); + button.button('enable').append(fileInput); + }); + }, + + _disableFileInputButton: function () { + this.element.find('.fileinput-button input:file:enabled') + .each(function () { + var fileInput = $(this), + button = fileInput.parent(); + fileInput.detach().prop('disabled', true); + button.button('disable').append(fileInput); + }); + }, + + _initTemplates: function () { + // Handle cases where the templates are defined + // after the widget library has been included: + if (this.options.uploadTemplate instanceof $ && + !this.options.uploadTemplate.length) { + this.options.uploadTemplate = $( + this.options.uploadTemplate.selector + ); + } + if (this.options.downloadTemplate instanceof $ && + !this.options.downloadTemplate.length) { + this.options.downloadTemplate = $( + this.options.downloadTemplate.selector + ); + } + }, + + _create: function () { + $.blueimp.fileupload.prototype._create.call(this); + this._initTemplates(); + this.element + .addClass('ui-widget'); + this._initFileUploadButtonBar(); + this.element.find('.fileupload-content') + .addClass('ui-widget-content ui-corner-bottom'); + this.element.find('.fileupload-progressbar') + .hide().progressbar(); + }, + + destroy: function () { + this.element.find('.fileupload-progressbar') + .progressbar('destroy'); + this.element.find('.fileupload-content') + .removeClass('ui-widget-content ui-corner-bottom'); + this._destroyFileUploadButtonBar(); + this.element.removeClass('ui-widget'); + $.blueimp.fileupload.prototype.destroy.call(this); + }, + + enable: function () { + $.blueimp.fileupload.prototype.enable.call(this); + this.element.find(':ui-button').not('.fileinput-button') + .button('enable'); + this._enableFileInputButton(); + }, + + disable: function () { + this.element.find(':ui-button').not('.fileinput-button') + .button('disable'); + this._disableFileInputButton(); + $.blueimp.fileupload.prototype.disable.call(this); + } + + }); + +}(jQuery)); \ No newline at end of file diff --git a/underlays/attachments/ikiwiki/jquery.fileupload.js b/underlays/attachments/ikiwiki/jquery.fileupload.js new file mode 100644 index 000000000..1e3c6bf97 --- /dev/null +++ b/underlays/attachments/ikiwiki/jquery.fileupload.js @@ -0,0 +1,720 @@ +/* + * jQuery File Upload Plugin 5.0.2 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://creativecommons.org/licenses/MIT/ + */ + +/*jslint nomen: true, unparam: true, regexp: true */ +/*global document, XMLHttpRequestUpload, Blob, File, FormData, location, jQuery */ + +(function ($) { + 'use strict'; + + // The fileupload widget listens for change events on file input fields + // defined via fileInput setting and drop events of the given dropZone. + // In addition to the default jQuery Widget methods, the fileupload widget + // exposes the "add" and "send" methods, to add or directly send files + // using the fileupload API. + // By default, files added via file input selection, drag & drop or + // "add" method are uploaded immediately, but it is possible to override + // the "add" callback option to queue file uploads. + $.widget('blueimp.fileupload', { + + options: { + // The namespace used for event handler binding on the dropZone and + // fileInput collections. + // If not set, the name of the widget ("fileupload") is used. + namespace: undefined, + // The drop target collection, by the default the complete document. + // Set to null or an empty collection to disable drag & drop support: + dropZone: $(document), + // The file input field collection, that is listened for change events. + // If undefined, it is set to the file input fields inside + // of the widget element on plugin initialization. + // Set to null or an empty collection to disable the change listener. + fileInput: undefined, + // By default, the file input field is replaced with a clone after + // each input field change event. This is required for iframe transport + // queues and allows change events to be fired for the same file + // selection, but can be disabled by setting the following option to false: + replaceFileInput: true, + // The parameter name for the file form data (the request argument name). + // If undefined or empty, the name property of the file input field is + // used, or "files[]" if the file input name property is also empty: + paramName: undefined, + // By default, each file of a selection is uploaded using an individual + // request for XHR type uploads. Set to false to upload file + // selections in one request each: + singleFileUploads: true, + // Set the following option to true to issue all file upload requests + // in a sequential order: + sequentialUploads: false, + // Set the following option to true to force iframe transport uploads: + forceIframeTransport: false, + // By default, XHR file uploads are sent as multipart/form-data. + // The iframe transport is always using multipart/form-data. + // Set to false to enable non-multipart XHR uploads: + multipart: true, + // To upload large files in smaller chunks, set the following option + // to a preferred maximum chunk size. If set to 0, null or undefined, + // or the browser does not support the required Blob API, files will + // be uploaded as a whole. + maxChunkSize: undefined, + // When a non-multipart upload or a chunked multipart upload has been + // aborted, this option can be used to resume the upload by setting + // it to the size of the already uploaded bytes. This option is most + // useful when modifying the options object inside of the "add" or + // "send" callbacks, as the options are cloned for each file upload. + uploadedBytes: undefined, + // By default, failed (abort or error) file uploads are removed from the + // global progress calculation. Set the following option to false to + // prevent recalculating the global progress data: + recalculateProgress: true, + + // Additional form data to be sent along with the file uploads can be set + // using this option, which accepts an array of objects with name and + // value properties, a function returning such an array, a FormData + // object (for XHR file uploads), or a simple object. + // The form of the first fileInput is given as parameter to the function: + formData: function (form) { + return form.serializeArray(); + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop or add API call). + // If the singleFileUploads option is enabled, this callback will be + // called once for each file in the selection for XHR file uplaods, else + // once for each file selection. + // The upload starts when the submit method is invoked on the data parameter. + // The data object contains a files property holding the added files + // and allows to override plugin options as well as define ajax settings. + // Listeners for this callback can also be bound the following way: + // .bind('fileuploadadd', func); + // data.submit() returns a Promise object and allows to attach additional + // handlers using jQuery's Deferred callbacks: + // data.submit().done(func).fail(func).always(func); + add: function (e, data) { + data.submit(); + }, + + // Other callbacks: + // Callback for the start of each file upload request: + // send: function (e, data) {}, // .bind('fileuploadsend', func); + // Callback for successful uploads: + // done: function (e, data) {}, // .bind('fileuploaddone', func); + // Callback for failed (abort or error) uploads: + // fail: function (e, data) {}, // .bind('fileuploadfail', func); + // Callback for completed (success, abort or error) requests: + // always: function (e, data) {}, // .bind('fileuploadalways', func); + // Callback for upload progress events: + // progress: function (e, data) {}, // .bind('fileuploadprogress', func); + // Callback for global upload progress events: + // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); + // Callback for uploads start, equivalent to the global ajaxStart event: + // start: function (e) {}, // .bind('fileuploadstart', func); + // Callback for uploads stop, equivalent to the global ajaxStop event: + // stop: function (e) {}, // .bind('fileuploadstop', func); + // Callback for change events of the fileInput collection: + // change: function (e, data) {}, // .bind('fileuploadchange', func); + // Callback for drop events of the dropZone collection: + // drop: function (e, data) {}, // .bind('fileuploaddrop', func); + // Callback for dragover events of the dropZone collection: + // dragover: function (e) {}, // .bind('fileuploaddragover', func); + + // The plugin options are used as settings object for the ajax calls. + // The following are jQuery ajax settings required for the file uploads: + processData: false, + contentType: false, + cache: false + }, + + // A list of options that require a refresh after assigning a new value: + _refreshOptionsList: ['namespace', 'dropZone', 'fileInput'], + + _isXHRUpload: function (options) { + var undef = 'undefined'; + return !options.forceIframeTransport && + typeof XMLHttpRequestUpload !== undef && typeof File !== undef && + (!options.multipart || typeof FormData !== undef); + }, + + _getFormData: function (options) { + var formData; + if (typeof options.formData === 'function') { + return options.formData(options.form); + } else if ($.isArray(options.formData)) { + return options.formData; + } else if (options.formData) { + formData = []; + $.each(options.formData, function (name, value) { + formData.push({name: name, value: value}); + }); + return formData; + } + return []; + }, + + _getTotal: function (files) { + var total = 0; + $.each(files, function (index, file) { + total += file.size || 1; + }); + return total; + }, + + _onProgress: function (e, data) { + if (e.lengthComputable) { + var total = data.total || this._getTotal(data.files), + loaded = parseInt( + e.loaded / e.total * (data.chunkSize || total), + 10 + ) + (data.uploadedBytes || 0); + this._loaded += loaded - (data.loaded || data.uploadedBytes || 0); + data.lengthComputable = true; + data.loaded = loaded; + data.total = total; + // Trigger a custom progress event with a total data property set + // to the file size(s) of the current upload and a loaded data + // property calculated accordingly: + this._trigger('progress', e, data); + // Trigger a global progress event for all current file uploads, + // including ajax calls queued for sequential file uploads: + this._trigger('progressall', e, { + lengthComputable: true, + loaded: this._loaded, + total: this._total + }); + } + }, + + _initProgressListener: function (options) { + var that = this, + xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); + // Accesss to the native XHR object is required to add event listeners + // for the upload progress event: + if (xhr.upload && xhr.upload.addEventListener) { + xhr.upload.addEventListener('progress', function (e) { + that._onProgress(e, options); + }, false); + options.xhr = function () { + return xhr; + }; + } + }, + + _initXHRData: function (options) { + var formData, + file = options.files[0]; + if (!options.multipart || options.blob) { + // For non-multipart uploads and chunked uploads, + // file meta data is not part of the request body, + // so we transmit this data as part of the HTTP headers. + // For cross domain requests, these headers must be allowed + // via Access-Control-Allow-Headers or removed using + // the beforeSend callback: + options.headers = $.extend(options.headers, { + 'X-File-Name': file.name, + 'X-File-Type': file.type, + 'X-File-Size': file.size + }); + if (!options.blob) { + // Non-chunked non-multipart upload: + options.contentType = file.type; + options.data = file; + } else if (!options.multipart) { + // Chunked non-multipart upload: + options.contentType = 'application/octet-stream'; + options.data = options.blob; + } + } + if (options.multipart && typeof FormData !== 'undefined') { + if (options.formData instanceof FormData) { + formData = options.formData; + } else { + formData = new FormData(); + $.each(this._getFormData(options), function (index, field) { + formData.append(field.name, field.value); + }); + } + if (options.blob) { + formData.append(options.paramName, options.blob); + } else { + $.each(options.files, function (index, file) { + // File objects are also Blob instances. + // This check allows the tests to run with + // dummy objects: + if (file instanceof Blob) { + formData.append(options.paramName, file); + } + }); + } + options.data = formData; + } + // Blob reference is not needed anymore, free memory: + options.blob = null; + }, + + _initIframeSettings: function (options) { + // Setting the dataType to iframe enables the iframe transport: + options.dataType = 'iframe ' + (options.dataType || ''); + // The iframe transport accepts a serialized array as form data: + options.formData = this._getFormData(options); + }, + + _initDataSettings: function (options) { + if (this._isXHRUpload(options)) { + if (!this._chunkedUpload(options, true)) { + if (!options.data) { + this._initXHRData(options); + } + this._initProgressListener(options); + } + } else { + this._initIframeSettings(options); + } + }, + + _initFormSettings: function (options) { + // Retrieve missing options from the input field and the + // associated form, if available: + if (!options.form || !options.form.length) { + options.form = $(options.fileInput.prop('form')); + } + if (!options.paramName) { + options.paramName = options.fileInput.prop('name') || + 'files[]'; + } + if (!options.url) { + options.url = options.form.prop('action') || location.href; + } + // The HTTP request method must be "POST" or "PUT": + options.type = (options.type || options.form.prop('method') || '') + .toUpperCase(); + if (options.type !== 'POST' && options.type !== 'PUT') { + options.type = 'POST'; + } + }, + + _getAJAXSettings: function (data) { + var options = $.extend({}, this.options, data); + this._initFormSettings(options); + this._initDataSettings(options); + return options; + }, + + // Maps jqXHR callbacks to the equivalent + // methods of the given Promise object: + _enhancePromise: function (promise) { + promise.success = promise.done; + promise.error = promise.fail; + promise.complete = promise.always; + return promise; + }, + + // Creates and returns a Promise object enhanced with + // the jqXHR methods abort, success, error and complete: + _getXHRPromise: function (resolveOrReject, context, args) { + var dfd = $.Deferred(), + promise = dfd.promise(); + context = context || this.options.context || promise; + if (resolveOrReject === true) { + dfd.resolveWith(context, args); + } else if (resolveOrReject === false) { + dfd.rejectWith(context, args); + } + promise.abort = dfd.promise; + return this._enhancePromise(promise); + }, + + // Uploads a file in multiple, sequential requests + // by splitting the file up in multiple blob chunks. + // If the second parameter is true, only tests if the file + // should be uploaded in chunks, but does not invoke any + // upload requests: + _chunkedUpload: function (options, testOnly) { + var that = this, + file = options.files[0], + fs = file.size, + ub = options.uploadedBytes = options.uploadedBytes || 0, + mcs = options.maxChunkSize || fs, + // Use the Blob methods with the slice implementation + // according to the W3C Blob API specification: + slice = file.webkitSlice || file.mozSlice || file.slice, + upload, + n, + jqXHR, + pipe; + if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || + options.data) { + return false; + } + if (testOnly) { + return true; + } + if (ub >= fs) { + file.error = 'uploadedBytes'; + return this._getXHRPromise(false); + } + // n is the number of blobs to upload, + // calculated via filesize, uploaded bytes and max chunk size: + n = Math.ceil((fs - ub) / mcs); + // The chunk upload method accepting the chunk number as parameter: + upload = function (i) { + if (!i) { + return that._getXHRPromise(true); + } + // Upload the blobs in sequential order: + return upload(i -= 1).pipe(function () { + // Clone the options object for each chunk upload: + var o = $.extend({}, options); + o.blob = slice.call( + file, + ub + i * mcs, + ub + (i + 1) * mcs + ); + // Store the current chunk size, as the blob itself + // will be dereferenced after data processing: + o.chunkSize = o.blob.size; + // Process the upload data (the blob and potential form data): + that._initXHRData(o); + // Add progress listeners for this chunk upload: + that._initProgressListener(o); + jqXHR = ($.ajax(o) || that._getXHRPromise(false, o.context)) + .done(function () { + // Create a progress event if upload is done and + // no progress event has been invoked for this chunk: + if (!o.loaded) { + that._onProgress($.Event('progress', { + lengthComputable: true, + loaded: o.chunkSize, + total: o.chunkSize + }), o); + } + options.uploadedBytes = o.uploadedBytes + += o.chunkSize; + }); + return jqXHR; + }); + }; + // Return the piped Promise object, enhanced with an abort method, + // which is delegated to the jqXHR object of the current upload, + // and jqXHR callbacks mapped to the equivalent Promise methods: + pipe = upload(n); + pipe.abort = function () { + return jqXHR.abort(); + }; + return this._enhancePromise(pipe); + }, + + _beforeSend: function (e, data) { + if (this._active === 0) { + // the start callback is triggered when an upload starts + // and no other uploads are currently running, + // equivalent to the global ajaxStart event: + this._trigger('start'); + } + this._active += 1; + // Initialize the global progress values: + this._loaded += data.uploadedBytes || 0; + this._total += this._getTotal(data.files); + }, + + _onDone: function (result, textStatus, jqXHR, options) { + if (!this._isXHRUpload(options)) { + // Create a progress event for each iframe load: + this._onProgress($.Event('progress', { + lengthComputable: true, + loaded: 1, + total: 1 + }), options); + } + options.result = result; + options.textStatus = textStatus; + options.jqXHR = jqXHR; + this._trigger('done', null, options); + }, + + _onFail: function (jqXHR, textStatus, errorThrown, options) { + options.jqXHR = jqXHR; + options.textStatus = textStatus; + options.errorThrown = errorThrown; + this._trigger('fail', null, options); + if (options.recalculateProgress) { + // Remove the failed (error or abort) file upload from + // the global progress calculation: + this._loaded -= options.loaded || options.uploadedBytes || 0; + this._total -= options.total || this._getTotal(options.files); + } + }, + + _onAlways: function (result, textStatus, jqXHR, errorThrown, options) { + this._active -= 1; + options.result = result; + options.textStatus = textStatus; + options.jqXHR = jqXHR; + options.errorThrown = errorThrown; + this._trigger('always', null, options); + if (this._active === 0) { + // The stop callback is triggered when all uploads have + // been completed, equivalent to the global ajaxStop event: + this._trigger('stop'); + // Reset the global progress values: + this._loaded = this._total = 0; + } + }, + + _onSend: function (e, data) { + var that = this, + jqXHR, + pipe, + options = that._getAJAXSettings(data), + send = function (resolve, args) { + jqXHR = jqXHR || ( + (resolve !== false && + that._trigger('send', e, options) !== false && + (that._chunkedUpload(options) || $.ajax(options))) || + that._getXHRPromise(false, options.context, args) + ).done(function (result, textStatus, jqXHR) { + that._onDone(result, textStatus, jqXHR, options); + }).fail(function (jqXHR, textStatus, errorThrown) { + that._onFail(jqXHR, textStatus, errorThrown, options); + }).always(function (a1, a2, a3) { + if (!a3 || typeof a3 === 'string') { + that._onAlways(undefined, a2, a1, a3, options); + } else { + that._onAlways(a1, a2, a3, undefined, options); + } + }); + return jqXHR; + }; + this._beforeSend(e, options); + if (this.options.sequentialUploads) { + // Return the piped Promise object, enhanced with an abort method, + // which is delegated to the jqXHR object of the current upload, + // and jqXHR callbacks mapped to the equivalent Promise methods: + pipe = (this._sequence = this._sequence.pipe(send, send)); + pipe.abort = function () { + if (!jqXHR) { + return send(false, [undefined, 'abort', 'abort']); + } + return jqXHR.abort(); + }; + return this._enhancePromise(pipe); + } + return send(); + }, + + _onAdd: function (e, data) { + var that = this, + result = true, + options = $.extend({}, this.options, data); + if (options.singleFileUploads && this._isXHRUpload(options)) { + $.each(data.files, function (index, file) { + var newData = $.extend({}, data, {files: [file]}); + newData.submit = function () { + return that._onSend(e, newData); + }; + return (result = that._trigger('add', e, newData)); + }); + return result; + } else if (data.files.length) { + data = $.extend({}, data); + data.submit = function () { + return that._onSend(e, data); + }; + return this._trigger('add', e, data); + } + }, + + // File Normalization for Gecko 1.9.1 (Firefox 3.5) support: + _normalizeFile: function (index, file) { + if (file.name === undefined && file.size === undefined) { + file.name = file.fileName; + file.size = file.fileSize; + } + }, + + _replaceFileInput: function (input) { + var inputClone = input.clone(true); + $('
').append(inputClone)[0].reset(); + // Detaching allows to insert the fileInput on another form + // without loosing the file input value: + input.after(inputClone).detach(); + // Replace the original file input element in the fileInput + // collection with the clone, which has been copied including + // event handlers: + this.options.fileInput = this.options.fileInput.map(function (i, el) { + if (el === input[0]) { + return inputClone[0]; + } + return el; + }); + }, + + _onChange: function (e) { + var that = e.data.fileupload, + data = { + files: $.each($.makeArray(e.target.files), that._normalizeFile), + fileInput: $(e.target), + form: $(e.target.form) + }; + if (!data.files.length) { + // If the files property is not available, the browser does not + // support the File API and we add a pseudo File object with + // the input value as name with path information removed: + data.files = [{name: e.target.value.replace(/^.*\\/, '')}]; + } + // Store the form reference as jQuery data for other event handlers, + // as the form property is not available after replacing the file input: + if (data.form.length) { + data.fileInput.data('blueimp.fileupload.form', data.form); + } else { + data.form = data.fileInput.data('blueimp.fileupload.form'); + } + if (that.options.replaceFileInput) { + that._replaceFileInput(data.fileInput); + } + if (that._trigger('change', e, data) === false || + that._onAdd(e, data) === false) { + return false; + } + }, + + _onDrop: function (e) { + var that = e.data.fileupload, + dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer, + data = { + files: $.each( + $.makeArray(dataTransfer && dataTransfer.files), + that._normalizeFile + ) + }; + if (that._trigger('drop', e, data) === false || + that._onAdd(e, data) === false) { + return false; + } + e.preventDefault(); + }, + + _onDragOver: function (e) { + var that = e.data.fileupload, + dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer; + if (that._trigger('dragover', e) === false) { + return false; + } + if (dataTransfer) { + dataTransfer.dropEffect = dataTransfer.effectAllowed = 'copy'; + } + e.preventDefault(); + }, + + _initEventHandlers: function () { + var ns = this.options.namespace || this.name; + this.options.dropZone + .bind('dragover.' + ns, {fileupload: this}, this._onDragOver) + .bind('drop.' + ns, {fileupload: this}, this._onDrop); + this.options.fileInput + .bind('change.' + ns, {fileupload: this}, this._onChange); + }, + + _destroyEventHandlers: function () { + var ns = this.options.namespace || this.name; + this.options.dropZone + .unbind('dragover.' + ns, this._onDragOver) + .unbind('drop.' + ns, this._onDrop); + this.options.fileInput + .unbind('change.' + ns, this._onChange); + }, + + _beforeSetOption: function (key, value) { + this._destroyEventHandlers(); + }, + + _afterSetOption: function (key, value) { + var options = this.options; + if (!options.fileInput) { + options.fileInput = $(); + } + if (!options.dropZone) { + options.dropZone = $(); + } + this._initEventHandlers(); + }, + + _setOption: function (key, value) { + var refresh = $.inArray(key, this._refreshOptionsList) !== -1; + if (refresh) { + this._beforeSetOption(key, value); + } + $.Widget.prototype._setOption.call(this, key, value); + if (refresh) { + this._afterSetOption(key, value); + } + }, + + _create: function () { + var options = this.options; + if (options.fileInput === undefined) { + options.fileInput = this.element.is('input:file') ? + this.element : this.element.find('input:file'); + } else if (!options.fileInput) { + options.fileInput = $(); + } + if (!options.dropZone) { + options.dropZone = $(); + } + this._sequence = this._getXHRPromise(true); + this._active = this._loaded = this._total = 0; + this._initEventHandlers(); + }, + + destroy: function () { + this._destroyEventHandlers(); + $.Widget.prototype.destroy.call(this); + }, + + enable: function () { + $.Widget.prototype.enable.call(this); + this._initEventHandlers(); + }, + + disable: function () { + this._destroyEventHandlers(); + $.Widget.prototype.disable.call(this); + }, + + // This method is exposed to the widget API and allows adding files + // using the fileupload API. The data parameter accepts an object which + // must have a files property and can contain additional options: + // .fileupload('add', {files: filesList}); + add: function (data) { + if (!data || this.options.disabled) { + return; + } + data.files = $.each($.makeArray(data.files), this._normalizeFile); + this._onAdd(null, data); + }, + + // This method is exposed to the widget API and allows sending files + // using the fileupload API. The data parameter accepts an object which + // must have a files property and can contain additional options: + // .fileupload('send', {files: filesList}); + // The method returns a Promise object for the file upload call. + send: function (data) { + if (data && !this.options.disabled) { + data.files = $.each($.makeArray(data.files), this._normalizeFile); + if (data.files.length) { + return this._onSend(null, data); + } + } + return this._getXHRPromise(false, data && data.context); + } + + }); + +}(jQuery)); \ No newline at end of file diff --git a/underlays/attachments/ikiwiki/jquery.iframe-transport.js b/underlays/attachments/ikiwiki/jquery.iframe-transport.js new file mode 100644 index 000000000..e859dfe49 --- /dev/null +++ b/underlays/attachments/ikiwiki/jquery.iframe-transport.js @@ -0,0 +1,133 @@ +/* + * jQuery Iframe Transport Plugin 1.1 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://creativecommons.org/licenses/MIT/ + */ + +/*jslint unparam: true */ +/*global jQuery */ + +(function ($) { + 'use strict'; + + // Helper variable to create unique names for the transport iframes: + var counter = 0; + + // The iframe transport accepts two additional options: + // options.fileInput: a jQuery collection of file input fields + // options.formData: an array of objects with name and value properties, + // equivalent to the return data of .serializeArray(), e.g.: + // [{name: a, value: 1}, {name: b, value: 2}] + $.ajaxTransport('iframe', function (options, originalOptions, jqXHR) { + if (options.type === 'POST' || options.type === 'GET') { + var form, + iframe; + return { + send: function (headers, completeCallback) { + form = $('
'); + // javascript:false as initial iframe src + // prevents warning popups on HTTPS in IE6. + // IE versions below IE8 cannot set the name property of + // elements that have already been added to the DOM, + // so we set the name along with the iframe HTML markup: + iframe = $( + '' + ).bind('load', function () { + var fileInputClones; + iframe + .unbind('load') + .bind('load', function () { + // The complete callback returns the + // iframe content document as response object: + completeCallback( + 200, + 'success', + {'iframe': iframe.contents()} + ); + // Fix for IE endless progress bar activity bug + // (happens on form submits to iframe targets): + $('') + .appendTo(form); + form.remove(); + }); + form + .prop('target', iframe.prop('name')) + .prop('action', options.url) + .prop('method', options.type); + if (options.formData) { + $.each(options.formData, function (index, field) { + $('') + .prop('name', field.name) + .val(field.value) + .appendTo(form); + }); + } + if (options.fileInput && options.fileInput.length && + options.type === 'POST') { + fileInputClones = options.fileInput.clone(); + // Insert a clone for each file input field: + options.fileInput.after(function (index) { + return fileInputClones[index]; + }); + // Appending the file input fields to the hidden form + // removes them from their original location: + form + .append(options.fileInput) + .prop('enctype', 'multipart/form-data') + // enctype must be set as encoding for IE: + .prop('encoding', 'multipart/form-data'); + } + form.submit(); + // Insert the file input fields at their original location + // by replacing the clones with the originals: + if (fileInputClones && fileInputClones.length) { + options.fileInput.each(function (index, input) { + $(fileInputClones[index]).replaceWith(input); + }); + } + }); + form.append(iframe).appendTo('body'); + }, + abort: function () { + if (iframe) { + // javascript:false as iframe src aborts the request + // and prevents warning popups on HTTPS in IE6. + // concat is used to avoid the "Script URL" JSLint error: + iframe + .unbind('load') + .prop('src', 'javascript'.concat(':false;')); + } + if (form) { + form.remove(); + } + } + }; + } + }); + + // The iframe transport returns the iframe content document as response. + // The following adds converters from iframe to text, json, html, and script: + $.ajaxSetup({ + converters: { + 'iframe text': function (iframe) { + return iframe.text(); + }, + 'iframe json': function (iframe) { + return $.parseJSON(iframe.text()); + }, + 'iframe html': function (iframe) { + return iframe.find('body').html(); + }, + 'iframe script': function (iframe) { + return $.globalEval(iframe.text()); + } + } + }); + +}(jQuery)); \ No newline at end of file diff --git a/underlays/attachments/ikiwiki/pbar-ani.gif b/underlays/attachments/ikiwiki/pbar-ani.gif new file mode 100644 index 0000000000000000000000000000000000000000..fbcce6bc9abfcc7893e65ef20b3e77ee16ec37b1 GIT binary patch literal 3323 zcmcK7eNYp38VB%=tbvu|O{=v;j+Z$Bg`>zI2IVD3Ovp-%sA$2LilR;|zMv9BB_t%- zylmcIh`dQLinJh9P{0Ja6I28#3PquysCf3GB1bBEXgfV_qnx8-n07k#kNmNdnVrvm zpZz`0v#~Ih7`{0em<2RA0wa|vr`!U1uI~v zF&O+zyfdOBoHdq{xOf7$)mN>lwcHgLVd-HT}JxYe94Gv(X_H__aHV~b7Ud4Pvk@t?84W?#& z+#WmNc5cUNoapyda#-P`2+n z-sGAbb$_$TW887}6BiJKaV$E-69{23pNeG2=_;8-CM9c*OiHOp|F9Igl_JQHqf)Y3 zhR7s(mQBh5ER~A&6^|p|OQqmd+(8 zZCb1qD*K}>!|B|YVuCsS=V0!N=?-)po(^$x*kptYX9N)B^~VJ^d7-@(M1WK;9&Liw~wO?NN(E}be} z`e0?=)*y}@Xkgx6S$ZS(8d|ifzK_`x?=N$)pWk;s0Ve?`qDX{%L@X0)6guz8Adh+qh(mq90aE=iAZp(dGccGHgmlkOke19p-dfci z@1Ni#x_B;&@l8icy*4roEXN?9&vh-w;n!Fp3;~Dn4#aOVi22tbo-KY@B1*TAgMsta zMh~68EXdvgJe$<9yOgn~E8aiQNo1$l`FUCI52J)nHW8+@|McSTe{t30@NpO^cv#waY+68ew4oT={#AGoA?Je^k$f`&u}bW zOjoRA_$utq5rspZ;5HXZvqE1#D) z5*M}9jPoq=-V*<cQuuK%yr=$XZl2k``40P4ol60hjb({{!c5Rtn3Urk4?uRRFNQuFGPhZnTBMO zu{W&|u4t53w>M}v(wtDAV!oHP(QY&~_X0qupII*-&GksTPf#SZ#4eaOCrG~hUblo4 zya(R){3rfyxiH2%l{iN;xS(G)OWOx8>UV9u+Vm_iN98oT20F}U!8utl4(lm=E)S6) zN~xM^Q?U;_!Ku4seH{bquy+4OfwlV{0C?tW&XteqJmP<|V)6eK9ey(~1Sbd1rgF1{ zd@&*xkriY}MS><}4DI}o3>d2jm74@fNpg-Vf(zPY?8nj=O8o$gp_gzr3~{MEs({T( z5+EcMIErR!`wwLZHp{c_?{mEhe%dlbNH8E^nm+5|owEXTR;S2+hYWR^j zWB!qbQWumI=vg;ZQvO$8W9QeMX~YdqNb^K%y~T6@tCTOlSM-j*$OOSSZX6cRfzPb* zP-Q}8v|6Q&$hAFfGA(f5>_XzuqhFP@h(0x@%#tsExSB}v(zy6^qc>xW*XCVMBPKf` zF>gsuD~+eKuRP{tWLo#OoJ2?fR@_0f5Olf}}Khc$ZhB$$3I7S9&n z^G{G0pQnju2Md}?zmbr Date: Wed, 15 Jun 2011 18:45:32 -0400 Subject: [PATCH 18/31] deleted a lot of code ikiwiki does not need removed most of the css, going for standard plain ikiwiki look Removed support for image previews, file size limits, delete buttons, maximum number of files, file size display. Ikiwiki handles all that. Turned on autoupload. --- .../ikiwiki/jquery.fileupload-ui.css | 92 ------ .../ikiwiki/jquery.fileupload-ui.js | 291 +----------------- 2 files changed, 3 insertions(+), 380 deletions(-) diff --git a/underlays/attachments/ikiwiki/jquery.fileupload-ui.css b/underlays/attachments/ikiwiki/jquery.fileupload-ui.css index 48224023f..d792724b7 100644 --- a/underlays/attachments/ikiwiki/jquery.fileupload-ui.css +++ b/underlays/attachments/ikiwiki/jquery.fileupload-ui.css @@ -1,57 +1,3 @@ -@charset 'UTF-8'; -/* - * jQuery File Upload UI Plugin CSS 5.0.6 - * https://github.com/blueimp/jQuery-File-Upload - * - * Copyright 2010, Sebastian Tschan - * https://blueimp.net - * - * Licensed under the MIT license: - * http://creativecommons.org/licenses/MIT/ - */ - -.fileupload-buttonbar .ui-button input { - position: absolute; - top: 0; - right: 0; - margin: 0; - border: solid transparent; - border-width: 0 0 100px 200px; - opacity: 0; - filter: alpha(opacity=0); - -o-transform: translate(250px, -50px) scale(1); - -moz-transform: translate(-300px, 0) scale(4); - direction: ltr; - cursor: pointer; -} - -.fileinput-button { - overflow: hidden; -} - -/* Fix for IE 6: */ -*html .fileinput-button { - padding: 2px 0; -} - -/* Fix for IE 7: */ -*+html .fileinput-button { - padding: 2px 0; -} - -.fileupload-buttonbar { - padding: 0.2em 0.4em; -} - -.fileupload-buttonbar .ui-button { - vertical-align: middle; -} - -.fileupload-content { - padding: 0.2em 0.4em; - border-top-width: 0; -} - .fileupload-content .ui-progressbar { width: 200px; height: 20px; @@ -60,41 +6,3 @@ .fileupload-content .ui-progressbar-value { background: url(pbar-ani.gif); } - -.fileupload-content .fileupload-progressbar { - width: 400px; - margin: 10px 0; -} - -.files { - margin: 10px 0; - border-collapse: collapse; -} - -.files td { - padding: 5px; - border-spacing: 5px; -} - -.files img { - border: none; -} - -.files .name { - padding: 0 10px; -} - -.files .size { - padding: 0 10px 0 0; - text-align: right; - white-space: nowrap; -} - -.ui-state-disabled .ui-state-disabled { - opacity: 1; - filter: alpha(opacity=100); -} - -.ui-state-disabled input { - cursor: default; -} \ No newline at end of file diff --git a/underlays/attachments/ikiwiki/jquery.fileupload-ui.js b/underlays/attachments/ikiwiki/jquery.fileupload-ui.js index 46b78e299..61897ba55 100644 --- a/underlays/attachments/ikiwiki/jquery.fileupload-ui.js +++ b/underlays/attachments/ikiwiki/jquery.fileupload-ui.js @@ -24,28 +24,7 @@ // By default, files added to the widget are uploaded as soon // as the user clicks on the start buttons. To enable automatic // uploads, set the following option to true: - autoUpload: false, - // The following option limits the number of files that are - // allowed to be uploaded using this widget: - maxNumberOfFiles: undefined, - // The maximum allowed file size: - maxFileSize: undefined, - // The minimum allowed file size: - minFileSize: 1, - // The regular expression for allowed file types, matches - // against either file type or file name: - acceptFileTypes: /.+$/i, - // The regular expression to define for which files a preview - // image is shown, matched against the file type: - previewFileTypes: /^image\/(gif|jpeg|png)$/, - // The maximum width of the preview images: - previewMaxWidth: 80, - // The maximum height of the preview images: - previewMaxHeight: 80, - // By default, preview images are displayed as canvas elements - // if supported by the browser. Set the following option to false - // to always display preview images as img elements: - previewAsCanvas: true, + autoUpload: true, // The file upload template that is given as first argument to the // jQuery.tmpl method to render the file uploads: uploadTemplate: $('#template-upload'), @@ -61,7 +40,6 @@ // See the basic file upload widget for more information: add: function (e, data) { var that = $(this).data('fileupload'); - that._adjustMaxNumberOfFiles(-data.files.length); data.isAdjusted = true; data.isValidated = that._validate(data.files); data.context = that._renderUpload(data.files) @@ -78,9 +56,6 @@ send: function (e, data) { if (!data.isValidated) { var that = $(this).data('fileupload'); - if (!data.isAdjusted) { - that._adjustMaxNumberOfFiles(-data.files.length); - } if (!that._validate(data.files)) { return false; } @@ -103,9 +78,6 @@ data.context.each(function (index) { var file = ($.isArray(data.result) && data.result[index]) || {error: 'emptyResult'}; - if (file.error) { - that._adjustMaxNumberOfFiles(1); - } $(this).fadeOut(function () { that._renderDownload([file]) .css('display', 'none') @@ -129,7 +101,6 @@ // Callback for failed (abort or error) uploads: fail: function (e, data) { var that = $(this).data('fileupload'); - that._adjustMaxNumberOfFiles(data.files.length); if (data.context) { data.context.each(function (index) { $(this).fadeOut(function () { @@ -150,7 +121,6 @@ }); }); } else if (data.errorThrown !== 'abort') { - that._adjustMaxNumberOfFiles(-data.files.length); data.context = that._renderUpload(data.files) .css('display', 'none') .appendTo($(this).find('.files')) @@ -185,54 +155,8 @@ stop: function () { $(this).find('.fileupload-progressbar').fadeOut(); }, - // Callback for file deletion: - destroy: function (e, data) { - var that = $(this).data('fileupload'); - if (data.url) { - $.ajax(data) - .success(function () { - that._adjustMaxNumberOfFiles(1); - $(this).fadeOut(function () { - $(this).remove(); - }); - }); - } else { - data.context.fadeOut(function () { - $(this).remove(); - }); - } - } }, - - // Scales the given image (img HTML element) - // using the given options. - // Returns a canvas object if the canvas option is true - // and the browser supports canvas, else the scaled image: - _scaleImage: function (img, options) { - options = options || {}; - var canvas = document.createElement('canvas'), - scale = Math.min( - (options.maxWidth || img.width) / img.width, - (options.maxHeight || img.height) / img.height - ); - if (scale >= 1) { - scale = Math.max( - (options.minWidth || img.width) / img.width, - (options.minHeight || img.height) / img.height - ); - } - img.width = parseInt(img.width * scale, 10); - img.height = parseInt(img.height * scale, 10); - if (!options.canvas || !canvas.getContext) { - return img; - } - canvas.width = img.width; - canvas.height = img.height; - canvas.getContext('2d') - .drawImage(img, 0, 0, img.width, img.height); - return canvas; - }, - + _createObjectURL: function (file) { var undef = 'undefined', urlAPI = (typeof window.createObjectURL !== undef && window) || @@ -249,44 +173,6 @@ return urlAPI ? urlAPI.revokeObjectURL(url) : false; }, - // Loads a given File object via FileReader interface, - // invokes the callback with a data url: - _loadFile: function (file, callback) { - if (typeof FileReader !== 'undefined' && - FileReader.prototype.readAsDataURL) { - var fileReader = new FileReader(); - fileReader.onload = function (e) { - callback(e.target.result); - }; - fileReader.readAsDataURL(file); - return true; - } - return false; - }, - - // Loads an image for a given File object. - // Invokes the callback with an img or optional canvas - // element (if supported by the browser) as parameter: - _loadImage: function (file, callback, options) { - var that = this, - url, - img; - if (!options || !options.fileTypes || - options.fileTypes.test(file.type)) { - url = this._createObjectURL(file); - img = $('').bind('load', function () { - $(this).unbind('load'); - that._revokeObjectURL(url); - callback(that._scaleImage(img[0], options)); - }).prop('src', url); - if (!url) { - this._loadFile(file, function (url) { - img.prop('src', url); - }); - } - } - }, - // Link handler, that allows to download files // by drag & drop of the links to the desktop: _enableDragToDesktop: function () { @@ -305,55 +191,10 @@ }); }, - _adjustMaxNumberOfFiles: function (operand) { - if (typeof this.options.maxNumberOfFiles === 'number') { - this.options.maxNumberOfFiles += operand; - if (this.options.maxNumberOfFiles < 1) { - this._disableFileInputButton(); - } else { - this._enableFileInputButton(); - } - } - }, - - _formatFileSize: function (file) { - if (typeof file.size !== 'number') { - return ''; - } - if (file.size >= 1000000000) { - return (file.size / 1000000000).toFixed(2) + ' GB'; - } - if (file.size >= 1000000) { - return (file.size / 1000000).toFixed(2) + ' MB'; - } - return (file.size / 1000).toFixed(2) + ' KB'; - }, - _hasError: function (file) { if (file.error) { return file.error; } - // The number of added files is subtracted from - // maxNumberOfFiles before validation, so we check if - // maxNumberOfFiles is below 0 (instead of below 1): - if (this.options.maxNumberOfFiles < 0) { - return 'maxNumberOfFiles'; - } - // Files are accepted if either the file type or the file name - // matches against the acceptFileTypes regular expression, as - // only browsers with support for the File API report the type: - if (!(this.options.acceptFileTypes.test(file.type) || - this.options.acceptFileTypes.test(file.name))) { - return 'acceptFileTypes'; - } - if (this.options.maxFileSize && - file.size > this.options.maxFileSize) { - return 'maxFileSize'; - } - if (typeof file.size === 'number' && - file.size < this.options.minFileSize) { - return 'minFileSize'; - } return null; }, @@ -368,7 +209,6 @@ }, _uploadTemplateHelper: function (file) { - file.sizef = this._formatFileSize(file); return file; }, @@ -406,25 +246,10 @@ text: false, icons: {primary: 'ui-icon-cancel'} }); - tmpl.find('.preview').each(function (index, node) { - that._loadImage( - files[index], - function (img) { - $(img).hide().appendTo(node).fadeIn(); - }, - { - maxWidth: options.previewMaxWidth, - maxHeight: options.previewMaxHeight, - fileTypes: options.previewFileTypes, - canvas: options.previewAsCanvas - } - ); - }); return tmpl; }, _downloadTemplateHelper: function (file) { - file.sizef = this._formatFileSize(file); return file; }, @@ -444,10 +269,6 @@ return $(); } tmpl.css('display', 'none'); - tmpl.find('.delete button').button({ - text: false, - icons: {primary: 'ui-icon-trash'} - }); tmpl.find('a').each(this._enableDragToDesktop); return tmpl; }, @@ -474,17 +295,6 @@ } }, - _deleteHandler: function (e) { - e.preventDefault(); - var button = $(this); - e.data.fileupload._trigger('destroy', e, { - context: button.closest('.template-download'), - url: button.attr('data-url'), - type: button.attr('data-type'), - dataType: e.data.fileupload.options.dataType - }); - }, - _initEventHandlers: function () { $.blueimp.fileupload.prototype._initEventHandlers.call(this); var filesList = this.element.find('.files'), @@ -501,12 +311,6 @@ eventData, this._cancelHandler ); - filesList.find('.delete button') - .live( - 'click.' + this.options.namespace, - eventData, - this._deleteHandler - ); }, _destroyEventHandlers: function () { @@ -515,75 +319,9 @@ .die('click.' + this.options.namespace); filesList.find('.cancel button') .die('click.' + this.options.namespace); - filesList.find('.delete button') - .die('click.' + this.options.namespace); $.blueimp.fileupload.prototype._destroyEventHandlers.call(this); }, - _initFileUploadButtonBar: function () { - var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'), - filesList = this.element.find('.files'), - ns = this.options.namespace; - fileUploadButtonBar - .addClass('ui-widget-header ui-corner-top'); - this.element.find('.fileinput-button').each(function () { - var fileInput = $(this).find('input:file').detach(); - $(this).button({icons: {primary: 'ui-icon-plusthick'}}) - .append(fileInput); - }); - fileUploadButtonBar.find('.start') - .button({icons: {primary: 'ui-icon-circle-arrow-e'}}) - .bind('click.' + ns, function (e) { - e.preventDefault(); - filesList.find('.start button').click(); - }); - fileUploadButtonBar.find('.cancel') - .button({icons: {primary: 'ui-icon-cancel'}}) - .bind('click.' + ns, function (e) { - e.preventDefault(); - filesList.find('.cancel button').click(); - }); - fileUploadButtonBar.find('.delete') - .button({icons: {primary: 'ui-icon-trash'}}) - .bind('click.' + ns, function (e) { - e.preventDefault(); - filesList.find('.delete button').click(); - }); - }, - - _destroyFileUploadButtonBar: function () { - this.element.find('.fileupload-buttonbar') - .removeClass('ui-widget-header ui-corner-top'); - this.element.find('.fileinput-button').each(function () { - var fileInput = $(this).find('input:file').detach(); - $(this).button('destroy') - .append(fileInput); - }); - this.element.find('.fileupload-buttonbar button') - .unbind('click.' + this.options.namespace) - .button('destroy'); - }, - - _enableFileInputButton: function () { - this.element.find('.fileinput-button input:file:disabled') - .each(function () { - var fileInput = $(this), - button = fileInput.parent(); - fileInput.detach().prop('disabled', false); - button.button('enable').append(fileInput); - }); - }, - - _disableFileInputButton: function () { - this.element.find('.fileinput-button input:file:enabled') - .each(function () { - var fileInput = $(this), - button = fileInput.parent(); - fileInput.detach().prop('disabled', true); - button.button('disable').append(fileInput); - }); - }, - _initTemplates: function () { // Handle cases where the templates are defined // after the widget library has been included: @@ -604,39 +342,16 @@ _create: function () { $.blueimp.fileupload.prototype._create.call(this); this._initTemplates(); - this.element - .addClass('ui-widget'); - this._initFileUploadButtonBar(); - this.element.find('.fileupload-content') - .addClass('ui-widget-content ui-corner-bottom'); - this.element.find('.fileupload-progressbar') - .hide().progressbar(); - }, - - destroy: function () { - this.element.find('.fileupload-progressbar') - .progressbar('destroy'); - this.element.find('.fileupload-content') - .removeClass('ui-widget-content ui-corner-bottom'); - this._destroyFileUploadButtonBar(); - this.element.removeClass('ui-widget'); - $.blueimp.fileupload.prototype.destroy.call(this); }, enable: function () { $.blueimp.fileupload.prototype.enable.call(this); - this.element.find(':ui-button').not('.fileinput-button') - .button('enable'); - this._enableFileInputButton(); }, disable: function () { - this.element.find(':ui-button').not('.fileinput-button') - .button('disable'); - this._disableFileInputButton(); $.blueimp.fileupload.prototype.disable.call(this); } }); -}(jQuery)); \ No newline at end of file +}(jQuery)); From 3a939f05c509ef57740d6524c785667e36ccff24 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Wed, 15 Jun 2011 18:56:36 -0400 Subject: [PATCH 19/31] update copyright --- debian/copyright | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/copyright b/debian/copyright index 82c72d734..16e0b3583 100644 --- a/debian/copyright +++ b/debian/copyright @@ -207,6 +207,13 @@ Files: underlays/openid-selector/ikiwiki/openid/jquery.js Copyright: © 2005-2008 by John Resig, Branden Aaron & Jörn Zaefferer License: GPL-2 +Files: underlays/attachments/ikiwiki/ +Copyright: 2010, 2011 Sebastian Tschan +Comment: + blueimp / jQuery-File-Upload widget, + from https://github.com/blueimp/jQuery-File-Upload +License: Expat + Files: underlays/themes/blueview/style.css Copyright: © 2009,2010 Bernd Zeimetz © 2008 Yahoo! Inc. From e2b43578a677894d23fa526d9201e5a89b231abc Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Wed, 15 Jun 2011 19:08:40 -0400 Subject: [PATCH 20/31] load attachment javascript into template the clean way --- IkiWiki/Plugin/attachment.pm | 17 ++++++++++++++--- doc/style.css | 8 ++++++++ templates/editpage.tmpl | 8 -------- .../ikiwiki/jquery.fileupload-ui.js | 0 .../ikiwiki/jquery.fileupload.js | 0 .../ikiwiki/jquery.iframe-transport.js | 0 .../ikiwiki/pbar-ani.gif | Bin .../ikiwiki/jquery.fileupload-ui.css | 8 -------- 8 files changed, 22 insertions(+), 19 deletions(-) rename underlays/{attachments => attachment}/ikiwiki/jquery.fileupload-ui.js (100%) rename underlays/{attachments => attachment}/ikiwiki/jquery.fileupload.js (100%) rename underlays/{attachments => attachment}/ikiwiki/jquery.iframe-transport.js (100%) rename underlays/{attachments => attachment}/ikiwiki/pbar-ani.gif (100%) delete mode 100644 underlays/attachments/ikiwiki/jquery.fileupload-ui.css diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index fdae8d1e0..be30e97b9 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -7,6 +7,7 @@ use IkiWiki 3.00; sub import { add_underlay("javascript"); + add_underlay("attachment"); hook(type => "getsetup", id => "attachment", call => \&getsetup); hook(type => "checkconfig", id => "attachment", call => \&checkconfig); hook(type => "formbuilder_setup", id => "attachment", call => \&formbuilder_setup); @@ -89,10 +90,20 @@ sub formbuilder_setup (@) { $form->tmpl_param("field-upload" => ''); $form->tmpl_param("field-link" => ''); - # Add the toggle javascript; the attachments interface uses - # it to toggle visibility. + # Add all the javascript used by the attachments interface. require IkiWiki::Plugin::toggle; - $form->tmpl_param("javascript" => IkiWiki::Plugin::toggle::include_javascript($params{page})); + my $js=IkiWiki::Plugin::toggle::include_javascript($params{page}); + $js.='\n'; + my @jsfiles=qw{jquery.min jquery-ui.min + jquery.tmpl.min jquery.iframe-transport + jquery.fileupload jquery.fileupload-ui + }; + foreach my $file (@jsfiles) { + $js.=''."\n"; + } + $form->tmpl_param("javascript" => $js); + # Start with the attachments interface toggled invisible, # but if it was used, keep it open. if ($form->submitted ne "Upload Attachment" && diff --git a/doc/style.css b/doc/style.css index fcf39be6a..42985c9f4 100644 --- a/doc/style.css +++ b/doc/style.css @@ -493,3 +493,11 @@ a.openid_large_btn:focus { .openid_selected { border: 4px solid #DDD; } + +.fileupload-content .ui-progressbar { + width: 200px; + height: 20px; +} +.fileupload-content .ui-progressbar-value { + background: url(ikiwiki/pbar-ani.gif); +} diff --git a/templates/editpage.tmpl b/templates/editpage.tmpl index b3098a240..efe077f84 100644 --- a/templates/editpage.tmpl +++ b/templates/editpage.tmpl @@ -1,5 +1,3 @@ - -
@@ -29,12 +27,6 @@ Attachments
- - - - - - diff --git a/underlays/attachments/ikiwiki/jquery.fileupload-ui.js b/underlays/attachment/ikiwiki/jquery.fileupload-ui.js similarity index 100% rename from underlays/attachments/ikiwiki/jquery.fileupload-ui.js rename to underlays/attachment/ikiwiki/jquery.fileupload-ui.js diff --git a/underlays/attachments/ikiwiki/jquery.fileupload.js b/underlays/attachment/ikiwiki/jquery.fileupload.js similarity index 100% rename from underlays/attachments/ikiwiki/jquery.fileupload.js rename to underlays/attachment/ikiwiki/jquery.fileupload.js diff --git a/underlays/attachments/ikiwiki/jquery.iframe-transport.js b/underlays/attachment/ikiwiki/jquery.iframe-transport.js similarity index 100% rename from underlays/attachments/ikiwiki/jquery.iframe-transport.js rename to underlays/attachment/ikiwiki/jquery.iframe-transport.js diff --git a/underlays/attachments/ikiwiki/pbar-ani.gif b/underlays/attachment/ikiwiki/pbar-ani.gif similarity index 100% rename from underlays/attachments/ikiwiki/pbar-ani.gif rename to underlays/attachment/ikiwiki/pbar-ani.gif diff --git a/underlays/attachments/ikiwiki/jquery.fileupload-ui.css b/underlays/attachments/ikiwiki/jquery.fileupload-ui.css deleted file mode 100644 index d792724b7..000000000 --- a/underlays/attachments/ikiwiki/jquery.fileupload-ui.css +++ /dev/null @@ -1,8 +0,0 @@ -.fileupload-content .ui-progressbar { - width: 200px; - height: 20px; -} - -.fileupload-content .ui-progressbar-value { - background: url(pbar-ani.gif); -} From a695b5b2f8876873e934f8ad3410eb92be3c6fe1 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Wed, 15 Jun 2011 19:15:06 -0400 Subject: [PATCH 21/31] updated jquery and made it its own underlay --- IkiWiki/Plugin/openid.pm | 1 + debian/changelog | 1 + debian/copyright | 5 +-- templates/openid-selector.tmpl | 2 +- underlays/jquery/ikiwiki/jquery.min.js | 23 +++++++++++++ .../openid-selector/ikiwiki/openid/jquery.js | 32 ------------------- 6 files changed, 29 insertions(+), 35 deletions(-) create mode 100644 underlays/jquery/ikiwiki/jquery.min.js delete mode 100644 underlays/openid-selector/ikiwiki/openid/jquery.js diff --git a/IkiWiki/Plugin/openid.pm b/IkiWiki/Plugin/openid.pm index bd2cfdc44..b6642619a 100644 --- a/IkiWiki/Plugin/openid.pm +++ b/IkiWiki/Plugin/openid.pm @@ -8,6 +8,7 @@ use IkiWiki 3.00; sub import { add_underlay("openid-selector"); + add_underlay("jquery"); hook(type => "checkconfig", id => "openid", call => \&checkconfig); hook(type => "getsetup", id => "openid", call => \&getsetup); hook(type => "auth", id => "openid", call => \&auth); diff --git a/debian/changelog b/debian/changelog index 4fe011f4a..cb0ee6856 100644 --- a/debian/changelog +++ b/debian/changelog @@ -2,6 +2,7 @@ ikiwiki (3.20110609) UNRELEASED; urgency=low * userlist: New plugin, lets admins see a list of users and their info. * aggregate: Improve checking for too long aggregated filenames. + * Updated to jQuery 1.6.1. -- Joey Hess Thu, 09 Jun 2011 10:06:44 -0400 diff --git a/debian/copyright b/debian/copyright index 16e0b3583..0571bdf8e 100644 --- a/debian/copyright +++ b/debian/copyright @@ -203,8 +203,9 @@ Comment: From http://code.google.com/p/openid-selector/ License: BSD-2-clause -Files: underlays/openid-selector/ikiwiki/openid/jquery.js -Copyright: © 2005-2008 by John Resig, Branden Aaron & Jörn Zaefferer +Files: underlays/jquery/* +Copyright: © 2005-2011 by John Resig, Branden Aaron & Jörn Zaefferer + © 2011 The Dojo Foundation License: GPL-2 Files: underlays/attachments/ikiwiki/ diff --git a/templates/openid-selector.tmpl b/templates/openid-selector.tmpl index 0659ee583..b6be2720c 100644 --- a/templates/openid-selector.tmpl +++ b/templates/openid-selector.tmpl @@ -1,4 +1,4 @@ - +