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