Dateimanagement: Mahnung: Mahnrechnung bekommt Typ dunning_invoice statt dunning
[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 $file      = SL::File->get(id => $::form->{file_id});
321   my $thumbnail = _create_thumbnail($file, $::form->{size});
322
323   my $overlay_selector = '#enlarged_thumb_' . $::form->{file_id};
324   $self->js
325     ->attr($overlay_selector, 'src', 'data:' . $thumbnail->{thumbnail_img_content_type} . ';base64,' . MIME::Base64::encode_base64($thumbnail->{thumbnail_img_content}))
326     ->data($overlay_selector, 'is-overlay-loaded', '1')
327     ->render;
328 }
329
330
331 #
332 # filters
333 #
334
335 sub check_object_params {
336   my ($self) = @_;
337
338   my $id      = ($::form->{object_id} // 0) * 1;
339   my $draftid = ($::form->{draft_id}  // 0) * 1;
340   my $gldoc   = 0;
341   my $type    = undef;
342
343   if ( $draftid == 0 && $id == 0 && $::form->{is_global} ) {
344     $gldoc = 1;
345     $type  = $::form->{object_type};
346   }
347   elsif ( $id == 0 ) {
348     $id   = $::form->{draft_id};
349     $type = 'draft';
350   } elsif ( $::form->{object_type} ) {
351     $type = $::form->{object_type};
352   }
353   die "No object type"      unless $type;
354   die "No file type"        unless $::form->{file_type};
355   die "Unknown object type" unless $file_types{$type};
356
357   $self->is_global($gldoc);
358   $self->file_type($::form->{file_type});
359   $self->object_type($type);
360   $self->object_id($id);
361   $self->object_model($file_types{$type}->{model});
362   $self->object_right($file_types{$type}->{right});
363
364  # $::auth->assert($self->object_right);
365
366  # my $model = 'SL::DB::' . $self->object_model;
367  # $self->object($model->new(id => $self->object_id)->load || die "Record not found");
368
369   return 1;
370 }
371
372 #
373 # private methods
374 #
375
376 sub _delete_all {
377   my ($self, $do_unimport, $infotext) = @_;
378   my $files = '';
379   my $ids = $::form->{ids};
380   foreach my $id_version (@{ $::form->{$ids} || [] }) {
381     my ($id, $version) = split /_/, $id_version;
382     my $dbfile = SL::File->get(id => $id);
383     if ( $dbfile ) {
384       if ( $version ) {
385         $dbfile->version($version);
386         $files .= ' ' . $dbfile->file_name if $dbfile->delete_version;
387       } else {
388         $files .= ' ' . $dbfile->file_name if $dbfile->delete;
389       }
390     }
391   }
392   $self->js->flash('info', $infotext . $files) if $files;
393   $self->_do_list(1);
394 }
395
396 sub _do_list {
397   my ($self, $json) = @_;
398   my @files;
399   if ( $self->file_type eq 'document' ) {
400     my @object_types;
401     push @object_types, $self->object_type;
402     push @object_types, qw(dunning1 dunning2 dunning3 dunning_invoice dunning_orig_invoice) if $self->object_type eq 'invoice'; # hardcoded object types?
403     @files = SL::File->get_all_versions(object_id   => $self->object_id,
404                                         object_type => \@object_types,
405                                         file_type   => $self->file_type,
406                                        );
407
408   }
409   elsif ( $self->file_type eq 'attachment' || $self->file_type eq 'image' ) {
410     @files   = SL::File->get_all(object_id   => $self->object_id,
411                                  object_type => $self->object_type,
412                                  file_type   => $self->file_type,
413                                 );
414   }
415   $self->files(\@files);
416
417   $_->{thumbnail} = _create_thumbnail($_) for @files;
418
419   if($self->object_type eq 'shop_image'){
420     $self->js
421       ->run('kivi.ShopPart.show_images', $self->object_id)
422       ->render();
423   }else{
424     $self->_mk_render('file/list', 1, 0, $json);
425   }
426 }
427
428 sub _get_from_import {
429   my ($self, $path) = @_;
430   my @foundfiles ;
431
432   my $language = $::lx_office_conf{system}->{language};
433   my $timezone = $::locale->get_local_time_zone()->name;
434   if (opendir my $dir, $path) {
435     my @files = (readdir $dir);
436     foreach my $file ( @files) {
437       next if (($file eq '.') || ($file eq '..'));
438       $file = Encode::decode('utf-8', $file);
439
440       next if ( -d "$path/$file" );
441
442       my $tmppath = File::Spec->catfile( $path, $file );
443       next if( ! -f $tmppath );
444
445       my $st = stat($tmppath);
446       my $dt = DateTime->from_epoch( epoch => $st->mtime, time_zone => $timezone, locale => $language );
447       my $sname = $main::locale->quote_special_chars('HTML', $file);
448       push @foundfiles, {
449         'name'     => $file,
450         'filename' => $sname,
451         'path'     => $tmppath,
452         'mtime'    => $st->mtime,
453         'date'     => $dt->dmy('.') . " " . $dt->hms,
454       };
455
456     }
457     closedir($dir);
458
459   } else {
460     $::lxdebug->message(LXDebug::WARN(), "SL::File::_get_from_import opendir failed to open dir " . $path);
461   }
462
463   return @foundfiles;
464 }
465
466 sub _mk_render {
467   my ($self, $template, $edit, $scanner, $json) = @_;
468   my $err;
469   eval {
470     ##TODO make code configurable
471
472     my $title;
473     my @sources = $self->_get_sources();
474     foreach my $source ( @sources ) {
475       @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files };
476     }
477     if ( $self->file_type eq 'document' ) {
478       $title = $main::locale->text('Documents');
479     } elsif ( $self->file_type eq 'attachment' ) {
480       $title = $main::locale->text('Attachments');
481     } elsif ( $self->file_type eq 'image' ) {
482       $title = $main::locale->text('Images');
483     }
484
485     my $output         = SL::Presenter->get->render(
486       $template,
487       title            => $title,
488       SOURCES          => \@sources,
489       edit_attachments => $edit,
490       object_type      => $self->object_type,
491       object_id        => $self->object_id,
492       file_type        => $self->file_type,
493       is_global        => $self->is_global,
494       json             => $json,
495     );
496     if ( $json ) {
497       $self->js->html('#'.$self->file_type.'_list_'.$self->object_type, $output);
498       if ( $self->existing && scalar(@{$self->existing}) > 0) {
499         my $first = shift @{$self->existing};
500         my ($first_id, $sfile) = split('_', $first, 2);
501         my $file = SL::File->get(id => $first_id );
502         $self->js->run('kivi.File.askForRename', $first_id, $file->file_type, $file->file_name, $sfile, join (',', @{$self->existing}), $self->is_global);
503       }
504       $self->js->render();
505     } else {
506         $self->render(\$output, { layout => 0, process => 0 });
507     }
508     1;
509   } or do {
510     if ($json ){
511       $self->js->flash(       'error', t8('internal error (see details)'))
512                ->flash_detail('error', $@)->render;
513     } else {
514       $self->render('generic/error', { layout => 0 }, label_error => $@);
515     }
516   };
517 }
518
519
520 sub _get_sources {
521   my ($self) = @_;
522   my @sources;
523   if ( $self->file_type eq 'document' ) {
524     # TODO statt gen neue attribute in filetypes :
525     if (($file_types{$self->object_type}->{gen}*1 & 4)==4) {
526       # bit 3 is set => means upload
527       my $source = {
528         'name'         => 'uploaded',
529         'title'        => $main::locale->text('uploaded Documents'),
530         'chk_action'   => 'uploaded_documents_delete',
531         'chk_title'    => $main::locale->text('Delete Documents'),
532         'chkall_title' => $main::locale->text('Delete all'),
533         'file_title'   => $main::locale->text('filename'),
534         'confirm_text' => $main::locale->text('delete'),
535         'can_rename'   => 1,
536         'are_existing' => $self->existing ? 1 : 0,
537         'rename_title' => $main::locale->text('Rename Attachments'),
538         'can_upload'   => 1,
539         'can_delete'   => 1,
540         'upload_title' => $main::locale->text('Upload Documents'),
541         'done_text'    => $main::locale->text('deleted')
542       };
543       push @sources , $source;
544     }
545
546     if (($file_types{$self->object_type}->{gen}*1 & 1)==1) {
547       my $gendata = {
548         'name'         => 'created',
549         'title'        => $main::locale->text('generated Files'),
550         'chk_action'   => 'documents_delete',
551         'chk_title'    => $main::locale->text('Delete Documents'),
552         'chkall_title' => $main::locale->text('Delete all'),
553         'file_title'   => $main::locale->text('filename'),
554         'confirm_text' => $main::locale->text('delete'),
555         'can_delete'   => $::instance_conf->get_doc_delete_printfiles,
556         'can_rename'   => $::instance_conf->get_doc_delete_printfiles,
557         'rename_title' => $main::locale->text('Rename Documents'),
558         'done_text'    => $main::locale->text('deleted')
559       };
560       push @sources , $gendata;
561     }
562
563     if (($file_types{$self->object_type}->{gen}*1 & 2)==2) {
564       my @others =  SL::File->get_other_sources();
565       foreach my $scanner_or_mailrx (@others) {
566         my $other = {
567           'name'         => $scanner_or_mailrx->{name},
568           'title'        => $main::locale->text('from \'#1\' imported Files', $scanner_or_mailrx->{description}),
569           'chk_action'   => $scanner_or_mailrx->{name}.'_unimport',
570           'chk_title'    => $main::locale->text('Unimport documents'),
571           'chkall_title' => $main::locale->text('Unimport all'),
572           'file_title'   => $main::locale->text('filename'),
573           'confirm_text' => $main::locale->text('unimport'),
574           'can_rename'   => 1,
575           'rename_title' => $main::locale->text('Rename Documents'),
576           'can_import'   => 1,
577           'can_delete'   => 0,
578           'import_title' => $main::locale->text('Add Document from \'#1\'', $scanner_or_mailrx->{name}),
579           'path'         => $scanner_or_mailrx->{directory},
580           'done_text'    => $main::locale->text('unimported')
581         };
582         push @sources , $other;
583       }
584     }
585   }
586   elsif ( $self->file_type eq 'attachment' ) {
587     my $attdata = {
588       'name'         => 'uploaded',
589       'title'        => $main::locale->text(''),
590       'chk_action'   => 'attachments_delete',
591       'chk_title'    => $main::locale->text('Delete Attachments'),
592       'chkall_title' => $main::locale->text('Delete all'),
593       'file_title'   => $main::locale->text('filename'),
594       'confirm_text' => $main::locale->text('delete'),
595       'can_rename'   => 1,
596       'are_existing' => $self->existing ? 1 : 0,
597       'rename_title' => $main::locale->text('Rename Attachments'),
598       'can_upload'   => 1,
599       'can_delete'   => 1,
600       'upload_title' => $main::locale->text('Upload Attachments'),
601       'done_text'    => $main::locale->text('deleted')
602     };
603     push @sources , $attdata;
604   }
605   elsif ( $self->file_type eq 'image' ) {
606     my $attdata = {
607       'name'         => 'uploaded',
608       'title'        => $main::locale->text(''),
609       'chk_action'   => 'images_delete',
610       'chk_title'    => $main::locale->text('Delete Images'),
611       'chkall_title' => $main::locale->text('Delete all'),
612       'file_title'   => $main::locale->text('filename'),
613       'confirm_text' => $main::locale->text('delete'),
614       'can_rename'   => 1,
615       'are_existing' => $self->existing ? 1 : 0,
616       'rename_title' => $main::locale->text('Rename Images'),
617       'can_upload'   => 1,
618       'can_delete'   => 1,
619       'upload_title' => $main::locale->text('Upload Images'),
620       'done_text'    => $main::locale->text('deleted')
621     };
622     push @sources , $attdata;
623   }
624   return @sources;
625 }
626
627 # ignores all errros
628 # todo: cache thumbs?
629 sub _create_thumbnail {
630   my ($file, $size) = @_;
631
632   $size //= 64;
633
634   my $filename;
635   if (!eval { $filename = $file->get_file(); 1; }) {
636     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail get_file failed: " . $EVAL_ERROR);
637     return;
638   }
639
640   # Workaround for pfds which are not handled by file_probe_type.
641   # Maybe use mime info stored in db?
642   my $mime_type = File::MimeInfo::Magic::magic($filename);
643   if ($mime_type =~ m{pdf}) {
644     $filename = _convert_pdf_to_png($filename, size => $size);
645   }
646   return if !$filename;
647
648   my $content;
649   if (!eval { $content = slurp $filename; 1; }) {
650     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail slurp failed: " . $EVAL_ERROR);
651     return;
652   }
653
654   my $ret;
655   if (!eval { $ret = file_probe_type($content, size => $size); 1; }) {
656     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail file_probe_type failed: " . $EVAL_ERROR);
657     return;
658   }
659
660   # file_probe_type returns a hash ref with thumbnail info and content
661   # or an error message
662   if ('HASH' ne ref $ret) {
663     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail file_probe_type returned an error: " . $ret);
664     return;
665   }
666
667   return $ret;
668 }
669
670 sub _convert_pdf_to_png {
671   my ($filename, %params) = @_;
672
673   my $size    = $params{size} // 64;
674   my $sfile   = SL::SessionFile::Random->new();
675   my $command = 'pdftoppm -singlefile -scale-to ' . $size . ' -png' . ' ' . $filename . ' ' . $sfile->file_name;
676
677   if (system($command) == -1) {
678     $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: system call failed: " . $ERRNO);
679     return;
680   }
681   if ($CHILD_ERROR) {
682     $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: pdftoppm failed with error code: " . ($CHILD_ERROR >> 8));
683     return;
684   }
685
686   return $sfile->file_name . '.png';
687 }
688
689 1;
690
691 __END__
692
693 =pod
694
695 =encoding utf-8
696
697 =head1 NAME
698
699 SL::Controller::File - Controller for managing files
700
701 =head1 SYNOPSIS
702
703 The Controller is called directly from the webpages
704
705     <a href="controller.pl?action=File/list&file_type=document\
706        &object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">
707
708
709 or indirectly via javascript functions from js/kivi.File.js
710
711     kivi.popup_dialog({ url:     'controller.pl',
712                         data:    { action     : 'File/ajax_upload',
713                                    file_type  : 'uploaded',
714                                    object_type: type,
715                                    object_id  : id
716                                  }
717                            ...
718
719 =head1 DESCRIPTION
720
721 This is a controller for handling files in a storage independent way.
722 The storage may be a Filesystem,a WebDAV, a Database or DMS.
723 These backends must be configered in ClientConfig.
724 This Controller use as intermediate layer for storage C<SL::File>.
725
726 The Controller is responsible to display forms for displaying the files at the ERP-objects and
727 for uploading and downloading the files.
728
729 More description of the intermediate layer see L<SL::File>.
730
731 =head1 METHODS
732
733 =head2 C<action_list>
734
735 This loads a list of files on a webpage. This can be done with a normal submit or via an ajax/json call.
736 Dependent of file_type different sources are available.
737
738 For documents there are the 'created' source and the imports from scanners or email.
739 For attachments and images only the 'uploaded' source available.
740
741 Available C<FORM PARAMS>:
742
743 =over 4
744
745 =item C<form.object_id>
746
747 The Id of the ERP-object.
748
749 =item C<form.object_type>
750
751 The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
752
753 =item C<form.file_type>
754
755 For one ERP-object may exists different type of documents the type may be "documents","attachments" or "images".
756 This file_type is a filter for the list.
757
758 =item C<form.json>
759
760 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.
761
762 =back
763
764
765 =head2 C<action_ajax_upload>
766
767
768 A new file or more files can selected by a dialog and insert into the system.
769
770
771 Available C<FORM PARAMS>:
772
773 =over 4
774
775 =item C<form.file_type>
776
777 This parameter describe here the source for a new file :
778 "attachments" and "images"
779
780 This is a normal upload selection, which may be more then one file to upload.
781
782 =item C<form.object_id>
783
784 and
785
786 =item C<form.object_type>
787
788 are the same as at C<action_list>
789
790 =back
791
792 =head2  C<action_ajax_files_uploaded>
793
794 The Upload of selected Files. The "multipart_formdata" is parsed in SL::Request into the formsvariable "form.ATTACHMENTS".
795 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?).
796 If the same filename still exists at this object after the download for each existing filename a rename dialog will be opened.
797
798 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.
799
800 Available C<FORM PARAMS>:
801
802 =over 4
803
804 =item C<form.ATTACHMENTS.uploadfiles>
805
806 This is an array of elements which have {filename} for the name and {data} for the contents.
807
808 Also object_id, object_type and file_type
809
810 =back
811
812 =head2 C<action_download>
813
814 This is the real download of a file normally called via javascript "$.download("controller.pl", data);"
815
816 Available C<FORM PARAMS>:
817
818 =over 4
819
820 Also object_id, object_type and file_type
821
822 =back
823
824 =head2 C<action_ajax_importdialog>
825
826 A Dialog with all available and not imported files to import is open.
827 More then one file can be selected.
828
829 Available C<FORM PARAMS>:
830
831 =over 4
832
833 =item C<form.source>
834
835 The name of the source like "scanner1" or "email"
836
837 =item C<form.path>
838
839 The full path to the directory on the server, where the files to import can found
840
841 Also object_id, object_type and file_type
842
843 =back
844
845 =head2 C<action_ajax_delete>
846
847 Some files can be deleted
848
849 Available C<FORM PARAMS>:
850
851 =over 4
852
853 =item C<form.ids>
854
855 The ids of the files to delete. Only this files are deleted not all versions of a file if the exists
856
857 =back
858
859 =head2 C<action_ajax_unimport>
860
861 Some files can be unimported, dependent of the source of the file. This means they are moved
862 back to the directory of the source
863
864 Available C<FORM PARAMS>:
865
866 =over 4
867
868 =item C<form.ids>
869
870 The ids of the files to unimport. Only these files are unimported not all versions of a file if the exists
871
872 =back
873
874 =head2 C<action_ajax_rename>
875
876 One file can be renamed. There can be some checks if the same filename still exists at one object.
877
878 =head1 AUTHOR
879
880 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
881
882 =cut