Added biomejs as formatter and linter

This commit is contained in:
SamTV12345 2024-04-17 21:29:15 +02:00
parent 1d3e899249
commit c64c4a4073
339 changed files with 78646 additions and 66730 deletions

View file

@ -1,4 +1,4 @@
'use strict';
"use strict";
/**
* The API Handler handles all API http requests
*/
@ -19,140 +19,139 @@
* limitations under the License.
*/
import {MapArrayType} from "../types/MapType";
import { MapArrayType } from "../types/MapType";
const api = require('../db/API');
const padManager = require('../db/PadManager');
import createHTTPError from 'http-errors';
import {Http2ServerRequest, Http2ServerResponse} from "node:http2";
import {publicKeyExported} from "../security/OAuth2Provider";
import {jwtVerify} from "jose";
const api = require("../db/API");
const padManager = require("../db/PadManager");
import createHTTPError from "http-errors";
import { Http2ServerRequest, Http2ServerResponse } from "node:http2";
import { publicKeyExported } from "../security/OAuth2Provider";
import { jwtVerify } from "jose";
// a list of all functions
const version:MapArrayType<any> = {};
const version: MapArrayType<any> = {};
version['1'] = {
createGroup: [],
createGroupIfNotExistsFor: ['groupMapper'],
deleteGroup: ['groupID'],
listPads: ['groupID'],
createPad: ['padID', 'text'],
createGroupPad: ['groupID', 'padName', 'text'],
createAuthor: ['name'],
createAuthorIfNotExistsFor: ['authorMapper', 'name'],
listPadsOfAuthor: ['authorID'],
createSession: ['groupID', 'authorID', 'validUntil'],
deleteSession: ['sessionID'],
getSessionInfo: ['sessionID'],
listSessionsOfGroup: ['groupID'],
listSessionsOfAuthor: ['authorID'],
getText: ['padID', 'rev'],
setText: ['padID', 'text'],
getHTML: ['padID', 'rev'],
setHTML: ['padID', 'html'],
getRevisionsCount: ['padID'],
getLastEdited: ['padID'],
deletePad: ['padID'],
getReadOnlyID: ['padID'],
setPublicStatus: ['padID', 'publicStatus'],
getPublicStatus: ['padID'],
listAuthorsOfPad: ['padID'],
padUsersCount: ['padID'],
version["1"] = {
createGroup: [],
createGroupIfNotExistsFor: ["groupMapper"],
deleteGroup: ["groupID"],
listPads: ["groupID"],
createPad: ["padID", "text"],
createGroupPad: ["groupID", "padName", "text"],
createAuthor: ["name"],
createAuthorIfNotExistsFor: ["authorMapper", "name"],
listPadsOfAuthor: ["authorID"],
createSession: ["groupID", "authorID", "validUntil"],
deleteSession: ["sessionID"],
getSessionInfo: ["sessionID"],
listSessionsOfGroup: ["groupID"],
listSessionsOfAuthor: ["authorID"],
getText: ["padID", "rev"],
setText: ["padID", "text"],
getHTML: ["padID", "rev"],
setHTML: ["padID", "html"],
getRevisionsCount: ["padID"],
getLastEdited: ["padID"],
deletePad: ["padID"],
getReadOnlyID: ["padID"],
setPublicStatus: ["padID", "publicStatus"],
getPublicStatus: ["padID"],
listAuthorsOfPad: ["padID"],
padUsersCount: ["padID"],
};
version['1.1'] = {
...version['1'],
getAuthorName: ['authorID'],
padUsers: ['padID'],
sendClientsMessage: ['padID', 'msg'],
listAllGroups: [],
version["1.1"] = {
...version["1"],
getAuthorName: ["authorID"],
padUsers: ["padID"],
sendClientsMessage: ["padID", "msg"],
listAllGroups: [],
};
version['1.2'] = {
...version['1.1'],
checkToken: [],
version["1.2"] = {
...version["1.1"],
checkToken: [],
};
version['1.2.1'] = {
...version['1.2'],
listAllPads: [],
version["1.2.1"] = {
...version["1.2"],
listAllPads: [],
};
version['1.2.7'] = {
...version['1.2.1'],
createDiffHTML: ['padID', 'startRev', 'endRev'],
getChatHistory: ['padID', 'start', 'end'],
getChatHead: ['padID'],
version["1.2.7"] = {
...version["1.2.1"],
createDiffHTML: ["padID", "startRev", "endRev"],
getChatHistory: ["padID", "start", "end"],
getChatHead: ["padID"],
};
version['1.2.8'] = {
...version['1.2.7'],
getAttributePool: ['padID'],
getRevisionChangeset: ['padID', 'rev'],
version["1.2.8"] = {
...version["1.2.7"],
getAttributePool: ["padID"],
getRevisionChangeset: ["padID", "rev"],
};
version['1.2.9'] = {
...version['1.2.8'],
copyPad: ['sourceID', 'destinationID', 'force'],
movePad: ['sourceID', 'destinationID', 'force'],
version["1.2.9"] = {
...version["1.2.8"],
copyPad: ["sourceID", "destinationID", "force"],
movePad: ["sourceID", "destinationID", "force"],
};
version['1.2.10'] = {
...version['1.2.9'],
getPadID: ['roID'],
version["1.2.10"] = {
...version["1.2.9"],
getPadID: ["roID"],
};
version['1.2.11'] = {
...version['1.2.10'],
getSavedRevisionsCount: ['padID'],
listSavedRevisions: ['padID'],
saveRevision: ['padID', 'rev'],
restoreRevision: ['padID', 'rev'],
version["1.2.11"] = {
...version["1.2.10"],
getSavedRevisionsCount: ["padID"],
listSavedRevisions: ["padID"],
saveRevision: ["padID", "rev"],
restoreRevision: ["padID", "rev"],
};
version['1.2.12'] = {
...version['1.2.11'],
appendChatMessage: ['padID', 'text', 'authorID', 'time'],
version["1.2.12"] = {
...version["1.2.11"],
appendChatMessage: ["padID", "text", "authorID", "time"],
};
version['1.2.13'] = {
...version['1.2.12'],
appendText: ['padID', 'text'],
version["1.2.13"] = {
...version["1.2.12"],
appendText: ["padID", "text"],
};
version['1.2.14'] = {
...version['1.2.13'],
getStats: [],
version["1.2.14"] = {
...version["1.2.13"],
getStats: [],
};
version['1.2.15'] = {
...version['1.2.14'],
copyPadWithoutHistory: ['sourceID', 'destinationID', 'force'],
version["1.2.15"] = {
...version["1.2.14"],
copyPadWithoutHistory: ["sourceID", "destinationID", "force"],
};
version['1.3.0'] = {
...version['1.2.15'],
appendText: ['padID', 'text', 'authorId'],
copyPadWithoutHistory: ['sourceID', 'destinationID', 'force', 'authorId'],
createGroupPad: ['groupID', 'padName', 'text', 'authorId'],
createPad: ['padID', 'text', 'authorId'],
restoreRevision: ['padID', 'rev', 'authorId'],
setHTML: ['padID', 'html', 'authorId'],
setText: ['padID', 'text', 'authorId'],
version["1.3.0"] = {
...version["1.2.15"],
appendText: ["padID", "text", "authorId"],
copyPadWithoutHistory: ["sourceID", "destinationID", "force", "authorId"],
createGroupPad: ["groupID", "padName", "text", "authorId"],
createPad: ["padID", "text", "authorId"],
restoreRevision: ["padID", "rev", "authorId"],
setHTML: ["padID", "html", "authorId"],
setText: ["padID", "text", "authorId"],
};
// set the latest available API version here
exports.latestApiVersion = '1.3.0';
exports.latestApiVersion = "1.3.0";
// exports the versions so it can be used by the new Swagger endpoint
exports.version = version;
type APIFields = {
api_key: string;
padID: string;
padName: string;
}
api_key: string;
padID: string;
padName: string;
};
/**
* Handles an HTTP API call
@ -162,46 +161,54 @@ type APIFields = {
* @param req express request object
* @param res express response object
*/
exports.handle = async function (apiVersion: string, functionName: string, fields: APIFields, req: Http2ServerRequest, res: Http2ServerResponse) {
// say goodbye if this is an unknown API version
if (!(apiVersion in version)) {
throw new createHTTPError.NotFound('no such api version');
}
exports.handle = async function (
apiVersion: string,
functionName: string,
fields: APIFields,
req: Http2ServerRequest,
res: Http2ServerResponse,
) {
// say goodbye if this is an unknown API version
if (!(apiVersion in version)) {
throw new createHTTPError.NotFound("no such api version");
}
// say goodbye if this is an unknown function
if (!(functionName in version[apiVersion])) {
throw new createHTTPError.NotFound('no such function');
}
// say goodbye if this is an unknown function
if (!(functionName in version[apiVersion])) {
throw new createHTTPError.NotFound("no such function");
}
if(!req.headers.authorization) {
throw new createHTTPError.Unauthorized('no or wrong API Key');
}
if (!req.headers.authorization) {
throw new createHTTPError.Unauthorized("no or wrong API Key");
}
try {
await jwtVerify(req.headers.authorization!.replace("Bearer ", ""), publicKeyExported!, {algorithms: ['RS256'],
requiredClaims: ["admin"]})
try {
await jwtVerify(
req.headers.authorization!.replace("Bearer ", ""),
publicKeyExported!,
{ algorithms: ["RS256"], requiredClaims: ["admin"] },
);
} catch (e) {
throw new createHTTPError.Unauthorized("no or wrong API Key");
}
} catch (e) {
throw new createHTTPError.Unauthorized('no or wrong API Key');
}
// sanitize any padIDs before continuing
if (fields.padID) {
fields.padID = await padManager.sanitizePadId(fields.padID);
}
// there was an 'else' here before - removed it to ensure
// that this sanitize step can't be circumvented by forcing
// the first branch to be taken
if (fields.padName) {
fields.padName = await padManager.sanitizePadId(fields.padName);
}
// put the function parameters in an array
// @ts-ignore
const functionParams = version[apiVersion][functionName].map(
(field) => fields[field],
);
// sanitize any padIDs before continuing
if (fields.padID) {
fields.padID = await padManager.sanitizePadId(fields.padID);
}
// there was an 'else' here before - removed it to ensure
// that this sanitize step can't be circumvented by forcing
// the first branch to be taken
if (fields.padName) {
fields.padName = await padManager.sanitizePadId(fields.padName);
}
// put the function parameters in an array
// @ts-ignore
const functionParams = version[apiVersion][functionName].map((field) => fields[field]);
// call the api function
return api[functionName].apply(this, functionParams);
// call the api function
return api[functionName].apply(this, functionParams);
};

View file

@ -1,4 +1,4 @@
'use strict';
"use strict";
/**
* Handles the export requests
*/
@ -20,15 +20,15 @@
* limitations under the License.
*/
const exporthtml = require('../utils/ExportHtml');
const exporttxt = require('../utils/ExportTxt');
const exportEtherpad = require('../utils/ExportEtherpad');
import fs from 'fs';
const settings = require('../utils/Settings');
import os from 'os';
const hooks = require('../../static/js/pluginfw/hooks');
import util from 'util';
const { checkValidRev } = require('../utils/checkValidRev');
const exporthtml = require("../utils/ExportHtml");
const exporttxt = require("../utils/ExportTxt");
const exportEtherpad = require("../utils/ExportEtherpad");
import fs from "fs";
const settings = require("../utils/Settings");
import os from "os";
const hooks = require("../../static/js/pluginfw/hooks");
import util from "util";
const { checkValidRev } = require("../utils/checkValidRev");
const fsp_writeFile = util.promisify(fs.writeFile);
const fsp_unlink = util.promisify(fs.unlink);
@ -43,84 +43,101 @@ const tempDirectory = os.tmpdir();
* @param {String} readOnlyId the read only id of the pad to export
* @param {String} type the type to export
*/
exports.doExport = async (req: any, res: any, padId: string, readOnlyId: string, type:string) => {
// avoid naming the read-only file as the original pad's id
let fileName = readOnlyId ? readOnlyId : padId;
exports.doExport = async (
req: any,
res: any,
padId: string,
readOnlyId: string,
type: string,
) => {
// avoid naming the read-only file as the original pad's id
let fileName = readOnlyId ? readOnlyId : padId;
// allow fileName to be overwritten by a hook, the type type is kept static for security reasons
const hookFileName = await hooks.aCallFirst('exportFileName', padId);
// allow fileName to be overwritten by a hook, the type type is kept static for security reasons
const hookFileName = await hooks.aCallFirst("exportFileName", padId);
// if fileName is set then set it to the padId, note that fileName is returned as an array.
if (hookFileName.length) {
fileName = hookFileName;
}
// if fileName is set then set it to the padId, note that fileName is returned as an array.
if (hookFileName.length) {
fileName = hookFileName;
}
// tell the browser that this is a downloadable file
res.attachment(`${fileName}.${type}`);
// tell the browser that this is a downloadable file
res.attachment(`${fileName}.${type}`);
if (req.params.rev !== undefined) {
// ensure revision is a number
// modify req, as we use it in a later call to exportConvert
req.params.rev = checkValidRev(req.params.rev);
}
if (req.params.rev !== undefined) {
// ensure revision is a number
// modify req, as we use it in a later call to exportConvert
req.params.rev = checkValidRev(req.params.rev);
}
// if this is a plain text export, we can do this directly
// We have to over engineer this because tabs are stored as attributes and not plain text
if (type === 'etherpad') {
const pad = await exportEtherpad.getPadRaw(padId, readOnlyId);
res.send(pad);
} else if (type === 'txt') {
const txt = await exporttxt.getPadTXTDocument(padId, req.params.rev);
res.send(txt);
} else {
// render the html document
let html = await exporthtml.getPadHTMLDocument(padId, req.params.rev, readOnlyId);
// if this is a plain text export, we can do this directly
// We have to over engineer this because tabs are stored as attributes and not plain text
if (type === "etherpad") {
const pad = await exportEtherpad.getPadRaw(padId, readOnlyId);
res.send(pad);
} else if (type === "txt") {
const txt = await exporttxt.getPadTXTDocument(padId, req.params.rev);
res.send(txt);
} else {
// render the html document
let html = await exporthtml.getPadHTMLDocument(
padId,
req.params.rev,
readOnlyId,
);
// decide what to do with the html export
// decide what to do with the html export
// if this is a html export, we can send this from here directly
if (type === 'html') {
// do any final changes the plugin might want to make
const newHTML = await hooks.aCallFirst('exportHTMLSend', html);
if (newHTML.length) html = newHTML;
res.send(html);
return;
}
// if this is a html export, we can send this from here directly
if (type === "html") {
// do any final changes the plugin might want to make
const newHTML = await hooks.aCallFirst("exportHTMLSend", html);
if (newHTML.length) html = newHTML;
res.send(html);
return;
}
// else write the html export to a file
const randNum = Math.floor(Math.random() * 0xFFFFFFFF);
const srcFile = `${tempDirectory}/etherpad_export_${randNum}.html`;
await fsp_writeFile(srcFile, html);
// else write the html export to a file
const randNum = Math.floor(Math.random() * 0xffffffff);
const srcFile = `${tempDirectory}/etherpad_export_${randNum}.html`;
await fsp_writeFile(srcFile, html);
// ensure html can be collected by the garbage collector
html = null;
// ensure html can be collected by the garbage collector
html = null;
// send the convert job to the converter (abiword, libreoffice, ..)
const destFile = `${tempDirectory}/etherpad_export_${randNum}.${type}`;
// send the convert job to the converter (abiword, libreoffice, ..)
const destFile = `${tempDirectory}/etherpad_export_${randNum}.${type}`;
// Allow plugins to overwrite the convert in export process
const result = await hooks.aCallAll('exportConvert', {srcFile, destFile, req, res});
if (result.length > 0) {
// console.log("export handled by plugin", destFile);
} else {
const converter =
settings.soffice != null ? require('../utils/LibreOffice')
: settings.abiword != null ? require('../utils/Abiword')
: null;
await converter.convertFile(srcFile, destFile, type);
}
// Allow plugins to overwrite the convert in export process
const result = await hooks.aCallAll("exportConvert", {
srcFile,
destFile,
req,
res,
});
if (result.length > 0) {
// console.log("export handled by plugin", destFile);
} else {
const converter =
settings.soffice != null
? require("../utils/LibreOffice")
: settings.abiword != null
? require("../utils/Abiword")
: null;
await converter.convertFile(srcFile, destFile, type);
}
// send the file
await res.sendFile(destFile, null);
// send the file
await res.sendFile(destFile, null);
// clean up temporary files
await fsp_unlink(srcFile);
// clean up temporary files
await fsp_unlink(srcFile);
// 100ms delay to accommodate for slow windows fs
if (os.type().indexOf('Windows') > -1) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
// 100ms delay to accommodate for slow windows fs
if (os.type().indexOf("Windows") > -1) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
await fsp_unlink(destFile);
}
await fsp_unlink(destFile);
}
};

View file

@ -1,4 +1,4 @@
'use strict';
"use strict";
/**
* Handles the import requests
*/
@ -21,53 +21,53 @@
* limitations under the License.
*/
const padManager = require('../db/PadManager');
const padMessageHandler = require('./PadMessageHandler');
import {promises as fs} from 'fs';
import path from 'path';
const settings = require('../utils/Settings');
const {Formidable} = require('formidable');
import os from 'os';
const importHtml = require('../utils/ImportHtml');
const importEtherpad = require('../utils/ImportEtherpad');
import log4js from 'log4js';
const hooks = require('../../static/js/pluginfw/hooks.js');
const padManager = require("../db/PadManager");
const padMessageHandler = require("./PadMessageHandler");
import { promises as fs } from "fs";
import path from "path";
const settings = require("../utils/Settings");
const { Formidable } = require("formidable");
import os from "os";
const importHtml = require("../utils/ImportHtml");
const importEtherpad = require("../utils/ImportEtherpad");
import log4js from "log4js";
const hooks = require("../../static/js/pluginfw/hooks.js");
const logger = log4js.getLogger('ImportHandler');
const logger = log4js.getLogger("ImportHandler");
// `status` must be a string supported by `importErrorMessage()` in `src/static/js/pad_impexp.js`.
class ImportError extends Error {
status: string;
constructor(status: string, ...args:any) {
super(...args);
if (Error.captureStackTrace) Error.captureStackTrace(this, ImportError);
this.name = 'ImportError';
this.status = status;
const msg = this.message == null ? '' : String(this.message);
if (status !== '') this.message = msg === '' ? status : `${status}: ${msg}`;
}
status: string;
constructor(status: string, ...args: any) {
super(...args);
if (Error.captureStackTrace) Error.captureStackTrace(this, ImportError);
this.name = "ImportError";
this.status = status;
const msg = this.message == null ? "" : String(this.message);
if (status !== "") this.message = msg === "" ? status : `${status}: ${msg}`;
}
}
const rm = async (path: string) => {
try {
await fs.unlink(path);
} catch (err:any) {
if (err.code !== 'ENOENT') throw err;
}
try {
await fs.unlink(path);
} catch (err: any) {
if (err.code !== "ENOENT") throw err;
}
};
let converter:any = null;
let exportExtension = 'htm';
let converter: any = null;
let exportExtension = "htm";
// load abiword only if it is enabled and if soffice is disabled
if (settings.abiword != null && settings.soffice == null) {
converter = require('../utils/Abiword');
converter = require("../utils/Abiword");
}
// load soffice only if it is enabled
if (settings.soffice != null) {
converter = require('../utils/LibreOffice');
exportExtension = 'html';
converter = require("../utils/LibreOffice");
exportExtension = "html";
}
const tmpDirectory = os.tmpdir();
@ -79,163 +79,193 @@ const tmpDirectory = os.tmpdir();
* @param {String} padId the pad id to export
* @param {String} authorId the author id to use for the import
*/
const doImport = async (req:any, res:any, padId:string, authorId:string) => {
// pipe to a file
// convert file to html via abiword or soffice
// set html in the pad
const randNum = Math.floor(Math.random() * 0xFFFFFFFF);
const doImport = async (
req: any,
res: any,
padId: string,
authorId: string,
) => {
// pipe to a file
// convert file to html via abiword or soffice
// set html in the pad
const randNum = Math.floor(Math.random() * 0xffffffff);
// setting flag for whether to use converter or not
let useConverter = (converter != null);
// setting flag for whether to use converter or not
let useConverter = converter != null;
const form = new Formidable({
keepExtensions: true,
uploadDir: tmpDirectory,
maxFileSize: settings.importMaxFileSize,
});
const form = new Formidable({
keepExtensions: true,
uploadDir: tmpDirectory,
maxFileSize: settings.importMaxFileSize,
});
let srcFile;
let files;
let fields;
try {
[fields, files] = await form.parse(req);
} catch (err:any) {
logger.warn(`Import failed due to form error: ${err.stack || err}`);
if (err.code === Formidable.formidableErrors.biggerThanMaxFileSize) {
throw new ImportError('maxFileSize');
}
throw new ImportError('uploadFailed');
}
if (!files.file) {
logger.warn('Import failed because form had no file');
throw new ImportError('uploadFailed');
} else {
srcFile = files.file[0].filepath;
}
let srcFile;
let files;
let fields;
try {
[fields, files] = await form.parse(req);
} catch (err: any) {
logger.warn(`Import failed due to form error: ${err.stack || err}`);
if (err.code === Formidable.formidableErrors.biggerThanMaxFileSize) {
throw new ImportError("maxFileSize");
}
throw new ImportError("uploadFailed");
}
if (!files.file) {
logger.warn("Import failed because form had no file");
throw new ImportError("uploadFailed");
} else {
srcFile = files.file[0].filepath;
}
// ensure this is a file ending we know, else we change the file ending to .txt
// this allows us to accept source code files like .c or .java
const fileEnding = path.extname(files.file[0].originalFilename).toLowerCase();
const knownFileEndings =
['.txt', '.doc', '.docx', '.pdf', '.odt', '.html', '.htm', '.etherpad', '.rtf'];
const fileEndingUnknown = (knownFileEndings.indexOf(fileEnding) < 0);
// ensure this is a file ending we know, else we change the file ending to .txt
// this allows us to accept source code files like .c or .java
const fileEnding = path.extname(files.file[0].originalFilename).toLowerCase();
const knownFileEndings = [
".txt",
".doc",
".docx",
".pdf",
".odt",
".html",
".htm",
".etherpad",
".rtf",
];
const fileEndingUnknown = knownFileEndings.indexOf(fileEnding) < 0;
if (fileEndingUnknown) {
// the file ending is not known
if (fileEndingUnknown) {
// the file ending is not known
if (settings.allowUnknownFileEnds === true) {
// we need to rename this file with a .txt ending
const oldSrcFile = srcFile;
if (settings.allowUnknownFileEnds === true) {
// we need to rename this file with a .txt ending
const oldSrcFile = srcFile;
srcFile = path.join(path.dirname(srcFile), `${path.basename(srcFile, fileEnding)}.txt`);
await fs.rename(oldSrcFile, srcFile);
} else {
logger.warn(`Not allowing unknown file type to be imported: ${fileEnding}`);
throw new ImportError('uploadFailed');
}
}
srcFile = path.join(
path.dirname(srcFile),
`${path.basename(srcFile, fileEnding)}.txt`,
);
await fs.rename(oldSrcFile, srcFile);
} else {
logger.warn(
`Not allowing unknown file type to be imported: ${fileEnding}`,
);
throw new ImportError("uploadFailed");
}
}
const destFile = path.join(tmpDirectory, `etherpad_import_${randNum}.${exportExtension}`);
const context = {srcFile, destFile, fileEnding, padId, ImportError};
const importHandledByPlugin = (await hooks.aCallAll('import', context)).some((x:string) => x);
const fileIsEtherpad = (fileEnding === '.etherpad');
const fileIsHTML = (fileEnding === '.html' || fileEnding === '.htm');
const fileIsTXT = (fileEnding === '.txt');
const destFile = path.join(
tmpDirectory,
`etherpad_import_${randNum}.${exportExtension}`,
);
const context = { srcFile, destFile, fileEnding, padId, ImportError };
const importHandledByPlugin = (await hooks.aCallAll("import", context)).some(
(x: string) => x,
);
const fileIsEtherpad = fileEnding === ".etherpad";
const fileIsHTML = fileEnding === ".html" || fileEnding === ".htm";
const fileIsTXT = fileEnding === ".txt";
let directDatabaseAccess = false;
if (fileIsEtherpad) {
// Use '\n' to avoid the default pad text if the pad doesn't yet exist.
const pad = await padManager.getPad(padId, '\n', authorId);
const headCount = pad.head;
if (headCount >= 10) {
logger.warn('Aborting direct database import attempt of a pad that already has content');
throw new ImportError('padHasData');
}
const text = await fs.readFile(srcFile, 'utf8');
directDatabaseAccess = true;
await importEtherpad.setPadRaw(padId, text, authorId);
}
let directDatabaseAccess = false;
if (fileIsEtherpad) {
// Use '\n' to avoid the default pad text if the pad doesn't yet exist.
const pad = await padManager.getPad(padId, "\n", authorId);
const headCount = pad.head;
if (headCount >= 10) {
logger.warn(
"Aborting direct database import attempt of a pad that already has content",
);
throw new ImportError("padHasData");
}
const text = await fs.readFile(srcFile, "utf8");
directDatabaseAccess = true;
await importEtherpad.setPadRaw(padId, text, authorId);
}
// convert file to html if necessary
if (!importHandledByPlugin && !directDatabaseAccess) {
if (fileIsTXT) {
// Don't use converter for text files
useConverter = false;
}
// convert file to html if necessary
if (!importHandledByPlugin && !directDatabaseAccess) {
if (fileIsTXT) {
// Don't use converter for text files
useConverter = false;
}
// See https://github.com/ether/etherpad-lite/issues/2572
if (fileIsHTML || !useConverter) {
// if no converter only rename
await fs.rename(srcFile, destFile);
} else {
try {
await converter.convertFile(srcFile, destFile, exportExtension);
} catch (err:any) {
logger.warn(`Converting Error: ${err.stack || err}`);
throw new ImportError('convertFailed');
}
}
}
// See https://github.com/ether/etherpad-lite/issues/2572
if (fileIsHTML || !useConverter) {
// if no converter only rename
await fs.rename(srcFile, destFile);
} else {
try {
await converter.convertFile(srcFile, destFile, exportExtension);
} catch (err: any) {
logger.warn(`Converting Error: ${err.stack || err}`);
throw new ImportError("convertFailed");
}
}
}
if (!useConverter && !directDatabaseAccess) {
// Read the file with no encoding for raw buffer access.
const buf = await fs.readFile(destFile);
if (!useConverter && !directDatabaseAccess) {
// Read the file with no encoding for raw buffer access.
const buf = await fs.readFile(destFile);
// Check if there are only ascii chars in the uploaded file
const isAscii = !Array.prototype.some.call(buf, (c) => (c > 240));
// Check if there are only ascii chars in the uploaded file
const isAscii = !Array.prototype.some.call(buf, (c) => c > 240);
if (!isAscii) {
logger.warn('Attempt to import non-ASCII file');
throw new ImportError('uploadFailed');
}
}
if (!isAscii) {
logger.warn("Attempt to import non-ASCII file");
throw new ImportError("uploadFailed");
}
}
// Use '\n' to avoid the default pad text if the pad doesn't yet exist.
let pad = await padManager.getPad(padId, '\n', authorId);
// Use '\n' to avoid the default pad text if the pad doesn't yet exist.
let pad = await padManager.getPad(padId, "\n", authorId);
// read the text
let text;
// read the text
let text;
if (!directDatabaseAccess) {
text = await fs.readFile(destFile, 'utf8');
if (!directDatabaseAccess) {
text = await fs.readFile(destFile, "utf8");
// node on windows has a delay on releasing of the file lock.
// We add a 100ms delay to work around this
if (os.type().indexOf('Windows') > -1) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
// node on windows has a delay on releasing of the file lock.
// We add a 100ms delay to work around this
if (os.type().indexOf("Windows") > -1) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
// change text of the pad and broadcast the changeset
if (!directDatabaseAccess) {
if (importHandledByPlugin || useConverter || fileIsHTML) {
try {
await importHtml.setPadHTML(pad, text, authorId);
} catch (err:any) {
logger.warn(`Error importing, possibly caused by malformed HTML: ${err.stack || err}`);
}
} else {
await pad.setText(text, authorId);
}
}
// change text of the pad and broadcast the changeset
if (!directDatabaseAccess) {
if (importHandledByPlugin || useConverter || fileIsHTML) {
try {
await importHtml.setPadHTML(pad, text, authorId);
} catch (err: any) {
logger.warn(
`Error importing, possibly caused by malformed HTML: ${
err.stack || err
}`,
);
}
} else {
await pad.setText(text, authorId);
}
}
// Load the Pad into memory then broadcast updates to all clients
padManager.unloadPad(padId);
pad = await padManager.getPad(padId, '\n', authorId);
padManager.unloadPad(padId);
// Load the Pad into memory then broadcast updates to all clients
padManager.unloadPad(padId);
pad = await padManager.getPad(padId, "\n", authorId);
padManager.unloadPad(padId);
// Direct database access means a pad user should reload the pad and not attempt to receive
// updated pad data.
if (directDatabaseAccess) return true;
// Direct database access means a pad user should reload the pad and not attempt to receive
// updated pad data.
if (directDatabaseAccess) return true;
// tell clients to update
await padMessageHandler.updatePadClients(pad);
// tell clients to update
await padMessageHandler.updatePadClients(pad);
// clean up temporary files
rm(srcFile);
rm(destFile);
// clean up temporary files
rm(srcFile);
rm(destFile);
return false;
return false;
};
/**
@ -246,19 +276,27 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
* @param {String} authorId the author id to use for the import
* @return {Promise<void>} a promise
*/
exports.doImport = async (req:any, res:any, padId:string, authorId:string = '') => {
let httpStatus = 200;
let code = 0;
let message = 'ok';
let directDatabaseAccess;
try {
directDatabaseAccess = await doImport(req, res, padId, authorId);
} catch (err:any) {
const known = err instanceof ImportError && err.status;
if (!known) logger.error(`Internal error during import: ${err.stack || err}`);
httpStatus = known ? 400 : 500;
code = known ? 1 : 2;
message = known ? err.status : 'internalError';
}
res.status(httpStatus).json({code, message, data: {directDatabaseAccess}});
exports.doImport = async (
req: any,
res: any,
padId: string,
authorId: string = "",
) => {
let httpStatus = 200;
let code = 0;
let message = "ok";
let directDatabaseAccess;
try {
directDatabaseAccess = await doImport(req, res, padId, authorId);
} catch (err: any) {
const known = err instanceof ImportError && err.status;
if (!known)
logger.error(`Internal error during import: ${err.stack || err}`);
httpStatus = known ? 400 : 500;
code = known ? 1 : 2;
message = known ? err.status : "internalError";
}
res
.status(httpStatus)
.json({ code, message, data: { directDatabaseAccess } });
};

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
'use strict';
"use strict";
/**
* This is the Socket.IO Router. It routes the Messages between the
* components of the Server. The components are at the moment: pad and timeslider
@ -20,87 +20,98 @@
* limitations under the License.
*/
import {MapArrayType} from "../types/MapType";
import {SocketModule} from "../types/SocketModule";
const log4js = require('log4js');
const settings = require('../utils/Settings');
const stats = require('../../node/stats')
import { MapArrayType } from "../types/MapType";
import { SocketModule } from "../types/SocketModule";
const log4js = require("log4js");
const settings = require("../utils/Settings");
const stats = require("../../node/stats");
const logger = log4js.getLogger('socket.io');
const logger = log4js.getLogger("socket.io");
/**
* Saves all components
* key is the component name
* value is the component module
*/
const components:MapArrayType<any> = {};
const components: MapArrayType<any> = {};
let io:any;
let io: any;
/** adds a component
* @param {string} moduleName
* @param {Module} module
*/
exports.addComponent = (moduleName: string, module: SocketModule) => {
if (module == null) return exports.deleteComponent(moduleName);
components[moduleName] = module;
module.setSocketIO(io);
if (module == null) return exports.deleteComponent(moduleName);
components[moduleName] = module;
module.setSocketIO(io);
};
/**
* removes a component
* @param {Module} moduleName
*/
exports.deleteComponent = (moduleName: string) => { delete components[moduleName]; };
exports.deleteComponent = (moduleName: string) => {
delete components[moduleName];
};
/**
* sets the socket.io and adds event functions for routing
* @param {Object} _io the socket.io instance
*/
exports.setSocketIO = (_io:any) => {
io = _io;
exports.setSocketIO = (_io: any) => {
io = _io;
io.sockets.on('connection', (socket:any) => {
const ip = settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip;
logger.debug(`${socket.id} connected from IP ${ip}`);
io.sockets.on("connection", (socket: any) => {
const ip = settings.disableIPlogging ? "ANONYMOUS" : socket.request.ip;
logger.debug(`${socket.id} connected from IP ${ip}`);
// wrap the original send function to log the messages
socket._send = socket.send;
socket.send = (message: string) => {
logger.debug(`to ${socket.id}: ${JSON.stringify(message)}`);
socket._send(message);
};
// wrap the original send function to log the messages
socket._send = socket.send;
socket.send = (message: string) => {
logger.debug(`to ${socket.id}: ${JSON.stringify(message)}`);
socket._send(message);
};
// tell all components about this connect
for (const i of Object.keys(components)) {
components[i].handleConnect(socket);
}
// tell all components about this connect
for (const i of Object.keys(components)) {
components[i].handleConnect(socket);
}
socket.on('message', (message: any, ack: any = () => {}) => (async () => {
if (!message.component || !components[message.component]) {
throw new Error(`unknown message component: ${message.component}`);
}
logger.debug(`from ${socket.id}:`, message);
return await components[message.component].handleMessage(socket, message);
})().then(
(val) => ack(null, val),
(err) => {
logger.error(
`Error handling ${message.component} message from ${socket.id}: ${err.stack || err}`);
ack({name: err.name, message: err.message}); // socket.io can't handle Error objects.
}));
socket.on("message", (message: any, ack: any = () => {}) =>
(async () => {
if (!message.component || !components[message.component]) {
throw new Error(`unknown message component: ${message.component}`);
}
logger.debug(`from ${socket.id}:`, message);
return await components[message.component].handleMessage(
socket,
message,
);
})().then(
(val) => ack(null, val),
(err) => {
logger.error(
`Error handling ${message.component} message from ${socket.id}: ${
err.stack || err
}`,
);
ack({ name: err.name, message: err.message }); // socket.io can't handle Error objects.
},
),
);
socket.on('disconnect', (reason: string) => {
logger.debug(`${socket.id} disconnected: ${reason}`);
// store the lastDisconnect as a timestamp, this is useful if you want to know
// when the last user disconnected. If your activePads is 0 and totalUsers is 0
// you can say, if there has been no active pads or active users for 10 minutes
// this instance can be brought out of a scaling cluster.
stats.gauge('lastDisconnect', () => Date.now());
// tell all components about this disconnect
for (const i of Object.keys(components)) {
components[i].handleDisconnect(socket);
}
});
});
socket.on("disconnect", (reason: string) => {
logger.debug(`${socket.id} disconnected: ${reason}`);
// store the lastDisconnect as a timestamp, this is useful if you want to know
// when the last user disconnected. If your activePads is 0 and totalUsers is 0
// you can say, if there has been no active pads or active users for 10 minutes
// this instance can be brought out of a scaling cluster.
stats.gauge("lastDisconnect", () => Date.now());
// tell all components about this disconnect
for (const i of Object.keys(components)) {
components[i].handleDisconnect(socket);
}
});
});
};