/** * * (c) Copyright Ascensio System SIA 2025 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // connect the necessary packages and modules const express = require('express'); const path = require('path'); const favicon = require('serve-favicon'); const bodyParser = require('body-parser'); const fileSystem = require('fs'); const formidable = require('formidable'); const jwt = require('jsonwebtoken'); const config = require('config'); const mime = require('mime'); const urllib = require('urllib'); const urlModule = require('url'); const { emitWarning } = require('process'); const DocManager = require('./helpers/docManager'); const documentService = require('./helpers/documentService'); const fileUtility = require('./helpers/fileUtility'); const wopiApp = require('./helpers/wopi/wopiRouting'); const users = require('./helpers/users'); const configServer = config.get('server'); const siteUrl = configServer.get('siteUrl'); const enableForgotten = configServer.get('enableForgotten'); const fileChoiceUrl = configServer.has('fileChoiceUrl') ? configServer.get('fileChoiceUrl') : ''; const cfgSignatureEnable = configServer.get('token.enable'); const cfgSignatureUseForRequest = configServer.get('token.useforrequest'); const cfgSignatureAuthorizationHeader = configServer.get('token.authorizationHeader'); const cfgSignatureAuthorizationHeaderPrefix = configServer.get('token.authorizationHeaderPrefix'); const cfgSignatureSecretExpiresIn = configServer.get('token.expiresIn'); const cfgSignatureSecret = configServer.get('token.secret'); const verifyPeerOff = configServer.get('verify_peer_off'); const plugins = config.get('plugins'); if (verifyPeerOff) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; } String.prototype.hashCode = function hashCode() { const len = this.length; let ret = 0; for (let i = 0; i < len; i++) { ret = Math.imul(ret, 31) + this.charCodeAt(i); } return ret; }; String.prototype.format = function format(...args) { let text = this.toString(); if (!args.length) return text; for (let i = 0; i < args.length; i++) { text = text.replace(new RegExp(`\\{${i}\\}`, 'gi'), args[i]); } return text; }; const app = express(); // create an application object app.disable('x-powered-by'); app.set('views', path.join(__dirname, 'views')); // specify the path to the main template app.set('view engine', 'ejs'); // specify which template engine is used app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); // allow any Internet domain to access the resources of this site next(); }); app.use(express.static(path.join(__dirname, 'public'))); // public directory app.use(favicon(`${__dirname}/public/images/favicon.ico`)); // use favicon app.use(bodyParser.json()); // connect middleware that parses json app.use(bodyParser.urlencoded({ extended: false })); // connect middleware that parses urlencoded bodies app.get('/', (req, res) => { // define a handler for default page try { req.DocManager = new DocManager(req, res); res.render('index', { // render index template with the parameters specified preloaderUrl: siteUrl + configServer.get('preloaderUrl'), fillExts: fileUtility.getFillExtensions(), storedFiles: req.DocManager.getStoredFiles(), params: req.DocManager.getCustomParams(), users, languages: configServer.get('languages'), serverVersion: config.get('version'), enableForgotten, }); } catch (ex) { console.log(ex); // display error message in the console res.status(500); // write status parameter to the response res.render('error', { message: 'Server error' }); // render error template with the message parameter specified } }); app.get('/forgotten', async (req, res) => { if (!enableForgotten) { res.status(403); res.render( 'error', { message: 'The forgotten page is disabled.' }, ); return; } let forgottenFiles = []; function getForgottenList() { return new Promise((resolve, reject) => { documentService.commandRequest('getForgottenList', '', (err, data, ress) => { if (err) { reject(err); } else { resolve(JSON.parse(ress.data)); } }); }); } function getForgottenFile(key) { return new Promise((resolve, reject) => { documentService.commandRequest('getForgotten', key, (err, data) => { if (err) { reject(err); } else { const parsedData = JSON.parse(data); resolve({ name: parsedData.key, documentType: fileUtility.getFileType(parsedData.url), url: parsedData.url, }); } }); }); } try { const forgottenListResponse = await getForgottenList(); const { keys } = forgottenListResponse; forgottenFiles = await Promise.all(keys.map(getForgottenFile)); } catch (error) { console.error(error); } req.DocManager = new DocManager(req, res); res.render('forgotten', { forgottenFiles }); }); app.delete('/forgotten', (req, res) => { // define a handler for removing forgotten file if (!enableForgotten) { res.sendStatus(403); return; } try { const fileName = req.query.filename; if (fileName && typeof fileName === 'string') { // if the forgotten file name is defined documentService.commandRequest('deleteForgotten', fileName); res.status(204).send(); } } catch (ex) { console.log(ex); res.write('Server error'); res.status(500).send(); } }); app.get('/download', (req, res) => { // define a handler for downloading files req.DocManager = new DocManager(req, res); const fileName = fileUtility.getFileName(req.query.fileName); const userAddress = req.query.useraddress; let token = ''; if (!!userAddress && cfgSignatureEnable && cfgSignatureUseForRequest) { const authorization = req.get(cfgSignatureAuthorizationHeader); if (authorization && authorization.startsWith(cfgSignatureAuthorizationHeaderPrefix)) { token = authorization.substring(cfgSignatureAuthorizationHeaderPrefix.length); } try { jwt.verify(token, cfgSignatureSecret); } catch (err) { console.log(`checkJwtHeader error: name = ${err.name} message = ${err.message} token = ${token}`); res.sendStatus(403); return; } } // get the path to the force saved document version let filePath = req.DocManager.forcesavePath(fileName, userAddress, false); if (filePath === '') { filePath = req.DocManager.storagePath(fileName, userAddress); // or to the original document } // add headers to the response to specify the page parameters res.setHeader('Content-Length', fileSystem.statSync(filePath).size); res.setHeader('Content-Type', mime.getType(filePath)); res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`); const filestream = fileSystem.createReadStream(filePath); filestream.pipe(res); // send file information to the response by streams }); app.get('/history', (req, res) => { req.DocManager = new DocManager(req, res); if (cfgSignatureEnable && cfgSignatureUseForRequest) { const authorization = req.get(cfgSignatureAuthorizationHeader); if (authorization && authorization.startsWith(cfgSignatureAuthorizationHeaderPrefix)) { const token = authorization.substring(cfgSignatureAuthorizationHeaderPrefix.length); try { jwt.verify(token, cfgSignatureSecret); } catch (err) { console.log(`checkJwtHeader error: name = ${err.name} message = ${err.message} token = ${token}`); res.sendStatus(403); return; } } else { res.sendStatus(403); return; } } const { fileName } = req.query; const userAddress = req.query.useraddress; const { ver } = req.query; const { file } = req.query; let Path = ''; if (file.includes('diff')) { Path = req.DocManager.diffPath(fileName, userAddress, ver); } else if (file.includes('prev')) { Path = req.DocManager.prevFilePath(fileName, userAddress, ver); } else { res.sendStatus(403); return; } // add headers to the response to specify the page parameters res.setHeader('Content-Length', fileSystem.statSync(Path).size); res.setHeader('Content-Type', mime.getType(Path)); res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(file)}`); const filestream = fileSystem.createReadStream(Path); filestream.pipe(res); // send file information to the response by streams }); app.post('/upload', (req, res) => { // define a handler for uploading files req.DocManager = new DocManager(req, res); req.DocManager.storagePath(''); // mkdir if not exist const userIp = req.DocManager.curUserHostAddress(); // get the path to the user host const uploadDir = req.DocManager.storageRootPath(userIp); const uploadDirTmp = path.join(uploadDir, 'tmp'); // and create directory for temporary files if it doesn't exist req.DocManager.createDirectory(uploadDirTmp); const fileSizeLimit = configServer.get('maxFileSize'); // create a new incoming form const form = new formidable.IncomingForm({ maxFileSize: fileSizeLimit, maxTotalFileSize: fileSizeLimit }); form.uploadDir = uploadDirTmp; // and write there all the necessary parameters form.keepExtensions = true; form.parse(req, (err, fields, files) => { // parse this form if (err) { // if an error occurs // DocManager.cleanFolderRecursive(uploadDirTmp, true); // clean the folder with temporary files res.writeHead(200, { 'Content-Type': 'text/plain' }); // and write the error status and message to the response res.write(`{ "error": "${err.message}"}`); res.end(); return; } const file = files.uploadedFile[0]; if (file === undefined) { // if file parameter is undefined res.writeHead(200, { 'Content-Type': 'text/plain' }); // write the error status and message to the response res.write('{ "error": "Uploaded file not found"}'); res.end(); return; } file.originalFilename = req.DocManager.getCorrectName(file.originalFilename); // check if the file size exceeds the maximum file size if (fileSizeLimit < file.size || file.size <= 0) { // DocManager.cleanFolderRecursive(uploadDirTmp, true); // clean the folder with temporary files res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write('{ "error": "File size is incorrect"}'); res.end(); return; } const exts = fileUtility.getSuppotredExtensions(); // all the supported file extensions const curExt = fileUtility.getFileExtension(file.originalFilename, true); const documentType = fileUtility.getFileType(file.originalFilename); if (exts.indexOf(curExt) === -1 || fileUtility.getFormatActions(curExt).length === 0) { // DocManager.cleanFolderRecursive(uploadDirTmp, true); // if not, clean the folder with temporary files res.writeHead(200, { 'Content-Type': 'text/plain' }); // and write the error status and message to the response res.write('{ "error": "File type is not supported"}'); res.end(); return; } fileSystem.rename(file.filepath, `${uploadDir}/${file.originalFilename}`, (error) => { // rename a file // DocManager.cleanFolderRecursive(uploadDirTmp, true); // clean the folder with temporary files res.writeHead(200, { 'Content-Type': 'text/plain' }); if (error) { // if an error occurs res.write(`{ "error": "${error}"}`); // write an error message to the response } else { // otherwise, write a new file name to the response res.write(`{ "filename": "${file.originalFilename}", "documentType": "${documentType}" }`); // get user id and name parameters or set them to the default values const user = users.getUser(req.query.userid); req.DocManager.saveFileData(file.originalFilename, user.id, user.name); } res.end(); }); }); }); app.post('/create', (req, res) => { const { title } = req.body; const fileUrl = req.body.url; try { req.DocManager = new DocManager(req, res); let host = siteUrl; if (host.indexOf('/') === 0) { host = req.DocManager.getServerHost(); } if (urlModule.parse(fileUrl).host !== urlModule.parse(host).host) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.write(JSON.stringify({ error: 'File domain is incorrect' })); res.end(); return; } req.DocManager.storagePath(''); // mkdir if not exist const fileName = req.DocManager.getCorrectName(title); const userAddress = req.DocManager.curUserHostAddress(); req.DocManager.historyPath(fileName, userAddress, true); urllib.request(fileUrl, { method: 'GET' }, (err, data) => { // check if the file size exceeds the maximum file size if (configServer.get('maxFileSize') < data.length || data.length <= 0) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.write(JSON.stringify({ error: 'File size is incorrect' })); res.end(); return; } const exts = fileUtility.getSuppotredExtensions(); // all the supported file extensions const curExt = fileUtility.getFileExtension(fileName, true); if (exts.indexOf(curExt) === -1 || fileUtility.getFormatActions(curExt).length === 0) { // and write the error status and message to the response res.writeHead(200, { 'Content-Type': 'application/json' }); res.write(JSON.stringify({ error: 'File type is not supported' })); res.end(); return; } fileSystem.writeFileSync(req.DocManager.storagePath(fileName), data); res.writeHead(200, { 'Content-Type': 'application/json' }); res.write(JSON.stringify({ file: fileName })); res.end(); }); } catch (e) { res.status(500); res.write(JSON.stringify({ error: 1, message: e.message, })); res.end(); } }); app.post('/convert', (req, res) => { // define a handler for converting files req.DocManager = new DocManager(req, res); const fileName = fileUtility.getFileName(req.body.filename); const filePass = req.body.filePass ? req.body.filePass : null; const lang = req.body.lang ? req.body.lang : null; const fileUri = req.DocManager.getDownloadUrl(fileName, true); const fileExt = fileUtility.getFileExtension(fileName, true); const internalFileExt = 'ooxml'; const convExt = req.body.fileExt ? req.body.fileExt : internalFileExt; const { keepOriginal } = req.body; const response = res; const writeResult = function writeResult(filename, step, error) { const result = {}; // write file name, step and error values to the result object if they are defined if (filename !== null) result.filename = filename; if (step !== null) result.step = step; if (error !== null) result.error = error; response.setHeader('Content-Type', 'application/json'); response.write(JSON.stringify(result)); response.end(); }; const callback = async function callback(err, resp) { if (err) { // if an error occurs // check what type of error it is if (err.name === 'ConnectionTimeoutError' || err.name === 'ResponseTimeoutError') { writeResult(fileName, 0, null); // despite the timeout errors, write the file to the result object } else { writeResult(null, null, JSON.stringify(err)); // other errors trigger an error message } return; } try { const responseData = documentService.getResponseUri(resp.toString()); const result = responseData.percent; const newFileUri = responseData.uri; // get the callback url const newFileType = `.${responseData.fileType}`; // get the file type if (result !== 100) { // if the status isn't 100 writeResult(fileName, result, null); // write the origin file to the result object return; } // get the file name with a new extension const correctName = req.DocManager.getCorrectName(fileUtility.getFileName(fileName, true) + newFileType); const { status, data } = await urllib.request(newFileUri, { method: 'GET' }); if (status !== 200) throw new Error(`Conversion service returned status: ${status}`); // write a file with a new extension, but with the content from the origin file if (fileUtility.getFileType(correctName) !== null) { fileSystem.writeFileSync(req.DocManager.storagePath(correctName), data); } else { writeResult(newFileUri.replace('http://localhost/', siteUrl), result, 'FileTypeIsNotSupported'); return; } // remove file with the origin extension if (!keepOriginal) fileSystem.unlinkSync(req.DocManager.storagePath(fileName)); const userAddress = req.DocManager.curUserHostAddress(); const historyPath = req.DocManager.historyPath(fileName, userAddress, true); // get the history path to the file with a new extension const correctHistoryPath = req.DocManager.historyPath(correctName, userAddress, true); if (!keepOriginal) { fileSystem.renameSync(historyPath, correctHistoryPath); // change the previous history path fileSystem.renameSync( path.join(correctHistoryPath, `${fileName}.txt`), path.join(correctHistoryPath, `${correctName}.txt`), ); // change the name of the .txt file with document information } else if (newFileType !== null) { const user = users.getUser(req.query.userid); req.DocManager.saveFileData(correctName, user.id, user.name); } writeResult(correctName, result, null); // write a file with a new name to the result object } catch (e) { if (!e.message.includes('-9')) console.log(e); // display error message in the console writeResult(null, null, e.message); } }; try { // check if the file with such an extension can be converted if ((fileUtility.getConvertExtensions().indexOf(fileExt) !== -1) || ('fileExt' in req.body)) { const storagePath = req.DocManager.storagePath(fileName); const stat = fileSystem.statSync(storagePath); let key = fileUri + stat.mtime.getTime(); key = documentService.generateRevisionId(key); // get document key // get the url to the converted file documentService.getConvertedUri(fileUri, fileExt, convExt, key, true, callback, filePass, lang, fileName); } else { // if the file with such an extension can't be converted, write the origin file to the result object writeResult(fileName, null, null); } } catch (ex) { console.log(ex); writeResult(null, null, 'Server error'); } }); app.get('/files', (req, res) => { // define a handler for getting files information try { req.DocManager = new DocManager(req, res); // get the information about the files from the storage path const filesInDirectoryInfo = req.DocManager.getFilesInfo(); res.setHeader('Content-Type', 'application/json'); res.write(JSON.stringify(filesInDirectoryInfo)); // transform files information into the json string } catch (ex) { console.log(ex); res.write('Server error'); } res.end(); }); app.get('/files/file/:fileId', (req, res) => { // define a handler for getting file information by its id try { req.DocManager = new DocManager(req, res); const { fileId } = req.params; // get the information about the file specified by a file id const fileInfoById = req.DocManager.getFilesInfo(fileId); res.setHeader('Content-Type', 'application/json'); res.write(JSON.stringify(fileInfoById)); } catch (ex) { console.log(ex); res.write('Server error'); } res.end(); }); app.delete('/file', (req, res) => { // define a handler for removing file try { req.DocManager = new DocManager(req, res); let fileName = req.query.filename; if (fileName) { // if the file name is defined fileName = fileUtility.getFileName(fileName); // get its part without an extension req.DocManager.fileRemove(fileName); // delete file and his history } else { // if the file name is undefined, clean the storage folder req.DocManager.cleanFolderRecursive(req.DocManager.storagePath(''), false); } res.write('{"success":true}'); } catch (ex) { console.log(ex); res.write('Server error'); } res.end(); }); app.get('/csv', (req, res) => { // define a handler for downloading csv files const fileName = 'csv.csv'; const csvPath = path.join(__dirname, 'public', 'assets', 'document-templates', 'sample', fileName); // add headers to the response to specify the page parameters res.setHeader('Content-Length', fileSystem.statSync(csvPath).size); res.setHeader('Content-Type', mime.getType(csvPath)); res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`); const filestream = fileSystem.createReadStream(csvPath); filestream.pipe(res); // send file information to the response by streams }); app.post('/reference', (req, res) => { // define a handler for renaming file req.DocManager = new DocManager(req, res); const result = function result(data) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.write(JSON.stringify(data)); res.end(); }; const { referenceData } = req.body; let fileName = ''; if (referenceData) { const { instanceId } = referenceData; if (instanceId === req.DocManager.getInstanceId()) { const fileKey = JSON.parse(referenceData.fileKey); const { userAddress } = fileKey; if (userAddress === req.DocManager.curUserHostAddress() && req.DocManager.existsSync(req.DocManager.storagePath(fileKey.fileName))) { ({ fileName } = fileKey); } } } if (!fileName && !!req.body.path) { const filePath = fileUtility.getFileName(req.body.path); if (req.DocManager.existsSync(req.DocManager.storagePath(filePath))) { fileName = filePath; } } if (!fileName && !!req.body.link) { if (req.body.link.indexOf(req.DocManager.getServerUrl()) === -1) { result({ url: req.body.link, directUrl: req.body.link, }); return; } const urlObj = urlModule.parse(req.body.link, true); fileName = urlObj.query.fileName; if (!req.DocManager.existsSync(req.DocManager.storagePath(fileName))) { result({ error: 'File is not exist' }); return; } } if (!fileName) { result({ error: 'File is not found' }); return; } const data = { fileType: fileUtility.getFileExtension(fileName).slice(1), key: req.DocManager.getKey(fileName), url: req.DocManager.getDownloadUrl(fileName, true), directUrl: req.body.directUrl ? req.DocManager.getDownloadUrl(fileName) : null, referenceData: { fileKey: JSON.stringify({ fileName, userAddress: req.DocManager.curUserHostAddress() }), instanceId: req.DocManager.getServerUrl(), }, link: `${req.DocManager.getServerUrl()}/editor?fileName=${encodeURIComponent(fileName)}`, path: fileName, }; if (cfgSignatureEnable) { // sign token with given data using signature secret data.token = jwt.sign(data, cfgSignatureSecret, { expiresIn: cfgSignatureSecretExpiresIn }); } result(data); }); app.put('/restore', async (req, res) => { // define a handler for restore file version const { fileName, version, url } = req.body; const result = {}; if (fileName) { req.DocManager = new DocManager(req, res); const userAddress = req.DocManager.curUserHostAddress(); const key = req.DocManager.getKey(fileName); const filePath = req.DocManager.storagePath(fileName, userAddress); const historyPath = req.DocManager.historyPath(fileName, userAddress); const newVersion = req.DocManager.countVersion(historyPath) + 1; const newVersionPath = path.join(`${historyPath}`, `${newVersion}`); if (url) { const { status, data } = await urllib.request(url, { method: 'GET' }); if (status === 200) { req.DocManager.createDirectory(newVersionPath); req.DocManager.copyFile( filePath, path.join(`${newVersionPath}`, `prev${fileUtility.getFileExtension(fileName)}`), ); fileSystem.writeFileSync(path.join(`${newVersionPath}`, 'key.txt'), key); fileSystem.writeFileSync(filePath, data); result.success = true; } else { result.success = false; result.error = `Document editing service returned status: ${status}`; } } else { const versionPath = path.join(`${historyPath}`, `${version}`, `prev${fileUtility.getFileExtension(fileName)}`); if (fileSystem.existsSync(versionPath)) { req.DocManager.createDirectory(newVersionPath); req.DocManager.copyFile( filePath, path.join(`${newVersionPath}`, `prev${fileUtility.getFileExtension(fileName)}`), ); fileSystem.writeFileSync(path.join(`${newVersionPath}`, 'key.txt'), key); req.DocManager.copyFile(versionPath, filePath); result.success = true; } else { result.success = false; result.error = 'Version path does not exists'; } } } else { result.success = false; result.error = 'Filename is empty'; } res.writeHead(200, { 'Content-Type': 'application/json' }); res.write(JSON.stringify(result)); res.end(); }); app.post('/track', async (req, res) => { // define a handler for tracking file changes req.DocManager = new DocManager(req, res); let uAddress = req.query.useraddress; let fName = fileUtility.getFileName(req.query.filename); let version = 0; // track file changes const processTrack = async function processTrack(response, bodyTrack, fileNameTrack, userAddressTrack) { // callback file saving process const callbackProcessSave = async function callbackProcessSave( downloadUri, body, fileName, userAddress, newFileName, ) { try { if (!req.DocManager.existsSync(req.DocManager.storagePath(fileName, userAddress))) { console.log(`callbackProcessSave error: name = ${fileName} userAddress = ${userAddress} is not exist`); response.write('{"error":1, "message":"file is not exist"}'); response.end(); return; } const { status, data } = await urllib.request(downloadUri, { method: 'GET' }); if (status !== 200) throw new Error(`Document editing service returned status: ${status}`); const storagePath = req.DocManager.storagePath(newFileName, userAddress); let historyPath = req.DocManager.historyPath(newFileName, userAddress); // get the path to the history data if (historyPath === '') { // if the history path doesn't exist historyPath = req.DocManager.historyPath(newFileName, userAddress, true); // create it req.DocManager.createDirectory(historyPath); // and create a directory for the history data } const countVersion = req.DocManager.countVersion(historyPath); // get the next file version number version = countVersion + 1; // get the path to the specified file version const versionPath = req.DocManager.versionPath(newFileName, userAddress, version); req.DocManager.createDirectory(versionPath); // create a directory to the specified file version const downloadZip = body.changesurl; if (downloadZip) { // get the path to the file with document versions differences const pathChanges = req.DocManager.diffPath(newFileName, userAddress, version); const zip = await urllib.request(downloadZip, { method: 'GET' }); const statusZip = zip.status; const dataZip = zip.data; if (statusZip === 200) { fileSystem.writeFileSync(pathChanges, dataZip); // write the document version differences to the archive } else { emitWarning(`Document editing service returned status: ${statusZip}`); } } const changeshistory = body.changeshistory || JSON.stringify(body.history); if (changeshistory) { // get the path to the file with document changes const pathChangesJson = req.DocManager.changesPath(newFileName, userAddress, version); fileSystem.writeFileSync(pathChangesJson, changeshistory); // and write this data to the path in json format } const pathKey = req.DocManager.keyPath(newFileName, userAddress, version); // get the path to the key.txt file fileSystem.writeFileSync(pathKey, body.key); // write the key value to the key.txt file // get the path to the previous file version const pathPrev = path.join(versionPath, `prev${fileUtility.getFileExtension(fileName)}`); // and write it to the current path fileSystem.renameSync(req.DocManager.storagePath(fileName, userAddress), pathPrev); fileSystem.writeFileSync(storagePath, data); // get the path to the forcesaved file const forcesavePath = req.DocManager.forcesavePath(newFileName, userAddress, false); if (forcesavePath !== '') { // if this path is empty fileSystem.unlinkSync(forcesavePath); // remove it } } catch (ex) { console.log(ex); response.write(`{"error":1,"message":${JSON.stringify(ex)}}`); response.end(); return; } response.write('{"error":0}'); response.end(); }; // file saving process const processSave = async function processSave(downloadUri, body, fileName, userAddress) { if (!downloadUri) { response.write('{"error":1,"message":"save uri is empty"}'); response.end(); return; } const curExt = fileUtility.getFileExtension(fileName); // get current file extension const downloadExt = `.${body.filetype}`; // get the extension of the downloaded file let newFileName = fileName; // convert downloaded file to the file with the current extension if these extensions aren't equal if (downloadExt !== curExt) { const key = documentService.generateRevisionId(downloadUri); // get the correct file name if it already exists newFileName = req.DocManager.getCorrectName(fileUtility.getFileName(fileName, true) + downloadExt, userAddress); try { documentService.getConvertedUriSync(downloadUri, downloadExt, curExt, key, async (err, data) => { if (err) { await callbackProcessSave(downloadUri, body, fileName, userAddress, newFileName); return; } try { const resp = documentService.getResponseUri(data); await callbackProcessSave(resp.uri, body, fileName, userAddress, fileName); } catch (ex) { console.log(ex); await callbackProcessSave(downloadUri, body, fileName, userAddress, newFileName); } }); return; } catch (ex) { console.log(ex); } } await callbackProcessSave(downloadUri, body, fileName, userAddress, newFileName); }; // callback file force saving process const callbackProcessForceSave = async function callbackProcessForceSave( downloadUri, body, fileName, userAddress, newFileName = false, ) { try { const { status, data } = await urllib.request(downloadUri, { method: 'GET' }); if (status !== 200) throw new Error(`Document editing service returned status: ${status}`); const downloadExt = `.${body.fileType}`; const isSubmitForm = body.forcesavetype === 3; // SubmitForm let correctName = fileName; let forcesavePath = ''; if (isSubmitForm) { // new file if (newFileName) { correctName = req.DocManager.getCorrectName( `${fileUtility.getFileName(fileName, true)}-form${downloadExt}`, userAddress, ); } else { const ext = fileUtility.getFileExtension(fileName); correctName = req.DocManager.getCorrectName( `${fileUtility.getFileName(fileName, true)}-form${ext}`, userAddress, ); } forcesavePath = req.DocManager.storagePath(correctName, userAddress); } else { if (newFileName) { correctName = req.DocManager.getCorrectName(fileUtility.getFileName( fileName, true, ) + downloadExt, userAddress); } // create forcesave path if it doesn't exist forcesavePath = req.DocManager.forcesavePath(correctName, userAddress, false); if (forcesavePath === '') { forcesavePath = req.DocManager.forcesavePath(correctName, userAddress, true); } } fileSystem.writeFileSync(forcesavePath, data); if (isSubmitForm) { const uid = body.actions[0].userid; req.DocManager.saveFileData(correctName, uid, 'Filling Form', userAddress); const { formsdataurl } = body; if (formsdataurl) { const formsdataName = req.DocManager.getCorrectName( `${fileUtility.getFileName(correctName, true)}.txt`, userAddress, ); // get the path to the file with forms data const formsdataPath = req.DocManager.storagePath(formsdataName, userAddress); const formsdata = await urllib.request(formsdataurl, { method: 'GET' }); const statusFormsdata = formsdata.status; const dataFormsdata = formsdata.data; if (statusFormsdata === 200) { fileSystem.writeFileSync(formsdataPath, dataFormsdata); // write the forms data } else { emitWarning(`Document editing service returned status: ${statusFormsdata}`); } } else { emitWarning('Document editing service do not returned formsdataurl'); } } } catch (ex) { console.log(ex); response.write(`{"error":1,"message":${JSON.stringify(ex)}}`); response.end(); return; } response.write('{"error":0}'); response.end(); }; // file force saving process const processForceSave = async function processForceSave(downloadUri, body, fileName, userAddress) { if (!downloadUri) { response.write('{"error":1,"message":"forcesave uri is empty"}'); response.end(); return; } const curExt = fileUtility.getFileExtension(fileName); const downloadExt = `.${body.filetype}`; // convert downloaded file to the file with the current extension if these extensions aren't equal if (downloadExt !== curExt) { const key = documentService.generateRevisionId(downloadUri); try { documentService.getConvertedUriSync(downloadUri, downloadExt, curExt, key, async (err, data) => { if (err) { await callbackProcessForceSave(downloadUri, body, fileName, userAddress, true); return; } try { const resp = documentService.getResponseUri(data); await callbackProcessForceSave(resp.uri, body, fileName, userAddress, false); } catch (ex) { console.log(ex); await callbackProcessForceSave(downloadUri, body, fileName, userAddress, true); } }); return; } catch (ex) { console.log(ex); } } await callbackProcessForceSave(downloadUri, body, fileName, userAddress, false); }; if (bodyTrack.status === 1) { // editing if (bodyTrack.actions && bodyTrack.actions[0].type === 0) { // finished edit const user = bodyTrack.actions[0].userid; if (bodyTrack.users.indexOf(user) === -1) { const { key } = bodyTrack; try { documentService.commandRequest('forcesave', key); // call the forcesave command } catch (ex) { console.log(ex); } } } } else if (bodyTrack.status === 2 || bodyTrack.status === 3) { // MustSave, Corrupted await processSave(bodyTrack.url, bodyTrack, fileNameTrack, userAddressTrack); // save file return; } else if (bodyTrack.status === 6 || bodyTrack.status === 7) { // MustForceSave, CorruptedForceSave await processForceSave(bodyTrack.url, bodyTrack, fileNameTrack, userAddressTrack); // force save file return; } response.write('{"error":0}'); response.end(); }; // read request body const readbody = async function readbody(request, response, fileName, userAddress) { let content = ''; request.on('data', async (data) => { // get data from the request content += data; }); request.on('end', async () => { const body = JSON.parse(content); await processTrack(response, body, fileName, userAddress); // and track file changes }); }; // check jwt token if (cfgSignatureEnable && cfgSignatureUseForRequest) { let body = null; if (req.body.hasOwnProperty('token')) { // if request body has its own token body = documentService.readToken(req.body.token); // read and verify it } else { const checkJwtHeaderRes = documentService.checkJwtHeader(req); // otherwise, check jwt token headers if (checkJwtHeaderRes) { // if they exist if (checkJwtHeaderRes.payload) { body = checkJwtHeaderRes.payload; // get the payload object } // get user address and file name from the query if (checkJwtHeaderRes.query) { if (checkJwtHeaderRes.query.useraddress) { uAddress = checkJwtHeaderRes.query.useraddress; } if (checkJwtHeaderRes.query.filename) { fName = fileUtility.getFileName(checkJwtHeaderRes.query.filename); } } } } if (!body) { res.write('{"error":1,"message":"body is empty"}'); res.end(); return; } await processTrack(res, body, fName, uAddress); return; } if (req.body.hasOwnProperty('status')) { // if the request body has status parameter await processTrack(res, req.body, fName, uAddress); // track file changes } else { await readbody(req, res, fName, uAddress); // otherwise, read request body first } }); app.get('/config', async (req, res) => { try { req.DocManager = new DocManager(req, res); const fileName = fileUtility.getFileName(req.query.fileName); const userAddress = req.DocManager.curUserHostAddress(); // if the file with a given name doesn't exist if (!req.DocManager.existsSync(req.DocManager.storagePath(fileName, userAddress))) { throw new Error(`File not found: ${fileName}`); // display error message } // file config data const data = { document: { key: req.DocManager.getKey(fileName), title: fileName, url: req.DocManager.getDownloadUrl(fileName, true), permissions: JSON.parse(req.query.permissions || '{}'), referenceData: { fileKey: JSON.stringify({ fileName, userAddress: req.DocManager.curUserHostAddress() }), instanceId: req.DocManager.getInstanceId(), }, }, editorConfig: { callbackUrl: req.DocManager.getCallback(fileName), mode: 'edit', }, }; if (req.query.directUrl === 'true') { data.document.directUrl = req.DocManager.getDownloadUrl(fileName); } if (cfgSignatureEnable) { // sign token with given data using signature secret data.token = jwt.sign(data, cfgSignatureSecret, { expiresIn: cfgSignatureSecretExpiresIn }); } res.setHeader('Content-Type', 'application/json'); res.write(JSON.stringify(data)); } catch (ex) { console.log(ex); res.status(500); res.write('error'); } res.end(); }); app.get('/editor', (req, res) => { // define a handler for editing document try { req.DocManager = new DocManager(req, res); let { fileExt } = req.query; const user = users.getUser(req.query.userid); const userid = user.id; const { name } = user; if (fileExt) { fileExt = fileUtility.getFileExtension(fileUtility.getFileName(fileExt), true); // create demo document of a given extension const fName = req.DocManager.createDemo(!!req.query.sample, fileExt, userid, name, false); // get the redirect path const redirectPath = `${req.DocManager.getServerUrl()}/editor?mode=edit&fileName=` + `${encodeURIComponent(fName)}${req.DocManager.getCustomParams()}`; res.redirect(redirectPath); return; } const fileName = fileUtility.getFileName(req.query.fileName); const lang = req.DocManager.getLang(); const userDirectUrl = req.query.directUrl === 'true'; let actionData = 'null'; if (req.query.action) { try { actionData = JSON.stringify(JSON.parse(req.query.action)); } catch (ex) { console.log(ex); } } let type = req.query.type || ''; // type: embedded/mobile/desktop if (type === '') { type = new RegExp(configServer.get('mobileRegEx'), 'i').test(req.get('User-Agent')) ? 'mobile' : 'desktop'; } else if (type !== 'mobile' && type !== 'embedded') { type = 'desktop'; } const templatesImageUrl = req.DocManager.getTemplateImageUrl(fileUtility.getFileType(fileName)); const createUrl = req.DocManager.getCreateUrl(fileUtility.getFileType(fileName), userid, type, lang); let templates = null; if (createUrl != null) { templates = [ { image: '', title: 'Blank', url: createUrl, }, { image: templatesImageUrl, title: 'With sample content', url: `${createUrl}&sample=true`, }, ]; } const userGroup = user.group; const { reviewGroups } = user; const { commentGroups } = user; const { userInfoGroups } = user; const userRoles = user.roles; const usersInfo = []; const usersForProtect = []; if (user.id !== 'uid-0') { users.getAllUsers().forEach((userInfo) => { const u = userInfo; u.image = userInfo.avatar ? `${req.DocManager.getServerUrl()}/images/${userInfo.id}.png` : null; usersInfo.push(u); }, usersInfo); users.getUsersForProtect(user.id).forEach((userInfo) => { const u = userInfo; u.image = userInfo.avatar ? `${req.DocManager.getServerUrl()}/images/${userInfo.id}.png` : null; usersForProtect.push(u); }, usersForProtect); } fileExt = fileUtility.getFileExtension(fileName); const userAddress = req.DocManager.curUserHostAddress(); // if the file with a given name doesn't exist if (!req.DocManager.existsSync(req.DocManager.storagePath(fileName, userAddress))) { throw new Error(`File not found: ${fileName}`); // display error message } const key = req.DocManager.getKey(fileName); const url = req.DocManager.getDownloadUrl(fileName, true); const directUrl = req.DocManager.getDownloadUrl(fileName); let mode = req.query.mode || 'edit'; // mode: view/edit/review/comment/fillForms/embedded let canEdit = fileUtility.getEditExtensions().indexOf(fileExt.slice(1)) !== -1; // check if this file can be edited if (((!canEdit && mode === 'edit') || mode === 'fillForms') && fileUtility.getFillExtensions().indexOf(fileExt.slice(1)) !== -1) { mode = 'fillForms'; canEdit = true; } if (!canEdit && mode === 'edit') { mode = 'view'; } let submitForm = false; if (mode !== 'view') { submitForm = user.canSubmitForm; } if (user.goback != null) { user.goback.url = `${req.DocManager.getServerUrl()}`; } // file config data const argss = { apiUrl: siteUrl + configServer.get('apiUrl'), file: { name: fileName, ext: fileUtility.getFileExtension(fileName, true), uri: url, directUrl: !userDirectUrl ? null : directUrl, uriUser: directUrl, created: new Date().toDateString(), favorite: user.favorite != null ? user.favorite : 'null', }, editor: { type, documentType: fileUtility.getFileType(fileName), key, token: '', callbackUrl: req.DocManager.getCallback(fileName), createUrl: userid !== 'uid-0' ? createUrl : null, templates: user.templates ? templates : null, isEdit: canEdit && (mode === 'edit' || mode === 'view' || mode === 'filter' || mode === 'blockcontent'), review: canEdit && (mode === 'edit' || mode === 'review'), chat: userid !== 'uid-0', coEditing: mode === 'view' && userid === 'uid-0' ? { mode: 'strict', change: false } : null, comment: mode !== 'view' && mode !== 'fillForms' && mode !== 'embedded' && mode !== 'blockcontent', fillForms: mode !== 'view' && mode !== 'comment' && mode !== 'blockcontent', modifyFilter: mode !== 'filter', modifyContentControl: mode !== 'blockcontent', copy: !user.deniedPermissions.includes('copy'), download: !user.deniedPermissions.includes('download'), print: !user.deniedPermissions.includes('print'), mode: mode !== 'view' ? 'edit' : 'view', canBackToFolder: type !== 'embedded', curUserHostAddress: req.DocManager.curUserHostAddress(), lang, userid: userid !== 'uid-0' ? userid : null, userImage: user.avatar ? `${req.DocManager.getServerUrl()}/images/${user.id}.png` : null, name, userGroup, userRoles, reviewGroups: JSON.stringify(reviewGroups), commentGroups: JSON.stringify(commentGroups), userInfoGroups: JSON.stringify(userInfoGroups), fileChoiceUrl, submitForm, plugins: JSON.stringify(plugins), actionData, fileKey: userid !== 'uid-0' ? JSON.stringify({ fileName, userAddress: req.DocManager.curUserHostAddress() }) : null, instanceId: userid !== 'uid-0' ? req.DocManager.getInstanceId() : null, protect: !user.deniedPermissions.includes('protect'), goback: user.goback != null ? user.goback : '', close: user.close, }, dataInsertImage: { fileType: 'svg', url: `${req.DocManager.getServerUrl(true)}/images/logo.svg`, directUrl: !userDirectUrl ? null : `${req.DocManager.getServerUrl()}/images/logo.svg`, }, dataDocument: { fileType: 'docx', url: `${req.DocManager.getServerUrl(true)}/assets/document-templates/sample/sample.docx`, directUrl: !userDirectUrl ? null : `${req.DocManager.getServerUrl()}/assets/document-templates/sample/sample.docx`, }, dataSpreadsheet: { fileType: 'csv', url: `${req.DocManager.getServerUrl(true)}/csv`, directUrl: !userDirectUrl ? null : `${req.DocManager.getServerUrl()}/csv`, }, usersForMentions: user.id !== 'uid-0' ? users.getUsersForMentions(user.id) : null, usersForProtect, usersInfo, }; if (cfgSignatureEnable) { app.render('config', argss, (err, html) => { // render a config template with the parameters specified if (err) { console.log(err); } else { // sign token with given data using signature secret argss.editor.token = jwt.sign( JSON.parse(`{${html}}`), cfgSignatureSecret, { expiresIn: cfgSignatureSecretExpiresIn }, ); argss.dataInsertImage.token = jwt.sign( argss.dataInsertImage, cfgSignatureSecret, { expiresIn: cfgSignatureSecretExpiresIn }, ); argss.dataDocument.token = jwt.sign( argss.dataDocument, cfgSignatureSecret, { expiresIn: cfgSignatureSecretExpiresIn }, ); argss.dataSpreadsheet.token = jwt.sign( argss.dataSpreadsheet, cfgSignatureSecret, { expiresIn: cfgSignatureSecretExpiresIn }, ); } res.render('editor', argss); // render the editor template with the parameters specified }); } else { res.render('editor', argss); } } catch (ex) { console.log(ex); res.status(500); res.render('error', { message: `Server error: ${ex.message}` }); } }); app.post('/rename', (req, res) => { // define a handler for renaming file let { newfilename } = req.body; const origExt = req.body.ext; const curExt = fileUtility.getFileExtension(newfilename, true); if (curExt !== origExt) { newfilename += `.${origExt}`; } const { dockey } = req.body; const meta = { title: newfilename }; const result = function result(err, data, ress) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.write(JSON.stringify({ result: ress })); res.end(); }; documentService.commandRequest('meta', dockey, result, meta); }); app.post('/historyObj', (req, res) => { req.DocManager = new DocManager(req, res); const { fileName } = req.body; const { directUrl } = req.body || null; const historyObj = req.DocManager.getHistoryObject(fileName, null, directUrl); if (cfgSignatureEnable) { for (let i = 0; i < historyObj.historyData.length; i++) { // sign token with given data using signature secret historyObj.historyData[i].token = jwt.sign( historyObj.historyData[i], cfgSignatureSecret, { expiresIn: cfgSignatureSecretExpiresIn }, ); } } res.writeHead(200, { 'Content-Type': 'application/json' }); res.write(JSON.stringify(historyObj)); res.end(); }); app.get('/formats', (req, res) => { try { const formats = fileUtility.getFormats(); res.json({ formats, }); } catch (ex) { console.log(ex); // display error message in the console res.status(500); // write status parameter to the response res.render('error', { message: 'Server error' }); // render error template with the message parameter specified } }); wopiApp.registerRoutes(app); // "Not found" error with 404 status app.use((req, res, next) => { const err = new Error('Not Found'); err.status = 404; next(err); }); // render the error template with the parameters specified // eslint-disable-next-line no-unused-vars app.use((err, req, res, next) => { res.status(err.status || 500); res.render('error', { message: err.message, }); }); // save all the functions to the app module to export it later in other files module.exports = app;