mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-31 19:02:59 +01:00
Rewrote server in typescript.
This commit is contained in:
parent
331cf3d79f
commit
3c2129b1cc
57 changed files with 668 additions and 490 deletions
3
src/.gitignore
vendored
3
src/.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
dist
|
||||
dist/
|
||||
node_modules
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
import Changeset from '../../static/js/Changeset';
|
||||
import ChatMessage from '../../static/js/ChatMessage';
|
||||
import CustomError from '../utils/customError';
|
||||
import {CustomError} from '../utils/customError';
|
||||
import {doesPadExist, getPad, isValidPadId, listAllPads} from './PadManager';
|
||||
import {
|
||||
handleCustomMessage,
|
||||
|
@ -40,11 +40,11 @@ import {
|
|||
} 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';
|
||||
import {getTXTFromAtext} from '../utils/ExportTxt';
|
||||
import {setPadHTML} from '../utils/ImportHtml';
|
||||
const cleanText = require('./Pad').cleanText;
|
||||
import PadDiff from '../utils/padDiff';
|
||||
import {PadDiff} from '../utils/padDiff';
|
||||
import {getPadHTMLDocument} from "../utils/ExportHtml";
|
||||
|
||||
/* ********************
|
||||
* GROUP FUNCTIONS ****
|
||||
|
@ -192,7 +192,7 @@ export const getText = async (padID, rev) => {
|
|||
}
|
||||
|
||||
// the client wants the latest text, lets return it to him
|
||||
const text = exportTxt.getTXTFromAtext(pad, pad.atext);
|
||||
const text = getTXTFromAtext(pad, pad.atext);
|
||||
return {text};
|
||||
};
|
||||
|
||||
|
@ -263,7 +263,7 @@ export const getHTML = async (padID, rev) => {
|
|||
}
|
||||
|
||||
// get the html of this revision
|
||||
let html = await exportHtml.getPadHTML(pad, rev);
|
||||
let html = await getPadHTMLDocument(pad, rev);
|
||||
|
||||
// wrap the HTML
|
||||
html = `<!DOCTYPE HTML><html><body>${html}</body></html>`;
|
||||
|
@ -289,7 +289,7 @@ export const setHTML = async (padID, html, authorId = '') => {
|
|||
|
||||
// add a new changeset with the new html to the pad
|
||||
try {
|
||||
await importHtml.setPadHTML(pad, cleanText(html), authorId);
|
||||
await setPadHTML(pad, cleanText(html), authorId);
|
||||
} catch (e) {
|
||||
throw new CustomError('HTML is malformed', 'apierror');
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
*/
|
||||
|
||||
import {db} from './DB';
|
||||
import CustomError from '../utils/customError';
|
||||
import {CustomError} from '../utils/customError';
|
||||
import hooks from '../../static/js/pluginfw/hooks.js';
|
||||
|
||||
const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
|
||||
|
@ -283,7 +283,7 @@ export const addPad = async (authorID: string, padID: string) => {
|
|||
* @param {String} authorID The id of the author
|
||||
* @param {String} padID The id of the pad the author contributes to
|
||||
*/
|
||||
export const removePad = async (authorID: string, padID: string) => {
|
||||
export const removePad = async (authorID: string, padID?: string) => {
|
||||
const author = await db.get(`globalAuthor:${authorID}`);
|
||||
|
||||
if (author == null) return;
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import CustomError from '../utils/customError';
|
||||
import {CustomError} from '../utils/customError';
|
||||
const randomString = require('../../static/js/pad_utils').randomString;
|
||||
import {db} from './DB';
|
||||
import {doesPadExist, getPad} from './PadManager';
|
||||
|
|
|
@ -7,22 +7,21 @@ import AttributeMap from '../../static/js/AttributeMap';
|
|||
import Changeset from '../../static/js/Changeset';
|
||||
import ChatMessage from '../../static/js/ChatMessage';
|
||||
import {AttributePool} from '../../static/js/AttributePool';
|
||||
import Stream from '../utils/Stream';
|
||||
import {Stream} from '../utils/Stream';
|
||||
import assert, {strict} from 'assert'
|
||||
import {db} from './DB';
|
||||
import {defaultPadText} from '../utils/Settings';
|
||||
import {addPad, getAuthorColorId, getAuthorName, getColorPalette, removePad} from './AuthorManager';
|
||||
import {Revision} from "../models/Revision";
|
||||
const padManager = require('./PadManager');
|
||||
const padMessageHandler = require('../handler/PadMessageHandler');
|
||||
const groupManager = require('./GroupManager');
|
||||
const CustomError = require('../utils/customError');
|
||||
const readOnlyManager = require('./ReadOnlyManager');
|
||||
const randomString = require('../utils/randomstring');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
|
||||
const promises = require('../utils/promises');
|
||||
|
||||
import {doesPadExist, getPad} from './PadManager';
|
||||
import {kickSessionsFromPad} from '../handler/PadMessageHandler';
|
||||
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 {timesLimit} from '../utils/promises';
|
||||
import {padutils} from '../../static/js/pad_utils';
|
||||
/**
|
||||
* Copied from the Etherpad source code. It converts Windows line breaks to Unix
|
||||
* line breaks and convert Tabs to spaces
|
||||
|
@ -114,11 +113,11 @@ export class Pad {
|
|||
pad: this,
|
||||
authorId,
|
||||
get author() {
|
||||
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||
padutils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||
return this.authorId;
|
||||
},
|
||||
set author(authorId) {
|
||||
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||
padutils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||
this.authorId = authorId;
|
||||
},
|
||||
...this.head === 0 ? {} : {
|
||||
|
@ -403,16 +402,16 @@ export class Pad {
|
|||
for (const p of new Stream(promises).batch(100).buffer(99)) await p;
|
||||
|
||||
// Initialize the new pad (will update the listAllPads cache)
|
||||
const dstPad = await padManager.getPad(destinationID, null);
|
||||
const dstPad = await getPad(destinationID, null);
|
||||
|
||||
// let the plugins know the pad was copied
|
||||
await hooks.aCallAll('padCopy', {
|
||||
get originalPad() {
|
||||
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||
padutils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||
return this.srcPad;
|
||||
},
|
||||
get destinationID() {
|
||||
warnDeprecated(
|
||||
padutils.warnDeprecated(
|
||||
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
||||
return this.dstPad.id;
|
||||
},
|
||||
|
@ -428,7 +427,7 @@ export class Pad {
|
|||
|
||||
if (destinationID.indexOf('$') >= 0) {
|
||||
destGroupID = destinationID.split('$')[0];
|
||||
const groupExists = await groupManager.doesGroupExist(destGroupID);
|
||||
const groupExists = await doesGroupExist(destGroupID);
|
||||
|
||||
// group does not exist
|
||||
if (!groupExists) {
|
||||
|
@ -440,7 +439,7 @@ export class Pad {
|
|||
|
||||
async removePadIfForceIsTrueAndAlreadyExist(destinationID, force) {
|
||||
// if the pad exists, we should abort, unless forced.
|
||||
const exists = await padManager.doesPadExist(destinationID);
|
||||
const exists = await doesPadExist(destinationID);
|
||||
|
||||
// allow force to be a string
|
||||
if (typeof force === 'string') {
|
||||
|
@ -456,7 +455,7 @@ export class Pad {
|
|||
}
|
||||
|
||||
// exists and forcing
|
||||
const pad = await padManager.getPad(destinationID);
|
||||
const pad = await getPad(destinationID);
|
||||
await pad.remove();
|
||||
}
|
||||
}
|
||||
|
@ -485,7 +484,7 @@ export class Pad {
|
|||
}
|
||||
|
||||
// initialize the pad with a new line to avoid getting the defaultText
|
||||
const dstPad = await padManager.getPad(destinationID, '\n', authorId);
|
||||
const dstPad = await getPad(destinationID, '\n', authorId);
|
||||
dstPad.pool = this.pool.clone();
|
||||
|
||||
const oldAText = this.atext;
|
||||
|
@ -509,11 +508,11 @@ export class Pad {
|
|||
|
||||
await hooks.aCallAll('padCopy', {
|
||||
get originalPad() {
|
||||
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||
padutils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||
return this.srcPad;
|
||||
},
|
||||
get destinationID() {
|
||||
warnDeprecated(
|
||||
padutils.warnDeprecated(
|
||||
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
||||
return this.dstPad.id;
|
||||
},
|
||||
|
@ -529,7 +528,7 @@ export class Pad {
|
|||
const p = [];
|
||||
|
||||
// kick everyone from this pad
|
||||
padMessageHandler.kickSessionsFromPad(padID);
|
||||
kickSessionsFromPad(padID);
|
||||
|
||||
// delete all relations - the original code used async.parallel but
|
||||
// none of the operations except getting the group depended on callbacks
|
||||
|
@ -550,18 +549,18 @@ export class Pad {
|
|||
}
|
||||
|
||||
// remove the readonly entries
|
||||
p.push(readOnlyManager.getReadOnlyId(padID).then(async (readonlyID) => {
|
||||
p.push(getReadOnlyId(padID).then(async (readonlyID) => {
|
||||
await db.remove(`readonly2pad:${readonlyID}`);
|
||||
}));
|
||||
p.push(db.remove(`pad2readonly:${padID}`));
|
||||
|
||||
// delete all chat messages
|
||||
p.push(promises.timesLimit(this.chatHead + 1, 500, async (i) => {
|
||||
p.push(timesLimit(this.chatHead + 1, 500, async (i) => {
|
||||
await this.db.remove(`pad:${this.id}:chat:${i}`, null);
|
||||
}));
|
||||
|
||||
// delete all revisions
|
||||
p.push(promises.timesLimit(this.head + 1, 500, async (i) => {
|
||||
p.push(timesLimit(this.head + 1, 500, async (i) => {
|
||||
await this.db.remove(`pad:${this.id}:revs:${i}`, null);
|
||||
}));
|
||||
|
||||
|
@ -571,10 +570,10 @@ export class Pad {
|
|||
});
|
||||
|
||||
// delete the pad entry and delete pad from padManager
|
||||
p.push(padManager.removePad(padID));
|
||||
p.push(removePad(padID));
|
||||
p.push(hooks.aCallAll('padRemove', {
|
||||
get padID() {
|
||||
warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
|
||||
padutils.warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
|
||||
return this.pad.id;
|
||||
},
|
||||
pad: this,
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import CustomError from '../utils/customError';
|
||||
import {CustomError} from '../utils/customError';
|
||||
import {Pad} from './Pad';
|
||||
import {db} from './DB';
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
|
||||
import {db} from './DB';
|
||||
import randomString from '../utils/randomstring';
|
||||
import {randomString} from '../utils/randomstring';
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -31,7 +31,7 @@ import {findAuthorID} from "./SessionManager";
|
|||
|
||||
import {editOnly, loadTest, requireAuthentication, requireSession} from "../utils/Settings";
|
||||
|
||||
import webaccess from "../hooks/express/webaccess";
|
||||
import {normalizeAuthzLevel} from "../hooks/express/webaccess";
|
||||
|
||||
import log4js from "log4js";
|
||||
|
||||
|
@ -92,7 +92,7 @@ export const checkAccess = async (padID, sessionCookie, token, userSettings) =>
|
|||
// Note: userSettings.padAuthorizations should still be populated even if
|
||||
// settings.requireAuthorization is false.
|
||||
const padAuthzs = userSettings.padAuthorizations || {};
|
||||
const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]);
|
||||
const level = normalizeAuthzLevel(padAuthzs[padID]);
|
||||
if (!level) {
|
||||
authLogger.debug('access denied: unauthorized');
|
||||
return DENY;
|
||||
|
|
|
@ -19,12 +19,12 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import absolutePaths from '../utils/AbsolutePaths';
|
||||
import {makeAbsolute} from '../utils/AbsolutePaths';
|
||||
import fs from 'fs';
|
||||
import * as api from '../db/API';
|
||||
import log4js from 'log4js';
|
||||
import {sanitizePadId} from '../db/PadManager';
|
||||
import randomString from '../utils/randomstring';
|
||||
import {randomString} from '../utils/randomstring';
|
||||
const argv = require('../utils/Cli').argv;
|
||||
const createHTTPError = require('http-errors');
|
||||
|
||||
|
@ -32,7 +32,7 @@ const apiHandlerLogger = log4js.getLogger('APIHandler');
|
|||
|
||||
// ensure we have an apikey
|
||||
let apikey = null;
|
||||
const apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || './APIKEY.txt');
|
||||
const apikeyFilename = makeAbsolute(argv.apikey || './APIKEY.txt');
|
||||
|
||||
try {
|
||||
apikey = fs.readFileSync(apikeyFilename, 'utf8');
|
||||
|
|
|
@ -29,8 +29,8 @@ import {promises as fs} from "fs";
|
|||
import {abiword, allowUnknownFileEnds, importMaxFileSize, soffice} from '../utils/Settings';
|
||||
import {Formidable} from 'formidable';
|
||||
import os from 'os';
|
||||
import importHtml from '../utils/ImportHtml';
|
||||
import importEtherpad from '../utils/ImportEtherpad';
|
||||
import {setPadHTML} from '../utils/ImportHtml';
|
||||
import {setPadRaw} from '../utils/ImportEtherpad';
|
||||
import log4js from 'log4js';
|
||||
import hooks from '../../static/js/pluginfw/hooks.js';
|
||||
|
||||
|
@ -150,7 +150,7 @@ const doImport = async (req, res, padId, authorId) => {
|
|||
}
|
||||
const text = await fs.readFile(srcFile, 'utf8');
|
||||
directDatabaseAccess = true;
|
||||
await importEtherpad.setPadRaw(padId, text, authorId);
|
||||
await setPadRaw(padId, text, authorId);
|
||||
}
|
||||
|
||||
// convert file to html if necessary
|
||||
|
@ -207,7 +207,7 @@ const doImport = async (req, res, padId, authorId) => {
|
|||
if (!directDatabaseAccess) {
|
||||
if (importHandledByPlugin || useConverter || fileIsHTML) {
|
||||
try {
|
||||
await importHtml.setPadHTML(pad, text, authorId);
|
||||
await setPadHTML(pad, text, authorId);
|
||||
} catch (err) {
|
||||
logger.warn(`Error importing, possibly caused by malformed HTML: ${err.stack || err}`);
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ import {createCollection} from '../stats';
|
|||
import {strict as assert} from "assert";
|
||||
|
||||
import {RateLimiterMemory} from 'rate-limiter-flexible';
|
||||
import webaccess from '../hooks/express/webaccess';
|
||||
import {userCanModify} from '../hooks/express/webaccess';
|
||||
import {ErrorCaused} from "../models/ErrorCaused";
|
||||
import {Pad} from "../db/Pad";
|
||||
import {SessionInfo} from "../models/SessionInfo";
|
||||
|
@ -164,7 +164,7 @@ const padChannels = new Channels((ch, {socket, message}) => handleUserChanges(so
|
|||
* This Method is called by server.js to tell the message handler on which socket it should send
|
||||
* @param socket_io The Socket
|
||||
*/
|
||||
exports.setSocketIO = (socket_io) => {
|
||||
export const setSocketIO = (socket_io) => {
|
||||
socketio = socket_io;
|
||||
};
|
||||
|
||||
|
@ -172,7 +172,7 @@ exports.setSocketIO = (socket_io) => {
|
|||
* Handles the connection of a new user
|
||||
* @param socket the socket.io Socket object for the new connection from the client
|
||||
*/
|
||||
exports.handleConnect = (socket) => {
|
||||
export const handleConnect = (socket) => {
|
||||
createCollection.meter('connects').mark();
|
||||
|
||||
// Initialize sessioninfos for this new session
|
||||
|
@ -186,7 +186,7 @@ exports.handleConnect = (socket) => {
|
|||
/**
|
||||
* Kicks all sessions from a pad
|
||||
*/
|
||||
exports.kickSessionsFromPad = (padID) => {
|
||||
export const kickSessionsFromPad = (padID) => {
|
||||
if (typeof socketio.sockets.clients !== 'function') return;
|
||||
|
||||
// skip if there is nobody on this pad
|
||||
|
@ -200,7 +200,7 @@ exports.kickSessionsFromPad = (padID) => {
|
|||
* Handles the disconnection of a user
|
||||
* @param socket the socket.io Socket object for the client
|
||||
*/
|
||||
exports.handleDisconnect = async (socket) => {
|
||||
export const handleDisconnect = async (socket) => {
|
||||
createCollection.meter('disconnects').mark();
|
||||
const session = sessioninfos[socket.id];
|
||||
delete sessioninfos[socket.id];
|
||||
|
@ -238,7 +238,7 @@ exports.handleDisconnect = async (socket) => {
|
|||
* @param socket the socket.io Socket object for the client
|
||||
* @param message the message from the client
|
||||
*/
|
||||
exports.handleMessage = async (socket, message) => {
|
||||
export const handleMessage = async (socket, message) => {
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
|
||||
if (env === 'production') {
|
||||
|
@ -272,7 +272,7 @@ exports.handleMessage = async (socket, message) => {
|
|||
thisSession.padId = padIds.padId;
|
||||
thisSession.readOnlyPadId = padIds.readOnlyPadId;
|
||||
thisSession.readonly =
|
||||
padIds.readonly || !webaccess.userCanModify(thisSession.auth.padID, socket.client.request);
|
||||
padIds.readonly || !userCanModify(thisSession.auth.padID, socket.client.request);
|
||||
}
|
||||
// Outside of the checks done by this function, message.padId must not be accessed because it is
|
||||
// too easy to introduce a security vulnerability that allows malicious users to read or modify
|
||||
|
@ -416,7 +416,7 @@ const handleSaveRevisionMessage = async (socket, message) => {
|
|||
* @param msg {Object} the message we're sending
|
||||
* @param sessionID {string} the socketIO session to which we're sending this message
|
||||
*/
|
||||
exports.handleCustomObjectMessage = (msg, sessionID) => {
|
||||
export const handleCustomObjectMessage = (msg, sessionID) => {
|
||||
if (msg.data.type === 'CUSTOM') {
|
||||
if (sessionID) {
|
||||
// a sessionID is targeted: directly to this sessionID
|
||||
|
@ -684,7 +684,7 @@ const handleUserChanges = async (socket, message) => {
|
|||
socket.json.send({type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev}});
|
||||
thisSession.rev = newRev;
|
||||
if (newRev !== r) thisSession.time = await pad.getRevisionDate(newRev);
|
||||
await exports.updatePadClients(pad);
|
||||
await updatePadClients(pad);
|
||||
} catch (err) {
|
||||
socket.json.send({disconnect: 'badChangeset'});
|
||||
createCollection.meter('failedChangesets').mark();
|
||||
|
|
|
@ -39,18 +39,18 @@ let io;
|
|||
/**
|
||||
* adds a component
|
||||
*/
|
||||
exports.addComponent = (moduleName, module) => {
|
||||
if (module == null) return exports.deleteComponent(moduleName);
|
||||
export const addComponent = (moduleName, module) => {
|
||||
if (module == null) return deleteComponent(moduleName);
|
||||
components[moduleName] = module;
|
||||
module.setSocketIO(io);
|
||||
};
|
||||
|
||||
exports.deleteComponent = (moduleName) => { delete components[moduleName]; };
|
||||
export const deleteComponent = (moduleName) => { delete components[moduleName]; };
|
||||
|
||||
/**
|
||||
* sets the socket.io and adds event functions for routing
|
||||
*/
|
||||
exports.setSocketIO = (_io) => {
|
||||
export const setSocketIO = (_io) => {
|
||||
io = _io;
|
||||
|
||||
io.sockets.on('connection', (socket) => {
|
||||
|
|
|
@ -1,34 +1,57 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('underscore');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const events = require('events');
|
||||
const express = require('express');
|
||||
const expressSession = require('express-session');
|
||||
const fs = require('fs');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const log4js = require('log4js');
|
||||
const SessionStore = require('../db/SessionStore');
|
||||
const settings = require('../utils/Settings');
|
||||
const stats = require('../stats');
|
||||
const util = require('util');
|
||||
const webaccess = require('./express/webaccess');
|
||||
import _ from 'underscore';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import events from "events";
|
||||
|
||||
import express from "express";
|
||||
|
||||
import fs from "fs";
|
||||
|
||||
import expressSession from "express-session";
|
||||
|
||||
import hooks from "../../static/js/pluginfw/hooks";
|
||||
|
||||
import log4js from "log4js";
|
||||
|
||||
import SessionStore from "../db/SessionStore";
|
||||
|
||||
import {
|
||||
cookie,
|
||||
exposeVersion,
|
||||
getEpVersion,
|
||||
getGitCommit,
|
||||
ip,
|
||||
loglevel,
|
||||
port,
|
||||
sessionKey,
|
||||
ssl, sslKeys,
|
||||
trustProxy,
|
||||
users
|
||||
} from "../utils/Settings";
|
||||
|
||||
import {createCollection} from "../stats";
|
||||
|
||||
import util from "util";
|
||||
|
||||
import {checkAccess, checkAccess2} from "./express/webaccess";
|
||||
import {Socket} from "net";
|
||||
|
||||
const logger = log4js.getLogger('http');
|
||||
let serverName;
|
||||
let sessionStore;
|
||||
const sockets = new Set();
|
||||
const sockets = new Set<Socket>();
|
||||
const socketsEvents = new events.EventEmitter();
|
||||
const startTime = stats.settableGauge('httpStartTime');
|
||||
|
||||
exports.server = null;
|
||||
const startTime = createCollection.settableGauge('httpStartTime');
|
||||
|
||||
export let server = null;
|
||||
export let sessionMiddleware;
|
||||
const closeServer = async () => {
|
||||
if (exports.server != null) {
|
||||
if (server != null) {
|
||||
logger.info('Closing HTTP server...');
|
||||
// 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(exports.server.close.bind(exports.server))();
|
||||
const p = util.promisify(server.close.bind(server))();
|
||||
await hooks.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
|
||||
|
@ -47,7 +70,7 @@ const closeServer = async () => {
|
|||
}
|
||||
await p;
|
||||
clearTimeout(timeout);
|
||||
exports.server = null;
|
||||
server = null;
|
||||
startTime.setValue(0);
|
||||
logger.info('HTTP server closed');
|
||||
}
|
||||
|
@ -55,24 +78,24 @@ const closeServer = async () => {
|
|||
sessionStore = null;
|
||||
};
|
||||
|
||||
exports.createServer = async () => {
|
||||
export const createServer = async () => {
|
||||
console.log('Report bugs at https://github.com/ether/etherpad-lite/issues');
|
||||
|
||||
serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`;
|
||||
serverName = `Etherpad ${getGitCommit()} (https://etherpad.org)`;
|
||||
|
||||
console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`);
|
||||
console.log(`Your Etherpad version is ${getEpVersion()} (${getGitCommit()})`);
|
||||
|
||||
await exports.restartServer();
|
||||
await restartServer();
|
||||
|
||||
if (settings.ip === '') {
|
||||
if (ip.length===0) {
|
||||
// using Unix socket for connectivity
|
||||
console.log(`You can access your Etherpad instance using the Unix socket at ${settings.port}`);
|
||||
console.log(`You can access your Etherpad instance using the Unix socket at ${port}`);
|
||||
} else {
|
||||
console.log(`You can access your Etherpad instance at http://${settings.ip}:${settings.port}/`);
|
||||
console.log(`You can access your Etherpad instance at http://${ip}:${port}/`);
|
||||
}
|
||||
|
||||
if (!_.isEmpty(settings.users)) {
|
||||
console.log(`The plugin admin page is at http://${settings.ip}:${settings.port}/admin/plugins`);
|
||||
if (!_.isEmpty(users)) {
|
||||
console.log(`The plugin admin page is at http://${ip}:${port}/admin/plugins`);
|
||||
} else {
|
||||
console.warn('Admin username and password not set in settings.json. ' +
|
||||
'To access admin please uncomment and edit "users" in settings.json');
|
||||
|
@ -87,39 +110,40 @@ exports.createServer = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
exports.restartServer = async () => {
|
||||
export const restartServer = async () => {
|
||||
await closeServer();
|
||||
|
||||
const app = express(); // New syntax for express v3
|
||||
|
||||
if (settings.ssl) {
|
||||
if (ssl) {
|
||||
console.log('SSL -- enabled');
|
||||
console.log(`SSL -- server key file: ${settings.ssl.key}`);
|
||||
console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`);
|
||||
console.log(`SSL -- server key file: ${sslKeys.key}`);
|
||||
console.log(`SSL -- Certificate Authority's certificate file: ${sslKeys.cert}`);
|
||||
|
||||
const options = {
|
||||
key: fs.readFileSync(settings.ssl.key),
|
||||
cert: fs.readFileSync(settings.ssl.cert),
|
||||
key: fs.readFileSync(sslKeys.key),
|
||||
cert: fs.readFileSync(sslKeys.cert),
|
||||
ca: undefined
|
||||
};
|
||||
|
||||
if (settings.ssl.ca) {
|
||||
if (sslKeys.ca) {
|
||||
options.ca = [];
|
||||
for (let i = 0; i < settings.ssl.ca.length; i++) {
|
||||
const caFileName = settings.ssl.ca[i];
|
||||
for (let i = 0; i < sslKeys.ca.length; i++) {
|
||||
const caFileName = sslKeys.ca[i];
|
||||
options.ca.push(fs.readFileSync(caFileName));
|
||||
}
|
||||
}
|
||||
|
||||
const https = require('https');
|
||||
exports.server = https.createServer(options, app);
|
||||
server = https.createServer(options, app);
|
||||
} else {
|
||||
const http = require('http');
|
||||
exports.server = http.createServer(app);
|
||||
server = http.createServer(app);
|
||||
}
|
||||
|
||||
app.use((req, res, next) => {
|
||||
// res.header("X-Frame-Options", "deny"); // breaks embedded pads
|
||||
if (settings.ssl) {
|
||||
if (ssl) {
|
||||
// we use SSL
|
||||
res.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||
}
|
||||
|
@ -138,14 +162,14 @@ exports.restartServer = async () => {
|
|||
res.header('Referrer-Policy', 'same-origin');
|
||||
|
||||
// send git version in the Server response header if exposeVersion is true.
|
||||
if (settings.exposeVersion) {
|
||||
if (exposeVersion) {
|
||||
res.header('Server', serverName);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
if (settings.trustProxy) {
|
||||
if (trustProxy) {
|
||||
/*
|
||||
* If 'trust proxy' === true, the client’s IP address in req.ip will be the
|
||||
* left-most entry in the X-Forwarded-* header.
|
||||
|
@ -157,8 +181,10 @@ exports.restartServer = async () => {
|
|||
|
||||
// Measure response time
|
||||
app.use((req, res, next) => {
|
||||
const stopWatch = stats.timer('httpRequests').start();
|
||||
const stopWatch = createCollection.timer('httpRequests').start();
|
||||
const sendFn = res.send.bind(res);
|
||||
// FIXME Check if this is still needed
|
||||
// @ts-ignore
|
||||
res.send = (...args) => { stopWatch.end(); sendFn(...args); };
|
||||
next();
|
||||
});
|
||||
|
@ -167,20 +193,20 @@ exports.restartServer = async () => {
|
|||
// starts listening to requests as reported in issue #158. Not installing the log4js connect
|
||||
// logger when the log level has a higher severity than INFO since it would not log at that level
|
||||
// anyway.
|
||||
if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) {
|
||||
if (!(loglevel === 'WARN') && loglevel === 'ERROR') {
|
||||
app.use(log4js.connectLogger(logger, {
|
||||
level: log4js.levels.DEBUG,
|
||||
level: loglevel,
|
||||
format: ':status, :method :url',
|
||||
}));
|
||||
}
|
||||
|
||||
app.use(cookieParser(settings.sessionKey, {}));
|
||||
app.use(cookieParser(sessionKey, {}));
|
||||
|
||||
sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval);
|
||||
exports.sessionMiddleware = expressSession({
|
||||
sessionStore = new SessionStore(cookie.sessionRefreshInterval);
|
||||
sessionMiddleware = expressSession({
|
||||
propagateTouch: true,
|
||||
rolling: true,
|
||||
secret: settings.sessionKey,
|
||||
secret: sessionKey,
|
||||
store: sessionStore,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
|
@ -188,8 +214,8 @@ exports.restartServer = async () => {
|
|||
// cleaner :)
|
||||
name: 'express_sid',
|
||||
cookie: {
|
||||
maxAge: settings.cookie.sessionLifetime || null, // Convert 0 to null.
|
||||
sameSite: settings.cookie.sameSite,
|
||||
maxAge: cookie.sessionLifetime || null, // Convert 0 to null.
|
||||
sameSite: cookie.sameSite,
|
||||
|
||||
// The automatic express-session mechanism for determining if the application is being served
|
||||
// over ssl is similar to the one used for setting the language cookie, which check if one of
|
||||
|
@ -215,15 +241,15 @@ exports.restartServer = async () => {
|
|||
// 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});
|
||||
app.use(exports.sessionMiddleware);
|
||||
app.use(sessionMiddleware);
|
||||
|
||||
app.use(webaccess.checkAccess);
|
||||
app.use(checkAccess2);
|
||||
|
||||
await Promise.all([
|
||||
hooks.aCallAll('expressConfigure', {app}),
|
||||
hooks.aCallAll('expressCreateServer', {app, server: exports.server}),
|
||||
hooks.aCallAll('expressCreateServer', {app, server: server}),
|
||||
]);
|
||||
exports.server.on('connection', (socket) => {
|
||||
server.on('connection', (socket) => {
|
||||
sockets.add(socket);
|
||||
socketsEvents.emit('updated');
|
||||
socket.on('close', () => {
|
||||
|
@ -231,11 +257,11 @@ exports.restartServer = async () => {
|
|||
socketsEvents.emit('updated');
|
||||
});
|
||||
});
|
||||
await util.promisify(exports.server.listen).bind(exports.server)(settings.port, settings.ip);
|
||||
await util.promisify(server.listen).bind(server)(port, ip);
|
||||
startTime.setValue(Date.now());
|
||||
logger.info('HTTP server listening for connections');
|
||||
};
|
||||
|
||||
exports.shutdown = async (hookName, context) => {
|
||||
export const shutdown = async (hookName, context) => {
|
||||
await closeServer();
|
||||
};
|
|
@ -8,7 +8,7 @@ import {doesPadExist} from '../../db/PadManager';
|
|||
import {getPadId, isReadOnlyId} from '../../db/ReadOnlyManager';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import {checkAccess} from '../../db/SecurityManager';
|
||||
import webaccess from './webaccess';
|
||||
import {userCanModify} from './webaccess';
|
||||
|
||||
exports.expressCreateServer = (hookName, args, cb) => {
|
||||
importExportRateLimiting.onLimitReached = (req, res, options) => {
|
||||
|
@ -72,7 +72,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
const {session: {user} = {}}:SessionSocketModel = req;
|
||||
const {accessStatus, authorID: authorId} = await checkAccess(
|
||||
req.params.pad, req.cookies.sessionID, req.cookies.token, user);
|
||||
if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) {
|
||||
if (accessStatus !== 'grant' || !userCanModify(req.params.pad, req)) {
|
||||
return res.status(403).send('Forbidden');
|
||||
}
|
||||
await doImport2(req, res, req.params.pad, authorId);
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
'use strict';
|
||||
|
||||
const padManager = require('../../db/PadManager');
|
||||
import {isValidPadId, sanitizePadId} from '../../db/PadManager';
|
||||
|
||||
exports.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 () => {
|
||||
// ensure the padname is valid and the url doesn't end with a /
|
||||
if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) {
|
||||
if (!isValidPadId(padId) || /\/$/.test(req.url)) {
|
||||
res.status(404).send('Such a padname is forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedPadId = await padManager.sanitizePadId(padId);
|
||||
const sanitizedPadId = await sanitizePadId(padId);
|
||||
|
||||
if (sanitizedPadId === padId) {
|
||||
// the pad id was fine, so just render it
|
|
@ -1,14 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
const events = require('events');
|
||||
const express = require('../express');
|
||||
const log4js = require('log4js');
|
||||
const proxyaddr = require('proxy-addr');
|
||||
const settings = require('../../utils/Settings');
|
||||
const socketio = require('socket.io');
|
||||
const socketIORouter = require('../../handler/SocketIORouter');
|
||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
||||
const padMessageHandler = require('../../handler/PadMessageHandler');
|
||||
import events from 'events';
|
||||
import {sessionMiddleware} from '../express';
|
||||
import log4js from 'log4js';
|
||||
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 * as padMessageHandler from '../../handler/PadMessageHandler';
|
||||
|
||||
let io;
|
||||
const logger = log4js.getLogger('socket.io');
|
||||
|
@ -52,7 +52,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
// transports in this list at once
|
||||
// e.g. XHR is disabled in IE by default, so in IE it should use jsonp-polling
|
||||
io = socketio({
|
||||
transports: settings.socketTransportProtocols,
|
||||
transports: socketTransportProtocols,
|
||||
}).listen(args.server, {
|
||||
/*
|
||||
* Do not set the "io" cookie.
|
||||
|
@ -74,7 +74,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
* https://github.com/socketio/socket.io/issues/2276#issuecomment-147184662 (not totally true, actually, see above)
|
||||
*/
|
||||
cookie: false,
|
||||
maxHttpBufferSize: settings.socketIo.maxHttpBufferSize,
|
||||
maxHttpBufferSize: socketIo.maxHttpBufferSize,
|
||||
});
|
||||
|
||||
io.on('connect', (socket) => {
|
||||
|
@ -90,7 +90,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
const req = socket.request;
|
||||
// Express sets req.ip but socket.io does not. Replicate Express's behavior here.
|
||||
if (req.ip == null) {
|
||||
if (settings.trustProxy) {
|
||||
if (trustProxy) {
|
||||
req.ip = proxyaddr(req, args.app.get('trust proxy fn'));
|
||||
} else {
|
||||
req.ip = socket.handshake.address;
|
||||
|
@ -102,7 +102,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
req.headers.cookie = socket.handshake.query.cookie;
|
||||
}
|
||||
// See: https://socket.io/docs/faq/#Usage-with-express-session
|
||||
express.sessionMiddleware(req, {}, next);
|
||||
sessionMiddleware(req, {}, next);
|
||||
});
|
||||
|
||||
io.use((socket, next) => {
|
||||
|
@ -130,8 +130,8 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
// if(settings.minify) io.enable('browser client minification');
|
||||
|
||||
// Initialize the Socket.IO Router
|
||||
socketIORouter.setSocketIO(io);
|
||||
socketIORouter.addComponent('pad', padMessageHandler);
|
||||
setSocketIO(io);
|
||||
addComponent('pad', padMessageHandler);
|
||||
|
||||
hooks.callAll('socketio', {app: args.app, io, server: args.server});
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const eejs = require('../../eejs');
|
||||
const fs = require('fs');
|
||||
import path from 'path';
|
||||
import {required} from '../../eejs';
|
||||
import fs from 'fs';
|
||||
const fsp = fs.promises;
|
||||
const toolbar = require('../../utils/toolbar');
|
||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
||||
const settings = require('../../utils/Settings');
|
||||
const util = require('util');
|
||||
const webaccess = require('./webaccess');
|
||||
import {} from '../../utils/toolbar';
|
||||
import hooks 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}) => {
|
||||
// This endpoint is intended to conform to:
|
||||
|
@ -17,25 +17,25 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
res.set('Content-Type', 'application/health+json');
|
||||
res.json({
|
||||
status: 'pass',
|
||||
releaseId: settings.getEpVersion(),
|
||||
releaseId: getEpVersion(),
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/stats', (req, res) => {
|
||||
res.json(require('../../stats').toJSON());
|
||||
res.json(required('../../stats').toJSON());
|
||||
});
|
||||
|
||||
app.get('/javascript', (req, res) => {
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req}));
|
||||
res.send(required('ep_etherpad-lite/templates/javascript.html', {req}));
|
||||
});
|
||||
|
||||
app.get('/robots.txt', (req, res) => {
|
||||
let filePath =
|
||||
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt');
|
||||
path.join(root, 'src', 'static', 'skins', skinName, 'robots.txt');
|
||||
res.sendFile(filePath, (err) => {
|
||||
// there is no custom robots.txt, send the default robots.txt which dissallows all
|
||||
if (err) {
|
||||
filePath = path.join(settings.root, 'src', 'static', 'robots.txt');
|
||||
filePath = path.join(root, 'src', 'static', 'robots.txt');
|
||||
res.sendFile(filePath);
|
||||
}
|
||||
});
|
||||
|
@ -44,9 +44,9 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
app.get('/favicon.ico', (req, res, next) => {
|
||||
(async () => {
|
||||
const fns = [
|
||||
...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []),
|
||||
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'),
|
||||
path.join(settings.root, 'src', 'static', 'favicon.ico'),
|
||||
...(favicon ? [path.resolve(root, favicon)] : []),
|
||||
path.join(root, 'src', 'static', 'skins', skinName, 'favicon.ico'),
|
||||
path.join(root, 'src', 'static', 'favicon.ico'),
|
||||
];
|
||||
for (const fn of fns) {
|
||||
try {
|
||||
|
@ -54,7 +54,7 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
} catch (err) {
|
||||
continue;
|
||||
}
|
||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
||||
res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
|
||||
await util.promisify(res.sendFile.bind(res))(fn);
|
||||
return;
|
||||
}
|
||||
|
@ -66,13 +66,13 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
exports.expressCreateServer = (hookName, args, cb) => {
|
||||
// serve index.html under /
|
||||
args.app.get('/', (req, res) => {
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req}));
|
||||
res.send(required('ep_etherpad-lite/templates/index.html', {req}));
|
||||
});
|
||||
|
||||
// serve pad.html under /p
|
||||
args.app.get('/p/:pad', (req, res, next) => {
|
||||
// The below might break for pads being rewritten
|
||||
const isReadOnly = !webaccess.userCanModify(req.params.pad, req);
|
||||
const isReadOnly = !userCanModify(req.params.pad, req);
|
||||
|
||||
hooks.callAll('padInitToolbar', {
|
||||
toolbar,
|
||||
|
@ -81,7 +81,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
|
||||
// can be removed when require-kernel is dropped
|
||||
res.header('Feature-Policy', 'sync-xhr \'self\'');
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/pad.html', {
|
||||
res.send(required('ep_etherpad-lite/templates/pad.html', {
|
||||
req,
|
||||
toolbar,
|
||||
isReadOnly,
|
||||
|
@ -94,7 +94,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
toolbar,
|
||||
});
|
||||
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
|
||||
res.send(required('ep_etherpad-lite/templates/timeslider.html', {
|
||||
req,
|
||||
toolbar,
|
||||
}));
|
|
@ -19,8 +19,9 @@ const getTar = async () => {
|
|||
};
|
||||
const tarJson = await fs.readFile(path.join(settings.root, 'src/node/utils/tar.json'), 'utf8');
|
||||
const tar = {};
|
||||
for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) {
|
||||
const files = relativeFiles.map(prefixLocalLibraryPath);
|
||||
for (let [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) {
|
||||
const relativeFilesMapped = relativeFiles as string[];
|
||||
const files = relativeFilesMapped.map(prefixLocalLibraryPath);
|
||||
tar[prefixLocalLibraryPath(key)] = files
|
||||
.concat(files.map((p) => p.replace(/\.js$/, '')))
|
||||
.concat(files.map((p) => `${p.replace(/\.js$/, '')}/index.js`));
|
||||
|
@ -28,7 +29,7 @@ const getTar = async () => {
|
|||
return tar;
|
||||
};
|
||||
|
||||
exports.expressPreSession = async (hookName, {app}) => {
|
||||
export const expressPreSession = async (hookName, {app}) => {
|
||||
// Cache both minified and static.
|
||||
const assetCache = new CachingMiddleware();
|
||||
app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache));
|
||||
|
@ -62,8 +63,9 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
const clientParts = plugins.parts.filter((part) => part.client_hooks != null);
|
||||
const clientPlugins = {};
|
||||
for (const name of new Set(clientParts.map((part) => part.plugin))) {
|
||||
clientPlugins[name] = {...plugins.plugins[name]};
|
||||
delete clientPlugins[name].package;
|
||||
const mappedName = name as any
|
||||
clientPlugins[mappedName] = {...plugins.plugins[mappedName]};
|
||||
delete clientPlugins[mappedName].package;
|
||||
}
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
|
@ -1,11 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fsp = require('fs').promises;
|
||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const sanitizePathname = require('../../utils/sanitizePathname');
|
||||
const settings = require('../../utils/Settings');
|
||||
import path from 'path';
|
||||
import {promises as fsp} from "fs";
|
||||
|
||||
import plugins from "../../../static/js/pluginfw/plugin_defs";
|
||||
|
||||
import sanitizePathname from "../../utils/sanitizePathname";
|
||||
|
||||
import {enableAdminUITests, root} from "../../utils/Settings";
|
||||
import {Presession} from "../../models/Presession";
|
||||
// Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/'
|
||||
// instead of path.sep to separate pathname components.
|
||||
const findSpecs = async (specDir) => {
|
||||
|
@ -29,16 +32,17 @@ const findSpecs = async (specDir) => {
|
|||
return specs;
|
||||
};
|
||||
|
||||
exports.expressPreSession = async (hookName, {app}) => {
|
||||
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]) => {
|
||||
let {package: {path: pluginPath}} = def;
|
||||
const mappedDef = def as Presession;
|
||||
let {package: {path: pluginPath}} = mappedDef;
|
||||
if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep;
|
||||
const specDir = `${plugin === 'ep_etherpad-lite' ? '' : 'static/'}tests/frontend/specs`;
|
||||
for (const spec of await findSpecs(path.join(pluginPath, specDir))) {
|
||||
if (plugin === 'ep_etherpad-lite' && !settings.enableAdminUITests &&
|
||||
if (plugin === 'ep_etherpad-lite' && !enableAdminUITests &&
|
||||
spec.startsWith('admin')) continue;
|
||||
modules.push(`${plugin}/${specDir}/${spec.replace(/\.js$/, '')}`);
|
||||
}
|
||||
|
@ -57,7 +61,7 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
})().catch((err) => next(err || new Error(err)));
|
||||
});
|
||||
|
||||
const rootTestFolder = path.join(settings.root, 'src/tests/frontend/');
|
||||
const rootTestFolder = path.join(root, 'src/tests/frontend/');
|
||||
|
||||
app.get('/tests/frontend/index.html', (req, res) => {
|
||||
res.redirect(['./', ...req.url.split('?').slice(1)].join('?'));
|
|
@ -1,12 +1,17 @@
|
|||
'use strict';
|
||||
|
||||
const assert = require('assert').strict;
|
||||
const log4js = require('log4js');
|
||||
const httpLogger = log4js.getLogger('http');
|
||||
const settings = require('../../utils/Settings');
|
||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
||||
const readOnlyManager = require('../../db/ReadOnlyManager');
|
||||
import {strict as assert} from "assert";
|
||||
|
||||
import log4js from "log4js";
|
||||
|
||||
import {requireAuthentication, requireAuthorization, setUsers, users} from "../../utils/Settings";
|
||||
|
||||
import hooks 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';
|
||||
|
||||
// Promisified wrapper around hooks.aCallFirst.
|
||||
|
@ -17,7 +22,7 @@ const aCallFirst = (hookName, context, pred = null) => new Promise((resolve, rej
|
|||
const aCallFirst0 =
|
||||
async (hookName, context, pred = null) => (await aCallFirst(hookName, context, pred))[0];
|
||||
|
||||
exports.normalizeAuthzLevel = (level) => {
|
||||
export const normalizeAuthzLevel = (level) => {
|
||||
if (!level) return false;
|
||||
switch (level) {
|
||||
case true:
|
||||
|
@ -32,20 +37,20 @@ exports.normalizeAuthzLevel = (level) => {
|
|||
return false;
|
||||
};
|
||||
|
||||
exports.userCanModify = (padId, req) => {
|
||||
if (readOnlyManager.isReadOnlyId(padId)) return false;
|
||||
if (!settings.requireAuthentication) return true;
|
||||
const {session: {user} = {}} = req;
|
||||
export const userCanModify = (padId, req) => {
|
||||
if (isReadOnlyId(padId)) return false;
|
||||
if (!requireAuthentication) return true;
|
||||
const {session: {user} = {}}:SessionSocketModel = req;
|
||||
if (!user || user.readOnly) return false;
|
||||
assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization.
|
||||
const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]);
|
||||
const level = normalizeAuthzLevel(user.padAuthorizations[padId]);
|
||||
return level && level !== 'readOnly';
|
||||
};
|
||||
|
||||
// Exported so that tests can set this to 0 to avoid unnecessary test slowness.
|
||||
exports.authnFailureDelayMs = 1000;
|
||||
export const authnFailureDelayMs = 1000;
|
||||
|
||||
const checkAccess = async (req, res, next) => {
|
||||
export const checkAccess = async (req, res, next) => {
|
||||
const requireAdmin = req.path.toLowerCase().startsWith('/admin');
|
||||
|
||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -88,16 +93,16 @@ const checkAccess = async (req, res, next) => {
|
|||
// authentication is checked and once after (if settings.requireAuthorization is true).
|
||||
const authorize = async () => {
|
||||
const grant = async (level) => {
|
||||
level = exports.normalizeAuthzLevel(level);
|
||||
level = normalizeAuthzLevel(level);
|
||||
if (!level) return false;
|
||||
const user = req.session.user;
|
||||
if (user == null) return true; // This will happen if authentication is not required.
|
||||
const encodedPadId = (req.path.match(/^\/p\/([^/]*)/) || [])[1];
|
||||
if (encodedPadId == null) return true;
|
||||
let padId = decodeURIComponent(encodedPadId);
|
||||
if (readOnlyManager.isReadOnlyId(padId)) {
|
||||
if (isReadOnlyId(padId)) {
|
||||
// pad is read-only, first get the real pad ID
|
||||
padId = await readOnlyManager.getPadId(padId);
|
||||
padId = await getPadId(padId);
|
||||
if (padId == null) return false;
|
||||
}
|
||||
// The user was granted access to a pad. Remember the authorization level in the user's
|
||||
|
@ -108,11 +113,11 @@ const checkAccess = async (req, res, next) => {
|
|||
};
|
||||
const isAuthenticated = req.session && req.session.user;
|
||||
if (isAuthenticated && req.session.user.is_admin) return await grant('create');
|
||||
const requireAuthn = requireAdmin || settings.requireAuthentication;
|
||||
const requireAuthn = requireAdmin || requireAuthentication;
|
||||
if (!requireAuthn) return await grant('create');
|
||||
if (!isAuthenticated) return await grant(false);
|
||||
if (requireAdmin && !req.session.user.is_admin) return await grant(false);
|
||||
if (!settings.requireAuthorization) return await grant('create');
|
||||
if (!requireAuthorization) return await grant('create');
|
||||
return await grant(await aCallFirst0('authorize', {req, res, next, resource: req.path}));
|
||||
};
|
||||
|
||||
|
@ -131,8 +136,10 @@ const checkAccess = async (req, res, next) => {
|
|||
// page).
|
||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
if (settings.users == null) settings.users = {};
|
||||
const ctx = {req, res, users: settings.users, next};
|
||||
if (users == null) setUsers({});
|
||||
const ctx = {req, res, users: users, next, username: undefined,
|
||||
password: undefined
|
||||
};
|
||||
// If the HTTP basic auth header is present, extract the username and password so it can be given
|
||||
// to authn plugins.
|
||||
const httpBasicAuth = req.headers.authorization && req.headers.authorization.startsWith('Basic ');
|
||||
|
@ -148,7 +155,7 @@ const checkAccess = async (req, res, next) => {
|
|||
}
|
||||
if (!(await aCallFirst0('authenticate', ctx))) {
|
||||
// Fall back to HTTP basic auth.
|
||||
const {[ctx.username]: {password} = {}} = settings.users;
|
||||
const {[ctx.username]: {password} = {password: undefined}}:UserIndexedModel = users;
|
||||
if (!httpBasicAuth || !ctx.username || password == null || password !== ctx.password) {
|
||||
httpLogger.info(`Failed authentication from IP ${req.ip}`);
|
||||
if (await aCallFirst0('authnFailure', {req, res})) return;
|
||||
|
@ -156,14 +163,14 @@ const checkAccess = async (req, res, next) => {
|
|||
// No plugin handled the authentication failure. Fall back to basic authentication.
|
||||
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
|
||||
// Delay the error response for 1s to slow down brute force attacks.
|
||||
await new Promise((resolve) => setTimeout(resolve, exports.authnFailureDelayMs));
|
||||
await new Promise((resolve) => setTimeout(resolve, authnFailureDelayMs));
|
||||
res.status(401).send('Authentication Required');
|
||||
return;
|
||||
}
|
||||
settings.users[ctx.username].username = ctx.username;
|
||||
users[ctx.username].username = ctx.username;
|
||||
// Make a shallow copy so that the password property can be deleted (to prevent it from
|
||||
// appearing in logs or in the database) without breaking future authentication attempts.
|
||||
req.session.user = {...settings.users[ctx.username]};
|
||||
req.session.user = {...users[ctx.username]};
|
||||
delete req.session.user.password;
|
||||
}
|
||||
if (req.session.user == null) {
|
||||
|
@ -190,6 +197,7 @@ const checkAccess = async (req, res, next) => {
|
|||
* Express middleware to authenticate the user and check authorization. Must be installed after the
|
||||
* express-session middleware.
|
||||
*/
|
||||
exports.checkAccess = (req, res, next) => {
|
||||
// FIXME why same method twice?
|
||||
export const checkAccess2 = (req, res, next) => {
|
||||
checkAccess(req, res, next).catch((err) => next(err || new Error(err)));
|
||||
};
|
|
@ -1,12 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
const languages = require('languages4translatewiki');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const _ = require('underscore');
|
||||
const pluginDefs = require('../../static/js/pluginfw/plugin_defs.js');
|
||||
const existsSync = require('../utils/path_exists');
|
||||
const settings = require('../utils/Settings');
|
||||
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 {check} from '../utils/path_exists';
|
||||
import {customLocaleStrings, maxAge, root} from '../utils/Settings';
|
||||
import {Presession} from "../models/Presession";
|
||||
|
||||
// returns all existing messages merged together and grouped by langcode
|
||||
// {es: {"foo": "string"}, en:...}
|
||||
|
@ -17,7 +18,7 @@ const getAllLocales = () => {
|
|||
// into `locales2paths` (files from various dirs are grouped by lang code)
|
||||
// (only json files with valid language code as name)
|
||||
const extractLangs = (dir) => {
|
||||
if (!existsSync(dir)) return;
|
||||
if (!check(dir)) return;
|
||||
let stat = fs.lstatSync(dir);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return;
|
||||
|
||||
|
@ -37,13 +38,14 @@ const getAllLocales = () => {
|
|||
};
|
||||
|
||||
// add core supported languages first
|
||||
extractLangs(path.join(settings.root, 'src/locales'));
|
||||
extractLangs(path.join(root, 'src/locales'));
|
||||
|
||||
// add plugins languages (if any)
|
||||
for (const {package: {path: pluginPath}} of Object.values(pluginDefs.plugins)) {
|
||||
for (const val of Object.values(pluginDefs.plugins)) {
|
||||
const pluginPath:Presession = val as Presession
|
||||
// plugin locales should overwrite etherpad's core locales
|
||||
if (pluginPath.endsWith('/ep_etherpad-lite') === true) continue;
|
||||
extractLangs(path.join(pluginPath, 'locales'));
|
||||
if (pluginPath.package.path.endsWith('/ep_etherpad-lite') === true) continue;
|
||||
extractLangs(path.join(pluginPath.package.path, 'locales'));
|
||||
}
|
||||
|
||||
// Build a locale index (merge all locale data other than user-supplied overrides)
|
||||
|
@ -68,9 +70,9 @@ const getAllLocales = () => {
|
|||
const wrongFormatErr = Error(
|
||||
'customLocaleStrings in wrong format. See documentation ' +
|
||||
'for Customization for Administrators, under Localization.');
|
||||
if (settings.customLocaleStrings) {
|
||||
if (typeof settings.customLocaleStrings !== 'object') throw wrongFormatErr;
|
||||
_.each(settings.customLocaleStrings, (overrides, langcode) => {
|
||||
if (customLocaleStrings) {
|
||||
if (typeof customLocaleStrings !== 'object') throw wrongFormatErr;
|
||||
_.each(customLocaleStrings, (overrides, langcode) => {
|
||||
if (typeof overrides !== 'object') throw wrongFormatErr;
|
||||
_.each(overrides, (localeString, key) => {
|
||||
if (typeof localeString !== 'string') throw wrongFormatErr;
|
||||
|
@ -112,7 +114,7 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
// works with /locale/en and /locale/en.json requests
|
||||
const locale = req.params.locale.split('.')[0];
|
||||
if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) {
|
||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
||||
res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`);
|
||||
} else {
|
||||
|
@ -121,7 +123,7 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
});
|
||||
|
||||
app.get('/locales.json', (req, res) => {
|
||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
||||
res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.send(localeIndex);
|
||||
});
|
11
src/node/models/CMDOptions.ts
Normal file
11
src/node/models/CMDOptions.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
export type CMDOptions = {
|
||||
cwd?: string,
|
||||
stdio?: string|any[],
|
||||
env?: NodeJS.ProcessEnv
|
||||
}
|
||||
|
||||
export type CMDPromise = {
|
||||
stdout: string,
|
||||
stderr: string,
|
||||
child: any
|
||||
}
|
1
src/node/models/LogLevel.ts
Normal file
1
src/node/models/LogLevel.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export type LogLevel = "DEBUG"|"INFO"|"WARN"|"ERROR"
|
3
src/node/models/PadDiffModel.ts
Normal file
3
src/node/models/PadDiffModel.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export type PadDiffModel = {
|
||||
_authors: any[]
|
||||
}
|
5
src/node/models/Presession.ts
Normal file
5
src/node/models/Presession.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type Presession = {
|
||||
package:{
|
||||
path: string,
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@ type SessionSocketModel = {
|
|||
user?: {
|
||||
username?: string,
|
||||
is_admin?: boolean
|
||||
readOnly?: boolean,
|
||||
padAuthorizations?: any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
5
src/node/models/UserIndexedModel.ts
Normal file
5
src/node/models/UserIndexedModel.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type UserIndexedModel = {
|
||||
[key: string]: {
|
||||
password?: string|undefined,
|
||||
}
|
||||
}
|
|
@ -203,11 +203,13 @@ export const stop = async () => {
|
|||
} catch (err) {
|
||||
logger.error('Error occurred while stopping Etherpad');
|
||||
state = State.STATE_TRANSITION_FAILED;
|
||||
// @ts-ignore
|
||||
stopDoneGate.resolve();
|
||||
return await exit(err);
|
||||
}
|
||||
logger.info('Etherpad stopped');
|
||||
state = State.STOPPED;
|
||||
// @ts-ignore
|
||||
stopDoneGate.resolve();
|
||||
};
|
||||
|
||||
|
|
|
@ -19,20 +19,21 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const spawn = require('child_process').spawn;
|
||||
const async = require('async');
|
||||
const settings = require('./Settings');
|
||||
const os = require('os');
|
||||
import {spawn} from "child_process";
|
||||
|
||||
import async from 'async';
|
||||
import {abiword} from './Settings';
|
||||
import os from 'os';
|
||||
|
||||
// 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) => {
|
||||
const abiword = spawn(settings.abiword, [`--to=${destFile}`, srcFile]);
|
||||
const abiword2 = spawn(abiword, [`--to=${destFile}`, srcFile]);
|
||||
let stdoutBuffer = '';
|
||||
abiword.stdout.on('data', (data) => { stdoutBuffer += data.toString(); });
|
||||
abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
|
||||
await new Promise((resolve, reject) => {
|
||||
abiword2.stdout.on('data', (data) => { stdoutBuffer += data.toString(); });
|
||||
abiword2.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
abiword.on('exit', (code) => {
|
||||
if (code !== 0) return reject(new Error(`Abiword died with exit code ${code}`));
|
||||
if (stdoutBuffer !== '') {
|
||||
|
@ -46,14 +47,14 @@ if (os.type().indexOf('Windows') > -1) {
|
|||
// communicate with it via stdin/stdout
|
||||
// thats much faster, about factor 10
|
||||
} else {
|
||||
let abiword;
|
||||
let abiword2;
|
||||
let stdoutCallback = null;
|
||||
const spawnAbiword = () => {
|
||||
abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']);
|
||||
abiword2 = spawn(abiword, ['--plugin', 'AbiCommand']);
|
||||
let stdoutBuffer = '';
|
||||
let firstPrompt = true;
|
||||
abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
|
||||
abiword.on('exit', (code) => {
|
||||
abiword2.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
|
||||
abiword2.on('exit', (code) => {
|
||||
spawnAbiword();
|
||||
if (stdoutCallback != null) {
|
||||
stdoutCallback(new Error(`Abiword died with exit code ${code}`));
|
|
@ -19,9 +19,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const log4js = require('log4js');
|
||||
const path = require('path');
|
||||
const _ = require('underscore');
|
||||
import log4js from 'log4js';
|
||||
import path from "path";
|
||||
|
||||
import _ from "underscore";
|
||||
|
||||
const absPathLogger = log4js.getLogger('AbsolutePaths');
|
||||
|
||||
|
@ -75,7 +76,7 @@ const popIfEndsWith = (stringArray, lastDesiredElements) => {
|
|||
* @return {string} The identified absolute base path. If such path cannot be
|
||||
* identified, prints a log and exits the application.
|
||||
*/
|
||||
exports.findEtherpadRoot = () => {
|
||||
export const findEtherpadRoot = () => {
|
||||
if (etherpadRoot != null) {
|
||||
return etherpadRoot;
|
||||
}
|
||||
|
@ -131,12 +132,12 @@ exports.findEtherpadRoot = () => {
|
|||
* it is returned unchanged. Otherwise it is interpreted
|
||||
* relative to exports.root.
|
||||
*/
|
||||
exports.makeAbsolute = (somePath) => {
|
||||
export const makeAbsolute = (somePath) => {
|
||||
if (path.isAbsolute(somePath)) {
|
||||
return somePath;
|
||||
}
|
||||
|
||||
const rewrittenPath = path.join(exports.findEtherpadRoot(), somePath);
|
||||
const rewrittenPath = path.join(findEtherpadRoot(), somePath);
|
||||
|
||||
absPathLogger.debug(`Relative path "${somePath}" can be rewritten to "${rewrittenPath}"`);
|
||||
return rewrittenPath;
|
||||
|
@ -150,7 +151,7 @@ exports.makeAbsolute = (somePath) => {
|
|||
* a subdirectory of the base one
|
||||
* @return {boolean}
|
||||
*/
|
||||
exports.isSubdir = (parent, arbitraryDir) => {
|
||||
export const isSubdir = (parent, arbitraryDir) => {
|
||||
// modified from: https://stackoverflow.com/questions/37521893/determine-if-a-path-is-subdirectory-of-another-in-node-js#45242825
|
||||
const relative = path.relative(parent, arbitraryDir);
|
||||
const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
@ -21,9 +21,8 @@
|
|||
*/
|
||||
|
||||
// An object containing the parsed command-line options
|
||||
exports.argv = {};
|
||||
export const argv = process.argv.slice(2);
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
let arg, prevArg;
|
||||
|
||||
// Loop through args
|
|
@ -14,17 +14,20 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {Stream} from "./Stream";
|
||||
|
||||
const Stream = require('./Stream');
|
||||
const assert = require('assert').strict;
|
||||
const authorManager = require('../db/AuthorManager');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const padManager = require('../db/PadManager');
|
||||
import {strict as assert} from "assert";
|
||||
|
||||
exports.getPadRaw = async (padId, readOnlyId) => {
|
||||
import {getAuthor} from "../db/AuthorManager";
|
||||
|
||||
import hooks from "../../static/js/pluginfw/hooks";
|
||||
|
||||
import {getPad} from "../db/PadManager";
|
||||
|
||||
export const getPadRaw = async (padId, readOnlyId) => {
|
||||
const dstPfx = `pad:${readOnlyId || padId}`;
|
||||
const [pad, customPrefixes] = await Promise.all([
|
||||
padManager.getPad(padId),
|
||||
getPad(padId),
|
||||
hooks.aCallAll('exportEtherpadAdditionalContent'),
|
||||
]);
|
||||
const pluginRecords = await Promise.all(customPrefixes.map(async (customPrefix) => {
|
||||
|
@ -43,7 +46,7 @@ exports.getPadRaw = async (padId, readOnlyId) => {
|
|||
const records = (function* () {
|
||||
for (const authorId of pad.getAllAuthors()) {
|
||||
yield [`globalAuthor:${authorId}`, (async () => {
|
||||
const authorEntry = await authorManager.getAuthor(authorId);
|
||||
const authorEntry = await getAuthor(authorId);
|
||||
if (!authorEntry) return undefined; // Becomes unset when converted to JSON.
|
||||
if (authorEntry.padIDs) authorEntry.padIDs = readOnlyId || padId;
|
||||
return authorEntry;
|
|
@ -19,11 +19,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const AttributeMap = require('../../static/js/AttributeMap');
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
import AttributeMap from '../../static/js/AttributeMap';
|
||||
import Changeset from '../../static/js/Changeset';
|
||||
|
||||
exports.getPadPlainText = (pad, revNum) => {
|
||||
const _analyzeLine = exports._analyzeLine;
|
||||
export const getPadPlainText = (pad, revNum) => {
|
||||
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext);
|
||||
const textLines = atext.text.slice(0, -1).split('\n');
|
||||
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
||||
|
@ -45,8 +44,14 @@ exports.getPadPlainText = (pad, revNum) => {
|
|||
};
|
||||
|
||||
|
||||
exports._analyzeLine = (text, aline, apool) => {
|
||||
const line = {};
|
||||
export const _analyzeLine = (text, aline, apool) => {
|
||||
const line = {
|
||||
listLevel: undefined,
|
||||
text: undefined,
|
||||
listTypeName: undefined,
|
||||
aline: undefined,
|
||||
start: undefined
|
||||
};
|
||||
|
||||
// identify list
|
||||
let lineMarker = 0;
|
||||
|
@ -81,5 +86,5 @@ exports._analyzeLine = (text, aline, apool) => {
|
|||
};
|
||||
|
||||
|
||||
exports._encodeWhitespace =
|
||||
export const _encodeWhitespace =
|
||||
(s) => s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`);
|
|
@ -15,16 +15,19 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const attributes = require('../../static/js/attributes');
|
||||
const padManager = require('../db/PadManager');
|
||||
const _ = require('underscore');
|
||||
const Security = require('../../static/js/security');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const eejs = require('../eejs');
|
||||
const _analyzeLine = require('./ExportHelper')._analyzeLine;
|
||||
const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace;
|
||||
const padutils = require('../../static/js/pad_utils').padutils;
|
||||
import Changeset from '../../static/js/Changeset';
|
||||
import attributes from "../../static/js/attributes";
|
||||
|
||||
import {getPad} from "../db/PadManager";
|
||||
|
||||
import _ from "underscore";
|
||||
|
||||
import Security from '../../static/js/security';
|
||||
import hooks from '../../static/js/pluginfw/hooks';
|
||||
import {required} from '../eejs';
|
||||
import {_analyzeLine, _encodeWhitespace} from "./ExportHelper";
|
||||
|
||||
import {padutils} from "../../static/js/pad_utils";
|
||||
|
||||
const getPadHTML = async (pad, revNum) => {
|
||||
let atext = pad.atext;
|
||||
|
@ -38,7 +41,7 @@ const getPadHTML = async (pad, revNum) => {
|
|||
return await getHTMLFromAtext(pad, atext);
|
||||
};
|
||||
|
||||
const getHTMLFromAtext = async (pad, atext, authorColors) => {
|
||||
export const getHTMLFromAtext = async (pad, atext, authorColors?) => {
|
||||
const apool = pad.apool();
|
||||
const textLines = atext.text.slice(0, -1).split('\n');
|
||||
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
||||
|
@ -456,8 +459,8 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
|
|||
return pieces.join('');
|
||||
};
|
||||
|
||||
exports.getPadHTMLDocument = async (padId, revNum, readOnlyId) => {
|
||||
const pad = await padManager.getPad(padId);
|
||||
export const getPadHTMLDocument = async (padId, revNum, readOnlyId?) => {
|
||||
const pad = await getPad(padId);
|
||||
|
||||
// Include some Styles into the Head for Export
|
||||
let stylesForExportCSS = '';
|
||||
|
@ -472,7 +475,7 @@ exports.getPadHTMLDocument = async (padId, revNum, readOnlyId) => {
|
|||
html += hookHtml;
|
||||
}
|
||||
|
||||
return eejs.require('ep_etherpad-lite/templates/export_html.html', {
|
||||
return required('ep_etherpad-lite/templates/export_html.html', {
|
||||
body: html,
|
||||
padId: Security.escapeHTML(readOnlyId || padId),
|
||||
extraCSS: stylesForExportCSS,
|
||||
|
@ -525,7 +528,4 @@ const _processSpaces = (s) => {
|
|||
}
|
||||
}
|
||||
return parts.join('');
|
||||
};
|
||||
|
||||
exports.getPadHTML = getPadHTML;
|
||||
exports.getHTMLFromAtext = getHTMLFromAtext;
|
||||
}
|
|
@ -39,7 +39,7 @@ const getPadTXT = async (pad, revNum) => {
|
|||
|
||||
// This is different than the functionality provided in ExportHtml as it provides formatting
|
||||
// functionality that is designed specifically for TXT exports
|
||||
const getTXTFromAtext = (pad, atext, authorColors) => {
|
||||
export const getTXTFromAtext = (pad, atext, authorColors?) => {
|
||||
const apool = pad.apool();
|
||||
const textLines = atext.text.slice(0, -1).split('\n');
|
||||
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
||||
|
@ -56,7 +56,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
|
|||
});
|
||||
|
||||
const getLineTXT = (text, attribs) => {
|
||||
const propVals = [false, false, false];
|
||||
const propVals:(boolean|number)[] = [false, false, false];
|
||||
const ENTER = 1;
|
||||
const STAY = 2;
|
||||
const LEAVE = 0;
|
||||
|
@ -256,9 +256,8 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
|
|||
return pieces.join('');
|
||||
};
|
||||
|
||||
exports.getTXTFromAtext = getTXTFromAtext;
|
||||
|
||||
exports.getPadTXTDocument = async (padId, revNum) => {
|
||||
export const getPadTXTDocument = async (padId, revNum) => {
|
||||
const pad = await padManager.getPad(padId);
|
||||
return getPadTXT(pad, revNum);
|
||||
};
|
|
@ -16,19 +16,21 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const AttributePool = require('../../static/js/AttributePool');
|
||||
const {Pad} = require('../db/Pad');
|
||||
const Stream = require('./Stream');
|
||||
const authorManager = require('../db/AuthorManager');
|
||||
const db = require('../db/DB');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const log4js = require('log4js');
|
||||
const supportedElems = require('../../static/js/contentcollector').supportedElems;
|
||||
const ueberdb = require('ueberdb2');
|
||||
import {AttributePool} from '../../static/js/AttributePool';
|
||||
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 log4js from "log4js";
|
||||
|
||||
import {supportedElems} from "../../static/js/contentcollector";
|
||||
|
||||
import ueberdb from 'ueberdb2';
|
||||
|
||||
const logger = log4js.getLogger('ImportEtherpad');
|
||||
|
||||
exports.setPadRaw = async (padId, r, authorId = '') => {
|
||||
export const setPadRaw = async (padId, r, authorId = '') => {
|
||||
const records = JSON.parse(r);
|
||||
|
||||
// get supported block Elements from plugins, we will use this later.
|
||||
|
@ -69,7 +71,7 @@ exports.setPadRaw = async (padId, r, authorId = '') => {
|
|||
throw new TypeError('globalAuthor padIDs subkey is not a string');
|
||||
}
|
||||
checkOriginalPadId(value.padIDs);
|
||||
if (await authorManager.doesAuthorExist(id)) {
|
||||
if (await doesAuthorExist(id)) {
|
||||
existingAuthors.add(id);
|
||||
return;
|
||||
}
|
||||
|
@ -115,7 +117,7 @@ exports.setPadRaw = async (padId, r, authorId = '') => {
|
|||
|
||||
const writeOps = (function* () {
|
||||
for (const [k, v] of data) yield db.set(k, v);
|
||||
for (const a of existingAuthors) yield authorManager.addPad(a, padId);
|
||||
for (const a of existingAuthors) yield addPad(a as string, padId);
|
||||
})();
|
||||
for (const op of new Stream(writeOps).batch(100).buffer(99)) await op;
|
||||
};
|
|
@ -15,15 +15,15 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const log4js = require('log4js');
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const contentcollector = require('../../static/js/contentcollector');
|
||||
const jsdom = require('jsdom');
|
||||
import log4js from 'log4js';
|
||||
import Changeset from '../../static/js/Changeset';
|
||||
import contentcollector from '../../static/js/contentcollector';
|
||||
import jsdom from 'jsdom';
|
||||
|
||||
const apiLogger = log4js.getLogger('ImportHtml');
|
||||
let processor;
|
||||
|
||||
exports.setPadHTML = async (pad, html, authorId = '') => {
|
||||
export const setPadHTML = async (pad, html, authorId = '') => {
|
||||
if (processor == null) {
|
||||
const [{rehype}, {default: minifyWhitespace}] =
|
||||
await Promise.all([import('rehype'), import('rehype-minify-whitespace')]);
|
||||
|
@ -90,4 +90,4 @@ exports.setPadHTML = async (pad, html, authorId = '') => {
|
|||
apiLogger.debug(`The changeset: ${theChangeset}`);
|
||||
await pad.setText('\n', authorId);
|
||||
await pad.appendRevision(theChangeset, authorId);
|
||||
};
|
||||
}
|
|
@ -17,20 +17,23 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const async = require('async');
|
||||
const fs = require('fs').promises;
|
||||
const log4js = require('log4js');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const runCmd = require('./run_cmd');
|
||||
const settings = require('./Settings');
|
||||
import async from 'async';
|
||||
import log4js from 'log4js';
|
||||
import {promises as fs} from "fs";
|
||||
|
||||
import os from 'os';
|
||||
import path from "path";
|
||||
|
||||
import {exportCMD} from "./run_cmd";
|
||||
|
||||
import {soffice} from "./Settings";
|
||||
|
||||
const logger = log4js.getLogger('LibreOffice');
|
||||
|
||||
const doConvertTask = async (task) => {
|
||||
const tmpDir = os.tmpdir();
|
||||
const p = runCmd([
|
||||
settings.soffice,
|
||||
const p = await exportCMD([
|
||||
soffice,
|
||||
'--headless',
|
||||
'--invisible',
|
||||
'--nologo',
|
||||
|
@ -81,7 +84,7 @@ const queue = async.queue(doConvertTask, 1);
|
|||
* @param {String} type The type to convert into
|
||||
* @param {Function} callback Standard callback function
|
||||
*/
|
||||
exports.convertFile = async (srcFile, destFile, type) => {
|
||||
export const convertFile = async (srcFile, destFile, type) => {
|
||||
// Used for the moving of the file, not the conversion
|
||||
const fileExtension = type;
|
||||
|
|
@ -21,21 +21,28 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const settings = require('./Settings');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const plugins = require('../../static/js/pluginfw/plugin_defs');
|
||||
const RequireKernel = require('etherpad-require-kernel');
|
||||
const mime = require('mime-types');
|
||||
const Threads = require('threads');
|
||||
const log4js = require('log4js');
|
||||
import {maxAge, root} from './Settings';
|
||||
import {promises as fs} from "fs";
|
||||
|
||||
import path from "path";
|
||||
|
||||
import plugins from "../../static/js/pluginfw/plugin_defs";
|
||||
|
||||
import RequireKernel from "etherpad-require-kernel";
|
||||
|
||||
import mime from "mime-types";
|
||||
|
||||
import Threads from "threads";
|
||||
|
||||
import log4js from "log4js";
|
||||
|
||||
const sanitizePathname = require('./sanitizePathname');
|
||||
|
||||
const logger = log4js.getLogger('Minify');
|
||||
|
||||
const ROOT_DIR = path.join(settings.root, 'src/static/');
|
||||
const ROOT_DIR = path.join(root, 'src/static/');
|
||||
|
||||
const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2);
|
||||
const threadsPool = Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2);
|
||||
|
||||
const LIBRARY_WHITELIST = [
|
||||
'async',
|
||||
|
@ -89,7 +96,7 @@ const requestURI = async (url, method, headers) => {
|
|||
return await p;
|
||||
};
|
||||
|
||||
const requestURIs = (locations, method, headers, callback) => {
|
||||
export const requestURIs = (locations, method, headers, callback) => {
|
||||
Promise.all(locations.map(async (loc) => {
|
||||
try {
|
||||
return await requestURI(loc, method, headers);
|
||||
|
@ -176,10 +183,10 @@ const minify = async (req, res) => {
|
|||
date.setMilliseconds(0);
|
||||
res.setHeader('last-modified', date.toUTCString());
|
||||
res.setHeader('date', (new Date()).toUTCString());
|
||||
if (settings.maxAge !== undefined) {
|
||||
const expiresDate = new Date(Date.now() + settings.maxAge * 1000);
|
||||
if (maxAge !== undefined) {
|
||||
const expiresDate = new Date(Date.now() + maxAge * 1000);
|
||||
res.setHeader('expires', expiresDate.toUTCString());
|
||||
res.setHeader('cache-control', `max-age=${settings.maxAge}`);
|
||||
res.setHeader('cache-control', `max-age=${maxAge}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -271,7 +278,7 @@ const requireDefinition = () => `var require = ${RequireKernel.kernelSource};\n`
|
|||
|
||||
const getFileCompressed = async (filename, contentType) => {
|
||||
let content = await getFile(filename);
|
||||
if (!content || !settings.minify) {
|
||||
if (!content || !minify) {
|
||||
return content;
|
||||
} else if (contentType === 'application/javascript') {
|
||||
return await new Promise((resolve) => {
|
||||
|
@ -317,10 +324,8 @@ const getFile = async (filename) => {
|
|||
return await fs.readFile(path.resolve(ROOT_DIR, filename));
|
||||
};
|
||||
|
||||
exports.minify = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err)));
|
||||
export const minifyExp = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err)));
|
||||
|
||||
exports.requestURIs = requestURIs;
|
||||
|
||||
exports.shutdown = async (hookName, context) => {
|
||||
export const shutdown = async (hookName, context) => {
|
||||
await threadsPool.terminate();
|
||||
};
|
|
@ -3,11 +3,14 @@
|
|||
* Worker thread to minify JS & CSS files out of the main NodeJS thread
|
||||
*/
|
||||
|
||||
const CleanCSS = require('clean-css');
|
||||
const Terser = require('terser');
|
||||
const fsp = require('fs').promises;
|
||||
const path = require('path');
|
||||
const Threads = require('threads');
|
||||
import CleanCSS from 'clean-css';
|
||||
import Terser from "terser";
|
||||
|
||||
import {promises as fsp} from "fs";
|
||||
|
||||
import path from "path";
|
||||
|
||||
import Threads from "threads";
|
||||
|
||||
const compressJS = (content) => Terser.minify(content);
|
||||
|
|
@ -27,6 +27,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import exp from "constants";
|
||||
|
||||
const absolutePaths = require('./AbsolutePaths');
|
||||
const deepEqual = require('fast-deep-equal/es6');
|
||||
import fs from 'fs';
|
||||
|
@ -35,6 +37,7 @@ 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');
|
||||
const suppressDisableMsg = ' -- To suppress these warning messages change ' +
|
||||
'suppressErrorsInPadText to true in your settings.json\n';
|
||||
|
@ -70,7 +73,7 @@ initLogging(defaultLogLevel, defaultLogConfig());
|
|||
/* Root path of the installation */
|
||||
export const root = absolutePaths.findEtherpadRoot();
|
||||
logger.info('All relative paths will be interpreted relative to the identified ' +
|
||||
`Etherpad base dir: ${exports.root}`);
|
||||
`Etherpad base dir: ${root}`);
|
||||
export const settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json');
|
||||
export const credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json');
|
||||
|
||||
|
@ -93,14 +96,14 @@ export const favicon = null;
|
|||
* Initialized to null, so we can spot an old configuration file and invite the
|
||||
* user to update it before falling back to the default.
|
||||
*/
|
||||
export const skinName = null;
|
||||
export let skinName = null;
|
||||
|
||||
export const skinVariants = 'super-light-toolbar super-light-editor light-background';
|
||||
|
||||
/**
|
||||
* The IP ep-lite should listen to
|
||||
*/
|
||||
export const ip = '0.0.0.0';
|
||||
export const ip:String = '0.0.0.0';
|
||||
|
||||
/**
|
||||
* The Port ep-lite should listen to
|
||||
|
@ -118,6 +121,12 @@ export const suppressErrorsInPadText = false;
|
|||
*/
|
||||
export const ssl = false;
|
||||
|
||||
export const sslKeys = {
|
||||
cert: undefined,
|
||||
key: undefined,
|
||||
ca: undefined,
|
||||
}
|
||||
|
||||
/**
|
||||
* socket.io transport methods
|
||||
**/
|
||||
|
@ -142,12 +151,12 @@ export const dbType = 'dirty';
|
|||
/**
|
||||
* This setting is passed with dbType to ueberDB to set up the database
|
||||
*/
|
||||
export const dbSettings = {filename: path.join(exports.root, 'var/dirty.db')};
|
||||
export const dbSettings = {filename: path.join(root, 'var/dirty.db')};
|
||||
|
||||
/**
|
||||
* The default Text of a new pad
|
||||
*/
|
||||
export const defaultPadText = [
|
||||
export let defaultPadText = [
|
||||
'Welcome to Etherpad!',
|
||||
'',
|
||||
'This pad text is synchronized as you type, so that everyone viewing this page sees the same ' +
|
||||
|
@ -244,12 +253,12 @@ export const minify = true;
|
|||
/**
|
||||
* The path of the abiword executable
|
||||
*/
|
||||
export const abiword = null;
|
||||
export let abiword = null;
|
||||
|
||||
/**
|
||||
* The path of the libreoffice executable
|
||||
*/
|
||||
export const soffice = null;
|
||||
export let soffice = null;
|
||||
|
||||
/**
|
||||
* The path of the tidy executable
|
||||
|
@ -264,7 +273,7 @@ export const allowUnknownFileEnds = true;
|
|||
/**
|
||||
* The log level of log4js
|
||||
*/
|
||||
export const loglevel = defaultLogLevel;
|
||||
export const loglevel:LogLevel = defaultLogLevel;
|
||||
|
||||
/**
|
||||
* Disable IP logging
|
||||
|
@ -299,7 +308,7 @@ export const logconfig = defaultLogConfig();
|
|||
/*
|
||||
* Session Key, do not sure this.
|
||||
*/
|
||||
export const sessionKey = false;
|
||||
export let sessionKey: string|boolean = false;
|
||||
|
||||
/*
|
||||
* Trust Proxy, whether or not trust the x-forwarded-for header.
|
||||
|
@ -333,7 +342,7 @@ export const cookie = {
|
|||
*/
|
||||
export const requireAuthentication = false;
|
||||
export const requireAuthorization = false;
|
||||
export const users = {};
|
||||
export let users = {};
|
||||
|
||||
/*
|
||||
* Show settings in admin page, by default it is true
|
||||
|
@ -384,6 +393,10 @@ export const exposeVersion = false;
|
|||
*/
|
||||
export const customLocaleStrings = {};
|
||||
|
||||
export const setUsers = (newUsers:any) => {
|
||||
users = newUsers;
|
||||
}
|
||||
|
||||
/*
|
||||
* From Etherpad 1.8.3 onwards, import and export of pads is always rate
|
||||
* limited.
|
||||
|
@ -434,7 +447,7 @@ export const enableAdminUITests = false;
|
|||
|
||||
// checks if abiword is avaiable
|
||||
export const abiwordAvailable = () => {
|
||||
if (exports.abiword != null) {
|
||||
if (abiword != null) {
|
||||
return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
|
||||
} else {
|
||||
return 'no';
|
||||
|
@ -442,7 +455,7 @@ export const abiwordAvailable = () => {
|
|||
};
|
||||
|
||||
export const sofficeAvailable = () => {
|
||||
if (exports.soffice != null) {
|
||||
if (soffice != null) {
|
||||
return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
|
||||
} else {
|
||||
return 'no';
|
||||
|
@ -450,7 +463,7 @@ export const sofficeAvailable = () => {
|
|||
};
|
||||
|
||||
export const exportAvailable = () => {
|
||||
const abiword = exports.abiwordAvailable();
|
||||
const abiword = abiwordAvailable();
|
||||
const soffice = sofficeAvailable();
|
||||
|
||||
if (abiword === 'no' && soffice === 'no') {
|
||||
|
@ -467,7 +480,7 @@ export const exportAvailable = () => {
|
|||
export const getGitCommit = () => {
|
||||
let version = '';
|
||||
try {
|
||||
let rootPath = exports.root;
|
||||
let rootPath = root;
|
||||
if (fs.lstatSync(`${rootPath}/.git`).isFile()) {
|
||||
rootPath = fs.readFileSync(`${rootPath}/.git`, 'utf8');
|
||||
rootPath = rootPath.split(' ').pop().trim();
|
||||
|
@ -735,88 +748,90 @@ export const reloadSettings = () => {
|
|||
storeSettings(settings);
|
||||
storeSettings(credentials);
|
||||
|
||||
initLogging(exports.loglevel, exports.logconfig);
|
||||
initLogging(loglevel, logconfig);
|
||||
|
||||
if (!exports.skinName) {
|
||||
if (!skinName) {
|
||||
logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' +
|
||||
'update your settings.json. Falling back to the default "colibris".');
|
||||
exports.skinName = 'colibris';
|
||||
skinName = 'colibris';
|
||||
}
|
||||
|
||||
// checks if skinName has an acceptable value, otherwise falls back to "colibris"
|
||||
if (exports.skinName) {
|
||||
const skinBasePath = path.join(exports.root, 'src', 'static', 'skins');
|
||||
const countPieces = exports.skinName.split(path.sep).length;
|
||||
if (skinName) {
|
||||
const skinBasePath = path.join(root, 'src', 'static', 'skins');
|
||||
const countPieces = skinName.split(path.sep).length;
|
||||
|
||||
if (countPieces !== 1) {
|
||||
logger.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` +
|
||||
`not valid: "${exports.skinName}". Falling back to the default "colibris".`);
|
||||
`not valid: "${skinName}". Falling back to the default "colibris".`);
|
||||
|
||||
exports.skinName = 'colibris';
|
||||
skinName = 'colibris';
|
||||
}
|
||||
|
||||
// informative variable, just for the log messages
|
||||
let skinPath = path.join(skinBasePath, exports.skinName);
|
||||
let skinPath = path.join(skinBasePath, skinName);
|
||||
|
||||
// what if someone sets skinName == ".." or "."? We catch him!
|
||||
if (absolutePaths.isSubdir(skinBasePath, skinPath) === false) {
|
||||
logger.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. ` +
|
||||
'Falling back to the default "colibris".');
|
||||
|
||||
exports.skinName = 'colibris';
|
||||
skinPath = path.join(skinBasePath, exports.skinName);
|
||||
skinName = 'colibris';
|
||||
skinPath = path.join(skinBasePath, skinName);
|
||||
}
|
||||
|
||||
if (fs.existsSync(skinPath) === false) {
|
||||
logger.error(`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`);
|
||||
exports.skinName = 'colibris';
|
||||
skinPath = path.join(skinBasePath, exports.skinName);
|
||||
skinName = 'colibris';
|
||||
skinPath = path.join(skinBasePath, skinName);
|
||||
}
|
||||
|
||||
logger.info(`Using skin "${exports.skinName}" in dir: ${skinPath}`);
|
||||
logger.info(`Using skin "${skinName}" in dir: ${skinPath}`);
|
||||
}
|
||||
|
||||
if (exports.abiword) {
|
||||
if (abiword) {
|
||||
// Check abiword actually exists
|
||||
if (exports.abiword != null) {
|
||||
fs.exists(exports.abiword, (exists: boolean) => {
|
||||
if (abiword != null) {
|
||||
fs.exists(abiword, (exists: boolean) => {
|
||||
if (!exists) {
|
||||
const abiwordError = 'Abiword does not exist at this path, check your settings file.';
|
||||
if (!exports.suppressErrorsInPadText) {
|
||||
exports.defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`;
|
||||
if (!suppressErrorsInPadText) {
|
||||
defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`;
|
||||
}
|
||||
logger.error(`${abiwordError} File location: ${exports.abiword}`);
|
||||
exports.abiword = null;
|
||||
logger.error(`${abiwordError} File location: ${abiword}`);
|
||||
abiword = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (exports.soffice) {
|
||||
fs.exists(exports.soffice, (exists: boolean) => {
|
||||
if (soffice) {
|
||||
fs.exists(soffice, (exists: boolean) => {
|
||||
if (!exists) {
|
||||
const sofficeError =
|
||||
'soffice (libreoffice) does not exist at this path, check your settings file.';
|
||||
|
||||
if (!exports.suppressErrorsInPadText) {
|
||||
exports.defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`;
|
||||
if (!suppressErrorsInPadText) {
|
||||
defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`;
|
||||
}
|
||||
logger.error(`${sofficeError} File location: ${exports.soffice}`);
|
||||
exports.soffice = null;
|
||||
logger.error(`${sofficeError} File location: ${soffice}`);
|
||||
soffice = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!exports.sessionKey) {
|
||||
if (!sessionKey) {
|
||||
const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt');
|
||||
try {
|
||||
exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8');
|
||||
sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8');
|
||||
logger.info(`Session key loaded from: ${sessionkeyFilename}`);
|
||||
} catch (e) {
|
||||
logger.info(
|
||||
`Session key file "${sessionkeyFilename}" not found. Creating with random contents.`);
|
||||
exports.sessionKey = randomString(32);
|
||||
fs.writeFileSync(sessionkeyFilename, exports.sessionKey, 'utf8');
|
||||
sessionKey = randomString(32);
|
||||
// FIXME Check out why this can be string boolean or Array
|
||||
// @ts-ignore
|
||||
fs.writeFileSync(sessionkeyFilename, sessionKey, 'utf8');
|
||||
}
|
||||
} else {
|
||||
logger.warn('Declaring the sessionKey in the settings.json is deprecated. ' +
|
||||
|
@ -825,17 +840,17 @@ export const reloadSettings = () => {
|
|||
'Interface then you can ignore this message.');
|
||||
}
|
||||
|
||||
if (exports.dbType === 'dirty') {
|
||||
if (dbType === 'dirty') {
|
||||
const dirtyWarning = 'DirtyDB is used. This is not recommended for production.';
|
||||
if (!exports.suppressErrorsInPadText) {
|
||||
exports.defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`;
|
||||
if (!suppressErrorsInPadText) {
|
||||
defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`;
|
||||
}
|
||||
|
||||
exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename);
|
||||
logger.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`);
|
||||
dbSettings.filename = absolutePaths.makeAbsolute(dbSettings.filename);
|
||||
logger.warn(`${dirtyWarning} File location: ${dbSettings.filename}`);
|
||||
}
|
||||
|
||||
if (exports.ip === '') {
|
||||
if (ip === '') {
|
||||
// using Unix socket for connectivity
|
||||
logger.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' +
|
||||
'"port" parameter will be interpreted as the path to a Unix socket to bind at.');
|
||||
|
@ -852,7 +867,7 @@ 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: ${exports.randomVersionString}`);
|
||||
logger.info(`Random string used for versioning assets: ${randomVersionString}`);
|
||||
};
|
||||
|
||||
exports.exportedForTestingOnly = {
|
||||
|
@ -860,6 +875,6 @@ exports.exportedForTestingOnly = {
|
|||
};
|
||||
|
||||
// initially load settings
|
||||
exports.reloadSettings();
|
||||
reloadSettings();
|
||||
|
||||
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
* Wrapper around any iterable that adds convenience methods that standard JavaScript iterable
|
||||
* objects lack.
|
||||
*/
|
||||
class Stream {
|
||||
export class Stream {
|
||||
private readonly _iter: any;
|
||||
private _next: null;
|
||||
/**
|
||||
* @returns {Stream} A Stream that yields values in the half-open range [start, end).
|
||||
*/
|
||||
|
@ -130,5 +132,3 @@ class Stream {
|
|||
*/
|
||||
[Symbol.iterator]() { return this._iter; }
|
||||
}
|
||||
|
||||
module.exports = Stream;
|
|
@ -3,16 +3,17 @@
|
|||
* Tidy up the HTML in a given file
|
||||
*/
|
||||
|
||||
const log4js = require('log4js');
|
||||
const settings = require('./Settings');
|
||||
const spawn = require('child_process').spawn;
|
||||
import log4js from 'log4js';
|
||||
import {tidyHtml} from "./Settings";
|
||||
|
||||
import {spawn} from "child_process";
|
||||
|
||||
exports.tidy = (srcFile) => {
|
||||
const logger = log4js.getLogger('TidyHtml');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Don't do anything if Tidy hasn't been enabled
|
||||
if (!settings.tidyHtml) {
|
||||
if (!tidyHtml) {
|
||||
logger.debug('tidyHtml has not been configured yet, ignoring tidy request');
|
||||
return resolve(null);
|
||||
}
|
||||
|
@ -21,7 +22,7 @@ exports.tidy = (srcFile) => {
|
|||
|
||||
// Spawn a new tidy instance that cleans up the file inline
|
||||
logger.debug(`Tidying ${srcFile}`);
|
||||
const tidy = spawn(settings.tidyHtml, ['-modify', srcFile]);
|
||||
const tidy = spawn(tidyHtml, ['-modify', srcFile]);
|
||||
|
||||
// Keep track of any error messages
|
||||
tidy.stderr.on('data', (data) => {
|
|
@ -16,15 +16,20 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Buffer = require('buffer').Buffer;
|
||||
const fs = require('fs');
|
||||
const fsp = fs.promises;
|
||||
const path = require('path');
|
||||
const zlib = require('zlib');
|
||||
const settings = require('./Settings');
|
||||
const existsSync = require('./path_exists');
|
||||
const util = require('util');
|
||||
import {Buffer} from "buffer";
|
||||
|
||||
import fs, {Stats} from "fs";
|
||||
|
||||
import path from "path";
|
||||
|
||||
import {check} from './path_exists'
|
||||
import zlib from "zlib";
|
||||
|
||||
import {root} from "./Settings";
|
||||
|
||||
import util from "util";
|
||||
|
||||
const fsp = fs.promises;
|
||||
/*
|
||||
* The crypto module can be absent on reduced node installations.
|
||||
*
|
||||
|
@ -45,8 +50,8 @@ try {
|
|||
_crypto = undefined;
|
||||
}
|
||||
|
||||
let CACHE_DIR = path.join(settings.root, 'var/');
|
||||
CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined;
|
||||
let CACHE_DIR = path.join(root, 'var/');
|
||||
CACHE_DIR = check(CACHE_DIR) ? CACHE_DIR : undefined;
|
||||
|
||||
const responseCache = {};
|
||||
|
||||
|
@ -78,7 +83,7 @@ if (_crypto) {
|
|||
should replace this.
|
||||
*/
|
||||
|
||||
module.exports = class CachingMiddleware {
|
||||
export class CachingMiddleware {
|
||||
handle(req, res, next) {
|
||||
this._handle(req, res, next).catch((err) => next(err || new Error(err)));
|
||||
}
|
||||
|
@ -88,8 +93,15 @@ module.exports = class CachingMiddleware {
|
|||
return next(undefined, req, res);
|
||||
}
|
||||
|
||||
const oldReq = {};
|
||||
const oldRes = {};
|
||||
const oldReq = {
|
||||
method: undefined
|
||||
};
|
||||
const oldRes = {
|
||||
write: undefined,
|
||||
end: undefined,
|
||||
setHeader: undefined,
|
||||
writeHead: undefined
|
||||
};
|
||||
|
||||
const supportsGzip =
|
||||
(req.get('Accept-Encoding') || '').indexOf('gzip') !== -1;
|
||||
|
@ -100,7 +112,7 @@ module.exports = class CachingMiddleware {
|
|||
const stats = await fsp.stat(`${CACHE_DIR}minified_${cacheKey}`).catch(() => {});
|
||||
const modifiedSince =
|
||||
req.headers['if-modified-since'] && new Date(req.headers['if-modified-since']);
|
||||
if (stats != null && stats.mtime && responseCache[cacheKey]) {
|
||||
if (stats != null && stats instanceof Object && "mtime" in stats && responseCache[cacheKey]) {
|
||||
req.headers['if-modified-since'] = stats.mtime.toUTCString();
|
||||
} else {
|
||||
delete req.headers['if-modified-since'];
|
||||
|
@ -200,4 +212,4 @@ module.exports = class CachingMiddleware {
|
|||
|
||||
next(undefined, req, res);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -7,7 +7,9 @@
|
|||
* @class CustomError
|
||||
* @extends {Error}
|
||||
*/
|
||||
class CustomError extends Error {
|
||||
export class CustomError extends Error {
|
||||
code: any;
|
||||
signal: any;
|
||||
/**
|
||||
* Creates an instance of CustomError.
|
||||
* @param {*} message
|
||||
|
@ -20,5 +22,3 @@ class CustomError extends Error {
|
|||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CustomError;
|
|
@ -1,11 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
const AttributeMap = require('../../static/js/AttributeMap');
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const attributes = require('../../static/js/attributes');
|
||||
const exportHtml = require('./ExportHtml');
|
||||
import AttributeMap from '../../static/js/AttributeMap';
|
||||
import Changeset from '../../static/js/Changeset';
|
||||
import attributes from '../../static/js/attributes';
|
||||
import {getHTMLFromAtext} from './ExportHtml';
|
||||
import {PadDiffModel} from "ep_etherpad-lite/node/models/PadDiffModel";
|
||||
|
||||
function PadDiff(pad, fromRev, toRev) {
|
||||
export const PadDiff = (pad, fromRev, toRev)=> {
|
||||
// check parameters
|
||||
if (!pad || !pad.id || !pad.atext || !pad.pool) {
|
||||
throw new Error('Invalid pad');
|
||||
|
@ -14,10 +15,16 @@ function PadDiff(pad, fromRev, toRev) {
|
|||
const range = pad.getValidRevisionRange(fromRev, toRev);
|
||||
if (!range) throw new Error(`Invalid revision range. startRev: ${fromRev} endRev: ${toRev}`);
|
||||
|
||||
// FIXME How to fix this?
|
||||
// @ts-ignore
|
||||
this._pad = pad;
|
||||
// @ts-ignore
|
||||
this._fromRev = range.startRev;
|
||||
// @ts-ignore
|
||||
this._toRev = range.endRev;
|
||||
// @ts-ignore
|
||||
this._html = null;
|
||||
// @ts-ignore
|
||||
this._authors = [];
|
||||
}
|
||||
|
||||
|
@ -108,9 +115,11 @@ PadDiff.prototype._getChangesetsInBulk = async function (startRev, count) {
|
|||
return {changesets, authors};
|
||||
};
|
||||
|
||||
PadDiff.prototype._addAuthors = function (authors) {
|
||||
const self = this;
|
||||
|
||||
PadDiff.prototype._addAuthors = (authors)=> {
|
||||
let self: undefined|PadDiffModel = this;
|
||||
if(!self){
|
||||
self = {_authors: []}
|
||||
}
|
||||
// add to array if not in the array
|
||||
authors.forEach((author) => {
|
||||
if (self._authors.indexOf(author) === -1) {
|
||||
|
@ -187,7 +196,7 @@ PadDiff.prototype.getHtml = async function () {
|
|||
const authorColors = await this._pad.getAllAuthorColors();
|
||||
|
||||
// convert the atext to html
|
||||
this._html = await exportHtml.getHTMLFromAtext(this._pad, atext, authorColors);
|
||||
this._html = await getHTMLFromAtext(this._pad, atext, authorColors);
|
||||
|
||||
return this._html;
|
||||
};
|
||||
|
@ -198,6 +207,10 @@ PadDiff.prototype.getAuthors = async function () {
|
|||
if (this._html == null) {
|
||||
await this.getHtml();
|
||||
}
|
||||
let self: undefined|PadDiffModel = this;
|
||||
if(!self){
|
||||
self = {_authors: []}
|
||||
}
|
||||
|
||||
return self._authors;
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
const fs = require('fs');
|
||||
import fs from 'fs';
|
||||
|
||||
const check = (path) => {
|
||||
export const check = (path) => {
|
||||
const existsSync = fs.statSync || fs.existsSync || path.existsSync;
|
||||
|
||||
let result;
|
||||
|
@ -11,6 +11,4 @@ const check = (path) => {
|
|||
result = false;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = check;
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
// `predicate`. Resolves to `undefined` if none of the Promises satisfy `predicate`, or if
|
||||
// `promises` is empty. If `predicate` is nullish, the truthiness of the resolved value is used as
|
||||
// the predicate.
|
||||
exports.firstSatisfies = (promises, predicate) => {
|
||||
export const firstSatisfies = (promises, predicate) => {
|
||||
if (predicate == null) predicate = (x) => x;
|
||||
|
||||
// Transform each original Promise into a Promise that never resolves if the original resolved
|
||||
|
@ -42,7 +42,7 @@ exports.firstSatisfies = (promises, predicate) => {
|
|||
// `total` is greater than `concurrency`, then `concurrency` Promises will be created right away,
|
||||
// and each remaining Promise will be created once one of the earlier Promises resolves.) This async
|
||||
// function resolves once all `total` Promises have resolved.
|
||||
exports.timesLimit = async (total, concurrency, promiseCreator) => {
|
||||
export const timesLimit = async (total, concurrency, promiseCreator) => {
|
||||
if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive');
|
||||
let next = 0;
|
||||
const addAnother = () => promiseCreator(next++).finally(() => {
|
||||
|
@ -55,11 +55,14 @@ exports.timesLimit = async (total, concurrency, promiseCreator) => {
|
|||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* An ordinary Promise except the `resolve` and `reject` executor functions are exposed as
|
||||
* properties.
|
||||
*/
|
||||
class Gate extends Promise {
|
||||
//FIXME Why is the constructor diviating from the Promise constructor?
|
||||
// @ts-ignore
|
||||
export class Gate extends Promise {
|
||||
// Coax `.then()` into returning an ordinary Promise, not a Gate. See
|
||||
// https://stackoverflow.com/a/65669070 for the rationale.
|
||||
static get [Symbol.species]() { return Promise; }
|
||||
|
@ -73,4 +76,3 @@ class Gate extends Promise {
|
|||
Object.assign(this, props);
|
||||
}
|
||||
}
|
||||
exports.Gate = Gate;
|
|
@ -1,12 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
const spawn = require('cross-spawn');
|
||||
const log4js = require('log4js');
|
||||
const path = require('path');
|
||||
const settings = require('./Settings');
|
||||
|
||||
import spawn from 'cross-spawn';
|
||||
import log4js from 'log4js';
|
||||
import path from 'path';
|
||||
import {root} from "./Settings";
|
||||
import {CMDOptions, CMDPromise} from '../models/CMDOptions'
|
||||
const logger = log4js.getLogger('runCmd');
|
||||
|
||||
import {CustomError} from './customError'
|
||||
const logLines = (readable, logLineFn) => {
|
||||
readable.setEncoding('utf8');
|
||||
// The process won't necessarily write full lines every time -- it might write a part of a line
|
||||
|
@ -69,33 +69,40 @@ const logLines = (readable, logLineFn) => {
|
|||
* - `stderr`: Similar to `stdout` but for stderr.
|
||||
* - `child`: The ChildProcess object.
|
||||
*/
|
||||
module.exports = exports = (args, opts = {}) => {
|
||||
export const exportCMD: (args: string[], opts:CMDOptions)=>void = async (args, opts = {
|
||||
cwd: undefined,
|
||||
stdio: undefined,
|
||||
env: undefined
|
||||
}) => {
|
||||
logger.debug(`Executing command: ${args.join(' ')}`);
|
||||
|
||||
opts = {cwd: settings.root, ...opts};
|
||||
opts = {cwd: root, ...opts};
|
||||
logger.debug(`cwd: ${opts.cwd}`);
|
||||
|
||||
// Log stdout and stderr by default.
|
||||
const stdio =
|
||||
Array.isArray(opts.stdio) ? opts.stdio.slice() // Copy to avoid mutating the caller's array.
|
||||
: typeof opts.stdio === 'function' ? [null, opts.stdio, opts.stdio]
|
||||
: opts.stdio === 'string' ? [null, 'string', 'string']
|
||||
: Array(3).fill(opts.stdio);
|
||||
: typeof opts.stdio === 'function' ? [null, opts.stdio, opts.stdio]
|
||||
: opts.stdio === 'string' ? [null, 'string', 'string']
|
||||
: Array(3).fill(opts.stdio);
|
||||
const cmdLogger = log4js.getLogger(`runCmd|${args[0]}`);
|
||||
if (stdio[1] == null) stdio[1] = (line) => cmdLogger.info(line);
|
||||
if (stdio[2] == null) stdio[2] = (line) => cmdLogger.error(line);
|
||||
if (stdio[1] == null && stdio instanceof Array) stdio[1] = (line) => cmdLogger.info(line);
|
||||
if (stdio[2] == null && stdio instanceof Array) stdio[2] = (line) => cmdLogger.error(line);
|
||||
const stdioLoggers = [];
|
||||
const stdioSaveString = [];
|
||||
for (const fd of [1, 2]) {
|
||||
if (typeof stdio[fd] === 'function') {
|
||||
stdioLoggers[fd] = stdio[fd];
|
||||
stdio[fd] = 'pipe';
|
||||
if (stdio instanceof Array)
|
||||
stdio[fd] = 'pipe';
|
||||
} else if (stdio[fd] === 'string') {
|
||||
stdioSaveString[fd] = true;
|
||||
stdio[fd] = 'pipe';
|
||||
if (stdio instanceof Array)
|
||||
stdio[fd] = 'pipe';
|
||||
}
|
||||
}
|
||||
opts.stdio = stdio;
|
||||
if (opts.stdio instanceof Array) {
|
||||
opts.stdio = stdio;
|
||||
}
|
||||
|
||||
// On Windows the PATH environment var might be spelled "Path".
|
||||
const pathVarName =
|
||||
|
@ -107,8 +114,8 @@ module.exports = exports = (args, opts = {}) => {
|
|||
opts.env = {
|
||||
...env, // Copy env to avoid modifying process.env or the caller's supplied env.
|
||||
[pathVarName]: [
|
||||
path.join(settings.root, 'src', 'node_modules', '.bin'),
|
||||
path.join(settings.root, 'node_modules', '.bin'),
|
||||
path.join(root, 'src', 'node_modules', '.bin'),
|
||||
path.join(root, 'node_modules', '.bin'),
|
||||
...(PATH ? PATH.split(path.delimiter) : []),
|
||||
].join(path.delimiter),
|
||||
};
|
||||
|
@ -116,13 +123,15 @@ module.exports = exports = (args, opts = {}) => {
|
|||
|
||||
// Create an error object to use in case the process fails. This is done here rather than in the
|
||||
// process's `exit` handler so that we get a useful stack trace.
|
||||
const procFailedErr = new Error();
|
||||
const procFailedErr:CustomError = new CustomError({});
|
||||
|
||||
const proc = spawn(args[0], args.slice(1), opts);
|
||||
const streams = [undefined, proc.stdout, proc.stderr];
|
||||
|
||||
let px;
|
||||
const p = new Promise((resolve, reject) => { px = {resolve, reject}; });
|
||||
const p = await new Promise<CMDPromise>((resolve, reject) => {
|
||||
px = {resolve, reject};
|
||||
});
|
||||
[, p.stdout, p.stderr] = streams;
|
||||
p.child = proc;
|
||||
|
||||
|
@ -132,6 +141,8 @@ module.exports = exports = (args, opts = {}) => {
|
|||
if (stdioLoggers[fd] != null) {
|
||||
logLines(streams[fd], stdioLoggers[fd]);
|
||||
} else if (stdioSaveString[fd]) {
|
||||
//FIXME How to solve this?
|
||||
// @ts-ignore
|
||||
p[[null, 'stdout', 'stderr'][fd]] = stdioStringPromises[fd] = (async () => {
|
||||
const chunks = [];
|
||||
for await (const chunk of streams[fd]) chunks.push(chunk);
|
|
@ -1,10 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
import path from 'path';
|
||||
|
||||
// Normalizes p and ensures that it is a relative path that does not reach outside. See
|
||||
// https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context.
|
||||
module.exports = (p, pathApi = path) => {
|
||||
export default (p, pathApi = path) => {
|
||||
// The documentation for path.normalize() says that it resolves '..' and '.' segments. The word
|
||||
// "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might
|
||||
// not be the same thing as 'b'. Most path normalization functions from other libraries (e.g.,
|
||||
|
@ -20,4 +20,4 @@ module.exports = (p, pathApi = path) => {
|
|||
// pathname would not be normalized away before being converted to '../'.
|
||||
if (pathApi.sep === '\\') p = p.replace(/\\/g, '/');
|
||||
return p;
|
||||
};
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
/**
|
||||
* The Toolbar Module creates and renders the toolbars and buttons
|
||||
*/
|
||||
const _ = require('underscore');
|
||||
import _ from 'underscore';
|
||||
|
||||
const removeItem = (array, what) => {
|
||||
let ax;
|
||||
|
@ -12,13 +12,13 @@ const removeItem = (array, what) => {
|
|||
return array;
|
||||
};
|
||||
|
||||
const defaultButtonAttributes = (name, overrides) => ({
|
||||
const defaultButtonAttributes = (name, overrides?) => ({
|
||||
command: name,
|
||||
localizationId: `pad.toolbar.${name}.title`,
|
||||
class: `buttonicon buttonicon-${name}`,
|
||||
});
|
||||
|
||||
const tag = (name, attributes, contents) => {
|
||||
const tag = (name, attributes, contents?) => {
|
||||
const aStr = tagAttributes(attributes);
|
||||
|
||||
if (_.isString(contents) && contents.length > 0) {
|
23
src/package-lock.json
generated
23
src/package-lock.json
generated
|
@ -58,6 +58,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "4.17.17",
|
||||
"@types/jquery": "^3.5.16",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/node": "^20.3.1",
|
||||
"concurrently": "^8.2.0",
|
||||
"eslint": "^8.14.0",
|
||||
|
@ -1081,6 +1083,21 @@
|
|||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jquery": {
|
||||
"version": "3.5.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz",
|
||||
"integrity": "sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/sizzle": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/js-cookie": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.3.tgz",
|
||||
"integrity": "sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
|
||||
|
@ -1181,6 +1198,12 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/sizzle": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
|
||||
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/stoppable": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.1.tgz",
|
||||
|
|
|
@ -78,23 +78,25 @@
|
|||
"etherpad-lite": "node/server.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "4.17.17",
|
||||
"@types/jquery": "^3.5.16",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/node": "^20.3.1",
|
||||
"concurrently": "^8.2.0",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-config-etherpad": "^3.0.13",
|
||||
"etherpad-cli-client": "^2.0.1",
|
||||
"mocha": "^9.2.2",
|
||||
"mocha-froth": "^0.2.10",
|
||||
"nodeify": "^1.0.1",
|
||||
"nodemon": "^2.0.22",
|
||||
"openapi-schema-validation": "^0.4.2",
|
||||
"selenium-webdriver": "^4.10.0",
|
||||
"set-cookie-parser": "^2.4.8",
|
||||
"sinon": "^13.0.2",
|
||||
"split-grid": "^1.0.11",
|
||||
"supertest": "^6.3.3",
|
||||
"typescript": "^4.9.5",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/express": "4.17.17",
|
||||
"concurrently": "^8.2.0",
|
||||
"nodemon": "^2.0.22"
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.15.0",
|
||||
|
|
|
@ -22,11 +22,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const isNodeText = (node) => (node.nodeType === 3);
|
||||
export const isNodeText = (node) => (node.nodeType === 3);
|
||||
|
||||
const getAssoc = (obj, name) => obj[`_magicdom_${name}`];
|
||||
export const getAssoc = (obj, name) => obj[`_magicdom_${name}`];
|
||||
|
||||
const setAssoc = (obj, name, value) => {
|
||||
export const setAssoc = (obj, name, value) => {
|
||||
// note that in IE designMode, properties of a node can get
|
||||
// copied to new nodes that are spawned during editing; also,
|
||||
// properties representable in HTML text can survive copy-and-paste
|
||||
|
@ -38,7 +38,7 @@ const setAssoc = (obj, name, value) => {
|
|||
// between false and true, a number between 0 and numItems inclusive.
|
||||
|
||||
|
||||
const binarySearch = (numItems, func) => {
|
||||
export const binarySearch = (numItems, func) => {
|
||||
if (numItems < 1) return 0;
|
||||
if (func(0)) return 0;
|
||||
if (!func(numItems - 1)) return numItems;
|
||||
|
@ -52,17 +52,10 @@ const binarySearch = (numItems, func) => {
|
|||
return high;
|
||||
};
|
||||
|
||||
const binarySearchInfinite = (expectedLength, func) => {
|
||||
export const binarySearchInfinite = (expectedLength, func) => {
|
||||
let i = 0;
|
||||
while (!func(i)) i += expectedLength;
|
||||
return binarySearch(i, func);
|
||||
};
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
exports.isNodeText = isNodeText;
|
||||
exports.getAssoc = getAssoc;
|
||||
exports.setAssoc = setAssoc;
|
||||
exports.binarySearch = binarySearch;
|
||||
exports.binarySearchInfinite = binarySearchInfinite;
|
||||
exports.noop = noop;
|
||||
export const noop = () => {};
|
|
@ -22,13 +22,13 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Security = require('./security');
|
||||
import Security from './security';
|
||||
|
||||
/**
|
||||
* Generates a random String with the given length. Is needed to generate the Author, Group,
|
||||
* readonly, session Ids
|
||||
*/
|
||||
const randomString = (len) => {
|
||||
const randomString = (len?) => {
|
||||
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
let randomstring = '';
|
||||
len = len || 20;
|
||||
|
@ -91,7 +91,9 @@ const urlRegex = (() => {
|
|||
// https://stackoverflow.com/a/68957976
|
||||
const base64url = /^(?=(?:.{4})*$)[A-Za-z0-9_-]*(?:[AQgw]==|[AEIMQUYcgkosw048]=)?$/;
|
||||
|
||||
const padutils = {
|
||||
export const padutils = {
|
||||
setupGlobalExceptionHandler: undefined,
|
||||
|
||||
/**
|
||||
* Prints a warning message followed by a stack trace (to make it easier to figure out what code
|
||||
* is using the deprecated function).
|
||||
|
@ -107,25 +109,32 @@ const padutils = {
|
|||
* @param {...*} args - Passed to `padutils.warnDeprecated.logger.warn` (or `console.warn` if no
|
||||
* logger is set), with a stack trace appended if available.
|
||||
*/
|
||||
warnDeprecated: (...args) => {
|
||||
if (padutils.warnDeprecated.disabledForTestingOnly) return;
|
||||
warnDeprecated: (...args) => {
|
||||
if ("disabledForTestingOnly" in padutils.warnDeprecated) return;
|
||||
const err = new Error();
|
||||
if (Error.captureStackTrace) Error.captureStackTrace(err, padutils.warnDeprecated);
|
||||
err.name = '';
|
||||
// Rate limit identical deprecation warnings (as determined by the stack) to avoid log spam.
|
||||
if (typeof err.stack === 'string') {
|
||||
if (padutils.warnDeprecated._rl == null) {
|
||||
if (typeof err.stack === 'string' && "_rl" in padutils.warnDeprecated) {
|
||||
padutils.warnDeprecated._rl =
|
||||
{prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000};
|
||||
}
|
||||
const rl = padutils.warnDeprecated._rl;
|
||||
const now = rl.now();
|
||||
const prev = rl.prevs.get(err.stack);
|
||||
if (prev != null && now - prev < rl.period) return;
|
||||
rl.prevs.set(err.stack, now);
|
||||
if (rl instanceof Object && "now" in rl && "prevs" in rl && "period" in rl
|
||||
&& rl.now instanceof Function && rl.prevs instanceof Map && rl.period instanceof Number){
|
||||
const now = rl.now();
|
||||
const prev = rl.prevs.get(err.stack);
|
||||
if (prev != null && now - prev < rl.period) return;
|
||||
rl.prevs.set(err.stack, now);
|
||||
}
|
||||
}
|
||||
if (err.stack) args.push(err.stack);
|
||||
(padutils.warnDeprecated.logger || console).warn(...args);
|
||||
if ("logger" in padutils.warnDeprecated && padutils.warnDeprecated.logger instanceof Object
|
||||
&& "warn" in padutils.warnDeprecated.logger && padutils.warnDeprecated.logger.warn instanceof Function){
|
||||
padutils.warnDeprecated.logger.warn(...args)
|
||||
}
|
||||
else{
|
||||
console.warn(...args)
|
||||
}
|
||||
},
|
||||
|
||||
escapeHtml: (x) => Security.escapeHTML(String(x)),
|
||||
|
@ -350,7 +359,11 @@ const padutils = {
|
|||
* Returns a string that can be used in the `token` cookie as a secret that authenticates a
|
||||
* particular author.
|
||||
*/
|
||||
generateAuthorToken: () => `t.${randomString()}`,
|
||||
generateAuthorToken: () => {
|
||||
const randomAuthToken = randomString()
|
||||
|
||||
return `t.+${randomAuthToken}`
|
||||
}
|
||||
};
|
||||
|
||||
let globalExceptionHandler = null;
|
||||
|
@ -400,6 +413,8 @@ padutils.setupGlobalExceptionHandler = () => {
|
|||
.append(txt(`UserAgent: ${navigator.userAgent}`)).append($('<br>')),
|
||||
];
|
||||
|
||||
//FIXME gritter not defined
|
||||
// @ts-ignore
|
||||
$.gritter.add({
|
||||
title: 'An error occurred',
|
||||
text: errorMsg,
|
||||
|
@ -429,7 +444,7 @@ padutils.setupGlobalExceptionHandler = () => {
|
|||
}
|
||||
};
|
||||
|
||||
padutils.binarySearch = require('./ace2_common').binarySearch;
|
||||
import {binarySearch} from '../../static/js/ace2_common';
|
||||
|
||||
// https://stackoverflow.com/a/42660748
|
||||
const inThirdPartyIframe = () => {
|
||||
|
@ -439,11 +454,12 @@ const inThirdPartyIframe = () => {
|
|||
return true;
|
||||
}
|
||||
};
|
||||
import cookies from 'js-cookie/dist/js.cookie';
|
||||
|
||||
// This file is included from Node so that it can reuse randomString, but Node doesn't have a global
|
||||
// window object.
|
||||
if (typeof window !== 'undefined') {
|
||||
exports.Cookies = require('js-cookie/dist/js.cookie').withAttributes({
|
||||
cookies.withAttributes({
|
||||
// Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case
|
||||
// use `SameSite=None`. For iframes from another site, only `None` has a chance of working
|
||||
// because the cookies are third-party (not same-site). Many browsers/users block third-party
|
||||
|
@ -456,5 +472,3 @@ if (typeof window !== 'undefined') {
|
|||
secure: window.location.protocol === 'https:',
|
||||
});
|
||||
}
|
||||
exports.randomString = randomString;
|
||||
exports.padutils = padutils;
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"types": ["node", "jquery"],
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"target": "es6",
|
||||
|
|
Loading…
Reference in a new issue