From 7faaaf585e249dee884a2a0f8ca9ec0a82b2f148 Mon Sep 17 00:00:00 2001 From: Georgii Petrov Date: Wed, 10 Jan 2024 17:16:28 +0300 Subject: [PATCH 1/4] [feature] WOPI created zero-weight file replacement with template --- Common/sources/constants.js | 30 ++++++++++++++++++++++++++ Common/sources/utils.js | 12 +++++++++++ DocService/sources/wopiClient.js | 36 ++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/Common/sources/constants.js b/Common/sources/constants.js index 36471cad7..29ae60265 100644 --- a/Common/sources/constants.js +++ b/Common/sources/constants.js @@ -281,3 +281,33 @@ exports.FILE_STATUS_UPDATE_VERSION = 'updateversion'; exports.ACTIVEMQ_QUEUE_PREFIX = 'queue://'; exports.ACTIVEMQ_TOPIC_PREFIX = 'topic://'; + +exports.LOCALE_MAP = { + 'az': 'az-Latn-AZ', + 'bg': 'bg-BG', + 'cs': 'cs-CZ', + 'de': 'de-DE', + 'el': 'el-GR', + 'en': 'en-US', // collision + 'es': 'es-ES', + 'eu': 'eu-ES', + 'fr': 'fr-FR', + 'gl': 'gl-ES', + 'hy': 'hy-AM', + 'it': 'it-IT', + 'ja': 'ja-JP', + 'ko': 'ko-KR', + 'lv': 'lv-LV', + 'ms': 'ms-MY', + 'nl': 'nl-NL', + 'pl': 'pl-PL', + 'pt': 'pt-BR', // collision + 'ru': 'ru-RU', + 'si': 'si-LK', + 'sk': 'sk-SK', + 'sv': 'sv-SE', + 'tr': 'tr-TR', + 'uk': 'uk-UA', + 'vi': 'vi-VN', + 'zh': 'zh-CH' // collision +}; diff --git a/Common/sources/utils.js b/Common/sources/utils.js index 3b02a5f9f..0899e8893 100644 --- a/Common/sources/utils.js +++ b/Common/sources/utils.js @@ -1111,3 +1111,15 @@ exports.checksumFile = function(hashName, path) { stream.on('end', () => resolve(hash.digest('hex'))); }); }; + +exports.getLocaleFullCode = function(unformattedLocale) { + // Expects that full locale string must starts with at last 2 lowercase characters, + // then comes optional part with dash and at last 2 characters in any case + // and ends with at last 2 uppercase characters with dash. + const isAlreadyFullLocale = /^[a-z]{2,}(-[A-Za-z]{2,})?-[A-Z]{2,}$/.test(unformattedLocale); + if (isAlreadyFullLocale) { + return unformattedLocale; + } + + return constants.LOCALE_MAP[unformattedLocale]; +} diff --git a/DocService/sources/wopiClient.js b/DocService/sources/wopiClient.js index 607b2adf8..2d950944c 100644 --- a/DocService/sources/wopiClient.js +++ b/DocService/sources/wopiClient.js @@ -38,6 +38,7 @@ const {URL} = require('url'); const co = require('co'); const jwt = require('jsonwebtoken'); const config = require('config'); +const fs = require('fs'); const utf7 = require('utf7'); const mimeDB = require('mime-db'); const xmlbuilder2 = require('xmlbuilder2'); @@ -45,6 +46,7 @@ const logger = require('./../../Common/sources/logger'); const utils = require('./../../Common/sources/utils'); const constants = require('./../../Common/sources/constants'); const commonDefines = require('./../../Common/sources/commondefines'); +const formatChecker = require('./../../Common/sources/formatchecker'); const operationContext = require('./../../Common/sources/operationContext'); const tenantManager = require('./../../Common/sources/tenantManager'); const sqlBase = require('./databaseConnectors/baseConnector'); @@ -57,6 +59,7 @@ const cfgTokenOutboxExpires = config.get('services.CoAuthoring.token.outbox.expi const cfgTokenEnableBrowser = config.get('services.CoAuthoring.token.enable.browser'); const cfgCallbackRequestTimeout = config.get('services.CoAuthoring.server.callbackRequestTimeout'); const cfgAllowPrivateIPAddressForSignedRequests = config.get('services.CoAuthoring.server.allowPrivateIPAddressForSignedRequests'); +const cfgNewFileTemplate = config.get('services.CoAuthoring.server.newFileTemplate'); const cfgDownloadTimeout = config.get('FileConverter.converter.downloadTimeout'); const cfgWopiFileInfoBlockList = config.get('wopi.fileInfoBlockList'); const cfgWopiWopiZone = config.get('wopi.wopiZone'); @@ -440,6 +443,39 @@ function getEditorHtml(req, res) { yield canvasService.commandOpenStartPromise(ctx, docId, utils.getBaseUrlByRequest(ctx, req), commonInfo, fileType); } + const fileFormat = (extension) => { + const format = formatChecker.getFormatFromString(extension); + if (formatChecker.isDocumentFormat(format)) { + if (format === constants.AVS_OFFICESTUDIO_FILE_DOCUMENT_DOCXF) { + return 'docxf'; + } + + return 'docx'; + } + if (formatChecker.isSpreadsheetFormat(format)) { + return 'xlsx'; + } + if (formatChecker.isPresentationFormat(format)) { + return 'pptx'; + } + + return ''; + }; + const fileType = getFileTypeByInfo(fileInfo); + const format = fileFormat(fileType); + + // TODO: throw error if format not supported? + if (fileInfo.Size === 0 && format.length !== 0) { + const wopiParams = getWopiParams('', fileInfo, wopiSrc, access_token, access_token_ttl); + const locale = utils.getLocaleFullCode(params.queryParams.lang || params.queryParams.ui || 'en-US'); + const defaultFilePath = `${cfgNewFileTemplate}/en-US/new.${format}`; + const expectedPath = `${cfgNewFileTemplate}/${locale}/new.${format}`; + const filePath = fs.existsSync(expectedPath) ? expectedPath : defaultFilePath; + const templateFileStream = fs.createReadStream(filePath); + const templateFileSize = fs.lstatSync(filePath).size; + yield putFile(ctx, wopiParams, undefined, templateFileStream, templateFileSize, fileInfo.UserId, false, false, false); + } + //Lock if ('view' !== mode) { let lockRes = yield lock(ctx, 'LOCK', lockId, fileInfo, userAuth); From d77b59591bde7a66696967e4e7ac283c52756e83 Mon Sep 17 00:00:00 2001 From: Georgii Petrov Date: Tue, 16 Jan 2024 09:57:46 +0300 Subject: [PATCH 2/4] [feature] WOPI locale fixes --- Common/sources/constants.js | 34 +++++----------------------- Common/sources/utils.js | 12 ---------- DocService/sources/wopiClient.js | 38 ++++++++++++++++++++++++++------ 3 files changed, 37 insertions(+), 47 deletions(-) diff --git a/Common/sources/constants.js b/Common/sources/constants.js index 29ae60265..14f5b9f99 100644 --- a/Common/sources/constants.js +++ b/Common/sources/constants.js @@ -282,32 +282,10 @@ exports.FILE_STATUS_UPDATE_VERSION = 'updateversion'; exports.ACTIVEMQ_QUEUE_PREFIX = 'queue://'; exports.ACTIVEMQ_TOPIC_PREFIX = 'topic://'; -exports.LOCALE_MAP = { - 'az': 'az-Latn-AZ', - 'bg': 'bg-BG', - 'cs': 'cs-CZ', - 'de': 'de-DE', - 'el': 'el-GR', - 'en': 'en-US', // collision - 'es': 'es-ES', - 'eu': 'eu-ES', - 'fr': 'fr-FR', - 'gl': 'gl-ES', - 'hy': 'hy-AM', - 'it': 'it-IT', - 'ja': 'ja-JP', - 'ko': 'ko-KR', - 'lv': 'lv-LV', - 'ms': 'ms-MY', - 'nl': 'nl-NL', - 'pl': 'pl-PL', - 'pt': 'pt-BR', // collision - 'ru': 'ru-RU', - 'si': 'si-LK', - 'sk': 'sk-SK', - 'sv': 'sv-SE', - 'tr': 'tr-TR', - 'uk': 'uk-UA', - 'vi': 'vi-VN', - 'zh': 'zh-CH' // collision +exports.TEMPLATES_FOLDER_LOCALE_COLLISON_MAP = { + 'en': 'en-US', + 'pt': 'pt-BR', + 'zh': 'zh-CH', + 'pt-PT': 'pt-PT', + 'zh-TW': 'zh-TW' }; diff --git a/Common/sources/utils.js b/Common/sources/utils.js index 0899e8893..3b02a5f9f 100644 --- a/Common/sources/utils.js +++ b/Common/sources/utils.js @@ -1111,15 +1111,3 @@ exports.checksumFile = function(hashName, path) { stream.on('end', () => resolve(hash.digest('hex'))); }); }; - -exports.getLocaleFullCode = function(unformattedLocale) { - // Expects that full locale string must starts with at last 2 lowercase characters, - // then comes optional part with dash and at last 2 characters in any case - // and ends with at last 2 uppercase characters with dash. - const isAlreadyFullLocale = /^[a-z]{2,}(-[A-Za-z]{2,})?-[A-Z]{2,}$/.test(unformattedLocale); - if (isAlreadyFullLocale) { - return unformattedLocale; - } - - return constants.LOCALE_MAP[unformattedLocale]; -} diff --git a/DocService/sources/wopiClient.js b/DocService/sources/wopiClient.js index 2d950944c..d06e50a94 100644 --- a/DocService/sources/wopiClient.js +++ b/DocService/sources/wopiClient.js @@ -38,7 +38,8 @@ const {URL} = require('url'); const co = require('co'); const jwt = require('jsonwebtoken'); const config = require('config'); -const fs = require('fs'); +const { createReadStream } = require('fs'); +const { lstat, readdir } = require('fs/promises'); const utf7 = require('utf7'); const mimeDB = require('mime-db'); const xmlbuilder2 = require('xmlbuilder2'); @@ -83,6 +84,8 @@ const cfgWopiExponentOld = config.get('wopi.exponentOld'); const cfgWopiPrivateKeyOld = config.get('wopi.privateKeyOld'); const cfgWopiHost = config.get('wopi.host'); +let templatesFolderLocalesCache = null; + let mimeTypesByExt = (function() { let mimeTypesByExt = {}; for (let mimeType in mimeDB) { @@ -467,13 +470,34 @@ function getEditorHtml(req, res) { // TODO: throw error if format not supported? if (fileInfo.Size === 0 && format.length !== 0) { const wopiParams = getWopiParams('', fileInfo, wopiSrc, access_token, access_token_ttl); - const locale = utils.getLocaleFullCode(params.queryParams.lang || params.queryParams.ui || 'en-US'); - const defaultFilePath = `${cfgNewFileTemplate}/en-US/new.${format}`; + + if (templatesFolderLocalesCache === null) { + const dirContent = yield readdir(`${cfgNewFileTemplate}/`, { withFileTypes: true }); + templatesFolderLocalesCache = dirContent.filter(dirObject => dirObject.isDirectory()).map(dirObject => dirObject.name); + } + + const localePrefix = params.queryParams.lang || params.queryParams.ui || 'en'; + let locale = constants.TEMPLATES_FOLDER_LOCALE_COLLISON_MAP[localePrefix] ?? templatesFolderLocalesCache.find(locale => locale.startsWith(localePrefix)); + if (locale === undefined) { + locale = 'en-US'; + } + + let templateFileInfo; + let filePath = `${cfgNewFileTemplate}/en-US/new.${format}`; const expectedPath = `${cfgNewFileTemplate}/${locale}/new.${format}`; - const filePath = fs.existsSync(expectedPath) ? expectedPath : defaultFilePath; - const templateFileStream = fs.createReadStream(filePath); - const templateFileSize = fs.lstatSync(filePath).size; - yield putFile(ctx, wopiParams, undefined, templateFileStream, templateFileSize, fileInfo.UserId, false, false, false); + try { + templateFileInfo = yield lstat(expectedPath); + filePath = expectedPath; + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + + templateFileInfo = yield lstat(filePath); + } + + const templateFileStream = createReadStream(filePath); + yield putFile(ctx, wopiParams, undefined, templateFileStream, templateFileInfo.size, fileInfo.UserId, false, false, false); } //Lock From a58c86da6aadcaeb4b226a33788d02726b7f6147 Mon Sep 17 00:00:00 2001 From: Georgii Petrov Date: Thu, 18 Jan 2024 18:32:38 +0300 Subject: [PATCH 3/4] [feature] WOPI discovery editnew template extensions filter --- Common/sources/constants.js | 5 +++ DocService/sources/wopiClient.js | 56 +++++++++----------------------- 2 files changed, 21 insertions(+), 40 deletions(-) diff --git a/Common/sources/constants.js b/Common/sources/constants.js index 14f5b9f99..4fe783573 100644 --- a/Common/sources/constants.js +++ b/Common/sources/constants.js @@ -289,3 +289,8 @@ exports.TEMPLATES_FOLDER_LOCALE_COLLISON_MAP = { 'pt-PT': 'pt-PT', 'zh-TW': 'zh-TW' }; +exports.SUPPORTED_TEMPLATES_EXTENSIONS = { + 'Word': ['docx', 'docxf'], + 'Excel': ['xlsx'], + 'PowerPoint': ['pptx'] +}; \ No newline at end of file diff --git a/DocService/sources/wopiClient.js b/DocService/sources/wopiClient.js index d06e50a94..6dbcff86c 100644 --- a/DocService/sources/wopiClient.js +++ b/DocService/sources/wopiClient.js @@ -85,6 +85,7 @@ const cfgWopiPrivateKeyOld = config.get('wopi.privateKeyOld'); const cfgWopiHost = config.get('wopi.host'); let templatesFolderLocalesCache = null; +const templateFilesSizeCache = {}; let mimeTypesByExt = (function() { let mimeTypesByExt = {}; @@ -175,13 +176,16 @@ function discovery(req, res) { xmlApp.ele('action', {name: 'view', ext: ext.edit[j], urlsrc: urlTemplateView}).up(); xmlApp.ele('action', {name: 'embedview', ext: ext.edit[j], urlsrc: urlTemplateEmbedView}).up(); xmlApp.ele('action', {name: 'mobileView', ext: ext.edit[j], urlsrc: urlTemplateMobileView}).up(); - if ("oform" !== ext.edit[j]) { - //todo config - xmlApp.ele('action', {name: 'editnew', ext: ext.edit[j], requires: 'locks,update', urlsrc: urlTemplateEdit}).up(); - } + // if ("oform" !== ext.edit[j]) { + // //todo config + // xmlApp.ele('action', {name: 'editnew', ext: ext.edit[j], requires: 'locks,update', urlsrc: urlTemplateEdit}).up(); + // } xmlApp.ele('action', {name: 'edit', ext: ext.edit[j], default: 'true', requires: 'locks,update', urlsrc: urlTemplateEdit}).up(); xmlApp.ele('action', {name: 'mobileEdit', ext: ext.edit[j], requires: 'locks,update', urlsrc: urlTemplateMobileEdit}).up(); } + constants.SUPPORTED_TEMPLATES_EXTENSIONS[name].forEach( + extension => xmlApp.ele('action', {name: 'editnew', ext: extension, requires: 'locks,update', urlsrc: urlTemplateEdit}).up() + ); xmlApp.up(); } //end section for MS WOPI connectors @@ -379,6 +383,7 @@ function getEditorHtml(req, res) { try { ctx.initFromRequest(req); yield ctx.initTenantCache(); + const tenNewFileTemplate = ctx.getCfg('services.CoAuthoring.server.newFileTemplate', cfgNewFileTemplate); const tenTokenEnableBrowser = ctx.getCfg('services.CoAuthoring.token.enable.browser', cfgTokenEnableBrowser); const tenTokenOutboxAlgorithm = ctx.getCfg('services.CoAuthoring.token.outbox.algorithm', cfgTokenOutboxAlgorithm); const tenTokenOutboxExpires = ctx.getCfg('services.CoAuthoring.token.outbox.expires', cfgTokenOutboxExpires); @@ -439,40 +444,19 @@ function getEditorHtml(req, res) { return; } //save common info + const fileType = getFileTypeByInfo(fileInfo); if (undefined === lockId) { - let fileType = getFileTypeByInfo(fileInfo); lockId = crypto.randomBytes(16).toString('base64'); let commonInfo = JSON.stringify({lockId: lockId, fileInfo: fileInfo}); yield canvasService.commandOpenStartPromise(ctx, docId, utils.getBaseUrlByRequest(ctx, req), commonInfo, fileType); } - const fileFormat = (extension) => { - const format = formatChecker.getFormatFromString(extension); - if (formatChecker.isDocumentFormat(format)) { - if (format === constants.AVS_OFFICESTUDIO_FILE_DOCUMENT_DOCXF) { - return 'docxf'; - } - - return 'docx'; - } - if (formatChecker.isSpreadsheetFormat(format)) { - return 'xlsx'; - } - if (formatChecker.isPresentationFormat(format)) { - return 'pptx'; - } - - return ''; - }; - const fileType = getFileTypeByInfo(fileInfo); - const format = fileFormat(fileType); - // TODO: throw error if format not supported? - if (fileInfo.Size === 0 && format.length !== 0) { + if (fileInfo.Size === 0 && fileType.length !== 0) { const wopiParams = getWopiParams('', fileInfo, wopiSrc, access_token, access_token_ttl); if (templatesFolderLocalesCache === null) { - const dirContent = yield readdir(`${cfgNewFileTemplate}/`, { withFileTypes: true }); + const dirContent = yield readdir(`${tenNewFileTemplate}/`, { withFileTypes: true }); templatesFolderLocalesCache = dirContent.filter(dirObject => dirObject.isDirectory()).map(dirObject => dirObject.name); } @@ -482,20 +466,12 @@ function getEditorHtml(req, res) { locale = 'en-US'; } - let templateFileInfo; - let filePath = `${cfgNewFileTemplate}/en-US/new.${format}`; - const expectedPath = `${cfgNewFileTemplate}/${locale}/new.${format}`; - try { - templateFileInfo = yield lstat(expectedPath); - filePath = expectedPath; - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - - templateFileInfo = yield lstat(filePath); + const filePath = `${tenNewFileTemplate}/${locale}/new.${fileType}`; + if (!templateFilesSizeCache[filePath]) { + templateFilesSizeCache[filePath] = yield lstat(filePath); } + const templateFileInfo = templateFilesSizeCache[filePath]; const templateFileStream = createReadStream(filePath); yield putFile(ctx, wopiParams, undefined, templateFileStream, templateFileInfo.size, fileInfo.UserId, false, false, false); } From 28bdee705c99655fe7d9c130115daa279a2a8f63 Mon Sep 17 00:00:00 2001 From: Sergey Konovalov Date: Mon, 22 Jan 2024 19:54:02 +0300 Subject: [PATCH 4/4] [feature] Omit X-WOPI-Lock during document creation. --- DocService/sources/wopiClient.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/DocService/sources/wopiClient.js b/DocService/sources/wopiClient.js index 6dbcff86c..328d06f08 100644 --- a/DocService/sources/wopiClient.js +++ b/DocService/sources/wopiClient.js @@ -176,10 +176,6 @@ function discovery(req, res) { xmlApp.ele('action', {name: 'view', ext: ext.edit[j], urlsrc: urlTemplateView}).up(); xmlApp.ele('action', {name: 'embedview', ext: ext.edit[j], urlsrc: urlTemplateEmbedView}).up(); xmlApp.ele('action', {name: 'mobileView', ext: ext.edit[j], urlsrc: urlTemplateMobileView}).up(); - // if ("oform" !== ext.edit[j]) { - // //todo config - // xmlApp.ele('action', {name: 'editnew', ext: ext.edit[j], requires: 'locks,update', urlsrc: urlTemplateEdit}).up(); - // } xmlApp.ele('action', {name: 'edit', ext: ext.edit[j], default: 'true', requires: 'locks,update', urlsrc: urlTemplateEdit}).up(); xmlApp.ele('action', {name: 'mobileEdit', ext: ext.edit[j], requires: 'locks,update', urlsrc: urlTemplateMobileEdit}).up(); } @@ -401,6 +397,8 @@ function getEditorHtml(req, res) { let mode = req.params.mode; let sc = req.query['sc']; let hostSessionId = req.query['hid']; + let lang = req.query['lang']; + let ui = req.query['ui']; let access_token = req.body['access_token'] || ""; let access_token_ttl = parseInt(req.body['access_token_ttl']) || 0; @@ -453,14 +451,14 @@ function getEditorHtml(req, res) { // TODO: throw error if format not supported? if (fileInfo.Size === 0 && fileType.length !== 0) { - const wopiParams = getWopiParams('', fileInfo, wopiSrc, access_token, access_token_ttl); + const wopiParams = getWopiParams(undefined, fileInfo, wopiSrc, access_token, access_token_ttl); if (templatesFolderLocalesCache === null) { const dirContent = yield readdir(`${tenNewFileTemplate}/`, { withFileTypes: true }); templatesFolderLocalesCache = dirContent.filter(dirObject => dirObject.isDirectory()).map(dirObject => dirObject.name); } - const localePrefix = params.queryParams.lang || params.queryParams.ui || 'en'; + const localePrefix = lang || ui || 'en'; let locale = constants.TEMPLATES_FOLDER_LOCALE_COLLISON_MAP[localePrefix] ?? templatesFolderLocalesCache.find(locale => locale.startsWith(localePrefix)); if (locale === undefined) { locale = 'en-US'; @@ -542,7 +540,7 @@ function getConverterHtml(req, res) { return; } - let wopiParams = getWopiParams(null, fileInfo, wopiSrc, access_token, access_token_ttl); + let wopiParams = getWopiParams(undefined, fileInfo, wopiSrc, access_token, access_token_ttl); let docId = yield converterService.convertAndEdit(ctx, wopiParams, ext, targetext); if (docId) {