]> wagnertech.de Git - mfinanz.git/blob - SL/Controller/File.pm
Merge branch 'master' of http://wagnertech.de/git/mfinanz
[mfinanz.git] / SL / Controller / File.pm
1 package SL::Controller::File;
2
3 use strict;
4
5 use parent qw(SL::Controller::Base);
6
7 use List::Util qw(first max);
8
9 use utf8;
10 use Encode qw(decode);
11 use English qw( -no_match_vars );
12 use URI::Escape;
13 use Cwd;
14 use DateTime;
15 use File::stat;
16 use File::Slurp qw(slurp);
17 use File::Spec::Unix;
18 use File::Spec::Win32;
19 use File::MimeInfo::Magic;
20 use MIME::Base64;
21 use SL::DB::Helper::Mappings;
22 use SL::DB::Order;
23 use SL::DB::DeliveryOrder;
24 use SL::DB::Invoice;
25
26 use SL::DB::PurchaseInvoice;
27 use SL::DB::Part;
28 use SL::DB::GLTransaction;
29 use SL::DB::Draft;
30 use SL::DB::History;
31 use SL::JSON;
32 use SL::Helper::CreatePDF qw(:all);
33 use SL::Locale::String;
34 use SL::SessionFile;
35 use SL::SessionFile::Random;
36 use SL::File;
37 use SL::Controller::Helper::ThumbnailCreator qw(file_probe_image_type file_probe_type);
38
39 use constant DO_DELETE   => 0;
40 use constant DO_UNIMPORT => 1;
41
42 use Rose::Object::MakeMethods::Generic
43 (
44     'scalar --get_set_init' => [ qw() ],
45     'scalar' => [ qw(object object_type object_model object_id object_right file_type files is_global existing) ],
46 );
47
48 __PACKAGE__->run_before('check_object_params', only => [ qw(list ajax_delete ajax_importdialog ajax_import ajax_unimport ajax_upload ajax_files_uploaded) ]);
49
50 # gen:    bitmask: bit 1 (value is 1, 3, 5 or 7) => file created
51 #                  bit 2 (value is 2, 3, 6 or 7) => file from other source (e.g. directory for scanned documents)
52 #                  bit 3 (value is 4, 5, 6 or 7) => upload as other source
53 # gltype: is this used somewhere?
54 # dir:    is this used somewhere?
55 # model:  base name of the rose model
56 # right:  access right used for import
57 my %file_types = (
58   'sales_quotation'             => { gen => 7, gltype => '',   dir =>'SalesQuotation',            model => 'Order',          right => 'import_ar'  },
59   'sales_order_intake'          => { gen => 7, gltype => '',   dir =>'SalesOrderIntake',          model => 'Order',          right => 'import_ar'  },
60   'sales_order'                 => { gen => 7, gltype => '',   dir =>'SalesOrder',                model => 'Order',          right => 'import_ar'  },
61   'sales_delivery_order'        => { gen => 7, gltype => '',   dir =>'SalesDeliveryOrder',        model => 'DeliveryOrder',  right => 'import_ar'  },
62   'sales_reclamation'           => { gen => 7, gltype => '',   dir =>'SalesReclamation',          model => 'Reclamation',    right => 'import_ar'  },
63   'invoice'                     => { gen => 7, gltype => 'ar', dir =>'SalesInvoice',              model => 'Invoice',        right => 'import_ar'  },
64   'invoice_for_advance_payment' => { gen => 7, gltype => 'ar', dir =>'SalesInvoice',              model => 'Invoice',        right => 'import_ar'  },
65   'final_invoice'               => { gen => 7, gltype => 'ar', dir =>'SalesInvoice',              model => 'Invoice',        right => 'import_ar'  },
66   'credit_note'                 => { gen => 7, gltype => '',   dir =>'CreditNote',                model => 'Invoice',        right => 'import_ar'  },
67   'request_quotation'           => { gen => 7, gltype => '',   dir =>'RequestForQuotation',       model => 'Order',          right => 'import_ap'  },
68   'purchase_quotation_intake'   => { gen => 7, gltype => '',   dir =>'PurchaseQuotationIntake',   model => 'Order',          right => 'import_ap'  },
69   'purchase_order'              => { gen => 7, gltype => '',   dir =>'PurchaseOrder',             model => 'Order',          right => 'import_ap'  },
70   'purchase_order_confirmation' => { gen => 7, gltype => '',   dir =>'PurchaseOrderConfirmation', model => 'Order',          right => 'import_ap'  },
71   'purchase_delivery_order'     => { gen => 7, gltype => '',   dir =>'PurchaseDeliveryOrder',     model => 'DeliveryOrder',  right => 'import_ap'  },
72   'purchase_reclamation'        => { gen => 7, gltype => '',   dir =>'PurchaseReclamation',       model => 'Reclamation',    right => 'import_ap'  },
73   'purchase_invoice'            => { gen => 7, gltype => 'ap', dir =>'PurchaseInvoice',           model => 'PurchaseInvoice',right => 'import_ap'  },
74   'supplier_delivery_order'     => { gen => 7, gltype => '',   dir =>'SupplierDeliveryOrder',     model => 'DeliveryOrder',  right => 'import_ap'  },
75   'rma_delivery_order'          => { gen => 7, gltype => '',   dir =>'RMADeliveryOrder',          model => 'DeliveryOrder',  right => 'import_ar'  },
76   'vendor'                      => { gen => 0, gltype => '',   dir =>'Vendor',                    model => 'Vendor',         right => 'xx'         },
77   'customer'                    => { gen => 1, gltype => '',   dir =>'Customer',                  model => 'Customer',       right => 'xx'         },
78   'project'                     => { gen => 0, gltype => '',   dir =>'Project',                   model => 'Project',        right => 'xx'         },
79   'part'                        => { gen => 0, gltype => '',   dir =>'Part',                      model => 'Part',           right => 'xx'         },
80   'gl_transaction'              => { gen => 6, gltype => 'gl', dir =>'GeneralLedger',             model => 'GLTransaction',  right => 'import_ap'  },
81   'draft'                       => { gen => 0, gltype => '',   dir =>'Draft',                     model => 'Draft',          right => 'xx'         },
82   'csv_customer'                => { gen => 1, gltype => '',   dir =>'Reports',                   model => 'Customer',       right => 'xx'         },
83   'csv_vendor'                  => { gen => 1, gltype => '',   dir =>'Reports',                   model => 'Vendor',         right => 'xx'         },
84   'shop_image'                  => { gen => 0, gltype => '',   dir =>'ShopImages',                model => 'Part',           right => 'xx'         },
85   'letter'                      => { gen => 7, gltype => '',   dir =>'Letter',                    model => 'Letter',         right => 'sales_letter_edit | purchase_letter_edit' },
86 );
87
88 #--- 4 locale ---#
89 # $main::locale->text('imported')
90
91 #
92 # actions
93 #
94
95 sub action_list {
96   my ($self) = @_;
97
98   my $is_json = 0;
99   $is_json = 1 if $::form->{json};
100
101   $self->_do_list($is_json);
102 }
103
104 sub action_ajax_importdialog {
105   my ($self) = @_;
106   $::auth->assert($self->object_right);
107   my $path   = $::form->{path};
108   my @files  = $self->_get_from_import($path);
109   my $source = {
110     'name'         => $::form->{source},
111     'path'         => $path ,
112     'chk_action'   => $::form->{source}.'_import',
113     'chk_title'    => $main::locale->text('Import scanned documents'),
114     'chkall_title' => $main::locale->text('Import all'),
115     'files'        => \@files
116   };
117   $self->render('file/import_dialog',
118                 { layout => 0
119                 },
120                 source => $source
121   );
122 }
123
124 sub action_ajax_import {
125   my ($self) = @_;
126   $::auth->assert($self->object_right);
127   my $ids    = $::form->{ids};
128   my $source = $::form->{source};
129   my $path   = $::form->{path};
130   my @files  = $self->_get_from_import($path);
131   foreach my $filename (@{ $::form->{$ids} || [] }) {
132     my ($file, undef) = grep { $_->{name} eq $filename } @files;
133     if ( $file ) {
134       my $obj = SL::File->save(object_id   => $self->object_id,
135                                object_type => $self->object_type,
136                                mime_type   => 'application/pdf',
137                                source      => $source,
138                                file_type   => 'document',
139                                file_name   => $file->{filename},
140                                file_path   => $file->{path}
141                              );
142       unlink($file->{path}) if $obj;
143     }
144   }
145   $self->_do_list(1);
146 }
147
148 sub action_ajax_delete {
149   my ($self) = @_;
150   $self->_delete_all(DO_DELETE, $::locale->text('Following files are deleted:'));
151 }
152
153 sub action_ajax_unimport {
154   my ($self) = @_;
155   $self->_delete_all(DO_UNIMPORT, $::locale->text('Following files are unimported:'));
156 }
157
158 sub action_ajax_rename {
159   my ($self) = @_;
160   my $guid = $::form->{id};
161   my $file = SL::File->get(guid => $guid);
162   if ( ! $file ) {
163     $self->js->flash('error', $::locale->text('File not exists !'))->render();
164     return;
165   }
166   my $sessionfile = $::form->{sessionfile};
167   if ( $sessionfile && -f $sessionfile ) {
168     # new uploaded file
169     if ( $::form->{to} eq $file->file_name ) {
170       # no rename so use as new version
171       $file->save_file($sessionfile);
172       $self->js->flash('warning', $::locale->text('File \'#1\' is used as new Version !', $file->file_name));
173
174     } else {
175       # new filename, so it is a new file with the same attributes as the old file
176       eval {
177         SL::File->save(object_id   => $file->object_id,
178                        object_type => $file->object_type,
179                        mime_type   => $file->mime_type,
180                        source      => $file->source,
181                        file_type   => $file->file_type,
182                        file_name   => $::form->{to},
183                        file_path   => $sessionfile
184                      );
185         unlink($sessionfile);
186         1;
187       } or do {
188         $self->js->flash(       'error', t8('internal error (see details)'))
189                  ->flash_detail('error', $@)->render;
190         return;
191       }
192     }
193
194   } else {
195     # normal rename
196     my $result;
197
198     eval {
199       $result = $file->rename($::form->{to});
200       1;
201     } or do {
202       $self->js->flash(       'error', t8('internal error (see details)'))
203                ->flash_detail('error', $@)->render;
204       return;
205     };
206
207     if ($result != SL::File::RENAME_OK) {
208       $self->js->flash('error',
209                          $result == SL::File::RENAME_EXISTS ? $::locale->text('File still exists !')
210                        : $result == SL::File::RENAME_SAME   ? $::locale->text('Same Filename !')
211                        :                                      $::locale->text('File not exists !'))
212         ->render;
213       return;
214     }
215   }
216   $self->is_global($::form->{is_global});
217   $self->file_type(  $file->file_type);
218   $self->object_type($file->object_type);
219   $self->object_id(  $file->object_id);
220   #$self->object_model($file_types{$file->module}->{model});
221   #$self->object_right($file_types{$file->module}->{right});
222   if ( $::form->{next_ids} ) {
223     my @existing = split(/,/, $::form->{next_ids});
224     $self->existing(\@existing);
225   }
226   $self->_do_list(1);
227 }
228
229 sub action_ajax_upload {
230   my ($self) = @_;
231   $self->{maxsize} = $::instance_conf->get_doc_max_filesize;
232   $self->{accept_types} = '';
233   $self->{accept_types} = 'image/png,image/gif,image/jpeg,image/tiff,*png,*gif,*.jpg,*.tif' if $self->{file_type} eq 'image';
234   $self->render('file/upload_dialog',
235                 { layout => 0
236                 },
237   );
238 }
239
240 sub action_ajax_files_uploaded {
241   my ($self) = @_;
242
243   my $source = 'uploaded';
244   my @existing;
245   if ( $::form->{ATTACHMENTS}->{uploadfiles} ) {
246     my @upfiles = @{ $::form->{ATTACHMENTS}->{uploadfiles} };
247     foreach my $idx (0 .. scalar(@upfiles) - 1) {
248       eval {
249         my $fname = uri_unescape($upfiles[$idx]->{filename});
250         # normalize and find basename
251         # first split with unix rules
252         # after that split with windows rules
253         my ($volume, $directories, $basefile) = File::Spec::Unix->splitpath($fname);
254         ($volume, $directories, $basefile) = File::Spec::Win32->splitpath($basefile);
255
256         # to find real mime_type by magic we must save the filedata
257
258         my $sess_fname = "file_upload_" . $self->object_type . "_" . $self->object_id . "_" . $idx;
259         my $sfile      = SL::SessionFile->new($sess_fname, mode => 'w');
260
261         $sfile->fh->print(${$upfiles[$idx]->{data}});
262         $sfile->fh->close;
263         my $mime_type = File::MimeInfo::Magic::magic($sfile->file_name);
264
265         if (! $mime_type) {
266           # if filename has the suffix "pdf", but isn't really a pdf, set mimetype for no suffix
267           $mime_type = File::MimeInfo::Magic::mimetype($basefile);
268           $mime_type = 'application/octet-stream' if $mime_type eq 'application/pdf' || !$mime_type;
269         }
270         if ( $self->file_type eq 'image' && $self->file_probe_image_type($mime_type, $basefile)) {
271           next;
272         }
273         my ($existobj) = SL::File->get_all(object_id   => $self->object_id,
274                                            object_type => $self->object_type,
275                                            mime_type   => $mime_type,
276                                            source      => $source,
277                                            file_type   => $self->file_type,
278                                            file_name   => $basefile,
279                                       );
280
281         if ($existobj) {
282           push @existing, ($existobj->versions)[0]->file_version->guid.'_'.$sfile->file_name;
283         } else {
284           my $fileobj = SL::File->save(object_id        => $self->object_id,
285                                        object_type      => $self->object_type,
286                                        mime_type        => $mime_type,
287                                        source           => $source,
288                                        file_type        => $self->file_type,
289                                        file_name        => $basefile,
290                                        title            => $::form->{title},
291                                        description      => $::form->{description},
292                                        ## two possibilities: what is better ? content or sessionfile ??
293                                        file_contents    => ${$upfiles[$idx]->{data}},
294                                        file_path        => $sfile->file_name
295                                      );
296           unlink($sfile->file_name);
297         }
298         1;
299       } or do {
300         $self->js->flash(       'error', t8('internal error (see details)'))
301                  ->flash_detail('error', $@)->render;
302         return;
303       }
304     }
305   }
306   $self->existing(\@existing);
307   $self->_do_list(1);
308 }
309
310 sub action_download {
311   my ($self) = @_;
312
313   my $id      = $::form->{id};
314   my $version = $::form->{version};
315
316   my $file = SL::File->get(id => $id );
317   $file->version($version) if $version;
318   my $ref  = $file->get_content;
319   if ( $file && $ref ) {
320     return $self->send_file($ref,
321       type => $file->mime_type,
322       name => $file->file_name,
323     );
324   }
325 }
326
327 sub action_ajax_get_thumbnail {
328   my ($self) = @_;
329
330   my $id      = $::form->{file_id};
331   my $version = $::form->{file_version};
332   my $file    = SL::File->get(id => $id);
333
334   $file->version($version) if $version;
335
336   my $thumbnail = _create_thumbnail($file, $::form->{size});
337
338   my $overlay_selector  = '#enlarged_thumb_' . $id;
339   $overlay_selector    .= '_' . $version            if $version;
340   $self->js
341     ->attr($overlay_selector, 'src', 'data:' . $thumbnail->{thumbnail_img_content_type} . ';base64,' . MIME::Base64::encode_base64($thumbnail->{thumbnail_img_content}))
342     ->data($overlay_selector, 'is-overlay-loaded', '1')
343     ->render;
344 }
345
346
347 #
348 # filters
349 #
350
351 sub check_object_params {
352   my ($self) = @_;
353
354   my $id      = ($::form->{object_id} // 0) * 1;
355   my $draftid = ($::form->{draft_id}  // 0) * 1;
356   my $gldoc   = 0;
357   my $type    = undef;
358
359   if ( $draftid == 0 && $id == 0 && $::form->{is_global} ) {
360     $gldoc = 1;
361     $type  = $::form->{object_type};
362   }
363   elsif ( $id == 0 ) {
364     $id   = $::form->{draft_id};
365     $type = 'draft';
366   } elsif ( $::form->{object_type} ) {
367     $type = $::form->{object_type};
368   }
369   die "No object type"      unless $type;
370   die "No file type"        unless $::form->{file_type};
371   die "Unknown object type" unless $file_types{$type};
372
373   $self->is_global($gldoc);
374   $self->file_type($::form->{file_type});
375   $self->object_type($type);
376   $self->object_id($id);
377   $self->object_model($file_types{$type}->{model});
378   $self->object_right($file_types{$type}->{right});
379
380  # $::auth->assert($self->object_right);
381
382  # my $model = 'SL::DB::' . $self->object_model;
383  # $self->object($model->new(id => $self->object_id)->load || die "Record not found");
384
385   return 1;
386 }
387
388 #
389 # private methods
390 #
391
392 sub _delete_all {
393   my ($self, $do_unimport, $infotext) = @_;
394   my $files = '';
395   my $ids = $::form->{ids};
396   foreach my $version_guid (@{ $::form->{$ids} || [] }) {
397     my $dbfile = SL::File->get(guid => $version_guid);
398     if ($dbfile) {
399       $files .= ' ' . $dbfile->file_name if $dbfile->delete_file_version;
400     }
401   }
402   $self->js->flash('info', $infotext . $files) if $files;
403   $self->_do_list(1);
404 }
405
406 sub _do_list {
407   my ($self, $json) = @_;
408
409   my @files;
410   my @object_types = ($self->object_type);
411   if ( $self->file_type eq 'document' ) {
412     push @object_types, qw(dunning1 dunning2 dunning3 dunning_invoice dunning_orig_invoice) if $self->object_type eq 'invoice'; # hardcoded object types?
413   }
414   @files = SL::File->get_all_versions(object_id   => $self->object_id,
415                                       object_type => \@object_types,
416                                       file_type   => $self->file_type,
417                                      );
418
419   $self->files(\@files);
420
421   $_->{thumbnail}     = _create_thumbnail($_)                     for @files;
422   $_->{version_count} = SL::File->get_version_count(id => $_->id) for @files;
423
424   if($self->object_type eq 'shop_image'){
425     $self->js
426       ->run('kivi.ShopPart.show_images', $self->object_id)
427       ->render();
428   }else{
429     $self->_mk_render('file/list', 1, 0, $json);
430   }
431 }
432
433 sub _get_from_import {
434   my ($self, $path) = @_;
435   my @foundfiles ;
436
437   my $language = $::lx_office_conf{system}->{language};
438   my $timezone = $::locale->get_local_time_zone()->name;
439   if (opendir my $dir, $path) {
440     my @files = (readdir $dir);
441     foreach my $file ( @files) {
442       next if (($file eq '.') || ($file eq '..'));
443       $file = Encode::decode('utf-8', $file);
444
445       next if ( -d "$path/$file" );
446
447       my $tmppath = File::Spec->catfile( $path, $file );
448       next if( ! -f $tmppath );
449
450       my $st = stat($tmppath);
451       my $dt = DateTime->from_epoch( epoch => $st->mtime, time_zone => $timezone, locale => $language );
452       my $sname = $main::locale->quote_special_chars('HTML', $file);
453       push @foundfiles, {
454         'name'     => $file,
455         'filename' => $sname,
456         'path'     => $tmppath,
457         'mtime'    => $st->mtime,
458         'date'     => $dt->dmy('.') . " " . $dt->hms,
459       };
460
461     }
462     closedir($dir);
463
464   } else {
465     $::lxdebug->message(LXDebug::WARN(), "SL::File::_get_from_import opendir failed to open dir " . $path);
466   }
467
468   return @foundfiles;
469 }
470
471 sub _mk_render {
472   my ($self, $template, $edit, $scanner, $json) = @_;
473   my $err;
474   eval {
475     ##TODO make code configurable
476
477     my $title;
478     my @sources = $self->_get_sources();
479     foreach my $source ( @sources ) {
480       @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files };
481     }
482     if ( $self->file_type eq 'document' ) {
483       $title = $main::locale->text('Documents');
484     } elsif ( $self->file_type eq 'attachment' ) {
485       $title = $main::locale->text('Attachments');
486     } elsif ( $self->file_type eq 'image' ) {
487       $title = $main::locale->text('Images');
488     }
489
490     my $output         = SL::Presenter->get->render(
491       $template,
492       title            => $title,
493       SOURCES          => \@sources,
494       edit_attachments => $edit,
495       object_type      => $self->object_type,
496       object_id        => $self->object_id,
497       file_type        => $self->file_type,
498       is_global        => $self->is_global,
499       json             => $json,
500     );
501     if ( $json ) {
502       $self->js->html('#'.$self->file_type.'_list_'.$self->object_type, $output);
503       if ( $self->existing && scalar(@{$self->existing}) > 0) {
504         my $first = shift @{$self->existing};
505         my ($first_guid, $sfile) = split('_', $first, 2);
506         my $file = SL::File->get(guid => $first_guid );
507         $self->js->run('kivi.File.askForRename', $first_guid, $file->file_type, $file->file_name, $sfile, join (',', @{$self->existing}), $self->is_global);
508       }
509       $self->js->render();
510     } else {
511         $self->render(\$output, { layout => 0, process => 0 });
512     }
513     1;
514   } or do {
515     if ($json ){
516       $self->js->flash(       'error', t8('internal error (see details)'))
517                ->flash_detail('error', $@)->render;
518     } else {
519       $self->render('generic/error', { layout => 0 }, label_error => $@);
520     }
521   };
522 }
523
524
525 sub _get_sources {
526   my ($self) = @_;
527   my @sources;
528   if ( $self->file_type eq 'document' ) {
529     # TODO statt gen neue attribute in filetypes :
530     if (($file_types{$self->object_type}->{gen}*1 & 4)==4) {
531       # bit 3 is set => means upload
532       my $source = {
533         'name'         => 'uploaded',
534         'title'        => $main::locale->text('uploaded Documents'),
535         'chk_action'   => 'uploaded_documents_delete',
536         'chk_title'    => $main::locale->text('Delete Documents'),
537         'chkall_title' => $main::locale->text('Delete all'),
538         'file_title'   => $main::locale->text('filename'),
539         'confirm_text' => $main::locale->text('delete'),
540         'can_rename'   => 1,
541         'are_existing' => $self->existing ? 1 : 0,
542         'rename_title' => $main::locale->text('Rename Attachments'),
543         'can_upload'   => 1,
544         'can_delete'   => 1,
545         'upload_title' => $main::locale->text('Upload Documents'),
546         'done_text'    => $main::locale->text('deleted')
547       };
548       push @sources , $source;
549     }
550
551     if (($file_types{$self->object_type}->{gen}*1 & 1)==1) {
552       my $gendata = {
553         'name'         => 'created',
554         'title'        => $main::locale->text('generated Files'),
555         'chk_action'   => 'documents_delete',
556         'chk_title'    => $main::locale->text('Delete Documents'),
557         'chkall_title' => $main::locale->text('Delete all'),
558         'file_title'   => $main::locale->text('filename'),
559         'confirm_text' => $main::locale->text('delete'),
560         'can_delete'   => $::instance_conf->get_doc_delete_printfiles,
561         'can_rename'   => $::instance_conf->get_doc_delete_printfiles,
562         'rename_title' => $main::locale->text('Rename Documents'),
563         'done_text'    => $main::locale->text('deleted')
564       };
565       push @sources , $gendata;
566     }
567
568     if (($file_types{$self->object_type}->{gen}*1 & 2)==2) {
569       my @others =  SL::File->get_other_sources();
570       foreach my $scanner_or_mailrx (@others) {
571         my $other = {
572           'name'         => $scanner_or_mailrx->{name},
573           'title'        => $main::locale->text('from \'#1\' imported Files', $scanner_or_mailrx->{description}),
574           'chk_action'   => $scanner_or_mailrx->{name}.'_unimport',
575           'chk_title'    => $main::locale->text('Unimport documents'),
576           'chkall_title' => $main::locale->text('Unimport all'),
577           'file_title'   => $main::locale->text('filename'),
578           'confirm_text' => $main::locale->text('unimport'),
579           'can_rename'   => 1,
580           'rename_title' => $main::locale->text('Rename Documents'),
581           'can_import'   => 1,
582           'can_delete'   => 0,
583           'import_title' => $main::locale->text('Add Document from \'#1\'', $scanner_or_mailrx->{name}),
584           'path'         => $scanner_or_mailrx->{directory},
585           'done_text'    => $main::locale->text('unimported')
586         };
587         push @sources , $other;
588       }
589     }
590   }
591   elsif ( $self->file_type eq 'attachment' ) {
592     my $attdata = {
593       'name'         => 'uploaded',
594       'title'        => $main::locale->text(''),
595       'chk_action'   => 'attachments_delete',
596       'chk_title'    => $main::locale->text('Delete Attachments'),
597       'chkall_title' => $main::locale->text('Delete all'),
598       'file_title'   => $main::locale->text('filename'),
599       'confirm_text' => $main::locale->text('delete'),
600       'can_rename'   => 1,
601       'are_existing' => $self->existing ? 1 : 0,
602       'rename_title' => $main::locale->text('Rename Attachments'),
603       'can_upload'   => 1,
604       'can_delete'   => 1,
605       'upload_title' => $main::locale->text('Upload Attachments'),
606       'done_text'    => $main::locale->text('deleted')
607     };
608     push @sources , $attdata;
609   }
610   elsif ( $self->file_type eq 'image' ) {
611     my $attdata = {
612       'name'         => 'uploaded',
613       'title'        => $main::locale->text(''),
614       'chk_action'   => 'images_delete',
615       'chk_title'    => $main::locale->text('Delete Images'),
616       'chkall_title' => $main::locale->text('Delete all'),
617       'file_title'   => $main::locale->text('filename'),
618       'confirm_text' => $main::locale->text('delete'),
619       'can_rename'   => 1,
620       'are_existing' => $self->existing ? 1 : 0,
621       'rename_title' => $main::locale->text('Rename Images'),
622       'can_upload'   => 1,
623       'can_delete'   => 1,
624       'upload_title' => $main::locale->text('Upload Images'),
625       'done_text'    => $main::locale->text('deleted')
626     };
627     push @sources , $attdata;
628   }
629   return @sources;
630 }
631
632 # ignores all errors
633 # todo: cache thumbs?
634 sub _create_thumbnail {
635   my ($file, $size) = @_;
636
637   $size //= 64;
638
639   my $filename;
640   if (!eval { $filename = $file->get_file(); 1; }) {
641     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail get_file failed: " . $EVAL_ERROR);
642     return;
643   }
644
645   # Workaround for pfds which are not handled by file_probe_type.
646   # Maybe use mime info stored in db?
647   my $mime_type = File::MimeInfo::Magic::magic($filename);
648   if ($mime_type =~ m{pdf}) {
649     $filename = _convert_pdf_to_png($filename, size => $size);
650   }
651   return if !$filename;
652
653   my $content;
654   if (!eval { $content = slurp $filename; 1; }) {
655     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail slurp failed: " . $EVAL_ERROR);
656     return;
657   }
658
659   my $ret;
660   if (!eval { $ret = file_probe_type($content, size => $size); 1; }) {
661     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail file_probe_type failed: " . $EVAL_ERROR);
662     return;
663   }
664
665   # file_probe_type returns a hash ref with thumbnail info and content
666   # or an error message
667   if ('HASH' ne ref $ret) {
668     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail file_probe_type returned an error: " . $ret);
669     return;
670   }
671
672   return $ret;
673 }
674
675 sub _convert_pdf_to_png {
676   my ($filename, %params) = @_;
677
678   my $size    = $params{size} // 64;
679   my $sfile   = SL::SessionFile::Random->new();
680   unless (-f $filename) {
681     $::lxdebug->message(LXDebug::WARN(), "_convert_pdf_to_png failed, no file found: $filename");
682     return;
683   }
684   # quotemeta for storno case "storno\ zu\ 1020" *nix only
685   my $command = 'pdftoppm -singlefile -scale-to ' . $size . ' -png' . ' ' . quotemeta($filename) . ' ' . $sfile->file_name;
686
687   if (system($command) == -1) {
688     $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: system call failed: " . $ERRNO);
689     return;
690   }
691   if ($CHILD_ERROR) {
692     $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: pdftoppm failed with error code: " . ($CHILD_ERROR >> 8));
693     $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: File: $filename");
694     return;
695   }
696
697   return $sfile->file_name . '.png';
698 }
699
700 1;
701
702 __END__
703
704 =pod
705
706 =encoding utf-8
707
708 =head1 NAME
709
710 SL::Controller::File - Controller for managing files
711
712 =head1 SYNOPSIS
713
714 The Controller is called directly from the webpages
715
716     <a href="controller.pl?action=File/list&file_type=document\
717        &object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">
718
719
720 or indirectly via javascript functions from js/kivi.File.js
721
722     kivi.popup_dialog({ url:     'controller.pl',
723                         data:    { action     : 'File/ajax_upload',
724                                    file_type  : 'uploaded',
725                                    object_type: type,
726                                    object_id  : id
727                                  }
728                            ...
729
730 =head1 DESCRIPTION
731
732 This is a controller for handling files in a storage independent way.
733 The storage may be a Filesystem,a WebDAV, a Database or DMS.
734 These backends must be configered in ClientConfig.
735 This Controller use as intermediate layer for storage C<SL::File>.
736
737 The Controller is responsible to display forms for displaying the files at the ERP-objects and
738 for uploading and downloading the files.
739
740 More description of the intermediate layer see L<SL::File>.
741
742 =head1 METHODS
743
744 =head2 C<action_list>
745
746 This loads a list of files on a webpage. This can be done with a normal submit or via an ajax/json call.
747 Dependent of file_type different sources are available.
748
749 For documents there are the 'created' source and the imports from scanners or email.
750 For attachments and images only the 'uploaded' source available.
751
752 Available C<FORM PARAMS>:
753
754 =over 4
755
756 =item C<form.object_id>
757
758 The Id of the ERP-object.
759
760 =item C<form.object_type>
761
762 The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
763
764 =item C<form.file_type>
765
766 For one ERP-object may exists different type of documents the type may be "documents","attachments" or "images".
767 This file_type is a filter for the list.
768
769 =item C<form.json>
770
771 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.
772
773 =back
774
775
776 =head2 C<action_ajax_upload>
777
778
779 A new file or more files can selected by a dialog and insert into the system.
780
781
782 Available C<FORM PARAMS>:
783
784 =over 4
785
786 =item C<form.file_type>
787
788 This parameter describe here the source for a new file :
789 "attachments" and "images"
790
791 This is a normal upload selection, which may be more then one file to upload.
792
793 =item C<form.object_id>
794
795 and
796
797 =item C<form.object_type>
798
799 are the same as at C<action_list>
800
801 =back
802
803 =head2  C<action_ajax_files_uploaded>
804
805 The Upload of selected Files. The "multipart_formdata" is parsed in SL::Request into the formsvariable "form.ATTACHMENTS".
806 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?).
807 If the same filename still exists at this object after the download for each existing filename a rename dialog will be opened.
808
809 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.
810
811 Available C<FORM PARAMS>:
812
813 =over 4
814
815 =item C<form.ATTACHMENTS.uploadfiles>
816
817 This is an array of elements which have {filename} for the name and {data} for the contents.
818
819 Also object_id, object_type and file_type
820
821 =back
822
823 =head2 C<action_download>
824
825 This is the real download of a file normally called via javascript "$.download("controller.pl", data);"
826
827 Available C<FORM PARAMS>:
828
829 =over 4
830
831 Also object_id, object_type and file_type
832
833 =back
834
835 =head2 C<action_ajax_importdialog>
836
837 A Dialog with all available and not imported files to import is open.
838 More then one file can be selected.
839
840 Available C<FORM PARAMS>:
841
842 =over 4
843
844 =item C<form.source>
845
846 The name of the source like "scanner1" or "email"
847
848 =item C<form.path>
849
850 The full path to the directory on the server, where the files to import can found
851
852 Also object_id, object_type and file_type
853
854 =back
855
856 =head2 C<action_ajax_delete>
857
858 Some files can be deleted
859
860 Available C<FORM PARAMS>:
861
862 =over 4
863
864 =item C<form.ids>
865
866 The ids of the files to delete. Only this files are deleted not all versions of a file if the exists
867
868 =back
869
870 =head2 C<action_ajax_unimport>
871
872 Some files can be unimported, dependent of the source of the file. This means they are moved
873 back to the directory of the source
874
875 Available C<FORM PARAMS>:
876
877 =over 4
878
879 =item C<form.ids>
880
881 The ids of the files to unimport. Only these files are unimported not all versions of a file if the exists
882
883 =back
884
885 =head2 C<action_ajax_rename>
886
887 One file can be renamed. There can be some checks if the same filename still exists at one object.
888
889 =head1 AUTHOR
890
891 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
892
893 =cut