1 package SL::Controller::File;
 
   5 use parent qw(SL::Controller::Base);
 
   7 use List::Util qw(first max);
 
  10 use Encode qw(decode);
 
  16 use File::Spec::Win32;
 
  17 use File::MimeInfo::Magic;
 
  18 use SL::DB::Helper::Mappings;
 
  20 use SL::DB::DeliveryOrder;
 
  23 use SL::DB::PurchaseInvoice;
 
  25 use SL::DB::GLTransaction;
 
  29 use SL::Helper::CreatePDF qw(:all);
 
  30 use SL::Locale::String;
 
  33 use SL::Controller::Helper::ThumbnailCreator qw(file_probe_image_type);
 
  35 use constant DO_DELETE   => 0;
 
  36 use constant DO_UNIMPORT => 1;
 
  38 use Rose::Object::MakeMethods::Generic
 
  40     'scalar --get_set_init' => [ qw() ],
 
  41     'scalar' => [ qw(object object_type object_model object_id object_right file_type files is_global existing) ],
 
  44 __PACKAGE__->run_before('check_object_params', only => [ qw(list ajax_delete ajax_importdialog ajax_import ajax_unimport ajax_upload ajax_files_uploaded) ]);
 
  47   'sales_quotation'         => { gen => 1, gltype => '',   dir =>'SalesQuotation',       model => 'Order',          right => 'import_ar'  },
 
  48   'sales_order'             => { gen => 1, gltype => '',   dir =>'SalesOrder',           model => 'Order',          right => 'import_ar'  },
 
  49   'sales_delivery_order'    => { gen => 1, gltype => '',   dir =>'SalesDeliveryOrder',   model => 'DeliveryOrder',  right => 'import_ar'  },
 
  50   'invoice'                 => { gen => 1, gltype => 'ar', dir =>'SalesInvoice',         model => 'Invoice',        right => 'import_ar'  },
 
  51   'credit_note'             => { gen => 1, gltype => '',   dir =>'CreditNote',           model => 'Invoice',        right => 'import_ar'  },
 
  52   'request_quotation'       => { gen => 3, gltype => '',   dir =>'RequestForQuotation',  model => 'Order',          right => 'import_ap'  },
 
  53   'purchase_order'          => { gen => 3, gltype => '',   dir =>'PurchaseOrder',        model => 'Order',          right => 'import_ap'  },
 
  54   'purchase_delivery_order' => { gen => 3, gltype => '',   dir =>'PurchaseDeliveryOrder',model => 'DeliveryOrder',  right => 'import_ap'  },
 
  55   'purchase_invoice'        => { gen => 2, gltype => 'ap', dir =>'PurchaseInvoice',      model => 'PurchaseInvoice',right => 'import_ap'  },
 
  56   'vendor'                  => { gen => 0, gltype => '',   dir =>'Vendor',               model => 'Vendor',         right => 'xx'         },
 
  57   'customer'                => { gen => 1, gltype => '',   dir =>'Customer',             model => 'Customer',       right => 'xx'         },
 
  58   'part'                    => { gen => 0, gltype => '',   dir =>'Part',                 model => 'Part',           right => 'xx'         },
 
  59   'gl_transaction'          => { gen => 2, gltype => 'gl', dir =>'GeneralLedger',        model => 'GLTransaction',  right => 'import_ap'  },
 
  60   'draft'                   => { gen => 0, gltype => '',   dir =>'Draft',                model => 'Draft',          right => 'xx'         },
 
  61   'csv_customer'            => { gen => 1, gltype => '',   dir =>'Reports',              model => 'Customer',       right => 'xx'         },
 
  62   'csv_vendor'              => { gen => 1, gltype => '',   dir =>'Reports',              model => 'Vendor',         right => 'xx'         },
 
  63   'shop_image'              => { gen => 0, gltype => '',   dir =>'ShopImages',           model => 'Part',           right => 'xx'         },
 
  67 # $main::locale->text('imported')
 
  77   $is_json = 1 if $::form->{json};
 
  79   $self->_do_list($is_json);
 
  82 sub action_ajax_importdialog {
 
  84   $::auth->assert($self->object_right);
 
  85   my $path   = $::form->{path};
 
  86   my @files  = $self->_get_from_import($path);
 
  88     'name'         => $::form->{source},
 
  90     'chk_action'   => $::form->{source}.'_import',
 
  91     'chk_title'    => $main::locale->text('Import scanned documents'),
 
  92     'chkall_title' => $main::locale->text('Import all'),
 
  95   $self->render('file/import_dialog',
 
 102 sub action_ajax_import {
 
 104   $::auth->assert($self->object_right);
 
 105   my $ids    = $::form->{ids};
 
 106   my $source = $::form->{source};
 
 107   my $path   = $::form->{path};
 
 108   my @files  = $self->_get_from_import($path);
 
 109   foreach my $filename (@{ $::form->{$ids} || [] }) {
 
 110     my ($file, undef) = grep { $_->{name} eq $filename } @files;
 
 112       my $obj = SL::File->save(object_id   => $self->object_id,
 
 113                                object_type => $self->object_type,
 
 114                                mime_type   => 'application/pdf',
 
 116                                file_type   => 'document',
 
 117                                file_name   => $file->{filename},
 
 118                                file_path   => $file->{path}
 
 120       unlink($file->{path}) if $obj;
 
 126 sub action_ajax_delete {
 
 128   $self->_delete_all(DO_DELETE, $::locale->text('Following files are deleted:'));
 
 131 sub action_ajax_unimport {
 
 133   $self->_delete_all(DO_UNIMPORT, $::locale->text('Following files are unimported:'));
 
 136 sub action_ajax_rename {
 
 138   my ($id, $version) = split /_/, $::form->{id};
 
 139   my $file = SL::File->get(id => $id);
 
 141     $self->js->flash('error', $::locale->text('File not exists !'))->render();
 
 144   my $sessionfile = $::form->{sessionfile};
 
 145   if ( $sessionfile && -f $sessionfile ) {
 
 147     if ( $::form->{to} eq $file->file_name ) {
 
 148       # no rename so use as new version
 
 149       $file->save_file($sessionfile);
 
 150       $self->js->flash('warning', $::locale->text('File \'#1\' is used as new Version !', $file->file_name));
 
 153       # new filename, so it is a new file with the same attributes as the old file
 
 155         SL::File->save(object_id   => $file->object_id,
 
 156                        object_type => $file->object_type,
 
 157                        mime_type   => $file->mime_type,
 
 158                        source      => $file->source,
 
 159                        file_type   => $file->file_type,
 
 160                        file_name   => $::form->{to},
 
 161                        file_path   => $sessionfile
 
 163         unlink($sessionfile);
 
 166         $self->js->flash(       'error', t8('internal error (see details)'))
 
 167                  ->flash_detail('error', $@)->render;
 
 177       $result = $file->rename($::form->{to});
 
 180       $self->js->flash(       'error', t8('internal error (see details)'))
 
 181                ->flash_detail('error', $@)->render;
 
 185     if ($result != SL::File::RENAME_OK) {
 
 186       $self->js->flash('error',
 
 187                          $result == SL::File::RENAME_EXISTS ? $::locale->text('File still exists !')
 
 188                        : $result == SL::File::RENAME_SAME   ? $::locale->text('Same Filename !')
 
 189                        :                                      $::locale->text('File not exists !'))
 
 194   $self->is_global($::form->{is_global});
 
 195   $self->file_type(  $file->file_type);
 
 196   $self->object_type($file->object_type);
 
 197   $self->object_id(  $file->object_id);
 
 198   #$self->object_model($file_types{$file->module}->{model});
 
 199   #$self->object_right($file_types{$file->module}->{right});
 
 200   if ( $::form->{next_ids} ) {
 
 201     my @existing = split(/,/, $::form->{next_ids});
 
 202     $self->existing(\@existing);
 
 207 sub action_ajax_upload {
 
 209   $self->{maxsize} = $::instance_conf->get_doc_max_filesize;
 
 210   $self->{accept_types} = '';
 
 211   $self->{accept_types} = 'image/png,image/gif,image/jpeg,image/tiff,*png,*gif,*.jpg,*.tif' if $self->{file_type} eq 'image';
 
 212   $self->render('file/upload_dialog',
 
 218 sub action_ajax_files_uploaded {
 
 221   my $source = 'uploaded';
 
 223   if ( $::form->{ATTACHMENTS}->{uploadfiles} ) {
 
 224     my @upfiles = @{ $::form->{ATTACHMENTS}->{uploadfiles} };
 
 225     foreach my $idx (0 .. scalar(@upfiles) - 1) {
 
 227         my $fname = uri_unescape($upfiles[$idx]->{filename});
 
 228         # normalize and find basename
 
 229         # first split with unix rules
 
 230         # after that split with windows rules
 
 231         my ($volume, $directories, $basefile) = File::Spec::Unix->splitpath($fname);
 
 232         ($volume, $directories, $basefile) = File::Spec::Win32->splitpath($basefile);
 
 234         # to find real mime_type by magic we must save the filedata
 
 236         my $sess_fname = "file_upload_" . $self->object_type . "_" . $self->object_id . "_" . $idx;
 
 237         my $sfile      = SL::SessionFile->new($sess_fname, mode => 'w');
 
 239         $sfile->fh->print(${$upfiles[$idx]->{data}});
 
 241         my $mime_type = File::MimeInfo::Magic::magic($sfile->file_name);
 
 244           # if filename has the suffix "pdf", but isn't really a pdf, set mimetype for no suffix
 
 245           $mime_type = File::MimeInfo::Magic::mimetype($basefile);
 
 246           $mime_type = 'application/octet-stream' if $mime_type eq 'application/pdf' || !$mime_type;
 
 248         if ( $self->file_type eq 'image' && $self->file_probe_image_type($mime_type, $basefile)) {
 
 251         my ($existobj) = SL::File->get_all(object_id   => $self->object_id,
 
 252                                            object_type => $self->object_type,
 
 253                                            mime_type   => $mime_type,
 
 255                                            file_type   => $self->file_type,
 
 256                                            file_name   => $basefile,
 
 260           push @existing, $existobj->id.'_'.$sfile->file_name;
 
 262           my $fileobj = SL::File->save(object_id        => $self->object_id,
 
 263                                        object_type      => $self->object_type,
 
 264                                        mime_type        => $mime_type,
 
 266                                        file_type        => $self->file_type,
 
 267                                        file_name        => $basefile,
 
 268                                        title            => $::form->{title},
 
 269                                        description      => $::form->{description},
 
 270                                        ## two possibilities: what is better ? content or sessionfile ??
 
 271                                        file_contents    => ${$upfiles[$idx]->{data}},
 
 272                                        file_path        => $sfile->file_name
 
 274           unlink($sfile->file_name);
 
 278         $self->js->flash(       'error', t8('internal error (see details)'))
 
 279                  ->flash_detail('error', $@)->render;
 
 284   $self->existing(\@existing);
 
 288 sub action_download {
 
 290   my ($id, $version) = split /_/, $::form->{id};
 
 291   my $file = SL::File->get(id => $id );
 
 292   $file->version($version) if $version;
 
 293   my $ref  = $file->get_content;
 
 294   if ( $file && $ref ) {
 
 295     return $self->send_file($ref,
 
 296       type => $file->mime_type,
 
 297       name => $file->file_name,
 
 306 sub check_object_params {
 
 309   my $id      = ($::form->{object_id} // 0) * 1;
 
 310   my $draftid = ($::form->{draft_id}  // 0) * 1;
 
 314   if ( $draftid == 0 && $id == 0 && $::form->{is_global} ) {
 
 316     $type  = $::form->{object_type};
 
 319     $id   = $::form->{draft_id};
 
 321   } elsif ( $::form->{object_type} ) {
 
 322     $type = $::form->{object_type};
 
 324   die "No object type"     unless $type;
 
 325   die "No file type"       unless $::form->{file_type};
 
 326   die "Unkown object type" unless $file_types{$type};
 
 328   $self->is_global($gldoc);
 
 329   $self->file_type($::form->{file_type});
 
 330   $self->object_type($type);
 
 331   $self->object_id($id);
 
 332   $self->object_model($file_types{$type}->{model});
 
 333   $self->object_right($file_types{$type}->{right});
 
 335  # $::auth->assert($self->object_right);
 
 337  # my $model = 'SL::DB::' . $self->object_model;
 
 338  # $self->object($model->new(id => $self->object_id)->load || die "Record not found");
 
 348   my ($self, $do_unimport, $infotext) = @_;
 
 350   my $ids = $::form->{ids};
 
 351   foreach my $id_version (@{ $::form->{$ids} || [] }) {
 
 352     my ($id, $version) = split /_/, $id_version;
 
 353     my $dbfile = SL::File->get(id => $id);
 
 356         $dbfile->version($version);
 
 357         $files .= ' ' . $dbfile->file_name if $dbfile->delete_version;
 
 359         $files .= ' ' . $dbfile->file_name if $dbfile->delete;
 
 363   $self->js->flash('info', $infotext . $files) if $files;
 
 368   my ($self, $json) = @_;
 
 370   if ( $self->file_type eq 'document' ) {
 
 372     push @object_types, $self->object_type;
 
 373     push @object_types, qw(dunning dunning1 dunning2 dunning3) if $self->object_type eq 'invoice'; # hardcoded object types?
 
 374     @files = SL::File->get_all_versions(object_id   => $self->object_id,
 
 375                                         object_type => \@object_types,
 
 376                                         file_type   => $self->file_type,
 
 380   elsif ( $self->file_type eq 'attachment' || $self->file_type eq 'image' ) {
 
 381     @files   = SL::File->get_all(object_id   => $self->object_id,
 
 382                                  object_type => $self->object_type,
 
 383                                  file_type   => $self->file_type,
 
 386   $self->files(\@files);
 
 388   if($self->object_type eq 'shop_image'){
 
 390       ->run('kivi.ShopPart.show_images', $self->object_id)
 
 393     $self->_mk_render('file/list', 1, 0, $json);
 
 397 sub _get_from_import {
 
 398   my ($self, $path) = @_;
 
 401   my $language = $::lx_office_conf{system}->{language};
 
 402   my $timezone = $::locale->get_local_time_zone()->name;
 
 403   if (opendir my $dir, $path) {
 
 404     my @files = ( readdir $dir);
 
 405     foreach my $file ( @files) {
 
 406       next if (($file eq '.') || ($file eq '..'));
 
 407       $file = Encode::decode('utf-8', $file);
 
 409       next if ( -d "$path/$file" );
 
 411       my $tmppath = File::Spec->catfile( $path, $file );
 
 412       next if( ! -f $tmppath );
 
 414       my $st = stat($tmppath);
 
 415       my $dt = DateTime->from_epoch( epoch => $st->mtime, time_zone => $timezone, locale => $language );
 
 416       my $sname = $main::locale->quote_special_chars('HTML', $file);
 
 419         'filename' => $sname,
 
 421         'mtime'    => $st->mtime,
 
 422         'date'     => $dt->dmy('.') . " " . $dt->hms,
 
 431   my ($self, $template, $edit, $scanner, $json) = @_;
 
 434     ##TODO make code configurable
 
 437     my @sources = $self->_get_sources();
 
 438     foreach my $source ( @sources ) {
 
 439       @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files };
 
 441     if ( $self->file_type eq 'document' ) {
 
 442       $title = $main::locale->text('Documents');
 
 443     } elsif ( $self->file_type eq 'attachment' ) {
 
 444       $title = $main::locale->text('Attachments');
 
 445     } elsif ( $self->file_type eq 'image' ) {
 
 446       $title = $main::locale->text('Images');
 
 449     my $output         = SL::Presenter->get->render(
 
 452       SOURCES          => \@sources,
 
 453       edit_attachments => $edit,
 
 454       object_type      => $self->object_type,
 
 455       object_id        => $self->object_id,
 
 456       file_type        => $self->file_type,
 
 457       is_global        => $self->is_global,
 
 461       $self->js->html('#'.$self->file_type.'_list_'.$self->object_type, $output);
 
 462       if ( $self->existing && scalar(@{$self->existing}) > 0) {
 
 463         my $first = shift @{$self->existing};
 
 464         my ($first_id, $sfile) = split('_', $first, 2);
 
 465         my $file = SL::File->get(id => $first_id );
 
 466         $self->js->run('kivi.File.askForRename', $first_id, $file->file_type, $file->file_name, $sfile, join (',', @{$self->existing}), $self->is_global);
 
 470         $self->render(\$output, { layout => 0, process => 0 });
 
 475       $self->js->flash(       'error', t8('internal error (see details)'))
 
 476                ->flash_detail('error', $@)->render;
 
 478       $self->render('generic/error', { layout => 0 }, label_error => $@);
 
 487   if ( $self->file_type eq 'document' ) {
 
 488     # TODO statt gen neue attribute in filetypes :
 
 489     if (($file_types{$self->object_type}->{gen}*1 & 1)==1) {
 
 492         'title'        => $main::locale->text('generated Files'),
 
 493         'chk_action'   => 'documents_delete',
 
 494         'chk_title'    => $main::locale->text('Delete Documents'),
 
 495         'chkall_title' => $main::locale->text('Delete all'),
 
 496         'file_title'   => $main::locale->text('filename'),
 
 497         'confirm_text' => $main::locale->text('delete'),
 
 498         'can_delete'   => $::instance_conf->get_doc_delete_printfiles,
 
 499         'can_rename'   => $::instance_conf->get_doc_delete_printfiles,
 
 500         'rename_title' => $main::locale->text('Rename Documents'),
 
 501         'done_text'    => $main::locale->text('deleted')
 
 503       push @sources , $gendata;
 
 505     if (($file_types{$self->object_type}->{gen}*1 & 2)==2) {
 
 506       my @others =  SL::File->get_other_sources();
 
 507       foreach my $scanner_or_mailrx (@others) {
 
 509           'name'         => $scanner_or_mailrx->{name},
 
 510           'title'        => $main::locale->text('from \'#1\' imported Files', $scanner_or_mailrx->{description}),
 
 511           'chk_action'   => $scanner_or_mailrx->{name}.'_unimport',
 
 512           'chk_title'    => $main::locale->text('Unimport documents'),
 
 513           'chkall_title' => $main::locale->text('Unimport all'),
 
 514           'file_title'   => $main::locale->text('filename'),
 
 515           'confirm_text' => $main::locale->text('unimport'),
 
 517           'rename_title' => $main::locale->text('Rename Documents'),
 
 520           'import_title' => $main::locale->text('Add Document from \'#1\'', $scanner_or_mailrx->{name}),
 
 521           'path'         => $scanner_or_mailrx->{directory},
 
 522           'done_text'    => $main::locale->text('unimported')
 
 524         push @sources , $other;
 
 528   elsif ( $self->file_type eq 'attachment' ) {
 
 530       'name'         => 'uploaded',
 
 531       'title'        => $main::locale->text(''),
 
 532       'chk_action'   => 'attachments_delete',
 
 533       'chk_title'    => $main::locale->text('Delete Attachments'),
 
 534       'chkall_title' => $main::locale->text('Delete all'),
 
 535       'file_title'   => $main::locale->text('filename'),
 
 536       'confirm_text' => $main::locale->text('delete'),
 
 538       'are_existing' => $self->existing ? 1 : 0,
 
 539       'rename_title' => $main::locale->text('Rename Attachments'),
 
 542       'upload_title' => $main::locale->text('Upload Attachments'),
 
 543       'done_text'    => $main::locale->text('deleted')
 
 545     push @sources , $attdata;
 
 547   elsif ( $self->file_type eq 'image' ) {
 
 549       'name'         => 'uploaded',
 
 550       'title'        => $main::locale->text(''),
 
 551       'chk_action'   => 'images_delete',
 
 552       'chk_title'    => $main::locale->text('Delete Images'),
 
 553       'chkall_title' => $main::locale->text('Delete all'),
 
 554       'file_title'   => $main::locale->text('filename'),
 
 555       'confirm_text' => $main::locale->text('delete'),
 
 557       'are_existing' => $self->existing ? 1 : 0,
 
 558       'rename_title' => $main::locale->text('Rename Images'),
 
 561       'upload_title' => $main::locale->text('Upload Images'),
 
 562       'done_text'    => $main::locale->text('deleted')
 
 564     push @sources , $attdata;
 
 579 SL::Controller::File - Controller for managing files
 
 583 The Controller is called directly from the webpages
 
 585     <a href="controller.pl?action=File/list&file_type=document\
 
 586        &object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">
 
 589 or indirectly via javascript functions from js/kivi.File.js
 
 591     kivi.popup_dialog({ url:     'controller.pl',
 
 592                         data:    { action     : 'File/ajax_upload',
 
 593                                    file_type  : 'uploaded',
 
 601 This is a controller for handling files in a storage independent way.
 
 602 The storage may be a Filesystem,a WebDAV, a Database or DMS.
 
 603 These backends must be configered in ClientConfig.
 
 604 This Controller use as intermediate layer for storage C<SL::File>.
 
 606 The Controller is responsible to display forms for displaying the files at the ERP-objects and
 
 607 for uploading and downloading the files.
 
 609 More description of the intermediate layer see L<SL::File>.
 
 613 =head2 C<action_list>
 
 615 This loads a list of files on a webpage. This can be done with a normal submit or via an ajax/json call.
 
 616 Dependent of file_type different sources are available.
 
 618 For documents there are the 'created' source and the imports from scanners or email.
 
 619 For attachments and images only the 'uploaded' source available.
 
 621 Available C<FORM PARAMS>:
 
 625 =item C<form.object_id>
 
 627 The Id of the ERP-object.
 
 629 =item C<form.object_type>
 
 631 The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
 
 633 =item C<form.file_type>
 
 635 For one ERP-object may exists different type of documents the type may be "documents","attachments" or "images".
 
 636 This file_type is a filter for the list.
 
 640 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.
 
 645 =head2 C<action_ajax_upload>
 
 648 A new file or more files can selected by a dialog and insert into the system.
 
 651 Available C<FORM PARAMS>:
 
 655 =item C<form.file_type>
 
 657 This parameter describe here the source for a new file :
 
 658 "attachments" and "images"
 
 660 This is a normal upload selection, which may be more then one file to upload.
 
 662 =item C<form.object_id>
 
 666 =item C<form.object_type>
 
 668 are the same as at C<action_list>
 
 672 =head2  C<action_ajax_files_uploaded>
 
 674 The Upload of selected Files. The "multipart_formdata" is parsed in SL::Request into the formsvariable "form.ATTACHMENTS".
 
 675 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?).
 
 676 If the same filename still exists at this object after the download for each existing filename a rename dialog will be opened.
 
 678 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.
 
 680 Available C<FORM PARAMS>:
 
 684 =item C<form.ATTACHMENTS.uploadfiles>
 
 686 This is an array of elements which have {filename} for the name and {data} for the contents.
 
 688 Also object_id, object_type and file_type
 
 692 =head2 C<action_download>
 
 694 This is the real download of a file normally called via javascript "$.download("controller.pl", data);"
 
 696 Available C<FORM PARAMS>:
 
 700 Also object_id, object_type and file_type
 
 704 =head2 C<action_ajax_importdialog>
 
 706 A Dialog with all available and not imported files to import is open.
 
 707 More then one file can be selected.
 
 709 Available C<FORM PARAMS>:
 
 715 The name of the source like "scanner1" or "email"
 
 719 The full path to the directory on the server, where the files to import can found
 
 721 Also object_id, object_type and file_type
 
 725 =head2 C<action_ajax_delete>
 
 727 Some files can be deleted
 
 729 Available C<FORM PARAMS>:
 
 735 The ids of the files to delete. Only this files are deleted not all versions of a file if the exists
 
 739 =head2 C<action_ajax_unimport>
 
 741 Some files can be unimported, dependent of the source of the file. This means they are moved
 
 742 back to the directory of the source
 
 744 Available C<FORM PARAMS>:
 
 750 The ids of the files to unimport. Only these files are unimported not all versions of a file if the exists
 
 754 =head2 C<action_ajax_rename>
 
 756 One file can be renamed. There can be some checks if the same filename still exists at one object.
 
 760 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>