Added typescript support for most backend files.

pull/5750/merge^2
SamTV12345 2023-06-22 22:54:02 +02:00
parent d6abab6c74
commit 331cf3d79f
No known key found for this signature in database
GPG Key ID: E63EEC7466038043
46 changed files with 19975 additions and 7995 deletions

1
src/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist

View File

@ -19,59 +19,72 @@
* limitations under the License.
*/
const Changeset = require('../../static/js/Changeset');
const ChatMessage = require('../../static/js/ChatMessage');
const CustomError = require('../utils/customError');
const padManager = require('./PadManager');
const padMessageHandler = require('../handler/PadMessageHandler');
const readOnlyManager = require('./ReadOnlyManager');
const groupManager = require('./GroupManager');
const authorManager = require('./AuthorManager');
const sessionManager = require('./SessionManager');
const exportHtml = require('../utils/ExportHtml');
const exportTxt = require('../utils/ExportTxt');
const importHtml = require('../utils/ImportHtml');
import Changeset from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage';
import CustomError from '../utils/customError';
import {doesPadExist, getPad, isValidPadId, listAllPads} from './PadManager';
import {
handleCustomMessage,
sendChatMessageToPadClients,
sessioninfos,
updatePadClients
} from '../handler/PadMessageHandler';
import {getPadId, getReadOnlyId} from './ReadOnlyManager';
import {
createGroup,
createGroupIfNotExistsFor,
createGroupPad,
deleteGroup,
listAllGroups,
listPads
} from './GroupManager';
import {createAuthor, createAuthorIfNotExistsFor, getAuthorName, listPadsOfAuthor} from './AuthorManager';
import {} from './SessionManager';
import exportHtml from '../utils/ExportHtml';
import exportTxt from '../utils/ExportTxt';
import importHtml from '../utils/ImportHtml';
const cleanText = require('./Pad').cleanText;
const PadDiff = require('../utils/padDiff');
import PadDiff from '../utils/padDiff';
/* ********************
* GROUP FUNCTIONS ****
******************** */
exports.listAllGroups = groupManager.listAllGroups;
exports.createGroup = groupManager.createGroup;
exports.createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor;
exports.deleteGroup = groupManager.deleteGroup;
exports.listPads = groupManager.listPads;
exports.createGroupPad = groupManager.createGroupPad;
/*
exports.listAllGroups = listAllGroups;
exports.createGroup = createGroup;
exports.createGroupIfNotExistsFor = createGroupIfNotExistsFor;
exports.deleteGroup = deleteGroup;
exports.listPads = listPads;
exports.createGroupPad = createGroupPad;
*/
/* ********************
* PADLIST FUNCTION ***
******************** */
/*
exports.listAllPads = padManager.listAllPads;
*/
/* ********************
* AUTHOR FUNCTIONS ***
******************** */
exports.createAuthor = authorManager.createAuthor;
exports.createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor;
exports.getAuthorName = authorManager.getAuthorName;
exports.listPadsOfAuthor = authorManager.listPadsOfAuthor;
/*
exports.createAuthor = createAuthor;
exports.createAuthorIfNotExistsFor = createAuthorIfNotExistsFor;
exports.getAuthorName = getAuthorName;
exports.listPadsOfAuthor = listPadsOfAuthor;
exports.padUsers = padMessageHandler.padUsers;
exports.padUsersCount = padMessageHandler.padUsersCount;
*/
/* ********************
* SESSION FUNCTIONS **
******************** */
/*
exports.createSession = sessionManager.createSession;
exports.deleteSession = sessionManager.deleteSession;
exports.getSessionInfo = sessionManager.getSessionInfo;
exports.listSessionsOfGroup = sessionManager.listSessionsOfGroup;
exports.listSessionsOfAuthor = sessionManager.listSessionsOfAuthor;
*/
/* ***********************
* PAD CONTENT FUNCTIONS *
*********************** */
@ -103,7 +116,7 @@ Example returns:
}
*/
exports.getAttributePool = async (padID) => {
export const getAttributePool = async (padID: string) => {
const pad = await getPadSafe(padID, true);
return {pool: pad.pool};
};
@ -121,7 +134,7 @@ Example returns:
}
*/
exports.getRevisionChangeset = async (padID, rev) => {
export const getRevisionChangeset = async (padID, rev) => {
// try to parse the revision number
if (rev !== undefined) {
rev = checkValidRev(rev);
@ -154,7 +167,7 @@ Example returns:
{code: 0, message:"ok", data: {text:"Welcome Text"}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getText = async (padID, rev) => {
export const getText = async (padID, rev) => {
// try to parse the revision number
if (rev !== undefined) {
rev = checkValidRev(rev);
@ -192,7 +205,7 @@ Example returns:
{code: 1, message:"padID does not exist", data: null}
{code: 1, message:"text too long", data: null}
*/
exports.setText = async (padID, text, authorId = '') => {
export const setText = async (padID, text, authorId = '') => {
// text is required
if (typeof text !== 'string') {
throw new CustomError('text is not a string', 'apierror');
@ -202,7 +215,7 @@ exports.setText = async (padID, text, authorId = '') => {
const pad = await getPadSafe(padID, true);
await pad.setText(text, authorId);
await padMessageHandler.updatePadClients(pad);
await updatePadClients(pad);
};
/**
@ -214,7 +227,7 @@ Example returns:
{code: 1, message:"padID does not exist", data: null}
{code: 1, message:"text too long", data: null}
*/
exports.appendText = async (padID, text, authorId = '') => {
export const appendText = async (padID, text, authorId = '') => {
// text is required
if (typeof text !== 'string') {
throw new CustomError('text is not a string', 'apierror');
@ -222,7 +235,7 @@ exports.appendText = async (padID, text, authorId = '') => {
const pad = await getPadSafe(padID, true);
await pad.appendText(text, authorId);
await padMessageHandler.updatePadClients(pad);
await updatePadClients(pad);
};
/**
@ -233,7 +246,7 @@ Example returns:
{code: 0, message:"ok", data: {text:"Welcome <strong>Text</strong>"}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getHTML = async (padID, rev) => {
export const getHTML = async (padID, rev) => {
if (rev !== undefined) {
rev = checkValidRev(rev);
}
@ -265,7 +278,7 @@ Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.setHTML = async (padID, html, authorId = '') => {
export const setHTML = async (padID, html, authorId = '') => {
// html string is required
if (typeof html !== 'string') {
throw new CustomError('html is not a string', 'apierror');
@ -282,7 +295,7 @@ exports.setHTML = async (padID, html, authorId = '') => {
}
// update the clients on the pad
padMessageHandler.updatePadClients(pad);
updatePadClients(pad);
};
/* ****************
@ -303,7 +316,7 @@ Example returns:
{code: 1, message:"padID does not exist", data: null}
*/
exports.getChatHistory = async (padID, start, end) => {
export const getChatHistory = async (padID, start, end) => {
if (start && end) {
if (start < 0) {
throw new CustomError('start is below zero', 'apierror');
@ -349,7 +362,7 @@ Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.appendChatMessage = async (padID, text, authorID, time) => {
export const appendChatMessage = async (padID, text, authorID, time) => {
// text is required
if (typeof text !== 'string') {
throw new CustomError('text is not a string', 'apierror');
@ -363,7 +376,7 @@ exports.appendChatMessage = async (padID, text, authorID, time) => {
// @TODO - missing getPadSafe() call ?
// save chat message to database and send message to all connected clients
await padMessageHandler.sendChatMessageToPadClients(new ChatMessage(text, authorID, time), padID);
await sendChatMessageToPadClients(new ChatMessage(text, authorID, time), padID);
};
/* ***************
@ -378,7 +391,7 @@ Example returns:
{code: 0, message:"ok", data: {revisions: 56}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getRevisionsCount = async (padID) => {
export const getRevisionsCount = async (padID) => {
// get the pad
const pad = await getPadSafe(padID, true);
return {revisions: pad.getHeadRevisionNumber()};
@ -392,7 +405,7 @@ Example returns:
{code: 0, message:"ok", data: {savedRevisions: 42}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getSavedRevisionsCount = async (padID) => {
export const getSavedRevisionsCount = async (padID) => {
// get the pad
const pad = await getPadSafe(padID, true);
return {savedRevisions: pad.getSavedRevisionsNumber()};
@ -406,7 +419,7 @@ Example returns:
{code: 0, message:"ok", data: {savedRevisions: [2, 42, 1337]}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.listSavedRevisions = async (padID) => {
export const listSavedRevisions = async (padID) => {
// get the pad
const pad = await getPadSafe(padID, true);
return {savedRevisions: pad.getSavedRevisionsList()};
@ -420,7 +433,7 @@ Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.saveRevision = async (padID, rev) => {
export const saveRevision = async (padID, rev) => {
// check if rev is a number
if (rev !== undefined) {
rev = checkValidRev(rev);
@ -439,7 +452,7 @@ exports.saveRevision = async (padID, rev) => {
rev = pad.getHeadRevisionNumber();
}
const author = await authorManager.createAuthor('API');
const author = await createAuthor('API');
await pad.addSavedRevision(rev, author.authorID, 'Saved through API call');
};
@ -451,7 +464,7 @@ Example returns:
{code: 0, message:"ok", data: {lastEdited: 1340815946602}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getLastEdited = async (padID) => {
export const getLastEdited = async (padID) => {
// get the pad
const pad = await getPadSafe(padID, true);
const lastEdited = await pad.getLastEdit();
@ -466,7 +479,7 @@ Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"pad does already exist", data: null}
*/
exports.createPad = async (padID, text, authorId = '') => {
export const createPad = async (padID, text, authorId = '') => {
if (padID) {
// ensure there is no $ in the padID
if (padID.indexOf('$') !== -1) {
@ -491,7 +504,7 @@ Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.deletePad = async (padID) => {
export const deletePad = async (padID) => {
const pad = await getPadSafe(padID, true);
await pad.remove();
};
@ -504,7 +517,7 @@ exports.deletePad = async (padID) => {
{code:0, message:"ok", data:null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.restoreRevision = async (padID, rev, authorId = '') => {
export const restoreRevision = async (padID, rev, authorId = '') => {
// check if rev is a number
if (rev === undefined) {
throw new CustomError('rev is not defined', 'apierror');
@ -556,7 +569,7 @@ exports.restoreRevision = async (padID, rev, authorId = '') => {
const changeset = builder.toString();
await pad.appendRevision(changeset, authorId);
await padMessageHandler.updatePadClients(pad);
await updatePadClients(pad);
};
/**
@ -568,7 +581,7 @@ Example returns:
{code: 0, message:"ok", data: {padID: destinationID}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.copyPad = async (sourceID, destinationID, force) => {
export const copyPad = async (sourceID, destinationID, force) => {
const pad = await getPadSafe(sourceID, true);
await pad.copy(destinationID, force);
};
@ -582,7 +595,7 @@ Example returns:
{code: 0, message:"ok", data: {padID: destinationID}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.copyPadWithoutHistory = async (sourceID, destinationID, force, authorId = '') => {
export const copyPadWithoutHistory = async (sourceID, destinationID, force, authorId = '') => {
const pad = await getPadSafe(sourceID, true);
await pad.copyPadWithoutHistory(destinationID, force, authorId);
};
@ -596,7 +609,7 @@ Example returns:
{code: 0, message:"ok", data: {padID: destinationID}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.movePad = async (sourceID, destinationID, force) => {
export const movePad = async (sourceID, destinationID, force) => {
const pad = await getPadSafe(sourceID, true);
await pad.copy(destinationID, force);
await pad.remove();
@ -610,12 +623,12 @@ Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getReadOnlyID = async (padID) => {
export const getReadOnlyID = async (padID) => {
// we don't need the pad object, but this function does all the security stuff for us
await getPadSafe(padID, true);
// get the readonlyId
const readOnlyID = await readOnlyManager.getReadOnlyId(padID);
const readOnlyID = await getReadOnlyId(padID);
return {readOnlyID};
};
@ -628,9 +641,9 @@ Example returns:
{code: 0, message:"ok", data: {padID: padID}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getPadID = async (roID) => {
export const getPadID = async (roID) => {
// get the PadId
const padID = await readOnlyManager.getPadId(roID);
const padID = await getPadId(roID);
if (padID == null) {
throw new CustomError('padID does not exist', 'apierror');
}
@ -646,7 +659,7 @@ Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.setPublicStatus = async (padID, publicStatus) => {
export const setPublicStatus = async (padID, publicStatus) => {
// ensure this is a group pad
checkGroupPad(padID, 'publicStatus');
@ -669,7 +682,7 @@ Example returns:
{code: 0, message:"ok", data: {publicStatus: true}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getPublicStatus = async (padID) => {
export const getPublicStatus = async (padID) => {
// ensure this is a group pad
checkGroupPad(padID, 'publicStatus');
@ -686,7 +699,7 @@ Example returns:
{code: 0, message:"ok", data: {authorIDs : ["a.s8oes9dhwrvt0zif", "a.akf8finncvomlqva"]}
{code: 1, message:"padID does not exist", data: null}
*/
exports.listAuthorsOfPad = async (padID) => {
export const listAuthorsOfPad = async (padID) => {
// get the pad
const pad = await getPadSafe(padID, true);
const authorIDs = pad.getAllAuthors();
@ -716,9 +729,9 @@ Example returns:
{code: 1, message:"padID does not exist"}
*/
exports.sendClientsMessage = async (padID, msg) => {
export const sendClientsMessage = async (padID, msg) => {
await getPadSafe(padID, true); // Throw if the padID is invalid or if the pad does not exist.
padMessageHandler.handleCustomMessage(padID, msg);
handleCustomMessage(padID, msg);
};
/**
@ -740,7 +753,7 @@ Example returns:
{code: 0, message:"ok", data: {chatHead: 42}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getChatHead = async (padID) => {
export const getChatHead = async (padID) => {
// get the pad
const pad = await getPadSafe(padID, true);
return {chatHead: pad.chatHead};
@ -764,7 +777,7 @@ Example returns:
{"code":4,"message":"no or wrong API Key","data":null}
*/
exports.createDiffHTML = async (padID, startRev, endRev) => {
export const createDiffHTML = async (padID, startRev, endRev) => {
// check if startRev is a number
if (startRev !== undefined) {
startRev = checkValidRev(startRev);
@ -803,13 +816,13 @@ exports.createDiffHTML = async (padID, startRev, endRev) => {
{"code":4,"message":"no or wrong API Key","data":null}
*/
exports.getStats = async () => {
const sessionInfos = padMessageHandler.sessioninfos;
export const getStats = async () => {
const sessionInfos = sessioninfos;
const sessionKeys = Object.keys(sessionInfos);
const activePads = new Set(Object.entries(sessionInfos).map((k) => k[1].padId));
const {padIDs} = await padManager.listAllPads();
const {padIDs} = await listAllPads();
return {
totalPads: padIDs.length,
@ -826,19 +839,19 @@ exports.getStats = async () => {
const isInt = (value) => (parseFloat(value) === parseInt(value, 10)) && !isNaN(value);
// gets a pad safe
const getPadSafe = async (padID, shouldExist, text, authorId = '') => {
const getPadSafe = async (padID, shouldExist, text?, authorId = '') => {
// check if padID is a string
if (typeof padID !== 'string') {
throw new CustomError('padID is not a string', 'apierror');
}
// check if the padID maches the requirements
if (!padManager.isValidPadId(padID)) {
if (!isValidPadId(padID)) {
throw new CustomError('padID did not match requirements', 'apierror');
}
// check if the pad exists
const exists = await padManager.doesPadExists(padID);
const exists = await doesPadExist(padID);
if (!exists && shouldExist) {
// does not exist, but should
@ -851,7 +864,7 @@ const getPadSafe = async (padID, shouldExist, text, authorId = '') => {
}
// pad exists, let's get it
return padManager.getPad(padID, text, authorId);
return getPad(padID, text, authorId);
};
// checks if a rev is a legal number

View File

@ -19,12 +19,13 @@
* limitations under the License.
*/
const db = require('./DB');
const CustomError = require('../utils/customError');
const hooks = require('../../static/js/pluginfw/hooks.js');
import {db} from './DB';
import CustomError from '../utils/customError';
import hooks from '../../static/js/pluginfw/hooks.js';
const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
exports.getColorPalette = () => [
export const getColorPalette = () => [
'#ffc7c7',
'#fff1c7',
'#e3ffc7',
@ -94,26 +95,23 @@ exports.getColorPalette = () => [
/**
* Checks if the author exists
*/
exports.doesAuthorExist = async (authorID) => {
export const doesAuthorExist = async (authorID: string) => {
const author = await db.get(`globalAuthor:${authorID}`);
return author != null;
};
}
/* exported for backwards compatibility */
exports.doesAuthorExists = exports.doesAuthorExist;
const getAuthor4Token = async (token) => {
const getAuthor4Token2 = async (token: string) => {
const author = await mapAuthorWithDBKey('token2author', token);
// return only the sub value authorID
return author ? author.authorID : author;
};
exports.getAuthorId = async (token, user) => {
export const getAuthorId = async (token, user) => {
const context = {dbKey: token, token, user};
let [authorId] = await hooks.aCallFirst('getAuthorId', context);
if (!authorId) authorId = await getAuthor4Token(context.dbKey);
if (!authorId) authorId = await getAuthor4Token2(context.dbKey);
return authorId;
};
@ -123,18 +121,18 @@ exports.getAuthorId = async (token, user) => {
* @deprecated Use `getAuthorId` instead.
* @param {String} token The token
*/
exports.getAuthor4Token = async (token) => {
export const getAuthor4Token = async (token) => {
warnDeprecated(
'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead');
return await getAuthor4Token(token);
return await getAuthor4Token2(token);
};
/**
* Returns the AuthorID for a mapper.
* @param {String} token The mapper
* @param authorMapper
* @param {String} name The name of the author (optional)
*/
exports.createAuthorIfNotExistsFor = async (authorMapper, name) => {
export const createAuthorIfNotExistsFor = async (authorMapper, name: string) => {
const author = await mapAuthorWithDBKey('mapper2author', authorMapper);
if (name) {
@ -151,7 +149,7 @@ exports.createAuthorIfNotExistsFor = async (authorMapper, name) => {
* @param {String} mapperkey The database key name for this mapper
* @param {String} mapper The mapper
*/
const mapAuthorWithDBKey = async (mapperkey, mapper) => {
export const mapAuthorWithDBKey = async (mapperkey: string, mapper) => {
// try to map to an author
const author = await db.get(`${mapperkey}:${mapper}`);
@ -178,7 +176,7 @@ const mapAuthorWithDBKey = async (mapperkey, mapper) => {
* Internal function that creates the database entry for an author
* @param {String} name The name of the author
*/
exports.createAuthor = async (name) => {
export const createAuthor = async (name) => {
// create the new author name
const author = `a.${randomString(16)}`;
@ -199,41 +197,41 @@ exports.createAuthor = async (name) => {
* Returns the Author Obj of the author
* @param {String} author The id of the author
*/
exports.getAuthor = async (author) => await db.get(`globalAuthor:${author}`);
export const getAuthor = async (author: string) => await db.get(`globalAuthor:${author}`);
/**
* Returns the color Id of the author
* @param {String} author The id of the author
*/
exports.getAuthorColorId = async (author) => await db.getSub(`globalAuthor:${author}`, ['colorId']);
export const getAuthorColorId = async (author: string) => await db.getSub(`globalAuthor:${author}`, ['colorId']);
/**
* Sets the color Id of the author
* @param {String} author The id of the author
* @param {String} colorId The color id of the author
*/
exports.setAuthorColorId = async (author, colorId) => await db.setSub(
export const setAuthorColorId = async (author: string, colorId: string) => await db.setSub(
`globalAuthor:${author}`, ['colorId'], colorId);
/**
* Returns the name of the author
* @param {String} author The id of the author
*/
exports.getAuthorName = async (author) => await db.getSub(`globalAuthor:${author}`, ['name']);
export const getAuthorName = async (author: string) => await db.getSub(`globalAuthor:${author}`, ['name']);
/**
* Sets the name of the author
* @param {String} author The id of the author
* @param {String} name The name of the author
*/
exports.setAuthorName = async (author, name) => await db.setSub(
export const setAuthorName = async (author: string, name:string) => await db.setSub(
`globalAuthor:${author}`, ['name'], name);
/**
* Returns an array of all pads this author contributed to
* @param {String} author The id of the author
* @param {String} authorID The id of the author
*/
exports.listPadsOfAuthor = async (authorID) => {
export const listPadsOfAuthor = async (authorID:string) => {
/* There are two other places where this array is manipulated:
* (1) When the author is added to a pad, the author object is also updated
* (2) When a pad is deleted, each author of that pad is also updated
@ -255,10 +253,10 @@ exports.listPadsOfAuthor = async (authorID) => {
/**
* Adds a new pad to the list of contributions
* @param {String} author The id of the author
* @param {String} authorID The id of the author
* @param {String} padID The id of the pad the author contributes to
*/
exports.addPad = async (authorID, padID) => {
export const addPad = async (authorID: string, padID: string) => {
// get the entry
const author = await db.get(`globalAuthor:${authorID}`);
@ -282,10 +280,10 @@ exports.addPad = async (authorID, padID) => {
/**
* Removes a pad from the list of contributions
* @param {String} author The id of the author
* @param {String} authorID The id of the author
* @param {String} padID The id of the pad the author contributes to
*/
exports.removePad = async (authorID, padID) => {
export const removePad = async (authorID: string, padID: string) => {
const author = await db.get(`globalAuthor:${authorID}`);
if (author == null) return;

View File

@ -21,28 +21,29 @@
* limitations under the License.
*/
const ueberDB = require('ueberdb2');
const settings = require('../utils/Settings');
const log4js = require('log4js');
const stats = require('../stats');
import ueberDB from 'ueberdb2';
import {dbSettings, dbType} from '../utils/Settings';
import log4js from 'log4js';
import {shutdown as statsShutdown,createCollection} from '../stats';
import {} from 'measured-core'
const logger = log4js.getLogger('ueberDB');
/**
* The UeberDB Object that provides the database functions
*/
exports.db = null;
const db = null;
/**
* Initializes the database with the settings provided by the settings module
*/
exports.init = async () => {
exports.db = new ueberDB.Database(settings.dbType, settings.dbSettings, null, logger);
const init = async () => {
exports.db = new ueberDB.Database(dbType, dbSettings, null, logger);
await exports.db.init();
if (exports.db.metrics != null) {
for (const [metric, value] of Object.entries(exports.db.metrics)) {
if (typeof value !== 'number') continue;
stats.gauge(`ueberdb_${metric}`, () => exports.db.metrics[metric]);
// FIXME find a better replacement for measure-core
createCollection.gauge(`ueberdb_${metric}`, () => exports.db.metrics[metric]);
}
}
for (const fn of ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove']) {
@ -53,8 +54,10 @@ exports.init = async () => {
}
};
exports.shutdown = async (hookName, context) => {
const shutdown = async (hookName, context) => {
if (exports.db != null) await exports.db.close();
exports.db = null;
logger.log('Database closed');
};
export {db,init,shutdown}

View File

@ -19,13 +19,13 @@
* limitations under the License.
*/
const CustomError = require('../utils/customError');
import CustomError from '../utils/customError';
const randomString = require('../../static/js/pad_utils').randomString;
const db = require('./DB');
const padManager = require('./PadManager');
const sessionManager = require('./SessionManager');
import {db} from './DB';
import {doesPadExist, getPad} from './PadManager';
import {deleteSession} from './SessionManager';
exports.listAllGroups = async () => {
export const listAllGroups = async () => {
let groups = await db.get('groups');
groups = groups || {};
@ -33,7 +33,7 @@ exports.listAllGroups = async () => {
return {groupIDs};
};
exports.deleteGroup = async (groupID) => {
export const deleteGroup = async (groupID) => {
const group = await db.get(`group:${groupID}`);
// ensure group exists
@ -44,7 +44,7 @@ exports.deleteGroup = async (groupID) => {
// iterate through all pads of this group and delete them (in parallel)
await Promise.all(Object.keys(group.pads).map(async (padId) => {
const pad = await padManager.getPad(padId);
const pad = await getPad(padId);
await pad.remove();
}));
@ -52,7 +52,7 @@ exports.deleteGroup = async (groupID) => {
// record because deleting a session updates the group2sessions record.
const {sessionIDs = {}} = await db.get(`group2sessions:${groupID}`) || {};
await Promise.all(Object.keys(sessionIDs).map(async (sessionId) => {
await sessionManager.deleteSession(sessionId);
await deleteSession(sessionId);
}));
await Promise.all([
@ -68,14 +68,14 @@ exports.deleteGroup = async (groupID) => {
await db.remove(`group:${groupID}`);
};
exports.doesGroupExist = async (groupID) => {
export const doesGroupExist = async (groupID) => {
// try to get the group entry
const group = await db.get(`group:${groupID}`);
return (group != null);
};
exports.createGroup = async () => {
export const createGroup = async () => {
const groupID = `g.${randomString(16)}`;
await db.set(`group:${groupID}`, {pads: {}, mappings: {}});
// Add the group to the `groups` record after the group's individual record is created so that
@ -85,7 +85,7 @@ exports.createGroup = async () => {
return {groupID};
};
exports.createGroupIfNotExistsFor = async (groupMapper) => {
export const createGroupIfNotExistsFor = async (groupMapper) => {
if (typeof groupMapper !== 'string') {
throw new CustomError('groupMapper is not a string', 'apierror');
}
@ -103,19 +103,19 @@ exports.createGroupIfNotExistsFor = async (groupMapper) => {
return result;
};
exports.createGroupPad = async (groupID, padName, text, authorId = '') => {
export const createGroupPad = async (groupID, padName, text, authorId = '') => {
// create the padID
const padID = `${groupID}$${padName}`;
// ensure group exists
const groupExists = await exports.doesGroupExist(groupID);
const groupExists = await doesGroupExist(groupID);
if (!groupExists) {
throw new CustomError('groupID does not exist', 'apierror');
}
// ensure pad doesn't exist already
const padExists = await padManager.doesPadExists(padID);
const padExists = await doesPadExist(padID);
if (padExists) {
// pad exists already
@ -123,7 +123,7 @@ exports.createGroupPad = async (groupID, padName, text, authorId = '') => {
}
// create the pad
await padManager.getPad(padID, text, authorId);
await getPad(padID, text, authorId);
// create an entry in the group for this pad
await db.setSub(`group:${groupID}`, ['pads', padID], 1);
@ -131,7 +131,7 @@ exports.createGroupPad = async (groupID, padName, text, authorId = '') => {
return {padID};
};
exports.listPads = async (groupID) => {
export const listPads = async (groupID) => {
const exists = await exports.doesGroupExist(groupID);
// ensure the group exists

View File

@ -3,15 +3,16 @@
* The pad object, defined with joose
*/
const AttributeMap = require('../../static/js/AttributeMap');
const Changeset = require('../../static/js/Changeset');
const ChatMessage = require('../../static/js/ChatMessage');
const AttributePool = require('../../static/js/AttributePool');
const Stream = require('../utils/Stream');
const assert = require('assert').strict;
const db = require('./DB');
const settings = require('../utils/Settings');
const authorManager = require('./AuthorManager');
import AttributeMap from '../../static/js/AttributeMap';
import Changeset from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage';
import {AttributePool} from '../../static/js/AttributePool';
import Stream from '../utils/Stream';
import assert, {strict} from 'assert'
import {db} from './DB';
import {defaultPadText} from '../utils/Settings';
import {addPad, getAuthorColorId, getAuthorName, getColorPalette, removePad} from './AuthorManager';
import {Revision} from "../models/Revision";
const padManager = require('./PadManager');
const padMessageHandler = require('../handler/PadMessageHandler');
const groupManager = require('./GroupManager');
@ -32,15 +33,24 @@ exports.cleanText = (txt) => txt.replace(/\r\n/g, '\n')
.replace(/\t/g, ' ')
.replace(/\xa0/g, ' ');
class Pad {
export class Pad {
private db: any;
private atext: any;
private pool: AttributePool;
private head: number;
private chatHead: number;
private publicStatus: boolean;
private id: string;
private savedRevisions: Revision[];
/**
* @param id the id of this pad
* @param [database] - Database object to access this pad's records (and only this pad's records;
* the shared global Etherpad database object is still used for all other pad accesses, such
* as copying the pad). Defaults to the shared global Etherpad database object. This parameter
* can be used to shard pad storage across multiple database backends, to put each pad in its
* own database table, or to validate imported pad data before it is written to the database.
*/
constructor(id, database = db) {
constructor(id: string, database = db) {
this.db = database;
this.atext = Changeset.makeAText('\n');
this.pool = new AttributePool();
@ -99,7 +109,7 @@ class Pad {
},
}),
this.saveToDatabase(),
authorId && authorManager.addPad(authorId, this.id),
authorId && addPad(authorId, this.id),
hooks.aCallAll(hook, {
pad: this,
authorId,
@ -121,7 +131,7 @@ class Pad {
}
toJSON() {
const o = {...this, pool: this.pool.toJsonable()};
const o:{db: any, id: any} = {...this, pool: this.pool.toJsonable()}
delete o.db;
delete o.id;
return o;
@ -190,10 +200,10 @@ class Pad {
async getAllAuthorColors() {
const authorIds = this.getAllAuthors();
const returnTable = {};
const colorPalette = authorManager.getColorPalette();
const colorPalette = getColorPalette();
await Promise.all(
authorIds.map((authorId) => authorManager.getAuthorColorId(authorId).then((colorId) => {
authorIds.map((authorId) => getAuthorColorId(authorId).then((colorId) => {
// colorId might be a hex color or an number out of the palette
returnTable[authorId] = colorPalette[colorId] || colorId;
})));
@ -315,7 +325,7 @@ class Pad {
const entry = await this.db.get(`pad:${this.id}:chat:${entryNum}`);
if (entry == null) return null;
const message = ChatMessage.fromObject(entry);
message.displayName = await authorManager.getAuthorName(message.authorId);
message.displayName = await getAuthorName(message.authorId);
return message;
}
@ -352,7 +362,7 @@ class Pad {
if ('pool' in value) this.pool = new AttributePool().fromJsonable(value.pool);
} else {
if (text == null) {
const context = {pad: this, authorId, type: 'text', content: settings.defaultPadText};
const context = {pad: this, authorId, type: 'text', content: defaultPadText};
await hooks.aCallAll('padDefaultContent', context);
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
text = exports.cleanText(context.content);
@ -454,7 +464,7 @@ class Pad {
async copyAuthorInfoToDestinationPad(destinationID) {
// add the new sourcePad to all authors who contributed to the old one
await Promise.all(this.getAllAuthors().map(
(authorID) => authorManager.addPad(authorID, destinationID)));
(authorID) => addPad(authorID, destinationID)));
}
async copyPadWithoutHistory(destinationID, force, authorId = '') {
@ -557,7 +567,7 @@ class Pad {
// remove pad from all authors who contributed
this.getAllAuthors().forEach((authorId) => {
p.push(authorManager.removePad(authorId, padID));
p.push(removePad(authorId, padID));
});
// delete the pad entry and delete pad from padManager
@ -587,12 +597,13 @@ class Pad {
}
// build the saved revision object
const savedRevision = {};
savedRevision.revNum = revNum;
savedRevision.savedById = savedById;
savedRevision.label = label || `Revision ${revNum}`;
savedRevision.timestamp = Date.now();
savedRevision.id = randomString(10);
const savedRevision:Revision = {
label: label || `Revision ${revNum}`,
revNum: revNum,
savedById: savedById,
timestamp: Date.now(),
id: randomString(10)
}
// save this new saved revision
this.savedRevisions.push(savedRevision);

View File

@ -19,9 +19,9 @@
* limitations under the License.
*/
const CustomError = require('../utils/customError');
const Pad = require('../db/Pad');
const db = require('./DB');
import CustomError from '../utils/customError';
import {Pad} from './Pad';
import {db} from './DB';
/**
* A cache of all loaded Pads.
@ -50,6 +50,9 @@ const globalPads = {
* Updated without db access as new pads are created/old ones removed.
*/
const padList = new class {
private _cachedList: any[];
private _list: Set<any>;
private _loaded: Promise<void>
constructor() {
this._cachedList = null;
this._list = new Set();
@ -94,9 +97,9 @@ const padList = new class {
* @param {string} [authorId] - Optional author ID of the user that initiated the pad creation (if
* applicable).
*/
exports.getPad = async (id, text, authorId = '') => {
export const getPad = async (id, text?, authorId = '') => {
// check if this is a valid padId
if (!exports.isValidPadId(id)) {
if (!isValidPadId(id)) {
throw new CustomError(`${id} is not a valid padId`, 'apierror');
}
@ -121,7 +124,7 @@ exports.getPad = async (id, text, authorId = '') => {
}
// try to load pad
pad = new Pad.Pad(id);
pad = new Pad(id);
// initialize the pad
await pad.init(text, authorId);
@ -131,21 +134,18 @@ exports.getPad = async (id, text, authorId = '') => {
return pad;
};
exports.listAllPads = async () => {
export const listAllPads = async () => {
const padIDs = await padList.getPads();
return {padIDs};
};
// checks if a pad exists
exports.doesPadExist = async (padId) => {
export const doesPadExist = async (padId) => {
const value = await db.get(`pad:${padId}`);
return (value != null && value.atext);
};
// alias for backwards compatibility
exports.doesPadExists = exports.doesPadExist;
}
/**
* An array of padId transformations. These represent changes in pad name policy over
@ -157,9 +157,9 @@ const padIdTransforms = [
];
// returns a sanitized padId, respecting legacy pad id formats
exports.sanitizePadId = async (padId) => {
export const sanitizePadId = async (padId) => {
for (let i = 0, n = padIdTransforms.length; i < n; ++i) {
const exists = await exports.doesPadExist(padId);
const exists = await doesPadExist(padId);
if (exists) {
return padId;
@ -174,19 +174,19 @@ exports.sanitizePadId = async (padId) => {
return padId;
};
exports.isValidPadId = (padId) => /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
export const isValidPadId = (padId) => /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
/**
* Removes the pad from database and unloads it.
*/
exports.removePad = async (padId) => {
export const removePad = async (padId) => {
const p = db.remove(`pad:${padId}`);
exports.unloadPad(padId);
unloadPad(padId);
padList.removePad(padId);
await p;
};
// removes a pad from the cache
exports.unloadPad = (padId) => {
export const unloadPad = (padId) => {
globalPads.remove(padId);
};

View File

@ -20,21 +20,21 @@
*/
const db = require('./DB');
const randomString = require('../utils/randomstring');
import {db} from './DB';
import randomString from '../utils/randomstring';
/**
* checks if the id pattern matches a read-only pad id
* @param {String} the pad's id
* @param {String} id the pad's id
*/
exports.isReadOnlyId = (id) => id.startsWith('r.');
export const isReadOnlyId = (id: string) => id.startsWith('r.');
/**
* returns a read only id for a pad
* @param {String} padId the id of the pad
*/
exports.getReadOnlyId = async (padId) => {
export const getReadOnlyId = async (padId) => {
// check if there is a pad2readonly entry
let readOnlyId = await db.get(`pad2readonly:${padId}`);
@ -54,13 +54,13 @@ exports.getReadOnlyId = async (padId) => {
* returns the padId for a read only id
* @param {String} readOnlyId read only id
*/
exports.getPadId = async (readOnlyId) => await db.get(`readonly2pad:${readOnlyId}`);
export const getPadId = async (readOnlyId) => await db.get(`readonly2pad:${readOnlyId}`);
/**
* returns the padId and readonlyPadId in an object for any id
* @param {String} padIdOrReadonlyPadId read only id or real pad id
* @param {String} id padIdOrReadonlyPadId read only id or real pad id
*/
exports.getIds = async (id) => {
export const getIds = async (id: string) => {
const readonly = exports.isReadOnlyId(id);
// Might be null, if this is an unknown read-only id

View File

@ -19,18 +19,29 @@
* limitations under the License.
*/
const authorManager = require('./AuthorManager');
const hooks = require('../../static/js/pluginfw/hooks.js');
const padManager = require('./PadManager');
const readOnlyManager = require('./ReadOnlyManager');
const sessionManager = require('./SessionManager');
const settings = require('../utils/Settings');
const webaccess = require('../hooks/express/webaccess');
const log4js = require('log4js');
const authLogger = log4js.getLogger('auth');
const {padutils} = require('../../static/js/pad_utils');
import {getAuthorId} from "./AuthorManager";
const DENY = Object.freeze({accessStatus: 'deny'});
import hooks from "../../static/js/pluginfw/hooks.js";
import {doesPadExist, getPad} from "./PadManager";
import {getPadId} from "./ReadOnlyManager";
import {findAuthorID} from "./SessionManager";
import {editOnly, loadTest, requireAuthentication, requireSession} from "../utils/Settings";
import webaccess from "../hooks/express/webaccess";
import log4js from "log4js";
import {padutils} from "../../static/js/pad_utils";
import {isReadOnlyId} from "./ReadOnlyManager.js";
const authLogger = log4js.getLogger('auth');
const DENY = Object.freeze({accessStatus: 'deny', authorID: null});
/**
* Determines whether the user can access a pad.
@ -50,17 +61,17 @@ const DENY = Object.freeze({accessStatus: 'deny'});
* WARNING: Tokens and session IDs MUST be kept secret, otherwise users will be able to impersonate
* each other (which might allow them to gain privileges).
*/
exports.checkAccess = async (padID, sessionCookie, token, userSettings) => {
export const checkAccess = async (padID, sessionCookie, token, userSettings) => {
if (!padID) {
authLogger.debug('access denied: missing padID');
return DENY;
}
let canCreate = !settings.editOnly;
let canCreate = !editOnly;
if (readOnlyManager.isReadOnlyId(padID)) {
if (isReadOnlyId(padID)) {
canCreate = false;
padID = await readOnlyManager.getPadId(padID);
padID = await getPadId(padID);
if (padID == null) {
authLogger.debug('access denied: read-only pad ID for a pad that does not exist');
return DENY;
@ -68,10 +79,10 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => {
}
// Authentication and authorization checks.
if (settings.loadTest) {
if (loadTest) {
console.warn(
'bypassing socket.io authentication and authorization checks due to settings.loadTest');
} else if (settings.requireAuthentication) {
} else if (requireAuthentication) {
if (userSettings == null) {
authLogger.debug('access denied: authentication is required');
return DENY;
@ -96,14 +107,14 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => {
return DENY;
}
const padExists = await padManager.doesPadExist(padID);
const padExists = await doesPadExist(padID);
if (!padExists && !canCreate) {
authLogger.debug('access denied: user attempted to create a pad, which is prohibited');
return DENY;
}
const sessionAuthorID = await sessionManager.findAuthorID(padID.split('$')[0], sessionCookie);
if (settings.requireSession && !sessionAuthorID) {
const sessionAuthorID = await findAuthorID(padID.split('$')[0], sessionCookie);
if (requireSession && !sessionAuthorID) {
authLogger.debug('access denied: HTTP API session is required');
return DENY;
}
@ -115,7 +126,7 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => {
const grant = {
accessStatus: 'grant',
authorID: sessionAuthorID || await authorManager.getAuthorId(token, userSettings),
authorID: sessionAuthorID || await getAuthorId(token, userSettings),
};
if (!padID.includes('$')) {
@ -132,7 +143,7 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => {
return grant;
}
const pad = await padManager.getPad(padID);
const pad = await getPad(padID);
if (!pad.getPublicStatus() && sessionAuthorID == null) {
authLogger.debug('access denied: must have an HTTP API session to access private group pads');

View File

@ -36,7 +36,7 @@ const authorManager = require('./AuthorManager');
* sessionCookie, and is bound to a group with the given ID, then this returns the author ID
* bound to the session. Otherwise, returns undefined.
*/
exports.findAuthorID = async (groupID, sessionCookie) => {
export const findAuthorID = async (groupID, sessionCookie) => {
if (!sessionCookie) return undefined;
/*
* Sometimes, RFC 6265-compliant web servers may send back a cookie whose
@ -64,7 +64,7 @@ exports.findAuthorID = async (groupID, sessionCookie) => {
const sessionIDs = sessionCookie.replace(/^"|"$/g, '').split(',');
const sessionInfoPromises = sessionIDs.map(async (id) => {
try {
return await exports.getSessionInfo(id);
return await getSessionInfo(id);
} catch (err) {
if (err.message === 'sessionID does not exist') {
console.debug(`SessionManager getAuthorID: no session exists with ID ${id}`);
@ -81,7 +81,7 @@ exports.findAuthorID = async (groupID, sessionCookie) => {
return sessionInfo.authorID;
};
exports.doesSessionExist = async (sessionID) => {
export const doesSessionExist = async (sessionID) => {
// check if the database entry of this session exists
const session = await db.get(`session:${sessionID}`);
return (session != null);
@ -90,7 +90,7 @@ exports.doesSessionExist = async (sessionID) => {
/**
* Creates a new session between an author and a group
*/
exports.createSession = async (groupID, authorID, validUntil) => {
export const createSession = async (groupID, authorID, validUntil) => {
// check if the group exists
const groupExists = await groupManager.doesGroupExist(groupID);
if (!groupExists) {
@ -146,7 +146,7 @@ exports.createSession = async (groupID, authorID, validUntil) => {
return {sessionID};
};
exports.getSessionInfo = async (sessionID) => {
export const getSessionInfo = async (sessionID) => {
// check if the database entry of this session exists
const session = await db.get(`session:${sessionID}`);
@ -162,7 +162,7 @@ exports.getSessionInfo = async (sessionID) => {
/**
* Deletes a session
*/
exports.deleteSession = async (sessionID) => {
export const deleteSession = async (sessionID) => {
// ensure that the session exists
const session = await db.get(`session:${sessionID}`);
if (session == null) {
@ -186,7 +186,7 @@ exports.deleteSession = async (sessionID) => {
await db.remove(`session:${sessionID}`);
};
exports.listSessionsOfGroup = async (groupID) => {
export const listSessionsOfGroup = async (groupID) => {
// check that the group exists
const exists = await groupManager.doesGroupExist(groupID);
if (!exists) {
@ -197,7 +197,7 @@ exports.listSessionsOfGroup = async (groupID) => {
return sessions;
};
exports.listSessionsOfAuthor = async (authorID) => {
export const listSessionsOfAuthor = async (authorID) => {
// check that the author exists
const exists = await authorManager.doesAuthorExist(authorID);
if (!exists) {
@ -218,8 +218,7 @@ const listSessionsWithDBKey = async (dbkey) => {
// iterate through the sessions and get the sessioninfos
for (const sessionID of Object.keys(sessions || {})) {
try {
const sessionInfo = await exports.getSessionInfo(sessionID);
sessions[sessionID] = sessionInfo;
sessions[sessionID] = await getSessionInfo(sessionID);
} catch (err) {
if (err.name === 'apierror') {
console.warn(`Found bad session ${sessionID} in ${dbkey}`);

View File

@ -1,13 +1,19 @@
'use strict';
const DB = require('./DB');
const Store = require('express-session').Store;
const log4js = require('log4js');
const util = require('util');
import {db} from "./DB";
import {Store} from "express-session";
import log4js from "log4js";
import util from "util";
import {SessionModel} from "../models/SessionModel";
const logger = log4js.getLogger('SessionStore');
class SessionStore extends Store {
private _refresh: any;
private _expirations: Map<any, any>;
/**
* @param {?number} [refresh] - How often (in milliseconds) `touch()` will update a session's
* database record with the cookie's latest expiration time. If the difference between the
@ -34,10 +40,10 @@ class SessionStore extends Store {
for (const {timeout} of this._expirations.values()) clearTimeout(timeout);
}
async _updateExpirations(sid, sess, updateDbExp = true) {
async _updateExpirations(sid, sess: SessionModel, updateDbExp = true) {
const exp = this._expirations.get(sid) || {};
clearTimeout(exp.timeout);
const {cookie: {expires} = {}} = sess || {};
const {cookie: {expires} = {expires: sess.cookie.expires}} = sess || {cookie:{expires:undefined}};
if (expires) {
const sessExp = new Date(expires).getTime();
if (updateDbExp) exp.db = sessExp;
@ -64,12 +70,12 @@ class SessionStore extends Store {
}
async _write(sid, sess) {
await DB.set(`sessionstorage:${sid}`, sess);
await db.set(`sessionstorage:${sid}`, sess);
}
async _get(sid) {
logger.debug(`GET ${sid}`);
const s = await DB.get(`sessionstorage:${sid}`);
const s = await db.get(`sessionstorage:${sid}`);
return await this._updateExpirations(sid, s);
}
@ -83,7 +89,7 @@ class SessionStore extends Store {
logger.debug(`DESTROY ${sid}`);
clearTimeout((this._expirations.get(sid) || {}).timeout);
this._expirations.delete(sid);
await DB.remove(`sessionstorage:${sid}`);
await db.remove(`sessionstorage:${sid}`);
}
// Note: express-session might call touch() before it calls set() for the first time. Ideally this
@ -110,4 +116,4 @@ for (const m of ['get', 'set', 'destroy', 'touch']) {
SessionStore.prototype[m] = util.callbackify(SessionStore.prototype[`_${m}`]);
}
module.exports = SessionStore;
export default SessionStore;

View File

@ -20,56 +20,61 @@
* require("./index").require("./path/to/template.ejs")
*/
const ejs = require('ejs');
const fs = require('fs');
const hooks = require('../../static/js/pluginfw/hooks.js');
const path = require('path');
const resolve = require('resolve');
const settings = require('../utils/Settings');
import ejs from 'ejs';
import fs from "fs";
import hooks from "../../static/js/pluginfw/hooks.js";
import path from "path";
import resolve from "resolve";
import {maxAge} from "../utils/Settings";
const templateCache = new Map();
exports.info = {
export const info = {
__output_stack: [],
block_stack: [],
file_stack: [],
args: [],
args: [], __output: undefined
};
const getCurrentFile = () => exports.info.file_stack[exports.info.file_stack.length - 1];
const getCurrentFile = () => info.file_stack[info.file_stack.length - 1];
exports._init = (b, recursive) => {
exports.info.__output_stack.push(exports.info.__output);
exports.info.__output = b;
export const _init = (b, recursive) => {
info.__output_stack.push(info.__output)
info.__output = b
};
exports._exit = (b, recursive) => {
exports.info.__output = exports.info.__output_stack.pop();
export const _exit = (b, recursive) => {
info.__output = info.__output_stack.pop();
};
exports.begin_block = (name) => {
exports.info.block_stack.push(name);
exports.info.__output_stack.push(exports.info.__output.get());
exports.info.__output.set('');
export const begin_block = (name) => {
info.block_stack.push(name);
info.__output_stack.push(info.__output.get());
info.__output.set('');
};
exports.end_block = () => {
const name = exports.info.block_stack.pop();
const renderContext = exports.info.args[exports.info.args.length - 1];
const content = exports.info.__output.get();
exports.info.__output.set(exports.info.__output_stack.pop());
export const end_block = () => {
const name = info.block_stack.pop();
const renderContext = info.args[info.args.length - 1];
const content = info.__output.get();
info.__output.set(info.__output_stack.pop());
const args = {content, renderContext};
hooks.callAll(`eejsBlock_${name}`, args);
exports.info.__output.set(exports.info.__output.get().concat(args.content));
info.__output.set(info.__output.get().concat(args.content));
};
exports.require = (name, args, mod) => {
export const required = (name, args?, mod?) => {
if (args == null) args = {};
let basedir = __dirname;
let paths = [];
if (exports.info.file_stack.length) {
if (info.file_stack.length) {
basedir = path.dirname(getCurrentFile().path);
}
if (mod) {
@ -82,18 +87,18 @@ exports.require = (name, args, mod) => {
args.e = exports;
args.require = require;
const cache = settings.maxAge !== 0;
const cache = maxAge !== 0;
const template = cache && templateCache.get(ejspath) || ejs.compile(
'<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>' +
`${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`,
{filename: ejspath});
if (cache) templateCache.set(ejspath, template);
exports.info.args.push(args);
exports.info.file_stack.push({path: ejspath});
info.args.push(args);
info.file_stack.push({path: ejspath});
const res = template(args);
exports.info.file_stack.pop();
exports.info.args.pop();
info.file_stack.pop();
info.args.pop();
return res;
};

View File

@ -19,12 +19,12 @@
* limitations under the License.
*/
const absolutePaths = require('../utils/AbsolutePaths');
const fs = require('fs');
const api = require('../db/API');
const log4js = require('log4js');
const padManager = require('../db/PadManager');
const randomString = require('../utils/randomstring');
import absolutePaths from '../utils/AbsolutePaths';
import fs from 'fs';
import * as api from '../db/API';
import log4js from 'log4js';
import {sanitizePadId} from '../db/PadManager';
import randomString from '../utils/randomstring';
const argv = require('../utils/Cli').argv;
const createHTTPError = require('http-errors');
@ -45,7 +45,7 @@ try {
}
// a list of all functions
const version = {};
export const version = {};
version['1'] = {
createGroup: [],
@ -158,10 +158,9 @@ version['1.3.0'] = {
};
// set the latest available API version here
exports.latestApiVersion = '1.3.0';
export const latestApiVersion = '1.3.0';
// exports the versions so it can be used by the new Swagger endpoint
exports.version = version;
/**
* Handles a HTTP API call
@ -170,7 +169,7 @@ exports.version = version;
* @req express request object
* @res express response object
*/
exports.handle = async function (apiVersion, functionName, fields, req, res) {
export const handle = async function (apiVersion, functionName, fields, req, res) {
// say goodbye if this is an unknown API version
if (!(apiVersion in version)) {
throw new createHTTPError.NotFound('no such api version');
@ -190,13 +189,13 @@ exports.handle = async function (apiVersion, functionName, fields, req, res) {
// sanitize any padIDs before continuing
if (fields.padID) {
fields.padID = await padManager.sanitizePadId(fields.padID);
fields.padID = await 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);
fields.padName = await sanitizePadId(fields.padName);
}
// put the function parameters in an array
@ -206,6 +205,6 @@ exports.handle = async function (apiVersion, functionName, fields, req, res) {
return api[functionName].apply(this, functionParams);
};
exports.exportedForTestingOnly = {
export const exportedForTestingOnly = {
apiKey: apikey,
};

View File

@ -38,7 +38,7 @@ const tempDirectory = os.tmpdir();
/**
* do a requested export
*/
exports.doExport = async (req, res, padId, readOnlyId, type) => {
export const doExport = async (req, res, padId, readOnlyId, type) => {
// avoid naming the read-only file as the original pad's id
let fileName = readOnlyId ? readOnlyId : padId;
@ -114,4 +114,4 @@ exports.doExport = async (req, res, padId, readOnlyId, type) => {
await fsp_unlink(destFile);
}
};
}

View File

@ -21,22 +21,24 @@
* limitations under the License.
*/
const padManager = require('../db/PadManager');
const padMessageHandler = require('./PadMessageHandler');
const fs = require('fs').promises;
const path = require('path');
const settings = require('../utils/Settings');
const {Formidable} = require('formidable');
const os = require('os');
const importHtml = require('../utils/ImportHtml');
const importEtherpad = require('../utils/ImportEtherpad');
const log4js = require('log4js');
const hooks = require('../../static/js/pluginfw/hooks.js');
import {getPad, unloadPad} from '../db/PadManager';
import {updatePadClients} from './PadMessageHandler';
import path from 'path';
import {promises as fs} from "fs";
import {abiword, allowUnknownFileEnds, importMaxFileSize, soffice} from '../utils/Settings';
import {Formidable} from 'formidable';
import os from 'os';
import importHtml from '../utils/ImportHtml';
import importEtherpad from '../utils/ImportEtherpad';
import log4js from 'log4js';
import hooks from '../../static/js/pluginfw/hooks.js';
const logger = log4js.getLogger('ImportHandler');
// `status` must be a string supported by `importErrorMessage()` in `src/static/js/pad_impexp.js`.
class ImportError extends Error {
public status: any;
constructor(status, ...args) {
super(...args);
if (Error.captureStackTrace) Error.captureStackTrace(this, ImportError);
@ -59,12 +61,12 @@ let converter = null;
let exportExtension = 'htm';
// load abiword only if it is enabled and if soffice is disabled
if (settings.abiword != null && settings.soffice == null) {
if (abiword != null && soffice == null) {
converter = require('../utils/Abiword');
}
// load soffice only if it is enabled
if (settings.soffice != null) {
if (soffice != null) {
converter = require('../utils/LibreOffice');
exportExtension = 'html';
}
@ -86,11 +88,11 @@ const doImport = async (req, res, padId, authorId) => {
const form = new Formidable({
keepExtensions: true,
uploadDir: tmpDirectory,
maxFileSize: settings.importMaxFileSize,
maxFileSize: importMaxFileSize,
});
// locally wrapped Promise, since form.parse requires a callback
let srcFile = await new Promise((resolve, reject) => {
let srcFile = await new Promise<string>((resolve, reject) => {
form.parse(req, (err, fields, files) => {
if (err != null) {
logger.warn(`Import failed due to form error: ${err.stack || err}`);
@ -118,7 +120,7 @@ const doImport = async (req, res, padId, authorId) => {
if (fileEndingUnknown) {
// the file ending is not known
if (settings.allowUnknownFileEnds === true) {
if (allowUnknownFileEnds === true) {
// we need to rename this file with a .txt ending
const oldSrcFile = srcFile;
@ -140,7 +142,7 @@ const doImport = async (req, res, padId, 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 pad = await 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');
@ -186,7 +188,7 @@ const doImport = async (req, res, padId, authorId) => {
}
// Use '\n' to avoid the default pad text if the pad doesn't yet exist.
let pad = await padManager.getPad(padId, '\n', authorId);
let pad = await getPad(padId, '\n', authorId);
// read the text
let text;
@ -215,16 +217,16 @@ const doImport = async (req, res, padId, 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);
unloadPad(padId);
pad = await getPad(padId, '\n', authorId);
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;
// tell clients to update
await padMessageHandler.updatePadClients(pad);
await updatePadClients(pad);
// clean up temporary files
rm(srcFile);
@ -233,7 +235,7 @@ const doImport = async (req, res, padId, authorId) => {
return false;
};
exports.doImport = async (req, res, padId, authorId = '') => {
export const doImport2 = async (req, res, padId, authorId = '') => {
let httpStatus = 200;
let code = 0;
let message = 'ok';

View File

@ -19,34 +19,62 @@
* limitations under the License.
*/
const AttributeMap = require('../../static/js/AttributeMap');
const padManager = require('../db/PadManager');
const Changeset = require('../../static/js/Changeset');
const ChatMessage = require('../../static/js/ChatMessage');
const AttributePool = require('../../static/js/AttributePool');
const AttributeManager = require('../../static/js/AttributeManager');
const authorManager = require('../db/AuthorManager');
const {padutils} = require('../../static/js/pad_utils');
const readOnlyManager = require('../db/ReadOnlyManager');
const settings = require('../utils/Settings');
import AttributeMap from '../../static/js/AttributeMap';
import {getPad} from '../db/PadManager';
import Changeset from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage';
import {AttributePool} from '../../static/js/AttributePool';
import AttributeManager from '../../static/js/AttributeManager';
import {
getAuthor,
getAuthorColorId,
getAuthorName,
getColorPalette,
setAuthorColorId,
setAuthorName
} from '../db/AuthorManager';
import {padutils} from '../../static/js/pad_utils';
import {getIds} from '../db/ReadOnlyManager';
import {
abiwordAvailable,
automaticReconnectionTimeout,
commitRateLimiting,
cookie,
disableIPlogging,
exportAvailable,
indentationOnNewLine,
padOptions,
padShortcutEnabled,
randomVersionString,
scrollWhenFocusLineIsOutOfViewport,
skinName,
skinVariants,
sofficeAvailable
} from '../utils/Settings';
import plugins from '../../static/js/pluginfw/plugin_defs.js';
import log4js from "log4js";
import hooks from '../../static/js/pluginfw/hooks.js';
import {createCollection} from '../stats';
import {strict as assert} from "assert";
import {RateLimiterMemory} from 'rate-limiter-flexible';
import webaccess from '../hooks/express/webaccess';
import {ErrorCaused} from "../models/ErrorCaused";
import {Pad} from "../db/Pad";
import {SessionInfo} from "../models/SessionInfo";
const securityManager = require('../db/SecurityManager');
const plugins = require('../../static/js/pluginfw/plugin_defs.js');
const log4js = require('log4js');
const messageLogger = log4js.getLogger('message');
const accessLogger = log4js.getLogger('access');
const hooks = require('../../static/js/pluginfw/hooks.js');
const stats = require('../stats');
const assert = require('assert').strict;
const {RateLimiterMemory} = require('rate-limiter-flexible');
const webaccess = require('../hooks/express/webaccess');
let rateLimiter;
let socketio = null;
hooks.deprecationNotices.clientReady = 'use the userJoin hook instead';
const addContextToError = (err, pfx) => {
const newErr = new Error(`${pfx}${err.message}`, {cause: err});
const addContextToError = (err: Error, pfx) => {
const newErr = new ErrorCaused(`${pfx}${err.message}`, err);
if (Error.captureStackTrace) Error.captureStackTrace(newErr, addContextToError);
// Check for https://github.com/tc39/proposal-error-cause support, available in Node.js >= v16.10.
if (newErr.cause === err) return newErr;
@ -54,11 +82,11 @@ const addContextToError = (err, pfx) => {
return err;
};
exports.socketio = () => {
export const socketiofn = () => {
// The rate limiter is created in this hook so that restarting the server resets the limiter. The
// settings.commitRateLimiting object is passed directly to the rate limiter so that the limits
// can be dynamically changed during runtime by modifying its properties.
rateLimiter = new RateLimiterMemory(settings.commitRateLimiting);
rateLimiter = new RateLimiterMemory(commitRateLimiting);
};
/**
@ -79,11 +107,10 @@ exports.socketio = () => {
* - readonly: Whether the client has read-only access (true) or read/write access (false).
* - rev: The last revision that was sent to the client.
*/
const sessioninfos = {};
exports.sessioninfos = sessioninfos;
export const sessioninfos: SessionInfo = {};
stats.gauge('totalUsers', () => socketio ? Object.keys(socketio.sockets.sockets).length : 0);
stats.gauge('activePads', () => {
createCollection.gauge('totalUsers', () => socketio ? Object.keys(socketio.sockets.sockets).length : 0);
createCollection.gauge('activePads', () => {
const padIds = new Set();
for (const {padId} of Object.values(sessioninfos)) {
if (!padId) continue;
@ -96,6 +123,8 @@ stats.gauge('activePads', () => {
* Processes one task at a time per channel.
*/
class Channels {
private readonly _exec: (ch, task) => any;
private _promiseChains: Map<any, any>;
/**
* @param {(ch, task) => any} [exec] - Task executor. If omitted, tasks are assumed to be
* functions that will be executed with the channel as the only argument.
@ -144,10 +173,14 @@ exports.setSocketIO = (socket_io) => {
* @param socket the socket.io Socket object for the new connection from the client
*/
exports.handleConnect = (socket) => {
stats.meter('connects').mark();
createCollection.meter('connects').mark();
// Initialize sessioninfos for this new session
sessioninfos[socket.id] = {};
sessioninfos[socket.id] = {
rev: 0, time: undefined,
auth: {padID: undefined, sessionID: undefined, token: undefined},
readOnlyPadId: undefined,
padId:undefined,readonly:false,author:undefined};
};
/**
@ -168,17 +201,17 @@ exports.kickSessionsFromPad = (padID) => {
* @param socket the socket.io Socket object for the client
*/
exports.handleDisconnect = async (socket) => {
stats.meter('disconnects').mark();
createCollection.meter('disconnects').mark();
const session = sessioninfos[socket.id];
delete sessioninfos[socket.id];
// session.padId can be nullish if the user disconnects before sending CLIENT_READY.
if (!session || !session.author || !session.padId) return;
const {session: {user} = {}} = socket.client.request;
const {session: {user} = {}}: SessionSocketModel = socket.client.request;
/* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */
accessLogger.info('[LEAVE]' +
` pad:${session.padId}` +
` socket:${socket.id}` +
` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` +
` IP:${disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` +
` authorID:${session.author}` +
(user && user.username ? ` username:${user.username}` : ''));
/* eslint-enable prefer-template */
@ -187,7 +220,7 @@ exports.handleDisconnect = async (socket) => {
data: {
type: 'USER_LEAVE',
userInfo: {
colorId: await authorManager.getAuthorColorId(session.author),
colorId: await getAuthorColorId(session.author),
userId: session.author,
},
},
@ -214,7 +247,7 @@ exports.handleMessage = async (socket, message) => {
} catch (err) {
messageLogger.warn(`Rate limited IP ${socket.request.ip}. To reduce the amount of rate ` +
'limiting that happens edit the rateLimit values in settings.json');
stats.meter('rateLimited').mark();
createCollection.meter('rateLimited').mark();
socket.json.send({disconnect: 'rateLimited'});
throw err;
}
@ -235,7 +268,7 @@ exports.handleMessage = async (socket, message) => {
padID: message.padId,
token: message.token,
};
const padIds = await readOnlyManager.getIds(thisSession.auth.padID);
const padIds = await getIds(thisSession.auth.padID);
thisSession.padId = padIds.padId;
thisSession.readOnlyPadId = padIds.readOnlyPadId;
thisSession.readonly =
@ -252,12 +285,12 @@ exports.handleMessage = async (socket, message) => {
const auth = thisSession.auth;
if (!auth) {
const ip = settings.disableIPlogging ? 'ANONYMOUS' : (socket.request.ip || '<unknown>');
const ip = disableIPlogging ? 'ANONYMOUS' : (socket.request.ip || '<unknown>');
const msg = JSON.stringify(message, null, 2);
throw new Error(`pre-CLIENT_READY message from IP ${ip}: ${msg}`);
}
const {session: {user} = {}} = socket.client.request;
const {session: {user} = {}}:SessionSocketModel = socket.client.request;
const {accessStatus, authorID} =
await securityManager.checkAccess(auth.padID, auth.sessionID, auth.token, user);
if (accessStatus !== 'grant') {
@ -269,7 +302,7 @@ exports.handleMessage = async (socket, message) => {
throw new Error([
'Author ID changed mid-session. Bad or missing token or sessionID?',
`socket:${socket.id}`,
`IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}`,
`IP:${disableIPlogging ? 'ANONYMOUS' : socket.request.ip}`,
`originalAuthorID:${thisSession.author}`,
`newAuthorID:${authorID}`,
...(user && user.username) ? [`username:${user.username}`] : [],
@ -331,7 +364,7 @@ exports.handleMessage = async (socket, message) => {
try {
switch (type) {
case 'USER_CHANGES':
stats.counter('pendingEdits').inc();
createCollection.counter('pendingEdits').inc();
await padChannels.enqueue(thisSession.padId, {socket, message});
break;
case 'USERINFO_UPDATE': await handleUserInfoUpdate(socket, message); break;
@ -372,7 +405,7 @@ exports.handleMessage = async (socket, message) => {
*/
const handleSaveRevisionMessage = async (socket, message) => {
const {padId, author: authorId} = sessioninfos[socket.id];
const pad = await padManager.getPad(padId, null, authorId);
const pad = await getPad(padId, null, authorId);
await pad.addSavedRevision(pad.head, authorId);
};
@ -401,7 +434,7 @@ exports.handleCustomObjectMessage = (msg, sessionID) => {
* @param padID {Pad} the pad to which we're sending this message
* @param msgString {String} the message we're sending
*/
exports.handleCustomMessage = (padID, msgString) => {
export const handleCustomMessage = (padID, msgString) => {
const time = Date.now();
const msg = {
type: 'COLLABROOM',
@ -424,7 +457,7 @@ const handleChatMessage = async (socket, message) => {
// Don't trust the user-supplied values.
chatMessage.time = Date.now();
chatMessage.authorId = authorId;
await exports.sendChatMessageToPadClients(chatMessage, padId);
await sendChatMessageToPadClients(chatMessage, padId);
};
/**
@ -438,15 +471,15 @@ const handleChatMessage = async (socket, message) => {
* @param {string} [padId] - The destination pad ID. Deprecated; pass a chat message
* object as the first argument and the destination pad ID as the second argument instead.
*/
exports.sendChatMessageToPadClients = async (mt, puId, text = null, padId = null) => {
export const sendChatMessageToPadClients = async (mt, puId, text = null, padId = null) => {
const message = mt instanceof ChatMessage ? mt : new ChatMessage(text, puId, mt);
padId = mt instanceof ChatMessage ? puId : padId;
const pad = await padManager.getPad(padId, null, message.authorId);
const pad = await getPad(padId, null, message.authorId);
await hooks.aCallAll('chatNewMessage', {message, pad, padId});
// pad.appendChatMessage() ignores the displayName property so we don't need to wait for
// authorManager.getAuthorName() to resolve before saving the message to the database.
const promise = pad.appendChatMessage(message);
message.displayName = await authorManager.getAuthorName(message.authorId);
message.displayName = await getAuthorName(message.authorId);
socketio.sockets.in(padId).json.send({
type: 'COLLABROOM',
data: {type: 'CHAT_MESSAGE', message},
@ -465,7 +498,7 @@ const handleGetChatMessages = async (socket, {data: {start, end}}) => {
const count = end - start;
if (count < 0 || count > 100) throw new Error(`invalid number of messages: ${count}`);
const {padId, author: authorId} = sessioninfos[socket.id];
const pad = await padManager.getPad(padId, null, authorId);
const pad = await getPad(padId, null, authorId);
const chatMessages = await pad.getChatMessages(start, end);
const infoMsg = {
@ -517,8 +550,8 @@ const handleUserInfoUpdate = async (socket, {data: {userInfo: {name, colorId}}})
// Tell the authorManager about the new attributes
const p = Promise.all([
authorManager.setAuthorColorId(author, colorId),
authorManager.setAuthorName(author, name),
setAuthorColorId(author, colorId),
setAuthorName(author, name),
]);
const padId = session.padId;
@ -555,7 +588,7 @@ const handleUserInfoUpdate = async (socket, {data: {userInfo: {name, colorId}}})
*/
const handleUserChanges = async (socket, message) => {
// This one's no longer pending, as we're gonna process it now
stats.counter('pendingEdits').dec();
createCollection.counter('pendingEdits').dec();
// The client might disconnect between our callbacks. We should still
// finish processing the changeset, so keep a reference to the session.
@ -567,14 +600,14 @@ const handleUserChanges = async (socket, message) => {
if (!thisSession) throw new Error('client disconnected');
// Measure time to process edit
const stopWatch = stats.timer('edits').start();
const stopWatch = createCollection.timer('edits').start();
try {
const {data: {baseRev, apool, changeset}} = message;
if (baseRev == null) throw new Error('missing baseRev');
if (apool == null) throw new Error('missing apool');
if (changeset == null) throw new Error('missing changeset');
const wireApool = (new AttributePool()).fromJsonable(apool);
const pad = await padManager.getPad(thisSession.padId, null, thisSession.author);
const pad = await getPad(thisSession.padId, null, thisSession.author);
// Verify that the changeset has valid syntax and is in canonical form
Changeset.checkRep(changeset);
@ -654,7 +687,7 @@ const handleUserChanges = async (socket, message) => {
await exports.updatePadClients(pad);
} catch (err) {
socket.json.send({disconnect: 'badChangeset'});
stats.meter('failedChangesets').mark();
createCollection.meter('failedChangesets').mark();
messageLogger.warn(`Failed to apply USER_CHANGES from author ${thisSession.author} ` +
`(socket ${socket.id}) on pad ${thisSession.padId}: ${err.stack || err}`);
} finally {
@ -662,7 +695,7 @@ const handleUserChanges = async (socket, message) => {
}
};
exports.updatePadClients = async (pad) => {
export const updatePadClients = async (pad) => {
// skip this if no-one is on this pad
const roomSockets = _getRoomSockets(pad.id);
if (roomSockets.length === 0) return;
@ -784,13 +817,13 @@ const handleClientReady = async (socket, message) => {
authorColorId = null;
}
await Promise.all([
authorName && authorManager.setAuthorName(sessionInfo.author, authorName),
authorColorId && authorManager.setAuthorColorId(sessionInfo.author, authorColorId),
authorName && setAuthorName(sessionInfo.author, authorName),
authorColorId && setAuthorColorId(sessionInfo.author, authorColorId),
]);
({colorId: authorColorId, name: authorName} = await authorManager.getAuthor(sessionInfo.author));
({colorId: authorColorId, name: authorName} = await getAuthor(sessionInfo.author));
// load the pad-object from the database
const pad = await padManager.getPad(sessionInfo.padId, null, sessionInfo.author);
const pad = await createCollection.getPad(sessionInfo.padId, null, sessionInfo.author);
// these db requests all need the pad object (timestamp of latest revision, author data)
const authors = pad.getAllAuthors();
@ -801,7 +834,7 @@ const handleClientReady = async (socket, message) => {
// get all author data out of the database (in parallel)
const historicalAuthorData = {};
await Promise.all(authors.map(async (authorId) => {
const author = await authorManager.getAuthor(authorId);
const author = await getAuthor(authorId);
if (!author) {
messageLogger.error(`There is no author for authorId: ${authorId}. ` +
'This is possibly related to https://github.com/ether/etherpad-lite/issues/2802');
@ -825,18 +858,18 @@ const handleClientReady = async (socket, message) => {
const sinfo = sessioninfos[otherSocket.id];
if (sinfo && sinfo.author === sessionInfo.author) {
// fix user's counter, works on page refresh or if user closes browser window and then rejoins
sessioninfos[otherSocket.id] = {};
sessioninfos[otherSocket.id] = undefined
otherSocket.leave(sessionInfo.padId);
otherSocket.json.send({disconnect: 'userdup'});
}
}
const {session: {user} = {}} = socket.client.request;
const {session: {user} = {}}:SessionSocketModel = socket.client.request;
/* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */
accessLogger.info(`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` +
` pad:${sessionInfo.padId}` +
` socket:${socket.id}` +
` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` +
` IP:${disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` +
` authorID:${sessionInfo.author}` +
(user && user.username ? ` username:${user.username}` : ''));
/* eslint-enable prefer-template */
@ -922,13 +955,13 @@ const handleClientReady = async (socket, message) => {
// Warning: never ever send sessionInfo.padId to the client. If the client is read only you
// would open a security hole 1 swedish mile wide...
const clientVars = {
skinName: settings.skinName,
skinVariants: settings.skinVariants,
randomVersionString: settings.randomVersionString,
skinName: skinName,
skinVariants: skinVariants,
randomVersionString: randomVersionString,
accountPrivs: {
maxRevisions: 100,
},
automaticReconnectionTimeout: settings.automaticReconnectionTimeout,
automaticReconnectionTimeout: automaticReconnectionTimeout,
initialRevisionList: [],
initialOptions: {},
savedRevisions: pad.getSavedRevisions(),
@ -941,12 +974,12 @@ const handleClientReady = async (socket, message) => {
rev: pad.getHeadRevisionNumber(),
time: currentTime,
},
colorPalette: authorManager.getColorPalette(),
colorPalette: getColorPalette(),
clientIp: '127.0.0.1',
userColor: authorColorId,
padId: sessionInfo.auth.padID,
padOptions: settings.padOptions,
padShortcutEnabled: settings.padShortcutEnabled,
padOptions: padOptions,
padShortcutEnabled: padShortcutEnabled,
initialTitle: `Pad: ${sessionInfo.auth.padID}`,
opts: {},
// tell the client the number of the latest chat-message, which will be
@ -956,29 +989,30 @@ const handleClientReady = async (socket, message) => {
readOnlyId: sessionInfo.readOnlyPadId,
readonly: sessionInfo.readonly,
serverTimestamp: Date.now(),
sessionRefreshInterval: settings.cookie.sessionRefreshInterval,
sessionRefreshInterval: cookie.sessionRefreshInterval,
userId: sessionInfo.author,
abiwordAvailable: settings.abiwordAvailable(),
sofficeAvailable: settings.sofficeAvailable(),
exportAvailable: settings.exportAvailable(),
abiwordAvailable: abiwordAvailable(),
sofficeAvailable: sofficeAvailable(),
exportAvailable: exportAvailable(),
plugins: {
plugins: plugins.plugins,
parts: plugins.parts,
},
indentationOnNewLine: settings.indentationOnNewLine,
indentationOnNewLine: indentationOnNewLine,
scrollWhenFocusLineIsOutOfViewport: {
percentage: {
editionAboveViewport:
settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport,
scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport,
editionBelowViewport:
settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport,
scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport,
},
duration: settings.scrollWhenFocusLineIsOutOfViewport.duration,
duration: scrollWhenFocusLineIsOutOfViewport.duration,
scrollWhenCaretIsInTheLastLineOfViewport:
settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport,
scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport,
percentageToScrollWhenUserPressesArrowUp:
settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
},
userName: undefined,
initialChangesets: [], // FIXME: REMOVE THIS SHIT
};
@ -1034,7 +1068,7 @@ const handleClientReady = async (socket, message) => {
if (authorId == null) return;
// reuse previously created cache of author's data
const authorInfo = historicalAuthorData[authorId] || await authorManager.getAuthor(authorId);
const authorInfo = historicalAuthorData[authorId] || await getAuthor(authorId);
if (authorInfo == null) {
messageLogger.error(
`Author ${authorId} connected via socket.io session ${roomSocket.id} is missing from ` +
@ -1079,7 +1113,7 @@ const handleChangesetRequest = async (socket, {data: {granularity, start, reques
if (requestID == null) throw new Error('mising requestID');
const end = start + (100 * granularity);
const {padId, author: authorId} = sessioninfos[socket.id];
const pad = await padManager.getPad(padId, null, authorId);
const pad = await createCollection.getPad(padId, null, authorId);
const data = await getChangesetInfo(pad, start, end, granularity);
data.requestID = requestID;
socket.json.send({type: 'CHANGESET_REQ', data});
@ -1089,7 +1123,7 @@ const handleChangesetRequest = async (socket, {data: {granularity, start, reques
* Tries to rebuild the getChangestInfo function of the original Etherpad
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144
*/
const getChangesetInfo = async (pad, startNum, endNum, granularity) => {
const getChangesetInfo = async (pad: Pad, startNum: number, endNum: number, granularity: number) => {
const headRevision = pad.getHeadRevisionNumber();
// calculate the last full endnum
@ -1120,8 +1154,7 @@ const getChangesetInfo = async (pad, startNum, endNum, granularity) => {
getPadLines(pad, startNum - 1),
// Get all needed composite Changesets.
...compositesChangesetNeeded.map(async (item) => {
const changeset = await composePadChangesets(pad, item.start, item.end);
composedChangesets[`${item.start}/${item.end}`] = changeset;
composedChangesets[`${item.start}/${item.end}`] = await composePadChangesets(pad, item.start, item.end);
}),
// Get all needed revision Dates.
...revTimesNeeded.map(async (revNum) => {
@ -1159,7 +1192,9 @@ const getChangesetInfo = async (pad, startNum, endNum, granularity) => {
return {forwardsChangesets, backwardsChangesets,
apool: apool.toJsonable(), actualEndNum: endNum,
timeDeltas, start: startNum, granularity};
timeDeltas, start: startNum, granularity, requestID: undefined
};
};
/**
@ -1238,21 +1273,21 @@ const _getRoomSockets = (padID) => {
/**
* Get the number of users in a pad
*/
exports.padUsersCount = (padID) => ({
export const padUsersCount = (padID) => ({
padUsersCount: _getRoomSockets(padID).length,
});
/**
* Get the list of users in a pad
*/
exports.padUsers = async (padID) => {
export const padUsers = async (padID) => {
const padUsers = [];
// iterate over all clients (in parallel)
await Promise.all(_getRoomSockets(padID).map(async (roomSocket) => {
const s = sessioninfos[roomSocket.id];
if (s) {
const author = await authorManager.getAuthor(s.author);
const author = await getAuthor(s.author);
// Fixes: https://github.com/ether/etherpad-lite/issues/4120
// On restart author might not be populated?
if (author) {
@ -1263,6 +1298,4 @@ exports.padUsers = async (padID) => {
}));
return {padUsers};
};
exports.sessioninfos = sessioninfos;
}

View File

@ -20,9 +20,10 @@
* limitations under the License.
*/
const log4js = require('log4js');
const settings = require('../utils/Settings');
const stats = require('../stats');
import log4js from 'log4js';
import {disableIPlogging} from "../utils/Settings";
import {createCollection} from '../stats';
const logger = log4js.getLogger('socket.io');
@ -53,7 +54,7 @@ exports.setSocketIO = (_io) => {
io = _io;
io.sockets.on('connection', (socket) => {
const ip = settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip;
const ip = disableIPlogging ? 'ANONYMOUS' : socket.request.ip;
logger.debug(`${socket.id} connected from IP ${ip}`);
// wrap the original send function to log the messages
@ -68,14 +69,14 @@ exports.setSocketIO = (_io) => {
components[i].handleConnect(socket);
}
socket.on('message', (message, ack = () => {}) => (async () => {
socket.on('message', (message, ack = (p: { name: any; message: 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),
(val) => ack({name: val.name, message: val.message}),
(err) => {
logger.error(
`Error handling ${message.component} message from ${socket.id}: ${err.stack || err}`);
@ -88,7 +89,7 @@ exports.setSocketIO = (_io) => {
// 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());
createCollection.gauge('lastDisconnect', () => Date.now());
// tell all components about this disconnect
for (const i of Object.keys(components)) {
components[i].handleDisconnect(socket);

View File

@ -1,10 +0,0 @@
'use strict';
const eejs = require('../../eejs');
exports.expressCreateServer = (hookName, args, cb) => {
args.app.get('/admin', (req, res) => {
if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/');
res.send(eejs.require('ep_etherpad-lite/templates/admin/index.html', {req}));
});
return cb();
};

View File

@ -0,0 +1,10 @@
'use strict';
import {required} from '../../eejs';
export const expressCreateServer = (hookName:string, args, cb) => {
args.app.get('/admin', (req, res) => {
if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/');
res.send(required('ep_etherpad-lite/templates/admin/index.html', {req}));
});
return cb();
};

View File

@ -1,16 +1,21 @@
'use strict';
const eejs = require('../../eejs');
const settings = require('../../utils/Settings');
const installer = require('../../../static/js/pluginfw/installer');
const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
const plugins = require('../../../static/js/pluginfw/plugins');
const semver = require('semver');
const UpdateCheck = require('../../utils/UpdateCheck');
import {required} from '../../eejs';
import {getEpVersion, getGitCommit} from "../../utils/Settings";
exports.expressCreateServer = (hookName, args, cb) => {
import installer from "../../../static/js/pluginfw/installer";
import pluginDefs from "../../../static/js/pluginfw/plugin_defs";
import plugins from "../../../static/js/pluginfw/plugins";
import semver from "semver";
import UpdateCheck from "../../utils/UpdateCheck";
export const expressCreateServer = (hookName, args, cb) => {
args.app.get('/admin/plugins', (req, res) => {
res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins.html', {
res.send(required('ep_etherpad-lite/templates/admin/plugins.html', {
plugins: pluginDefs.plugins,
req,
errors: [],
@ -18,10 +23,10 @@ exports.expressCreateServer = (hookName, args, cb) => {
});
args.app.get('/admin/plugins/info', (req, res) => {
const gitCommit = settings.getGitCommit();
const epVersion = settings.getEpVersion();
const gitCommit = getGitCommit();
const epVersion = getEpVersion();
res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins-info.html', {
res.send(required('ep_etherpad-lite/templates/admin/plugins-info.html', {
gitCommit,
epVersion,
installedPlugins: `<pre>${plugins.formatPlugins().replace(/, /g, '\n')}</pre>`,
@ -36,10 +41,10 @@ exports.expressCreateServer = (hookName, args, cb) => {
return cb();
};
exports.socketio = (hookName, args, cb) => {
export const socketio = (hookName, args, cb) => {
const io = args.io.of('/pluginfw/installer');
io.on('connection', (socket) => {
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
const {session: {user: {is_admin: isAdmin} = {}} = {}}:SessionSocketModel = socket.conn.request;
if (!isAdmin) return;
socket.on('getInstalled', (query) => {

View File

@ -1,14 +1,18 @@
'use strict';
const eejs = require('../../eejs');
const fsp = require('fs').promises;
const hooks = require('../../../static/js/pluginfw/hooks');
const plugins = require('../../../static/js/pluginfw/plugins');
const settings = require('../../utils/Settings');
import {required} from '../../eejs';
import {promises as fsp} from "fs";
import hooks from "../../../static/js/pluginfw/hooks";
import plugins from "../../../static/js/pluginfw/plugins";
import {reloadSettings, settingsFilename, showSettingsInAdminPage} from "../../utils/Settings";
import * as settings from "../../utils/Settings";
exports.expressCreateServer = (hookName, {app}) => {
app.get('/admin/settings', (req, res) => {
res.send(eejs.require('ep_etherpad-lite/templates/admin/settings.html', {
res.send(required('ep_etherpad-lite/templates/admin/settings.html', {
req,
settings: '',
errors: [],
@ -18,18 +22,20 @@ exports.expressCreateServer = (hookName, {app}) => {
exports.socketio = (hookName, {io}) => {
io.of('/settings').on('connection', (socket) => {
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
const {session: {user: {is_admin: isAdmin} = {}} = {}}:SessionSocketModel = socket.conn.request;
if (!isAdmin) return;
socket.on('load', async (query) => {
let data;
try {
data = await fsp.readFile(settings.settingsFilename, 'utf8');
data = await fsp.readFile(settingsFilename, 'utf8');
} catch (err) {
return console.log(err);
}
// if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result
if (settings.showSettingsInAdminPage === false) {
//FIXME Is this intentional to never change
// @ts-ignore
if (showSettingsInAdminPage === false) {
socket.emit('settings', {results: 'NOT_ALLOWED'});
} else {
socket.emit('settings', {results: data});
@ -37,15 +43,15 @@ exports.socketio = (hookName, {io}) => {
});
socket.on('saveSettings', async (newSettings) => {
await fsp.writeFile(settings.settingsFilename, newSettings);
await fsp.writeFile(settingsFilename, newSettings);
socket.emit('saveprogress', 'saved');
});
socket.on('restartServer', async () => {
console.log('Admin request to restart server through a socket on /admin/settings');
settings.reloadSettings();
reloadSettings();
await plugins.update();
await hooks.aCallAll('loadSettings', {settings});
await hooks.aCallAll('loadSettings', {});
await hooks.aCallAll('restartServer');
});
});

View File

@ -1,12 +1,15 @@
'use strict';
const log4js = require('log4js');
const clientLogger = log4js.getLogger('client');
const {Formidable} = require('formidable');
const apiHandler = require('../../handler/APIHandler');
const util = require('util');
import log4js from "log4js";
exports.expressPreSession = async (hookName, {app}) => {
import {Formidable} from "formidable";
import {latestApiVersion} from "../../handler/APIHandler";
import util from "util";
const clientLogger = log4js.getLogger('client');
export const expressPreSession = async (hookName, {app}) => {
// The Etherpad client side sends information about how a disconnect happened
app.post('/ep/pad/connection-diagnostic-info', (req, res) => {
new Formidable().parse(req, (err, fields, files) => {
@ -26,7 +29,7 @@ exports.expressPreSession = async (hookName, {app}) => {
// The Etherpad client side sends information about client side javscript errors
app.post('/jserror', (req, res, next) => {
(async () => {
const data = JSON.parse(await parseJserrorForm(req));
const data = JSON.parse(<string>await parseJserrorForm(req));
clientLogger.warn(`${data.msg} --`, {
[util.inspect.custom]: (depth, options) => {
// Depth is forced to infinity to ensure that all of the provided data is logged.
@ -40,6 +43,6 @@ exports.expressPreSession = async (hookName, {app}) => {
// Provide a possibility to query the latest available API version
app.get('/api', (req, res) => {
res.json({currentVersion: apiHandler.latestApiVersion});
res.json({currentVersion: latestApiVersion});
});
};

View File

@ -1,6 +1,6 @@
'use strict';
const stats = require('../../stats');
import {createCollection} from '../../stats';
exports.expressCreateServer = (hook_name, args, cb) => {
exports.app = args.app;
@ -12,7 +12,7 @@ exports.expressCreateServer = (hook_name, args, cb) => {
// allowing you to respond however you like
res.status(500).send({error: 'Sorry, something bad happened!'});
console.error(err.stack ? err.stack : err.toString());
stats.meter('http500').mark();
createCollection.meter('http500').mark();
});
return cb();

View File

@ -1,23 +1,23 @@
'use strict';
const hasPadAccess = require('../../padaccess');
const settings = require('../../utils/Settings');
const exportHandler = require('../../handler/ExportHandler');
const importHandler = require('../../handler/ImportHandler');
const padManager = require('../../db/PadManager');
const readOnlyManager = require('../../db/ReadOnlyManager');
const rateLimit = require('express-rate-limit');
const securityManager = require('../../db/SecurityManager');
const webaccess = require('./webaccess');
import hasPadAccess from '../../padaccess';
import {exportAvailable, importExportRateLimiting} from '../../utils/Settings';
import {doExport} from '../../handler/ExportHandler';
import {doImport2} from '../../handler/ImportHandler';
import {doesPadExist} from '../../db/PadManager';
import {getPadId, isReadOnlyId} from '../../db/ReadOnlyManager';
import rateLimit from 'express-rate-limit';
import {checkAccess} from '../../db/SecurityManager';
import webaccess from './webaccess';
exports.expressCreateServer = (hookName, args, cb) => {
settings.importExportRateLimiting.onLimitReached = (req, res, options) => {
importExportRateLimiting.onLimitReached = (req, res, options) => {
// when the rate limiter triggers, write a warning in the logs
console.warn('Import/Export rate limiter triggered on ' +
`"${req.originalUrl}" for IP address ${req.ip}`);
};
// The rate limiter is created in this hook so that restarting the server resets the limiter.
const limiter = rateLimit(settings.importExportRateLimiting);
const limiter = rateLimit(importExportRateLimiting);
// handle export requests
args.app.use('/p/:pad/:rev?/export/:type', limiter);
@ -30,7 +30,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
}
// if abiword is disabled, and this is a format we only support with abiword, output a message
if (settings.exportAvailable() === 'no' &&
if (exportAvailable() === 'no' &&
['odt', 'pdf', 'doc'].indexOf(req.params.type) !== -1) {
console.error(`Impossible to export pad "${req.params.pad}" in ${req.params.type} format.` +
' There is no converter configured');
@ -48,19 +48,19 @@ exports.expressCreateServer = (hookName, args, cb) => {
let padId = req.params.pad;
let readOnlyId = null;
if (readOnlyManager.isReadOnlyId(padId)) {
if (isReadOnlyId(padId)) {
readOnlyId = padId;
padId = await readOnlyManager.getPadId(readOnlyId);
padId = await getPadId(readOnlyId);
}
const exists = await padManager.doesPadExists(padId);
const exists = await doesPadExist(padId);
if (!exists) {
console.warn(`Someone tried to export a pad that doesn't exist (${padId})`);
return next();
}
console.log(`Exporting pad "${req.params.pad}" in ${req.params.type} format`);
await exportHandler.doExport(req, res, padId, readOnlyId, req.params.type);
await doExport(req, res, padId, readOnlyId, req.params.type);
}
})().catch((err) => next(err || new Error(err)));
});
@ -69,13 +69,13 @@ exports.expressCreateServer = (hookName, args, cb) => {
args.app.use('/p/:pad/import', limiter);
args.app.post('/p/:pad/import', (req, res, next) => {
(async () => {
const {session: {user} = {}} = req;
const {accessStatus, authorID: authorId} = await securityManager.checkAccess(
const {session: {user} = {}}:SessionSocketModel = req;
const {accessStatus, authorID: authorId} = await checkAccess(
req.params.pad, req.cookies.sessionID, req.cookies.token, user);
if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) {
return res.status(403).send('Forbidden');
}
await importHandler.doImport(req, res, req.params.pad, authorId);
await doImport2(req, res, req.params.pad, authorId);
})().catch((err) => next(err || new Error(err)));
});

View File

@ -1,5 +1,8 @@
'use strict';
import {ResponseSpec} from "../../models/ResponseSpec";
import {APIResource} from "../../models/APIResource";
/**
* node/hooks/express/openapi.js
*
@ -54,7 +57,7 @@ const APIPathStyle = {
};
// API resources - describe your API endpoints here
const resources = {
const resources: APIResource = {
// Group
group: {
create: {
@ -375,7 +378,8 @@ const defaultResponses = {
const defaultResponseRefs = {
200: {
$ref: '#/components/responses/Success',
$ref: '#/components/responses/Success', content: undefined
},
400: {
$ref: '#/components/responses/ApiError',
@ -491,7 +495,11 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
// build operations
for (const funcName of Object.keys(apiHandler.version[version])) {
let operation = {};
let operation: ResponseSpec = {
parameters: undefined,
_restPath: "",
operationId: undefined
};
if (operations[funcName]) {
operation = {...operations[funcName]};
} else {

View File

@ -0,0 +1,48 @@
import {ResponseSpec} from "./ResponseSpec";
export type APIResource = {
group:{
create:ResponseSpec,
createIfNotExistsFor: ResponseSpec,
delete: ResponseSpec,
listPads: ResponseSpec,
createPad: ResponseSpec,
listSessions: ResponseSpec,
list: ResponseSpec,
},
author: {
create: ResponseSpec,
createIfNotExistsFor: ResponseSpec,
listPads: ResponseSpec,
listSessions: ResponseSpec,
getName: ResponseSpec,
},
session:{
create: ResponseSpec,
delete: ResponseSpec,
info: ResponseSpec,
},
pad:{
listAll: ResponseSpec,
createDiffHTML: ResponseSpec,
create: ResponseSpec,
getText: ResponseSpec,
setText: ResponseSpec,
getHTML: ResponseSpec,
setHTML: ResponseSpec,
getRevisionsCount: ResponseSpec,
getLastEdited: ResponseSpec,
delete: ResponseSpec,
getReadOnlyID: ResponseSpec,
setPublicStatus: ResponseSpec,
getPublicStatus: ResponseSpec,
authors: ResponseSpec,
usersCount: ResponseSpec,
users: ResponseSpec,
sendClientsMessage: ResponseSpec,
checkToken: ResponseSpec,
getChatHistory: ResponseSpec,
getChatHead: ResponseSpec,
appendChatMessage: ResponseSpec,
}
}

View File

@ -0,0 +1,14 @@
import ts from "typescript/lib/tsserverlibrary";
export class ErrorCaused extends Error{
cause: Error;
constructor(message: string, cause: Error) {
super(message);
this.cause = cause;
this.name = "ErrorCaused";
}
}
type ErrorCause = {
}

View File

@ -0,0 +1,6 @@
export type Plugin = {
package: {
name: string,
version: string
}
}

View File

@ -0,0 +1,43 @@
export type ResponseSpec = {
parameters?: ParameterSpec[],
_restPath?: string,
operationId?: string,
responses?: any
description?: string,
summary?: string,
responseSchema?:{
groupID?: APIResponseSpecType
groupIDs?: APIResponseSpecType
padIDs?: APIResponseSpecType,
sessions?: APIResponseSpecType,
authorID?: APIResponseSpecType,
info?: APIResponseSpecType,
sessionID?: APIResponseSpecType,
text?: APIResponseSpecType,
html?: APIResponseSpecType,
revisions?: APIResponseSpecType,
lastEdited?: APIResponseSpecType,
readOnlyID?: APIResponseSpecType,
publicStatus?: APIResponseSpecType,
authorIDs?: APIResponseSpecType,
padUsersCount?: APIResponseSpecType,
padUsers?: APIResponseSpecType,
messages?: APIResponseSpecType,
chatHead?: APIResponseSpecType,
}
}
export type APIResponseSpecType = {
type?:string,
items?: {
type?:string,
$ref?:string
},
$ref?:string
}
export type ParameterSpec = {
$ref?: string,
}

View File

@ -0,0 +1,7 @@
export type Revision = {
revNum: number,
savedById: string,
label: string,
timestamp: number,
id: string,
}

View File

@ -0,0 +1,10 @@
export type SessionInfo = {
[key: string]:{
time: any;
rev: number;
readOnlyPadId: any;
auth: { padID: any; sessionID: any; token: any };
readonly: boolean;
padId: string,
author: string
}}

View File

@ -0,0 +1,5 @@
export type SessionModel = {
cookie: {
expires?: string
}
}

View File

@ -0,0 +1,8 @@
type SessionSocketModel = {
session:{
user?: {
username?: string,
is_admin?: boolean
}
}
}

View File

@ -1,10 +1,10 @@
'use strict';
const securityManager = require('./db/SecurityManager');
import {checkAccess} from './db/SecurityManager';
// checks for padAccess
module.exports = async (req, res) => {
const {session: {user} = {}} = req;
const accessObj = await securityManager.checkAccess(
export default async (req, res) => {
const {session: {user} = {}}:SessionSocketModel = req;
const accessObj = await checkAccess(
req.params.pad, req.cookies.sessionID, req.cookies.token, user);
if (accessObj.accessStatus === 'grant') {

89
src/node/server.js → src/node/server.ts Executable file → Normal file
View File

@ -24,10 +24,16 @@
* limitations under the License.
*/
const log4js = require('log4js');
log4js.replaceConsole();
const settings = require('./utils/Settings');
import log4js from 'log4js';
import * as settings from "./utils/Settings";
/*
* early check for version compatibility before calling
* any modules that require newer versions of NodeJS
*/
import {checkDeprecationStatus, enforceMinNodeVersion} from './utils/NodeVersion'
import {Gate} from './utils/promises';
import * as UpdateCheck from "./utils/UpdateCheck";
import {Plugin} from "./models/Plugin";
let wtfnode;
if (settings.dumpOnUncleanExit) {
@ -36,24 +42,20 @@ if (settings.dumpOnUncleanExit) {
wtfnode = require('wtfnode');
}
/*
* early check for version compatibility before calling
* any modules that require newer versions of NodeJS
*/
const NodeVersion = require('./utils/NodeVersion');
NodeVersion.enforceMinNodeVersion('12.17.0');
NodeVersion.checkDeprecationStatus('12.17.0', '1.9.0');
enforceMinNodeVersion('12.17.0');
checkDeprecationStatus('12.17.0', '1.9.0');
const UpdateCheck = require('./utils/UpdateCheck');
const db = require('./db/DB');
const express = require('./hooks/express');
const hooks = require('../static/js/pluginfw/hooks');
const pluginDefs = require('../static/js/pluginfw/plugin_defs');
const plugins = require('../static/js/pluginfw/plugins');
const {Gate} = require('./utils/promises');
const stats = require('./stats');
import db = require('./db/DB');
import {} from './db/DB'
import express = require('./hooks/express');
import hooks = require('../static/js/pluginfw/hooks');
import pluginDefs = require('../static/js/pluginfw/plugin_defs');
import plugins = require('../static/js/pluginfw/plugins');
import stats = require('./stats');
import {createCollection} from "./stats";
const logger = log4js.getLogger('server');
console.log = logger.info.bind(logger); // do the same for others - console.debug, etc.
const State = {
INITIAL: 1,
@ -70,20 +72,20 @@ let state = State.INITIAL;
const removeSignalListener = (signal, listener) => {
logger.debug(`Removing ${signal} listener because it might interfere with shutdown tasks. ` +
`Function code:\n${listener.toString()}\n` +
`Current stack:\n${(new Error()).stack.split('\n').slice(1).join('\n')}`);
`Function code:\n${listener.toString()}\n` +
`Current stack:\n${(new Error()).stack.split('\n').slice(1).join('\n')}`);
process.off(signal, listener);
};
let startDoneGate;
exports.start = async () => {
export const start = async () => {
switch (state) {
case State.INITIAL:
break;
case State.STARTING:
await startDoneGate;
// Retry. Don't fall through because it might have transitioned to STATE_TRANSITION_FAILED.
return await exports.start();
return await start();
case State.RUNNING:
return express.server;
case State.STOPPING:
@ -100,16 +102,16 @@ exports.start = async () => {
state = State.STARTING;
try {
// Check if Etherpad version is up-to-date
UpdateCheck.check();
UpdateCheck.default.check();
stats.gauge('memoryUsage', () => process.memoryUsage().rss);
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
createCollection.gauge('memoryUsage', () => process.memoryUsage().rss);
createCollection.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
process.on('uncaughtException', (err) => {
logger.debug(`uncaught exception: ${err.stack || err}`);
// eslint-disable-next-line promise/no-promise-in-callback
exports.exit(err)
exit(err)
.catch((err) => {
logger.error('Error in process exit', err);
// eslint-disable-next-line n/no-process-exit
@ -118,7 +120,8 @@ exports.start = async () => {
});
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => {
process.on('unhandledRejection', (err:Error) => {
logger.debug(`unhandled rejection: ${err.stack || err}`);
throw err;
});
@ -128,10 +131,10 @@ exports.start = async () => {
// done cleaning up. See https://github.com/andywer/threads.js/pull/329 for an example of a
// problematic listener. This means that exports.exit is solely responsible for performing all
// necessary cleanup tasks.
for (const listener of process.listeners(signal)) {
for (const listener of process.listeners(signal as any)) {
removeSignalListener(signal, listener);
}
process.on(signal, exports.exit);
process.on(signal, exit);
// Prevent signal listeners from being added in the future.
process.on('newListener', (event, listener) => {
if (event !== signal) return;
@ -141,7 +144,7 @@ exports.start = async () => {
await db.init();
await plugins.update();
const installedPlugins = Object.values(pluginDefs.plugins)
const installedPlugins = (Object.values(pluginDefs.plugins) as Plugin[])
.filter((plugin) => plugin.package.name !== 'ep_etherpad-lite')
.map((plugin) => `${plugin.package.name}@${plugin.package.version}`)
.join(', ');
@ -154,7 +157,7 @@ exports.start = async () => {
logger.error('Error occurred while starting Etherpad');
state = State.STATE_TRANSITION_FAILED;
startDoneGate.resolve();
return await exports.exit(err);
return await exit(err);
}
logger.info('Etherpad is running');
@ -166,12 +169,12 @@ exports.start = async () => {
};
const stopDoneGate = new Gate();
exports.stop = async () => {
export const stop = async () => {
switch (state) {
case State.STARTING:
await exports.start();
await start();
// Don't fall through to State.RUNNING in case another caller is also waiting for startup.
return await exports.stop();
return await stop();
case State.RUNNING:
break;
case State.STOPPING:
@ -201,7 +204,7 @@ exports.stop = async () => {
logger.error('Error occurred while stopping Etherpad');
state = State.STATE_TRANSITION_FAILED;
stopDoneGate.resolve();
return await exports.exit(err);
return await exit(err);
}
logger.info('Etherpad stopped');
state = State.STOPPED;
@ -210,14 +213,14 @@ exports.stop = async () => {
let exitGate;
let exitCalled = false;
exports.exit = async (err = null) => {
export const exit = async (err = null) => {
/* eslint-disable no-process-exit */
if (err === 'SIGTERM') {
// Termination from SIGTERM is not treated as an abnormal termination.
logger.info('Received SIGTERM signal');
err = null;
} else if (err != null) {
logger.error(`Metrics at time of fatal error:\n${JSON.stringify(stats.toJSON(), null, 2)}`);
logger.error(`Metrics at time of fatal error:\n${JSON.stringify(createCollection.toJSON(), null, 2)}`);
logger.error(err.stack || err.toString());
process.exitCode = 1;
if (exitCalled) {
@ -231,11 +234,11 @@ exports.exit = async (err = null) => {
case State.STARTING:
case State.RUNNING:
case State.STOPPING:
await exports.stop();
await stop();
// Don't fall through to State.STOPPED in case another caller is also waiting for stop().
// Don't pass err to exports.exit() because this err has already been processed. (If err is
// passed again to exit() then exit() will think that a second error occurred while exiting.)
return await exports.exit();
return await exit();
case State.INITIAL:
case State.STOPPED:
case State.STATE_TRANSITION_FAILED:
@ -257,13 +260,13 @@ exports.exit = async (err = null) => {
// on the timeout so that the timeout itself does not prevent Node.js from exiting.
setTimeout(() => {
logger.error('Something that should have been cleaned up during the shutdown hook (such as ' +
'a timer, worker thread, or open connection) is preventing Node.js from exiting');
'a timer, worker thread, or open connection) is preventing Node.js from exiting');
if (settings.dumpOnUncleanExit) {
wtfnode.dump();
} else {
logger.error('Enable `dumpOnUncleanExit` setting to get a dump of objects preventing a ' +
'clean exit');
'clean exit');
}
logger.error('Forcing an unclean exit...');
@ -275,4 +278,4 @@ exports.exit = async (err = null) => {
/* eslint-enable no-process-exit */
};
if (require.main === module) exports.start();
if (require.main === module) start();

View File

@ -1,9 +0,0 @@
'use strict';
const measured = require('measured-core');
module.exports = measured.createCollection();
module.exports.shutdown = async (hookName, context) => {
module.exports.end();
};

9
src/node/stats.ts Normal file
View File

@ -0,0 +1,9 @@
'use strict';
import measured from 'measured-core'
export const createCollection = measured.createCollection();
export const shutdown = async (hookName, context) => {
module.exports.end();
}

View File

@ -26,19 +26,19 @@ const semver = require('semver');
*
* @param {String} minNodeVersion Minimum required Node version
*/
exports.enforceMinNodeVersion = (minNodeVersion) => {
const enforceMinNodeVersion = (minNodeVersion:string) => {
const currentNodeVersion = process.version;
// we cannot use template literals, since we still do not know if we are
// running under Node >= 4.0
if (semver.lt(currentNodeVersion, minNodeVersion)) {
console.error(`Running Etherpad on Node ${currentNodeVersion} is not supported. ` +
`Please upgrade at least to Node ${minNodeVersion}`);
`Please upgrade at least to Node ${minNodeVersion}`);
process.exit(1);
}
console.debug(`Running on Node ${currentNodeVersion} ` +
`(minimum required Node version: ${minNodeVersion})`);
`(minimum required Node version: ${minNodeVersion})`);
};
/**
@ -49,7 +49,7 @@ exports.enforceMinNodeVersion = (minNodeVersion) => {
* @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated
* Node releases
*/
exports.checkDeprecationStatus = (lowestNonDeprecatedNodeVersion, epRemovalVersion) => {
const checkDeprecationStatus = (lowestNonDeprecatedNodeVersion:string, epRemovalVersion:string) => {
const currentNodeVersion = process.version;
if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) {
@ -58,3 +58,5 @@ exports.checkDeprecationStatus = (lowestNonDeprecatedNodeVersion, epRemovalVersi
`Please consider updating at least to Node ${lowestNonDeprecatedNodeVersion}`);
}
};
export {checkDeprecationStatus, enforceMinNodeVersion}

View File

@ -29,12 +29,12 @@
const absolutePaths = require('./AbsolutePaths');
const deepEqual = require('fast-deep-equal/es6');
const fs = require('fs');
const os = require('os');
const path = require('path');
import fs from 'fs';
import os from 'os';
import path from 'path';
const argv = require('./Cli').argv;
const jsonminify = require('jsonminify');
const log4js = require('log4js');
import jsonminify from 'jsonminify';
import log4js from 'log4js';
const randomString = require('./randomstring');
const suppressDisableMsg = ' -- To suppress these warning messages change ' +
'suppressErrorsInPadText to true in your settings.json\n';
@ -57,8 +57,8 @@ const initLogging = (logLevel, config) => {
// log4js.configure() modifies exports.logconfig so check for equality first.
const logConfigIsDefault = deepEqual(config, defaultLogConfig());
log4js.configure(config);
log4js.setGlobalLogLevel(logLevel);
log4js.replaceConsole();
log4js.getLogger("console");
console.log = logger.info.bind(logger)
// Log the warning after configuring log4js to increase the chances the user will see it.
if (!logConfigIsDefault) logger.warn('The logconfig setting is deprecated.');
};
@ -68,16 +68,16 @@ const initLogging = (logLevel, config) => {
initLogging(defaultLogLevel, defaultLogConfig());
/* Root path of the installation */
exports.root = absolutePaths.findEtherpadRoot();
export const root = absolutePaths.findEtherpadRoot();
logger.info('All relative paths will be interpreted relative to the identified ' +
`Etherpad base dir: ${exports.root}`);
exports.settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json');
exports.credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json');
`Etherpad base dir: ${exports.root}`);
export const settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json');
export const credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json');
/**
* The app title, visible e.g. in the browser window
*/
exports.title = 'Etherpad';
export const title = 'Etherpad';
/**
* Pathname of the favicon you want to use. If null, the skin's favicon is
@ -85,7 +85,7 @@ exports.title = 'Etherpad';
* is used. If this is a relative path it is interpreted as relative to the
* Etherpad root directory.
*/
exports.favicon = null;
export const favicon = null;
/*
* Skin name.
@ -93,37 +93,37 @@ exports.favicon = null;
* Initialized to null, so we can spot an old configuration file and invite the
* user to update it before falling back to the default.
*/
exports.skinName = null;
export const skinName = null;
exports.skinVariants = 'super-light-toolbar super-light-editor light-background';
export const skinVariants = 'super-light-toolbar super-light-editor light-background';
/**
* The IP ep-lite should listen to
*/
exports.ip = '0.0.0.0';
export const ip = '0.0.0.0';
/**
* The Port ep-lite should listen to
*/
exports.port = process.env.PORT || 9001;
export const port = process.env.PORT || 9001;
/**
* Should we suppress Error messages from being in Pad Contents
*/
exports.suppressErrorsInPadText = false;
export const suppressErrorsInPadText = false;
/**
* The SSL signed server key and the Certificate Authority's own certificate
* default case: ep-lite does *not* use SSL. A signed server key is not required in this case.
*/
exports.ssl = false;
export const ssl = false;
/**
* socket.io transport methods
**/
exports.socketTransportProtocols = ['xhr-polling', 'jsonp-polling', 'htmlfile'];
export const socketTransportProtocols = ['xhr-polling', 'jsonp-polling', 'htmlfile'];
exports.socketIo = {
export const socketIo = {
/**
* Maximum permitted client message size (in bytes).
*
@ -138,20 +138,20 @@ exports.socketIo = {
/*
* The Type of the database
*/
exports.dbType = 'dirty';
export const dbType = 'dirty';
/**
* This setting is passed with dbType to ueberDB to set up the database
*/
exports.dbSettings = {filename: path.join(exports.root, 'var/dirty.db')};
export const dbSettings = {filename: path.join(exports.root, 'var/dirty.db')};
/**
* The default Text of a new pad
*/
exports.defaultPadText = [
export const defaultPadText = [
'Welcome to Etherpad!',
'',
'This pad text is synchronized as you type, so that everyone viewing this page sees the same ' +
'text. This allows you to collaborate seamlessly on documents!',
'text. This allows you to collaborate seamlessly on documents!',
'',
'Etherpad on Github: https://github.com/ether/etherpad-lite',
].join('\n');
@ -159,7 +159,7 @@ exports.defaultPadText = [
/**
* The default Pad Settings for a user (Can be overridden by changing the setting
*/
exports.padOptions = {
export const padOptions = {
noColors: false,
showControls: true,
showChat: true,
@ -176,7 +176,7 @@ exports.padOptions = {
/**
* Whether certain shortcut keys are enabled for a user in the pad
*/
exports.padShortcutEnabled = {
export const padShortcutEnabled = {
altF9: true,
altC: true,
delete: true,
@ -204,7 +204,7 @@ exports.padShortcutEnabled = {
/**
* The toolbar buttons and order.
*/
exports.toolbar = {
export const toolbar = {
left: [
['bold', 'italic', 'underline', 'strikethrough'],
['orderedlist', 'unorderedlist', 'indent', 'outdent'],
@ -224,92 +224,92 @@ exports.toolbar = {
/**
* A flag that requires any user to have a valid session (via the api) before accessing a pad
*/
exports.requireSession = false;
export const requireSession = false;
/**
* A flag that prevents users from creating new pads
*/
exports.editOnly = false;
export const editOnly = false;
/**
* Max age that responses will have (affects caching layer).
*/
exports.maxAge = 1000 * 60 * 60 * 6; // 6 hours
export const maxAge = 1000 * 60 * 60 * 6; // 6 hours
/**
* A flag that shows if minification is enabled or not
*/
exports.minify = true;
export const minify = true;
/**
* The path of the abiword executable
*/
exports.abiword = null;
export const abiword = null;
/**
* The path of the libreoffice executable
*/
exports.soffice = null;
export const soffice = null;
/**
* The path of the tidy executable
*/
exports.tidyHtml = null;
export const tidyHtml = null;
/**
* Should we support none natively supported file types on import?
*/
exports.allowUnknownFileEnds = true;
export const allowUnknownFileEnds = true;
/**
* The log level of log4js
*/
exports.loglevel = defaultLogLevel;
export const loglevel = defaultLogLevel;
/**
* Disable IP logging
*/
exports.disableIPlogging = false;
export const disableIPlogging = false;
/**
* Number of seconds to automatically reconnect pad
*/
exports.automaticReconnectionTimeout = 0;
export const automaticReconnectionTimeout = 0;
/**
* Disable Load Testing
*/
exports.loadTest = false;
export const loadTest = false;
/**
* Disable dump of objects preventing a clean exit
*/
exports.dumpOnUncleanExit = false;
export const dumpOnUncleanExit = false;
/**
* Enable indentation on new lines
*/
exports.indentationOnNewLine = true;
export const indentationOnNewLine = true;
/*
* log4js appender configuration
*/
exports.logconfig = defaultLogConfig();
export const logconfig = defaultLogConfig();
/*
* Session Key, do not sure this.
*/
exports.sessionKey = false;
export const sessionKey = false;
/*
* Trust Proxy, whether or not trust the x-forwarded-for header.
*/
exports.trustProxy = false;
export const trustProxy = false;
/*
* Settings controlling the session cookie issued by Etherpad.
*/
exports.cookie = {
export const cookie = {
/*
* Value of the SameSite cookie property. "Lax" is recommended unless
* Etherpad will be embedded in an iframe from another site, in which case
@ -331,20 +331,20 @@ exports.cookie = {
* authorization. Note: /admin always requires authentication, and
* either authorization by a module, or a user with is_admin set
*/
exports.requireAuthentication = false;
exports.requireAuthorization = false;
exports.users = {};
export const requireAuthentication = false;
export const requireAuthorization = false;
export const users = {};
/*
* Show settings in admin page, by default it is true
*/
exports.showSettingsInAdminPage = true;
export const showSettingsInAdminPage = true;
/*
* By default, when caret is moved out of viewport, it scrolls the minimum
* height needed to make this line visible.
*/
exports.scrollWhenFocusLineIsOutOfViewport = {
export const scrollWhenFocusLineIsOutOfViewport = {
/*
* Percentage of viewport height to be additionally scrolled.
*/
@ -377,12 +377,12 @@ exports.scrollWhenFocusLineIsOutOfViewport = {
*
* Do not enable on production machines.
*/
exports.exposeVersion = false;
export const exposeVersion = false;
/*
* Override any strings found in locale directories
*/
exports.customLocaleStrings = {};
export const customLocaleStrings = {};
/*
* From Etherpad 1.8.3 onwards, import and export of pads is always rate
@ -393,12 +393,13 @@ exports.customLocaleStrings = {};
*
* See https://github.com/nfriedly/express-rate-limit for more options
*/
exports.importExportRateLimiting = {
export const importExportRateLimiting = {
// duration of the rate limit window (milliseconds)
windowMs: 90000,
// maximum number of requests per IP to allow during the rate limit window
max: 10,
max: 10, onLimitReached: undefined
};
/*
@ -409,7 +410,7 @@ exports.importExportRateLimiting = {
*
* See https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options
*/
exports.commitRateLimiting = {
export const commitRateLimiting = {
// duration of the rate limit window (seconds)
duration: 1,
@ -423,16 +424,16 @@ exports.commitRateLimiting = {
*
* File size is specified in bytes. Default is 50 MB.
*/
exports.importMaxFileSize = 50 * 1024 * 1024;
export const importMaxFileSize = 50 * 1024 * 1024;
/*
* Disable Admin UI tests
*/
exports.enableAdminUITests = false;
export const enableAdminUITests = false;
// checks if abiword is avaiable
exports.abiwordAvailable = () => {
export const abiwordAvailable = () => {
if (exports.abiword != null) {
return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
} else {
@ -440,7 +441,7 @@ exports.abiwordAvailable = () => {
}
};
exports.sofficeAvailable = () => {
export const sofficeAvailable = () => {
if (exports.soffice != null) {
return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
} else {
@ -448,9 +449,9 @@ exports.sofficeAvailable = () => {
}
};
exports.exportAvailable = () => {
export const exportAvailable = () => {
const abiword = exports.abiwordAvailable();
const soffice = exports.sofficeAvailable();
const soffice = sofficeAvailable();
if (abiword === 'no' && soffice === 'no') {
return 'no';
@ -463,7 +464,7 @@ exports.exportAvailable = () => {
};
// Provide git version if available
exports.getGitCommit = () => {
export const getGitCommit = () => {
let version = '';
try {
let rootPath = exports.root;
@ -482,13 +483,14 @@ exports.getGitCommit = () => {
}
version = version.substring(0, 7);
} catch (e) {
logger.warn(`Can't get git version for server header\n${e.message}`);
const errorCast = e as Error
logger.warn(`Can't get git version for server header\n${errorCast.message}`);
}
return version;
};
// Return etherpad version from package.json
exports.getEpVersion = () => require('../../package.json').version;
export const getEpVersion = () => require('../../package.json').version;
/**
* Receives a settingsObj and, if the property name is a valid configuration
@ -497,7 +499,8 @@ exports.getEpVersion = () => require('../../package.json').version;
* This code refactors a previous version that copied & pasted the same code for
* both "settings.json" and "credentials.json".
*/
const storeSettings = (settingsObj) => {
//FIXME find out what settingsObj is
const storeSettings = (settingsObj: any) => {
for (const i of Object.keys(settingsObj || {})) {
if (nonSettings.includes(i)) {
logger.warn(`Ignoring setting: '${i}'`);
@ -536,9 +539,10 @@ const storeSettings = (settingsObj) => {
* short syntax "${ABIWORD}", and not "${ABIWORD:null}": the latter would result
* in the literal string "null", instead.
*/
const coerceValue = (stringValue) => {
const coerceValue = (stringValue: string) => {
// cooked from https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number
const isNumeric = !isNaN(stringValue) && !isNaN(parseFloat(stringValue) && isFinite(stringValue));
const numberToEvaluate = Number(stringValue)
const isNumeric = !isNaN(numberToEvaluate) && !isNaN(parseFloat(stringValue)) && isFinite(numberToEvaluate);
if (isNumeric) {
// detected numeric string. Coerce to a number
@ -591,7 +595,7 @@ const coerceValue = (stringValue) => {
*
* see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter
*/
const lookupEnvironmentVariables = (obj) => {
const lookupEnvironmentVariables = (obj: any) => {
const stringifiedAndReplaced = JSON.stringify(obj, (key, value) => {
/*
* the first invocation of replacer() is with an empty key. Just go on, or
@ -636,9 +640,9 @@ const lookupEnvironmentVariables = (obj) => {
if ((envVarValue === undefined) && (defaultValue === undefined)) {
logger.warn(`Environment variable "${envVarName}" does not contain any value for ` +
`configuration key "${key}", and no default was given. Using null. ` +
'THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF ETHERPAD; you should ' +
'explicitly use "null" as the default if you want to continue to use null.');
`configuration key "${key}", and no default was given. Using null. ` +
'THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF ETHERPAD; you should ' +
'explicitly use "null" as the default if you want to continue to use null.');
/*
* We have to return null, because if we just returned undefined, the
@ -649,9 +653,12 @@ const lookupEnvironmentVariables = (obj) => {
if ((envVarValue === undefined) && (defaultValue !== undefined)) {
logger.debug(`Environment variable "${envVarName}" not found for ` +
`configuration key "${key}". Falling back to default value.`);
`configuration key "${key}". Falling back to default value.`);
return coerceValue(defaultValue);
} else if ((envVarValue === undefined))
{
return coerceValue("none")
}
// envVarName contained some value.
@ -666,9 +673,7 @@ const lookupEnvironmentVariables = (obj) => {
return coerceValue(envVarValue);
});
const newSettings = JSON.parse(stringifiedAndReplaced);
return newSettings;
return JSON.parse(stringifiedAndReplaced);
};
/**
@ -679,7 +684,7 @@ const lookupEnvironmentVariables = (obj) => {
*
* The isSettings variable only controls the error logging.
*/
const parseSettings = (settingsFilename, isSettings) => {
const parseSettings = (settingsFilename: string, isSettings:boolean) => {
let settingsStr = '';
let settingsType, notFoundMessage, notFoundFunction;
@ -711,20 +716,22 @@ const parseSettings = (settingsFilename, isSettings) => {
logger.info(`${settingsType} loaded from: ${settingsFilename}`);
const replacedSettings = lookupEnvironmentVariables(settings);
return replacedSettings;
return lookupEnvironmentVariables(settings);
} catch (e) {
const error = e as Error
logger.error(`There was an error processing your ${settingsType} ` +
`file from ${settingsFilename}: ${e.message}`);
`file from ${settingsFilename}: ${error.message}`);
process.exit(1);
}
};
exports.reloadSettings = () => {
const settings = parseSettings(exports.settingsFilename, true);
const credentials = parseSettings(exports.credentialsFilename, false);
export const randomVersionString = randomString(4);
export const reloadSettings = () => {
const settings = parseSettings(settingsFilename, true);
const credentials = parseSettings(credentialsFilename, false);
storeSettings(settings);
storeSettings(credentials);
@ -732,7 +739,7 @@ exports.reloadSettings = () => {
if (!exports.skinName) {
logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' +
'update your settings.json. Falling back to the default "colibris".');
'update your settings.json. Falling back to the default "colibris".');
exports.skinName = 'colibris';
}
@ -743,7 +750,7 @@ exports.reloadSettings = () => {
if (countPieces !== 1) {
logger.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` +
`not valid: "${exports.skinName}". Falling back to the default "colibris".`);
`not valid: "${exports.skinName}". Falling back to the default "colibris".`);
exports.skinName = 'colibris';
}
@ -754,7 +761,7 @@ exports.reloadSettings = () => {
// what if someone sets skinName == ".." or "."? We catch him!
if (absolutePaths.isSubdir(skinBasePath, skinPath) === false) {
logger.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. ` +
'Falling back to the default "colibris".');
'Falling back to the default "colibris".');
exports.skinName = 'colibris';
skinPath = path.join(skinBasePath, exports.skinName);
@ -772,7 +779,7 @@ exports.reloadSettings = () => {
if (exports.abiword) {
// Check abiword actually exists
if (exports.abiword != null) {
fs.exists(exports.abiword, (exists) => {
fs.exists(exports.abiword, (exists: boolean) => {
if (!exists) {
const abiwordError = 'Abiword does not exist at this path, check your settings file.';
if (!exports.suppressErrorsInPadText) {
@ -786,7 +793,7 @@ exports.reloadSettings = () => {
}
if (exports.soffice) {
fs.exists(exports.soffice, (exists) => {
fs.exists(exports.soffice, (exists: boolean) => {
if (!exists) {
const sofficeError =
'soffice (libreoffice) does not exist at this path, check your settings file.';
@ -813,9 +820,9 @@ exports.reloadSettings = () => {
}
} else {
logger.warn('Declaring the sessionKey in the settings.json is deprecated. ' +
'This value is auto-generated now. Please remove the setting from the file. -- ' +
'If you are seeing this error after restarting using the Admin User ' +
'Interface then you can ignore this message.');
'This value is auto-generated now. Please remove the setting from the file. -- ' +
'If you are seeing this error after restarting using the Admin User ' +
'Interface then you can ignore this message.');
}
if (exports.dbType === 'dirty') {
@ -831,7 +838,7 @@ exports.reloadSettings = () => {
if (exports.ip === '') {
// using Unix socket for connectivity
logger.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' +
'"port" parameter will be interpreted as the path to a Unix socket to bind at.');
'"port" parameter will be interpreted as the path to a Unix socket to bind at.');
}
/*
@ -845,7 +852,6 @@ exports.reloadSettings = () => {
* ACHTUNG: this may prevent caching HTTP proxies to work
* TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead
*/
exports.randomVersionString = randomString(4);
logger.info(`Random string used for versioning assets: ${exports.randomVersionString}`);
};
@ -855,3 +861,5 @@ exports.exportedForTestingOnly = {
// initially load settings
exports.reloadSettings();

View File

@ -1,43 +0,0 @@
'use strict';
const semver = require('semver');
const settings = require('./Settings');
const request = require('request');
let infos;
const loadEtherpadInformations = () => new Promise((resolve, reject) => {
request('https://static.etherpad.org/info.json', (er, response, body) => {
if (er) return reject(er);
try {
infos = JSON.parse(body);
return resolve(infos);
} catch (err) {
return reject(err);
}
});
});
exports.getLatestVersion = () => {
exports.needsUpdate();
return infos.latestVersion;
};
exports.needsUpdate = (cb) => {
loadEtherpadInformations().then((info) => {
if (semver.gt(info.latestVersion, settings.getEpVersion())) {
if (cb) return cb(true);
}
}).catch((err) => {
console.error(`Can not perform Etherpad update check: ${err}`);
if (cb) return cb(false);
});
};
exports.check = () => {
exports.needsUpdate((needsUpdate) => {
if (needsUpdate) {
console.warn(`Update available: Download the actual version ${infos.latestVersion}`);
}
});
};

View File

@ -0,0 +1,59 @@
'use strict';
import semver from 'semver';
import {getEpVersion} from './Settings';
import request from 'request';
type InfoModel = {
latestVersion: string
}
let infos: InfoModel|undefined;
const loadEtherpadInformations = () => new Promise<InfoModel>((resolve, reject) => {
request('https://static.etherpad.org/info.json', (er, response, body) => {
if (er) reject(er);
try {
infos = JSON.parse(body);
if (infos === undefined|| infos === null){
reject("Could not retrieve current version")
return
}
resolve(infos);
} catch (err) {
reject(err);
}
});
});
const getLatestVersion = () => {
exports.needsUpdate();
if(infos === undefined){
throw new Error("Could not retrieve latest version")
}
return infos.latestVersion;
}
exports.needsUpdate = (cb?:(arg0: boolean)=>void) => {
loadEtherpadInformations().then((info) => {
if (semver.gt(info.latestVersion, getEpVersion())) {
if (cb) return cb(true);
}
}).catch((err) => {
console.error(`Can not perform Etherpad update check: ${err}`);
if (cb) return cb(false);
})
}
const check = () => {
const needsUpdate = ((needsUpdate: boolean) => {
if (needsUpdate && infos) {
console.warn(`Update available: Download the latest version ${infos.latestVersion}`);
}
})
needsUpdate(infos.latestVersion > getEpVersion());
}
export default {check, getLatestVersion}

View File

@ -3,8 +3,6 @@
* Generates a random String with the given length. Is needed to generate the
* Author, Group, readonly, session Ids
*/
const crypto = require('crypto');
import crypto from 'crypto'
const randomString = (len) => crypto.randomBytes(len).toString('hex');
module.exports = randomString;
export const randomString = (len) => crypto.randomBytes(len).toString('hex');

26358
src/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,7 @@
"jsonminify": "0.4.2",
"languages4translatewiki": "0.1.3",
"lodash.clonedeep": "4.5.0",
"log4js": "0.6.38",
"log4js": "^6.9.1",
"measured-core": "^2.0.0",
"mime-types": "^2.1.35",
"npm": "^6.14.15",
@ -90,7 +90,11 @@
"sinon": "^13.0.2",
"split-grid": "^1.0.11",
"supertest": "^6.3.3",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"@types/node": "^20.3.1",
"@types/express": "4.17.17",
"concurrently": "^8.2.0",
"nodemon": "^2.0.22"
},
"engines": {
"node": ">=14.15.0",
@ -103,8 +107,10 @@
"scripts": {
"lint": "eslint .",
"test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
"test-container": "mocha --timeout 5000 tests/container/specs/api"
"test-container": "mocha --timeout 5000 tests/container/specs/api",
"dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/server.js\""
},
"version": "1.9.0",
"license": "Apache-2.0"
"license": "Apache-2.0",
"type": "module"
}

View File

@ -54,7 +54,10 @@
* There is one attribute pool per pad, and it includes every current and historical attribute used
* in the pad.
*/
class AttributePool {
export class AttributePool {
numToAttrib: {};
private attribToNum: {};
private nextNum: number;
constructor() {
/**
* Maps an attribute identifier to the attribute's `[key, value]` string pair.

11
src/tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist"
},
"lib": ["es2015"]
}