[wopi] Add "convert" as wopi discovery action

This commit is contained in:
Sergey Konovalov
2022-09-07 14:00:54 +03:00
parent 42fe73459a
commit 620cfc040d
4 changed files with 230 additions and 14 deletions

View File

@ -81,7 +81,8 @@
"favIconUrlCell" : "/web-apps/apps/spreadsheeteditor/main/resources/img/favicon.ico",
"favIconUrlSlide" : "/web-apps/apps/presentationeditor/main/resources/img/favicon.ico",
"fileInfoBlockList" : ["FileUrl"],
"wordView": ["pdf", "djvu", "xps", "oxps", "doc", "dotx", "dotm", "dot", "fodt", "ott", "rtf", "mht", "html", "htm", "xml", "epub", "fb2"],
"pdfView": ["pdf", "djvu", "xps", "oxps"],
"wordView": ["doc", "dotx", "dotm", "dot", "fodt", "ott", "rtf", "mht", "html", "htm", "xml", "epub", "fb2"],
"wordEdit": ["docx", "docm", "docxf", "oform", "odt", "txt"],
"cellView": ["xls", "xlsb", "xltx", "xltm", "xlt", "fods", "ots"],
"cellEdit": ["xlsx", "xlsm", "ods", "csv"],

View File

@ -43,6 +43,7 @@ var constants = require('./../../Common/sources/constants');
var commonDefines = require('./../../Common/sources/commondefines');
var docsCoServer = require('./DocsCoServer');
var canvasService = require('./canvasservice');
var wopiClient = require('./wopiClient');
var storage = require('./../../Common/sources/storage-base');
var formatChecker = require('./../../Common/sources/formatchecker');
var statsDClient = require('./../../Common/sources/statsdclient');
@ -50,20 +51,20 @@ var storageBase = require('./../../Common/sources/storage-base');
var operationContext = require('./../../Common/sources/operationContext');
const sqlBase = require('./baseConnector');
const cfgTokenEnableBrowser = config.get('services.CoAuthoring.token.enable.browser');
var CONVERT_ASYNC_DELAY = 1000;
var clientStatsD = statsDClient.getClient();
function* getConvertStatus(ctx, cmd, selectRes, opt_checkPassword) {
function* getConvertStatus(ctx, docId, encryptedUserPassword, selectRes, opt_checkPassword) {
var status = new commonDefines.ConvertStatus(constants.NO_ERROR);
if (selectRes.length > 0) {
var docId = cmd.getDocId();
var row = selectRes[0];
let password = opt_checkPassword && sqlBase.DocumentPassword.prototype.getCurPassword(ctx, row.password);
switch (row.status) {
case taskResult.FileStatus.Ok:
if (password) {
let encryptedUserPassword = cmd.getPassword();
let isCorrectPassword;
if (encryptedUserPassword) {
let decryptedPassword = yield utils.decryptPassword(password);
@ -147,7 +148,7 @@ function* convertByCmd(ctx, cmd, async, opt_fileTo, opt_taskExist, opt_priority,
var status;
if (!bCreate) {
selectRes = yield taskResult.select(ctx, docId);
status = yield* getConvertStatus(ctx, cmd, selectRes, opt_checkPassword);
status = yield* getConvertStatus(ctx, cmd.getDocId() ,cmd.getPassword(), selectRes, opt_checkPassword);
} else {
var queueData = new commonDefines.TaskQueueData();
queueData.setCtx(ctx);
@ -169,7 +170,7 @@ function* convertByCmd(ctx, cmd, async, opt_fileTo, opt_taskExist, opt_priority,
}
yield utils.sleep(CONVERT_ASYNC_DELAY);
selectRes = yield taskResult.select(ctx, docId);
status = yield* getConvertStatus(ctx, cmd, selectRes, opt_checkPassword);
status = yield* getConvertStatus(ctx, cmd.getDocId() ,cmd.getPassword(), selectRes, opt_checkPassword);
waitTime += CONVERT_ASYNC_DELAY;
if (waitTime > utils.CONVERTION_TIMEOUT) {
status.err = constants.CONVERT_TIMEOUT;
@ -519,8 +520,110 @@ function convertTo(req, res) {
}
});
}
function convertAndEdit(ctx, wopiParams, filetypeFrom, filetypeTo) {
return co(function*() {
try {
ctx.logger.info('convert-and-edit start');
let task = yield* taskResult.addRandomKeyTask(ctx, undefined, 'conv_', 8);
let docId = task.key;
let outputFormat = formatChecker.getFormatFromString(filetypeTo);
if (constants.AVS_OFFICESTUDIO_FILE_UNKNOWN === outputFormat) {
ctx.logger.debug('convert-and-edit unknown outputFormat %s', filetypeTo);
return;
}
let cmd = new commonDefines.InputCommand();
cmd.setCommand('conv');
cmd.setDocId(docId);
cmd.setUrl('dummy-url');
cmd.setWopiParams(wopiParams);
cmd.setFormat(filetypeFrom);
cmd.setOutputFormat(outputFormat);
let fileTo = constants.OUTPUT_NAME;
let outputExt = formatChecker.getStringFromFormat(outputFormat);
if (outputExt) {
fileTo += '.' + outputExt;
}
let queueData = new commonDefines.TaskQueueData();
queueData.setCtx(ctx);
queueData.setCmd(cmd);
queueData.setToFile(fileTo);
yield* docsCoServer.addTask(queueData, constants.QUEUE_PRIORITY_LOW);
let async = true;
yield* convertByCmd(ctx, cmd, async, fileTo);
return docId;
} catch (err) {
ctx.logger.error('convert-and-edit error:%s', err.stack);
} finally {
ctx.logger.info('convert-and-edit end');
}
});
}
function getConverterHtmlHandler(req, res) {
return co(function*() {
let isJson = true;
let ctx = new operationContext.Context();
try {
ctx.initFromRequest(req);
ctx.logger.info('convert-and-edit-handler start');
let wopiSrc = req.query['wopisrc'];
let access_token = req.query['access_token'];
let targetext = req.query['targetext'];
let docId = req.query['docid'];
ctx.setDocId(docId);
if (!(wopiSrc && access_token && access_token && targetext && docId) ||
constants.AVS_OFFICESTUDIO_FILE_UNKNOWN === formatChecker.getFormatFromString(targetext)) {
ctx.logger.debug('convert-and-edit-handler invalid params: wopiSrc=%s; access_token=%s; targetext=%s; docId=%s', wopiSrc, access_token, targetext, docId);
utils.fillResponse(req, res, new commonDefines.ConvertStatus(constants.CONVERT_PARAMS), isJson);
return;
}
let token = req.query['token'];
if (cfgTokenEnableBrowser) {
let checkJwtRes = yield docsCoServer.checkJwt(ctx, token, commonDefines.c_oAscSecretType.Browser);
if (checkJwtRes.decoded) {
docId = checkJwtRes.decoded.docId;
} else {
ctx.logger.debug('convert-and-edit-handler invalid token %j', token);
utils.fillResponse(req, res, new commonDefines.ConvertStatus(constants.VKEY), isJson);
return;
}
}
ctx.setDocId(docId);
let selectRes = yield taskResult.select(ctx, docId);
let status = yield* getConvertStatus(ctx, docId, undefined, selectRes);
if (status.end && constants.NO_ERROR === status.err) {
let fileTo = `${docId}/${constants.OUTPUT_NAME}.${targetext}`;
let metadata = yield storage.headObject(ctx, fileTo);
let streamObj = yield storage.createReadStream(ctx, fileTo);
let postRes = yield wopiClient.putRelativeFile(ctx, wopiSrc, access_token, null, streamObj.readStream, metadata.ContentLength, `.${targetext}`, true);
if (postRes) {
let fileInfo = JSON.parse(postRes.body);
status.setUrl(fileInfo.HostEditUrl);
status.setExtName('.' + targetext);
} else {
status.err = constants.UNKNOWN;
}
}
utils.fillResponse(req, res, status, isJson);
} catch (err) {
ctx.logger.error('convert-and-edit-handler error:%s', err.stack);
utils.fillResponse(req, res, new commonDefines.ConvertStatus(constants.UNKNOWN), isJson);
} finally {
ctx.logger.info('convert-and-edit-handler end');
}
});
}
exports.convertFromChanges = convertFromChanges;
exports.convertJson = convertRequestJson;
exports.convertXml = convertRequestXml;
exports.convertTo = convertTo;
exports.convertAndEdit = convertAndEdit;
exports.getConverterHtmlHandler = getConverterHtmlHandler;
exports.builder = builderRequest;

View File

@ -251,6 +251,8 @@ docsCoServer.install(server, () => {
app.post('/lool/convert-to/:format?', utils.checkClientIp, urleEcodedParser, fileForms.single('data'), converterService.convertTo);
app.post('/cool/convert-to/:format?', utils.checkClientIp, urleEcodedParser, fileForms.single('data'), converterService.convertTo);
app.post('/hosting/wopi/:documentType/:mode', urleEcodedParser, forms.none(), utils.lowercaseQueryString, wopiClient.getEditorHtml);
app.post('/hosting/wopi/convert-and-edit/:ext/:targetext', urleEcodedParser, forms.none(), utils.lowercaseQueryString, wopiClient.getConverterHtml);
app.get('/hosting/wopi/convert-and-edit-handler', utils.lowercaseQueryString, converterService.getConverterHtmlHandler);
}
app.post('/dummyCallback', utils.checkClientIp, rawFileParser, function(req, res){

View File

@ -49,6 +49,7 @@ const tenantManager = require('./../../Common/sources/tenantManager');
const sqlBase = require('./baseConnector');
const taskResult = require('./taskresult');
const canvasService = require('./canvasservice');
const converterService = require('./converterservice');
const cfgTokenOutboxAlgorithm = config.get('services.CoAuthoring.token.outbox.algorithm');
const cfgTokenOutboxExpires = config.get('services.CoAuthoring.token.outbox.expires');
@ -57,6 +58,7 @@ const cfgCallbackRequestTimeout = config.get('services.CoAuthoring.server.callba
const cfgDownloadTimeout = config.get('FileConverter.converter.downloadTimeout');
const cfgWopiFileInfoBlockList = config.get('wopi.fileInfoBlockList');
const cfgWopiWopiZone = config.get('wopi.wopiZone');
const cfgWopiPdfView = config.get('wopi.pdfView');
const cfgWopiWordView = config.get('wopi.wordView');
const cfgWopiWordEdit = config.get('wopi.wordEdit');
const cfgWopiCellView = config.get('wopi.cellView');
@ -105,8 +107,12 @@ function discovery(req, res) {
let baseUrl = cfgWopiHost || utils.getBaseUrlByRequest(req);
let names = ['Word','Excel','PowerPoint'];
let favIconUrls = [cfgWopiFavIconUrlWord, cfgWopiFavIconUrlCell, cfgWopiFavIconUrlSlide];
let exts = [{view: cfgWopiWordView, edit: cfgWopiWordEdit}, {view: cfgWopiCellView, edit: cfgWopiCellEdit},
{view: cfgWopiSlideView, edit: cfgWopiSlideEdit}];
let exts = [
{targetext: 'docx', view: cfgWopiPdfView.concat(cfgWopiWordView), edit: cfgWopiWordEdit},
{targetext: 'xlsx', view: cfgWopiCellView, edit: cfgWopiCellEdit},
{targetext: 'pptx', view: cfgWopiSlideView, edit: cfgWopiSlideEdit}
];
let templateStart = `${baseUrl}/hosting/wopi`;
let templateEnd = `&<rs=DC_LLCC&><dchat=DISABLE_CHAT&><embed=EMBEDDED&>`;
templateEnd += `<fs=FULLSCREEN&><hid=HOST_SESSION_ID&><rec=RECORDING&>`;
@ -129,6 +135,10 @@ function discovery(req, res) {
for (let j = 0; j < ext.view.length; ++j) {
output += `<action name="view" ext="${ext.view[j]}" urlsrc="${urlTemplateView}" />`;
output += `<action name="embedview" ext="${ext.view[j]}" urlsrc="${urlTemplateEmbedView}" />`;
if (-1 === cfgWopiPdfView.indexOf(ext.view[j])) {
let urlConvert = `${templateStart}/convert-and-edit/${ext.view[j]}/${ext.targetext}?${templateEnd}`;
output += `<action name="convert" ext="${ext.view[j]}" targetext="${ext.targetext}" requires="update" urlsrc="${urlConvert}" />`;
}
}
for (let j = 0; j < ext.edit.length; ++j) {
output += `<action name="view" ext="${ext.edit[j]}" urlsrc="${urlTemplateView}" />`;
@ -155,6 +165,10 @@ function discovery(req, res) {
output += `<app name="${value}">`;
output += `<action name="view" ext="" default="true" urlsrc="${urlTemplateView}" />`;
output += `<action name="embedview" ext="" urlsrc="${urlTemplateEmbedView}" />`;
if (-1 === cfgWopiPdfView.indexOf(ext.view[j])) {
let urlConvert = `${templateStart}/convert-and-edit/${ext.view[j]}/${ext.targetext}?${templateEnd}`;
output += `<action name="convert" ext="" targetext="${ext.targetext}" requires="update" urlsrc="${urlConvert}" />`;
}
output += `</app>`;
});
}
@ -328,9 +342,8 @@ function getEditorHtml(req, res) {
let access_token = req.body['access_token'] || "";
let access_token_ttl = parseInt(req.body['access_token_ttl']) || 0;
let uri = `${encodeURI(wopiSrc)}?access_token=${encodeURIComponent(access_token)}`;
let fileInfo = params.fileInfo = yield checkFileInfo(ctx, uri, access_token, sc);
let fileInfo = params.fileInfo = yield checkFileInfo(ctx, wopiSrc, access_token, sc);
if (!fileInfo) {
params.fileInfo = {};
return;
@ -394,7 +407,7 @@ function getEditorHtml(req, res) {
if (cfgTokenEnableBrowser) {
let options = {algorithm: cfgTokenOutboxAlgorithm, expiresIn: cfgTokenOutboxExpires};
let secret = yield tenantManager.getTenantSecret(ctx, commonDefines.c_oAscSecretType.Inbox);
let secret = yield tenantManager.getTenantSecret(ctx, commonDefines.c_oAscSecretType.Browser);
params.token = jwt.sign(params, secret, options);
}
} catch (err) {
@ -412,6 +425,64 @@ function getEditorHtml(req, res) {
}
});
}
function getConverterHtml(req, res) {
return co(function*() {
let params = {statusHandler: undefined};
let ctx = new operationContext.Context();
try {
ctx.initFromRequest(req);
let wopiSrc = req.query['wopisrc'];
let fileId = wopiSrc.substring(wopiSrc.lastIndexOf('/') + 1);
ctx.setDocId(fileId);
ctx.logger.info('convert-and-edit start');
let access_token = req.body['access_token'] || "";
let access_token_ttl = parseInt(req.body['access_token_ttl']) || 0;
let ext = req.params.ext;
let targetext = req.params.targetext;
if (!(wopiSrc && access_token && access_token_ttl && ext && targetext)) {
ctx.logger.debug('convert-and-edit invalid params: wopiSrc=%s; access_token=%s; access_token_ttl=%s; ext=%s; targetext=%s', wopiSrc, access_token, access_token_ttl, ext, targetext);
return;
}
let fileInfo = yield checkFileInfo(ctx, wopiSrc, access_token);
if (!fileInfo) {
ctx.logger.info('convert-and-edit checkFileInfo error');
return;
}
let wopiParams = getWopiParams(null, fileInfo, wopiSrc, access_token, access_token_ttl);
let docId = yield converterService.convertAndEdit(ctx, wopiParams, ext, targetext);
if (docId) {
let baseUrl = cfgWopiHost || utils.getBaseUrlByRequest(req);
params.statusHandler = `${baseUrl}/hosting/wopi/convert-and-edit-handler`;
params.statusHandler += `?wopiSrc=${encodeURI(wopiSrc)}&access_token=${encodeURI(access_token)}`;
params.statusHandler += `&targetext=${encodeURI(targetext)}&docId=${encodeURI(docId)}`;
if (cfgTokenEnableBrowser) {
let tokenData = {docId: docId};
let options = {algorithm: cfgTokenOutboxAlgorithm, expiresIn: cfgTokenOutboxExpires};
let secret = yield tenantManager.getTenantSecret(ctx, commonDefines.c_oAscSecretType.Browser);
let token = jwt.sign(tokenData, secret, options);
params.statusHandler += `&token=${encodeURI(token)}`;
}
}
} catch (err) {
ctx.logger.error('convert-and-edit error:%s', err.stack);
} finally {
ctx.logger.debug('convert-and-edit render params=%j', params);
try {
res.render("convert-and-edit-wopi", params);
} catch (err) {
ctx.logger.error('convert-and-edit error:%s', err.stack);
res.sendStatus(400);
}
ctx.logger.info('convert-and-edit end');
}
});
}
function putFile(ctx, wopiParams, data, dataStream, dataSize, userLastChangeId, isModifiedByUser, isAutosave, isExitSave) {
return co(function* () {
let postRes = null;
@ -457,6 +528,34 @@ function putFile(ctx, wopiParams, data, dataStream, dataSize, userLastChangeId,
return postRes;
});
}
function putRelativeFile(ctx, wopiSrc, access_token, data, dataStream, dataSize, suggestedTarget, isFileConversion) {
return co(function* () {
let postRes = null;
try {
ctx.logger.info('wopi putRelativeFile start');
let uri = `${wopiSrc}?access_token=${access_token}`;
let filterStatus = yield checkIpFilter(ctx, uri);
if (0 !== filterStatus) {
return postRes;
}
let headers = {'X-WOPI-Override': 'PUT_RELATIVE', 'X-WOPI-SuggestedTarget': utf7.encode(suggestedTarget),
'X-WOPI-FileConversion': isFileConversion};
fillStandardHeaders(headers, uri, access_token);
ctx.logger.debug('wopi putRelativeFile request uri=%s headers=%j', uri, headers);
postRes = yield utils.postRequestPromise(uri, data, dataStream, dataSize, cfgCallbackRequestTimeout, undefined, headers);
ctx.logger.debug('wopi putRelativeFile response headers=%j', postRes.response.headers);
ctx.logger.debug('wopi putRelativeFile response body:%s', postRes.body);
} catch (err) {
ctx.logger.error('wopi error putRelativeFile:%s', err.stack);
} finally {
ctx.logger.info('wopi putRelativeFile end');
}
return postRes;
});
}
function renameFile(ctx, wopiParams, name) {
return co(function* () {
let res = undefined;
@ -501,18 +600,19 @@ function renameFile(ctx, wopiParams, name) {
return res;
});
}
function checkFileInfo(ctx, uri, access_token, sc) {
function checkFileInfo(ctx, wopiSrc, access_token, opt_sc) {
return co(function* () {
let fileInfo = undefined;
try {
ctx.logger.info('wopi checkFileInfo start');
let uri = `${encodeURI(wopiSrc)}?access_token=${encodeURIComponent(access_token)}`;
let filterStatus = yield checkIpFilter(ctx, uri);
if (0 !== filterStatus) {
return fileInfo;
}
let headers = {};
if (sc) {
headers['X-WOPI-SessionContext'] = sc;
if (opt_sc) {
headers['X-WOPI-SessionContext'] = opt_sc;
}
fillStandardHeaders(headers, uri, access_token);
ctx.logger.debug('wopi checkFileInfo request uri=%s headers=%j', uri, headers);
@ -650,12 +750,22 @@ function checkIpFilter(ctx, uri){
return filterStatus;
});
}
function getWopiParams(lockId, fileInfo, wopiSrc, access_token, access_token_ttl) {
let commonInfo = {lockId: lockId, fileInfo: fileInfo};
let userAuth = {
wopiSrc: wopiSrc, access_token: access_token, access_token_ttl: access_token_ttl,
hostSessionId: null, userSessionId: null, mode: null
};
return {commonInfo: commonInfo, userAuth: userAuth, LastModifiedTime: null};
};
exports.discovery = discovery;
exports.collaboraCapabilities = collaboraCapabilities;
exports.parseWopiCallback = parseWopiCallback;
exports.getEditorHtml = getEditorHtml;
exports.getConverterHtml = getConverterHtml;
exports.putFile = putFile;
exports.putRelativeFile = putRelativeFile;
exports.renameFile = renameFile;
exports.lock = lock;
exports.unlock = unlock;