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   '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' },
 
  82 # $main::locale->text('imported')
 
  92   $is_json = 1 if $::form->{json};
 
  94   $self->_do_list($is_json);
 
  97 sub action_ajax_importdialog {
 
  99   $::auth->assert($self->object_right);
 
 100   my $path   = $::form->{path};
 
 101   my @files  = $self->_get_from_import($path);
 
 103     'name'         => $::form->{source},
 
 105     'chk_action'   => $::form->{source}.'_import',
 
 106     'chk_title'    => $main::locale->text('Import scanned documents'),
 
 107     'chkall_title' => $main::locale->text('Import all'),
 
 110   $self->render('file/import_dialog',
 
 117 sub action_ajax_import {
 
 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;
 
 127       my $obj = SL::File->save(object_id   => $self->object_id,
 
 128                                object_type => $self->object_type,
 
 129                                mime_type   => 'application/pdf',
 
 131                                file_type   => 'document',
 
 132                                file_name   => $file->{filename},
 
 133                                file_path   => $file->{path}
 
 135       unlink($file->{path}) if $obj;
 
 141 sub action_ajax_delete {
 
 143   $self->_delete_all(DO_DELETE, $::locale->text('Following files are deleted:'));
 
 146 sub action_ajax_unimport {
 
 148   $self->_delete_all(DO_UNIMPORT, $::locale->text('Following files are unimported:'));
 
 151 sub action_ajax_rename {
 
 153   my ($id, $version) = split /_/, $::form->{id};
 
 154   my $file = SL::File->get(id => $id);
 
 156     $self->js->flash('error', $::locale->text('File not exists !'))->render();
 
 159   my $sessionfile = $::form->{sessionfile};
 
 160   if ( $sessionfile && -f $sessionfile ) {
 
 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));
 
 168       # new filename, so it is a new file with the same attributes as the old file
 
 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
 
 178         unlink($sessionfile);
 
 181         $self->js->flash(       'error', t8('internal error (see details)'))
 
 182                  ->flash_detail('error', $@)->render;
 
 192       $result = $file->rename($::form->{to});
 
 195       $self->js->flash(       'error', t8('internal error (see details)'))
 
 196                ->flash_detail('error', $@)->render;
 
 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 !'))
 
 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);
 
 222 sub action_ajax_upload {
 
 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',
 
 233 sub action_ajax_files_uploaded {
 
 236   my $source = 'uploaded';
 
 238   if ( $::form->{ATTACHMENTS}->{uploadfiles} ) {
 
 239     my @upfiles = @{ $::form->{ATTACHMENTS}->{uploadfiles} };
 
 240     foreach my $idx (0 .. scalar(@upfiles) - 1) {
 
 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);
 
 249         # to find real mime_type by magic we must save the filedata
 
 251         my $sess_fname = "file_upload_" . $self->object_type . "_" . $self->object_id . "_" . $idx;
 
 252         my $sfile      = SL::SessionFile->new($sess_fname, mode => 'w');
 
 254         $sfile->fh->print(${$upfiles[$idx]->{data}});
 
 256         my $mime_type = File::MimeInfo::Magic::magic($sfile->file_name);
 
 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;
 
 263         if ( $self->file_type eq 'image' && $self->file_probe_image_type($mime_type, $basefile)) {
 
 266         my ($existobj) = SL::File->get_all(object_id   => $self->object_id,
 
 267                                            object_type => $self->object_type,
 
 268                                            mime_type   => $mime_type,
 
 270                                            file_type   => $self->file_type,
 
 271                                            file_name   => $basefile,
 
 275           push @existing, $existobj->id.'_'.$sfile->file_name;
 
 277           my $fileobj = SL::File->save(object_id        => $self->object_id,
 
 278                                        object_type      => $self->object_type,
 
 279                                        mime_type        => $mime_type,
 
 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
 
 289           unlink($sfile->file_name);
 
 293         $self->js->flash(       'error', t8('internal error (see details)'))
 
 294                  ->flash_detail('error', $@)->render;
 
 299   $self->existing(\@existing);
 
 303 sub action_download {
 
 306   my $id      = $::form->{id};
 
 307   my $version = $::form->{version};
 
 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,
 
 320 sub action_ajax_get_thumbnail {
 
 323   my $id      = $::form->{file_id};
 
 324   my $version = $::form->{file_version};
 
 325   my $file    = SL::File->get(id => $id);
 
 327   $file->version($version) if $version;
 
 329   my $thumbnail = _create_thumbnail($file, $::form->{size});
 
 331   my $overlay_selector  = '#enlarged_thumb_' . $id;
 
 332   $overlay_selector    .= '_' . $version            if $version;
 
 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')
 
 344 sub check_object_params {
 
 347   my $id      = ($::form->{object_id} // 0) * 1;
 
 348   my $draftid = ($::form->{draft_id}  // 0) * 1;
 
 352   if ( $draftid == 0 && $id == 0 && $::form->{is_global} ) {
 
 354     $type  = $::form->{object_type};
 
 357     $id   = $::form->{draft_id};
 
 359   } elsif ( $::form->{object_type} ) {
 
 360     $type = $::form->{object_type};
 
 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};
 
 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});
 
 373  # $::auth->assert($self->object_right);
 
 375  # my $model = 'SL::DB::' . $self->object_model;
 
 376  # $self->object($model->new(id => $self->object_id)->load || die "Record not found");
 
 386   my ($self, $do_unimport, $infotext) = @_;
 
 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);
 
 394         $dbfile->version($version);
 
 395         $files .= ' ' . $dbfile->file_name if $dbfile->delete_version;
 
 397         $files .= ' ' . $dbfile->file_name if $dbfile->delete;
 
 401   $self->js->flash('info', $infotext . $files) if $files;
 
 406   my ($self, $json) = @_;
 
 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?
 
 413   @files = SL::File->get_all_versions(object_id   => $self->object_id,
 
 414                                       object_type => \@object_types,
 
 415                                       file_type   => $self->file_type,
 
 418   $self->files(\@files);
 
 420   $_->{thumbnail}     = _create_thumbnail($_)                     for @files;
 
 421   $_->{version_count} = SL::File->get_version_count(id => $_->id) for @files;
 
 423   if($self->object_type eq 'shop_image'){
 
 425       ->run('kivi.ShopPart.show_images', $self->object_id)
 
 428     $self->_mk_render('file/list', 1, 0, $json);
 
 432 sub _get_from_import {
 
 433   my ($self, $path) = @_;
 
 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);
 
 444       next if ( -d "$path/$file" );
 
 446       my $tmppath = File::Spec->catfile( $path, $file );
 
 447       next if( ! -f $tmppath );
 
 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);
 
 454         'filename' => $sname,
 
 456         'mtime'    => $st->mtime,
 
 457         'date'     => $dt->dmy('.') . " " . $dt->hms,
 
 464     $::lxdebug->message(LXDebug::WARN(), "SL::File::_get_from_import opendir failed to open dir " . $path);
 
 471   my ($self, $template, $edit, $scanner, $json) = @_;
 
 474     ##TODO make code configurable
 
 477     my @sources = $self->_get_sources();
 
 478     foreach my $source ( @sources ) {
 
 479       @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files };
 
 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');
 
 489     my $output         = SL::Presenter->get->render(
 
 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,
 
 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);
 
 510         $self->render(\$output, { layout => 0, process => 0 });
 
 515       $self->js->flash(       'error', t8('internal error (see details)'))
 
 516                ->flash_detail('error', $@)->render;
 
 518       $self->render('generic/error', { layout => 0 }, label_error => $@);
 
 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
 
 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'),
 
 540         'are_existing' => $self->existing ? 1 : 0,
 
 541         'rename_title' => $main::locale->text('Rename Attachments'),
 
 544         'upload_title' => $main::locale->text('Upload Documents'),
 
 545         'done_text'    => $main::locale->text('deleted')
 
 547       push @sources , $source;
 
 550     if (($file_types{$self->object_type}->{gen}*1 & 1)==1) {
 
 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')
 
 564       push @sources , $gendata;
 
 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) {
 
 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'),
 
 579           'rename_title' => $main::locale->text('Rename Documents'),
 
 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')
 
 586         push @sources , $other;
 
 590   elsif ( $self->file_type eq 'attachment' ) {
 
 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'),
 
 600       'are_existing' => $self->existing ? 1 : 0,
 
 601       'rename_title' => $main::locale->text('Rename Attachments'),
 
 604       'upload_title' => $main::locale->text('Upload Attachments'),
 
 605       'done_text'    => $main::locale->text('deleted')
 
 607     push @sources , $attdata;
 
 609   elsif ( $self->file_type eq 'image' ) {
 
 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'),
 
 619       'are_existing' => $self->existing ? 1 : 0,
 
 620       'rename_title' => $main::locale->text('Rename Images'),
 
 623       'upload_title' => $main::locale->text('Upload Images'),
 
 624       'done_text'    => $main::locale->text('deleted')
 
 626     push @sources , $attdata;
 
 632 # todo: cache thumbs?
 
 633 sub _create_thumbnail {
 
 634   my ($file, $size) = @_;
 
 639   if (!eval { $filename = $file->get_file(); 1; }) {
 
 640     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail get_file failed: " . $EVAL_ERROR);
 
 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);
 
 650   return if !$filename;
 
 653   if (!eval { $content = slurp $filename; 1; }) {
 
 654     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail slurp failed: " . $EVAL_ERROR);
 
 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);
 
 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);
 
 674 sub _convert_pdf_to_png {
 
 675   my ($filename, %params) = @_;
 
 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");
 
 683   # quotemeta for storno case "storno\ zu\ 1020" *nix only
 
 684   my $command = 'pdftoppm -singlefile -scale-to ' . $size . ' -png' . ' ' . quotemeta($filename) . ' ' . $sfile->file_name;
 
 686   if (system($command) == -1) {
 
 687     $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: system call failed: " . $ERRNO);
 
 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");
 
 696   return $sfile->file_name . '.png';
 
 709 SL::Controller::File - Controller for managing files
 
 713 The Controller is called directly from the webpages
 
 715     <a href="controller.pl?action=File/list&file_type=document\
 
 716        &object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">
 
 719 or indirectly via javascript functions from js/kivi.File.js
 
 721     kivi.popup_dialog({ url:     'controller.pl',
 
 722                         data:    { action     : 'File/ajax_upload',
 
 723                                    file_type  : 'uploaded',
 
 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>.
 
 736 The Controller is responsible to display forms for displaying the files at the ERP-objects and
 
 737 for uploading and downloading the files.
 
 739 More description of the intermediate layer see L<SL::File>.
 
 743 =head2 C<action_list>
 
 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.
 
 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.
 
 751 Available C<FORM PARAMS>:
 
 755 =item C<form.object_id>
 
 757 The Id of the ERP-object.
 
 759 =item C<form.object_type>
 
 761 The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
 
 763 =item C<form.file_type>
 
 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.
 
 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.
 
 775 =head2 C<action_ajax_upload>
 
 778 A new file or more files can selected by a dialog and insert into the system.
 
 781 Available C<FORM PARAMS>:
 
 785 =item C<form.file_type>
 
 787 This parameter describe here the source for a new file :
 
 788 "attachments" and "images"
 
 790 This is a normal upload selection, which may be more then one file to upload.
 
 792 =item C<form.object_id>
 
 796 =item C<form.object_type>
 
 798 are the same as at C<action_list>
 
 802 =head2  C<action_ajax_files_uploaded>
 
 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.
 
 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.
 
 810 Available C<FORM PARAMS>:
 
 814 =item C<form.ATTACHMENTS.uploadfiles>
 
 816 This is an array of elements which have {filename} for the name and {data} for the contents.
 
 818 Also object_id, object_type and file_type
 
 822 =head2 C<action_download>
 
 824 This is the real download of a file normally called via javascript "$.download("controller.pl", data);"
 
 826 Available C<FORM PARAMS>:
 
 830 Also object_id, object_type and file_type
 
 834 =head2 C<action_ajax_importdialog>
 
 836 A Dialog with all available and not imported files to import is open.
 
 837 More then one file can be selected.
 
 839 Available C<FORM PARAMS>:
 
 845 The name of the source like "scanner1" or "email"
 
 849 The full path to the directory on the server, where the files to import can found
 
 851 Also object_id, object_type and file_type
 
 855 =head2 C<action_ajax_delete>
 
 857 Some files can be deleted
 
 859 Available C<FORM PARAMS>:
 
 865 The ids of the files to delete. Only this files are deleted not all versions of a file if the exists
 
 869 =head2 C<action_ajax_unimport>
 
 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
 
 874 Available C<FORM PARAMS>:
 
 880 The ids of the files to unimport. Only these files are unimported not all versions of a file if the exists
 
 884 =head2 C<action_ajax_rename>
 
 886 One file can be renamed. There can be some checks if the same filename still exists at one object.
 
 890 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>