Files
2025-03-28 13:42:34 +03:00

1380 lines
50 KiB
JavaScript
Executable File

/**
*
* (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;