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) ]);
46 # gen: bitmask: bit 1 (value is 1 or 3) => file created
47 # bit 2 (value is 2 or 3) => file from other source
48 # gltype: is this used somewhere?
49 # dir: is this used somewhere?
50 # model: base name of the rose model
51 # right: access right used for import
53 'sales_quotation' => { gen => 1, gltype => '', dir =>'SalesQuotation', model => 'Order', right => 'import_ar' },
54 'sales_order' => { gen => 1, gltype => '', dir =>'SalesOrder', model => 'Order', right => 'import_ar' },
55 'sales_delivery_order' => { gen => 1, gltype => '', dir =>'SalesDeliveryOrder', model => 'DeliveryOrder', right => 'import_ar' },
56 'invoice' => { gen => 1, gltype => 'ar', dir =>'SalesInvoice', model => 'Invoice', right => 'import_ar' },
57 'credit_note' => { gen => 1, gltype => '', dir =>'CreditNote', model => 'Invoice', right => 'import_ar' },
58 'request_quotation' => { gen => 3, gltype => '', dir =>'RequestForQuotation', model => 'Order', right => 'import_ap' },
59 'purchase_order' => { gen => 3, gltype => '', dir =>'PurchaseOrder', model => 'Order', right => 'import_ap' },
60 'purchase_delivery_order' => { gen => 3, gltype => '', dir =>'PurchaseDeliveryOrder',model => 'DeliveryOrder', right => 'import_ap' },
61 'purchase_invoice' => { gen => 2, gltype => 'ap', dir =>'PurchaseInvoice', model => 'PurchaseInvoice',right => 'import_ap' },
62 'vendor' => { gen => 0, gltype => '', dir =>'Vendor', model => 'Vendor', right => 'xx' },
63 'customer' => { gen => 1, gltype => '', dir =>'Customer', model => 'Customer', right => 'xx' },
64 'part' => { gen => 0, gltype => '', dir =>'Part', model => 'Part', right => 'xx' },
65 'gl_transaction' => { gen => 2, gltype => 'gl', dir =>'GeneralLedger', model => 'GLTransaction', right => 'import_ap' },
66 'draft' => { gen => 0, gltype => '', dir =>'Draft', model => 'Draft', right => 'xx' },
67 'csv_customer' => { gen => 1, gltype => '', dir =>'Reports', model => 'Customer', right => 'xx' },
68 'csv_vendor' => { gen => 1, gltype => '', dir =>'Reports', model => 'Vendor', right => 'xx' },
69 'shop_image' => { gen => 0, gltype => '', dir =>'ShopImages', model => 'Part', right => 'xx' },
70 'letter' => { gen => 3, gltype => '', dir =>'Letter', model => 'Letter', right => 'sales_letter_edit | purchase_letter_edit' },
74 # $main::locale->text('imported')
84 $is_json = 1 if $::form->{json};
86 $self->_do_list($is_json);
89 sub action_ajax_importdialog {
91 $::auth->assert($self->object_right);
92 my $path = $::form->{path};
93 my @files = $self->_get_from_import($path);
95 'name' => $::form->{source},
97 'chk_action' => $::form->{source}.'_import',
98 'chk_title' => $main::locale->text('Import scanned documents'),
99 'chkall_title' => $main::locale->text('Import all'),
102 $self->render('file/import_dialog',
109 sub action_ajax_import {
111 $::auth->assert($self->object_right);
112 my $ids = $::form->{ids};
113 my $source = $::form->{source};
114 my $path = $::form->{path};
115 my @files = $self->_get_from_import($path);
116 foreach my $filename (@{ $::form->{$ids} || [] }) {
117 my ($file, undef) = grep { $_->{name} eq $filename } @files;
119 my $obj = SL::File->save(object_id => $self->object_id,
120 object_type => $self->object_type,
121 mime_type => 'application/pdf',
123 file_type => 'document',
124 file_name => $file->{filename},
125 file_path => $file->{path}
127 unlink($file->{path}) if $obj;
133 sub action_ajax_delete {
135 $self->_delete_all(DO_DELETE, $::locale->text('Following files are deleted:'));
138 sub action_ajax_unimport {
140 $self->_delete_all(DO_UNIMPORT, $::locale->text('Following files are unimported:'));
143 sub action_ajax_rename {
145 my ($id, $version) = split /_/, $::form->{id};
146 my $file = SL::File->get(id => $id);
148 $self->js->flash('error', $::locale->text('File not exists !'))->render();
151 my $sessionfile = $::form->{sessionfile};
152 if ( $sessionfile && -f $sessionfile ) {
154 if ( $::form->{to} eq $file->file_name ) {
155 # no rename so use as new version
156 $file->save_file($sessionfile);
157 $self->js->flash('warning', $::locale->text('File \'#1\' is used as new Version !', $file->file_name));
160 # new filename, so it is a new file with the same attributes as the old file
162 SL::File->save(object_id => $file->object_id,
163 object_type => $file->object_type,
164 mime_type => $file->mime_type,
165 source => $file->source,
166 file_type => $file->file_type,
167 file_name => $::form->{to},
168 file_path => $sessionfile
170 unlink($sessionfile);
173 $self->js->flash( 'error', t8('internal error (see details)'))
174 ->flash_detail('error', $@)->render;
184 $result = $file->rename($::form->{to});
187 $self->js->flash( 'error', t8('internal error (see details)'))
188 ->flash_detail('error', $@)->render;
192 if ($result != SL::File::RENAME_OK) {
193 $self->js->flash('error',
194 $result == SL::File::RENAME_EXISTS ? $::locale->text('File still exists !')
195 : $result == SL::File::RENAME_SAME ? $::locale->text('Same Filename !')
196 : $::locale->text('File not exists !'))
201 $self->is_global($::form->{is_global});
202 $self->file_type( $file->file_type);
203 $self->object_type($file->object_type);
204 $self->object_id( $file->object_id);
205 #$self->object_model($file_types{$file->module}->{model});
206 #$self->object_right($file_types{$file->module}->{right});
207 if ( $::form->{next_ids} ) {
208 my @existing = split(/,/, $::form->{next_ids});
209 $self->existing(\@existing);
214 sub action_ajax_upload {
216 $self->{maxsize} = $::instance_conf->get_doc_max_filesize;
217 $self->{accept_types} = '';
218 $self->{accept_types} = 'image/png,image/gif,image/jpeg,image/tiff,*png,*gif,*.jpg,*.tif' if $self->{file_type} eq 'image';
219 $self->render('file/upload_dialog',
225 sub action_ajax_files_uploaded {
228 my $source = 'uploaded';
230 if ( $::form->{ATTACHMENTS}->{uploadfiles} ) {
231 my @upfiles = @{ $::form->{ATTACHMENTS}->{uploadfiles} };
232 foreach my $idx (0 .. scalar(@upfiles) - 1) {
234 my $fname = uri_unescape($upfiles[$idx]->{filename});
235 # normalize and find basename
236 # first split with unix rules
237 # after that split with windows rules
238 my ($volume, $directories, $basefile) = File::Spec::Unix->splitpath($fname);
239 ($volume, $directories, $basefile) = File::Spec::Win32->splitpath($basefile);
241 # to find real mime_type by magic we must save the filedata
243 my $sess_fname = "file_upload_" . $self->object_type . "_" . $self->object_id . "_" . $idx;
244 my $sfile = SL::SessionFile->new($sess_fname, mode => 'w');
246 $sfile->fh->print(${$upfiles[$idx]->{data}});
248 my $mime_type = File::MimeInfo::Magic::magic($sfile->file_name);
251 # if filename has the suffix "pdf", but isn't really a pdf, set mimetype for no suffix
252 $mime_type = File::MimeInfo::Magic::mimetype($basefile);
253 $mime_type = 'application/octet-stream' if $mime_type eq 'application/pdf' || !$mime_type;
255 if ( $self->file_type eq 'image' && $self->file_probe_image_type($mime_type, $basefile)) {
258 my ($existobj) = SL::File->get_all(object_id => $self->object_id,
259 object_type => $self->object_type,
260 mime_type => $mime_type,
262 file_type => $self->file_type,
263 file_name => $basefile,
267 push @existing, $existobj->id.'_'.$sfile->file_name;
269 my $fileobj = SL::File->save(object_id => $self->object_id,
270 object_type => $self->object_type,
271 mime_type => $mime_type,
273 file_type => $self->file_type,
274 file_name => $basefile,
275 title => $::form->{title},
276 description => $::form->{description},
277 ## two possibilities: what is better ? content or sessionfile ??
278 file_contents => ${$upfiles[$idx]->{data}},
279 file_path => $sfile->file_name
281 unlink($sfile->file_name);
285 $self->js->flash( 'error', t8('internal error (see details)'))
286 ->flash_detail('error', $@)->render;
291 $self->existing(\@existing);
295 sub action_download {
297 my ($id, $version) = split /_/, $::form->{id};
298 my $file = SL::File->get(id => $id );
299 $file->version($version) if $version;
300 my $ref = $file->get_content;
301 if ( $file && $ref ) {
302 return $self->send_file($ref,
303 type => $file->mime_type,
304 name => $file->file_name,
313 sub check_object_params {
316 my $id = ($::form->{object_id} // 0) * 1;
317 my $draftid = ($::form->{draft_id} // 0) * 1;
321 if ( $draftid == 0 && $id == 0 && $::form->{is_global} ) {
323 $type = $::form->{object_type};
326 $id = $::form->{draft_id};
328 } elsif ( $::form->{object_type} ) {
329 $type = $::form->{object_type};
331 die "No object type" unless $type;
332 die "No file type" unless $::form->{file_type};
333 die "Unknown object type" unless $file_types{$type};
335 $self->is_global($gldoc);
336 $self->file_type($::form->{file_type});
337 $self->object_type($type);
338 $self->object_id($id);
339 $self->object_model($file_types{$type}->{model});
340 $self->object_right($file_types{$type}->{right});
342 # $::auth->assert($self->object_right);
344 # my $model = 'SL::DB::' . $self->object_model;
345 # $self->object($model->new(id => $self->object_id)->load || die "Record not found");
355 my ($self, $do_unimport, $infotext) = @_;
357 my $ids = $::form->{ids};
358 foreach my $id_version (@{ $::form->{$ids} || [] }) {
359 my ($id, $version) = split /_/, $id_version;
360 my $dbfile = SL::File->get(id => $id);
363 $dbfile->version($version);
364 $files .= ' ' . $dbfile->file_name if $dbfile->delete_version;
366 $files .= ' ' . $dbfile->file_name if $dbfile->delete;
370 $self->js->flash('info', $infotext . $files) if $files;
375 my ($self, $json) = @_;
377 if ( $self->file_type eq 'document' ) {
379 push @object_types, $self->object_type;
380 push @object_types, qw(dunning dunning1 dunning2 dunning3) if $self->object_type eq 'invoice'; # hardcoded object types?
381 @files = SL::File->get_all_versions(object_id => $self->object_id,
382 object_type => \@object_types,
383 file_type => $self->file_type,
387 elsif ( $self->file_type eq 'attachment' || $self->file_type eq 'image' ) {
388 @files = SL::File->get_all(object_id => $self->object_id,
389 object_type => $self->object_type,
390 file_type => $self->file_type,
393 $self->files(\@files);
395 if($self->object_type eq 'shop_image'){
397 ->run('kivi.ShopPart.show_images', $self->object_id)
400 $self->_mk_render('file/list', 1, 0, $json);
404 sub _get_from_import {
405 my ($self, $path) = @_;
408 my $language = $::lx_office_conf{system}->{language};
409 my $timezone = $::locale->get_local_time_zone()->name;
410 if (opendir my $dir, $path) {
411 my @files = ( readdir $dir);
412 foreach my $file ( @files) {
413 next if (($file eq '.') || ($file eq '..'));
414 $file = Encode::decode('utf-8', $file);
416 next if ( -d "$path/$file" );
418 my $tmppath = File::Spec->catfile( $path, $file );
419 next if( ! -f $tmppath );
421 my $st = stat($tmppath);
422 my $dt = DateTime->from_epoch( epoch => $st->mtime, time_zone => $timezone, locale => $language );
423 my $sname = $main::locale->quote_special_chars('HTML', $file);
426 'filename' => $sname,
428 'mtime' => $st->mtime,
429 'date' => $dt->dmy('.') . " " . $dt->hms,
438 my ($self, $template, $edit, $scanner, $json) = @_;
441 ##TODO make code configurable
444 my @sources = $self->_get_sources();
445 foreach my $source ( @sources ) {
446 @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files };
448 if ( $self->file_type eq 'document' ) {
449 $title = $main::locale->text('Documents');
450 } elsif ( $self->file_type eq 'attachment' ) {
451 $title = $main::locale->text('Attachments');
452 } elsif ( $self->file_type eq 'image' ) {
453 $title = $main::locale->text('Images');
456 my $output = SL::Presenter->get->render(
459 SOURCES => \@sources,
460 edit_attachments => $edit,
461 object_type => $self->object_type,
462 object_id => $self->object_id,
463 file_type => $self->file_type,
464 is_global => $self->is_global,
468 $self->js->html('#'.$self->file_type.'_list_'.$self->object_type, $output);
469 if ( $self->existing && scalar(@{$self->existing}) > 0) {
470 my $first = shift @{$self->existing};
471 my ($first_id, $sfile) = split('_', $first, 2);
472 my $file = SL::File->get(id => $first_id );
473 $self->js->run('kivi.File.askForRename', $first_id, $file->file_type, $file->file_name, $sfile, join (',', @{$self->existing}), $self->is_global);
477 $self->render(\$output, { layout => 0, process => 0 });
482 $self->js->flash( 'error', t8('internal error (see details)'))
483 ->flash_detail('error', $@)->render;
485 $self->render('generic/error', { layout => 0 }, label_error => $@);
494 if ( $self->file_type eq 'document' ) {
495 # TODO statt gen neue attribute in filetypes :
496 if (($file_types{$self->object_type}->{gen}*1 & 1)==1) {
499 'title' => $main::locale->text('generated Files'),
500 'chk_action' => 'documents_delete',
501 'chk_title' => $main::locale->text('Delete Documents'),
502 'chkall_title' => $main::locale->text('Delete all'),
503 'file_title' => $main::locale->text('filename'),
504 'confirm_text' => $main::locale->text('delete'),
505 'can_delete' => $::instance_conf->get_doc_delete_printfiles,
506 'can_rename' => $::instance_conf->get_doc_delete_printfiles,
507 'rename_title' => $main::locale->text('Rename Documents'),
508 'done_text' => $main::locale->text('deleted')
510 push @sources , $gendata;
512 if (($file_types{$self->object_type}->{gen}*1 & 2)==2) {
513 my @others = SL::File->get_other_sources();
514 foreach my $scanner_or_mailrx (@others) {
516 'name' => $scanner_or_mailrx->{name},
517 'title' => $main::locale->text('from \'#1\' imported Files', $scanner_or_mailrx->{description}),
518 'chk_action' => $scanner_or_mailrx->{name}.'_unimport',
519 'chk_title' => $main::locale->text('Unimport documents'),
520 'chkall_title' => $main::locale->text('Unimport all'),
521 'file_title' => $main::locale->text('filename'),
522 'confirm_text' => $main::locale->text('unimport'),
524 'rename_title' => $main::locale->text('Rename Documents'),
527 'import_title' => $main::locale->text('Add Document from \'#1\'', $scanner_or_mailrx->{name}),
528 'path' => $scanner_or_mailrx->{directory},
529 'done_text' => $main::locale->text('unimported')
531 push @sources , $other;
535 elsif ( $self->file_type eq 'attachment' ) {
537 'name' => 'uploaded',
538 'title' => $main::locale->text(''),
539 'chk_action' => 'attachments_delete',
540 'chk_title' => $main::locale->text('Delete Attachments'),
541 'chkall_title' => $main::locale->text('Delete all'),
542 'file_title' => $main::locale->text('filename'),
543 'confirm_text' => $main::locale->text('delete'),
545 'are_existing' => $self->existing ? 1 : 0,
546 'rename_title' => $main::locale->text('Rename Attachments'),
549 'upload_title' => $main::locale->text('Upload Attachments'),
550 'done_text' => $main::locale->text('deleted')
552 push @sources , $attdata;
554 elsif ( $self->file_type eq 'image' ) {
556 'name' => 'uploaded',
557 'title' => $main::locale->text(''),
558 'chk_action' => 'images_delete',
559 'chk_title' => $main::locale->text('Delete Images'),
560 'chkall_title' => $main::locale->text('Delete all'),
561 'file_title' => $main::locale->text('filename'),
562 'confirm_text' => $main::locale->text('delete'),
564 'are_existing' => $self->existing ? 1 : 0,
565 'rename_title' => $main::locale->text('Rename Images'),
568 'upload_title' => $main::locale->text('Upload Images'),
569 'done_text' => $main::locale->text('deleted')
571 push @sources , $attdata;
586 SL::Controller::File - Controller for managing files
590 The Controller is called directly from the webpages
592 <a href="controller.pl?action=File/list&file_type=document\
593 &object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">
596 or indirectly via javascript functions from js/kivi.File.js
598 kivi.popup_dialog({ url: 'controller.pl',
599 data: { action : 'File/ajax_upload',
600 file_type : 'uploaded',
608 This is a controller for handling files in a storage independent way.
609 The storage may be a Filesystem,a WebDAV, a Database or DMS.
610 These backends must be configered in ClientConfig.
611 This Controller use as intermediate layer for storage C<SL::File>.
613 The Controller is responsible to display forms for displaying the files at the ERP-objects and
614 for uploading and downloading the files.
616 More description of the intermediate layer see L<SL::File>.
620 =head2 C<action_list>
622 This loads a list of files on a webpage. This can be done with a normal submit or via an ajax/json call.
623 Dependent of file_type different sources are available.
625 For documents there are the 'created' source and the imports from scanners or email.
626 For attachments and images only the 'uploaded' source available.
628 Available C<FORM PARAMS>:
632 =item C<form.object_id>
634 The Id of the ERP-object.
636 =item C<form.object_type>
638 The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
640 =item C<form.file_type>
642 For one ERP-object may exists different type of documents the type may be "documents","attachments" or "images".
643 This file_type is a filter for the list.
647 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.
652 =head2 C<action_ajax_upload>
655 A new file or more files can selected by a dialog and insert into the system.
658 Available C<FORM PARAMS>:
662 =item C<form.file_type>
664 This parameter describe here the source for a new file :
665 "attachments" and "images"
667 This is a normal upload selection, which may be more then one file to upload.
669 =item C<form.object_id>
673 =item C<form.object_type>
675 are the same as at C<action_list>
679 =head2 C<action_ajax_files_uploaded>
681 The Upload of selected Files. The "multipart_formdata" is parsed in SL::Request into the formsvariable "form.ATTACHMENTS".
682 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?).
683 If the same filename still exists at this object after the download for each existing filename a rename dialog will be opened.
685 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.
687 Available C<FORM PARAMS>:
691 =item C<form.ATTACHMENTS.uploadfiles>
693 This is an array of elements which have {filename} for the name and {data} for the contents.
695 Also object_id, object_type and file_type
699 =head2 C<action_download>
701 This is the real download of a file normally called via javascript "$.download("controller.pl", data);"
703 Available C<FORM PARAMS>:
707 Also object_id, object_type and file_type
711 =head2 C<action_ajax_importdialog>
713 A Dialog with all available and not imported files to import is open.
714 More then one file can be selected.
716 Available C<FORM PARAMS>:
722 The name of the source like "scanner1" or "email"
726 The full path to the directory on the server, where the files to import can found
728 Also object_id, object_type and file_type
732 =head2 C<action_ajax_delete>
734 Some files can be deleted
736 Available C<FORM PARAMS>:
742 The ids of the files to delete. Only this files are deleted not all versions of a file if the exists
746 =head2 C<action_ajax_unimport>
748 Some files can be unimported, dependent of the source of the file. This means they are moved
749 back to the directory of the source
751 Available C<FORM PARAMS>:
757 The ids of the files to unimport. Only these files are unimported not all versions of a file if the exists
761 =head2 C<action_ajax_rename>
763 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>