1 package SL::Controller::File;
5 use parent qw(SL::Controller::Base);
7 use List::Util qw(first max);
10 use Encode qw(decode);
16 use File::Spec::Win32;
17 use File::MimeInfo::Magic;
18 use SL::DB::Helper::Mappings;
20 use SL::DB::DeliveryOrder;
23 use SL::DB::PurchaseInvoice;
25 use SL::DB::GLTransaction;
29 use SL::Helper::CreatePDF qw(:all);
30 use SL::Locale::String;
33 use SL::Controller::Helper::ThumbnailCreator qw(file_probe_image_type);
35 use constant DO_DELETE => 0;
36 use constant DO_UNIMPORT => 1;
39 use Rose::Object::MakeMethods::Generic
41 'scalar --get_set_init' => [ qw() ],
42 'scalar' => [ qw(object object_type object_model object_id object_right file_type files is_global existing) ],
45 __PACKAGE__->run_before('check_object_params', only => [ qw(list ajax_delete ajax_importdialog ajax_import ajax_unimport ajax_upload ajax_files_uploaded) ]);
48 'sales_quotation' => { gen => 1 ,gltype => '', dir =>'SalesQuotation', model => 'Order', right => 'import_ar' },
49 'sales_order' => { gen => 1 ,gltype => '', dir =>'SalesOrder', model => 'Order', right => 'import_ar' },
50 'sales_delivery_order' => { gen => 1 ,gltype => '', dir =>'SalesDeliveryOrder', model => 'DeliveryOrder', right => 'import_ar' },
51 'invoice' => { gen => 1 ,gltype => 'ar', dir =>'SalesInvoice', model => 'Invoice', right => 'import_ar' },
52 'credit_note' => { gen => 1 ,gltype => '', dir =>'CreditNote', model => 'Invoice', right => 'import_ar' },
53 'request_quotation' => { gen => 3 ,gltype => '', dir =>'RequestForQuotation', model => 'Order', right => 'import_ap' },
54 'purchase_order' => { gen => 3 ,gltype => '', dir =>'PurchaseOrder', model => 'Order', right => 'import_ap' },
55 'purchase_delivery_order' => { gen => 3 ,gltype => '', dir =>'PurchaseDeliveryOrder',model => 'DeliveryOrder', right => 'import_ap' },
56 'purchase_invoice' => { gen => 2 ,gltype => 'ap', dir =>'PurchaseInvoice', model => 'PurchaseInvoice',right => 'import_ap' },
57 'vendor' => { gen => 0 ,gltype => '', dir =>'Vendor', model => 'Vendor', right => 'xx' },
58 'customer' => { gen => 1 ,gltype => '', dir =>'Customer', model => 'Customer', right => 'xx' },
59 'part' => { gen => 0 ,gltype => '', dir =>'Part', model => 'Part', right => 'xx' },
60 'gl_transaction' => { gen => 2 ,gltype => 'gl', dir =>'GeneralLedger', model => 'GLTransaction', right => 'import_ap' },
61 'draft' => { gen => 0 ,gltype => '', dir =>'Draft', model => 'Draft', right => 'xx' },
62 'csv_customer' => { gen => 1 ,gltype => '', dir =>'Reports', model => 'Customer', right => 'xx' },
63 'csv_vendor' => { gen => 1 ,gltype => '', dir =>'Reports', model => 'Vendor', right => 'xx' },
67 # $main::locale->text('imported')
77 $isjson = 1 if $::form->{json};
79 $self->_do_list($isjson);
82 sub action_ajax_importdialog {
84 $::auth->assert($self->object_right);
85 my $path = $::form->{path};
86 my @files = $self->_get_from_import($path);
88 'name' => $::form->{source},
90 'chk_action' => $::form->{source}.'_import',
91 'chk_title' => $main::locale->text('Import scanned documents'),
92 'chkall_title' => $main::locale->text('Import all'),
95 $self->render('file/import_dialog',
102 sub action_ajax_import {
104 $::auth->assert($self->object_right);
105 my $ids = $::form->{ids};
106 my $source = $::form->{source};
107 my $path = $::form->{path};
108 my @files = $self->_get_from_import($path);
109 foreach my $filename (@{ $::form->{$ids} || [] }) {
110 my ($file,undef) = grep { $_->{name} eq $filename } @files;
112 my $obj = SL::File->save(object_id => $self->object_id,
113 object_type => $self->object_type,
114 mime_type => 'application/pdf',
116 file_type => 'document',
117 file_name => $file->{filename},
118 file_path => $file->{path}
120 unlink($file->{path}) if $obj;
126 sub action_ajax_delete {
128 $self->_delete_all(DO_DELETE,$::locale->text('Following files are deleted:'));
131 sub action_ajax_unimport {
133 $self->_delete_all(DO_UNIMPORT,$::locale->text('Following files are unimported:'));
136 sub action_ajax_rename {
138 my $file = SL::File->get(id => $::form->{id});
140 $self->js->flash('error',$::locale->text('File not exists !'))->render();
143 $main::lxdebug->message(LXDebug->DEBUG2(), "object_id=".$file->object_id." object_type=".$file->object_type." dbfile=".$file);
144 my $sessionfile = $::form->{sessionfile};
145 $main::lxdebug->message(LXDebug->DEBUG2(), "sessionfile=".$sessionfile);
146 if ( $sessionfile && -f $sessionfile ) {
147 $main::lxdebug->message(LXDebug->DEBUG2(), "file=".$file->file_name." to=".$::form->{to}." sessionfile=".$sessionfile);
149 if ( $::form->{to} eq $file->file_name ) {
150 # no rename so use as new version
151 $file->save_file($sessionfile);
152 $self->js->flash('warning',$::locale->text('File \'#1\' is used as new Version !',$file->file_name));
155 # new filename so it is a new file with same attributes as old file
157 SL::File->save(object_id => $file->object_id,
158 object_type => $file->object_type,
159 mime_type => $file->mime_type,
160 source => $file->source,
161 file_type => $file->file_type,
162 file_name => $::form->{to},
163 file_path => $sessionfile
165 unlink($sessionfile);
168 $self->js->flash( 'error', t8('internal error (see details)'))
169 ->flash_detail('error', $@)->render;
177 my $res = $file->rename($::form->{to});
178 $main::lxdebug->message(LXDebug->DEBUG2(), "rename result=".$res);
179 if ($res > SL::File::RENAME_OK) {
180 $self->js->flash('error',
181 $res == SL::File::RENAME_EXISTS ? $::locale->text('File still exists !')
182 : $res == SL::File::RENAME_SAME ? $::locale->text('Same Filename !')
183 : $::locale->text('File not exists !'))->render;
188 $self->js->flash( 'error', t8('internal error (see details)'))
189 ->flash_detail('error', $@)->render;
193 $self->is_global($::form->{is_global});
194 $self->file_type( $file->file_type);
195 $self->object_type($file->object_type);
196 $self->object_id( $file->object_id);
197 #$self->object_model($file_types{$file->module}->{model});
198 #$self->object_right($file_types{$file->module}->{right});
199 if ( $::form->{next_ids} ) {
200 my @existing = split(/,/, $::form->{next_ids});
201 $self->existing(\@existing);
206 sub action_ajax_upload {
208 $self->{maxsize} = $::instance_conf->get_doc_max_filesize;
209 $self->{accept_types} = '';
210 $self->{accept_types} = 'image/png,image/gif,image/jpeg,image/tiff,*png,*gif,*.jpg,*.tif' if $self->{file_type} eq 'image';
211 $self->render('file/upload_dialog',
217 sub action_ajax_files_uploaded {
220 my $source = 'uploaded';
221 $main::lxdebug->message(LXDebug->DEBUG2(), "file_upload UPLOAD=".$::form->{ATTACHMENTS}->{uploadfiles});
223 if ( $::form->{ATTACHMENTS}->{uploadfiles} ) {
224 my @upfiles = @{ $::form->{ATTACHMENTS}->{uploadfiles} };
225 foreach my $idx (0 .. scalar(@upfiles) - 1) {
227 my $fname = uri_unescape($upfiles[$idx]->{filename});
228 $main::lxdebug->message(LXDebug->DEBUG2(), "file_upload name=".$fname);
229 ## normalize and find basename
230 # first split with unix rules
231 # after that split with windows rules
232 my ($volume,$directories,$basefile) = File::Spec::Unix->splitpath($fname);
233 ($volume,$directories,$basefile) = File::Spec::Win32->splitpath($basefile);
235 # to find real mime_type by magic we must save the filedata
237 my $sess_fname = "file_upload_".$self->object_type."_".$self->object_id."_".$idx;
238 my $sfile = SL::SessionFile->new($sess_fname, mode => 'w');
240 $sfile->fh->print(${$upfiles[$idx]->{data}});
242 my $mime_type = File::MimeInfo::Magic::magic($sfile->file_name);
245 # if filename has the suffix "pdf", but is really no pdf set mimetype for no suffix
246 $mime_type = File::MimeInfo::Magic::mimetype($basefile);
247 $mime_type = 'application/octet-stream' if $mime_type eq 'application/pdf' || !$mime_type;
249 $main::lxdebug->message(LXDebug->DEBUG2(), "mime_type=".$mime_type);
250 if ( $self->file_type eq 'image' && $self->file_probe_image_type($mime_type, $basefile)) {
253 my ($existobj) = SL::File->get_all(object_id => $self->object_id,
254 object_type => $self->object_type,
255 mime_type => $mime_type,
257 file_type => $self->file_type,
258 file_name => $basefile,
261 $main::lxdebug->message(LXDebug->DEBUG2(), "store1 exist=".$existobj);
263 $main::lxdebug->message(LXDebug->DEBUG2(), "id=".$existobj->id." sessionfile=". $sfile->file_name);
264 push @existing, $existobj->id.'_'.$sfile->file_name;
266 my $fileobj = SL::File->save(object_id => $self->object_id,
267 object_type => $self->object_type,
268 mime_type => $mime_type,
270 file_type => $self->file_type,
271 file_name => $basefile,
272 ## two possibilities: what is better ? content or sessionfile ??
273 #file_contents => ${$upfiles[$idx]->{data}},
274 file_path => $sfile->file_name
276 $main::lxdebug->message(LXDebug->DEBUG2(), "obj=".$fileobj);
277 unlink($sfile->file_name);
281 $self->js->flash( 'error', t8('internal error (see details)'))
282 ->flash_detail('error', $@)->render;
287 $self->existing(\@existing);
291 sub action_download {
293 my ($id,$version) = split /_/, $::form->{id};
294 my $file = SL::File->get(id => $id );
295 $file->version($version) if $version;
296 my $ref = $file->get_content;
297 if ( $file && $ref ) {
298 return $self->send_file($ref,
299 type => $file->mime_type,
300 name => $file->file_name,
309 sub check_object_params {
312 my $id = ($::form->{object_id} // 0) * 1;
313 my $draftid = ($::form->{draft_id} // 0) * 1;
317 if ( $draftid == 0 && $id == 0 && $::form->{is_global} ) {
319 $type = $::form->{object_type};
322 $id = $::form->{draft_id};
324 } elsif ( $::form->{object_type} ) {
325 $type = $::form->{object_type};
327 die "No object type" if ! $type;
328 die "No file type" if ! $::form->{file_type};
329 die "Unkown object type" if ! $file_types{$type};
331 $self->is_global($gldoc);
332 $self->file_type($::form->{file_type});
333 $self->object_type($type);
334 $self->object_id($id);
335 $self->object_model($file_types{$type}->{model});
336 $self->object_right($file_types{$type}->{right});
337 $main::lxdebug->message(LXDebug->DEBUG2(), "checked: object_id=".$self->object_id." object_type=".$self->object_type." is_global=".$self->is_global);
339 # $::auth->assert($self->object_right);
341 # my $model = 'SL::DB::' . $self->object_model;
342 # $self->object($model->new(id => $self->object_id)->load || die "Record not found");
352 my ($self,$do_unimport,$infotext) = @_;
354 my $ids = $::form->{ids};
355 foreach my $id_version (@{ $::form->{$ids} || [] }) {
356 my ($id,$version) = split /_/, $id_version;
357 my $dbfile = SL::File->get(id => $id);
358 $dbfile->version($version) if $dbfile && $version;
359 if ( $dbfile && $dbfile->delete ) {
360 $files .= ' '.$dbfile->file_name;
363 $self->js->flash('info',$infotext.$files) if $files;
368 my ($self,$json) = @_;
370 $main::lxdebug->message(LXDebug->DEBUG2(), "do_list: object_id=".$self->object_id." object_type=".$self->object_type." file_type=".$self->file_type." json=".$json);
371 if ( $self->file_type eq 'document' ) {
372 @files = SL::File->get_all_versions(object_id => $self->object_id ,
373 object_type => $self->object_type,
374 file_type => $self->file_type );
376 $main::lxdebug->message(LXDebug->DEBUG2(), "cnt1=".scalar(@files));
378 elsif ( $self->file_type eq 'attachment' || $self->file_type eq 'image' ) {
379 @files = SL::File->get_all(object_id => $self->object_id ,
380 object_type => $self->object_type,
381 file_type => $self->file_type );
382 $main::lxdebug->message(LXDebug->DEBUG2(), "cnt2=".scalar(@files));
384 $self->files(\@files);
385 $self->_mk_render('file/list',1,0,$json);
388 sub _get_from_import {
389 my ($self,$path) = @_;
392 $main::lxdebug->message(LXDebug->DEBUG2(), "import path=".$path);
393 my $language = $::lx_office_conf{system}->{language};
394 my $timezone = $::locale->get_local_time_zone()->name;
395 if (opendir my $dir, $path) {
396 my @files = ( readdir $dir);
397 foreach my $file ( @files) {
398 next if (($file eq '.') || ($file eq '..'));
399 $file = Encode::decode('utf-8', $file);
400 $main::lxdebug->message(LXDebug->DEBUG2(), "file=".$file);
402 next if( -d "$path/$file");
404 my $tmppath = File::Spec->catfile( $path, $file );
405 $main::lxdebug->message(LXDebug->DEBUG2(), "tmppath=".$tmppath." file=".$file);
406 next if( ! -f $tmppath);
408 my $st = stat($tmppath);
409 my $dt = DateTime->from_epoch( epoch => $st->mtime, time_zone => $timezone, locale => $language);
410 my $sname = $main::locale->quote_special_chars('HTML',$file);
413 'filename' => $sname,
415 'mtime' => $st->mtime,
416 'date' => $dt->dmy('.')." ".$dt->hms,
421 $main::lxdebug->message(LXDebug->DEBUG2(), "return ".scalar(@foundfiles)." files");
426 my ($self,$template,$edit,$scanner,$json) = @_;
429 ##TODO here a configurable code must be implemented
432 $main::lxdebug->message(LXDebug->DEBUG2(), "mk_render: object_id=".$self->object_id." object_type=".$self->object_type.
433 " file_type=".$self->file_type." json=".$json." filecount=".scalar(@{ $self->files })." is_global=".$self->is_global);
434 my @sources = $self->_get_sources();
435 foreach my $source ( @sources ) {
436 $main::lxdebug->message(LXDebug->DEBUG2(), "mk_render: source name=".$source->{name});
437 @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files };
439 if ( $self->file_type eq 'document' ) {
440 $title = $main::locale->text('Documents');
441 } elsif ( $self->file_type eq 'attachment' ) {
442 $title = $main::locale->text('Attachments');
443 } elsif ( $self->file_type eq 'image' ) {
444 $title = $main::locale->text('Images');
447 my $output = SL::Presenter->get->render(
450 SOURCES => \@sources,
451 edit_attachments => $edit,
452 object_type => $self->object_type,
453 object_id => $self->object_id,
454 file_type => $self->file_type,
455 is_global => $self->is_global,
459 $self->js->html('#'.$self->file_type.'_list_'.$self->object_type, $output);
460 if ( $self->existing && scalar(@{$self->existing}) > 0) {
461 my $first = shift @{$self->existing};
462 my ($first_id,$sfile) = split('_',$first,2);
463 #$main::lxdebug->message(LXDebug->DEBUG2(), "id=".$first_id." sessionfile=". $sfile);
464 my $file = SL::File->get(id => $first_id );
465 $self->js->run('kivi.File.askForRename',$first_id,$file->file_name,$sfile,join (',', @{$self->existing}), $self->is_global);
469 $self->render(\$output, { layout => 0, process => 0 });
474 $self->js->flash( 'error', t8('internal error (see details)'))
475 ->flash_detail('error', $@)->render;
477 $self->render('generic/error', { layout => 0 }, label_error => $@);
486 $main::lxdebug->message(LXDebug->DEBUG2(), "get_sources file_type=". $self->file_type);
487 if ( $self->file_type eq 'document' ) {
488 ##TODO statt gen neue attribute in filetypes :
489 if (($file_types{$self->object_type}->{gen}*1 & 1)==1) {
492 'title' => $main::locale->text('generated Files'),
493 'chk_action' => 'documents_delete',
494 'chk_title' => $main::locale->text('Delete Documents'),
495 'chkall_title' => $main::locale->text('Delete all'),
496 'file_title' => $main::locale->text('filename'),
497 'confirm_text' => $main::locale->text('delete'),
499 'rename_title' => $main::locale->text('Rename Documents'),
500 'done_text' => $main::locale->text('deleted')
502 push @sources , $gendata;
504 if (($file_types{$self->object_type}->{gen}*1 & 2)==2) {
505 my @others = SL::File->get_other_sources();
506 $main::lxdebug->message(LXDebug->DEBUG2(), "other cnt=". scalar(@others));
507 foreach my $scanner_or_mailrx (@others) {
509 'name' => $scanner_or_mailrx->{name},
510 'title' => $main::locale->text('from \'#1\' imported Files',$scanner_or_mailrx->{description}),
511 'chk_action' => $scanner_or_mailrx->{name}.'_unimport',
512 'chk_title' => $main::locale->text('Unimport documents'),
513 'chkall_title' => $main::locale->text('Unimport all'),
514 'file_title' => $main::locale->text('filename'),
515 'confirm_text' => $main::locale->text('unimport'),
517 'rename_title' => $main::locale->text('Rename Documents'),
519 'import_title' => $main::locale->text('Add Document from \'#1\'',$scanner_or_mailrx->{name}),
520 'path' => $scanner_or_mailrx->{directory},
521 'done_text' => $main::locale->text('unimported')
523 push @sources , $other;
527 elsif ( $self->file_type eq 'attachment' ) {
529 'name' => 'uploaded',
530 'title' => $main::locale->text(''),
531 'chk_action' => 'attachments_delete',
532 'chk_title' => $main::locale->text('Delete Attachments'),
533 'chkall_title' => $main::locale->text('Delete all'),
534 'file_title' => $main::locale->text('filename'),
535 'confirm_text' => $main::locale->text('delete'),
537 'are_existing' => $self->existing ? 1 : 0,
538 'rename_title' => $main::locale->text('Rename Attachments'),
540 'upload_title' => $main::locale->text('Upload Attachments'),
541 'done_text' => $main::locale->text('deleted')
543 push @sources , $attdata;
545 elsif ( $self->file_type eq 'image' ) {
547 'name' => 'uploaded',
548 'title' => $main::locale->text(''),
549 'chk_action' => 'images_delete',
550 'chk_title' => $main::locale->text('Delete Images'),
551 'chkall_title' => $main::locale->text('Delete all'),
552 'file_title' => $main::locale->text('filename'),
553 'confirm_text' => $main::locale->text('delete'),
555 'are_existing' => $self->existing ? 1 : 0,
556 'rename_title' => $main::locale->text('Rename Images'),
558 'upload_title' => $main::locale->text('Upload Images'),
559 'done_text' => $main::locale->text('deleted')
561 push @sources , $attdata;
563 $main::lxdebug->message(LXDebug->DEBUG2(), "get_sources count=".scalar(@sources));
577 SL::Controller::File - Controller for managing files
584 # The Controller is called direct from the webpages
587 <a href="controller.pl?action=File/list&file_type=document\
588 &object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">
591 # or indirect via javascript functions from js/kivi.File.js
594 kivi.popup_dialog({ url: 'controller.pl',
595 data: { action : 'File/ajax_upload',
596 file_type : 'uploaded',
607 This is a controller for handling files in a storage independent way.
608 The storage may be a Filesystem,a WebDAV, a Database or DMS.
609 These backends must be configered in ClientConfig.
610 This Controller use as intermediate layer for storage C<SL::File>.
612 The Controller is responsible to display forms for displaying the files at the ERP-objects and
613 for uploading and downloading the files.
615 More description of the intermediate layer see L<SL::File>.
619 =head2 C<action_list>
621 This loads a list of files on a webpage. This can be done with a normal submit or via an ajax/json call.
622 Dependent of file_type different sources are available.
624 For documents there are the 'created' source and the imports from scanners or email.
625 For attachments and images only the 'uploaded' source available.
627 Available C<FORM PARAMS>:
631 =item C<form.object_id>
633 The Id of the ERP-object.
635 =item C<form.object_type>
637 The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
639 =item C<form.file_type>
641 For one ERP-object may exists different type of documents the type may be "documents","attachments" or "images".
642 This file_type is a filter for the list.
646 The method can be used as normal HTTP-Request (json=0) or as AJAX-JSON call to refresh the list if the parameter is set to 1.
651 =head2 C<action_ajax_upload>
654 A new file or more files can selected by a dialog and insert into the system.
657 Available C<FORM PARAMS>:
661 =item C<form.file_type>
663 This parameter describe here the source for a new file :
664 "attachments" and "images"
666 This is a normal upload selection, which may be more then one file to upload.
668 =item C<form.object_id>
672 =item C<form.object_type>
674 are the same as at C<action_list>
678 =head2 C<action_ajax_files_uploaded>
680 The Upload of selected Files. The "multipart_formdata" is parsed in SL::Request into the formsvariable "form.ATTACHMENTS".
681 The filepaths are checked about Unix and Windows paths. Also the MIME type of the files are verified ( IS the contents of a *.pdf real PDF?).
682 If the same filename still exists at this object after the download for each existing filename a rename dialog will be opened.
684 If the filename is not changed the new uploaded file is a new version of the file, if the name is changed it is a new file.
686 Available C<FORM PARAMS>:
690 =item C<form.ATTACHMENTS.uploadfiles>
692 This is an array of elements which have {filename} for the name and {data} for the contents.
694 Also object_id, object_type and file_type
698 =head2 C<action_download>
700 This is the real download of a file normally called via javascript "$.download("controller.pl", data);"
702 Available C<FORM PARAMS>:
706 Also object_id, object_type and file_type
710 =head2 C<action_ajax_importdialog>
712 A Dialog with all available and not imported files to import is open.
713 More then one file can be selected.
715 Available C<FORM PARAMS>:
721 The name of the source like "scanner1" or "email"
725 The full path to the directory on the server, where the files to import can found
727 Also object_id, object_type and file_type
731 =head2 C<action_ajax_delete>
733 Some files can be deleted
735 Available C<FORM PARAMS>:
741 The ids of the files to delete. Only this files are deleted not all versions of a file if the exists
745 =head2 C<action_ajax_unimport>
747 Some files can be unimported, dependent of the source of the file. This means they are moved
748 back to the directory of the source
750 Available C<FORM PARAMS>:
756 The ids of the files to unimport. Only this files are unimported not all versions of a file if the exists
760 =head2 C<action_ajax_rename>
762 One file can be renamed. There can be some checks if the same filename still exists at one object.
767 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>