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