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