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