2098 lines
86 KiB
JavaScript
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;
|