mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-25 01:46:14 -04:00
Added biomejs as formatter and linter
This commit is contained in:
parent
1d3e899249
commit
c64c4a4073
339 changed files with 78646 additions and 66730 deletions
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue