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;
38 use Rose::Object::MakeMethods::Generic
40 'scalar --get_set_init' => [ qw() ],
41 'scalar' => [ qw(object object_type object_model object_id object_right file_type files is_global existing) ],
44 __PACKAGE__->run_before('check_object_params', only => [ qw(list ajax_delete ajax_importdialog ajax_import ajax_unimport ajax_upload ajax_files_uploaded) ]);
47 'sales_quotation' => { gen => 1, gltype => '', dir => 'SalesQuotation', model => 'Order', right => 'import_ar' },
48 'sales_order' => { gen => 1, gltype => '', dir => 'SalesOrder', model => 'Order', right => 'import_ar' },
49 'sales_delivery_order' => { gen => 1, gltype => '', dir => 'SalesDeliveryOrder', model => 'DeliveryOrder', right => 'import_ar' },
50 'invoice' => { gen => 1, gltype => 'ar', dir => 'SalesInvoice', model => 'Invoice', right => 'import_ar' },
51 'credit_note' => { gen => 1, gltype => '', dir => 'CreditNote', model => 'Invoice', right => 'import_ar' },
52 'request_quotation' => { gen => 3, gltype => '', dir => 'RequestForQuotation', model => 'Order', right => 'import_ap' },
53 'purchase_order' => { gen => 3, gltype => '', dir => 'PurchaseOrder', model => 'Order', right => 'import_ap' },
54 'purchase_delivery_order' => { gen => 3, gltype => '', dir => 'PurchaseDeliveryOrder', model => 'DeliveryOrder', right => 'import_ap' },
55 'purchase_invoice' => { gen => 2, gltype => 'ap', dir => 'PurchaseInvoice', model => 'PurchaseInvoice', right => 'import_ap' },
56 'vendor' => { gen => 0, gltype => '', dir => 'Vendor', model => 'Vendor', right => 'xx' },
57 'customer' => { gen => 1, gltype => '', dir => 'Customer', model => 'Customer', right => 'xx' },
58 'part' => { gen => 0, gltype => '', dir => 'Part', model => 'Part', right => 'xx' },
59 'gl_transaction' => { gen => 2, gltype => 'gl', dir => 'GeneralLedger', model => 'GLTransaction', right => 'import_ap' },
60 'draft' => { gen => 0, gltype => '', dir => 'Draft', model => 'Draft', right => 'xx' },
61 'csv_customer' => { gen => 1, gltype => '', dir => 'Reports', model => 'Customer', right => 'xx' },
62 'csv_vendor' => { gen => 1, gltype => '', dir => 'Reports', model => 'Vendor', right => 'xx' },
66 # $main::locale->text('imported')
76 $is_json = 1 if $::form->{json};
78 $self->_do_list($is_json);
81 sub action_ajax_importdialog {
83 $::auth->assert($self->object_right);
84 my $path = $::form->{path};
85 my @files = $self->_get_from_import($path);
87 'name' => $::form->{source},
89 'chk_action' => $::form->{source}.'_import',
90 'chk_title' => $main::locale->text('Import scanned documents'),
91 'chkall_title' => $main::locale->text('Import all'),
94 $self->render('file/import_dialog',
101 sub action_ajax_import {
103 $::auth->assert($self->object_right);
104 my $ids = $::form->{ids};
105 my $source = $::form->{source};
106 my $path = $::form->{path};
107 my @files = $self->_get_from_import($path);
108 foreach my $filename (@{ $::form->{$ids} || [] }) {
109 my ($file, undef) = grep { $_->{name} eq $filename } @files;
111 my $obj = SL::File->save(object_id => $self->object_id,
112 object_type => $self->object_type,
113 mime_type => 'application/pdf',
115 file_type => 'document',
116 file_name => $file->{filename},
117 file_path => $file->{path}
119 unlink($file->{path}) if $obj;
125 sub action_ajax_delete {
127 $self->_delete_all(DO_DELETE, $::locale->text('Following files are deleted:'));
130 sub action_ajax_unimport {
132 $self->_delete_all(DO_UNIMPORT, $::locale->text('Following files are unimported:'));
135 sub action_ajax_rename {
137 my $file = SL::File->get(id => $::form->{id});
139 $self->js->flash('error', $::locale->text('File not exists !'))->render();
142 my $sessionfile = $::form->{sessionfile};
143 if ( $sessionfile && -f $sessionfile ) {
145 if ( $::form->{to} eq $file->file_name ) {
146 # no rename so use as new version
147 $file->save_file($sessionfile);
148 $self->js->flash('warning', $::locale->text('File \'#1\' is used as new Version !', $file->file_name));
151 # new filename, so it is a new file with the same attributes as the old file
153 SL::File->save(object_id => $file->object_id,
154 object_type => $file->object_type,
155 mime_type => $file->mime_type,
156 source => $file->source,
157 file_type => $file->file_type,
158 file_name => $::form->{to},
159 file_path => $sessionfile
161 unlink($sessionfile);
164 $self->js->flash( 'error', t8('internal error (see details)'))
165 ->flash_detail('error', $@)->render;
175 $result = $file->rename($::form->{to});
178 $self->js->flash( 'error', t8('internal error (see details)'))
179 ->flash_detail('error', $@)->render;
183 if ($result != SL::File::RENAME_OK) {
184 $self->js->flash('error',
185 $result == SL::File::RENAME_EXISTS ? $::locale->text('File still exists !')
186 : $result == SL::File::RENAME_SAME ? $::locale->text('Same Filename !')
187 : $::locale->text('File not exists !'))
192 $self->is_global($::form->{is_global});
193 $self->file_type( $file->file_type);
194 $self->object_type($file->object_type);
195 $self->object_id( $file->object_id);
196 #$self->object_model($file_types{$file->module}->{model});
197 #$self->object_right($file_types{$file->module}->{right});
198 if ( $::form->{next_ids} ) {
199 my @existing = split(/,/, $::form->{next_ids});
200 $self->existing(\@existing);
205 sub action_ajax_upload {
207 $self->{maxsize} = $::instance_conf->get_doc_max_filesize;
208 $self->{accept_types} = '';
209 $self->{accept_types} = 'image/png,image/gif,image/jpeg,image/tiff,*png,*gif,*.jpg,*.tif' if $self->{file_type} eq 'image';
210 $self->render('file/upload_dialog',
216 sub action_ajax_files_uploaded {
219 my $source = 'uploaded';
221 if ( $::form->{ATTACHMENTS}->{uploadfiles} ) {
222 my @upfiles = @{ $::form->{ATTACHMENTS}->{uploadfiles} };
223 foreach my $idx (0 .. scalar(@upfiles) - 1) {
225 my $fname = uri_unescape($upfiles[$idx]->{filename});
226 # normalize and find basename
227 # first split with unix rules
228 # after that split with windows rules
229 my ($volume, $directories, $basefile) = File::Spec::Unix->splitpath($fname);
230 ($volume, $directories, $basefile) = File::Spec::Win32->splitpath($basefile);
232 # to find real mime_type by magic we must save the filedata
234 my $sess_fname = "file_upload_" . $self->object_type . "_" . $self->object_id . "_" . $idx;
235 my $sfile = SL::SessionFile->new($sess_fname, mode => 'w');
237 $sfile->fh->print(${$upfiles[$idx]->{data}});
239 my $mime_type = File::MimeInfo::Magic::magic($sfile->file_name);
242 # if filename has the suffix "pdf", but isn't really a pdf, set mimetype for no suffix
243 $mime_type = File::MimeInfo::Magic::mimetype($basefile);
244 $mime_type = 'application/octet-stream' if $mime_type eq 'application/pdf' || !$mime_type;
246 if ( $self->file_type eq 'image' && $self->file_probe_image_type($mime_type, $basefile)) {
249 my ($existobj) = SL::File->get_all(object_id => $self->object_id,
250 object_type => $self->object_type,
251 mime_type => $mime_type,
253 file_type => $self->file_type,
254 file_name => $basefile,
258 push @existing, $existobj->id.'_'.$sfile->file_name;
260 my $fileobj = SL::File->save(object_id => $self->object_id,
261 object_type => $self->object_type,
262 mime_type => $mime_type,
264 file_type => $self->file_type,
265 file_name => $basefile,
266 ## two possibilities: which is better ? content or sessionfile ??
267 #file_contents => ${$upfiles[$idx]->{data}},
268 file_path => $sfile->file_name
270 unlink($sfile->file_name);
274 $self->js->flash( 'error', t8('internal error (see details)'))
275 ->flash_detail('error', $@)->render;
280 $self->existing(\@existing);
284 sub action_download {
286 my ($id, $version) = split /_/, $::form->{id};
287 my $file = SL::File->get(id => $id );
288 $file->version($version) if $version;
289 my $ref = $file->get_content;
290 if ( $file && $ref ) {
291 return $self->send_file($ref,
292 type => $file->mime_type,
293 name => $file->file_name,
302 sub check_object_params {
305 my $id = ($::form->{object_id} // 0) * 1;
306 my $draftid = ($::form->{draft_id} // 0) * 1;
310 if ( $draftid == 0 && $id == 0 && $::form->{is_global} ) {
312 $type = $::form->{object_type};
315 $id = $::form->{draft_id};
317 } elsif ( $::form->{object_type} ) {
318 $type = $::form->{object_type};
320 die "No object type" unless $type;
321 die "No file type" unless $::form->{file_type};
322 die "Unkown object type" unless $file_types{$type};
324 $self->is_global($gldoc);
325 $self->file_type($::form->{file_type});
326 $self->object_type($type);
327 $self->object_id($id);
328 $self->object_model($file_types{$type}->{model});
329 $self->object_right($file_types{$type}->{right});
331 # $::auth->assert($self->object_right);
333 # my $model = 'SL::DB::' . $self->object_model;
334 # $self->object($model->new(id => $self->object_id)->load || die "Record not found");
344 my ($self, $do_unimport, $infotext) = @_;
346 my $ids = $::form->{ids};
347 foreach my $id_version (@{ $::form->{$ids} || [] }) {
348 my ($id, $version) = split /_/, $id_version;
349 my $dbfile = SL::File->get(id => $id);
350 $dbfile->version($version) if $dbfile && $version;
351 if ( $dbfile && $dbfile->delete ) {
352 $files .= ' ' . $dbfile->file_name;
355 $self->js->flash('info', $infotext . $files) if $files;
360 my ($self, $json) = @_;
362 if ( $self->file_type eq 'document' ) {
364 push @object_types, $self->object_type;
365 push @object_types, qw(dunning dunning1 dunning2 dunning3) if $self->object_type eq 'invoice'; # hardcoded object types?
366 @files = SL::File->get_all_versions(object_id => $self->object_id,
367 object_type => \@object_types,
368 file_type => $self->file_type,
372 elsif ( $self->file_type eq 'attachment' || $self->file_type eq 'image' ) {
373 @files = SL::File->get_all(object_id => $self->object_id,
374 object_type => $self->object_type,
375 file_type => $self->file_type,
378 $self->files(\@files);
379 $self->_mk_render('file/list', 1, 0, $json);
382 sub _get_from_import {
383 my ($self, $path) = @_;
386 my $language = $::lx_office_conf{system}->{language};
387 my $timezone = $::locale->get_local_time_zone()->name;
388 if (opendir my $dir, $path) {
389 my @files = ( readdir $dir);
390 foreach my $file ( @files) {
391 next if (($file eq '.') || ($file eq '..'));
392 $file = Encode::decode('utf-8', $file);
394 next if ( -d "$path/$file" );
396 my $tmppath = File::Spec->catfile( $path, $file );
397 next if( ! -f $tmppath );
399 my $st = stat($tmppath);
400 my $dt = DateTime->from_epoch( epoch => $st->mtime, time_zone => $timezone, locale => $language );
401 my $sname = $main::locale->quote_special_chars('HTML', $file);
404 'filename' => $sname,
406 'mtime' => $st->mtime,
407 'date' => $dt->dmy('.') . " " . $dt->hms,
416 my ($self, $template, $edit, $scanner, $json) = @_;
419 ##TODO make code configurable
422 my @sources = $self->_get_sources();
423 foreach my $source ( @sources ) {
424 @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files };
426 if ( $self->file_type eq 'document' ) {
427 $title = $main::locale->text('Documents');
428 } elsif ( $self->file_type eq 'attachment' ) {
429 $title = $main::locale->text('Attachments');
430 } elsif ( $self->file_type eq 'image' ) {
431 $title = $main::locale->text('Images');
434 my $output = SL::Presenter->get->render(
437 SOURCES => \@sources,
438 edit_attachments => $edit,
439 object_type => $self->object_type,
440 object_id => $self->object_id,
441 file_type => $self->file_type,
442 is_global => $self->is_global,
446 $self->js->html('#'.$self->file_type.'_list_'.$self->object_type, $output);
447 if ( $self->existing && scalar(@{$self->existing}) > 0) {
448 my $first = shift @{$self->existing};
449 my ($first_id, $sfile) = split('_', $first, 2);
450 my $file = SL::File->get(id => $first_id );
451 $self->js->run('kivi.File.askForRename', $first_id, $file->file_name, $sfile, join (',', @{$self->existing}), $self->is_global);
455 $self->render(\$output, { layout => 0, process => 0 });
460 $self->js->flash( 'error', t8('internal error (see details)'))
461 ->flash_detail('error', $@)->render;
463 $self->render('generic/error', { layout => 0 }, label_error => $@);
472 if ( $self->file_type eq 'document' ) {
473 # TODO statt gen neue attribute in filetypes :
474 if (($file_types{$self->object_type}->{gen}*1 & 1)==1) {
477 'title' => $main::locale->text('generated Files'),
478 'chk_action' => 'documents_delete',
479 'chk_title' => $main::locale->text('Delete Documents'),
480 'chkall_title' => $main::locale->text('Delete all'),
481 'file_title' => $main::locale->text('filename'),
482 'confirm_text' => $main::locale->text('delete'),
484 'rename_title' => $main::locale->text('Rename Documents'),
485 'done_text' => $main::locale->text('deleted')
487 push @sources , $gendata;
489 if (($file_types{$self->object_type}->{gen}*1 & 2)==2) {
490 my @others = SL::File->get_other_sources();
491 foreach my $scanner_or_mailrx (@others) {
493 'name' => $scanner_or_mailrx->{name},
494 'title' => $main::locale->text('from \'#1\' imported Files', $scanner_or_mailrx->{description}),
495 'chk_action' => $scanner_or_mailrx->{name}.'_unimport',
496 'chk_title' => $main::locale->text('Unimport documents'),
497 'chkall_title' => $main::locale->text('Unimport all'),
498 'file_title' => $main::locale->text('filename'),
499 'confirm_text' => $main::locale->text('unimport'),
501 'rename_title' => $main::locale->text('Rename Documents'),
503 'import_title' => $main::locale->text('Add Document from \'#1\'', $scanner_or_mailrx->{name}),
504 'path' => $scanner_or_mailrx->{directory},
505 'done_text' => $main::locale->text('unimported')
507 push @sources , $other;
511 elsif ( $self->file_type eq 'attachment' ) {
513 'name' => 'uploaded',
514 'title' => $main::locale->text(''),
515 'chk_action' => 'attachments_delete',
516 'chk_title' => $main::locale->text('Delete Attachments'),
517 'chkall_title' => $main::locale->text('Delete all'),
518 'file_title' => $main::locale->text('filename'),
519 'confirm_text' => $main::locale->text('delete'),
521 'are_existing' => $self->existing ? 1 : 0,
522 'rename_title' => $main::locale->text('Rename Attachments'),
524 'upload_title' => $main::locale->text('Upload Attachments'),
525 'done_text' => $main::locale->text('deleted')
527 push @sources , $attdata;
529 elsif ( $self->file_type eq 'image' ) {
531 'name' => 'uploaded',
532 'title' => $main::locale->text(''),
533 'chk_action' => 'images_delete',
534 'chk_title' => $main::locale->text('Delete Images'),
535 'chkall_title' => $main::locale->text('Delete all'),
536 'file_title' => $main::locale->text('filename'),
537 'confirm_text' => $main::locale->text('delete'),
539 'are_existing' => $self->existing ? 1 : 0,
540 'rename_title' => $main::locale->text('Rename Images'),
542 'upload_title' => $main::locale->text('Upload Images'),
543 'done_text' => $main::locale->text('deleted')
545 push @sources , $attdata;
560 SL::Controller::File - Controller for managing files
564 The Controller is called directly from the webpages
566 <a href="controller.pl?action=File/list&file_type=document\
567 &object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">
570 or indirectly via javascript functions from js/kivi.File.js
572 kivi.popup_dialog({ url: 'controller.pl',
573 data: { action : 'File/ajax_upload',
574 file_type : 'uploaded',
582 This is a controller for handling files in a storage independent way.
583 The storage may be a Filesystem,a WebDAV, a Database or DMS.
584 These backends must be configered in ClientConfig.
585 This Controller use as intermediate layer for storage C<SL::File>.
587 The Controller is responsible to display forms for displaying the files at the ERP-objects and
588 for uploading and downloading the files.
590 More description of the intermediate layer see L<SL::File>.
594 =head2 C<action_list>
596 This loads a list of files on a webpage. This can be done with a normal submit or via an ajax/json call.
597 Dependent of file_type different sources are available.
599 For documents there are the 'created' source and the imports from scanners or email.
600 For attachments and images only the 'uploaded' source available.
602 Available C<FORM PARAMS>:
606 =item C<form.object_id>
608 The Id of the ERP-object.
610 =item C<form.object_type>
612 The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
614 =item C<form.file_type>
616 For one ERP-object may exists different type of documents the type may be "documents","attachments" or "images".
617 This file_type is a filter for the list.
621 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.
626 =head2 C<action_ajax_upload>
629 A new file or more files can selected by a dialog and insert into the system.
632 Available C<FORM PARAMS>:
636 =item C<form.file_type>
638 This parameter describe here the source for a new file :
639 "attachments" and "images"
641 This is a normal upload selection, which may be more then one file to upload.
643 =item C<form.object_id>
647 =item C<form.object_type>
649 are the same as at C<action_list>
653 =head2 C<action_ajax_files_uploaded>
655 The Upload of selected Files. The "multipart_formdata" is parsed in SL::Request into the formsvariable "form.ATTACHMENTS".
656 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?).
657 If the same filename still exists at this object after the download for each existing filename a rename dialog will be opened.
659 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.
661 Available C<FORM PARAMS>:
665 =item C<form.ATTACHMENTS.uploadfiles>
667 This is an array of elements which have {filename} for the name and {data} for the contents.
669 Also object_id, object_type and file_type
673 =head2 C<action_download>
675 This is the real download of a file normally called via javascript "$.download("controller.pl", data);"
677 Available C<FORM PARAMS>:
681 Also object_id, object_type and file_type
685 =head2 C<action_ajax_importdialog>
687 A Dialog with all available and not imported files to import is open.
688 More then one file can be selected.
690 Available C<FORM PARAMS>:
696 The name of the source like "scanner1" or "email"
700 The full path to the directory on the server, where the files to import can found
702 Also object_id, object_type and file_type
706 =head2 C<action_ajax_delete>
708 Some files can be deleted
710 Available C<FORM PARAMS>:
716 The ids of the files to delete. Only this files are deleted not all versions of a file if the exists
720 =head2 C<action_ajax_unimport>
722 Some files can be unimported, dependent of the source of the file. This means they are moved
723 back to the directory of the source
725 Available C<FORM PARAMS>:
731 The ids of the files to unimport. Only these files are unimported not all versions of a file if the exists
735 =head2 C<action_ajax_rename>
737 One file can be renamed. There can be some checks if the same filename still exists at one object.
741 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>