From 09f93f2b9d76111f733477101cf24877046a5835 Mon Sep 17 00:00:00 2001 From: nathan Date: Thu, 14 Oct 2021 09:18:55 -0600 Subject: [PATCH] * Add filemanager actions to convert editable files to PDF or PNG and a checkbox to merge file as PDF --- api/js/jsapi/egw_app.js | 41 +++++++++ api/js/jsapi/egw_app.ts | 51 +++++++++++- api/src/Storage/Merge.php | 170 ++++++++++++++++++++------------------ api/src/Vfs.php | 22 +++++ 4 files changed, 201 insertions(+), 83 deletions(-) diff --git a/api/js/jsapi/egw_app.js b/api/js/jsapi/egw_app.js index 82f0e9d96a..7b50a21e6d 100644 --- a/api/js/jsapi/egw_app.js +++ b/api/js/jsapi/egw_app.js @@ -10,6 +10,17 @@ * @author Hadi Nategh * @author Nathan Gray */ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.EgwApp = void 0; require("jquery"); @@ -562,6 +573,36 @@ var EgwApp = /** @class */ (function () { // in order to be able to destroy view on action this.et2_view.close = destroy; }; + /** + * Merge selected entries into template document + * + * @param {egwAction} _action + * @param {egwActionObject[]} _selected + */ + EgwApp.prototype.merge = function (_action, _selected) { + var _a; + // Find what we need + var nm = null; + var action = _action; + var as_pdf = false; + // Find Select all + while (nm == null && action != null) { + if (action.data != null && action.data.nextmatch) { + nm = action.data.nextmatch; + } + action = action.parent; + } + var all = (nm === null || nm === void 0 ? void 0 : nm.getSelection().all) || false; + as_pdf = ((_a = action.getActionById('as_pdf')) === null || _a === void 0 ? void 0 : _a.checked) || false; + // Get list of entry IDs + var ids = []; + for (var i = 0; !all && i < _selected.length; i++) { + var split = _selected[i].id.split("::"); + ids.push(split[1]); + } + var vars = __assign(__assign({}, _action.data.merge_data), { pdf: as_pdf, select_all: all, id: JSON.stringify(ids) }); + egw.open_link(egw.link('/index.php', vars), '_blank'); + }; /** * Initializes actions and handlers on sidebox (delete) * diff --git a/api/js/jsapi/egw_app.ts b/api/js/jsapi/egw_app.ts index 818798cd21..c5009a1c84 100644 --- a/api/js/jsapi/egw_app.ts +++ b/api/js/jsapi/egw_app.ts @@ -726,7 +726,7 @@ export abstract class EgwApp framework.pushState('view'); if(templateName) { - this.et2_view.load(this.appname+'.'+templateName,templateURL, data, typeof et2_callback == 'function'?et2_callback:function(){}, app); + this.et2_view.load(this.appname + '.' + templateName, templateURL, data, typeof et2_callback == 'function' ? et2_callback : function() {}, app); } // define a global close function for view template @@ -734,6 +734,49 @@ export abstract class EgwApp this.et2_view.close = destroy; } + /** + * Merge selected entries into template document + * + * @param {egwAction} _action + * @param {egwActionObject[]} _selected + */ + merge(_action : egwAction, _selected : egwActionObject[]) + { + // Find what we need + let nm = null; + let action = _action; + let as_pdf = false; + + // Find Select all + while(nm == null && action != null) + { + if(action.data != null && action.data.nextmatch) + { + nm = action.data.nextmatch; + } + action = action.parent; + } + let all = nm?.getSelection().all || false; + + as_pdf = action.getActionById('as_pdf')?.checked || false; + + // Get list of entry IDs + let ids = []; + for(let i = 0; !all && i < _selected.length; i++) + { + let split = _selected[i].id.split("::"); + ids.push(split[1]); + } + + let vars = { + ..._action.data.merge_data, + pdf: as_pdf, + select_all: all, + id: JSON.stringify(ids) + }; + egw.open_link(egw.link('/index.php', vars), '_blank'); + } + /** * Initializes actions and handlers on sidebox (delete) * @@ -742,12 +785,12 @@ export abstract class EgwApp _init_sidebox(sidebox) { // Initialize egw tutorial sidebox, but only for non-popups, as calendar edit app.js has this.et2 set to tutorial et2 object - if (!this.egw.is_popup()) + if(!this.egw.is_popup()) { var egw_fw = egw_getFramework(); - var tutorial = jQuery('#egw_tutorial_'+this.appname+'_sidebox', egw_fw ? egw_fw.sidemenuDiv : document); + var tutorial = jQuery('#egw_tutorial_' + this.appname + '_sidebox', egw_fw ? egw_fw.sidemenuDiv : document); // _init_sidebox gets currently called multiple times, which needs to be fixed - if (tutorial.length && !this.tutorial_initialised) + if(tutorial.length && !this.tutorial_initialised) { this.egwTutorial_init(tutorial[0]); this.tutorial_initialised = true; diff --git a/api/src/Storage/Merge.php b/api/src/Storage/Merge.php index 71755b5676..df50c6f6ef 100644 --- a/api/src/Storage/Merge.php +++ b/api/src/Storage/Merge.php @@ -16,6 +16,7 @@ namespace EGroupware\Api\Storage; use DOMDocument; use EGroupware\Api; use EGroupware\Api\Vfs; +use EGroupware\Collabora\Conversion; use EGroupware\Stylite; use tidy; use uiaccountsel; @@ -2061,7 +2062,6 @@ abstract class Merge $export_limit=null) { $documents = array(); - $editable_mimes = array(); if ($export_limit == null) $export_limit = self::getExportLimit(); // check if there is a globalsetting try { @@ -2146,13 +2146,6 @@ abstract class Merge } foreach($files as $file) { - $edit_attributes = array( - 'menuaction' => $GLOBALS['egw_info']['flags']['currentapp'].'.'.get_called_class().'.merge_entries', - 'document' => $file['path'], - 'merge' => get_called_class(), - 'id' => '$id', - 'select_all' => '$select_all' - ); if (count($dircount) > 1) { $name_arr = explode('/', $file['name']); @@ -2169,30 +2162,24 @@ abstract class Merge } switch($count) { - case (count($name_arr)-1): - $current_level[$prefix.$file['name']] = array( - 'icon' => Api\Vfs::mime_icon($file['mime']), - 'caption' => Api\Vfs::decodePath($name_arr[$count]), - 'group' => 2, - 'postSubmit' => true, // download needs post submit (not Ajax) to work, - 'target' => '_blank', - 'url' => urldecode(http_build_query($edit_attributes)) - ); - if ($file['mime'] == 'message/rfc822') + case (count($name_arr) - 1): + $current_level[$prefix . $file['name']]; + self::document_editable_action($current_level[$prefix . $file['name']], $file); + if($file['mime'] == 'message/rfc822') { - self::document_mail_action($current_level[$prefix.$file['name']], $file); + self::document_mail_action($current_level[$prefix . $file['name']], $file); } break; default: - if(!is_array($current_level[$prefix.$name_arr[$count]])) + if(!is_array($current_level[$prefix . $name_arr[$count]])) { // create parent folder - $current_level[$prefix.$name_arr[$count]] = array( - 'icon' => 'phpgwapi/foldertree_folder', - 'caption' => Api\Vfs::decodePath($name_arr[$count]), - 'group' => 2, - 'children' => array(), + $current_level[$prefix . $name_arr[$count]] = array( + 'icon' => 'phpgwapi/foldertree_folder', + 'caption' => Api\Vfs::decodePath($name_arr[$count]), + 'group' => 2, + 'children' => array(), ); } break; @@ -2201,50 +2188,47 @@ abstract class Merge } else if (count($files) >= self::SHOW_DOCS_BY_MIME_LIMIT) { - if (!isset($documents[$file['mime']])) + if(!isset($documents[$file['mime']])) { $documents[$file['mime']] = array( - 'icon' => Api\Vfs::mime_icon($file['mime']), - 'caption' => Api\MimeMagic::mime2label($file['mime']), - 'group' => 2, + 'icon' => Api\Vfs::mime_icon($file['mime']), + 'caption' => Api\MimeMagic::mime2label($file['mime']), + 'group' => 2, 'children' => array(), ); } - $documents[$file['mime']]['children'][$prefix.$file['name']] = array( - 'caption' => Api\Vfs::decodePath($file['name']), - 'target' => '_blank', - 'postSubmit' => true, // download needs post submit (not Ajax) to work - ); - $documents[$file['mime']]['children'][$prefix.$file['name']]['url'] = urldecode(http_build_query($edit_attributes)); - if ($file['mime'] == 'message/rfc822') + $documents[$file['mime']]['children'][$prefix . $file['name']] = array(); + self::document_editable_action($documents[$file['mime']]['children'][$prefix . $file['name']], $file); + if($file['mime'] == 'message/rfc822') { - self::document_mail_action($documents[$file['mime']]['children'][$prefix.$file['name']], $file); + self::document_mail_action($documents[$file['mime']]['children'][$prefix . $file['name']], $file); } } else { - $documents[$prefix.$file['name']] = array( - 'icon' => Api\Vfs::mime_icon($file['mime']), - 'caption' => Api\Vfs::decodePath($file['name']), - 'group' => 2, - 'target' => '_blank' - ); - $documents[$prefix.$file['name']]['url'] = urldecode(http_build_query($edit_attributes)); - if ($file['mime'] == 'message/rfc822') + $documents[$prefix . $file['name']] = array(); + self::document_editable_action($documents[$prefix . $file['name']], $file); + if($file['mime'] == 'message/rfc822') { - self::document_mail_action($documents[$prefix.$file['name']], $file); + self::document_mail_action($documents[$prefix . $file['name']], $file); } } } + // Add PDF checkbox + $documents['as_pdf'] = array( + 'caption' => 'As PDF', + 'checkbox' => true, + ); return array( - 'icon' => 'etemplate/merge', - 'caption' => $caption, - 'children' => $documents, + 'icon' => 'etemplate/merge', + 'caption' => $caption, + 'children' => $documents, // disable action if no document or export completly forbidden for non-admins - 'enabled' => (boolean)$documents && (self::hasExportLimit($export_limit,'ISALLOWED') || self::is_export_limit_excepted()), - 'hideOnDisabled' => true, // do not show 'Insert in document', if no documents defined or no export allowed - 'group' => $group, + 'enabled' => (boolean)$documents && (self::hasExportLimit($export_limit, 'ISALLOWED') || self::is_export_limit_excepted()), + 'hideOnDisabled' => true, + // do not show 'Insert in document', if no documents defined or no export allowed + 'group' => $group, ); } @@ -2294,15 +2278,24 @@ abstract class Merge */ private static function document_editable_action(Array &$action, $file) { - unset($action['postSubmit']); - $edit_attributes = array( - 'menuaction' => 'collabora.EGroupware\\collabora\\Ui.merge_edit', - 'document' => $file['path'], - 'merge' => get_called_class(), - 'id' => '$id', - 'select_all' => '$select_all' + static $action_base = array( + // The same for every file + 'group' => 2, + // Overwritten for every file + 'icon' => '', //Api\Vfs::mime_icon($file['mime']), + 'caption' => '', //Api\Vfs::decodePath($name_arr[$count]), ); - $action['url'] = urldecode(http_build_query($edit_attributes)); + $edit_attributes = array( + 'menuaction' => $GLOBALS['egw_info']['flags']['currentapp'] . '.' . get_called_class() . '.merge_entries', + 'document' => $file['path'], + 'merge' => get_called_class(), + ); + $action = array_merge($action_base, $action, array( + 'icon' => Api\Vfs::mime_icon($file['mime']), + 'caption' => Api\Vfs::decodePath($file['name']), + 'onExecute' => 'javaScript:app.' . $GLOBALS['egw_info']['flags']['currentapp'] . '.merge', + 'merge_data' => $edit_attributes + )); } /** @@ -2343,25 +2336,26 @@ abstract class Merge * Merge the selected IDs into the given document, save it to the VFS, then * either open it in the editor or have the browser download the file. * - * @param String[]|null $ids Allows extending classes to process IDs in their own way. Leave null to pull from request. + * @param string[]|null $ids Allows extending classes to process IDs in their own way. Leave null to pull from request. * @param Merge|null $document_merge Already instantiated Merge object to do the merge. + * @param boolean|null $pdf Convert result to PDF * @throws Api\Exception * @throws Api\Exception\AssertionFailed */ - public static function merge_entries(array $ids = null, Merge &$document_merge = null) + public static function merge_entries(array $ids = null, Merge &$document_merge = null, $pdf = null) { - if (is_null($document_merge) && class_exists($_REQUEST['merge']) && is_subclass_of($_REQUEST['merge'], 'EGroupware\\Api\\Storage\\Merge')) + if(is_null($document_merge) && class_exists($_REQUEST['merge']) && is_subclass_of($_REQUEST['merge'], 'EGroupware\\Api\\Storage\\Merge')) { $document_merge = new $_REQUEST['merge'](); } - elseif (is_null($document_merge)) + elseif(is_null($document_merge)) { $document_merge = new Api\Contacts\Merge(); } if(($error = $document_merge->check_document($_REQUEST['document'],''))) { - $response->error($error); + error_log(__METHOD__ . "({$_REQUEST['document']}) $error"); return; } @@ -2374,29 +2368,26 @@ abstract class Merge $ids = self::get_all_ids($document_merge); } + if(is_null($pdf)) + { + $pdf = (boolean)$_REQUEST['pdf']; + } + $filename = $document_merge->get_filename($_REQUEST['document']); $result = $document_merge->merge_file($_REQUEST['document'], $ids, $filename, '', $header); if(!is_file($result) || !is_readable($result)) { - throw new Api\Exception\AssertionFailed("Unable to generate merge file\n". $result); + throw new Api\Exception\AssertionFailed("Unable to generate merge file\n" . $result); } // Put it into the vfs using user's configured home dir if writable, // or expected home dir (/home/username) if not $target = $_target = (Vfs::is_writable(Vfs::get_home_dir()) ? Vfs::get_home_dir() : "/home/{$GLOBALS['egw_info']['user']['account_lid']}" - )."/$filename"; - $dupe_count = 0; - while(is_file(Vfs::PREFIX.$target)) - { - $dupe_count++; - $target = Vfs::dirname($_target) . '/' . - pathinfo($filename, PATHINFO_FILENAME) . - ' ('.($dupe_count + 1).')' . '.' . - pathinfo($filename, PATHINFO_EXTENSION); - } - copy($result, Vfs::PREFIX.$target); + ) . "/$filename"; + $target = Vfs::make_unique($target); + copy($result, Vfs::PREFIX . $target); unlink($result); // Find out what to do with it @@ -2416,11 +2407,32 @@ abstract class Merge // ignore failed discovery unset($e); } - if($editable_mimes[Vfs::mime_content_type($target)]) + + // PDF conversion + if($editable_mimes[Vfs::mime_content_type($target)] && $pdf) + { + $error = ''; + $converted_path = ''; + $convert = new Conversion(); + $convert->convert($target, $converted_path, 'pdf', $error); + + if($error) + { + error_log(__METHOD__ . "({$_REQUEST['document']}) $target => $converted_path Error in PDF conversion: $error"); + } + else + { + // Remove original + Vfs::unlink($target); + $target = $converted_path; + } + } + if($editable_mimes[Vfs::mime_content_type($target)] && + !in_array(Vfs::mime_content_type($target), explode(',', $GLOBALS['egw_info']['user']['preferences']['filemanager']['collab_excluded_mimes']))) { \Egroupware\Api\Egw::redirect_link('/index.php', array( 'menuaction' => 'collabora.EGroupware\\Collabora\\Ui.editor', - 'path'=> $target + 'path' => $target )); } else diff --git a/api/src/Vfs.php b/api/src/Vfs.php index 44a29de9c0..e2e7adcdb4 100644 --- a/api/src/Vfs.php +++ b/api/src/Vfs.php @@ -2386,6 +2386,28 @@ class Vfs extends Vfs\Base } return self::_call_on_backend('get_minimum_file_id', array($path)); } + + /** + * Make sure the path is unique, by appending (#) to the filename if it already exists + * + * @param string $path + * + * @return string The same path, but modified if it exists + */ + static function make_unique($path) + { + $filename = Vfs::basename($path); + $dupe_count = 0; + while(is_file(Vfs::PREFIX . $path)) + { + $dupe_count++; + $path = Vfs::dirname($path) . '/' . + pathinfo($filename, PATHINFO_FILENAME) . + ' (' . ($dupe_count + 1) . ')' . '.' . + pathinfo($filename, PATHINFO_EXTENSION); + } + return $path; + } } Vfs::init_static();