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   'project'                 => { gen => 0, gltype => '',   dir =>'Project',              model => 'Project',        right => 'xx'         },
 
  70   'part'                    => { gen => 0, gltype => '',   dir =>'Part',                 model => 'Part',           right => 'xx'         },
 
  71   'gl_transaction'          => { gen => 6, gltype => 'gl', dir =>'GeneralLedger',        model => 'GLTransaction',  right => 'import_ap'  },
 
  72   'draft'                   => { gen => 0, gltype => '',   dir =>'Draft',                model => 'Draft',          right => 'xx'         },
 
  73   'csv_customer'            => { gen => 1, gltype => '',   dir =>'Reports',              model => 'Customer',       right => 'xx'         },
 
  74   'csv_vendor'              => { gen => 1, gltype => '',   dir =>'Reports',              model => 'Vendor',         right => 'xx'         },
 
  75   'shop_image'              => { gen => 0, gltype => '',   dir =>'ShopImages',           model => 'Part',           right => 'xx'         },
 
  76   'letter'                  => { gen => 7, gltype => '',   dir =>'Letter',               model => 'Letter',         right => 'sales_letter_edit | purchase_letter_edit' },
 
  80 # $main::locale->text('imported')
 
  90   $is_json = 1 if $::form->{json};
 
  92   $self->_do_list($is_json);
 
  95 sub action_ajax_importdialog {
 
  97   $::auth->assert($self->object_right);
 
  98   my $path   = $::form->{path};
 
  99   my @files  = $self->_get_from_import($path);
 
 101     'name'         => $::form->{source},
 
 103     'chk_action'   => $::form->{source}.'_import',
 
 104     'chk_title'    => $main::locale->text('Import scanned documents'),
 
 105     'chkall_title' => $main::locale->text('Import all'),
 
 108   $self->render('file/import_dialog',
 
 115 sub action_ajax_import {
 
 117   $::auth->assert($self->object_right);
 
 118   my $ids    = $::form->{ids};
 
 119   my $source = $::form->{source};
 
 120   my $path   = $::form->{path};
 
 121   my @files  = $self->_get_from_import($path);
 
 122   foreach my $filename (@{ $::form->{$ids} || [] }) {
 
 123     my ($file, undef) = grep { $_->{name} eq $filename } @files;
 
 125       my $obj = SL::File->save(object_id   => $self->object_id,
 
 126                                object_type => $self->object_type,
 
 127                                mime_type   => 'application/pdf',
 
 129                                file_type   => 'document',
 
 130                                file_name   => $file->{filename},
 
 131                                file_path   => $file->{path}
 
 133       unlink($file->{path}) if $obj;
 
 139 sub action_ajax_delete {
 
 141   $self->_delete_all(DO_DELETE, $::locale->text('Following files are deleted:'));
 
 144 sub action_ajax_unimport {
 
 146   $self->_delete_all(DO_UNIMPORT, $::locale->text('Following files are unimported:'));
 
 149 sub action_ajax_rename {
 
 151   my ($id, $version) = split /_/, $::form->{id};
 
 152   my $file = SL::File->get(id => $id);
 
 154     $self->js->flash('error', $::locale->text('File not exists !'))->render();
 
 157   my $sessionfile = $::form->{sessionfile};
 
 158   if ( $sessionfile && -f $sessionfile ) {
 
 160     if ( $::form->{to} eq $file->file_name ) {
 
 161       # no rename so use as new version
 
 162       $file->save_file($sessionfile);
 
 163       $self->js->flash('warning', $::locale->text('File \'#1\' is used as new Version !', $file->file_name));
 
 166       # new filename, so it is a new file with the same attributes as the old file
 
 168         SL::File->save(object_id   => $file->object_id,
 
 169                        object_type => $file->object_type,
 
 170                        mime_type   => $file->mime_type,
 
 171                        source      => $file->source,
 
 172                        file_type   => $file->file_type,
 
 173                        file_name   => $::form->{to},
 
 174                        file_path   => $sessionfile
 
 176         unlink($sessionfile);
 
 179         $self->js->flash(       'error', t8('internal error (see details)'))
 
 180                  ->flash_detail('error', $@)->render;
 
 190       $result = $file->rename($::form->{to});
 
 193       $self->js->flash(       'error', t8('internal error (see details)'))
 
 194                ->flash_detail('error', $@)->render;
 
 198     if ($result != SL::File::RENAME_OK) {
 
 199       $self->js->flash('error',
 
 200                          $result == SL::File::RENAME_EXISTS ? $::locale->text('File still exists !')
 
 201                        : $result == SL::File::RENAME_SAME   ? $::locale->text('Same Filename !')
 
 202                        :                                      $::locale->text('File not exists !'))
 
 207   $self->is_global($::form->{is_global});
 
 208   $self->file_type(  $file->file_type);
 
 209   $self->object_type($file->object_type);
 
 210   $self->object_id(  $file->object_id);
 
 211   #$self->object_model($file_types{$file->module}->{model});
 
 212   #$self->object_right($file_types{$file->module}->{right});
 
 213   if ( $::form->{next_ids} ) {
 
 214     my @existing = split(/,/, $::form->{next_ids});
 
 215     $self->existing(\@existing);
 
 220 sub action_ajax_upload {
 
 222   $self->{maxsize} = $::instance_conf->get_doc_max_filesize;
 
 223   $self->{accept_types} = '';
 
 224   $self->{accept_types} = 'image/png,image/gif,image/jpeg,image/tiff,*png,*gif,*.jpg,*.tif' if $self->{file_type} eq 'image';
 
 225   $self->render('file/upload_dialog',
 
 231 sub action_ajax_files_uploaded {
 
 234   my $source = 'uploaded';
 
 236   if ( $::form->{ATTACHMENTS}->{uploadfiles} ) {
 
 237     my @upfiles = @{ $::form->{ATTACHMENTS}->{uploadfiles} };
 
 238     foreach my $idx (0 .. scalar(@upfiles) - 1) {
 
 240         my $fname = uri_unescape($upfiles[$idx]->{filename});
 
 241         # normalize and find basename
 
 242         # first split with unix rules
 
 243         # after that split with windows rules
 
 244         my ($volume, $directories, $basefile) = File::Spec::Unix->splitpath($fname);
 
 245         ($volume, $directories, $basefile) = File::Spec::Win32->splitpath($basefile);
 
 247         # to find real mime_type by magic we must save the filedata
 
 249         my $sess_fname = "file_upload_" . $self->object_type . "_" . $self->object_id . "_" . $idx;
 
 250         my $sfile      = SL::SessionFile->new($sess_fname, mode => 'w');
 
 252         $sfile->fh->print(${$upfiles[$idx]->{data}});
 
 254         my $mime_type = File::MimeInfo::Magic::magic($sfile->file_name);
 
 257           # if filename has the suffix "pdf", but isn't really a pdf, set mimetype for no suffix
 
 258           $mime_type = File::MimeInfo::Magic::mimetype($basefile);
 
 259           $mime_type = 'application/octet-stream' if $mime_type eq 'application/pdf' || !$mime_type;
 
 261         if ( $self->file_type eq 'image' && $self->file_probe_image_type($mime_type, $basefile)) {
 
 264         my ($existobj) = SL::File->get_all(object_id   => $self->object_id,
 
 265                                            object_type => $self->object_type,
 
 266                                            mime_type   => $mime_type,
 
 268                                            file_type   => $self->file_type,
 
 269                                            file_name   => $basefile,
 
 273           push @existing, $existobj->id.'_'.$sfile->file_name;
 
 275           my $fileobj = SL::File->save(object_id        => $self->object_id,
 
 276                                        object_type      => $self->object_type,
 
 277                                        mime_type        => $mime_type,
 
 279                                        file_type        => $self->file_type,
 
 280                                        file_name        => $basefile,
 
 281                                        title            => $::form->{title},
 
 282                                        description      => $::form->{description},
 
 283                                        ## two possibilities: what is better ? content or sessionfile ??
 
 284                                        file_contents    => ${$upfiles[$idx]->{data}},
 
 285                                        file_path        => $sfile->file_name
 
 287           unlink($sfile->file_name);
 
 291         $self->js->flash(       'error', t8('internal error (see details)'))
 
 292                  ->flash_detail('error', $@)->render;
 
 297   $self->existing(\@existing);
 
 301 sub action_download {
 
 304   my $id      = $::form->{id};
 
 305   my $version = $::form->{version};
 
 307   my $file = SL::File->get(id => $id );
 
 308   $file->version($version) if $version;
 
 309   my $ref  = $file->get_content;
 
 310   if ( $file && $ref ) {
 
 311     return $self->send_file($ref,
 
 312       type => $file->mime_type,
 
 313       name => $file->file_name,
 
 318 sub action_ajax_get_thumbnail {
 
 321   my $id      = $::form->{file_id};
 
 322   my $version = $::form->{file_version};
 
 323   my $file    = SL::File->get(id => $id);
 
 325   $file->version($version) if $version;
 
 327   my $thumbnail = _create_thumbnail($file, $::form->{size});
 
 329   my $overlay_selector  = '#enlarged_thumb_' . $id;
 
 330   $overlay_selector    .= '_' . $version            if $version;
 
 332     ->attr($overlay_selector, 'src', 'data:' . $thumbnail->{thumbnail_img_content_type} . ';base64,' . MIME::Base64::encode_base64($thumbnail->{thumbnail_img_content}))
 
 333     ->data($overlay_selector, 'is-overlay-loaded', '1')
 
 342 sub check_object_params {
 
 345   my $id      = ($::form->{object_id} // 0) * 1;
 
 346   my $draftid = ($::form->{draft_id}  // 0) * 1;
 
 350   if ( $draftid == 0 && $id == 0 && $::form->{is_global} ) {
 
 352     $type  = $::form->{object_type};
 
 355     $id   = $::form->{draft_id};
 
 357   } elsif ( $::form->{object_type} ) {
 
 358     $type = $::form->{object_type};
 
 360   die "No object type"      unless $type;
 
 361   die "No file type"        unless $::form->{file_type};
 
 362   die "Unknown object type" unless $file_types{$type};
 
 364   $self->is_global($gldoc);
 
 365   $self->file_type($::form->{file_type});
 
 366   $self->object_type($type);
 
 367   $self->object_id($id);
 
 368   $self->object_model($file_types{$type}->{model});
 
 369   $self->object_right($file_types{$type}->{right});
 
 371  # $::auth->assert($self->object_right);
 
 373  # my $model = 'SL::DB::' . $self->object_model;
 
 374  # $self->object($model->new(id => $self->object_id)->load || die "Record not found");
 
 384   my ($self, $do_unimport, $infotext) = @_;
 
 386   my $ids = $::form->{ids};
 
 387   foreach my $id_version (@{ $::form->{$ids} || [] }) {
 
 388     my ($id, $version) = split /_/, $id_version;
 
 389     my $dbfile = SL::File->get(id => $id);
 
 392         $dbfile->version($version);
 
 393         $files .= ' ' . $dbfile->file_name if $dbfile->delete_version;
 
 395         $files .= ' ' . $dbfile->file_name if $dbfile->delete;
 
 399   $self->js->flash('info', $infotext . $files) if $files;
 
 404   my ($self, $json) = @_;
 
 406   if ( $self->file_type eq 'document' ) {
 
 408     push @object_types, $self->object_type;
 
 409     push @object_types, qw(dunning1 dunning2 dunning3 dunning_invoice dunning_orig_invoice) if $self->object_type eq 'invoice'; # hardcoded object types?
 
 410     @files = SL::File->get_all_versions(object_id   => $self->object_id,
 
 411                                         object_type => \@object_types,
 
 412                                         file_type   => $self->file_type,
 
 416   elsif ( $self->file_type eq 'attachment' || $self->file_type eq 'image' ) {
 
 417     @files   = SL::File->get_all(object_id   => $self->object_id,
 
 418                                  object_type => $self->object_type,
 
 419                                  file_type   => $self->file_type,
 
 422   $self->files(\@files);
 
 424   $_->{thumbnail} = _create_thumbnail($_) for @files;
 
 426   if($self->object_type eq 'shop_image'){
 
 428       ->run('kivi.ShopPart.show_images', $self->object_id)
 
 431     $self->_mk_render('file/list', 1, 0, $json);
 
 435 sub _get_from_import {
 
 436   my ($self, $path) = @_;
 
 439   my $language = $::lx_office_conf{system}->{language};
 
 440   my $timezone = $::locale->get_local_time_zone()->name;
 
 441   if (opendir my $dir, $path) {
 
 442     my @files = (readdir $dir);
 
 443     foreach my $file ( @files) {
 
 444       next if (($file eq '.') || ($file eq '..'));
 
 445       $file = Encode::decode('utf-8', $file);
 
 447       next if ( -d "$path/$file" );
 
 449       my $tmppath = File::Spec->catfile( $path, $file );
 
 450       next if( ! -f $tmppath );
 
 452       my $st = stat($tmppath);
 
 453       my $dt = DateTime->from_epoch( epoch => $st->mtime, time_zone => $timezone, locale => $language );
 
 454       my $sname = $main::locale->quote_special_chars('HTML', $file);
 
 457         'filename' => $sname,
 
 459         'mtime'    => $st->mtime,
 
 460         'date'     => $dt->dmy('.') . " " . $dt->hms,
 
 467     $::lxdebug->message(LXDebug::WARN(), "SL::File::_get_from_import opendir failed to open dir " . $path);
 
 474   my ($self, $template, $edit, $scanner, $json) = @_;
 
 477     ##TODO make code configurable
 
 480     my @sources = $self->_get_sources();
 
 481     foreach my $source ( @sources ) {
 
 482       @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files };
 
 484     if ( $self->file_type eq 'document' ) {
 
 485       $title = $main::locale->text('Documents');
 
 486     } elsif ( $self->file_type eq 'attachment' ) {
 
 487       $title = $main::locale->text('Attachments');
 
 488     } elsif ( $self->file_type eq 'image' ) {
 
 489       $title = $main::locale->text('Images');
 
 492     my $output         = SL::Presenter->get->render(
 
 495       SOURCES          => \@sources,
 
 496       edit_attachments => $edit,
 
 497       object_type      => $self->object_type,
 
 498       object_id        => $self->object_id,
 
 499       file_type        => $self->file_type,
 
 500       is_global        => $self->is_global,
 
 504       $self->js->html('#'.$self->file_type.'_list_'.$self->object_type, $output);
 
 505       if ( $self->existing && scalar(@{$self->existing}) > 0) {
 
 506         my $first = shift @{$self->existing};
 
 507         my ($first_id, $sfile) = split('_', $first, 2);
 
 508         my $file = SL::File->get(id => $first_id );
 
 509         $self->js->run('kivi.File.askForRename', $first_id, $file->file_type, $file->file_name, $sfile, join (',', @{$self->existing}), $self->is_global);
 
 513         $self->render(\$output, { layout => 0, process => 0 });
 
 518       $self->js->flash(       'error', t8('internal error (see details)'))
 
 519                ->flash_detail('error', $@)->render;
 
 521       $self->render('generic/error', { layout => 0 }, label_error => $@);
 
 530   if ( $self->file_type eq 'document' ) {
 
 531     # TODO statt gen neue attribute in filetypes :
 
 532     if (($file_types{$self->object_type}->{gen}*1 & 4)==4) {
 
 533       # bit 3 is set => means upload
 
 535         'name'         => 'uploaded',
 
 536         'title'        => $main::locale->text('uploaded Documents'),
 
 537         'chk_action'   => 'uploaded_documents_delete',
 
 538         'chk_title'    => $main::locale->text('Delete Documents'),
 
 539         'chkall_title' => $main::locale->text('Delete all'),
 
 540         'file_title'   => $main::locale->text('filename'),
 
 541         'confirm_text' => $main::locale->text('delete'),
 
 543         'are_existing' => $self->existing ? 1 : 0,
 
 544         'rename_title' => $main::locale->text('Rename Attachments'),
 
 547         'upload_title' => $main::locale->text('Upload Documents'),
 
 548         'done_text'    => $main::locale->text('deleted')
 
 550       push @sources , $source;
 
 553     if (($file_types{$self->object_type}->{gen}*1 & 1)==1) {
 
 556         'title'        => $main::locale->text('generated Files'),
 
 557         'chk_action'   => 'documents_delete',
 
 558         'chk_title'    => $main::locale->text('Delete Documents'),
 
 559         'chkall_title' => $main::locale->text('Delete all'),
 
 560         'file_title'   => $main::locale->text('filename'),
 
 561         'confirm_text' => $main::locale->text('delete'),
 
 562         'can_delete'   => $::instance_conf->get_doc_delete_printfiles,
 
 563         'can_rename'   => $::instance_conf->get_doc_delete_printfiles,
 
 564         'rename_title' => $main::locale->text('Rename Documents'),
 
 565         'done_text'    => $main::locale->text('deleted')
 
 567       push @sources , $gendata;
 
 570     if (($file_types{$self->object_type}->{gen}*1 & 2)==2) {
 
 571       my @others =  SL::File->get_other_sources();
 
 572       foreach my $scanner_or_mailrx (@others) {
 
 574           'name'         => $scanner_or_mailrx->{name},
 
 575           'title'        => $main::locale->text('from \'#1\' imported Files', $scanner_or_mailrx->{description}),
 
 576           'chk_action'   => $scanner_or_mailrx->{name}.'_unimport',
 
 577           'chk_title'    => $main::locale->text('Unimport documents'),
 
 578           'chkall_title' => $main::locale->text('Unimport all'),
 
 579           'file_title'   => $main::locale->text('filename'),
 
 580           'confirm_text' => $main::locale->text('unimport'),
 
 582           'rename_title' => $main::locale->text('Rename Documents'),
 
 585           'import_title' => $main::locale->text('Add Document from \'#1\'', $scanner_or_mailrx->{name}),
 
 586           'path'         => $scanner_or_mailrx->{directory},
 
 587           'done_text'    => $main::locale->text('unimported')
 
 589         push @sources , $other;
 
 593   elsif ( $self->file_type eq 'attachment' ) {
 
 595       'name'         => 'uploaded',
 
 596       'title'        => $main::locale->text(''),
 
 597       'chk_action'   => 'attachments_delete',
 
 598       'chk_title'    => $main::locale->text('Delete Attachments'),
 
 599       'chkall_title' => $main::locale->text('Delete all'),
 
 600       'file_title'   => $main::locale->text('filename'),
 
 601       'confirm_text' => $main::locale->text('delete'),
 
 603       'are_existing' => $self->existing ? 1 : 0,
 
 604       'rename_title' => $main::locale->text('Rename Attachments'),
 
 607       'upload_title' => $main::locale->text('Upload Attachments'),
 
 608       'done_text'    => $main::locale->text('deleted')
 
 610     push @sources , $attdata;
 
 612   elsif ( $self->file_type eq 'image' ) {
 
 614       'name'         => 'uploaded',
 
 615       'title'        => $main::locale->text(''),
 
 616       'chk_action'   => 'images_delete',
 
 617       'chk_title'    => $main::locale->text('Delete Images'),
 
 618       'chkall_title' => $main::locale->text('Delete all'),
 
 619       'file_title'   => $main::locale->text('filename'),
 
 620       'confirm_text' => $main::locale->text('delete'),
 
 622       'are_existing' => $self->existing ? 1 : 0,
 
 623       'rename_title' => $main::locale->text('Rename Images'),
 
 626       'upload_title' => $main::locale->text('Upload Images'),
 
 627       'done_text'    => $main::locale->text('deleted')
 
 629     push @sources , $attdata;
 
 635 # todo: cache thumbs?
 
 636 sub _create_thumbnail {
 
 637   my ($file, $size) = @_;
 
 642   if (!eval { $filename = $file->get_file(); 1; }) {
 
 643     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail get_file failed: " . $EVAL_ERROR);
 
 647   # Workaround for pfds which are not handled by file_probe_type.
 
 648   # Maybe use mime info stored in db?
 
 649   my $mime_type = File::MimeInfo::Magic::magic($filename);
 
 650   if ($mime_type =~ m{pdf}) {
 
 651     $filename = _convert_pdf_to_png($filename, size => $size);
 
 653   return if !$filename;
 
 656   if (!eval { $content = slurp $filename; 1; }) {
 
 657     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail slurp failed: " . $EVAL_ERROR);
 
 662   if (!eval { $ret = file_probe_type($content, size => $size); 1; }) {
 
 663     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail file_probe_type failed: " . $EVAL_ERROR);
 
 667   # file_probe_type returns a hash ref with thumbnail info and content
 
 668   # or an error message
 
 669   if ('HASH' ne ref $ret) {
 
 670     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail file_probe_type returned an error: " . $ret);
 
 677 sub _convert_pdf_to_png {
 
 678   my ($filename, %params) = @_;
 
 680   my $size    = $params{size} // 64;
 
 681   my $sfile   = SL::SessionFile::Random->new();
 
 682   my $command = 'pdftoppm -singlefile -scale-to ' . $size . ' -png' . ' ' . $filename . ' ' . $sfile->file_name;
 
 684   if (system($command) == -1) {
 
 685     $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: system call failed: " . $ERRNO);
 
 689     $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: pdftoppm failed with error code: " . ($CHILD_ERROR >> 8));
 
 693   return $sfile->file_name . '.png';
 
 706 SL::Controller::File - Controller for managing files
 
 710 The Controller is called directly from the webpages
 
 712     <a href="controller.pl?action=File/list&file_type=document\
 
 713        &object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">
 
 716 or indirectly via javascript functions from js/kivi.File.js
 
 718     kivi.popup_dialog({ url:     'controller.pl',
 
 719                         data:    { action     : 'File/ajax_upload',
 
 720                                    file_type  : 'uploaded',
 
 728 This is a controller for handling files in a storage independent way.
 
 729 The storage may be a Filesystem,a WebDAV, a Database or DMS.
 
 730 These backends must be configered in ClientConfig.
 
 731 This Controller use as intermediate layer for storage C<SL::File>.
 
 733 The Controller is responsible to display forms for displaying the files at the ERP-objects and
 
 734 for uploading and downloading the files.
 
 736 More description of the intermediate layer see L<SL::File>.
 
 740 =head2 C<action_list>
 
 742 This loads a list of files on a webpage. This can be done with a normal submit or via an ajax/json call.
 
 743 Dependent of file_type different sources are available.
 
 745 For documents there are the 'created' source and the imports from scanners or email.
 
 746 For attachments and images only the 'uploaded' source available.
 
 748 Available C<FORM PARAMS>:
 
 752 =item C<form.object_id>
 
 754 The Id of the ERP-object.
 
 756 =item C<form.object_type>
 
 758 The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
 
 760 =item C<form.file_type>
 
 762 For one ERP-object may exists different type of documents the type may be "documents","attachments" or "images".
 
 763 This file_type is a filter for the list.
 
 767 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.
 
 772 =head2 C<action_ajax_upload>
 
 775 A new file or more files can selected by a dialog and insert into the system.
 
 778 Available C<FORM PARAMS>:
 
 782 =item C<form.file_type>
 
 784 This parameter describe here the source for a new file :
 
 785 "attachments" and "images"
 
 787 This is a normal upload selection, which may be more then one file to upload.
 
 789 =item C<form.object_id>
 
 793 =item C<form.object_type>
 
 795 are the same as at C<action_list>
 
 799 =head2  C<action_ajax_files_uploaded>
 
 801 The Upload of selected Files. The "multipart_formdata" is parsed in SL::Request into the formsvariable "form.ATTACHMENTS".
 
 802 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?).
 
 803 If the same filename still exists at this object after the download for each existing filename a rename dialog will be opened.
 
 805 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.
 
 807 Available C<FORM PARAMS>:
 
 811 =item C<form.ATTACHMENTS.uploadfiles>
 
 813 This is an array of elements which have {filename} for the name and {data} for the contents.
 
 815 Also object_id, object_type and file_type
 
 819 =head2 C<action_download>
 
 821 This is the real download of a file normally called via javascript "$.download("controller.pl", data);"
 
 823 Available C<FORM PARAMS>:
 
 827 Also object_id, object_type and file_type
 
 831 =head2 C<action_ajax_importdialog>
 
 833 A Dialog with all available and not imported files to import is open.
 
 834 More then one file can be selected.
 
 836 Available C<FORM PARAMS>:
 
 842 The name of the source like "scanner1" or "email"
 
 846 The full path to the directory on the server, where the files to import can found
 
 848 Also object_id, object_type and file_type
 
 852 =head2 C<action_ajax_delete>
 
 854 Some files can be deleted
 
 856 Available C<FORM PARAMS>:
 
 862 The ids of the files to delete. Only this files are deleted not all versions of a file if the exists
 
 866 =head2 C<action_ajax_unimport>
 
 868 Some files can be unimported, dependent of the source of the file. This means they are moved
 
 869 back to the directory of the source
 
 871 Available C<FORM PARAMS>:
 
 877 The ids of the files to unimport. Only these files are unimported not all versions of a file if the exists
 
 881 =head2 C<action_ajax_rename>
 
 883 One file can be renamed. There can be some checks if the same filename still exists at one object.
 
 887 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>