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