Files
DocumentServer-v-9.2.0/server/DocService/sources/canvasservice.js
Yajbir Singh f1b860b25c
Some checks failed
check / markdownlint (push) Has been cancelled
check / spellchecker (push) Has been cancelled
updated
2025-12-11 19:03:17 +05:30

2098 lines
86 KiB
JavaScript

/*
* (c) Copyright Ascensio System SIA 2010-2024
*
* This program is a free software product. You can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License (AGPL)
* version 3 as published by the Free Software Foundation. In accordance with
* Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
* that Ascensio System SIA expressly excludes the warranty of non-infringement
* of any third-party rights.
*
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
* details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
*
* You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
* street, Riga, Latvia, EU, LV-1050.
*
* The interactive user interfaces in modified source and object code versions
* of the Program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU AGPL version 3.
*
* Pursuant to Section 7(b) of the License you must retain the original Product
* logo when distributing the program. Pursuant to Section 7(e) we decline to
* grant you any rights under trademark law for use of our trademarks.
*
* All the Product's GUI elements, including illustrations and icon sets, as
* well as technical writing content are licensed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International. See the License
* terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
*
*/
'use strict';
const crypto = require('crypto');
const pathModule = require('path');
const urlModule = require('url');
const {pipeline} = require('node:stream/promises');
const co = require('co');
const ms = require('ms');
const retry = require('retry');
const MultiRange = require('multi-integer-range').MultiRange;
const sqlBase = require('./databaseConnectors/baseConnector');
const utilsDocService = require('./utilsDocService');
const docsCoServer = require('./DocsCoServer');
const taskResult = require('./taskresult');
const wopiUtils = require('./wopiUtils');
const wopiClient = require('./wopiClient');
const utils = require('./../../Common/sources/utils');
const constants = require('./../../Common/sources/constants');
const commonDefines = require('./../../Common/sources/commondefines');
const storage = require('./../../Common/sources/storage/storage-base');
const formatChecker = require('./../../Common/sources/formatchecker');
const statsDClient = require('./../../Common/sources/statsdclient');
const operationContext = require('./../../Common/sources/operationContext');
const tenantManager = require('./../../Common/sources/tenantManager');
const config = require('config');
const cfgTypesUpload = config.get('services.CoAuthoring.utils.limits_image_types_upload');
const cfgDocumentTypesUpload = config.get('services.CoAuthoring.utils.limits_document_types_upload');
const cfgImageSize = config.get('services.CoAuthoring.server.limits_image_size');
const cfgImageDownloadTimeout = config.get('services.CoAuthoring.server.limits_image_download_timeout');
const cfgRedisPrefix = config.get('services.CoAuthoring.redis.prefix');
const cfgTokenEnableBrowser = config.get('services.CoAuthoring.token.enable.browser');
const cfgTokenSessionAlgorithm = config.get('services.CoAuthoring.token.session.algorithm');
const cfgTokenSessionExpires = config.get('services.CoAuthoring.token.session.expires');
const cfgForgottenFiles = config.get('services.CoAuthoring.server.forgottenfiles');
const cfgForgottenFilesName = config.get('services.CoAuthoring.server.forgottenfilesname');
const cfgOpenProtectedFile = config.get('services.CoAuthoring.server.openProtectedFile');
const cfgExpUpdateVersionStatus = config.get('services.CoAuthoring.expire.updateVersionStatus');
const cfgCallbackBackoffOptions = config.get('services.CoAuthoring.callbackBackoffOptions');
const cfgAssemblyFormatAsOrigin = config.get('services.CoAuthoring.server.assemblyFormatAsOrigin');
const cfgDownloadMaxBytes = config.get('FileConverter.converter.maxDownloadBytes');
const cfgDownloadTimeout = config.get('FileConverter.converter.downloadTimeout');
const cfgDownloadFileAllowExt = config.get('services.CoAuthoring.server.downloadFileAllowExt');
const cfgNewFileTemplate = config.get('services.CoAuthoring.server.newFileTemplate');
const SAVE_TYPE_PART_START = 0;
const SAVE_TYPE_COMPLETE = 2;
const SAVE_TYPE_COMPLETE_ALL = 3;
const clientStatsD = statsDClient.getClient();
const redisKeyShutdown = cfgRedisPrefix + constants.REDIS_KEY_SHUTDOWN;
let hasPasswordCol = false; //stub on upgradev630.sql update failure
exports.hasAdditionalCol = false; //stub on upgradev710.sql update failure
function OutputDataWrap(type, data) {
this['type'] = type;
this['data'] = data;
}
OutputDataWrap.prototype = {
fromObject(data) {
this['type'] = data['type'];
this['data'] = new OutputData();
this['data'].fromObject(data['data']);
},
getType() {
return this['type'];
},
setType(data) {
this['type'] = data;
},
getData() {
return this['data'];
},
setData(data) {
this['data'] = data;
}
};
function OutputData(type) {
this['type'] = type;
this['status'] = undefined;
this['data'] = undefined;
this['filetype'] = undefined;
this['openedAt'] = undefined;
}
OutputData.prototype = {
fromObject(data) {
this['type'] = data['type'];
this['status'] = data['status'];
this['data'] = data['data'];
this['filetype'] = data['filetype'];
this['openedAt'] = data['openedAt'];
},
getType() {
return this['type'];
},
setType(data) {
this['type'] = data;
},
getStatus() {
return this['status'];
},
setStatus(data) {
this['status'] = data;
},
getData() {
return this['data'];
},
setData(data) {
this['data'] = data;
},
getExtName() {
return this['filetype'];
},
setExtName(data) {
this['filetype'] = data.substring(1);
},
getOpenedAt() {
return this['openedAt'];
},
setOpenedAt(data) {
this['openedAt'] = data;
}
};
function getOpenedAt(row) {
if (row) {
return sqlBase.DocumentAdditional.prototype.getOpenedAt(row.additional);
}
}
function getOpenedAtJSONParams(row) {
const documentLayout = row && sqlBase.DocumentAdditional.prototype.getDocumentLayout(row.additional);
if (documentLayout) {
return {documentLayout};
}
return undefined;
}
async function getOutputData(ctx, cmd, outputData, key, optConn, optAdditionalOutput, opt_bIsRestore) {
const tenExpUpdateVersionStatus = ms(ctx.getCfg('services.CoAuthoring.expire.updateVersionStatus', cfgExpUpdateVersionStatus));
let status, statusInfo, password, creationDate, openedAt, originFormat, row;
const selectRes = await taskResult.select(ctx, key);
if (selectRes.length > 0) {
row = selectRes[0];
status = row.status;
statusInfo = row.status_info;
password = sqlBase.DocumentPassword.prototype.getCurPassword(ctx, row.password);
creationDate = row.created_at && row.created_at.getTime();
openedAt = getOpenedAt(row);
originFormat = row.change_id;
if (optAdditionalOutput) {
optAdditionalOutput.row = row;
}
}
switch (status) {
case commonDefines.FileStatus.SaveVersion:
case commonDefines.FileStatus.UpdateVersion:
case commonDefines.FileStatus.Ok: {
if (commonDefines.FileStatus.Ok === status) {
outputData.setStatus('ok');
} else if (optConn && optConn.isCloseCoAuthoring) {
outputData.setStatus(constants.FILE_STATUS_UPDATE_VERSION);
} else if (optConn && optConn.user.view) {
outputData.setStatus('ok');
} else if (
commonDefines.FileStatus.SaveVersion === status ||
(!opt_bIsRestore && commonDefines.FileStatus.UpdateVersion === status && Date.now() - statusInfo * 60000 > tenExpUpdateVersionStatus)
) {
if (commonDefines.FileStatus.UpdateVersion === status) {
ctx.logger.warn('UpdateVersion expired');
}
const updateMask = new taskResult.TaskResultData();
updateMask.tenant = ctx.tenant;
updateMask.key = key;
updateMask.status = status;
updateMask.statusInfo = statusInfo;
const updateTask = new taskResult.TaskResultData();
updateTask.status = commonDefines.FileStatus.Ok;
updateTask.statusInfo = constants.NO_ERROR;
const updateIfRes = await taskResult.updateIf(ctx, updateTask, updateMask);
if (updateIfRes.affectedRows > 0) {
outputData.setStatus('ok');
} else {
outputData.setStatus(constants.FILE_STATUS_UPDATE_VERSION);
}
} else {
outputData.setStatus(constants.FILE_STATUS_UPDATE_VERSION);
}
const command = cmd.getCommand();
if ('open' != command && 'reopen' != command && !cmd.getOutputUrls()) {
const strPath = key + '/' + cmd.getOutputPath();
if (optConn) {
let url;
if (cmd.getInline()) {
url = await getPrintFileUrl(ctx, key, optConn.baseUrl, cmd.getTitle());
} else {
url = await storage.getSignedUrl(ctx, optConn.baseUrl, strPath, commonDefines.c_oAscUrlTypes.Temporary, cmd.getTitle());
}
outputData.setData(url);
outputData.setExtName(pathModule.extname(strPath));
} else if (optAdditionalOutput) {
optAdditionalOutput.needUrlKey = cmd.getInline() ? key : strPath;
optAdditionalOutput.needUrlMethod = 2;
optAdditionalOutput.needUrlType = commonDefines.c_oAscUrlTypes.Temporary;
}
} else {
const encryptedUserPassword = cmd.getPassword();
let userPassword;
let decryptedPassword;
let isCorrectPassword;
if (password && encryptedUserPassword) {
decryptedPassword = await utils.decryptPassword(ctx, password);
userPassword = await utils.decryptPassword(ctx, encryptedUserPassword);
isCorrectPassword = decryptedPassword === userPassword;
}
let isNeedPassword = password && !isCorrectPassword;
if (isNeedPassword && formatChecker.isBrowserEditorFormat(originFormat)) {
//check pdf form
//todo check without storage
const formEditor = await storage.listObjects(ctx, key + '/Editor.bin');
isNeedPassword = 0 !== formEditor.length;
}
if (isNeedPassword) {
ctx.logger.debug('getOutputData password mismatch');
if (encryptedUserPassword) {
outputData.setStatus('needpassword');
outputData.setData(constants.CONVERT_PASSWORD);
} else {
outputData.setStatus('needpassword');
outputData.setData(constants.CONVERT_DRM);
}
} else if (optConn) {
outputData.setOpenedAt(openedAt);
outputData.setData(await storage.getSignedUrls(ctx, optConn.baseUrl, key, commonDefines.c_oAscUrlTypes.Session, creationDate));
} else if (optAdditionalOutput) {
optAdditionalOutput.needUrlKey = key;
optAdditionalOutput.needUrlMethod = 0;
optAdditionalOutput.needUrlType = commonDefines.c_oAscUrlTypes.Session;
optAdditionalOutput.needUrlIsCorrectPassword = isCorrectPassword;
optAdditionalOutput.creationDate = creationDate;
optAdditionalOutput.openedAt = openedAt;
}
}
break;
}
case commonDefines.FileStatus.NeedParams: {
outputData.setStatus('needparams');
const settingsPath = key + '/' + 'origin.' + cmd.getFormat();
if (optConn) {
const url = await storage.getSignedUrl(ctx, optConn.baseUrl, settingsPath, commonDefines.c_oAscUrlTypes.Temporary);
outputData.setData(url);
} else if (optAdditionalOutput) {
optAdditionalOutput.needUrlKey = settingsPath;
optAdditionalOutput.needUrlMethod = 1;
optAdditionalOutput.needUrlType = commonDefines.c_oAscUrlTypes.Temporary;
}
break;
}
case commonDefines.FileStatus.NeedPassword:
outputData.setStatus('needpassword');
outputData.setData(statusInfo);
break;
case commonDefines.FileStatus.Err:
outputData.setStatus('err');
outputData.setData(statusInfo);
break;
case commonDefines.FileStatus.ErrToReload:
outputData.setStatus('err');
outputData.setData(statusInfo);
await cleanupErrToReload(ctx, key);
break;
case commonDefines.FileStatus.None:
//this status has no handler
break;
case commonDefines.FileStatus.WaitQueue:
{
const timeout = await utils.getConvertionTimeout(ctx);
console.log(timeout);
console.log(statusInfo);
console.log(Date.now() - statusInfo * 60000);
console.log(Date.now() - statusInfo * 60000 > timeout);
if (Date.now() - statusInfo * 60000 > timeout) {
ctx.logger.warn('WaitQueue expired');
const updateMask = new taskResult.TaskResultData();
updateMask.tenant = ctx.tenant;
updateMask.key = key;
updateMask.status = status;
updateMask.statusInfo = statusInfo;
const updateTask = new taskResult.TaskResultData();
updateTask.status = commonDefines.FileStatus.None;
updateTask.statusInfo = constants.NO_ERROR;
const updateIfRes = await taskResult.updateIf(ctx, updateTask, updateMask);
if (updateIfRes.affectedRows > 0) {
status = commonDefines.FileStatus.None;
}
}
}
//task in the queue. response will be after convertion
break;
default:
outputData.setStatus('err');
outputData.setData(constants.UNKNOWN);
break;
}
return status;
}
function* addRandomKeyTaskCmd(ctx, cmd) {
const docId = cmd.getDocId();
const task = yield* taskResult.addRandomKeyTask(ctx, docId);
//set saveKey as postfix to fix vulnerability with path traversal to docId or other files
cmd.setSaveKey(task.key.substring(docId.length));
}
function addPasswordToCmd(ctx, cmd, docPasswordStr, originFormat) {
const docPassword = sqlBase.DocumentPassword.prototype.getDocPassword(ctx, docPasswordStr);
if (docPassword.current) {
if (formatChecker.isBrowserEditorFormat(originFormat)) {
//todo not allowed different password
cmd.setPassword(docPassword.current);
}
cmd.setSavePassword(docPassword.current);
}
if (docPassword.change) {
cmd.setExternalChangeInfo(docPassword.change);
}
}
function addOriginFormat(ctx, cmd, row) {
cmd.setOriginFormat(row && row.change_id);
}
function changeFormatByOrigin(ctx, row, format) {
const tenAssemblyFormatAsOrigin = ctx.getCfg('services.CoAuthoring.server.assemblyFormatAsOrigin', cfgAssemblyFormatAsOrigin);
const originFormat = row && row.change_id;
if (originFormat && constants.AVS_OFFICESTUDIO_FILE_UNKNOWN !== originFormat) {
if (tenAssemblyFormatAsOrigin) {
format = originFormat;
} else {
//for wopi always save origin
const userAuthStr = sqlBase.UserCallback.prototype.getCallbackByUserIndex(ctx, row.callback);
const wopiParams = wopiClient.parseWopiCallback(ctx, userAuthStr, row.callback);
if (wopiParams) {
format = originFormat;
}
}
}
return format;
}
function* saveParts(ctx, cmd, filename) {
let result = false;
const saveType = cmd.getSaveType();
if (SAVE_TYPE_COMPLETE_ALL !== saveType) {
const ext = pathModule.extname(filename);
const saveIndex = parseInt(cmd.getSaveIndex()) || 1; //prevent path traversal
filename = pathModule.basename(filename, ext) + saveIndex + ext;
}
if ((SAVE_TYPE_PART_START === saveType || SAVE_TYPE_COMPLETE_ALL === saveType) && !cmd.getSaveKey()) {
yield* addRandomKeyTaskCmd(ctx, cmd);
}
if (cmd.getUrl()) {
result = true;
} else if (cmd.getData() && cmd.getData().length > 0 && cmd.getSaveKey()) {
const buffer = cmd.getData();
yield storage.putObject(ctx, cmd.getDocId() + cmd.getSaveKey() + '/' + filename, buffer, buffer.length);
//delete data to prevent serialize into json
cmd.data = null;
result = SAVE_TYPE_COMPLETE_ALL === saveType || SAVE_TYPE_COMPLETE === saveType;
} else {
result = true;
}
return result;
}
function getSaveTask(ctx, cmd) {
cmd.setData(null);
const queueData = new commonDefines.TaskQueueData();
queueData.setCtx(ctx);
queueData.setCmd(cmd);
queueData.setToFile(constants.OUTPUT_NAME + '.' + formatChecker.getStringFromFormat(cmd.getOutputFormat()));
//todo paid
//if (cmd.vkey) {
// bool
// bPaid;
// Signature.getVKeyParams(cmd.vkey, out bPaid);
// oTaskQueueData.m_bPaid = bPaid;
//}
return queueData;
}
async function getUpdateResponse(ctx, cmd) {
const tenOpenProtectedFile = ctx.getCfg('services.CoAuthoring.server.openProtectedFile', cfgOpenProtectedFile);
const updateTask = new taskResult.TaskResultData();
updateTask.tenant = ctx.tenant;
updateTask.key = cmd.getDocId();
if (cmd.getSaveKey()) {
updateTask.key += cmd.getSaveKey();
}
const statusInfo = cmd.getStatusInfo();
if (constants.NO_ERROR === statusInfo) {
updateTask.status = commonDefines.FileStatus.Ok;
const password = cmd.getPassword();
if (password) {
if (false === hasPasswordCol) {
const selectRes = await taskResult.select(ctx, updateTask.key);
hasPasswordCol = selectRes.length > 0 && undefined !== selectRes[0].password;
}
if (hasPasswordCol) {
updateTask.password = password;
}
}
} else if (constants.CONVERT_TEMPORARY === statusInfo) {
updateTask.status = commonDefines.FileStatus.ErrToReload;
} else if (constants.CONVERT_DOWNLOAD === statusInfo) {
updateTask.status = commonDefines.FileStatus.ErrToReload;
} else if (constants.CONVERT_LIMITS === statusInfo) {
updateTask.status = commonDefines.FileStatus.ErrToReload;
} else if (constants.CONVERT_NEED_PARAMS === statusInfo) {
updateTask.status = commonDefines.FileStatus.NeedParams;
} else if (constants.CONVERT_DRM === statusInfo || constants.CONVERT_PASSWORD === statusInfo) {
if (tenOpenProtectedFile) {
updateTask.status = commonDefines.FileStatus.NeedPassword;
} else {
updateTask.status = commonDefines.FileStatus.Err;
}
} else if (constants.CONVERT_DRM_UNSUPPORTED === statusInfo) {
updateTask.status = commonDefines.FileStatus.Err;
} else if (constants.CONVERT_DEAD_LETTER === statusInfo) {
updateTask.status = commonDefines.FileStatus.ErrToReload;
} else {
updateTask.status = commonDefines.FileStatus.Err;
}
updateTask.statusInfo = statusInfo;
return updateTask;
}
const cleanupCache = co.wrap(function* (ctx, docId) {
//todo redis ?
let res = false;
const removeRes = yield taskResult.remove(ctx, docId);
if (removeRes.affectedRows > 0) {
yield storage.deletePath(ctx, docId);
if (docsCoServer?.editorStatProxy?.deleteKey) {
yield docsCoServer.editorStatProxy.deleteKey(docId);
}
res = true;
}
ctx.logger.debug('cleanupCache docId=%s db.affectedRows=%d', docId, removeRes.affectedRows);
return res;
});
const cleanupCacheIf = co.wrap(function* (ctx, mask) {
//todo redis ?
let res = false;
const removeRes = yield taskResult.removeIf(ctx, mask);
if (removeRes.affectedRows > 0) {
sqlBase.deleteChanges(ctx, mask.key, null);
yield storage.deletePath(ctx, mask.key);
if (docsCoServer?.editorStatProxy?.deleteKey) {
yield docsCoServer.editorStatProxy.deleteKey(mask.key);
}
res = true;
}
ctx.logger.debug('cleanupCacheIf db.affectedRows=%d', removeRes.affectedRows);
return res;
});
async function cleanupErrToReload(ctx, key) {
const updateTask = new taskResult.TaskResultData();
updateTask.tenant = ctx.tenant;
updateTask.key = key;
updateTask.status = commonDefines.FileStatus.None;
updateTask.statusInfo = constants.NO_ERROR;
await taskResult.update(ctx, updateTask);
}
function commandOpenStartPromise(ctx, docId, baseUrl, opt_documentCallbackUrl, opt_format) {
const task = new taskResult.TaskResultData();
task.tenant = ctx.tenant;
task.key = docId;
//None instead WaitQueue to prevent: conversion task is lost when entering and leaving the editor quickly(that leads to an endless opening)
task.status = commonDefines.FileStatus.None;
task.statusInfo = constants.NO_ERROR;
task.baseurl = baseUrl;
if (opt_documentCallbackUrl) {
task.callback = opt_documentCallbackUrl;
}
if (opt_format) {
task.changeId = formatChecker.getFormatFromString(opt_format);
}
return taskResult.upsert(ctx, task);
}
function* commandOpen(ctx, conn, cmd, outputData, opt_upsertRes, opt_bIsRestore) {
const tenForgottenFiles = ctx.getCfg('services.CoAuthoring.server.forgottenfiles', cfgForgottenFiles);
let upsertRes;
if (opt_upsertRes) {
upsertRes = opt_upsertRes;
} else {
upsertRes = yield commandOpenStartPromise(ctx, cmd.getDocId(), utils.getBaseUrlByConnection(ctx, conn), undefined, cmd.getFormat());
}
const bCreate = upsertRes.isInsert;
let needAddTask = bCreate;
if (!bCreate) {
needAddTask = yield* commandOpenFillOutput(ctx, conn, cmd, outputData, opt_bIsRestore);
}
if (conn.encrypted) {
ctx.logger.debug('commandOpen encrypted %j', outputData);
if (constants.FILE_STATUS_UPDATE_VERSION !== outputData.getStatus()) {
//don't send output data
outputData.setStatus(undefined);
}
} else if (needAddTask) {
const updateMask = new taskResult.TaskResultData();
updateMask.tenant = ctx.tenant;
updateMask.key = cmd.getDocId();
updateMask.status = commonDefines.FileStatus.None;
const task = new taskResult.TaskResultData();
task.status = commonDefines.FileStatus.WaitQueue;
task.statusInfo = Math.floor(Date.now() / 60000); //minutes
const updateIfRes = yield taskResult.updateIf(ctx, task, updateMask);
if (updateIfRes.affectedRows > 0) {
const forgotten = yield storage.listObjects(ctx, cmd.getDocId(), tenForgottenFiles);
//replace url with forgotten file because it absorbed all lost changes
if (forgotten.length > 0) {
ctx.logger.debug('commandOpen from forgotten');
cmd.setUrl(undefined);
cmd.setForgotten(cmd.getDocId());
}
//add task
if (!cmd.getOutputFormat()) {
//todo remove getOpenFormatByEditor after 8.2.1
cmd.setOutputFormat(docsCoServer.getOpenFormatByEditor(conn.editorType));
}
cmd.setEmbeddedFonts(false);
const dataQueue = new commonDefines.TaskQueueData();
dataQueue.setCtx(ctx);
dataQueue.setCmd(cmd);
dataQueue.setToFile('Editor.bin');
yield* docsCoServer.addTask(dataQueue, constants.QUEUE_PRIORITY_HIGH);
} else {
yield* commandOpenFillOutput(ctx, conn, cmd, outputData, opt_bIsRestore);
}
}
}
function* commandOpenFillOutput(ctx, conn, cmd, outputData, opt_bIsRestore) {
const status = yield getOutputData(ctx, cmd, outputData, cmd.getDocId(), conn, undefined, opt_bIsRestore);
return commonDefines.FileStatus.None === status;
}
function* commandReopen(ctx, conn, cmd, outputData) {
const tenOpenProtectedFile = ctx.getCfg('services.CoAuthoring.server.openProtectedFile', cfgOpenProtectedFile);
let res = true;
const isPassword = undefined !== cmd.getPassword();
if (isPassword) {
const selectRes = yield taskResult.select(ctx, cmd.getDocId());
if (selectRes.length > 0) {
const row = selectRes[0];
if (sqlBase.DocumentPassword.prototype.getCurPassword(ctx, row.password)) {
ctx.logger.debug('commandReopen has password');
yield* commandOpenFillOutput(ctx, conn, cmd, outputData, false);
yield docsCoServer.modifyConnectionForPassword(ctx, conn, constants.FILE_STATUS_OK === outputData.getStatus());
return res;
}
}
}
if (!isPassword || tenOpenProtectedFile) {
const updateMask = new taskResult.TaskResultData();
updateMask.tenant = ctx.tenant;
updateMask.key = cmd.getDocId();
updateMask.status = isPassword ? commonDefines.FileStatus.NeedPassword : commonDefines.FileStatus.NeedParams;
const task = new taskResult.TaskResultData();
task.status = commonDefines.FileStatus.WaitQueue;
task.statusInfo = Math.floor(Date.now() / 60000); //minutes
const upsertRes = yield taskResult.updateIf(ctx, task, updateMask);
if (upsertRes.affectedRows > 0) {
//add task
cmd.setUrl(null); //url may expire
if (!cmd.getOutputFormat()) {
//todo remove getOpenFormatByEditor after 8.2.1
cmd.setOutputFormat(docsCoServer.getOpenFormatByEditor(conn.editorType));
}
cmd.setEmbeddedFonts(false);
if (isPassword) {
cmd.setUserConnectionId(conn.user.id);
}
const dataQueue = new commonDefines.TaskQueueData();
dataQueue.setCtx(ctx);
dataQueue.setCmd(cmd);
dataQueue.setToFile('Editor.bin');
dataQueue.setFromSettings(true);
yield* docsCoServer.addTask(dataQueue, constants.QUEUE_PRIORITY_HIGH);
} else {
outputData.setStatus('needpassword');
outputData.setData(constants.CONVERT_PASSWORD);
}
} else {
res = false;
}
return res;
}
function* commandSave(ctx, cmd, outputData) {
const format = cmd.getFormat() || 'bin';
const completeParts = yield* saveParts(ctx, cmd, 'Editor.' + format);
if (completeParts) {
const queueData = getSaveTask(ctx, cmd);
yield* docsCoServer.addTask(queueData, constants.QUEUE_PRIORITY_LOW);
}
outputData.setStatus('ok');
outputData.setData(cmd.getSaveKey());
}
function* commandSendMailMerge(ctx, cmd, outputData) {
const mailMergeSend = cmd.getMailMergeSend();
const isJson = mailMergeSend.getIsJsonKey();
const completeParts = yield* saveParts(ctx, cmd, isJson ? 'Editor.json' : 'Editor.bin');
let isErr = false;
if (completeParts && !isJson) {
isErr = true;
const getRes = yield docsCoServer.getCallback(ctx, cmd.getDocId(), cmd.getUserIndex());
if (getRes && !getRes.wopiParams) {
mailMergeSend.setUrl(getRes.server.href);
mailMergeSend.setBaseUrl(getRes.baseUrl);
//we change JsonKey and SaveKey, a new key is needed because a part is done in one conversion, and json is always needed
mailMergeSend.setJsonKey(cmd.getSaveKey());
mailMergeSend.setRecordErrorCount(0);
yield* addRandomKeyTaskCmd(ctx, cmd);
const queueData = getSaveTask(ctx, cmd);
yield* docsCoServer.addTask(queueData, constants.QUEUE_PRIORITY_LOW);
isErr = false;
} else if (getRes.wopiParams) {
ctx.logger.warn('commandSendMailMerge unexpected with wopi');
}
}
if (isErr) {
outputData.setStatus('err');
outputData.setData(constants.UNKNOWN);
} else {
outputData.setStatus('ok');
outputData.setData(cmd.getSaveKey());
}
}
const commandSfctByCmd = co.wrap(function* (ctx, cmd, opt_priority, opt_expiration, opt_queue, opt_initShardKey) {
const selectRes = yield taskResult.selectWithCache(ctx, cmd.getDocId());
const row = selectRes.length > 0 ? selectRes[0] : null;
if (!row) {
return false;
}
if (opt_initShardKey) {
ctx.setShardKey(sqlBase.DocumentAdditional.prototype.getShardKey(row.additional));
ctx.setWopiSrc(sqlBase.DocumentAdditional.prototype.getWopiSrc(row.additional));
}
yield* addRandomKeyTaskCmd(ctx, cmd);
addPasswordToCmd(ctx, cmd, row.password, row.change_id);
addOriginFormat(ctx, cmd, row);
const userAuthStr = sqlBase.UserCallback.prototype.getCallbackByUserIndex(ctx, row.callback);
cmd.setWopiParams(wopiClient.parseWopiCallback(ctx, userAuthStr, row.callback));
cmd.setOutputFormat(changeFormatByOrigin(ctx, row, cmd.getOutputFormat()));
cmd.appendJsonParams(getOpenedAtJSONParams(row));
const queueData = getSaveTask(ctx, cmd);
queueData.setFromChanges(true);
const priority = null != opt_priority ? opt_priority : constants.QUEUE_PRIORITY_LOW;
yield* docsCoServer.addTask(queueData, priority, opt_queue, opt_expiration);
return true;
});
function isDisplayedImage(strName) {
let res = 0;
if (strName) {
//template display[N]image.ext
const findStr = constants.DISPLAY_PREFIX;
const index = strName.indexOf(findStr);
if (-1 != index) {
if (index + findStr.length < strName.length) {
const displayN = parseInt(strName[index + findStr.length]);
if (!isNaN(displayN)) {
const imageIndex = index + findStr.length + 1;
if (imageIndex == strName.indexOf('image', imageIndex)) {
res = displayN;
}
}
}
}
}
return res;
}
function* commandImgurls(ctx, conn, cmd, outputData) {
const tenTypesUpload = ctx.getCfg('services.CoAuthoring.utils.limits_image_types_upload', cfgTypesUpload);
const tenDocumentTypesUpload = ctx.getCfg('services.CoAuthoring.utils.limits_document_types_upload', cfgDocumentTypesUpload);
const tenImageSize = ctx.getCfg('services.CoAuthoring.server.limits_image_size', cfgImageSize);
const tenImageDownloadTimeout = ctx.getCfg('services.CoAuthoring.server.limits_image_download_timeout', cfgImageDownloadTimeout);
const tenTokenEnableBrowser = ctx.getCfg('services.CoAuthoring.token.enable.browser', cfgTokenEnableBrowser);
let errorCode = constants.NO_ERROR;
let urls = cmd.getData();
const authorizations = [];
let isInJwtToken = false;
const token = cmd.getTokenDownload();
if (tenTokenEnableBrowser && token) {
// allow requests without token
const checkJwtRes = yield docsCoServer.checkJwt(ctx, token, commonDefines.c_oAscSecretType.Browser);
if (checkJwtRes.decoded) {
//todo multiple url case
if (checkJwtRes.decoded.images) {
urls = checkJwtRes.decoded.images.map(curValue => {
return curValue.url;
});
} else {
urls = [checkJwtRes.decoded.url];
}
for (let i = 0; i < urls.length; ++i) {
if (utils.canIncludeOutboxAuthorization(ctx, urls[i])) {
const secret = yield tenantManager.getTenantSecret(ctx, commonDefines.c_oAscSecretType.Outbox);
authorizations[i] = [utils.fillJwtForRequest(ctx, {url: urls[i]}, secret, false)];
}
}
isInJwtToken = true;
} else {
ctx.logger.warn('Error commandImgurls jwt: %s', checkJwtRes.description);
errorCode = constants.VKEY_ENCRYPT;
}
}
const supportedFormats = tenTypesUpload || 'jpg';
const supportedDocumentFormats = tenDocumentTypesUpload || 'xlsx';
const outputUrls = [];
if (constants.NO_ERROR === errorCode && !conn.user.view && !conn.isCloseCoAuthoring) {
//todo Promise.all()
const displayedImageMap = {}; //to make one prefix for ole object urls
for (let i = 0; i < urls.length; ++i) {
const urlSource = urls[i];
let urlParsed;
let isDocument = false;
let data = undefined;
if (urlSource?.startsWith('data:')) {
const delimiterIndex = urlSource.indexOf(',');
if (-1 != delimiterIndex) {
const dataLen = urlSource.length - (delimiterIndex + 1);
if ('hex' === urlSource.substring(delimiterIndex - 3, delimiterIndex).toLowerCase()) {
if (dataLen * 0.5 <= tenImageSize) {
data = Buffer.from(urlSource.substring(delimiterIndex + 1), 'hex');
} else {
errorCode = constants.UPLOAD_CONTENT_LENGTH;
}
} else {
if (dataLen * 0.75 <= tenImageSize) {
data = Buffer.from(urlSource.substring(delimiterIndex + 1), 'base64');
} else {
errorCode = constants.UPLOAD_CONTENT_LENGTH;
}
}
}
} else if (urlSource) {
try {
if (authorizations[i]) {
const urlParsed = urlModule.parse(urlSource);
const filterStatus = yield* utils.checkHostFilter(ctx, urlParsed.hostname);
if (0 !== filterStatus) {
throw Error('checkIpFilter');
}
}
//todo stream
const getRes = yield utils.downloadUrlPromise(ctx, urlSource, tenImageDownloadTimeout, tenImageSize, authorizations[i], isInJwtToken);
data = getRes.body;
urlParsed = urlModule.parse(urlSource);
} catch (e) {
data = undefined;
ctx.logger.error('error commandImgurls download: url = %s; %s', urlSource, e.stack);
if (e.code === 'EMSGSIZE') {
errorCode = constants.UPLOAD_CONTENT_LENGTH;
} else {
errorCode = constants.UPLOAD_URL;
}
}
}
let outputUrl = {url: 'error', path: 'error'};
if (data) {
// process image: fix EXIF rotation and convert unsupported formats to optimal format
data = yield utilsDocService.processImageOptimal(ctx, data);
const format = formatChecker.getImageFormat(ctx, data);
let formatStr;
let isAllow = false;
if (constants.AVS_OFFICESTUDIO_FILE_UNKNOWN !== format) {
formatStr = formatChecker.getStringFromFormat(format);
if (formatStr && -1 !== supportedFormats.indexOf(formatStr)) {
isAllow = true;
}
} else if (constants.AVS_OFFICESTUDIO_FILE_SPREADSHEET_XLSX === formatChecker.getOfficeZipFormatBySignature(data)) {
formatStr = 'xlsx';
if (-1 !== supportedDocumentFormats.indexOf(formatStr)) {
isAllow = true;
isDocument = true;
}
}
if (!isAllow && urlParsed) {
//for ole object, presentation video/audio
const ext = pathModule.extname(urlParsed.pathname).substring(1);
const urlBasename = pathModule.basename(urlParsed.pathname);
const displayedImageName = urlBasename.substring(0, urlBasename.length - ext.length - 1);
if (Object.hasOwn(displayedImageMap, displayedImageName)) {
formatStr = ext;
isAllow = true;
}
}
if (isAllow) {
let strLocalPath = 'media/' + crypto.randomBytes(16).toString('hex') + '_';
if (urlParsed) {
const urlBasename = pathModule.basename(urlParsed.pathname);
const displayN = isDisplayedImage(urlBasename);
if (displayN > 0) {
const displayedImageName = urlBasename.substring(0, urlBasename.length - formatStr.length - 1);
if (displayedImageMap[displayedImageName]) {
strLocalPath = displayedImageMap[displayedImageName];
} else {
displayedImageMap[displayedImageName] = strLocalPath;
}
strLocalPath += constants.DISPLAY_PREFIX + displayN;
}
}
if (isDocument) {
strLocalPath += 'document1' + '.' + formatStr;
} else {
strLocalPath += 'image1' + '.' + formatStr;
}
const strPath = cmd.getDocId() + '/' + strLocalPath;
yield storage.putObject(ctx, strPath, data, data.length);
const imgUrl = yield storage.getSignedUrl(ctx, conn.baseUrl, strPath, commonDefines.c_oAscUrlTypes.Session);
outputUrl = {url: imgUrl, path: strLocalPath};
}
}
if (constants.NO_ERROR === errorCode && ('error' === outputUrl.url || 'error' === outputUrl.path)) {
errorCode = constants.UPLOAD_EXTENSION;
}
outputUrls.push(outputUrl);
}
} else if (constants.NO_ERROR === errorCode) {
ctx.logger.warn('error commandImgurls: access deny');
errorCode = constants.UPLOAD;
}
if (constants.NO_ERROR !== errorCode && 0 == outputUrls.length) {
outputData.setStatus('err');
outputData.setData(errorCode);
} else {
outputData.setStatus('ok');
outputData.setData({error: errorCode, urls: outputUrls});
}
}
function* commandPathUrls(ctx, conn, data, outputData) {
const listImages = data.map(currentValue => {
return conn.docId + '/' + currentValue;
});
const urls = yield storage.getSignedUrlsArrayByArray(ctx, conn.baseUrl, listImages, commonDefines.c_oAscUrlTypes.Session);
outputData.setStatus('ok');
outputData.setData(urls);
}
function* commandPathUrl(ctx, conn, cmd, outputData) {
const strPath = conn.docId + '/' + cmd.getData();
const url = yield storage.getSignedUrl(ctx, conn.baseUrl, strPath, commonDefines.c_oAscUrlTypes.Temporary, cmd.getTitle());
const errorCode = constants.NO_ERROR;
if (constants.NO_ERROR !== errorCode) {
outputData.setStatus('err');
outputData.setData(errorCode);
} else {
outputData.setStatus('ok');
outputData.setData(url);
outputData.setExtName(pathModule.extname(strPath));
}
}
function* commandSaveFromOrigin(ctx, cmd, outputData, password) {
const completeParts = yield* saveParts(ctx, cmd, 'changes0.json');
if (completeParts) {
const docPassword = sqlBase.DocumentPassword.prototype.getDocPassword(ctx, password);
//Use current password for pdf because password is entered in the browser when opening and is set via setPassword
if (docPassword.initial || docPassword.current) {
cmd.setPassword(docPassword.initial || docPassword.current);
}
//todo setLCID in browser
const queueData = getSaveTask(ctx, cmd);
queueData.setFromOrigin(true);
queueData.setFromChanges(true);
yield* docsCoServer.addTask(queueData, constants.QUEUE_PRIORITY_LOW);
}
outputData.setStatus('ok');
outputData.setData(cmd.getSaveKey());
}
function* commandSetPassword(ctx, conn, cmd, outputData) {
const tenOpenProtectedFile = ctx.getCfg('services.CoAuthoring.server.openProtectedFile', cfgOpenProtectedFile);
let hasDocumentPassword = false;
let isDocumentPasswordModified = true;
let originFormat;
const selectRes = yield taskResult.select(ctx, cmd.getDocId());
if (selectRes.length > 0) {
const row = selectRes[0];
originFormat = row.change_id;
hasPasswordCol = undefined !== row.password;
if (commonDefines.FileStatus.Ok === row.status) {
const documentPasswordCurEnc = sqlBase.DocumentPassword.prototype.getCurPassword(ctx, row.password);
if (documentPasswordCurEnc) {
hasDocumentPassword = true;
if (cmd.getPassword()) {
const passwordCurPlain = yield utils.decryptPassword(ctx, documentPasswordCurEnc);
const passwordPlain = yield utils.decryptPassword(ctx, cmd.getPassword());
isDocumentPasswordModified = passwordCurPlain !== passwordPlain;
}
}
}
}
//https://github.com/ONLYOFFICE/web-apps/blob/4a7879b4f88f315fe94d9f7d97c0ed8aa9f82221/apps/documenteditor/main/app/controller/Main.js#L1652
//this.appOptions.isPasswordSupport = this.appOptions.isEdit && this.api.asc_isProtectionSupport() && (this.permissions.protect!==false);
const isPasswordSupport = tenOpenProtectedFile && !conn.user?.view && false !== conn.permissions?.protect;
ctx.logger.debug(
'commandSetPassword isEnterCorrectPassword=%s, hasDocumentPassword=%s, hasPasswordCol=%s, isPasswordSupport=%s',
conn.isEnterCorrectPassword,
hasDocumentPassword,
hasPasswordCol,
isPasswordSupport
);
if (isPasswordSupport && hasPasswordCol && hasDocumentPassword && !isDocumentPasswordModified) {
outputData.setStatus('ok');
} else if (isPasswordSupport && (conn.isEnterCorrectPassword || !hasDocumentPassword) && hasPasswordCol) {
const updateMask = new taskResult.TaskResultData();
updateMask.tenant = ctx.tenant;
updateMask.key = cmd.getDocId();
updateMask.status = commonDefines.FileStatus.Ok;
const newChangesLastDate = new Date();
newChangesLastDate.setMilliseconds(0); //remove milliseconds avoid issues with MySQL datetime rounding
const task = new taskResult.TaskResultData();
task.password = cmd.getPassword() || '';
let changeInfo = null;
if (conn.user && (hasDocumentPassword || !formatChecker.isBrowserEditorFormat(originFormat))) {
changeInfo = task.innerPasswordChange = docsCoServer.getExternalChangeInfo(conn.user, newChangesLastDate.getTime(), conn.lang);
}
const upsertRes = yield taskResult.updateIf(ctx, task, updateMask);
if (upsertRes.affectedRows > 0) {
outputData.setStatus('ok');
if (!conn.isEnterCorrectPassword) {
yield docsCoServer.modifyConnectionForPassword(ctx, conn, true);
}
if (changeInfo) {
const forceSave = yield docsCoServer.editorData.getForceSave(ctx, cmd.getDocId());
const index = forceSave?.index || 0;
yield docsCoServer.resetForceSaveAfterChanges(
ctx,
cmd.getDocId(),
newChangesLastDate.getTime(),
index,
utils.getBaseUrlByConnection(ctx, conn),
changeInfo
);
}
} else {
ctx.logger.debug('commandSetPassword sql update error');
outputData.setStatus('err');
outputData.setData(constants.PASSWORD);
}
} else {
outputData.setStatus('err');
outputData.setData(constants.PASSWORD);
}
}
function* commandChangeDocInfo(ctx, conn, cmd, outputData) {
const res = yield docsCoServer.changeConnectionInfo(ctx, conn, cmd);
if (res) {
outputData.setStatus('ok');
} else {
outputData.setStatus('err');
outputData.setData(constants.CHANGE_DOC_INFO);
}
}
function checkAndFixAuthorizationLength(authorization, data) {
//todo it is stub (remove in future versions)
//8kb(https://stackoverflow.com/questions/686217/maximum-on-http-header-values) - 1kb(for other headers)
const res = authorization.length < 7168;
if (!res) {
data.setChangeUrl(undefined);
data.setChangeHistory({});
}
return res;
}
const commandSfcCallback = co.wrap(function* (ctx, cmd, isSfcm, isEncrypted) {
const tenForgottenFiles = ctx.getCfg('services.CoAuthoring.server.forgottenfiles', cfgForgottenFiles);
const tenForgottenFilesName = ctx.getCfg('services.CoAuthoring.server.forgottenfilesname', cfgForgottenFilesName);
const tenCallbackBackoffOptions = ctx.getCfg('services.CoAuthoring.callbackBackoffOptions', cfgCallbackBackoffOptions);
const docId = cmd.getDocId();
ctx.logger.debug('Start commandSfcCallback');
const statusInfo = cmd.getStatusInfo();
//setUserId - set from changes in convert
//setUserActionId - used in case of save without changes(forgotten files)
const userLastChangeId = cmd.getUserId() || cmd.getUserActionId();
const userLastChangeIndex = cmd.getUserIndex() || cmd.getUserActionIndex();
let replyStr;
let isSfcmSuccess = false;
let isSfcSuccess = false;
let needRetry = false;
let needUpdateVersionEvent = !isSfcm && !isEncrypted;
if (constants.EDITOR_CHANGES !== statusInfo || isSfcm) {
const saveKey = docId + cmd.getSaveKey();
let isError = constants.NO_ERROR != statusInfo;
const isErrorCorrupted = constants.CONVERT_CORRUPTED == statusInfo;
const savePathDoc = saveKey + '/' + cmd.getOutputPath();
const savePathChanges = saveKey + '/changes.zip';
const savePathHistory = saveKey + '/changesHistory.json';
const forceSave = cmd.getForceSave();
const forceSaveType = forceSave ? forceSave.getType() : commonDefines.c_oAscForceSaveTypes.Command;
const forceSaveUserId = forceSave ? forceSave.getAuthorUserId() : undefined;
const forceSaveUserIndex = forceSave ? forceSave.getAuthorUserIndex() : undefined;
const callbackUserIndex = forceSaveUserIndex || 0 === forceSaveUserIndex ? forceSaveUserIndex : userLastChangeIndex;
let uri, baseUrl, wopiParams, lastOpenDate;
const selectRes = yield taskResult.select(ctx, docId);
const row = selectRes.length > 0 ? selectRes[0] : null;
if (row) {
if (row.callback) {
uri = sqlBase.UserCallback.prototype.getCallbackByUserIndex(ctx, row.callback, callbackUserIndex);
wopiParams = wopiClient.parseWopiCallback(ctx, uri, row.callback);
}
if (row.baseurl) {
baseUrl = row.baseurl;
}
lastOpenDate = row.last_open_date;
}
let storeForgotten = false;
let statusOk;
let statusErr;
if (isSfcm) {
statusOk = docsCoServer.c_oAscServerStatus.MustSaveForce;
statusErr = docsCoServer.c_oAscServerStatus.CorruptedForce;
} else {
statusOk = docsCoServer.c_oAscServerStatus.MustSave;
statusErr = docsCoServer.c_oAscServerStatus.Corrupted;
}
const recoverTask = new taskResult.TaskResultData();
recoverTask.status = commonDefines.FileStatus.Ok;
recoverTask.statusInfo = constants.NO_ERROR;
let updateIfTask = new taskResult.TaskResultData();
updateIfTask.status = commonDefines.FileStatus.UpdateVersion;
updateIfTask.statusInfo = Math.floor(Date.now() / 60000); //minutes
let updateIfRes;
const updateMask = new taskResult.TaskResultData();
updateMask.tenant = ctx.tenant;
updateMask.key = docId;
if (row) {
if (isEncrypted) {
recoverTask.status = updateMask.status = row.status;
recoverTask.statusInfo = updateMask.statusInfo = row.status_info;
} else if (
(commonDefines.FileStatus.SaveVersion === row.status && cmd.getStatusInfoIn() === row.status_info) ||
commonDefines.FileStatus.UpdateVersion === row.status
) {
if (commonDefines.FileStatus.UpdateVersion === row.status) {
updateIfRes = {affectedRows: 1};
}
recoverTask.status = commonDefines.FileStatus.SaveVersion;
recoverTask.statusInfo = cmd.getStatusInfoIn();
updateMask.status = row.status;
updateMask.statusInfo = row.status_info;
} else {
updateIfRes = {affectedRows: 0};
}
} else {
isError = true;
}
let outputSfc;
if (uri && baseUrl && userLastChangeId) {
ctx.logger.debug('Callback commandSfcCallback: callback = %s', uri);
outputSfc = new commonDefines.OutputSfcData(docId);
outputSfc.setEncrypted(isEncrypted);
const users = [];
let isOpenFromForgotten = false;
if (userLastChangeId) {
users.push(userLastChangeId);
}
outputSfc.setUsers(users);
if (!isSfcm) {
const actions = [];
//use UserId case UserActionId miss in gc convertion
const userActionId = cmd.getUserActionId() || cmd.getUserId();
if (userActionId) {
actions.push(new commonDefines.OutputAction(commonDefines.c_oAscUserAction.Out, userActionId));
}
outputSfc.setActions(actions);
} else if (forceSaveUserId) {
outputSfc.setActions([new commonDefines.OutputAction(commonDefines.c_oAscUserAction.ForceSaveButton, forceSaveUserId)]);
}
outputSfc.setUserData(cmd.getUserData());
const formsData = cmd.getFormData();
if (formsData) {
const formsDataPath = saveKey + '/formsdata.json';
const formsBuffer = Buffer.from(JSON.stringify(formsData), 'utf8');
yield storage.putObject(ctx, formsDataPath, formsBuffer, formsBuffer.length);
const formsDataUrl = yield storage.getSignedUrl(ctx, baseUrl, formsDataPath, commonDefines.c_oAscUrlTypes.Temporary);
outputSfc.setFormsDataUrl(formsDataUrl);
}
if (!isError || isErrorCorrupted) {
try {
const forgotten = yield storage.listObjects(ctx, docId, tenForgottenFiles);
let isSendHistory = 0 === forgotten.length;
if (!isSendHistory) {
//check indicator file to determine if opening was from the forgotten file
const forgottenMarkPath = docId + '/' + tenForgottenFilesName + '.txt';
const forgottenMark = yield storage.listObjects(ctx, forgottenMarkPath);
isOpenFromForgotten = 0 !== forgottenMark.length;
isSendHistory = !isOpenFromForgotten;
ctx.logger.debug('commandSfcCallback forgotten no empty: isSendHistory = %s', isSendHistory);
}
if (isSendHistory && !isEncrypted) {
//don't send history info because changes isn't from file in storage
const data = yield storage.getObject(ctx, savePathHistory);
outputSfc.setChangeHistory(JSON.parse(data.toString('utf-8')));
const changeUrl = yield storage.getSignedUrl(ctx, baseUrl, savePathChanges, commonDefines.c_oAscUrlTypes.Temporary);
outputSfc.setChangeUrl(changeUrl);
} else {
//for backward compatibility. remove this when Community is ready
outputSfc.setChangeHistory({});
}
const url = yield storage.getSignedUrl(ctx, baseUrl, savePathDoc, commonDefines.c_oAscUrlTypes.Temporary);
outputSfc.setUrl(url);
outputSfc.setExtName(pathModule.extname(savePathDoc));
} catch (e) {
ctx.logger.error('Error commandSfcCallback: %s', e.stack);
}
if (outputSfc.getUrl() && outputSfc.getUsers().length > 0) {
outputSfc.setStatus(statusOk);
} else {
isError = true;
}
}
if (isError) {
outputSfc.setStatus(statusErr);
}
if (isSfcm) {
const selectRes = yield taskResult.select(ctx, docId);
const row = selectRes.length > 0 ? selectRes[0] : null;
//send only if FileStatus.Ok to prevent forcesave after final save
if (row && row.status == commonDefines.FileStatus.Ok) {
if (forceSave) {
const forceSaveDate = forceSave.getTime() ? new Date(forceSave.getTime()) : new Date();
outputSfc.setForceSaveType(forceSaveType);
outputSfc.setLastSave(forceSaveDate.toISOString());
}
if (forceSave && forceSaveType === commonDefines.c_oAscForceSaveTypes.Internal) {
//send to browser only if internal forcesave
isSfcmSuccess = true;
} else {
try {
if (wopiParams) {
if (outputSfc.getUrl()) {
if (forceSaveType === commonDefines.c_oAscForceSaveTypes.Form) {
yield processWopiSaveAs(ctx, cmd);
replyStr = JSON.stringify({error: 0});
} else {
const isAutoSave =
forceSaveType !== commonDefines.c_oAscForceSaveTypes.Button && forceSaveType !== commonDefines.c_oAscForceSaveTypes.Form;
replyStr = yield processWopiPutFile(ctx, docId, wopiParams, savePathDoc, userLastChangeId, true, isAutoSave, false);
}
} else {
replyStr = JSON.stringify({error: 1, descr: 'wopi: no file'});
}
} else {
replyStr = yield docsCoServer.sendServerRequest(ctx, uri, outputSfc, checkAndFixAuthorizationLength);
}
const replyData = docsCoServer.parseReplyData(ctx, replyStr);
isSfcmSuccess = replyData && commonDefines.c_oAscServerCommandErrors.NoError == replyData.error;
if (replyData && commonDefines.c_oAscServerCommandErrors.NoError != replyData.error) {
ctx.logger.warn('sendServerRequest returned an error: data = %s', replyStr);
}
} catch (err) {
ctx.logger.error('sendServerRequest error: url = %s;data = %j %s', uri, outputSfc, err.stack);
}
}
}
} else {
//if anybody in document stop save
const editorsCount = yield docsCoServer.getEditorsCountPromise(ctx, docId);
ctx.logger.debug('commandSfcCallback presence: count = %d', editorsCount);
if (0 === editorsCount || (isEncrypted && 1 === editorsCount)) {
if (!updateIfRes) {
updateIfRes = yield taskResult.updateIf(ctx, updateIfTask, updateMask);
}
if (updateIfRes.affectedRows > 0) {
const actualForceSave = yield docsCoServer.editorData.getForceSave(ctx, docId);
const forceSaveDate = actualForceSave && actualForceSave.time ? new Date(actualForceSave.time) : new Date();
const notModified = actualForceSave && true === actualForceSave.ended;
outputSfc.setLastSave(forceSaveDate.toISOString());
outputSfc.setNotModified(notModified);
updateMask.status = updateIfTask.status;
updateMask.statusInfo = updateIfTask.statusInfo;
try {
if (wopiParams) {
if (outputSfc.getUrl()) {
replyStr = yield processWopiPutFile(ctx, docId, wopiParams, savePathDoc, userLastChangeId, !notModified, false, true);
} else {
replyStr = JSON.stringify({error: 1, descr: 'wopi: no file'});
}
} else {
replyStr = yield docsCoServer.sendServerRequest(ctx, uri, outputSfc, checkAndFixAuthorizationLength);
}
} catch (err) {
ctx.logger.error('sendServerRequest error: url = %s;data = %j %s', uri, outputSfc, err.stack);
const retryHttpStatus = new MultiRange(tenCallbackBackoffOptions.httpStatus);
if (!isEncrypted && !docsCoServer.getIsShutdown() && (!err.statusCode || retryHttpStatus.has(err.statusCode.toString()))) {
const attempt = cmd.getAttempt() || 0;
if (attempt < tenCallbackBackoffOptions.retries) {
needRetry = true;
} else {
ctx.logger.warn('commandSfcCallback backoff limit exceeded');
}
}
}
let requestRes = false;
const replyData = docsCoServer.parseReplyData(ctx, replyStr);
if (replyData && commonDefines.c_oAscServerCommandErrors.NoError == replyData.error) {
//in the case of a community server, a request will come to the Command Service, check the result
const savedVal = yield docsCoServer.editorData.getdelSaved(ctx, docId);
requestRes = null == savedVal || '1' === savedVal;
}
if (replyData && commonDefines.c_oAscServerCommandErrors.NoError != replyData.error) {
ctx.logger.warn('sendServerRequest returned an error: data = %s', replyStr);
}
if (requestRes) {
isSfcSuccess = true;
updateIfTask = undefined;
yield docsCoServer.cleanDocumentOnExitPromise(ctx, docId, true, callbackUserIndex);
if (isOpenFromForgotten) {
//remove forgotten file in cache
yield cleanupCache(ctx, docId);
}
if (lastOpenDate) {
//todo error case
const time = new Date() - lastOpenDate;
ctx.logger.debug('commandSfcCallback saveAfterEditingSessionClosed=%d', time);
if (clientStatsD) {
clientStatsD.timing('coauth.saveAfterEditingSessionClosed', time);
}
}
} else {
storeForgotten = true;
}
} else {
updateIfTask = undefined;
needUpdateVersionEvent = false;
}
} else {
needUpdateVersionEvent = false;
}
}
} else {
ctx.logger.warn('Empty Callback=%s or baseUrl=%s or userLastChangeId=%s commandSfcCallback', uri, baseUrl, userLastChangeId);
storeForgotten = true;
}
if (undefined !== updateIfTask && !isSfcm) {
ctx.logger.debug('commandSfcCallback restore %d status', recoverTask.status);
updateIfTask.status = recoverTask.status;
updateIfTask.statusInfo = recoverTask.statusInfo;
updateIfRes = yield taskResult.updateIf(ctx, updateIfTask, updateMask);
if (updateIfRes.affectedRows > 0) {
updateMask.status = updateIfTask.status;
updateMask.statusInfo = updateIfTask.statusInfo;
} else {
ctx.logger.debug('commandSfcCallback restore %d status failed', recoverTask.status);
}
}
if (storeForgotten && !needRetry && !isEncrypted && (!isError || isErrorCorrupted)) {
try {
ctx.logger.warn('storeForgotten');
const forgottenName = tenForgottenFilesName + pathModule.extname(cmd.getOutputPath());
yield storage.copyObject(ctx, savePathDoc, docId + '/' + forgottenName, undefined, tenForgottenFiles);
} catch (err) {
ctx.logger.error('Error storeForgotten: %s', err.stack);
}
if (!isSfcm) {
//todo simultaneous opening
//clean redis (redisKeyPresenceSet and redisKeyPresenceHash removed with last element)
yield docsCoServer.editorData.cleanDocumentOnExit(ctx, docId);
if (docsCoServer.getIsPreStop() && docsCoServer?.editorStatProxy?.deleteKey) {
yield docsCoServer.editorStatProxy.deleteKey(docId);
}
//to unlock wopi file
yield docsCoServer.unlockWopiDoc(ctx, docId, callbackUserIndex);
//cleanupRes can be false in case of simultaneous opening. it is OK
const cleanupRes = yield cleanupCacheIf(ctx, updateMask);
ctx.logger.debug('storeForgotten cleanupRes=%s', cleanupRes);
}
}
if (forceSave) {
yield* docsCoServer.setForceSave(ctx, docId, forceSave, cmd, isSfcmSuccess && !isError, outputSfc?.getUrl());
}
if (needRetry) {
const attempt = cmd.getAttempt() || 0;
cmd.setAttempt(attempt + 1);
const queueData = new commonDefines.TaskQueueData();
queueData.setCtx(ctx);
queueData.setCmd(cmd);
const timeout = retry.createTimeout(attempt, tenCallbackBackoffOptions.timeout);
ctx.logger.debug('commandSfcCallback backoff timeout = %d', timeout);
yield* docsCoServer.addDelayed(queueData, timeout);
}
} else {
ctx.logger.debug('commandSfcCallback cleanDocumentOnExitNoChangesPromise');
yield docsCoServer.cleanDocumentOnExitNoChangesPromise(ctx, docId, undefined, userLastChangeIndex, true);
}
if (needUpdateVersionEvent && !needRetry) {
yield docsCoServer.publish(ctx, {type: commonDefines.c_oPublishType.updateVersion, ctx, docId, success: isSfcSuccess});
}
if ((docsCoServer.getIsShutdown() && !isSfcm) || cmd.getRedisKey()) {
const keyRedis = cmd.getRedisKey() ? cmd.getRedisKey() : redisKeyShutdown;
yield docsCoServer.editorStat.removeShutdown(keyRedis, docId);
}
ctx.logger.debug('End commandSfcCallback');
return replyStr;
});
function* processWopiPutFile(ctx, docId, wopiParams, savePathDoc, userLastChangeId, isModifiedByUser, isAutosave, isExitSave) {
let res = '{"error": 1}';
const metadata = yield storage.headObject(ctx, savePathDoc);
const streamObj = yield storage.createReadStream(ctx, savePathDoc);
const postRes = yield wopiClient.putFile(
ctx,
wopiParams,
null,
streamObj.readStream,
metadata.ContentLength,
userLastChangeId,
isModifiedByUser,
isAutosave,
isExitSave
);
if (postRes) {
res = '{"error": 0}';
const body = wopiClient.parsePutFileResponse(ctx, postRes);
//collabora nexcloud connector
if (body?.LastModifiedTime) {
const lastModifiedTimeInfo = wopiClient.getWopiModifiedMarker(wopiParams, body.LastModifiedTime);
yield commandOpenStartPromise(ctx, docId, undefined, lastModifiedTimeInfo);
}
}
return res;
}
function* commandSendMMCallback(ctx, cmd) {
const docId = cmd.getDocId();
ctx.logger.debug('Start commandSendMMCallback');
const saveKey = docId + cmd.getSaveKey();
const statusInfo = cmd.getStatusInfo();
const outputSfc = new commonDefines.OutputSfcData(docId);
if (constants.NO_ERROR == statusInfo) {
outputSfc.setStatus(docsCoServer.c_oAscServerStatus.MailMerge);
} else {
outputSfc.setStatus(docsCoServer.c_oAscServerStatus.Corrupted);
}
const mailMergeSendData = cmd.getMailMergeSend();
const outputMailMerge = new commonDefines.OutputMailMerge(mailMergeSendData);
outputSfc.setMailMerge(outputMailMerge);
outputSfc.setUsers([mailMergeSendData.getUserId()]);
const data = yield storage.getObject(ctx, saveKey + '/' + cmd.getOutputPath());
const xml = data.toString('utf8');
const files = xml.match(/[< ]file.*?\/>/g);
const recordRemain = mailMergeSendData.getRecordTo() - mailMergeSendData.getRecordFrom() + 1;
const recordIndexStart = mailMergeSendData.getRecordCount() - recordRemain;
for (let i = 0; i < files.length; ++i) {
const file = files[i];
const fieldRes = /field=["'](.*?)["']/.exec(file);
outputMailMerge.setTo(fieldRes[1]);
outputMailMerge.setRecordIndex(recordIndexStart + i);
const pathRes = /path=["'](.*?)["']/.exec(file);
const signedUrl = yield storage.getSignedUrl(
ctx,
mailMergeSendData.getBaseUrl(),
saveKey + '/' + pathRes[1],
commonDefines.c_oAscUrlTypes.Temporary
);
outputSfc.setUrl(signedUrl);
outputSfc.setExtName(pathModule.extname(pathRes[1]));
const uri = mailMergeSendData.getUrl();
let replyStr = null;
try {
replyStr = yield docsCoServer.sendServerRequest(ctx, uri, outputSfc);
} catch (err) {
replyStr = null;
ctx.logger.error('sendServerRequest error: url = %s;data = %j %s', uri, outputSfc, err.stack);
}
const replyData = docsCoServer.parseReplyData(ctx, replyStr);
if (!(replyData && commonDefines.c_oAscServerCommandErrors.NoError == replyData.error)) {
let recordErrorCount = mailMergeSendData.getRecordErrorCount();
recordErrorCount++;
outputMailMerge.setRecordErrorCount(recordErrorCount);
mailMergeSendData.setRecordErrorCount(recordErrorCount);
}
if (replyData && commonDefines.c_oAscServerCommandErrors.NoError != replyData.error) {
ctx.logger.warn('sendServerRequest returned an error: data = %s', docId, replyStr);
}
}
const newRecordFrom = mailMergeSendData.getRecordFrom() + Math.max(files.length, 1);
if (newRecordFrom <= mailMergeSendData.getRecordTo()) {
mailMergeSendData.setRecordFrom(newRecordFrom);
yield* addRandomKeyTaskCmd(ctx, cmd);
const queueData = getSaveTask(ctx, cmd);
yield* docsCoServer.addTask(queueData, constants.QUEUE_PRIORITY_LOW);
} else {
ctx.logger.debug('End MailMerge');
}
ctx.logger.debug('End commandSendMMCallback');
}
exports.openDocument = function (ctx, conn, cmd, opt_upsertRes, opt_bIsRestore) {
return co(function* () {
let outputData;
try {
let startDate = null;
if (clientStatsD) {
startDate = new Date();
}
ctx.logger.debug('Start command: %s', JSON.stringify(cmd));
outputData = new OutputData(cmd.getCommand());
let res = true;
switch (cmd.getCommand()) {
case 'open':
yield* commandOpen(ctx, conn, cmd, outputData, opt_upsertRes, opt_bIsRestore);
break;
case 'reopen':
res = yield* commandReopen(ctx, conn, cmd, outputData);
break;
case 'imgurls':
yield* commandImgurls(ctx, conn, cmd, outputData);
break;
case 'pathurl':
yield* commandPathUrl(ctx, conn, cmd, outputData);
break;
case 'pathurls':
yield* commandPathUrls(ctx, conn, cmd.getData(), outputData);
break;
case 'setpassword':
yield* commandSetPassword(ctx, conn, cmd, outputData);
break;
case 'changedocinfo':
yield* commandChangeDocInfo(ctx, conn, cmd, outputData);
break;
default:
res = false;
break;
}
if (!res) {
outputData.setStatus('err');
outputData.setData(constants.UNKNOWN);
}
if (clientStatsD) {
clientStatsD.timing('coauth.openDocument.' + cmd.getCommand(), new Date() - startDate);
}
} catch (e) {
ctx.logger.error('Error openDocument: %s', e.stack);
if (!outputData) {
outputData = new OutputData();
}
outputData.setStatus('err');
outputData.setData(constants.UNKNOWN);
} finally {
if (outputData?.getStatus()) {
ctx.logger.debug('Response command: %s', JSON.stringify(outputData));
docsCoServer.sendData(ctx, conn, new OutputDataWrap('documentOpen', outputData));
}
ctx.logger.debug('End command');
}
});
};
exports.downloadAs = function (req, res) {
return co(function* () {
let docId = 'null';
const ctx = new operationContext.Context();
try {
let startDate = null;
if (clientStatsD) {
startDate = new Date();
}
ctx.initFromRequest(req);
yield ctx.initTenantCache();
const strCmd = req.query['cmd'];
const cmd = new commonDefines.InputCommand(JSON.parse(strCmd));
docId = cmd.getDocId();
let userId = cmd.getUserId();
ctx.setDocId(docId);
ctx.setUserId(userId);
ctx.logger.debug('Start downloadAs: %s', strCmd);
const tenTokenEnableBrowser = ctx.getCfg('services.CoAuthoring.token.enable.browser', cfgTokenEnableBrowser);
if (tenTokenEnableBrowser || cmd.getTokenDownload() || cmd.getTokenSession()) {
let isValidJwt = false;
if (cmd.getTokenDownload()) {
const checkJwtRes = yield docsCoServer.checkJwt(ctx, cmd.getTokenDownload(), commonDefines.c_oAscSecretType.Browser);
if (checkJwtRes.decoded) {
isValidJwt = true;
cmd.setFormat(checkJwtRes.decoded.fileType);
cmd.setUrl(checkJwtRes.decoded.url);
cmd.setWithAuthorization(true);
} else {
ctx.logger.warn('Error downloadAs jwt: %s', checkJwtRes.description);
}
} else {
const checkJwtRes = yield docsCoServer.checkJwt(ctx, cmd.getTokenSession(), commonDefines.c_oAscSecretType.Session);
if (checkJwtRes.decoded) {
const decoded = checkJwtRes.decoded;
const doc = checkJwtRes.decoded.document;
if (!doc.permissions || false !== doc.permissions.download || false !== doc.permissions.print) {
isValidJwt = true;
docId = doc.key;
cmd.setDocId(doc.key);
userId = decoded.editorConfig?.user?.id;
cmd.setUserIndex(decoded.editorConfig?.user?.index);
} else {
ctx.logger.warn('Error downloadAs jwt: %s', 'access deny');
}
} else {
ctx.logger.warn('Error downloadAs jwt: %s', checkJwtRes.description);
}
}
if (!isValidJwt) {
res.sendStatus(403);
return;
}
}
ctx.setDocId(docId);
ctx.setUserId(userId);
const selectRes = yield taskResult.select(ctx, docId);
const row = selectRes.length > 0 ? selectRes[0] : null;
if (!cmd.getWithoutPassword()) {
addPasswordToCmd(ctx, cmd, row && row.password, row && row.change_id);
}
addOriginFormat(ctx, cmd, row);
cmd.setData(req.body);
const outputData = new OutputData(cmd.getCommand());
switch (cmd.getCommand()) {
case 'save':
yield* commandSave(ctx, cmd, outputData);
break;
case 'savefromorigin':
yield* commandSaveFromOrigin(ctx, cmd, outputData, row && row.password);
break;
case 'sendmm':
yield* commandSendMailMerge(ctx, cmd, outputData);
break;
default:
outputData.setStatus('err');
outputData.setData(constants.UNKNOWN);
break;
}
const strRes = JSON.stringify(outputData);
res.setHeader('Content-Type', 'application/json');
res.send(strRes);
ctx.logger.debug('End downloadAs: %s', strRes);
if (clientStatsD) {
clientStatsD.timing('coauth.downloadAs.' + cmd.getCommand(), new Date() - startDate);
}
} catch (e) {
ctx.logger.error('Error downloadAs: %s', e.stack);
res.sendStatus(400);
}
});
};
exports.saveFile = function (req, res) {
return co(function* () {
let docId = 'null';
const ctx = new operationContext.Context();
try {
let startDate = null;
if (clientStatsD) {
startDate = new Date();
}
ctx.initFromRequest(req);
yield ctx.initTenantCache();
const strCmd = req.query['cmd'];
const cmd = new commonDefines.InputCommand(JSON.parse(strCmd));
docId = cmd.getDocId();
ctx.setDocId(docId);
ctx.logger.debug('Start saveFile');
let isValidJwt = false;
const checkJwtRes = yield docsCoServer.checkJwt(ctx, cmd.getTokenSession(), commonDefines.c_oAscSecretType.Session);
if (checkJwtRes.decoded) {
const doc = checkJwtRes.decoded.document;
const edit = checkJwtRes.decoded.editorConfig;
if (doc.ds_encrypted && !edit.ds_view && !edit.ds_isCloseCoAuthoring) {
isValidJwt = true;
docId = doc.key;
cmd.setDocId(doc.key);
} else {
ctx.logger.warn('Error saveFile jwt: %s', 'access deny');
}
} else {
ctx.logger.warn('Error saveFile jwt: %s', checkJwtRes.description);
}
if (!isValidJwt) {
res.sendStatus(403);
return;
}
ctx.setDocId(docId);
cmd.setStatusInfo(constants.NO_ERROR);
yield* addRandomKeyTaskCmd(ctx, cmd);
cmd.setOutputPath(constants.OUTPUT_NAME + pathModule.extname(cmd.getOutputPath()));
yield storage.putObject(ctx, docId + cmd.getSaveKey() + '/' + cmd.getOutputPath(), req.body, req.body.length);
const replyStr = yield commandSfcCallback(ctx, cmd, false, true);
if (replyStr) {
utils.fillResponseSimple(res, replyStr, 'application/json');
} else {
res.sendStatus(400);
}
ctx.logger.debug('End saveFile: %s', replyStr);
if (clientStatsD) {
clientStatsD.timing('coauth.saveFile', new Date() - startDate);
}
} catch (e) {
ctx.logger.error('Error saveFile: %s', e.stack);
res.sendStatus(400);
}
});
};
function getPrintFileUrl(ctx, docId, baseUrl, filename) {
return co(function* () {
const tenTokenSessionAlgorithm = ctx.getCfg('services.CoAuthoring.token.session.algorithm', cfgTokenSessionAlgorithm);
const tenTokenSessionExpires = ms(ctx.getCfg('services.CoAuthoring.token.session.expires', cfgTokenSessionExpires));
baseUrl = utils.checkBaseUrl(ctx, baseUrl);
const payload = {document: {key: docId}};
const token = yield docsCoServer.signToken(
ctx,
payload,
tenTokenSessionAlgorithm,
tenTokenSessionExpires / 1000,
commonDefines.c_oAscSecretType.Session
);
//while save printed file Chrome's extension seems to rely on the resource name set in the URI https://stackoverflow.com/a/53593453
//replace '/' with %2f before encodeURIComponent becase nginx determine %2f as '/' and get wrong system path
const userFriendlyName = encodeURIComponent(filename.replace(/\//g, '%2f'));
let res = `${baseUrl}/printfile/${encodeURIComponent(docId)}/${userFriendlyName}?token=${encodeURIComponent(token)}`;
if (ctx.shardKey) {
res += `&${constants.SHARD_KEY_API_NAME}=${encodeURIComponent(ctx.shardKey)}`;
}
if (ctx.wopiSrc) {
res += `&${constants.SHARD_KEY_WOPI_NAME}=${encodeURIComponent(ctx.wopiSrc)}`;
}
if (ctx.userSessionId) {
res += `&${constants.USER_SESSION_ID_NAME}=${encodeURIComponent(ctx.userSessionId)}`;
}
res += `&filename=${userFriendlyName}`;
return res;
});
}
exports.getPrintFileUrl = getPrintFileUrl;
exports.printFile = function (req, res) {
return co(function* () {
let docId = 'null';
const ctx = new operationContext.Context();
try {
let startDate = null;
if (clientStatsD) {
startDate = new Date();
}
ctx.initFromRequest(req);
yield ctx.initTenantCache();
const filename = req.query['filename'];
const token = req.query['token'];
docId = req.params.docid;
ctx.setDocId(docId);
ctx.logger.info('Start printFile');
const checkJwtRes = yield docsCoServer.checkJwt(ctx, token, commonDefines.c_oAscSecretType.Session);
if (checkJwtRes.decoded) {
const docIdBase = checkJwtRes.decoded.document.key;
if (!docId.startsWith(docIdBase)) {
ctx.logger.warn('Error printFile jwt: description = %s', 'access deny');
res.sendStatus(403);
return;
}
} else {
ctx.logger.warn('Error printFile jwt: description = %s', checkJwtRes.description);
res.sendStatus(403);
return;
}
ctx.setDocId(docId);
const streamObj = yield storage.createReadStream(ctx, `${docId}/${constants.OUTPUT_NAME}.pdf`);
res.setHeader('Content-Disposition', utils.getContentDisposition(filename, null, constants.CONTENT_DISPOSITION_INLINE));
res.setHeader('Content-Length', streamObj.contentLength);
res.setHeader('Content-Type', 'application/pdf');
yield utils.pipeHttpStreams(streamObj.readStream, res);
if (clientStatsD) {
clientStatsD.timing('coauth.printFile', new Date() - startDate);
}
} catch (e) {
ctx.logger.error('Error printFile: %s', e.stack);
res.sendStatus(400);
} finally {
ctx.logger.info('End printFile');
}
});
};
/**
* Proxy download file request to the file storage
* @param {object} req - The HTTP request object
* @param {object} res - The HTTP response object
* @returns {Promise}
*/
exports.downloadFile = function (req, res) {
return co(function* () {
const ctx = new operationContext.Context();
let stream = null;
try {
let startDate = null;
if (clientStatsD) {
startDate = new Date();
}
const docId = req.params.docid;
if (!docId) {
res.status(400).send('docid is required');
return;
}
ctx.initFromRequest(req);
yield ctx.initTenantCache();
ctx.setDocId(docId);
//todo remove in 8.1. For compatibility
let url = req.get('x-url');
if (url) {
url = decodeURI(url);
}
ctx.logger.info('Start downloadFile');
const tenTokenEnableBrowser = ctx.getCfg('services.CoAuthoring.token.enable.browser', cfgTokenEnableBrowser);
const tenDownloadMaxBytes = ctx.getCfg('FileConverter.converter.maxDownloadBytes', cfgDownloadMaxBytes);
const tenDownloadTimeout = ctx.getCfg('FileConverter.converter.downloadTimeout', cfgDownloadTimeout);
const tenDownloadFileAllowExt = ctx.getCfg('services.CoAuthoring.server.downloadFileAllowExt', cfgDownloadFileAllowExt);
const tenNewFileTemplate = ctx.getCfg('services.CoAuthoring.server.newFileTemplate', cfgNewFileTemplate);
let authorization;
let isInJwtToken = false;
let errorDescription;
let headers, fromTemplate;
const authRes = yield docsCoServer.getRequestParams(ctx, req);
if (authRes.code === constants.NO_ERROR) {
const decoded = authRes.params;
if (decoded.changesUrl) {
url = decoded.changesUrl;
isInJwtToken = true;
} else if (decoded.document && -1 !== tenDownloadFileAllowExt.indexOf(decoded.document.fileType)) {
url = decoded.document.url;
isInJwtToken = true;
} else if (decoded.url && -1 !== tenDownloadFileAllowExt.indexOf(decoded.fileType)) {
url = decoded.url;
isInJwtToken = true;
} else if (wopiClient.isWopiJwtToken(decoded)) {
if (decoded.fileInfo.Size === 0) {
//editnew case
fromTemplate = pathModule.extname(decoded.fileInfo.BaseFileName).substring(1);
} else {
({url, headers} = yield wopiUtils.getWopiFileUrl(ctx, decoded.fileInfo, decoded.userAuth));
const filterStatus = yield wopiClient.checkIpFilter(ctx, url);
if (0 === filterStatus) {
//todo false? (true because it passed checkIpFilter for wopi)
//todo use directIfIn
isInJwtToken = true;
} else {
errorDescription = 'access deny';
}
}
} else if (!tenTokenEnableBrowser) {
//todo token required
if (decoded.url) {
url = decoded.url;
isInJwtToken = true;
}
} else {
errorDescription = 'access deny';
}
} else {
errorDescription = authRes.description || 'need token';
}
if (errorDescription) {
ctx.logger.warn('Error downloadFile jwt: description = %s', errorDescription);
res.sendStatus(403);
return;
}
if (fromTemplate) {
ctx.logger.debug('downloadFile from file template: %s', fromTemplate);
const locale = constants.TEMPLATES_DEFAULT_LOCALE;
const fileTemplatePath = pathModule.join(tenNewFileTemplate, locale, 'new.' + fromTemplate);
res.sendFile(pathModule.resolve(fileTemplatePath));
} else {
if (utils.canIncludeOutboxAuthorization(ctx, url)) {
const secret = yield tenantManager.getTenantSecret(ctx, commonDefines.c_oAscSecretType.Outbox);
authorization = utils.fillJwtForRequest(ctx, {url}, secret, false);
}
const urlParsed = urlModule.parse(url);
const filterStatus = yield* utils.checkHostFilter(ctx, urlParsed.hostname);
if (0 !== filterStatus) {
ctx.logger.warn('Error downloadFile checkIpFilter error: url = %s', url);
res.sendStatus(filterStatus);
return;
}
if (req.get('Range')) {
if (!headers) {
headers = {};
}
headers['Range'] = req.get('Range');
}
const downloadResult = yield utils.downloadUrlPromise(
ctx,
url,
tenDownloadTimeout,
tenDownloadMaxBytes,
authorization,
isInJwtToken,
headers,
true
);
const response = downloadResult.response;
stream = downloadResult.stream;
// Sanitize Content-Disposition by removing control chars (prevents CRLF/header injection)
if (response.headers['content-disposition']) {
response.headers['content-disposition'] = response.headers['content-disposition'].replace(/\p{Cc}/gu, '');
}
//Set-Cookie resets browser session
delete response.headers['set-cookie'];
// Set the response headers to match the target response
res.set(response.headers);
// Use pipeline to pipe the response data to the client
yield pipeline(stream, res);
}
if (clientStatsD) {
clientStatsD.timing('coauth.downloadFile', new Date() - startDate);
}
} catch (err) {
if (err.code === 'ERR_STREAM_PREMATURE_CLOSE') {
ctx.logger.debug('Error downloadFile: %s', err.stack);
if (!res.headersSent) {
res.sendStatus(499);
}
} else {
ctx.logger.error('Error downloadFile: %s', err.stack);
//catch errors because status may be sent while piping to response
if (!res.headersSent) {
try {
if (err.code === 'ETIMEDOUT' || err.code === 'ESOCKETTIMEDOUT') {
res.sendStatus(408);
} else if (err.code === 'EMSGSIZE') {
res.sendStatus(413);
} else if (err.statusCode) {
res.sendStatus(err.statusCode);
} else {
res.sendStatus(400);
}
} catch (err) {
ctx.logger.error('Error downloadFile: %s', err.stack);
}
}
}
} finally {
// Ensure stream is properly destroyed
if (stream && typeof stream.destroy === 'function') {
try {
stream.destroy();
} catch (destroyErr) {
ctx.logger.warn('Error destroying stream: %s', destroyErr.stack);
}
}
ctx.logger.info('End downloadFile');
}
});
};
exports.saveFromChanges = function (ctx, docId, statusInfo, optFormat, opt_userId, opt_userIndex, opt_userLcid, opt_queue, opt_initShardKey) {
return co(function* () {
try {
let startDate = null;
if (clientStatsD) {
startDate = new Date();
}
ctx.logger.debug('Start saveFromChanges');
//we do a select, because during the timeout the information could change
const selectRes = yield taskResult.select(ctx, docId);
const row = selectRes.length > 0 ? selectRes[0] : null;
if (row && row.status == commonDefines.FileStatus.SaveVersion && row.status_info == statusInfo) {
if (null == optFormat) {
optFormat = changeFormatByOrigin(ctx, row, constants.AVS_OFFICESTUDIO_FILE_OTHER_OOXML);
}
if (opt_initShardKey) {
ctx.setShardKey(sqlBase.DocumentAdditional.prototype.getShardKey(row.additional));
ctx.setWopiSrc(sqlBase.DocumentAdditional.prototype.getWopiSrc(row.additional));
}
const cmd = new commonDefines.InputCommand();
cmd.setCommand('sfc');
cmd.setDocId(docId);
cmd.setOutputFormat(optFormat);
cmd.setStatusInfoIn(statusInfo);
cmd.setUserActionId(opt_userId);
cmd.setUserActionIndex(opt_userIndex);
cmd.appendJsonParams(getOpenedAtJSONParams(row));
//todo lang and region are different
cmd.setLCID(opt_userLcid);
const userAuthStr = sqlBase.UserCallback.prototype.getCallbackByUserIndex(ctx, row.callback);
cmd.setWopiParams(wopiClient.parseWopiCallback(ctx, userAuthStr, row.callback));
addPasswordToCmd(ctx, cmd, row && row.password, row && row.change_id);
addOriginFormat(ctx, cmd, row);
yield* addRandomKeyTaskCmd(ctx, cmd);
const queueData = getSaveTask(ctx, cmd);
queueData.setFromChanges(true);
yield* docsCoServer.addTask(queueData, constants.QUEUE_PRIORITY_NORMAL, opt_queue);
if (docsCoServer.getIsShutdown()) {
yield docsCoServer.editorStat.addShutdown(redisKeyShutdown, docId);
}
ctx.logger.debug('AddTask saveFromChanges');
} else {
if (row) {
ctx.logger.debug('saveFromChanges status mismatch: row: %d; %d; expected: %d', row.status, row.status_info, statusInfo);
}
}
if (clientStatsD) {
clientStatsD.timing('coauth.saveFromChanges', new Date() - startDate);
}
} catch (e) {
ctx.logger.error('Error saveFromChanges: %s', e.stack);
}
});
};
async function processWopiSaveAs(ctx, cmd) {
let res;
const info = await docsCoServer.getCallback(ctx, cmd.getDocId(), cmd.getUserIndex());
// info.wopiParams is null if it is not wopi
if (info?.wopiParams) {
const suggestedExt = `.${formatChecker.getStringFromFormat(cmd.getOutputFormat())}`;
const suggestedTarget = cmd.getSaveAsPath();
const storageFilePath = `${cmd.getDocId()}${cmd.getSaveKey()}/${cmd.getOutputPath()}`;
const stream = await storage.createReadStream(ctx, storageFilePath);
const {wopiSrc, access_token} = info.wopiParams.userAuth;
res = await wopiClient.putRelativeFile(
ctx,
wopiSrc,
access_token,
null,
stream.readStream,
stream.contentLength,
suggestedExt,
suggestedTarget,
false
);
}
return {res, wopiParams: info?.wopiParams};
}
exports.receiveTask = function (data, ack) {
return co(function* () {
const ctx = new operationContext.Context();
try {
const task = new commonDefines.TaskQueueData(JSON.parse(data));
if (task) {
const cmd = task.getCmd();
ctx.initFromTaskQueueData(task);
yield ctx.initTenantCache();
ctx.logger.info('receiveTask start: %s', data);
const updateTask = yield getUpdateResponse(ctx, cmd);
const updateRes = yield taskResult.update(ctx, updateTask);
if (updateRes.affectedRows > 0) {
const outputData = new OutputData(cmd.getCommand());
const command = cmd.getCommand();
const additionalOutput = {
needUrlKey: null,
needUrlMethod: null,
needUrlType: null,
needUrlIsCorrectPassword: undefined,
creationDate: undefined,
openedAt: undefined,
row: undefined
};
if ('open' === command || 'reopen' === command) {
yield getOutputData(ctx, cmd, outputData, cmd.getDocId(), null, additionalOutput);
//wopi from TemplateSource
if (additionalOutput.row) {
const row = additionalOutput.row;
const userAuthStr = sqlBase.UserCallback.prototype.getCallbackByUserIndex(ctx, row.callback);
const wopiParams = wopiClient.parseWopiCallback(ctx, userAuthStr, row.callback);
if (wopiParams?.commonInfo?.fileInfo?.TemplateSource) {
ctx.logger.debug('receiveTask: save document opened from TemplateSource');
//todo
//no need to wait to open file faster
void docsCoServer.startForceSave(
ctx,
cmd.getDocId(),
commonDefines.c_oAscForceSaveTypes.Timeout,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
row.baseurl,
undefined,
undefined,
undefined,
undefined,
undefined,
cmd.getExternalChangeInfo()
);
}
}
} else if ('save' === command || 'savefromorigin' === command) {
const status = yield getOutputData(ctx, cmd, outputData, cmd.getDocId() + cmd.getSaveKey(), null, additionalOutput);
if (commonDefines.FileStatus.Ok === status && (cmd.getSaveAsPath() || cmd.getIsSaveAs())) {
//todo in case of wopi no need to send url. send it to avoid stubs in sdk
const saveAsRes = yield processWopiSaveAs(ctx, cmd);
if (!saveAsRes.res && saveAsRes.wopiParams) {
outputData.setStatus('err');
outputData.setData(constants.CONVERT);
additionalOutput.needUrlKey = null;
}
}
} else if ('sfcm' === command) {
yield commandSfcCallback(ctx, cmd, true);
} else if ('sfc' === command) {
yield commandSfcCallback(ctx, cmd, false);
} else if ('sendmm' === command) {
yield* commandSendMMCallback(ctx, cmd);
} else if ('conv' === command) {
//nothing
}
if (outputData.getStatus()) {
ctx.logger.debug('receiveTask publish: %s', JSON.stringify(outputData));
const output = new OutputDataWrap('documentOpen', outputData);
yield docsCoServer.publish(ctx, {
type: commonDefines.c_oPublishType.receiveTask,
ctx,
cmd,
output,
needUrlKey: additionalOutput.needUrlKey,
needUrlMethod: additionalOutput.needUrlMethod,
needUrlType: additionalOutput.needUrlType,
needUrlIsCorrectPassword: additionalOutput.needUrlIsCorrectPassword,
creationDate: additionalOutput.creationDate,
openedAt: additionalOutput.openedAt
});
}
}
}
} catch (err) {
ctx.logger.error('receiveTask error: %s', err.stack);
} finally {
ctx.logger.info('receiveTask end');
ack();
}
});
};
exports.cleanupCache = cleanupCache;
exports.cleanupCacheIf = cleanupCacheIf;
exports.cleanupErrToReload = cleanupErrToReload;
exports.getOpenedAt = getOpenedAt;
exports.commandSfctByCmd = commandSfctByCmd;
exports.commandOpenStartPromise = commandOpenStartPromise;
exports.commandPathUrls = commandPathUrls;
exports.commandSfcCallback = commandSfcCallback;
exports.OutputDataWrap = OutputDataWrap;
exports.OutputData = OutputData;