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, 3, 5 or 7) => file created
47 # bit 2 (value is 2, 3, 6 or 7) => file from other source (e.g. directory for scanned documents)
48 # bit 3 (value is 4, 5, 6 or 7) => upload as other source
49 # gltype: is this used somewhere?
50 # dir: is this used somewhere?
51 # model: base name of the rose model
52 # right: access right used for import
54 'sales_quotation' => { gen => 1, gltype => '', dir =>'SalesQuotation', model => 'Order', right => 'import_ar' },
55 'sales_order' => { gen => 5, gltype => '', dir =>'SalesOrder', model => 'Order', right => 'import_ar' },
56 'sales_delivery_order' => { gen => 1, gltype => '', dir =>'SalesDeliveryOrder', model => 'DeliveryOrder', right => 'import_ar' },
57 'invoice' => { gen => 1, gltype => 'ar', dir =>'SalesInvoice', model => 'Invoice', right => 'import_ar' },
58 'credit_note' => { gen => 1, gltype => '', dir =>'CreditNote', model => 'Invoice', right => 'import_ar' },
59 'request_quotation' => { gen => 7, gltype => '', dir =>'RequestForQuotation', model => 'Order', right => 'import_ap' },
60 'purchase_order' => { gen => 7, gltype => '', dir =>'PurchaseOrder', model => 'Order', right => 'import_ap' },
61 'purchase_delivery_order' => { gen => 7, gltype => '', dir =>'PurchaseDeliveryOrder',model => 'DeliveryOrder', right => 'import_ap' },
62 'purchase_invoice' => { gen => 6, gltype => 'ap', dir =>'PurchaseInvoice', model => 'PurchaseInvoice',right => 'import_ap' },
63 'vendor' => { gen => 0, gltype => '', dir =>'Vendor', model => 'Vendor', right => 'xx' },
64 'customer' => { gen => 1, gltype => '', dir =>'Customer', model => 'Customer', right => 'xx' },
65 'part' => { gen => 0, gltype => '', dir =>'Part', model => 'Part', right => 'xx' },
66 'gl_transaction' => { gen => 6, gltype => 'gl', dir =>'GeneralLedger', model => 'GLTransaction', right => 'import_ap' },
67 'draft' => { gen => 0, gltype => '', dir =>'Draft', model => 'Draft', right => 'xx' },
68 'csv_customer' => { gen => 1, gltype => '', dir =>'Reports', model => 'Customer', right => 'xx' },
69 'csv_vendor' => { gen => 1, gltype => '', dir =>'Reports', model => 'Vendor', right => 'xx' },
70 'shop_image' => { gen => 0, gltype => '', dir =>'ShopImages', model => 'Part', right => 'xx' },
71 'letter' => { gen => 7, gltype => '', dir =>'Letter', model => 'Letter', right => 'sales_letter_edit | purchase_letter_edit' },
75 # $main::locale->text('imported')
85 $is_json = 1 if $::form->{json};
87 $self->_do_list($is_json);
90 sub action_ajax_importdialog {
92 $::auth->assert($self->object_right);
93 my $path = $::form->{path};
94 my @files = $self->_get_from_import($path);
96 'name' => $::form->{source},
98 'chk_action' => $::form->{source}.'_import',
99 'chk_title' => $main::locale->text('Import scanned documents'),
100 'chkall_title' => $main::locale->text('Import all'),
103 $self->render('file/import_dialog',
110 sub action_ajax_import {
112 $::auth->assert($self->object_right);
113 my $ids = $::form->{ids};
114 my $source = $::form->{source};
115 my $path = $::form->{path};
116 my @files = $self->_get_from_import($path);
117 foreach my $filename (@{ $::form->{$ids} || [] }) {
118 my ($file, undef) = grep { $_->{name} eq $filename } @files;
120 my $obj = SL::File->save(object_id => $self->object_id,
121 object_type => $self->object_type,
122 mime_type => 'application/pdf',
124 file_type => 'document',
125 file_name => $file->{filename},
126 file_path => $file->{path}
128 unlink($file->{path}) if $obj;
134 sub action_ajax_delete {
136 $self->_delete_all(DO_DELETE, $::locale->text('Following files are deleted:'));
139 sub action_ajax_unimport {
141 $self->_delete_all(DO_UNIMPORT, $::locale->text('Following files are unimported:'));
144 sub action_ajax_rename {
146 my ($id, $version) = split /_/, $::form->{id};
147 my $file = SL::File->get(id => $id);
149 $self->js->flash('error', $::locale->text('File not exists !'))->render();
152 my $sessionfile = $::form->{sessionfile};
153 if ( $sessionfile && -f $sessionfile ) {
155 if ( $::form->{to} eq $file->file_name ) {
156 # no rename so use as new version
157 $file->save_file($sessionfile);
158 $self->js->flash('warning', $::locale->text('File \'#1\' is used as new Version !', $file->file_name));
161 # new filename, so it is a new file with the same attributes as the old file
163 SL::File->save(object_id => $file->object_id,
164 object_type => $file->object_type,
165 mime_type => $file->mime_type,
166 source => $file->source,
167 file_type => $file->file_type,
168 file_name => $::form->{to},
169 file_path => $sessionfile
171 unlink($sessionfile);
174 $self->js->flash( 'error', t8('internal error (see details)'))
175 ->flash_detail('error', $@)->render;
185 $result = $file->rename($::form->{to});
188 $self->js->flash( 'error', t8('internal error (see details)'))
189 ->flash_detail('error', $@)->render;
193 if ($result != SL::File::RENAME_OK) {
194 $self->js->flash('error',
195 $result == SL::File::RENAME_EXISTS ? $::locale->text('File still exists !')
196 : $result == SL::File::RENAME_SAME ? $::locale->text('Same Filename !')
197 : $::locale->text('File not exists !'))
202 $self->is_global($::form->{is_global});
203 $self->file_type( $file->file_type);
204 $self->object_type($file->object_type);
205 $self->object_id( $file->object_id);
206 #$self->object_model($file_types{$file->module}->{model});
207 #$self->object_right($file_types{$file->module}->{right});
208 if ( $::form->{next_ids} ) {
209 my @existing = split(/,/, $::form->{next_ids});
210 $self->existing(\@existing);
215 sub action_ajax_upload {
217 $self->{maxsize} = $::instance_conf->get_doc_max_filesize;
218 $self->{accept_types} = '';
219 $self->{accept_types} = 'image/png,image/gif,image/jpeg,image/tiff,*png,*gif,*.jpg,*.tif' if $self->{file_type} eq 'image';
220 $self->render('file/upload_dialog',
226 sub action_ajax_files_uploaded {
229 my $source = 'uploaded';
231 if ( $::form->{ATTACHMENTS}->{uploadfiles} ) {
232 my @upfiles = @{ $::form->{ATTACHMENTS}->{uploadfiles} };
233 foreach my $idx (0 .. scalar(@upfiles) - 1) {
235 my $fname = uri_unescape($upfiles[$idx]->{filename});
236 # normalize and find basename
237 # first split with unix rules
238 # after that split with windows rules
239 my ($volume, $directories, $basefile) = File::Spec::Unix->splitpath($fname);
240 ($volume, $directories, $basefile) = File::Spec::Win32->splitpath($basefile);
242 # to find real mime_type by magic we must save the filedata
244 my $sess_fname = "file_upload_" . $self->object_type . "_" . $self->object_id . "_" . $idx;
245 my $sfile = SL::SessionFile->new($sess_fname, mode => 'w');
247 $sfile->fh->print(${$upfiles[$idx]->{data}});
249 my $mime_type = File::MimeInfo::Magic::magic($sfile->file_name);
252 # if filename has the suffix "pdf", but isn't really a pdf, set mimetype for no suffix
253 $mime_type = File::MimeInfo::Magic::mimetype($basefile);
254 $mime_type = 'application/octet-stream' if $mime_type eq 'application/pdf' || !$mime_type;
256 if ( $self->file_type eq 'image' && $self->file_probe_image_type($mime_type, $basefile)) {
259 my ($existobj) = SL::File->get_all(object_id => $self->object_id,
260 object_type => $self->object_type,
261 mime_type => $mime_type,
263 file_type => $self->file_type,
264 file_name => $basefile,
268 push @existing, $existobj->id.'_'.$sfile->file_name;
270 my $fileobj = SL::File->save(object_id => $self->object_id,
271 object_type => $self->object_type,
272 mime_type => $mime_type,
274 file_type => $self->file_type,
275 file_name => $basefile,
276 title => $::form->{title},
277 description => $::form->{description},
278 ## two possibilities: what is better ? content or sessionfile ??
279 file_contents => ${$upfiles[$idx]->{data}},
280 file_path => $sfile->file_name
282 unlink($sfile->file_name);
286 $self->js->flash( 'error', t8('internal error (see details)'))
287 ->flash_detail('error', $@)->render;
292 $self->existing(\@existing);
296 sub action_download {
298 my ($id, $version) = split /_/, $::form->{id};
299 my $file = SL::File->get(id => $id );
300 $file->version($version) if $version;
301 my $ref = $file->get_content;
302 if ( $file && $ref ) {
303 return $self->send_file($ref,
304 type => $file->mime_type,
305 name => $file->file_name,
314 sub check_object_params {
317 my $id = ($::form->{object_id} // 0) * 1;
318 my $draftid = ($::form->{draft_id} // 0) * 1;
322 if ( $draftid == 0 && $id == 0 && $::form->{is_global} ) {
324 $type = $::form->{object_type};
327 $id = $::form->{draft_id};
329 } elsif ( $::form->{object_type} ) {
330 $type = $::form->{object_type};
332 die "No object type" unless $type;
333 die "No file type" unless $::form->{file_type};
334 die "Unknown object type" unless $file_types{$type};
336 $self->is_global($gldoc);
337 $self->file_type($::form->{file_type});
338 $self->object_type($type);
339 $self->object_id($id);
340 $self->object_model($file_types{$type}->{model});
341 $self->object_right($file_types{$type}->{right});
343 # $::auth->assert($self->object_right);
345 # my $model = 'SL::DB::' . $self->object_model;
346 # $self->object($model->new(id => $self->object_id)->load || die "Record not found");
356 my ($self, $do_unimport, $infotext) = @_;
358 my $ids = $::form->{ids};
359 foreach my $id_version (@{ $::form->{$ids} || [] }) {
360 my ($id, $version) = split /_/, $id_version;
361 my $dbfile = SL::File->get(id => $id);
364 $dbfile->version($version);
365 $files .= ' ' . $dbfile->file_name if $dbfile->delete_version;
367 $files .= ' ' . $dbfile->file_name if $dbfile->delete;
371 $self->js->flash('info', $infotext . $files) if $files;
376 my ($self, $json) = @_;
378 if ( $self->file_type eq 'document' ) {
380 push @object_types, $self->object_type;
381 push @object_types, qw(dunning dunning1 dunning2 dunning3) if $self->object_type eq 'invoice'; # hardcoded object types?
382 @files = SL::File->get_all_versions(object_id => $self->object_id,
383 object_type => \@object_types,
384 file_type => $self->file_type,
388 elsif ( $self->file_type eq 'attachment' || $self->file_type eq 'image' ) {
389 @files = SL::File->get_all(object_id => $self->object_id,
390 object_type => $self->object_type,
391 file_type => $self->file_type,
394 $self->files(\@files);
396 if($self->object_type eq 'shop_image'){
398 ->run('kivi.ShopPart.show_images', $self->object_id)
401 $self->_mk_render('file/list', 1, 0, $json);
405 sub _get_from_import {
406 my ($self, $path) = @_;
409 my $language = $::lx_office_conf{system}->{language};
410 my $timezone = $::locale->get_local_time_zone()->name;
411 if (opendir my $dir, $path) {
412 my @files = (readdir $dir);
413 foreach my $file ( @files) {
414 next if (($file eq '.') || ($file eq '..'));
415 $file = Encode::decode('utf-8', $file);
417 next if ( -d "$path/$file" );
419 my $tmppath = File::Spec->catfile( $path, $file );
420 next if( ! -f $tmppath );
422 my $st = stat($tmppath);
423 my $dt = DateTime->from_epoch( epoch => $st->mtime, time_zone => $timezone, locale => $language );
424 my $sname = $main::locale->quote_special_chars('HTML', $file);
427 'filename' => $sname,
429 'mtime' => $st->mtime,
430 'date' => $dt->dmy('.') . " " . $dt->hms,
437 $::lxdebug->message(LXDebug::WARN(), "SL::File::_get_from_import opendir failed to open dir " . $path);
444 my ($self, $template, $edit, $scanner, $json) = @_;
447 ##TODO make code configurable
450 my @sources = $self->_get_sources();
451 foreach my $source ( @sources ) {
452 @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files };
454 if ( $self->file_type eq 'document' ) {
455 $title = $main::locale->text('Documents');
456 } elsif ( $self->file_type eq 'attachment' ) {
457 $title = $main::locale->text('Attachments');
458 } elsif ( $self->file_type eq 'image' ) {
459 $title = $main::locale->text('Images');
462 my $output = SL::Presenter->get->render(
465 SOURCES => \@sources,
466 edit_attachments => $edit,
467 object_type => $self->object_type,
468 object_id => $self->object_id,
469 file_type => $self->file_type,
470 is_global => $self->is_global,
474 $self->js->html('#'.$self->file_type.'_list_'.$self->object_type, $output);
475 if ( $self->existing && scalar(@{$self->existing}) > 0) {
476 my $first = shift @{$self->existing};
477 my ($first_id, $sfile) = split('_', $first, 2);
478 my $file = SL::File->get(id => $first_id );
479 $self->js->run('kivi.File.askForRename', $first_id, $file->file_type, $file->file_name, $sfile, join (',', @{$self->existing}), $self->is_global);
483 $self->render(\$output, { layout => 0, process => 0 });
488 $self->js->flash( 'error', t8('internal error (see details)'))
489 ->flash_detail('error', $@)->render;
491 $self->render('generic/error', { layout => 0 }, label_error => $@);
500 if ( $self->file_type eq 'document' ) {
501 # TODO statt gen neue attribute in filetypes :
502 if (($file_types{$self->object_type}->{gen}*1 & 4)==4) {
503 # bit 3 is set => means upload
505 'name' => 'uploaded',
506 'title' => $main::locale->text('uploaded Documents'),
507 'chk_action' => 'uploaded_documents_delete',
508 'chk_title' => $main::locale->text('Delete Documents'),
509 'chkall_title' => $main::locale->text('Delete all'),
510 'file_title' => $main::locale->text('filename'),
511 'confirm_text' => $main::locale->text('delete'),
513 'are_existing' => $self->existing ? 1 : 0,
514 'rename_title' => $main::locale->text('Rename Attachments'),
517 'upload_title' => $main::locale->text('Upload Documents'),
518 'done_text' => $main::locale->text('deleted')
520 push @sources , $source;
523 if (($file_types{$self->object_type}->{gen}*1 & 1)==1) {
526 'title' => $main::locale->text('generated Files'),
527 'chk_action' => 'documents_delete',
528 'chk_title' => $main::locale->text('Delete Documents'),
529 'chkall_title' => $main::locale->text('Delete all'),
530 'file_title' => $main::locale->text('filename'),
531 'confirm_text' => $main::locale->text('delete'),
532 'can_delete' => $::instance_conf->get_doc_delete_printfiles,
533 'can_rename' => $::instance_conf->get_doc_delete_printfiles,
534 'rename_title' => $main::locale->text('Rename Documents'),
535 'done_text' => $main::locale->text('deleted')
537 push @sources , $gendata;
540 if (($file_types{$self->object_type}->{gen}*1 & 2)==2) {
541 my @others = SL::File->get_other_sources();
542 foreach my $scanner_or_mailrx (@others) {
544 'name' => $scanner_or_mailrx->{name},
545 'title' => $main::locale->text('from \'#1\' imported Files', $scanner_or_mailrx->{description}),
546 'chk_action' => $scanner_or_mailrx->{name}.'_unimport',
547 'chk_title' => $main::locale->text('Unimport documents'),
548 'chkall_title' => $main::locale->text('Unimport all'),
549 'file_title' => $main::locale->text('filename'),
550 'confirm_text' => $main::locale->text('unimport'),
552 'rename_title' => $main::locale->text('Rename Documents'),
555 'import_title' => $main::locale->text('Add Document from \'#1\'', $scanner_or_mailrx->{name}),
556 'path' => $scanner_or_mailrx->{directory},
557 'done_text' => $main::locale->text('unimported')
559 push @sources , $other;
563 elsif ( $self->file_type eq 'attachment' ) {
565 'name' => 'uploaded',
566 'title' => $main::locale->text(''),
567 'chk_action' => 'attachments_delete',
568 'chk_title' => $main::locale->text('Delete Attachments'),
569 'chkall_title' => $main::locale->text('Delete all'),
570 'file_title' => $main::locale->text('filename'),
571 'confirm_text' => $main::locale->text('delete'),
573 'are_existing' => $self->existing ? 1 : 0,
574 'rename_title' => $main::locale->text('Rename Attachments'),
577 'upload_title' => $main::locale->text('Upload Attachments'),
578 'done_text' => $main::locale->text('deleted')
580 push @sources , $attdata;
582 elsif ( $self->file_type eq 'image' ) {
584 'name' => 'uploaded',
585 'title' => $main::locale->text(''),
586 'chk_action' => 'images_delete',
587 'chk_title' => $main::locale->text('Delete Images'),
588 'chkall_title' => $main::locale->text('Delete all'),
589 'file_title' => $main::locale->text('filename'),
590 'confirm_text' => $main::locale->text('delete'),
592 'are_existing' => $self->existing ? 1 : 0,
593 'rename_title' => $main::locale->text('Rename Images'),
596 'upload_title' => $main::locale->text('Upload Images'),
597 'done_text' => $main::locale->text('deleted')
599 push @sources , $attdata;
614 SL::Controller::File - Controller for managing files
618 The Controller is called directly from the webpages
620 <a href="controller.pl?action=File/list&file_type=document\
621 &object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">
624 or indirectly via javascript functions from js/kivi.File.js
626 kivi.popup_dialog({ url: 'controller.pl',
627 data: { action : 'File/ajax_upload',
628 file_type : 'uploaded',
636 This is a controller for handling files in a storage independent way.
637 The storage may be a Filesystem,a WebDAV, a Database or DMS.
638 These backends must be configered in ClientConfig.
639 This Controller use as intermediate layer for storage C<SL::File>.
641 The Controller is responsible to display forms for displaying the files at the ERP-objects and
642 for uploading and downloading the files.
644 More description of the intermediate layer see L<SL::File>.
648 =head2 C<action_list>
650 This loads a list of files on a webpage. This can be done with a normal submit or via an ajax/json call.
651 Dependent of file_type different sources are available.
653 For documents there are the 'created' source and the imports from scanners or email.
654 For attachments and images only the 'uploaded' source available.
656 Available C<FORM PARAMS>:
660 =item C<form.object_id>
662 The Id of the ERP-object.
664 =item C<form.object_type>
666 The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
668 =item C<form.file_type>
670 For one ERP-object may exists different type of documents the type may be "documents","attachments" or "images".
671 This file_type is a filter for the list.
675 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.
680 =head2 C<action_ajax_upload>
683 A new file or more files can selected by a dialog and insert into the system.
686 Available C<FORM PARAMS>:
690 =item C<form.file_type>
692 This parameter describe here the source for a new file :
693 "attachments" and "images"
695 This is a normal upload selection, which may be more then one file to upload.
697 =item C<form.object_id>
701 =item C<form.object_type>
703 are the same as at C<action_list>
707 =head2 C<action_ajax_files_uploaded>
709 The Upload of selected Files. The "multipart_formdata" is parsed in SL::Request into the formsvariable "form.ATTACHMENTS".
710 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?).
711 If the same filename still exists at this object after the download for each existing filename a rename dialog will be opened.
713 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.
715 Available C<FORM PARAMS>:
719 =item C<form.ATTACHMENTS.uploadfiles>
721 This is an array of elements which have {filename} for the name and {data} for the contents.
723 Also object_id, object_type and file_type
727 =head2 C<action_download>
729 This is the real download of a file normally called via javascript "$.download("controller.pl", data);"
731 Available C<FORM PARAMS>:
735 Also object_id, object_type and file_type
739 =head2 C<action_ajax_importdialog>
741 A Dialog with all available and not imported files to import is open.
742 More then one file can be selected.
744 Available C<FORM PARAMS>:
750 The name of the source like "scanner1" or "email"
754 The full path to the directory on the server, where the files to import can found
756 Also object_id, object_type and file_type
760 =head2 C<action_ajax_delete>
762 Some files can be deleted
764 Available C<FORM PARAMS>:
770 The ids of the files to delete. Only this files are deleted not all versions of a file if the exists
774 =head2 C<action_ajax_unimport>
776 Some files can be unimported, dependent of the source of the file. This means they are moved
777 back to the directory of the source
779 Available C<FORM PARAMS>:
785 The ids of the files to unimport. Only these files are unimported not all versions of a file if the exists
789 =head2 C<action_ajax_rename>
791 One file can be renamed. There can be some checks if the same filename still exists at one object.
795 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>