Added typescript support for most backend files.

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

View file

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

View file

@ -21,28 +21,29 @@
* limitations under the License. * limitations under the License.
*/ */
const ueberDB = require('ueberdb2'); import ueberDB from 'ueberdb2';
const settings = require('../utils/Settings'); import {dbSettings, dbType} from '../utils/Settings';
const log4js = require('log4js'); import log4js from 'log4js';
const stats = require('../stats'); import {shutdown as statsShutdown,createCollection} from '../stats';
import {} from 'measured-core'
const logger = log4js.getLogger('ueberDB'); const logger = log4js.getLogger('ueberDB');
/** /**
* The UeberDB Object that provides the database functions * 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 * Initializes the database with the settings provided by the settings module
*/ */
exports.init = async () => { const init = async () => {
exports.db = new ueberDB.Database(settings.dbType, settings.dbSettings, null, logger); exports.db = new ueberDB.Database(dbType, dbSettings, null, logger);
await exports.db.init(); await exports.db.init();
if (exports.db.metrics != null) { if (exports.db.metrics != null) {
for (const [metric, value] of Object.entries(exports.db.metrics)) { for (const [metric, value] of Object.entries(exports.db.metrics)) {
if (typeof value !== 'number') continue; 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']) { 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(); if (exports.db != null) await exports.db.close();
exports.db = null; exports.db = null;
logger.log('Database closed'); logger.log('Database closed');
}; };
export {db,init,shutdown}

View file

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

View file

@ -3,15 +3,16 @@
* The pad object, defined with joose * The pad object, defined with joose
*/ */
const AttributeMap = require('../../static/js/AttributeMap'); import AttributeMap from '../../static/js/AttributeMap';
const Changeset = require('../../static/js/Changeset'); import Changeset from '../../static/js/Changeset';
const ChatMessage = require('../../static/js/ChatMessage'); import ChatMessage from '../../static/js/ChatMessage';
const AttributePool = require('../../static/js/AttributePool'); import {AttributePool} from '../../static/js/AttributePool';
const Stream = require('../utils/Stream'); import Stream from '../utils/Stream';
const assert = require('assert').strict; import assert, {strict} from 'assert'
const db = require('./DB'); import {db} from './DB';
const settings = require('../utils/Settings'); import {defaultPadText} from '../utils/Settings';
const authorManager = require('./AuthorManager'); import {addPad, getAuthorColorId, getAuthorName, getColorPalette, removePad} from './AuthorManager';
import {Revision} from "../models/Revision";
const padManager = require('./PadManager'); const padManager = require('./PadManager');
const padMessageHandler = require('../handler/PadMessageHandler'); const padMessageHandler = require('../handler/PadMessageHandler');
const groupManager = require('./GroupManager'); const groupManager = require('./GroupManager');
@ -32,15 +33,24 @@ exports.cleanText = (txt) => txt.replace(/\r\n/g, '\n')
.replace(/\t/g, ' ') .replace(/\t/g, ' ')
.replace(/\xa0/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; * @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 * 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 * 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 * 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. * 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.db = database;
this.atext = Changeset.makeAText('\n'); this.atext = Changeset.makeAText('\n');
this.pool = new AttributePool(); this.pool = new AttributePool();
@ -99,7 +109,7 @@ class Pad {
}, },
}), }),
this.saveToDatabase(), this.saveToDatabase(),
authorId && authorManager.addPad(authorId, this.id), authorId && addPad(authorId, this.id),
hooks.aCallAll(hook, { hooks.aCallAll(hook, {
pad: this, pad: this,
authorId, authorId,
@ -121,7 +131,7 @@ class Pad {
} }
toJSON() { toJSON() {
const o = {...this, pool: this.pool.toJsonable()}; const o:{db: any, id: any} = {...this, pool: this.pool.toJsonable()}
delete o.db; delete o.db;
delete o.id; delete o.id;
return o; return o;
@ -190,10 +200,10 @@ class Pad {
async getAllAuthorColors() { async getAllAuthorColors() {
const authorIds = this.getAllAuthors(); const authorIds = this.getAllAuthors();
const returnTable = {}; const returnTable = {};
const colorPalette = authorManager.getColorPalette(); const colorPalette = getColorPalette();
await Promise.all( 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 // colorId might be a hex color or an number out of the palette
returnTable[authorId] = colorPalette[colorId] || colorId; returnTable[authorId] = colorPalette[colorId] || colorId;
}))); })));
@ -315,7 +325,7 @@ class Pad {
const entry = await this.db.get(`pad:${this.id}:chat:${entryNum}`); const entry = await this.db.get(`pad:${this.id}:chat:${entryNum}`);
if (entry == null) return null; if (entry == null) return null;
const message = ChatMessage.fromObject(entry); const message = ChatMessage.fromObject(entry);
message.displayName = await authorManager.getAuthorName(message.authorId); message.displayName = await getAuthorName(message.authorId);
return message; return message;
} }
@ -352,7 +362,7 @@ class Pad {
if ('pool' in value) this.pool = new AttributePool().fromJsonable(value.pool); if ('pool' in value) this.pool = new AttributePool().fromJsonable(value.pool);
} else { } else {
if (text == null) { 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); await hooks.aCallAll('padDefaultContent', context);
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`); if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
text = exports.cleanText(context.content); text = exports.cleanText(context.content);
@ -454,7 +464,7 @@ class Pad {
async copyAuthorInfoToDestinationPad(destinationID) { async copyAuthorInfoToDestinationPad(destinationID) {
// add the new sourcePad to all authors who contributed to the old one // add the new sourcePad to all authors who contributed to the old one
await Promise.all(this.getAllAuthors().map( await Promise.all(this.getAllAuthors().map(
(authorID) => authorManager.addPad(authorID, destinationID))); (authorID) => addPad(authorID, destinationID)));
} }
async copyPadWithoutHistory(destinationID, force, authorId = '') { async copyPadWithoutHistory(destinationID, force, authorId = '') {
@ -557,7 +567,7 @@ class Pad {
// remove pad from all authors who contributed // remove pad from all authors who contributed
this.getAllAuthors().forEach((authorId) => { this.getAllAuthors().forEach((authorId) => {
p.push(authorManager.removePad(authorId, padID)); p.push(removePad(authorId, padID));
}); });
// delete the pad entry and delete pad from padManager // delete the pad entry and delete pad from padManager
@ -587,12 +597,13 @@ class Pad {
} }
// build the saved revision object // build the saved revision object
const savedRevision = {}; const savedRevision:Revision = {
savedRevision.revNum = revNum; label: label || `Revision ${revNum}`,
savedRevision.savedById = savedById; revNum: revNum,
savedRevision.label = label || `Revision ${revNum}`; savedById: savedById,
savedRevision.timestamp = Date.now(); timestamp: Date.now(),
savedRevision.id = randomString(10); id: randomString(10)
}
// save this new saved revision // save this new saved revision
this.savedRevisions.push(savedRevision); this.savedRevisions.push(savedRevision);

View file

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

View file

@ -20,21 +20,21 @@
*/ */
const db = require('./DB'); import {db} from './DB';
const randomString = require('../utils/randomstring'); import randomString from '../utils/randomstring';
/** /**
* checks if the id pattern matches a read-only pad id * 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 * returns a read only id for a pad
* @param {String} padId the id of the 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 // check if there is a pad2readonly entry
let readOnlyId = await db.get(`pad2readonly:${padId}`); let readOnlyId = await db.get(`pad2readonly:${padId}`);
@ -54,13 +54,13 @@ exports.getReadOnlyId = async (padId) => {
* returns the padId for a read only id * returns the padId for a read only id
* @param {String} readOnlyId 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 * 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); const readonly = exports.isReadOnlyId(id);
// Might be null, if this is an unknown read-only id // Might be null, if this is an unknown read-only id

View file

@ -19,18 +19,29 @@
* limitations under the License. * limitations under the License.
*/ */
const authorManager = require('./AuthorManager'); import {getAuthorId} from "./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');
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. * 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 * 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). * 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) { if (!padID) {
authLogger.debug('access denied: missing padID'); authLogger.debug('access denied: missing padID');
return DENY; return DENY;
} }
let canCreate = !settings.editOnly; let canCreate = !editOnly;
if (readOnlyManager.isReadOnlyId(padID)) { if (isReadOnlyId(padID)) {
canCreate = false; canCreate = false;
padID = await readOnlyManager.getPadId(padID); padID = await getPadId(padID);
if (padID == null) { if (padID == null) {
authLogger.debug('access denied: read-only pad ID for a pad that does not exist'); authLogger.debug('access denied: read-only pad ID for a pad that does not exist');
return DENY; return DENY;
@ -68,10 +79,10 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => {
} }
// Authentication and authorization checks. // Authentication and authorization checks.
if (settings.loadTest) { if (loadTest) {
console.warn( console.warn(
'bypassing socket.io authentication and authorization checks due to settings.loadTest'); 'bypassing socket.io authentication and authorization checks due to settings.loadTest');
} else if (settings.requireAuthentication) { } else if (requireAuthentication) {
if (userSettings == null) { if (userSettings == null) {
authLogger.debug('access denied: authentication is required'); authLogger.debug('access denied: authentication is required');
return DENY; return DENY;
@ -96,14 +107,14 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => {
return DENY; return DENY;
} }
const padExists = await padManager.doesPadExist(padID); const padExists = await doesPadExist(padID);
if (!padExists && !canCreate) { if (!padExists && !canCreate) {
authLogger.debug('access denied: user attempted to create a pad, which is prohibited'); authLogger.debug('access denied: user attempted to create a pad, which is prohibited');
return DENY; return DENY;
} }
const sessionAuthorID = await sessionManager.findAuthorID(padID.split('$')[0], sessionCookie); const sessionAuthorID = await findAuthorID(padID.split('$')[0], sessionCookie);
if (settings.requireSession && !sessionAuthorID) { if (requireSession && !sessionAuthorID) {
authLogger.debug('access denied: HTTP API session is required'); authLogger.debug('access denied: HTTP API session is required');
return DENY; return DENY;
} }
@ -115,7 +126,7 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => {
const grant = { const grant = {
accessStatus: 'grant', accessStatus: 'grant',
authorID: sessionAuthorID || await authorManager.getAuthorId(token, userSettings), authorID: sessionAuthorID || await getAuthorId(token, userSettings),
}; };
if (!padID.includes('$')) { if (!padID.includes('$')) {
@ -132,7 +143,7 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => {
return grant; return grant;
} }
const pad = await padManager.getPad(padID); const pad = await getPad(padID);
if (!pad.getPublicStatus() && sessionAuthorID == null) { if (!pad.getPublicStatus() && sessionAuthorID == null) {
authLogger.debug('access denied: must have an HTTP API session to access private group pads'); 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 * sessionCookie, and is bound to a group with the given ID, then this returns the author ID
* bound to the session. Otherwise, returns undefined. * bound to the session. Otherwise, returns undefined.
*/ */
exports.findAuthorID = async (groupID, sessionCookie) => { export const findAuthorID = async (groupID, sessionCookie) => {
if (!sessionCookie) return undefined; if (!sessionCookie) return undefined;
/* /*
* Sometimes, RFC 6265-compliant web servers may send back a cookie whose * 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 sessionIDs = sessionCookie.replace(/^"|"$/g, '').split(',');
const sessionInfoPromises = sessionIDs.map(async (id) => { const sessionInfoPromises = sessionIDs.map(async (id) => {
try { try {
return await exports.getSessionInfo(id); return await getSessionInfo(id);
} catch (err) { } catch (err) {
if (err.message === 'sessionID does not exist') { if (err.message === 'sessionID does not exist') {
console.debug(`SessionManager getAuthorID: no session exists with ID ${id}`); console.debug(`SessionManager getAuthorID: no session exists with ID ${id}`);
@ -81,7 +81,7 @@ exports.findAuthorID = async (groupID, sessionCookie) => {
return sessionInfo.authorID; return sessionInfo.authorID;
}; };
exports.doesSessionExist = async (sessionID) => { export const doesSessionExist = async (sessionID) => {
// check if the database entry of this session exists // check if the database entry of this session exists
const session = await db.get(`session:${sessionID}`); const session = await db.get(`session:${sessionID}`);
return (session != null); return (session != null);
@ -90,7 +90,7 @@ exports.doesSessionExist = async (sessionID) => {
/** /**
* Creates a new session between an author and a group * 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 // check if the group exists
const groupExists = await groupManager.doesGroupExist(groupID); const groupExists = await groupManager.doesGroupExist(groupID);
if (!groupExists) { if (!groupExists) {
@ -146,7 +146,7 @@ exports.createSession = async (groupID, authorID, validUntil) => {
return {sessionID}; return {sessionID};
}; };
exports.getSessionInfo = async (sessionID) => { export const getSessionInfo = async (sessionID) => {
// check if the database entry of this session exists // check if the database entry of this session exists
const session = await db.get(`session:${sessionID}`); const session = await db.get(`session:${sessionID}`);
@ -162,7 +162,7 @@ exports.getSessionInfo = async (sessionID) => {
/** /**
* Deletes a session * Deletes a session
*/ */
exports.deleteSession = async (sessionID) => { export const deleteSession = async (sessionID) => {
// ensure that the session exists // ensure that the session exists
const session = await db.get(`session:${sessionID}`); const session = await db.get(`session:${sessionID}`);
if (session == null) { if (session == null) {
@ -186,7 +186,7 @@ exports.deleteSession = async (sessionID) => {
await db.remove(`session:${sessionID}`); await db.remove(`session:${sessionID}`);
}; };
exports.listSessionsOfGroup = async (groupID) => { export const listSessionsOfGroup = async (groupID) => {
// check that the group exists // check that the group exists
const exists = await groupManager.doesGroupExist(groupID); const exists = await groupManager.doesGroupExist(groupID);
if (!exists) { if (!exists) {
@ -197,7 +197,7 @@ exports.listSessionsOfGroup = async (groupID) => {
return sessions; return sessions;
}; };
exports.listSessionsOfAuthor = async (authorID) => { export const listSessionsOfAuthor = async (authorID) => {
// check that the author exists // check that the author exists
const exists = await authorManager.doesAuthorExist(authorID); const exists = await authorManager.doesAuthorExist(authorID);
if (!exists) { if (!exists) {
@ -218,8 +218,7 @@ const listSessionsWithDBKey = async (dbkey) => {
// iterate through the sessions and get the sessioninfos // iterate through the sessions and get the sessioninfos
for (const sessionID of Object.keys(sessions || {})) { for (const sessionID of Object.keys(sessions || {})) {
try { try {
const sessionInfo = await exports.getSessionInfo(sessionID); sessions[sessionID] = await getSessionInfo(sessionID);
sessions[sessionID] = sessionInfo;
} catch (err) { } catch (err) {
if (err.name === 'apierror') { if (err.name === 'apierror') {
console.warn(`Found bad session ${sessionID} in ${dbkey}`); console.warn(`Found bad session ${sessionID} in ${dbkey}`);

View file

@ -1,13 +1,19 @@
'use strict'; 'use strict';
const DB = require('./DB'); import {db} from "./DB";
const Store = require('express-session').Store;
const log4js = require('log4js'); import {Store} from "express-session";
const util = require('util');
import log4js from "log4js";
import util from "util";
import {SessionModel} from "../models/SessionModel";
const logger = log4js.getLogger('SessionStore'); const logger = log4js.getLogger('SessionStore');
class SessionStore extends Store { 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 * @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 * 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); 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) || {}; const exp = this._expirations.get(sid) || {};
clearTimeout(exp.timeout); clearTimeout(exp.timeout);
const {cookie: {expires} = {}} = sess || {}; const {cookie: {expires} = {expires: sess.cookie.expires}} = sess || {cookie:{expires:undefined}};
if (expires) { if (expires) {
const sessExp = new Date(expires).getTime(); const sessExp = new Date(expires).getTime();
if (updateDbExp) exp.db = sessExp; if (updateDbExp) exp.db = sessExp;
@ -64,12 +70,12 @@ class SessionStore extends Store {
} }
async _write(sid, sess) { async _write(sid, sess) {
await DB.set(`sessionstorage:${sid}`, sess); await db.set(`sessionstorage:${sid}`, sess);
} }
async _get(sid) { async _get(sid) {
logger.debug(`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); return await this._updateExpirations(sid, s);
} }
@ -83,7 +89,7 @@ class SessionStore extends Store {
logger.debug(`DESTROY ${sid}`); logger.debug(`DESTROY ${sid}`);
clearTimeout((this._expirations.get(sid) || {}).timeout); clearTimeout((this._expirations.get(sid) || {}).timeout);
this._expirations.delete(sid); 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 // 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}`]); 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") * require("./index").require("./path/to/template.ejs")
*/ */
const ejs = require('ejs'); import ejs from 'ejs';
const fs = require('fs'); import fs from "fs";
const hooks = require('../../static/js/pluginfw/hooks.js');
const path = require('path'); import hooks from "../../static/js/pluginfw/hooks.js";
const resolve = require('resolve');
const settings = require('../utils/Settings'); import path from "path";
import resolve from "resolve";
import {maxAge} from "../utils/Settings";
const templateCache = new Map(); const templateCache = new Map();
exports.info = { export const info = {
__output_stack: [], __output_stack: [],
block_stack: [], block_stack: [],
file_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) => { export const _init = (b, recursive) => {
exports.info.__output_stack.push(exports.info.__output); info.__output_stack.push(info.__output)
exports.info.__output = b; info.__output = b
}; };
exports._exit = (b, recursive) => { export const _exit = (b, recursive) => {
exports.info.__output = exports.info.__output_stack.pop(); info.__output = info.__output_stack.pop();
}; };
exports.begin_block = (name) => { export const begin_block = (name) => {
exports.info.block_stack.push(name); info.block_stack.push(name);
exports.info.__output_stack.push(exports.info.__output.get()); info.__output_stack.push(info.__output.get());
exports.info.__output.set(''); info.__output.set('');
}; };
exports.end_block = () => { export const end_block = () => {
const name = exports.info.block_stack.pop(); const name = info.block_stack.pop();
const renderContext = exports.info.args[exports.info.args.length - 1]; const renderContext = info.args[info.args.length - 1];
const content = exports.info.__output.get(); const content = info.__output.get();
exports.info.__output.set(exports.info.__output_stack.pop()); info.__output.set(info.__output_stack.pop());
const args = {content, renderContext}; const args = {content, renderContext};
hooks.callAll(`eejsBlock_${name}`, args); 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 = {}; if (args == null) args = {};
let basedir = __dirname; let basedir = __dirname;
let paths = []; let paths = [];
if (exports.info.file_stack.length) { if (info.file_stack.length) {
basedir = path.dirname(getCurrentFile().path); basedir = path.dirname(getCurrentFile().path);
} }
if (mod) { if (mod) {
@ -82,18 +87,18 @@ exports.require = (name, args, mod) => {
args.e = exports; args.e = exports;
args.require = require; args.require = require;
const cache = settings.maxAge !== 0; const cache = maxAge !== 0;
const template = cache && templateCache.get(ejspath) || ejs.compile( const template = cache && templateCache.get(ejspath) || ejs.compile(
'<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>' + '<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>' +
`${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`, `${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`,
{filename: ejspath}); {filename: ejspath});
if (cache) templateCache.set(ejspath, template); if (cache) templateCache.set(ejspath, template);
exports.info.args.push(args); info.args.push(args);
exports.info.file_stack.push({path: ejspath}); info.file_stack.push({path: ejspath});
const res = template(args); const res = template(args);
exports.info.file_stack.pop(); info.file_stack.pop();
exports.info.args.pop(); info.args.pop();
return res; return res;
}; };

View file

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

View file

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

View file

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

View file

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

View file

@ -20,9 +20,10 @@
* limitations under the License. * limitations under the License.
*/ */
const log4js = require('log4js'); import log4js from 'log4js';
const settings = require('../utils/Settings'); import {disableIPlogging} from "../utils/Settings";
const stats = require('../stats');
import {createCollection} from '../stats';
const logger = log4js.getLogger('socket.io'); const logger = log4js.getLogger('socket.io');
@ -53,7 +54,7 @@ exports.setSocketIO = (_io) => {
io = _io; io = _io;
io.sockets.on('connection', (socket) => { 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}`); logger.debug(`${socket.id} connected from IP ${ip}`);
// wrap the original send function to log the messages // wrap the original send function to log the messages
@ -68,14 +69,14 @@ exports.setSocketIO = (_io) => {
components[i].handleConnect(socket); 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]) { if (!message.component || !components[message.component]) {
throw new Error(`unknown message component: ${message.component}`); throw new Error(`unknown message component: ${message.component}`);
} }
logger.debug(`from ${socket.id}:`, message); logger.debug(`from ${socket.id}:`, message);
return await components[message.component].handleMessage(socket, message); return await components[message.component].handleMessage(socket, message);
})().then( })().then(
(val) => ack(null, val), (val) => ack({name: val.name, message: val.message}),
(err) => { (err) => {
logger.error( logger.error(
`Error handling ${message.component} message from ${socket.id}: ${err.stack || err}`); `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 // 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 // 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. // 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 // tell all components about this disconnect
for (const i of Object.keys(components)) { for (const i of Object.keys(components)) {
components[i].handleDisconnect(socket); 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'; 'use strict';
const eejs = require('../../eejs'); import {required} from '../../eejs';
const settings = require('../../utils/Settings'); import {getEpVersion, getGitCommit} from "../../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');
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) => { 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, plugins: pluginDefs.plugins,
req, req,
errors: [], errors: [],
@ -18,10 +23,10 @@ exports.expressCreateServer = (hookName, args, cb) => {
}); });
args.app.get('/admin/plugins/info', (req, res) => { args.app.get('/admin/plugins/info', (req, res) => {
const gitCommit = settings.getGitCommit(); const gitCommit = getGitCommit();
const epVersion = settings.getEpVersion(); 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, gitCommit,
epVersion, epVersion,
installedPlugins: `<pre>${plugins.formatPlugins().replace(/, /g, '\n')}</pre>`, installedPlugins: `<pre>${plugins.formatPlugins().replace(/, /g, '\n')}</pre>`,
@ -36,10 +41,10 @@ exports.expressCreateServer = (hookName, args, cb) => {
return cb(); return cb();
}; };
exports.socketio = (hookName, args, cb) => { export const socketio = (hookName, args, cb) => {
const io = args.io.of('/pluginfw/installer'); const io = args.io.of('/pluginfw/installer');
io.on('connection', (socket) => { 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; if (!isAdmin) return;
socket.on('getInstalled', (query) => { socket.on('getInstalled', (query) => {

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,8 @@
'use strict'; 'use strict';
import {ResponseSpec} from "../../models/ResponseSpec";
import {APIResource} from "../../models/APIResource";
/** /**
* node/hooks/express/openapi.js * node/hooks/express/openapi.js
* *
@ -54,7 +57,7 @@ const APIPathStyle = {
}; };
// API resources - describe your API endpoints here // API resources - describe your API endpoints here
const resources = { const resources: APIResource = {
// Group // Group
group: { group: {
create: { create: {
@ -375,7 +378,8 @@ const defaultResponses = {
const defaultResponseRefs = { const defaultResponseRefs = {
200: { 200: {
$ref: '#/components/responses/Success', $ref: '#/components/responses/Success', content: undefined
}, },
400: { 400: {
$ref: '#/components/responses/ApiError', $ref: '#/components/responses/ApiError',
@ -491,7 +495,11 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
// build operations // build operations
for (const funcName of Object.keys(apiHandler.version[version])) { for (const funcName of Object.keys(apiHandler.version[version])) {
let operation = {}; let operation: ResponseSpec = {
parameters: undefined,
_restPath: "",
operationId: undefined
};
if (operations[funcName]) { if (operations[funcName]) {
operation = {...operations[funcName]}; operation = {...operations[funcName]};
} else { } 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'; 'use strict';
const securityManager = require('./db/SecurityManager'); import {checkAccess} from './db/SecurityManager';
// checks for padAccess // checks for padAccess
module.exports = async (req, res) => { export default async (req, res) => {
const {session: {user} = {}} = req; const {session: {user} = {}}:SessionSocketModel = req;
const accessObj = await securityManager.checkAccess( const accessObj = await checkAccess(
req.params.pad, req.cookies.sessionID, req.cookies.token, user); req.params.pad, req.cookies.sessionID, req.cookies.token, user);
if (accessObj.accessStatus === 'grant') { if (accessObj.accessStatus === 'grant') {

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

@ -24,10 +24,16 @@
* limitations under the License. * limitations under the License.
*/ */
const log4js = require('log4js'); import log4js from 'log4js';
log4js.replaceConsole(); import * as settings from "./utils/Settings";
/*
const settings = require('./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; let wtfnode;
if (settings.dumpOnUncleanExit) { if (settings.dumpOnUncleanExit) {
@ -36,24 +42,20 @@ if (settings.dumpOnUncleanExit) {
wtfnode = require('wtfnode'); wtfnode = require('wtfnode');
} }
/* enforceMinNodeVersion('12.17.0');
* early check for version compatibility before calling checkDeprecationStatus('12.17.0', '1.9.0');
* 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');
const UpdateCheck = require('./utils/UpdateCheck'); import db = require('./db/DB');
const db = require('./db/DB'); import {} from './db/DB'
const express = require('./hooks/express'); import express = require('./hooks/express');
const hooks = require('../static/js/pluginfw/hooks'); import hooks = require('../static/js/pluginfw/hooks');
const pluginDefs = require('../static/js/pluginfw/plugin_defs'); import pluginDefs = require('../static/js/pluginfw/plugin_defs');
const plugins = require('../static/js/pluginfw/plugins'); import plugins = require('../static/js/pluginfw/plugins');
const {Gate} = require('./utils/promises'); import stats = require('./stats');
const stats = require('./stats'); import {createCollection} from "./stats";
const logger = log4js.getLogger('server'); const logger = log4js.getLogger('server');
console.log = logger.info.bind(logger); // do the same for others - console.debug, etc.
const State = { const State = {
INITIAL: 1, INITIAL: 1,
@ -76,14 +78,14 @@ const removeSignalListener = (signal, listener) => {
}; };
let startDoneGate; let startDoneGate;
exports.start = async () => { export const start = async () => {
switch (state) { switch (state) {
case State.INITIAL: case State.INITIAL:
break; break;
case State.STARTING: case State.STARTING:
await startDoneGate; await startDoneGate;
// Retry. Don't fall through because it might have transitioned to STATE_TRANSITION_FAILED. // Retry. Don't fall through because it might have transitioned to STATE_TRANSITION_FAILED.
return await exports.start(); return await start();
case State.RUNNING: case State.RUNNING:
return express.server; return express.server;
case State.STOPPING: case State.STOPPING:
@ -100,16 +102,16 @@ exports.start = async () => {
state = State.STARTING; state = State.STARTING;
try { try {
// Check if Etherpad version is up-to-date // Check if Etherpad version is up-to-date
UpdateCheck.check(); UpdateCheck.default.check();
stats.gauge('memoryUsage', () => process.memoryUsage().rss); createCollection.gauge('memoryUsage', () => process.memoryUsage().rss);
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed); createCollection.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err) => {
logger.debug(`uncaught exception: ${err.stack || err}`); logger.debug(`uncaught exception: ${err.stack || err}`);
// eslint-disable-next-line promise/no-promise-in-callback // eslint-disable-next-line promise/no-promise-in-callback
exports.exit(err) exit(err)
.catch((err) => { .catch((err) => {
logger.error('Error in process exit', err); logger.error('Error in process exit', err);
// eslint-disable-next-line n/no-process-exit // 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 // 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. // 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}`); logger.debug(`unhandled rejection: ${err.stack || err}`);
throw 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 // 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 // problematic listener. This means that exports.exit is solely responsible for performing all
// necessary cleanup tasks. // necessary cleanup tasks.
for (const listener of process.listeners(signal)) { for (const listener of process.listeners(signal as any)) {
removeSignalListener(signal, listener); removeSignalListener(signal, listener);
} }
process.on(signal, exports.exit); process.on(signal, exit);
// Prevent signal listeners from being added in the future. // Prevent signal listeners from being added in the future.
process.on('newListener', (event, listener) => { process.on('newListener', (event, listener) => {
if (event !== signal) return; if (event !== signal) return;
@ -141,7 +144,7 @@ exports.start = async () => {
await db.init(); await db.init();
await plugins.update(); 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') .filter((plugin) => plugin.package.name !== 'ep_etherpad-lite')
.map((plugin) => `${plugin.package.name}@${plugin.package.version}`) .map((plugin) => `${plugin.package.name}@${plugin.package.version}`)
.join(', '); .join(', ');
@ -154,7 +157,7 @@ exports.start = async () => {
logger.error('Error occurred while starting Etherpad'); logger.error('Error occurred while starting Etherpad');
state = State.STATE_TRANSITION_FAILED; state = State.STATE_TRANSITION_FAILED;
startDoneGate.resolve(); startDoneGate.resolve();
return await exports.exit(err); return await exit(err);
} }
logger.info('Etherpad is running'); logger.info('Etherpad is running');
@ -166,12 +169,12 @@ exports.start = async () => {
}; };
const stopDoneGate = new Gate(); const stopDoneGate = new Gate();
exports.stop = async () => { export const stop = async () => {
switch (state) { switch (state) {
case State.STARTING: case State.STARTING:
await exports.start(); await start();
// Don't fall through to State.RUNNING in case another caller is also waiting for startup. // 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: case State.RUNNING:
break; break;
case State.STOPPING: case State.STOPPING:
@ -201,7 +204,7 @@ exports.stop = async () => {
logger.error('Error occurred while stopping Etherpad'); logger.error('Error occurred while stopping Etherpad');
state = State.STATE_TRANSITION_FAILED; state = State.STATE_TRANSITION_FAILED;
stopDoneGate.resolve(); stopDoneGate.resolve();
return await exports.exit(err); return await exit(err);
} }
logger.info('Etherpad stopped'); logger.info('Etherpad stopped');
state = State.STOPPED; state = State.STOPPED;
@ -210,14 +213,14 @@ exports.stop = async () => {
let exitGate; let exitGate;
let exitCalled = false; let exitCalled = false;
exports.exit = async (err = null) => { export const exit = async (err = null) => {
/* eslint-disable no-process-exit */ /* eslint-disable no-process-exit */
if (err === 'SIGTERM') { if (err === 'SIGTERM') {
// Termination from SIGTERM is not treated as an abnormal termination. // Termination from SIGTERM is not treated as an abnormal termination.
logger.info('Received SIGTERM signal'); logger.info('Received SIGTERM signal');
err = null; err = null;
} else if (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()); logger.error(err.stack || err.toString());
process.exitCode = 1; process.exitCode = 1;
if (exitCalled) { if (exitCalled) {
@ -231,11 +234,11 @@ exports.exit = async (err = null) => {
case State.STARTING: case State.STARTING:
case State.RUNNING: case State.RUNNING:
case State.STOPPING: 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 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 // 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.) // 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.INITIAL:
case State.STOPPED: case State.STOPPED:
case State.STATE_TRANSITION_FAILED: case State.STATE_TRANSITION_FAILED:
@ -275,4 +278,4 @@ exports.exit = async (err = null) => {
/* eslint-enable no-process-exit */ /* 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,7 +26,7 @@ const semver = require('semver');
* *
* @param {String} minNodeVersion Minimum required Node version * @param {String} minNodeVersion Minimum required Node version
*/ */
exports.enforceMinNodeVersion = (minNodeVersion) => { const enforceMinNodeVersion = (minNodeVersion:string) => {
const currentNodeVersion = process.version; const currentNodeVersion = process.version;
// we cannot use template literals, since we still do not know if we are // we cannot use template literals, since we still do not know if we are
@ -49,7 +49,7 @@ exports.enforceMinNodeVersion = (minNodeVersion) => {
* @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated * @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated
* Node releases * Node releases
*/ */
exports.checkDeprecationStatus = (lowestNonDeprecatedNodeVersion, epRemovalVersion) => { const checkDeprecationStatus = (lowestNonDeprecatedNodeVersion:string, epRemovalVersion:string) => {
const currentNodeVersion = process.version; const currentNodeVersion = process.version;
if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) { if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) {
@ -58,3 +58,5 @@ exports.checkDeprecationStatus = (lowestNonDeprecatedNodeVersion, epRemovalVersi
`Please consider updating at least to Node ${lowestNonDeprecatedNodeVersion}`); `Please consider updating at least to Node ${lowestNonDeprecatedNodeVersion}`);
} }
}; };
export {checkDeprecationStatus, enforceMinNodeVersion}

View file

@ -29,12 +29,12 @@
const absolutePaths = require('./AbsolutePaths'); const absolutePaths = require('./AbsolutePaths');
const deepEqual = require('fast-deep-equal/es6'); const deepEqual = require('fast-deep-equal/es6');
const fs = require('fs'); import fs from 'fs';
const os = require('os'); import os from 'os';
const path = require('path'); import path from 'path';
const argv = require('./Cli').argv; const argv = require('./Cli').argv;
const jsonminify = require('jsonminify'); import jsonminify from 'jsonminify';
const log4js = require('log4js'); import log4js from 'log4js';
const randomString = require('./randomstring'); const randomString = require('./randomstring');
const suppressDisableMsg = ' -- To suppress these warning messages change ' + const suppressDisableMsg = ' -- To suppress these warning messages change ' +
'suppressErrorsInPadText to true in your settings.json\n'; '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. // log4js.configure() modifies exports.logconfig so check for equality first.
const logConfigIsDefault = deepEqual(config, defaultLogConfig()); const logConfigIsDefault = deepEqual(config, defaultLogConfig());
log4js.configure(config); log4js.configure(config);
log4js.setGlobalLogLevel(logLevel); log4js.getLogger("console");
log4js.replaceConsole(); console.log = logger.info.bind(logger)
// Log the warning after configuring log4js to increase the chances the user will see it. // Log the warning after configuring log4js to increase the chances the user will see it.
if (!logConfigIsDefault) logger.warn('The logconfig setting is deprecated.'); if (!logConfigIsDefault) logger.warn('The logconfig setting is deprecated.');
}; };
@ -68,16 +68,16 @@ const initLogging = (logLevel, config) => {
initLogging(defaultLogLevel, defaultLogConfig()); initLogging(defaultLogLevel, defaultLogConfig());
/* Root path of the installation */ /* 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 ' + logger.info('All relative paths will be interpreted relative to the identified ' +
`Etherpad base dir: ${exports.root}`); `Etherpad base dir: ${exports.root}`);
exports.settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json'); export const settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json');
exports.credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json'); export const credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json');
/** /**
* The app title, visible e.g. in the browser window * 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 * 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 * is used. If this is a relative path it is interpreted as relative to the
* Etherpad root directory. * Etherpad root directory.
*/ */
exports.favicon = null; export const favicon = null;
/* /*
* Skin name. * Skin name.
@ -93,37 +93,37 @@ exports.favicon = null;
* Initialized to null, so we can spot an old configuration file and invite the * Initialized to null, so we can spot an old configuration file and invite the
* user to update it before falling back to the default. * 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 * 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 * 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 * 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 * 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. * 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 * 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). * Maximum permitted client message size (in bytes).
* *
@ -138,16 +138,16 @@ exports.socketIo = {
/* /*
* The Type of the database * 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 * 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 * The default Text of a new pad
*/ */
exports.defaultPadText = [ export const defaultPadText = [
'Welcome to Etherpad!', 'Welcome to Etherpad!',
'', '',
'This pad text is synchronized as you type, so that everyone viewing this page sees the same ' + 'This pad text is synchronized as you type, so that everyone viewing this page sees the same ' +
@ -159,7 +159,7 @@ exports.defaultPadText = [
/** /**
* The default Pad Settings for a user (Can be overridden by changing the setting * The default Pad Settings for a user (Can be overridden by changing the setting
*/ */
exports.padOptions = { export const padOptions = {
noColors: false, noColors: false,
showControls: true, showControls: true,
showChat: true, showChat: true,
@ -176,7 +176,7 @@ exports.padOptions = {
/** /**
* Whether certain shortcut keys are enabled for a user in the pad * Whether certain shortcut keys are enabled for a user in the pad
*/ */
exports.padShortcutEnabled = { export const padShortcutEnabled = {
altF9: true, altF9: true,
altC: true, altC: true,
delete: true, delete: true,
@ -204,7 +204,7 @@ exports.padShortcutEnabled = {
/** /**
* The toolbar buttons and order. * The toolbar buttons and order.
*/ */
exports.toolbar = { export const toolbar = {
left: [ left: [
['bold', 'italic', 'underline', 'strikethrough'], ['bold', 'italic', 'underline', 'strikethrough'],
['orderedlist', 'unorderedlist', 'indent', 'outdent'], ['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 * 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 * 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). * 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 * A flag that shows if minification is enabled or not
*/ */
exports.minify = true; export const minify = true;
/** /**
* The path of the abiword executable * The path of the abiword executable
*/ */
exports.abiword = null; export const abiword = null;
/** /**
* The path of the libreoffice executable * The path of the libreoffice executable
*/ */
exports.soffice = null; export const soffice = null;
/** /**
* The path of the tidy executable * The path of the tidy executable
*/ */
exports.tidyHtml = null; export const tidyHtml = null;
/** /**
* Should we support none natively supported file types on import? * Should we support none natively supported file types on import?
*/ */
exports.allowUnknownFileEnds = true; export const allowUnknownFileEnds = true;
/** /**
* The log level of log4js * The log level of log4js
*/ */
exports.loglevel = defaultLogLevel; export const loglevel = defaultLogLevel;
/** /**
* Disable IP logging * Disable IP logging
*/ */
exports.disableIPlogging = false; export const disableIPlogging = false;
/** /**
* Number of seconds to automatically reconnect pad * Number of seconds to automatically reconnect pad
*/ */
exports.automaticReconnectionTimeout = 0; export const automaticReconnectionTimeout = 0;
/** /**
* Disable Load Testing * Disable Load Testing
*/ */
exports.loadTest = false; export const loadTest = false;
/** /**
* Disable dump of objects preventing a clean exit * Disable dump of objects preventing a clean exit
*/ */
exports.dumpOnUncleanExit = false; export const dumpOnUncleanExit = false;
/** /**
* Enable indentation on new lines * Enable indentation on new lines
*/ */
exports.indentationOnNewLine = true; export const indentationOnNewLine = true;
/* /*
* log4js appender configuration * log4js appender configuration
*/ */
exports.logconfig = defaultLogConfig(); export const logconfig = defaultLogConfig();
/* /*
* Session Key, do not sure this. * Session Key, do not sure this.
*/ */
exports.sessionKey = false; export const sessionKey = false;
/* /*
* Trust Proxy, whether or not trust the x-forwarded-for header. * 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. * Settings controlling the session cookie issued by Etherpad.
*/ */
exports.cookie = { export const cookie = {
/* /*
* Value of the SameSite cookie property. "Lax" is recommended unless * Value of the SameSite cookie property. "Lax" is recommended unless
* Etherpad will be embedded in an iframe from another site, in which case * 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 * authorization. Note: /admin always requires authentication, and
* either authorization by a module, or a user with is_admin set * either authorization by a module, or a user with is_admin set
*/ */
exports.requireAuthentication = false; export const requireAuthentication = false;
exports.requireAuthorization = false; export const requireAuthorization = false;
exports.users = {}; export const users = {};
/* /*
* Show settings in admin page, by default it is true * 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 * By default, when caret is moved out of viewport, it scrolls the minimum
* height needed to make this line visible. * height needed to make this line visible.
*/ */
exports.scrollWhenFocusLineIsOutOfViewport = { export const scrollWhenFocusLineIsOutOfViewport = {
/* /*
* Percentage of viewport height to be additionally scrolled. * Percentage of viewport height to be additionally scrolled.
*/ */
@ -377,12 +377,12 @@ exports.scrollWhenFocusLineIsOutOfViewport = {
* *
* Do not enable on production machines. * Do not enable on production machines.
*/ */
exports.exposeVersion = false; export const exposeVersion = false;
/* /*
* Override any strings found in locale directories * 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 * 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 * See https://github.com/nfriedly/express-rate-limit for more options
*/ */
exports.importExportRateLimiting = { export const importExportRateLimiting = {
// duration of the rate limit window (milliseconds) // duration of the rate limit window (milliseconds)
windowMs: 90000, windowMs: 90000,
// maximum number of requests per IP to allow during the rate limit window // 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 * 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 of the rate limit window (seconds)
duration: 1, duration: 1,
@ -423,16 +424,16 @@ exports.commitRateLimiting = {
* *
* File size is specified in bytes. Default is 50 MB. * 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 * Disable Admin UI tests
*/ */
exports.enableAdminUITests = false; export const enableAdminUITests = false;
// checks if abiword is avaiable // checks if abiword is avaiable
exports.abiwordAvailable = () => { export const abiwordAvailable = () => {
if (exports.abiword != null) { if (exports.abiword != null) {
return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
} else { } else {
@ -440,7 +441,7 @@ exports.abiwordAvailable = () => {
} }
}; };
exports.sofficeAvailable = () => { export const sofficeAvailable = () => {
if (exports.soffice != null) { if (exports.soffice != null) {
return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
} else { } else {
@ -448,9 +449,9 @@ exports.sofficeAvailable = () => {
} }
}; };
exports.exportAvailable = () => { export const exportAvailable = () => {
const abiword = exports.abiwordAvailable(); const abiword = exports.abiwordAvailable();
const soffice = exports.sofficeAvailable(); const soffice = sofficeAvailable();
if (abiword === 'no' && soffice === 'no') { if (abiword === 'no' && soffice === 'no') {
return 'no'; return 'no';
@ -463,7 +464,7 @@ exports.exportAvailable = () => {
}; };
// Provide git version if available // Provide git version if available
exports.getGitCommit = () => { export const getGitCommit = () => {
let version = ''; let version = '';
try { try {
let rootPath = exports.root; let rootPath = exports.root;
@ -482,13 +483,14 @@ exports.getGitCommit = () => {
} }
version = version.substring(0, 7); version = version.substring(0, 7);
} catch (e) { } 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 version;
}; };
// Return etherpad version from package.json // 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 * 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 * This code refactors a previous version that copied & pasted the same code for
* both "settings.json" and "credentials.json". * 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 || {})) { for (const i of Object.keys(settingsObj || {})) {
if (nonSettings.includes(i)) { if (nonSettings.includes(i)) {
logger.warn(`Ignoring setting: '${i}'`); logger.warn(`Ignoring setting: '${i}'`);
@ -536,9 +539,10 @@ const storeSettings = (settingsObj) => {
* short syntax "${ABIWORD}", and not "${ABIWORD:null}": the latter would result * short syntax "${ABIWORD}", and not "${ABIWORD:null}": the latter would result
* in the literal string "null", instead. * 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 // 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) { if (isNumeric) {
// detected numeric string. Coerce to a number // 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 * 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) => { const stringifiedAndReplaced = JSON.stringify(obj, (key, value) => {
/* /*
* the first invocation of replacer() is with an empty key. Just go on, or * the first invocation of replacer() is with an empty key. Just go on, or
@ -652,6 +656,9 @@ const lookupEnvironmentVariables = (obj) => {
`configuration key "${key}". Falling back to default value.`); `configuration key "${key}". Falling back to default value.`);
return coerceValue(defaultValue); return coerceValue(defaultValue);
} else if ((envVarValue === undefined))
{
return coerceValue("none")
} }
// envVarName contained some value. // envVarName contained some value.
@ -666,9 +673,7 @@ const lookupEnvironmentVariables = (obj) => {
return coerceValue(envVarValue); return coerceValue(envVarValue);
}); });
const newSettings = JSON.parse(stringifiedAndReplaced); return JSON.parse(stringifiedAndReplaced);
return newSettings;
}; };
/** /**
@ -679,7 +684,7 @@ const lookupEnvironmentVariables = (obj) => {
* *
* The isSettings variable only controls the error logging. * The isSettings variable only controls the error logging.
*/ */
const parseSettings = (settingsFilename, isSettings) => { const parseSettings = (settingsFilename: string, isSettings:boolean) => {
let settingsStr = ''; let settingsStr = '';
let settingsType, notFoundMessage, notFoundFunction; let settingsType, notFoundMessage, notFoundFunction;
@ -711,20 +716,22 @@ const parseSettings = (settingsFilename, isSettings) => {
logger.info(`${settingsType} loaded from: ${settingsFilename}`); logger.info(`${settingsType} loaded from: ${settingsFilename}`);
const replacedSettings = lookupEnvironmentVariables(settings); return lookupEnvironmentVariables(settings);
return replacedSettings;
} catch (e) { } catch (e) {
const error = e as Error
logger.error(`There was an error processing your ${settingsType} ` + logger.error(`There was an error processing your ${settingsType} ` +
`file from ${settingsFilename}: ${e.message}`); `file from ${settingsFilename}: ${error.message}`);
process.exit(1); process.exit(1);
} }
}; };
exports.reloadSettings = () => { export const randomVersionString = randomString(4);
const settings = parseSettings(exports.settingsFilename, true);
const credentials = parseSettings(exports.credentialsFilename, false);
export const reloadSettings = () => {
const settings = parseSettings(settingsFilename, true);
const credentials = parseSettings(credentialsFilename, false);
storeSettings(settings); storeSettings(settings);
storeSettings(credentials); storeSettings(credentials);
@ -772,7 +779,7 @@ exports.reloadSettings = () => {
if (exports.abiword) { if (exports.abiword) {
// Check abiword actually exists // Check abiword actually exists
if (exports.abiword != null) { if (exports.abiword != null) {
fs.exists(exports.abiword, (exists) => { fs.exists(exports.abiword, (exists: boolean) => {
if (!exists) { if (!exists) {
const abiwordError = 'Abiword does not exist at this path, check your settings file.'; const abiwordError = 'Abiword does not exist at this path, check your settings file.';
if (!exports.suppressErrorsInPadText) { if (!exports.suppressErrorsInPadText) {
@ -786,7 +793,7 @@ exports.reloadSettings = () => {
} }
if (exports.soffice) { if (exports.soffice) {
fs.exists(exports.soffice, (exists) => { fs.exists(exports.soffice, (exists: boolean) => {
if (!exists) { if (!exists) {
const sofficeError = const sofficeError =
'soffice (libreoffice) does not exist at this path, check your settings file.'; 'soffice (libreoffice) does not exist at this path, check your settings file.';
@ -845,7 +852,6 @@ exports.reloadSettings = () => {
* ACHTUNG: this may prevent caching HTTP proxies to work * ACHTUNG: this may prevent caching HTTP proxies to work
* TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead * 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}`); logger.info(`Random string used for versioning assets: ${exports.randomVersionString}`);
}; };
@ -855,3 +861,5 @@ exports.exportedForTestingOnly = {
// initially load settings // initially load settings
exports.reloadSettings(); 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 * Generates a random String with the given length. Is needed to generate the
* Author, Group, readonly, session Ids * Author, Group, readonly, session Ids
*/ */
const crypto = require('crypto'); import crypto from 'crypto'
const randomString = (len) => crypto.randomBytes(len).toString('hex'); export const randomString = (len) => crypto.randomBytes(len).toString('hex');
module.exports = randomString;

22038
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", "jsonminify": "0.4.2",
"languages4translatewiki": "0.1.3", "languages4translatewiki": "0.1.3",
"lodash.clonedeep": "4.5.0", "lodash.clonedeep": "4.5.0",
"log4js": "0.6.38", "log4js": "^6.9.1",
"measured-core": "^2.0.0", "measured-core": "^2.0.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"npm": "^6.14.15", "npm": "^6.14.15",
@ -90,7 +90,11 @@
"sinon": "^13.0.2", "sinon": "^13.0.2",
"split-grid": "^1.0.11", "split-grid": "^1.0.11",
"supertest": "^6.3.3", "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": { "engines": {
"node": ">=14.15.0", "node": ">=14.15.0",
@ -103,8 +107,10 @@
"scripts": { "scripts": {
"lint": "eslint .", "lint": "eslint .",
"test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", "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", "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 * There is one attribute pool per pad, and it includes every current and historical attribute used
* in the pad. * in the pad.
*/ */
class AttributePool { export class AttributePool {
numToAttrib: {};
private attribToNum: {};
private nextNum: number;
constructor() { constructor() {
/** /**
* Maps an attribute identifier to the attribute's `[key, value]` string pair. * 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"]
}