Compare commits

...

11 Commits

Author SHA1 Message Date
SamTV12345 f6d7939b9e Split at dist path. 2023-07-05 06:31:05 +02:00
SamTV12345 9a3b600666 Added config for main node package. 2023-07-04 22:14:25 +02:00
SamTV12345 8ddac2db45 Added rollup config. 2023-07-04 21:57:11 +02:00
SamTV12345 8926677a66
Start server without paths. 2023-06-25 16:49:17 +02:00
SamTV12345 798543fb45
Server starts up. 2023-06-25 15:26:34 +02:00
SamTV12345 aa6323e488
Fixed startup to "Running npm to get a list of installed plugins" 2023-06-25 13:22:01 +02:00
SamTV12345 76a6f665a4
Move all files to esm 2023-06-25 00:05:01 +02:00
SamTV12345 237f7242ec
Moved to ts for other dependencies. 2023-06-23 21:18:12 +02:00
SamTV12345 7b99edc471
Moved to ts for other dependencies. 2023-06-23 20:53:55 +02:00
SamTV12345 3c2129b1cc
Rewrote server in typescript. 2023-06-23 18:57:36 +02:00
SamTV12345 331cf3d79f
Added typescript support for most backend files. 2023-06-22 22:54:02 +02:00
287 changed files with 31466 additions and 35279 deletions

1
node_modules/ep_etherpad-lite generated vendored
View File

@ -1 +0,0 @@
../src

3
src/.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env"]
}

View File

