@@ -, +, @@ offline_circ/process_koc.pl and enqueue_koc.pl. We now use ajax to get progress figures instead of launching perl script upload-file-progress. The js changes now also allow for aborting a file upload. files at once. we could have some files with errors and others without errors. The upload is now marked as Failed only if there was no upload at all. Deleting an upload is now presented via the search results form. temporary directory (/tmp ?), subfolder koha_upload. Bonus: Remove permissions on this subfolder. Retry, check error and restore permissions again. directory again. You should have a partial file, but no record. test an individual image or a zip file including images and a file called datalink.txt (with lines biblionumber,filename). Enable AllowOfflineCirculation, and create a koc file (plain text): Line1: Version=1.0\tA=1\tB=2 Line2: 2015-08-06 08:00:00 345\treturn\t[barcode] Note: Replace tabs and barcode. The number of tabs is essential! Checkout the item with your barcode. Go to Offline circulation file upload. Upload and click Apply directly. Checkout again. Upload again, click Add to offline circulation queue. public. Check the results in the table. Verify that you can download the file in OPAC without being logged in. same category. One upload should succeed. Check for reported error. Goto Cataloguing editor. In an empty 856$u, click the tag editor. Upload a file and click Choose. Save the record. Open the record in the OPAC and click the link. Copy this link to your clipboard for next step. Choose for Delete. Open the link in your clipboard again. Error message? with some diacritical characters in the filename. (Bonus points for adding special chars in the category code.) Note: You can add categories via authorized values, UPLOAD key. --- cataloguing/value_builder/upload.pl | 153 ++------- koha-tmpl/intranet-tmpl/prog/en/js/file-upload.js | 32 ++ .../prog/en/modules/offline_circ/process_koc.tt | 39 ++- .../prog/en/modules/tools/stage-marc-import.tt | 64 +++- .../prog/en/modules/tools/upload-images.tt | 45 ++- .../intranet-tmpl/prog/en/modules/tools/upload.tt | 342 +++++++++++++++++++++ .../bootstrap/en/modules/opac-retrieve-file.tt | 6 + offline_circ/enqueue_koc.pl | 6 +- offline_circ/process_koc.pl | 8 +- opac/opac-retrieve-file.pl | 36 ++- tools/stage-marc-import.pl | 9 +- tools/upload-cover-image.pl | 8 +- tools/upload-file.pl | 82 ++--- tools/upload.pl | 101 ++++++ 14 files changed, 711 insertions(+), 220 deletions(-) create mode 100644 koha-tmpl/intranet-tmpl/prog/en/js/file-upload.js create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/tools/upload.tt create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-retrieve-file.tt create mode 100755 tools/upload.pl --- a/cataloguing/value_builder/upload.pl +++ a/cataloguing/value_builder/upload.pl @@ -4,6 +4,7 @@ # This file is part of Koha. # +# Copyright (C) 2015 Rijksmuseum # Copyright (C) 2011-2012 BibLibre # # Koha is free software; you can redistribute it and/or modify it @@ -20,145 +21,33 @@ # along with Koha; if not, see . use Modern::Perl; -use CGI qw/-utf8/; -use C4::Auth; -use C4::Context; -use C4::Output; -use C4::UploadedFiles; +# This plugin does not use the plugin launcher. It refers to tools/upload.pl. +# That script and template support using it as a plugin. + +# If the plugin is called with the pattern [id=some_hashvalue] in the +# corresponding field, it starts the upload script as a search, providing +# the possibility to delete the uploaded file. If the field is empty, you +# can upload a new file. my $builder = sub { my ( $params ) = @_; - my $function_name = $params->{id}; - my $res = " - -"; - return $res; -}; - -my $launcher = sub { - my ( $params ) = @_; - my $input = $params->{cgi}; - my $index = $input->param('index'); - my $id = $input->param('id'); - my $delete = $input->param('delete'); - my $uploaded_file = $input->param('uploaded_file'); - my $from_popup = $input->param('from_popup'); - my $isfileuploadurl = $input->param('IsFileUploadUrl'); - my $dangling = C4::UploadedFiles::DanglingEntry($id,$isfileuploadurl); - my $template_name; - if ($delete || ($id && ($dangling==0 || $dangling==1))) { - $template_name = "upload_delete_file.tt"; - } - else { - $template_name = "upload.tt"; - } - - my ( $template, $loggedinuser, $cookie ) = get_template_and_user( - { template_name => "cataloguing/value_builder/$template_name", - query => $input, - type => "intranet", - authnotrequired => 0, - flagsrequired => { editcatalogue => '*' }, - debug => 1, - } - ); - - if ($dangling==2) { - $template->param( dangling => 1 ); - } - - # Dealing with the uploaded file - my $dir = $input->param('uploadcategory'); - if ($uploaded_file and $dir) { - my $fh = $input->upload('uploaded_file'); - - $id = C4::UploadedFiles::UploadFile($uploaded_file, $dir, $fh->handle); - my $OPACBaseURL = C4::Context->preference('OPACBaseURL') // ''; - $OPACBaseURL =~ s#/$##; - if (!$OPACBaseURL) { - $template->param(MissingURL => 1); - } - if($id && $OPACBaseURL) { - my $return = "$OPACBaseURL/cgi-bin/koha/opac-retrieve-file.pl?id=$id"; - $template->param( - success => 1, - return => $return, - uploaded_file => $uploaded_file, - ); - } else { - $template->param(error => 1); - } - } elsif ($delete || ($id && ($dangling==0 || $dangling==1))) { - # If there's already a file uploaded for this field, - # We handle its deletion - if ($delete) { - if(C4::UploadedFiles::DelUploadedFile($id)==0) {; - $template->param(error => 1); + var str = document.getElementById(index).value; + var myurl, term; + if( str && str.match(/id=([0-9a-f]+)/) ) { + term = RegExp.\$1; + myurl = '../tools/upload.pl?op=search&index='+index+'&term='+term+'&plugin=1'; } else { - $template->param(success => 1); + myurl = '../tools/upload.pl?op=new&index='+index+'&plugin=1'; } + window.open( myurl, 'tag_editor', 'width=800,height=400,toolbar=false,scrollbars=yes' ); } - } else { - my $upload_path = C4::Context->config('upload_path'); - if ($upload_path) { - my $filefield = CGI::filefield( - -name => 'uploaded_file', - -size => 50, - ); - $template->param( - filefield => $filefield, - uploadcategories => C4::UploadedFiles::getCategories(), - ); - } else { - $template->param( error_upload_path_not_configured => 1 ); - } - - if (!$uploaded_file && !$dir && $from_popup) { - $template->param(error_nothing_selected => 1); - } - elsif (!$uploaded_file && $dir) { - $template->param(error_no_file_selected => 1); - } - if ($uploaded_file and not $dir) { - $template->param(error_no_dir_selected => 1); - } - - } - - $template->param( - index => $index, - id => $id, - ); - - output_html_with_http_headers $input, $cookie, $template->output; + +SCRIPT }; -return { builder => $builder, launcher => $launcher }; - -1; - -__END__ - -=head1 upload.pl - -This plugin allows to upload files on the server and reference it in a marc -field. - -It uses config variable upload_path and pref OPACBaseURL. - -=cut +return { builder => $builder }; --- a/koha-tmpl/intranet-tmpl/prog/en/js/file-upload.js +++ a/koha-tmpl/intranet-tmpl/prog/en/js/file-upload.js @@ -0,0 +1,32 @@ +function AjaxUpload ( input, progressbar, xtra, callback ) { + // input and progressbar are jQuery objects + // callback is the callback function for completion + var formData= new FormData(); + $.each( input.prop('files'), function( dx, file ) { + formData.append( "uploadfile", file ); + }); + var xhr= new XMLHttpRequest(); + var url= '/cgi-bin/koha/tools/upload-file.pl?' + xtra; + progressbar.val( 0 ); + progressbar.next('.fileuploadpercent').text( '0' ); + xhr.open('POST', url, true); + xhr.upload.onprogress = function (e) { + var p = Math.round( (e.loaded/e.total) * 100 ); + progressbar.val( p ); + progressbar.next('.fileuploadpercent').text( p ); + } + xhr.onload = function (e) { + var data = JSON.parse( xhr.responseText ); + if( data.status == 'done' ) { + progressbar.val( 100 ); + progressbar.next('.fileuploadpercent').text( '100' ); + } + callback( data.status, data.fileid, data.errors ); + } + xhr.onerror = function (e) { + // Probably only fires for network failure + alert('An error occurred while uploading.'); + } + xhr.send( formData ); + return xhr; +} --- a/koha-tmpl/intranet-tmpl/prog/en/modules/offline_circ/process_koc.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/offline_circ/process_koc.tt @@ -1,22 +1,43 @@ [% INCLUDE 'doc-head-open.inc' %] Koha › Circulation › Offline circulation file upload [% INCLUDE 'doc-head-close.inc' %] -[% INCLUDE 'file-upload.inc' %] + + + + + #fileuploadprogress,#jobprogress { width:150px;height:10px;border:1px solid #666;background:url('[% interface %]/[% theme %]/img/progress.png') -300px 0px no-repeat; } + + @@ -90,11 +131,20 @@ function CheckForm(f) { -
+
+ + +
-
Upload progress:
0%
-
+
+
Upload progress: + + + 0% +
+
+
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/upload-images.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/upload-images.tt @@ -1,14 +1,40 @@ [% INCLUDE 'doc-head-open.inc' %] Koha › Tools › Upload images [% INCLUDE 'doc-head-close.inc' %] -[% INCLUDE 'file-upload.inc' %] + + + + #fileuploadprogress,#jobprogress { width:150px;height:10px;border:1px solid #666;background:url('[% interface %]/[% theme %]/img/progress.png') -300px 0px no-repeat; } + + + + + + + +[% IF !plugin %] + [% INCLUDE 'header.inc' %] + [% INCLUDE 'cat-search.inc' %] + [% PROCESS breadcrumbs %] +[% END %] + +
+
+
+
+ +

