Moved to ts for other dependencies.

This commit is contained in:
SamTV12345 2023-06-23 20:53:55 +02:00
parent 3c2129b1cc
commit 7b99edc471
No known key found for this signature in database
GPG key ID: E63EEC7466038043
47 changed files with 1056 additions and 1363 deletions

View file

@ -50,41 +50,18 @@ import {getPadHTMLDocument} from "../utils/ExportHtml";
* GROUP FUNCTIONS ****
******************** */
/*
exports.listAllGroups = listAllGroups;
exports.createGroup = createGroup;
exports.createGroupIfNotExistsFor = createGroupIfNotExistsFor;
exports.deleteGroup = deleteGroup;
exports.listPads = listPads;
exports.createGroupPad = createGroupPad;
*/
/* ********************
* PADLIST FUNCTION ***
******************** */
/*
exports.listAllPads = padManager.listAllPads;
*/
/* ********************
* AUTHOR FUNCTIONS ***
******************** */
/*
exports.createAuthor = createAuthor;
exports.createAuthorIfNotExistsFor = createAuthorIfNotExistsFor;
exports.getAuthorName = getAuthorName;
exports.listPadsOfAuthor = listPadsOfAuthor;
exports.padUsers = padMessageHandler.padUsers;
exports.padUsersCount = padMessageHandler.padUsersCount;
*/
/* ********************
* SESSION FUNCTIONS **
******************** */
/*
exports.createSession = sessionManager.createSession;
exports.deleteSession = sessionManager.deleteSession;
exports.getSessionInfo = sessionManager.getSessionInfo;
exports.listSessionsOfGroup = sessionManager.listSessionsOfGroup;
exports.listSessionsOfAuthor = sessionManager.listSessionsOfAuthor;
*/
/* ***********************
* PAD CONTENT FUNCTIONS *
*********************** */
@ -742,7 +719,7 @@ Example returns:
{"code":0,"message":"ok","data":null}
{"code":4,"message":"no or wrong API Key","data":null}
*/
exports.checkToken = async () => {
export const checkToken = async () => {
};
/**

View file

@ -21,7 +21,7 @@
import {db} from './DB';
import {CustomError} from '../utils/customError';
import hooks from '../../static/js/pluginfw/hooks.js';
import {aCallFirst} from '../../static/js/pluginfw/hooks.js';
const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
@ -110,7 +110,7 @@ const getAuthor4Token2 = async (token: string) => {
export const getAuthorId = async (token, user) => {
const context = {dbKey: token, token, user};
let [authorId] = await hooks.aCallFirst('getAuthorId', context);
let [authorId] = await aCallFirst('getAuthorId', context);
if (!authorId) authorId = await getAuthor4Token2(context.dbKey);
return authorId;
};
@ -137,7 +137,7 @@ export const createAuthorIfNotExistsFor = async (authorMapper, name: string) =>
if (name) {
// set the name of this author
await exports.setAuthorName(author.authorID, name);
await setAuthorName(author.authorID, name);
}
return author;
@ -155,7 +155,7 @@ export const mapAuthorWithDBKey = async (mapperkey: string, mapper) => {
if (author == null) {
// there is no author with this mapper, so create one
const author = await exports.createAuthor(null);
const author = await createAuthor(null);
// create the token2author relation
await db.set(`${mapperkey}:${mapper}`, author.authorID);
@ -182,7 +182,7 @@ export const createAuthor = async (name) => {
// create the globalAuthors db entry
const authorObj = {
colorId: Math.floor(Math.random() * (exports.getColorPalette().length)),
colorId: Math.floor(Math.random() * (getColorPalette().length)),
name,
timestamp: Date.now(),
};

View file

@ -31,32 +31,32 @@ const logger = log4js.getLogger('ueberDB');
/**
* The UeberDB Object that provides the database functions
*/
const db = null;
let db = null;
/**
* Initializes the database with the settings provided by the settings module
*/
const init = async () => {
exports.db = new ueberDB.Database(dbType, dbSettings, null, logger);
await exports.db.init();
if (exports.db.metrics != null) {
for (const [metric, value] of Object.entries(exports.db.metrics)) {
db = new ueberDB.Database(dbType, dbSettings, null, logger);
await db.init();
if (db.metrics != null) {
for (const [metric, value] of Object.entries(db.metrics)) {
if (typeof value !== 'number') continue;
// FIXME find a better replacement for measure-core
createCollection.gauge(`ueberdb_${metric}`, () => exports.db.metrics[metric]);
createCollection.gauge(`ueberdb_${metric}`, () => db.metrics[metric]);
}
}
for (const fn of ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove']) {
const f = exports.db[fn];
exports[fn] = async (...args) => await f.call(exports.db, ...args);
const f = db[fn];
exports[fn] = async (...args) => await f.call(db, ...args);
Object.setPrototypeOf(exports[fn], Object.getPrototypeOf(f));
Object.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f));
}
};
const shutdown = async (hookName, context) => {
if (exports.db != null) await exports.db.close();
exports.db = null;
if (db != null) await db.close();
db = null;
logger.log('Database closed');
};

View file

@ -90,8 +90,8 @@ export const createGroupIfNotExistsFor = async (groupMapper) => {
throw new CustomError('groupMapper is not a string', 'apierror');
}
const groupID = await db.get(`mapper2group:${groupMapper}`);
if (groupID && await exports.doesGroupExist(groupID)) return {groupID};
const result = await exports.createGroup();
if (groupID && await doesGroupExist(groupID)) return {groupID};
const result = await createGroup();
await Promise.all([
db.set(`mapper2group:${groupMapper}`, result.groupID),
// Remember the mapping in the group record so that it can be cleaned up when the group is
@ -132,7 +132,7 @@ export const createGroupPad = async (groupID, padName, text, authorId = '') => {
};
export const listPads = async (groupID) => {
const exists = await exports.doesGroupExist(groupID);
const exists = await doesGroupExist(groupID);
// ensure the group exists
if (!exists) {

View file

@ -4,7 +4,7 @@
*/
import AttributeMap from '../../static/js/AttributeMap';
import Changeset from '../../static/js/Changeset';
import {applyToAText, makeAText} from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage';
import {AttributePool} from '../../static/js/AttributePool';
import {Stream} from '../utils/Stream';
@ -19,7 +19,7 @@ import {doesGroupExist} from './GroupManager';
import {CustomError} from '../utils/customError';
import {getReadOnlyId} from './ReadOnlyManager';
import {randomString} from '../utils/randomstring';
import hooks from '../../static/js/pluginfw/hooks';
import {aCallAll} from '../../static/js/pluginfw/hooks';
import {timesLimit} from '../utils/promises';
import {padutils} from '../../static/js/pad_utils';
/**
@ -27,7 +27,7 @@ import {padutils} from '../../static/js/pad_utils';
* line breaks and convert Tabs to spaces
* @param txt
*/
exports.cleanText = (txt) => txt.replace(/\r\n/g, '\n')
export const cleanText = (txt) => txt.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.replace(/\t/g, ' ')
.replace(/\xa0/g, ' ');
@ -51,7 +51,7 @@ export class Pad {
*/
constructor(id: string, database = db) {
this.db = database;
this.atext = Changeset.makeAText('\n');
this.atext = makeAText('\n');
this.pool = new AttributePool();
this.head = -1;
this.chatHead = -1;
@ -83,11 +83,11 @@ export class Pad {
}
async appendRevision(aChangeset, authorId = '') {
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
const newAText = applyToAText(aChangeset, this.atext, this.pool);
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs) {
return this.head;
}
Changeset.copyAText(newAText, this.atext);
copyAText(newAText, this.atext);
const newRev = ++this.head;
@ -109,7 +109,7 @@ export class Pad {
}),
this.saveToDatabase(),
authorId && addPad(authorId, this.id),
hooks.aCallAll(hook, {
aCallAll(hook, {
pad: this,
authorId,
get author() {
@ -188,7 +188,7 @@ export class Pad {
]);
const apool = this.apool();
let atext = keyAText;
for (const cs of changesets) atext = Changeset.applyToAText(cs, atext, apool);
for (const cs of changesets) atext = applyToAText(cs, atext, apool);
return atext;
}
@ -259,7 +259,7 @@ export class Pad {
const orig = this.text();
assert(orig.endsWith('\n'));
if (start + ndel > orig.length) throw new RangeError('start/delete past the end of the text');
ins = exports.cleanText(ins);
ins = cleanText(ins);
const willEndWithNewline =
start + ndel < orig.length || // Keeping last char (which is guaranteed to be a newline).
ins.endsWith('\n') ||
@ -362,14 +362,14 @@ export class Pad {
} else {
if (text == null) {
const context = {pad: this, authorId, type: 'text', content: defaultPadText};
await hooks.aCallAll('padDefaultContent', context);
await aCallAll('padDefaultContent', context);
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
text = exports.cleanText(context.content);
text = cleanText(context.content);
}
const firstChangeset = Changeset.makeSplice('\n', 0, 0, text);
await this.appendRevision(firstChangeset, authorId);
}
await hooks.aCallAll('padLoad', {pad: this});
await aCallAll('padLoad', {pad: this});
}
async copy(destinationID, force) {
@ -405,7 +405,7 @@ export class Pad {
const dstPad = await getPad(destinationID, null);
// let the plugins know the pad was copied
await hooks.aCallAll('padCopy', {
await aCallAll('padCopy', {
get originalPad() {
padutils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
return this.srcPad;
@ -506,7 +506,7 @@ export class Pad {
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
dstPad.appendRevision(changeset, authorId);
await hooks.aCallAll('padCopy', {
await aCallAll('padCopy', {
get originalPad() {
padutils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
return this.srcPad;
@ -571,7 +571,7 @@ export class Pad {
// delete the pad entry and delete pad from padManager
p.push(removePad(padID));
p.push(hooks.aCallAll('padRemove', {
p.push(aCallAll('padRemove', {
get padID() {
padutils.warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
return this.pad.id;
@ -730,7 +730,6 @@ export class Pad {
.batch(100).buffer(99);
for (const p of chats) await p;
await hooks.aCallAll('padCheck', {pad: this});
await aCallAll('padCheck', {pad: this});
}
}
exports.Pad = Pad;

View file

@ -61,11 +61,11 @@ export const getPadId = async (readOnlyId) => await db.get(`readonly2pad:${readO
* @param {String} id padIdOrReadonlyPadId read only id or real pad id
*/
export const getIds = async (id: string) => {
const readonly = exports.isReadOnlyId(id);
const readonly = isReadOnlyId(id);
// Might be null, if this is an unknown read-only id
const readOnlyPadId = readonly ? id : await exports.getReadOnlyId(id);
const padId = readonly ? await exports.getPadId(id) : id;
const readOnlyPadId = readonly ? id : await getReadOnlyId(id);
const padId = readonly ? await getPadId(id) : id;
return {readOnlyPadId, padId, readonly};
};

View file

@ -21,7 +21,7 @@
import {getAuthorId} from "./AuthorManager";
import hooks from "../../static/js/pluginfw/hooks.js";
import {callAll} from "../../static/js/pluginfw/hooks.js";
import {doesPadExist, getPad} from "./PadManager";
@ -102,7 +102,7 @@ export const checkAccess = async (padID, sessionCookie, token, userSettings) =>
// allow plugins to deny access
const isFalse = (x) => x === false;
if (hooks.callAll('onAccessCheck', {padID, token, sessionCookie}).some(isFalse)) {
if (callAll('onAccessCheck', {padID, token, sessionCookie}).some(isFalse)) {
authLogger.debug('access denied: an onAccessCheck hook function returned false');
return DENY;
}

View file

@ -23,7 +23,7 @@
import ejs from 'ejs';
import fs from "fs";
import hooks from "../../static/js/pluginfw/hooks.js";
import {callAll} from "../../static/js/pluginfw/hooks.js";
import path from "path";
@ -64,7 +64,7 @@ export const end_block = () => {
const content = info.__output.get();
info.__output.set(info.__output_stack.pop());
const args = {content, renderContext};
hooks.callAll(`eejsBlock_${name}`, args);
callAll(`eejsBlock_${name}`, args);
info.__output.set(info.__output.get().concat(args.content));
};

View file

@ -32,7 +32,7 @@ import os from 'os';
import {setPadHTML} from '../utils/ImportHtml';
import {setPadRaw} from '../utils/ImportEtherpad';
import log4js from 'log4js';
import hooks from '../../static/js/pluginfw/hooks.js';
import {aCallAll} from '../../static/js/pluginfw/hooks.js';
const logger = log4js.getLogger('ImportHandler');
@ -134,7 +134,7 @@ const doImport = async (req, res, padId, authorId) => {
const destFile = path.join(tmpDirectory, `etherpad_import_${randNum}.${exportExtension}`);
const context = {srcFile, destFile, fileEnding, padId, ImportError};
const importHandledByPlugin = (await hooks.aCallAll('import', context)).some((x) => x);
const importHandledByPlugin = (await aCallAll('import', context)).some((x) => x);
const fileIsEtherpad = (fileEnding === '.etherpad');
const fileIsHTML = (fileEnding === '.html' || fileEnding === '.htm');
const fileIsTXT = (fileEnding === '.txt');

View file

@ -45,15 +45,14 @@ import {
indentationOnNewLine,
padOptions,
padShortcutEnabled,
randomVersionString,
scrollWhenFocusLineIsOutOfViewport,
skinName,
skinVariants,
sofficeAvailable
} from '../utils/Settings';
import plugins from '../../static/js/pluginfw/plugin_defs.js';
import {parts, plugins} from '../../static/js/pluginfw/plugin_defs.js';
import log4js from "log4js";
import hooks from '../../static/js/pluginfw/hooks.js';
import {aCallAll, deprecationNotices} from '../../static/js/pluginfw/hooks.js';
import {createCollection} from '../stats';
import {strict as assert} from "assert";
@ -62,6 +61,7 @@ import {userCanModify} from '../hooks/express/webaccess';
import {ErrorCaused} from "../models/ErrorCaused";
import {Pad} from "../db/Pad";
import {SessionInfo} from "../models/SessionInfo";
import {randomString} from "../utils/randomstring";
const securityManager = require('../db/SecurityManager');
@ -71,7 +71,7 @@ const accessLogger = log4js.getLogger('access');
let rateLimiter;
let socketio = null;
hooks.deprecationNotices.clientReady = 'use the userJoin hook instead';
deprecationNotices.clientReady = 'use the userJoin hook instead';
const addContextToError = (err: Error, pfx) => {
const newErr = new ErrorCaused(`${pfx}${err.message}`, err);
@ -225,7 +225,7 @@ export const handleDisconnect = async (socket) => {
},
},
});
await hooks.aCallAll('userLeave', {
await aCallAll('userLeave', {
...session, // For backwards compatibility.
authorId: session.author,
readOnly: session.readonly,
@ -328,7 +328,7 @@ export const handleMessage = async (socket, message) => {
return this.socket;
},
};
for (const res of await hooks.aCallAll('handleMessageSecurity', context)) {
for (const res of await aCallAll('handleMessageSecurity', context)) {
switch (res) {
case true:
padutils.warnDeprecated(
@ -346,7 +346,7 @@ export const handleMessage = async (socket, message) => {
}
// Call handleMessage hook. If a plugin returns null, the message will be dropped.
if ((await hooks.aCallAll('handleMessage', context)).some((m) => m == null)) {
if ((await aCallAll('handleMessage', context)).some((m) => m == null)) {
return;
}
@ -475,7 +475,7 @@ export const sendChatMessageToPadClients = async (mt, puId, text = null, padId =
const message = mt instanceof ChatMessage ? mt : new ChatMessage(text, puId, mt);
padId = mt instanceof ChatMessage ? puId : padId;
const pad = await getPad(padId, null, message.authorId);
await hooks.aCallAll('chatNewMessage', {message, pad, padId});
await aCallAll('chatNewMessage', {message, pad, padId});
// pad.appendChatMessage() ignores the displayName property so we don't need to wait for
// authorManager.getAuthorName() to resolve before saving the message to the database.
const promise = pad.appendChatMessage(message);
@ -809,7 +809,7 @@ const handleClientReady = async (socket, message) => {
if (sessionInfo == null) throw new Error('client disconnected');
assert(sessionInfo.author);
await hooks.aCallAll('clientReady', message); // Deprecated due to awkward context.
await aCallAll('clientReady', message); // Deprecated due to awkward context.
let {colorId: authorColorId, name: authorName} = message.userInfo || {};
if (authorColorId && !/^#(?:[0-9A-F]{3}){1,2}$/i.test(authorColorId)) {
@ -957,7 +957,7 @@ const handleClientReady = async (socket, message) => {
const clientVars = {
skinName: skinName,
skinVariants: skinVariants,
randomVersionString: randomVersionString,
randomVersionString: randomString(4),
accountPrivs: {
maxRevisions: 100,
},
@ -995,8 +995,8 @@ const handleClientReady = async (socket, message) => {
sofficeAvailable: sofficeAvailable(),
exportAvailable: exportAvailable(),
plugins: {
plugins: plugins.plugins,
parts: plugins.parts,
plugins: plugins,
parts: parts,
},
indentationOnNewLine: indentationOnNewLine,
scrollWhenFocusLineIsOutOfViewport: {
@ -1022,7 +1022,7 @@ const handleClientReady = async (socket, message) => {
}
// call the clientVars-hook so plugins can modify them before they get sent to the client
const messages = await hooks.aCallAll('clientVars', {clientVars, pad, socket});
const messages = await aCallAll('clientVars', {clientVars, pad, socket});
// combine our old object with the new attributes from the hook
for (const msg of messages) {
@ -1093,7 +1093,7 @@ const handleClientReady = async (socket, message) => {
socket.json.send(msg);
}));
await hooks.aCallAll('userJoin', {
await aCallAll('userJoin', {
authorId: sessionInfo.author,
displayName: authorName,
padId: sessionInfo.padId,

View file

@ -10,7 +10,7 @@ import fs from "fs";
import expressSession from "express-session";
import hooks from "../../static/js/pluginfw/hooks";
import {aCallAll} from "../../static/js/pluginfw/hooks";
import log4js from "log4js";
@ -52,7 +52,7 @@ const closeServer = async () => {
// Call exports.server.close() to reject new connections but don't await just yet because the
// Promise won't resolve until all preexisting connections are closed.
const p = util.promisify(server.close.bind(server))();
await hooks.aCallAll('expressCloseServer');
await aCallAll('expressCloseServer');
// Give existing connections some time to close on their own before forcibly terminating. The
// time should be long enough to avoid interrupting most preexisting transmissions but short
// enough to avoid a noticeable outage.
@ -240,14 +240,14 @@ sessionMiddleware = expressSession({
// Give plugins an opportunity to install handlers/middleware before the express-session
// middleware. This allows plugins to avoid creating an express-session record in the database
// when it is not needed (e.g., public static content).
await hooks.aCallAll('expressPreSession', {app});
await aCallAll('expressPreSession', {app});
app.use(sessionMiddleware);
app.use(checkAccess2);
await Promise.all([
hooks.aCallAll('expressConfigure', {app}),
hooks.aCallAll('expressCreateServer', {app, server: server}),
aCallAll('expressConfigure', {app}),
aCallAll('expressCreateServer', {app, server: server}),
]);
server.on('connection', (socket) => {
sockets.add(socket);

View file

@ -3,11 +3,11 @@
import {required} from '../../eejs';
import {getEpVersion, getGitCommit} from "../../utils/Settings";
import installer from "../../../static/js/pluginfw/installer";
import {getAvailablePlugins, install, search, uninstall} from "../../../static/js/pluginfw/installer";
import pluginDefs from "../../../static/js/pluginfw/plugin_defs";
import {plugins} from "../../../static/js/pluginfw/plugin_defs";
import plugins from "../../../static/js/pluginfw/plugins";
import {formatHooks, formatParts, formatPlugins} from "../../../static/js/pluginfw/plugins";
import semver from "semver";
@ -16,7 +16,7 @@ import UpdateCheck from "../../utils/UpdateCheck";
export const expressCreateServer = (hookName, args, cb) => {
args.app.get('/admin/plugins', (req, res) => {
res.send(required('ep_etherpad-lite/templates/admin/plugins.html', {
plugins: pluginDefs.plugins,
plugins: plugins,
req,
errors: [],
}));
@ -29,10 +29,10 @@ export const expressCreateServer = (hookName, args, cb) => {
res.send(required('ep_etherpad-lite/templates/admin/plugins-info.html', {
gitCommit,
epVersion,
installedPlugins: `<pre>${plugins.formatPlugins().replace(/, /g, '\n')}</pre>`,
installedParts: `<pre>${plugins.formatParts()}</pre>`,
installedServerHooks: `<div>${plugins.formatHooks('hooks', true)}</div>`,
installedClientHooks: `<div>${plugins.formatHooks('client_hooks', true)}</div>`,
installedPlugins: `<pre>${formatPlugins().replace(/, /g, '\n')}</pre>`,
installedParts: `<pre>${formatParts()}</pre>`,
installedServerHooks: `<div>${formatHooks('hooks', true)}</div>`,
installedClientHooks: `<div>${formatHooks('client_hooks', true)}</div>`,
latestVersion: UpdateCheck.getLatestVersion(),
req,
}));
@ -50,7 +50,7 @@ export const socketio = (hookName, args, cb) => {
socket.on('getInstalled', (query) => {
// send currently installed plugins
const installed =
Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package);
Object.keys(plugins).map((plugin) => plugins[plugin].package);
socket.emit('results:installed', {installed});
});
@ -58,13 +58,13 @@ export const socketio = (hookName, args, cb) => {
socket.on('checkUpdates', async () => {
// Check plugins for updates
try {
const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ 60 * 10);
const results = await getAvailablePlugins(/* maxCacheAge:*/ 60 * 10);
const updatable = Object.keys(pluginDefs.plugins).filter((plugin) => {
const updatable = Object.keys(plugins).filter((plugin) => {
if (!results[plugin]) return false;
const latestVersion = results[plugin].version;
const currentVersion = pluginDefs.plugins[plugin].package.version;
const currentVersion = plugins[plugin].package.version;
return semver.gt(latestVersion, currentVersion);
});
@ -79,7 +79,7 @@ export const socketio = (hookName, args, cb) => {
socket.on('getAvailable', async (query) => {
try {
const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ false);
const results = await getAvailablePlugins(/* maxCacheAge:*/ false);
socket.emit('results:available', results);
} catch (er) {
console.error(er);
@ -89,10 +89,10 @@ export const socketio = (hookName, args, cb) => {
socket.on('search', async (query) => {
try {
const results = await installer.search(query.searchTerm, /* maxCacheAge:*/ 60 * 10);
const results = await search(query.searchTerm, /* maxCacheAge:*/ 60 * 10);
let res = Object.keys(results)
.map((pluginName) => results[pluginName])
.filter((plugin) => !pluginDefs.plugins[plugin.name]);
.filter((plugin) => !plugins[plugin.name]);
res = sortPluginList(res, query.sortBy, query.sortDir)
.slice(query.offset, query.offset + query.limit);
socket.emit('results:search', {results: res, query});
@ -104,7 +104,7 @@ export const socketio = (hookName, args, cb) => {
});
socket.on('install', (pluginName) => {
installer.install(pluginName, (err) => {
install(pluginName, (err) => {
if (err) console.warn(err.stack || err.toString());
socket.emit('finished:install', {
@ -116,7 +116,7 @@ export const socketio = (hookName, args, cb) => {
});
socket.on('uninstall', (pluginName) => {
installer.uninstall(pluginName, (err) => {
uninstall(pluginName, (err) => {
if (err) console.warn(err.stack || err.toString());
socket.emit('finished:uninstall', {plugin: pluginName, error: err ? err.message : null});

View file

@ -3,14 +3,14 @@
import {required} from '../../eejs';
import {promises as fsp} from "fs";
import hooks from "../../../static/js/pluginfw/hooks";
import {aCallAll} from "../../../static/js/pluginfw/hooks";
import plugins from "../../../static/js/pluginfw/plugins";
import {update} from "../../../static/js/pluginfw/plugins";
import {reloadSettings, settingsFilename, showSettingsInAdminPage} from "../../utils/Settings";
import * as settings from "../../utils/Settings";
exports.expressCreateServer = (hookName, {app}) => {
export const expressCreateServer = (hookName, {app}) => {
app.get('/admin/settings', (req, res) => {
res.send(required('ep_etherpad-lite/templates/admin/settings.html', {
req,
@ -20,7 +20,7 @@ exports.expressCreateServer = (hookName, {app}) => {
});
};
exports.socketio = (hookName, {io}) => {
export const socketio = (hookName, {io}) => {
io.of('/settings').on('connection', (socket) => {
const {session: {user: {is_admin: isAdmin} = {}} = {}}:SessionSocketModel = socket.conn.request;
if (!isAdmin) return;
@ -50,9 +50,9 @@ exports.socketio = (hookName, {io}) => {
socket.on('restartServer', async () => {
console.log('Admin request to restart server through a socket on /admin/settings');
reloadSettings();
await plugins.update();
await hooks.aCallAll('loadSettings', {});
await hooks.aCallAll('restartServer');
await update();
await aCallAll('loadSettings', {});
await aCallAll('restartServer');
});
});
};

View file

@ -2,8 +2,9 @@
import {createCollection} from '../../stats';
exports.expressCreateServer = (hook_name, args, cb) => {
exports.app = args.app;
export let app:any;
export const expressCreateServer = (hook_name, args, cb) => {
app = args.app;
// Handle errors
args.app.use((err, req, res, next) => {

View file

@ -10,7 +10,7 @@ import rateLimit from 'express-rate-limit';
import {checkAccess} from '../../db/SecurityManager';
import {userCanModify} from './webaccess';
exports.expressCreateServer = (hookName, args, cb) => {
export const expressCreateServer = (hookName, args, cb) => {
importExportRateLimiting.onLimitReached = (req, res, options) => {
// when the rate limiter triggers, write a warning in the logs
console.warn('Import/Export rate limiter triggered on ' +

View file

@ -548,7 +548,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
return definition;
};
exports.expressPreSession = async (hookName, {app}) => {
export const expressPreSession = async (hookName, {app}) => {
// create openapi-backend handlers for each api version under /api/{version}/*
for (const version of Object.keys(apiHandler.version)) {
// we support two different styles of api: flat + rest

View file

@ -2,7 +2,7 @@
import {isValidPadId, sanitizePadId} from '../../db/PadManager';
exports.expressCreateServer = (hookName, args, cb) => {
export const expressCreateServer = (hookName, args, cb) => {
// redirects browser to the pad's sanitized url if needed. otherwise, renders the html
args.app.param('pad', (req, res, next, padId) => {
(async () => {

View file

@ -7,7 +7,7 @@ import proxyaddr from 'proxy-addr';
import {socketIo, socketTransportProtocols, trustProxy} from '../../utils/Settings';
import socketio from 'socket.io';
import {addComponent, setSocketIO} from '../../handler/SocketIORouter';
import hooks from '../../../static/js/pluginfw/hooks';
import {callAll} from '../../../static/js/pluginfw/hooks';
import * as padMessageHandler from '../../handler/PadMessageHandler';
let io;
@ -15,7 +15,7 @@ const logger = log4js.getLogger('socket.io');
const sockets = new Set();
const socketsEvents = new events.EventEmitter();
exports.expressCloseServer = async () => {
export const expressCloseServer = async () => {
if (io == null) return;
logger.info('Closing socket.io engine...');
// Close the socket.io engine to disconnect existing clients and reject new clients. Don't call
@ -46,7 +46,7 @@ exports.expressCloseServer = async () => {
logger.info('All socket.io clients have disconnected');
};
exports.expressCreateServer = (hookName, args, cb) => {
export const expressCreateServer = (hookName, args, cb) => {
// init socket.io and redirect all requests to the MessageHandler
// there shouldn't be a browser that isn't compatible to all
// transports in this list at once
@ -133,7 +133,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
setSocketIO(io);
addComponent('pad', padMessageHandler);
hooks.callAll('socketio', {app: args.app, io, server: args.server});
callAll('socketio', {app: args.app, io, server: args.server});
return cb();
};

View file

@ -5,12 +5,12 @@ import {required} from '../../eejs';
import fs from 'fs';
const fsp = fs.promises;
import {} from '../../utils/toolbar';
import hooks from '../../../static/js/pluginfw/hooks';
import {callAll} from '../../../static/js/pluginfw/hooks';
import {favicon, getEpVersion, maxAge, root, skinName} from '../../utils/Settings';
import util from 'util';
import {userCanModify} from './webaccess';
exports.expressPreSession = async (hookName, {app}) => {
export const expressPreSession = async (hookName, {app}) => {
// This endpoint is intended to conform to:
// https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html
app.get('/health', (req, res) => {
@ -63,7 +63,7 @@ exports.expressPreSession = async (hookName, {app}) => {
});
};
exports.expressCreateServer = (hookName, args, cb) => {
export const expressCreateServer = (hookName, args, cb) => {
// serve index.html under /
args.app.get('/', (req, res) => {
res.send(required('ep_etherpad-lite/templates/index.html', {req}));
@ -74,7 +74,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
// The below might break for pads being rewritten
const isReadOnly = !userCanModify(req.params.pad, req);
hooks.callAll('padInitToolbar', {
callAll('padInitToolbar', {
toolbar,
isReadOnly,
});
@ -90,7 +90,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
// serve timeslider.html under /p/$padname/timeslider
args.app.get('/p/:pad/timeslider', (req, res, next) => {
hooks.callAll('padInitToolbar', {
callAll('padInitToolbar', {
toolbar,
});

View file

@ -3,7 +3,7 @@
import path from 'path';
import {promises as fsp} from "fs";
import plugins from "../../../static/js/pluginfw/plugin_defs";
import {plugins} from "../../../static/js/pluginfw/plugin_defs";
import sanitizePathname from "../../utils/sanitizePathname";
@ -36,7 +36,7 @@ export const expressPreSession = async (hookName, {app}) => {
app.get('/tests/frontend/frontendTestSpecs.json', (req, res, next) => {
(async () => {
const modules = [];
await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => {
await Promise.all(Object.entries(plugins).map(async ([plugin, def]) => {
const mappedDef = def as Presession;
let {package: {path: pluginPath}} = mappedDef;
if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep;

View file

@ -6,17 +6,18 @@ import log4js from "log4js";
import {requireAuthentication, requireAuthorization, setUsers, users} from "../../utils/Settings";
import hooks from "../../../static/js/pluginfw/hooks";
import {deprecationNotices} from "../../../static/js/pluginfw/hooks";
import {getPadId, isReadOnlyId} from "../../db/ReadOnlyManager";
import {UserIndexedModel} from "../../models/UserIndexedModel";
const httpLogger = log4js.getLogger('http');
hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
// Promisified wrapper around hooks.aCallFirst.
const aCallFirst = (hookName, context, pred = null) => new Promise((resolve, reject) => {
hooks.aCallFirst(hookName, context, (err, r) => err != null ? reject(err) : resolve(r), pred);
// FIXME Why are there 4 arguments but only 3 parameters?
aCallFirst(hookName, context, (err, r) => err != null ? reject(err) : resolve(r));
});
const aCallFirst0 =

View file

@ -4,7 +4,7 @@ import languages from 'languages4translatewiki';
import fs from 'fs';
import path from 'path';
import _ from 'underscore';
import pluginDefs from '../../static/js/pluginfw/plugin_defs.js';
import {plugins} from '../../static/js/pluginfw/plugin_defs.js';
import {check} from '../utils/path_exists';
import {customLocaleStrings, maxAge, root} from '../utils/Settings';
import {Presession} from "../models/Presession";
@ -41,7 +41,7 @@ const getAllLocales = () => {
extractLangs(path.join(root, 'src/locales'));
// add plugins languages (if any)
for (const val of Object.values(pluginDefs.plugins)) {
for (const val of Object.values(plugins)) {
const pluginPath:Presession = val as Presession
// plugin locales should overwrite etherpad's core locales
if (pluginPath.package.path.endsWith('/ep_etherpad-lite') === true) continue;
@ -86,7 +86,7 @@ const getAllLocales = () => {
// returns a hash of all available languages availables with nativeName and direction
// e.g. { es: {nativeName: "español", direction: "ltr"}, ... }
const getAvailableLangs = (locales) => {
export const getAvailableLangs = (locales) => {
const result = {};
for (const langcode of Object.keys(locales)) {
result[langcode] = languages.getLanguageInfo(langcode);
@ -104,16 +104,16 @@ const generateLocaleIndex = (locales) => {
};
exports.expressPreSession = async (hookName, {app}) => {
export const expressPreSession = async (hookName, {app}) => {
// regenerate locales on server restart
const locales = getAllLocales();
const localeIndex = generateLocaleIndex(locales);
exports.availableLangs = getAvailableLangs(locales);
let availableLangs = getAvailableLangs(locales);
app.get('/locales/:locale', (req, res) => {
// works with /locale/en and /locale/en.json requests
const locale = req.params.locale.split('.')[0];
if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) {
if (Object.prototype.hasOwnProperty.call(availableLangs, locale)) {
res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`);

View file

@ -0,0 +1,3 @@
export type CMDArgv = {
[key: string]: any
}

View file

@ -25,10 +25,12 @@ import async from 'async';
import {abiword} from './Settings';
import os from 'os';
export let convertFile;
// on windows we have to spawn a process for each convertion,
// cause the plugin abicommand doesn't exist on this platform
if (os.type().indexOf('Windows') > -1) {
exports.convertFile = async (srcFile, destFile, type) => {
convertFile = async (srcFile, destFile, type) => {
const abiword2 = spawn(abiword, [`--to=${destFile}`, srcFile]);
let stdoutBuffer = '';
abiword2.stdout.on('data', (data) => { stdoutBuffer += data.toString(); });
@ -85,7 +87,7 @@ if (os.type().indexOf('Windows') > -1) {
};
}, 1);
exports.convertFile = async (srcFile, destFile, type) => {
convertFile = async (srcFile, destFile, type) => {
await queue.pushAsync({srcFile, destFile, type});
};
}

View file

@ -21,32 +21,35 @@
*/
// An object containing the parsed command-line options
export const argv = process.argv.slice(2);
import {CMDArgv} from "../models/CMDArgv";
let arg, prevArg;
const argvcmd = process.argv.slice(2);
let arg, prevArg
export const argv:CMDArgv|undefined = {};
// Loop through args
for (let i = 0; i < argv.length; i++) {
for (let i = 0; i < argvcmd.length; i++) {
arg = argv[i];
// Override location of settings.json file
if (prevArg === '--settings' || prevArg === '-s') {
exports.argv.settings = arg;
argv.settings = arg;
}
// Override location of credentials.json file
if (prevArg === '--credentials') {
exports.argv.credentials = arg;
argv.credentials = arg;
}
// Override location of settings.json file
if (prevArg === '--sessionkey') {
exports.argv.sessionkey = arg;
argv.sessionkey = arg;
}
// Override location of settings.json file
if (prevArg === '--apikey') {
exports.argv.apikey = arg;
argv.apikey = arg;
}
prevArg = arg;

View file

@ -20,7 +20,7 @@ import {strict as assert} from "assert";
import {getAuthor} from "../db/AuthorManager";
import hooks from "../../static/js/pluginfw/hooks";
import {aCallAll} from "../../static/js/pluginfw/hooks";
import {getPad} from "../db/PadManager";
@ -28,7 +28,7 @@ export const getPadRaw = async (padId, readOnlyId) => {
const dstPfx = `pad:${readOnlyId || padId}`;
const [pad, customPrefixes] = await Promise.all([
getPad(padId),
hooks.aCallAll('exportEtherpadAdditionalContent'),
aCallAll('exportEtherpadAdditionalContent'),
]);
const pluginRecords = await Promise.all(customPrefixes.map(async (customPrefix) => {
const srcPfx = `${customPrefix}:${padId}`;
@ -58,7 +58,7 @@ export const getPadRaw = async (padId, readOnlyId) => {
})();
const data = {[dstPfx]: pad};
for (const [dstKey, p] of new Stream(records).batch(100).buffer(99)) data[dstKey] = await p;
await hooks.aCallAll('exportEtherpad', {
await aCallAll('exportEtherpad', {
pad,
data,
dstPadId: readOnlyId || padId,

View file

@ -23,7 +23,7 @@ import {getPad} from "../db/PadManager";
import _ from "underscore";
import Security from '../../static/js/security';
import hooks from '../../static/js/pluginfw/hooks';
import {aCallAll} from '../../static/js/pluginfw/hooks';
import {required} from '../eejs';
import {_analyzeLine, _encodeWhitespace} from "./ExportHelper";
@ -51,7 +51,7 @@ export const getHTMLFromAtext = async (pad, atext, authorColors?) => {
await Promise.all([
// prepare tags stored as ['tag', true] to be exported
hooks.aCallAll('exportHtmlAdditionalTags', pad).then((newProps) => {
aCallAll('exportHtmlAdditionalTags', pad).then((newProps) => {
newProps.forEach((prop) => {
tags.push(prop);
props.push(prop);
@ -59,7 +59,7 @@ export const getHTMLFromAtext = async (pad, atext, authorColors?) => {
}),
// prepare tags stored as ['tag', 'value'] to be exported. This will generate HTML with tags
// like <span data-tag="value">
hooks.aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps) => {
aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps) => {
newProps.forEach((prop) => {
tags.push(`span data-${prop[0]}="${prop[1]}"`);
props.push(prop);
@ -314,7 +314,7 @@ export const getHTMLFromAtext = async (pad, atext, authorColors?) => {
if (i < textLines.length) {
nextLine = _analyzeLine(textLines[i + 1], attribLines[i + 1], apool);
}
await hooks.aCallAll('getLineHTMLForExport', context);
await aCallAll('getLineHTMLForExport', context);
// To create list parent elements
if ((!prevLine || prevLine.listLevel !== line.listLevel) ||
(line.listTypeName !== prevLine.listTypeName)) {
@ -451,7 +451,7 @@ export const getHTMLFromAtext = async (pad, atext, authorColors?) => {
padId: pad.id,
};
await hooks.aCallAll('getLineHTMLForExport', context);
await aCallAll('getLineHTMLForExport', context);
pieces.push(context.lineContent, '<br>');
}
}
@ -464,14 +464,14 @@ export const getPadHTMLDocument = async (padId, revNum, readOnlyId?) => {
// Include some Styles into the Head for Export
let stylesForExportCSS = '';
const stylesForExport = await hooks.aCallAll('stylesForExport', padId);
const stylesForExport = await aCallAll('stylesForExport', padId);
stylesForExport.forEach((css) => {
stylesForExportCSS += css;
});
let html = await getPadHTML(pad, revNum);
for (const hookHtml of await hooks.aCallAll('exportHTMLAdditionalContent', {padId})) {
for (const hookHtml of await aCallAll('exportHTMLAdditionalContent', {padId})) {
html += hookHtml;
}

View file

@ -21,7 +21,7 @@ import {Pad} from '../db/Pad';
import {Stream} from './Stream';
import {addPad, doesAuthorExist} from '../db/AuthorManager';
import {db} from '../db/DB';
import hooks from '../../static/js/pluginfw/hooks';
import {aCallAll, callAll} from '../../static/js/pluginfw/hooks';
import log4js from "log4js";
import {supportedElems} from "../../static/js/contentcollector";
@ -34,14 +34,14 @@ export const setPadRaw = async (padId, r, authorId = '') => {
const records = JSON.parse(r);
// get supported block Elements from plugins, we will use this later.
hooks.callAll('ccRegisterBlockElements').forEach((element) => {
callAll('ccRegisterBlockElements').forEach((element) => {
supportedElems.add(element);
});
// DB key prefixes for pad records. Each key is expected to have the form `${prefix}:${padId}` or
// `${prefix}:${padId}:${otherstuff}`.
const padKeyPrefixes = [
...await hooks.aCallAll('exportEtherpadAdditionalContent'),
...await aCallAll('exportEtherpadAdditionalContent'),
'pad',
];
@ -103,7 +103,7 @@ export const setPadRaw = async (padId, r, authorId = '') => {
const pad = new Pad(padId, padDb);
await pad.init(null, authorId);
await hooks.aCallAll('importEtherpad', {
await aCallAll('importEtherpad', {
pad,
// Shallow freeze meant to prevent accidental bugs. It would be better to deep freeze, but
// it's not worth the added complexity.

View file

@ -26,7 +26,7 @@ import {promises as fs} from "fs";
import path from "path";
import plugins from "../../static/js/pluginfw/plugin_defs";
import {plugins} from "../../static/js/pluginfw/plugin_defs";
import RequireKernel from "etherpad-require-kernel";
@ -155,8 +155,8 @@ const minify = async (req, res) => {
const library = match[1];
const libraryPath = match[2] || '';
if (plugins.plugins[library] && match[3]) {
const plugin = plugins.plugins[library];
if (plugins[library] && match[3]) {
const plugin = plugins[library];
const pluginPath = plugin.package.realPath;
filename = path.join(pluginPath, libraryPath);
// On Windows, path.relative converts forward slashes to backslashes. Convert them back

View file

@ -29,19 +29,20 @@
import exp from "constants";
const absolutePaths = require('./AbsolutePaths');
const deepEqual = require('fast-deep-equal/es6');
import {findEtherpadRoot, makeAbsolute, isSubdir} from './AbsolutePaths';
import deepEqual from 'fast-deep-equal/es6';
import fs from 'fs';
import os from 'os';
import path from 'path';
const argv = require('./Cli').argv;
import jsonminify from 'jsonminify';
import log4js from 'log4js';
import {LogLevel} from "../models/LogLevel";
const randomString = require('./randomstring');
import {argv} from "./Cli";
import {randomString} from './randomstring';
const suppressDisableMsg = ' -- To suppress these warning messages change ' +
'suppressErrorsInPadText to true in your settings.json\n';
const _ = require('underscore');
import _ from 'underscore';
const logger = log4js.getLogger('settings');
@ -53,7 +54,10 @@ const nonSettings = [
// This is a function to make it easy to create a new instance. It is important to not reuse a
// config object after passing it to log4js.configure() because that method mutates the object. :(
const defaultLogConfig = () => ({appenders: [{type: 'console'}]});
const defaultLogConfig = () => ({appenders: { console: { type: 'console' } },
categories:{
default: { appenders: ['console'], level: 'info'}
}});
const defaultLogLevel = 'INFO';
const initLogging = (logLevel, config) => {
@ -71,11 +75,11 @@ const initLogging = (logLevel, config) => {
initLogging(defaultLogLevel, defaultLogConfig());
/* Root path of the installation */
export const root = absolutePaths.findEtherpadRoot();
export const root = findEtherpadRoot();
logger.info('All relative paths will be interpreted relative to the identified ' +
`Etherpad base dir: ${root}`);
export const settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json');
export const credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json');
export const settingsFilename = makeAbsolute(argv.settings || 'settings.json');
export const credentialsFilename = makeAbsolute(argv.credentials || 'credentials.json');
/**
* The app title, visible e.g. in the browser window
@ -737,9 +741,7 @@ const parseSettings = (settingsFilename: string, isSettings:boolean) => {
process.exit(1);
}
};
export const randomVersionString = randomString(4);
}
export const reloadSettings = () => {
@ -772,7 +774,7 @@ export const reloadSettings = () => {
let skinPath = path.join(skinBasePath, skinName);
// what if someone sets skinName == ".." or "."? We catch him!
if (absolutePaths.isSubdir(skinBasePath, skinPath) === false) {
if (isSubdir(skinBasePath, skinPath) === false) {
logger.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. ` +
'Falling back to the default "colibris".');
@ -821,7 +823,7 @@ export const reloadSettings = () => {
}
if (!sessionKey) {
const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt');
const sessionkeyFilename = makeAbsolute(argv.sessionkey || './SESSIONKEY.txt');
try {
sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8');
logger.info(`Session key loaded from: ${sessionkeyFilename}`);
@ -846,7 +848,7 @@ export const reloadSettings = () => {
defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`;
}
dbSettings.filename = absolutePaths.makeAbsolute(dbSettings.filename);
dbSettings.filename = makeAbsolute(dbSettings.filename);
logger.warn(`${dirtyWarning} File location: ${dbSettings.filename}`);
}
@ -867,10 +869,12 @@ export const reloadSettings = () => {
* ACHTUNG: this may prevent caching HTTP proxies to work
* TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead
*/
logger.info(`Random string used for versioning assets: ${randomVersionString}`);
const randomVersionStringWith4Chars = randomString(4);
logger.info(`Random string used for versioning assets: ${randomVersionStringWith4Chars}`);
return settings
};
exports.exportedForTestingOnly = {
export const exportedForTestingOnly = {
parseSettings,
};

View file

@ -8,7 +8,7 @@ import {tidyHtml} from "./Settings";
import {spawn} from "child_process";
exports.tidy = (srcFile) => {
export const tidy = (srcFile) => {
const logger = log4js.getLogger('TidyHtml');
return new Promise((resolve, reject) => {

View file

@ -28,7 +28,7 @@ const loadEtherpadInformations = () => new Promise<InfoModel>((resolve, reject)
});
const getLatestVersion = () => {
exports.needsUpdate();
needsUpdate();
if(infos === undefined){
throw new Error("Could not retrieve latest version")
}
@ -36,7 +36,7 @@ const getLatestVersion = () => {
return infos.latestVersion;
}
exports.needsUpdate = (cb?:(arg0: boolean)=>void) => {
export const needsUpdate = (cb?:(arg0: boolean)=>void) => {
loadEtherpadInformations().then((info) => {
if (semver.gt(info.latestVersion, getEpVersion())) {
if (cb) return cb(true);

View file

@ -10,6 +10,7 @@
export class CustomError extends Error {
code: any;
signal: any;
easysync: boolean
/**
* Creates an instance of CustomError.
* @param {*} message

View file

@ -254,7 +254,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
// They include final newlines on lines.
const linesGet = (idx) => {
if (lines.get) {
if ("get" in lines && lines.get instanceof Function) {
return lines.get(idx);
} else {
return lines[idx];
@ -262,7 +262,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
};
const aLinesGet = (idx) => {
if (alines.get) {
if ("get" in alines && alines.get instanceof Function) {
return alines.get(idx);
} else {
return alines[idx];

View file

@ -69,7 +69,7 @@ const logLines = (readable, logLineFn) => {
* - `stderr`: Similar to `stdout` but for stderr.
* - `child`: The ChildProcess object.
*/
export const exportCMD: (args: string[], opts:CMDOptions)=>void = async (args, opts = {
export const exportCMD: (args: string[], opts?:CMDOptions)=>void = async (args, opts = {
cwd: undefined,
stdio: undefined,
env: undefined

1653
src/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -31,6 +31,7 @@
],
"dependencies": {
"async": "^3.2.2",
"cjstoesm": "^2.1.2",
"clean-css": "^5.3.2",
"cookie-parser": "^1.4.6",
"cross-spawn": "^7.0.3",
@ -113,6 +114,5 @@
"dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/server.js\""
},
"version": "1.9.0",
"license": "Apache-2.0",
"type": "module"
"license": "Apache-2.0"
}

View file

@ -22,6 +22,8 @@
* https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js
*/
import {CustomError} from "../../node/utils/customError";
const AttributeMap = require('./AttributeMap');
const AttributePool = require('./AttributePool');
const attributes = require('./attributes');
@ -47,8 +49,8 @@ const {padutils} = require('./pad_utils');
*
* @param {string} msg - Just some message
*/
const error = (msg) => {
const e = new Error(msg);
export const error = (msg) => {
const e = new CustomError(msg);
e.easysync = true;
throw e;
};
@ -71,7 +73,7 @@ const assert = (b, msg) => {
* @param {string} str - string of the number in base 36
* @returns {number} number
*/
exports.parseNum = (str) => parseInt(str, 36);
export const parseNum = (str) => parseInt(str, 36);
/**
* Writes a number in base 36 and puts it in a string.
@ -79,12 +81,16 @@ exports.parseNum = (str) => parseInt(str, 36);
* @param {number} num - number
* @returns {string} string
*/
exports.numToString = (num) => num.toString(36).toLowerCase();
export const numToString = (num) => num.toString(36).toLowerCase();
/**
* An operation to apply to a shared document.
*/
class Op {
opcode: string;
chars: number;
lines: number;
attribs: string;
/**
* @param {(''|'='|'+'|'-')} [opcode=''] - Initial value of the `opcode` property.
*/
@ -146,11 +152,11 @@ class Op {
toString() {
if (!this.opcode) throw new TypeError('null op');
if (typeof this.attribs !== 'string') throw new TypeError('attribs must be a string');
const l = this.lines ? `|${exports.numToString(this.lines)}` : '';
return this.attribs + l + this.opcode + exports.numToString(this.chars);
const l = this.lines ? `|${numToString(this.lines)}` : '';
return this.attribs + l + this.opcode + numToString(this.chars);
}
}
exports.Op = Op;
export const Op = Op;
/**
* Describes changes to apply to a document. Does not include the attribute pool or the original
@ -170,7 +176,7 @@ exports.Op = Op;
* @param {string} cs - String representation of the Changeset
* @returns {number} oldLen property
*/
exports.oldLen = (cs) => exports.unpack(cs).oldLen;
export const oldLen = (cs) => exports.unpack(cs).oldLen;
/**
* Returns the length of the text after changeset is applied.
@ -178,7 +184,7 @@ exports.oldLen = (cs) => exports.unpack(cs).oldLen;
* @param {string} cs - String representation of the Changeset
* @returns {number} newLen property
*/
exports.newLen = (cs) => exports.unpack(cs).newLen;
export const newLen = (cs) => exports.unpack(cs).newLen;
/**
* Parses a string of serialized changeset operations.
@ -187,7 +193,7 @@ exports.newLen = (cs) => exports.unpack(cs).newLen;
* @yields {Op}
* @returns {Generator<Op>}
*/
exports.deserializeOps = function* (ops) {
export const deserializeOps = function* (ops) {
// TODO: Migrate to String.prototype.matchAll() once there is enough browser support.
const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|(.)/g;
let match;
@ -195,8 +201,8 @@ exports.deserializeOps = function* (ops) {
if (match[5] === '$') return; // Start of the insert operation character bank.
if (match[5] != null) error(`invalid operation: ${ops.slice(regex.lastIndex - 1)}`);
const op = new Op(match[3]);
op.lines = exports.parseNum(match[2] || '0');
op.chars = exports.parseNum(match[4]);
op.lines = parseNum(match[2] || '0');
op.chars = parseNum(match[4]);
op.attribs = match[1];
yield op;
}
@ -210,11 +216,18 @@ exports.deserializeOps = function* (ops) {
* @deprecated Use `deserializeOps` instead.
*/
class OpIter {
private _next: {
value: any;
done: boolean,
};
private _gen: any;
/**
* @param {string} ops - String encoding the change operations to iterate over.
*/
constructor(ops) {
this._gen = exports.deserializeOps(ops);
this._gen = deserializeOps(ops);
this._next = this._gen.next();
}
@ -252,7 +265,7 @@ class OpIter {
* @param {string} opsStr - String encoding of the change operations to perform.
* @returns {OpIter} Operator iterator object.
*/
exports.opIterator = (opsStr) => {
export const opIterator = (opsStr) => {
padutils.warnDeprecated(
'Changeset.opIterator() is deprecated; use Changeset.deserializeOps() instead');
return new OpIter(opsStr);
@ -277,7 +290,7 @@ const clearOp = (op) => {
* @param {('+'|'-'|'='|'')} [optOpcode=''] - The operation's operator.
* @returns {Op}
*/
exports.newOp = (optOpcode) => {
export const newOp = (optOpcode) => {
padutils.warnDeprecated('Changeset.newOp() is deprecated; use the Changeset.Op class instead');
return new Op(optOpcode);
};
@ -370,17 +383,17 @@ const opsFromText = function* (opcode, text, attribs = '', pool = null) {
* @param {string} cs - Changeset to check
* @returns {string} the checked Changeset
*/
exports.checkRep = (cs) => {
const unpacked = exports.unpack(cs);
export const checkRep = (cs) => {
const unpacked = unpack(cs);
const oldLen = unpacked.oldLen;
const newLen = unpacked.newLen;
const ops = unpacked.ops;
let charBank = unpacked.charBank;
const assem = exports.smartOpAssembler();
const assem = smartOpAssembler();
let oldPos = 0;
let calcNewLen = 0;
for (const o of exports.deserializeOps(ops)) {
for (const o of deserializeOps(ops)) {
switch (o.opcode) {
case '=':
oldPos += o.chars;
@ -413,7 +426,7 @@ exports.checkRep = (cs) => {
assert(calcNewLen === newLen, 'Invalid changeset: claimed length does not match actual length');
assert(charBank === '', 'Invalid changeset: excess characters in the charBank');
assem.endDocument();
const normalized = exports.pack(oldLen, calcNewLen, assem.toString(), unpacked.charBank);
const normalized = pack(oldLen, calcNewLen, assem.toString(), unpacked.charBank);
assert(normalized === cs, 'Invalid changeset: not in canonical form');
return cs;
};
@ -421,11 +434,11 @@ exports.checkRep = (cs) => {
/**
* @returns {SmartOpAssembler}
*/
exports.smartOpAssembler = () => {
const minusAssem = exports.mergingOpAssembler();
const plusAssem = exports.mergingOpAssembler();
const keepAssem = exports.mergingOpAssembler();
const assem = exports.stringAssembler();
export const smartOpAssembler = () => {
const minusAssem = mergingOpAssembler();
const plusAssem = mergingOpAssembler();
const keepAssem = mergingOpAssembler();
const assem = stringAssembler();
let lastOpcode = '';
let lengthChange = 0;
@ -515,8 +528,8 @@ exports.smartOpAssembler = () => {
/**
* @returns {MergingOpAssembler}
*/
exports.mergingOpAssembler = () => {
const assem = exports.opAssembler();
export const mergingOpAssembler = () => {
const assem = opAssembler();
const bufOp = new Op();
// If we get, for example, insertions [xxx\n,yyy], those don't merge,
@ -528,7 +541,7 @@ exports.mergingOpAssembler = () => {
/**
* @param {boolean} [isEndDocument]
*/
const flush = (isEndDocument) => {
const flush = (isEndDocument?: boolean) => {
if (!bufOp.opcode) return;
if (isEndDocument && bufOp.opcode === '=' && !bufOp.attribs) {
// final merged keep, leave it implicit
@ -589,7 +602,7 @@ exports.mergingOpAssembler = () => {
/**
* @returns {OpAssembler}
*/
exports.opAssembler = () => {
export const opAssembler = () => {
let serialized = '';
/**
@ -627,7 +640,7 @@ exports.opAssembler = () => {
* @param {string} str - String to iterate over
* @returns {StringIterator}
*/
exports.stringIterator = (str) => {
export const stringIterator = (str) => {
let curIndex = 0;
// newLines is the number of \n between curIndex and str.length
let newLines = str.split('\n').length - 1;
@ -677,7 +690,7 @@ exports.stringIterator = (str) => {
/**
* @returns {StringAssembler}
*/
exports.stringAssembler = () => ({
export const stringAssembler = () => ({
_str: '',
clear() { this._str = ''; },
/**
@ -711,6 +724,11 @@ exports.stringAssembler = () => ({
* with no newlines.
*/
class TextLinesMutator {
private _lines: any;
private _curSplice: number[]|{[key: string]:any};
private _inSplice: boolean;
private _curLine: number;
private _curCol: number;
/**
* @param {(string[]|StringArrayLike)} lines - Lines to mutate (in place).
*/
@ -798,10 +816,12 @@ class TextLinesMutator {
* close or TODO(doc).
*/
_leaveSplice() {
this._lines.splice(...this._curSplice);
this._curSplice.length = 2;
this._curSplice[0] = this._curSplice[1] = 0;
this._inSplice = false;
if(this._curSplice instanceof Array) {
this._lines.splice(...this._curSplice);
this._curSplice.length = 2;
this._curSplice[0] = this._curSplice[1] = 0;
this._inSplice = false;
}
}
/**
@ -837,7 +857,7 @@ class TextLinesMutator {
* @param {number} L -
* @param {boolean} includeInSplice - Indicates that attributes are present.
*/
skipLines(L, includeInSplice) {
skipLines(L, includeInSplice?:boolean) {
if (!L) return;
if (includeInSplice) {
if (!this._inSplice) this._enterSplice();
@ -905,7 +925,7 @@ class TextLinesMutator {
return this._linesSlice(m, m + k).join('');
};
let removed = '';
let removed:string|number = '';
if (this._isCurLineInSplice()) {
if (this._curCol === 0) {
removed = this._curSplice[this._curSplice.length - 1];
@ -958,7 +978,7 @@ class TextLinesMutator {
if (!text) return;
if (!this._inSplice) this._enterSplice();
if (L) {
const newLines = exports.splitTextLines(text);
const newLines = splitTextLines(text);
if (this._isCurLineInSplice()) {
const sline = this._curSplice.length - 1;
/** @type {string} */
@ -1039,11 +1059,11 @@ class TextLinesMutator {
* @returns {string} the integrated changeset
*/
const applyZip = (in1, in2, func) => {
const ops1 = exports.deserializeOps(in1);
const ops2 = exports.deserializeOps(in2);
const ops1 = deserializeOps(in1);
const ops2 = deserializeOps(in2);
let next1 = ops1.next();
let next2 = ops2.next();
const assem = exports.smartOpAssembler();
const assem = smartOpAssembler();
while (!next1.done || !next2.done) {
if (!next1.done && !next1.value.opcode) next1 = ops1.next();
if (!next2.done && !next2.value.opcode) next2 = ops2.next();
@ -1063,13 +1083,13 @@ const applyZip = (in1, in2, func) => {
* @param {string} cs - The encoded changeset.
* @returns {Changeset}
*/
exports.unpack = (cs) => {
export const unpack = (cs) => {
const headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/;
const headerMatch = headerRegex.exec(cs);
if ((!headerMatch) || (!headerMatch[0])) error(`Not a changeset: ${cs}`);
const oldLen = exports.parseNum(headerMatch[1]);
const oldLen = parseNum(headerMatch[1]);
const changeSign = (headerMatch[2] === '>') ? 1 : -1;
const changeMag = exports.parseNum(headerMatch[3]);
const changeMag = parseNum(headerMatch[3]);
const newLen = oldLen + changeSign * changeMag;
const opsStart = headerMatch[0].length;
let opsEnd = cs.indexOf('$');
@ -1091,12 +1111,12 @@ exports.unpack = (cs) => {
* @param {string} bank - Characters for insert operations.
* @returns {string} The encoded changeset.
*/
exports.pack = (oldLen, newLen, opsStr, bank) => {
export const pack = (oldLen, newLen, opsStr, bank) => {
const lenDiff = newLen - oldLen;
const lenDiffStr = (lenDiff >= 0 ? `>${exports.numToString(lenDiff)}`
: `<${exports.numToString(-lenDiff)}`);
const lenDiffStr = (lenDiff >= 0 ? `>${numToString(lenDiff)}`
: `<${numToString(-lenDiff)}`);
const a = [];
a.push('Z:', exports.numToString(oldLen), lenDiffStr, opsStr, '$', bank);
a.push('Z:', numToString(oldLen), lenDiffStr, opsStr, '$', bank);
return a.join('');
};
@ -1107,13 +1127,13 @@ exports.pack = (oldLen, newLen, opsStr, bank) => {
* @param {string} str - String to which a Changeset should be applied
* @returns {string}
*/
exports.applyToText = (cs, str) => {
const unpacked = exports.unpack(cs);
export const applyToText = (cs, str) => {
const unpacked = unpack(cs);
assert(str.length === unpacked.oldLen, `mismatched apply: ${str.length} / ${unpacked.oldLen}`);
const bankIter = exports.stringIterator(unpacked.charBank);
const strIter = exports.stringIterator(str);
const assem = exports.stringAssembler();
for (const op of exports.deserializeOps(unpacked.ops)) {
const bankIter = stringIterator(unpacked.charBank);
const strIter = stringIterator(str);
const assem = stringAssembler();
for (const op of deserializeOps(unpacked.ops)) {
switch (op.opcode) {
case '+':
// op is + and op.lines 0: no newlines must be in op.chars
@ -1151,11 +1171,11 @@ exports.applyToText = (cs, str) => {
* @param {string} cs - the changeset to apply
* @param {string[]} lines - The lines to which the changeset needs to be applied
*/
exports.mutateTextLines = (cs, lines) => {
const unpacked = exports.unpack(cs);
const bankIter = exports.stringIterator(unpacked.charBank);
export const mutateTextLines = (cs, lines) => {
const unpacked = unpack(cs);
const bankIter = stringIterator(unpacked.charBank);
const mut = new TextLinesMutator(lines);
for (const op of exports.deserializeOps(unpacked.ops)) {
for (const op of deserializeOps(unpacked.ops)) {
switch (op.opcode) {
case '+':
mut.insert(bankIter.take(op.chars), op.lines);
@ -1180,7 +1200,7 @@ exports.mutateTextLines = (cs, lines) => {
* @param {AttributePool} pool - attribute pool
* @returns {string}
*/
exports.composeAttributes = (att1, att2, resultIsMutation, pool) => {
export const composeAttributes = (att1, att2, resultIsMutation, pool) => {
// att1 and att2 are strings like "*3*f*1c", asMutation is a boolean.
// Sometimes attribute (key,value) pairs are treated as attribute presence
// information, while other times they are treated as operations that
@ -1258,7 +1278,7 @@ const slicerZipperFunc = (attOp, csOp, pool) => {
// normally be the empty string. However, padDiff.js adds attributes to remove ops and needs
// them preserved so they are copied here.
? csOp.attribs
: exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode === '=', pool);
: composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode === '=', pool);
partiallyConsumedOp.chars -= fullyConsumedOp.chars;
partiallyConsumedOp.lines -= fullyConsumedOp.lines;
if (!partiallyConsumedOp.chars) partiallyConsumedOp.opcode = '';
@ -1275,8 +1295,8 @@ const slicerZipperFunc = (attOp, csOp, pool) => {
* @param {AttributePool} pool - the attibutes pool
* @returns {string}
*/
exports.applyToAttribution = (cs, astr, pool) => {
const unpacked = exports.unpack(cs);
export const applyToAttribution = (cs, astr, pool) => {
const unpacked = unpack(cs);
return applyZip(astr, unpacked.ops, (op1, op2) => slicerZipperFunc(op1, op2, pool));
};
@ -1749,7 +1769,7 @@ exports.mapAttribNumbers = (cs, func) => {
* attributes
* @returns {AText}
*/
exports.makeAText = (text, attribs) => ({
export const makeAText = (text, attribs) => ({
text,
attribs: (attribs || exports.makeAttribution(text)),
});
@ -1762,7 +1782,7 @@ exports.makeAText = (text, attribs) => ({
* @param {AttributePool} pool - Attribute Pool to add to
* @returns {AText}
*/
exports.applyToAText = (cs, atext, pool) => ({
export const applyToAText = (cs, atext, pool) => ({
text: exports.applyToText(cs, atext.text),
attribs: exports.applyToAttribution(cs, atext.attribs, pool),
});
@ -1787,7 +1807,7 @@ exports.cloneAText = (atext) => {
* @param {AText} atext1 -
* @param {AText} atext2 -
*/
exports.copyAText = (atext1, atext2) => {
export const copyAText = (atext1, atext2) => {
atext2.text = atext1.text;
atext2.attribs = atext1.attribs;
};
@ -1799,10 +1819,10 @@ exports.copyAText = (atext1, atext2) => {
* @yields {Op}
* @returns {Generator<Op>}
*/
exports.opsFromAText = function* (atext) {
export const opsFromAText = function* (atext) {
// intentionally skips last newline char of atext
let lastOp = null;
for (const op of exports.deserializeOps(atext.attribs)) {
for (const op of deserializeOps(atext.attribs)) {
if (lastOp != null) yield lastOp;
lastOp = op;
}

View file

@ -1,6 +1,6 @@
'use strict';
const pluginDefs = require('./plugin_defs');
import {hooks} from './plugin_defs';
// Maps the name of a server-side hook to a string explaining the deprecation
// (e.g., 'use the foo hook instead').
@ -10,12 +10,15 @@ const pluginDefs = require('./plugin_defs');
// const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
// hooks.deprecationNotices.fooBar = 'use the newSpiffy hook instead';
//
exports.deprecationNotices = {};
export const deprecationNotices:{
authFailure?: string;
clientReady?: string
} = {};
const deprecationWarned = {};
const checkDeprecation = (hook) => {
const notice = exports.deprecationNotices[hook.hook_name];
const notice = deprecationNotices[hook.hook_name];
if (notice == null) return;
if (deprecationWarned[hook.hook_fn_name]) return;
console.warn(`${hook.hook_name} hook used by the ${hook.part.plugin} plugin ` +
@ -189,10 +192,10 @@ const callHookFnSync = (hook, context) => {
// 1. Collect all values returned by the hook functions into an array.
// 2. Convert each `undefined` entry into `[]`.
// 3. Flatten one level.
exports.callAll = (hookName, context) => {
export const callAll = (hookName, context?) => {
if (context == null) context = {};
const hooks = pluginDefs.hooks[hookName] || [];
return flatten1(hooks.map((hook) => normalizeValue(callHookFnSync(hook, context))));
const hooksResult = hooks[hookName] || [];
return flatten1(hooksResult.map((hook) => normalizeValue(callHookFnSync(hook, context))));
};
// Calls the hook function asynchronously and returns a Promise that either resolves to the hook
@ -342,23 +345,23 @@ const callHookFnAsync = async (hook, context) => {
// 2. Convert each `undefined` entry into `[]`.
// 3. Flatten one level.
// If cb is non-null, this function resolves to the value returned by cb.
exports.aCallAll = async (hookName, context, cb = null) => {
if (cb != null) return await attachCallback(exports.aCallAll(hookName, context), cb);
export const aCallAll = async (hookName, context?, cb = null) => {
if (cb != null) return await attachCallback(aCallAll(hookName, context), cb);
if (context == null) context = {};
const hooks = pluginDefs.hooks[hookName] || [];
const hooksResult = hooks[hookName] || [];
const results = await Promise.all(
hooks.map(async (hook) => normalizeValue(await callHookFnAsync(hook, context))));
hooksResult.map(async (hook) => normalizeValue(await callHookFnAsync(hook, context))));
return flatten1(results);
};
// Like `aCallAll()` except the hook functions are called one at a time instead of concurrently.
// Only use this function if the hook functions must be called one at a time, otherwise use
// `aCallAll()`.
exports.callAllSerial = async (hookName, context) => {
export const callAllSerial = async (hookName, context) => {
if (context == null) context = {};
const hooks = pluginDefs.hooks[hookName] || [];
const hooksResult = hooks[hookName] || [];
const results = [];
for (const hook of hooks) {
for (const hook of hooksResult) {
results.push(normalizeValue(await callHookFnAsync(hook, context)));
}
return flatten1(results);
@ -367,11 +370,11 @@ exports.callAllSerial = async (hookName, context) => {
// DEPRECATED: Use `aCallFirst()` instead.
//
// Like `aCallFirst()`, but synchronous. Hook functions must provide their values synchronously.
exports.callFirst = (hookName, context) => {
export const callFirst = (hookName, context) => {
if (context == null) context = {};
const predicate = (val) => val.length;
const hooks = pluginDefs.hooks[hookName] || [];
for (const hook of hooks) {
const hooksResult = hooks[hookName] || [];
for (const hook of hooksResult) {
const val = normalizeValue(callHookFnSync(hook, context));
if (predicate(val)) return val;
}
@ -399,21 +402,21 @@ exports.callFirst = (hookName, context) => {
// If cb is nullish, resolves to an array that is either the normalized value that satisfied the
// predicate or empty if the predicate was never satisfied. If cb is non-nullish, resolves to the
// value returned from cb().
exports.aCallFirst = async (hookName, context, cb = null, predicate = null) => {
export const aCallFirst = async (hookName, context, cb = null, predicate = null) => {
if (cb != null) {
return await attachCallback(exports.aCallFirst(hookName, context, null, predicate), cb);
return await attachCallback(aCallFirst(hookName, context, null, predicate), cb);
}
if (context == null) context = {};
if (predicate == null) predicate = (val) => val.length;
const hooks = pluginDefs.hooks[hookName] || [];
for (const hook of hooks) {
const hooksResult = hooks[hookName] || [];
for (const hook of hooksResult) {
const val = normalizeValue(await callHookFnAsync(hook, context));
if (predicate(val)) return val;
}
return [];
};
exports.exportedForTestingOnly = {
export const exportedForTestingOnly = {
callHookFnAsync,
callHookFnSync,
deprecationWarned,

View file

@ -1,18 +1,23 @@
'use strict';
const log4js = require('log4js');
const plugins = require('./plugins');
const hooks = require('./hooks');
const request = require('request');
const runCmd = require('../../../node/utils/run_cmd');
const settings = require('../../../node/utils/Settings');
import log4js from 'log4js';
import {prefix, update} from "./plugins";
import {aCallAll} from "./hooks";
import request from "request";
import {exportCMD} from "../../../node/utils/run_cmd";
import {reloadSettings} from "../../../node/utils/Settings";
import {InstallerModel} from "../../module/InstallerModel";
const logger = log4js.getLogger('plugins');
const onAllTasksFinished = async () => {
settings.reloadSettings();
await hooks.aCallAll('loadSettings', {settings});
await hooks.aCallAll('restartServer');
const settings = reloadSettings();
await aCallAll('loadSettings', {settings});
await aCallAll('restartServer');
};
let tasks = 0;
@ -27,54 +32,54 @@ const wrapTaskCb = (cb) => {
};
};
exports.uninstall = async (pluginName, cb = null) => {
export const uninstall = async (pluginName, cb = null) => {
cb = wrapTaskCb(cb);
logger.info(`Uninstalling plugin ${pluginName}...`);
try {
// The --no-save flag prevents npm from creating package.json or package-lock.json.
// The --legacy-peer-deps flag is required to work around a bug in npm v7:
// https://github.com/npm/cli/issues/2199
await runCmd(['npm', 'uninstall', '--no-save', '--legacy-peer-deps', pluginName]);
await exportCMD(['npm', 'uninstall', '--no-save', '--legacy-peer-deps', pluginName]);
} catch (err) {
logger.error(`Failed to uninstall plugin ${pluginName}`);
cb(err || new Error(err));
throw err;
}
logger.info(`Successfully uninstalled plugin ${pluginName}`);
await hooks.aCallAll('pluginUninstall', {pluginName});
await plugins.update();
await aCallAll('pluginUninstall', {pluginName});
await update();
cb(null);
};
exports.install = async (pluginName, cb = null) => {
export const install = async (pluginName, cb = null) => {
cb = wrapTaskCb(cb);
logger.info(`Installing plugin ${pluginName}...`);
try {
// The --no-save flag prevents npm from creating package.json or package-lock.json.
// The --legacy-peer-deps flag is required to work around a bug in npm v7:
// https://github.com/npm/cli/issues/2199
await runCmd(['npm', 'install', '--no-save', '--legacy-peer-deps', pluginName]);
await exportCMD(['npm', 'install', '--no-save', '--legacy-peer-deps', pluginName]);
} catch (err) {
logger.error(`Failed to install plugin ${pluginName}`);
cb(err || new Error(err));
throw err;
}
logger.info(`Successfully installed plugin ${pluginName}`);
await hooks.aCallAll('pluginInstall', {pluginName});
await plugins.update();
await aCallAll('pluginInstall', {pluginName});
await update();
cb(null);
};
exports.availablePlugins = null;
export let availablePlugins = null;
let cacheTimestamp = 0;
exports.getAvailablePlugins = (maxCacheAge) => {
export const getAvailablePlugins = (maxCacheAge) => {
const nowTimestamp = Math.round(Date.now() / 1000);
return new Promise((resolve, reject) => {
// check cache age before making any request
if (exports.availablePlugins && maxCacheAge && (nowTimestamp - cacheTimestamp) <= maxCacheAge) {
return resolve(exports.availablePlugins);
if (availablePlugins && maxCacheAge && (nowTimestamp - cacheTimestamp) <= maxCacheAge) {
return resolve(availablePlugins);
}
request('https://static.etherpad.org/plugins.json', (er, response, plugins) => {
@ -87,7 +92,7 @@ exports.getAvailablePlugins = (maxCacheAge) => {
plugins = [];
}
exports.availablePlugins = plugins;
availablePlugins = plugins;
cacheTimestamp = nowTimestamp;
resolve(plugins);
});
@ -95,18 +100,19 @@ exports.getAvailablePlugins = (maxCacheAge) => {
};
exports.search = (searchTerm, maxCacheAge) => exports.getAvailablePlugins(maxCacheAge).then(
(results) => {
export const search = (searchTerm, maxCacheAge) => getAvailablePlugins(maxCacheAge).then(
(results: InstallerModel[]) => {
const res = {};
if (searchTerm) {
searchTerm = searchTerm.toLowerCase();
}
for (const pluginName in results) {
// for every available plugin
// TODO: Also search in keywords here!
if (pluginName.indexOf(plugins.prefix) !== 0) continue;
if (pluginName.indexOf(prefix) !== 0) continue;
if (searchTerm && !~results[pluginName].name.toLowerCase().indexOf(searchTerm) &&
(typeof results[pluginName].description !== 'undefined' &&

View file

@ -8,13 +8,13 @@
// * hook_fn: Plugin-supplied hook function.
// * hook_fn_name: Name of the hook function, with the form <filename>:<functionName>.
// * part: The ep.json part object that declared the hook. See exports.plugins.
exports.hooks = {};
export const hooks = {};
// Whether the plugins have been loaded.
exports.loaded = false;
export let loaded = false;
// Topologically sorted list of parts from exports.plugins.
exports.parts = [];
export const parts = [];
// Maps the name of a plugin to the plugin's definition provided in ep.json. The ep.json object is
// augmented with additional metadata:
@ -25,4 +25,20 @@ exports.parts = [];
// - version
// - path
// - realPath
exports.plugins = {};
export const plugins = {};
export const setPlugins = (newPlugins) => {
Object.assign(plugins, newPlugins);
}
export const setParts = (newParts) => {
Object.assign(parts, newParts);
}
export const setHooks = (newHooks) => {
Object.assign(hooks, newHooks);
}
export const setLoaded = (newLoaded) => {
loaded = newLoaded;
}

View file

@ -1,20 +1,28 @@
'use strict';
const fs = require('fs').promises;
const hooks = require('./hooks');
const log4js = require('log4js');
const path = require('path');
const runCmd = require('../../../node/utils/run_cmd');
const tsort = require('./tsort');
const pluginUtils = require('./shared');
const defs = require('./plugin_defs');
import {promises as fs} from "fs";
import {aCallAll} from "./hooks";
import log4js from "log4js";
import path from "path";
import {exportCMD} from "../../../node/utils/run_cmd";
import {tsort} from "./tsort";
import {extractHooks} from "./shared";
import {loaded, parts, plugins, setHooks, setLoaded, setParts, setPlugins} from "./plugin_defs";
import {PluginInfo} from "../../module/PluginInfo";
const logger = log4js.getLogger('plugins');
// Log the version of npm at startup.
(async () => {
try {
const version = await runCmd(['npm', '--version'], {stdio: [null, 'string']});
const version = await exportCMD(['npm', '--version'], {stdio: [null, 'string']});
logger.info(`npm --version: ${version}`);
} catch (err) {
logger.error(`Failed to get npm version: ${err.stack || err}`);
@ -22,16 +30,19 @@ const logger = log4js.getLogger('plugins');
}
})();
exports.prefix = 'ep_';
type PartType = {
[keys: string]:any
}
export const prefix = 'ep_';
exports.formatPlugins = () => Object.keys(defs.plugins).join(', ');
export const formatPlugins = () => Object.keys(plugins).join(', ');
exports.formatParts = () => defs.parts.map((part) => part.full_name).join('\n');
export const formatParts = () => parts.map((part) => part.full_name).join('\n');
exports.formatHooks = (hookSetName, html) => {
export const formatHooks = (hookSetName, html) => {
let hooks = new Map();
for (const [pluginName, def] of Object.entries(defs.plugins)) {
for (const part of def.parts) {
for (const [pluginName, def] of Object.entries(plugins)) {
for (const part of parts) {
for (const [hookName, hookFnName] of Object.entries(part[hookSetName] || {})) {
let hookEntry = hooks.get(hookName);
if (!hookEntry) {
@ -53,7 +64,7 @@ exports.formatHooks = (hookSetName, html) => {
hooks = new Map([...hooks].sort(sortStringKeys));
for (const [hookName, hookEntry] of hooks) {
lines.push(html ? ` <dt>${hookName}:</dt><dd><dl>` : ` ${hookName}:`);
const sortedHookEntry = new Map([...hookEntry].sort(sortStringKeys));
const sortedHookEntry = new Map<any,any>([...hookEntry].sort(sortStringKeys));
hooks.set(hookName, sortedHookEntry);
for (const [pluginName, pluginEntry] of sortedHookEntry) {
lines.push(html ? ` <dt>${pluginName}:</dt><dd><dl>` : ` ${pluginName}:`);
@ -72,20 +83,20 @@ exports.formatHooks = (hookSetName, html) => {
return lines.join('\n');
};
exports.pathNormalization = (part, hookFnName, hookName) => {
export const pathNormalization = (part, hookFnName, hookName) => {
const tmp = hookFnName.split(':'); // hookFnName might be something like 'C:\\foo.js:myFunc'.
// If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'.
const functionName = (tmp.length > 1 ? tmp.pop() : null) || hookName;
const moduleName = tmp.join(':') || part.plugin;
const packageDir = path.dirname(defs.plugins[part.plugin].package.path);
const packageDir = path.dirname(plugins[part.plugin].package.path);
const fileName = path.join(packageDir, moduleName);
return `${fileName}:${functionName}`;
};
exports.update = async () => {
export const update = async () => {
const packages = await exports.getPackages();
const parts = {}; // Key is full name. sortParts converts this into a topologically sorted array.
const plugins = {};
let parts:{[keys: string]:any} = {}; // Key is full name. sortParts converts this into a topologically sorted array.
let plugins = {};
// Load plugin metadata ep.json
await Promise.all(Object.keys(packages).map(async (pluginName) => {
@ -94,13 +105,13 @@ exports.update = async () => {
}));
logger.info(`Loaded ${Object.keys(packages).length} plugins`);
defs.plugins = plugins;
defs.parts = sortParts(parts);
defs.hooks = pluginUtils.extractHooks(defs.parts, 'hooks', exports.pathNormalization);
defs.loaded = true;
await Promise.all(Object.keys(defs.plugins).map(async (p) => {
setPlugins(plugins);
setParts(sortParts(parts))
setHooks(extractHooks(parts, 'hooks', exports.pathNormalization));
setLoaded(true)
await Promise.all(Object.keys(plugins).map(async (p) => {
const logger = log4js.getLogger(`plugin:${p}`);
await hooks.aCallAll(`init_${p}`, {logger});
await aCallAll(`init_${p}`, {logger});
}));
};
@ -112,13 +123,15 @@ exports.getPackages = async () => {
// unset or set to `development`) because otherwise `npm ls` will not mention any packages
// that are not included in `package.json` (which is expected to not exist).
const cmd = ['npm', 'ls', '--long', '--json', '--depth=0', '--no-production'];
const {dependencies = {}} = JSON.parse(await runCmd(cmd, {stdio: [null, 'string']}));
const {dependencies = {}} = JSON.parse(await exportCMD(cmd, {stdio: [null, 'string']}) as unknown as string);
await Promise.all(Object.entries(dependencies).map(async ([pkg, info]) => {
if (!pkg.startsWith(exports.prefix)) {
delete dependencies[pkg];
return;
}
info.realPath = await fs.realpath(info.path);
const mappedInfo = info as PluginInfo
mappedInfo.realPath = await fs.realpath(mappedInfo.path);
}));
return dependencies;
};
@ -126,7 +139,7 @@ exports.getPackages = async () => {
const loadPlugin = async (packages, pluginName, plugins, parts) => {
const pluginPath = path.resolve(packages[pluginName].path, 'ep.json');
try {
const data = await fs.readFile(pluginPath);
const data = await fs.readFile(pluginPath, "utf8");
try {
const plugin = JSON.parse(data);
plugin.package = packages[pluginName];
@ -145,7 +158,7 @@ const loadPlugin = async (packages, pluginName, plugins, parts) => {
};
const partsToParentChildList = (parts) => {
const res = [];
const res:(string|number)[][] = [];
for (const name of Object.keys(parts)) {
for (const childName of parts[name].post || []) {
res.push([name, childName]);
@ -161,6 +174,7 @@ const partsToParentChildList = (parts) => {
};
// Used only in Node, so no need for _
const sortParts = (parts) => tsort(partsToParentChildList(parts))
.filter((name) => parts[name] !== undefined)
.map((name) => parts[name]);
//FIXME Is this better
const sortParts = (parts:PartType) => tsort(partsToParentChildList(parts))
.filter((name) => name !== undefined)
.map((name) => name);

View file

@ -1,6 +1,6 @@
'use strict';
const defs = require('./plugin_defs');
import {parts} from './plugin_defs';
const disabledHookReasons = {
hooks: {
@ -9,7 +9,7 @@ const disabledHookReasons = {
},
};
const loadFn = (path, hookName) => {
export const loadFn = (path, hookName) => {
let functionName;
const parts = path.split(':');
@ -33,7 +33,7 @@ const loadFn = (path, hookName) => {
return fn;
};
const extractHooks = (parts, hookSetName, normalizer) => {
export const extractHooks = (parts, hookSetName, normalizer) => {
const hooks = {};
for (const part of parts) {
for (const [hookName, regHookFnName] of Object.entries(part[hookSetName] || {})) {
@ -72,9 +72,7 @@ const extractHooks = (parts, hookSetName, normalizer) => {
}
}
return hooks;
};
exports.extractHooks = extractHooks;
}
/*
* Returns an array containing the names of the installed client-side plugins
@ -88,8 +86,8 @@ exports.extractHooks = extractHooks;
* No plugins: []
* Some plugins: [ 'ep_adminpads', 'ep_add_buttons', 'ep_activepads' ]
*/
exports.clientPluginNames = () => {
const clientPluginNames = defs.parts
export const clientPluginNames = () => {
const clientPluginNames = parts
.filter((part) => Object.prototype.hasOwnProperty.call(part, 'client_hooks'))
.map((part) => `plugin-${part.plugin}`);
return [...new Set(clientPluginNames)];

View file

@ -4,14 +4,14 @@
* general topological sort
* from https://gist.github.com/1232505
* @author SHIN Suzuki (shinout310@gmail.com)
* @param Array<Array> edges : list of edges. each edge forms Array<ID,ID> e.g. [12 , 3]
* @param edges Array<Array> edges : list of edges. each edge forms Array<ID,ID> e.g. [12 , 3]
*
* @returns Array : topological sorted list of IDs
**/
const tsort = (edges) => {
export const tsort = (edges: (string|number)[][]) => {
const nodes = {}; // hash: stringified id of the node => { id: id, afters: lisf of ids }
const sorted = []; // sorted list of IDs ( returned value )
const sorted: (string|number)[][]= []; // sorted list of IDs ( returned value )
const visited = {}; // hash: id of already visited node => true
const Node = function (id) {
@ -62,7 +62,7 @@ const tsort = (edges) => {
**/
const tsortTest = () => {
// example 1: success
let edges = [
let edges:(string|number)[][] = [
[1, 2],
[1, 3],
[2, 4],

View file

@ -0,0 +1,5 @@
export type InstallerModel = {
name: string,
description: string,
}

View file

@ -0,0 +1,4 @@
export type PluginInfo = {
realPath: string,
path: string,
}

View file

@ -1,5 +1,6 @@
{
"compilerOptions": {
"allowJs": true,
"typeRoots": ["node_modules/@types"],
"types": ["node", "jquery"],
"module": "commonjs",
@ -7,7 +8,7 @@
"target": "es6",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist"
"outDir": "../dist"
},
"lib": ["es2015"]
}