@ -1,10 +1,6 @@
import "eslint-config-etherpad/patch/modern-module-resolution";
'use strict';
// This is a workaround for https://github.com/eslint/eslint/issues/3458
require('eslint-config-etherpad/patch/modern-module-resolution');
module.exports = {
ignorePatterns: [
export const ignorePatterns = [
'/static/js/admin/jquery.autosize.js',
'/static/js/admin/minify.json.js',
'/static/js/vendors/browser.js',
@ -14,128 +10,132 @@ module.exports = {
'/static/js/vendors/jquery.js',
'/static/js/vendors/nice-select.js',
'/tests/frontend/lib/',
],
overrides: [
];
export const overrides = [
{
files: [
'**/.eslintrc.*',
],
extends: 'etherpad/node',
files: [
'**/.eslintrc.*',
],
extends: 'etherpad/node',
},
{
files: [
'**/*',
],
excludedFiles: [
'**/.eslintrc.*',
'tests/frontend/**/*',
],
extends: 'etherpad/node',
},
{
files: [
'static/**/*',
'tests/frontend/helper.js',
'tests/frontend/helper/**/*',
],
excludedFiles: [
'**/.eslintrc.*',
],
extends: 'etherpad/browser',
env: {
'shared-node-browser': true,
},
overrides: [
{
files: [
'tests/frontend/helper/**/*',
],
globals: {
helper: 'readonly',
},
},
],
},
{
files: [
'tests/**/*',
],
excludedFiles: [
'**/.eslintrc.*',
'tests/frontend/cypress/**/*',
'tests/frontend/helper.js',
'tests/frontend/helper/**/*',
'tests/frontend/travis/**/*',
'tests/ratelimit/**/*',
],
extends: 'etherpad/tests',
rules: {
'mocha/no-exports': 'off',
'mocha/no-top-level-hooks': 'off',
},
},
{
files: [
'tests/backend/**/*',
],
excludedFiles: [
'**/.eslintrc.*',
],
extends: 'etherpad/tests/backend',
overrides: [
{
files: [
'tests/backend/**/*',
],
excludedFiles: [
'tests/backend/specs/**/*',
],
rules: {
'mocha/no-exports': 'off',
'mocha/no-top-level-hooks': 'off',
},
},
],
},
{
files: [
'tests/frontend/**/*',
],
excludedFiles: [
'**/.eslintrc.*',
'tests/frontend/cypress/**/*',
'tests/frontend/helper.js',
'tests/frontend/helper/**/*',
'tests/frontend/travis/**/*',
],
extends: 'etherpad/tests/frontend',
overrides: [
{
files: [
files: [
'**/*',
],
excludedFiles: [
'**/.eslintrc.*',
'tests/frontend/**/*',
],
excludedFiles: [
'tests/frontend/specs/**/*',
],
rules: {
],
extends: 'etherpad/node',
},
{
files: [
'static/**/*',
'tests/frontend/helper.js',
'tests/frontend/helper/**/*',
],
excludedFiles: [
'**/.eslintrc.*',
],
extends: 'etherpad/browser',
env: {
'shared-node-browser': true,
},
overrides: [
{
files: [
'tests/frontend/helper/**/*',
],
globals: {
helper: 'readonly',
},
},
],
},
{
files: [
'tests/**/*',
],
excludedFiles: [
'**/.eslintrc.*',
'tests/frontend/cypress/**/*',
'tests/frontend/helper.js',
'tests/frontend/helper/**/*',
'tests/frontend/travis/**/*',
'tests/ratelimit/**/*',
],
extends: 'etherpad/tests',
rules: {
'mocha/no-exports': 'off',
'mocha/no-top-level-hooks': 'off',
},
},
],
},
{
files: [
'tests/frontend/cypress/**/*',
],
extends: 'etherpad/tests/cypress',
files: [
'tests/backend/**/*',
],
excludedFiles: [
'**/.eslintrc.*',
],
extends: 'etherpad/tests/backend',
overrides: [
{
files: [
'tests/backend/**/*',
],
excludedFiles: [
'tests/backend/specs/**/*',
],
rules: {
'mocha/no-exports': 'off',
'mocha/no-top-level-hooks': 'off',
},
},
],
},
{
files: [
'tests/frontend/travis/**/*',
],
extends: 'etherpad/node',
files: [
'tests/frontend/**/*',
],
excludedFiles: [
'**/.eslintrc.*',
'tests/frontend/cypress/**/*',
'tests/frontend/helper.js',
'tests/frontend/helper/**/*',
'tests/frontend/travis/**/*',
],
extends: 'etherpad/tests/frontend',
overrides: [
{
files: [
'tests/frontend/**/*',
],
excludedFiles: [
'tests/frontend/specs/**/*',
],
rules: {
'mocha/no-exports': 'off',
'mocha/no-top-level-hooks': 'off',
},
},
],
},
],
root: true,
{
files: [
'tests/frontend/cypress/**/*',
],
extends: 'etherpad/tests/cypress',
},
{
files: [
'tests/frontend/travis/**/*',
],
extends: 'etherpad/node',
},
];
export const root = true;
export default {
ignorePatterns,
overrides,
root
};

2
src/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
dist/
node_modules

0
src/bin/buildDebian.sh Executable file → Normal file
View File

0
src/bin/buildForWindows.sh Executable file → Normal file
View File

0
src/bin/cleanRun.sh Executable file → Normal file
View File

0
src/bin/createRelease.sh Executable file → Normal file
View File

0
src/bin/deb-src/DEBIAN/postinst Executable file → Normal file
View File

0
src/bin/deb-src/DEBIAN/preinst Executable file → Normal file
View File

0
src/bin/deb-src/DEBIAN/prerm Executable file → Normal file
View File

0
src/bin/debugRun.sh Executable file → Normal file
View File

0
src/bin/etherpad-healthcheck Executable file → Normal file
View File

0
src/bin/fastRun.sh Executable file → Normal file
View File

0
src/bin/installDeps.sh Executable file → Normal file
View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 0 B

0
src/bin/plugins/README.md Executable file → Normal file
View File

0
src/bin/plugins/checkPlugin.js Executable file → Normal file
View File

0
src/bin/plugins/getCorePlugins.sh Executable file → Normal file
View File

0
src/bin/plugins/lib/README.md Executable file → Normal file
View File

0
src/bin/plugins/lib/gitignore Executable file → Normal file
View File

0
src/bin/plugins/listOfficialPlugins Executable file → Normal file
View File

0
src/bin/plugins/reTestAllPlugins.sh Executable file → Normal file
View File

0
src/bin/plugins/updateAllPluginsScript.sh Executable file → Normal file
View File

0
src/bin/plugins/updateCorePlugins.sh Executable file → Normal file
View File

2
src/bin/run.sh Executable file → Normal file
View File

@ -32,4 +32,4 @@ src/bin/installDeps.sh "$@" || exit 1
# Move to the node folder and start
log "Starting Etherpad..."
exec node src/node/server.js "$@"
exec node src/dist/node/server.js "$@"

0
src/bin/safeRun.sh Executable file → Normal file
View File

0
src/bin/updatePlugins.sh Executable file → Normal file
View File

View File

@ -19,59 +19,49 @@
* limitations under the License.
*/
const Changeset = require('../../static/js/Changeset');
const ChatMessage = require('../../static/js/ChatMessage');
const CustomError = require('../utils/customError');
const padManager = require('./PadManager');
const padMessageHandler = require('../handler/PadMessageHandler');
const readOnlyManager = require('./ReadOnlyManager');
const groupManager = require('./GroupManager');
const authorManager = require('./AuthorManager');
const sessionManager = require('./SessionManager');
const exportHtml = require('../utils/ExportHtml');
const exportTxt = require('../utils/ExportTxt');
const importHtml = require('../utils/ImportHtml');
import {builder, deserializeOps} from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage';
import {CustomError} from '../utils/customError';
import {doesPadExist, getPad, isValidPadId, listAllPads} from './PadManager';
import {
handleCustomMessage,
sendChatMessageToPadClients,
sessioninfos,
updatePadClients
} from '../handler/PadMessageHandler';
import {getPadId, getReadOnlyId} from './ReadOnlyManager';
import {
createGroup,
createGroupIfNotExistsFor,
createGroupPad,
deleteGroup,
listAllGroups,
listPads
} from './GroupManager';
import {createAuthor, createAuthorIfNotExistsFor, getAuthorName, listPadsOfAuthor} from './AuthorManager';
import {} from './SessionManager';
import {getTXTFromAtext} from '../utils/ExportTxt';
import {setPadHTML} from '../utils/ImportHtml';
const cleanText = require('./Pad').cleanText;
const PadDiff = require('../utils/padDiff');
import {PadDiff} from '../utils/padDiff';
import {getPadHTMLDocument} from "../utils/ExportHtml";
/* ********************
* GROUP FUNCTIONS ****
******************** */
exports.listAllGroups = groupManager.listAllGroups;
exports.createGroup = groupManager.createGroup;
exports.createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor;
exports.deleteGroup = groupManager.deleteGroup;
exports.listPads = groupManager.listPads;
exports.createGroupPad = groupManager.createGroupPad;
/* ********************
* PADLIST FUNCTION ***
******************** */
exports.listAllPads = padManager.listAllPads;
/* ********************
* AUTHOR FUNCTIONS ***
******************** */
exports.createAuthor = authorManager.createAuthor;
exports.createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor;
exports.getAuthorName = authorManager.getAuthorName;
exports.listPadsOfAuthor = authorManager.listPadsOfAuthor;
exports.padUsers = padMessageHandler.padUsers;
exports.padUsersCount = padMessageHandler.padUsersCount;
/* ********************
* SESSION FUNCTIONS **
******************** */
exports.createSession = sessionManager.createSession;
exports.deleteSession = sessionManager.deleteSession;
exports.getSessionInfo = sessionManager.getSessionInfo;
exports.listSessionsOfGroup = sessionManager.listSessionsOfGroup;
exports.listSessionsOfAuthor = sessionManager.listSessionsOfAuthor;
/* ***********************
* PAD CONTENT FUNCTIONS *
*********************** */
@ -103,7 +93,7 @@ Example returns:
}
*/
exports.getAttributePool = async (padID) => {
export const getAttributePool = async (padID: string) => {
const pad = await getPadSafe(padID, true);
return {pool: pad.pool};
};
@ -121,7 +111,7 @@ Example returns:
}
*/
exports.getRevisionChangeset = async (padID, rev) => {
export const getRevisionChangeset = async (padID, rev) => {
// try to parse the revision number
if (rev !== undefined) {
rev = checkValidRev(rev);
@ -154,7 +144,7 @@ Example returns:
{code: 0, message:"ok", data: {text:"Welcome Text"}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getText = async (padID, rev) => {
export const getText = async (padID, rev) => {
// try to parse the revision number
if (rev !== undefined) {
rev = checkValidRev(rev);
@ -179,7 +169,7 @@ exports.getText = async (padID, rev) => {
}
// the client wants the latest text, lets return it to him
const text = exportTxt.getTXTFromAtext(pad, pad.atext);
const text = getTXTFromAtext(pad, pad.atext);
return {text};
};
@ -192,7 +182,7 @@ Example returns:
{code: 1, message:"padID does not exist", data: null}
{code: 1, message:"text too long", data: null}
*/
exports.setText = async (padID, text, authorId = '') => {
export const setText = async (padID, text, authorId = '') => {
// text is required
if (typeof text !== 'string') {
throw new CustomError('text is not a string', 'apierror');
@ -202,7 +192,7 @@ exports.setText = async (padID, text, authorId = '') => {
const pad = await getPadSafe(padID, true);
await pad.setText(text, authorId);
await padMessageHandler.updatePadClients(pad);
await updatePadClients(pad);
};
/**
@ -214,7 +204,7 @@ Example returns:
{code: 1, message:"padID does not exist", data: null}
{code: 1, message:"text too long", data: null}
*/
exports.appendText = async (padID, text, authorId = '') => {
export const appendText = async (padID, text, authorId = '') => {
// text is required
if (typeof text !== 'string') {
throw new CustomError('text is not a string', 'apierror');
@ -222,7 +212,7 @@ exports.appendText = async (padID, text, authorId = '') => {
const pad = await getPadSafe(padID, true);
await pad.appendText(text, authorId);
await padMessageHandler.updatePadClients(pad);
await updatePadClients(pad);
};
/**
@ -233,7 +223,7 @@ Example returns:
{code: 0, message:"ok", data: {text:"Welcome <strong>Text</strong>"}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getHTML = async (padID, rev) => {
export const getHTML = async (padID, rev) => {
if (rev !== undefined) {
rev = checkValidRev(rev);
}
@ -250,7 +240,7 @@ exports.getHTML = async (padID, rev) => {
}
// get the html of this revision
let html = await exportHtml.getPadHTML(pad, rev);
let html = await getPadHTMLDocument(pad, rev);
// wrap the HTML
html = `<!DOCTYPE HTML><html><body>${html}</body></html>`;
@ -265,7 +255,7 @@ Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.setHTML = async (padID, html, authorId = '') => {
export const setHTML = async (padID, html, authorId = '') => {
// html string is required
if (typeof html !== 'string') {
throw new CustomError('html is not a string', 'apierror');
@ -276,13 +266,13 @@ exports.setHTML = async (padID, html, authorId = '') => {
// add a new changeset with the new html to the pad
try {
await importHtml.setPadHTML(pad, cleanText(html), authorId);
await setPadHTML(pad, cleanText(html), authorId);
} catch (e) {
throw new CustomError('HTML is malformed', 'apierror');
}
// update the clients on the pad
padMessageHandler.updatePadClients(pad);
updatePadClients(pad);
};
/* ****************
@ -303,7 +293,7 @@ Example returns:
{code: 1, message:"padID does not exist", data: null}
*/
exports.getChatHistory = async (padID, start, end) => {
export const getChatHistory = async (padID, start, end) => {
if (start && end) {
if (start < 0) {
throw new CustomError('start is below zero', 'apierror');
@ -349,7 +339,7 @@ Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.appendChatMessage = async (padID, text, authorID, time) => {
export const appendChatMessage = async (padID, text, authorID, time) => {
// text is required
if (typeof text !== 'string') {
throw new CustomError('text is not a string', 'apierror');
@ -363,7 +353,7 @@ exports.appendChatMessage = async (padID, text, authorID, time) => {
// @TODO - missing getPadSafe() call ?
// save chat message to database and send message to all connected clients
await padMessageHandler.sendChatMessageToPadClients(new ChatMessage(text, authorID, time), padID);
await sendChatMessageToPadClients(new ChatMessage(text, authorID, time), padID);
};
/* ***************
@ -378,7 +368,7 @@ Example returns:
{code: 0, message:"ok", data: {revisions: 56}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getRevisionsCount = async (padID) => {
export const getRevisionsCount = async (padID) => {
// get the pad
const pad = await getPadSafe(padID, true);
return {revisions: pad.getHeadRevisionNumber()};
@ -392,7 +382,7 @@ Example returns:
{code: 0, message:"ok", data: {savedRevisions: 42}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getSavedRevisionsCount = async (padID) => {
export const getSavedRevisionsCount = async (padID) => {
// get the pad
const pad = await getPadSafe(padID, true);
return {savedRevisions: pad.getSavedRevisionsNumber()};
@ -406,7 +396,7 @@ Example returns:
{code: 0, message:"ok", data: {savedRevisions: [2, 42, 1337]}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.listSavedRevisions = async (padID) => {
export const listSavedRevisions = async (padID) => {
// get the pad
const pad = await getPadSafe(padID, true);
return {savedRevisions: pad.getSavedRevisionsList()};
@ -420,7 +410,7 @@ Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.saveRevision = async (padID, rev) => {
export const saveRevision = async (padID, rev) => {
// check if rev is a number
if (rev !== undefined) {
rev = checkValidRev(rev);
@ -439,7 +429,7 @@ exports.saveRevision = async (padID, rev) => {
rev = pad.getHeadRevisionNumber();
}
const author = await authorManager.createAuthor('API');
const author = await createAuthor('API');
await pad.addSavedRevision(rev, author.authorID, 'Saved through API call');
};
@ -451,7 +441,7 @@ Example returns:
{code: 0, message:"ok", data: {lastEdited: 1340815946602}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getLastEdited = async (padID) => {
export const getLastEdited = async (padID) => {
// get the pad
const pad = await getPadSafe(padID, true);
const lastEdited = await pad.getLastEdit();
@ -466,7 +456,7 @@ Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"pad does already exist", data: null}
*/
exports.createPad = async (padID, text, authorId = '') => {
export const createPad = async (padID, text, authorId = '') => {
if (padID) {
// ensure there is no $ in the padID
if (padID.indexOf('$') !== -1) {
@ -491,7 +481,7 @@ Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.deletePad = async (padID) => {
export const deletePad = async (padID) => {
const pad = await getPadSafe(padID, true);
await pad.remove();
};
@ -504,7 +494,7 @@ exports.deletePad = async (padID) => {
{code:0, message:"ok", data:null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.restoreRevision = async (padID, rev, authorId = '') => {
export const restoreRevision = async (padID, rev, authorId = '') => {
// check if rev is a number
if (rev === undefined) {
throw new CustomError('rev is not defined', 'apierror');
@ -528,7 +518,7 @@ exports.restoreRevision = async (padID, rev, authorId = '') => {
let textIndex = 0;
const newTextStart = 0;
const newTextEnd = atext.text.length;
for (const op of Changeset.deserializeOps(attribs)) {
for (const op of deserializeOps(attribs)) {
const nextIndex = textIndex + op.chars;
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
@ -538,25 +528,25 @@ exports.restoreRevision = async (padID, rev, authorId = '') => {
};
// create a new changeset with a helper builder object
const builder = Changeset.builder(oldText.length);
const builder2 = builder(oldText.length);
// assemble each line into the builder
eachAttribRun(atext.attribs, (start, end, attribs) => {
builder.insert(atext.text.substring(start, end), attribs);
builder2.insert(atext.text.substring(start, end), attribs);
});
const lastNewlinePos = oldText.lastIndexOf('\n');
if (lastNewlinePos < 0) {
builder.remove(oldText.length - 1, 0);
builder2.remove(oldText.length - 1, 0);
} else {
builder.remove(lastNewlinePos, oldText.match(/\n/g).length - 1);
builder.remove(oldText.length - lastNewlinePos - 1, 0);
builder2.remove(lastNewlinePos, oldText.match(/\n/g).length - 1);
builder2.remove(oldText.length - lastNewlinePos - 1, 0);
}
const changeset = builder.toString();
await pad.appendRevision(changeset, authorId);
await padMessageHandler.updatePadClients(pad);
await updatePadClients(pad);
};
/**
@ -568,7 +558,7 @@ Example returns:
{code: 0, message:"ok", data: {padID: destinationID}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.copyPad = async (sourceID, destinationID, force) => {
export const copyPad = async (sourceID, destinationID, force) => {
const pad = await getPadSafe(sourceID, true);
await pad.copy(destinationID, force);
};
@ -582,7 +572,7 @@ Example returns:
{code: 0, message:"ok", data: {padID: destinationID}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.copyPadWithoutHistory = async (sourceID, destinationID, force, authorId = '') => {
export const copyPadWithoutHistory = async (sourceID, destinationID, force, authorId = '') => {
const pad = await getPadSafe(sourceID, true);
await pad.copyPadWithoutHistory(destinationID, force, authorId);
};
@ -596,7 +586,7 @@ Example returns:
{code: 0, message:"ok", data: {padID: destinationID}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.movePad = async (sourceID, destinationID, force) => {
export const movePad = async (sourceID, destinationID, force) => {
const pad = await getPadSafe(sourceID, true);
await pad.copy(destinationID, force);
await pad.remove();
@ -610,12 +600,12 @@ Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getReadOnlyID = async (padID) => {
export const getReadOnlyID = async (padID) => {
// we don't need the pad object, but this function does all the security stuff for us
await getPadSafe(padID, true);
// get the readonlyId
const readOnlyID = await readOnlyManager.getReadOnlyId(padID);
const readOnlyID = await getReadOnlyId(padID);
return {readOnlyID};
};
@ -628,9 +618,9 @@ Example returns:
{code: 0, message:"ok", data: {padID: padID}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getPadID = async (roID) => {
export const getPadID = async (roID) => {
// get the PadId
const padID = await readOnlyManager.getPadId(roID);
const padID = await getPadId(roID);
if (padID == null) {
throw new CustomError('padID does not exist', 'apierror');
}
@ -646,7 +636,7 @@ Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.setPublicStatus = async (padID, publicStatus) => {
export const setPublicStatus = async (padID, publicStatus) => {
// ensure this is a group pad
checkGroupPad(padID, 'publicStatus');
@ -669,7 +659,7 @@ Example returns:
{code: 0, message:"ok", data: {publicStatus: true}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getPublicStatus = async (padID) => {
export const getPublicStatus = async (padID) => {
// ensure this is a group pad
checkGroupPad(padID, 'publicStatus');
@ -686,7 +676,7 @@ Example returns:
{code: 0, message:"ok", data: {authorIDs : ["a.s8oes9dhwrvt0zif", "a.akf8finncvomlqva"]}
{code: 1, message:"padID does not exist", data: null}
*/
exports.listAuthorsOfPad = async (padID) => {
export const listAuthorsOfPad = async (padID) => {
// get the pad
const pad = await getPadSafe(padID, true);
const authorIDs = pad.getAllAuthors();
@ -716,9 +706,9 @@ Example returns:
{code: 1, message:"padID does not exist"}
*/
exports.sendClientsMessage = async (padID, msg) => {
export const sendClientsMessage = async (padID, msg) => {
await getPadSafe(padID, true); // Throw if the padID is invalid or if the pad does not exist.
padMessageHandler.handleCustomMessage(padID, msg);
handleCustomMessage(padID, msg);
};
/**
@ -729,7 +719,7 @@ Example returns:
{"code":0,"message":"ok","data":null}
{"code":4,"message":"no or wrong API Key","data":null}
*/
exports.checkToken = async () => {
export const checkToken = async () => {
};
/**
@ -740,7 +730,7 @@ Example returns:
{code: 0, message:"ok", data: {chatHead: 42}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getChatHead = async (padID) => {
export const getChatHead = async (padID) => {
// get the pad
const pad = await getPadSafe(padID, true);
return {chatHead: pad.chatHead};
@ -764,7 +754,7 @@ Example returns:
{"code":4,"message":"no or wrong API Key","data":null}
*/
exports.createDiffHTML = async (padID, startRev, endRev) => {
export const createDiffHTML = async (padID, startRev, endRev) => {
// check if startRev is a number
if (startRev !== undefined) {
startRev = checkValidRev(startRev);
@ -803,13 +793,13 @@ exports.createDiffHTML = async (padID, startRev, endRev) => {
{"code":4,"message":"no or wrong API Key","data":null}
*/
exports.getStats = async () => {
const sessionInfos = padMessageHandler.sessioninfos;
export const getStats = async () => {
const sessionInfos = sessioninfos;
const sessionKeys = Object.keys(sessionInfos);
const activePads = new Set(Object.entries(sessionInfos).map((k) => k[1].padId));
const {padIDs} = await padManager.listAllPads();
const {padIDs} = await listAllPads();
return {
totalPads: padIDs.length,
@ -826,19 +816,19 @@ exports.getStats = async () => {
const isInt = (value) => (parseFloat(value) === parseInt(value, 10)) && !isNaN(value);
// gets a pad safe
const getPadSafe = async (padID, shouldExist, text, authorId = '') => {
const getPadSafe = async (padID, shouldExist, text?, authorId = '') => {
// check if padID is a string
if (typeof padID !== 'string') {
throw new CustomError('padID is not a string', 'apierror');
}
// check if the padID maches the requirements
if (!padManager.isValidPadId(padID)) {
if (!isValidPadId(padID)) {
throw new CustomError('padID did not match requirements', 'apierror');
}
// check if the pad exists
const exists = await padManager.doesPadExists(padID);
const exists = await doesPadExist(padID);
if (!exists && shouldExist) {
// does not exist, but should
@ -851,7 +841,7 @@ const getPadSafe = async (padID, shouldExist, text, authorId = '') => {
}
// pad exists, let's get it
return padManager.getPad(padID, text, authorId);
return getPad(padID, text, authorId);
};
// checks if a rev is a legal number

View File

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

View File

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

View File

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

View File

@ -3,46 +3,63 @@
* The pad object, defined with joose
*/
const AttributeMap = require('../../static/js/AttributeMap');
const Changeset = require('../../static/js/Changeset');
const ChatMessage = require('../../static/js/ChatMessage');
const AttributePool = require('../../static/js/AttributePool');
const Stream = require('../utils/Stream');
const assert = require('assert').strict;
const db = require('./DB');
const settings = require('../utils/Settings');
const authorManager = require('./AuthorManager');
const padManager = require('./PadManager');
const padMessageHandler = require('../handler/PadMessageHandler');
const groupManager = require('./GroupManager');
const CustomError = require('../utils/customError');
const readOnlyManager = require('./ReadOnlyManager');
const randomString = require('../utils/randomstring');
const hooks = require('../../static/js/pluginfw/hooks');
const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
const promises = require('../utils/promises');
import AttributeMap from '../../static/js/AttributeMap';
import {
applyToAText, checkRep,
copyAText, deserializeOps,
makeAText,
makeSplice,
opsFromAText,
pack,
smartOpAssembler, unpack
} from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage';
import {AttributePool} from '../../static/js/AttributePool';
import {Stream} from '../utils/Stream';
import assert, {strict} from 'assert'
import {db} from './DB';
import {defaultPadText} from '../utils/Settings';
import {addPad, getAuthorColorId, getAuthorName, getColorPalette, removePad} from './AuthorManager';
import {Revision} from "../models/Revision";
import {doesPadExist, getPad} from './PadManager';
import {kickSessionsFromPad} from '../handler/PadMessageHandler';
import {doesGroupExist} from './GroupManager';
import {CustomError} from '../utils/customError';
import {getReadOnlyId} from './ReadOnlyManager';
import {randomString} from '../utils/randomstring';
import {aCallAll} from '../../static/js/pluginfw/hooks';
import {timesLimit} from '../utils/promises';
import {padutils} from '../../static/js/pad_utils';
/**
* Copied from the Etherpad source code. It converts Windows line breaks to Unix
* line breaks and convert Tabs to spaces
* @param txt
*/
exports.cleanText = (txt) => txt.replace(/\r\n/g, '\n')
export const cleanText = (txt) => txt.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.replace(/\t/g, ' ')
.replace(/\xa0/g, ' ');
class Pad {
export class Pad {
private db: any;
private atext: any;
private pool: AttributePool;
private head: number;
private chatHead: number;
private publicStatus: boolean;
private id: string;
private savedRevisions: Revision[];
/**
* @param id the id of this pad
* @param [database] - Database object to access this pad's records (and only this pad's records;
* the shared global Etherpad database object is still used for all other pad accesses, such
* as copying the pad). Defaults to the shared global Etherpad database object. This parameter
* can be used to shard pad storage across multiple database backends, to put each pad in its
* own database table, or to validate imported pad data before it is written to the database.
*/
constructor(id, database = db) {
constructor(id: string, database = db) {
this.db = database;
this.atext = Changeset.makeAText('\n');
this.atext = makeAText('\n');
this.pool = new AttributePool();
this.head = -1;
this.chatHead = -1;
@ -74,11 +91,11 @@ class Pad {
}
async appendRevision(aChangeset, authorId = '') {
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
const newAText = applyToAText(aChangeset, this.atext, this.pool);
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs) {
return this.head;
}
Changeset.copyAText(newAText, this.atext);
copyAText(newAText, this.atext);
const newRev = ++this.head;
@ -99,16 +116,16 @@ class Pad {
},
}),
this.saveToDatabase(),
authorId && authorManager.addPad(authorId, this.id),
hooks.aCallAll(hook, {
authorId && addPad(authorId, this.id),
aCallAll(hook, {
pad: this,
authorId,
get author() {
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
padutils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
return this.authorId;
},
set author(authorId) {
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
padutils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
this.authorId = authorId;
},
...this.head === 0 ? {} : {
@ -121,7 +138,7 @@ class Pad {
}
toJSON() {
const o = {...this, pool: this.pool.toJsonable()};
const o:{db: any, id: any} = {...this, pool: this.pool.toJsonable()}
delete o.db;
delete o.id;
return o;
@ -179,7 +196,7 @@ class Pad {
]);
const apool = this.apool();
let atext = keyAText;
for (const cs of changesets) atext = Changeset.applyToAText(cs, atext, apool);
for (const cs of changesets) atext = applyToAText(cs, atext, apool);
return atext;
}
@ -190,10 +207,10 @@ class Pad {
async getAllAuthorColors() {
const authorIds = this.getAllAuthors();
const returnTable = {};
const colorPalette = authorManager.getColorPalette();
const colorPalette = getColorPalette();
await Promise.all(
authorIds.map((authorId) => authorManager.getAuthorColorId(authorId).then((colorId) => {
authorIds.map((authorId) => getAuthorColorId(authorId).then((colorId) => {
// colorId might be a hex color or an number out of the palette
returnTable[authorId] = colorPalette[colorId] || colorId;
})));
@ -250,14 +267,14 @@ class Pad {
const orig = this.text();
assert(orig.endsWith('\n'));
if (start + ndel > orig.length) throw new RangeError('start/delete past the end of the text');
ins = exports.cleanText(ins);
ins = cleanText(ins);
const willEndWithNewline =
start + ndel < orig.length || // Keeping last char (which is guaranteed to be a newline).
ins.endsWith('\n') ||
(!ins && start > 0 && orig[start - 1] === '\n');
if (!willEndWithNewline) ins += '\n';
if (ndel === 0 && ins.length === 0) return;
const changeset = Changeset.makeSplice(orig, start, ndel, ins);
const changeset = makeSplice(orig, start, ndel, ins);
await this.appendRevision(changeset, authorId);
}
@ -315,7 +332,7 @@ class Pad {
const entry = await this.db.get(`pad:${this.id}:chat:${entryNum}`);
if (entry == null) return null;
const message = ChatMessage.fromObject(entry);
message.displayName = await authorManager.getAuthorName(message.authorId);
message.displayName = await getAuthorName(message.authorId);
return message;
}
@ -352,15 +369,15 @@ class Pad {
if ('pool' in value) this.pool = new AttributePool().fromJsonable(value.pool);
} else {
if (text == null) {
const context = {pad: this, authorId, type: 'text', content: settings.defaultPadText};
await hooks.aCallAll('padDefaultContent', context);
const context = {pad: this, authorId, type: 'text', content: defaultPadText};
await aCallAll('padDefaultContent', context);
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
text = exports.cleanText(context.content);
text = cleanText(context.content);
}
const firstChangeset = Changeset.makeSplice('\n', 0, 0, text);
const firstChangeset = makeSplice('\n', 0, 0, text);
await this.appendRevision(firstChangeset, authorId);
}
await hooks.aCallAll('padLoad', {pad: this});
await aCallAll('padLoad', {pad: this});
}
async copy(destinationID, force) {
@ -393,16 +410,16 @@ class Pad {
for (const p of new Stream(promises).batch(100).buffer(99)) await p;
// Initialize the new pad (will update the listAllPads cache)
const dstPad = await padManager.getPad(destinationID, null);
const dstPad = await getPad(destinationID, null);
// let the plugins know the pad was copied
await hooks.aCallAll('padCopy', {
await aCallAll('padCopy', {
get originalPad() {
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
padutils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
return this.srcPad;
},
get destinationID() {
warnDeprecated(
padutils.warnDeprecated(
'padCopy destinationID context property is deprecated; use dstPad.id instead');
return this.dstPad.id;
},
@ -418,7 +435,7 @@ class Pad {
if (destinationID.indexOf('$') >= 0) {
destGroupID = destinationID.split('$')[0];
const groupExists = await groupManager.doesGroupExist(destGroupID);
const groupExists = await doesGroupExist(destGroupID);
// group does not exist
if (!groupExists) {
@ -430,7 +447,7 @@ class Pad {
async removePadIfForceIsTrueAndAlreadyExist(destinationID, force) {
// if the pad exists, we should abort, unless forced.
const exists = await padManager.doesPadExist(destinationID);
const exists = await doesPadExist(destinationID);
// allow force to be a string
if (typeof force === 'string') {
@ -446,7 +463,7 @@ class Pad {
}
// exists and forcing
const pad = await padManager.getPad(destinationID);
const pad = await getPad(destinationID);
await pad.remove();
}
}
@ -454,7 +471,7 @@ class Pad {
async copyAuthorInfoToDestinationPad(destinationID) {
// add the new sourcePad to all authors who contributed to the old one
await Promise.all(this.getAllAuthors().map(
(authorID) => authorManager.addPad(authorID, destinationID)));
(authorID) => addPad(authorID, destinationID)));
}
async copyPadWithoutHistory(destinationID, force, authorId = '') {
@ -475,14 +492,14 @@ class Pad {
}
// initialize the pad with a new line to avoid getting the defaultText
const dstPad = await padManager.getPad(destinationID, '\n', authorId);
const dstPad = await getPad(destinationID, '\n', authorId);
dstPad.pool = this.pool.clone();
const oldAText = this.atext;
// based on Changeset.makeSplice
const assem = Changeset.smartOpAssembler();
for (const op of Changeset.opsFromAText(oldAText)) assem.append(op);
const assem = smartOpAssembler();
for (const op of opsFromAText(oldAText)) assem.append(op);
assem.endDocument();
// although we have instantiated the dstPad with '\n', an additional '\n' is
@ -494,16 +511,16 @@ class Pad {
// create a changeset that removes the previous text and add the newText with
// all atributes present on the source pad
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
const changeset = pack(oldLength, newLength, assem.toString(), newText);
dstPad.appendRevision(changeset, authorId);
await hooks.aCallAll('padCopy', {
await aCallAll('padCopy', {
get originalPad() {
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
padutils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
return this.srcPad;
},
get destinationID() {
warnDeprecated(
padutils.warnDeprecated(
'padCopy destinationID context property is deprecated; use dstPad.id instead');
return this.dstPad.id;
},
@ -519,7 +536,7 @@ class Pad {
const p = [];
// kick everyone from this pad
padMessageHandler.kickSessionsFromPad(padID);
kickSessionsFromPad(padID);
// delete all relations - the original code used async.parallel but
// none of the operations except getting the group depended on callbacks
@ -540,31 +557,31 @@ class Pad {
}
// remove the readonly entries
p.push(readOnlyManager.getReadOnlyId(padID).then(async (readonlyID) => {
p.push(getReadOnlyId(padID).then(async (readonlyID) => {
await db.remove(`readonly2pad:${readonlyID}`);
}));
p.push(db.remove(`pad2readonly:${padID}`));
// delete all chat messages
p.push(promises.timesLimit(this.chatHead + 1, 500, async (i) => {
p.push(timesLimit(this.chatHead + 1, 500, async (i) => {
await this.db.remove(`pad:${this.id}:chat:${i}`, null);
}));
// delete all revisions
p.push(promises.timesLimit(this.head + 1, 500, async (i) => {
p.push(timesLimit(this.head + 1, 500, async (i) => {
await this.db.remove(`pad:${this.id}:revs:${i}`, null);
}));
// remove pad from all authors who contributed
this.getAllAuthors().forEach((authorId) => {
p.push(authorManager.removePad(authorId, padID));
p.push(removePad(authorId, padID));
});
// delete the pad entry and delete pad from padManager
p.push(padManager.removePad(padID));
p.push(hooks.aCallAll('padRemove', {
p.push(removePad(padID));
p.push(aCallAll('padRemove', {
get padID() {
warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
padutils.warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
return this.pad.id;
},
pad: this,
@ -587,12 +604,13 @@ class Pad {
}
// build the saved revision object
const savedRevision = {};
savedRevision.revNum = revNum;
savedRevision.savedById = savedById;
savedRevision.label = label || `Revision ${revNum}`;
savedRevision.timestamp = Date.now();
savedRevision.id = randomString(10);
const savedRevision:Revision = {
label: label || `Revision ${revNum}`,
revNum: revNum,
savedById: savedById,
timestamp: Date.now(),
id: randomString(10)
}
// save this new saved revision
this.savedRevisions.push(savedRevision);
@ -667,7 +685,7 @@ class Pad {
}
})
.batch(100).buffer(99);
let atext = Changeset.makeAText('\n');
let atext = makeAText('\n');
for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) {
try {
assert(authorId != null);
@ -678,10 +696,10 @@ class Pad {
assert(timestamp > 0);
assert(changeset != null);
assert.equal(typeof changeset, 'string');
Changeset.checkRep(changeset);
const unpacked = Changeset.unpack(changeset);
checkRep(changeset);
const unpacked = unpack(changeset);
let text = atext.text;
for (const op of Changeset.deserializeOps(unpacked.ops)) {
for (const op of deserializeOps(unpacked.ops)) {
if (['=', '-'].includes(op.opcode)) {
assert(text.length >= op.chars);
const consumed = text.slice(0, op.chars);
@ -692,7 +710,7 @@ class Pad {
}
assert.equal(op.attribs, AttributeMap.fromString(op.attribs, pool).toString());
}
atext = Changeset.applyToAText(changeset, atext, pool);
atext = applyToAText(changeset, atext, pool);
if (isKeyRev) assert.deepEqual(keyAText, atext);
} catch (err) {
err.message = `(pad ${this.id} revision ${r}) ${err.message}`;
@ -720,7 +738,6 @@ class Pad {
.batch(100).buffer(99);
for (const p of chats) await p;
await hooks.aCallAll('padCheck', {pad: this});
await aCallAll('padCheck', {pad: this});
}
}
exports.Pad = Pad;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,34 +19,72 @@
* limitations under the License.
*/
const AttributeMap = require('../../static/js/AttributeMap');
const padManager = require('../db/PadManager');
const Changeset = require('../../static/js/Changeset');
const ChatMessage = require('../../static/js/ChatMessage');
const AttributePool = require('../../static/js/AttributePool');
const AttributeManager = require('../../static/js/AttributeManager');
const authorManager = require('../db/AuthorManager');
const {padutils} = require('../../static/js/pad_utils');
const readOnlyManager = require('../db/ReadOnlyManager');
const settings = require('../utils/Settings');
import AttributeMap from '../../static/js/AttributeMap';
import {getPad} from '../db/PadManager';
import {
builder,
checkRep, cloneAText, compose,
deserializeOps,
follow, inverse, makeAText,
makeSplice,
moveOpsToNewPool, mutateAttributionLines, mutateTextLines,
oldLen, prepareForWire, splitAttributionLines, splitTextLines,
unpack
} from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage';
import {AttributePool} from '../../static/js/AttributePool';
import AttributeManager from '../../static/js/AttributeManager';
import {
getAuthor,
getAuthorColorId,
getAuthorName,
getColorPalette,
setAuthorColorId,
setAuthorName
} from '../db/AuthorManager';
import {padutils} from '../../static/js/pad_utils';
import {getIds} from '../db/ReadOnlyManager';
import {
abiwordAvailable,
automaticReconnectionTimeout,
commitRateLimiting,
cookie,
disableIPlogging,
exportAvailable,
indentationOnNewLine,
padOptions,
padShortcutEnabled,
scrollWhenFocusLineIsOutOfViewport,
skinName,
skinVariants,
sofficeAvailable
} from '../utils/Settings';
import {parts, plugins} from '../../static/js/pluginfw/plugin_defs.js';
import log4js from "log4js";
import {aCallAll, deprecationNotices} from '../../static/js/pluginfw/hooks.js';
import {createCollection} from '../stats';
import {strict as assert} from "assert";
import {RateLimiterMemory} from 'rate-limiter-flexible';
import {userCanModify} from '../hooks/express/webaccess';
import {ErrorCaused} from "../models/ErrorCaused";
import {Pad} from "../db/Pad";
import {SessionInfo} from "../models/SessionInfo";
import {randomString} from "../utils/randomstring";
import {identity} from "lodash";
const securityManager = require('../db/SecurityManager');
const plugins = require('../../static/js/pluginfw/plugin_defs.js');
const log4js = require('log4js');
const messageLogger = log4js.getLogger('message');
const accessLogger = log4js.getLogger('access');
const hooks = require('../../static/js/pluginfw/hooks.js');
const stats = require('../stats');
const assert = require('assert').strict;
const {RateLimiterMemory} = require('rate-limiter-flexible');
const webaccess = require('../hooks/express/webaccess');
let rateLimiter;
let socketio = null;
hooks.deprecationNotices.clientReady = 'use the userJoin hook instead';
deprecationNotices.clientReady = 'use the userJoin hook instead';
const addContextToError = (err, pfx) => {
const newErr = new Error(`${pfx}${err.message}`, {cause: err});
const addContextToError = (err: Error, pfx) => {
const newErr = new ErrorCaused(`${pfx}${err.message}`, err);
if (Error.captureStackTrace) Error.captureStackTrace(newErr, addContextToError);
// Check for https://github.com/tc39/proposal-error-cause support, available in Node.js >= v16.10.
if (newErr.cause === err) return newErr;
@ -54,11 +92,11 @@ const addContextToError = (err, pfx) => {
return err;
};
exports.socketio = () => {
export const socketiofn = () => {
// The rate limiter is created in this hook so that restarting the server resets the limiter. The
// settings.commitRateLimiting object is passed directly to the rate limiter so that the limits
// can be dynamically changed during runtime by modifying its properties.
rateLimiter = new RateLimiterMemory(settings.commitRateLimiting);
rateLimiter = new RateLimiterMemory(commitRateLimiting);
};
/**
@ -79,11 +117,10 @@ exports.socketio = () => {
* - readonly: Whether the client has read-only access (true) or read/write access (false).
* - rev: The last revision that was sent to the client.
*/
const sessioninfos = {};
exports.sessioninfos = sessioninfos;
export const sessioninfos: SessionInfo = {};
stats.gauge('totalUsers', () => socketio ? Object.keys(socketio.sockets.sockets).length : 0);
stats.gauge('activePads', () => {
createCollection.gauge('totalUsers', () => socketio ? Object.keys(socketio.sockets.sockets).length : 0);
createCollection.gauge('activePads', () => {
const padIds = new Set();
for (const {padId} of Object.values(sessioninfos)) {
if (!padId) continue;
@ -96,6 +133,8 @@ stats.gauge('activePads', () => {
* Processes one task at a time per channel.
*/
class Channels {
private readonly _exec: (ch, task) => any;
private _promiseChains: Map<any, any>;
/**
* @param {(ch, task) => any} [exec] - Task executor. If omitted, tasks are assumed to be
* functions that will be executed with the channel as the only argument.
@ -135,7 +174,7 @@ const padChannels = new Channels((ch, {socket, message}) => handleUserChanges(so
* This Method is called by server.js to tell the message handler on which socket it should send
* @param socket_io The Socket
*/
exports.setSocketIO = (socket_io) => {
export const setSocketIO = (socket_io) => {
socketio = socket_io;
};
@ -143,17 +182,21 @@ exports.setSocketIO = (socket_io) => {
* Handles the connection of a new user
* @param socket the socket.io Socket object for the new connection from the client
*/
exports.handleConnect = (socket) => {
stats.meter('connects').mark();
export const handleConnect = (socket) => {
createCollection.meter('connects').mark();
// Initialize sessioninfos for this new session
sessioninfos[socket.id] = {};
sessioninfos[socket.id] = {
rev: 0, time: undefined,
auth: {padID: undefined, sessionID: undefined, token: undefined},
readOnlyPadId: undefined,
padId:undefined,readonly:false,author:undefined};
};
/**
* Kicks all sessions from a pad
*/
exports.kickSessionsFromPad = (padID) => {
export const kickSessionsFromPad = (padID) => {
if (typeof socketio.sockets.clients !== 'function') return;
// skip if there is nobody on this pad
@ -167,18 +210,18 @@ exports.kickSessionsFromPad = (padID) => {
* Handles the disconnection of a user
* @param socket the socket.io Socket object for the client
*/
exports.handleDisconnect = async (socket) => {
stats.meter('disconnects').mark();
export const handleDisconnect = async (socket) => {
createCollection.meter('disconnects').mark();
const session = sessioninfos[socket.id];
delete sessioninfos[socket.id];
// session.padId can be nullish if the user disconnects before sending CLIENT_READY.
if (!session || !session.author || !session.padId) return;
const {session: {user} = {}} = socket.client.request;
const {session: {user} = {}}: SessionSocketModel = socket.client.request;
/* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */
accessLogger.info('[LEAVE]' +
` pad:${session.padId}` +
` socket:${socket.id}` +
` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` +
` IP:${disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` +
` authorID:${session.author}` +
(user && user.username ? ` username:${user.username}` : ''));
/* eslint-enable prefer-template */
@ -187,12 +230,12 @@ exports.handleDisconnect = async (socket) => {
data: {
type: 'USER_LEAVE',
userInfo: {
colorId: await authorManager.getAuthorColorId(session.author),
colorId: await getAuthorColorId(session.author),
userId: session.author,
},
},
});
await hooks.aCallAll('userLeave', {
await aCallAll('userLeave', {
...session, // For backwards compatibility.
authorId: session.author,
readOnly: session.readonly,
@ -205,7 +248,7 @@ exports.handleDisconnect = async (socket) => {
* @param socket the socket.io Socket object for the client
* @param message the message from the client
*/
exports.handleMessage = async (socket, message) => {
export const handleMessage = async (socket, message) => {
const env = process.env.NODE_ENV || 'development';
if (env === 'production') {
@ -214,7 +257,7 @@ exports.handleMessage = async (socket, message) => {
} catch (err) {
messageLogger.warn(`Rate limited IP ${socket.request.ip}. To reduce the amount of rate ` +
'limiting that happens edit the rateLimit values in settings.json');
stats.meter('rateLimited').mark();
createCollection.meter('rateLimited').mark();
socket.json.send({disconnect: 'rateLimited'});
throw err;
}
@ -235,11 +278,11 @@ exports.handleMessage = async (socket, message) => {
padID: message.padId,
token: message.token,
};
const padIds = await readOnlyManager.getIds(thisSession.auth.padID);
const padIds = await getIds(thisSession.auth.padID);
thisSession.padId = padIds.padId;
thisSession.readOnlyPadId = padIds.readOnlyPadId;
thisSession.readonly =
padIds.readonly || !webaccess.userCanModify(thisSession.auth.padID, socket.client.request);
padIds.readonly || !userCanModify(thisSession.auth.padID, socket.client.request);
}
// Outside of the checks done by this function, message.padId must not be accessed because it is
// too easy to introduce a security vulnerability that allows malicious users to read or modify
@ -252,12 +295,12 @@ exports.handleMessage = async (socket, message) => {
const auth = thisSession.auth;
if (!auth) {
const ip = settings.disableIPlogging ? 'ANONYMOUS' : (socket.request.ip || '<unknown>');
const ip = disableIPlogging ? 'ANONYMOUS' : (socket.request.ip || '<unknown>');
const msg = JSON.stringify(message, null, 2);
throw new Error(`pre-CLIENT_READY message from IP ${ip}: ${msg}`);
}
const {session: {user} = {}} = socket.client.request;
const {session: {user} = {}}:SessionSocketModel = socket.client.request;
const {accessStatus, authorID} =
await securityManager.checkAccess(auth.padID, auth.sessionID, auth.token, user);
if (accessStatus !== 'grant') {
@ -269,7 +312,7 @@ exports.handleMessage = async (socket, message) => {
throw new Error([
'Author ID changed mid-session. Bad or missing token or sessionID?',
`socket:${socket.id}`,
`IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}`,
`IP:${disableIPlogging ? 'ANONYMOUS' : socket.request.ip}`,
`originalAuthorID:${thisSession.author}`,
`newAuthorID:${authorID}`,
...(user && user.username) ? [`username:${user.username}`] : [],
@ -295,7 +338,7 @@ exports.handleMessage = async (socket, message) => {
return this.socket;
},
};
for (const res of await hooks.aCallAll('handleMessageSecurity', context)) {
for (const res of await aCallAll('handleMessageSecurity', context)) {
switch (res) {
case true:
padutils.warnDeprecated(
@ -313,7 +356,7 @@ exports.handleMessage = async (socket, message) => {
}
// Call handleMessage hook. If a plugin returns null, the message will be dropped.
if ((await hooks.aCallAll('handleMessage', context)).some((m) => m == null)) {
if ((await aCallAll('handleMessage', context)).some((m) => m == null)) {
return;
}
@ -331,7 +374,7 @@ exports.handleMessage = async (socket, message) => {
try {
switch (type) {
case 'USER_CHANGES':
stats.counter('pendingEdits').inc();
createCollection.counter('pendingEdits').inc();
await padChannels.enqueue(thisSession.padId, {socket, message});
break;
case 'USERINFO_UPDATE': await handleUserInfoUpdate(socket, message); break;
@ -372,7 +415,7 @@ exports.handleMessage = async (socket, message) => {
*/
const handleSaveRevisionMessage = async (socket, message) => {
const {padId, author: authorId} = sessioninfos[socket.id];
const pad = await padManager.getPad(padId, null, authorId);
const pad = await getPad(padId, null, authorId);
await pad.addSavedRevision(pad.head, authorId);
};
@ -383,7 +426,7 @@ const handleSaveRevisionMessage = async (socket, message) => {
* @param msg {Object} the message we're sending
* @param sessionID {string} the socketIO session to which we're sending this message
*/
exports.handleCustomObjectMessage = (msg, sessionID) => {
export const handleCustomObjectMessage = (msg, sessionID) => {
if (msg.data.type === 'CUSTOM') {
if (sessionID) {
// a sessionID is targeted: directly to this sessionID
@ -401,7 +444,7 @@ exports.handleCustomObjectMessage = (msg, sessionID) => {
* @param padID {Pad} the pad to which we're sending this message
* @param msgString {String} the message we're sending
*/
exports.handleCustomMessage = (padID, msgString) => {
export const handleCustomMessage = (padID, msgString) => {
const time = Date.now();
const msg = {
type: 'COLLABROOM',
@ -424,7 +467,7 @@ const handleChatMessage = async (socket, message) => {
// Don't trust the user-supplied values.
chatMessage.time = Date.now();
chatMessage.authorId = authorId;
await exports.sendChatMessageToPadClients(chatMessage, padId);
await sendChatMessageToPadClients(chatMessage, padId);
};
/**
@ -438,15 +481,15 @@ const handleChatMessage = async (socket, message) => {
* @param {string} [padId] - The destination pad ID. Deprecated; pass a chat message
* object as the first argument and the destination pad ID as the second argument instead.
*/
exports.sendChatMessageToPadClients = async (mt, puId, text = null, padId = null) => {
export const sendChatMessageToPadClients = async (mt, puId, text = null, padId = null) => {
const message = mt instanceof ChatMessage ? mt : new ChatMessage(text, puId, mt);
padId = mt instanceof ChatMessage ? puId : padId;
const pad = await padManager.getPad(padId, null, message.authorId);
await hooks.aCallAll('chatNewMessage', {message, pad, padId});
const pad = await getPad(padId, null, message.authorId);
await aCallAll('chatNewMessage', {message, pad, padId});
// pad.appendChatMessage() ignores the displayName property so we don't need to wait for
// authorManager.getAuthorName() to resolve before saving the message to the database.
const promise = pad.appendChatMessage(message);
message.displayName = await authorManager.getAuthorName(message.authorId);
message.displayName = await getAuthorName(message.authorId);
socketio.sockets.in(padId).json.send({
type: 'COLLABROOM',
data: {type: 'CHAT_MESSAGE', message},
@ -465,7 +508,7 @@ const handleGetChatMessages = async (socket, {data: {start, end}}) => {
const count = end - start;
if (count < 0 || count > 100) throw new Error(`invalid number of messages: ${count}`);
const {padId, author: authorId} = sessioninfos[socket.id];
const pad = await padManager.getPad(padId, null, authorId);
const pad = await getPad(padId, null, authorId);
const chatMessages = await pad.getChatMessages(start, end);
const infoMsg = {
@ -517,8 +560,8 @@ const handleUserInfoUpdate = async (socket, {data: {userInfo: {name, colorId}}})
// Tell the authorManager about the new attributes
const p = Promise.all([
authorManager.setAuthorColorId(author, colorId),
authorManager.setAuthorName(author, name),
setAuthorColorId(author, colorId),
setAuthorName(author, name),
]);
const padId = session.padId;
@ -555,7 +598,7 @@ const handleUserInfoUpdate = async (socket, {data: {userInfo: {name, colorId}}})
*/
const handleUserChanges = async (socket, message) => {
// This one's no longer pending, as we're gonna process it now
stats.counter('pendingEdits').dec();
createCollection.counter('pendingEdits').dec();
// The client might disconnect between our callbacks. We should still
// finish processing the changeset, so keep a reference to the session.
@ -567,20 +610,20 @@ const handleUserChanges = async (socket, message) => {
if (!thisSession) throw new Error('client disconnected');
// Measure time to process edit
const stopWatch = stats.timer('edits').start();
const stopWatch = createCollection.timer('edits').start();
try {
const {data: {baseRev, apool, changeset}} = message;
if (baseRev == null) throw new Error('missing baseRev');
if (apool == null) throw new Error('missing apool');
if (changeset == null) throw new Error('missing changeset');
const wireApool = (new AttributePool()).fromJsonable(apool);
const pad = await padManager.getPad(thisSession.padId, null, thisSession.author);
const pad = await getPad(thisSession.padId, null, thisSession.author);
// Verify that the changeset has valid syntax and is in canonical form
Changeset.checkRep(changeset);
checkRep(changeset);
// Validate all added 'author' attribs to be the same value as the current user
for (const op of Changeset.deserializeOps(Changeset.unpack(changeset).ops)) {
for (const op of deserializeOps(unpack(changeset).ops)) {
// + can add text with attribs
// = can change or add attribs
// - can have attribs, but they are discarded and don't show up in the attribs -
@ -599,7 +642,7 @@ const handleUserChanges = async (socket, message) => {
// ex. adoptChangesetAttribs
// Afaik, it copies the new attributes from the changeset, to the global Attribute Pool
let rebasedChangeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool);
let rebasedChangeset = moveOpsToNewPool(changeset, wireApool, pad.pool);
// ex. applyUserChanges
let r = baseRev;
@ -612,21 +655,21 @@ const handleUserChanges = async (socket, message) => {
const {changeset: c, meta: {author: authorId}} = await pad.getRevision(r);
if (changeset === c && thisSession.author === authorId) {
// Assume this is a retransmission of an already applied changeset.
rebasedChangeset = Changeset.identity(Changeset.unpack(changeset).oldLen);
rebasedChangeset = identity(unpack(changeset).oldLen);
}
// At this point, both "c" (from the pad) and "changeset" (from the
// client) are relative to revision r - 1. The follow function
// rebases "changeset" so that it is relative to revision r
// and can be applied after "c".
rebasedChangeset = Changeset.follow(c, rebasedChangeset, false, pad.pool);
rebasedChangeset = follow(c, rebasedChangeset, false, pad.pool);
}
const prevText = pad.text();
if (Changeset.oldLen(rebasedChangeset) !== prevText.length) {
if (oldLen(rebasedChangeset) !== prevText.length) {
throw new Error(
`Can't apply changeset ${rebasedChangeset} with oldLen ` +
`${Changeset.oldLen(rebasedChangeset)} to document of length ${prevText.length}`);
`${oldLen(rebasedChangeset)} to document of length ${prevText.length}`);
}
const newRev = await pad.appendRevision(rebasedChangeset, thisSession.author);
@ -641,7 +684,7 @@ const handleUserChanges = async (socket, message) => {
// Make sure the pad always ends with an empty line.
if (pad.text().lastIndexOf('\n') !== pad.text().length - 1) {
const nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length - 1, 0, '\n');
const nlChangeset = makeSplice(pad.text(), pad.text().length - 1, 0, '\n');
await pad.appendRevision(nlChangeset, thisSession.author);
}
@ -651,10 +694,10 @@ const handleUserChanges = async (socket, message) => {
socket.json.send({type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev}});
thisSession.rev = newRev;
if (newRev !== r) thisSession.time = await pad.getRevisionDate(newRev);
await exports.updatePadClients(pad);
await updatePadClients(pad);
} catch (err) {
socket.json.send({disconnect: 'badChangeset'});
stats.meter('failedChangesets').mark();
createCollection.meter('failedChangesets').mark();
messageLogger.warn(`Failed to apply USER_CHANGES from author ${thisSession.author} ` +
`(socket ${socket.id}) on pad ${thisSession.padId}: ${err.stack || err}`);
} finally {
@ -662,7 +705,7 @@ const handleUserChanges = async (socket, message) => {
}
};
exports.updatePadClients = async (pad) => {
export const updatePadClients = async (pad) => {
// skip this if no-one is on this pad
const roomSockets = _getRoomSockets(pad.id);
if (roomSockets.length === 0) return;
@ -696,7 +739,7 @@ exports.updatePadClients = async (pad) => {
const revChangeset = revision.changeset;
const currentTime = revision.meta.timestamp;
const forWire = Changeset.prepareForWire(revChangeset, pad.pool);
const forWire = prepareForWire(revChangeset, pad.pool);
const msg = {
type: 'COLLABROOM',
data: {
@ -731,7 +774,7 @@ const _correctMarkersInPad = (atext, apool) => {
// that aren't at the start of a line
const badMarkers = [];
let offset = 0;
for (const op of Changeset.deserializeOps(atext.attribs)) {
for (const op of deserializeOps(atext.attribs)) {
const attribs = AttributeMap.fromString(op.attribs, apool);
const hasMarker = AttributeManager.lineAttributes.some((a) => attribs.has(a));
if (hasMarker) {
@ -753,17 +796,18 @@ const _correctMarkersInPad = (atext, apool) => {
// create changeset that removes these bad markers
offset = 0;
const builder = Changeset.builder(text.length);
const builder2 = builder(text.length);
badMarkers.forEach((pos) => {
builder.keepText(text.substring(offset, pos));
builder.remove(1);
builder2.keepText(text.substring(offset, pos));
builder2.remove(1);
offset = pos + 1;
});
return builder.toString();
return builder2.toString();
};
export let clientVars:any
/**
* Handles a CLIENT_READY. A CLIENT_READY is the first message from the client
* to the server. The Client sends his token
@ -776,7 +820,7 @@ const handleClientReady = async (socket, message) => {
if (sessionInfo == null) throw new Error('client disconnected');
assert(sessionInfo.author);
await hooks.aCallAll('clientReady', message); // Deprecated due to awkward context.
await aCallAll('clientReady', message); // Deprecated due to awkward context.
let {colorId: authorColorId, name: authorName} = message.userInfo || {};
if (authorColorId && !/^#(?:[0-9A-F]{3}){1,2}$/i.test(authorColorId)) {
@ -784,13 +828,13 @@ const handleClientReady = async (socket, message) => {
authorColorId = null;
}
await Promise.all([
authorName && authorManager.setAuthorName(sessionInfo.author, authorName),
authorColorId && authorManager.setAuthorColorId(sessionInfo.author, authorColorId),
authorName && setAuthorName(sessionInfo.author, authorName),
authorColorId && setAuthorColorId(sessionInfo.author, authorColorId),
]);
({colorId: authorColorId, name: authorName} = await authorManager.getAuthor(sessionInfo.author));
({colorId: authorColorId, name: authorName} = await getAuthor(sessionInfo.author));
// load the pad-object from the database
const pad = await padManager.getPad(sessionInfo.padId, null, sessionInfo.author);
const pad = await createCollection.getPad(sessionInfo.padId, null, sessionInfo.author);
// these db requests all need the pad object (timestamp of latest revision, author data)
const authors = pad.getAllAuthors();
@ -801,7 +845,7 @@ const handleClientReady = async (socket, message) => {
// get all author data out of the database (in parallel)
const historicalAuthorData = {};
await Promise.all(authors.map(async (authorId) => {
const author = await authorManager.getAuthor(authorId);
const author = await getAuthor(authorId);
if (!author) {
messageLogger.error(`There is no author for authorId: ${authorId}. ` +
'This is possibly related to https://github.com/ether/etherpad-lite/issues/2802');
@ -825,18 +869,18 @@ const handleClientReady = async (socket, message) => {
const sinfo = sessioninfos[otherSocket.id];
if (sinfo && sinfo.author === sessionInfo.author) {
// fix user's counter, works on page refresh or if user closes browser window and then rejoins
sessioninfos[otherSocket.id] = {};
sessioninfos[otherSocket.id] = undefined
otherSocket.leave(sessionInfo.padId);
otherSocket.json.send({disconnect: 'userdup'});
}
}
const {session: {user} = {}} = socket.client.request;
const {session: {user} = {}}:SessionSocketModel = socket.client.request;
/* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */
accessLogger.info(`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` +
` pad:${sessionInfo.padId}` +
` socket:${socket.id}` +
` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` +
` IP:${disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` +
` authorID:${sessionInfo.author}` +
(user && user.username ? ` username:${user.username}` : ''));
/* eslint-enable prefer-template */
@ -884,7 +928,7 @@ const handleClientReady = async (socket, message) => {
// return pending changesets
for (const r of revisionsNeeded) {
const forWire = Changeset.prepareForWire(changesets[r].changeset, pad.pool);
const forWire = prepareForWire(changesets[r].changeset, pad.pool);
const wireMsg = {type: 'COLLABROOM',
data: {type: 'CLIENT_RECONNECT',
headRev: pad.getHeadRevisionNumber(),
@ -909,8 +953,8 @@ const handleClientReady = async (socket, message) => {
let apool;
// prepare all values for the wire, there's a chance that this throws, if the pad is corrupted
try {
atext = Changeset.cloneAText(pad.atext);
const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
atext = cloneAText(pad.atext);
const attribsForWire = prepareForWire(atext.attribs, pad.pool);
apool = attribsForWire.pool.toJsonable();
atext.attribs = attribsForWire.translated;
} catch (e) {
@ -921,14 +965,14 @@ const handleClientReady = async (socket, message) => {
// Warning: never ever send sessionInfo.padId to the client. If the client is read only you
// would open a security hole 1 swedish mile wide...
const clientVars = {
skinName: settings.skinName,
skinVariants: settings.skinVariants,
randomVersionString: settings.randomVersionString,
clientVars = {
skinName: skinName,
skinVariants: skinVariants,
randomVersionString: randomString(4),
accountPrivs: {
maxRevisions: 100,
},
automaticReconnectionTimeout: settings.automaticReconnectionTimeout,
automaticReconnectionTimeout: automaticReconnectionTimeout,
initialRevisionList: [],
initialOptions: {},
savedRevisions: pad.getSavedRevisions(),
@ -941,12 +985,12 @@ const handleClientReady = async (socket, message) => {
rev: pad.getHeadRevisionNumber(),
time: currentTime,
},
colorPalette: authorManager.getColorPalette(),
colorPalette: getColorPalette(),
clientIp: '127.0.0.1',
userColor: authorColorId,
padId: sessionInfo.auth.padID,
padOptions: settings.padOptions,
padShortcutEnabled: settings.padShortcutEnabled,
padOptions: padOptions,
padShortcutEnabled: padShortcutEnabled,
initialTitle: `Pad: ${sessionInfo.auth.padID}`,
opts: {},
// tell the client the number of the latest chat-message, which will be
@ -956,29 +1000,30 @@ const handleClientReady = async (socket, message) => {
readOnlyId: sessionInfo.readOnlyPadId,
readonly: sessionInfo.readonly,
serverTimestamp: Date.now(),
sessionRefreshInterval: settings.cookie.sessionRefreshInterval,
sessionRefreshInterval: cookie.sessionRefreshInterval,
userId: sessionInfo.author,
abiwordAvailable: settings.abiwordAvailable(),
sofficeAvailable: settings.sofficeAvailable(),
exportAvailable: settings.exportAvailable(),
abiwordAvailable: abiwordAvailable(),
sofficeAvailable: sofficeAvailable(),
exportAvailable: exportAvailable(),
plugins: {
plugins: plugins.plugins,
parts: plugins.parts,
plugins: plugins,
parts: parts,
},
indentationOnNewLine: settings.indentationOnNewLine,
indentationOnNewLine: indentationOnNewLine,
scrollWhenFocusLineIsOutOfViewport: {
percentage: {
editionAboveViewport:
settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport,
scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport,
editionBelowViewport:
settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport,
scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport,
},
duration: settings.scrollWhenFocusLineIsOutOfViewport.duration,
duration: scrollWhenFocusLineIsOutOfViewport.duration,
scrollWhenCaretIsInTheLastLineOfViewport:
settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport,
scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport,
percentageToScrollWhenUserPressesArrowUp:
settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
},
userName: undefined,
initialChangesets: [], // FIXME: REMOVE THIS SHIT
};
@ -988,7 +1033,7 @@ const handleClientReady = async (socket, message) => {
}
// call the clientVars-hook so plugins can modify them before they get sent to the client
const messages = await hooks.aCallAll('clientVars', {clientVars, pad, socket});
const messages = await aCallAll('clientVars', {clientVars, pad, socket});
// combine our old object with the new attributes from the hook
for (const msg of messages) {
@ -1034,7 +1079,7 @@ const handleClientReady = async (socket, message) => {
if (authorId == null) return;
// reuse previously created cache of author's data
const authorInfo = historicalAuthorData[authorId] || await authorManager.getAuthor(authorId);
const authorInfo = historicalAuthorData[authorId] || await getAuthor(authorId);
if (authorInfo == null) {
messageLogger.error(
`Author ${authorId} connected via socket.io session ${roomSocket.id} is missing from ` +
@ -1059,7 +1104,7 @@ const handleClientReady = async (socket, message) => {
socket.json.send(msg);
}));
await hooks.aCallAll('userJoin', {
await aCallAll('userJoin', {
authorId: sessionInfo.author,
displayName: authorName,
padId: sessionInfo.padId,
@ -1079,7 +1124,7 @@ const handleChangesetRequest = async (socket, {data: {granularity, start, reques
if (requestID == null) throw new Error('mising requestID');
const end = start + (100 * granularity);
const {padId, author: authorId} = sessioninfos[socket.id];
const pad = await padManager.getPad(padId, null, authorId);
const pad = await createCollection.getPad(padId, null, authorId);
const data = await getChangesetInfo(pad, start, end, granularity);
data.requestID = requestID;
socket.json.send({type: 'CHANGESET_REQ', data});
@ -1089,7 +1134,7 @@ const handleChangesetRequest = async (socket, {data: {granularity, start, reques
* Tries to rebuild the getChangestInfo function of the original Etherpad
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144
*/
const getChangesetInfo = async (pad, startNum, endNum, granularity) => {
const getChangesetInfo = async (pad: Pad, startNum: number, endNum: number, granularity: number) => {
const headRevision = pad.getHeadRevisionNumber();
// calculate the last full endnum
@ -1120,8 +1165,7 @@ const getChangesetInfo = async (pad, startNum, endNum, granularity) => {
getPadLines(pad, startNum - 1),
// Get all needed composite Changesets.
...compositesChangesetNeeded.map(async (item) => {
const changeset = await composePadChangesets(pad, item.start, item.end);
composedChangesets[`${item.start}/${item.end}`] = changeset;
composedChangesets[`${item.start}/${item.end}`] = await composePadChangesets(pad, item.start, item.end);
}),
// Get all needed revision Dates.
...revTimesNeeded.map(async (revNum) => {
@ -1141,13 +1185,13 @@ const getChangesetInfo = async (pad, startNum, endNum, granularity) => {
if (compositeEnd > endNum || compositeEnd > headRevision + 1) break;
const forwards = composedChangesets[`${compositeStart}/${compositeEnd}`];
const backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool());
const backwards = inverse(forwards, lines.textlines, lines.alines, pad.apool());
Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool());
Changeset.mutateTextLines(forwards, lines.textlines);
mutateAttributionLines(forwards, lines.alines, pad.apool());
mutateTextLines(forwards, lines.textlines);
const forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool);
const backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool);
const forwards2 = moveOpsToNewPool(forwards, pad.apool(), apool);
const backwards2 = moveOpsToNewPool(backwards, pad.apool(), apool);
const t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 1];
const t2 = revisionDate[compositeEnd - 1];
@ -1159,7 +1203,9 @@ const getChangesetInfo = async (pad, startNum, endNum, granularity) => {
return {forwardsChangesets, backwardsChangesets,
apool: apool.toJsonable(), actualEndNum: endNum,
timeDeltas, start: startNum, granularity};
timeDeltas, start: startNum, granularity, requestID: undefined
};
};
/**
@ -1173,12 +1219,12 @@ const getPadLines = async (pad, revNum) => {
if (revNum >= 0) {
atext = await pad.getInternalRevisionAText(revNum);
} else {
atext = Changeset.makeAText('\n');
atext = makeAText('\n');
}
return {
textlines: Changeset.splitTextLines(atext.text),
alines: Changeset.splitAttributionLines(atext.attribs, atext.text),
textlines: splitTextLines(atext.text),
alines: splitAttributionLines(atext.attribs, atext.text),
};
};
@ -1213,7 +1259,7 @@ const composePadChangesets = async (pad, startNum, endNum) => {
for (r = startNum + 1; r < endNum; r++) {
const cs = changesets[r];
changeset = Changeset.compose(changeset, cs, pool);
changeset = compose(changeset, cs, pool);
}
return changeset;
} catch (e) {
@ -1238,21 +1284,21 @@ const _getRoomSockets = (padID) => {
/**
* Get the number of users in a pad
*/
exports.padUsersCount = (padID) => ({
export const padUsersCount = (padID) => ({
padUsersCount: _getRoomSockets(padID).length,
});
/**
* Get the list of users in a pad
*/
exports.padUsers = async (padID) => {
export const padUsers = async (padID) => {
const padUsers = [];
// iterate over all clients (in parallel)
await Promise.all(_getRoomSockets(padID).map(async (roomSocket) => {
const s = sessioninfos[roomSocket.id];
if (s) {
const author = await authorManager.getAuthor(s.author);
const author = await getAuthor(s.author);
// Fixes: https://github.com/ether/etherpad-lite/issues/4120
// On restart author might not be populated?
if (author) {
@ -1263,6 +1309,4 @@ exports.padUsers = async (padID) => {
}));
return {padUsers};
};
exports.sessioninfos = sessioninfos;
}

View File

@ -20,9 +20,10 @@
* limitations under the License.
*/
const log4js = require('log4js');
const settings = require('../utils/Settings');
const stats = require('../stats');
import log4js from 'log4js';
import {disableIPlogging} from "../utils/Settings";
import {createCollection} from '../stats';
const logger = log4js.getLogger('socket.io');
@ -38,22 +39,22 @@ let io;
/**
* adds a component
*/
exports.addComponent = (moduleName, module) => {
if (module == null) return exports.deleteComponent(moduleName);
export const addComponent = (moduleName, module) => {
if (module == null) return deleteComponent(moduleName);
components[moduleName] = module;
module.setSocketIO(io);
};
exports.deleteComponent = (moduleName) => { delete components[moduleName]; };
export const deleteComponent = (moduleName) => { delete components[moduleName]; };
/**
* sets the socket.io and adds event functions for routing
*/
exports.setSocketIO = (_io) => {
export const setSocketIO = (_io) => {
io = _io;
io.sockets.on('connection', (socket) => {
const ip = settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip;
const ip = disableIPlogging ? 'ANONYMOUS' : socket.request.ip;
logger.debug(`${socket.id} connected from IP ${ip}`);
// wrap the original send function to log the messages
@ -68,14 +69,14 @@ exports.setSocketIO = (_io) => {
components[i].handleConnect(socket);
}
socket.on('message', (message, ack = () => {}) => (async () => {
socket.on('message', (message, ack = (p: { name: any; message: any }) => {}) => (async () => {
if (!message.component || !components[message.component]) {
throw new Error(`unknown message component: ${message.component}`);
}
logger.debug(`from ${socket.id}:`, message);
return await components[message.component].handleMessage(socket, message);
})().then(
(val) => ack(null, val),
(val) => ack({name: val.name, message: val.message}),
(err) => {
logger.error(
`Error handling ${message.component} message from ${socket.id}: ${err.stack || err}`);
@ -88,7 +89,7 @@ exports.setSocketIO = (_io) => {
// when the last user disconnected. If your activePads is 0 and totalUsers is 0
// you can say, if there has been no active pads or active users for 10 minutes
// this instance can be brought out of a scaling cluster.
stats.gauge('lastDisconnect', () => Date.now());
createCollection.gauge('lastDisconnect', () => Date.now());
// tell all components about this disconnect
for (const i of Object.keys(components)) {
components[i].handleDisconnect(socket);

View File

@ -1,35 +1,58 @@
'use strict';
const _ = require('underscore');
const cookieParser = require('cookie-parser');
const events = require('events');
const express = require('express');
const expressSession = require('express-session');
const fs = require('fs');
const hooks = require('../../static/js/pluginfw/hooks');
const log4js = require('log4js');
const SessionStore = require('../db/SessionStore');
const settings = require('../utils/Settings');
const stats = require('../stats');
const util = require('util');
const webaccess = require('./express/webaccess');
import _ from 'underscore';
import cookieParser from 'cookie-parser';
import events from "events";
import express from "express";
import fs from "fs";
import expressSession from "express-session";
import {aCallAll} from "../../static/js/pluginfw/hooks";
import log4js from "log4js";
import SessionStore from "../db/SessionStore";
import {
cookie,
exposeVersion,
getEpVersion,
getGitCommit,
ip,
loglevel,
port,
sessionKey,
ssl, sslKeys,
trustProxy,
users
} from "../utils/Settings";
import {createCollection} from "../stats";
import util from "util";
import {checkAccess, checkAccess2} from "./express/webaccess";
import {Socket} from "net";
const logger = log4js.getLogger('http');
let serverName;
let sessionStore;
const sockets = new Set();
const sockets = new Set<Socket>();
const socketsEvents = new events.EventEmitter();
const startTime = stats.settableGauge('httpStartTime');
exports.server = null;
const startTime = createCollection.settableGauge('httpStartTime');
export let server = null;
export let sessionMiddleware;
const closeServer = async () => {
if (exports.server != null) {
if (server != null) {
logger.info('Closing HTTP server...');
// Call exports.server.close() to reject new connections but don't await just yet because the
// Promise won't resolve until all preexisting connections are closed.
const p = util.promisify(exports.server.close.bind(exports.server))();
await hooks.aCallAll('expressCloseServer');
const p = util.promisify(server.close.bind(server))();
await aCallAll('expressCloseServer');
// Give existing connections some time to close on their own before forcibly terminating. The
// time should be long enough to avoid interrupting most preexisting transmissions but short
// enough to avoid a noticeable outage.
@ -47,7 +70,7 @@ const closeServer = async () => {
}
await p;
clearTimeout(timeout);
exports.server = null;
server = null;
startTime.setValue(0);
logger.info('HTTP server closed');
}
@ -55,71 +78,73 @@ const closeServer = async () => {
sessionStore = null;
};
exports.createServer = async () => {
console.log('Report bugs at https://github.com/ether/etherpad-lite/issues');
export const createServer = async () => {
logger.info('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()})`);
logger.info(`Your Etherpad version is ${getEpVersion()} (${getGitCommit()})`);
await exports.restartServer();
await restartServer();
if (settings.ip === '') {
if (ip.length===0) {
// using Unix socket for connectivity
console.log(`You can access your Etherpad instance using the Unix socket at ${settings.port}`);
logger.info(`You can access your Etherpad instance using the Unix socket at ${port}`);
} else {
console.log(`You can access your Etherpad instance at http://${settings.ip}:${settings.port}/`);
logger.info(`You can access your Etherpad instance at http://${ip}:${port}/`);
}
if (!_.isEmpty(settings.users)) {
console.log(`The plugin admin page is at http://${settings.ip}:${settings.port}/admin/plugins`);
if (!_.isEmpty(users)) {
logger.info(`The plugin admin page is at http://${ip}:${port}/admin/plugins`);
} else {
console.warn('Admin username and password not set in settings.json. ' +
logger.info('Admin username and password not set in settings.json. ' +
'To access admin please uncomment and edit "users" in settings.json');
}
const env = process.env.NODE_ENV || 'development';
if (env !== 'production') {
console.warn('Etherpad is running in Development mode. This mode is slower for users and ' +
logger.warn('Etherpad is running in Development mode. This mode is slower for users and ' +
'less secure than production mode. You should set the NODE_ENV environment ' +
'variable to production by using: export NODE_ENV=production');
}
};
export const app = express();
exports.restartServer = async () => {
import http from 'http'
import https from 'https'
export const restartServer = async () => {
await closeServer();
const app = express(); // New syntax for express v3
// New syntax for express v3
if (settings.ssl) {
if (ssl) {
console.log('SSL -- enabled');
console.log(`SSL -- server key file: ${settings.ssl.key}`);
console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`);
console.log(`SSL -- server key file: ${sslKeys.key}`);
console.log(`SSL -- Certificate Authority's certificate file: ${sslKeys.cert}`);
const options = {
key: fs.readFileSync(settings.ssl.key),
cert: fs.readFileSync(settings.ssl.cert),
key: fs.readFileSync(sslKeys.key),
cert: fs.readFileSync(sslKeys.cert),
ca: undefined
};
if (settings.ssl.ca) {
if (sslKeys.ca) {
options.ca = [];
for (let i = 0; i < settings.ssl.ca.length; i++) {
const caFileName = settings.ssl.ca[i];
for (let i = 0; i < sslKeys.ca.length; i++) {
const caFileName = sslKeys.ca[i];
options.ca.push(fs.readFileSync(caFileName));
}
}
const https = require('https');
exports.server = https.createServer(options, app);
server = https.createServer(options, app);
} else {
const http = require('http');
exports.server = http.createServer(app);
server = http.createServer(app);
}
app.use((req, res, next) => {
// res.header("X-Frame-Options", "deny"); // breaks embedded pads
if (settings.ssl) {
if (ssl) {
// we use SSL
res.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
@ -138,14 +163,14 @@ exports.restartServer = async () => {
res.header('Referrer-Policy', 'same-origin');
// send git version in the Server response header if exposeVersion is true.
if (settings.exposeVersion) {
if (exposeVersion) {
res.header('Server', serverName);
}
next();
});
if (settings.trustProxy) {
if (trustProxy) {
/*
* If 'trust proxy' === true, the clients IP address in req.ip will be the
* left-most entry in the X-Forwarded-* header.
@ -157,8 +182,10 @@ exports.restartServer = async () => {
// Measure response time
app.use((req, res, next) => {
const stopWatch = stats.timer('httpRequests').start();
const stopWatch = createCollection.timer('httpRequests').start();
const sendFn = res.send.bind(res);
// FIXME Check if this is still needed
// @ts-ignore
res.send = (...args) => { stopWatch.end(); sendFn(...args); };
next();
});
@ -167,20 +194,20 @@ exports.restartServer = async () => {
// starts listening to requests as reported in issue #158. Not installing the log4js connect
// logger when the log level has a higher severity than INFO since it would not log at that level
// anyway.
if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) {
if (!(loglevel === 'WARN') && loglevel === 'ERROR') {
app.use(log4js.connectLogger(logger, {
level: log4js.levels.DEBUG,
level: loglevel,
format: ':status, :method :url',
}));
}
app.use(cookieParser(settings.sessionKey, {}));
app.use(cookieParser(sessionKey, {}));
sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval);
exports.sessionMiddleware = expressSession({
sessionStore = new SessionStore(cookie.sessionRefreshInterval);
sessionMiddleware = expressSession({
propagateTouch: true,
rolling: true,
secret: settings.sessionKey,
secret: sessionKey,
store: sessionStore,
resave: false,
saveUninitialized: false,
@ -188,8 +215,8 @@ exports.restartServer = async () => {
// cleaner :)
name: 'express_sid',
cookie: {
maxAge: settings.cookie.sessionLifetime || null, // Convert 0 to null.
sameSite: settings.cookie.sameSite,
maxAge: cookie.sessionLifetime || null, // Convert 0 to null.
sameSite: cookie.sameSite,
// The automatic express-session mechanism for determining if the application is being served
// over ssl is similar to the one used for setting the language cookie, which check if one of
@ -214,16 +241,16 @@ exports.restartServer = async () => {
// Give plugins an opportunity to install handlers/middleware before the express-session
// middleware. This allows plugins to avoid creating an express-session record in the database
// when it is not needed (e.g., public static content).
await hooks.aCallAll('expressPreSession', {app});
app.use(exports.sessionMiddleware);
await aCallAll('expressPreSession', {app});
app.use(sessionMiddleware);
app.use(webaccess.checkAccess);
app.use(checkAccess2);
await Promise.all([
hooks.aCallAll('expressConfigure', {app}),
hooks.aCallAll('expressCreateServer', {app, server: exports.server}),
aCallAll('expressConfigure', {app}),
aCallAll('expressCreateServer', {app, server: server}),
]);
exports.server.on('connection', (socket) => {
server.on('connection', (socket) => {
sockets.add(socket);
socketsEvents.emit('updated');
socket.on('close', () => {
@ -231,11 +258,11 @@ exports.restartServer = async () => {
socketsEvents.emit('updated');
});
});
await util.promisify(exports.server.listen).bind(exports.server)(settings.port, settings.ip);
await util.promisify(server.listen).bind(server)(port, ip);
startTime.setValue(Date.now());
logger.info('HTTP server listening for connections');
};
exports.shutdown = async (hookName, context) => {
export const shutdown = async (hookName, context) => {
await closeServer();
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
'use strict';
import {ResponseSpec} from "../../models/ResponseSpec";
import {APIResource} from "../../models/APIResource";
/**
* node/hooks/express/openapi.js
*
@ -54,7 +57,7 @@ const APIPathStyle = {
};
// API resources - describe your API endpoints here
const resources = {
const resources: APIResource = {
// Group
group: {
create: {
@ -375,7 +378,8 @@ const defaultResponses = {
const defaultResponseRefs = {
200: {
$ref: '#/components/responses/Success',
$ref: '#/components/responses/Success', content: undefined
},
400: {
$ref: '#/components/responses/ApiError',
@ -491,7 +495,11 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
// build operations
for (const funcName of Object.keys(apiHandler.version[version])) {
let operation = {};
let operation: ResponseSpec = {
parameters: undefined,
_restPath: "",
operationId: undefined
};
if (operations[funcName]) {
operation = {...operations[funcName]};
} else {
@ -540,7 +548,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
return definition;
};
exports.expressPreSession = async (hookName, {app}) => {
export const expressPreSession = async (hookName, {app}) => {
// create openapi-backend handlers for each api version under /api/{version}/*
for (const version of Object.keys(apiHandler.version)) {
// we support two different styles of api: flat + rest

View File

@ -1,18 +1,18 @@
'use strict';
const padManager = require('../../db/PadManager');
import {isValidPadId, sanitizePadId} from '../../db/PadManager';
exports.expressCreateServer = (hookName, args, cb) => {
export const expressCreateServer = (hookName, args, cb) => {
// redirects browser to the pad's sanitized url if needed. otherwise, renders the html
args.app.param('pad', (req, res, next, padId) => {
(async () => {
// ensure the padname is valid and the url doesn't end with a /
if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) {
if (!isValidPadId(padId) || /\/$/.test(req.url)) {
res.status(404).send('Such a padname is forbidden');
return;
}
const sanitizedPadId = await padManager.sanitizePadId(padId);
const sanitizedPadId = await sanitizePadId(padId);
if (sanitizedPadId === padId) {
// the pad id was fine, so just render it

View File

@ -1,21 +1,21 @@
'use strict';
const events = require('events');
const express = require('../express');
const log4js = require('log4js');
const proxyaddr = require('proxy-addr');
const settings = require('../../utils/Settings');
const socketio = require('socket.io');
const socketIORouter = require('../../handler/SocketIORouter');
const hooks = require('../../../static/js/pluginfw/hooks');
const padMessageHandler = require('../../handler/PadMessageHandler');
import events from 'events';
import {sessionMiddleware} from '../express';
import log4js from 'log4js';
import proxyaddr from 'proxy-addr';
import {socketIo, socketTransportProtocols, trustProxy} from '../../utils/Settings';
import socketio from 'socket.io';
import {addComponent, setSocketIO} from '../../handler/SocketIORouter';
import {callAll} from '../../../static/js/pluginfw/hooks';
import * as padMessageHandler from '../../handler/PadMessageHandler';
let io;
const logger = log4js.getLogger('socket.io');
const sockets = new Set();
const socketsEvents = new events.EventEmitter();
exports.expressCloseServer = async () => {
export const expressCloseServer = async () => {
if (io == null) return;
logger.info('Closing socket.io engine...');
// Close the socket.io engine to disconnect existing clients and reject new clients. Don't call
@ -46,13 +46,13 @@ exports.expressCloseServer = async () => {
logger.info('All socket.io clients have disconnected');
};
exports.expressCreateServer = (hookName, args, cb) => {
export const expressCreateServer = (hookName, args, cb) => {
// init socket.io and redirect all requests to the MessageHandler
// there shouldn't be a browser that isn't compatible to all
// transports in this list at once
// e.g. XHR is disabled in IE by default, so in IE it should use jsonp-polling
io = socketio({
transports: settings.socketTransportProtocols,
transports: socketTransportProtocols,
}).listen(args.server, {
/*
* Do not set the "io" cookie.
@ -74,7 +74,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
* https://github.com/socketio/socket.io/issues/2276#issuecomment-147184662 (not totally true, actually, see above)
*/
cookie: false,
maxHttpBufferSize: settings.socketIo.maxHttpBufferSize,
maxHttpBufferSize: socketIo.maxHttpBufferSize,
});
io.on('connect', (socket) => {
@ -90,7 +90,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
const req = socket.request;
// Express sets req.ip but socket.io does not. Replicate Express's behavior here.
if (req.ip == null) {
if (settings.trustProxy) {
if (trustProxy) {
req.ip = proxyaddr(req, args.app.get('trust proxy fn'));
} else {
req.ip = socket.handshake.address;
@ -102,7 +102,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
req.headers.cookie = socket.handshake.query.cookie;
}
// See: https://socket.io/docs/faq/#Usage-with-express-session
express.sessionMiddleware(req, {}, next);
sessionMiddleware(req, {}, next);
});
io.use((socket, next) => {
@ -130,10 +130,10 @@ exports.expressCreateServer = (hookName, args, cb) => {
// if(settings.minify) io.enable('browser client minification');
// Initialize the Socket.IO Router
socketIORouter.setSocketIO(io);
socketIORouter.addComponent('pad', padMessageHandler);
setSocketIO(io);
addComponent('pad', padMessageHandler);
hooks.callAll('socketio', {app: args.app, io, server: args.server});
callAll('socketio', {app: args.app, io, server: args.server});
return cb();
};

View File

@ -1,41 +1,41 @@
'use strict';
const path = require('path');
const eejs = require('../../eejs');
const fs = require('fs');
import path from 'path';
import {required} from '../../eejs';
import fs from 'fs';
const fsp = fs.promises;
const toolbar = require('../../utils/toolbar');
const hooks = require('../../../static/js/pluginfw/hooks');
const settings = require('../../utils/Settings');
const util = require('util');
const webaccess = require('./webaccess');
import {} from '../../utils/toolbar';
import {callAll} from '../../../static/js/pluginfw/hooks';
import {favicon, getEpVersion, maxAge, root, skinName} from '../../utils/Settings';
import util from 'util';
import {userCanModify} from './webaccess';
exports.expressPreSession = async (hookName, {app}) => {
export const expressPreSession = async (hookName, {app}) => {
// This endpoint is intended to conform to:
// https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html
app.get('/health', (req, res) => {
res.set('Content-Type', 'application/health+json');
res.json({
status: 'pass',
releaseId: settings.getEpVersion(),
releaseId: getEpVersion(),
});
});
app.get('/stats', (req, res) => {
res.json(require('../../stats').toJSON());
res.json(required('../../stats').toJSON());
});
app.get('/javascript', (req, res) => {
res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req}));
res.send(required('ep_etherpad-lite/templates/javascript.html', {req}));
});
app.get('/robots.txt', (req, res) => {
let filePath =
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt');
path.join(root, 'src', 'static', 'skins', skinName, 'robots.txt');
res.sendFile(filePath, (err) => {
// there is no custom robots.txt, send the default robots.txt which dissallows all
if (err) {
filePath = path.join(settings.root, 'src', 'static', 'robots.txt');
filePath = path.join(root, 'src', 'static', 'robots.txt');
res.sendFile(filePath);
}
});
@ -44,9 +44,9 @@ exports.expressPreSession = async (hookName, {app}) => {
app.get('/favicon.ico', (req, res, next) => {
(async () => {
const fns = [
...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []),
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'),
path.join(settings.root, 'src', 'static', 'favicon.ico'),
...(favicon ? [path.resolve(root, favicon)] : []),
path.join(root, 'src', 'static', 'skins', skinName, 'favicon.ico'),
path.join(root, 'src', 'static', 'favicon.ico'),
];
for (const fn of fns) {
try {
@ -54,7 +54,7 @@ exports.expressPreSession = async (hookName, {app}) => {
} catch (err) {
continue;
}
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
await util.promisify(res.sendFile.bind(res))(fn);
return;
}
@ -63,25 +63,25 @@ exports.expressPreSession = async (hookName, {app}) => {
});
};
exports.expressCreateServer = (hookName, args, cb) => {
export const expressCreateServer = (hookName, args, cb) => {
// serve index.html under /
args.app.get('/', (req, res) => {
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req}));
res.send(required('ep_etherpad-lite/templates/index.html', {req}));
});
// serve pad.html under /p
args.app.get('/p/:pad', (req, res, next) => {
// The below might break for pads being rewritten
const isReadOnly = !webaccess.userCanModify(req.params.pad, req);
const isReadOnly = !userCanModify(req.params.pad, req);
hooks.callAll('padInitToolbar', {
callAll('padInitToolbar', {
toolbar,
isReadOnly,
});
// can be removed when require-kernel is dropped
res.header('Feature-Policy', 'sync-xhr \'self\'');
res.send(eejs.require('ep_etherpad-lite/templates/pad.html', {
res.send(required('ep_etherpad-lite/templates/pad.html', {
req,
toolbar,
isReadOnly,
@ -90,11 +90,11 @@ exports.expressCreateServer = (hookName, args, cb) => {
// serve timeslider.html under /p/$padname/timeslider
args.app.get('/p/:pad/timeslider', (req, res, next) => {
hooks.callAll('padInitToolbar', {
callAll('padInitToolbar', {
toolbar,
});
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
res.send(required('ep_etherpad-lite/templates/timeslider.html', {
req,
toolbar,
}));

View File

@ -19,8 +19,9 @@ const getTar = async () => {
};
const tarJson = await fs.readFile(path.join(settings.root, 'src/node/utils/tar.json'), 'utf8');
const tar = {};
for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) {
const files = relativeFiles.map(prefixLocalLibraryPath);
for (let [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) {
const relativeFilesMapped = relativeFiles as string[];
const files = relativeFilesMapped.map(prefixLocalLibraryPath);
tar[prefixLocalLibraryPath(key)] = files
.concat(files.map((p) => p.replace(/\.js$/, '')))
.concat(files.map((p) => `${p.replace(/\.js$/, '')}/index.js`));
@ -28,7 +29,7 @@ const getTar = async () => {
return tar;
};
exports.expressPreSession = async (hookName, {app}) => {
export const expressPreSession = async (hookName, {app}) => {
// Cache both minified and static.
const assetCache = new CachingMiddleware();
app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache));
@ -62,8 +63,9 @@ exports.expressPreSession = async (hookName, {app}) => {
const clientParts = plugins.parts.filter((part) => part.client_hooks != null);
const clientPlugins = {};
for (const name of new Set(clientParts.map((part) => part.plugin))) {
clientPlugins[name] = {...plugins.plugins[name]};
delete clientPlugins[name].package;
const mappedName = name as any
clientPlugins[mappedName] = {...plugins.plugins[mappedName]};
delete clientPlugins[mappedName].package;
}
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);

View File

@ -1,11 +1,14 @@
'use strict';
const path = require('path');
const fsp = require('fs').promises;
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const sanitizePathname = require('../../utils/sanitizePathname');
const settings = require('../../utils/Settings');
import path from 'path';
import {promises as fsp} from "fs";
import {plugins} from "../../../static/js/pluginfw/plugin_defs";
import sanitizePathname from "../../utils/sanitizePathname";
import {enableAdminUITests, root} from "../../utils/Settings";
import {Presession} from "../../models/Presession";
// Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/'
// instead of path.sep to separate pathname components.
const findSpecs = async (specDir) => {
@ -29,16 +32,17 @@ const findSpecs = async (specDir) => {
return specs;
};
exports.expressPreSession = async (hookName, {app}) => {
export const expressPreSession = async (hookName, {app}) => {
app.get('/tests/frontend/frontendTestSpecs.json', (req, res, next) => {
(async () => {
const modules = [];
await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => {
let {package: {path: pluginPath}} = def;
await Promise.all(Object.entries(plugins).map(async ([plugin, def]) => {
const mappedDef = def as Presession;
let {package: {path: pluginPath}} = mappedDef;
if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep;
const specDir = `${plugin === 'ep_etherpad-lite' ? '' : 'static/'}tests/frontend/specs`;
for (const spec of await findSpecs(path.join(pluginPath, specDir))) {
if (plugin === 'ep_etherpad-lite' && !settings.enableAdminUITests &&
if (plugin === 'ep_etherpad-lite' && !enableAdminUITests &&
spec.startsWith('admin')) continue;
modules.push(`${plugin}/${specDir}/${spec.replace(/\.js$/, '')}`);
}
@ -57,7 +61,7 @@ exports.expressPreSession = async (hookName, {app}) => {
})().catch((err) => next(err || new Error(err)));
});
const rootTestFolder = path.join(settings.root, 'src/tests/frontend/');
const rootTestFolder = path.join(root, 'src/tests/frontend/');
app.get('/tests/frontend/index.html', (req, res) => {
res.redirect(['./', ...req.url.split('?').slice(1)].join('?'));

View File

@ -1,23 +1,31 @@
'use strict';
const assert = require('assert').strict;
const log4js = require('log4js');
const httpLogger = log4js.getLogger('http');
const settings = require('../../utils/Settings');
const hooks = require('../../../static/js/pluginfw/hooks');
const readOnlyManager = require('../../db/ReadOnlyManager');
import {strict as assert} from "assert";
hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
import log4js from "log4js";
import {requireAuthentication, requireAuthorization, setUsers, users} from "../../utils/Settings";
import {deprecationNotices} from "../../../static/js/pluginfw/hooks";
import {getPadId, isReadOnlyId} from "../../db/ReadOnlyManager";
import {UserIndexedModel} from "../../models/UserIndexedModel";
const httpLogger = log4js.getLogger('http');
import {aCallFirst as hookCall} from "../../../static/js/pluginfw/hooks";
deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
// Promisified wrapper around hooks.aCallFirst.
const aCallFirst = (hookName, context, pred = null) => new Promise((resolve, reject) => {
hooks.aCallFirst(hookName, context, (err, r) => err != null ? reject(err) : resolve(r), pred);
hookCall(hookName, context, (err, r) => err != null ? reject(err) : resolve(r));
});
const aCallFirst0 =
async (hookName, context, pred = null) => (await aCallFirst(hookName, context, pred))[0];
exports.normalizeAuthzLevel = (level) => {
export const normalizeAuthzLevel = (level) => {
if (!level) return false;
switch (level) {
case true:
@ -32,20 +40,20 @@ exports.normalizeAuthzLevel = (level) => {
return false;
};
exports.userCanModify = (padId, req) => {
if (readOnlyManager.isReadOnlyId(padId)) return false;
if (!settings.requireAuthentication) return true;
const {session: {user} = {}} = req;
export const userCanModify = (padId, req) => {
if (isReadOnlyId(padId)) return false;
if (!requireAuthentication) return true;
const {session: {user} = {}}:SessionSocketModel = req;
if (!user || user.readOnly) return false;
assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization.
const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]);
const level = normalizeAuthzLevel(user.padAuthorizations[padId]);
return level && level !== 'readOnly';
};
// Exported so that tests can set this to 0 to avoid unnecessary test slowness.
exports.authnFailureDelayMs = 1000;
export let authnFailureDelayMs = 1000;
const checkAccess = async (req, res, next) => {
export const checkAccess = async (req, res, next) => {
const requireAdmin = req.path.toLowerCase().startsWith('/admin');
// ///////////////////////////////////////////////////////////////////////////////////////////////
@ -88,16 +96,16 @@ const checkAccess = async (req, res, next) => {
// authentication is checked and once after (if settings.requireAuthorization is true).
const authorize = async () => {
const grant = async (level) => {
level = exports.normalizeAuthzLevel(level);
level = normalizeAuthzLevel(level);
if (!level) return false;
const user = req.session.user;
if (user == null) return true; // This will happen if authentication is not required.
const encodedPadId = (req.path.match(/^\/p\/([^/]*)/) || [])[1];
if (encodedPadId == null) return true;
let padId = decodeURIComponent(encodedPadId);
if (readOnlyManager.isReadOnlyId(padId)) {
if (isReadOnlyId(padId)) {
// pad is read-only, first get the real pad ID
padId = await readOnlyManager.getPadId(padId);
padId = await getPadId(padId);
if (padId == null) return false;
}
// The user was granted access to a pad. Remember the authorization level in the user's
@ -108,11 +116,11 @@ const checkAccess = async (req, res, next) => {
};
const isAuthenticated = req.session && req.session.user;
if (isAuthenticated && req.session.user.is_admin) return await grant('create');
const requireAuthn = requireAdmin || settings.requireAuthentication;
const requireAuthn = requireAdmin || requireAuthentication;
if (!requireAuthn) return await grant('create');
if (!isAuthenticated) return await grant(false);
if (requireAdmin && !req.session.user.is_admin) return await grant(false);
if (!settings.requireAuthorization) return await grant('create');
if (!requireAuthorization) return await grant('create');
return await grant(await aCallFirst0('authorize', {req, res, next, resource: req.path}));
};
@ -131,8 +139,10 @@ const checkAccess = async (req, res, next) => {
// page).
// ///////////////////////////////////////////////////////////////////////////////////////////////
if (settings.users == null) settings.users = {};
const ctx = {req, res, users: settings.users, next};
if (users == null) setUsers({});
const ctx = {req, res, users: users, next, username: undefined,
password: undefined
};
// If the HTTP basic auth header is present, extract the username and password so it can be given
// to authn plugins.
const httpBasicAuth = req.headers.authorization && req.headers.authorization.startsWith('Basic ');
@ -148,7 +158,7 @@ const checkAccess = async (req, res, next) => {
}
if (!(await aCallFirst0('authenticate', ctx))) {
// Fall back to HTTP basic auth.
const {[ctx.username]: {password} = {}} = settings.users;
const {[ctx.username]: {password} = {password: undefined}}:UserIndexedModel = users;
if (!httpBasicAuth || !ctx.username || password == null || password !== ctx.password) {
httpLogger.info(`Failed authentication from IP ${req.ip}`);
if (await aCallFirst0('authnFailure', {req, res})) return;
@ -156,14 +166,14 @@ const checkAccess = async (req, res, next) => {
// No plugin handled the authentication failure. Fall back to basic authentication.
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
// Delay the error response for 1s to slow down brute force attacks.
await new Promise((resolve) => setTimeout(resolve, exports.authnFailureDelayMs));
await new Promise((resolve) => setTimeout(resolve, authnFailureDelayMs));
res.status(401).send('Authentication Required');
return;
}
settings.users[ctx.username].username = ctx.username;
users[ctx.username].username = ctx.username;
// Make a shallow copy so that the password property can be deleted (to prevent it from
// appearing in logs or in the database) without breaking future authentication attempts.
req.session.user = {...settings.users[ctx.username]};
req.session.user = {...users[ctx.username]};
delete req.session.user.password;
}
if (req.session.user == null) {
@ -190,6 +200,12 @@ const checkAccess = async (req, res, next) => {
* Express middleware to authenticate the user and check authorization. Must be installed after the
* express-session middleware.
*/
exports.checkAccess = (req, res, next) => {
// FIXME why same method twice?
export const checkAccess2 = (req, res, next) => {
checkAccess(req, res, next).catch((err) => next(err || new Error(err)));
};
// Setters
export const setauthnFailureDelayMs = (value) => {
authnFailureDelayMs = value;
}

View File

@ -1,12 +1,13 @@
'use strict';
const languages = require('languages4translatewiki');
const fs = require('fs');
const path = require('path');
const _ = require('underscore');
const pluginDefs = require('../../static/js/pluginfw/plugin_defs.js');
const existsSync = require('../utils/path_exists');
const settings = require('../utils/Settings');
import languages from 'languages4translatewiki';
import fs from 'fs';
import path from 'path';
import _ from 'underscore';
import {plugins} from '../../static/js/pluginfw/plugin_defs.js';
import {check} from '../utils/path_exists';
import {customLocaleStrings, maxAge, root} from '../utils/Settings';
import {Presession} from "../models/Presession";
// returns all existing messages merged together and grouped by langcode
// {es: {"foo": "string"}, en:...}
@ -17,7 +18,7 @@ const getAllLocales = () => {
// into `locales2paths` (files from various dirs are grouped by lang code)
// (only json files with valid language code as name)
const extractLangs = (dir) => {
if (!existsSync(dir)) return;
if (!check(dir)) return;
let stat = fs.lstatSync(dir);
if (!stat.isDirectory() || stat.isSymbolicLink()) return;
@ -37,18 +38,19 @@ const getAllLocales = () => {
};
// add core supported languages first
extractLangs(path.join(settings.root, 'src/locales'));
extractLangs(path.join(root, 'src/locales'));
// add plugins languages (if any)
for (const {package: {path: pluginPath}} of Object.values(pluginDefs.plugins)) {
for (const val of Object.values(plugins)) {
const pluginPath:Presession = val as Presession
// plugin locales should overwrite etherpad's core locales
if (pluginPath.endsWith('/ep_etherpad-lite') === true) continue;
extractLangs(path.join(pluginPath, 'locales'));
if (pluginPath.package.path.endsWith('/ep_etherpad-lite') === true) continue;
extractLangs(path.join(pluginPath.package.path, 'locales'));
}
// Build a locale index (merge all locale data other than user-supplied overrides)
const locales = {};
_.each(locales2paths, (files, langcode) => {
_.each(locales2paths, (files:[], langcode) => {
locales[langcode] = {};
files.forEach((file) => {
@ -68,9 +70,9 @@ const getAllLocales = () => {
const wrongFormatErr = Error(
'customLocaleStrings in wrong format. See documentation ' +
'for Customization for Administrators, under Localization.');
if (settings.customLocaleStrings) {
if (typeof settings.customLocaleStrings !== 'object') throw wrongFormatErr;
_.each(settings.customLocaleStrings, (overrides, langcode) => {
if (customLocaleStrings) {
if (typeof customLocaleStrings !== 'object') throw wrongFormatErr;
_.each(customLocaleStrings, (overrides, langcode) => {
if (typeof overrides !== 'object') throw wrongFormatErr;
_.each(overrides, (localeString, key) => {
if (typeof localeString !== 'string') throw wrongFormatErr;
@ -84,7 +86,7 @@ const getAllLocales = () => {
// returns a hash of all available languages availables with nativeName and direction
// e.g. { es: {nativeName: "español", direction: "ltr"}, ... }
const getAvailableLangs = (locales) => {
export const getAvailableLangs = (locales) => {
const result = {};
for (const langcode of Object.keys(locales)) {
result[langcode] = languages.getLanguageInfo(langcode);
@ -102,17 +104,17 @@ const generateLocaleIndex = (locales) => {
};
exports.expressPreSession = async (hookName, {app}) => {
export const expressPreSession = async (hookName, {app}) => {
// regenerate locales on server restart
const locales = getAllLocales();
const localeIndex = generateLocaleIndex(locales);
exports.availableLangs = getAvailableLangs(locales);
let availableLangs = getAvailableLangs(locales);
app.get('/locales/:locale', (req, res) => {
// works with /locale/en and /locale/en.json requests
const locale = req.params.locale.split('.')[0];
if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) {
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
if (Object.prototype.hasOwnProperty.call(availableLangs, locale)) {
res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`);
} else {
@ -121,7 +123,7 @@ exports.expressPreSession = async (hookName, {app}) => {
});
app.get('/locales.json', (req, res) => {
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.send(localeIndex);
});

View File

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

View File

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

View File

@ -0,0 +1,11 @@
export type CMDOptions = {
cwd?: string,
stdio?: string|any[],
env?: NodeJS.ProcessEnv
}
export type CMDPromise = {
stdout: string,
stderr: string,
child: any
}

View File

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

View File

@ -0,0 +1 @@
export type LogLevel = "DEBUG"|"INFO"|"WARN"|"ERROR"

View File

@ -0,0 +1,3 @@
export type PadDiffModel = {
_authors: any[]
}

View File

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

View File

@ -0,0 +1,5 @@
export type Presession = {
package:{
path: string,
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
type SessionSocketModel = {
session:{
user?: {
username?: string,
is_admin?: boolean
readOnly?: boolean,
padAuthorizations?: any
}
}
}

View File

@ -0,0 +1,5 @@
export type UserIndexedModel = {
[key: string]: {
password?: string|undefined,
}
}

View File

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

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

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

View File

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

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

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

View File

@ -19,20 +19,23 @@
* limitations under the License.
*/
const spawn = require('child_process').spawn;
const async = require('async');
const settings = require('./Settings');
const os = require('os');
import {spawn} from "child_process";
import async from 'async';
import {abiword} from './Settings';
import os from 'os';
export let convertFile;
// on windows we have to spawn a process for each convertion,
// cause the plugin abicommand doesn't exist on this platform
if (os.type().indexOf('Windows') > -1) {
exports.convertFile = async (srcFile, destFile, type) => {
const abiword = spawn(settings.abiword, [`--to=${destFile}`, srcFile]);
convertFile = async (srcFile, destFile, type) => {
const abiword2 = spawn(abiword, [`--to=${destFile}`, srcFile]);
let stdoutBuffer = '';
abiword.stdout.on('data', (data) => { stdoutBuffer += data.toString(); });
abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
await new Promise((resolve, reject) => {
abiword2.stdout.on('data', (data) => { stdoutBuffer += data.toString(); });
abiword2.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
await new Promise<void>((resolve, reject) => {
abiword.on('exit', (code) => {
if (code !== 0) return reject(new Error(`Abiword died with exit code ${code}`));
if (stdoutBuffer !== '') {
@ -46,14 +49,14 @@ if (os.type().indexOf('Windows') > -1) {
// communicate with it via stdin/stdout
// thats much faster, about factor 10
} else {
let abiword;
let abiword2;
let stdoutCallback = null;
const spawnAbiword = () => {
abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']);
abiword2 = spawn(abiword, ['--plugin', 'AbiCommand']);
let stdoutBuffer = '';
let firstPrompt = true;
abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
abiword.on('exit', (code) => {
abiword2.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
abiword2.on('exit', (code) => {
spawnAbiword();
if (stdoutCallback != null) {
stdoutCallback(new Error(`Abiword died with exit code ${code}`));
@ -84,7 +87,7 @@ if (os.type().indexOf('Windows') > -1) {
};
}, 1);
exports.convertFile = async (srcFile, destFile, type) => {
convertFile = async (srcFile, destFile, type) => {
await queue.pushAsync({srcFile, destFile, type});
};
}

View File

@ -19,9 +19,10 @@
* limitations under the License.
*/
const log4js = require('log4js');
const path = require('path');
const _ = require('underscore');
import log4js from 'log4js';
import path from "path";
import _ from "underscore";
const absPathLogger = log4js.getLogger('AbsolutePaths');
@ -75,7 +76,7 @@ const popIfEndsWith = (stringArray, lastDesiredElements) => {
* @return {string} The identified absolute base path. If such path cannot be
* identified, prints a log and exits the application.
*/
exports.findEtherpadRoot = () => {
export const findEtherpadRoot = () => {
if (etherpadRoot != null) {
return etherpadRoot;
}
@ -90,7 +91,7 @@ exports.findEtherpadRoot = () => {
*
* <BASE_DIR>\src
*/
let maybeEtherpadRoot = popIfEndsWith(splitFoundRoot, ['src']);
let maybeEtherpadRoot = popIfEndsWith(splitFoundRoot, ['dist']);
if ((maybeEtherpadRoot === false) && (process.platform === 'win32')) {
/*
@ -131,12 +132,12 @@ exports.findEtherpadRoot = () => {
* it is returned unchanged. Otherwise it is interpreted
* relative to exports.root.
*/
exports.makeAbsolute = (somePath) => {
export const makeAbsolute = (somePath) => {
if (path.isAbsolute(somePath)) {
return somePath;
}
const rewrittenPath = path.join(exports.findEtherpadRoot(), somePath);
const rewrittenPath = path.join(findEtherpadRoot(), somePath);
absPathLogger.debug(`Relative path "${somePath}" can be rewritten to "${rewrittenPath}"`);
return rewrittenPath;
@ -150,7 +151,7 @@ exports.makeAbsolute = (somePath) => {
* a subdirectory of the base one
* @return {boolean}
*/
exports.isSubdir = (parent, arbitraryDir) => {
export const isSubdir = (parent, arbitraryDir) => {
// modified from: https://stackoverflow.com/questions/37521893/determine-if-a-path-is-subdirectory-of-another-in-node-js#45242825
const relative = path.relative(parent, arbitraryDir);
const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);

View File

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

View File

@ -14,18 +14,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Stream} from "./Stream";
const Stream = require('./Stream');
const assert = require('assert').strict;
const authorManager = require('../db/AuthorManager');
const hooks = require('../../static/js/pluginfw/hooks');
const padManager = require('../db/PadManager');
import {strict as assert} from "assert";
exports.getPadRaw = async (padId, readOnlyId) => {
import {getAuthor} from "../db/AuthorManager";
import {aCallAll} from "../../static/js/pluginfw/hooks";
import {getPad} from "../db/PadManager";
export const getPadRaw = async (padId, readOnlyId) => {
const dstPfx = `pad:${readOnlyId || padId}`;
const [pad, customPrefixes] = await Promise.all([
padManager.getPad(padId),
hooks.aCallAll('exportEtherpadAdditionalContent'),
getPad(padId),
aCallAll('exportEtherpadAdditionalContent'),
]);
const pluginRecords = await Promise.all(customPrefixes.map(async (customPrefix) => {
const srcPfx = `${customPrefix}:${padId}`;
@ -43,7 +46,7 @@ exports.getPadRaw = async (padId, readOnlyId) => {
const records = (function* () {
for (const authorId of pad.getAllAuthors()) {
yield [`globalAuthor:${authorId}`, (async () => {
const authorEntry = await authorManager.getAuthor(authorId);
const authorEntry = await getAuthor(authorId);
if (!authorEntry) return undefined; // Becomes unset when converted to JSON.
if (authorEntry.padIDs) authorEntry.padIDs = readOnlyId || padId;
return authorEntry;
@ -55,7 +58,7 @@ exports.getPadRaw = async (padId, readOnlyId) => {
})();
const data = {[dstPfx]: pad};
for (const [dstKey, p] of new Stream(records).batch(100).buffer(99)) data[dstKey] = await p;
await hooks.aCallAll('exportEtherpad', {
await aCallAll('exportEtherpad', {
pad,
data,
dstPadId: readOnlyId || padId,

View File

@ -19,14 +19,13 @@
* limitations under the License.
*/
const AttributeMap = require('../../static/js/AttributeMap');
const Changeset = require('../../static/js/Changeset');
import AttributeMap from '../../static/js/AttributeMap';
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
exports.getPadPlainText = (pad, revNum) => {
const _analyzeLine = exports._analyzeLine;
export const getPadPlainText = (pad, revNum) => {
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext);
const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
const attribLines = splitAttributionLines(atext.attribs, atext.text);
const apool = pad.pool;
const pieces = [];
@ -45,14 +44,20 @@ exports.getPadPlainText = (pad, revNum) => {
};
exports._analyzeLine = (text, aline, apool) => {
const line = {};
export const _analyzeLine = (text, aline, apool) => {
const line = {
listLevel: undefined,
text: undefined,
listTypeName: undefined,
aline: undefined,
start: undefined
};
// identify list
let lineMarker = 0;
line.listLevel = 0;
if (aline) {
const [op] = Changeset.deserializeOps(aline);
const [op] = deserializeOps(aline);
if (op != null) {
const attribs = AttributeMap.fromString(op.attribs, apool);
let listType = attribs.get('list');
@ -72,7 +77,7 @@ exports._analyzeLine = (text, aline, apool) => {
}
if (lineMarker) {
line.text = text.substring(1);
line.aline = Changeset.subattribution(aline, 1);
line.aline = subattribution(aline, 1);
} else {
line.text = text;
line.aline = aline;
@ -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)};`);

View File

@ -15,16 +15,27 @@
* limitations under the License.
*/
const Changeset = require('../../static/js/Changeset');
const attributes = require('../../static/js/attributes');
const padManager = require('../db/PadManager');
const _ = require('underscore');
const Security = require('../../static/js/security');
const hooks = require('../../static/js/pluginfw/hooks');
const eejs = require('../eejs');
const _analyzeLine = require('./ExportHelper')._analyzeLine;
const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace;
const padutils = require('../../static/js/pad_utils').padutils;
import {
deserializeOps,
splitAttributionLines,
stringAssembler,
stringIterator,
subattribution
} from '../../static/js/Changeset';
import {decodeAttribString} from "../../static/js/attributes";
import {getPad} from "../db/PadManager";
import _ from "underscore";
// FIXME this is a hack to get around the fact that we don't have a good way
// @ts-ignore
import {escapeHTML,escapeHTMLAttribute} from '../../static/js/security';
import {aCallAll} from '../../static/js/pluginfw/hooks';
import {required} from '../eejs';
import {_analyzeLine, _encodeWhitespace} from "./ExportHelper";
import {padutils} from "../../static/js/pad_utils";
const getPadHTML = async (pad, revNum) => {
let atext = pad.atext;
@ -38,17 +49,17 @@ const getPadHTML = async (pad, revNum) => {
return await getHTMLFromAtext(pad, atext);
};
const getHTMLFromAtext = async (pad, atext, authorColors) => {
export const getHTMLFromAtext = async (pad, atext, authorColors?) => {
const apool = pad.apool();
const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
const attribLines = splitAttributionLines(atext.attribs, atext.text);
const tags = ['h1', 'h2', 'strong', 'em', 'u', 's'];
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
await Promise.all([
// prepare tags stored as ['tag', true] to be exported
hooks.aCallAll('exportHtmlAdditionalTags', pad).then((newProps) => {
aCallAll('exportHtmlAdditionalTags', pad).then((newProps) => {
newProps.forEach((prop) => {
tags.push(prop);
props.push(prop);
@ -56,7 +67,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
}),
// prepare tags stored as ['tag', 'value'] to be exported. This will generate HTML with tags
// like <span data-tag="value">
hooks.aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps) => {
aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps) => {
newProps.forEach((prop) => {
tags.push(`span data-${prop[0]}="${prop[1]}"`);
props.push(prop);
@ -124,8 +135,8 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
// becomes
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
const taker = Changeset.stringIterator(text);
const assem = Changeset.stringAssembler();
const taker = stringIterator(text);
const assem = stringAssembler();
const openTags = [];
const getSpanClassFor = (i) => {
@ -197,7 +208,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
return;
}
const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
const ops = deserializeOps(subattribution(attribs, idx, idx + numChars));
idx += numChars;
// this iterates over every op string and decides which tags to open or to close
@ -206,7 +217,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
const usedAttribs = [];
// mark all attribs as used
for (const a of attributes.decodeAttribString(o.attribs)) {
for (const a of decodeAttribString(o.attribs)) {
if (a in anumMap) {
usedAttribs.push(anumMap[a]); // i = 0 => bold, etc.
}
@ -246,7 +257,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
// from but they break the abiword parser and are completly useless
s = s.replace(String.fromCharCode(12), '');
assem.append(_encodeWhitespace(Security.escapeHTML(s)));
assem.append(_encodeWhitespace(escapeHTML(s)));
} // end iteration over spans in line
// close all the tags that are open after the last op
@ -269,7 +280,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
// https://html.spec.whatwg.org/multipage/links.html#link-type-noopener
// https://mathiasbynens.github.io/rel-noopener/
// https://github.com/ether/etherpad-lite/pull/3636
assem.append(`<a href="${Security.escapeHTMLAttribute(url)}" rel="noreferrer noopener">`);
assem.append(`<a href="${escapeHTMLAttribute(url)}" rel="noreferrer noopener">`);
processNextChars(urlLength);
assem.append('</a>');
});
@ -311,7 +322,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
if (i < textLines.length) {
nextLine = _analyzeLine(textLines[i + 1], attribLines[i + 1], apool);
}
await hooks.aCallAll('getLineHTMLForExport', context);
await aCallAll('getLineHTMLForExport', context);
// To create list parent elements
if ((!prevLine || prevLine.listLevel !== line.listLevel) ||
(line.listTypeName !== prevLine.listTypeName)) {
@ -448,7 +459,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
padId: pad.id,
};
await hooks.aCallAll('getLineHTMLForExport', context);
await aCallAll('getLineHTMLForExport', context);
pieces.push(context.lineContent, '<br>');
}
}
@ -456,25 +467,25 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
return pieces.join('');
};
exports.getPadHTMLDocument = async (padId, revNum, readOnlyId) => {
const pad = await padManager.getPad(padId);
export const getPadHTMLDocument = async (padId, revNum, readOnlyId?) => {
const pad = await getPad(padId);
// Include some Styles into the Head for Export
let stylesForExportCSS = '';
const stylesForExport = await hooks.aCallAll('stylesForExport', padId);
const stylesForExport = await aCallAll('stylesForExport', padId);
stylesForExport.forEach((css) => {
stylesForExportCSS += css;
});
let html = await getPadHTML(pad, revNum);
for (const hookHtml of await hooks.aCallAll('exportHTMLAdditionalContent', {padId})) {
for (const hookHtml of await aCallAll('exportHTMLAdditionalContent', {padId})) {
html += hookHtml;
}
return eejs.require('ep_etherpad-lite/templates/export_html.html', {
return required('ep_etherpad-lite/templates/export_html.html', {
body: html,
padId: Security.escapeHTML(readOnlyId || padId),
padId: escapeHTML(readOnlyId || padId),
extraCSS: stylesForExportCSS,
});
};
@ -525,7 +536,4 @@ const _processSpaces = (s) => {
}
}
return parts.join('');
};
exports.getPadHTML = getPadHTML;
exports.getHTMLFromAtext = getHTMLFromAtext;
}

View File

@ -39,7 +39,7 @@ const getPadTXT = async (pad, revNum) => {
// This is different than the functionality provided in ExportHtml as it provides formatting
// functionality that is designed specifically for TXT exports
const getTXTFromAtext = (pad, atext, authorColors) => {
export const getTXTFromAtext = (pad, atext, authorColors?) => {
const apool = pad.apool();
const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
@ -56,7 +56,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
});
const getLineTXT = (text, attribs) => {
const propVals = [false, false, false];
const propVals:(boolean|number)[] = [false, false, false];
const ENTER = 1;
const STAY = 2;
const LEAVE = 0;
@ -256,9 +256,8 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
return pieces.join('');
};
exports.getTXTFromAtext = getTXTFromAtext;
exports.getPadTXTDocument = async (padId, revNum) => {
export const getPadTXTDocument = async (padId, revNum) => {
const pad = await padManager.getPad(padId);
return getPadTXT(pad, revNum);
};

View File

@ -16,30 +16,32 @@
* limitations under the License.
*/
const AttributePool = require('../../static/js/AttributePool');
const {Pad} = require('../db/Pad');
const Stream = require('./Stream');
const authorManager = require('../db/AuthorManager');
const db = require('../db/DB');
const hooks = require('../../static/js/pluginfw/hooks');
const log4js = require('log4js');
const supportedElems = require('../../static/js/contentcollector').supportedElems;
const ueberdb = require('ueberdb2');
import {AttributePool} from '../../static/js/AttributePool';
import {Pad} from '../db/Pad';
import {Stream} from './Stream';
import {addPad, doesAuthorExist} from '../db/AuthorManager';
import {db} from '../db/DB';
import {aCallAll, callAll} from '../../static/js/pluginfw/hooks';
import log4js from "log4js";
import {supportedElems} from "../../static/js/contentcollector";
import ueberdb from 'ueberdb2';
const logger = log4js.getLogger('ImportEtherpad');
exports.setPadRaw = async (padId, r, authorId = '') => {
export const setPadRaw = async (padId, r, authorId = '') => {
const records = JSON.parse(r);
// get supported block Elements from plugins, we will use this later.
hooks.callAll('ccRegisterBlockElements').forEach((element) => {
callAll('ccRegisterBlockElements').forEach((element) => {
supportedElems.add(element);
});
// DB key prefixes for pad records. Each key is expected to have the form `${prefix}:${padId}` or
// `${prefix}:${padId}:${otherstuff}`.
const padKeyPrefixes = [
...await hooks.aCallAll('exportEtherpadAdditionalContent'),
...await aCallAll('exportEtherpadAdditionalContent'),
'pad',
];
@ -69,7 +71,7 @@ exports.setPadRaw = async (padId, r, authorId = '') => {
throw new TypeError('globalAuthor padIDs subkey is not a string');
}
checkOriginalPadId(value.padIDs);
if (await authorManager.doesAuthorExist(id)) {
if (await doesAuthorExist(id)) {
existingAuthors.add(id);
return;
}
@ -101,7 +103,7 @@ exports.setPadRaw = async (padId, r, authorId = '') => {
const pad = new Pad(padId, padDb);
await pad.init(null, authorId);
await hooks.aCallAll('importEtherpad', {
await aCallAll('importEtherpad', {
pad,
// Shallow freeze meant to prevent accidental bugs. It would be better to deep freeze, but
// it's not worth the added complexity.
@ -115,7 +117,7 @@ exports.setPadRaw = async (padId, r, authorId = '') => {
const writeOps = (function* () {
for (const [k, v] of data) yield db.set(k, v);
for (const a of existingAuthors) yield authorManager.addPad(a, padId);
for (const a of existingAuthors) yield addPad(a as string, padId);
})();
for (const op of new Stream(writeOps).batch(100).buffer(99)) await op;
};

View File

@ -15,15 +15,15 @@
* limitations under the License.
*/
const log4js = require('log4js');
const Changeset = require('../../static/js/Changeset');
const contentcollector = require('../../static/js/contentcollector');
const jsdom = require('jsdom');
import log4js from 'log4js';
import {builder, deserializeOps} from '../../static/js/Changeset';
import {makeContentCollector} from '../../static/js/contentcollector';
import jsdom from 'jsdom';
const apiLogger = log4js.getLogger('ImportHtml');
let processor;
exports.setPadHTML = async (pad, html, authorId = '') => {
export const setPadHTML = async (pad, html, authorId = '') => {
if (processor == null) {
const [{rehype}, {default: minifyWhitespace}] =
await Promise.all([import('rehype'), import('rehype-minify-whitespace')]);
@ -42,7 +42,7 @@ exports.setPadHTML = async (pad, html, authorId = '') => {
// Convert a dom tree into a list of lines and attribute liens
// using the content collector object
const cc = contentcollector.makeContentCollector(true, null, pad.pool);
const cc = makeContentCollector(true, null, pad.pool);
try {
// we use a try here because if the HTML is bad it will blow up
cc.collectContent(document.body);
@ -68,26 +68,26 @@ exports.setPadHTML = async (pad, html, authorId = '') => {
const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`;
// create a new changeset with a helper builder object
const builder = Changeset.builder(1);
const builder2 = builder(1);
// assemble each line into the builder
let textIndex = 0;
const newTextStart = 0;
const newTextEnd = newText.length;
for (const op of Changeset.deserializeOps(newAttribs)) {
for (const op of deserializeOps(newAttribs)) {
const nextIndex = textIndex + op.chars;
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
const start = Math.max(newTextStart, textIndex);
const end = Math.min(newTextEnd, nextIndex);
builder.insert(newText.substring(start, end), op.attribs);
builder2.insert(newText.substring(start, end), op.attribs);
}
textIndex = nextIndex;
}
// the changeset is ready!
const theChangeset = builder.toString();
const theChangeset = builder2.toString();
apiLogger.debug(`The changeset: ${theChangeset}`);
await pad.setText('\n', authorId);
await pad.appendRevision(theChangeset, authorId);
};
}

View File

@ -17,20 +17,23 @@
* limitations under the License.
*/
const async = require('async');
const fs = require('fs').promises;
const log4js = require('log4js');
const os = require('os');
const path = require('path');
const runCmd = require('./run_cmd');
const settings = require('./Settings');
import async from 'async';
import log4js from 'log4js';
import {promises as fs} from "fs";
import os from 'os';
import path from "path";
import exportCMD from "./run_cmd";
import {soffice} from "./Settings";
const logger = log4js.getLogger('LibreOffice');
const doConvertTask = async (task) => {
const tmpDir = os.tmpdir();
const p = runCmd([
settings.soffice,
const p = await exportCMD([
soffice,
'--headless',
'--invisible',
'--nologo',
@ -81,7 +84,7 @@ const queue = async.queue(doConvertTask, 1);
* @param {String} type The type to convert into
* @param {Function} callback Standard callback function
*/
exports.convertFile = async (srcFile, destFile, type) => {
export const convertFile = async (srcFile, destFile, type) => {
// Used for the moving of the file, not the conversion
const fileExtension = type;

View File

@ -21,21 +21,28 @@
* limitations under the License.
*/
const settings = require('./Settings');
const fs = require('fs').promises;
const path = require('path');
const plugins = require('../../static/js/pluginfw/plugin_defs');
const RequireKernel = require('etherpad-require-kernel');
const mime = require('mime-types');
const Threads = require('threads');
const log4js = require('log4js');
import {maxAge, root} from './Settings';
import {promises as fs} from "fs";
import path from "path";
import {plugins} from "../../static/js/pluginfw/plugin_defs";
import RequireKernel from "etherpad-require-kernel";
import mime from "mime-types";
import Threads from "threads";
import log4js from "log4js";
const sanitizePathname = require('./sanitizePathname');
const logger = log4js.getLogger('Minify');
const ROOT_DIR = path.join(settings.root, 'src/static/');
const ROOT_DIR = path.join(root, 'src/static/');
const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2);
const threadsPool = Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2);
const LIBRARY_WHITELIST = [
'async',
@ -89,7 +96,7 @@ const requestURI = async (url, method, headers) => {
return await p;
};
const requestURIs = (locations, method, headers, callback) => {
export const requestURIs = (locations, method, headers, callback) => {
Promise.all(locations.map(async (loc) => {
try {
return await requestURI(loc, method, headers);
@ -148,8 +155,8 @@ const minify = async (req, res) => {
const library = match[1];
const libraryPath = match[2] || '';
if (plugins.plugins[library] && match[3]) {
const plugin = plugins.plugins[library];
if (plugins[library] && match[3]) {
const plugin = plugins[library];
const pluginPath = plugin.package.realPath;
filename = path.join(pluginPath, libraryPath);
// On Windows, path.relative converts forward slashes to backslashes. Convert them back
@ -176,10 +183,10 @@ const minify = async (req, res) => {
date.setMilliseconds(0);
res.setHeader('last-modified', date.toUTCString());
res.setHeader('date', (new Date()).toUTCString());
if (settings.maxAge !== undefined) {
const expiresDate = new Date(Date.now() + settings.maxAge * 1000);
if (maxAge !== undefined) {
const expiresDate = new Date(Date.now() + maxAge * 1000);
res.setHeader('expires', expiresDate.toUTCString());
res.setHeader('cache-control', `max-age=${settings.maxAge}`);
res.setHeader('cache-control', `max-age=${maxAge}`);
}
}
@ -271,7 +278,7 @@ const requireDefinition = () => `var require = ${RequireKernel.kernelSource};\n`
const getFileCompressed = async (filename, contentType) => {
let content = await getFile(filename);
if (!content || !settings.minify) {
if (!content || !minify) {
return content;
} else if (contentType === 'application/javascript') {
return await new Promise((resolve) => {
@ -317,10 +324,8 @@ const getFile = async (filename) => {
return await fs.readFile(path.resolve(ROOT_DIR, filename));
};
exports.minify = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err)));
export const minifyExp = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err)));
exports.requestURIs = requestURIs;
exports.shutdown = async (hookName, context) => {
export const shutdown = async (hookName, context) => {
await threadsPool.terminate();
};

View File

@ -3,11 +3,14 @@
* Worker thread to minify JS & CSS files out of the main NodeJS thread
*/
const CleanCSS = require('clean-css');
const Terser = require('terser');
const fsp = require('fs').promises;
const path = require('path');
const Threads = require('threads');
import CleanCSS from 'clean-css';
import Terser from "terser";
import {promises as fsp} from "fs";
import path from "path";
import Threads from "threads";
const compressJS = (content) => Terser.minify(content);

View File

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

View File

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

View File

@ -4,7 +4,9 @@
* Wrapper around any iterable that adds convenience methods that standard JavaScript iterable
* objects lack.
*/
class Stream {
export class Stream {
private readonly _iter: any;
private _next: null;
/**
* @returns {Stream} A Stream that yields values in the half-open range [start, end).
*/
@ -130,5 +132,3 @@ class Stream {
*/
[Symbol.iterator]() { return this._iter; }
}
module.exports = Stream;

View File

@ -3,16 +3,17 @@
* Tidy up the HTML in a given file
*/
const log4js = require('log4js');
const settings = require('./Settings');
const spawn = require('child_process').spawn;
import log4js from 'log4js';
import {tidyHtml} from "./Settings";
exports.tidy = (srcFile) => {
import {spawn} from "child_process";
export const tidy = (srcFile) => {
const logger = log4js.getLogger('TidyHtml');
return new Promise((resolve, reject) => {
// Don't do anything if Tidy hasn't been enabled
if (!settings.tidyHtml) {
if (!tidyHtml) {
logger.debug('tidyHtml has not been configured yet, ignoring tidy request');
return resolve(null);
}
@ -21,7 +22,7 @@ exports.tidy = (srcFile) => {
// Spawn a new tidy instance that cleans up the file inline
logger.debug(`Tidying ${srcFile}`);
const tidy = spawn(settings.tidyHtml, ['-modify', srcFile]);
const tidy = spawn(tidyHtml, ['-modify', srcFile]);
// Keep track of any error messages
tidy.stderr.on('data', (data) => {

View File

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

View File

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

View File

@ -16,15 +16,20 @@
* limitations under the License.
*/
const Buffer = require('buffer').Buffer;
const fs = require('fs');
const fsp = fs.promises;
const path = require('path');
const zlib = require('zlib');
const settings = require('./Settings');
const existsSync = require('./path_exists');
const util = require('util');
import {Buffer} from "buffer";
import fs, {Stats} from "fs";
import path from "path";
import {check} from './path_exists'
import zlib from "zlib";
import {root} from "./Settings";
import util from "util";
const fsp = fs.promises;
/*
* The crypto module can be absent on reduced node installations.
*
@ -45,8 +50,8 @@ try {
_crypto = undefined;
}
let CACHE_DIR = path.join(settings.root, 'var/');
CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined;
let CACHE_DIR = path.join(root, 'var/');
CACHE_DIR = check(CACHE_DIR) ? CACHE_DIR : undefined;
const responseCache = {};
@ -78,7 +83,7 @@ if (_crypto) {
should replace this.
*/
module.exports = class CachingMiddleware {
export class CachingMiddleware {
handle(req, res, next) {
this._handle(req, res, next).catch((err) => next(err || new Error(err)));
}
@ -88,8 +93,15 @@ module.exports = class CachingMiddleware {
return next(undefined, req, res);
}
const oldReq = {};
const oldRes = {};
const oldReq = {
method: undefined
};
const oldRes = {
write: undefined,
end: undefined,
setHeader: undefined,
writeHead: undefined
};
const supportsGzip =
(req.get('Accept-Encoding') || '').indexOf('gzip') !== -1;
@ -100,7 +112,7 @@ module.exports = class CachingMiddleware {
const stats = await fsp.stat(`${CACHE_DIR}minified_${cacheKey}`).catch(() => {});
const modifiedSince =
req.headers['if-modified-since'] && new Date(req.headers['if-modified-since']);
if (stats != null && stats.mtime && responseCache[cacheKey]) {
if (stats != null && stats instanceof Object && "mtime" in stats && responseCache[cacheKey]) {
req.headers['if-modified-since'] = stats.mtime.toUTCString();
} else {
delete req.headers['if-modified-since'];
@ -200,4 +212,4 @@ module.exports = class CachingMiddleware {
next(undefined, req, res);
}
};
}

View File

@ -7,7 +7,10 @@
* @class CustomError
* @extends {Error}
*/
class CustomError extends Error {
export class CustomError extends Error {
code: any;
signal: any;
easysync: boolean
/**
* Creates an instance of CustomError.
* @param {*} message
@ -20,5 +23,3 @@ class CustomError extends Error {
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = CustomError;

View File

@ -1,11 +1,21 @@
'use strict';
const AttributeMap = require('../../static/js/AttributeMap');
const Changeset = require('../../static/js/Changeset');
const attributes = require('../../static/js/attributes');
const exportHtml = require('./ExportHtml');
import AttributeMap from '../../static/js/AttributeMap';
import {
applyToAText,
builder, checkRep,
compose,
deserializeOps,
numToString, Op,
opAssembler, pack, splitAttributionLines, splitTextLines, stringAssembler,
unpack
} from '../../static/js/Changeset';
import {attribsFromString} from '../../static/js/attributes';
import {getHTMLFromAtext} from './ExportHtml';
// @ts-ignore
import {PadDiffModel} from "ep_etherpad-lite/node/models/PadDiffModel";
function PadDiff(pad, fromRev, toRev) {
export const PadDiff = (pad, fromRev, toRev)=> {
// check parameters
if (!pad || !pad.id || !pad.atext || !pad.pool) {
throw new Error('Invalid pad');
@ -14,16 +24,22 @@ function PadDiff(pad, fromRev, toRev) {
const range = pad.getValidRevisionRange(fromRev, toRev);
if (!range) throw new Error(`Invalid revision range. startRev: ${fromRev} endRev: ${toRev}`);
// FIXME How to fix this?
// @ts-ignore
this._pad = pad;
// @ts-ignore
this._fromRev = range.startRev;
// @ts-ignore
this._toRev = range.endRev;
// @ts-ignore
this._html = null;
// @ts-ignore
this._authors = [];
}
PadDiff.prototype._isClearAuthorship = function (changeset) {
// unpack
const unpacked = Changeset.unpack(changeset);
const unpacked = unpack(changeset);
// check if there is nothing in the charBank
if (unpacked.charBank !== '') {
@ -35,7 +51,7 @@ PadDiff.prototype._isClearAuthorship = function (changeset) {
return false;
}
const [clearOperator, anotherOp] = Changeset.deserializeOps(unpacked.ops);
const [clearOperator, anotherOp] = deserializeOps(unpacked.ops);
// check if there is only one operator
if (anotherOp != null) return false;
@ -52,7 +68,7 @@ PadDiff.prototype._isClearAuthorship = function (changeset) {
}
const [appliedAttribute, anotherAttribute] =
attributes.attribsFromString(clearOperator.attribs, this._pad.pool);
attribsFromString(clearOperator.attribs, this._pad.pool);
// Check that the operation has exactly one attribute.
if (appliedAttribute == null || anotherAttribute != null) return false;
@ -69,9 +85,9 @@ PadDiff.prototype._createClearAuthorship = async function (rev) {
const atext = await this._pad.getInternalRevisionAText(rev);
// build clearAuthorship changeset
const builder = Changeset.builder(atext.text.length);
builder.keepText(atext.text, [['author', '']], this._pad.pool);
const changeset = builder.toString();
const builder2 = builder(atext.text.length);
builder2.keepText(atext.text, [['author', '']], this._pad.pool);
const changeset = builder2.toString();
return changeset;
};
@ -84,7 +100,7 @@ PadDiff.prototype._createClearStartAtext = async function (rev) {
const changeset = await this._createClearAuthorship(rev);
// apply the clearAuthorship changeset
const newAText = Changeset.applyToAText(changeset, atext, this._pad.pool);
const newAText = applyToAText(changeset, atext, this._pad.pool);
return newAText;
};
@ -108,9 +124,11 @@ PadDiff.prototype._getChangesetsInBulk = async function (startRev, count) {
return {changesets, authors};
};
PadDiff.prototype._addAuthors = function (authors) {
const self = this;
PadDiff.prototype._addAuthors = (authors)=> {
let self: undefined|PadDiffModel = this;
if(!self){
self = {_authors: []}
}
// add to array if not in the array
authors.forEach((author) => {
if (self._authors.indexOf(author) === -1) {
@ -151,7 +169,7 @@ PadDiff.prototype._createDiffAtext = async function () {
if (superChangeset == null) {
superChangeset = changeset;
} else {
superChangeset = Changeset.compose(superChangeset, changeset, this._pad.pool);
superChangeset = compose(superChangeset, changeset, this._pad.pool);
}
}
@ -165,10 +183,10 @@ PadDiff.prototype._createDiffAtext = async function () {
const deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool);
// apply the superChangeset, which includes all addings
atext = Changeset.applyToAText(superChangeset, atext, this._pad.pool);
atext = applyToAText(superChangeset, atext, this._pad.pool);
// apply the deletionChangeset, which adds a deletions
atext = Changeset.applyToAText(deletionChangeset, atext, this._pad.pool);
atext = applyToAText(deletionChangeset, atext, this._pad.pool);
}
return atext;
@ -187,7 +205,7 @@ PadDiff.prototype.getHtml = async function () {
const authorColors = await this._pad.getAllAuthorColors();
// convert the atext to html
this._html = await exportHtml.getHTMLFromAtext(this._pad, atext, authorColors);
this._html = await getHTMLFromAtext(this._pad, atext, authorColors);
return this._html;
};
@ -198,28 +216,32 @@ PadDiff.prototype.getAuthors = async function () {
if (this._html == null) {
await this.getHtml();
}
let self: undefined|PadDiffModel = this;
if(!self){
self = {_authors: []}
}
return self._authors;
};
PadDiff.prototype._extendChangesetWithAuthor = (changeset, author, apool) => {
// unpack
const unpacked = Changeset.unpack(changeset);
const unpacked = unpack(changeset);
const assem = Changeset.opAssembler();
const assem = opAssembler();
// create deleted attribs
const authorAttrib = apool.putAttrib(['author', author || '']);
const deletedAttrib = apool.putAttrib(['removed', true]);
const attribs = `*${Changeset.numToString(authorAttrib)}*${Changeset.numToString(deletedAttrib)}`;
const attribs = `*${numToString(authorAttrib)}*${numToString(deletedAttrib)}`;
for (const operator of Changeset.deserializeOps(unpacked.ops)) {
for (const operator of deserializeOps(unpacked.ops)) {
if (operator.opcode === '-') {
// this is a delete operator, extend it with the author
operator.attribs = attribs;
} else if (operator.opcode === '=' && operator.attribs) {
// this is operator changes only attributes, let's mark which author did that
operator.attribs += `*${Changeset.numToString(authorAttrib)}`;
operator.attribs += `*${numToString(authorAttrib)}`;
}
// append the new operator to our assembler
@ -227,21 +249,21 @@ PadDiff.prototype._extendChangesetWithAuthor = (changeset, author, apool) => {
}
// return the modified changeset
return Changeset.pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank);
return pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank);
};
// this method is 80% like Changeset.inverse. I just changed so instead of reverting,
// it adds deletions and attribute changes to to the atext.
PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
const lines = Changeset.splitTextLines(startAText.text);
const alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text);
const lines = splitTextLines(startAText.text);
const alines = splitAttributionLines(startAText.attribs, startAText.text);
// lines and alines are what the exports is meant to apply to.
// They may be arrays or objects with .get(i) and .length methods.
// They include final newlines on lines.
const linesGet = (idx) => {
if (lines.get) {
if ("get" in lines && lines.get instanceof Function) {
return lines.get(idx);
} else {
return lines[idx];
@ -249,7 +271,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
};
const aLinesGet = (idx) => {
if (alines.get) {
if ("get" in alines && alines.get instanceof Function) {
return alines.get(idx);
} else {
return alines[idx];
@ -261,14 +283,14 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
let curLineOps = null;
let curLineOpsNext = null;
let curLineOpsLine;
let curLineNextOp = new Changeset.Op('+');
let curLineNextOp = new Op('+');
const unpacked = Changeset.unpack(cs);
const builder = Changeset.builder(unpacked.newLen);
const unpacked = unpack(cs);
const builder2 = builder(unpacked.newLen);
const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => {
if (!curLineOps || curLineOpsLine !== curLine) {
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
curLineOps = deserializeOps(aLinesGet(curLine));
curLineOpsNext = curLineOps.next();
curLineOpsLine = curLine;
let indexIntoLine = 0;
@ -289,13 +311,13 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
curChar = 0;
curLineOpsLine = curLine;
curLineNextOp.chars = 0;
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
curLineOps = deserializeOps(aLinesGet(curLine));
curLineOpsNext = curLineOps.next();
}
if (!curLineNextOp.chars) {
if (curLineOpsNext.done) {
curLineNextOp = new Changeset.Op();
curLineNextOp = new Op();
} else {
curLineNextOp = curLineOpsNext.value;
curLineOpsNext = curLineOps.next();
@ -330,7 +352,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
const nextText = (numChars) => {
let len = 0;
const assem = Changeset.stringAssembler();
const assem = stringAssembler();
const firstString = linesGet(curLine).substring(curChar);
len += firstString.length;
assem.append(firstString);
@ -358,7 +380,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
};
};
for (const csOp of Changeset.deserializeOps(unpacked.ops)) {
for (const csOp of deserializeOps(unpacked.ops)) {
if (csOp.opcode === '=') {
const textBank = nextText(csOp.chars);
@ -404,7 +426,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
textLeftToProcess = textLeftToProcess.substr(lengthToProcess);
if (lineBreak) {
builder.keep(1, 1); // just skip linebreaks, don't do a insert + keep for a linebreak
builder2.keep(1, 1); // just skip linebreaks, don't do a insert + keep for a linebreak
// consume the attributes of this linebreak
consumeAttribRuns(1, () => {});
@ -416,31 +438,31 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
// get the old attributes back
const oldAttribs = undoBackToAttribs(attribs);
builder.insert(processText.substr(textBankIndex, len), oldAttribs);
builder2.insert(processText.substr(textBankIndex, len), oldAttribs);
textBankIndex += len;
});
builder.keep(lengthToProcess, 0);
builder2.keep(lengthToProcess, 0);
}
}
} else {
skip(csOp.chars, csOp.lines);
builder.keep(csOp.chars, csOp.lines);
builder2.keep(csOp.chars, csOp.lines);
}
} else if (csOp.opcode === '+') {
builder.keep(csOp.chars, csOp.lines);
builder2.keep(csOp.chars, csOp.lines);
} else if (csOp.opcode === '-') {
const textBank = nextText(csOp.chars);
let textBankIndex = 0;
consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => {
builder.insert(textBank.substr(textBankIndex, len), attribs + csOp.attribs);
builder2.insert(textBank.substr(textBankIndex, len), attribs + csOp.attribs);
textBankIndex += len;
});
}
}
return Changeset.checkRep(builder.toString());
return checkRep(builder.toString());
};
// export the constructor

Some files were not shown because too many files have changed in this diff Show More