Files
DocumentServer-v-9.2.0/server/DocService/sources/DocsCoServer.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

4792 lines
186 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
*
*/
/*
-------------------------------------------------- --view-mode----------------------------------------------------- ------------
* 1) For the view mode, we update the page (without a quick transition) so that the user is not considered editable and does not
* held the document for assembly (if you do not wait, then the quick transition from view to edit is incomprehensible when the document has already been assembled)
* 2) If the user is in view mode, then he does not participate in editing (only in chat). When opened, it receives
* all current changes in the document at the time of opening. For view-mode we do not accept changes and do not send them
* view-users (because it is not clear what to do in a situation where 1-user has made changes,
* saved and made undo).
*---------------------------------------------------------------- -------------------------------------------------- --------------------
*------------------------------------------------Scheme save------------------------------------------------- ------
* a) One user - the first time changes come without an index, then changes come with an index, you can do
* undo-redo (history is not rubbed). If autosave is enabled, then it is for any action (no more than 5 seconds).
* b) As soon as the second user enters, co-editing begins. A lock is placed on the document so that
* the first user managed to save the document (or send unlock)
* c) When there are 2 or more users, each save rubs the history and is sent in its entirety (no index). If
* autosave is enabled, it is saved no more than once every 10 minutes.
* d) When the user is left alone, after accepting someone else's changes, point 'a' begins
*---------------------------------------------------------------- -------------------------------------------------- --------------------
*-------------------------------------------- Scheme of working with the server- -------------------------------------------------- -
* a) When everyone leaves, after the cfgAscSaveTimeOutDelay time, the assembly command is sent to the document server.
* b) If the status '1' comes to CommandService.ashx, then it was possible to save and raise the version. Clear callbacks and
* changes from base and from memory.
* c) If a status other than '1' arrives (this can include both the generation of the file and the work of an external subscriber
* with the finished result), then three callbacks, and leave the changes. Because you can go to the old
* version and get uncompiled changes. We also reset the status of the file to unassembled so that it can be
* open without version error message.
*---------------------------------------------------------------- -------------------------------------------------- --------------------
*------------------------------------------------Start server------------------------------------------------- ---------
* 1) Loading information about the collector
* 2) Loading information about callbacks
* 3) We collect only those files that have a callback and information for building
*---------------------------------------------------------------- -------------------------------------------------- --------------------
*------------------------------------------------Reconnect when disconnected--- ------------------------------------
* 1) Check the file for assembly. If it starts, then stop.
* 2) If the assembly has already completed, then we send the user a notification that it is impossible to edit further
* 3) Next, check the time of the last save and lock-and user. If someone has already managed to save or
* lock objects, then we can't edit further.
*---------------------------------------------------------------- -------------------------------------------------- --------------------
* */
'use strict';
const {Server} = require('socket.io');
const _ = require('underscore');
const url = require('url');
const crypto = require('crypto');
const pathModule = require('path');
const {isDeepStrictEqual} = require('util');
const co = require('co');
const jwt = require('jsonwebtoken');
const ms = require('ms');
const bytes = require('bytes');
const storage = require('./../../Common/sources/storage/storage-base');
const constants = require('./../../Common/sources/constants');
const utils = require('./../../Common/sources/utils');
const utilsDocService = require('./utilsDocService');
const commonDefines = require('./../../Common/sources/commondefines');
const statsDClient = require('./../../Common/sources/statsdclient');
const config = require('config');
const sqlBase = require('./databaseConnectors/baseConnector');
const canvasService = require('./canvasservice');
const converterService = require('./converterservice');
const taskResult = require('./taskresult');
const gc = require('./gc');
const shutdown = require('./shutdown');
const pubsubService = require('./pubsubRabbitMQ');
const wopiClient = require('./wopiClient');
const queueService = require('./../../Common/sources/taskqueueRabbitMQ');
const operationContext = require('./../../Common/sources/operationContext');
const tenantManager = require('./../../Common/sources/tenantManager');
const {notificationTypes, ...notificationService} = require('../../Common/sources/notificationService');
const aiProxyHandler = require('./ai/aiProxyHandler');
const cfgEditorDataStorage = config.get('services.CoAuthoring.server.editorDataStorage');
const cfgEditorStatStorage = config.get('services.CoAuthoring.server.editorStatStorage');
const editorDataStorage = require('./' + cfgEditorDataStorage);
const editorStatStorage = require('./' + (cfgEditorStatStorage || cfgEditorDataStorage));
const util = require('util');
const cfgEditSingleton = config.get('services.CoAuthoring.server.edit_singleton');
const cfgEditor = config.get('services.CoAuthoring.editor');
const cfgCallbackRequestTimeout = config.get('services.CoAuthoring.server.callbackRequestTimeout');
//The waiting time to document assembly when all out(not 0 in case of F5 in the browser)
const cfgAscSaveTimeOutDelay = config.get('services.CoAuthoring.server.savetimeoutdelay');
const cfgPubSubMaxChanges = config.get('services.CoAuthoring.pubsub.maxChanges');
const cfgExpSaveLock = config.get('services.CoAuthoring.expire.saveLock');
const cfgExpLockDoc = config.get('services.CoAuthoring.expire.lockDoc');
const cfgExpSessionIdle = config.get('services.CoAuthoring.expire.sessionidle');
const cfgExpSessionAbsolute = config.get('services.CoAuthoring.expire.sessionabsolute');
const cfgExpSessionCloseCommand = config.get('services.CoAuthoring.expire.sessionclosecommand');
const cfgExpUpdateVersionStatus = config.get('services.CoAuthoring.expire.updateVersionStatus');
const cfgTokenEnableBrowser = config.get('services.CoAuthoring.token.enable.browser');
const cfgTokenEnableRequestInbox = config.get('services.CoAuthoring.token.enable.request.inbox');
const cfgTokenSessionAlgorithm = config.get('services.CoAuthoring.token.session.algorithm');
const cfgTokenSessionExpires = config.get('services.CoAuthoring.token.session.expires');
const cfgTokenInboxHeader = config.get('services.CoAuthoring.token.inbox.header');
const cfgTokenInboxPrefix = config.get('services.CoAuthoring.token.inbox.prefix');
const cfgTokenVerifyOptions = config.util.cloneDeep(config.get('services.CoAuthoring.token.verifyOptions'));
const cfgForceSaveEnable = config.get('services.CoAuthoring.autoAssembly.enable');
const cfgForceSaveInterval = config.get('services.CoAuthoring.autoAssembly.interval');
const cfgQueueRetentionPeriod = config.get('queue.retentionPeriod');
const cfgForgottenFiles = config.get('services.CoAuthoring.server.forgottenfiles');
const cfgForgottenFilesName = config.get('services.CoAuthoring.server.forgottenfilesname');
const cfgMaxRequestChanges = config.get('services.CoAuthoring.server.maxRequestChanges');
const cfgWarningLimitPercents = config.get('license.warning_limit_percents');
const cfgNotificationRuleLicenseLimitEdit = config.get('notification.rules.licenseLimitEdit.template');
const cfgNotificationRuleLicenseLimitLiveViewer = config.get('notification.rules.licenseLimitLiveViewer.template');
const cfgErrorFiles = config.get('FileConverter.converter.errorfiles');
const cfgOpenProtectedFile = config.get('services.CoAuthoring.server.openProtectedFile');
const cfgIsAnonymousSupport = config.get('services.CoAuthoring.server.isAnonymousSupport');
const cfgTokenRequiredParams = config.get('services.CoAuthoring.server.tokenRequiredParams');
const cfgImageSize = config.get('services.CoAuthoring.server.limits_image_size');
const cfgTypesUpload = config.get('services.CoAuthoring.utils.limits_image_types_upload');
const cfgForceSaveUsingButtonWithoutChanges = config.get('services.CoAuthoring.server.forceSaveUsingButtonWithoutChanges');
//todo tenant
const cfgExpDocumentsCron = config.get('services.CoAuthoring.expire.documentsCron');
const cfgRefreshLockInterval = ms(config.get('wopi.refreshLockInterval'));
const cfgSocketIoConnection = config.util.cloneDeep(config.get('services.CoAuthoring.socketio.connection'));
const cfgTableResult = config.get('services.CoAuthoring.sql.tableResult');
const cfgTableChanges = config.get('services.CoAuthoring.sql.tableChanges');
const EditorTypes = {
document: 0,
spreadsheet: 1,
presentation: 2,
diagram: 3
};
const defaultHttpPort = 80,
defaultHttpsPort = 443; // Default ports (for http and https)
//todo remove editorDataStorage constructor usage after 8.1
const editorData = editorDataStorage.EditorData ? new editorDataStorage.EditorData() : new editorDataStorage();
const editorStat = editorStatStorage.EditorStat ? new editorStatStorage.EditorStat() : new editorDataStorage();
let editorStatProxy = null;
if (process.env.REDIS_SERVER_DB_KEYS_NUM) {
editorStatProxy = new editorStatStorage.EditorStat(process.env.REDIS_SERVER_DB_KEYS_NUM);
}
const clientStatsD = statsDClient.getClient();
let connections = []; // Active connections
const lockDocumentsTimerId = {}; //to drop connection that can't unlockDocument
let pubsub;
let queue;
let shutdownFlag = false;
let preStopFlag = false;
const expDocumentsStep = gc.getCronStep(cfgExpDocumentsCron);
const MIN_SAVE_EXPIRATION = 60000;
const SHARD_ID = crypto.randomBytes(16).toString('base64'); //16 as guid
const PRECISION = [
{name: 'hour', val: ms('1h')},
{name: 'day', val: ms('1d')},
{name: 'week', val: ms('7d')},
{name: 'month', val: ms('31d')}
];
function getIsShutdown() {
return shutdownFlag;
}
function getIsPreStop() {
return preStopFlag;
}
function getEditorConfig(ctx) {
let tenEditor = ctx.getCfg('services.CoAuthoring.editor', cfgEditor);
tenEditor = JSON.parse(JSON.stringify(tenEditor));
tenEditor['reconnection']['delay'] = ms(tenEditor['reconnection']['delay']);
tenEditor['websocketMaxPayloadSize'] = bytes.parse(tenEditor['websocketMaxPayloadSize']);
tenEditor['maxChangesSize'] = bytes.parse(tenEditor['maxChangesSize']);
return tenEditor;
}
function getForceSaveExpiration(ctx) {
const tenForceSaveInterval = ms(ctx.getCfg('services.CoAuthoring.autoAssembly.interval', cfgForceSaveInterval));
const tenQueueRetentionPeriod = ctx.getCfg('queue.retentionPeriod', cfgQueueRetentionPeriod);
return Math.min(Math.max(tenForceSaveInterval, MIN_SAVE_EXPIRATION), tenQueueRetentionPeriod * 1000);
}
function DocumentChanges(docId) {
this.docId = docId;
this.arrChanges = [];
return this;
}
DocumentChanges.prototype.getLength = function () {
return this.arrChanges.length;
};
DocumentChanges.prototype.push = function (change) {
this.arrChanges.push(change);
};
DocumentChanges.prototype.splice = function (start, deleteCount) {
this.arrChanges.splice(start, deleteCount);
};
DocumentChanges.prototype.slice = function (start, end) {
return this.arrChanges.splice(start, end);
};
DocumentChanges.prototype.concat = function (item) {
this.arrChanges = this.arrChanges.concat(item);
};
const c_oAscServerStatus = {
NotFound: 0,
Editing: 1,
MustSave: 2,
Corrupted: 3,
Closed: 4,
MailMerge: 5,
MustSaveForce: 6,
CorruptedForce: 7
};
const c_oAscChangeBase = {
No: 0,
Delete: 1,
All: 2
};
const c_oAscLockTimeOutDelay = 500; // Timeout to save when database is clamped
const c_oAscRecalcIndexTypes = {
RecalcIndexAdd: 1,
RecalcIndexRemove: 2
};
/**
* lock types
* @const
*/
const c_oAscLockTypes = {
kLockTypeNone: 1, // no one has locked this object
kLockTypeMine: 2, // this object is locked by the current user
kLockTypeOther: 3, // this object is locked by another (not the current) user
kLockTypeOther2: 4, // this object is locked by another (not the current) user (updates have already arrived)
kLockTypeOther3: 5 // this object has been locked (updates have arrived) and is now locked again
};
const c_oAscLockTypeElem = {
Range: 1,
Object: 2,
Sheet: 3
};
const c_oAscLockTypeElemSubType = {
DeleteColumns: 1,
InsertColumns: 2,
DeleteRows: 3,
InsertRows: 4,
ChangeProperties: 5
};
const c_oAscLockTypeElemPresentation = {
Object: 1,
Slide: 2,
Presentation: 3
};
function CRecalcIndexElement(recalcType, position, bIsSaveIndex) {
if (!(this instanceof CRecalcIndexElement)) {
return new CRecalcIndexElement(recalcType, position, bIsSaveIndex);
}
this._recalcType = recalcType; // Type of changes (removal or addition)
this._position = position; // The position where the changes happened
this._count = 1; // We consider all changes as the simplest
this.m_bIsSaveIndex = !!bIsSaveIndex; // These are indexes from other users' changes (that we haven't applied yet)
return this;
}
CRecalcIndexElement.prototype = {
constructor: CRecalcIndexElement,
// recalculate for others
getLockOther(position, type) {
const inc = c_oAscRecalcIndexTypes.RecalcIndexAdd === this._recalcType ? +1 : -1;
if (position === this._position && c_oAscRecalcIndexTypes.RecalcIndexRemove === this._recalcType && true === this.m_bIsSaveIndex) {
// We haven't applied someone else's changes yet (so insert doesn't need to be rendered)
// RecalcIndexRemove (because we flip it for proper processing, from another user
// RecalcIndexAdd arrived
return null;
} else if (
position === this._position &&
c_oAscRecalcIndexTypes.RecalcIndexRemove === this._recalcType &&
c_oAscLockTypes.kLockTypeMine === type &&
false === this.m_bIsSaveIndex
) {
// For the user who deleted the column, draw previously locked cells in this column
// no need
return null;
} else if (position < this._position) {
return position;
} else {
return position + inc;
}
},
// Recalculation for others (save only)
getLockSaveOther(position, type) {
if (this.m_bIsSaveIndex) {
return position;
}
const inc = c_oAscRecalcIndexTypes.RecalcIndexAdd === this._recalcType ? +1 : -1;
if (position === this._position && c_oAscRecalcIndexTypes.RecalcIndexRemove === this._recalcType && true === this.m_bIsSaveIndex) {
// We haven't applied someone else's changes yet (so insert doesn't need to be rendered)
// RecalcIndexRemove (because we flip it for proper processing, from another user
// RecalcIndexAdd arrived
return null;
} else if (
position === this._position &&
c_oAscRecalcIndexTypes.RecalcIndexRemove === this._recalcType &&
c_oAscLockTypes.kLockTypeMine === type &&
false === this.m_bIsSaveIndex
) {
// For the user who deleted the column, draw previously locked cells in this column
// no need
return null;
} else if (position < this._position) {
return position;
} else {
return position + inc;
}
},
// recalculate for ourselves
getLockMe(position) {
const inc = c_oAscRecalcIndexTypes.RecalcIndexAdd === this._recalcType ? -1 : +1;
if (position < this._position) {
return position;
} else {
return position + inc;
}
},
// Only when other users change (for recalculation)
getLockMe2(position) {
const inc = c_oAscRecalcIndexTypes.RecalcIndexAdd === this._recalcType ? -1 : +1;
if (true !== this.m_bIsSaveIndex || position < this._position) {
return position;
} else {
return position + inc;
}
}
};
function CRecalcIndex() {
if (!(this instanceof CRecalcIndex)) {
return new CRecalcIndex();
}
this._arrElements = []; // CRecalcIndexElement array
return this;
}
CRecalcIndex.prototype = {
constructor: CRecalcIndex,
add(recalcType, position, count, bIsSaveIndex) {
for (let i = 0; i < count; ++i) {
this._arrElements.push(new CRecalcIndexElement(recalcType, position, bIsSaveIndex));
}
},
clear() {
this._arrElements.length = 0;
},
getLockOther(position, type) {
let newPosition = position;
const count = this._arrElements.length;
for (let i = 0; i < count; ++i) {
newPosition = this._arrElements[i].getLockOther(newPosition, type);
if (null === newPosition) {
break;
}
}
return newPosition;
},
// Recalculation for others (save only)
getLockSaveOther(position, type) {
let newPosition = position;
const count = this._arrElements.length;
for (let i = 0; i < count; ++i) {
newPosition = this._arrElements[i].getLockSaveOther(newPosition, type);
if (null === newPosition) {
break;
}
}
return newPosition;
},
// recalculate for ourselves
getLockMe(position) {
let newPosition = position;
const count = this._arrElements.length;
for (let i = count - 1; i >= 0; --i) {
newPosition = this._arrElements[i].getLockMe(newPosition);
if (null === newPosition) {
break;
}
}
return newPosition;
},
// Only when other users change (for recalculation)
getLockMe2(position) {
let newPosition = position;
const count = this._arrElements.length;
for (let i = count - 1; i >= 0; --i) {
newPosition = this._arrElements[i].getLockMe2(newPosition);
if (null === newPosition) {
break;
}
}
return newPosition;
}
};
function updatePresenceCounters(ctx, conn, val) {
return co(function* () {
let aggregationCtx;
if (tenantManager.isMultitenantMode(ctx) && !tenantManager.isDefaultTenant(ctx)) {
//aggregated server stats
aggregationCtx = new operationContext.Context();
aggregationCtx.init(tenantManager.getDefautTenant(), ctx.docId, ctx.userId);
//yield ctx.initTenantCache(); //no need.only global config
}
if (utils.isLiveViewer(conn)) {
yield editorStat.incrLiveViewerConnectionsCountByShard(ctx, SHARD_ID, val);
if (aggregationCtx) {
yield editorStat.incrLiveViewerConnectionsCountByShard(aggregationCtx, SHARD_ID, val);
}
if (clientStatsD) {
const countLiveView = yield editorStat.getLiveViewerConnectionsCount(ctx, connections);
clientStatsD.gauge('expireDoc.connections.liveview', countLiveView);
}
} else if (conn.isCloseCoAuthoring || (conn.user && conn.user.view)) {
yield editorStat.incrViewerConnectionsCountByShard(ctx, SHARD_ID, val);
if (aggregationCtx) {
yield editorStat.incrViewerConnectionsCountByShard(aggregationCtx, SHARD_ID, val);
}
if (clientStatsD) {
const countView = yield editorStat.getViewerConnectionsCount(ctx, connections);
clientStatsD.gauge('expireDoc.connections.view', countView);
}
} else {
yield editorStat.incrEditorConnectionsCountByShard(ctx, SHARD_ID, val);
if (aggregationCtx) {
yield editorStat.incrEditorConnectionsCountByShard(aggregationCtx, SHARD_ID, val);
}
if (clientStatsD) {
const countEditors = yield editorStat.getEditorConnectionsCount(ctx, connections);
clientStatsD.gauge('expireDoc.connections.edit', countEditors);
}
}
});
}
function addPresence(ctx, conn, updateCunters) {
return co(function* () {
yield editorData.addPresence(ctx, conn.docId, conn.user.id, utils.getConnectionInfoStr(conn));
if (updateCunters) {
yield updatePresenceCounters(ctx, conn, 1);
}
});
}
async function updatePresence(ctx, conn) {
if (editorData.updatePresence) {
return await editorData.updatePresence(ctx, conn.docId, conn.user.id);
} else {
//todo remove if after 7.6. code for backward compatibility, because redis in separate repo
return await editorData.addPresence(ctx, conn.docId, conn.user.id, utils.getConnectionInfoStr(conn));
}
}
function removePresence(ctx, conn) {
return co(function* () {
yield editorData.removePresence(ctx, conn.docId, conn.user.id);
yield updatePresenceCounters(ctx, conn, -1);
});
}
const changeConnectionInfo = co.wrap(function* (ctx, conn, cmd) {
if (!conn.denyChangeName && conn.user) {
yield publish(ctx, {type: commonDefines.c_oPublishType.changeConnecitonInfo, ctx, docId: conn.docId, useridoriginal: conn.user.idOriginal, cmd});
return true;
}
return false;
});
function signToken(ctx, payload, algorithm, expiresIn, secretElem) {
return co(function* () {
const options = {algorithm, expiresIn};
const secret = yield tenantManager.getTenantSecret(ctx, secretElem);
return jwt.sign(payload, utils.getJwtHsKey(secret), options);
});
}
function needSendChanges(conn) {
return !conn.user?.view || utils.isLiveViewer(conn);
}
function fillJwtByConnection(ctx, conn) {
return co(function* () {
const tenTokenSessionAlgorithm = ctx.getCfg('services.CoAuthoring.token.session.algorithm', cfgTokenSessionAlgorithm);
const tenTokenSessionExpires = ms(ctx.getCfg('services.CoAuthoring.token.session.expires', cfgTokenSessionExpires));
const payload = {document: {}, editorConfig: {user: {}}};
const doc = payload.document;
doc.key = conn.docId;
doc.permissions = conn.permissions;
doc.ds_encrypted = conn.encrypted;
const edit = payload.editorConfig;
//todo
//edit.callbackUrl = callbackUrl;
//edit.lang = conn.lang;
//edit.mode = conn.mode;
const user = edit.user;
user.id = conn.user.idOriginal;
user.name = conn.user.username;
user.index = conn.user.indexUser;
user.customerId = conn.user.customerId;
if (conn.coEditingMode) {
edit.coEditing = {mode: conn.coEditingMode};
}
//no standart
edit.ds_isCloseCoAuthoring = conn.isCloseCoAuthoring;
edit.ds_isEnterCorrectPassword = conn.isEnterCorrectPassword;
// presenter viewer opens with same session jwt. do not put sessionId to jwt
// edit.ds_sessionId = conn.sessionId;
edit.ds_sessionTimeConnect = conn.sessionTimeConnect;
return yield signToken(ctx, payload, tenTokenSessionAlgorithm, tenTokenSessionExpires / 1000, commonDefines.c_oAscSecretType.Session);
});
}
function sendData(ctx, conn, data) {
conn.emit('message', data);
const type = data ? data.type : null;
ctx.logger.debug('sendData: type = %s', type);
}
function sendDataWarning(ctx, conn, code, description) {
sendData(ctx, conn, {type: 'warning', code, message: description});
}
function sendDataMessage(ctx, conn, msg) {
if (!conn.permissions || false !== conn.permissions.chat) {
sendData(ctx, conn, {type: 'message', messages: msg});
} else {
ctx.logger.debug('sendDataMessage permissions.chat==false');
}
}
function sendDataCursor(ctx, conn, msg) {
sendData(ctx, conn, {type: 'cursor', messages: msg});
}
function sendDataMeta(ctx, conn, msg) {
sendData(ctx, conn, {type: 'meta', messages: msg});
}
function sendDataSession(ctx, conn, msg) {
sendData(ctx, conn, {type: 'session', messages: msg});
}
function sendDataRefreshToken(ctx, conn, msg) {
sendData(ctx, conn, {type: 'refreshToken', messages: msg});
}
function sendDataRpc(ctx, conn, responseKey, data) {
sendData(ctx, conn, {type: 'rpc', responseKey, data});
}
function sendDataDrop(ctx, conn, code, description) {
sendData(ctx, conn, {type: 'drop', code, description});
}
function sendDataDisconnectReason(ctx, conn, code, description) {
sendData(ctx, conn, {type: 'disconnectReason', code, description});
}
function sendReleaseLock(ctx, conn, userLocks) {
sendData(ctx, conn, {
type: 'releaseLock',
locks: _.map(userLocks, e => {
return {
block: e.block,
user: e.user,
time: Date.now(),
changes: null
};
})
});
}
function modifyConnectionForPassword(ctx, conn, isEnterCorrectPassword) {
return co(function* () {
if (isEnterCorrectPassword) {
conn.isEnterCorrectPassword = true;
const sessionToken = yield fillJwtByConnection(ctx, conn);
sendDataRefreshToken(ctx, conn, sessionToken);
}
});
}
function modifyConnectionEditorToView(ctx, conn) {
if (conn.user) {
conn.user.view = true;
}
delete conn.coEditingMode;
}
function getParticipants(docId, excludeClosed, excludeUserId, excludeViewer) {
return _.filter(connections, el => {
return el.docId === docId && el.isCloseCoAuthoring !== excludeClosed && el.user.id !== excludeUserId && el.user.view !== excludeViewer;
});
}
function getParticipantUser(docId, includeUserId) {
return _.filter(connections, el => {
return el.docId === docId && el.user.id === includeUserId;
});
}
function* updateEditUsers(ctx, licenseInfo, userId, anonym, isLiveViewer) {
if (!licenseInfo.usersCount) {
return;
}
const now = new Date();
const expireAt = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1) / 1000 + licenseInfo.usersExpire - 1;
const period = utils.getLicensePeriod(licenseInfo.startDate, now);
if (isLiveViewer) {
yield editorStat.addPresenceUniqueViewUser(ctx, userId, expireAt, {anonym});
yield editorStat.addPresenceUniqueViewUsersOfMonth(ctx, userId, period, {anonym, firstOpenDate: now.toISOString()});
} else {
yield editorStat.addPresenceUniqueUser(ctx, userId, expireAt, {anonym});
yield editorStat.addPresenceUniqueUsersOfMonth(ctx, userId, period, {anonym, firstOpenDate: now.toISOString()});
}
}
function* getEditorsCount(ctx, docId, opt_hvals) {
let elem,
editorsCount = 0;
let hvals;
if (opt_hvals) {
hvals = opt_hvals;
} else {
hvals = yield editorData.getPresence(ctx, docId, connections);
}
for (let i = 0; i < hvals.length; ++i) {
elem = JSON.parse(hvals[i]);
if (!elem.view && !elem.isCloseCoAuthoring) {
editorsCount++;
break;
}
}
return editorsCount;
}
function* hasEditors(ctx, docId, opt_hvals) {
const editorsCount = yield* getEditorsCount(ctx, docId, opt_hvals);
return editorsCount > 0;
}
function* isUserReconnect(ctx, docId, userId, connectionId) {
let elem;
const hvals = yield editorData.getPresence(ctx, docId, connections);
for (let i = 0; i < hvals.length; ++i) {
elem = JSON.parse(hvals[i]);
if (userId === elem.id && connectionId !== elem.connectionId) {
return true;
}
}
return false;
}
let pubsubOnMessage = null; //todo move function
async function publish(ctx, data, optDocId, optUserId, opt_pubsub) {
let needPublish = true;
let hvals;
if (optDocId && optUserId) {
needPublish = false;
hvals = await editorData.getPresence(ctx, optDocId, connections);
for (let i = 0; i < hvals.length; ++i) {
const elem = JSON.parse(hvals[i]);
if (optUserId != elem.id) {
needPublish = true;
break;
}
}
}
if (needPublish) {
const msg = JSON.stringify(data);
const realPubsub = opt_pubsub ? opt_pubsub : pubsub;
//don't use pubsub if all connections are local
if (pubsubOnMessage && hvals && hvals.length === getLocalConnectionCount(ctx, optDocId)) {
ctx.logger.debug('pubsub locally');
//todo send connections from getLocalConnectionCount to pubsubOnMessage
pubsubOnMessage(msg);
} else if (realPubsub) {
await realPubsub.publish(msg);
}
}
return needPublish;
}
function* addTask(data, priority, opt_queue, opt_expiration) {
const realQueue = opt_queue ? opt_queue : queue;
yield realQueue.addTask(data, priority, opt_expiration);
}
function* addResponse(data, opt_queue) {
const realQueue = opt_queue ? opt_queue : queue;
yield realQueue.addResponse(data);
}
function* addDelayed(data, ttl, opt_queue) {
const realQueue = opt_queue ? opt_queue : queue;
yield realQueue.addDelayed(data, ttl);
}
function* removeResponse(data) {
yield queue.removeResponse(data);
}
async function getOriginalParticipantsId(ctx, docId) {
const result = [],
tmpObject = {};
const hvals = await editorData.getPresence(ctx, docId, connections);
for (let i = 0; i < hvals.length; ++i) {
const elem = JSON.parse(hvals[i]);
if (!elem.view && !elem.isCloseCoAuthoring) {
tmpObject[elem.idOriginal] = 1;
}
}
for (const name in tmpObject) {
if (Object.hasOwn(tmpObject, name)) {
result.push(name);
}
}
return result;
}
async function sendServerRequest(ctx, uri, dataObject, opt_checkAndFixAuthorizationLength) {
const tenCallbackRequestTimeout = ctx.getCfg('services.CoAuthoring.server.callbackRequestTimeout', cfgCallbackRequestTimeout);
const tenTokenEnableRequestInbox = ctx.getCfg('services.CoAuthoring.token.enable.request.inbox', cfgTokenEnableRequestInbox);
ctx.logger.debug('postData request: url = %s;data = %j', uri, dataObject);
let auth;
if (utils.canIncludeOutboxAuthorization(ctx, uri)) {
const secret = await tenantManager.getTenantSecret(ctx, commonDefines.c_oAscSecretType.Outbox);
const bodyToken = utils.fillJwtForRequest(ctx, dataObject, secret, true);
auth = utils.fillJwtForRequest(ctx, dataObject, secret, false);
const authLen = auth.length;
if (opt_checkAndFixAuthorizationLength && !opt_checkAndFixAuthorizationLength(auth, dataObject)) {
auth = utils.fillJwtForRequest(ctx, dataObject, secret, false);
ctx.logger.warn('authorization too large. Use body token instead. size reduced from %d to %d', authLen, auth.length);
}
dataObject.setToken(bodyToken);
}
const headers = {'Content-Type': 'application/json'};
//isInJwtToken is true because callbackUrl is required field in jwt token
const postRes = await utils.postRequestPromise(
ctx,
uri,
JSON.stringify(dataObject),
undefined,
undefined,
tenCallbackRequestTimeout,
auth,
tenTokenEnableRequestInbox,
headers
);
ctx.logger.debug('postData response: data = %s', postRes.body);
return postRes.body;
}
function parseUrl(ctx, callbackUrl) {
let result = null;
try {
//no need to do decodeURIComponent http://expressjs.com/en/4x/api.html#app.settings.table
//by default express uses 'query parser' = 'extended', but even in 'simple' version decode is done
//percent-encoded characters within the query string will be assumed to use UTF-8 encoding
const parseObject = url.parse(callbackUrl);
const isHttps = 'https:' === parseObject.protocol;
let port = parseObject.port;
if (!port) {
port = isHttps ? defaultHttpsPort : defaultHttpPort;
}
result = {
https: isHttps,
host: parseObject.hostname,
port,
path: parseObject.path,
href: parseObject.href
};
} catch (e) {
ctx.logger.error('error parseUrl %s: %s', callbackUrl, e.stack);
result = null;
}
return result;
}
async function getCallback(ctx, id, opt_userIndex) {
let callbackUrl = null;
let baseUrl = null;
let wopiParams = null;
const selectRes = await taskResult.select(ctx, id);
if (selectRes.length > 0) {
const row = selectRes[0];
if (row.callback) {
callbackUrl = sqlBase.UserCallback.prototype.getCallbackByUserIndex(ctx, row.callback, opt_userIndex);
wopiParams = wopiClient.parseWopiCallback(ctx, callbackUrl, row.callback);
}
if (row.baseurl) {
baseUrl = row.baseurl;
}
}
if (null != callbackUrl && null != baseUrl) {
return {server: parseUrl(ctx, callbackUrl), baseUrl, wopiParams};
} else {
return null;
}
}
function* getChangesIndex(ctx, docId) {
let res = 0;
const getRes = yield sqlBase.getChangesIndexPromise(ctx, docId);
if (getRes && getRes.length > 0 && null != getRes[0]['change_id']) {
res = getRes[0]['change_id'] + 1;
}
return res;
}
const hasChanges = co.wrap(function* (ctx, docId) {
//todo check editorData.getForceSave in case of "undo all changes"
const puckerIndex = yield* getChangesIndex(ctx, docId);
if (0 === puckerIndex) {
const selectRes = yield taskResult.select(ctx, docId);
if (selectRes.length > 0 && selectRes[0].password) {
return sqlBase.DocumentPassword.prototype.hasPasswordChanges(ctx, selectRes[0].password);
}
return false;
}
return true;
});
function* setForceSave(ctx, docId, forceSave, cmd, success, url) {
const forceSaveType = forceSave.getType();
let end = success;
if (commonDefines.c_oAscForceSaveTypes.Form === forceSaveType || commonDefines.c_oAscForceSaveTypes.Internal === forceSaveType) {
const forceSave = yield editorData.getForceSave(ctx, docId);
end = forceSave.ended;
}
const convertInfo = new commonDefines.InputCommand(cmd, true);
//remove request specific fields from cmd
convertInfo.setUserConnectionDocId(undefined);
convertInfo.setUserConnectionId(undefined);
convertInfo.setResponseKey(undefined);
convertInfo.setFormData(undefined);
if (convertInfo.getForceSave()) {
//type must be saved to distinguish c_oAscForceSaveTypes.Form
//convertInfo.getForceSave().setType(undefined);
convertInfo.getForceSave().setAuthorUserId(undefined);
convertInfo.getForceSave().setAuthorUserIndex(undefined);
}
yield editorData.checkAndSetForceSave(ctx, docId, forceSave.getTime(), forceSave.getIndex(), end, end, convertInfo);
if (commonDefines.c_oAscForceSaveTypes.Command !== forceSaveType) {
let data = {type: forceSaveType, time: forceSave.getTime(), success};
if (commonDefines.c_oAscForceSaveTypes.Form === forceSaveType || commonDefines.c_oAscForceSaveTypes.Internal === forceSaveType) {
const code = success ? commonDefines.c_oAscServerCommandErrors.NoError : commonDefines.c_oAscServerCommandErrors.UnknownError;
data = {code, time: forceSave.getTime(), inProgress: false};
if (commonDefines.c_oAscForceSaveTypes.Internal === forceSaveType) {
data.url = url;
}
const userId = cmd.getUserConnectionId();
docId = cmd.getUserConnectionDocId() || docId;
yield publish(ctx, {type: commonDefines.c_oPublishType.rpc, ctx, docId, userId, data, responseKey: cmd.getResponseKey()});
} else {
yield publish(ctx, {type: commonDefines.c_oPublishType.forceSave, ctx, docId, data}, cmd.getUserConnectionId());
}
}
}
/**
* @param {commonDefines.InputCommand} cmd - Information about the document conversion
* @returns {string|null} The constructed document path if saveKey and outputPath exist, null otherwise
*/
function getForceSaveDocPath(cmd) {
if (cmd) {
const saveKey = cmd.getDocId() + cmd.getSaveKey();
const outputPath = cmd.getOutputPath();
if (saveKey && outputPath) {
return saveKey + '/' + outputPath;
}
}
return null;
}
/**
* Checks if a force save cache exists and is valid for the provided conversion information
* @param {operationContext.Context} ctx - The request context
* @param {Object} convertInfo - Information about the document conversion
* @returns {Promise<Object>} Object containing cache status information:
* - hasCache {boolean} - Whether cache information exists
* - hasValidCache {boolean} - Whether the cache is valid
* - cmd {commonDefines.InputCommand|null} - Command object (if available)
*/
async function checkForceSaveCache(ctx, convertInfo) {
const res = {hasCache: false, hasValidCache: false, cmd: null};
if (convertInfo) {
res.hasCache = true;
const cmd = new commonDefines.InputCommand(convertInfo, true);
const docPath = getForceSaveDocPath(cmd);
if (docPath) {
const metadata = await storage.headObject(ctx, docPath);
res.hasValidCache = !!metadata;
res.cmd = cmd;
}
}
return res;
}
/**
* Generates a signed URL for accessing a force-saved document
* @param {operationContext.Context} ctx - The request context
* @param {string} baseUrl - Base URL for the document
* @param {Object} convertInfo - Information about the document conversion
* @returns {Promise<string|null>} The signed URL for the force-saved document or null if path cannot be generated
*/
async function getForceSaveUrl(ctx, baseUrl, convertInfo) {
const cmd = new commonDefines.InputCommand(convertInfo, true);
const docPath = getForceSaveDocPath(cmd);
if (docPath) {
return await storage.getSignedUrl(ctx, baseUrl, docPath, commonDefines.c_oAscUrlTypes.Temporary);
}
return null;
}
async function applyForceSaveCache(
ctx,
docId,
forceSave,
type,
opt_userConnectionId,
opt_userConnectionDocId,
opt_responseKey,
opt_formdata,
opt_userId,
opt_userIndex,
opt_prevTime
) {
const res = {ok: false, notModified: false, inProgress: false, startedForceSave: null};
if (!forceSave) {
res.notModified = true;
return res;
}
const forceSaveCache = await checkForceSaveCache(ctx, forceSave.convertInfo);
if (forceSaveCache.hasCache || forceSave.ended) {
if (commonDefines.c_oAscForceSaveTypes.Form === type || commonDefines.c_oAscForceSaveTypes.Internal === type || !forceSave.ended) {
//c_oAscForceSaveTypes.Form has uniqueue options {'documentLayout': {'isPrint': true}}; dont use it for other types
const forceSaveCached = forceSaveCache.cmd?.getForceSave()?.getType();
const cacheHasSameOptions =
(commonDefines.c_oAscForceSaveTypes.Form === type && commonDefines.c_oAscForceSaveTypes.Form === forceSaveCached) ||
(commonDefines.c_oAscForceSaveTypes.Form !== type && commonDefines.c_oAscForceSaveTypes.Form !== forceSaveCached);
if (forceSaveCache.hasValidCache && cacheHasSameOptions) {
//compare opt_prevTime because Internal command can be called by different users
if (commonDefines.c_oAscForceSaveTypes.Internal === type && forceSave.time === opt_prevTime) {
res.notModified = true;
} else {
const cmd = forceSaveCache.cmd;
cmd.setUserConnectionDocId(opt_userConnectionDocId);
cmd.setUserConnectionId(opt_userConnectionId);
cmd.setResponseKey(opt_responseKey);
cmd.setFormData(opt_formdata);
if (cmd.getForceSave()) {
cmd.getForceSave().setType(type);
cmd.getForceSave().setAuthorUserId(opt_userId);
cmd.getForceSave().setAuthorUserIndex(opt_userIndex);
}
//todo timeout because commandSfcCallback make request?
await canvasService.commandSfcCallback(ctx, cmd, true, false);
res.ok = true;
}
} else {
await editorData.checkAndSetForceSave(ctx, docId, forceSave.time, forceSave.index, false, false, null);
res.startedForceSave = await editorData.checkAndStartForceSave(ctx, docId);
res.ok = !!res.startedForceSave;
}
} else {
res.notModified = true;
}
} else if (!forceSave.started) {
const isTypeToSendFile =
commonDefines.c_oAscForceSaveTypes.Command === type ||
commonDefines.c_oAscForceSaveTypes.Button === type ||
commonDefines.c_oAscForceSaveTypes.Timeout === type ||
commonDefines.c_oAscForceSaveTypes.Form === type;
if (isTypeToSendFile) {
const selectRes = await taskResult.selectWithCache(ctx, docId);
if (selectRes.length > 0 && !selectRes[0].callback) {
ctx.logger.debug('applyForceSaveCache empty callback: %s', docId);
res.notModified = true;
return res;
}
}
res.startedForceSave = await editorData.checkAndStartForceSave(ctx, docId);
res.ok = !!res.startedForceSave;
return res;
} else if (commonDefines.c_oAscForceSaveTypes.Form === type || commonDefines.c_oAscForceSaveTypes.Internal === type) {
res.ok = true;
res.inProgress = true;
} else {
res.notModified = true;
}
return res;
}
async function startForceSave(
ctx,
docId,
type,
opt_userdata,
opt_formdata,
opt_userId,
opt_userConnectionId,
opt_userConnectionDocId,
opt_userIndex,
opt_responseKey,
opt_baseUrl,
opt_queue,
opt_pubsub,
opt_conn,
opt_initShardKey,
opt_jsonParams,
opt_changeInfo,
opt_prevTime
) {
const tenForceSaveUsingButtonWithoutChanges = ctx.getCfg(
'services.CoAuthoring.server.forceSaveUsingButtonWithoutChanges',
cfgForceSaveUsingButtonWithoutChanges
);
ctx.logger.debug('startForceSave start');
const res = {code: commonDefines.c_oAscServerCommandErrors.NoError, time: null, inProgress: false};
let startedForceSave;
let hasEncrypted = false;
if (!shutdownFlag) {
const hvals = await editorData.getPresence(ctx, docId, connections);
hasEncrypted = hvals.some(currentValue => {
return !!JSON.parse(currentValue).encrypted;
});
if (!hasEncrypted) {
let baseUrl = opt_baseUrl || '';
if (opt_conn) {
baseUrl = utils.getBaseUrlByConnection(ctx, opt_conn);
}
let forceSave = await editorData.getForceSave(ctx, docId);
const forceSaveWithConnection =
opt_conn &&
(commonDefines.c_oAscForceSaveTypes.Form === type ||
(commonDefines.c_oAscForceSaveTypes.Button === type && tenForceSaveUsingButtonWithoutChanges));
const startWithoutChanges = !forceSave && (forceSaveWithConnection || opt_changeInfo);
if (startWithoutChanges) {
//stub to send forms without changes
const newChangesLastDate = new Date();
newChangesLastDate.setMilliseconds(0); //remove milliseconds avoid issues with MySQL datetime rounding
const newChangesLastTime = newChangesLastDate.getTime();
let changeInfo = opt_changeInfo;
if (opt_conn) {
changeInfo = getExternalChangeInfo(opt_conn.user, newChangesLastTime, opt_conn.lang);
}
await editorData.setForceSave(ctx, docId, newChangesLastTime, 0, baseUrl, changeInfo, null);
forceSave = await editorData.getForceSave(ctx, docId);
}
const applyCacheRes = await applyForceSaveCache(
ctx,
docId,
forceSave,
type,
opt_userConnectionId,
opt_userConnectionDocId,
opt_responseKey,
opt_formdata,
opt_userId,
opt_userIndex,
opt_prevTime
);
startedForceSave = applyCacheRes.startedForceSave;
if (applyCacheRes.notModified) {
const selectRes = await taskResult.select(ctx, docId);
if (selectRes.length > 0) {
res.code = commonDefines.c_oAscServerCommandErrors.NotModified;
if (forceSave) {
res.url = await getForceSaveUrl(ctx, baseUrl, forceSave.convertInfo);
}
} else {
res.code = commonDefines.c_oAscServerCommandErrors.DocumentIdError;
}
} else if (!applyCacheRes.ok) {
res.code = commonDefines.c_oAscServerCommandErrors.UnknownError;
}
res.inProgress = applyCacheRes.inProgress;
}
}
ctx.logger.debug('startForceSave canStart: hasEncrypted = %s; applyCacheRes = %j; startedForceSave = %j', hasEncrypted, res, startedForceSave);
if (startedForceSave) {
const baseUrl = opt_baseUrl || startedForceSave.baseUrl;
const forceSave = new commonDefines.CForceSaveData(startedForceSave);
forceSave.setType(type);
forceSave.setAuthorUserId(opt_userId);
forceSave.setAuthorUserIndex(opt_userIndex);
let priority;
let expiration;
if (commonDefines.c_oAscForceSaveTypes.Timeout === type) {
priority = constants.QUEUE_PRIORITY_VERY_LOW;
expiration = getForceSaveExpiration(ctx);
} else {
priority = constants.QUEUE_PRIORITY_LOW;
}
//start new convert
const status = await converterService.convertFromChanges(
ctx,
docId,
baseUrl,
forceSave,
startedForceSave.changeInfo,
opt_userdata,
opt_formdata,
opt_userConnectionId,
opt_userConnectionDocId,
opt_responseKey,
priority,
expiration,
opt_queue,
undefined,
opt_initShardKey,
opt_jsonParams
);
if (constants.NO_ERROR === status.err) {
res.time = forceSave.getTime();
if (commonDefines.c_oAscForceSaveTypes.Timeout === type) {
await publish(
ctx,
{
type: commonDefines.c_oPublishType.forceSave,
ctx,
docId,
data: {type, time: forceSave.getTime(), start: true}
},
undefined,
undefined,
opt_pubsub
);
}
} else {
res.code = commonDefines.c_oAscServerCommandErrors.UnknownError;
}
ctx.logger.debug('startForceSave convertFromChanges: status = %d', status.err);
}
ctx.logger.debug('startForceSave end');
return res;
}
function getExternalChangeInfo(user, date, lang) {
return {user_id: user.id, user_id_original: user.idOriginal, user_name: user.username, lang, change_date: date};
}
const resetForceSaveAfterChanges = co.wrap(function* (ctx, docId, newChangesLastTime, puckerIndex, baseUrl, changeInfo) {
const tenForceSaveEnable = ctx.getCfg('services.CoAuthoring.autoAssembly.enable', cfgForceSaveEnable);
const tenForceSaveInterval = ms(ctx.getCfg('services.CoAuthoring.autoAssembly.interval', cfgForceSaveInterval));
//last save
if (newChangesLastTime) {
yield editorData.setForceSave(ctx, docId, newChangesLastTime, puckerIndex, baseUrl, changeInfo, null);
if (tenForceSaveEnable) {
const expireAt = newChangesLastTime + tenForceSaveInterval;
yield editorData.addForceSaveTimerNX(ctx, docId, expireAt);
}
}
});
async function saveRelativeFromChanges(ctx, conn, responseKey, data) {
const tenTokenEnableBrowser = ctx.getCfg('services.CoAuthoring.token.enable.browser', cfgTokenEnableBrowser);
let docId = data.docId;
const token = data.token;
let forceSaveRes;
if (tenTokenEnableBrowser || token) {
docId = null;
const checkJwtRes = await checkJwt(ctx, token, commonDefines.c_oAscSecretType.Browser);
if (checkJwtRes.decoded) {
docId = checkJwtRes.decoded.key;
} else {
ctx.logger.warn('Error saveRelativeFromChanges jwt: %s', checkJwtRes.description);
forceSaveRes = {code: commonDefines.c_oAscServerCommandErrors.Token, time: null, inProgress: false};
}
}
if (!forceSaveRes) {
forceSaveRes = await startForceSave(
ctx,
docId,
commonDefines.c_oAscForceSaveTypes.Internal,
undefined,
undefined,
undefined,
conn.user.id,
conn.docId,
undefined,
responseKey,
undefined,
undefined,
undefined,
conn,
undefined,
undefined,
undefined,
data.time
);
}
if (commonDefines.c_oAscServerCommandErrors.NoError !== forceSaveRes.code || forceSaveRes.inProgress) {
sendDataRpc(ctx, conn, responseKey, forceSaveRes);
}
}
async function startWopiRPC(ctx, docId, userId, userIdOriginal, data) {
let res;
const selectRes = await taskResult.select(ctx, docId);
const row = selectRes.length > 0 ? selectRes[0] : null;
if (row) {
if (row.callback) {
const userIndex = utils.getIndexFromUserId(userId, userIdOriginal);
const uri = sqlBase.UserCallback.prototype.getCallbackByUserIndex(ctx, row.callback, userIndex);
const wopiParams = wopiClient.parseWopiCallback(ctx, uri, row.callback);
if (wopiParams) {
switch (data.type) {
case 'wopi_RenameFile':
res = await wopiClient.renameFile(ctx, wopiParams, data.name);
//publish for coeditors
if (res?.Name) {
const meta = {title: res.Name};
await publish(ctx, {type: commonDefines.c_oPublishType.meta, ctx, docId, meta});
}
break;
case 'wopi_RefreshFile':
res = await wopiClient.refreshFile(ctx, wopiParams, row.baseurl);
break;
}
}
}
}
return res;
}
function* startRPC(ctx, conn, responseKey, data) {
const docId = conn.docId;
ctx.logger.debug('startRPC start responseKey:%s , %j', responseKey, data);
switch (data.type) {
case 'sendForm': {
let forceSaveRes;
if (conn.user) {
//isPrint - to remove forms
const jsonParams = {documentLayout: {isPrint: true}};
forceSaveRes = yield startForceSave(
ctx,
docId,
commonDefines.c_oAscForceSaveTypes.Form,
undefined,
data.formdata,
conn.user.idOriginal,
conn.user.id,
undefined,
conn.user.indexUser,
responseKey,
undefined,
undefined,
undefined,
conn,
undefined,
jsonParams
);
}
if (!forceSaveRes || commonDefines.c_oAscServerCommandErrors.NoError !== forceSaveRes.code || forceSaveRes.inProgress) {
sendDataRpc(ctx, conn, responseKey, forceSaveRes);
}
break;
}
case 'saveRelativeFromChanges': {
yield saveRelativeFromChanges(ctx, conn, responseKey, data);
break;
}
case 'wopi_RenameFile':
case 'wopi_RefreshFile': {
const res = yield startWopiRPC(ctx, conn.docId, conn.user.id, conn.user.idOriginal, data);
sendDataRpc(ctx, conn, responseKey, res);
break;
}
case 'pathurls': {
const outputData = new canvasService.OutputData(data.type);
yield* canvasService.commandPathUrls(ctx, conn, data.data, outputData);
sendDataRpc(ctx, conn, responseKey, outputData);
break;
}
}
ctx.logger.debug('startRPC end');
}
function handleDeadLetter(data, ack) {
return co(function* () {
const ctx = new operationContext.Context();
try {
let isRequeued = false;
const task = new commonDefines.TaskQueueData(JSON.parse(data));
if (task) {
ctx.initFromTaskQueueData(task);
yield ctx.initTenantCache();
const cmd = task.getCmd();
ctx.logger.warn('handleDeadLetter start: %s', data);
const forceSave = cmd.getForceSave();
if (forceSave && commonDefines.c_oAscForceSaveTypes.Timeout == forceSave.getType()) {
const actualForceSave = yield editorData.getForceSave(ctx, cmd.getDocId());
//check that there are no new changes
if (actualForceSave && forceSave.getTime() === actualForceSave.time && forceSave.getIndex() === actualForceSave.index) {
//requeue task
yield* addTask(task, constants.QUEUE_PRIORITY_VERY_LOW, undefined, getForceSaveExpiration(ctx));
isRequeued = true;
}
} else if (!forceSave && task.getFromChanges()) {
yield* addTask(task, constants.QUEUE_PRIORITY_NORMAL, undefined);
isRequeued = true;
} else if (cmd.getAttempt()) {
ctx.logger.warn('handleDeadLetter addResponse delayed = %d', cmd.getAttempt());
yield* addResponse(task);
} else {
//simulate error response
cmd.setStatusInfo(constants.CONVERT_DEAD_LETTER);
canvasService.receiveTask(JSON.stringify(task), () => {});
}
}
ctx.logger.warn('handleDeadLetter end: requeue = %s', isRequeued);
} catch (err) {
ctx.logger.error('handleDeadLetter error: %s', err.stack);
} finally {
ack();
}
});
}
/**
* Sending status to know when the document started editing and when it ended
* @param docId
* @param {number} bChangeBase
* @param callback
* @param baseUrl
*/
async function sendStatusDocument(ctx, docId, bChangeBase, opt_userAction, opt_userIndex, opt_callback, opt_baseUrl, opt_userData, opt_forceClose) {
if (!opt_callback) {
const getRes = await getCallback(ctx, docId, opt_userIndex);
if (getRes) {
opt_callback = getRes.server;
if (!opt_baseUrl) {
opt_baseUrl = getRes.baseUrl;
}
if (getRes.wopiParams) {
ctx.logger.debug('sendStatusDocument wopi stub');
return opt_callback;
}
}
}
if (null == opt_callback) {
return;
}
let status = c_oAscServerStatus.Editing;
const participants = await getOriginalParticipantsId(ctx, docId);
if (0 === participants.length) {
const bHasChanges = await hasChanges(ctx, docId);
if (!bHasChanges || opt_forceClose) {
status = c_oAscServerStatus.Closed;
}
}
if (c_oAscChangeBase.No !== bChangeBase) {
//update callback even if the connection is closed to avoid script:
//open->make changes->disconnect->subscription from community->reconnect
if (c_oAscChangeBase.All === bChangeBase) {
//always override callback to avoid expired callbacks
const updateTask = new taskResult.TaskResultData();
updateTask.tenant = ctx.tenant;
updateTask.key = docId;
updateTask.callback = opt_callback.href;
updateTask.baseurl = opt_baseUrl;
const updateIfRes = await taskResult.update(ctx, updateTask);
if (updateIfRes.affectedRows > 0) {
ctx.logger.debug('sendStatusDocument updateIf');
} else {
ctx.logger.debug('sendStatusDocument updateIf no effect');
}
}
}
const sendData = new commonDefines.OutputSfcData(docId);
sendData.setStatus(status);
if (c_oAscServerStatus.Closed !== status) {
sendData.setUsers(participants);
}
if (opt_userAction) {
sendData.setActions([opt_userAction]);
}
if (opt_userData) {
sendData.setUserData(opt_userData);
}
const uri = opt_callback.href;
let replyData = null;
try {
replyData = await sendServerRequest(ctx, uri, sendData);
} catch (err) {
replyData = null;
ctx.logger.error('postData error: url = %s;data = %j %s', uri, sendData, err.stack);
}
await onReplySendStatusDocument(ctx, docId, replyData);
return sendData;
}
function parseReplyData(ctx, replyData) {
let res = null;
if (replyData) {
try {
res = JSON.parse(replyData);
} catch (e) {
ctx.logger.error('error parseReplyData: data = %s %s', replyData, e.stack);
res = null;
}
}
return res;
}
const onReplySendStatusDocument = co.wrap(function* (ctx, docId, replyData) {
const oData = parseReplyData(ctx, replyData);
if (!(oData && commonDefines.c_oAscServerCommandErrors.NoError == oData.error)) {
// Error subscribing to callback, send warning
yield publish(ctx, {type: commonDefines.c_oPublishType.warning, ctx, docId, description: 'Error on save server subscription!'});
}
});
function* publishCloseUsersConnection(ctx, docId, users, isOriginalId, code, description) {
if (Array.isArray(users)) {
const usersMap = users.reduce((map, val) => {
map[val] = 1;
return map;
}, {});
yield publish(ctx, {
type: commonDefines.c_oPublishType.closeConnection,
ctx,
docId,
usersMap,
isOriginalId,
code,
description
});
}
}
function closeUsersConnection(ctx, docId, usersMap, isOriginalId, code, description) {
//close
let conn;
for (let i = connections.length - 1; i >= 0; --i) {
conn = connections[i];
if (conn.docId === docId) {
if (isOriginalId ? usersMap[conn.user.idOriginal] : usersMap[conn.user.id]) {
sendDataDisconnectReason(ctx, conn, code, description);
conn.disconnect(true);
}
}
}
}
async function dropUsersFromDocument(ctx, docId, opt_users) {
await publish(ctx, {type: commonDefines.c_oPublishType.drop, ctx, docId, users: opt_users, description: ''});
}
function dropUserFromDocument(ctx, docId, users, description) {
let elConnection;
for (let i = 0, length = connections.length; i < length; ++i) {
elConnection = connections[i];
if (elConnection.docId === docId && !elConnection.isCloseCoAuthoring && (!users || users.includes(elConnection.user.idOriginal))) {
sendDataDrop(ctx, elConnection, description);
}
}
}
function getLocalConnectionCount(ctx, docId) {
return connections.reduce((count, conn) => {
if (conn.docId === docId && conn.tenant === ctx.tenant) {
count++;
}
return count;
}, 0);
}
// Event subscription:
function* bindEvents(ctx, docId, callback, baseUrl, opt_userAction, opt_userData) {
// Subscribe to events:
// - if there are no users and no changes, then send the status "closed" and do not add to the database
// - if there are no users, but there are changes, then send the "editing" status without users, but add it to the database
// - if there are users, then just add to the database
let bChangeBase;
let oCallbackUrl;
if (!callback) {
const getRes = yield getCallback(ctx, docId);
if (getRes && !getRes.wopiParams) {
oCallbackUrl = getRes.server;
bChangeBase = c_oAscChangeBase.Delete;
}
} else {
oCallbackUrl = parseUrl(ctx, callback);
bChangeBase = c_oAscChangeBase.No;
if (null !== oCallbackUrl) {
const filterStatus = yield* utils.checkHostFilter(ctx, oCallbackUrl.host);
if (filterStatus > 0) {
ctx.logger.warn('checkIpFilter error: url = %s', callback);
//todo add new error type
oCallbackUrl = null;
}
}
}
if (null !== oCallbackUrl) {
return yield sendStatusDocument(ctx, docId, bChangeBase, opt_userAction, undefined, oCallbackUrl, baseUrl, opt_userData);
}
return null;
}
const unlockWopiDoc = co.wrap(function* (ctx, docId, opt_userIndex) {
//wopi unlock
const getRes = yield getCallback(ctx, docId, opt_userIndex);
if (getRes && getRes.wopiParams && getRes.wopiParams.userAuth && 'view' !== getRes.wopiParams.userAuth.mode) {
const unlockRes = yield wopiClient.unlock(ctx, getRes.wopiParams);
const unlockInfo = wopiClient.getWopiUnlockMarker(getRes.wopiParams);
if (unlockInfo && unlockRes) {
yield canvasService.commandOpenStartPromise(ctx, docId, undefined, unlockInfo);
}
}
});
function* cleanDocumentOnExit(ctx, docId, deleteChanges, opt_userIndex) {
const tenForgottenFiles = ctx.getCfg('services.CoAuthoring.server.forgottenfiles', cfgForgottenFiles);
//clean redis (redisKeyPresenceSet and redisKeyPresenceHash removed with last element)
yield editorData.cleanDocumentOnExit(ctx, docId);
if (preStopFlag && editorStatProxy?.deleteKey) {
yield editorStatProxy.deleteKey(docId);
}
//remove changes
if (deleteChanges) {
yield taskResult.restoreInitialPassword(ctx, docId);
sqlBase.deleteChanges(ctx, docId, null);
//delete forgotten after successful send on callbackUrl
yield storage.deletePath(ctx, docId, tenForgottenFiles);
}
yield unlockWopiDoc(ctx, docId, opt_userIndex);
}
function* cleanDocumentOnExitNoChanges(ctx, docId, opt_userId, opt_userIndex, opt_forceClose, opt_deleteChanges) {
const userAction = opt_userId ? new commonDefines.OutputAction(commonDefines.c_oAscUserAction.Out, opt_userId) : null;
// We send that everyone is gone and there are no changes (to set the status on the server about the end of editing)
yield sendStatusDocument(ctx, docId, c_oAscChangeBase.No, userAction, opt_userIndex, undefined, undefined, undefined, opt_forceClose);
//if the user entered the document, the connection was broken, all information was deleted on the server,
//when the connection is restored, the userIndex will be saved and it will match the userIndex of the next user
yield* cleanDocumentOnExit(ctx, docId, opt_deleteChanges || false, opt_userIndex);
}
function createSaveTimer(ctx, docId, opt_userId, opt_userIndex, opt_userLcid, opt_queue, opt_noDelay, opt_initShardKey) {
return co(function* () {
const tenAscSaveTimeOutDelay = ctx.getCfg('services.CoAuthoring.server.savetimeoutdelay', cfgAscSaveTimeOutDelay);
const updateMask = new taskResult.TaskResultData();
updateMask.tenant = ctx.tenant;
updateMask.key = docId;
updateMask.status = commonDefines.FileStatus.Ok;
updateMask.callback = 'NOT_EMPTY';
const updateTask = new taskResult.TaskResultData();
updateTask.status = commonDefines.FileStatus.SaveVersion;
updateTask.statusInfo = utils.getMillisecondsOfHour(new Date());
const updateIfRes = yield taskResult.updateIf(ctx, updateTask, updateMask);
if (updateIfRes.affectedRows > 0) {
if (!opt_noDelay) {
yield utils.sleep(tenAscSaveTimeOutDelay);
}
while (true) {
if (!sqlBase.isLockCriticalSection(docId)) {
yield canvasService.saveFromChanges(
ctx,
docId,
updateTask.statusInfo,
null,
opt_userId,
opt_userIndex,
opt_userLcid,
opt_queue,
opt_initShardKey
);
break;
}
yield utils.sleep(c_oAscLockTimeOutDelay);
}
} else {
const selectRes = yield taskResult.select(ctx, docId);
if (selectRes.length > 0 && selectRes[0].callback) {
//if it didn't work, it means FileStatus=SaveVersion(someone else started building) or UpdateVersion(build completed)
// in this case, nothing needs to be done
ctx.logger.debug('createSaveTimer updateIf no effect');
} else {
ctx.logger.debug('createSaveTimer empty callback: %s', docId);
yield* cleanDocumentOnExitNoChanges(ctx, docId, opt_userId, opt_userIndex, false, true);
}
}
});
}
function checkJwt(ctx, token, type) {
return co(function* () {
const tenTokenVerifyOptions = ctx.getCfg('services.CoAuthoring.token.verifyOptions', cfgTokenVerifyOptions);
const res = {decoded: null, description: null, code: null, token};
const secret = yield tenantManager.getTenantSecret(ctx, type);
if (undefined == secret) {
ctx.logger.warn('empty secret: token = %s', token);
}
try {
res.decoded = jwt.verify(token, utils.getJwtHsKey(secret), tenTokenVerifyOptions);
ctx.logger.debug('checkJwt success: decoded = %j', res.decoded);
} catch (err) {
ctx.logger.warn('checkJwt error: name = %s message = %s token = %s', err.name, err.message, token);
if ('TokenExpiredError' === err.name) {
res.code = constants.JWT_EXPIRED_CODE;
res.description = constants.JWT_EXPIRED_REASON + err.message;
} else if ('JsonWebTokenError' === err.name) {
res.code = constants.JWT_ERROR_CODE;
res.description = constants.JWT_ERROR_REASON + err.message;
}
}
return res;
});
}
function checkJwtHeader(ctx, req, opt_header, opt_prefix, opt_secretType) {
return co(function* () {
const tenTokenInboxHeader = ctx.getCfg('services.CoAuthoring.token.inbox.header', cfgTokenInboxHeader);
const tenTokenInboxPrefix = ctx.getCfg('services.CoAuthoring.token.inbox.prefix', cfgTokenInboxPrefix);
const header = opt_header || tenTokenInboxHeader;
const prefix = opt_prefix || tenTokenInboxPrefix;
const secretType = opt_secretType || commonDefines.c_oAscSecretType.Inbox;
const authorization = req.get(header);
if (authorization && authorization.startsWith(prefix)) {
const token = authorization.substring(prefix.length);
return yield checkJwt(ctx, token, secretType);
}
return null;
});
}
function getRequestParams(ctx, req, _opt_isNotInBody) {
return co(function* () {
const tenTokenEnableRequestInbox = ctx.getCfg('services.CoAuthoring.token.enable.request.inbox', cfgTokenEnableRequestInbox);
const tenTokenRequiredParams = ctx.getCfg('services.CoAuthoring.server.tokenRequiredParams', cfgTokenRequiredParams);
const res = {code: constants.NO_ERROR, description: '', isDecoded: false, params: undefined};
if (req.body && Buffer.isBuffer(req.body) && req.body.length > 0) {
try {
res.params = JSON.parse(req.body.toString('utf8'));
} catch (err) {
ctx.logger.debug('getRequestParams error parsing json body: %s', err.stack);
}
}
if (!res.params) {
res.params = req.query;
}
if (tenTokenEnableRequestInbox) {
res.code = constants.VKEY;
let checkJwtRes;
if (res.params.token) {
checkJwtRes = yield checkJwt(ctx, res.params.token, commonDefines.c_oAscSecretType.Inbox);
} else {
checkJwtRes = yield checkJwtHeader(ctx, req);
}
if (checkJwtRes) {
if (checkJwtRes.decoded) {
res.code = constants.NO_ERROR;
res.isDecoded = true;
if (tenTokenRequiredParams) {
res.params = {};
}
Object.assign(res.params, checkJwtRes.decoded);
if (!utils.isEmptyObject(checkJwtRes.decoded.payload)) {
Object.assign(res.params, checkJwtRes.decoded.payload);
}
if (!utils.isEmptyObject(checkJwtRes.decoded.query)) {
Object.assign(res.params, checkJwtRes.decoded.query);
}
} else if (constants.JWT_EXPIRED_CODE == checkJwtRes.code) {
res.code = constants.VKEY_KEY_EXPIRE;
}
res.description = checkJwtRes.description;
}
}
return res;
});
}
function getLicenseNowUtc() {
const now = new Date();
return Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds()) / 1000;
}
const getParticipantMap = co.wrap(function* (ctx, docId, opt_hvals) {
const participantsMap = [];
let hvals;
if (opt_hvals) {
hvals = opt_hvals;
} else {
hvals = yield editorData.getPresence(ctx, docId, connections);
}
for (let i = 0; i < hvals.length; ++i) {
const elem = JSON.parse(hvals[i]);
if (!elem.isCloseCoAuthoring) {
participantsMap.push(elem);
}
}
return participantsMap;
});
function getOpenFormatByEditor(editorType) {
let res;
switch (editorType) {
case EditorTypes.spreadsheet:
res = constants.AVS_OFFICESTUDIO_FILE_CANVAS_SPREADSHEET;
break;
case EditorTypes.presentation:
res = constants.AVS_OFFICESTUDIO_FILE_CANVAS_PRESENTATION;
break;
case EditorTypes.diagram:
res = constants.AVS_OFFICESTUDIO_FILE_DRAW_VSDX;
break;
default:
res = constants.AVS_OFFICESTUDIO_FILE_CANVAS_WORD;
break;
}
return res;
}
async function isSchemaCompatible([tableName, tableSchema]) {
const resultSchema = await sqlBase.getTableColumns(operationContext.global, tableName);
if (resultSchema.length === 0) {
operationContext.global.logger.error('DB table "%s" does not exist', tableName);
return false;
}
const columnArray = resultSchema.map(row => row['column_name']);
const hashedResult = new Set(columnArray);
const schemaDiff = tableSchema.filter(column => !hashedResult.has(column));
if (schemaDiff.length > 0) {
operationContext.global.logger.error(`DB table "${tableName}" does not contain columns: ${schemaDiff}, columns info: ${columnArray}`);
return false;
}
return true;
}
exports.c_oAscServerStatus = c_oAscServerStatus;
exports.editorData = editorData;
exports.editorStat = editorStat;
exports.editorStatProxy = editorStatProxy;
exports.sendData = sendData;
exports.modifyConnectionForPassword = modifyConnectionForPassword;
exports.parseUrl = parseUrl;
exports.parseReplyData = parseReplyData;
exports.sendServerRequest = sendServerRequest;
exports.createSaveTimer = createSaveTimer;
exports.changeConnectionInfo = changeConnectionInfo;
exports.signToken = signToken;
exports.publish = publish;
exports.addTask = addTask;
exports.addDelayed = addDelayed;
exports.removeResponse = removeResponse;
exports.hasEditors = hasEditors;
exports.getEditorsCountPromise = co.wrap(getEditorsCount);
exports.getCallback = getCallback;
exports.getIsShutdown = getIsShutdown;
exports.getIsPreStop = getIsPreStop;
exports.hasChanges = hasChanges;
exports.cleanDocumentOnExitPromise = co.wrap(cleanDocumentOnExit);
exports.cleanDocumentOnExitNoChangesPromise = co.wrap(cleanDocumentOnExitNoChanges);
exports.unlockWopiDoc = unlockWopiDoc;
exports.setForceSave = setForceSave;
exports.startForceSave = startForceSave;
exports.resetForceSaveAfterChanges = resetForceSaveAfterChanges;
exports.getExternalChangeInfo = getExternalChangeInfo;
exports.checkJwt = checkJwt;
exports.getRequestParams = getRequestParams;
exports.checkJwtHeader = checkJwtHeader;
async function encryptPasswordParams(ctx, data) {
let dataWithPassword;
if (data.type === 'openDocument' && data.message) {
dataWithPassword = data.message;
} else if (data.type === 'auth' && data.openCmd) {
dataWithPassword = data.openCmd;
}
if (dataWithPassword && dataWithPassword.password) {
if (dataWithPassword.password.length > constants.PASSWORD_MAX_LENGTH) {
//todo send back error
ctx.logger.warn(
'encryptPasswordParams password too long actual = %s; max = %s',
dataWithPassword.password.length,
constants.PASSWORD_MAX_LENGTH
);
dataWithPassword.password = null;
} else {
dataWithPassword.password = await utils.encryptPassword(ctx, dataWithPassword.password);
}
}
if (dataWithPassword && dataWithPassword.savepassword) {
if (dataWithPassword.savepassword.length > constants.PASSWORD_MAX_LENGTH) {
//todo send back error
ctx.logger.warn(
'encryptPasswordParams password too long actual = %s; max = %s',
dataWithPassword.savepassword.length,
constants.PASSWORD_MAX_LENGTH
);
dataWithPassword.savepassword = null;
} else {
dataWithPassword.savepassword = await utils.encryptPassword(ctx, dataWithPassword.savepassword);
}
}
}
exports.encryptPasswordParams = encryptPasswordParams;
exports.getOpenFormatByEditor = getOpenFormatByEditor;
exports.install = function (server, app, callbackFunction) {
const io = new Server(server, cfgSocketIoConnection);
io.use((socket, next) => {
co(function* () {
const ctx = new operationContext.Context();
let res;
let checkJwtRes;
try {
ctx.initFromConnection(socket);
yield ctx.initTenantCache();
ctx.logger.info('io.use start');
const tenTokenEnableBrowser = ctx.getCfg('services.CoAuthoring.token.enable.browser', cfgTokenEnableBrowser);
const handshake = socket.handshake;
const token = handshake?.auth?.session || handshake?.auth?.token;
if (tenTokenEnableBrowser || token) {
const secretType = handshake?.auth?.session ? commonDefines.c_oAscSecretType.Session : commonDefines.c_oAscSecretType.Browser;
checkJwtRes = yield checkJwt(ctx, token, secretType);
if (!checkJwtRes.decoded) {
res = new Error('not authorized');
res.data = {code: checkJwtRes.code, description: checkJwtRes.description};
}
}
} catch (err) {
ctx.logger.error('io.use error: %s', err.stack);
} finally {
ctx.logger.info('io.use end');
next(res);
}
});
});
io.on('connection', async conn => {
const ctx = new operationContext.Context();
try {
if (!conn) {
operationContext.global.logger.error('null == conn');
return;
}
ctx.initFromConnection(conn);
await ctx.initTenantCache();
if (constants.DEFAULT_DOC_ID === ctx.docId) {
ctx.logger.error('io.on connection unexpected key use key pattern = "%s" url = %s', constants.DOC_ID_PATTERN, conn.handshake?.url);
sendDataDisconnectReason(ctx, conn, constants.ACCESS_DENIED_CODE, constants.ACCESS_DENIED_REASON);
conn.disconnect(true);
return;
}
if (getIsShutdown()) {
sendDataDisconnectReason(ctx, conn, constants.SHUTDOWN_CODE, constants.SHUTDOWN_REASON);
conn.disconnect(true);
return;
}
conn.baseUrl = utils.getBaseUrlByConnection(ctx, conn);
conn.sessionIsSendWarning = false;
conn.sessionTimeConnect = conn.sessionTimeLastAction = new Date().getTime();
conn.on('message', data => {
return co(function* () {
let docId = 'null';
const ctx = new operationContext.Context();
try {
ctx.initFromConnection(conn);
yield ctx.initTenantCache();
const tenErrorFiles = ctx.getCfg('FileConverter.converter.errorfiles', cfgErrorFiles);
let startDate = null;
if (clientStatsD) {
startDate = new Date();
}
docId = conn.docId;
ctx.logger.info('data.type = %s', data.type);
if (getIsShutdown()) {
ctx.logger.debug('Server shutdown receive data');
return;
}
if (
(conn.isCloseCoAuthoring || (conn.user && conn.user.view)) &&
('getLock' == data.type || 'saveChanges' == data.type || 'isSaveLock' == data.type)
) {
ctx.logger.warn('conn.user.view||isCloseCoAuthoring access deny: type = %s', data.type);
sendDataDisconnectReason(ctx, conn, constants.ACCESS_DENIED_CODE, constants.ACCESS_DENIED_REASON);
conn.disconnect(true);
return;
}
yield encryptPasswordParams(ctx, data);
switch (data.type) {
case 'auth':
try {
yield* auth(ctx, conn, data);
} catch (err) {
ctx.logger.error('auth error: %s', err.stack);
sendDataDisconnectReason(ctx, conn, constants.ACCESS_DENIED_CODE, constants.ACCESS_DENIED_REASON);
conn.disconnect(true);
return;
}
break;
case 'message':
yield* onMessage(ctx, conn, data);
break;
case 'cursor':
yield* onCursor(ctx, conn, data);
break;
case 'getLock':
yield getLock(ctx, conn, data, false);
break;
case 'saveChanges':
yield* saveChanges(ctx, conn, data);
break;
case 'isSaveLock':
yield* isSaveLock(ctx, conn, data);
break;
case 'unSaveLock':
yield* unSaveLock(ctx, conn, -1, -1, -1);
break; // The index is sent -1, because this is an emergency withdrawal without saving
case 'getMessages':
yield* getMessages(ctx, conn, data);
break;
case 'unLockDocument':
yield* checkEndAuthLock(ctx, data.unlock, data.isSave, docId, conn.user.id, data.releaseLocks, data.deleteIndex, conn);
break;
case 'close':
yield* closeDocument(ctx, conn);
break;
case 'openDocument': {
const cmd = new commonDefines.InputCommand(data.message);
cmd.fillFromConnection(conn);
yield canvasService.openDocument(ctx, conn, cmd);
break;
}
case 'clientLog':
yield handleClientLog(ctx, conn, docId, data, tenErrorFiles);
break;
case 'extendSession':
ctx.logger.debug('extendSession idletime: %d', data.idletime);
conn.sessionIsSendWarning = false;
conn.sessionTimeLastAction = new Date().getTime() - data.idletime;
break;
case 'forceSaveStart': {
let forceSaveRes;
if (conn.user) {
forceSaveRes = yield startForceSave(
ctx,
docId,
commonDefines.c_oAscForceSaveTypes.Button,
undefined,
undefined,
conn.user.idOriginal,
conn.user.id,
undefined,
conn.user.indexUser,
undefined,
undefined,
undefined,
undefined,
conn
);
} else {
forceSaveRes = {code: commonDefines.c_oAscServerCommandErrors.UnknownError, time: null};
}
sendData(ctx, conn, {type: 'forceSaveStart', messages: forceSaveRes});
break;
}
case 'rpc':
yield* startRPC(ctx, conn, data.responseKey, data.data);
break;
case 'authChangesAck':
delete conn.authChangesAck;
break;
default:
ctx.logger.debug('unknown command %j', data);
break;
}
if (clientStatsD) {
const isSendMetric = 'auth' === data.type || 'getLock' === data.type || 'saveChanges' === data.type;
if (isSendMetric) {
clientStatsD.timing('coauth.data.' + data.type, new Date() - startDate);
}
}
} catch (e) {
ctx.logger.error('error receiving response: type = %s %s', data && data.type ? data.type : 'null', e.stack);
}
});
});
conn.on('disconnect', reason => {
return co(function* () {
const ctx = new operationContext.Context();
try {
ctx.initFromConnection(conn);
yield ctx.initTenantCache();
yield* closeDocument(ctx, conn, reason);
} catch (err) {
ctx.logger.error('Error conn close: %s', err.stack);
}
});
});
_checkLicense(ctx, conn);
} catch (err) {
ctx.logger.error('connection error: %s', err.stack);
sendDataDisconnectReason(ctx, conn, constants.DROP_CODE, constants.DROP_REASON);
conn.disconnect(true);
}
});
io.engine.on('connection_error', err => {
let logger = operationContext.global.logger;
if (err.req) {
const ctx = new operationContext.Context();
// Ensure raw IncomingMessage has Express properties for consistent context init
utils.expressifyIncomingMessage(err.req, app);
ctx.initFromConnectionRequest(err.req);
logger = ctx.logger;
}
logger.warn('io.connection_error code=%s, message=%s, url=%s', err?.code, err?.message, err?.req?.url);
});
/**
*
* @param ctx
* @param conn
* @param reason - the reason of the disconnection (either client or server-side)
*/
function* closeDocument(ctx, conn, reason) {
const tenForgottenFiles = ctx.getCfg('services.CoAuthoring.server.forgottenfiles', cfgForgottenFiles);
ctx.logger.info('Connection closed or timed out: reason = %s', reason);
let userLocks,
reconnected = false,
bHasEditors,
bHasChanges;
const docId = conn.docId;
if (null == docId) {
return;
}
let hvals;
let participantsTimestamp;
const tmpUser = conn.user;
const isView = tmpUser.view;
const isCloseCoAuthoringTmp = conn.isCloseCoAuthoring;
if (reason) {
//Notify that participant has gone
connections = _.reject(connections, el => {
return el.id === conn.id; //Delete this connection
});
//Check if it's not already reconnected
reconnected = yield* isUserReconnect(ctx, docId, tmpUser.id, conn.id);
if (reconnected) {
ctx.logger.info('reconnected');
} else {
yield removePresence(ctx, conn);
hvals = yield editorData.getPresence(ctx, docId, connections);
participantsTimestamp = Date.now();
if (hvals.length <= 0) {
yield editorData.removePresenceDocument(ctx, docId);
}
}
} else {
if (!conn.isCloseCoAuthoring && !isView) {
modifyConnectionEditorToView(ctx, conn);
conn.isCloseCoAuthoring = true;
yield addPresence(ctx, conn, true);
const sessionToken = yield fillJwtByConnection(ctx, conn);
sendDataRefreshToken(ctx, conn, sessionToken);
}
}
if (isCloseCoAuthoringTmp) {
//we already close connection
return;
}
if (!reconnected) {
//revert old view to send event
const tmpView = tmpUser.view;
tmpUser.view = isView;
const participants = yield getParticipantMap(ctx, docId, hvals);
if (!participantsTimestamp) {
participantsTimestamp = Date.now();
}
yield publish(
ctx,
{type: commonDefines.c_oPublishType.participantsState, ctx, docId, userId: tmpUser.id, participantsTimestamp, participants},
docId,
tmpUser.id
);
tmpUser.view = tmpView;
// editors only
if (false === isView) {
// For this user, we remove the lock from saving
yield editorData.unlockSave(ctx, docId, conn.user.id);
bHasEditors = yield* hasEditors(ctx, docId, hvals);
bHasChanges = yield hasChanges(ctx, docId);
let needSendStatus = true;
if (conn.encrypted) {
const selectRes = yield taskResult.select(ctx, docId);
if (selectRes.length > 0) {
const row = selectRes[0];
if (commonDefines.FileStatus.UpdateVersion === row.status) {
needSendStatus = false;
}
}
}
//Release locks
userLocks = yield removeUserLocks(ctx, docId, conn.user.id);
if (0 < userLocks.length) {
//todo send nothing in case of close document
//sendReleaseLock(conn, userLocks);
yield publish(
ctx,
{type: commonDefines.c_oPublishType.releaseLock, ctx, docId, userId: conn.user.id, locks: userLocks},
docId,
conn.user.id
);
}
// For this user, remove the Lock from the document
yield* checkEndAuthLock(ctx, true, false, docId, conn.user.id);
const userIndex = utils.getIndexFromUserId(tmpUser.id, tmpUser.idOriginal);
// If we do not have users, then delete all messages
if (!bHasEditors) {
// Just in case, remove the lock
yield editorData.unlockSave(ctx, docId, tmpUser.id);
let needSaveChanges = bHasChanges;
if (!needSaveChanges) {
//start save changes if forgotten file exists.
//more effective to send file without sfc, but this method is simpler by code
const forgotten = yield storage.listObjects(ctx, docId, tenForgottenFiles);
needSaveChanges = forgotten.length > 0;
ctx.logger.debug('closeDocument hasForgotten %s', needSaveChanges);
}
if (needSaveChanges && !conn.encrypted) {
// Send changes to save server
const user_lcid = utilsDocService.localeToLCID(conn.lang);
//noDelay=true if the client intentionally closes connection or server shuts down
const noDelay = !reason || getIsShutdown();
yield createSaveTimer(ctx, docId, tmpUser.idOriginal, userIndex, user_lcid, undefined, noDelay);
} else if (needSendStatus) {
yield* cleanDocumentOnExitNoChanges(ctx, docId, tmpUser.idOriginal, userIndex);
} else {
yield* cleanDocumentOnExit(ctx, docId, false, userIndex);
}
} else if (needSendStatus) {
yield sendStatusDocument(
ctx,
docId,
c_oAscChangeBase.No,
new commonDefines.OutputAction(commonDefines.c_oAscUserAction.Out, tmpUser.idOriginal),
userIndex
);
}
} else {
if (preStopFlag && hvals?.length <= 0 && editorStatProxy?.deleteKey) {
yield editorStatProxy.deleteKey(docId);
}
}
const sessionType = isView ? 'view' : 'edit';
const sessionTimeMs = new Date().getTime() - conn.sessionTimeConnect;
ctx.logger.debug(`closeDocument %s session time:%s`, sessionType, sessionTimeMs);
if (clientStatsD) {
clientStatsD.timing(`coauth.session.${sessionType}`, sessionTimeMs);
}
}
}
/**
* Handle client log message and create error files once per connection on first error.
* @param {object} ctx - Operation context
* @param {object} conn - Socket connection
* @param {string} docId - Document identifier
* @param {{level?: string, msg?: string}} data - Client log data
* @param {object} tenErrorFiles - Error files storage configuration
* @returns {Promise<void>}
*/
async function handleClientLog(ctx, conn, docId, data, tenErrorFiles) {
const level = data.level?.toLowerCase();
if ('trace' === level || 'debug' === level || 'info' === level || 'warn' === level || 'error' === level || 'fatal' === level) {
ctx.logger[level]('clientLog: %s', data.msg);
}
if ('error' === level && tenErrorFiles && docId && !conn.clientError) {
conn.clientError = true;
const destDir = 'browser/' + docId;
const list = await storage.listObjects(ctx, destDir, tenErrorFiles);
if (list.length === 0) {
await storage.copyPath(ctx, docId, destDir, undefined, tenErrorFiles);
await saveErrorChanges(ctx, docId, destDir);
}
}
}
// Getting changes for the document (either from the cache or accessing the database, but only if there were saves)
function* getDocumentChanges(ctx, docId, optStartIndex, optEndIndex) {
// If during that moment, while we were waiting for a response from the database, everyone left, then nothing needs to be sent
const arrayElements = yield sqlBase.getChangesPromise(ctx, docId, optStartIndex, optEndIndex);
let j, element;
const objChangesDocument = new DocumentChanges(docId);
for (j = 0; j < arrayElements.length; ++j) {
element = arrayElements[j];
// We add GMT, because. we write UTC to the database, but the string without UTC is saved there and the time will be wrong when reading
objChangesDocument.push({
docid: docId,
change: element['change_data'],
time: element['change_date'].getTime(),
user: element['user_id'],
useridoriginal: element['user_id_original']
});
}
return objChangesDocument;
}
async function removeUserLocks(ctx, docId, userId) {
const locks = await editorData.getLocks(ctx, docId);
const res = [];
const toRemove = {};
for (const lockId in locks) {
const lock = locks[lockId];
if (lock.user === userId) {
toRemove[lockId] = lock;
res.push(lock);
}
}
await editorData.removeLocks(ctx, docId, toRemove);
return res;
}
function* checkEndAuthLock(ctx, unlock, isSave, docId, userId, releaseLocks, deleteIndex, conn) {
let result = false;
if (null != deleteIndex && -1 !== deleteIndex) {
let puckerIndex = yield* getChangesIndex(ctx, docId);
const deleteCount = puckerIndex - deleteIndex;
if (0 < deleteCount) {
puckerIndex -= deleteCount;
yield sqlBase.deleteChangesPromise(ctx, docId, deleteIndex);
} else if (0 > deleteCount) {
ctx.logger.error('Error checkEndAuthLock: deleteIndex: %s ; startIndex: %s ; deleteCount: %s', deleteIndex, puckerIndex, deleteCount);
}
}
if (unlock) {
const unlockRes = yield editorData.unlockAuth(ctx, docId, userId);
if (commonDefines.c_oAscUnlockRes.Unlocked === unlockRes) {
const participantsMap = yield getParticipantMap(ctx, docId);
yield publish(ctx, {
type: commonDefines.c_oPublishType.auth,
ctx,
docId,
userId,
participantsMap
});
result = true;
}
}
//Release locks
if (releaseLocks && conn) {
const userLocks = yield removeUserLocks(ctx, docId, userId);
if (0 < userLocks.length) {
sendReleaseLock(ctx, conn, userLocks);
yield publish(
ctx,
{
type: commonDefines.c_oPublishType.releaseLock,
ctx,
docId,
userId,
locks: userLocks
},
docId,
userId
);
}
}
if (isSave && conn) {
// Automatically remove the lock ourselves
yield* unSaveLock(ctx, conn, -1, -1, -1);
}
return result;
}
/**
* Schedule lock cleanup for a document after the configured timeout.
* @param {operationContext} ctx - Operation context
* @param {string} docId - Document identifier
* @param {string} userId - User identifier associated with the lock
*/
function setLockDocumentTimer(ctx, docId, userId) {
const tenExpLockDoc = ctx.getCfg('services.CoAuthoring.expire.lockDoc', cfgExpLockDoc);
const timerId = setTimeout(() => {
return co(function* () {
try {
ctx.logger.warn('lockDocumentsTimerId timeout');
delete lockDocumentsTimerId[docId];
//todo remove checkEndAuthLock(only needed for lost connections in redis)
yield* checkEndAuthLock(ctx, true, false, docId, userId);
yield* publishCloseUsersConnection(ctx, docId, [userId], false, constants.DROP_CODE, constants.DROP_REASON);
} catch (e) {
ctx.logger.error('lockDocumentsTimerId error: %s', e.stack);
}
});
}, 1000 * tenExpLockDoc);
lockDocumentsTimerId[docId] = {timerId, userId};
ctx.logger.debug('lockDocumentsTimerId set');
}
function cleanLockDocumentTimer(docId, lockDocumentTimer) {
clearTimeout(lockDocumentTimer.timerId);
delete lockDocumentsTimerId[docId];
}
function sendParticipantsState(ctx, participants, data) {
_.each(participants, participant => {
sendData(ctx, participant, {
type: 'connectState',
participantsTimestamp: data.participantsTimestamp,
participants: data.participants,
waitAuth: !!data.waitAuthUserId
});
});
}
function sendFileError(ctx, conn, errorId, code, opt_notWarn) {
if (opt_notWarn) {
ctx.logger.debug('error description: errorId = %s', errorId);
} else {
ctx.logger.warn('error description: errorId = %s', errorId);
}
sendData(ctx, conn, {type: 'error', description: errorId, code});
}
function* sendFileErrorAuth(ctx, conn, sessionId, errorId, code, opt_notWarn) {
conn.sessionId = sessionId; //restore old
//Kill previous connections
connections = _.reject(connections, el => {
return el.sessionId === sessionId; //Delete this connection
});
//closing could happen during async action
if (constants.CONN_CLOSED !== conn.conn.readyState) {
modifyConnectionEditorToView(ctx, conn);
conn.isCloseCoAuthoring = true;
// We put it in an array, because we need to send data to open/save the document
connections.push(conn);
yield addPresence(ctx, conn, true);
const sessionToken = yield fillJwtByConnection(ctx, conn);
sendDataRefreshToken(ctx, conn, sessionToken);
sendFileError(ctx, conn, errorId, code, opt_notWarn);
}
}
// Recalculation only for foreign Lock when saving on a client that added/deleted rows or columns
function _recalcLockArray(userId, _locks, oRecalcIndexColumns, oRecalcIndexRows) {
const res = {};
if (null == _locks) {
return res;
}
let element = null,
oRangeOrObjectId = null;
let sheetId = -1;
for (const lockId in _locks) {
let isModify = false;
const lock = _locks[lockId];
// we do not count for ourselves
if (userId === lock.user) {
continue;
}
element = lock.block;
if (
c_oAscLockTypeElem.Range !== element['type'] ||
c_oAscLockTypeElemSubType.InsertColumns === element['subType'] ||
c_oAscLockTypeElemSubType.InsertRows === element['subType']
) {
continue;
}
sheetId = element['sheetId'];
oRangeOrObjectId = element['rangeOrObjectId'];
if (oRecalcIndexColumns && Object.hasOwn(oRecalcIndexColumns, sheetId)) {
// Column index recalculation
oRangeOrObjectId['c1'] = oRecalcIndexColumns[sheetId].getLockMe2(oRangeOrObjectId['c1']);
oRangeOrObjectId['c2'] = oRecalcIndexColumns[sheetId].getLockMe2(oRangeOrObjectId['c2']);
isModify = true;
}
if (oRecalcIndexRows && Object.hasOwn(oRecalcIndexRows, sheetId)) {
// row index recalculation
oRangeOrObjectId['r1'] = oRecalcIndexRows[sheetId].getLockMe2(oRangeOrObjectId['r1']);
oRangeOrObjectId['r2'] = oRecalcIndexRows[sheetId].getLockMe2(oRangeOrObjectId['r2']);
isModify = true;
}
if (isModify) {
res[lockId] = lock;
}
}
return res;
}
function _addRecalcIndex(oRecalcIndex) {
if (null == oRecalcIndex) {
return null;
}
let nIndex = 0;
let nRecalcType = c_oAscRecalcIndexTypes.RecalcIndexAdd;
let oRecalcIndexElement = null;
const oRecalcIndexResult = {};
for (const sheetId in oRecalcIndex) {
if (Object.hasOwn(oRecalcIndex, sheetId)) {
if (!Object.hasOwn(oRecalcIndexResult, sheetId)) {
oRecalcIndexResult[sheetId] = new CRecalcIndex();
}
for (; nIndex < oRecalcIndex[sheetId]._arrElements.length; ++nIndex) {
oRecalcIndexElement = oRecalcIndex[sheetId]._arrElements[nIndex];
if (true === oRecalcIndexElement.m_bIsSaveIndex) {
continue;
}
nRecalcType =
c_oAscRecalcIndexTypes.RecalcIndexAdd === oRecalcIndexElement._recalcType
? c_oAscRecalcIndexTypes.RecalcIndexRemove
: c_oAscRecalcIndexTypes.RecalcIndexAdd;
// Duplicate to return the result (we only need to recalculate by the last index
oRecalcIndexResult[sheetId].add(nRecalcType, oRecalcIndexElement._position, oRecalcIndexElement._count, /*bIsSaveIndex*/ true);
}
}
}
return oRecalcIndexResult;
}
function compareExcelBlock(newBlock, oldBlock) {
// This is a lock to remove or add rows/columns
if (null !== newBlock.subType && null !== oldBlock.subType) {
return true;
}
// Ignore lock from ChangeProperties (only if it's not a leaf lock)
if (
(c_oAscLockTypeElemSubType.ChangeProperties === oldBlock.subType && c_oAscLockTypeElem.Sheet !== newBlock.type) ||
(c_oAscLockTypeElemSubType.ChangeProperties === newBlock.subType && c_oAscLockTypeElem.Sheet !== oldBlock.type)
) {
return false;
}
let resultLock = false;
if (newBlock.type === c_oAscLockTypeElem.Range) {
if (oldBlock.type === c_oAscLockTypeElem.Range) {
// We do not take into account lock from Insert
if (c_oAscLockTypeElemSubType.InsertRows === oldBlock.subType || c_oAscLockTypeElemSubType.InsertColumns === oldBlock.subType) {
resultLock = false;
} else if (isInterSection(newBlock.rangeOrObjectId, oldBlock.rangeOrObjectId)) {
resultLock = true;
}
} else if (oldBlock.type === c_oAscLockTypeElem.Sheet) {
resultLock = true;
}
} else if (newBlock.type === c_oAscLockTypeElem.Sheet) {
resultLock = true;
} else if (newBlock.type === c_oAscLockTypeElem.Object) {
if (oldBlock.type === c_oAscLockTypeElem.Sheet) {
resultLock = true;
} else if (oldBlock.type === c_oAscLockTypeElem.Object && oldBlock.rangeOrObjectId === newBlock.rangeOrObjectId) {
resultLock = true;
}
}
return resultLock;
}
function isInterSection(range1, range2) {
if (range2.c1 > range1.c2 || range2.c2 < range1.c1 || range2.r1 > range1.r2 || range2.r2 < range1.r1) {
return false;
}
return true;
}
function comparePresentationBlock(newBlock, oldBlock) {
let resultLock = false;
switch (newBlock.type) {
case c_oAscLockTypeElemPresentation.Presentation:
if (c_oAscLockTypeElemPresentation.Presentation === oldBlock.type) {
resultLock = newBlock.val === oldBlock.val;
}
break;
case c_oAscLockTypeElemPresentation.Slide:
if (c_oAscLockTypeElemPresentation.Slide === oldBlock.type) {
resultLock = newBlock.val === oldBlock.val;
} else if (c_oAscLockTypeElemPresentation.Object === oldBlock.type) {
resultLock = newBlock.val === oldBlock.slideId;
}
break;
case c_oAscLockTypeElemPresentation.Object:
if (c_oAscLockTypeElemPresentation.Slide === oldBlock.type) {
resultLock = newBlock.slideId === oldBlock.val;
} else if (c_oAscLockTypeElemPresentation.Object === oldBlock.type) {
resultLock = newBlock.objId === oldBlock.objId;
}
break;
}
return resultLock;
}
function* authRestore(ctx, conn, sessionId) {
conn.sessionId = sessionId; //restore old
//Kill previous connections
connections = _.reject(connections, el => {
return el.sessionId === sessionId; //Delete this connection
});
yield* endAuth(ctx, conn, true);
}
function fillUsername(ctx, data) {
let name;
const user = data.user;
if (user.firstname && user.lastname) {
//as in web-apps/apps/common/main/lib/util/utils.js
const isRu = data.lang && /^ru/.test(data.lang);
name = isRu ? user.lastname + ' ' + user.firstname : user.firstname + ' ' + user.lastname;
} else {
name = user.username || 'Anonymous';
}
if (name.length > constants.USER_NAME_MAX_LENGTH) {
ctx.logger.warn('fillUsername user name too long actual = %s; max = %s', name.length, constants.USER_NAME_MAX_LENGTH);
name = name.substr(0, constants.USER_NAME_MAX_LENGTH);
}
return name;
}
function isEditMode(permissions, mode) {
//like this.api.asc_setViewMode(!this.appOptions.isEdit && !this.appOptions.isRestrictedEdit);
//https://github.com/ONLYOFFICE/web-apps/blob/4a7879b4f88f315fe94d9f7d97c0ed8aa9f82221/apps/documenteditor/main/app/controller/Main.js#L1743
//todo permissions in embed editor
//https://github.com/ONLYOFFICE/web-apps/blob/72b8350c71e7b314b63b8eec675e76156bb4a2e4/apps/documenteditor/forms/app/controller/ApplicationController.js#L627
return (
(!mode || mode !== 'view') &&
(!permissions || permissions.edit !== false || permissions.review === true || permissions.comment === true || permissions.fillForms === true)
);
}
function fillDataFromWopiJwt(decoded, data) {
const res = true;
const openCmd = data.openCmd;
if (decoded.key) {
data.docid = decoded.key;
}
if (decoded.userAuth) {
data.documentCallbackUrl = JSON.stringify(decoded.userAuth);
data.mode = decoded.userAuth.mode;
data.forcedViewMode = decoded.userAuth.forcedViewMode;
}
if (decoded.queryParams) {
const queryParams = decoded.queryParams;
data.lang = queryParams.lang || queryParams.ui || constants.TEMPLATES_DEFAULT_LOCALE;
}
if (wopiClient.isWopiJwtToken(decoded)) {
const fileInfo = decoded.fileInfo;
const queryParams = decoded.queryParams;
if (openCmd) {
openCmd.format = wopiClient.getFileTypeByInfo(fileInfo);
openCmd.title = fileInfo.BreadcrumbDocName || fileInfo.BaseFileName;
}
const name = fileInfo.IsAnonymousUser ? '' : fileInfo.UserFriendlyName;
if (name) {
data.user.username = name;
data.denyChangeName = true;
}
if (null != fileInfo.UserId) {
data.user.id = fileInfo.UserId;
if (openCmd) {
openCmd.userid = fileInfo.UserId;
}
}
const permissionsEdit = !fileInfo.ReadOnly && !fileInfo.UserCanOnlyComment && fileInfo.UserCanWrite && queryParams?.formsubmit !== '1';
const permissionsReview =
fileInfo.UserCanOnlyComment || fileInfo.SupportsReviewing === false
? false
: fileInfo.UserCanReview === false
? false
: fileInfo.UserCanReview;
const permissionsComment = permissionsEdit || !!fileInfo.UserCanOnlyComment;
const permissionsFillForm = permissionsEdit || queryParams?.formsubmit === '1';
const permissions = {
edit: permissionsEdit,
review: permissionsReview,
comment: permissionsComment,
copy: fileInfo.CopyPasteRestrictions !== 'CurrentDocumentOnly' && fileInfo.CopyPasteRestrictions !== 'BlockAll',
print: !fileInfo.DisablePrint && !fileInfo.HidePrintOption,
chat: queryParams?.dchat !== '1',
fillForms: permissionsFillForm
};
//todo (review: undefined)
// res = isDeepStrictEqual(data.permissions, permissions);
if (!data.permissions) {
data.permissions = {};
}
//not '=' because if it jwt from previous version, we must use values from data
Object.assign(data.permissions, permissions);
}
return res;
}
function validateAuthToken(data, decoded) {
let res = '';
if (!decoded?.document?.key) {
res = 'document.key';
} else if (data.permissions && !decoded?.document?.permissions) {
res = 'document.permissions';
} else if (!decoded?.document?.url) {
res = 'document.url';
} else if (data.documentCallbackUrl && !decoded?.editorConfig?.callbackUrl) {
//todo callbackUrl required
res = 'editorConfig.callbackUrl';
} else if (data.mode && 'view' !== data.mode && !decoded?.editorConfig?.mode) {
//allow to restrict rights to 'view'
res = 'editorConfig.mode';
}
return res;
}
function fillDataFromJwt(ctx, decoded, data) {
let res = true;
const openCmd = data.openCmd;
if (decoded.document) {
const doc = decoded.document;
if (null != doc.key) {
data.docid = doc.key;
if (openCmd) {
openCmd.id = doc.key;
}
}
if (doc.permissions) {
res = isDeepStrictEqual(data.permissions, doc.permissions);
if (!res) {
ctx.logger.warn('fillDataFromJwt token has modified permissions');
}
if (!data.permissions) {
data.permissions = {};
}
//not '=' because if it jwt from previous version, we must use values from data
Object.assign(data.permissions, doc.permissions);
}
if (openCmd) {
if (null != doc.fileType) {
openCmd.format = doc.fileType;
}
if (null != doc.title) {
openCmd.title = doc.title;
}
if (null != doc.url) {
openCmd.url = doc.url;
}
}
if (null != doc.ds_encrypted) {
data.encrypted = doc.ds_encrypted;
}
}
if (decoded.editorConfig) {
const edit = decoded.editorConfig;
if (null != edit.callbackUrl) {
data.documentCallbackUrl = edit.callbackUrl;
}
if (null != edit.lang) {
data.lang = edit.lang;
}
//allow to restrict rights so don't use token mode in case of 'view'
if (null != edit.mode && 'view' !== data.mode) {
data.mode = edit.mode;
}
if (edit.coEditing?.mode) {
data.coEditingMode = edit.coEditing.mode;
if (edit.coEditing?.change) {
data.coEditingMode = 'fast';
}
//offline viewer for pdf|djvu|xps|oxps and embeded
const type = constants.VIEWER_ONLY.exec(decoded.document?.fileType);
if ((type && typeof type[1] === 'string') || 'embedded' === decoded.type) {
data.coEditingMode = 'strict';
}
}
if (null != edit.ds_isCloseCoAuthoring) {
data.isCloseCoAuthoring = edit.ds_isCloseCoAuthoring;
}
data.isEnterCorrectPassword = edit.ds_isEnterCorrectPassword;
data.denyChangeName = edit.ds_denyChangeName;
// data.sessionId = edit.ds_sessionId;
data.sessionTimeConnect = edit.ds_sessionTimeConnect;
if (edit.user) {
const dataUser = data.user;
const user = edit.user;
if (user.id) {
dataUser.id = user.id;
if (openCmd) {
openCmd.userid = user.id;
}
}
if (null != user.index) {
dataUser.indexUser = user.index;
}
if (user.firstname) {
dataUser.firstname = user.firstname;
}
if (user.lastname) {
dataUser.lastname = user.lastname;
}
if (user.name) {
dataUser.username = user.name;
}
if (user.group) {
//like in Common.Utils.fillUserInfo(web-apps/apps/common/main/lib/util/utils.js)
dataUser.username = user.group.toString() + String.fromCharCode(160) + dataUser.username;
}
if (user.customerId) {
dataUser.customerId = user.customerId;
}
}
if (edit.user && edit.user.name) {
data.denyChangeName = true;
}
}
//todo make required fields
if (decoded.url || decoded.payload || (decoded.key && !wopiClient.isWopiJwtToken(decoded))) {
ctx.logger.warn('fillDataFromJwt token has invalid format');
res = false;
}
return res;
}
function fillVersionHistoryFromJwt(ctx, decoded, data) {
const openCmd = data.openCmd;
data.mode = 'view';
data.coEditingMode = 'strict';
data.docid = decoded.key;
openCmd.url = decoded.url;
if (decoded.changesUrl && decoded.previous) {
const versionMatch = openCmd.serverVersion === commonDefines.buildVersion;
const openPreviousVersion = openCmd.id === decoded.previous.key;
if (versionMatch && openPreviousVersion) {
data.docid = decoded.previous.key;
openCmd.url = decoded.previous.url;
} else {
ctx.logger.warn(
'fillVersionHistoryFromJwt serverVersion mismatch or mismatch between previous url and changes. serverVersion=%s docId=%s',
openCmd.serverVersion,
openCmd.id
);
}
}
return true;
}
function* auth(ctx, conn, data) {
const tenExpUpdateVersionStatus = ms(ctx.getCfg('services.CoAuthoring.expire.updateVersionStatus', cfgExpUpdateVersionStatus));
const tenTokenEnableBrowser = ctx.getCfg('services.CoAuthoring.token.enable.browser', cfgTokenEnableBrowser);
const tenIsAnonymousSupport = ctx.getCfg('services.CoAuthoring.server.isAnonymousSupport', cfgIsAnonymousSupport);
const tenTokenRequiredParams = ctx.getCfg('services.CoAuthoring.server.tokenRequiredParams', cfgTokenRequiredParams);
//TODO: Do authorization etc. check md5 or query db
ctx.logger.debug('auth time: %d', data.time);
if (data.token && data.user) {
ctx.setUserId(data.user.id);
const [licenseInfo] = yield tenantManager.getTenantLicense(ctx);
let isDecoded = false;
//check jwt
const token = data.jwtSession || data.jwtOpen;
if (tenTokenEnableBrowser || token) {
const secretType = data.jwtSession ? commonDefines.c_oAscSecretType.Session : commonDefines.c_oAscSecretType.Browser;
const checkJwtRes = yield checkJwt(ctx, token, secretType);
if (checkJwtRes.decoded) {
isDecoded = true;
const decoded = checkJwtRes.decoded;
let fillDataFromJwtRes = false;
if (wopiClient.isWopiJwtToken(decoded)) {
//wopi
fillDataFromJwtRes = fillDataFromWopiJwt(decoded, data);
} else if (decoded.editorConfig && undefined !== decoded.editorConfig.ds_sessionTimeConnect) {
//reconnection
fillDataFromJwtRes = fillDataFromJwt(ctx, decoded, data);
} else if (decoded.version) {
//version required, but maybe add new type like jwtSession?
//version history
fillDataFromJwtRes = fillVersionHistoryFromJwt(ctx, decoded, data);
} else {
//opening
const validationErr = validateAuthToken(data, decoded);
if (!validationErr) {
fillDataFromJwtRes = fillDataFromJwt(ctx, decoded, data);
} else {
ctx.logger.error('auth missing required parameter %s (since 7.1 version)', validationErr);
if (tenTokenRequiredParams) {
sendDataDisconnectReason(ctx, conn, constants.JWT_ERROR_CODE, constants.JWT_ERROR_REASON);
conn.disconnect(true);
return;
} else {
fillDataFromJwtRes = fillDataFromJwt(ctx, decoded, data);
}
}
}
if (!fillDataFromJwtRes) {
ctx.logger.warn('fillDataFromJwt return false');
sendDataDisconnectReason(ctx, conn, constants.ACCESS_DENIED_CODE, constants.ACCESS_DENIED_REASON);
conn.disconnect(true);
return;
}
} else {
sendDataDisconnectReason(ctx, conn, checkJwtRes.code, checkJwtRes.description);
conn.disconnect(true);
return;
}
}
ctx.setUserId(data.user.id);
const docId = data.docid;
const user = data.user;
let wopiParams = null,
wopiParamsFull = null,
openedAtStr;
if (data.documentCallbackUrl) {
wopiParams = wopiClient.parseWopiCallback(ctx, data.documentCallbackUrl);
if (wopiParams && wopiParams.userAuth) {
conn.access_token_ttl = wopiParams.userAuth.access_token_ttl;
}
}
let cmd = null;
if (data.openCmd) {
cmd = new commonDefines.InputCommand(data.openCmd);
cmd.setDocId(docId);
if (isDecoded) {
cmd.setWithAuthorization(true);
}
}
//todo minimize select calls on opening
const result = yield taskResult.select(ctx, docId);
const resultRow = result.length > 0 ? result[0] : null;
if (wopiParams) {
if (resultRow && resultRow.callback) {
wopiParamsFull = wopiClient.parseWopiCallback(ctx, data.documentCallbackUrl, resultRow.callback);
cmd?.setWopiParams(wopiParamsFull);
}
if (!wopiParamsFull || !wopiParamsFull.userAuth || !wopiParamsFull.commonInfo) {
ctx.logger.warn('invalid wopi callback (maybe postgres<9.5) %j', wopiParams);
sendDataDisconnectReason(ctx, conn, constants.DROP_CODE, constants.DROP_REASON);
conn.disconnect(true);
return;
}
}
//get user index
const bIsRestore = null != data.sessionId;
let upsertRes = null;
let curIndexUser, documentCallback;
if (bIsRestore) {
// If we restore, we also restore the index
curIndexUser = user.indexUser;
} else {
if (data.documentCallbackUrl && !wopiParams) {
documentCallback = url.parse(data.documentCallbackUrl);
const filterStatus = yield* utils.checkHostFilter(ctx, documentCallback.hostname);
if (0 !== filterStatus) {
ctx.logger.warn('checkIpFilter error: url = %s', data.documentCallbackUrl);
sendDataDisconnectReason(ctx, conn, constants.DROP_CODE, constants.DROP_REASON);
conn.disconnect(true);
return;
}
}
const format = data.openCmd && data.openCmd.format;
upsertRes = yield canvasService.commandOpenStartPromise(
ctx,
docId,
utils.getBaseUrlByConnection(ctx, conn),
data.documentCallbackUrl,
format
);
curIndexUser = upsertRes.insertId;
//todo update additional in commandOpenStartPromise
if (
(upsertRes.isInsert || (wopiParams && 2 === curIndexUser)) &&
(undefined !== data.timezoneOffset || data.headingsColor || ctx.shardKey || ctx.wopiSrc)
) {
//todo insert in commandOpenStartPromise. insert here for database compatibility
if (false === canvasService.hasAdditionalCol) {
const selectRes = yield taskResult.select(ctx, docId);
canvasService.hasAdditionalCol = selectRes.length > 0 && undefined !== selectRes[0].additional;
}
if (canvasService.hasAdditionalCol) {
const task = new taskResult.TaskResultData();
task.tenant = ctx.tenant;
task.key = docId;
if (undefined !== data.timezoneOffset || data.headingsColor) {
//todo duplicate created_at because CURRENT_TIMESTAMP uses server timezone
openedAtStr = sqlBase.DocumentAdditional.prototype.setOpenedAt(Date.now(), data.timezoneOffset, data.headingsColor);
task.additional = openedAtStr;
}
if (ctx.shardKey) {
task.additional += sqlBase.DocumentAdditional.prototype.setShardKey(ctx.shardKey);
}
if (ctx.wopiSrc) {
task.additional += sqlBase.DocumentAdditional.prototype.setWopiSrc(ctx.wopiSrc);
}
yield taskResult.update(ctx, task);
} else {
ctx.logger.warn('auth unknown column "additional"');
}
}
}
if (constants.CONN_CLOSED === conn.conn.readyState) {
//closing could happen during async action
return;
}
const curUserIdOriginal = String(user.id);
const curUserId = curUserIdOriginal + curIndexUser;
conn.tenant = tenantManager.getTenantByConnection(ctx, conn);
conn.docId = data.docid;
conn.permissions = data.permissions;
conn.user = {
id: curUserId,
idOriginal: curUserIdOriginal,
username: fillUsername(ctx, data),
customerId: user.customerId,
indexUser: curIndexUser,
view: !isEditMode(data.permissions, data.mode)
};
if (conn.user.view && utils.isLiveViewerSupport(licenseInfo)) {
conn.coEditingMode = data.coEditingMode;
}
conn.isCloseCoAuthoring = data.isCloseCoAuthoring;
conn.isEnterCorrectPassword = data.isEnterCorrectPassword;
conn.denyChangeName = data.denyChangeName;
conn.editorType = data['editorType'];
if (data.sessionTimeConnect) {
conn.sessionTimeConnect = data.sessionTimeConnect;
}
if (data.sessionTimeIdle >= 0) {
conn.sessionTimeLastAction = new Date().getTime() - data.sessionTimeIdle;
}
conn.unsyncTime = null;
conn.encrypted = data.encrypted;
conn.lang = data.lang;
conn.supportAuthChangesAck = data.supportAuthChangesAck;
const c_LR = constants.LICENSE_RESULT;
conn.licenseType = c_LR.Success;
const isLiveViewer = utils.isLiveViewer(conn);
if (!conn.user.view || isLiveViewer) {
let licenseType = yield* _checkLicenseAuth(ctx, licenseInfo, conn.user.idOriginal, isLiveViewer);
let aggregationCtx, licenseInfoAggregation;
if (
(c_LR.Success === licenseType || c_LR.SuccessLimit === licenseType) &&
tenantManager.isMultitenantMode(ctx) &&
!tenantManager.isDefaultTenant(ctx)
) {
//check server aggregation license
aggregationCtx = new operationContext.Context();
aggregationCtx.init(tenantManager.getDefautTenant(), ctx.docId, ctx.userId);
//yield ctx.initTenantCache(); //no need
licenseInfoAggregation = tenantManager.getServerLicense();
licenseType = yield* _checkLicenseAuth(aggregationCtx, licenseInfoAggregation, `${ctx.tenant}:${conn.user.idOriginal}`, isLiveViewer);
}
conn.licenseType = licenseType;
if ((c_LR.Success !== licenseType && c_LR.SuccessLimit !== licenseType) || (!tenIsAnonymousSupport && data.IsAnonymousUser)) {
if (!tenIsAnonymousSupport && data.IsAnonymousUser) {
//do not modify the licenseType because this information is already sent in _checkLicense
ctx.logger.error('auth: access to editor or live viewer is denied for anonymous users');
}
modifyConnectionEditorToView(ctx, conn);
} else {
//don't check IsAnonymousUser via jwt because substituting it doesn't lead to any trouble
yield* updateEditUsers(ctx, licenseInfo, conn.user.idOriginal, !!data.IsAnonymousUser, isLiveViewer);
if (aggregationCtx && licenseInfoAggregation) {
//update server aggregation license
yield* updateEditUsers(
aggregationCtx,
licenseInfoAggregation,
`${ctx.tenant}:${conn.user.idOriginal}`,
!!data.IsAnonymousUser,
isLiveViewer
);
}
}
}
// Situation when the user is already disabled from co-authoring
if (bIsRestore && data.isCloseCoAuthoring) {
conn.sessionId = data.sessionId; //restore old
// delete previous connections
connections = _.reject(connections, el => {
return el.sessionId === data.sessionId; //Delete this connection
});
//closing could happen during async action
if (constants.CONN_CLOSED !== conn.conn.readyState) {
// We put it in an array, because we need to send data to open/save the document
connections.push(conn);
yield addPresence(ctx, conn, true);
// Sending a formal authorization to confirm the connection
yield* sendAuthInfo(ctx, conn, bIsRestore, undefined);
if (cmd) {
yield canvasService.openDocument(ctx, conn, cmd, upsertRes, bIsRestore);
}
}
return;
}
if (conn.user.idOriginal.length > constants.USER_ID_MAX_LENGTH) {
//todo refactor DB and remove restrictions
ctx.logger.warn('auth user id too long actual = %s; max = %s', curUserIdOriginal.length, constants.USER_ID_MAX_LENGTH);
yield* sendFileErrorAuth(ctx, conn, data.sessionId, 'User id too long');
return;
}
if (!conn.user.view) {
const status = result && result.length > 0 ? result[0]['status'] : null;
if (commonDefines.FileStatus.Ok === status) {
// Everything is fine, the status does not need to be updated
} else if (
commonDefines.FileStatus.SaveVersion === status ||
(!bIsRestore &&
commonDefines.FileStatus.UpdateVersion === status &&
Date.now() - result[0]['status_info'] * 60000 > tenExpUpdateVersionStatus)
) {
let newStatus = commonDefines.FileStatus.Ok;
if (commonDefines.FileStatus.UpdateVersion === status) {
ctx.logger.warn('UpdateVersion expired');
//FileStatus.None to open file again from new url
newStatus = commonDefines.FileStatus.None;
}
// Update the status of the file (the build is in progress, you need to stop it)
const updateMask = new taskResult.TaskResultData();
updateMask.tenant = ctx.tenant;
updateMask.key = docId;
updateMask.status = status;
updateMask.statusInfo = result[0]['status_info'];
const updateTask = new taskResult.TaskResultData();
updateTask.status = newStatus;
updateTask.statusInfo = constants.NO_ERROR;
const updateIfRes = yield taskResult.updateIf(ctx, updateTask, updateMask);
if (!(updateIfRes.affectedRows > 0)) {
// error version
//log level is debug because error handled via refreshFile
yield* sendFileErrorAuth(ctx, conn, data.sessionId, 'Update Version error', constants.UPDATE_VERSION_CODE, true);
return;
}
} else if (commonDefines.FileStatus.UpdateVersion === status) {
modifyConnectionEditorToView(ctx, conn);
conn.isCloseCoAuthoring = true;
if (bIsRestore) {
// error version
yield* sendFileErrorAuth(ctx, conn, data.sessionId, 'Update Version error', constants.UPDATE_VERSION_CODE, true);
return;
}
} else if (commonDefines.FileStatus.None === status && conn.encrypted) {
//ok
} else if (bIsRestore) {
// Other error
if (null === status) {
yield* sendFileErrorAuth(ctx, conn, data.sessionId, 'Other error', constants.NO_CACHE_CODE, true);
} else {
yield* sendFileErrorAuth(ctx, conn, data.sessionId, 'Other error');
}
return;
}
} else if (data.forcedViewMode) {
sendDataWarning(ctx, conn, constants.FORCED_VIEW_MODE, 'Forced view mode');
}
//Set the unique ID
if (bIsRestore) {
ctx.logger.info('restored old session: id = %s', data.sessionId);
if (!conn.user.view) {
// Stop the assembly (suddenly it started)
// When reconnecting, we need to check for file assembly
try {
const puckerIndex = yield* getChangesIndex(ctx, docId);
let bIsSuccessRestore = true;
if (puckerIndex > 0) {
const objChangesDocument = yield* getDocumentChanges(ctx, docId, puckerIndex - 1, puckerIndex);
const change = objChangesDocument.arrChanges[objChangesDocument.getLength() - 1];
if (change) {
if (change['change']) {
if (change['user'] !== curUserId) {
bIsSuccessRestore = 0 === ((data['lastOtherSaveTime'] - change['time']) / 1000) >> 0;
}
}
} else {
bIsSuccessRestore = false;
}
}
if (bIsSuccessRestore) {
// check locks
const arrayBlocks = data['block'];
const getLockRes = yield getLock(ctx, conn, data, true);
if (arrayBlocks && (0 === arrayBlocks.length || getLockRes)) {
let wopiLockRes = true;
if (wopiParamsFull) {
wopiLockRes = yield wopiClient.lock(
ctx,
'LOCK',
wopiParamsFull.commonInfo.lockId,
wopiParamsFull.commonInfo.fileInfo,
wopiParamsFull.userAuth
);
}
if (wopiLockRes) {
yield* authRestore(ctx, conn, data.sessionId);
} else {
yield* sendFileErrorAuth(ctx, conn, data.sessionId, 'Restore error. Wopi lock error.', constants.RESTORE_CODE, true);
}
} else {
yield* sendFileErrorAuth(ctx, conn, data.sessionId, 'Restore error. Locks not checked.', constants.RESTORE_CODE, true);
}
} else {
yield* sendFileErrorAuth(ctx, conn, data.sessionId, 'Restore error. Document modified.', constants.RESTORE_CODE, true);
}
} catch (err) {
ctx.logger.error('DataBase error: %s', err.stack);
yield* sendFileErrorAuth(ctx, conn, data.sessionId, 'DataBase error', constants.RESTORE_CODE, true);
}
} else {
yield* authRestore(ctx, conn, data.sessionId);
}
} else {
conn.sessionId = conn.id;
const openedAt = openedAtStr ? sqlBase.DocumentAdditional.prototype.getOpenedAt(openedAtStr) : canvasService.getOpenedAt(resultRow);
const endAuthRes = yield* endAuth(ctx, conn, false, documentCallback, openedAt);
if (endAuthRes && cmd) {
//todo to allow forcesave TemplateSource after convertion(move to better place)
if (wopiParamsFull?.commonInfo?.fileInfo?.TemplateSource) {
const newChangesLastDate = new Date();
newChangesLastDate.setMilliseconds(0); //remove milliseconds avoid issues with MySQL datetime rounding
cmd.setExternalChangeInfo(getExternalChangeInfo(conn.user, newChangesLastDate.getTime(), conn.lang));
}
yield canvasService.openDocument(ctx, conn, cmd, upsertRes, bIsRestore);
}
}
}
}
function* endAuth(ctx, conn, bIsRestore, documentCallback, opt_openedAt) {
const tenExpLockDoc = ctx.getCfg('services.CoAuthoring.expire.lockDoc', cfgExpLockDoc);
const tenForgottenFiles = ctx.getCfg('services.CoAuthoring.server.forgottenfiles', cfgForgottenFiles);
const res = true;
const docId = conn.docId;
const tmpUser = conn.user;
let hasForgotten;
if (constants.CONN_CLOSED === conn.conn.readyState) {
//closing could happen during async action
return false;
}
connections.push(conn);
let firstParticipantNoView,
countNoView = 0;
yield addPresence(ctx, conn, true);
const participantsMap = yield getParticipantMap(ctx, docId);
const participantsTimestamp = Date.now();
for (let i = 0; i < participantsMap.length; ++i) {
const elem = participantsMap[i];
if (!elem.view) {
++countNoView;
if (!firstParticipantNoView && elem.id !== tmpUser.id) {
firstParticipantNoView = elem;
}
}
}
if (constants.CONN_CLOSED === conn.conn.readyState) {
//closing could happen during async action
return false;
}
// Sending to an external callback only for those who edit
if (!tmpUser.view) {
const userIndex = utils.getIndexFromUserId(tmpUser.id, tmpUser.idOriginal);
const userAction = new commonDefines.OutputAction(commonDefines.c_oAscUserAction.In, tmpUser.idOriginal);
//make async request to speed up file opening
sendStatusDocument(ctx, docId, c_oAscChangeBase.No, userAction, userIndex, documentCallback, conn.baseUrl).catch(err =>
ctx.logger.error('endAuth sendStatusDocument error: %s', err.stack)
);
if (!bIsRestore) {
//check forgotten file
const forgotten = yield storage.listObjects(ctx, docId, tenForgottenFiles);
hasForgotten = forgotten.length > 0;
ctx.logger.debug('endAuth hasForgotten %s', hasForgotten);
}
}
if (constants.CONN_CLOSED === conn.conn.readyState) {
//closing could happen during async action
return false;
}
let lockDocument = null;
let waitAuthUserId;
if (!bIsRestore && 2 === countNoView && !tmpUser.view && firstParticipantNoView) {
// lock a document
const lockRes = yield editorData.lockAuth(ctx, docId, firstParticipantNoView.id, 2 * tenExpLockDoc);
if (constants.CONN_CLOSED === conn.conn.readyState) {
//closing could happen during async action
return false;
}
if (lockRes) {
lockDocument = firstParticipantNoView;
waitAuthUserId = lockDocument.id;
const lockDocumentTimer = lockDocumentsTimerId[docId];
if (lockDocumentTimer) {
cleanLockDocumentTimer(docId, lockDocumentTimer);
}
setLockDocumentTimer(ctx, docId, lockDocument.id);
}
}
if (constants.CONN_CLOSED === conn.conn.readyState) {
//closing could happen during async action
return false;
}
if (lockDocument && !tmpUser.view) {
// waiting for the editor to switch to co-editing mode
const sendObject = {
type: 'waitAuth',
lockDocument
};
sendData(ctx, conn, sendObject); //Or 0 if fails
} else {
if (!bIsRestore && needSendChanges(conn)) {
yield* sendAuthChanges(ctx, conn.docId, [conn]);
}
if (constants.CONN_CLOSED === conn.conn.readyState) {
//closing could happen during async action
return false;
}
yield* sendAuthInfo(ctx, conn, bIsRestore, participantsMap, hasForgotten, opt_openedAt);
}
if (constants.CONN_CLOSED === conn.conn.readyState) {
//closing could happen during async action
return false;
}
yield publish(
ctx,
{
type: commonDefines.c_oPublishType.participantsState,
ctx,
docId,
userId: tmpUser.id,
participantsTimestamp,
participants: participantsMap,
waitAuthUserId
},
docId,
tmpUser.id
);
return res;
}
/**
* Save document changes to error files storage for debugging purposes.
* Retrieves changes from database and creates JSON chunks stored as separate files.
*
* @param {object} ctx - Operation context with configuration and logger
* @param {string} docId - Document identifier to retrieve changes for
* @param {string} destDir - Destination directory path in storage for error files
* @returns {Promise<void>} Resolves when all changes are saved to storage
*/
async function saveErrorChanges(ctx, docId, destDir) {
const tenEditor = getEditorConfig(ctx);
const tenMaxRequestChanges = ctx.getCfg('services.CoAuthoring.server.maxRequestChanges', cfgMaxRequestChanges);
const tenErrorFiles = ctx.getCfg('FileConverter.converter.errorfiles', cfgErrorFiles);
let index = 0;
let indexChunk = 1;
let changes;
const changesPrefix = destDir + '/' + constants.CHANGES_NAME + '/' + constants.CHANGES_NAME + '.json.';
do {
changes = await sqlBase.getChangesPromise(ctx, docId, index, index + tenMaxRequestChanges);
if (changes.length > 0) {
let buffer;
if (tenEditor['binaryChanges']) {
const buffers = changes.map(elem => elem.change_data);
buffers.unshift(Buffer.from(utils.getChangesFileHeader(), 'utf8'));
buffer = Buffer.concat(buffers);
} else {
let changesJSON = indexChunk > 1 ? ',[' : '[';
changesJSON += changes[0].change_data;
for (let i = 1; i < changes.length; ++i) {
changesJSON += ',';
changesJSON += changes[i].change_data;
}
changesJSON += ']\r\n';
buffer = Buffer.from(changesJSON, 'utf8');
}
await storage.putObject(ctx, changesPrefix + (indexChunk++).toString().padStart(3, '0'), buffer, buffer.length, tenErrorFiles);
}
index += tenMaxRequestChanges;
} while (changes && tenMaxRequestChanges === changes.length);
}
function sendAuthChangesByChunks(ctx, changes, connections) {
return co(function* () {
//websocket payload size is limited by https://github.com/faye/faye-websocket-node#initialization-options (64 MiB)
//xhr payload size is limited by nginx param client_max_body_size (current 100MB)
//"1.5MB" is choosen to avoid disconnect(after 25s) while downloading/uploading oversized changes with 0.5Mbps connection
const tenEditor = getEditorConfig(ctx);
let startIndex = 0;
let endIndex = 0;
while (endIndex < changes.length) {
startIndex = endIndex;
let curBytes = 0;
for (; endIndex < changes.length && curBytes < tenEditor['websocketMaxPayloadSize']; ++endIndex) {
curBytes += JSON.stringify(changes[endIndex]).length + 24; //24 - for JSON overhead
}
//todo simplify 'authChanges' format to reduce message size and JSON overhead
const sendObject = {
type: 'authChanges',
changes: changes.slice(startIndex, endIndex)
};
for (let i = 0; i < connections.length; ++i) {
const conn = connections[i];
if (needSendChanges(conn)) {
if (conn.supportAuthChangesAck) {
conn.authChangesAck = true;
}
sendData(ctx, conn, sendObject);
}
}
//todo use emit callback
//wait ack
let time = 0;
const interval = 100;
const limit = 30000;
for (let i = 0; i < connections.length; ++i) {
const conn = connections[i];
while (constants.CONN_CLOSED !== conn.readyState && needSendChanges(conn) && conn.authChangesAck && time < limit) {
yield utils.sleep(interval);
time += interval;
}
delete conn.authChangesAck;
}
}
});
}
function* sendAuthChanges(ctx, docId, connections) {
const tenMaxRequestChanges = ctx.getCfg('services.CoAuthoring.server.maxRequestChanges', cfgMaxRequestChanges);
let index = 0;
let changes;
do {
const objChangesDocument = yield getDocumentChanges(ctx, docId, index, index + tenMaxRequestChanges);
changes = objChangesDocument.arrChanges;
yield sendAuthChangesByChunks(ctx, changes, connections);
connections = connections.filter(conn => {
return constants.CONN_CLOSED !== conn.readyState;
});
index += tenMaxRequestChanges;
} while (connections.length > 0 && changes && tenMaxRequestChanges === changes.length);
}
function* sendAuthInfo(ctx, conn, bIsRestore, participantsMap, opt_hasForgotten, opt_openedAt) {
const tenImageSize = ctx.getCfg('services.CoAuthoring.server.limits_image_size', cfgImageSize);
const tenTypesUpload = ctx.getCfg('services.CoAuthoring.utils.limits_image_types_upload', cfgTypesUpload);
const docId = conn.docId;
let docLock = yield editorData.getLocks(ctx, docId);
if (EditorTypes.document !== conn.editorType) {
const docLockList = [];
for (const lockId in docLock) {
docLockList.push(docLock[lockId]);
}
docLock = docLockList;
}
let allMessages = yield editorData.getMessages(ctx, docId);
allMessages = allMessages.length > 0 ? allMessages : undefined; //todo client side
let sessionToken;
if (!bIsRestore) {
sessionToken = yield fillJwtByConnection(ctx, conn);
}
const tenEditor = getEditorConfig(ctx);
tenEditor['limits_image_size'] = tenImageSize;
tenEditor['limits_image_types_upload'] = tenTypesUpload;
const sendObject = {
type: 'auth',
result: 1,
sessionId: conn.sessionId,
sessionTimeConnect: conn.sessionTimeConnect,
participants: participantsMap,
messages: allMessages,
locks: docLock,
indexUser: conn.user.indexUser,
hasForgotten: opt_hasForgotten,
jwt: sessionToken,
g_cAscSpellCheckUrl: tenEditor['spellcheckerUrl'],
buildVersion: commonDefines.buildVersion,
buildNumber: commonDefines.buildNumber,
licenseType: conn.licenseType,
settings: tenEditor,
openedAt: opt_openedAt
};
sendData(ctx, conn, sendObject); //Or 0 if fails
}
function* onMessage(ctx, conn, data) {
if (false === conn.permissions?.chat) {
ctx.logger.warn('insert message permissions.chat==false');
return;
}
const docId = conn.docId;
const userId = conn.user.id;
const msg = {
docid: docId,
message: data.message,
time: Date.now(),
user: userId,
useridoriginal: conn.user.idOriginal,
username: conn.user.username
};
yield editorData.addMessage(ctx, docId, msg);
// insert
ctx.logger.info('insert message: %j', msg);
const messages = [msg];
sendDataMessage(ctx, conn, messages);
yield publish(ctx, {type: commonDefines.c_oPublishType.message, ctx, docId, userId, messages}, docId, userId);
}
function* onCursor(ctx, conn, data) {
const docId = conn.docId;
const userId = conn.user.id;
const msg = {cursor: data.cursor, time: Date.now(), user: userId, useridoriginal: conn.user.idOriginal};
ctx.logger.info('send cursor: %s', msg);
const messages = [msg];
yield publish(ctx, {type: commonDefines.c_oPublishType.cursor, ctx, docId, userId, messages}, docId, userId);
}
// For Word block is now string "guid"
// For Excel block is now object { sheetId, type, rangeOrObjectId, guid }
// For presentations, this is an object { type, val } or { type, slideId, objId }
async function getLock(ctx, conn, data, bIsRestore) {
ctx.logger.debug('getLock');
let fCheckLock = null;
switch (conn.editorType) {
case EditorTypes.document:
// Word
fCheckLock = _checkLockWord;
break;
case EditorTypes.spreadsheet:
// Excel
fCheckLock = _checkLockExcel;
break;
case EditorTypes.presentation:
case EditorTypes.diagram:
// PP
fCheckLock = _checkLockPresentation;
break;
default:
return false;
}
const docId = conn.docId,
userId = conn.user.id,
arrayBlocks = data.block;
const locks = arrayBlocks.reduce((map, block) => {
//todo use one id
map[block.guid || block] = {time: Date.now(), user: userId, block};
return map;
}, {});
const addRes = await editorData.addLocksNX(ctx, docId, locks);
const documentLocks = addRes.allLocks;
const isAllAdded = Object.keys(addRes.lockConflict).length === 0;
if (!isAllAdded && !fCheckLock(ctx, docId, documentLocks, locks, arrayBlocks, userId)) {
//remove new locks
const toRemove = {};
for (const lockId in locks) {
if (!addRes.lockConflict[lockId]) {
toRemove[lockId] = locks[lockId];
delete documentLocks[lockId];
}
}
await editorData.removeLocks(ctx, docId, toRemove);
if (bIsRestore) {
return false;
}
}
sendData(ctx, conn, {type: 'getLock', locks: documentLocks});
await publish(ctx, {type: commonDefines.c_oPublishType.getLock, ctx, docId, userId, documentLocks}, docId, userId);
return true;
}
function sendGetLock(ctx, participants, documentLocks) {
_.each(participants, participant => {
sendData(ctx, participant, {type: 'getLock', locks: documentLocks});
});
}
// For Excel, it is necessary to recalculate locks when adding / deleting rows / columns
function* saveChanges(ctx, conn, data) {
const tenEditor = getEditorConfig(ctx);
const tenPubSubMaxChanges = ctx.getCfg('services.CoAuthoring.pubsub.maxChanges', cfgPubSubMaxChanges);
const tenExpSaveLock = ctx.getCfg('services.CoAuthoring.expire.saveLock', cfgExpSaveLock);
const docId = conn.docId,
userId = conn.user.id;
ctx.logger.info('Start saveChanges: reSave: %s', data.reSave);
const lockRes = yield editorData.lockSave(ctx, docId, userId, tenExpSaveLock);
if (!lockRes) {
//should not be here. cfgExpSaveLock - 60sec, sockjs disconnects after 25sec
ctx.logger.warn('saveChanges lockSave error');
return;
}
let puckerIndex = yield* getChangesIndex(ctx, docId);
if (constants.CONN_CLOSED === conn.conn.readyState) {
//closing could happen during async action
return;
}
let deleteIndex = -1;
if (data.startSaveChanges && null != data.deleteIndex) {
deleteIndex = data.deleteIndex;
if (-1 !== deleteIndex) {
const deleteCount = puckerIndex - deleteIndex;
if (0 < deleteCount) {
puckerIndex -= deleteCount;
yield sqlBase.deleteChangesPromise(ctx, docId, deleteIndex);
} else if (0 > deleteCount) {
ctx.logger.error('Error saveChanges: deleteIndex: %s ; startIndex: %s ; deleteCount: %s', deleteIndex, puckerIndex, deleteCount);
}
}
}
if (constants.CONN_CLOSED === conn.conn.readyState) {
//closing could happen during async action
return;
}
// Starting index change when adding
const startIndex = puckerIndex;
const newChanges = tenEditor['binaryChanges'] ? data.changes : JSON.parse(data.changes);
const newChangesLastDate = new Date();
newChangesLastDate.setMilliseconds(0); //remove milliseconds avoid issues with MySQL datetime rounding
const newChangesLastTime = newChangesLastDate.getTime();
const arrNewDocumentChanges = [];
ctx.logger.info('saveChanges: deleteIndex: %s ; startIndex: %s ; length: %s', deleteIndex, startIndex, newChanges.length);
if (0 < newChanges.length) {
let oElement = null;
for (let i = 0; i < newChanges.length; ++i) {
oElement = newChanges[i];
const change = tenEditor['binaryChanges'] ? oElement : JSON.stringify(oElement);
arrNewDocumentChanges.push({docid: docId, change, time: newChangesLastDate, user: userId, useridoriginal: conn.user.idOriginal});
}
puckerIndex += arrNewDocumentChanges.length;
yield sqlBase.insertChangesPromise(ctx, arrNewDocumentChanges, docId, startIndex, conn.user);
}
const changesIndex = -1 === deleteIndex && data.startSaveChanges ? startIndex : -1;
if (data.endSaveChanges) {
// For Excel, you need to recalculate indexes for locks
if (data.isExcel && false !== data.isCoAuthoring && data.excelAdditionalInfo) {
const tmpAdditionalInfo = JSON.parse(data.excelAdditionalInfo);
// This is what we got recalcIndexColumns and recalcIndexRows
const oRecalcIndexColumns = _addRecalcIndex(tmpAdditionalInfo['indexCols']);
const oRecalcIndexRows = _addRecalcIndex(tmpAdditionalInfo['indexRows']);
// Now we need to recalculate indexes for lock elements
if (null !== oRecalcIndexColumns || null !== oRecalcIndexRows) {
const docLock = yield editorData.getLocks(ctx, docId);
const docLockMod = _recalcLockArray(userId, docLock, oRecalcIndexColumns, oRecalcIndexRows);
if (Object.keys(docLockMod).length > 0) {
yield editorData.addLocks(ctx, docId, docLockMod);
}
}
}
let userLocks = [];
if (data.releaseLocks) {
//Release locks
userLocks = yield removeUserLocks(ctx, docId, userId);
}
// For this user, we remove Lock from the document if the unlock flag has arrived
const checkEndAuthLockRes = yield* checkEndAuthLock(ctx, data.unlock, false, docId, userId);
if (!checkEndAuthLockRes) {
const arrLocks = _.map(userLocks, e => {
return {
block: e.block,
user: e.user,
time: Date.now(),
changes: null
};
});
let changesToSend = arrNewDocumentChanges;
if (changesToSend.length > tenPubSubMaxChanges) {
changesToSend = null;
} else {
changesToSend.forEach(value => {
value.time = value.time.getTime();
});
}
yield publish(
ctx,
{
type: commonDefines.c_oPublishType.changes,
ctx,
docId,
userId,
changes: changesToSend,
startIndex,
changesIndex: puckerIndex,
syncChangesIndex: puckerIndex,
locks: arrLocks,
excelAdditionalInfo: data.excelAdditionalInfo,
endSaveChanges: data.endSaveChanges
},
docId,
userId
);
}
// Automatically remove the lock ourselves and send the index to save
yield* unSaveLock(ctx, conn, changesIndex, newChangesLastTime, puckerIndex);
//last save
const changeInfo = getExternalChangeInfo(conn.user, newChangesLastTime, conn.lang);
yield resetForceSaveAfterChanges(ctx, docId, newChangesLastTime, puckerIndex, utils.getBaseUrlByConnection(ctx, conn), changeInfo);
} else {
let changesToSend = arrNewDocumentChanges;
if (changesToSend.length > tenPubSubMaxChanges) {
changesToSend = null;
} else {
changesToSend.forEach(value => {
value.time = value.time.getTime();
});
}
const isPublished = yield publish(
ctx,
{
type: commonDefines.c_oPublishType.changes,
ctx,
docId,
userId,
changes: changesToSend,
startIndex,
changesIndex: puckerIndex,
syncChangesIndex: puckerIndex,
locks: [],
excelAdditionalInfo: undefined,
endSaveChanges: data.endSaveChanges
},
docId,
userId
);
sendData(ctx, conn, {type: 'savePartChanges', changesIndex, syncChangesIndex: puckerIndex});
if (!isPublished) {
//stub for lockDocumentsTimerId
yield publish(ctx, {type: commonDefines.c_oPublishType.changesNotify, ctx, docId});
}
}
}
// Can we save?
function* isSaveLock(ctx, conn, data) {
const tenExpSaveLock = ctx.getCfg('services.CoAuthoring.expire.saveLock', cfgExpSaveLock);
if (!conn.user) {
return;
}
let lockRes = true;
//check changesIndex for compatibility or 0 in case of first save
if (data.syncChangesIndex) {
const forceSave = yield editorData.getForceSave(ctx, conn.docId);
if (forceSave && forceSave.index !== data.syncChangesIndex) {
if (!conn.unsyncTime) {
conn.unsyncTime = new Date();
}
if (Date.now() - conn.unsyncTime.getTime() < tenExpSaveLock * 1000) {
lockRes = false;
ctx.logger.debug(
'isSaveLock editor unsynced since %j serverIndex:%s clientIndex:%s ',
conn.unsyncTime,
forceSave.index,
data.syncChangesIndex
);
sendData(ctx, conn, {type: 'saveLock', saveLock: !lockRes});
return;
} else {
ctx.logger.warn(
'isSaveLock editor unsynced since %j serverIndex:%s clientIndex:%s ',
conn.unsyncTime,
forceSave.index,
data.syncChangesIndex
);
}
}
}
conn.unsyncTime = null;
lockRes = yield editorData.lockSave(ctx, conn.docId, conn.user.id, tenExpSaveLock);
ctx.logger.debug('isSaveLock lockRes: %s', lockRes);
// We send only to the one who asked (you can not send to everyone)
sendData(ctx, conn, {type: 'saveLock', saveLock: !lockRes});
}
// Removing lock from save
function* unSaveLock(ctx, conn, index, time, syncChangesIndex) {
const unlockRes = yield editorData.unlockSave(ctx, conn.docId, conn.user.id);
if (commonDefines.c_oAscUnlockRes.Locked !== unlockRes) {
sendData(ctx, conn, {type: 'unSaveLock', index, time, syncChangesIndex});
} else {
ctx.logger.warn('unSaveLock failure');
}
}
// Returning all messages for a document
function* getMessages(ctx, conn) {
let allMessages = yield editorData.getMessages(ctx, conn.docId);
allMessages = allMessages.length > 0 ? allMessages : undefined; //todo client side
sendDataMessage(ctx, conn, allMessages);
}
function _checkLockWord(_ctx, _docId, _documentLocks, _newLocks, _arrayBlocks, _userId) {
return true;
}
function _checkLockExcel(ctx, docId, documentLocks, newLocks, arrayBlocks, userId) {
// Data is array now
let documentLock;
let isLock = false;
let isExistInArray = false;
let i, blockRange;
const lengthArray = arrayBlocks ? arrayBlocks.length : 0;
for (i = 0; i < lengthArray && false === isLock; ++i) {
blockRange = arrayBlocks[i];
for (const keyLockInArray in documentLocks) {
if (newLocks[keyLockInArray]) {
//skip just added
continue;
}
documentLock = documentLocks[keyLockInArray];
// Checking if an object is in an array (the current user sent a lock again)
if (
documentLock.user === userId &&
blockRange.sheetId === documentLock.block.sheetId &&
blockRange.type === c_oAscLockTypeElem.Object &&
documentLock.block.type === c_oAscLockTypeElem.Object &&
documentLock.block.rangeOrObjectId === blockRange.rangeOrObjectId
) {
isExistInArray = true;
break;
}
if (c_oAscLockTypeElem.Sheet === blockRange.type && c_oAscLockTypeElem.Sheet === documentLock.block.type) {
// If the current user sent a lock of the current sheet, then we do not enter it into the array, and if a new one, then we enter it
if (documentLock.user === userId) {
if (blockRange.sheetId === documentLock.block.sheetId) {
isExistInArray = true;
break;
} else {
// new sheet
continue;
}
} else {
// If someone has locked a sheet, then no one else can lock sheets (otherwise you can delete all sheets)
isLock = true;
break;
}
}
if (documentLock.user === userId || !documentLock.block || blockRange.sheetId !== documentLock.block.sheetId) {
continue;
}
isLock = compareExcelBlock(blockRange, documentLock.block);
if (true === isLock) {
break;
}
}
}
if (0 === lengthArray) {
isLock = true;
}
return !isLock && !isExistInArray;
}
function _checkLockPresentation(ctx, docId, documentLocks, newLocks, arrayBlocks, userId) {
// Data is array now
let isLock = false;
let i, blockRange;
const lengthArray = arrayBlocks ? arrayBlocks.length : 0;
for (i = 0; i < lengthArray && false === isLock; ++i) {
blockRange = arrayBlocks[i];
for (const keyLockInArray in documentLocks) {
if (newLocks[keyLockInArray]) {
//skip just added
continue;
}
const documentLock = documentLocks[keyLockInArray];
if (documentLock.user === userId || !documentLock.block) {
continue;
}
isLock = comparePresentationBlock(blockRange, documentLock.block);
if (true === isLock) {
break;
}
}
}
if (0 === lengthArray) {
isLock = true;
}
return !isLock;
}
function _checkLicense(ctx, conn) {
return co(function* () {
try {
ctx.logger.info('_checkLicense start');
const tenEditSingleton = ctx.getCfg('services.CoAuthoring.server.edit_singleton', cfgEditSingleton);
const tenOpenProtectedFile = ctx.getCfg('services.CoAuthoring.server.openProtectedFile', cfgOpenProtectedFile);
const tenIsAnonymousSupport = ctx.getCfg('services.CoAuthoring.server.isAnonymousSupport', cfgIsAnonymousSupport);
let rights = constants.RIGHTS.Edit;
if (tenEditSingleton) {
// ToDo docId from url ?
const handshake = conn.handshake;
const docIdParsed = constants.DOC_ID_SOCKET_PATTERN.exec(handshake.url);
if (docIdParsed && 1 < docIdParsed.length) {
const participantsMap = yield getParticipantMap(ctx, docIdParsed[1]);
for (let i = 0; i < participantsMap.length; ++i) {
const elem = participantsMap[i];
if (!elem.view) {
rights = constants.RIGHTS.View;
break;
}
}
}
}
const [licenseInfo] = yield tenantManager.getTenantLicense(ctx);
const pluginSettings = yield aiProxyHandler.getPluginSettingsForInterface(ctx);
sendData(ctx, conn, {
type: 'license',
license: {
type: licenseInfo.type,
light: false, //todo remove in sdk
mode: licenseInfo.mode,
rights,
buildVersion: commonDefines.buildVersion,
buildNumber: commonDefines.buildNumber,
protectionSupport: tenOpenProtectedFile, //todo find a better place
isAnonymousSupport: tenIsAnonymousSupport, //todo find a better place
liveViewerSupport: utils.isLiveViewerSupport(licenseInfo),
branding: licenseInfo.branding,
customization: licenseInfo.customization,
advancedApi: licenseInfo.advancedApi
},
aiPluginSettings: pluginSettings
});
ctx.logger.info('_checkLicense end');
} catch (err) {
ctx.logger.error('_checkLicense error: %s', err.stack);
}
});
}
function* _checkLicenseAuth(ctx, licenseInfo, userId, isLiveViewer) {
const tenWarningLimitPercents = ctx.getCfg('license.warning_limit_percents', cfgWarningLimitPercents) / 100;
const tenNotificationRuleLicenseLimitEdit = ctx.getCfg(`notification.rules.licenseLimitEdit.template`, cfgNotificationRuleLicenseLimitEdit);
const tenNotificationRuleLicenseLimitLiveViewer = ctx.getCfg(
`notification.rules.licenseLimitLiveViewer.template`,
cfgNotificationRuleLicenseLimitLiveViewer
);
const c_LR = constants.LICENSE_RESULT;
let licenseType = licenseInfo.type;
if (c_LR.Success === licenseType || c_LR.SuccessLimit === licenseType) {
let notificationLimit, notificationLimitTitle;
let notificationTemplate = tenNotificationRuleLicenseLimitEdit;
let notificationType = notificationTypes.LICENSE_LIMIT_EDIT;
let notificationPercent = 100;
if (licenseInfo.usersCount) {
const nowUTC = getLicenseNowUtc();
notificationLimitTitle = 'user';
notificationLimit = 'users';
if (isLiveViewer) {
notificationTemplate = tenNotificationRuleLicenseLimitLiveViewer;
notificationType = notificationTypes.LICENSE_LIMIT_LIVE_VIEWER;
const arrUsers = yield editorStat.getPresenceUniqueViewUser(ctx, nowUTC);
if (
arrUsers.length >= licenseInfo.usersViewCount &&
-1 ===
arrUsers.findIndex(element => {
return element.userid === userId;
})
) {
licenseType = licenseInfo.hasLicense ? c_LR.UsersViewCount : c_LR.UsersViewCountOS;
} else if (licenseInfo.usersViewCount * tenWarningLimitPercents <= arrUsers.length) {
notificationPercent = tenWarningLimitPercents * 100;
}
} else {
const arrUsers = yield editorStat.getPresenceUniqueUser(ctx, nowUTC);
if (
arrUsers.length >= licenseInfo.usersCount &&
-1 ===
arrUsers.findIndex(element => {
return element.userid === userId;
})
) {
licenseType = licenseInfo.hasLicense ? c_LR.UsersCount : c_LR.UsersCountOS;
} else if (licenseInfo.usersCount * tenWarningLimitPercents <= arrUsers.length) {
notificationPercent = tenWarningLimitPercents * 100;
}
}
} else {
notificationLimitTitle = 'connection';
notificationLimit = 'connections';
if (isLiveViewer) {
notificationTemplate = tenNotificationRuleLicenseLimitLiveViewer;
notificationType = notificationTypes.LICENSE_LIMIT_LIVE_VIEWER;
const connectionsLiveCount = licenseInfo.connectionsView;
const liveViewerConnectionsCount = yield editorStat.getLiveViewerConnectionsCount(ctx, connections);
if (liveViewerConnectionsCount >= connectionsLiveCount) {
licenseType = licenseInfo.hasLicense ? c_LR.ConnectionsLive : c_LR.ConnectionsLiveOS;
} else if (connectionsLiveCount * tenWarningLimitPercents <= liveViewerConnectionsCount) {
notificationPercent = tenWarningLimitPercents * 100;
}
} else {
const connectionsCount = licenseInfo.connections;
const editConnectionsCount = yield editorStat.getEditorConnectionsCount(ctx, connections);
if (editConnectionsCount >= connectionsCount) {
licenseType = licenseInfo.hasLicense ? c_LR.Connections : c_LR.ConnectionsOS;
} else if (connectionsCount * tenWarningLimitPercents <= editConnectionsCount) {
notificationPercent = tenWarningLimitPercents * 100;
}
}
}
if ((c_LR.Success !== licenseType && c_LR.SuccessLimit !== licenseType) || 100 !== notificationPercent) {
const applicationName = (process.env.APPLICATION_NAME || '').toUpperCase();
const title = util.format(notificationTemplate.title, applicationName, notificationLimitTitle);
const message = util.format(notificationTemplate.body, notificationPercent, notificationLimit);
if (100 !== notificationPercent) {
ctx.logger.warn(message);
} else {
ctx.logger.error(message);
}
//todo with yield service could throw error
void notificationService.notify(ctx, notificationType, title, message, notificationType + notificationPercent);
}
}
return licenseType;
}
//publish subscribe message brocker
pubsubOnMessage = function (msg) {
return co(function* () {
const ctx = new operationContext.Context();
try {
const data = JSON.parse(msg);
ctx.initFromPubSub(data);
yield ctx.initTenantCache();
ctx.logger.debug('pubsub message start:%s', msg);
let participants;
let participant;
let objChangesDocument;
let i;
let lockDocumentTimer, cmd;
switch (data.type) {
case commonDefines.c_oPublishType.drop:
dropUserFromDocument(ctx, data.docId, data.users, data.description);
break;
case commonDefines.c_oPublishType.closeConnection:
closeUsersConnection(ctx, data.docId, data.usersMap, data.isOriginalId, data.code, data.description);
break;
case commonDefines.c_oPublishType.releaseLock:
participants = getParticipants(data.docId, true, data.userId, true);
_.each(participants, participant => {
sendReleaseLock(ctx, participant, data.locks);
});
break;
case commonDefines.c_oPublishType.participantsState:
participants = getParticipants(data.docId, true, data.userId);
sendParticipantsState(ctx, participants, data);
break;
case commonDefines.c_oPublishType.message:
participants = getParticipants(data.docId, true, data.userId);
_.each(participants, participant => {
sendDataMessage(ctx, participant, data.messages);
});
break;
case commonDefines.c_oPublishType.getLock:
participants = getParticipants(data.docId, true, data.userId, true);
sendGetLock(ctx, participants, data.documentLocks);
break;
case commonDefines.c_oPublishType.changes:
lockDocumentTimer = lockDocumentsTimerId[data.docId];
if (lockDocumentTimer) {
ctx.logger.debug('lockDocumentsTimerId update c_oPublishType.changes');
cleanLockDocumentTimer(data.docId, lockDocumentTimer);
setLockDocumentTimer(ctx, data.docId, lockDocumentTimer.userId);
}
participants = getParticipants(data.docId, true, data.userId);
if (participants.length > 0) {
let changes = data.changes;
if (null == changes) {
objChangesDocument = yield* getDocumentChanges(ctx, data.docId, data.startIndex, data.changesIndex);
changes = objChangesDocument.arrChanges;
}
_.each(participants, participant => {
if (!needSendChanges(participant)) {
return;
}
sendData(ctx, participant, {
type: 'saveChanges',
changes,
changesIndex: data.changesIndex,
syncChangesIndex: data.syncChangesIndex,
endSaveChanges: data.endSaveChanges,
locks: data.locks,
excelAdditionalInfo: data.excelAdditionalInfo
});
});
}
break;
case commonDefines.c_oPublishType.changesNotify:
lockDocumentTimer = lockDocumentsTimerId[data.docId];
if (lockDocumentTimer) {
ctx.logger.debug('lockDocumentsTimerId update c_oPublishType.changesNotify');
cleanLockDocumentTimer(data.docId, lockDocumentTimer);
setLockDocumentTimer(ctx, data.docId, lockDocumentTimer.userId);
}
break;
case commonDefines.c_oPublishType.auth:
lockDocumentTimer = lockDocumentsTimerId[data.docId];
if (lockDocumentTimer) {
ctx.logger.debug('lockDocumentsTimerId clear');
cleanLockDocumentTimer(data.docId, lockDocumentTimer);
}
participants = getParticipants(data.docId, true, data.userId, true);
if (participants.length > 0) {
yield* sendAuthChanges(ctx, data.docId, participants);
for (i = 0; i < participants.length; ++i) {
participant = participants[i];
yield* sendAuthInfo(ctx, participant, false, data.participantsMap);
}
}
break;
case commonDefines.c_oPublishType.receiveTask: {
cmd = new commonDefines.InputCommand(data.cmd, true);
const output = new canvasService.OutputDataWrap();
output.fromObject(data.output);
const outputData = output.getData();
const docId = cmd.getDocId();
if (cmd.getUserConnectionId()) {
participants = getParticipantUser(docId, cmd.getUserConnectionId());
} else {
participants = getParticipants(docId);
}
for (i = 0; i < participants.length; ++i) {
participant = participants[i];
if (data.needUrlKey) {
if (0 === data.needUrlMethod) {
outputData.setData(yield storage.getSignedUrls(ctx, participant.baseUrl, data.needUrlKey, data.needUrlType, data.creationDate));
} else if (1 === data.needUrlMethod) {
outputData.setData(
yield storage.getSignedUrl(ctx, participant.baseUrl, data.needUrlKey, data.needUrlType, undefined, data.creationDate)
);
} else {
let url;
if (cmd.getInline()) {
url = yield canvasService.getPrintFileUrl(ctx, data.needUrlKey, participant.baseUrl, cmd.getTitle());
outputData.setExtName('.pdf');
} else {
url = yield storage.getSignedUrl(ctx, participant.baseUrl, data.needUrlKey, data.needUrlType, cmd.getTitle(), data.creationDate);
outputData.setExtName(pathModule.extname(data.needUrlKey));
}
outputData.setData(url);
}
if (undefined !== data.openedAt) {
outputData.setOpenedAt(data.openedAt);
}
yield modifyConnectionForPassword(ctx, participant, data.needUrlIsCorrectPassword);
}
sendData(ctx, participant, output);
}
break;
}
case commonDefines.c_oPublishType.warning:
participants = getParticipants(data.docId);
_.each(participants, participant => {
sendDataWarning(ctx, participant, undefined, data.description);
});
break;
case commonDefines.c_oPublishType.cursor:
participants = getParticipants(data.docId, true, data.userId);
_.each(participants, participant => {
sendDataCursor(ctx, participant, data.messages);
});
break;
case commonDefines.c_oPublishType.shutdown:
//flag prevent new socket connections and receive data from exist connections
shutdownFlag = data.status;
wopiClient.setIsShutdown(shutdownFlag);
ctx.logger.warn('start shutdown:%s', shutdownFlag);
if (shutdownFlag) {
ctx.logger.warn('active connections: %d', connections.length);
//do not stop the server, because sockets and all requests will be unavailable
//bad because you may need to convert the output file and the fact that requests for the CommandService will not be processed
//server.close();
//in the cycle we will remove elements so copy array
const connectionsTmp = connections.slice();
//destroy all open connections
for (i = 0; i < connectionsTmp.length; ++i) {
sendDataDisconnectReason(ctx, connectionsTmp[i], constants.SHUTDOWN_CODE, constants.SHUTDOWN_REASON);
connectionsTmp[i].disconnect(true);
}
}
ctx.logger.warn('end shutdown');
break;
case commonDefines.c_oPublishType.meta:
participants = getParticipants(data.docId);
_.each(participants, participant => {
sendDataMeta(ctx, participant, data.meta);
});
break;
case commonDefines.c_oPublishType.forceSave:
participants = getParticipants(data.docId, true, data.userId, true);
_.each(participants, participant => {
sendData(ctx, participant, {type: 'forceSave', messages: data.data});
});
break;
case commonDefines.c_oPublishType.changeConnecitonInfo: {
let hasChanges = false;
cmd = new commonDefines.InputCommand(data.cmd, true);
participants = getParticipants(data.docId);
for (i = 0; i < participants.length; ++i) {
participant = participants[i];
if (!participant.denyChangeName && participant.user.idOriginal === data.useridoriginal) {
hasChanges = true;
ctx.logger.debug('changeConnectionInfo: userId = %s', data.useridoriginal);
participant.user.username = cmd.getUserName();
yield addPresence(ctx, participant, false);
const sessionToken = yield fillJwtByConnection(ctx, participant);
sendDataRefreshToken(ctx, participant, sessionToken);
}
}
if (hasChanges) {
const participants = yield getParticipantMap(ctx, data.docId);
const participantsTimestamp = Date.now();
yield publish(ctx, {
type: commonDefines.c_oPublishType.participantsState,
ctx,
docId: data.docId,
userId: null,
participantsTimestamp,
participants
});
}
break;
}
case commonDefines.c_oPublishType.rpc:
participants = getParticipantUser(data.docId, data.userId);
_.each(participants, participant => {
sendDataRpc(ctx, participant, data.responseKey, data.data);
});
break;
case commonDefines.c_oPublishType.updateVersion:
// To finalize form or refresh file in live view
participants = getParticipants(data.docId);
_.each(participants, participant => {
sendData(ctx, participant, {type: 'updateVersion', success: data.success});
});
break;
default:
ctx.logger.debug('pubsub unknown message type:%s', msg);
}
} catch (err) {
ctx.logger.error('pubsub message error: %s', err.stack);
}
});
};
function* collectStats(ctx, countEdit, countLiveView, countView) {
const now = Date.now();
yield editorStat.setEditorConnections(ctx, countEdit, countLiveView, countView, now, PRECISION);
}
function expireDoc() {
return co(function* () {
const ctx = new operationContext.Context();
try {
const tenants = {};
let countEditByShard = 0;
let countLiveViewByShard = 0;
let countViewByShard = 0;
ctx.logger.debug('expireDoc connections.length = %d', connections.length);
const nowMs = new Date().getTime();
for (let i = 0; i < connections.length; ++i) {
const conn = connections[i];
ctx.initFromConnection(conn);
//todo group by tenant
yield ctx.initTenantCache();
let tenExpSessionIdle = ms(ctx.getCfg('services.CoAuthoring.expire.sessionidle', cfgExpSessionIdle)) || 0;
const tenExpSessionAbsolute = ms(ctx.getCfg('services.CoAuthoring.expire.sessionabsolute', cfgExpSessionAbsolute));
const tenExpSessionCloseCommand = ms(ctx.getCfg('services.CoAuthoring.expire.sessionclosecommand', cfgExpSessionCloseCommand));
if (preStopFlag && (tenExpSessionIdle > 5 * 60 * 1000 || tenExpSessionIdle <= 0)) {
tenExpSessionIdle = 5 * 60 * 1000; //5 minutes
}
const maxMs = nowMs + Math.max(tenExpSessionCloseCommand, expDocumentsStep);
let tenant = tenants[ctx.tenant];
if (!tenant) {
tenant = tenants[ctx.tenant] = {countEditByShard: 0, countLiveViewByShard: 0, countViewByShard: 0};
}
//wopi access_token_ttl;
if (tenExpSessionAbsolute > 0 || conn.access_token_ttl) {
if (
((tenExpSessionAbsolute > 0 && maxMs - conn.sessionTimeConnect > tenExpSessionAbsolute) ||
(conn.access_token_ttl && maxMs > conn.access_token_ttl)) &&
!conn.sessionIsSendWarning
) {
conn.sessionIsSendWarning = true;
sendDataSession(ctx, conn, {
code: constants.SESSION_ABSOLUTE_CODE,
reason: constants.SESSION_ABSOLUTE_REASON
});
} else if (nowMs - conn.sessionTimeConnect > tenExpSessionAbsolute) {
ctx.logger.debug('expireDoc close absolute session');
sendDataDisconnectReason(ctx, conn, constants.SESSION_ABSOLUTE_CODE, constants.SESSION_ABSOLUTE_REASON);
conn.disconnect(true);
continue;
}
}
if (tenExpSessionIdle > 0 && !(conn.user?.view || conn.isCloseCoAuthoring)) {
if (maxMs - conn.sessionTimeLastAction > tenExpSessionIdle && !conn.sessionIsSendWarning) {
conn.sessionIsSendWarning = true;
sendDataSession(ctx, conn, {
code: constants.SESSION_IDLE_CODE,
reason: constants.SESSION_IDLE_REASON,
interval: tenExpSessionIdle
});
} else if (nowMs - conn.sessionTimeLastAction > tenExpSessionIdle) {
ctx.logger.debug('expireDoc close idle session');
sendDataDisconnectReason(ctx, conn, constants.SESSION_IDLE_CODE, constants.SESSION_IDLE_REASON);
conn.disconnect(true);
continue;
}
}
if (constants.CONN_CLOSED === conn.conn.readyState) {
ctx.logger.error('expireDoc connection closed');
}
yield updatePresence(ctx, conn);
if (utils.isLiveViewer(conn)) {
countLiveViewByShard++;
tenant.countLiveViewByShard++;
} else if (conn.isCloseCoAuthoring || (conn.user && conn.user.view)) {
countViewByShard++;
tenant.countViewByShard++;
} else {
countEditByShard++;
tenant.countEditByShard++;
}
}
for (const tenantId in tenants) {
if (Object.hasOwn(tenants, tenantId)) {
ctx.setTenant(tenantId);
const tenant = tenants[tenantId];
yield* collectStats(ctx, tenant.countEditByShard, tenant.countLiveViewByShard, tenant.countViewByShard);
yield editorStat.setEditorConnectionsCountByShard(ctx, SHARD_ID, tenant.countEditByShard);
yield editorStat.setLiveViewerConnectionsCountByShard(ctx, SHARD_ID, tenant.countLiveViewByShard);
yield editorStat.setViewerConnectionsCountByShard(ctx, SHARD_ID, tenant.countViewByShard);
if (clientStatsD) {
//todo with multitenant
const countEdit = yield editorStat.getEditorConnectionsCount(ctx, connections);
clientStatsD.gauge('expireDoc.connections.edit', countEdit);
const countLiveView = yield editorStat.getLiveViewerConnectionsCount(ctx, connections);
clientStatsD.gauge('expireDoc.connections.liveview', countLiveView);
const countView = yield editorStat.getViewerConnectionsCount(ctx, connections);
clientStatsD.gauge('expireDoc.connections.view', countView);
}
}
}
if (tenantManager.isMultitenantMode(ctx) && !tenantManager.isDefaultTenant(ctx)) {
//aggregated tenant stats
const aggregationCtx = new operationContext.Context();
aggregationCtx.init(tenantManager.getDefautTenant(), ctx.docId, ctx.userId);
//yield ctx.initTenantCache();//no need
yield* collectStats(aggregationCtx, countEditByShard, countLiveViewByShard, countViewByShard);
yield editorStat.setEditorConnectionsCountByShard(aggregationCtx, SHARD_ID, countEditByShard);
yield editorStat.setLiveViewerConnectionsCountByShard(aggregationCtx, SHARD_ID, countLiveViewByShard);
yield editorStat.setViewerConnectionsCountByShard(aggregationCtx, SHARD_ID, countViewByShard);
}
ctx.initDefault();
} catch (err) {
ctx.logger.error('expireDoc error: %s', err.stack);
} finally {
setTimeout(expireDoc, expDocumentsStep);
}
});
}
setTimeout(expireDoc, expDocumentsStep);
function refreshWopiLock() {
return co(function* () {
const ctx = new operationContext.Context();
try {
ctx.logger.info('refreshWopiLock start');
const docIds = new Map();
for (let i = 0; i < connections.length; ++i) {
const conn = connections[i];
ctx.initFromConnection(conn);
//todo group by tenant
yield ctx.initTenantCache();
const docId = conn.docId;
if ((conn.user && conn.user.view) || docIds.has(docId)) {
continue;
}
docIds.set(docId, 1);
if (undefined === conn.access_token_ttl) {
continue;
}
const selectRes = yield taskResult.select(ctx, docId);
if (selectRes.length > 0 && selectRes[0] && selectRes[0].callback) {
const callback = selectRes[0].callback;
const callbackUrl = sqlBase.UserCallback.prototype.getCallbackByUserIndex(ctx, callback);
const wopiParams = wopiClient.parseWopiCallback(ctx, callbackUrl, callback);
if (wopiParams && wopiParams.commonInfo) {
yield wopiClient.lock(ctx, 'REFRESH_LOCK', wopiParams.commonInfo.lockId, wopiParams.commonInfo.fileInfo, wopiParams.userAuth);
}
}
}
ctx.initDefault();
ctx.logger.info('refreshWopiLock end');
} catch (err) {
ctx.logger.error('refreshWopiLock error:%s', err.stack);
} finally {
setTimeout(refreshWopiLock, cfgRefreshLockInterval);
}
});
}
setTimeout(refreshWopiLock, cfgRefreshLockInterval);
pubsub = new pubsubService();
pubsub.on('message', pubsubOnMessage);
pubsub.init(err => {
if (null != err) {
operationContext.global.logger.error('createPubSub error: %s', err.stack);
}
queue = new queueService();
queue.on('dead', handleDeadLetter);
queue.on('response', canvasService.receiveTask);
queue.init(true, true, false, true, true, true, err => {
if (null != err) {
operationContext.global.logger.error('createTaskQueue error: %s', err.stack);
}
gc.startGC();
//check data base compatibility
const tables = [
[cfgTableResult, constants.TABLE_RESULT_SCHEMA],
[cfgTableChanges, constants.TABLE_CHANGES_SCHEMA]
];
const requestPromises = tables.map(table => isSchemaCompatible(table));
Promise.all(requestPromises).then(
checkResult => {
if (checkResult.includes(false)) {
return;
}
editorData
.connect()
.then(() => editorStat.connect())
.then(() => callbackFunction())
.catch(err => {
operationContext.global.logger.error('editorData error: %s', err.stack);
});
},
error => operationContext.global.logger.error('getTableColumns error: %s', error.stack)
);
});
});
};
exports.setLicenseInfo = async function (globalCtx, data, original) {
tenantManager.setDefLicense(data, original);
await utilsDocService.notifyLicenseExpiration(globalCtx, data.endDate);
const tenantsList = await tenantManager.getAllTenants(globalCtx);
for (const tenant of tenantsList) {
const ctx = new operationContext.Context();
ctx.setTenant(tenant);
await ctx.initTenantCache();
const [licenseInfo] = await tenantManager.getTenantLicense(ctx);
await utilsDocService.notifyLicenseExpiration(ctx, licenseInfo.endDate);
}
};
exports.healthCheck = function (req, res) {
return co(function* () {
let output = false;
const ctx = new operationContext.Context();
try {
ctx.initFromRequest(req);
yield ctx.initTenantCache();
ctx.logger.info('healthCheck start');
//database
yield sqlBase.healthCheck(ctx);
ctx.logger.debug('healthCheck database');
//check redis connection
const healthData = yield editorData.healthCheck();
if (healthData) {
ctx.logger.debug('healthCheck editorData');
} else {
throw new Error('editorData');
}
const healthStat = yield editorStat.healthCheck();
if (healthStat) {
ctx.logger.debug('healthCheck editorStat');
} else {
throw new Error('editorStat');
}
const healthPubsub = yield pubsub.healthCheck();
if (healthPubsub) {
ctx.logger.debug('healthCheck pubsub');
} else {
throw new Error('pubsub');
}
const healthQueue = yield queue.healthCheck();
if (healthQueue) {
ctx.logger.debug('healthCheck queue');
} else {
throw new Error('queue');
}
//storage
yield storage.healthCheck(ctx);
ctx.logger.debug('healthCheck storage');
if (storage.isDifferentPersistentStorage()) {
yield storage.healthCheck(ctx, cfgForgottenFiles);
ctx.logger.debug('healthCheck storage persistent');
}
output = true;
ctx.logger.info('healthCheck end');
} catch (err) {
ctx.logger.error('healthCheck error %s', err.stack);
} finally {
res.setHeader('Content-Type', 'text/plain');
res.send(output.toString());
}
});
};
function validateInputParams(ctx, authRes, command) {
const commandsWithoutKey = ['version', 'license', 'getForgottenList'];
const isValidWithoutKey = commandsWithoutKey.includes(command.c);
const isDocIdString = typeof command.key === 'string';
ctx.setDocId(command.key);
if (authRes.code === constants.VKEY_KEY_EXPIRE) {
return commonDefines.c_oAscServerCommandErrors.TokenExpire;
} else if (authRes.code !== constants.NO_ERROR) {
return commonDefines.c_oAscServerCommandErrors.Token;
}
if (isValidWithoutKey || isDocIdString) {
return commonDefines.c_oAscServerCommandErrors.NoError;
} else {
return commonDefines.c_oAscServerCommandErrors.DocumentIdError;
}
}
function* getFilesKeys(ctx, opt_specialDir) {
const directoryList = yield storage.listObjects(ctx, '', opt_specialDir);
const keys = directoryList.map(directory => directory.split('/')[0]);
const filteredKeys = [];
let previousKey = null;
// Key is a folder name. This folder could consist of several files, which leads to N same strings in "keys" array in a row.
for (const key of keys) {
if (previousKey !== key) {
previousKey = key;
filteredKeys.push(key);
}
}
return filteredKeys;
}
function* findForgottenFile(ctx, docId) {
const tenForgottenFiles = ctx.getCfg('services.CoAuthoring.server.forgottenfiles', cfgForgottenFiles);
const tenForgottenFilesName = ctx.getCfg('services.CoAuthoring.server.forgottenfilesname', cfgForgottenFilesName);
const forgottenList = yield storage.listObjects(ctx, docId, tenForgottenFiles);
return forgottenList.find(forgotten => tenForgottenFilesName === pathModule.basename(forgotten, pathModule.extname(forgotten)));
}
function* commandLicense(ctx) {
const nowUTC = getLicenseNowUtc();
const users = yield editorStat.getPresenceUniqueUser(ctx, nowUTC);
const users_view = yield editorStat.getPresenceUniqueViewUser(ctx, nowUTC);
const [licenseInfo, licenseOriginal] = yield tenantManager.getTenantLicense(ctx);
return {
license: licenseOriginal || utils.convertLicenseInfoToFileParams(licenseInfo),
server: utils.convertLicenseInfoToServerParams(licenseInfo),
quota: {users, users_view}
};
}
async function proxyCommand(ctx, req, params) {
const tenCallbackRequestTimeout = ctx.getCfg('services.CoAuthoring.server.callbackRequestTimeout', cfgCallbackRequestTimeout);
const tenTokenEnableRequestInbox = ctx.getCfg('services.CoAuthoring.token.enable.request.inbox', cfgTokenEnableRequestInbox);
//todo gen shardkey as in sdkjs
const shardkey = params.key;
const baseUrl = utils.getBaseUrlByRequest(ctx, req);
let url = `${baseUrl}/command?&${constants.SHARD_KEY_API_NAME}=${encodeURIComponent(shardkey)}`;
for (const name in req.query) {
url += `&${name}=${encodeURIComponent(req.query[name])}`;
}
ctx.logger.info('commandFromServer proxy request with "key" to correctly process commands in sharded cluster to url:%s', url);
//isInJwtToken is true because 'command' is always internal
return await utils.postRequestPromise(
ctx,
url,
req.body,
null,
req.body.length,
tenCallbackRequestTimeout,
undefined,
tenTokenEnableRequestInbox,
req.headers
);
}
/**
* Server commands handler.
* @param ctx Local context.
* @param params Request parameters.
* @param req Request object.
* @param output{{ key: string, error: number, version: undefined | string, users: [string]}}} Mutable. Response body.
* @returns undefined.
*/
function* commandHandle(ctx, params, req, output) {
const tenForgottenFiles = ctx.getCfg('services.CoAuthoring.server.forgottenfiles', cfgForgottenFiles);
const docId = params.key;
const forgottenData = {};
switch (params.c) {
case 'info': {
//If no files in the database means they have not been edited.
const selectRes = yield taskResult.select(ctx, docId);
if (selectRes.length > 0) {
const sendData = yield* bindEvents(ctx, docId, params.callback, utils.getBaseUrlByRequest(ctx, req), undefined, params.userdata);
if (sendData) {
output.users = sendData.users || [];
} else {
output.error = commonDefines.c_oAscServerCommandErrors.ParseError;
}
} else {
output.error = commonDefines.c_oAscServerCommandErrors.DocumentIdError;
}
break;
}
case 'drop': {
if (params.users) {
const users = typeof params.users === 'string' ? JSON.parse(params.users) : params.users;
yield dropUsersFromDocument(ctx, docId, users);
} else {
yield dropUsersFromDocument(ctx, docId);
}
break;
}
case 'saved': {
// Result from document manager about file save processing status after assembly
if ('1' !== params.status) {
//"saved" request is done synchronously so populate a variable to check it after sendServerRequest
yield editorData.setSaved(ctx, docId, params.status);
ctx.logger.warn('saved corrupted id = %s status = %s conv = %s', docId, params.status, params.conv);
} else {
ctx.logger.info('saved id = %s status = %s conv = %s', docId, params.status, params.conv);
}
break;
}
case 'forcesave': {
const forceSaveRes = yield startForceSave(
ctx,
docId,
commonDefines.c_oAscForceSaveTypes.Command,
params.userdata,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
utils.getBaseUrlByRequest(ctx, req)
);
output.error = forceSaveRes.code;
break;
}
case 'meta': {
if (params.meta) {
yield publish(ctx, {type: commonDefines.c_oPublishType.meta, ctx, docId, meta: params.meta});
} else {
output.error = commonDefines.c_oAscServerCommandErrors.UnknownCommand;
}
break;
}
case 'getForgotten': {
// Checking for files existence.
const forgottenFileFullPath = yield* findForgottenFile(ctx, docId);
if (!forgottenFileFullPath) {
output.error = commonDefines.c_oAscServerCommandErrors.DocumentIdError;
break;
}
const forgottenFile = pathModule.basename(forgottenFileFullPath);
// Creating URLs from files.
const baseUrl = utils.getBaseUrlByRequest(ctx, req);
forgottenData.url = yield storage.getSignedUrl(
ctx,
baseUrl,
forgottenFileFullPath,
commonDefines.c_oAscUrlTypes.Temporary,
forgottenFile,
undefined,
tenForgottenFiles
);
break;
}
case 'deleteForgotten': {
const forgottenFile = yield* findForgottenFile(ctx, docId);
if (!forgottenFile) {
output.error = commonDefines.c_oAscServerCommandErrors.DocumentIdError;
break;
}
yield storage.deletePath(ctx, docId, tenForgottenFiles);
break;
}
case 'getForgottenList': {
forgottenData.keys = yield* getFilesKeys(ctx, tenForgottenFiles);
break;
}
case 'version': {
output.version = `${commonDefines.buildVersion}.${commonDefines.buildNumber}`;
break;
}
case 'license': {
const outputLicense = yield* commandLicense(ctx);
Object.assign(output, outputLicense);
break;
}
default: {
output.error = commonDefines.c_oAscServerCommandErrors.UnknownCommand;
break;
}
}
Object.assign(output, forgottenData);
}
// Command from the server (specifically teamlab)
exports.commandFromServer = function (req, res) {
return co(function* () {
const output = {key: 'commandFromServer', error: commonDefines.c_oAscServerCommandErrors.NoError, version: undefined, users: undefined};
const ctx = new operationContext.Context();
let postRes = null;
try {
ctx.initFromRequest(req);
yield ctx.initTenantCache();
ctx.logger.info('commandFromServer start');
const authRes = yield getRequestParams(ctx, req);
const params = authRes.params;
// Key is document id
output.key = params.key;
output.error = validateInputParams(ctx, authRes, params);
if (output.error === commonDefines.c_oAscServerCommandErrors.NoError) {
if (params.key && !req.query[constants.SHARD_KEY_API_NAME] && !req.query[constants.SHARD_KEY_WOPI_NAME] && process.env.DEFAULT_SHARD_KEY) {
postRes = yield proxyCommand(ctx, req, params);
} else {
ctx.logger.debug('commandFromServer: c = %s', params.c);
yield* commandHandle(ctx, params, req, output);
}
}
} catch (err) {
output.error = commonDefines.c_oAscServerCommandErrors.UnknownError;
ctx.logger.error('Error commandFromServer: %s', err.stack);
} finally {
let outputBuffer;
if (postRes) {
outputBuffer = postRes.body;
} else {
outputBuffer = Buffer.from(JSON.stringify(output), 'utf8');
}
res.setHeader('Content-Type', 'application/json');
res.setHeader('Content-Length', outputBuffer.length);
res.send(outputBuffer);
ctx.logger.info('commandFromServer end : %s', outputBuffer);
}
});
};
exports.shutdown = function (req, res) {
return co(function* () {
let output = false;
const ctx = new operationContext.Context();
try {
ctx.initFromRequest(req);
yield ctx.initTenantCache();
ctx.logger.info('shutdown start');
output = yield shutdown.shutdown(ctx, editorStat, req.method === 'PUT');
} catch (err) {
ctx.logger.error('shutdown error %s', err.stack);
} finally {
res.setHeader('Content-Type', 'text/plain');
res.send(output.toString());
ctx.logger.info('shutdown end');
}
});
};
exports.preStop = async function (req, res) {
let output = false;
const ctx = new operationContext.Context();
try {
ctx.initFromRequest(req);
await ctx.initTenantCache();
preStopFlag = req.method === 'PUT';
ctx.logger.info('preStop set flag', preStopFlag);
if (preStopFlag) {
await gc.checkFileExpire(0);
}
output = true;
} catch (err) {
ctx.logger.error('preStop error %s', err.stack);
} finally {
res.setHeader('Content-Type', 'text/plain');
res.send(output.toString());
ctx.logger.info('preStop end');
}
};
/**
* Get active connections array
* @returns {Array} Active connections
*/
function getConnections() {
return connections;
}
exports.getConnections = getConnections;
exports.getEditorConnectionsCount = function (req, res) {
const ctx = new operationContext.Context();
let count = 0;
try {
ctx.initFromRequest(req);
for (let i = 0; i < connections.length; ++i) {
const conn = connections[i];
if (!(conn.isCloseCoAuthoring || (conn.user && conn.user.view))) {
count++;
}
}
ctx.logger.info('getConnectionsCount count=%d', count);
} catch (err) {
ctx.logger.error('getConnectionsCount error %s', err.stack);
} finally {
res.setHeader('Content-Type', 'text/plain');
res.send(count.toString());
}
};