Compare commits
11 Commits
develop
...
feature/ty
Author | SHA1 | Date |
---|---|---|
SamTV12345 | f6d7939b9e | |
SamTV12345 | 9a3b600666 | |
SamTV12345 | 8ddac2db45 | |
SamTV12345 | 8926677a66 | |
SamTV12345 | 798543fb45 | |
SamTV12345 | aa6323e488 | |
SamTV12345 | 76a6f665a4 | |
SamTV12345 | 237f7242ec | |
SamTV12345 | 7b99edc471 | |
SamTV12345 | 3c2129b1cc | |
SamTV12345 | 331cf3d79f |
|
@ -1 +0,0 @@
|
|||
../src
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env"]
|
||||
}
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
dist/
|
||||
node_modules
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 0 B |
|
@ -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 "$@"
|
||||
|
|
|
@ -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
|
|
@ -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;
|
|
@ -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}
|
|
@ -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) {
|
|
@ -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;
|
|
@ -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);
|
||||
};
|
|
@ -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};
|
||||
};
|
|
@ -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');
|
|
@ -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}`);
|
|
@ -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;
|
|
@ -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;
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
|
@ -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 client’s 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();
|
||||
};
|
|
@ -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();
|
||||
};
|
|
@ -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();
|
||||
};
|
|
@ -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});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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});
|
||||
})
|
||||
}
|
|
@ -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();
|
|
@ -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)));
|
||||
});
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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();
|
||||
};
|
|
@ -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,
|
||||
}));
|
|
@ -19,8 +19,9 @@ const getTar = async () => {
|
|||
};
|
||||
const tarJson = await fs.readFile(path.join(settings.root, 'src/node/utils/tar.json'), 'utf8');
|
||||
const tar = {};
|
||||
for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) {
|
||||
const files = relativeFiles.map(prefixLocalLibraryPath);
|
||||
for (let [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) {
|
||||
const relativeFilesMapped = relativeFiles as string[];
|
||||
const files = relativeFilesMapped.map(prefixLocalLibraryPath);
|
||||
tar[prefixLocalLibraryPath(key)] = files
|
||||
.concat(files.map((p) => p.replace(/\.js$/, '')))
|
||||
.concat(files.map((p) => `${p.replace(/\.js$/, '')}/index.js`));
|
||||
|
@ -28,7 +29,7 @@ const getTar = async () => {
|
|||
return tar;
|
||||
};
|
||||
|
||||
exports.expressPreSession = async (hookName, {app}) => {
|
||||
export const expressPreSession = async (hookName, {app}) => {
|
||||
// Cache both minified and static.
|
||||
const assetCache = new CachingMiddleware();
|
||||
app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache));
|
||||
|
@ -62,8 +63,9 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
const clientParts = plugins.parts.filter((part) => part.client_hooks != null);
|
||||
const clientPlugins = {};
|
||||
for (const name of new Set(clientParts.map((part) => part.plugin))) {
|
||||
clientPlugins[name] = {...plugins.plugins[name]};
|
||||
delete clientPlugins[name].package;
|
||||
const mappedName = name as any
|
||||
clientPlugins[mappedName] = {...plugins.plugins[mappedName]};
|
||||
delete clientPlugins[mappedName].package;
|
||||
}
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
|
@ -1,11 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fsp = require('fs').promises;
|
||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const sanitizePathname = require('../../utils/sanitizePathname');
|
||||
const settings = require('../../utils/Settings');
|
||||
import path from 'path';
|
||||
import {promises as fsp} from "fs";
|
||||
|
||||
import {plugins} from "../../../static/js/pluginfw/plugin_defs";
|
||||
|
||||
import sanitizePathname from "../../utils/sanitizePathname";
|
||||
|
||||
import {enableAdminUITests, root} from "../../utils/Settings";
|
||||
import {Presession} from "../../models/Presession";
|
||||
// Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/'
|
||||
// instead of path.sep to separate pathname components.
|
||||
const findSpecs = async (specDir) => {
|
||||
|
@ -29,16 +32,17 @@ const findSpecs = async (specDir) => {
|
|||
return specs;
|
||||
};
|
||||
|
||||
exports.expressPreSession = async (hookName, {app}) => {
|
||||
export const expressPreSession = async (hookName, {app}) => {
|
||||
app.get('/tests/frontend/frontendTestSpecs.json', (req, res, next) => {
|
||||
(async () => {
|
||||
const modules = [];
|
||||
await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => {
|
||||
let {package: {path: pluginPath}} = def;
|
||||
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('?'));
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
});
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export type CMDArgv = {
|
||||
[key: string]: any
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
export type CMDOptions = {
|
||||
cwd?: string,
|
||||
stdio?: string|any[],
|
||||
env?: NodeJS.ProcessEnv
|
||||
}
|
||||
|
||||
export type CMDPromise = {
|
||||
stdout: string,
|
||||
stderr: string,
|
||||
child: any
|
||||
}
|
|
@ -0,0 +1,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 = {
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export type LogLevel = "DEBUG"|"INFO"|"WARN"|"ERROR"
|
|
@ -0,0 +1,3 @@
|
|||
export type PadDiffModel = {
|
||||
_authors: any[]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export type Plugin = {
|
||||
package: {
|
||||
name: string,
|
||||
version: string
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export type Presession = {
|
||||
package:{
|
||||
path: string,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export type Revision = {
|
||||
revNum: number,
|
||||
savedById: string,
|
||||
label: string,
|
||||
timestamp: number,
|
||||
id: string,
|
||||
}
|
|
@ -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
|
||||
}}
|
|
@ -0,0 +1,5 @@
|
|||
export type SessionModel = {
|
||||
cookie: {
|
||||
expires?: string
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
type SessionSocketModel = {
|
||||
session:{
|
||||
user?: {
|
||||
username?: string,
|
||||
is_admin?: boolean
|
||||
readOnly?: boolean,
|
||||
padAuthorizations?: any
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export type UserIndexedModel = {
|
||||
[key: string]: {
|
||||
password?: string|undefined,
|
||||
}
|
||||
}
|
|
@ -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') {
|
|
@ -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"));
|
|
@ -1,9 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const measured = require('measured-core');
|
||||
|
||||
module.exports = measured.createCollection();
|
||||
|
||||
module.exports.shutdown = async (hookName, context) => {
|
||||
module.exports.end();
|
||||
};
|
|
@ -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();
|
||||
}
|
|
@ -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});
|
||||
};
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
|
@ -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,
|
|
@ -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)};`);
|
|
@ -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;
|
||||
}
|
|
@ -39,7 +39,7 @@ const getPadTXT = async (pad, revNum) => {
|
|||
|
||||
// This is different than the functionality provided in ExportHtml as it provides formatting
|
||||
// functionality that is designed specifically for TXT exports
|
||||
const getTXTFromAtext = (pad, atext, authorColors) => {
|
||||
export const getTXTFromAtext = (pad, atext, authorColors?) => {
|
||||
const apool = pad.apool();
|
||||
const textLines = atext.text.slice(0, -1).split('\n');
|
||||
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
||||
|
@ -56,7 +56,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
|
|||
});
|
||||
|
||||
const getLineTXT = (text, attribs) => {
|
||||
const propVals = [false, false, false];
|
||||
const propVals:(boolean|number)[] = [false, false, false];
|
||||
const ENTER = 1;
|
||||
const STAY = 2;
|
||||
const LEAVE = 0;
|
||||
|
@ -256,9 +256,8 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
|
|||
return pieces.join('');
|
||||
};
|
||||
|
||||
exports.getTXTFromAtext = getTXTFromAtext;
|
||||
|
||||
exports.getPadTXTDocument = async (padId, revNum) => {
|
||||
export const getPadTXTDocument = async (padId, revNum) => {
|
||||
const pad = await padManager.getPad(padId);
|
||||
return getPadTXT(pad, revNum);
|
||||
};
|
|
@ -16,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;
|
||||
};
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -17,20 +17,23 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const async = require('async');
|
||||
const fs = require('fs').promises;
|
||||
const log4js = require('log4js');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const runCmd = require('./run_cmd');
|
||||
const settings = require('./Settings');
|
||||
import async from 'async';
|
||||
import log4js from 'log4js';
|
||||
import {promises as fs} from "fs";
|
||||
|
||||
import os from 'os';
|
||||
import path from "path";
|
||||
|
||||
import exportCMD from "./run_cmd";
|
||||
|
||||
import {soffice} from "./Settings";
|
||||
|
||||
const logger = log4js.getLogger('LibreOffice');
|
||||
|
||||
const doConvertTask = async (task) => {
|
||||
const tmpDir = os.tmpdir();
|
||||
const p = runCmd([
|
||||
settings.soffice,
|
||||
const p = await exportCMD([
|
||||
soffice,
|
||||
'--headless',
|
||||
'--invisible',
|
||||
'--nologo',
|
||||
|
@ -81,7 +84,7 @@ const queue = async.queue(doConvertTask, 1);
|
|||
* @param {String} type The type to convert into
|
||||
* @param {Function} callback Standard callback function
|
||||
*/
|
||||
exports.convertFile = async (srcFile, destFile, type) => {
|
||||
export const convertFile = async (srcFile, destFile, type) => {
|
||||
// Used for the moving of the file, not the conversion
|
||||
const fileExtension = type;
|
||||
|
|
@ -21,21 +21,28 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const settings = require('./Settings');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const plugins = require('../../static/js/pluginfw/plugin_defs');
|
||||
const RequireKernel = require('etherpad-require-kernel');
|
||||
const mime = require('mime-types');
|
||||
const Threads = require('threads');
|
||||
const log4js = require('log4js');
|
||||
import {maxAge, root} from './Settings';
|
||||
import {promises as fs} from "fs";
|
||||
|
||||
import path from "path";
|
||||
|
||||
import {plugins} from "../../static/js/pluginfw/plugin_defs";
|
||||
|
||||
import RequireKernel from "etherpad-require-kernel";
|
||||
|
||||
import mime from "mime-types";
|
||||
|
||||
import Threads from "threads";
|
||||
|
||||
import log4js from "log4js";
|
||||
|
||||
const sanitizePathname = require('./sanitizePathname');
|
||||
|
||||
const logger = log4js.getLogger('Minify');
|
||||
|
||||
const ROOT_DIR = path.join(settings.root, 'src/static/');
|
||||
const ROOT_DIR = path.join(root, 'src/static/');
|
||||
|
||||
const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2);
|
||||
const threadsPool = Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2);
|
||||
|
||||
const LIBRARY_WHITELIST = [
|
||||
'async',
|
||||
|
@ -89,7 +96,7 @@ const requestURI = async (url, method, headers) => {
|
|||
return await p;
|
||||
};
|
||||
|
||||
const requestURIs = (locations, method, headers, callback) => {
|
||||
export const requestURIs = (locations, method, headers, callback) => {
|
||||
Promise.all(locations.map(async (loc) => {
|
||||
try {
|
||||
return await requestURI(loc, method, headers);
|
||||
|
@ -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();
|
||||
};
|
|
@ -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);
|
||||
|
|
@ -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}
|
|
@ -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;
|
||||
}
|
|
@ -4,7 +4,9 @@
|
|||
* Wrapper around any iterable that adds convenience methods that standard JavaScript iterable
|
||||
* objects lack.
|
||||
*/
|
||||
class Stream {
|
||||
export class Stream {
|
||||
private readonly _iter: any;
|
||||
private _next: null;
|
||||
/**
|
||||
* @returns {Stream} A Stream that yields values in the half-open range [start, end).
|
||||
*/
|
||||
|
@ -130,5 +132,3 @@ class Stream {
|
|||
*/
|
||||
[Symbol.iterator]() { return this._iter; }
|
||||
}
|
||||
|
||||
module.exports = Stream;
|
|
@ -3,16 +3,17 @@
|
|||
* Tidy up the HTML in a given file
|
||||
*/
|
||||
|
||||
const log4js = require('log4js');
|
||||
const settings = require('./Settings');
|
||||
const spawn = require('child_process').spawn;
|
||||
import log4js from 'log4js';
|
||||
import {tidyHtml} from "./Settings";
|
||||
|
||||
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) => {
|
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
};
|
|
@ -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}
|
|
@ -16,15 +16,20 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Buffer = require('buffer').Buffer;
|
||||
const fs = require('fs');
|
||||
const fsp = fs.promises;
|
||||
const path = require('path');
|
||||
const zlib = require('zlib');
|
||||
const settings = require('./Settings');
|
||||
const existsSync = require('./path_exists');
|
||||
const util = require('util');
|
||||
import {Buffer} from "buffer";
|
||||
|
||||
import fs, {Stats} from "fs";
|
||||
|
||||
import path from "path";
|
||||
|
||||
import {check} from './path_exists'
|
||||
import zlib from "zlib";
|
||||
|
||||
import {root} from "./Settings";
|
||||
|
||||
import util from "util";
|
||||
|
||||
const fsp = fs.promises;
|
||||
/*
|
||||
* The crypto module can be absent on reduced node installations.
|
||||
*
|
||||
|
@ -45,8 +50,8 @@ try {
|
|||
_crypto = undefined;
|
||||
}
|
||||
|
||||
let CACHE_DIR = path.join(settings.root, 'var/');
|
||||
CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined;
|
||||
let CACHE_DIR = path.join(root, 'var/');
|
||||
CACHE_DIR = check(CACHE_DIR) ? CACHE_DIR : undefined;
|
||||
|
||||
const responseCache = {};
|
||||
|
||||
|
@ -78,7 +83,7 @@ if (_crypto) {
|
|||
should replace this.
|
||||
*/
|
||||
|
||||
module.exports = class CachingMiddleware {
|
||||
export class CachingMiddleware {
|
||||
handle(req, res, next) {
|
||||
this._handle(req, res, next).catch((err) => next(err || new Error(err)));
|
||||
}
|
||||
|
@ -88,8 +93,15 @@ module.exports = class CachingMiddleware {
|
|||
return next(undefined, req, res);
|
||||
}
|
||||
|
||||
const oldReq = {};
|
||||
const oldRes = {};
|
||||
const oldReq = {
|
||||
method: undefined
|
||||
};
|
||||
const oldRes = {
|
||||
write: undefined,
|
||||
end: undefined,
|
||||
setHeader: undefined,
|
||||
writeHead: undefined
|
||||
};
|
||||
|
||||
const supportsGzip =
|
||||
(req.get('Accept-Encoding') || '').indexOf('gzip') !== -1;
|
||||
|
@ -100,7 +112,7 @@ module.exports = class CachingMiddleware {
|
|||
const stats = await fsp.stat(`${CACHE_DIR}minified_${cacheKey}`).catch(() => {});
|
||||
const modifiedSince =
|
||||
req.headers['if-modified-since'] && new Date(req.headers['if-modified-since']);
|
||||
if (stats != null && stats.mtime && responseCache[cacheKey]) {
|
||||
if (stats != null && stats instanceof Object && "mtime" in stats && responseCache[cacheKey]) {
|
||||
req.headers['if-modified-since'] = stats.mtime.toUTCString();
|
||||
} else {
|
||||
delete req.headers['if-modified-since'];
|
||||
|
@ -200,4 +212,4 @@ module.exports = class CachingMiddleware {
|
|||
|
||||
next(undefined, req, res);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -7,7 +7,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;
|
|
@ -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
Loading…
Reference in New Issue