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