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) ]);
46 # gen: bitmask: bit 1 (value is 1 or 3) => file created
47 # bit 2 (value is 2 or 3) => file from other source
48 # gltype: is this used somewhere?
49 # dir: is this used somewhere?
50 # model: base name of the rose model
51 # right: access right used for import
53 'sales_quotation' => { gen => 1, gltype => '', dir =>'SalesQuotation', model => 'Order', right => 'import_ar' },
54 'sales_order' => { gen => 1, gltype => '', dir =>'SalesOrder', model => 'Order', right => 'import_ar' },
55 'sales_delivery_order' => { gen => 1, gltype => '', dir =>'SalesDeliveryOrder', model => 'DeliveryOrder', right => 'import_ar' },
56 'invoice' => { gen => 1, gltype => 'ar', dir =>'SalesInvoice', model => 'Invoice', right => 'import_ar' },
57 'credit_note' => { gen => 1, gltype => '', dir =>'CreditNote', model => 'Invoice', right => 'import_ar' },
58 'request_quotation' => { gen => 3, gltype => '', dir =>'RequestForQuotation', model => 'Order', right => 'import_ap' },
59 'purchase_order' => { gen => 3, gltype => '', dir =>'PurchaseOrder', model => 'Order', right => 'import_ap' },
60 'purchase_delivery_order' => { gen => 3, gltype => '', dir =>'PurchaseDeliveryOrder',model => 'DeliveryOrder', right => 'import_ap' },
61 'purchase_invoice' => { gen => 2, gltype => 'ap', dir =>'PurchaseInvoice', model => 'PurchaseInvoice',right => 'import_ap' },
62 'vendor' => { gen => 0, gltype => '', dir =>'Vendor', model => 'Vendor', right => 'xx' },
63 'customer' => { gen => 1, gltype => '', dir =>'Customer', model => 'Customer', right => 'xx' },
64 'part' => { gen => 0, gltype => '', dir =>'Part', model => 'Part', right => 'xx' },
65 'gl_transaction' => { gen => 2, gltype => 'gl', dir =>'GeneralLedger', model => 'GLTransaction', right => 'import_ap' },
66 'draft' => { gen => 0, gltype => '', dir =>'Draft', model => 'Draft', right => 'xx' },
67 'csv_customer' => { gen => 1, gltype => '', dir =>'Reports', model => 'Customer', right => 'xx' },
68 'csv_vendor' => { gen => 1, gltype => '', dir =>'Reports', model => 'Vendor', right => 'xx' },
69 'shop_image' => { gen => 0, gltype => '', dir =>'ShopImages', model => 'Part', right => 'xx' },
70 'letter' => { gen => 3, gltype => '', dir =>'Letter', model => 'Letter', right => 'sales_letter_edit | purchase_letter_edit' },
74 # $main::locale->text('imported')
84 $is_json = 1 if $::form->{json};
86 $self->_do_list($is_json);
89 sub action_ajax_importdialog {
91 $::auth->assert($self->object_right);
92 my $path = $::form->{path};
93 my @files = $self->_get_from_import($path);
95 'name' => $::form->{source},
97 'chk_action' => $::form->{source}.'_import',
98 'chk_title' => $main::locale->text('Import scanned documents'),
99 'chkall_title' => $main::locale->text('Import all'),
102 $self->render('file/import_dialog',
109 sub action_ajax_import {
111 $::auth->assert($self->object_right);
112 my $ids = $::form->{ids};
113 my $source = $::form->{source};
114 my $path = $::form->{path};
115 my @files = $self->_get_from_import($path);
116 foreach my $filename (@{ $::form->{$ids} || [] }) {
117 my ($file, undef) = grep { $_->{name} eq $filename } @files;
119 my $obj = SL::File->save(object_id => $self->object_id,
120 object_type => $self->object_type,
121 mime_type => 'application/pdf',
123 file_type => 'document',
124 file_name => $file->{filename},
125 file_path => $file->{path}
127 unlink($file->{path}) if $obj;
133 sub action_ajax_delete {
135 $self->_delete_all(DO_DELETE, $::locale->text('Following files are deleted:'));
138 sub action_ajax_unimport {
140 $self->_delete_all(DO_UNIMPORT, $::locale->text('Following files are unimported:'));
143 sub action_ajax_rename {
145 my ($id, $version) = split /_/, $::form->{id};
146 my $file = SL::File->get(id => $id);
148 $self->js->flash('error', $::locale->text('File not exists !'))->render();
151 my $sessionfile = $::form->{sessionfile};
152 if ( $sessionfile && -f $sessionfile ) {
154 if ( $::form->{to} eq $file->file_name ) {
155 # no rename so use as new version
156 $file->save_file($sessionfile);
157 $self->js->flash('warning', $::locale->text('File \'#1\' is used as new Version !', $file->file_name));
160 # new filename, so it is a new file with the same attributes as the old file
162 SL::File->save(object_id => $file->object_id,
163 object_type => $file->object_type,
164 mime_type => $file->mime_type,
165 source => $file->source,
166 file_type => $file->file_type,
167 file_name => $::form->{to},
168 file_path => $sessionfile
170 unlink($sessionfile);
173 $self->js->flash( 'error', t8('internal error (see details)'))
174 ->flash_detail('error', $@)->render;
184 $result = $file->rename($::form->{to});
187 $self->js->flash( 'error', t8('internal error (see details)'))
188 ->flash_detail('error', $@)->render;
192 if ($result != SL::File::RENAME_OK) {
193 $self->js->flash('error',
194 $result == SL::File::RENAME_EXISTS ? $::locale->text('File still exists !')
195 : $result == SL::File::RENAME_SAME ? $::locale->text('Same Filename !')
196 : $::locale->text('File not exists !'))
201 $self->is_global($::form->{is_global});
202 $self->file_type( $file->file_type);
203 $self->object_type($file->object_type);
204 $self->object_id( $file->object_id);
205 #$self->object_model($file_types{$file->module}->{model});
206 #$self->object_right($file_types{$file->module}->{right});
207 if ( $::form->{next_ids} ) {
208 my @existing = split(/,/, $::form->{next_ids});
209 $self->existing(\@existing);
214 sub action_ajax_upload {
216 $self->{maxsize} = $::instance_conf->get_doc_max_filesize;
217 $self->{accept_types} = '';
218 $self->{accept_types} = 'image/png,image/gif,image/jpeg,image/tiff,*png,*gif,*.jpg,*.tif' if $self->{file_type} eq 'image';
219 $self->render('file/upload_dialog',
225 sub action_ajax_files_uploaded {
228 my $source = 'uploaded';
230 if ( $::form->{ATTACHMENTS}->{uploadfiles} ) {
231 my @upfiles = @{ $::form->{ATTACHMENTS}->{uploadfiles} };
232 foreach my $idx (0 .. scalar(@upfiles) - 1) {
234 my $fname = uri_unescape($upfiles[$idx]->{filename});
235 # normalize and find basename
236 # first split with unix rules
237 # after that split with windows rules
238 my ($volume, $directories, $basefile) = File::Spec::Unix->splitpath($fname);
239 ($volume, $directories, $basefile) = File::Spec::Win32->splitpath($basefile);
241 # to find real mime_type by magic we must save the filedata
243 my $sess_fname = "file_upload_" . $self->object_type . "_" . $self->object_id . "_" . $idx;
244 my $sfile = SL::SessionFile->new($sess_fname, mode => 'w');
246 $sfile->fh->print(${$upfiles[$idx]->{data}});
248 my $mime_type = File::MimeInfo::Magic::magic($sfile->file_name);
251 # if filename has the suffix "pdf", but isn't really a pdf, set mimetype for no suffix
252 $mime_type = File::MimeInfo::Magic::mimetype($basefile);
253 $mime_type = 'application/octet-stream' if $mime_type eq 'application/pdf' || !$mime_type;
255 if ( $self->file_type eq 'image' && $self->file_probe_image_type($mime_type, $basefile)) {
258 my ($existobj) = SL::File->get_all(object_id => $self->object_id,
259 object_type => $self->object_type,
260 mime_type => $mime_type,
262 file_type => $self->file_type,
263 file_name => $basefile,
267 push @existing, $existobj->id.'_'.$sfile->file_name;
269 my $fileobj = SL::File->save(object_id => $self->object_id,
270 object_type => $self->object_type,
271 mime_type => $mime_type,
273 file_type => $self->file_type,
274 file_name => $basefile,
275 title => $::form->{title},
276 description => $::form->{description},
277 ## two possibilities: what is better ? content or sessionfile ??
278 file_contents => ${$upfiles[$idx]->{data}},
279 file_path => $sfile->file_name
281 unlink($sfile->file_name);
285 $self->js->flash( 'error', t8('internal error (see details)'))
286 ->flash_detail('error', $@)->render;
291 $self->existing(\@existing);
295 sub action_download {
297 my ($id, $version) = split /_/, $::form->{id};
298 my $file = SL::File->get(id => $id );
299 $file->version($version) if $version;
300 my $ref = $file->get_content;
301 if ( $file && $ref ) {
302 return $self->send_file($ref,
303 type => $file->mime_type,
304 name => $file->file_name,
313 sub check_object_params {
316 my $id = ($::form->{object_id} // 0) * 1;
317 my $draftid = ($::form->{draft_id} // 0) * 1;
321 if ( $draftid == 0 && $id == 0 && $::form->{is_global} ) {
323 $type = $::form->{object_type};
326 $id = $::form->{draft_id};
328 } elsif ( $::form->{object_type} ) {
329 $type = $::form->{object_type};
331 die "No object type" unless $type;
332 die "No file type" unless $::form->{file_type};
333 die "Unknown object type" unless $file_types{$type};
335 $self->is_global($gldoc);
336 $self->file_type($::form->{file_type});
337 $self->object_type($type);
338 $self->object_id($id);
339 $self->object_model($file_types{$type}->{model});
340 $self->object_right($file_types{$type}->{right});
342 # $::auth->assert($self->object_right);
344 # my $model = 'SL::DB::' . $self->object_model;
345 # $self->object($model->new(id => $self->object_id)->load || die "Record not found");
355 my ($self, $do_unimport, $infotext) = @_;
357 my $ids = $::form->{ids};
358 foreach my $id_version (@{ $::form->{$ids} || [] }) {
359 my ($id, $version) = split /_/, $id_version;
360 my $dbfile = SL::File->get(id => $id);
363 $dbfile->version($version);
364 $files .= ' ' . $dbfile->file_name if $dbfile->delete_version;
366 $files .= ' ' . $dbfile->file_name if $dbfile->delete;
370 $self->js->flash('info', $infotext . $files) if $files;
375 my ($self, $json) = @_;
377 if ( $self->file_type eq 'document' ) {
379 push @object_types, $self->object_type;
380 push @object_types, qw(dunning dunning1 dunning2 dunning3) if $self->object_type eq 'invoice'; # hardcoded object types?
381 @files = SL::File->get_all_versions(object_id => $self->object_id,
382 object_type => \@object_types,
383 file_type => $self->file_type,
387 elsif ( $self->file_type eq 'attachment' || $self->file_type eq 'image' ) {
388 @files = SL::File->get_all(object_id => $self->object_id,
389 object_type => $self->object_type,
390 file_type => $self->file_type,
393 $self->files(\@files);
395 if($self->object_type eq 'shop_image'){
397 ->run('kivi.ShopPart.show_images', $self->object_id)
400 $self->_mk_render('file/list', 1, 0, $json);
404 sub _get_from_import {
405 my ($self, $path) = @_;
408 my $language = $::lx_office_conf{system}->{language};
409 my $timezone = $::locale->get_local_time_zone()->name;
410 if (opendir my $dir, $path) {
411 my @files = (readdir $dir);
412 foreach my $file ( @files) {
413 next if (($file eq '.') || ($file eq '..'));
414 $file = Encode::decode('utf-8', $file);
416 next if ( -d "$path/$file" );
418 my $tmppath = File::Spec->catfile( $path, $file );
419 next if( ! -f $tmppath );
421 my $st = stat($tmppath);
422 my $dt = DateTime->from_epoch( epoch => $st->mtime, time_zone => $timezone, locale => $language );
423 my $sname = $main::locale->quote_special_chars('HTML', $file);
426 'filename' => $sname,
428 'mtime' => $st->mtime,
429 'date' => $dt->dmy('.') . " " . $dt->hms,
436 $::lxdebug->message(LXDebug::WARN(), "SL::File::_get_from_import opendir failed to open dir " . $path);
443 my ($self, $template, $edit, $scanner, $json) = @_;
446 ##TODO make code configurable
449 my @sources = $self->_get_sources();
450 foreach my $source ( @sources ) {
451 @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files };
453 if ( $self->file_type eq 'document' ) {
454 $title = $main::locale->text('Documents');
455 } elsif ( $self->file_type eq 'attachment' ) {
456 $title = $main::locale->text('Attachments');
457 } elsif ( $self->file_type eq 'image' ) {
458 $title = $main::locale->text('Images');
461 my $output = SL::Presenter->get->render(
464 SOURCES => \@sources,
465 edit_attachments => $edit,
466 object_type => $self->object_type,
467 object_id => $self->object_id,
468 file_type => $self->file_type,
469 is_global => $self->is_global,
473 $self->js->html('#'.$self->file_type.'_list_'.$self->object_type, $output);
474 if ( $self->existing && scalar(@{$self->existing}) > 0) {
475 my $first = shift @{$self->existing};
476 my ($first_id, $sfile) = split('_', $first, 2);
477 my $file = SL::File->get(id => $first_id );
478 $self->js->run('kivi.File.askForRename', $first_id, $file->file_type, $file->file_name, $sfile, join (',', @{$self->existing}), $self->is_global);
482 $self->render(\$output, { layout => 0, process => 0 });
487 $self->js->flash( 'error', t8('internal error (see details)'))
488 ->flash_detail('error', $@)->render;
490 $self->render('generic/error', { layout => 0 }, label_error => $@);
499 if ( $self->file_type eq 'document' ) {
500 # TODO statt gen neue attribute in filetypes :
501 if (($file_types{$self->object_type}->{gen}*1 & 1)==1) {
504 'title' => $main::locale->text('generated Files'),
505 'chk_action' => 'documents_delete',
506 'chk_title' => $main::locale->text('Delete Documents'),
507 'chkall_title' => $main::locale->text('Delete all'),
508 'file_title' => $main::locale->text('filename'),
509 'confirm_text' => $main::locale->text('delete'),
510 'can_delete' => $::instance_conf->get_doc_delete_printfiles,
511 'can_rename' => $::instance_conf->get_doc_delete_printfiles,
512 'rename_title' => $main::locale->text('Rename Documents'),
513 'done_text' => $main::locale->text('deleted')
515 push @sources , $gendata;
517 if (($file_types{$self->object_type}->{gen}*1 & 2)==2) {
518 my @others = SL::File->get_other_sources();
519 foreach my $scanner_or_mailrx (@others) {
521 'name' => $scanner_or_mailrx->{name},
522 'title' => $main::locale->text('from \'#1\' imported Files', $scanner_or_mailrx->{description}),
523 'chk_action' => $scanner_or_mailrx->{name}.'_unimport',
524 'chk_title' => $main::locale->text('Unimport documents'),
525 'chkall_title' => $main::locale->text('Unimport all'),
526 'file_title' => $main::locale->text('filename'),
527 'confirm_text' => $main::locale->text('unimport'),
529 'rename_title' => $main::locale->text('Rename Documents'),
532 'import_title' => $main::locale->text('Add Document from \'#1\'', $scanner_or_mailrx->{name}),
533 'path' => $scanner_or_mailrx->{directory},
534 'done_text' => $main::locale->text('unimported')
536 push @sources , $other;
540 elsif ( $self->file_type eq 'attachment' ) {
542 'name' => 'uploaded',
543 'title' => $main::locale->text(''),
544 'chk_action' => 'attachments_delete',
545 'chk_title' => $main::locale->text('Delete Attachments'),
546 'chkall_title' => $main::locale->text('Delete all'),
547 'file_title' => $main::locale->text('filename'),
548 'confirm_text' => $main::locale->text('delete'),
550 'are_existing' => $self->existing ? 1 : 0,
551 'rename_title' => $main::locale->text('Rename Attachments'),
554 'upload_title' => $main::locale->text('Upload Attachments'),
555 'done_text' => $main::locale->text('deleted')
557 push @sources , $attdata;
559 elsif ( $self->file_type eq 'image' ) {
561 'name' => 'uploaded',
562 'title' => $main::locale->text(''),
563 'chk_action' => 'images_delete',
564 'chk_title' => $main::locale->text('Delete Images'),
565 'chkall_title' => $main::locale->text('Delete all'),
566 'file_title' => $main::locale->text('filename'),
567 'confirm_text' => $main::locale->text('delete'),
569 'are_existing' => $self->existing ? 1 : 0,
570 'rename_title' => $main::locale->text('Rename Images'),
573 'upload_title' => $main::locale->text('Upload Images'),
574 'done_text' => $main::locale->text('deleted')
576 push @sources , $attdata;
591 SL::Controller::File - Controller for managing files
595 The Controller is called directly from the webpages
597 <a href="controller.pl?action=File/list&file_type=document\
598 &object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">
601 or indirectly via javascript functions from js/kivi.File.js
603 kivi.popup_dialog({ url: 'controller.pl',
604 data: { action : 'File/ajax_upload',
605 file_type : 'uploaded',
613 This is a controller for handling files in a storage independent way.
614 The storage may be a Filesystem,a WebDAV, a Database or DMS.
615 These backends must be configered in ClientConfig.
616 This Controller use as intermediate layer for storage C<SL::File>.
618 The Controller is responsible to display forms for displaying the files at the ERP-objects and
619 for uploading and downloading the files.
621 More description of the intermediate layer see L<SL::File>.
625 =head2 C<action_list>
627 This loads a list of files on a webpage. This can be done with a normal submit or via an ajax/json call.
628 Dependent of file_type different sources are available.
630 For documents there are the 'created' source and the imports from scanners or email.
631 For attachments and images only the 'uploaded' source available.
633 Available C<FORM PARAMS>:
637 =item C<form.object_id>
639 The Id of the ERP-object.
641 =item C<form.object_type>
643 The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
645 =item C<form.file_type>
647 For one ERP-object may exists different type of documents the type may be "documents","attachments" or "images".
648 This file_type is a filter for the list.
652 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.
657 =head2 C<action_ajax_upload>
660 A new file or more files can selected by a dialog and insert into the system.
663 Available C<FORM PARAMS>:
667 =item C<form.file_type>
669 This parameter describe here the source for a new file :
670 "attachments" and "images"
672 This is a normal upload selection, which may be more then one file to upload.
674 =item C<form.object_id>
678 =item C<form.object_type>
680 are the same as at C<action_list>
684 =head2 C<action_ajax_files_uploaded>
686 The Upload of selected Files. The "multipart_formdata" is parsed in SL::Request into the formsvariable "form.ATTACHMENTS".
687 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?).
688 If the same filename still exists at this object after the download for each existing filename a rename dialog will be opened.
690 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.
692 Available C<FORM PARAMS>:
696 =item C<form.ATTACHMENTS.uploadfiles>
698 This is an array of elements which have {filename} for the name and {data} for the contents.
700 Also object_id, object_type and file_type
704 =head2 C<action_download>
706 This is the real download of a file normally called via javascript "$.download("controller.pl", data);"
708 Available C<FORM PARAMS>:
712 Also object_id, object_type and file_type
716 =head2 C<action_ajax_importdialog>
718 A Dialog with all available and not imported files to import is open.
719 More then one file can be selected.
721 Available C<FORM PARAMS>:
727 The name of the source like "scanner1" or "email"
731 The full path to the directory on the server, where the files to import can found
733 Also object_id, object_type and file_type
737 =head2 C<action_ajax_delete>
739 Some files can be deleted
741 Available C<FORM PARAMS>:
747 The ids of the files to delete. Only this files are deleted not all versions of a file if the exists
751 =head2 C<action_ajax_unimport>
753 Some files can be unimported, dependent of the source of the file. This means they are moved
754 back to the directory of the source
756 Available C<FORM PARAMS>:
762 The ids of the files to unimport. Only these files are unimported not all versions of a file if the exists
766 =head2 C<action_ajax_rename>
768 One file can be renamed. There can be some checks if the same filename still exists at one object.
772 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>