Merge pull request 'feature/refresh-file' (#10) from feature/refresh-file into develop

Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/server/pulls/10
This commit is contained in:
Sergey Konovalov
2024-11-29 15:53:06 +00:00
3 changed files with 159 additions and 157 deletions

View File

@ -50,7 +50,6 @@ function InputCommand(data, copyExplicit) {
this['username'] = data['username'];
this['tokenSession'] = data['tokenSession'];
this['tokenDownload'] = data['tokenDownload'];
this['tokenHistory'] = data['tokenHistory'];
this['data'] = data['data'];
this['editorid'] = data['editorid'];
this['format'] = data['format'];
@ -84,7 +83,6 @@ function InputCommand(data, copyExplicit) {
this['savekey'] = data['savekey'];
this['userconnectionid'] = data['userconnectionid'];
this['responsekey'] = data['responsekey'];
this['docconnectionid'] = data['docconnectionid'];
this['jsonparams'] = data['jsonparams'];
this['lcid'] = data['lcid'];
this['useractionid'] = data['useractionid'];
@ -101,7 +99,6 @@ function InputCommand(data, copyExplicit) {
this['savepassword'] = data['savepassword'];
this['withoutPassword'] = data['withoutPassword'];
this['outputurls'] = data['outputurls'];
this['closeonerror'] = data['closeonerror'];
this['serverVersion'] = data['serverVersion'];
this['rediskey'] = data['rediskey'];
this['nobase64'] = data['nobase64'];
@ -127,7 +124,6 @@ function InputCommand(data, copyExplicit) {
this['username'] = undefined;
this['tokenSession'] = undefined;//string validate
this['tokenDownload'] = undefined;//string validate
this['tokenHistory'] = undefined;//string validate
this['data'] = undefined;//string
//to open
this['editorid'] = undefined;//int
@ -152,7 +148,6 @@ function InputCommand(data, copyExplicit) {
this['savekey'] = undefined;//int document id to save
this['userconnectionid'] = undefined;//string internal
this['responsekey'] = undefined;
this['docconnectionid'] = undefined;//string internal
this['jsonparams'] = undefined;//string
this['lcid'] = undefined;
this['useractionid'] = undefined;
@ -165,7 +160,6 @@ function InputCommand(data, copyExplicit) {
this['savepassword'] = undefined;
this['withoutPassword'] = undefined;
this['outputurls'] = undefined;
this['closeonerror'] = undefined;
this['serverVersion'] = undefined;
this['rediskey'] = undefined;
this['nobase64'] = true;
@ -218,9 +212,6 @@ InputCommand.prototype = {
getTokenDownload: function() {
return this['tokenDownload'];
},
getTokenHistory: function() {
return this['tokenHistory'];
},
getData: function() {
return this['data'];
},
@ -359,12 +350,6 @@ InputCommand.prototype = {
setResponseKey: function(data) {
this['responsekey'] = data;
},
getDocConnectionId: function() {
return this['docconnectionid'];
},
setDocConnectionId: function(data) {
this['docconnectionid'] = data;
},
getJsonParams: function() {
return this['jsonparams'];
},
@ -447,12 +432,6 @@ InputCommand.prototype = {
getOutputUrls: function() {
return this['outputurls'];
},
getCloseOnError: function() {
return this['closeonerror'];
},
setCloseOnError: function(data) {
this['closeonerror'] = data;
},
getServerVersion: function() {
return this['serverVersion'];
},

View File

@ -1054,6 +1054,30 @@ let saveRelativeFromChanges = co.wrap(function*(ctx, conn, responseKey, data) {
sendDataRpc(ctx, conn, responseKey, forceSaveRes);
}
})
async function startWopiRPC(ctx, docId, userId, userIdOriginal, data) {
let res;
let selectRes = await taskResult.select(ctx, docId);
let row = selectRes.length > 0 ? selectRes[0] : null;
if (row) {
if (row.callback) {
let userIndex = utils.getIndexFromUserId(userId, userIdOriginal);
let uri = sqlBase.UserCallback.prototype.getCallbackByUserIndex(ctx, row.callback, userIndex);
let wopiParams = wopiClient.parseWopiCallback(ctx, uri, row.callback);
if (wopiParams) {
switch (data.type) {
case 'wopi_RenameFile':
res = await wopiClient.renameFile(ctx, wopiParams, data.name);
break;
case 'wopi_RefreshFile':
res = await wopiClient.refreshFile(ctx, wopiParams, row.baseurl);
break;
}
}
}
}
return res;
}
function* startRPC(ctx, conn, responseKey, data) {
let docId = conn.docId;
ctx.logger.debug('startRPC start responseKey:%s , %j', responseKey, data);
@ -1077,21 +1101,11 @@ function* startRPC(ctx, conn, responseKey, data) {
break;
}
case 'wopi_RenameFile':
let renameRes;
let selectRes = yield taskResult.select(ctx, docId);
let row = selectRes.length > 0 ? selectRes[0] : null;
if (row) {
if (row.callback) {
let userIndex = utils.getIndexFromUserId(conn.user.id, conn.user.idOriginal);
let uri = sqlBase.UserCallback.prototype.getCallbackByUserIndex(ctx, row.callback, userIndex);
let wopiParams = wopiClient.parseWopiCallback(ctx, uri, row.callback);
if (wopiParams) {
renameRes = yield wopiClient.renameFile(ctx, wopiParams, data.name);
}
}
}
sendDataRpc(ctx, conn, responseKey, renameRes);
case 'wopi_RefreshFile': {
let res = yield startWopiRPC(ctx, conn.docId, conn.user.id, conn.user.idOriginal, data);
sendDataRpc(ctx, conn, responseKey, res);
break;
}
case 'pathurls':
let outputData = new canvasService.OutputData(data.type);
yield* canvasService.commandPathUrls(ctx, conn, data.data, outputData);
@ -1720,11 +1734,6 @@ exports.install = function(server, callbackFunction) {
case 'close':
yield* closeDocument(ctx, conn);
break;
case 'versionHistory' : {
let cmd = new commonDefines.InputCommand(data.cmd);
yield* versionHistory(ctx, conn, cmd);
break;
}
case 'openDocument' : {
var cmd = new commonDefines.InputCommand(data.message);
cmd.fillFromConnection(conn);
@ -1935,42 +1944,6 @@ exports.install = function(server, callbackFunction) {
}
}
function* versionHistory(ctx, conn, cmd) {
const tenTokenEnableBrowser = ctx.getCfg('services.CoAuthoring.token.enable.browser', cfgTokenEnableBrowser);
var docIdOld = conn.docId;
var docIdNew = cmd.getDocId();
//check jwt
if (tenTokenEnableBrowser) {
var checkJwtRes = yield checkJwt(ctx, cmd.getTokenHistory(), commonDefines.c_oAscSecretType.Browser);
if (checkJwtRes.decoded) {
fillVersionHistoryFromJwt(ctx, checkJwtRes.decoded, cmd);
docIdNew = cmd.getDocId();
cmd.setWithAuthorization(true);
} else {
sendData(ctx, conn, {type: "expiredToken", code: checkJwtRes.code, description: checkJwtRes.description});
return;
}
}
if (docIdOld !== docIdNew) {
//remove presence(other data was removed before in closeDocument)
yield removePresence(ctx, conn);
var hvals = yield editorData.getPresence(ctx, docIdOld, connections);
if (hvals.length <= 0) {
yield editorData.removePresenceDocument(ctx, docIdOld);
}
//apply new
conn.docId = docIdNew;
yield addPresence(ctx, conn, true);
if (tenTokenEnableBrowser) {
let sessionToken = yield fillJwtByConnection(ctx, conn);
sendDataRefreshToken(ctx, conn, sessionToken);
}
}
//open
yield canvasService.openDocument(ctx, conn, cmd, null);
}
// 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
@ -2090,13 +2063,17 @@ exports.install = function(server, callbackFunction) {
});
}
function sendFileError(ctx, conn, errorId, code) {
ctx.logger.warn('error description: errorId = %s', errorId);
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);
}
conn.isCiriticalError = true;
sendData(ctx, conn, {type: 'error', description: errorId, code: code});
}
function* sendFileErrorAuth(ctx, conn, sessionId, errorId, code) {
function* sendFileErrorAuth(ctx, conn, sessionId, errorId, code, opt_notWarn) {
const tenTokenEnableBrowser = ctx.getCfg('services.CoAuthoring.token.enable.browser', cfgTokenEnableBrowser);
conn.sessionId = sessionId;//restore old
@ -2113,7 +2090,7 @@ exports.install = function(server, callbackFunction) {
let sessionToken = yield fillJwtByConnection(ctx, conn);
sendDataRefreshToken(ctx, conn, sessionToken);
}
sendFileError(ctx, conn, errorId, code);
sendFileError(ctx, conn, errorId, code, opt_notWarn);
}
}
@ -2333,14 +2310,14 @@ exports.install = function(server, callbackFunction) {
openCmd.userid = fileInfo.UserId;
}
}
let permissionsEdit = !fileInfo.ReadOnly && fileInfo.UserCanWrite && queryParams.formsubmit !== "1";
let permissionsFillForm = permissionsEdit || queryParams.formsubmit === "1";
let permissionsEdit = !fileInfo.ReadOnly && fileInfo.UserCanWrite && queryParams?.formsubmit !== "1";
let permissionsFillForm = permissionsEdit || queryParams?.formsubmit === "1";
let permissions = {
edit: permissionsEdit,
review: (fileInfo.SupportsReviewing === false) ? false : (fileInfo.UserCanReview === false ? false : fileInfo.UserCanReview),
copy: fileInfo.CopyPasteRestrictions !== "CurrentDocumentOnly" && fileInfo.CopyPasteRestrictions !== "BlockAll",
print: !fileInfo.DisablePrint && !fileInfo.HidePrintOption,
chat: queryParams.dchat!=="1",
chat: queryParams?.dchat!=="1",
fillForms: permissionsFillForm
};
//todo (review: undefiend)
@ -2351,11 +2328,6 @@ exports.install = function(server, callbackFunction) {
//not '=' because if it jwt from previous version, we must use values from data
Object.assign(data.permissions, permissions);
}
//issuer for secret
if (decoded.iss) {
data.iss = decoded.iss;
}
return res;
}
function validateAuthToken(data, decoded) {
@ -2477,29 +2449,25 @@ exports.install = function(server, callbackFunction) {
ctx.logger.warn('fillDataFromJwt token has invalid format');
res = false;
}
//issuer for secret
if (decoded.iss) {
data.iss = decoded.iss;
}
return res;
}
function fillVersionHistoryFromJwt(ctx, decoded, cmd) {
function fillVersionHistoryFromJwt(ctx, decoded, data) {
let openCmd = data.openCmd;
data.mode = 'view';
data.coEditingMode = 'strict';
data.docid = decoded.key;
openCmd.url = decoded.url;
if (decoded.changesUrl && decoded.previous) {
let versionMatch = cmd.getServerVersion() === commonDefines.buildVersion;
let openPreviousVersion = cmd.getDocId() === decoded.previous.key;
let versionMatch = openCmd.serverVersion === commonDefines.buildVersion;
let openPreviousVersion = openCmd.id === decoded.previous.key;
if (versionMatch && openPreviousVersion) {
cmd.setUrl(decoded.previous.url);
cmd.setDocId(decoded.previous.key);
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', cmd.getServerVersion(), cmd.getDocId());
cmd.setUrl(decoded.url);
cmd.setDocId(decoded.key);
ctx.logger.warn('fillVersionHistoryFromJwt serverVersion mismatch or mismatch between previous url and changes. serverVersion=%s docId=%s', openCmd.serverVersion, openCmd.id);
}
} else {
cmd.setUrl(decoded.url);
cmd.setDocId(decoded.key);
}
return true;
}
function* auth(ctx, conn, data) {
@ -2528,19 +2496,23 @@ exports.install = function(server, callbackFunction) {
} 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
let validationErr = validateAuthToken(data, decoded);
if (!validationErr) {
fillDataFromJwtRes = fillDataFromJwt(ctx, decoded, data);
} else if (tenTokenRequiredParams) {
ctx.logger.error("auth missing required parameter %s (since 7.1 version)", validationErr);
sendDataDisconnectReason(ctx, conn, constants.JWT_ERROR_CODE, constants.JWT_ERROR_REASON);
conn.disconnect(true);
return;
} else {
ctx.logger.warn("auth missing required parameter %s (since 7.1 version)", validationErr);
fillDataFromJwtRes = fillDataFromJwt(ctx, decoded, data);
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) {
@ -2756,13 +2728,14 @@ exports.install = function(server, callbackFunction) {
var updateIfRes = yield taskResult.updateIf(ctx, updateTask, updateMask);
if (!(updateIfRes.affectedRows > 0)) {
// error version
yield* sendFileErrorAuth(ctx, conn, data.sessionId, 'Update Version error', constants.UPDATE_VERSION_CODE);
//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) {
if (bIsRestore) {
// error version
yield* sendFileErrorAuth(ctx, conn, data.sessionId, 'Update Version error', constants.UPDATE_VERSION_CODE);
yield* sendFileErrorAuth(ctx, conn, data.sessionId, 'Update Version error', constants.UPDATE_VERSION_CODE, true);
return;
} else {
modifyConnectionEditorToView(ctx, conn);
@ -2772,8 +2745,11 @@ exports.install = function(server, callbackFunction) {
//ok
} else if (bIsRestore) {
// Other error
let code = null === status ? constants.NO_CACHE_CODE : undefined;
yield* sendFileErrorAuth(ctx, conn, data.sessionId, 'Other error', code);
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;
}
}
@ -2814,17 +2790,17 @@ exports.install = function(server, callbackFunction) {
if (wopiLockRes) {
yield* authRestore(ctx, conn, data.sessionId);
} else {
yield* sendFileErrorAuth(ctx, conn, data.sessionId, 'Restore error. Wopi lock error.', constants.RESTORE_CODE);
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);
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);
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);
yield* sendFileErrorAuth(ctx, conn, data.sessionId, 'DataBase error', constants.RESTORE_CODE, true);
}
} else {
yield* authRestore(ctx, conn, data.sessionId);
@ -2897,7 +2873,7 @@ exports.install = function(server, callbackFunction) {
}
let lockDocument = null;
let waitAuthUserId;
if (!bIsRestore && 2 === countNoView && !tmpUser.view) {
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) {
@ -3655,13 +3631,7 @@ exports.install = function(server, callbackFunction) {
output.fromObject(data.output);
var outputData = output.getData();
var docConnectionId = cmd.getDocConnectionId();
var docId;
if(docConnectionId){
docId = docConnectionId;
} else {
docId = cmd.getDocId();
}
var docId = cmd.getDocId();
if (cmd.getUserConnectionId()) {
participants = getParticipantUser(docId, cmd.getUserConnectionId());
} else {

View File

@ -518,6 +518,41 @@ async function checkAndReplaceEmptyFile(ctx, fileInfo, wopiSrc, access_token, ac
}
}
}
function createDocId(ctx, wopiSrc, mode, fileInfo) {
let fileId = wopiSrc.substring(wopiSrc.lastIndexOf('/') + 1);
let docId = undefined;
if ('view' !== mode) {
docId = `${fileId}`;
} else {
//todo rename operation requires lock
fileInfo.SupportsRename = false;
//todo change docId to avoid empty cache after editors are gone
if (fileInfo.LastModifiedTime) {
docId = `view.${fileId}.${fileInfo.LastModifiedTime}`;
} else {
docId = `view.${fileId}.${fileInfo.Version}`;
}
return docId;
}
docId = docId.replace(constants.DOC_ID_REPLACE_REGEX, '_').substring(0, constants.DOC_ID_MAX_LENGTH);
return docId;
}
async function preOpen(ctx, lockId, docId, fileInfo, userAuth, baseUrl, fileType) {
//todo move to lock and common info saving to websocket connection
//save common info
if (undefined === lockId) {
//Use deterministic(not random) lockId to fix issues with forgotten openings due to integrator failures
lockId = docId;
let commonInfo = JSON.stringify({lockId: lockId, fileInfo: fileInfo});
await canvasService.commandOpenStartPromise(ctx, docId, baseUrl, commonInfo, fileType);
}
//Lock
if ('view' !== userAuth.mode) {
let lockRes = await lock(ctx, 'LOCK', lockId, fileInfo, userAuth);
return !!lockRes;
}
return true;
}
function getEditorHtml(req, res) {
return co(function*() {
let params = {key: undefined, apiQuery: '', fileInfo: {}, userAuth: {}, queryParams: req.query, token: undefined, documentType: undefined, docs_api_config: {}};
@ -567,20 +602,8 @@ function getEditorHtml(req, res) {
mode = 'view';
}
//docId
let docId = undefined;
if ('view' !== mode) {
docId = `${fileId}`;
} else {
//todo rename operation requires lock
fileInfo.SupportsRename = false;
//todo change docId to avoid empty cache after editors are gone
if (fileInfo.LastModifiedTime) {
docId = `view.${fileId}.${fileInfo.LastModifiedTime}`;
} else {
docId = `view.${fileId}.${fileInfo.Version}`;
}
}
docId = docId.replace(constants.DOC_ID_REPLACE_REGEX, '_').substring(0, constants.DOC_ID_MAX_LENGTH);
let docId = createDocId(ctx, wopiSrc, mode, fileInfo);
ctx.setDocId(fileId);
ctx.logger.debug(`wopiEditor`);
params.key = docId;
let userAuth = params.userAuth = {
@ -590,27 +613,15 @@ function getEditorHtml(req, res) {
//check and invalidate cache
let checkRes = yield checkAndInvalidateCache(ctx, docId, fileInfo);
let lockId = checkRes.lockId;
if (!checkRes.success) {
params.fileInfo = {};
return;
}
if (!shutdownFlag) {
//save common info
if (undefined === lockId) {
//Use deterministic(not random) lockId to fix issues with forgotten openings due to integrator failures
lockId = docId;
let commonInfo = JSON.stringify({lockId: lockId, fileInfo: fileInfo});
yield canvasService.commandOpenStartPromise(ctx, docId, utils.getBaseUrlByRequest(ctx, req), commonInfo, fileType);
}
//Lock
if ('view' !== mode) {
let lockRes = yield lock(ctx, 'LOCK', lockId, fileInfo, userAuth);
if (!lockRes) {
params.fileInfo = {};
return;
}
let preOpenRes = yield preOpen(ctx, checkRes.lockId, docId, fileInfo, userAuth, utils.getBaseUrlByRequest(ctx, req), fileType);
if (!preOpenRes) {
params.fileInfo = {};
return;
}
}
@ -835,6 +846,47 @@ function renameFile(ctx, wopiParams, name) {
return res;
});
}
async function refreshFile(ctx, wopiParams, baseUrl) {
let res = {};
try {
ctx.logger.info('wopi RefreshFile start');
let userAuth = wopiParams.userAuth;
if (!userAuth) {
return;
}
const tenTokenEnableBrowser = ctx.getCfg('services.CoAuthoring.token.enable.browser', cfgTokenEnableBrowser);
const tenTokenOutboxAlgorithm = ctx.getCfg('services.CoAuthoring.token.outbox.algorithm', cfgTokenOutboxAlgorithm);
const tenTokenOutboxExpires = ctx.getCfg('services.CoAuthoring.token.outbox.expires', cfgTokenOutboxExpires);
const fileInfo = await checkFileInfo(ctx, userAuth.wopiSrc, userAuth.access_token);
const fileType = getFileTypeByInfo(fileInfo);
const docId = createDocId(ctx, userAuth.wopiSrc, userAuth.mode, res.fileInfo);
res.key = docId;
res.userAuth = userAuth;
res.fileInfo = fileInfo;
res.queryParams = undefined;
if (tenTokenEnableBrowser) {
let options = {algorithm: tenTokenOutboxAlgorithm, expiresIn: tenTokenOutboxExpires};
let secret = await tenantManager.getTenantSecret(ctx, commonDefines.c_oAscSecretType.Browser);
res.token = jwt.sign(res, secret, options);
}
let checkRes = await checkAndInvalidateCache(ctx, docId, fileInfo);
if (!checkRes.success) {
res = {};
return;
}
let preOpenRes = await preOpen(ctx, checkRes.lockId, docId, fileInfo, userAuth, baseUrl, fileType);
if (!preOpenRes) {
res = {};
}
} catch (err) {
ctx.logger.error('wopi error RefreshFile:%s', err.stack);
} finally {
ctx.logger.info('wopi RefreshFile end');
}
return res;
}
function checkFileInfo(ctx, wopiSrc, access_token, opt_sc) {
return co(function* () {
let fileInfo = undefined;
@ -1099,6 +1151,7 @@ exports.putFile = putFile;
exports.parsePutFileResponse = parsePutFileResponse;
exports.putRelativeFile = putRelativeFile;
exports.renameFile = renameFile;
exports.refreshFile = refreshFile;
exports.lock = lock;
exports.unlock = unlock;
exports.fillStandardHeaders = fillStandardHeaders;