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' },
63 'shop_image' => { gen => 0, gltype => '', dir =>'ShopImages', model => 'Part', right => 'xx' },
67 # $main::locale->text('imported')
77 $is_json = 1 if $::form->{json};
79 $self->_do_list($is_json);
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 my $sessionfile = $::form->{sessionfile};
144 if ( $sessionfile && -f $sessionfile ) {
146 if ( $::form->{to} eq $file->file_name ) {
147 # no rename so use as new version
148 $file->save_file($sessionfile);
149 $self->js->flash('warning', $::locale->text('File \'#1\' is used as new Version !', $file->file_name));
152 # new filename, so it is a new file with the same attributes as the old file
154 SL::File->save(object_id => $file->object_id,
155 object_type => $file->object_type,
156 mime_type => $file->mime_type,
157 source => $file->source,
158 file_type => $file->file_type,
159 file_name => $::form->{to},
160 file_path => $sessionfile
162 unlink($sessionfile);
165 $self->js->flash( 'error', t8('internal error (see details)'))
166 ->flash_detail('error', $@)->render;
176 $result = $file->rename($::form->{to});
179 $self->js->flash( 'error', t8('internal error (see details)'))
180 ->flash_detail('error', $@)->render;
184 if ($result != SL::File::RENAME_OK) {
185 $self->js->flash('error',
186 $result == SL::File::RENAME_EXISTS ? $::locale->text('File still exists !')
187 : $result == SL::File::RENAME_SAME ? $::locale->text('Same Filename !')
188 : $::locale->text('File not exists !'))
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';
222 if ( $::form->{ATTACHMENTS}->{uploadfiles} ) {
223 my @upfiles = @{ $::form->{ATTACHMENTS}->{uploadfiles} };
224 foreach my $idx (0 .. scalar(@upfiles) - 1) {
226 my $fname = uri_unescape($upfiles[$idx]->{filename});
227 # normalize and find basename
228 # first split with unix rules
229 # after that split with windows rules
230 my ($volume, $directories, $basefile) = File::Spec::Unix->splitpath($fname);
231 ($volume, $directories, $basefile) = File::Spec::Win32->splitpath($basefile);
233 # to find real mime_type by magic we must save the filedata
235 my $sess_fname = "file_upload_" . $self->object_type . "_" . $self->object_id . "_" . $idx;
236 my $sfile = SL::SessionFile->new($sess_fname, mode => 'w');
238 $sfile->fh->print(${$upfiles[$idx]->{data}});
240 my $mime_type = File::MimeInfo::Magic::magic($sfile->file_name);
243 # if filename has the suffix "pdf", but isn't really a pdf, set mimetype for no suffix
244 $mime_type = File::MimeInfo::Magic::mimetype($basefile);
245 $mime_type = 'application/octet-stream' if $mime_type eq 'application/pdf' || !$mime_type;
247 if ( $self->file_type eq 'image' && $self->file_probe_image_type($mime_type, $basefile)) {
250 my ($existobj) = SL::File->get_all(object_id => $self->object_id,
251 object_type => $self->object_type,
252 mime_type => $mime_type,
254 file_type => $self->file_type,
255 file_name => $basefile,
259 push @existing, $existobj->id.'_'.$sfile->file_name;
261 my $fileobj = SL::File->save(object_id => $self->object_id,
262 object_type => $self->object_type,
263 mime_type => $mime_type,
265 file_type => $self->file_type,
266 file_name => $basefile,
267 title => $::form->{title},
268 description => $::form->{description},
269 ## two possibilities: what is better ? content or sessionfile ??
270 file_contents => ${$upfiles[$idx]->{data}},
271 file_path => $sfile->file_name
273 unlink($sfile->file_name);
277 $self->js->flash( 'error', t8('internal error (see details)'))
278 ->flash_detail('error', $@)->render;
283 $self->existing(\@existing);
287 sub action_download {
289 my ($id, $version) = split /_/, $::form->{id};
290 my $file = SL::File->get(id => $id );
291 $file->version($version) if $version;
292 my $ref = $file->get_content;
293 if ( $file && $ref ) {
294 return $self->send_file($ref,
295 type => $file->mime_type,
296 name => $file->file_name,
305 sub check_object_params {
308 my $id = ($::form->{object_id} // 0) * 1;
309 my $draftid = ($::form->{draft_id} // 0) * 1;
313 if ( $draftid == 0 && $id == 0 && $::form->{is_global} ) {
315 $type = $::form->{object_type};
318 $id = $::form->{draft_id};
320 } elsif ( $::form->{object_type} ) {
321 $type = $::form->{object_type};
323 die "No object type" unless $type;
324 die "No file type" unless $::form->{file_type};
325 die "Unkown object type" unless $file_types{$type};
327 $self->is_global($gldoc);
328 $self->file_type($::form->{file_type});
329 $self->object_type($type);
330 $self->object_id($id);
331 $self->object_model($file_types{$type}->{model});
332 $self->object_right($file_types{$type}->{right});
334 # $::auth->assert($self->object_right);
336 # my $model = 'SL::DB::' . $self->object_model;
337 # $self->object($model->new(id => $self->object_id)->load || die "Record not found");
347 my ($self, $do_unimport, $infotext) = @_;
349 my $ids = $::form->{ids};
350 foreach my $id_version (@{ $::form->{$ids} || [] }) {
351 my ($id, $version) = split /_/, $id_version;
352 my $dbfile = SL::File->get(id => $id);
355 $dbfile->version($version);
356 $files .= ' ' . $dbfile->file_name if $dbfile->delete_version;
358 $files .= ' ' . $dbfile->file_name if $dbfile->delete;
362 $self->js->flash('info', $infotext . $files) if $files;
367 my ($self, $json) = @_;
369 if ( $self->file_type eq 'document' ) {
371 push @object_types, $self->object_type;
372 push @object_types, qw(dunning dunning1 dunning2 dunning3) if $self->object_type eq 'invoice'; # hardcoded object types?
373 @files = SL::File->get_all_versions(object_id => $self->object_id,
374 object_type => \@object_types,
375 file_type => $self->file_type,
379 elsif ( $self->file_type eq 'attachment' || $self->file_type eq 'image' ) {
380 @files = SL::File->get_all(object_id => $self->object_id,
381 object_type => $self->object_type,
382 file_type => $self->file_type,
385 $self->files(\@files);
387 if($self->object_type eq 'shop_image'){
389 ->run('kivi.ShopPart.show_images', $self->object_id)
392 $self->_mk_render('file/list', 1, 0, $json);
396 sub _get_from_import {
397 my ($self, $path) = @_;
400 my $language = $::lx_office_conf{system}->{language};
401 my $timezone = $::locale->get_local_time_zone()->name;
402 if (opendir my $dir, $path) {
403 my @files = ( readdir $dir);
404 foreach my $file ( @files) {
405 next if (($file eq '.') || ($file eq '..'));
406 $file = Encode::decode('utf-8', $file);
408 next if ( -d "$path/$file" );
410 my $tmppath = File::Spec->catfile( $path, $file );
411 next if( ! -f $tmppath );
413 my $st = stat($tmppath);
414 my $dt = DateTime->from_epoch( epoch => $st->mtime, time_zone => $timezone, locale => $language );
415 my $sname = $main::locale->quote_special_chars('HTML', $file);
418 'filename' => $sname,
420 'mtime' => $st->mtime,
421 'date' => $dt->dmy('.') . " " . $dt->hms,
430 my ($self, $template, $edit, $scanner, $json) = @_;
433 ##TODO make code configurable
436 my @sources = $self->_get_sources();
437 foreach my $source ( @sources ) {
438 @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files };
440 if ( $self->file_type eq 'document' ) {
441 $title = $main::locale->text('Documents');
442 } elsif ( $self->file_type eq 'attachment' ) {
443 $title = $main::locale->text('Attachments');
444 } elsif ( $self->file_type eq 'image' ) {
445 $title = $main::locale->text('Images');
448 my $output = SL::Presenter->get->render(
451 SOURCES => \@sources,
452 edit_attachments => $edit,
453 object_type => $self->object_type,
454 object_id => $self->object_id,
455 file_type => $self->file_type,
456 is_global => $self->is_global,
460 $self->js->html('#'.$self->file_type.'_list_'.$self->object_type, $output);
461 if ( $self->existing && scalar(@{$self->existing}) > 0) {
462 my $first = shift @{$self->existing};
463 my ($first_id, $sfile) = split('_', $first, 2);
464 my $file = SL::File->get(id => $first_id );
465 $self->js->run('kivi.File.askForRename', $first_id, $file->file_type, $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 if ( $self->file_type eq 'document' ) {
487 # TODO statt gen neue attribute in filetypes :
488 if (($file_types{$self->object_type}->{gen}*1 & 1)==1) {
491 'title' => $main::locale->text('generated Files'),
492 'chk_action' => 'documents_delete',
493 'chk_title' => $main::locale->text('Delete Documents'),
494 'chkall_title' => $main::locale->text('Delete all'),
495 'file_title' => $main::locale->text('filename'),
496 'confirm_text' => $main::locale->text('delete'),
498 'rename_title' => $main::locale->text('Rename Documents'),
499 'done_text' => $main::locale->text('deleted')
501 push @sources , $gendata;
503 if (($file_types{$self->object_type}->{gen}*1 & 2)==2) {
504 my @others = SL::File->get_other_sources();
505 foreach my $scanner_or_mailrx (@others) {
507 'name' => $scanner_or_mailrx->{name},
508 'title' => $main::locale->text('from \'#1\' imported Files', $scanner_or_mailrx->{description}),
509 'chk_action' => $scanner_or_mailrx->{name}.'_unimport',
510 'chk_title' => $main::locale->text('Unimport documents'),
511 'chkall_title' => $main::locale->text('Unimport all'),
512 'file_title' => $main::locale->text('filename'),
513 'confirm_text' => $main::locale->text('unimport'),
515 'rename_title' => $main::locale->text('Rename Documents'),
517 'import_title' => $main::locale->text('Add Document from \'#1\'', $scanner_or_mailrx->{name}),
518 'path' => $scanner_or_mailrx->{directory},
519 'done_text' => $main::locale->text('unimported')
521 push @sources , $other;
525 elsif ( $self->file_type eq 'attachment' ) {
527 'name' => 'uploaded',
528 'title' => $main::locale->text(''),
529 'chk_action' => 'attachments_delete',
530 'chk_title' => $main::locale->text('Delete Attachments'),
531 'chkall_title' => $main::locale->text('Delete all'),
532 'file_title' => $main::locale->text('filename'),
533 'confirm_text' => $main::locale->text('delete'),
535 'are_existing' => $self->existing ? 1 : 0,
536 'rename_title' => $main::locale->text('Rename Attachments'),
538 'upload_title' => $main::locale->text('Upload Attachments'),
539 'done_text' => $main::locale->text('deleted')
541 push @sources , $attdata;
543 elsif ( $self->file_type eq 'image' ) {
545 'name' => 'uploaded',
546 'title' => $main::locale->text(''),
547 'chk_action' => 'images_delete',
548 'chk_title' => $main::locale->text('Delete Images'),
549 'chkall_title' => $main::locale->text('Delete all'),
550 'file_title' => $main::locale->text('filename'),
551 'confirm_text' => $main::locale->text('delete'),
553 'are_existing' => $self->existing ? 1 : 0,
554 'rename_title' => $main::locale->text('Rename Images'),
556 'upload_title' => $main::locale->text('Upload Images'),
557 'done_text' => $main::locale->text('deleted')
559 push @sources , $attdata;
574 SL::Controller::File - Controller for managing files
578 The Controller is called directly from the webpages
580 <a href="controller.pl?action=File/list&file_type=document\
581 &object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">
584 or indirectly via javascript functions from js/kivi.File.js
586 kivi.popup_dialog({ url: 'controller.pl',
587 data: { action : 'File/ajax_upload',
588 file_type : 'uploaded',
596 This is a controller for handling files in a storage independent way.
597 The storage may be a Filesystem,a WebDAV, a Database or DMS.
598 These backends must be configered in ClientConfig.
599 This Controller use as intermediate layer for storage C<SL::File>.
601 The Controller is responsible to display forms for displaying the files at the ERP-objects and
602 for uploading and downloading the files.
604 More description of the intermediate layer see L<SL::File>.
608 =head2 C<action_list>
610 This loads a list of files on a webpage. This can be done with a normal submit or via an ajax/json call.
611 Dependent of file_type different sources are available.
613 For documents there are the 'created' source and the imports from scanners or email.
614 For attachments and images only the 'uploaded' source available.
616 Available C<FORM PARAMS>:
620 =item C<form.object_id>
622 The Id of the ERP-object.
624 =item C<form.object_type>
626 The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
628 =item C<form.file_type>
630 For one ERP-object may exists different type of documents the type may be "documents","attachments" or "images".
631 This file_type is a filter for the list.
635 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.
640 =head2 C<action_ajax_upload>
643 A new file or more files can selected by a dialog and insert into the system.
646 Available C<FORM PARAMS>:
650 =item C<form.file_type>
652 This parameter describe here the source for a new file :
653 "attachments" and "images"
655 This is a normal upload selection, which may be more then one file to upload.
657 =item C<form.object_id>
661 =item C<form.object_type>
663 are the same as at C<action_list>
667 =head2 C<action_ajax_files_uploaded>
669 The Upload of selected Files. The "multipart_formdata" is parsed in SL::Request into the formsvariable "form.ATTACHMENTS".
670 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?).
671 If the same filename still exists at this object after the download for each existing filename a rename dialog will be opened.
673 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.
675 Available C<FORM PARAMS>:
679 =item C<form.ATTACHMENTS.uploadfiles>
681 This is an array of elements which have {filename} for the name and {data} for the contents.
683 Also object_id, object_type and file_type
687 =head2 C<action_download>
689 This is the real download of a file normally called via javascript "$.download("controller.pl", data);"
691 Available C<FORM PARAMS>:
695 Also object_id, object_type and file_type
699 =head2 C<action_ajax_importdialog>
701 A Dialog with all available and not imported files to import is open.
702 More then one file can be selected.
704 Available C<FORM PARAMS>:
710 The name of the source like "scanner1" or "email"
714 The full path to the directory on the server, where the files to import can found
716 Also object_id, object_type and file_type
720 =head2 C<action_ajax_delete>
722 Some files can be deleted
724 Available C<FORM PARAMS>:
730 The ids of the files to delete. Only this files are deleted not all versions of a file if the exists
734 =head2 C<action_ajax_unimport>
736 Some files can be unimported, dependent of the source of the file. This means they are moved
737 back to the directory of the source
739 Available C<FORM PARAMS>:
745 The ids of the files to unimport. Only these files are unimported not all versions of a file if the exists
749 =head2 C<action_ajax_rename>
751 One file can be renamed. There can be some checks if the same filename still exists at one object.
755 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>