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) = @_;
 
 408   if ( $self->file_type eq 'document' ) {
 
 410     push @object_types, $self->object_type;
 
 411     push @object_types, qw(dunning1 dunning2 dunning3 dunning_invoice dunning_orig_invoice) if $self->object_type eq 'invoice'; # hardcoded object types?
 
 412     @files = SL::File->get_all_versions(object_id   => $self->object_id,
 
 413                                         object_type => \@object_types,
 
 414                                         file_type   => $self->file_type,
 
 418   elsif ( $self->file_type eq 'attachment' || $self->file_type eq 'image' ) {
 
 419     @files   = SL::File->get_all(object_id   => $self->object_id,
 
 420                                  object_type => $self->object_type,
 
 421                                  file_type   => $self->file_type,
 
 424   $self->files(\@files);
 
 426   $_->{thumbnail} = _create_thumbnail($_) for @files;
 
 428   if($self->object_type eq 'shop_image'){
 
 430       ->run('kivi.ShopPart.show_images', $self->object_id)
 
 433     $self->_mk_render('file/list', 1, 0, $json);
 
 437 sub _get_from_import {
 
 438   my ($self, $path) = @_;
 
 441   my $language = $::lx_office_conf{system}->{language};
 
 442   my $timezone = $::locale->get_local_time_zone()->name;
 
 443   if (opendir my $dir, $path) {
 
 444     my @files = (readdir $dir);
 
 445     foreach my $file ( @files) {
 
 446       next if (($file eq '.') || ($file eq '..'));
 
 447       $file = Encode::decode('utf-8', $file);
 
 449       next if ( -d "$path/$file" );
 
 451       my $tmppath = File::Spec->catfile( $path, $file );
 
 452       next if( ! -f $tmppath );
 
 454       my $st = stat($tmppath);
 
 455       my $dt = DateTime->from_epoch( epoch => $st->mtime, time_zone => $timezone, locale => $language );
 
 456       my $sname = $main::locale->quote_special_chars('HTML', $file);
 
 459         'filename' => $sname,
 
 461         'mtime'    => $st->mtime,
 
 462         'date'     => $dt->dmy('.') . " " . $dt->hms,
 
 469     $::lxdebug->message(LXDebug::WARN(), "SL::File::_get_from_import opendir failed to open dir " . $path);
 
 476   my ($self, $template, $edit, $scanner, $json) = @_;
 
 479     ##TODO make code configurable
 
 482     my @sources = $self->_get_sources();
 
 483     foreach my $source ( @sources ) {
 
 484       @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files };
 
 486     if ( $self->file_type eq 'document' ) {
 
 487       $title = $main::locale->text('Documents');
 
 488     } elsif ( $self->file_type eq 'attachment' ) {
 
 489       $title = $main::locale->text('Attachments');
 
 490     } elsif ( $self->file_type eq 'image' ) {
 
 491       $title = $main::locale->text('Images');
 
 494     my $output         = SL::Presenter->get->render(
 
 497       SOURCES          => \@sources,
 
 498       edit_attachments => $edit,
 
 499       object_type      => $self->object_type,
 
 500       object_id        => $self->object_id,
 
 501       file_type        => $self->file_type,
 
 502       is_global        => $self->is_global,
 
 506       $self->js->html('#'.$self->file_type.'_list_'.$self->object_type, $output);
 
 507       if ( $self->existing && scalar(@{$self->existing}) > 0) {
 
 508         my $first = shift @{$self->existing};
 
 509         my ($first_id, $sfile) = split('_', $first, 2);
 
 510         my $file = SL::File->get(id => $first_id );
 
 511         $self->js->run('kivi.File.askForRename', $first_id, $file->file_type, $file->file_name, $sfile, join (',', @{$self->existing}), $self->is_global);
 
 515         $self->render(\$output, { layout => 0, process => 0 });
 
 520       $self->js->flash(       'error', t8('internal error (see details)'))
 
 521                ->flash_detail('error', $@)->render;
 
 523       $self->render('generic/error', { layout => 0 }, label_error => $@);
 
 532   if ( $self->file_type eq 'document' ) {
 
 533     # TODO statt gen neue attribute in filetypes :
 
 534     if (($file_types{$self->object_type}->{gen}*1 & 4)==4) {
 
 535       # bit 3 is set => means upload
 
 537         'name'         => 'uploaded',
 
 538         'title'        => $main::locale->text('uploaded Documents'),
 
 539         'chk_action'   => 'uploaded_documents_delete',
 
 540         'chk_title'    => $main::locale->text('Delete Documents'),
 
 541         'chkall_title' => $main::locale->text('Delete all'),
 
 542         'file_title'   => $main::locale->text('filename'),
 
 543         'confirm_text' => $main::locale->text('delete'),
 
 545         'are_existing' => $self->existing ? 1 : 0,
 
 546         'rename_title' => $main::locale->text('Rename Attachments'),
 
 549         'upload_title' => $main::locale->text('Upload Documents'),
 
 550         'done_text'    => $main::locale->text('deleted')
 
 552       push @sources , $source;
 
 555     if (($file_types{$self->object_type}->{gen}*1 & 1)==1) {
 
 558         'title'        => $main::locale->text('generated Files'),
 
 559         'chk_action'   => 'documents_delete',
 
 560         'chk_title'    => $main::locale->text('Delete Documents'),
 
 561         'chkall_title' => $main::locale->text('Delete all'),
 
 562         'file_title'   => $main::locale->text('filename'),
 
 563         'confirm_text' => $main::locale->text('delete'),
 
 564         'can_delete'   => $::instance_conf->get_doc_delete_printfiles,
 
 565         'can_rename'   => $::instance_conf->get_doc_delete_printfiles,
 
 566         'rename_title' => $main::locale->text('Rename Documents'),
 
 567         'done_text'    => $main::locale->text('deleted')
 
 569       push @sources , $gendata;
 
 572     if (($file_types{$self->object_type}->{gen}*1 & 2)==2) {
 
 573       my @others =  SL::File->get_other_sources();
 
 574       foreach my $scanner_or_mailrx (@others) {
 
 576           'name'         => $scanner_or_mailrx->{name},
 
 577           'title'        => $main::locale->text('from \'#1\' imported Files', $scanner_or_mailrx->{description}),
 
 578           'chk_action'   => $scanner_or_mailrx->{name}.'_unimport',
 
 579           'chk_title'    => $main::locale->text('Unimport documents'),
 
 580           'chkall_title' => $main::locale->text('Unimport all'),
 
 581           'file_title'   => $main::locale->text('filename'),
 
 582           'confirm_text' => $main::locale->text('unimport'),
 
 584           'rename_title' => $main::locale->text('Rename Documents'),
 
 587           'import_title' => $main::locale->text('Add Document from \'#1\'', $scanner_or_mailrx->{name}),
 
 588           'path'         => $scanner_or_mailrx->{directory},
 
 589           'done_text'    => $main::locale->text('unimported')
 
 591         push @sources , $other;
 
 595   elsif ( $self->file_type eq 'attachment' ) {
 
 597       'name'         => 'uploaded',
 
 598       'title'        => $main::locale->text(''),
 
 599       'chk_action'   => 'attachments_delete',
 
 600       'chk_title'    => $main::locale->text('Delete Attachments'),
 
 601       'chkall_title' => $main::locale->text('Delete all'),
 
 602       'file_title'   => $main::locale->text('filename'),
 
 603       'confirm_text' => $main::locale->text('delete'),
 
 605       'are_existing' => $self->existing ? 1 : 0,
 
 606       'rename_title' => $main::locale->text('Rename Attachments'),
 
 609       'upload_title' => $main::locale->text('Upload Attachments'),
 
 610       'done_text'    => $main::locale->text('deleted')
 
 612     push @sources , $attdata;
 
 614   elsif ( $self->file_type eq 'image' ) {
 
 616       'name'         => 'uploaded',
 
 617       'title'        => $main::locale->text(''),
 
 618       'chk_action'   => 'images_delete',
 
 619       'chk_title'    => $main::locale->text('Delete Images'),
 
 620       'chkall_title' => $main::locale->text('Delete all'),
 
 621       'file_title'   => $main::locale->text('filename'),
 
 622       'confirm_text' => $main::locale->text('delete'),
 
 624       'are_existing' => $self->existing ? 1 : 0,
 
 625       'rename_title' => $main::locale->text('Rename Images'),
 
 628       'upload_title' => $main::locale->text('Upload Images'),
 
 629       'done_text'    => $main::locale->text('deleted')
 
 631     push @sources , $attdata;
 
 637 # todo: cache thumbs?
 
 638 sub _create_thumbnail {
 
 639   my ($file, $size) = @_;
 
 644   if (!eval { $filename = $file->get_file(); 1; }) {
 
 645     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail get_file failed: " . $EVAL_ERROR);
 
 649   # Workaround for pfds which are not handled by file_probe_type.
 
 650   # Maybe use mime info stored in db?
 
 651   my $mime_type = File::MimeInfo::Magic::magic($filename);
 
 652   if ($mime_type =~ m{pdf}) {
 
 653     $filename = _convert_pdf_to_png($filename, size => $size);
 
 655   return if !$filename;
 
 658   if (!eval { $content = slurp $filename; 1; }) {
 
 659     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail slurp failed: " . $EVAL_ERROR);
 
 664   if (!eval { $ret = file_probe_type($content, size => $size); 1; }) {
 
 665     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail file_probe_type failed: " . $EVAL_ERROR);
 
 669   # file_probe_type returns a hash ref with thumbnail info and content
 
 670   # or an error message
 
 671   if ('HASH' ne ref $ret) {
 
 672     $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail file_probe_type returned an error: " . $ret);
 
 679 sub _convert_pdf_to_png {
 
 680   my ($filename, %params) = @_;
 
 682   my $size    = $params{size} // 64;
 
 683   my $sfile   = SL::SessionFile::Random->new();
 
 684   unless (-f $filename) {
 
 685     $::lxdebug->message(LXDebug::WARN(), "_convert_pdf_to_png failed, no file found: $filename");
 
 688   # quotemeta for storno case "storno\ zu\ 1020" *nix only
 
 689   my $command = 'pdftoppm -singlefile -scale-to ' . $size . ' -png' . ' ' . quotemeta($filename) . ' ' . $sfile->file_name;
 
 691   if (system($command) == -1) {
 
 692     $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: system call failed: " . $ERRNO);
 
 696     $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: pdftoppm failed with error code: " . ($CHILD_ERROR >> 8));
 
 697     $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: File: $filename");
 
 701   return $sfile->file_name . '.png';
 
 714 SL::Controller::File - Controller for managing files
 
 718 The Controller is called directly from the webpages
 
 720     <a href="controller.pl?action=File/list&file_type=document\
 
 721        &object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">
 
 724 or indirectly via javascript functions from js/kivi.File.js
 
 726     kivi.popup_dialog({ url:     'controller.pl',
 
 727                         data:    { action     : 'File/ajax_upload',
 
 728                                    file_type  : 'uploaded',
 
 736 This is a controller for handling files in a storage independent way.
 
 737 The storage may be a Filesystem,a WebDAV, a Database or DMS.
 
 738 These backends must be configered in ClientConfig.
 
 739 This Controller use as intermediate layer for storage C<SL::File>.
 
 741 The Controller is responsible to display forms for displaying the files at the ERP-objects and
 
 742 for uploading and downloading the files.
 
 744 More description of the intermediate layer see L<SL::File>.
 
 748 =head2 C<action_list>
 
 750 This loads a list of files on a webpage. This can be done with a normal submit or via an ajax/json call.
 
 751 Dependent of file_type different sources are available.
 
 753 For documents there are the 'created' source and the imports from scanners or email.
 
 754 For attachments and images only the 'uploaded' source available.
 
 756 Available C<FORM PARAMS>:
 
 760 =item C<form.object_id>
 
 762 The Id of the ERP-object.
 
 764 =item C<form.object_type>
 
 766 The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
 
 768 =item C<form.file_type>
 
 770 For one ERP-object may exists different type of documents the type may be "documents","attachments" or "images".
 
 771 This file_type is a filter for the list.
 
 775 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.
 
 780 =head2 C<action_ajax_upload>
 
 783 A new file or more files can selected by a dialog and insert into the system.
 
 786 Available C<FORM PARAMS>:
 
 790 =item C<form.file_type>
 
 792 This parameter describe here the source for a new file :
 
 793 "attachments" and "images"
 
 795 This is a normal upload selection, which may be more then one file to upload.
 
 797 =item C<form.object_id>
 
 801 =item C<form.object_type>
 
 803 are the same as at C<action_list>
 
 807 =head2  C<action_ajax_files_uploaded>
 
 809 The Upload of selected Files. The "multipart_formdata" is parsed in SL::Request into the formsvariable "form.ATTACHMENTS".
 
 810 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?).
 
 811 If the same filename still exists at this object after the download for each existing filename a rename dialog will be opened.
 
 813 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.
 
 815 Available C<FORM PARAMS>:
 
 819 =item C<form.ATTACHMENTS.uploadfiles>
 
 821 This is an array of elements which have {filename} for the name and {data} for the contents.
 
 823 Also object_id, object_type and file_type
 
 827 =head2 C<action_download>
 
 829 This is the real download of a file normally called via javascript "$.download("controller.pl", data);"
 
 831 Available C<FORM PARAMS>:
 
 835 Also object_id, object_type and file_type
 
 839 =head2 C<action_ajax_importdialog>
 
 841 A Dialog with all available and not imported files to import is open.
 
 842 More then one file can be selected.
 
 844 Available C<FORM PARAMS>:
 
 850 The name of the source like "scanner1" or "email"
 
 854 The full path to the directory on the server, where the files to import can found
 
 856 Also object_id, object_type and file_type
 
 860 =head2 C<action_ajax_delete>
 
 862 Some files can be deleted
 
 864 Available C<FORM PARAMS>:
 
 870 The ids of the files to delete. Only this files are deleted not all versions of a file if the exists
 
 874 =head2 C<action_ajax_unimport>
 
 876 Some files can be unimported, dependent of the source of the file. This means they are moved
 
 877 back to the directory of the source
 
 879 Available C<FORM PARAMS>:
 
 885 The ids of the files to unimport. Only these files are unimported not all versions of a file if the exists
 
 889 =head2 C<action_ajax_rename>
 
 891 One file can be renamed. There can be some checks if the same filename still exists at one object.
 
 895 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>