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