1 package SL::Controller::File;
 
   5 use parent qw(SL::Controller::Base);
 
   7 use List::Util qw(first max);
 
  10 use Encode qw(decode);
 
  11 use English qw( -no_match_vars );
 
  16 use File::Slurp qw(slurp);
 
  18 use File::Spec::Win32;
 
  19 use File::MimeInfo::Magic;
 
  21 use SL::DB::Helper::Mappings;
 
  23 use SL::DB::DeliveryOrder;
 
  26 use SL::DB::PurchaseInvoice;
 
  28 use SL::DB::GLTransaction;
 
  32 use SL::Helper::CreatePDF qw(:all);
 
  33 use SL::Locale::String;
 
  35 use SL::SessionFile::Random;
 
  37 use SL::Controller::Helper::ThumbnailCreator qw(file_probe_image_type file_probe_type);
 
  39 use constant DO_DELETE   => 0;
 
  40 use constant DO_UNIMPORT => 1;
 
  42 use Rose::Object::MakeMethods::Generic
 
  44     'scalar --get_set_init' => [ qw() ],
 
  45     'scalar' => [ qw(object object_type object_model object_id object_right file_type files is_global existing) ],
 
  48 __PACKAGE__->run_before('check_object_params', only => [ qw(list ajax_delete ajax_importdialog ajax_import ajax_unimport ajax_upload ajax_files_uploaded) ]);
 
  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
 
  58   'sales_quotation'         => { gen => 1, gltype => '',   dir =>'SalesQuotation',       model => 'Order',          right => 'import_ar'  },
 
  59   'sales_order'             => { gen => 5, gltype => '',   dir =>'SalesOrder',           model => 'Order',          right => 'import_ar'  },
 
  60   'sales_delivery_order'    => { gen => 1, gltype => '',   dir =>'SalesDeliveryOrder',   model => 'DeliveryOrder',  right => 'import_ar'  },
 
  61   'invoice'                 => { gen => 1, gltype => 'ar', dir =>'SalesInvoice',         model => 'Invoice',        right => 'import_ar'  },
 
  62   'credit_note'             => { gen => 1, gltype => '',   dir =>'CreditNote',           model => 'Invoice',        right => 'import_ar'  },
 
  63   'request_quotation'       => { gen => 7, gltype => '',   dir =>'RequestForQuotation',  model => 'Order',          right => 'import_ap'  },
 
  64   'purchase_order'          => { gen => 7, gltype => '',   dir =>'PurchaseOrder',        model => 'Order',          right => 'import_ap'  },
 
  65   'purchase_delivery_order' => { gen => 7, gltype => '',   dir =>'PurchaseDeliveryOrder',model => 'DeliveryOrder',  right => 'import_ap'  },
 
  66   'purchase_invoice'        => { gen => 6, gltype => 'ap', dir =>'PurchaseInvoice',      model => 'PurchaseInvoice',right => 'import_ap'  },
 
  67   'vendor'                  => { gen => 0, gltype => '',   dir =>'Vendor',               model => 'Vendor',         right => 'xx'         },
 
  68   'customer'                => { gen => 1, gltype => '',   dir =>'Customer',             model => 'Customer',       right => 'xx'         },
 
  69   'part'                    => { gen => 0, gltype => '',   dir =>'Part',                 model => 'Part',           right => 'xx'         },
 
  70   'gl_transaction'          => { gen => 6, gltype => 'gl', dir =>'GeneralLedger',        model => 'GLTransaction',  right => 'import_ap'  },
 
  71   'draft'                   => { gen => 0, gltype => '',   dir =>'Draft',                model => 'Draft',          right => 'xx'         },
 
  72   'csv_customer'            => { gen => 1, gltype => '',   dir =>'Reports',              model => 'Customer',       right => 'xx'         },
 
  73   'csv_vendor'              => { gen => 1, gltype => '',   dir =>'Reports',              model => 'Vendor',         right => 'xx'         },
 
  74   'shop_image'              => { gen => 0, gltype => '',   dir =>'ShopImages',           model => 'Part',           right => 'xx'         },
 
  75   'letter'                  => { gen => 7, gltype => '',   dir =>'Letter',               model => 'Letter',         right => 'sales_letter_edit | purchase_letter_edit' },
 
  79 # $main::locale->text('imported')
 
  89   $is_json = 1 if $::form->{json};
 
  91   $self->_do_list($is_json);
 
  94 sub action_ajax_importdialog {
 
  96   $::auth->assert($self->object_right);
 
  97   my $path   = $::form->{path};
 
  98   my @files  = $self->_get_from_import($path);
 
 100     'name'         => $::form->{source},
 
 102     'chk_action'   => $::form->{source}.'_import',
 
 103     'chk_title'    => $main::locale->text('Import scanned documents'),
 
 104     'chkall_title' => $main::locale->text('Import all'),
 
 107   $self->render('file/import_dialog',
 
 114 sub action_ajax_import {
 
 116   $::auth->assert($self->object_right);
 
 117   my $ids    = $::form->{ids};
 
 118   my $source = $::form->{source};
 
 119   my $path   = $::form->{path};
 
 120   my @files  = $self->_get_from_import($path);
 
 121   foreach my $filename (@{ $::form->{$ids} || [] }) {
 
 122     my ($file, undef) = grep { $_->{name} eq $filename } @files;
 
 124       my $obj = SL::File->save(object_id   => $self->object_id,
 
 125                                object_type => $self->object_type,
 
 126                                mime_type   => 'application/pdf',
 
 128                                file_type   => 'document',
 
 129                                file_name   => $file->{filename},
 
 130                                file_path   => $file->{path}
 
 132       unlink($file->{path}) if $obj;
 
 138 sub action_ajax_delete {
 
 140   $self->_delete_all(DO_DELETE, $::locale->text('Following files are deleted:'));
 
 143 sub action_ajax_unimport {
 
 145   $self->_delete_all(DO_UNIMPORT, $::locale->text('Following files are unimported:'));
 
 148 sub action_ajax_rename {
 
 150   my ($id, $version) = split /_/, $::form->{id};
 
 151   my $file = SL::File->get(id => $id);
 
 153     $self->js->flash('error', $::locale->text('File not exists !'))->render();
 
 156   my $sessionfile = $::form->{sessionfile};
 
 157   if ( $sessionfile && -f $sessionfile ) {
 
 159     if ( $::form->{to} eq $file->file_name ) {
 
 160       # no rename so use as new version
 
 161       $file->save_file($sessionfile);
 
 162       $self->js->flash('warning', $::locale->text('File \'#1\' is used as new Version !', $file->file_name));
 
 165       # new filename, so it is a new file with the same attributes as the old file
 
 167         SL::File->save(object_id   => $file->object_id,
 
 168                        object_type => $file->object_type,
 
 169                        mime_type   => $file->mime_type,
 
 170                        source      => $file->source,
 
 171                        file_type   => $file->file_type,
 
 172                        file_name   => $::form->{to},
 
 173                        file_path   => $sessionfile
 
 175         unlink($sessionfile);
 
 178         $self->js->flash(       'error', t8('internal error (see details)'))
 
 179                  ->flash_detail('error', $@)->render;
 
 189       $result = $file->rename($::form->{to});
 
 192       $self->js->flash(       'error', t8('internal error (see details)'))
 
 193                ->flash_detail('error', $@)->render;
 
 197     if ($result != SL::File::RENAME_OK) {
 
 198       $self->js->flash('error',
 
 199                          $result == SL::File::RENAME_EXISTS ? $::locale->text('File still exists !')
 
 200                        : $result == SL::File::RENAME_SAME   ? $::locale->text('Same Filename !')
 
 201                        :                                      $::locale->text('File not exists !'))
 
 206   $self->is_global($::form->{is_global});
 
 207   $self->file_type(  $file->file_type);
 
 208   $self->object_type($file->object_type);
 
 209   $self->object_id(  $file->object_id);
 
 210   #$self->object_model($file_types{$file->module}->{model});
 
 211   #$self->object_right($file_types{$file->module}->{right});
 
 212   if ( $::form->{next_ids} ) {
 
 213     my @existing = split(/,/, $::form->{next_ids});
 
 214     $self->existing(\@existing);
 
 219 sub action_ajax_upload {
 
 221   $self->{maxsize} = $::instance_conf->get_doc_max_filesize;
 
 222   $self->{accept_types} = '';
 
 223   $self->{accept_types} = 'image/png,image/gif,image/jpeg,image/tiff,*png,*gif,*.jpg,*.tif' if $self->{file_type} eq 'image';
 
 224   $self->render('file/upload_dialog',
 
 230 sub action_ajax_files_uploaded {
 
 233   my $source = 'uploaded';
 
 235   if ( $::form->{ATTACHMENTS}->{uploadfiles} ) {
 
 236     my @upfiles = @{ $::form->{ATTACHMENTS}->{uploadfiles} };
 
 237     foreach my $idx (0 .. scalar(@upfiles) - 1) {
 
 239         my $fname = uri_unescape($upfiles[$idx]->{filename});
 
 240         # normalize and find basename
 
 241         # first split with unix rules
 
 242         # after that split with windows rules
 
 243         my ($volume, $directories, $basefile) = File::Spec::Unix->splitpath($fname);
 
 244         ($volume, $directories, $basefile) = File::Spec::Win32->splitpath($basefile);
 
 246         # to find real mime_type by magic we must save the filedata
 
 248         my $sess_fname = "file_upload_" . $self->object_type . "_" . $self->object_id . "_" . $idx;
 
 249         my $sfile      = SL::SessionFile->new($sess_fname, mode => 'w');
 
 251         $sfile->fh->print(${$upfiles[$idx]->{data}});
 
 253         my $mime_type = File::MimeInfo::Magic::magic($sfile->file_name);
 
 256           # if filename has the suffix "pdf", but isn't really a pdf, set mimetype for no suffix
 
 257           $mime_type = File::MimeInfo::Magic::mimetype($basefile);
 
 258           $mime_type = 'application/octet-stream' if $mime_type eq 'application/pdf' || !$mime_type;
 
 260         if ( $self->file_type eq 'image' && $self->file_probe_image_type($mime_type, $basefile)) {
 
 263         my ($existobj) = SL::File->get_all(object_id   => $self->object_id,
 
 264                                            object_type => $self->object_type,
 
 265                                            mime_type   => $mime_type,
 
 267                                            file_type   => $self->file_type,
 
 268                                            file_name   => $basefile,
 
 272           push @existing, $existobj->id.'_'.$sfile->file_name;
 
 274           my $fileobj = SL::File->save(object_id        => $self->object_id,
 
 275                                        object_type      => $self->object_type,
 
 276                                        mime_type        => $mime_type,
 
 278                                        file_type        => $self->file_type,
 
 279                                        file_name        => $basefile,
 
 280                                        title            => $::form->{title},
 
 281                                        description      => $::form->{description},
 
 282                                        ## two possibilities: what is better ? content or sessionfile ??
 
 283                                        file_contents    => ${$upfiles[$idx]->{data}},
 
 284                                        file_path        => $sfile->file_name
 
 286           unlink($sfile->file_name);
 
 290         $self->js->flash(       'error', t8('internal error (see details)'))
 
 291                  ->flash_detail('error', $@)->render;
 
 296   $self->existing(\@existing);
 
 300 sub action_download {
 
 303   my $id      = $::form->{id};
 
 304   my $version = $::form->{version};
 
 306   my $file = SL::File->get(id => $id );
 
 307   $file->version($version) if $version;
 
 308   my $ref  = $file->get_content;
 
 309   if ( $file && $ref ) {
 
 310     return $self->send_file($ref,
 
 311       type => $file->mime_type,
 
 312       name => $file->file_name,
 
 317 sub action_ajax_get_thumbnail {
 
 320   my $id      = $::form->{file_id};
 
 321   my $version = $::form->{file_version};
 
 322   my $file    = SL::File->get(id => $id);
 
 324   $file->version($version) if $version;
 
 326   my $thumbnail = _create_thumbnail($file, $::form->{size});
 
 328   my $overlay_selector  = '#enlarged_thumb_' . $id;
 
 329   $overlay_selector    .= '_' . $version            if $version;
 
 331     ->attr($overlay_selector, 'src', 'data:' . $thumbnail->{thumbnail_img_content_type} . ';base64,' . MIME::Base64::encode_base64($thumbnail->{thumbnail_img_content}))
 
 332     ->data($overlay_selector, 'is-overlay-loaded', '1')
 
 341 sub check_object_params {
 
 344   my $id      = ($::form->{object_id} // 0) * 1;
 
 345   my $draftid = ($::form->{draft_id}  // 0) * 1;
 
 349   if ( $draftid == 0 && $id == 0 && $::form->{is_global} ) {
 
 351     $type  = $::form->{object_type};
 
 354     $id   = $::form->{draft_id};
 
 356   } elsif ( $::form->{object_type} ) {
 
 357     $type = $::form->{object_type};
 
 359   die "No object type"      unless $type;
 
 360   die "No file type"        unless $::form->{file_type};
 
 361   die "Unknown object type" unless $file_types{$type};
 
 363   $self->is_global($gldoc);
 
 364   $self->file_type($::form->{file_type});
 
 365   $self->object_type($type);
 
 366   $self->object_id($id);
 
 367   $self->object_model($file_types{$type}->{model});
 
 368   $self->object_right($file_types{$type}->{right});
 
 370  # $::auth->assert($self->object_right);
 
 372  # my $model = 'SL::DB::' . $self->object_model;
 
 373  # $self->object($model->new(id => $self->object_id)->load || die "Record not found");
 
 383   my ($self, $do_unimport, $infotext) = @_;
 
 385   my $ids = $::form->{ids};
 
 386   foreach my $id_version (@{ $::form->{$ids} || [] }) {
 
 387     my ($id, $version) = split /_/, $id_version;
 
 388     my $dbfile = SL::File->get(id => $id);
 
 391         $dbfile->version($version);
 
 392         $files .= ' ' . $dbfile->file_name if $dbfile->delete_version;
 
 394         $files .= ' ' . $dbfile->file_name if $dbfile->delete;
 
 398   $self->js->flash('info', $infotext . $files) if $files;
 
 403   my ($self, $json) = @_;
 
 405   if ( $self->file_type eq 'document' ) {
 
 407     push @object_types, $self->object_type;
 
 408     push @object_types, qw(dunning1 dunning2 dunning3 dunning_invoice dunning_orig_invoice) if $self->object_type eq 'invoice'; # hardcoded object types?
 
 409     @files = SL::File->get_all_versions(object_id   => $self->object_id,
 
 410                                         object_type => \@object_types,
 
 411                                         file_type   => $self->file_type,
 
 415   elsif ( $self->file_type eq 'attachment' || $self->file_type eq 'image' ) {
 
 416     @files   = SL::File->get_all(object_id   => $self->object_id,
 
 417                                  object_type => $self->object_type,
 
 418                                  file_type   => $self->file_type,
 
 421   $self->files(\@files);
 
 423   $_->{thumbnail} = _create_thumbnail($_) for @files;
 
 425   if($self->object_type eq 'shop_image'){
 
 427       ->run('kivi.ShopPart.show_images', $self->object_id)
 
 430     $self->_mk_render('file/list', 1, 0, $json);
 
 434 sub _get_from_import {
 
 435   my ($self, $path) = @_;
 
 438   my $language = $::lx_office_conf{system}->{language};
 
 439   my $timezone = $::locale->get_local_time_zone()->name;
 
 440   if (opendir my $dir, $path) {
 
 441     my @files = (readdir $dir);
 
 442     foreach my $file ( @files) {
 
 443       next if (($file eq '.') || ($file eq '..'));
 
 444       $file = Encode::decode('utf-8', $file);
 
 446       next if ( -d "$path/$file" );
 
 448       my $tmppath = File::Spec->catfile( $path, $file );
 
 449       next if( ! -f $tmppath );
 
 451       my $st = stat($tmppath);
 
 452       my $dt = DateTime->from_epoch( epoch => $st->mtime, time_zone => $timezone, locale => $language );
 
 453       my $sname = $main::locale->quote_special_chars('HTML', $file);
 
 456         'filename' => $sname,
 
 458         'mtime'    => $st->mtime,
 
 459         'date'     => $dt->dmy('.') . " " . $dt->hms,
 
 466     $::lxdebug->message(LXDebug::WARN(), "SL::File::_get_from_import opendir failed to open dir " . $path);
 
 473   my ($self, $template, $edit, $scanner, $json) = @_;
 
 476     ##TODO make code configurable
 
 479     my @sources = $self->_get_sources();
 
 480     foreach my $source ( @sources ) {
 
 481       @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files };
 
 483     if ( $self->file_type eq 'document' ) {
 
 484       $title = $main::locale->text('Documents');
 
 485     } elsif ( $self->file_type eq 'attachment' ) {
 
 486       $title = $main::locale->text('Attachments');
 
 487     } elsif ( $self->file_type eq 'image' ) {
 
 488       $title = $main::locale->text('Images');
 
 491     my $output         = SL::Presenter->get->render(
 
 494       SOURCES          => \@sources,
 
 495       edit_attachments => $edit,
 
 496       object_type      => $self->object_type,
 
 497       object_id        => $self->object_id,
 
 498       file_type        => $self->file_type,
 
 499       is_global        => $self->is_global,
 
 503       $self->js->html('#'.$self->file_type.'_list_'.$self->object_type, $output);
 
 504       if ( $self->existing && scalar(@{$self->existing}) > 0) {
 
 505         my $first = shift @{$self->existing};
 
 506         my ($first_id, $sfile) = split('_', $first, 2);
 
 507         my $file = SL::File->get(id => $first_id );
 
 508         $self->js->run('kivi.File.askForRename', $first_id, $file->file_type, $file->file_name, $sfile, join (',', @{$self->existing}), $self->is_global);
 
 512         $self->render(\$output, { layout => 0, process => 0 });
 
 517       $self->js->flash(       'error', t8('internal error (see details)'))
 
 518                ->flash_detail('error', $@)->render;
 
 520       $self->render('generic/error', { layout => 0 }, label_error => $@);
 
 529   if ( $self->file_type eq 'document' ) {
 
 530     # TODO statt gen neue attribute in filetypes :
 
 531     if (($file_types{$self->object_type}->{gen}*1 & 4)==4) {
 
 532       # bit 3 is set => means upload
 
 534         'name'         => 'uploaded',
 
 535         'title'        => $main::locale->text('uploaded Documents'),
 
 536         'chk_action'   => 'uploaded_documents_delete',
 
 537         'chk_title'    => $main::locale->text('Delete Documents'),
 
 538         'chkall_title' => $main::locale->text('Delete all'),
 
 539         'file_title'   => $main::locale->text('filename'),
 
 540         'confirm_text' => $main::locale->text('delete'),
 
 542         'are_existing' => $self->existing ? 1 : 0,
 
 543         'rename_title' => $main::locale->text('Rename Attachments'),
 
 546         'upload_title' => $main::locale->text('Upload Documents'),
 
 547         'done_text'    => $main::locale->text('deleted')
 
 549       push @sources , $source;
 
 552     if (($file_types{$self->object_type}->{gen}*1 & 1)==1) {
 
 555         'title'        => $main::locale->text('generated Files'),
 
 556         'chk_action'   => 'documents_delete',
 
 557         'chk_title'    => $main::locale->text('Delete Documents'),
 
 558         'chkall_title' => $main::locale->text('Delete all'),
 
 559         'file_title'   => $main::locale->text('filename'),
 
 560         'confirm_text' => $main::locale->text('delete'),
 
 561         'can_delete'   => $::instance_conf->get_doc_delete_printfiles,
 
 562         'can_rename'   => $::instance_conf->get_doc_delete_printfiles,
 
 563         'rename_title' => $main::locale->text('Rename Documents'),
 
 564         'done_text'    => $main::locale->text('deleted')
 
 566       push @sources , $gendata;
 
 569     if (($file_types{$self->object_type}->{gen}*1 & 2)==2) {
 
 570       my @others =  SL::File->get_other_sources();
 
 571       foreach my $scanner_or_mailrx (@others) {
 
 573           'name'         => $scanner_or_mailrx->{name},
 
 574           'title'        => $main::locale->text('from \'#1\' imported Files', $scanner_or_mailrx->{description}),
 
 575           'chk_action'   => $scanner_or_mailrx->{name}.'_unimport',
 
 576           'chk_title'    => $main::locale->text('Unimport documents'),
 
 577           'chkall_title' => $main::locale->text('Unimport all'),
 
 578           'file_title'   => $main::locale->text('filename'),
 
 579           'confirm_text' => $main::locale->text('unimport'),
 
 581           'rename_title' => $main::locale->text('Rename Documents'),
 
 584           'import_title' => $main::locale->text('Add Document from \'#1\'', $scanner_or_mailrx->{name}),
 
 585           'path'         => $scanner_or_mailrx->{directory},
 
 586           'done_text'    => $main::locale->text('unimported')
 
 588         push @sources , $other;
 
 592   elsif ( $self->file_type eq 'attachment' ) {
 
 594       'name'         => 'uploaded',
 
 595       'title'        => $main::locale->text(''),
 
 596       'chk_action'   => 'attachments_delete',
 
 597       'chk_title'    => $main::locale->text('Delete Attachments'),
 
 598       'chkall_title' => $main::locale->text('Delete all'),
 
 599       'file_title'   => $main::locale->text('filename'),
 
 600       'confirm_text' => $main::locale->text('delete'),
 
 602       'are_existing' => $self->existing ? 1 : 0,
 
 603       'rename_title' => $main::locale->text('Rename Attachments'),
 
 606       'upload_title' => $main::locale->text('Upload Attachments'),
 
 607       'done_text'    => $main::locale->text('deleted')
 
 609     push @sources , $attdata;
 
 611   elsif ( $self->file_type eq 'image' ) {
 
 613       'name'         => 'uploaded',
 
 614       'title'        => $main::locale->text(''),
 
 615       'chk_action'   => 'images_delete',
 
 616       'chk_title'    => $main::locale->text('Delete Images'),
 
 617       'chkall_title' => $main::locale->text('Delete all'),
 
 618       'file_title'   => $main::locale->text('filename'),
 
 619       'confirm_text' => $main::locale->text('delete'),
 
 621       'are_existing' => $self->existing ? 1 : 0,
 
 622       'rename_title' => $main::locale->text('Rename Images'),
 
 625       'upload_title' => $main::locale->text('Upload Images'),
 
 626       'done_text'    => $main::locale->text('deleted')
 
 628     push @sources , $attdata;
 
 634 # todo: cache thumbs?
 
 635 sub _create_thumbnail {
 
 636   my ($file, $size) = @_;
 
 641   if (!eval { $filename = $file->get_file(); 1; }) {
 
 642     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail get_file failed: " . $EVAL_ERROR);
 
 646   # Workaround for pfds which are not handled by file_probe_type.
 
 647   # Maybe use mime info stored in db?
 
 648   my $mime_type = File::MimeInfo::Magic::magic($filename);
 
 649   if ($mime_type =~ m{pdf}) {
 
 650     $filename = _convert_pdf_to_png($filename, size => $size);
 
 652   return if !$filename;
 
 655   if (!eval { $content = slurp $filename; 1; }) {
 
 656     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail slurp failed: " . $EVAL_ERROR);
 
 661   if (!eval { $ret = file_probe_type($content, size => $size); 1; }) {
 
 662     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail file_probe_type failed: " . $EVAL_ERROR);
 
 666   # file_probe_type returns a hash ref with thumbnail info and content
 
 667   # or an error message
 
 668   if ('HASH' ne ref $ret) {
 
 669     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail file_probe_type returned an error: " . $ret);
 
 676 sub _convert_pdf_to_png {
 
 677   my ($filename, %params) = @_;
 
 679   my $size    = $params{size} // 64;
 
 680   my $sfile   = SL::SessionFile::Random->new();
 
 681   my $command = 'pdftoppm -singlefile -scale-to ' . $size . ' -png' . ' ' . $filename . ' ' . $sfile->file_name;
 
 683   if (system($command) == -1) {
 
 684     $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: system call failed: " . $ERRNO);
 
 688     $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: pdftoppm failed with error code: " . ($CHILD_ERROR >> 8));
 
 692   return $sfile->file_name . '.png';
 
 705 SL::Controller::File - Controller for managing files
 
 709 The Controller is called directly from the webpages
 
 711     <a href="controller.pl?action=File/list&file_type=document\
 
 712        &object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">
 
 715 or indirectly via javascript functions from js/kivi.File.js
 
 717     kivi.popup_dialog({ url:     'controller.pl',
 
 718                         data:    { action     : 'File/ajax_upload',
 
 719                                    file_type  : 'uploaded',
 
 727 This is a controller for handling files in a storage independent way.
 
 728 The storage may be a Filesystem,a WebDAV, a Database or DMS.
 
 729 These backends must be configered in ClientConfig.
 
 730 This Controller use as intermediate layer for storage C<SL::File>.
 
 732 The Controller is responsible to display forms for displaying the files at the ERP-objects and
 
 733 for uploading and downloading the files.
 
 735 More description of the intermediate layer see L<SL::File>.
 
 739 =head2 C<action_list>
 
 741 This loads a list of files on a webpage. This can be done with a normal submit or via an ajax/json call.
 
 742 Dependent of file_type different sources are available.
 
 744 For documents there are the 'created' source and the imports from scanners or email.
 
 745 For attachments and images only the 'uploaded' source available.
 
 747 Available C<FORM PARAMS>:
 
 751 =item C<form.object_id>
 
 753 The Id of the ERP-object.
 
 755 =item C<form.object_type>
 
 757 The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
 
 759 =item C<form.file_type>
 
 761 For one ERP-object may exists different type of documents the type may be "documents","attachments" or "images".
 
 762 This file_type is a filter for the list.
 
 766 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 =head2 C<action_ajax_upload>
 
 774 A new file or more files can selected by a dialog and insert into the system.
 
 777 Available C<FORM PARAMS>:
 
 781 =item C<form.file_type>
 
 783 This parameter describe here the source for a new file :
 
 784 "attachments" and "images"
 
 786 This is a normal upload selection, which may be more then one file to upload.
 
 788 =item C<form.object_id>
 
 792 =item C<form.object_type>
 
 794 are the same as at C<action_list>
 
 798 =head2  C<action_ajax_files_uploaded>
 
 800 The Upload of selected Files. The "multipart_formdata" is parsed in SL::Request into the formsvariable "form.ATTACHMENTS".
 
 801 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?).
 
 802 If the same filename still exists at this object after the download for each existing filename a rename dialog will be opened.
 
 804 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.
 
 806 Available C<FORM PARAMS>:
 
 810 =item C<form.ATTACHMENTS.uploadfiles>
 
 812 This is an array of elements which have {filename} for the name and {data} for the contents.
 
 814 Also object_id, object_type and file_type
 
 818 =head2 C<action_download>
 
 820 This is the real download of a file normally called via javascript "$.download("controller.pl", data);"
 
 822 Available C<FORM PARAMS>:
 
 826 Also object_id, object_type and file_type
 
 830 =head2 C<action_ajax_importdialog>
 
 832 A Dialog with all available and not imported files to import is open.
 
 833 More then one file can be selected.
 
 835 Available C<FORM PARAMS>:
 
 841 The name of the source like "scanner1" or "email"
 
 845 The full path to the directory on the server, where the files to import can found
 
 847 Also object_id, object_type and file_type
 
 851 =head2 C<action_ajax_delete>
 
 853 Some files can be deleted
 
 855 Available C<FORM PARAMS>:
 
 861 The ids of the files to delete. Only this files are deleted not all versions of a file if the exists
 
 865 =head2 C<action_ajax_unimport>
 
 867 Some files can be unimported, dependent of the source of the file. This means they are moved
 
 868 back to the directory of the source
 
 870 Available C<FORM PARAMS>:
 
 876 The ids of the files to unimport. Only these files are unimported not all versions of a file if the exists
 
 880 =head2 C<action_ajax_rename>
 
 882 One file can be renamed. There can be some checks if the same filename still exists at one object.
 
 886 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>