diff --git a/Common/config/default.json b/Common/config/default.json index 3be64ae5..0b588e01 100644 --- a/Common/config/default.json +++ b/Common/config/default.json @@ -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"], diff --git a/DocService/sources/converterservice.js b/DocService/sources/converterservice.js index 1d8642be..08f07f7a 100644 --- a/DocService/sources/converterservice.js +++ b/DocService/sources/converterservice.js @@ -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; diff --git a/DocService/sources/server.js b/DocService/sources/server.js index 3f0f4342..92fba29a 100644 --- a/DocService/sources/server.js +++ b/DocService/sources/server.js @@ -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){ diff --git a/DocService/sources/wopiClient.js b/DocService/sources/wopiClient.js index eb50f079..437c40d6 100644 --- a/DocService/sources/wopiClient.js +++ b/DocService/sources/wopiClient.js @@ -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 += ``; output += ``; + if (-1 === cfgWopiPdfView.indexOf(ext.view[j])) { + let urlConvert = `${templateStart}/convert-and-edit/${ext.view[j]}/${ext.targetext}?${templateEnd}`; + output += ``; + } } for (let j = 0; j < ext.edit.length; ++j) { output += ``; @@ -155,6 +165,10 @@ function discovery(req, res) { output += ``; output += ``; output += ``; + if (-1 === cfgWopiPdfView.indexOf(ext.view[j])) { + let urlConvert = `${templateStart}/convert-and-edit/${ext.view[j]}/${ext.targetext}?${templateEnd}`; + output += ``; + } output += ``; }); } @@ -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;