Upload

+ + + +[% PROCESS submitter %] +[% IF mode == 'new' || mode == 'deleted' %] + [% PROCESS form_new %] + [% PROCESS form_search %] + [% PROCESS closer %] +[% ELSIF mode == 'report' %] + [% IF uploads %] +

Your request gave the following results:

+ [% PROCESS table_results %] + [% PROCESS closer %] + [% ELSE %] +

Sorry, your request had no results.

+ [% PROCESS newsearch %] + [% END %] +[% END %] + +
+
+
+ +[% INCLUDE 'intranet-bottom.inc' %] --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-retrieve-file.tt +++ a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-retrieve-file.tt @@ -0,0 +1,6 @@ +[%# This template is called only for a very simple error message %] + + +Your search [% IF hash %]for [% hash %][% END %] was not successful. + + --- a/offline_circ/enqueue_koc.pl +++ a/offline_circ/enqueue_koc.pl @@ -32,7 +32,7 @@ use C4::Circulation; use C4::Items; use C4::Members; use C4::Stats; -use C4::UploadedFile; +use Koha::Upload; use Date::Calc qw( Add_Delta_Days Date_to_Days ); @@ -60,8 +60,8 @@ my $sessionID = $cookies{'CGISESSID'}->value; our $dbh = C4::Context->dbh(); if ($fileID) { - my $uploaded_file = C4::UploadedFile->fetch($sessionID, $fileID); - my $fh = $uploaded_file->fh(); + my $upload = Koha::Upload->new->get({ id => $fileID, filehandle => 1 }); + my $fh = $upload->{fh}; my @input_lines = <$fh>; my $header_line = shift @input_lines; --- a/offline_circ/process_koc.pl +++ a/offline_circ/process_koc.pl @@ -32,7 +32,7 @@ use C4::Circulation; use C4::Items; use C4::Members; use C4::Stats; -use C4::UploadedFile; +use Koha::Upload; use C4::BackgroundJob; use Date::Calc qw( Add_Delta_Days Date_to_Days ); @@ -69,11 +69,11 @@ if ($completedJobID) { $template->param(transactions_loaded => 1); $template->param(messages => $results->{results}); } elsif ($fileID) { - my $uploaded_file = C4::UploadedFile->fetch($sessionID, $fileID); - my $fh = $uploaded_file->fh(); + my $upload = Koha::Upload->new->get({ id => $fileID, filehandle => 1 }); + my $fh = $upload->{fh}; + my $filename = $upload->{name}; my @input_lines = <$fh>; - my $filename = $uploaded_file->name(); my $job = undef; if ($runinbackground) { --- a/opac/opac-retrieve-file.pl +++ a/opac/opac-retrieve-file.pl @@ -19,19 +19,33 @@ use Modern::Perl; use CGI; +use Encode; +use C4::Auth; use C4::Context; -use C4::UploadedFiles; +use C4::Output; +use Koha::Upload; -my $input = new CGI; +my $input = CGI::->new; +my $hash = $input->param('id'); # historically called id (used in URLs?) -my $id = $input->param('id'); -my $file = C4::UploadedFiles::GetUploadedFile($id); -exit 1 if !$file || !-f $file->{filepath}; - -open my $fh, '<', $file->{filepath} or die "Can't open file: $!"; -print $input->header( C4::UploadedFiles::httpheaders( $file->{filename} )); -while(<$fh>) { - print $_; +my $upl = Koha::Upload->new({ public => 1 }); +my $rec = $upl->get({ hashvalue => $hash, filehandle => 1 }); +my $fh = $rec->{fh}; +if( !$rec || !$fh ) { + my ( $template, $user, $cookie ) = get_template_and_user({ + query => $input, + template_name => 'opac-retrieve-file.tt', + type => 'opac', + authnotrequired => 1, + }); + $template->param( hash => $hash ); + output_html_with_http_headers $input, $cookie, $template->output; +} else { + my @hdr = $upl->httpheaders( $rec->{name} ); + print Encode::encode_utf8( $input->header( @hdr ) ); + while( <$fh> ) { + print $_; + } + $fh->close; } -close $fh; --- a/tools/stage-marc-import.pl +++ a/tools/stage-marc-import.pl @@ -39,7 +39,7 @@ use C4::Output; use C4::Biblio; use C4::ImportBatch; use C4::Matcher; -use C4::UploadedFile; +use Koha::Upload; use C4::BackgroundJob; use C4::MarcModificationTemplates; use Koha::Plugins; @@ -84,8 +84,9 @@ if ($completedJobID) { my $results = $job->results(); $template->param(map { $_ => $results->{$_} } keys %{ $results }); } elsif ($fileID) { - my $uploaded_file = C4::UploadedFile->fetch($sessionID, $fileID); - my $fh = $uploaded_file->fh(); + my $upload = Koha::Upload->new->get({ id => $fileID, filehandle => 1 }); + my $fh = $upload->{fh}; + my $filename = $upload->{name}; # filename only, no path my $marcrecord=''; $/ = "\035"; while (<$fh>) { @@ -93,8 +94,8 @@ if ($completedJobID) { s/\s+$//; $marcrecord.=$_; } + $fh->close; - my $filename = $uploaded_file->name(); my $job = undef; my $dbh; if ($runinbackground) { --- a/tools/upload-cover-image.pl +++ a/tools/upload-cover-image.pl @@ -47,7 +47,7 @@ use C4::Context; use C4::Auth; use C4::Output; use C4::Images; -use C4::UploadedFile; +use Koha::Upload; use C4::Log; my $debug = 1; @@ -83,9 +83,9 @@ $template->{VARS}->{'biblionumber'} = $biblionumber; my $total = 0; if ($fileID) { - my $uploaded_file = C4::UploadedFile->fetch( $sessionID, $fileID ); + my $upload = Koha::Upload->new->get({ id => $fileID, filehandle => 1 }); if ( $filetype eq 'image' ) { - my $fh = $uploaded_file->fh(); + my $fh = $upload->{fh}; my $srcimage = GD::Image->new($fh); if ( defined $srcimage ) { my $dberror = PutImage( $biblionumber, $srcimage, $replace ); @@ -102,7 +102,7 @@ if ($fileID) { undef $srcimage; } else { - my $filename = $uploaded_file->filename(); + my $filename = $upload->{path}; my $dirname = File::Temp::tempdir( CLEANUP => 1 ); unless ( system( "unzip", $filename, '-d', $dirname ) == 0 ) { $error = 'UZIPFAIL'; --- a/tools/upload-file.pl +++ a/tools/upload-file.pl @@ -17,18 +17,17 @@ # You should have received a copy of the GNU General Public License # along with Koha; if not, see . -use strict; -#use warnings; FIXME - Bug 2505 +use Modern::Perl; -# standard or CPAN modules used -use IO::File; use CGI qw ( -utf8 ); -use CGI::Session; +use CGI::Cookie; +use Encode; +use JSON; +use URI::Escape; + use C4::Context; use C4::Auth qw/check_cookie_auth haspermission/; -use CGI::Cookie; # need to check cookies before - # having CGI parse the POST request -use C4::UploadedFile; +use Koha::Upload; # upload-file.pl must authenticate the user # before processing the POST request, @@ -36,7 +35,7 @@ use C4::UploadedFile; # not authorized. Consequently, unlike # most of the other CGI scripts, upload-file.pl # requires that the session cookie already -# have been created. +# has been created. my $flags_required = [ {circulate => 'circulate_remaining_permissions'}, @@ -44,52 +43,53 @@ my $flags_required = [ {tools => 'upload_local_cover_images'} ]; -my %cookies = fetch CGI::Cookie; +my %cookies = CGI::Cookie->fetch; +my $sid = $cookies{'CGISESSID'}->value; my $auth_failure = 1; -my ( $auth_status, $sessionID ) = check_cookie_auth( $cookies{'CGISESSID'}->value ); +my ( $auth_status, $sessionID ) = check_cookie_auth( $sid ); +my $uid = C4::Auth::get_session($sid)->param('id'); foreach my $flag_required ( @{$flags_required} ) { - if ( my $flags = haspermission( C4::Context->config('user'), $flag_required ) ) { + if ( my $flags = haspermission( $uid, $flag_required ) ) { $auth_failure = 0 if $auth_status eq 'ok'; } } if ($auth_failure) { - $auth_status = 'denied' if $auth_status eq 'failed'; - send_reply($auth_status, ""); + send_reply( 'denied' ); exit 0; } -our $uploaded_file = C4::UploadedFile->new($sessionID); -unless (defined $uploaded_file) { - # FIXME - failed to create file for some reason - send_reply('failed', ''); - exit 0; +my $upload = Koha::Upload->new( upload_pars($ENV{QUERY_STRING}) ); +if( !$upload || !$upload->cgi || !$upload->count ) { + # not one upload succeeded + send_reply( 'failed', undef, $upload? $upload->err: undef ); +} else { + # in case of multiple uploads, at least one got through + send_reply( 'done', $upload->result, $upload->err ); } -$uploaded_file->max_size($ENV{'CONTENT_LENGTH'}); # may not be the file size, exactly - -my $query; -$query = new CGI \&upload_hook; -$uploaded_file->done(); -send_reply('done', $uploaded_file->id()); - -# FIXME - if possible, trap signal caused by user cancelling upload -# FIXME - something is wrong during cleanup: \t(in cleanup) Can't call method "commit" on unblessed reference at /usr/local/share/perl/5.8.8/CGI/Session/Driver/DBI.pm line 130 during global destruction. exit 0; -sub upload_hook { - my ($file_name, $buffer, $bytes_read, $session) = @_; - $uploaded_file->stash(\$buffer, $bytes_read); - if ( ! $uploaded_file->name && $file_name ) { # save name on first chunk - $uploaded_file->name($file_name); - } +sub send_reply { # response will be sent back as JSON + my ( $upload_status, $data, $error ) = @_; + my $reply = CGI->new(""); + print $reply->header( -type => 'text/html', -charset => 'UTF-8' ); + print JSON::encode_json({ + status => $upload_status, + fileid => $data, + errors => $error, + }); } -sub send_reply { - my ($upload_status, $fileid) = @_; - - my $reply = CGI->new(""); - print $reply->header(-type => 'text/html'); - # response will be sent back as JSON - print '{"status":"' . $upload_status . '","fileid":"' . $fileid . '"}'; +sub upload_pars { + my ( $qstr ) = @_; + $qstr = Encode::decode_utf8( uri_unescape( $qstr ) ); + # category could include a utf8 character + my $rv = {}; + foreach my $p ( qw[public category temp] ) { + if( $qstr =~ /(^|&)$p=(\w+)(&|$)/ ) { + $rv->{$p} = $2; + } + } + return $rv; } --- a/tools/upload.pl +++ a/tools/upload.pl @@ -0,0 +1,101 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Copyright (C) 2015 Rijksmuseum +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use CGI qw/-utf8/; +use JSON; + +use C4::Auth; +use C4::Output; +use Koha::Upload; + +my $input = CGI::->new; +my $op = $input->param('op') // 'new'; +my $plugin = $input->param('plugin'); +my $index = $input->param('index'); # MARC editor input field id +my $term = $input->param('term'); +my $id = $input->param('id'); +my $msg = $input->param('msg'); + +my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { template_name => "tools/upload.tt", + query => $input, + type => "intranet", + authnotrequired => 0, + flagsrequired => { editcatalogue => '*' }, + } +); + +$template->param( + plugin => $plugin, + index => $index, +); + +my $upar = $plugin ? { public => 1 } : {}; +if ( $op eq 'new' ) { + $template->param( + mode => 'new', + uploadcategories => Koha::Upload->getCategories, + ); + output_html_with_http_headers $input, $cookie, $template->output; +} elsif ( $op eq 'search' ) { + my $h = $id ? { id => $id } : { term => $term }; + my @uploads = Koha::Upload->new($upar)->get($h); + $template->param( + mode => 'report', + msg => $msg, + uploads => \@uploads, + ); + output_html_with_http_headers $input, $cookie, $template->output; +} elsif ( $op eq 'delete' ) { + + # delete only takes the id parameter + my $upl = Koha::Upload->new($upar); + my ($fn) = $upl->delete( { id => $id } ); + my $e = $upl->err; + my $msg = + $fn ? JSON::to_json( { $fn => 6 } ) + : $e ? JSON::to_json($e) + : undef; + $template->param( + mode => 'deleted', + msg => $msg, + uploadcategories => $upl->getCategories, + ); + output_html_with_http_headers $input, $cookie, $template->output; +} elsif ( $op eq 'download' ) { + my $upl = Koha::Upload->new($upar); + my $rec = $upl->get( { id => $id, filehandle => 1 } ); + my $fh = $rec->{fh}; + if ( !$rec || !$fh ) { + $template->param( + mode => 'new', + msg => JSON::to_json( { $id => 5 } ), + uploadcategories => $upl->getCategories, + ); + output_html_with_http_headers $input, $cookie, $template->output; + } else { + my @hdr = $upl->httpheaders( $rec->{name} ); + print Encode::encode_utf8( $input->header(@hdr) ); + while (<$fh>) { + print $_; + } + $fh->close; + } +} --