Merge branch 'develop' of github.com:ether/etherpad-lite into 3227-tests

This commit is contained in:
John McLear 2021-02-02 22:51:03 +00:00
commit 4682460b00
150 changed files with 6320 additions and 6944 deletions

View file

@ -49,12 +49,32 @@ jobs:
sudo apt update sudo apt update
sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport
- name: Install Etherpad plugins
run: >
npm install
ep_align
ep_author_hover
ep_cursortrace
ep_font_size
ep_hash_auth
ep_headings2
ep_markdown
ep_readonly_guest
ep_set_title_on_pad
ep_spellcheck
ep_subscript_and_superscript
ep_table_of_contents
# This must be run after installing the plugins, otherwise npm will try to
# hoist common dependencies by removing them from src/node_modules and
# installing them in the top-level node_modules. As of v6.14.10, npm's hoist
# logic appears to be buggy, because it sometimes removes dependencies from
# src/node_modules but fails to add them to the top-level node_modules. Even
# if npm correctly hoists the dependencies, the hoisting seems to confuse
# tools such as `npm outdated`, `npm update`, and some ESLint rules.
- name: Install all dependencies and symlink for ep_etherpad-lite - name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh run: bin/installDeps.sh
- name: Install etherpad plugins
run: npm install ep_align ep_author_hover ep_cursortrace ep_font_size ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest ep_spellcheck ep_subscript_and_superscript ep_table_of_contents
# configures some settings and runs npm run test # configures some settings and runs npm run test
- name: Run the backend tests - name: Run the backend tests
run: tests/frontend/travis/runnerBackend.sh run: tests/frontend/travis/runnerBackend.sh

View file

@ -55,12 +55,32 @@ jobs:
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }} TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
run: tests/frontend/travis/sauce_tunnel.sh run: tests/frontend/travis/sauce_tunnel.sh
- name: Install Etherpad plugins
run: >
npm install
ep_align
ep_author_hover
ep_cursortrace
ep_font_size
ep_hash_auth
ep_headings2
ep_markdown
ep_readonly_guest
ep_set_title_on_pad
ep_spellcheck
ep_subscript_and_superscript
ep_table_of_contents
# This must be run after installing the plugins, otherwise npm will try to
# hoist common dependencies by removing them from src/node_modules and
# installing them in the top-level node_modules. As of v6.14.10, npm's hoist
# logic appears to be buggy, because it sometimes removes dependencies from
# src/node_modules but fails to add them to the top-level node_modules. Even
# if npm correctly hoists the dependencies, the hoisting seems to confuse
# tools such as `npm outdated`, `npm update`, and some ESLint rules.
- name: Install all dependencies and symlink for ep_etherpad-lite - name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh run: bin/installDeps.sh
- name: Install etherpad plugins
run: npm install ep_align ep_author_hover ep_cursortrace ep_font_size ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest ep_spellcheck ep_subscript_and_superscript ep_table_of_contents ep_set_title_on_pad
- name: export GIT_HASH to env - name: export GIT_HASH to env
id: environment id: environment
run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})"

View file

@ -39,14 +39,34 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh
- name: Install etherpad-load-test - name: Install etherpad-load-test
run: sudo npm install -g etherpad-load-test run: sudo npm install -g etherpad-load-test
- name: Install etherpad plugins - name: Install etherpad plugins
run: npm install ep_align ep_author_hover ep_cursortrace ep_font_size ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest ep_spellcheck ep_subscript_and_superscript ep_table_of_contents run: >
npm install
ep_align
ep_author_hover
ep_cursortrace
ep_font_size
ep_hash_auth
ep_headings2
ep_markdown
ep_readonly_guest
ep_set_title_on_pad
ep_spellcheck
ep_subscript_and_superscript
ep_table_of_contents
# This must be run after installing the plugins, otherwise npm will try to
# hoist common dependencies by removing them from src/node_modules and
# installing them in the top-level node_modules. As of v6.14.10, npm's hoist
# logic appears to be buggy, because it sometimes removes dependencies from
# src/node_modules but fails to add them to the top-level node_modules. Even
# if npm correctly hoists the dependencies, the hoisting seems to confuse
# tools such as `npm outdated`, `npm update`, and some ESLint rules.
- name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh
# configures some settings and runs npm run test # configures some settings and runs npm run test
- name: Run load test - name: Run load test

View file

@ -1,4 +1,8 @@
# Changes for the next release # Changes for the next release
### Compatibility changes
* Node.js 10.17.0 or newer is now required.
### Notable new features ### Notable new features
* Database performance is significantly improved. * Database performance is significantly improved.

View file

@ -13,7 +13,7 @@ Etherpad is a real-time collaborative editor [scalable to thousands of simultane
# Installation # Installation
## Requirements ## Requirements
- `nodejs` >= **10.13.0**. - `nodejs` >= **10.17.0**.
## GNU/Linux and other UNIX-like systems ## GNU/Linux and other UNIX-like systems
@ -25,7 +25,7 @@ git clone --branch master https://github.com/ether/etherpad-lite.git && cd ether
``` ```
### Manual install ### Manual install
You'll need git and [node.js](https://nodejs.org) installed (minimum required Node version: **10.13.0**). You'll need git and [node.js](https://nodejs.org) installed (minimum required Node version: **10.17.0**).
**As any user (we recommend creating a separate user called etherpad):** **As any user (we recommend creating a separate user called etherpad):**

View file

@ -7,12 +7,14 @@
// unhandled rejection into an uncaught exception, which does cause Node.js to exit. // unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; }); process.on('unhandledRejection', (err) => { throw err; });
const npm = require('ep_etherpad-lite/node_modules/npm');
const util = require('util');
if (process.argv.length !== 2) throw new Error('Use: node bin/checkAllPads.js'); if (process.argv.length !== 2) throw new Error('Use: node bin/checkAllPads.js');
// load and initialize NPM (async () => {
const npm = require('ep_etherpad-lite/node_modules/npm'); await util.promisify(npm.load)({});
npm.load({}, async () => {
try {
// initialize the database // initialize the database
require('ep_etherpad-lite/node/utils/Settings'); require('ep_etherpad-lite/node/utils/Settings');
const db = require('ep_etherpad-lite/node/db/DB'); const db = require('ep_etherpad-lite/node/db/DB');
@ -30,7 +32,7 @@ npm.load({}, async () => {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
// check if the pad has a pool // check if the pad has a pool
if (pad.pool === undefined) { if (pad.pool == null) {
console.error(`[${pad.id}] Missing attribute pool`); console.error(`[${pad.id}] Missing attribute pool`);
continue; continue;
} }
@ -66,13 +68,13 @@ npm.load({}, async () => {
} }
// check if there is a atext in the keyRevisions // check if there is a atext in the keyRevisions
if (revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) { let {meta: {atext} = {}} = revisions[keyRev];
if (atext == null) {
console.error(`[${pad.id}] Missing atext in revision ${keyRev}`); console.error(`[${pad.id}] Missing atext in revision ${keyRev}`);
continue; continue;
} }
const apool = pad.pool; const apool = pad.pool;
let atext = revisions[keyRev].meta.atext;
for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) { for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) {
try { try {
const cs = revisions[rev].changeset; const cs = revisions[rev].changeset;
@ -88,8 +90,4 @@ npm.load({}, async () => {
throw new Error('No revisions tested'); throw new Error('No revisions tested');
} }
console.log(`Finished: Tested ${revTestedCount} revisions`); console.log(`Finished: Tested ${revTestedCount} revisions`);
} catch (err) { })();
console.trace(err);
throw err;
}
});

View file

@ -7,16 +7,18 @@
// unhandled rejection into an uncaught exception, which does cause Node.js to exit. // unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; }); process.on('unhandledRejection', (err) => { throw err; });
const npm = require('ep_etherpad-lite/node_modules/npm');
const util = require('util');
if (process.argv.length !== 3) throw new Error('Use: node bin/checkPad.js $PADID'); if (process.argv.length !== 3) throw new Error('Use: node bin/checkPad.js $PADID');
// get the padID // get the padID
const padId = process.argv[2]; const padId = process.argv[2];
let checkRevisionCount = 0; let checkRevisionCount = 0;
// load and initialize NPM; (async () => {
const npm = require('ep_etherpad-lite/node_modules/npm'); await util.promisify(npm.load)({});
npm.load({}, async () => {
try {
// initialize database // initialize database
require('ep_etherpad-lite/node/utils/Settings'); require('ep_etherpad-lite/node/utils/Settings');
const db = require('ep_etherpad-lite/node/db/DB'); const db = require('ep_etherpad-lite/node/db/DB');
@ -59,18 +61,16 @@ npm.load({}, async () => {
} }
// check if the pad has a pool // check if the pad has a pool
if (pad.pool === undefined) throw new Error('Attribute pool is missing'); if (pad.pool == null) throw new Error('Attribute pool is missing');
// check if there is an atext in the keyRevisions // check if there is an atext in the keyRevisions
if (revisions[keyRev] === undefined || let {meta: {atext} = {}} = revisions[keyRev] || {};
revisions[keyRev].meta === undefined || if (atext == null) {
revisions[keyRev].meta.atext === undefined) {
console.error(`No atext in key revision ${keyRev}`); console.error(`No atext in key revision ${keyRev}`);
continue; continue;
} }
const apool = pad.pool; const apool = pad.pool;
let atext = revisions[keyRev].meta.atext;
for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) { for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) {
checkRevisionCount++; checkRevisionCount++;
@ -84,8 +84,4 @@ npm.load({}, async () => {
} }
console.log(`Finished: Checked ${checkRevisionCount} revisions`); console.log(`Finished: Checked ${checkRevisionCount} revisions`);
} }
} catch (err) { })();
console.trace(err);
throw err;
}
});

View file

@ -12,12 +12,14 @@ if (process.argv.length !== 3) throw new Error('Use: node bin/checkPadDeltas.js
// get the padID // get the padID
const padId = process.argv[2]; const padId = process.argv[2];
// load and initialize NPM;
const expect = require('../tests/frontend/lib/expect'); const expect = require('../tests/frontend/lib/expect');
const diff = require('ep_etherpad-lite/node_modules/diff'); const diff = require('ep_etherpad-lite/node_modules/diff');
const npm = require('ep_etherpad-lite/node_modules/npm'); const npm = require('ep_etherpad-lite/node_modules/npm');
const util = require('util');
(async () => {
await util.promisify(npm.load)({});
npm.load({}, async () => {
// initialize database // initialize database
require('ep_etherpad-lite/node/utils/Settings'); require('ep_etherpad-lite/node/utils/Settings');
const db = require('ep_etherpad-lite/node/db/DB'); const db = require('ep_etherpad-lite/node/db/DB');
@ -54,10 +56,8 @@ npm.load({}, async () => {
// console.log('Fetching', revNum) // console.log('Fetching', revNum)
const revision = await db.get(`pad:${padId}:revs:${revNum}`); const revision = await db.get(`pad:${padId}:revs:${revNum}`);
// check if there is a atext in the keyRevisions // check if there is a atext in the keyRevisions
if (~keyRevisions.indexOf(revNum) && const {meta: {atext: revAtext} = {}} = revision || {};
(revision === undefined || if (~keyRevisions.indexOf(revNum) && revAtext == null) {
revision.meta === undefined ||
revision.meta.atext === undefined)) {
console.error(`No atext in key revision ${revNum}`); console.error(`No atext in key revision ${revNum}`);
continue; continue;
} }
@ -104,4 +104,4 @@ npm.load({}, async () => {
} }
})); }));
} }
}); })();

View file

@ -1,391 +0,0 @@
const startTime = Date.now();
const fs = require('fs');
const ueberDB = require('../src/node_modules/ueberdb2');
const mysql = require('../src/node_modules/ueberdb2/node_modules/mysql');
const async = require('../src/node_modules/async');
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
const AttributePool = require('ep_etherpad-lite/static/js/AttributePool');
const settingsFile = process.argv[2];
const sqlOutputFile = process.argv[3];
// stop if the settings file is not set
if (!settingsFile || !sqlOutputFile) {
console.error('Use: node convert.js $SETTINGSFILE $SQLOUTPUT');
process.exit(1);
}
log('read settings file...');
// read the settings file and parse the json
const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
log('done');
log('open output file...');
const sqlOutput = fs.openSync(sqlOutputFile, 'w');
const sql = 'SET CHARACTER SET UTF8;\n' +
'CREATE TABLE IF NOT EXISTS `store` ( \n' +
'`key` VARCHAR( 100 ) NOT NULL , \n' +
'`value` LONGTEXT NOT NULL , \n' +
'PRIMARY KEY ( `key` ) \n' +
') ENGINE = INNODB;\n' +
'START TRANSACTION;\n\n';
fs.writeSync(sqlOutput, sql);
log('done');
const etherpadDB = mysql.createConnection({
host: settings.etherpadDB.host,
user: settings.etherpadDB.user,
password: settings.etherpadDB.password,
database: settings.etherpadDB.database,
port: settings.etherpadDB.port,
});
// get the timestamp once
const timestamp = Date.now();
let padIDs;
async.series([
// get all padids out of the database...
function (callback) {
log('get all padIds out of the database...');
etherpadDB.query('SELECT ID FROM PAD_META', [], (err, _padIDs) => {
padIDs = _padIDs;
callback(err);
});
},
function (callback) {
log('done');
// create a queue with a concurrency 100
const queue = async.queue((padId, callback) => {
convertPad(padId, (err) => {
incrementPadStats();
callback(err);
});
}, 100);
// set the step callback as the queue callback
queue.drain = callback;
// add the padids to the worker queue
for (let i = 0, length = padIDs.length; i < length; i++) {
queue.push(padIDs[i].ID);
}
},
], (err) => {
if (err) throw err;
// write the groups
let sql = '';
for (const proID in proID2groupID) {
const groupID = proID2groupID[proID];
const subdomain = proID2subdomain[proID];
sql += `REPLACE INTO store VALUES (${etherpadDB.escape(`group:${groupID}`)}, ${etherpadDB.escape(JSON.stringify(groups[groupID]))});\n`;
sql += `REPLACE INTO store VALUES (${etherpadDB.escape(`mapper2group:subdomain:${subdomain}`)}, ${etherpadDB.escape(groupID)});\n`;
}
// close transaction
sql += 'COMMIT;';
// end the sql file
fs.writeSync(sqlOutput, sql, undefined, 'utf-8');
fs.closeSync(sqlOutput);
log('finished.');
process.exit(0);
});
function log(str) {
console.log(`${(Date.now() - startTime) / 1000}\t${str}`);
}
let padsDone = 0;
function incrementPadStats() {
padsDone++;
if (padsDone % 100 == 0) {
const averageTime = Math.round(padsDone / ((Date.now() - startTime) / 1000));
log(`${padsDone}/${padIDs.length}\t${averageTime} pad/s`);
}
}
var proID2groupID = {};
var proID2subdomain = {};
var groups = {};
function convertPad(padId, callback) {
const changesets = [];
const changesetsMeta = [];
const chatMessages = [];
const authors = [];
let apool;
let subdomain;
let padmeta;
async.series([
// get all needed db values
function (callback) {
async.parallel([
// get the pad revisions
function (callback) {
const sql = 'SELECT * FROM `PAD_CHAT_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_CHAT_META` WHERE ID=?)';
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
// parse the pages
for (let i = 0, length = results.length; i < length; i++) {
parsePage(chatMessages, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true);
}
} catch (e) { err = e; }
}
callback(err);
});
},
// get the chat entries
function (callback) {
const sql = 'SELECT * FROM `PAD_REVS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVS_META` WHERE ID=?)';
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
// parse the pages
for (let i = 0, length = results.length; i < length; i++) {
parsePage(changesets, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, false);
}
} catch (e) { err = e; }
}
callback(err);
});
},
// get the pad revisions meta data
function (callback) {
const sql = 'SELECT * FROM `PAD_REVMETA_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVMETA_META` WHERE ID=?)';
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
// parse the pages
for (let i = 0, length = results.length; i < length; i++) {
parsePage(changesetsMeta, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true);
}
} catch (e) { err = e; }
}
callback(err);
});
},
// get the attribute pool of this pad
function (callback) {
const sql = 'SELECT `JSON` FROM `PAD_APOOL` WHERE `ID` = ?';
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
apool = JSON.parse(results[0].JSON).x;
} catch (e) { err = e; }
}
callback(err);
});
},
// get the authors informations
function (callback) {
const sql = 'SELECT * FROM `PAD_AUTHORS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_AUTHORS_META` WHERE ID=?)';
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
// parse the pages
for (let i = 0, length = results.length; i < length; i++) {
parsePage(authors, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true);
}
} catch (e) { err = e; }
}
callback(err);
});
},
// get the pad information
function (callback) {
const sql = 'SELECT JSON FROM `PAD_META` WHERE ID=?';
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
padmeta = JSON.parse(results[0].JSON).x;
} catch (e) { err = e; }
}
callback(err);
});
},
// get the subdomain
function (callback) {
// skip if this is no proPad
if (padId.indexOf('$') == -1) {
callback();
return;
}
// get the proID out of this padID
const proID = padId.split('$')[0];
const sql = 'SELECT subDomain FROM pro_domains WHERE ID = ?';
etherpadDB.query(sql, [proID], (err, results) => {
if (!err) {
subdomain = results[0].subDomain;
}
callback(err);
});
},
], callback);
},
function (callback) {
// saves all values that should be written to the database
const values = {};
// this is a pro pad, let's convert it to a group pad
if (padId.indexOf('$') != -1) {
const padIdParts = padId.split('$');
const proID = padIdParts[0];
const padName = padIdParts[1];
let groupID;
// this proID is not converted so far, do it
if (proID2groupID[proID] == null) {
groupID = `g.${randomString(16)}`;
// create the mappers for this new group
proID2groupID[proID] = groupID;
proID2subdomain[proID] = subdomain;
groups[groupID] = {pads: {}};
}
// use the generated groupID;
groupID = proID2groupID[proID];
// rename the pad
padId = `${groupID}$${padName}`;
// set the value for this pad in the group
groups[groupID].pads[padId] = 1;
}
try {
const newAuthorIDs = {};
const oldName2newName = {};
// replace the authors with generated authors
// we need to do that cause where the original etherpad saves pad local authors, the new (lite) etherpad uses them global
for (var i in apool.numToAttrib) {
var key = apool.numToAttrib[i][0];
const value = apool.numToAttrib[i][1];
// skip non authors and anonymous authors
if (key != 'author' || value == '') continue;
// generate new author values
const authorID = `a.${randomString(16)}`;
const authorColorID = authors[i].colorId || Math.floor(Math.random() * (exports.getColorPalette().length));
const authorName = authors[i].name || null;
// overwrite the authorID of the attribute pool
apool.numToAttrib[i][1] = authorID;
// write the author to the database
values[`globalAuthor:${authorID}`] = {colorId: authorColorID, name: authorName, timestamp};
// save in mappers
newAuthorIDs[i] = authorID;
oldName2newName[value] = authorID;
}
// save all revisions
for (var i = 0; i < changesets.length; i++) {
values[`pad:${padId}:revs:${i}`] = {changeset: changesets[i],
meta: {
author: newAuthorIDs[changesetsMeta[i].a],
timestamp: changesetsMeta[i].t,
atext: changesetsMeta[i].atext || undefined,
}};
}
// save all chat messages
for (var i = 0; i < chatMessages.length; i++) {
values[`pad:${padId}:chat:${i}`] = {text: chatMessages[i].lineText,
userId: oldName2newName[chatMessages[i].userId],
time: chatMessages[i].time};
}
// generate the latest atext
const fullAPool = (new AttributePool()).fromJsonable(apool);
const keyRev = Math.floor(padmeta.head / padmeta.keyRevInterval) * padmeta.keyRevInterval;
let atext = changesetsMeta[keyRev].atext;
let curRev = keyRev;
while (curRev < padmeta.head) {
curRev++;
const changeset = changesets[curRev];
atext = Changeset.applyToAText(changeset, atext, fullAPool);
}
values[`pad:${padId}`] = {atext,
pool: apool,
head: padmeta.head,
chatHead: padmeta.numChatMessages};
} catch (e) {
console.error(`Error while converting pad ${padId}, pad skipped`);
console.error(e.stack ? e.stack : JSON.stringify(e));
callback();
return;
}
let sql = '';
for (var key in values) {
sql += `REPLACE INTO store VALUES (${etherpadDB.escape(key)}, ${etherpadDB.escape(JSON.stringify(values[key]))});\n`;
}
fs.writeSync(sqlOutput, sql, undefined, 'utf-8');
callback();
},
], callback);
}
/**
* This parses a Page like Etherpad uses them in the databases
* The offsets describes the length of a unit in the page, the data are
* all values behind each other
*/
function parsePage(array, pageStart, offsets, data, json) {
let start = 0;
const lengths = offsets.split(',');
for (let i = 0; i < lengths.length; i++) {
let unitLength = lengths[i];
// skip empty units
if (unitLength == '') continue;
// parse the number
unitLength = Number(unitLength);
// cut the unit out of data
const unit = data.substr(start, unitLength);
// put it into the array
array[pageStart + i] = json ? JSON.parse(unit) : unit;
// update start
start += unitLength;
}
}

View file

@ -1,4 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict';
// Copyright Joyent, Inc. and other Node contributors. // Copyright Joyent, Inc. and other Node contributors.
// //
// Permission is hereby granted, free of charge, to any person obtaining a // Permission is hereby granted, free of charge, to any person obtaining a
@ -20,7 +23,6 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE. // USE OR OTHER DEALINGS IN THE SOFTWARE.
const marked = require('marked');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
@ -33,12 +35,12 @@ let template = null;
let inputFile = null; let inputFile = null;
args.forEach((arg) => { args.forEach((arg) => {
if (!arg.match(/^\-\-/)) { if (!arg.match(/^--/)) {
inputFile = arg; inputFile = arg;
} else if (arg.match(/^\-\-format=/)) { } else if (arg.match(/^--format=/)) {
format = arg.replace(/^\-\-format=/, ''); format = arg.replace(/^--format=/, '');
} else if (arg.match(/^\-\-template=/)) { } else if (arg.match(/^--template=/)) {
template = arg.replace(/^\-\-template=/, ''); template = arg.replace(/^--template=/, '');
} }
}); });
@ -56,11 +58,11 @@ fs.readFile(inputFile, 'utf8', (er, input) => {
}); });
const includeExpr = /^@include\s+([A-Za-z0-9-_\/]+)(?:\.)?([a-zA-Z]*)$/gmi; const includeExpr = /^@include\s+([A-Za-z0-9-_/]+)(?:\.)?([a-zA-Z]*)$/gmi;
const includeData = {}; const includeData = {};
function processIncludes(inputFile, input, cb) { const processIncludes = (inputFile, input, cb) => {
const includes = input.match(includeExpr); const includes = input.match(includeExpr);
if (includes === null) return cb(null, input); if (includes == null) return cb(null, input);
let errState = null; let errState = null;
console.error(includes); console.error(includes);
let incCount = includes.length; let incCount = includes.length;
@ -70,7 +72,7 @@ function processIncludes(inputFile, input, cb) {
let fname = include.replace(/^@include\s+/, ''); let fname = include.replace(/^@include\s+/, '');
if (!fname.match(/\.md$/)) fname += '.md'; if (!fname.match(/\.md$/)) fname += '.md';
if (includeData.hasOwnProperty(fname)) { if (Object.prototype.hasOwnProperty.call(includeData, fname)) {
input = input.split(include).join(includeData[fname]); input = input.split(include).join(includeData[fname]);
incCount--; incCount--;
if (incCount === 0) { if (incCount === 0) {
@ -94,10 +96,10 @@ function processIncludes(inputFile, input, cb) {
}); });
}); });
}); });
} };
function next(er, input) { const next = (er, input) => {
if (er) throw er; if (er) throw er;
switch (format) { switch (format) {
case 'json': case 'json':
@ -117,4 +119,4 @@ function next(er, input) {
default: default:
throw new Error(`Invalid format: ${format}`); throw new Error(`Invalid format: ${format}`);
} }
} };

View file

@ -1,3 +1,5 @@
'use strict';
// Copyright Joyent, Inc. and other Node contributors. // Copyright Joyent, Inc. and other Node contributors.
// //
// Permission is hereby granted, free of charge, to any person obtaining a // Permission is hereby granted, free of charge, to any person obtaining a
@ -23,17 +25,17 @@ const fs = require('fs');
const marked = require('marked'); const marked = require('marked');
const path = require('path'); const path = require('path');
module.exports = toHTML;
function toHTML(input, filename, template, cb) { const toHTML = (input, filename, template, cb) => {
const lexed = marked.lexer(input); const lexed = marked.lexer(input);
fs.readFile(template, 'utf8', (er, template) => { fs.readFile(template, 'utf8', (er, template) => {
if (er) return cb(er); if (er) return cb(er);
render(lexed, filename, template, cb); render(lexed, filename, template, cb);
}); });
} };
module.exports = toHTML;
function render(lexed, filename, template, cb) { const render = (lexed, filename, template, cb) => {
// get the section // get the section
const section = getSection(lexed); const section = getSection(lexed);
@ -52,23 +54,23 @@ function render(lexed, filename, template, cb) {
// content has to be the last thing we do with // content has to be the last thing we do with
// the lexed tokens, because it's destructive. // the lexed tokens, because it's destructive.
content = marked.parser(lexed); const content = marked.parser(lexed);
template = template.replace(/__CONTENT__/g, content); template = template.replace(/__CONTENT__/g, content);
cb(null, template); cb(null, template);
}); });
} };
// just update the list item text in-place. // just update the list item text in-place.
// lists that come right after a heading are what we're after. // lists that come right after a heading are what we're after.
function parseLists(input) { const parseLists = (input) => {
let state = null; let state = null;
let depth = 0; let depth = 0;
const output = []; const output = [];
output.links = input.links; output.links = input.links;
input.forEach((tok) => { input.forEach((tok) => {
if (state === null) { if (state == null) {
if (tok.type === 'heading') { if (tok.type === 'heading') {
state = 'AFTERHEADING'; state = 'AFTERHEADING';
} }
@ -112,29 +114,27 @@ function parseLists(input) {
}); });
return output; return output;
} };
function parseListItem(text) { const parseListItem = (text) => {
text = text.replace(/\{([^\}]+)\}/, '<span class="type">$1</span>'); text = text.replace(/\{([^}]+)\}/, '<span class="type">$1</span>');
// XXX maybe put more stuff here? // XXX maybe put more stuff here?
return text; return text;
} };
// section is just the first heading // section is just the first heading
function getSection(lexed) { const getSection = (lexed) => {
const section = '';
for (let i = 0, l = lexed.length; i < l; i++) { for (let i = 0, l = lexed.length; i < l; i++) {
const tok = lexed[i]; const tok = lexed[i];
if (tok.type === 'heading') return tok.text; if (tok.type === 'heading') return tok.text;
} }
return ''; return '';
} };
function buildToc(lexed, filename, cb) { const buildToc = (lexed, filename, cb) => {
const indent = 0;
let toc = []; let toc = [];
let depth = 0; let depth = 0;
lexed.forEach((tok) => { lexed.forEach((tok) => {
@ -155,18 +155,18 @@ function buildToc(lexed, filename, cb) {
toc = marked.parse(toc.join('\n')); toc = marked.parse(toc.join('\n'));
cb(null, toc); cb(null, toc);
} };
const idCounters = {}; const idCounters = {};
function getId(text) { const getId = (text) => {
text = text.toLowerCase(); text = text.toLowerCase();
text = text.replace(/[^a-z0-9]+/g, '_'); text = text.replace(/[^a-z0-9]+/g, '_');
text = text.replace(/^_+|_+$/, ''); text = text.replace(/^_+|_+$/, '');
text = text.replace(/^([^a-z])/, '_$1'); text = text.replace(/^([^a-z])/, '_$1');
if (idCounters.hasOwnProperty(text)) { if (Object.prototype.hasOwnProperty.call(idCounters, text)) {
text += `_${++idCounters[text]}`; text += `_${++idCounters[text]}`;
} else { } else {
idCounters[text] = 0; idCounters[text] = 0;
} }
return text; return text;
} };

View file

@ -1,3 +1,4 @@
'use strict';
// Copyright Joyent, Inc. and other Node contributors. // Copyright Joyent, Inc. and other Node contributors.
// //
// Permission is hereby granted, free of charge, to any person obtaining a // Permission is hereby granted, free of charge, to any person obtaining a
@ -26,7 +27,7 @@ module.exports = doJSON;
const marked = require('marked'); const marked = require('marked');
function doJSON(input, filename, cb) { const doJSON = (input, filename, cb) => {
const root = {source: filename}; const root = {source: filename};
const stack = [root]; const stack = [root];
let depth = 0; let depth = 0;
@ -40,7 +41,7 @@ function doJSON(input, filename, cb) {
// <!-- type = module --> // <!-- type = module -->
// This is for cases where the markdown semantic structure is lacking. // This is for cases where the markdown semantic structure is lacking.
if (type === 'paragraph' || type === 'html') { if (type === 'paragraph' || type === 'html') {
const metaExpr = /<!--([^=]+)=([^\-]+)-->\n*/g; const metaExpr = /<!--([^=]+)=([^-]+)-->\n*/g;
text = text.replace(metaExpr, (_0, k, v) => { text = text.replace(metaExpr, (_0, k, v) => {
current[k.trim()] = v.trim(); current[k.trim()] = v.trim();
return ''; return '';
@ -146,7 +147,7 @@ function doJSON(input, filename, cb) {
} }
return cb(null, root); return cb(null, root);
} };
// go from something like this: // go from something like this:
@ -191,7 +192,7 @@ function doJSON(input, filename, cb) {
// desc: 'whether or not to send output to parent\'s stdio.', // desc: 'whether or not to send output to parent\'s stdio.',
// default: 'false' } ] } ] // default: 'false' } ] } ]
function processList(section) { const processList = (section) => {
const list = section.list; const list = section.list;
const values = []; const values = [];
let current; let current;
@ -203,13 +204,13 @@ function processList(section) {
if (type === 'space') return; if (type === 'space') return;
if (type === 'list_item_start') { if (type === 'list_item_start') {
if (!current) { if (!current) {
var n = {}; const n = {};
values.push(n); values.push(n);
current = n; current = n;
} else { } else {
current.options = current.options || []; current.options = current.options || [];
stack.push(current); stack.push(current);
var n = {}; const n = {};
current.options.push(n); current.options.push(n);
current = n; current = n;
} }
@ -247,11 +248,11 @@ function processList(section) {
switch (section.type) { switch (section.type) {
case 'ctor': case 'ctor':
case 'classMethod': case 'classMethod':
case 'method': case 'method': {
// each item is an argument, unless the name is 'return', // each item is an argument, unless the name is 'return',
// in which case it's the return value. // in which case it's the return value.
section.signatures = section.signatures || []; section.signatures = section.signatures || [];
var sig = {}; const sig = {};
section.signatures.push(sig); section.signatures.push(sig);
sig.params = values.filter((v) => { sig.params = values.filter((v) => {
if (v.name === 'return') { if (v.name === 'return') {
@ -262,11 +263,11 @@ function processList(section) {
}); });
parseSignature(section.textRaw, sig); parseSignature(section.textRaw, sig);
break; break;
}
case 'property': case 'property': {
// there should be only one item, which is the value. // there should be only one item, which is the value.
// copy the data up to the section. // copy the data up to the section.
var value = values[0] || {}; const value = values[0] || {};
delete value.name; delete value.name;
section.typeof = value.type; section.typeof = value.type;
delete value.type; delete value.type;
@ -274,20 +275,21 @@ function processList(section) {
section[k] = value[k]; section[k] = value[k];
}); });
break; break;
}
case 'event': case 'event': {
// event: each item is an argument. // event: each item is an argument.
section.params = values; section.params = values;
break; break;
} }
// section.listParsed = values;
delete section.list;
} }
delete section.list;
};
// textRaw = "someobject.someMethod(a, [b=100], [c])" // textRaw = "someobject.someMethod(a, [b=100], [c])"
function parseSignature(text, sig) { const parseSignature = (text, sig) => {
let params = text.match(paramExpr); let params = text.match(paramExpr);
if (!params) return; if (!params) return;
params = params[1]; params = params[1];
@ -322,10 +324,10 @@ function parseSignature(text, sig) {
if (optional) param.optional = true; if (optional) param.optional = true;
if (def !== undefined) param.default = def; if (def !== undefined) param.default = def;
}); });
} };
function parseListItem(item) { const parseListItem = (item) => {
if (item.options) item.options.forEach(parseListItem); if (item.options) item.options.forEach(parseListItem);
if (!item.textRaw) return; if (!item.textRaw) return;
@ -341,7 +343,7 @@ function parseListItem(item) {
item.name = 'return'; item.name = 'return';
text = text.replace(retExpr, ''); text = text.replace(retExpr, '');
} else { } else {
const nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/; const nameExpr = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/;
const name = text.match(nameExpr); const name = text.match(nameExpr);
if (name) { if (name) {
item.name = name[1]; item.name = name[1];
@ -358,7 +360,7 @@ function parseListItem(item) {
} }
text = text.trim(); text = text.trim();
const typeExpr = /^\{([^\}]+)\}/; const typeExpr = /^\{([^}]+)\}/;
const type = text.match(typeExpr); const type = text.match(typeExpr);
if (type) { if (type) {
item.type = type[1]; item.type = type[1];
@ -376,10 +378,10 @@ function parseListItem(item) {
text = text.replace(/^\s*-\s*/, ''); text = text.replace(/^\s*-\s*/, '');
text = text.trim(); text = text.trim();
if (text) item.desc = text; if (text) item.desc = text;
} };
function finishSection(section, parent) { const finishSection = (section, parent) => {
if (!section || !parent) { if (!section || !parent) {
throw new Error(`Invalid finishSection call\n${ throw new Error(`Invalid finishSection call\n${
JSON.stringify(section)}\n${ JSON.stringify(section)}\n${
@ -416,7 +418,7 @@ function finishSection(section, parent) {
ctor.signatures.forEach((sig) => { ctor.signatures.forEach((sig) => {
sig.desc = ctor.desc; sig.desc = ctor.desc;
}); });
sigs.push.apply(sigs, ctor.signatures); sigs.push(...ctor.signatures);
}); });
delete section.ctors; delete section.ctors;
} }
@ -479,50 +481,50 @@ function finishSection(section, parent) {
parent[plur] = parent[plur] || []; parent[plur] = parent[plur] || [];
parent[plur].push(section); parent[plur].push(section);
} };
// Not a general purpose deep copy. // Not a general purpose deep copy.
// But sufficient for these basic things. // But sufficient for these basic things.
function deepCopy(src, dest) { const deepCopy = (src, dest) => {
Object.keys(src).filter((k) => !dest.hasOwnProperty(k)).forEach((k) => { Object.keys(src).filter((k) => !Object.prototype.hasOwnProperty.call(dest, k)).forEach((k) => {
dest[k] = deepCopy_(src[k]); dest[k] = deepCopy_(src[k]);
}); });
} };
function deepCopy_(src) { const deepCopy_ = (src) => {
if (!src) return src; if (!src) return src;
if (Array.isArray(src)) { if (Array.isArray(src)) {
var c = new Array(src.length); const c = new Array(src.length);
src.forEach((v, i) => { src.forEach((v, i) => {
c[i] = deepCopy_(v); c[i] = deepCopy_(v);
}); });
return c; return c;
} }
if (typeof src === 'object') { if (typeof src === 'object') {
var c = {}; const c = {};
Object.keys(src).forEach((k) => { Object.keys(src).forEach((k) => {
c[k] = deepCopy_(src[k]); c[k] = deepCopy_(src[k]);
}); });
return c; return c;
} }
return src; return src;
} };
// these parse out the contents of an H# tag // these parse out the contents of an H# tag
const eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i; const eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i;
const classExpr = /^Class:\s*([^ ]+).*?$/i; const classExpr = /^Class:\s*([^ ]+).*?$/i;
const propExpr = /^(?:property:?\s*)?[^\.]+\.([^ \.\(\)]+)\s*?$/i; const propExpr = /^(?:property:?\s*)?[^.]+\.([^ .()]+)\s*?$/i;
const braceExpr = /^(?:property:?\s*)?[^\.\[]+(\[[^\]]+\])\s*?$/i; const braceExpr = /^(?:property:?\s*)?[^.[]+(\[[^\]]+\])\s*?$/i;
const classMethExpr = const classMethExpr =
/^class\s*method\s*:?[^\.]+\.([^ \.\(\)]+)\([^\)]*\)\s*?$/i; /^class\s*method\s*:?[^.]+\.([^ .()]+)\([^)]*\)\s*?$/i;
const methExpr = const methExpr =
/^(?:method:?\s*)?(?:[^\.]+\.)?([^ \.\(\)]+)\([^\)]*\)\s*?$/i; /^(?:method:?\s*)?(?:[^.]+\.)?([^ .()]+)\([^)]*\)\s*?$/i;
const newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/; const newExpr = /^new ([A-Z][a-z]+)\([^)]*\)\s*?$/;
var paramExpr = /\((.*)\);?$/; const paramExpr = /\((.*)\);?$/;
function newSection(tok) { const newSection = (tok) => {
const section = {}; const section = {};
// infer the type from the text. // infer the type from the text.
const text = section.textRaw = tok.text; const text = section.textRaw = tok.text;
@ -551,4 +553,4 @@ function newSection(tok) {
section.name = text; section.name = text;
} }
return section; return section;
} };

View file

@ -4,7 +4,7 @@
"description": "Internal tool for generating Node.js API docs", "description": "Internal tool for generating Node.js API docs",
"version": "0.0.0", "version": "0.0.0",
"engines": { "engines": {
"node": ">=0.6.10" "node": ">=10.17.0"
}, },
"dependencies": { "dependencies": {
"marked": "0.8.2" "marked": "0.8.2"

View file

@ -16,11 +16,11 @@ if (process.argv.length !== 3) throw new Error('Use: node extractPadData.js $PAD
const padId = process.argv[2]; const padId = process.argv[2];
const npm = require('ep_etherpad-lite/node_modules/npm'); const npm = require('ep_etherpad-lite/node_modules/npm');
const util = require('util');
npm.load({}, async (err) => { (async () => {
if (err) throw err; await util.promisify(npm.load)({});
try {
// initialize database // initialize database
require('ep_etherpad-lite/node/utils/Settings'); require('ep_etherpad-lite/node/utils/Settings');
const db = require('ep_etherpad-lite/node/db/DB'); const db = require('ep_etherpad-lite/node/db/DB');
@ -29,7 +29,6 @@ npm.load({}, async (err) => {
// load extra modules // load extra modules
const dirtyDB = require('ep_etherpad-lite/node_modules/dirty'); const dirtyDB = require('ep_etherpad-lite/node_modules/dirty');
const padManager = require('ep_etherpad-lite/node/db/PadManager'); const padManager = require('ep_etherpad-lite/node/db/PadManager');
const util = require('util');
// initialize output database // initialize output database
const dirty = dirtyDB(`${padId}.db`); const dirty = dirtyDB(`${padId}.db`);
@ -67,8 +66,4 @@ npm.load({}, async (err) => {
} }
console.log('finished'); console.log('finished');
} catch (err) { })();
console.error(err);
throw err;
}
});

View file

@ -4,6 +4,9 @@
// unhandled rejection into an uncaught exception, which does cause Node.js to exit. // unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; }); process.on('unhandledRejection', (err) => { throw err; });
const npm = require('ep_etherpad-lite/node_modules/npm');
const util = require('util');
const startTime = Date.now(); const startTime = Date.now();
const log = (str) => { const log = (str) => {
@ -43,10 +46,10 @@ const unescape = (val) => {
return val; return val;
}; };
(async () => {
await util.promisify(npm.load)({});
require('ep_etherpad-lite/node_modules/npm').load({}, (er, npm) => {
const fs = require('fs'); const fs = require('fs');
const ueberDB = require('ep_etherpad-lite/node_modules/ueberdb2'); const ueberDB = require('ep_etherpad-lite/node_modules/ueberdb2');
const settings = require('ep_etherpad-lite/node/utils/Settings'); const settings = require('ep_etherpad-lite/node/utils/Settings');
const log4js = require('ep_etherpad-lite/node_modules/log4js'); const log4js = require('ep_etherpad-lite/node_modules/log4js');
@ -68,11 +71,7 @@ require('ep_etherpad-lite/node_modules/npm').load({}, (er, npm) => {
if (!sqlFile) throw new Error('Use: node importSqlFile.js $SQLFILE'); if (!sqlFile) throw new Error('Use: node importSqlFile.js $SQLFILE');
log('initializing db'); log('initializing db');
db.init((err) => { await util.promisify(db.init.bind(db))();
// there was an error while initializing the database, output it and stop
if (err) {
throw err;
} else {
log('done'); log('done');
log('open output file...'); log('open output file...');
@ -101,9 +100,6 @@ require('ep_etherpad-lite/node_modules/npm').load({}, (er, npm) => {
process.stdout.write('done. waiting for db to finish transaction. ' + process.stdout.write('done. waiting for db to finish transaction. ' +
'depended on dbms this may take some time..\n'); 'depended on dbms this may take some time..\n');
db.close(() => { await util.promisify(db.close.bind(db))();
log(`finished, imported ${keyNo} keys.`); log(`finished, imported ${keyNo} keys.`);
}); })();
}
});
});

View file

@ -4,9 +4,12 @@
// unhandled rejection into an uncaught exception, which does cause Node.js to exit. // unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; }); process.on('unhandledRejection', (err) => { throw err; });
const npm = require('ep_etherpad-lite/node_modules/npm');
const util = require('util'); const util = require('util');
require('ep_etherpad-lite/node_modules/npm').load({}, async (er, npm) => { (async () => {
await util.promisify(npm.load)({});
process.chdir(`${npm.root}/..`); process.chdir(`${npm.root}/..`);
// This script requires that you have modified your settings.json file // This script requires that you have modified your settings.json file
@ -56,4 +59,4 @@ require('ep_etherpad-lite/node_modules/npm').load({}, async (er, npm) => {
await util.promisify(db.close.bind(db))(); await util.promisify(db.close.bind(db))();
console.log('Finished.'); console.log('Finished.');
}); })();

View file

@ -220,12 +220,12 @@ fs.readdir(pluginPath, (err, rootFiles) => {
} }
updateDeps(parsedPackageJSON, 'devDependencies', { updateDeps(parsedPackageJSON, 'devDependencies', {
'eslint': '^7.17.0', 'eslint': '^7.18.0',
'eslint-config-etherpad': '^1.0.22', 'eslint-config-etherpad': '^1.0.24',
'eslint-plugin-eslint-comments': '^3.2.0', 'eslint-plugin-eslint-comments': '^3.2.0',
'eslint-plugin-mocha': '^8.0.0', 'eslint-plugin-mocha': '^8.0.0',
'eslint-plugin-node': '^11.1.0', 'eslint-plugin-node': '^11.1.0',
'eslint-plugin-prefer-arrow': '^1.2.2', 'eslint-plugin-prefer-arrow': '^1.2.3',
'eslint-plugin-promise': '^4.2.1', 'eslint-plugin-promise': '^4.2.1',
'eslint-plugin-you-dont-need-lodash-underscore': '^6.10.0', 'eslint-plugin-you-dont-need-lodash-underscore': '^6.10.0',
}); });
@ -263,7 +263,7 @@ fs.readdir(pluginPath, (err, rootFiles) => {
console.warn('No engines or node engine in package.json'); console.warn('No engines or node engine in package.json');
if (autoFix) { if (autoFix) {
const engines = { const engines = {
node: '>=10.13.0', node: '^10.17.0 || >=11.14.0',
}; };
parsedPackageJSON.engines = engines; parsedPackageJSON.engines = engines;
writePackageJson(parsedPackageJSON); writePackageJson(parsedPackageJSON);

View file

@ -13,7 +13,6 @@ if (process.argv.length !== 4 && process.argv.length !== 5) {
throw new Error('Use: node bin/repairPad.js $PADID $REV [$NEWPADID]'); throw new Error('Use: node bin/repairPad.js $PADID $REV [$NEWPADID]');
} }
const async = require('ep_etherpad-lite/node_modules/async');
const npm = require('ep_etherpad-lite/node_modules/npm'); const npm = require('ep_etherpad-lite/node_modules/npm');
const util = require('util'); const util = require('util');
@ -21,50 +20,33 @@ const padId = process.argv[2];
const newRevHead = process.argv[3]; const newRevHead = process.argv[3];
const newPadId = process.argv[4] || `${padId}-rebuilt`; const newPadId = process.argv[4] || `${padId}-rebuilt`;
let db, oldPad, newPad; (async () => {
let Pad, PadManager; await util.promisify(npm.load)({});
async.series([ const db = require('ep_etherpad-lite/node/db/DB');
(callback) => npm.load({}, callback), await db.init();
(callback) => {
// Get a handle into the database const PadManager = require('ep_etherpad-lite/node/db/PadManager');
db = require('ep_etherpad-lite/node/db/DB'); const Pad = require('ep_etherpad-lite/node/db/Pad').Pad;
db.init(callback);
},
(callback) => {
Pad = require('ep_etherpad-lite/node/db/Pad').Pad;
PadManager = require('ep_etherpad-lite/node/db/PadManager');
// Get references to the original pad and to a newly created pad
// HACK: This is a standalone script, so we want to write everything
// out to the database immediately. The only problem with this is
// that a driver (like the mysql driver) can hardcode these values.
db.db.db.settings = {cache: 0, writeInterval: 0, json: true};
// Validate the newPadId if specified and that a pad with that ID does // Validate the newPadId if specified and that a pad with that ID does
// not already exist to avoid overwriting it. // not already exist to avoid overwriting it.
if (!PadManager.isValidPadId(newPadId)) { if (!PadManager.isValidPadId(newPadId)) {
throw new Error('Cannot create a pad with that id as it is invalid'); throw new Error('Cannot create a pad with that id as it is invalid');
} }
PadManager.doesPadExists(newPadId, (err, exists) => { const exists = await PadManager.doesPadExist(newPadId);
if (exists) throw new Error('Cannot create a pad with that id as it already exists'); if (exists) throw new Error('Cannot create a pad with that id as it already exists');
});
PadManager.getPad(padId, (err, pad) => { const oldPad = await PadManager.getPad(padId);
oldPad = pad; const newPad = new Pad(newPadId);
newPad = new Pad(newPadId);
callback();
});
},
(callback) => {
// Clone all Chat revisions // Clone all Chat revisions
const chatHead = oldPad.chatHead; const chatHead = oldPad.chatHead;
for (let i = 0, curHeadNum = 0; i <= chatHead; i++) { await Promise.all([...Array(chatHead + 1).keys()].map(async (i) => {
db.db.get(`pad:${padId}:chat:${i}`, (err, chat) => { const chat = await db.get(`pad:${padId}:chat:${i}`);
db.db.set(`pad:${newPadId}:chat:${curHeadNum++}`, chat); await db.set(`pad:${newPadId}:chat:${i}`, chat);
console.log(`Created: Chat Revision: pad:${newPadId}:chat:${curHeadNum}`); console.log(`Created: Chat Revision: pad:${newPadId}:chat:${i}`);
}); }));
}
callback();
},
(callback) => {
// Rebuild Pad from revisions up to and including the new revision head // Rebuild Pad from revisions up to and including the new revision head
const AuthorManager = require('ep_etherpad-lite/node/db/AuthorManager'); const AuthorManager = require('ep_etherpad-lite/node/db/AuthorManager');
const Changeset = require('ep_etherpad-lite/static/js/Changeset'); const Changeset = require('ep_etherpad-lite/static/js/Changeset');
@ -73,23 +55,18 @@ async.series([
// and, AFAICT, cannot be recreated any other way // and, AFAICT, cannot be recreated any other way
newPad.pool.numToAttrib = oldPad.pool.numToAttrib; newPad.pool.numToAttrib = oldPad.pool.numToAttrib;
for (let curRevNum = 0; curRevNum <= newRevHead; curRevNum++) { for (let curRevNum = 0; curRevNum <= newRevHead; curRevNum++) {
db.db.get(`pad:${padId}:revs:${curRevNum}`, (err, rev) => { const rev = await db.get(`pad:${padId}:revs:${curRevNum}`);
if (rev.meta) { if (!rev || !rev.meta) throw new Error('The specified revision number could not be found.');
throw new Error('The specified revision number could not be found.');
}
const newRevNum = ++newPad.head; const newRevNum = ++newPad.head;
const newRevId = `pad:${newPad.id}:revs:${newRevNum}`; const newRevId = `pad:${newPad.id}:revs:${newRevNum}`;
db.db.set(newRevId, rev); await Promise.all([
AuthorManager.addPad(rev.meta.author, newPad.id); db.set(newRevId, rev),
AuthorManager.addPad(rev.meta.author, newPad.id),
]);
newPad.atext = Changeset.applyToAText(rev.changeset, newPad.atext, newPad.pool); newPad.atext = Changeset.applyToAText(rev.changeset, newPad.atext, newPad.pool);
console.log(`Created: Revision: pad:${newPad.id}:revs:${newRevNum}`); console.log(`Created: Revision: pad:${newPad.id}:revs:${newRevNum}`);
if (newRevNum === newRevHead) {
callback();
} }
});
}
},
(callback) => {
// Add saved revisions up to the new revision head // Add saved revisions up to the new revision head
console.log(newPad.head); console.log(newPad.head);
const newSavedRevisions = []; const newSavedRevisions = [];
@ -100,16 +77,13 @@ async.series([
} }
} }
newPad.savedRevisions = newSavedRevisions; newPad.savedRevisions = newSavedRevisions;
callback();
},
(callback) => {
// Save the source pad // Save the source pad
db.db.set(`pad:${newPadId}`, newPad, (err) => { await db.set(`pad:${newPadId}`, newPad);
console.log(`Created: Source Pad: pad:${newPadId}`); console.log(`Created: Source Pad: pad:${newPadId}`);
util.callbackify(newPad.saveToDatabase.bind(newPad))(callback); await newPad.saveToDatabase();
});
}, await db.shutdown();
], (err) => {
if (err) throw err;
console.info('finished'); console.info('finished');
}); })();

View file

@ -18,8 +18,10 @@ const padId = process.argv[2];
let valueCount = 0; let valueCount = 0;
const npm = require('ep_etherpad-lite/node_modules/npm'); const npm = require('ep_etherpad-lite/node_modules/npm');
npm.load({}, async (err) => { const util = require('util');
if (err) throw err;
(async () => {
await util.promisify(npm.load)({});
// intialize database // intialize database
require('ep_etherpad-lite/node/utils/Settings'); require('ep_etherpad-lite/node/utils/Settings');
@ -56,4 +58,4 @@ npm.load({}, async (err) => {
} }
console.info(`Finished: Replaced ${valueCount} values in the database`); console.info(`Finished: Replaced ${valueCount} values in the database`);
}); })();

View file

@ -421,7 +421,20 @@ Things in context:
4. text - the text for that line 4. text - the text for that line
This hook allows you to validate/manipulate the text before it's sent to the This hook allows you to validate/manipulate the text before it's sent to the
server side. The return value should be the validated/manipulated text. server side. To change the text, either:
* Set the `text` context property to the desired value and return `undefined`.
* (Deprecated) Return a string. If a hook function changes the `text` context
property, the return value is ignored. If no hook function changes `text` but
multiple hook functions return a string, the first one wins.
Example:
```
exports.collectContentLineText = (hookName, context) => {
context.text = tweakText(context.text);
};
```
## collectContentLineBreak ## collectContentLineBreak

View file

@ -225,7 +225,7 @@ publish your plugin.
"author": "USERNAME (REAL NAME) <MAIL@EXAMPLE.COM>", "author": "USERNAME (REAL NAME) <MAIL@EXAMPLE.COM>",
"contributors": [], "contributors": [],
"dependencies": {"MODULE": "0.3.20"}, "dependencies": {"MODULE": "0.3.20"},
"engines": { "node": ">= 10.13.0"} "engines": { "node": "^10.17.0 || >=11.14.0"}
} }
``` ```

4292
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,12 +3,12 @@
"ep_etherpad-lite": "file:src" "ep_etherpad-lite": "file:src"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^7.15.0", "eslint": "^7.18.0",
"eslint-config-etherpad": "^1.0.20", "eslint-config-etherpad": "^1.0.24",
"eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-mocha": "^8.0.0", "eslint-plugin-mocha": "^8.0.0",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-prefer-arrow": "^1.2.2", "eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0" "eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0"
}, },
@ -39,7 +39,9 @@
"tests/**/*" "tests/**/*"
], ],
"excludedFiles": [ "excludedFiles": [
"**/.eslintrc.js" "**/.eslintrc.js",
"tests/frontend/travis/**/*",
"tests/ratelimit/**/*"
], ],
"extends": "etherpad/tests", "extends": "etherpad/tests",
"rules": { "rules": {
@ -75,7 +77,8 @@
"tests/frontend/**/*" "tests/frontend/**/*"
], ],
"excludedFiles": [ "excludedFiles": [
"**/.eslintrc.js" "**/.eslintrc.js",
"tests/frontend/travis/**/*"
], ],
"extends": "etherpad/tests/frontend", "extends": "etherpad/tests/frontend",
"overrides": [ "overrides": [
@ -92,6 +95,12 @@
} }
} }
] ]
},
{
"files": [
"tests/frontend/travis/**/*"
],
"extends": "etherpad/node"
} }
], ],
"root": true "root": true
@ -100,6 +109,6 @@
"lint": "eslint ." "lint": "eslint ."
}, },
"engines": { "engines": {
"node": ">=10.13.0" "node": "^10.17.0 || >=11.14.0"
} }
} }

View file

@ -11,24 +11,42 @@
"Sebastian Wallroth", "Sebastian Wallroth",
"Thargon", "Thargon",
"Tim.krieger", "Tim.krieger",
"Wikinaut" "Wikinaut",
"Zunkelty"
] ]
}, },
"admin.page-title": "Admin Dashboard - Etherpad",
"admin_plugins": "Plugins verwalten", "admin_plugins": "Plugins verwalten",
"admin_plugins.available": "Verfügbare Plugins", "admin_plugins.available": "Verfügbare Plugins",
"admin_plugins.available_not-found": "Keine Plugins gefunden.", "admin_plugins.available_not-found": "Keine Plugins gefunden.",
"admin_plugins.available_fetching": "Wird abgerufen...",
"admin_plugins.available_install.value": "Installieren", "admin_plugins.available_install.value": "Installieren",
"admin_plugins.available_search.placeholder": "Suche nach Plugins zum Installieren",
"admin_plugins.description": "Beschreibung", "admin_plugins.description": "Beschreibung",
"admin_plugins.installed": "Installierte Plugins",
"admin_plugins.installed_fetching": "Rufe installierte Plugins ab...",
"admin_plugins.installed_nothing": "Du hast bisher noch keine Plugins installiert.", "admin_plugins.installed_nothing": "Du hast bisher noch keine Plugins installiert.",
"admin_plugins.installed_uninstall.value": "Deinstallieren",
"admin_plugins.last-update": "Letze Aktualisierung", "admin_plugins.last-update": "Letze Aktualisierung",
"admin_plugins.name": "Name", "admin_plugins.name": "Name",
"admin_plugins.page-title": "Plugin Manager - Etherpad",
"admin_plugins.version": "Version", "admin_plugins.version": "Version",
"admin_plugins_info": "Hilfestellung",
"admin_plugins_info.hooks": "Installierte Hooks", "admin_plugins_info.hooks": "Installierte Hooks",
"admin_plugins_info.hooks_client": "Client-seitige Hooks",
"admin_plugins_info.hooks_server": "Server-seitige Hooks",
"admin_plugins_info.parts": "Installierte Teile",
"admin_plugins_info.plugins": "Installierte Plugins", "admin_plugins_info.plugins": "Installierte Plugins",
"admin_plugins_info.page-title": "Plugin Informationen - Etherpad",
"admin_plugins_info.version": "Etherpad Version",
"admin_plugins_info.version_latest": "Neueste Version",
"admin_plugins_info.version_number": "Versionsnummer", "admin_plugins_info.version_number": "Versionsnummer",
"admin_settings": "Einstellungen", "admin_settings": "Einstellungen",
"admin_settings.current": "Derzeitige Konfiguration", "admin_settings.current": "Derzeitige Konfiguration",
"admin_settings.current_example-devel": "Beispielhafte Entwicklungseinstellungs-Templates",
"admin_settings.current_restart.value": "Etherpad neustarten",
"admin_settings.current_save.value": "Einstellungen speichern", "admin_settings.current_save.value": "Einstellungen speichern",
"admin_settings.page-title": "Einstellungen - Etherpad",
"index.newPad": "Neues Pad", "index.newPad": "Neues Pad",
"index.createOpenPad": "oder ein Pad mit folgendem Namen erstellen/öffnen:", "index.createOpenPad": "oder ein Pad mit folgendem Namen erstellen/öffnen:",
"index.openPad": "Öffne ein vorhandenes Pad mit folgendem Namen:", "index.openPad": "Öffne ein vorhandenes Pad mit folgendem Namen:",
@ -78,7 +96,7 @@
"pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Du kannst nur aus reinen Text- oder HTML-Formaten importieren. Für umfangreichere Importfunktionen <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">muss AbiWord oder LibreOffice auf dem Server installiert werden</a>.", "pad.importExport.abiword.innerHTML": "Du kannst nur aus reinen Text- oder HTML-Formaten importieren. Für umfangreichere Importfunktionen <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">muss AbiWord oder LibreOffice auf dem Server installiert werden</a>.",
"pad.modals.connected": "Verbunden.", "pad.modals.connected": "Verbunden.",
"pad.modals.reconnecting": "Wiederherstellen der Verbindung …", "pad.modals.reconnecting": "Dein Pad wird neu verbunden...",
"pad.modals.forcereconnect": "Erneutes Verbinden erzwingen", "pad.modals.forcereconnect": "Erneutes Verbinden erzwingen",
"pad.modals.reconnecttimer": "Versuche Neuverbindung in", "pad.modals.reconnecttimer": "Versuche Neuverbindung in",
"pad.modals.cancel": "Abbrechen", "pad.modals.cancel": "Abbrechen",
@ -102,6 +120,7 @@
"pad.modals.deleted.explanation": "Dieses Pad wurde entfernt.", "pad.modals.deleted.explanation": "Dieses Pad wurde entfernt.",
"pad.modals.rateLimited": "Begrenzte Rate.", "pad.modals.rateLimited": "Begrenzte Rate.",
"pad.modals.rateLimited.explanation": "Sie haben zu viele Nachrichten an dieses Pad gesendet, so dass die Verbindung unterbrochen wurde.", "pad.modals.rateLimited.explanation": "Sie haben zu viele Nachrichten an dieses Pad gesendet, so dass die Verbindung unterbrochen wurde.",
"pad.modals.rejected.explanation": "Der Server hat eine Nachricht abgelehnt, die von deinem Browser gesendet wurde.",
"pad.modals.disconnected": "Ihre Verbindung wurde getrennt.", "pad.modals.disconnected": "Ihre Verbindung wurde getrennt.",
"pad.modals.disconnected.explanation": "Die Verbindung zum Server wurde unterbrochen.", "pad.modals.disconnected.explanation": "Die Verbindung zum Server wurde unterbrochen.",
"pad.modals.disconnected.cause": "Möglicherweise ist der Server nicht erreichbar. Bitte benachrichtige den Dienstadministrator, falls dies weiterhin passiert.", "pad.modals.disconnected.cause": "Möglicherweise ist der Server nicht erreichbar. Bitte benachrichtige den Dienstadministrator, falls dies weiterhin passiert.",

View file

@ -18,6 +18,7 @@
"VezonThunder" "VezonThunder"
] ]
}, },
"admin_plugins.available": "Saatavilla olevat liitännäiset",
"admin_plugins.available_install.value": "Lataa", "admin_plugins.available_install.value": "Lataa",
"admin_plugins.available_search.placeholder": "Etsi asennettavia laajennuksia", "admin_plugins.available_search.placeholder": "Etsi asennettavia laajennuksia",
"admin_plugins.description": "Kuvaus", "admin_plugins.description": "Kuvaus",
@ -92,7 +93,7 @@
"pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Tuonti on tuettu vain HTML- ja raakatekstitiedostoista. Monipuoliset tuontiominaisuudet ovat käytettävissä <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">asentamalla AbiWordin tai LibreOfficen</a>.", "pad.importExport.abiword.innerHTML": "Tuonti on tuettu vain HTML- ja raakatekstitiedostoista. Monipuoliset tuontiominaisuudet ovat käytettävissä <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">asentamalla AbiWordin tai LibreOfficen</a>.",
"pad.modals.connected": "Yhdistetty.", "pad.modals.connected": "Yhdistetty.",
"pad.modals.reconnecting": "Muodostetaan yhteyttä muistioon uudelleen...", "pad.modals.reconnecting": "Muodostetaan yhteyttä muistioon uudelleen",
"pad.modals.forcereconnect": "Pakota yhdistämään uudelleen", "pad.modals.forcereconnect": "Pakota yhdistämään uudelleen",
"pad.modals.reconnecttimer": "Yritetään yhdistää uudelleen", "pad.modals.reconnecttimer": "Yritetään yhdistää uudelleen",
"pad.modals.cancel": "Peruuta", "pad.modals.cancel": "Peruuta",

View file

@ -2,12 +2,47 @@
"@metadata": { "@metadata": {
"authors": [ "authors": [
"Elisardojm", "Elisardojm",
"Ghose",
"Toliño" "Toliño"
] ]
}, },
"admin.page-title": "Panel de administración - Etherpad",
"admin_plugins": "Xestor de complementos",
"admin_plugins.available": "Complementos dispoñibles",
"admin_plugins.available_not-found": "Non se atopan complementos.",
"admin_plugins.available_fetching": "Obtendo...",
"admin_plugins.available_install.value": "Instalar",
"admin_plugins.available_search.placeholder": "Buscar complementos para instalar",
"admin_plugins.description": "Descrición",
"admin_plugins.installed": "Complementos instalados",
"admin_plugins.installed_fetching": "Obtendo os complementos instalados...",
"admin_plugins.installed_nothing": "Aínda non instalaches ningún complemento.",
"admin_plugins.installed_uninstall.value": "Desinstalar",
"admin_plugins.last-update": "Última actualización",
"admin_plugins.name": "Nome",
"admin_plugins.page-title": "Xestos de complementos - Etherpad",
"admin_plugins.version": "Versión",
"admin_plugins_info": "Información para resolver problemas",
"admin_plugins_info.hooks": "Ganchos instalados",
"admin_plugins_info.hooks_client": "Ganchos do lado do cliente",
"admin_plugins_info.hooks_server": "Ganchos do lado do servidor",
"admin_plugins_info.parts": "Partes instaladas",
"admin_plugins_info.plugins": "Complementos instalados",
"admin_plugins_info.page-title": "Información do complemento - Etherpad",
"admin_plugins_info.version": "Versión de Etherpad",
"admin_plugins_info.version_latest": "Última versión dispoñible",
"admin_plugins_info.version_number": "Número da versión",
"admin_settings": "Axustes",
"admin_settings.current": "Configuración actual",
"admin_settings.current_example-devel": "Modelo de exemplo dos axustes de desenvolvemento",
"admin_settings.current_example-prod": "Modelo de exemplo dos axustes en produción",
"admin_settings.current_restart.value": "Reiniciar Etherpad",
"admin_settings.current_save.value": "Gardar axustes",
"admin_settings.page-title": "Axustes - Etherpad",
"index.newPad": "Novo documento", "index.newPad": "Novo documento",
"index.createOpenPad": "ou cree/abra un documento co nome:", "index.createOpenPad": "ou crea/abre un documento co nome:",
"pad.toolbar.bold.title": "Negra (Ctrl-B)", "index.openPad": "abrir un Pad existente co nome:",
"pad.toolbar.bold.title": "Resaltado (Ctrl-B)",
"pad.toolbar.italic.title": "Cursiva (Ctrl-I)", "pad.toolbar.italic.title": "Cursiva (Ctrl-I)",
"pad.toolbar.underline.title": "Subliñar (Ctrl-U)", "pad.toolbar.underline.title": "Subliñar (Ctrl-U)",
"pad.toolbar.strikethrough.title": "Riscar (Ctrl+5)", "pad.toolbar.strikethrough.title": "Riscar (Ctrl+5)",
@ -17,28 +52,30 @@
"pad.toolbar.unindent.title": "Sen sangría (Maiús.+TAB)", "pad.toolbar.unindent.title": "Sen sangría (Maiús.+TAB)",
"pad.toolbar.undo.title": "Desfacer (Ctrl-Z)", "pad.toolbar.undo.title": "Desfacer (Ctrl-Z)",
"pad.toolbar.redo.title": "Refacer (Ctrl-Y)", "pad.toolbar.redo.title": "Refacer (Ctrl-Y)",
"pad.toolbar.clearAuthorship.title": "Limpar as cores de identificación dos autores (Ctrl+Shift+C)", "pad.toolbar.clearAuthorship.title": "Eliminar as cores que identifican ás autoras (Ctrl+Shift+C)",
"pad.toolbar.import_export.title": "Importar/Exportar desde/a diferentes formatos de ficheiro", "pad.toolbar.import_export.title": "Importar/Exportar desde/a diferentes formatos de ficheiro",
"pad.toolbar.timeslider.title": "Liña do tempo", "pad.toolbar.timeslider.title": "Liña do tempo",
"pad.toolbar.savedRevision.title": "Gardar a revisión", "pad.toolbar.savedRevision.title": "Gardar a revisión",
"pad.toolbar.settings.title": "Configuracións", "pad.toolbar.settings.title": "Axustes",
"pad.toolbar.embed.title": "Compartir e incorporar este documento", "pad.toolbar.embed.title": "Compartir e incorporar este documento",
"pad.toolbar.showusers.title": "Mostrar os usuarios deste documento", "pad.toolbar.showusers.title": "Mostrar as usuarias deste documento",
"pad.colorpicker.save": "Gardar", "pad.colorpicker.save": "Gardar",
"pad.colorpicker.cancel": "Cancelar", "pad.colorpicker.cancel": "Cancelar",
"pad.loading": "Cargando...", "pad.loading": "Cargando...",
"pad.noCookie": "Non se puido atopar a cookie. Por favor, habilite as cookies no seu navegador!", "pad.noCookie": "Non se puido atopar a cookie. Por favor, habilita as cookies no teu navegador! A túa sesión e axustes non se gardarán entre visitas. Esto podería deberse a que Etherpad está incluído nalgún iFrame nalgúns navegadores. Asegúrate de que Etherpad está no mesmo subdominio/dominio que o iFrame pai",
"pad.permissionDenied": "Non ten permiso para acceder a este documento", "pad.permissionDenied": "Non tes permiso para acceder a este documento",
"pad.settings.padSettings": "Configuracións do documento", "pad.settings.padSettings": "Configuracións do documento",
"pad.settings.myView": "A miña vista", "pad.settings.myView": "A miña vista",
"pad.settings.stickychat": "Chat sempre visible", "pad.settings.stickychat": "Chat sempre visible",
"pad.settings.chatandusers": "Mostrar o chat e os usuarios", "pad.settings.chatandusers": "Mostrar o chat e os usuarios",
"pad.settings.colorcheck": "Cores de identificación", "pad.settings.colorcheck": "Cores de identificación",
"pad.settings.linenocheck": "Números de liña", "pad.settings.linenocheck": "Números de liña",
"pad.settings.rtlcheck": "Quere ler o contido da dereita á esquerda?", "pad.settings.rtlcheck": "Queres ler o contido da dereita á esquerda?",
"pad.settings.fontType": "Tipo de letra:", "pad.settings.fontType": "Tipo de letra:",
"pad.settings.fontType.normal": "Normal", "pad.settings.fontType.normal": "Normal",
"pad.settings.language": "Lingua:", "pad.settings.language": "Lingua:",
"pad.settings.about": "Acerca de",
"pad.settings.poweredBy": "Grazas a",
"pad.importExport.import_export": "Importar/Exportar", "pad.importExport.import_export": "Importar/Exportar",
"pad.importExport.import": "Cargar un ficheiro de texto ou documento", "pad.importExport.import": "Cargar un ficheiro de texto ou documento",
"pad.importExport.importSuccessful": "Correcto!", "pad.importExport.importSuccessful": "Correcto!",
@ -49,9 +86,9 @@
"pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF", "pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Só pode importar texto simple ou formatos HTML. Para obter máis información sobre as características de importación avanzadas <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instale AbiWord</a>.", "pad.importExport.abiword.innerHTML": "Só podes importar texto simple ou formatos HTML. Para obter máis información sobre as características de importación avanzadas <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instala AbiWord</a>.",
"pad.modals.connected": "Conectado.", "pad.modals.connected": "Conectado.",
"pad.modals.reconnecting": "Reconectando co seu documento...", "pad.modals.reconnecting": "Reconectando co teu documento...",
"pad.modals.forcereconnect": "Forzar a reconexión", "pad.modals.forcereconnect": "Forzar a reconexión",
"pad.modals.reconnecttimer": "Intentarase reconectar en", "pad.modals.reconnecttimer": "Intentarase reconectar en",
"pad.modals.cancel": "Cancelar", "pad.modals.cancel": "Cancelar",
@ -73,6 +110,10 @@
"pad.modals.corruptPad.cause": "Isto pode deberse a unha cofiguración errónea do servidor ou algún outro comportamento inesperado. Póñase en contacto co administrador do servizo.", "pad.modals.corruptPad.cause": "Isto pode deberse a unha cofiguración errónea do servidor ou algún outro comportamento inesperado. Póñase en contacto co administrador do servizo.",
"pad.modals.deleted": "Borrado.", "pad.modals.deleted": "Borrado.",
"pad.modals.deleted.explanation": "Este documento foi eliminado.", "pad.modals.deleted.explanation": "Este documento foi eliminado.",
"pad.modals.rateLimited": "Taxa limitada.",
"pad.modals.rateLimited.explanation": "Enviaches demasiadas mensaxes a este documento polo que te desconectamos.",
"pad.modals.rejected.explanation": "O servidor rexeitou unha mensaxe que o teu navegador enviou.",
"pad.modals.rejected.cause": "O servidor podería ter sido actualizado mentras ollabas o documento, ou pode que sexa un fallo de Etherpad. Intenta recargar a páxina.",
"pad.modals.disconnected": "Foi desconectado.", "pad.modals.disconnected": "Foi desconectado.",
"pad.modals.disconnected.explanation": "Perdeuse a conexión co servidor", "pad.modals.disconnected.explanation": "Perdeuse a conexión co servidor",
"pad.modals.disconnected.cause": "O servidor non está dispoñible. Póñase en contacto co administrador do servizo se o problema continúa.", "pad.modals.disconnected.cause": "O servidor non está dispoñible. Póñase en contacto co administrador do servizo se o problema continúa.",
@ -83,6 +124,9 @@
"pad.chat": "Chat", "pad.chat": "Chat",
"pad.chat.title": "Abrir o chat deste documento.", "pad.chat.title": "Abrir o chat deste documento.",
"pad.chat.loadmessages": "Cargar máis mensaxes", "pad.chat.loadmessages": "Cargar máis mensaxes",
"pad.chat.stick.title": "Pegar a conversa á pantalla",
"pad.chat.writeMessage.placeholder": "Escribe aquí a túa mensaxe",
"timeslider.followContents": "Segue as actualizacións do contido",
"timeslider.pageTitle": "Liña do tempo de {{appTitle}}", "timeslider.pageTitle": "Liña do tempo de {{appTitle}}",
"timeslider.toolbar.returnbutton": "Volver ao documento", "timeslider.toolbar.returnbutton": "Volver ao documento",
"timeslider.toolbar.authors": "Autores:", "timeslider.toolbar.authors": "Autores:",
@ -112,7 +156,7 @@
"pad.savedrevs.timeslider": "Pode consultar as revisións gardadas visitando a liña do tempo", "pad.savedrevs.timeslider": "Pode consultar as revisións gardadas visitando a liña do tempo",
"pad.userlist.entername": "Insira o seu nome", "pad.userlist.entername": "Insira o seu nome",
"pad.userlist.unnamed": "anónimo", "pad.userlist.unnamed": "anónimo",
"pad.editbar.clearcolors": "Quere limpar as cores de identificación dos autores en todo o documento?", "pad.editbar.clearcolors": "Eliminar as cores relativas aos autores en todo o documento? Non se poderán recuperar",
"pad.impexp.importbutton": "Importar agora", "pad.impexp.importbutton": "Importar agora",
"pad.impexp.importing": "Importando...", "pad.impexp.importing": "Importando...",
"pad.impexp.confirmimport": "A importación dun ficheiro ha sobrescribir o texto actual do documento. Está seguro de querer continuar?", "pad.impexp.confirmimport": "A importación dun ficheiro ha sobrescribir o texto actual do documento. Está seguro de querer continuar?",
@ -121,5 +165,6 @@
"pad.impexp.uploadFailed": "Houbo un erro ao cargar o ficheiro; inténteo de novo", "pad.impexp.uploadFailed": "Houbo un erro ao cargar o ficheiro; inténteo de novo",
"pad.impexp.importfailed": "Fallou a importación", "pad.impexp.importfailed": "Fallou a importación",
"pad.impexp.copypaste": "Copie e pegue", "pad.impexp.copypaste": "Copie e pegue",
"pad.impexp.exportdisabled": "A exportación en formato {{type}} está desactivada. Póñase en contacto co administrador do sistema se quere máis detalles." "pad.impexp.exportdisabled": "A exportación en formato {{type}} está desactivada. Póñase en contacto co administrador do sistema se quere máis detalles.",
"pad.impexp.maxFileSize": "Ficheiro demasiado granda. Contacta coa administración para aumentar o tamaño permitido para importacións"
} }

View file

@ -4,6 +4,7 @@
"Athena in Wonderland", "Athena in Wonderland",
"Cainamarques", "Cainamarques",
"GoEThe", "GoEThe",
"Guilha",
"Hamilton Abreu", "Hamilton Abreu",
"Imperadeiro98", "Imperadeiro98",
"Luckas", "Luckas",
@ -16,9 +17,42 @@
"Waldyrious" "Waldyrious"
] ]
}, },
"admin.page-title": "Painel do administrador - Etherpad",
"admin_plugins": "Gestor de plugins",
"admin_plugins.available": "Plugins disponíveis",
"admin_plugins.available_not-found": "Não foram encontrados plugins.",
"admin_plugins.available_fetching": "A obter...",
"admin_plugins.available_install.value": "Instalar",
"admin_plugins.available_search.placeholder": "Procura plugins para instalar",
"admin_plugins.description": "Descrição",
"admin_plugins.installed": "Plugins instalados",
"admin_plugins.installed_fetching": "A obter plugins instalados...",
"admin_plugins.installed_nothing": "Não instalas-te nenhum plugin ainda.",
"admin_plugins.installed_uninstall.value": "Desinstalar",
"admin_plugins.last-update": "Ultima atualização",
"admin_plugins.name": "Nome",
"admin_plugins.page-title": "Gestor de plugins - Etherpad",
"admin_plugins.version": "Versão",
"admin_plugins_info": "Informação de resolução de problemas",
"admin_plugins_info.hooks": "Hooks instalados",
"admin_plugins_info.hooks_client": "Hooks do lado-do-cliente",
"admin_plugins_info.hooks_server": "Hooks do lado-do-servidor",
"admin_plugins_info.parts": "Partes instaladas",
"admin_plugins_info.plugins": "Plugins instalados",
"admin_plugins_info.page-title": "Informação do plugin - Etherpad",
"admin_plugins_info.version": "Versão do Etherpad",
"admin_plugins_info.version_latest": "Última versão disponível",
"admin_plugins_info.version_number": "Número de versão",
"admin_settings": "Definições",
"admin_settings.current": "Configuração atual",
"admin_settings.current_example-devel": "Exemplo do modo de Desenvolvedor",
"admin_settings.current_example-prod": "Exemplo do modo de Produção",
"admin_settings.current_restart.value": "Reiniciar Etherpad",
"admin_settings.current_save.value": "Guardar Definições",
"admin_settings.page-title": "Definições - Etherpad",
"index.newPad": "Nova Nota", "index.newPad": "Nova Nota",
"index.createOpenPad": "ou crie/abra uma nota com o nome:", "index.createOpenPad": "ou cria/abre uma nota com o nome:",
"index.openPad": "abrir uma «Nota» existente com o nome:", "index.openPad": "abrir uma Nota existente com o nome:",
"pad.toolbar.bold.title": "Negrito (Ctrl+B)", "pad.toolbar.bold.title": "Negrito (Ctrl+B)",
"pad.toolbar.italic.title": "Itálico (Ctrl+I)", "pad.toolbar.italic.title": "Itálico (Ctrl+I)",
"pad.toolbar.underline.title": "Sublinhado (Ctrl+U)", "pad.toolbar.underline.title": "Sublinhado (Ctrl+U)",
@ -26,7 +60,7 @@
"pad.toolbar.ol.title": "Lista ordenada (Ctrl+Shift+N)", "pad.toolbar.ol.title": "Lista ordenada (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "Lista desordenada (Ctrl+Shift+L)", "pad.toolbar.ul.title": "Lista desordenada (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "Indentar (TAB)", "pad.toolbar.indent.title": "Indentar (TAB)",
"pad.toolbar.unindent.title": "Remover indentação (Shift+TAB)", "pad.toolbar.unindent.title": "Indentação (Shift+TAB)",
"pad.toolbar.undo.title": "Desfazer (Ctrl+Z)", "pad.toolbar.undo.title": "Desfazer (Ctrl+Z)",
"pad.toolbar.redo.title": "Refazer (Ctrl+Y)", "pad.toolbar.redo.title": "Refazer (Ctrl+Y)",
"pad.toolbar.clearAuthorship.title": "Limpar cores de autoria (Ctrl+Shift+C)", "pad.toolbar.clearAuthorship.title": "Limpar cores de autoria (Ctrl+Shift+C)",
@ -65,7 +99,7 @@
"pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Só pode fazer importações de texto não formatado ou com formato HTML. Para funcionalidades de importação de texto mais avançadas, <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instale AbiWord ou LibreOffice</a>, por favor.", "pad.importExport.abiword.innerHTML": "Só pode fazer importações de texto não formatado ou com formato HTML. Para funcionalidades de importação de texto mais avançadas, <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instale AbiWord ou LibreOffice</a>, por favor.",
"pad.modals.connected": "Ligado.", "pad.modals.connected": "Ligado.",
"pad.modals.reconnecting": "A restabelecer ligação ao seu bloco…", "pad.modals.reconnecting": "A restabelecer ligação à nota…",
"pad.modals.forcereconnect": "Forçar restabelecimento de ligação", "pad.modals.forcereconnect": "Forçar restabelecimento de ligação",
"pad.modals.reconnecttimer": "A tentar restabelecer ligação", "pad.modals.reconnecttimer": "A tentar restabelecer ligação",
"pad.modals.cancel": "Cancelar", "pad.modals.cancel": "Cancelar",
@ -89,6 +123,8 @@
"pad.modals.deleted.explanation": "Esta nota foi removida.", "pad.modals.deleted.explanation": "Esta nota foi removida.",
"pad.modals.rateLimited": "Limitado.", "pad.modals.rateLimited": "Limitado.",
"pad.modals.rateLimited.explanation": "Enviou demasiadas mensagens para este pad, por isso foi desligado.", "pad.modals.rateLimited.explanation": "Enviou demasiadas mensagens para este pad, por isso foi desligado.",
"pad.modals.rejected.explanation": "O servidor rejeitou a mensagem que foi enviada pelo teu navegador.",
"pad.modals.rejected.cause": "O server foi atualizado enquanto estávas a ver esta nota, ou talvez seja apenas um bug do Etherpad. Tenta recarregar a página.",
"pad.modals.disconnected": "Você foi desligado.", "pad.modals.disconnected": "Você foi desligado.",
"pad.modals.disconnected.explanation": "A ligação ao servidor foi perdida", "pad.modals.disconnected.explanation": "A ligação ao servidor foi perdida",
"pad.modals.disconnected.cause": "O servidor pode estar indisponível. Por favor, notifique o administrador de serviço se isto continuar a acontecer.", "pad.modals.disconnected.cause": "O servidor pode estar indisponível. Por favor, notifique o administrador de serviço se isto continuar a acontecer.",

View file

@ -18,21 +18,39 @@
"Арсен Асхат" "Арсен Асхат"
] ]
}, },
"admin.page-title": "Панель администратора — Etherpad",
"admin_plugins": "Менеджер плагинов", "admin_plugins": "Менеджер плагинов",
"admin_plugins.available": "Доступные плагины", "admin_plugins.available": "Доступные плагины",
"admin_plugins.available_not-found": "Плагины не найдены.", "admin_plugins.available_not-found": "Плагины не найдены.",
"admin_plugins.available_fetching": "Получение…",
"admin_plugins.available_install.value": "Установить", "admin_plugins.available_install.value": "Установить",
"admin_plugins.available_search.placeholder": "Искать плагины для установки",
"admin_plugins.description": "Описание", "admin_plugins.description": "Описание",
"admin_plugins.installed": "Установленные плагины", "admin_plugins.installed": "Установленные плагины",
"admin_plugins.installed_fetching": "Получение установленных плагинов…",
"admin_plugins.installed_nothing": "Вы еще не установили ни одного плагина.", "admin_plugins.installed_nothing": "Вы еще не установили ни одного плагина.",
"admin_plugins.installed_uninstall.value": "Удалить", "admin_plugins.installed_uninstall.value": "Удалить",
"admin_plugins.last-update": "Последнее обновление", "admin_plugins.last-update": "Последнее обновление",
"admin_plugins.name": "Название",
"admin_plugins.page-title": "Менеджер плагинов — Etherpad",
"admin_plugins.version": "Версия", "admin_plugins.version": "Версия",
"admin_plugins_info": "Информация об устранении неполадок",
"admin_plugins_info.hooks": "Установленные крючки", "admin_plugins_info.hooks": "Установленные крючки",
"admin_plugins_info.hooks_client": "Клиентские хуки",
"admin_plugins_info.hooks_server": "Серверные хуки",
"admin_plugins_info.parts": "Установленные части",
"admin_plugins_info.plugins": "Установленные плагины",
"admin_plugins_info.page-title": "Информация о плагине — Etherpad",
"admin_plugins_info.version": "Версия Etherpad",
"admin_plugins_info.version_latest": "Последняя доступная версия",
"admin_plugins_info.version_number": "Номер версии", "admin_plugins_info.version_number": "Номер версии",
"admin_settings": "Настройки", "admin_settings": "Настройки",
"admin_settings.current": "Текущая конфигурация", "admin_settings.current": "Текущая конфигурация",
"admin_settings.current_example-devel": "Пример шаблона настроек для среда разработки",
"admin_settings.current_example-prod": "Пример шаблона настроек для боевой среды",
"admin_settings.current_restart.value": "Перезагрузить Etherpad",
"admin_settings.current_save.value": "Сохранить настройки", "admin_settings.current_save.value": "Сохранить настройки",
"admin_settings.page-title": "Настройки — Etherpad",
"index.newPad": "Создать", "index.newPad": "Создать",
"index.createOpenPad": "или создать/открыть документ с именем:", "index.createOpenPad": "или создать/открыть документ с именем:",
"index.openPad": "откройте существующий документ с именем:", "index.openPad": "откройте существующий документ с именем:",

View file

@ -9,6 +9,10 @@
"Upwinxp" "Upwinxp"
] ]
}, },
"admin_plugins.last-update": "Zadnja posodobitev",
"admin_plugins.name": "Ime",
"admin_plugins.version": "Različica",
"admin_settings": "Nastavitve",
"index.newPad": "Nov dokument", "index.newPad": "Nov dokument",
"index.createOpenPad": "ali pa ustvari/odpri dokument z imenom:", "index.createOpenPad": "ali pa ustvari/odpri dokument z imenom:",
"pad.toolbar.bold.title": "Krepko (Ctrl-B)", "pad.toolbar.bold.title": "Krepko (Ctrl-B)",
@ -54,7 +58,7 @@
"pad.importExport.exportword": "DOC (zapis Microsoft Word)", "pad.importExport.exportword": "DOC (zapis Microsoft Word)",
"pad.importExport.exportpdf": "PDF (zapis Acrobat PDF)", "pad.importExport.exportpdf": "PDF (zapis Acrobat PDF)",
"pad.importExport.exportopen": "ODF (zapis Open Document)", "pad.importExport.exportopen": "ODF (zapis Open Document)",
"pad.importExport.abiword.innerHTML": "Uvoziti je mogoče le običajno neoblikovano besedilo in zapise HTML. Za naprednejše zmožnosti namestite <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">program AbiWord</a>.", "pad.importExport.abiword.innerHTML": "Uvoziti je mogoče le neoblikovano besedilo in zapise HTML. Za naprednejše možnosti uvoza namestite program <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">AbiWord</a>.",
"pad.modals.connected": "Povezano.", "pad.modals.connected": "Povezano.",
"pad.modals.reconnecting": "Poteka povezovanje z dokumentom ...", "pad.modals.reconnecting": "Poteka povezovanje z dokumentom ...",
"pad.modals.forcereconnect": "Vsili ponovno povezavo", "pad.modals.forcereconnect": "Vsili ponovno povezavo",
@ -64,7 +68,7 @@
"pad.modals.userdup.explanation": "Videti je, da je ta dokument odprt v več kot enem oknu brskalnika na tem računalniku.", "pad.modals.userdup.explanation": "Videti je, da je ta dokument odprt v več kot enem oknu brskalnika na tem računalniku.",
"pad.modals.userdup.advice": "Ponovno vzpostavite povezavo in uporabljajte to okno.", "pad.modals.userdup.advice": "Ponovno vzpostavite povezavo in uporabljajte to okno.",
"pad.modals.unauth": "Nepooblaščen dostop", "pad.modals.unauth": "Nepooblaščen dostop",
"pad.modals.unauth.explanation": "Med pregledovanjem te strani so se dovoljenja za ogled spremenila. Poskusite se ponovno povezati.", "pad.modals.unauth.explanation": "Med ogledovanjem strani so se dovoljenja za ogled spremenila. Poskusite se znova povezati.",
"pad.modals.looping.explanation": "Zaznane so težave pri komunikaciji s strežnikom za usklajevanje.", "pad.modals.looping.explanation": "Zaznane so težave pri komunikaciji s strežnikom za usklajevanje.",
"pad.modals.looping.cause": "Morda ste se povezali preko neustrezno nastavljenega požarnega zidu ali posredniškega strežnika.", "pad.modals.looping.cause": "Morda ste se povezali preko neustrezno nastavljenega požarnega zidu ali posredniškega strežnika.",
"pad.modals.initsocketfail": "Strežnik je nedosegljiv.", "pad.modals.initsocketfail": "Strežnik je nedosegljiv.",

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* This module provides all API functions * This module provides all API functions
*/ */
@ -18,8 +19,8 @@
* limitations under the License. * limitations under the License.
*/ */
const Changeset = require('ep_etherpad-lite/static/js/Changeset'); const Changeset = require('../../static/js/Changeset');
const customError = require('../utils/customError'); const CustomError = require('../utils/customError');
const padManager = require('./PadManager'); const padManager = require('./PadManager');
const padMessageHandler = require('../handler/PadMessageHandler'); const padMessageHandler = require('../handler/PadMessageHandler');
const readOnlyManager = require('./ReadOnlyManager'); const readOnlyManager = require('./ReadOnlyManager');
@ -101,7 +102,7 @@ Example returns:
} }
*/ */
exports.getAttributePool = async function (padID) { exports.getAttributePool = async (padID) => {
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
return {pool: pad.pool}; return {pool: pad.pool};
}; };
@ -119,7 +120,7 @@ Example returns:
} }
*/ */
exports.getRevisionChangeset = async function (padID, rev) { exports.getRevisionChangeset = async (padID, rev) => {
// try to parse the revision number // try to parse the revision number
if (rev !== undefined) { if (rev !== undefined) {
rev = checkValidRev(rev); rev = checkValidRev(rev);
@ -133,7 +134,7 @@ exports.getRevisionChangeset = async function (padID, rev) {
if (rev !== undefined) { if (rev !== undefined) {
// check if this is a valid revision // check if this is a valid revision
if (rev > head) { if (rev > head) {
throw new customError('rev is higher than the head revision of the pad', 'apierror'); throw new CustomError('rev is higher than the head revision of the pad', 'apierror');
} }
// get the changeset for this revision // get the changeset for this revision
@ -152,7 +153,7 @@ Example returns:
{code: 0, message:"ok", data: {text:"Welcome Text"}} {code: 0, message:"ok", data: {text:"Welcome Text"}}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.getText = async function (padID, rev) { exports.getText = async (padID, rev) => {
// try to parse the revision number // try to parse the revision number
if (rev !== undefined) { if (rev !== undefined) {
rev = checkValidRev(rev); rev = checkValidRev(rev);
@ -166,7 +167,7 @@ exports.getText = async function (padID, rev) {
if (rev !== undefined) { if (rev !== undefined) {
// check if this is a valid revision // check if this is a valid revision
if (rev > head) { if (rev > head) {
throw new customError('rev is higher than the head revision of the pad', 'apierror'); throw new CustomError('rev is higher than the head revision of the pad', 'apierror');
} }
// get the text of this revision // get the text of this revision
@ -188,10 +189,10 @@ Example returns:
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
{code: 1, message:"text too long", data: null} {code: 1, message:"text too long", data: null}
*/ */
exports.setText = async function (padID, text) { exports.setText = async (padID, text) => {
// text is required // text is required
if (typeof text !== 'string') { if (typeof text !== 'string') {
throw new customError('text is not a string', 'apierror'); throw new CustomError('text is not a string', 'apierror');
} }
// get the pad // get the pad
@ -212,10 +213,10 @@ Example returns:
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
{code: 1, message:"text too long", data: null} {code: 1, message:"text too long", data: null}
*/ */
exports.appendText = async function (padID, text) { exports.appendText = async (padID, text) => {
// text is required // text is required
if (typeof text !== 'string') { if (typeof text !== 'string') {
throw new customError('text is not a string', 'apierror'); throw new CustomError('text is not a string', 'apierror');
} }
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
@ -233,7 +234,7 @@ Example returns:
{code: 0, message:"ok", data: {text:"Welcome <strong>Text</strong>"}} {code: 0, message:"ok", data: {text:"Welcome <strong>Text</strong>"}}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.getHTML = async function (padID, rev) { exports.getHTML = async (padID, rev) => {
if (rev !== undefined) { if (rev !== undefined) {
rev = checkValidRev(rev); rev = checkValidRev(rev);
} }
@ -245,7 +246,7 @@ exports.getHTML = async function (padID, rev) {
// check if this is a valid revision // check if this is a valid revision
const head = pad.getHeadRevisionNumber(); const head = pad.getHeadRevisionNumber();
if (rev > head) { if (rev > head) {
throw new customError('rev is higher than the head revision of the pad', 'apierror'); throw new CustomError('rev is higher than the head revision of the pad', 'apierror');
} }
} }
@ -265,10 +266,10 @@ Example returns:
{code: 0, message:"ok", data: null} {code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.setHTML = async function (padID, html) { exports.setHTML = async (padID, html) => {
// html string is required // html string is required
if (typeof html !== 'string') { if (typeof html !== 'string') {
throw new customError('html is not a string', 'apierror'); throw new CustomError('html is not a string', 'apierror');
} }
// get the pad // get the pad
@ -278,7 +279,7 @@ exports.setHTML = async function (padID, html) {
try { try {
await importHtml.setPadHTML(pad, cleanText(html)); await importHtml.setPadHTML(pad, cleanText(html));
} catch (e) { } catch (e) {
throw new customError('HTML is malformed', 'apierror'); throw new CustomError('HTML is malformed', 'apierror');
} }
// update the clients on the pad // update the clients on the pad
@ -294,23 +295,25 @@ getChatHistory(padId, start, end), returns a part of or the whole chat-history o
Example returns: Example returns:
{"code":0,"message":"ok","data":{"messages":[{"text":"foo","authorID":"a.foo","time":1359199533759,"userName":"test"}, {"code":0,"message":"ok","data":{"messages":[
{"text":"bar","authorID":"a.foo","time":1359199534622,"userName":"test"}]}} {"text":"foo","authorID":"a.foo","time":1359199533759,"userName":"test"},
{"text":"bar","authorID":"a.foo","time":1359199534622,"userName":"test"}
]}}
{code: 1, message:"start is higher or equal to the current chatHead", data: null} {code: 1, message:"start is higher or equal to the current chatHead", data: null}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.getChatHistory = async function (padID, start, end) { exports.getChatHistory = async (padID, start, end) => {
if (start && end) { if (start && end) {
if (start < 0) { if (start < 0) {
throw new customError('start is below zero', 'apierror'); throw new CustomError('start is below zero', 'apierror');
} }
if (end < 0) { if (end < 0) {
throw new customError('end is below zero', 'apierror'); throw new CustomError('end is below zero', 'apierror');
} }
if (start > end) { if (start > end) {
throw new customError('start is higher than end', 'apierror'); throw new CustomError('start is higher than end', 'apierror');
} }
} }
@ -320,16 +323,16 @@ exports.getChatHistory = async function (padID, start, end) {
const chatHead = pad.chatHead; const chatHead = pad.chatHead;
// fall back to getting the whole chat-history if a parameter is missing // fall back to getting the whole chat-history if a parameter is missing
if (!start || !end) { if (!start || !end) {
start = 0; start = 0;
end = pad.chatHead; end = pad.chatHead;
} }
if (start > chatHead) { if (start > chatHead) {
throw new customError('start is higher than the current chatHead', 'apierror'); throw new CustomError('start is higher than the current chatHead', 'apierror');
} }
if (end > chatHead) { if (end > chatHead) {
throw new customError('end is higher than the current chatHead', 'apierror'); throw new CustomError('end is higher than the current chatHead', 'apierror');
} }
// the the whole message-log and return it to the client // the the whole message-log and return it to the client
@ -339,21 +342,22 @@ exports.getChatHistory = async function (padID, start, end) {
}; };
/** /**
appendChatMessage(padID, text, authorID, time), creates a chat message for the pad id, time is a timestamp appendChatMessage(padID, text, authorID, time), creates a chat message for the pad id,
time is a timestamp
Example returns: Example returns:
{code: 0, message:"ok", data: null} {code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.appendChatMessage = async function (padID, text, authorID, time) { exports.appendChatMessage = async (padID, text, authorID, time) => {
// text is required // text is required
if (typeof text !== 'string') { if (typeof text !== 'string') {
throw new customError('text is not a string', 'apierror'); throw new CustomError('text is not a string', 'apierror');
} }
// if time is not an integer value set time to current timestamp // if time is not an integer value set time to current timestamp
if (time === undefined || !is_int(time)) { if (time === undefined || !isInt(time)) {
time = Date.now(); time = Date.now();
} }
@ -375,7 +379,7 @@ Example returns:
{code: 0, message:"ok", data: {revisions: 56}} {code: 0, message:"ok", data: {revisions: 56}}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.getRevisionsCount = async function (padID) { exports.getRevisionsCount = async (padID) => {
// get the pad // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
return {revisions: pad.getHeadRevisionNumber()}; return {revisions: pad.getHeadRevisionNumber()};
@ -389,7 +393,7 @@ Example returns:
{code: 0, message:"ok", data: {savedRevisions: 42}} {code: 0, message:"ok", data: {savedRevisions: 42}}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.getSavedRevisionsCount = async function (padID) { exports.getSavedRevisionsCount = async (padID) => {
// get the pad // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
return {savedRevisions: pad.getSavedRevisionsNumber()}; return {savedRevisions: pad.getSavedRevisionsNumber()};
@ -403,7 +407,7 @@ Example returns:
{code: 0, message:"ok", data: {savedRevisions: [2, 42, 1337]}} {code: 0, message:"ok", data: {savedRevisions: [2, 42, 1337]}}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.listSavedRevisions = async function (padID) { exports.listSavedRevisions = async (padID) => {
// get the pad // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
return {savedRevisions: pad.getSavedRevisionsList()}; return {savedRevisions: pad.getSavedRevisionsList()};
@ -417,7 +421,7 @@ Example returns:
{code: 0, message:"ok", data: null} {code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.saveRevision = async function (padID, rev) { exports.saveRevision = async (padID, rev) => {
// check if rev is a number // check if rev is a number
if (rev !== undefined) { if (rev !== undefined) {
rev = checkValidRev(rev); rev = checkValidRev(rev);
@ -430,7 +434,7 @@ exports.saveRevision = async function (padID, rev) {
// the client asked for a special revision // the client asked for a special revision
if (rev !== undefined) { if (rev !== undefined) {
if (rev > head) { if (rev > head) {
throw new customError('rev is higher than the head revision of the pad', 'apierror'); throw new CustomError('rev is higher than the head revision of the pad', 'apierror');
} }
} else { } else {
rev = pad.getHeadRevisionNumber(); rev = pad.getHeadRevisionNumber();
@ -448,7 +452,7 @@ Example returns:
{code: 0, message:"ok", data: {lastEdited: 1340815946602}} {code: 0, message:"ok", data: {lastEdited: 1340815946602}}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.getLastEdited = async function (padID) { exports.getLastEdited = async (padID) => {
// get the pad // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
const lastEdited = await pad.getLastEdit(); const lastEdited = await pad.getLastEdit();
@ -463,16 +467,16 @@ Example returns:
{code: 0, message:"ok", data: null} {code: 0, message:"ok", data: null}
{code: 1, message:"pad does already exist", data: null} {code: 1, message:"pad does already exist", data: null}
*/ */
exports.createPad = async function (padID, text) { exports.createPad = async (padID, text) => {
if (padID) { if (padID) {
// ensure there is no $ in the padID // ensure there is no $ in the padID
if (padID.indexOf('$') !== -1) { if (padID.indexOf('$') !== -1) {
throw new customError("createPad can't create group pads", 'apierror'); throw new CustomError("createPad can't create group pads", 'apierror');
} }
// check for url special characters // check for url special characters
if (padID.match(/(\/|\?|&|#)/)) { if (padID.match(/(\/|\?|&|#)/)) {
throw new customError('malformed padID: Remove special characters', 'apierror'); throw new CustomError('malformed padID: Remove special characters', 'apierror');
} }
} }
@ -488,7 +492,7 @@ Example returns:
{code: 0, message:"ok", data: null} {code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.deletePad = async function (padID) { exports.deletePad = async (padID) => {
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
await pad.remove(); await pad.remove();
}; };
@ -501,10 +505,10 @@ exports.deletePad = async function (padID) {
{code:0, message:"ok", data:null} {code:0, message:"ok", data:null}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.restoreRevision = async function (padID, rev) { exports.restoreRevision = async (padID, rev) => {
// check if rev is a number // check if rev is a number
if (rev === undefined) { if (rev === undefined) {
throw new customError('rev is not defined', 'apierror'); throw new CustomError('rev is not defined', 'apierror');
} }
rev = checkValidRev(rev); rev = checkValidRev(rev);
@ -513,7 +517,7 @@ exports.restoreRevision = async function (padID, rev) {
// check if this is a valid revision // check if this is a valid revision
if (rev > pad.getHeadRevisionNumber()) { if (rev > pad.getHeadRevisionNumber()) {
throw new customError('rev is higher than the head revision of the pad', 'apierror'); throw new CustomError('rev is higher than the head revision of the pad', 'apierror');
} }
const atext = await pad.getInternalRevisionAText(rev); const atext = await pad.getInternalRevisionAText(rev);
@ -521,7 +525,7 @@ exports.restoreRevision = async function (padID, rev) {
const oldText = pad.text(); const oldText = pad.text();
atext.text += '\n'; atext.text += '\n';
function eachAttribRun(attribs, func) { const eachAttribRun = (attribs, func) => {
const attribsIter = Changeset.opIterator(attribs); const attribsIter = Changeset.opIterator(attribs);
let textIndex = 0; let textIndex = 0;
const newTextStart = 0; const newTextStart = 0;
@ -534,7 +538,7 @@ exports.restoreRevision = async function (padID, rev) {
} }
textIndex = nextIndex; textIndex = nextIndex;
} }
} };
// create a new changeset with a helper builder object // create a new changeset with a helper builder object
const builder = Changeset.builder(oldText.length); const builder = Changeset.builder(oldText.length);
@ -569,7 +573,7 @@ Example returns:
{code: 0, message:"ok", data: {padID: destinationID}} {code: 0, message:"ok", data: {padID: destinationID}}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.copyPad = async function (sourceID, destinationID, force) { exports.copyPad = async (sourceID, destinationID, force) => {
const pad = await getPadSafe(sourceID, true); const pad = await getPadSafe(sourceID, true);
await pad.copy(destinationID, force); await pad.copy(destinationID, force);
}; };
@ -583,7 +587,7 @@ Example returns:
{code: 0, message:"ok", data: {padID: destinationID}} {code: 0, message:"ok", data: {padID: destinationID}}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.copyPadWithoutHistory = async function (sourceID, destinationID, force) { exports.copyPadWithoutHistory = async (sourceID, destinationID, force) => {
const pad = await getPadSafe(sourceID, true); const pad = await getPadSafe(sourceID, true);
await pad.copyPadWithoutHistory(destinationID, force); await pad.copyPadWithoutHistory(destinationID, force);
}; };
@ -597,7 +601,7 @@ Example returns:
{code: 0, message:"ok", data: {padID: destinationID}} {code: 0, message:"ok", data: {padID: destinationID}}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.movePad = async function (sourceID, destinationID, force) { exports.movePad = async (sourceID, destinationID, force) => {
const pad = await getPadSafe(sourceID, true); const pad = await getPadSafe(sourceID, true);
await pad.copy(destinationID, force); await pad.copy(destinationID, force);
await pad.remove(); await pad.remove();
@ -611,7 +615,7 @@ Example returns:
{code: 0, message:"ok", data: null} {code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.getReadOnlyID = async function (padID) { exports.getReadOnlyID = async (padID) => {
// we don't need the pad object, but this function does all the security stuff for us // we don't need the pad object, but this function does all the security stuff for us
await getPadSafe(padID, true); await getPadSafe(padID, true);
@ -629,11 +633,11 @@ Example returns:
{code: 0, message:"ok", data: {padID: padID}} {code: 0, message:"ok", data: {padID: padID}}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.getPadID = async function (roID) { exports.getPadID = async (roID) => {
// get the PadId // get the PadId
const padID = await readOnlyManager.getPadId(roID); const padID = await readOnlyManager.getPadId(roID);
if (padID === null) { if (padID == null) {
throw new customError('padID does not exist', 'apierror'); throw new CustomError('padID does not exist', 'apierror');
} }
return {padID}; return {padID};
@ -647,7 +651,7 @@ Example returns:
{code: 0, message:"ok", data: null} {code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.setPublicStatus = async function (padID, publicStatus) { exports.setPublicStatus = async (padID, publicStatus) => {
// ensure this is a group pad // ensure this is a group pad
checkGroupPad(padID, 'publicStatus'); checkGroupPad(padID, 'publicStatus');
@ -670,7 +674,7 @@ Example returns:
{code: 0, message:"ok", data: {publicStatus: true}} {code: 0, message:"ok", data: {publicStatus: true}}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.getPublicStatus = async function (padID) { exports.getPublicStatus = async (padID) => {
// ensure this is a group pad // ensure this is a group pad
checkGroupPad(padID, 'publicStatus'); checkGroupPad(padID, 'publicStatus');
@ -687,7 +691,7 @@ Example returns:
{code: 0, message:"ok", data: {authorIDs : ["a.s8oes9dhwrvt0zif", "a.akf8finncvomlqva"]} {code: 0, message:"ok", data: {authorIDs : ["a.s8oes9dhwrvt0zif", "a.akf8finncvomlqva"]}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.listAuthorsOfPad = async function (padID) { exports.listAuthorsOfPad = async (padID) => {
// get the pad // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
const authorIDs = pad.getAllAuthors(); const authorIDs = pad.getAllAuthors();
@ -717,7 +721,7 @@ Example returns:
{code: 1, message:"padID does not exist"} {code: 1, message:"padID does not exist"}
*/ */
exports.sendClientsMessage = async function (padID, msg) { exports.sendClientsMessage = async (padID, msg) => {
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
padMessageHandler.handleCustomMessage(padID, msg); padMessageHandler.handleCustomMessage(padID, msg);
}; };
@ -730,7 +734,7 @@ Example returns:
{"code":0,"message":"ok","data":null} {"code":0,"message":"ok","data":null}
{"code":4,"message":"no or wrong API Key","data":null} {"code":4,"message":"no or wrong API Key","data":null}
*/ */
exports.checkToken = async function () { exports.checkToken = async () => {
}; };
/** /**
@ -741,7 +745,7 @@ Example returns:
{code: 0, message:"ok", data: {chatHead: 42}} {code: 0, message:"ok", data: {chatHead: 42}}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.getChatHead = async function (padID) { exports.getChatHead = async (padID) => {
// get the pad // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
return {chatHead: pad.chatHead}; return {chatHead: pad.chatHead};
@ -751,11 +755,21 @@ exports.getChatHead = async function (padID) {
createDiffHTML(padID, startRev, endRev) returns an object of diffs from 2 points in a pad createDiffHTML(padID, startRev, endRev) returns an object of diffs from 2 points in a pad
Example returns: Example returns:
{
{"code":0,"message":"ok","data":{"html":"<style>\n.authora_HKIv23mEbachFYfH {background-color: #a979d9}\n.authora_n4gEeMLsv1GivNeh {background-color: #a9b5d9}\n.removed {text-decoration: line-through; -ms-filter:'progid:DXImageTransform.Microsoft.Alpha(Opacity=80)'; filter: alpha(opacity=80); opacity: 0.8; }\n</style>Welcome to Etherpad!<br><br>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!<br><br>Get involved with Etherpad at <a href=\"http&#x3a;&#x2F;&#x2F;etherpad&#x2e;org\">http:&#x2F;&#x2F;etherpad.org</a><br><span class=\"authora_HKIv23mEbachFYfH\">aw</span><br><br>","authors":["a.HKIv23mEbachFYfH",""]}} "code": 0,
"message": "ok",
"data": {
"html": "...",
"authors": [
"a.HKIv23mEbachFYfH",
""
]
}
}
{"code":4,"message":"no or wrong API Key","data":null} {"code":4,"message":"no or wrong API Key","data":null}
*/ */
exports.createDiffHTML = async function (padID, startRev, endRev) { exports.createDiffHTML = async (padID, startRev, endRev) => {
// check if startRev is a number // check if startRev is a number
if (startRev !== undefined) { if (startRev !== undefined) {
startRev = checkValidRev(startRev); startRev = checkValidRev(startRev);
@ -768,8 +782,9 @@ exports.createDiffHTML = async function (padID, startRev, endRev) {
// get the pad // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
let padDiff;
try { try {
var padDiff = new PadDiff(pad, startRev, endRev); padDiff = new PadDiff(pad, startRev, endRev);
} catch (e) { } catch (e) {
throw {stop: e.message}; throw {stop: e.message};
} }
@ -793,7 +808,7 @@ exports.createDiffHTML = async function (padID, startRev, endRev) {
{"code":4,"message":"no or wrong API Key","data":null} {"code":4,"message":"no or wrong API Key","data":null}
*/ */
exports.getStats = async function () { exports.getStats = async () => {
const sessionInfos = padMessageHandler.sessioninfos; const sessionInfos = padMessageHandler.sessioninfos;
const sessionKeys = Object.keys(sessionInfos); const sessionKeys = Object.keys(sessionInfos);
@ -813,20 +828,18 @@ exports.getStats = async function () {
**************************** */ **************************** */
// checks if a number is an int // checks if a number is an int
function is_int(value) { const isInt = (value) => (parseFloat(value) === parseInt(value, 10)) && !isNaN(value);
return (parseFloat(value) == parseInt(value, 10)) && !isNaN(value);
}
// gets a pad safe // gets a pad safe
async function getPadSafe(padID, shouldExist, text) { async function getPadSafe(padID, shouldExist, text) {
// check if padID is a string // check if padID is a string
if (typeof padID !== 'string') { if (typeof padID !== 'string') {
throw new customError('padID is not a string', 'apierror'); throw new CustomError('padID is not a string', 'apierror');
} }
// check if the padID maches the requirements // check if the padID maches the requirements
if (!padManager.isValidPadId(padID)) { if (!padManager.isValidPadId(padID)) {
throw new customError('padID did not match requirements', 'apierror'); throw new CustomError('padID did not match requirements', 'apierror');
} }
// check if the pad exists // check if the pad exists
@ -834,12 +847,12 @@ async function getPadSafe(padID, shouldExist, text) {
if (!exists && shouldExist) { if (!exists && shouldExist) {
// does not exist, but should // does not exist, but should
throw new customError('padID does not exist', 'apierror'); throw new CustomError('padID does not exist', 'apierror');
} }
if (exists && !shouldExist) { if (exists && !shouldExist) {
// does exist, but shouldn't // does exist, but shouldn't
throw new customError('padID does already exist', 'apierror'); throw new CustomError('padID does already exist', 'apierror');
} }
// pad exists, let's get it // pad exists, let's get it
@ -848,33 +861,34 @@ async function getPadSafe(padID, shouldExist, text) {
// checks if a rev is a legal number // checks if a rev is a legal number
// pre-condition is that `rev` is not undefined // pre-condition is that `rev` is not undefined
function checkValidRev(rev) { const checkValidRev = (rev) => {
if (typeof rev !== 'number') { if (typeof rev !== 'number') {
rev = parseInt(rev, 10); rev = parseInt(rev, 10);
} }
// check if rev is a number // check if rev is a number
if (isNaN(rev)) { if (isNaN(rev)) {
throw new customError('rev is not a number', 'apierror'); throw new CustomError('rev is not a number', 'apierror');
} }
// ensure this is not a negative number // ensure this is not a negative number
if (rev < 0) { if (rev < 0) {
throw new customError('rev is not a negative number', 'apierror'); throw new CustomError('rev is not a negative number', 'apierror');
} }
// ensure this is not a float value // ensure this is not a float value
if (!is_int(rev)) { if (!isInt(rev)) {
throw new customError('rev is a float value', 'apierror'); throw new CustomError('rev is a float value', 'apierror');
} }
return rev; return rev;
} };
// checks if a padID is part of a group // checks if a padID is part of a group
function checkGroupPad(padID, field) { const checkGroupPad = (padID, field) => {
// ensure this is a group pad // ensure this is a group pad
if (padID && padID.indexOf('$') === -1) { if (padID && padID.indexOf('$') === -1) {
throw new customError(`You can only get/set the ${field} of pads that belong to a group`, 'apierror'); throw new CustomError(
} `You can only get/set the ${field} of pads that belong to a group`, 'apierror');
} }
};

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* The AuthorManager controlls all information about the Pad authors * The AuthorManager controlls all information about the Pad authors
*/ */
@ -19,11 +20,10 @@
*/ */
const db = require('./DB'); const db = require('./DB');
const customError = require('../utils/customError'); const CustomError = require('../utils/customError');
const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; const randomString = require('../../static/js/pad_utils').randomString;
exports.getColorPalette = function () { exports.getColorPalette = () => [
return [
'#ffc7c7', '#ffc7c7',
'#fff1c7', '#fff1c7',
'#e3ffc7', '#e3ffc7',
@ -89,15 +89,14 @@ exports.getColorPalette = function () {
'#f8d2a0', '#f8d2a0',
'#b3b3e6', '#b3b3e6',
]; ];
};
/** /**
* Checks if the author exists * Checks if the author exists
*/ */
exports.doesAuthorExist = async function (authorID) { exports.doesAuthorExist = async (authorID) => {
const author = await db.get(`globalAuthor:${authorID}`); const author = await db.get(`globalAuthor:${authorID}`);
return author !== null; return author != null;
}; };
/* exported for backwards compatibility */ /* exported for backwards compatibility */
@ -107,7 +106,7 @@ exports.doesAuthorExists = exports.doesAuthorExist;
* Returns the AuthorID for a token. * Returns the AuthorID for a token.
* @param {String} token The token * @param {String} token The token
*/ */
exports.getAuthor4Token = async function (token) { exports.getAuthor4Token = async (token) => {
const author = await mapAuthorWithDBKey('token2author', token); const author = await mapAuthorWithDBKey('token2author', token);
// return only the sub value authorID // return only the sub value authorID
@ -119,7 +118,7 @@ exports.getAuthor4Token = async function (token) {
* @param {String} token The mapper * @param {String} token The mapper
* @param {String} name The name of the author (optional) * @param {String} name The name of the author (optional)
*/ */
exports.createAuthorIfNotExistsFor = async function (authorMapper, name) { exports.createAuthorIfNotExistsFor = async (authorMapper, name) => {
const author = await mapAuthorWithDBKey('mapper2author', authorMapper); const author = await mapAuthorWithDBKey('mapper2author', authorMapper);
if (name) { if (name) {
@ -140,7 +139,7 @@ async function mapAuthorWithDBKey(mapperkey, mapper) {
// try to map to an author // try to map to an author
const author = await db.get(`${mapperkey}:${mapper}`); const author = await db.get(`${mapperkey}:${mapper}`);
if (author === null) { if (author == null) {
// there is no author with this mapper, so create one // there is no author with this mapper, so create one
const author = await exports.createAuthor(null); const author = await exports.createAuthor(null);
@ -163,7 +162,7 @@ async function mapAuthorWithDBKey(mapperkey, mapper) {
* Internal function that creates the database entry for an author * Internal function that creates the database entry for an author
* @param {String} name The name of the author * @param {String} name The name of the author
*/ */
exports.createAuthor = function (name) { exports.createAuthor = (name) => {
// create the new author name // create the new author name
const author = `a.${randomString(16)}`; const author = `a.${randomString(16)}`;
@ -185,50 +184,40 @@ exports.createAuthor = function (name) {
* Returns the Author Obj of the author * Returns the Author Obj of the author
* @param {String} author The id of the author * @param {String} author The id of the author
*/ */
exports.getAuthor = function (author) { exports.getAuthor = (author) => db.get(`globalAuthor:${author}`);
// NB: result is already a Promise
return db.get(`globalAuthor:${author}`);
};
/** /**
* Returns the color Id of the author * Returns the color Id of the author
* @param {String} author The id of the author * @param {String} author The id of the author
*/ */
exports.getAuthorColorId = function (author) { exports.getAuthorColorId = (author) => db.getSub(`globalAuthor:${author}`, ['colorId']);
return db.getSub(`globalAuthor:${author}`, ['colorId']);
};
/** /**
* Sets the color Id of the author * Sets the color Id of the author
* @param {String} author The id of the author * @param {String} author The id of the author
* @param {String} colorId The color id of the author * @param {String} colorId The color id of the author
*/ */
exports.setAuthorColorId = function (author, colorId) { exports.setAuthorColorId = (author, colorId) => db.setSub(
return db.setSub(`globalAuthor:${author}`, ['colorId'], colorId); `globalAuthor:${author}`, ['colorId'], colorId);
};
/** /**
* Returns the name of the author * Returns the name of the author
* @param {String} author The id of the author * @param {String} author The id of the author
*/ */
exports.getAuthorName = function (author) { exports.getAuthorName = (author) => db.getSub(`globalAuthor:${author}`, ['name']);
return db.getSub(`globalAuthor:${author}`, ['name']);
};
/** /**
* Sets the name of the author * Sets the name of the author
* @param {String} author The id of the author * @param {String} author The id of the author
* @param {String} name The name of the author * @param {String} name The name of the author
*/ */
exports.setAuthorName = function (author, name) { exports.setAuthorName = (author, name) => db.setSub(`globalAuthor:${author}`, ['name'], name);
return db.setSub(`globalAuthor:${author}`, ['name'], name);
};
/** /**
* Returns an array of all pads this author contributed to * Returns an array of all pads this author contributed to
* @param {String} author The id of the author * @param {String} author The id of the author
*/ */
exports.listPadsOfAuthor = async function (authorID) { exports.listPadsOfAuthor = async (authorID) => {
/* There are two other places where this array is manipulated: /* 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 * (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 * (2) When a pad is deleted, each author of that pad is also updated
@ -237,9 +226,9 @@ exports.listPadsOfAuthor = async function (authorID) {
// get the globalAuthor // get the globalAuthor
const author = await db.get(`globalAuthor:${authorID}`); const author = await db.get(`globalAuthor:${authorID}`);
if (author === null) { if (author == null) {
// author does not exist // author does not exist
throw new customError('authorID does not exist', 'apierror'); throw new CustomError('authorID does not exist', 'apierror');
} }
// everything is fine, return the pad IDs // everything is fine, return the pad IDs
@ -253,11 +242,11 @@ exports.listPadsOfAuthor = async function (authorID) {
* @param {String} author The id of the author * @param {String} author The id of the author
* @param {String} padID The id of the pad the author contributes to * @param {String} padID The id of the pad the author contributes to
*/ */
exports.addPad = async function (authorID, padID) { exports.addPad = async (authorID, padID) => {
// get the entry // get the entry
const author = await db.get(`globalAuthor:${authorID}`); const author = await db.get(`globalAuthor:${authorID}`);
if (author === null) return; if (author == null) return;
/* /*
* ACHTUNG: padIDs can also be undefined, not just null, so it is not possible * ACHTUNG: padIDs can also be undefined, not just null, so it is not possible
@ -280,12 +269,12 @@ exports.addPad = async function (authorID, padID) {
* @param {String} author The id of the author * @param {String} author The id of the author
* @param {String} padID The id of the pad the author contributes to * @param {String} padID The id of the pad the author contributes to
*/ */
exports.removePad = async function (authorID, padID) { exports.removePad = async (authorID, padID) => {
const author = await db.get(`globalAuthor:${authorID}`); const author = await db.get(`globalAuthor:${authorID}`);
if (author === null) return; if (author == null) return;
if (author.padIDs !== null) { if (author.padIDs != null) {
// remove pad from author // remove pad from author
delete author.padIDs[padID]; delete author.padIDs[padID];
await db.set(`globalAuthor:${authorID}`, author); await db.set(`globalAuthor:${authorID}`, author);

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* The Group Manager provides functions to manage groups in the database * The Group Manager provides functions to manage groups in the database
*/ */
@ -18,13 +19,13 @@
* limitations under the License. * limitations under the License.
*/ */
const customError = require('../utils/customError'); const CustomError = require('../utils/customError');
const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; const randomString = require('../../static/js/pad_utils').randomString;
const db = require('./DB'); const db = require('./DB');
const padManager = require('./PadManager'); const padManager = require('./PadManager');
const sessionManager = require('./SessionManager'); const sessionManager = require('./SessionManager');
exports.listAllGroups = async function () { exports.listAllGroups = async () => {
let groups = await db.get('groups'); let groups = await db.get('groups');
groups = groups || {}; groups = groups || {};
@ -32,17 +33,20 @@ exports.listAllGroups = async function () {
return {groupIDs}; return {groupIDs};
}; };
exports.deleteGroup = async function (groupID) { exports.deleteGroup = async (groupID) => {
const group = await db.get(`group:${groupID}`); const group = await db.get(`group:${groupID}`);
// ensure group exists // ensure group exists
if (group == null) { if (group == null) {
// group does not exist // group does not exist
throw new customError('groupID does not exist', 'apierror'); throw new CustomError('groupID does not exist', 'apierror');
} }
// iterate through all pads of this group and delete them (in parallel) // iterate through all pads of this group and delete them (in parallel)
await Promise.all(Object.keys(group.pads).map((padID) => padManager.getPad(padID).then((pad) => pad.remove()))); await Promise.all(Object.keys(group.pads)
.map((padID) => padManager.getPad(padID)
.then((pad) => pad.remove())
));
// iterate through group2sessions and delete all sessions // iterate through group2sessions and delete all sessions
const group2sessions = await db.get(`group2sessions:${groupID}`); const group2sessions = await db.get(`group2sessions:${groupID}`);
@ -76,14 +80,14 @@ exports.deleteGroup = async function (groupID) {
await db.set('groups', newGroups); await db.set('groups', newGroups);
}; };
exports.doesGroupExist = async function (groupID) { exports.doesGroupExist = async (groupID) => {
// try to get the group entry // try to get the group entry
const group = await db.get(`group:${groupID}`); const group = await db.get(`group:${groupID}`);
return (group != null); return (group != null);
}; };
exports.createGroup = async function () { exports.createGroup = async () => {
// search for non existing groupID // search for non existing groupID
const groupID = `g.${randomString(16)}`; const groupID = `g.${randomString(16)}`;
@ -103,10 +107,10 @@ exports.createGroup = async function () {
return {groupID}; return {groupID};
}; };
exports.createGroupIfNotExistsFor = async function (groupMapper) { exports.createGroupIfNotExistsFor = async (groupMapper) => {
// ensure mapper is optional // ensure mapper is optional
if (typeof groupMapper !== 'string') { if (typeof groupMapper !== 'string') {
throw new customError('groupMapper is not a string', 'apierror'); throw new CustomError('groupMapper is not a string', 'apierror');
} }
// try to get a group for this mapper // try to get a group for this mapper
@ -128,7 +132,7 @@ exports.createGroupIfNotExistsFor = async function (groupMapper) {
return result; return result;
}; };
exports.createGroupPad = async function (groupID, padName, text) { exports.createGroupPad = async (groupID, padName, text) => {
// create the padID // create the padID
const padID = `${groupID}$${padName}`; const padID = `${groupID}$${padName}`;
@ -136,7 +140,7 @@ exports.createGroupPad = async function (groupID, padName, text) {
const groupExists = await exports.doesGroupExist(groupID); const groupExists = await exports.doesGroupExist(groupID);
if (!groupExists) { if (!groupExists) {
throw new customError('groupID does not exist', 'apierror'); throw new CustomError('groupID does not exist', 'apierror');
} }
// ensure pad doesn't exist already // ensure pad doesn't exist already
@ -144,7 +148,7 @@ exports.createGroupPad = async function (groupID, padName, text) {
if (padExists) { if (padExists) {
// pad exists already // pad exists already
throw new customError('padName does already exist', 'apierror'); throw new CustomError('padName does already exist', 'apierror');
} }
// create the pad // create the pad
@ -156,12 +160,12 @@ exports.createGroupPad = async function (groupID, padName, text) {
return {padID}; return {padID};
}; };
exports.listPads = async function (groupID) { exports.listPads = async (groupID) => {
const exists = await exports.doesGroupExist(groupID); const exists = await exports.doesGroupExist(groupID);
// ensure the group exists // ensure the group exists
if (!exists) { if (!exists) {
throw new customError('groupID does not exist', 'apierror'); throw new CustomError('groupID does not exist', 'apierror');
} }
// group exists, let's get the pads // group exists, let's get the pads

View file

@ -1,21 +1,21 @@
'use strict';
/** /**
* The pad object, defined with joose * The pad object, defined with joose
*/ */
const Changeset = require('ep_etherpad-lite/static/js/Changeset'); const Changeset = require('../../static/js/Changeset');
const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); const AttributePool = require('../../static/js/AttributePool');
const db = require('./DB'); const db = require('./DB');
const settings = require('../utils/Settings'); const settings = require('../utils/Settings');
const authorManager = require('./AuthorManager'); const authorManager = require('./AuthorManager');
const padManager = require('./PadManager'); const padManager = require('./PadManager');
const padMessageHandler = require('../handler/PadMessageHandler'); const padMessageHandler = require('../handler/PadMessageHandler');
const groupManager = require('./GroupManager'); const groupManager = require('./GroupManager');
const customError = require('../utils/customError'); const CustomError = require('../utils/customError');
const readOnlyManager = require('./ReadOnlyManager'); const readOnlyManager = require('./ReadOnlyManager');
const crypto = require('crypto');
const randomString = require('../utils/randomstring'); const randomString = require('../utils/randomstring');
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); const hooks = require('../../static/js/pluginfw/hooks');
const promises = require('../utils/promises'); const promises = require('../utils/promises');
// serialization/deserialization attributes // serialization/deserialization attributes
@ -23,13 +23,14 @@ const attributeBlackList = ['id'];
const jsonableList = ['pool']; const jsonableList = ['pool'];
/** /**
* Copied from the Etherpad source code. It converts Windows line breaks to Unix line breaks and convert Tabs to spaces * Copied from the Etherpad source code. It converts Windows line breaks to Unix
* line breaks and convert Tabs to spaces
* @param txt * @param txt
*/ */
exports.cleanText = function (txt) { exports.cleanText = (txt) => txt.replace(/\r\n/g, '\n')
return txt.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\t/g, ' ').replace(/\xa0/g, ' '); .replace(/\r/g, '\n')
}; .replace(/\t/g, ' ')
.replace(/\xa0/g, ' ');
const Pad = function Pad(id) { const Pad = function Pad(id) {
this.atext = Changeset.makeAText('\n'); this.atext = Changeset.makeAText('\n');
@ -56,7 +57,7 @@ Pad.prototype.getSavedRevisionsNumber = function getSavedRevisionsNumber() {
}; };
Pad.prototype.getSavedRevisionsList = function getSavedRevisionsList() { Pad.prototype.getSavedRevisionsList = function getSavedRevisionsList() {
const savedRev = new Array(); const savedRev = [];
for (const rev in this.savedRevisions) { for (const rev in this.savedRevisions) {
savedRev.push(this.savedRevisions[rev].revNum); savedRev.push(this.savedRevisions[rev].revNum);
} }
@ -85,11 +86,11 @@ Pad.prototype.appendRevision = async function appendRevision(aChangeset, author)
newRevData.meta.timestamp = Date.now(); newRevData.meta.timestamp = Date.now();
// ex. getNumForAuthor // ex. getNumForAuthor
if (author != '') { if (author !== '') {
this.pool.putAttrib(['author', author || '']); this.pool.putAttrib(['author', author || '']);
} }
if (newRev % 100 == 0) { if (newRev % 100 === 0) {
newRevData.meta.pool = this.pool; newRevData.meta.pool = this.pool;
newRevData.meta.atext = this.atext; newRevData.meta.atext = this.atext;
} }
@ -104,7 +105,7 @@ Pad.prototype.appendRevision = async function appendRevision(aChangeset, author)
p.push(authorManager.addPad(author, this.id)); p.push(authorManager.addPad(author, this.id));
} }
if (this.head == 0) { if (this.head === 0) {
hooks.callAll('padCreate', {pad: this, author}); hooks.callAll('padCreate', {pad: this, author});
} else { } else {
hooks.callAll('padUpdate', {pad: this, author, revs: newRev, changeset: aChangeset}); hooks.callAll('padUpdate', {pad: this, author, revs: newRev, changeset: aChangeset});
@ -153,7 +154,7 @@ Pad.prototype.getAllAuthors = function getAllAuthors() {
const authors = []; const authors = [];
for (const key in this.pool.numToAttrib) { for (const key in this.pool.numToAttrib) {
if (this.pool.numToAttrib[key][0] == 'author' && this.pool.numToAttrib[key][1] != '') { if (this.pool.numToAttrib[key][0] === 'author' && this.pool.numToAttrib[key][1] !== '') {
authors.push(this.pool.numToAttrib[key][1]); authors.push(this.pool.numToAttrib[key][1]);
} }
} }
@ -177,7 +178,8 @@ Pad.prototype.getInternalRevisionAText = async function getInternalRevisionAText
// get all needed changesets // get all needed changesets
const changesets = []; const changesets = [];
await Promise.all(neededChangesets.map((item) => this.getRevisionChangeset(item).then((changeset) => { await Promise.all(
neededChangesets.map((item) => this.getRevisionChangeset(item).then((changeset) => {
changesets[item] = changeset; changesets[item] = changeset;
}))); })));
@ -204,7 +206,8 @@ Pad.prototype.getAllAuthorColors = async function getAllAuthorColors() {
const returnTable = {}; const returnTable = {};
const colorPalette = authorManager.getColorPalette(); const colorPalette = authorManager.getColorPalette();
await Promise.all(authors.map((author) => authorManager.getAuthorColorId(author).then((colorId) => { await Promise.all(
authors.map((author) => authorManager.getAuthorColorId(author).then((colorId) => {
// colorId might be a hex color or an number out of the palette // colorId might be a hex color or an number out of the palette
returnTable[author] = colorPalette[colorId] || colorId; returnTable[author] = colorPalette[colorId] || colorId;
}))); })));
@ -227,7 +230,7 @@ Pad.prototype.getValidRevisionRange = function getValidRevisionRange(startRev, e
endRev = head; endRev = head;
} }
if (startRev !== null && endRev !== null) { if (startRev != null && endRev != null) {
return {startRev, endRev}; return {startRev, endRev};
} }
return null; return null;
@ -251,7 +254,7 @@ Pad.prototype.setText = async function setText(newText) {
// We want to ensure the pad still ends with a \n, but otherwise keep // We want to ensure the pad still ends with a \n, but otherwise keep
// getText() and setText() consistent. // getText() and setText() consistent.
let changeset; let changeset;
if (newText[newText.length - 1] == '\n') { if (newText[newText.length - 1] === '\n') {
changeset = Changeset.makeSplice(oldText, 0, oldText.length, newText); changeset = Changeset.makeSplice(oldText, 0, oldText.length, newText);
} else { } else {
changeset = Changeset.makeSplice(oldText, 0, oldText.length - 1, newText); changeset = Changeset.makeSplice(oldText, 0, oldText.length - 1, newText);
@ -304,7 +307,8 @@ Pad.prototype.getChatMessages = async function getChatMessages(start, end) {
// get all entries out of the database // get all entries out of the database
const entries = []; const entries = [];
await Promise.all(neededEntries.map((entryObject) => this.getChatMessage(entryObject.entryNum).then((entry) => { await Promise.all(
neededEntries.map((entryObject) => this.getChatMessage(entryObject.entryNum).then((entry) => {
entries[entryObject.order] = entry; entries[entryObject.order] = entry;
}))); })));
@ -384,14 +388,16 @@ Pad.prototype.copy = async function copy(destinationID, force) {
// copy all chat messages // copy all chat messages
const chatHead = this.chatHead; const chatHead = this.chatHead;
for (let i = 0; i <= chatHead; ++i) { for (let i = 0; i <= chatHead; ++i) {
const p = db.get(`pad:${sourceID}:chat:${i}`).then((chat) => db.set(`pad:${destinationID}:chat:${i}`, chat)); const p = db.get(`pad:${sourceID}:chat:${i}`)
.then((chat) => db.set(`pad:${destinationID}:chat:${i}`, chat));
promises.push(p); promises.push(p);
} }
// copy all revisions // copy all revisions
const revHead = this.head; const revHead = this.head;
for (let i = 0; i <= revHead; ++i) { for (let i = 0; i <= revHead; ++i) {
const p = db.get(`pad:${sourceID}:revs:${i}`).then((rev) => db.set(`pad:${destinationID}:revs:${i}`, rev)); const p = db.get(`pad:${sourceID}:revs:${i}`)
.then((rev) => db.set(`pad:${destinationID}:revs:${i}`, rev));
promises.push(p); promises.push(p);
} }
@ -412,7 +418,7 @@ Pad.prototype.copy = async function copy(destinationID, force) {
await padManager.getPad(destinationID, null); // this runs too early. await padManager.getPad(destinationID, null); // this runs too early.
// let the plugins know the pad was copied // let the plugins know the pad was copied
hooks.callAll('padCopy', {originalPad: this, destinationID}); await hooks.aCallAll('padCopy', {originalPad: this, destinationID});
return {padID: destinationID}; return {padID: destinationID};
}; };
@ -426,7 +432,7 @@ Pad.prototype.checkIfGroupExistAndReturnIt = async function checkIfGroupExistAnd
// group does not exist // group does not exist
if (!groupExists) { if (!groupExists) {
throw new customError('groupID does not exist for destinationID', 'apierror'); throw new CustomError('groupID does not exist for destinationID', 'apierror');
} }
} }
return destGroupID; return destGroupID;
@ -446,7 +452,7 @@ Pad.prototype.removePadIfForceIsTrueAndAlreadyExist = async function removePadIf
if (exists) { if (exists) {
if (!force) { if (!force) {
console.error('erroring out without force'); console.error('erroring out without force');
throw new customError('destinationID already exists', 'apierror'); throw new CustomError('destinationID already exists', 'apierror');
} }
// exists and forcing // exists and forcing
@ -514,7 +520,7 @@ Pad.prototype.copyPadWithoutHistory = async function copyPadWithoutHistory(desti
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText); const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
newPad.appendRevision(changeset); newPad.appendRevision(changeset);
hooks.callAll('padCopy', {originalPad: this, destinationID}); await hooks.aCallAll('padCopy', {originalPad: this, destinationID});
return {padID: destinationID}; return {padID: destinationID};
}; };
@ -568,7 +574,7 @@ Pad.prototype.remove = async function remove() {
// delete the pad entry and delete pad from padManager // delete the pad entry and delete pad from padManager
p.push(padManager.removePad(padID)); p.push(padManager.removePad(padID));
hooks.callAll('padRemove', {padID}); p.push(hooks.aCallAll('padRemove', {padID}));
await Promise.all(p); await Promise.all(p);
}; };

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* The Pad Manager is a Factory for pad Objects * The Pad Manager is a Factory for pad Objects
*/ */
@ -18,7 +19,7 @@
* limitations under the License. * limitations under the License.
*/ */
const customError = require('../utils/customError'); const CustomError = require('../utils/customError');
const Pad = require('../db/Pad').Pad; const Pad = require('../db/Pad').Pad;
const db = require('./DB'); const db = require('./DB');
@ -109,22 +110,22 @@ const padList = {
* @param id A String with the id of the pad * @param id A String with the id of the pad
* @param {Function} callback * @param {Function} callback
*/ */
exports.getPad = async function (id, text) { exports.getPad = async (id, text) => {
// check if this is a valid padId // check if this is a valid padId
if (!exports.isValidPadId(id)) { if (!exports.isValidPadId(id)) {
throw new customError(`${id} is not a valid padId`, 'apierror'); throw new CustomError(`${id} is not a valid padId`, 'apierror');
} }
// check if this is a valid text // check if this is a valid text
if (text != null) { if (text != null) {
// check if text is a string // check if text is a string
if (typeof text !== 'string') { if (typeof text !== 'string') {
throw new customError('text is not a string', 'apierror'); throw new CustomError('text is not a string', 'apierror');
} }
// check if text is less than 100k chars // check if text is less than 100k chars
if (text.length > 100000) { if (text.length > 100000) {
throw new customError('text must be less than 100k chars', 'apierror'); throw new CustomError('text must be less than 100k chars', 'apierror');
} }
} }
@ -146,14 +147,14 @@ exports.getPad = async function (id, text) {
return pad; return pad;
}; };
exports.listAllPads = async function () { exports.listAllPads = async () => {
const padIDs = await padList.getPads(); const padIDs = await padList.getPads();
return {padIDs}; return {padIDs};
}; };
// checks if a pad exists // checks if a pad exists
exports.doesPadExist = async function (padId) { exports.doesPadExist = async (padId) => {
const value = await db.get(`pad:${padId}`); const value = await db.get(`pad:${padId}`);
return (value != null && value.atext); return (value != null && value.atext);
@ -189,9 +190,7 @@ exports.sanitizePadId = async function sanitizePadId(padId) {
return padId; return padId;
}; };
exports.isValidPadId = function (padId) { exports.isValidPadId = (padId) => /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
return /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
};
/** /**
* Removes the pad from database and unloads it. * Removes the pad from database and unloads it.
@ -204,6 +203,6 @@ exports.removePad = async (padId) => {
}; };
// removes a pad from the cache // removes a pad from the cache
exports.unloadPad = function (padId) { exports.unloadPad = (padId) => {
globalPads.remove(padId); globalPads.remove(padId);
}; };

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* The ReadOnlyManager manages the database and rendering releated to read only pads * The ReadOnlyManager manages the database and rendering releated to read only pads
*/ */
@ -27,15 +28,13 @@ const randomString = require('../utils/randomstring');
* checks if the id pattern matches a read-only pad id * checks if the id pattern matches a read-only pad id
* @param {String} the pad's id * @param {String} the pad's id
*/ */
exports.isReadOnlyId = function (id) { exports.isReadOnlyId = (id) => id.indexOf('r.') === 0;
return id.indexOf('r.') === 0;
};
/** /**
* returns a read only id for a pad * returns a read only id for a pad
* @param {String} padId the id of the pad * @param {String} padId the id of the pad
*/ */
exports.getReadOnlyId = async function (padId) { exports.getReadOnlyId = async (padId) => {
// check if there is a pad2readonly entry // check if there is a pad2readonly entry
let readOnlyId = await db.get(`pad2readonly:${padId}`); let readOnlyId = await db.get(`pad2readonly:${padId}`);
@ -53,15 +52,13 @@ exports.getReadOnlyId = async function (padId) {
* returns the padId for a read only id * returns the padId for a read only id
* @param {String} readOnlyId read only id * @param {String} readOnlyId read only id
*/ */
exports.getPadId = function (readOnlyId) { exports.getPadId = (readOnlyId) => db.get(`readonly2pad:${readOnlyId}`);
return db.get(`readonly2pad:${readOnlyId}`);
};
/** /**
* returns the padId and readonlyPadId in an object for any id * returns the padId and readonlyPadId in an object for any id
* @param {String} padIdOrReadonlyPadId read only id or real pad id * @param {String} padIdOrReadonlyPadId read only id or real pad id
*/ */
exports.getIds = async function (id) { exports.getIds = async (id) => {
const readonly = (id.indexOf('r.') === 0); const readonly = (id.indexOf('r.') === 0);
// Might be null, if this is an unknown read-only id // Might be null, if this is an unknown read-only id

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* Controls the security of pad access * Controls the security of pad access
*/ */
@ -19,7 +20,7 @@
*/ */
const authorManager = require('./AuthorManager'); const authorManager = require('./AuthorManager');
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks.js'); const hooks = require('../../static/js/pluginfw/hooks.js');
const padManager = require('./PadManager'); const padManager = require('./PadManager');
const sessionManager = require('./SessionManager'); const sessionManager = require('./SessionManager');
const settings = require('../utils/Settings'); const settings = require('../utils/Settings');
@ -47,7 +48,7 @@ const DENY = Object.freeze({accessStatus: 'deny'});
* WARNING: Tokens and session IDs MUST be kept secret, otherwise users will be able to impersonate * 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). * each other (which might allow them to gain privileges).
*/ */
exports.checkAccess = async function (padID, sessionCookie, token, userSettings) { exports.checkAccess = async (padID, sessionCookie, token, userSettings) => {
if (!padID) { if (!padID) {
authLogger.debug('access denied: missing padID'); authLogger.debug('access denied: missing padID');
return DENY; return DENY;

View file

@ -1,5 +1,7 @@
'use strict';
/** /**
* The Session Manager provides functions to manage session in the database, it only provides session management for sessions created by the API * The Session Manager provides functions to manage session in the database,
* it only provides session management for sessions created by the API
*/ */
/* /*
@ -18,7 +20,7 @@
* limitations under the License. * limitations under the License.
*/ */
const customError = require('../utils/customError'); const CustomError = require('../utils/customError');
const promises = require('../utils/promises'); const promises = require('../utils/promises');
const randomString = require('../utils/randomstring'); const randomString = require('../utils/randomstring');
const db = require('./DB'); const db = require('./DB');
@ -40,7 +42,8 @@ exports.findAuthorID = async (groupID, sessionCookie) => {
* Sometimes, RFC 6265-compliant web servers may send back a cookie whose * Sometimes, RFC 6265-compliant web servers may send back a cookie whose
* value is enclosed in double quotes, such as: * value is enclosed in double quotes, such as:
* *
* Set-Cookie: sessionCookie="s.37cf5299fbf981e14121fba3a588c02b,s.2b21517bf50729d8130ab85736a11346"; Version=1; Path=/; Domain=localhost; Discard * Set-Cookie: sessionCookie="s.37cf5299fbf981e14121fba3a588c02b,
* s.2b21517bf50729d8130ab85736a11346"; Version=1; Path=/; Domain=localhost; Discard
* *
* Where the double quotes at the start and the end of the header value are * Where the double quotes at the start and the end of the header value are
* just delimiters. This is perfectly legal: Etherpad parsing logic should * just delimiters. This is perfectly legal: Etherpad parsing logic should
@ -78,26 +81,26 @@ exports.findAuthorID = async (groupID, sessionCookie) => {
return sessionInfo.authorID; return sessionInfo.authorID;
}; };
exports.doesSessionExist = async function (sessionID) { exports.doesSessionExist = async (sessionID) => {
// check if the database entry of this session exists // check if the database entry of this session exists
const session = await db.get(`session:${sessionID}`); const session = await db.get(`session:${sessionID}`);
return (session !== null); return (session != null);
}; };
/** /**
* Creates a new session between an author and a group * Creates a new session between an author and a group
*/ */
exports.createSession = async function (groupID, authorID, validUntil) { exports.createSession = async (groupID, authorID, validUntil) => {
// check if the group exists // check if the group exists
const groupExists = await groupManager.doesGroupExist(groupID); const groupExists = await groupManager.doesGroupExist(groupID);
if (!groupExists) { if (!groupExists) {
throw new customError('groupID does not exist', 'apierror'); throw new CustomError('groupID does not exist', 'apierror');
} }
// check if the author exists // check if the author exists
const authorExists = await authorManager.doesAuthorExist(authorID); const authorExists = await authorManager.doesAuthorExist(authorID);
if (!authorExists) { if (!authorExists) {
throw new customError('authorID does not exist', 'apierror'); throw new CustomError('authorID does not exist', 'apierror');
} }
// try to parse validUntil if it's not a number // try to parse validUntil if it's not a number
@ -107,22 +110,22 @@ exports.createSession = async function (groupID, authorID, validUntil) {
// check it's a valid number // check it's a valid number
if (isNaN(validUntil)) { if (isNaN(validUntil)) {
throw new customError('validUntil is not a number', 'apierror'); throw new CustomError('validUntil is not a number', 'apierror');
} }
// ensure this is not a negative number // ensure this is not a negative number
if (validUntil < 0) { if (validUntil < 0) {
throw new customError('validUntil is a negative number', 'apierror'); throw new CustomError('validUntil is a negative number', 'apierror');
} }
// ensure this is not a float value // ensure this is not a float value
if (!is_int(validUntil)) { if (!isInt(validUntil)) {
throw new customError('validUntil is a float value', 'apierror'); throw new CustomError('validUntil is a float value', 'apierror');
} }
// check if validUntil is in the future // check if validUntil is in the future
if (validUntil < Math.floor(Date.now() / 1000)) { if (validUntil < Math.floor(Date.now() / 1000)) {
throw new customError('validUntil is in the past', 'apierror'); throw new CustomError('validUntil is in the past', 'apierror');
} }
// generate sessionID // generate sessionID
@ -170,13 +173,13 @@ exports.createSession = async function (groupID, authorID, validUntil) {
return {sessionID}; return {sessionID};
}; };
exports.getSessionInfo = async function (sessionID) { exports.getSessionInfo = async (sessionID) => {
// check if the database entry of this session exists // check if the database entry of this session exists
const session = await db.get(`session:${sessionID}`); const session = await db.get(`session:${sessionID}`);
if (session == null) { if (session == null) {
// session does not exist // session does not exist
throw new customError('sessionID does not exist', 'apierror'); throw new CustomError('sessionID does not exist', 'apierror');
} }
// everything is fine, return the sessioninfos // everything is fine, return the sessioninfos
@ -186,11 +189,11 @@ exports.getSessionInfo = async function (sessionID) {
/** /**
* Deletes a session * Deletes a session
*/ */
exports.deleteSession = async function (sessionID) { exports.deleteSession = async (sessionID) => {
// ensure that the session exists // ensure that the session exists
const session = await db.get(`session:${sessionID}`); const session = await db.get(`session:${sessionID}`);
if (session == null) { if (session == null) {
throw new customError('sessionID does not exist', 'apierror'); throw new CustomError('sessionID does not exist', 'apierror');
} }
// everything is fine, use the sessioninfos // everything is fine, use the sessioninfos
@ -217,22 +220,22 @@ exports.deleteSession = async function (sessionID) {
} }
}; };
exports.listSessionsOfGroup = async function (groupID) { exports.listSessionsOfGroup = async (groupID) => {
// check that the group exists // check that the group exists
const exists = await groupManager.doesGroupExist(groupID); const exists = await groupManager.doesGroupExist(groupID);
if (!exists) { if (!exists) {
throw new customError('groupID does not exist', 'apierror'); throw new CustomError('groupID does not exist', 'apierror');
} }
const sessions = await listSessionsWithDBKey(`group2sessions:${groupID}`); const sessions = await listSessionsWithDBKey(`group2sessions:${groupID}`);
return sessions; return sessions;
}; };
exports.listSessionsOfAuthor = async function (authorID) { exports.listSessionsOfAuthor = async (authorID) => {
// check that the author exists // check that the author exists
const exists = await authorManager.doesAuthorExist(authorID); const exists = await authorManager.doesAuthorExist(authorID);
if (!exists) { if (!exists) {
throw new customError('authorID does not exist', 'apierror'); throw new CustomError('authorID does not exist', 'apierror');
} }
const sessions = await listSessionsWithDBKey(`author2sessions:${authorID}`); const sessions = await listSessionsWithDBKey(`author2sessions:${authorID}`);
@ -241,7 +244,7 @@ exports.listSessionsOfAuthor = async function (authorID) {
// this function is basically the code listSessionsOfAuthor and listSessionsOfGroup has in common // this function is basically the code listSessionsOfAuthor and listSessionsOfGroup has in common
// required to return null rather than an empty object if there are none // required to return null rather than an empty object if there are none
async function listSessionsWithDBKey(dbkey) { const listSessionsWithDBKey = async (dbkey) => {
// get the group2sessions entry // get the group2sessions entry
const sessionObject = await db.get(dbkey); const sessionObject = await db.get(dbkey);
const sessions = sessionObject ? sessionObject.sessionIDs : null; const sessions = sessionObject ? sessionObject.sessionIDs : null;
@ -252,7 +255,7 @@ async function listSessionsWithDBKey(dbkey) {
const sessionInfo = await exports.getSessionInfo(sessionID); const sessionInfo = await exports.getSessionInfo(sessionID);
sessions[sessionID] = sessionInfo; sessions[sessionID] = sessionInfo;
} catch (err) { } catch (err) {
if (err == 'apierror: sessionID does not exist') { if (err === 'apierror: sessionID does not exist') {
console.warn(`Found bad session ${sessionID} in ${dbkey}`); console.warn(`Found bad session ${sessionID} in ${dbkey}`);
sessions[sessionID] = null; sessions[sessionID] = null;
} else { } else {
@ -262,9 +265,7 @@ async function listSessionsWithDBKey(dbkey) {
} }
return sessions; return sessions;
} };
// checks if a number is an int // checks if a number is an int
function is_int(value) { const isInt = (value) => (parseFloat(value) === parseInt(value)) && !isNaN(value);
return (parseFloat(value) == parseInt(value)) && !isNaN(value);
}

View file

@ -1,3 +1,4 @@
'use strict';
/* /*
* Stores session data in the database * Stores session data in the database
* Source; https://github.com/edy-b/SciFlowWriter/blob/develop/available_plugins/ep_sciflowwriter/db/DirtyStore.js * Source; https://github.com/edy-b/SciFlowWriter/blob/develop/available_plugins/ep_sciflowwriter/db/DirtyStore.js
@ -7,9 +8,9 @@
* express-session, which can't actually use promises anyway. * express-session, which can't actually use promises anyway.
*/ */
const DB = require('ep_etherpad-lite/node/db/DB'); const DB = require('./DB');
const Store = require('ep_etherpad-lite/node_modules/express-session').Store; const Store = require('express-session').Store;
const log4js = require('ep_etherpad-lite/node_modules/log4js'); const log4js = require('log4js');
const logger = log4js.getLogger('SessionStore'); const logger = log4js.getLogger('SessionStore');

View file

@ -1,6 +1,8 @@
'use strict';
/** /**
* I found this tests in the old Etherpad and used it to test if the Changeset library can be run on node.js. * I found this tests in the old Etherpad and used it to test if the Changeset library can be run on
* It has no use for ep-lite, but I thought I keep it cause it may help someone to understand the Changeset library * node.js. It has no use for ep-lite, but I thought I keep it cause it may help someone to
* understand the Changeset library
* https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2_tests.js * https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2_tests.js
*/ */
@ -21,52 +23,47 @@
*/ */
const Changeset = require('ep_etherpad-lite/static/js/Changeset'); const Changeset = require('../static/js/Changeset');
const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); const AttributePool = require('../static/js/AttributePool');
function random() { function random() {
this.nextInt = function (maxValue) { this.nextInt = (maxValue) => Math.floor(Math.random() * maxValue);
return Math.floor(Math.random() * maxValue); this.nextDouble = (maxValue) => Math.random();
};
this.nextDouble = function (maxValue) {
return Math.random();
};
} }
function runTests() { const runTests = () => {
function print(str) { const print = (str) => {
console.log(str); console.log(str);
} };
function assert(code, optMsg) { const assert = (code, optMsg) => {
if (!eval(code)) throw new Error(`FALSE: ${optMsg || code}`); if (!eval(code)) throw new Error(`FALSE: ${optMsg || code}`); /* eslint-disable-line no-eval */
} };
function literal(v) { const literal = (v) => {
if ((typeof v) === 'string') { if ((typeof v) === 'string') {
return `"${v.replace(/[\\\"]/g, '\\$1').replace(/\n/g, '\\n')}"`; return `"${v.replace(/[\\\"]/g, '\\$1').replace(/\n/g, '\\n')}"`;
} else { return JSON.stringify(v); } } else { return JSON.stringify(v); }
} };
function assertEqualArrays(a, b) { const assertEqualArrays = (a, b) => {
assert(`JSON.stringify(${literal(a)}) == JSON.stringify(${literal(b)})`); assert(`JSON.stringify(${literal(a)}) == JSON.stringify(${literal(b)})`);
} };
function assertEqualStrings(a, b) { const assertEqualStrings = (a, b) => {
assert(`${literal(a)} == ${literal(b)}`); assert(`${literal(a)} == ${literal(b)}`);
} };
function throughIterator(opsStr) { const throughIterator = (opsStr) => {
const iter = Changeset.opIterator(opsStr); const iter = Changeset.opIterator(opsStr);
const assem = Changeset.opAssembler(); const assem = Changeset.opAssembler();
while (iter.hasNext()) { while (iter.hasNext()) {
assem.append(iter.next()); assem.append(iter.next());
} }
return assem.toString(); return assem.toString();
} };
function throughSmartAssembler(opsStr) { const throughSmartAssembler = (opsStr) => {
const iter = Changeset.opIterator(opsStr); const iter = Changeset.opIterator(opsStr);
const assem = Changeset.smartOpAssembler(); const assem = Changeset.smartOpAssembler();
while (iter.hasNext()) { while (iter.hasNext()) {
@ -74,50 +71,50 @@ function runTests() {
} }
assem.endDocument(); assem.endDocument();
return assem.toString(); return assem.toString();
} };
(function () { (() => {
print('> throughIterator'); print('> throughIterator');
const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1';
assert(`throughIterator(${literal(x)}) == ${literal(x)}`); assert(`throughIterator(${literal(x)}) == ${literal(x)}`);
})(); })();
(function () { (() => {
print('> throughSmartAssembler'); print('> throughSmartAssembler');
const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1';
assert(`throughSmartAssembler(${literal(x)}) == ${literal(x)}`); assert(`throughSmartAssembler(${literal(x)}) == ${literal(x)}`);
})(); })();
function applyMutations(mu, arrayOfArrays) { const applyMutations = (mu, arrayOfArrays) => {
arrayOfArrays.forEach((a) => { arrayOfArrays.forEach((a) => {
const result = mu[a[0]].apply(mu, a.slice(1)); const result = mu[a[0]].apply(mu, a.slice(1));
if (a[0] == 'remove' && a[3]) { if (a[0] === 'remove' && a[3]) {
assertEqualStrings(a[3], result); assertEqualStrings(a[3], result);
} }
}); });
} };
function mutationsToChangeset(oldLen, arrayOfArrays) { const mutationsToChangeset = (oldLen, arrayOfArrays) => {
const assem = Changeset.smartOpAssembler(); const assem = Changeset.smartOpAssembler();
const op = Changeset.newOp(); const op = Changeset.newOp();
const bank = Changeset.stringAssembler(); const bank = Changeset.stringAssembler();
let oldPos = 0; let oldPos = 0;
let newLen = 0; let newLen = 0;
arrayOfArrays.forEach((a) => { arrayOfArrays.forEach((a) => {
if (a[0] == 'skip') { if (a[0] === 'skip') {
op.opcode = '='; op.opcode = '=';
op.chars = a[1]; op.chars = a[1];
op.lines = (a[2] || 0); op.lines = (a[2] || 0);
assem.append(op); assem.append(op);
oldPos += op.chars; oldPos += op.chars;
newLen += op.chars; newLen += op.chars;
} else if (a[0] == 'remove') { } else if (a[0] === 'remove') {
op.opcode = '-'; op.opcode = '-';
op.chars = a[1]; op.chars = a[1];
op.lines = (a[2] || 0); op.lines = (a[2] || 0);
assem.append(op); assem.append(op);
oldPos += op.chars; oldPos += op.chars;
} else if (a[0] == 'insert') { } else if (a[0] === 'insert') {
op.opcode = '+'; op.opcode = '+';
bank.append(a[1]); bank.append(a[1]);
op.chars = a[1].length; op.chars = a[1].length;
@ -129,9 +126,9 @@ function runTests() {
newLen += oldLen - oldPos; newLen += oldLen - oldPos;
assem.endDocument(); assem.endDocument();
return Changeset.pack(oldLen, newLen, assem.toString(), bank.toString()); return Changeset.pack(oldLen, newLen, assem.toString(), bank.toString());
} };
function runMutationTest(testId, origLines, muts, correct) { const runMutationTest = (testId, origLines, muts, correct) => {
print(`> runMutationTest#${testId}`); print(`> runMutationTest#${testId}`);
let lines = origLines.slice(); let lines = origLines.slice();
const mu = Changeset.textLinesMutator(lines); const mu = Changeset.textLinesMutator(lines);
@ -149,7 +146,7 @@ function runTests() {
// print(literal(cs)); // print(literal(cs));
const outText = Changeset.applyToText(cs, inText); const outText = Changeset.applyToText(cs, inText);
assertEqualStrings(correctText, outText); assertEqualStrings(correctText, outText);
} };
runMutationTest(1, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [ runMutationTest(1, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [
['remove', 1, 0, 'a'], ['remove', 1, 0, 'a'],
@ -220,7 +217,7 @@ function runTests() {
['skip', 1, 1, true], ['skip', 1, 1, true],
], ['banana\n', 'cabbage\n', 'duffle\n']); ], ['banana\n', 'cabbage\n', 'duffle\n']);
function poolOrArray(attribs) { const poolOrArray = (attribs) => {
if (attribs.getAttrib) { if (attribs.getAttrib) {
return attribs; // it's already an attrib pool return attribs; // it's already an attrib pool
} else { } else {
@ -231,23 +228,25 @@ function runTests() {
}); });
return p; return p;
} }
} };
function runApplyToAttributionTest(testId, attribs, cs, inAttr, outCorrect) { const runApplyToAttributionTest = (testId, attribs, cs, inAttr, outCorrect) => {
print(`> applyToAttribution#${testId}`); print(`> applyToAttribution#${testId}`);
const p = poolOrArray(attribs); const p = poolOrArray(attribs);
const result = Changeset.applyToAttribution( const result = Changeset.applyToAttribution(
Changeset.checkRep(cs), inAttr, p); Changeset.checkRep(cs), inAttr, p);
assertEqualStrings(outCorrect, result); assertEqualStrings(outCorrect, result);
} };
// turn c<b>a</b>ctus\n into a<b>c</b>tusabcd\n // turn c<b>a</b>ctus\n into a<b>c</b>tusabcd\n
runApplyToAttributionTest(1, ['bold,', 'bold,true'], 'Z:7>3-1*0=1*1=1=3+4$abcd', '+1*1+1|1+5', '+1*1+1|1+8'); runApplyToAttributionTest(1,
['bold,', 'bold,true'], 'Z:7>3-1*0=1*1=1=3+4$abcd', '+1*1+1|1+5', '+1*1+1|1+8');
// turn "david\ngreenspan\n" into "<b>david\ngreen</b>\n" // turn "david\ngreenspan\n" into "<b>david\ngreen</b>\n"
runApplyToAttributionTest(2, ['bold,', 'bold,true'], 'Z:g<4*1|1=6*1=5-4$', '|2+g', '*1|1+6*1+5|1+1'); runApplyToAttributionTest(2,
['bold,', 'bold,true'], 'Z:g<4*1|1=6*1=5-4$', '|2+g', '*1|1+6*1+5|1+1');
(function () { (() => {
print('> mutatorHasMore'); print('> mutatorHasMore');
const lines = ['1\n', '2\n', '3\n', '4\n']; const lines = ['1\n', '2\n', '3\n', '4\n'];
let mu; let mu;
@ -288,7 +287,7 @@ function runTests() {
assert(`${mu.hasMore()} == false`); assert(`${mu.hasMore()} == false`);
})(); })();
function runMutateAttributionTest(testId, attribs, cs, alines, outCorrect) { const runMutateAttributionTest = (testId, attribs, cs, alines, outCorrect) => {
print(`> runMutateAttributionTest#${testId}`); print(`> runMutateAttributionTest#${testId}`);
const p = poolOrArray(attribs); const p = poolOrArray(attribs);
const alines2 = Array.prototype.slice.call(alines); const alines2 = Array.prototype.slice.call(alines);
@ -298,30 +297,35 @@ function runTests() {
print(`> runMutateAttributionTest#${testId}.applyToAttribution`); print(`> runMutateAttributionTest#${testId}.applyToAttribution`);
function removeQuestionMarks(a) { const removeQuestionMarks = (a) => a.replace(/\?/g, '');
return a.replace(/\?/g, '');
}
const inMerged = Changeset.joinAttributionLines(alines.map(removeQuestionMarks)); const inMerged = Changeset.joinAttributionLines(alines.map(removeQuestionMarks));
const correctMerged = Changeset.joinAttributionLines(outCorrect.map(removeQuestionMarks)); const correctMerged = Changeset.joinAttributionLines(outCorrect.map(removeQuestionMarks));
const mergedResult = Changeset.applyToAttribution(cs, inMerged, p); const mergedResult = Changeset.applyToAttribution(cs, inMerged, p);
assertEqualStrings(correctMerged, mergedResult); assertEqualStrings(correctMerged, mergedResult);
} };
// turn 123\n 456\n 789\n into 123\n 4<b>5</b>6\n 789\n // turn 123\n 456\n 789\n into 123\n 4<b>5</b>6\n 789\n
runMutateAttributionTest(1, ['bold,true'], 'Z:c>0|1=4=1*0=1$', ['|1+4', '|1+4', '|1+4'], ['|1+4', '+1*0+1|1+2', '|1+4']); runMutateAttributionTest(1,
['bold,true'], 'Z:c>0|1=4=1*0=1$', ['|1+4', '|1+4', '|1+4'], ['|1+4', '+1*0+1|1+2', '|1+4']);
// make a document bold // make a document bold
runMutateAttributionTest(2, ['bold,true'], 'Z:c>0*0|3=c$', ['|1+4', '|1+4', '|1+4'], ['*0|1+4', '*0|1+4', '*0|1+4']); runMutateAttributionTest(2,
['bold,true'], 'Z:c>0*0|3=c$', ['|1+4', '|1+4', '|1+4'], ['*0|1+4', '*0|1+4', '*0|1+4']);
// clear bold on document // clear bold on document
runMutateAttributionTest(3, ['bold,', 'bold,true'], 'Z:c>0*0|3=c$', ['*1+1+1*1+1|1+1', '+1*1+1|1+2', '*1+1+1*1+1|1+1'], ['|1+4', '|1+4', '|1+4']); runMutateAttributionTest(3,
['bold,', 'bold,true'], 'Z:c>0*0|3=c$',
['*1+1+1*1+1|1+1', '+1*1+1|1+2', '*1+1+1*1+1|1+1'], ['|1+4', '|1+4', '|1+4']);
// add a character on line 3 of a document with 5 blank lines, and make sure // add a character on line 3 of a document with 5 blank lines, and make sure
// the optimization that skips purely-kept lines is working; if any attribution string // the optimization that skips purely-kept lines is working; if any attribution string
// with a '?' is parsed it will cause an error. // with a '?' is parsed it will cause an error.
runMutateAttributionTest(4, ['foo,bar', 'line,1', 'line,2', 'line,3', 'line,4', 'line,5'], 'Z:5>1|2=2+1$x', ['?*1|1+1', '?*2|1+1', '*3|1+1', '?*4|1+1', '?*5|1+1'], ['?*1|1+1', '?*2|1+1', '+1*3|1+1', '?*4|1+1', '?*5|1+1']); runMutateAttributionTest(4,
['foo,bar', 'line,1', 'line,2', 'line,3', 'line,4', 'line,5'],
'Z:5>1|2=2+1$x', ['?*1|1+1', '?*2|1+1', '*3|1+1', '?*4|1+1', '?*5|1+1'],
['?*1|1+1', '?*2|1+1', '+1*3|1+1', '?*4|1+1', '?*5|1+1']);
const testPoolWithChars = (function () { const testPoolWithChars = (() => {
const p = new AttributePool(); const p = new AttributePool();
p.putAttrib(['char', 'newline']); p.putAttrib(['char', 'newline']);
for (let i = 1; i < 36; i++) { for (let i = 1; i < 36; i++) {
@ -332,39 +336,66 @@ function runTests() {
})(); })();
// based on runMutationTest#1 // based on runMutationTest#1
runMutateAttributionTest(5, testPoolWithChars, 'Z:11>7-2*t+1*u+1|2=b|2+a=2*b+1*o+1*t+1*0|1+1*b+1*u+1=3|1-3-6$' + 'tucream\npie\nbot\nbu', ['*a+1*p+2*l+1*e+1*0|1+1', '*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1', '*c+1*a+1*b+2*a+1*g+1*e+1*0|1+1', '*d+1*u+1*f+2*l+1*e+1*0|1+1', '*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1'], ['*t+1*u+1*p+1*l+1*e+1*0|1+1', '*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1', '|1+6', '|1+4', '*c+1*a+1*b+1*o+1*t+1*0|1+1', '*b+1*u+1*b+2*a+1*0|1+1', '*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1']); runMutateAttributionTest(5, testPoolWithChars,
'Z:11>7-2*t+1*u+1|2=b|2+a=2*b+1*o+1*t+1*0|1+1*b+1*u+1=3|1-3-6$' + 'tucream\npie\nbot\nbu', ['*a+1*p+2*l+1*e+1*0|1+1', '*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1', '*c+1*a+1*b+2*a+1*g+1*e+1*0|1+1', '*d+1*u+1*f+2*l+1*e+1*0|1+1', '*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1'], ['*t+1*u+1*p+1*l+1*e+1*0|1+1', '*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1', '|1+6', '|1+4', '*c+1*a+1*b+1*o+1*t+1*0|1+1', '*b+1*u+1*b+2*a+1*0|1+1', '*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1']);
// based on runMutationTest#3 // based on runMutationTest#3
runMutateAttributionTest(6, testPoolWithChars, 'Z:11<f|1-6|2=f=6|1-1-8$', ['*a|1+6', '*b|1+7', '*c|1+8', '*d|1+7', '*e|1+9'], ['*b|1+7', '*c|1+8', '*d+6*e|1+1']); runMutateAttributionTest(6, testPoolWithChars,
'Z:11<f|1-6|2=f=6|1-1-8$', ['*a|1+6', '*b|1+7', '*c|1+8', '*d|1+7', '*e|1+9'],
['*b|1+7', '*c|1+8', '*d+6*e|1+1']);
// based on runMutationTest#4 // based on runMutationTest#4
runMutateAttributionTest(7, testPoolWithChars, 'Z:3>7=1|4+7$\n2\n3\n4\n', ['*1+1*5|1+2'], ['*1+1|1+1', '|1+2', '|1+2', '|1+2', '*5|1+2']); runMutateAttributionTest(7, testPoolWithChars, 'Z:3>7=1|4+7$\n2\n3\n4\n',
['*1+1*5|1+2'], ['*1+1|1+1', '|1+2', '|1+2', '|1+2', '*5|1+2']);
// based on runMutationTest#5 // based on runMutationTest#5
runMutateAttributionTest(8, testPoolWithChars, 'Z:a<7=1|4-7$', ['*1|1+2', '*2|1+2', '*3|1+2', '*4|1+2', '*5|1+2'], ['*1+1*5|1+2']); runMutateAttributionTest(8, testPoolWithChars, 'Z:a<7=1|4-7$',
['*1|1+2', '*2|1+2', '*3|1+2', '*4|1+2', '*5|1+2'], ['*1+1*5|1+2']);
// based on runMutationTest#6 // based on runMutationTest#6
runMutateAttributionTest(9, testPoolWithChars, 'Z:k<7*0+1*10|2=8|2-8$0', ['*1+1*2+1*3+1|1+1', '*a+1*b+1*c+1|1+1', '*d+1*e+1*f+1|1+1', '*g+1*h+1*i+1|1+1', '?*x+1*y+1*z+1|1+1'], ['*0+1|1+4', '|1+4', '?*x+1*y+1*z+1|1+1']); runMutateAttributionTest(9, testPoolWithChars, 'Z:k<7*0+1*10|2=8|2-8$0',
[
'*1+1*2+1*3+1|1+1',
'*a+1*b+1*c+1|1+1',
'*d+1*e+1*f+1|1+1',
'*g+1*h+1*i+1|1+1',
'?*x+1*y+1*z+1|1+1',
],
['*0+1|1+4', '|1+4', '?*x+1*y+1*z+1|1+1']);
runMutateAttributionTest(10, testPoolWithChars, 'Z:6>4=1+1=1+1|1=1+1=1*0+1$abcd', ['|1+3', '|1+3'], ['|1+5', '+2*0+1|1+2']); runMutateAttributionTest(10, testPoolWithChars, 'Z:6>4=1+1=1+1|1=1+1=1*0+1$abcd',
['|1+3', '|1+3'], ['|1+5', '+2*0+1|1+2']);
runMutateAttributionTest(11, testPoolWithChars, 'Z:s>1|1=4=6|1+1$\n', ['*0|1+4', '*0|1+8', '*0+5|1+1', '*0|1+1', '*0|1+5', '*0|1+1', '*0|1+1', '*0|1+1', '|1+1'], ['*0|1+4', '*0+6|1+1', '*0|1+2', '*0+5|1+1', '*0|1+1', '*0|1+5', '*0|1+1', '*0|1+1', '*0|1+1', '|1+1']); runMutateAttributionTest(11, testPoolWithChars, 'Z:s>1|1=4=6|1+1$\n',
['*0|1+4', '*0|1+8', '*0+5|1+1', '*0|1+1', '*0|1+5', '*0|1+1', '*0|1+1', '*0|1+1', '|1+1'],
[
'*0|1+4',
'*0+6|1+1',
'*0|1+2',
'*0+5|1+1',
'*0|1+1',
'*0|1+5',
'*0|1+1',
'*0|1+1',
'*0|1+1',
'|1+1',
]);
function randomInlineString(len, rand) { const randomInlineString = (len, rand) => {
const assem = Changeset.stringAssembler(); const assem = Changeset.stringAssembler();
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
assem.append(String.fromCharCode(rand.nextInt(26) + 97)); assem.append(String.fromCharCode(rand.nextInt(26) + 97));
} }
return assem.toString(); return assem.toString();
} };
function randomMultiline(approxMaxLines, approxMaxCols, rand) { const randomMultiline = (approxMaxLines, approxMaxCols, rand) => {
const numParts = rand.nextInt(approxMaxLines * 2) + 1; const numParts = rand.nextInt(approxMaxLines * 2) + 1;
const txt = Changeset.stringAssembler(); const txt = Changeset.stringAssembler();
txt.append(rand.nextInt(2) ? '\n' : ''); txt.append(rand.nextInt(2) ? '\n' : '');
for (let i = 0; i < numParts; i++) { for (let i = 0; i < numParts; i++) {
if ((i % 2) == 0) { if ((i % 2) === 0) {
if (rand.nextInt(10)) { if (rand.nextInt(10)) {
txt.append(randomInlineString(rand.nextInt(approxMaxCols) + 1, rand)); txt.append(randomInlineString(rand.nextInt(approxMaxCols) + 1, rand));
} else { } else {
@ -375,9 +406,9 @@ function runTests() {
} }
} }
return txt.toString(); return txt.toString();
} };
function randomStringOperation(numCharsLeft, rand) { const randomStringOperation = (numCharsLeft, rand) => {
let result; let result;
switch (rand.nextInt(9)) { switch (rand.nextInt(9)) {
case 0: case 0:
@ -476,26 +507,26 @@ function runTests() {
result.skip = Math.min(result.skip, maxOrig); result.skip = Math.min(result.skip, maxOrig);
} }
return result; return result;
} };
function randomTwoPropAttribs(opcode, rand) { const randomTwoPropAttribs = (opcode, rand) => {
// assumes attrib pool like ['apple,','apple,true','banana,','banana,true'] // assumes attrib pool like ['apple,','apple,true','banana,','banana,true']
if (opcode == '-' || rand.nextInt(3)) { if (opcode === '-' || rand.nextInt(3)) {
return ''; return '';
} else if (rand.nextInt(3)) { } else if (rand.nextInt(3)) {
if (opcode == '+' || rand.nextInt(2)) { if (opcode === '+' || rand.nextInt(2)) {
return `*${Changeset.numToString(rand.nextInt(2) * 2 + 1)}`; return `*${Changeset.numToString(rand.nextInt(2) * 2 + 1)}`;
} else { } else {
return `*${Changeset.numToString(rand.nextInt(2) * 2)}`; return `*${Changeset.numToString(rand.nextInt(2) * 2)}`;
} }
} else if (opcode == '+' || rand.nextInt(4) == 0) { } else if (opcode === '+' || rand.nextInt(4) === 0) {
return '*1*3'; return '*1*3';
} else { } else {
return ['*0*2', '*0*3', '*1*2'][rand.nextInt(3)]; return ['*0*2', '*0*3', '*1*2'][rand.nextInt(3)];
} }
} };
function randomTestChangeset(origText, rand, withAttribs) { const randomTestChangeset = (origText, rand, withAttribs) => {
const charBank = Changeset.stringAssembler(); const charBank = Changeset.stringAssembler();
let textLeft = origText; // always keep final newline let textLeft = origText; // always keep final newline
const outTextAssem = Changeset.stringAssembler(); const outTextAssem = Changeset.stringAssembler();
@ -504,13 +535,13 @@ function runTests() {
const nextOp = Changeset.newOp(); const nextOp = Changeset.newOp();
function appendMultilineOp(opcode, txt) { const appendMultilineOp = (opcode, txt) => {
nextOp.opcode = opcode; nextOp.opcode = opcode;
if (withAttribs) { if (withAttribs) {
nextOp.attribs = randomTwoPropAttribs(opcode, rand); nextOp.attribs = randomTwoPropAttribs(opcode, rand);
} }
txt.replace(/\n|[^\n]+/g, (t) => { txt.replace(/\n|[^\n]+/g, (t) => {
if (t == '\n') { if (t === '\n') {
nextOp.chars = 1; nextOp.chars = 1;
nextOp.lines = 1; nextOp.lines = 1;
opAssem.append(nextOp); opAssem.append(nextOp);
@ -521,26 +552,26 @@ function runTests() {
} }
return ''; return '';
}); });
} };
function doOp() { const doOp = () => {
const o = randomStringOperation(textLeft.length, rand); const o = randomStringOperation(textLeft.length, rand);
if (o.insert) { if (o.insert) {
var txt = o.insert; const txt = o.insert;
charBank.append(txt); charBank.append(txt);
outTextAssem.append(txt); outTextAssem.append(txt);
appendMultilineOp('+', txt); appendMultilineOp('+', txt);
} else if (o.skip) { } else if (o.skip) {
var txt = textLeft.substring(0, o.skip); const txt = textLeft.substring(0, o.skip);
textLeft = textLeft.substring(o.skip); textLeft = textLeft.substring(o.skip);
outTextAssem.append(txt); outTextAssem.append(txt);
appendMultilineOp('=', txt); appendMultilineOp('=', txt);
} else if (o.remove) { } else if (o.remove) {
var txt = textLeft.substring(0, o.remove); const txt = textLeft.substring(0, o.remove);
textLeft = textLeft.substring(o.remove); textLeft = textLeft.substring(o.remove);
appendMultilineOp('-', txt); appendMultilineOp('-', txt);
} }
} };
while (textLeft.length > 1) doOp(); while (textLeft.length > 1) doOp();
for (let i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen) for (let i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen)
@ -549,9 +580,9 @@ function runTests() {
const cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString()); const cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString());
Changeset.checkRep(cs); Changeset.checkRep(cs);
return [cs, outText]; return [cs, outText];
} };
function testCompose(randomSeed) { const testCompose = (randomSeed) => {
const rand = new random(); const rand = new random();
print(`> testCompose#${randomSeed}`); print(`> testCompose#${randomSeed}`);
@ -583,9 +614,9 @@ function runTests() {
assertEqualStrings(text2, Changeset.applyToText(change12, startText)); assertEqualStrings(text2, Changeset.applyToText(change12, startText));
assertEqualStrings(text3, Changeset.applyToText(change23, text1)); assertEqualStrings(text3, Changeset.applyToText(change23, text1));
assertEqualStrings(text3, Changeset.applyToText(change123, startText)); assertEqualStrings(text3, Changeset.applyToText(change123, startText));
} };
for (var i = 0; i < 30; i++) testCompose(i); for (let i = 0; i < 30; i++) testCompose(i);
(function simpleComposeAttributesTest() { (function simpleComposeAttributesTest() {
print('> simpleComposeAttributesTest'); print('> simpleComposeAttributesTest');
@ -607,12 +638,12 @@ function runTests() {
p.putAttrib(['y', 'abc']); p.putAttrib(['y', 'abc']);
p.putAttrib(['y', 'def']); p.putAttrib(['y', 'def']);
function testFollow(a, b, afb, bfa, merge) { const testFollow = (a, b, afb, bfa, merge) => {
assertEqualStrings(afb, Changeset.followAttributes(a, b, p)); assertEqualStrings(afb, Changeset.followAttributes(a, b, p));
assertEqualStrings(bfa, Changeset.followAttributes(b, a, p)); assertEqualStrings(bfa, Changeset.followAttributes(b, a, p));
assertEqualStrings(merge, Changeset.composeAttributes(a, afb, true, p)); assertEqualStrings(merge, Changeset.composeAttributes(a, afb, true, p));
assertEqualStrings(merge, Changeset.composeAttributes(b, bfa, true, p)); assertEqualStrings(merge, Changeset.composeAttributes(b, bfa, true, p));
} };
testFollow('', '', '', '', ''); testFollow('', '', '', '', '');
testFollow('*0', '', '', '*0', '*0'); testFollow('*0', '', '', '*0', '*0');
@ -624,7 +655,7 @@ function runTests() {
testFollow('*0*4', '*2', '', '*0*4', '*0*4'); testFollow('*0*4', '*2', '', '*0*4', '*0*4');
})(); })();
function testFollow(randomSeed) { const testFollow = (randomSeed) => {
const rand = new random(); const rand = new random();
print(`> testFollow#${randomSeed}`); print(`> testFollow#${randomSeed}`);
@ -642,37 +673,37 @@ function runTests() {
const merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa)); const merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa));
assertEqualStrings(merge1, merge2); assertEqualStrings(merge1, merge2);
} };
for (var i = 0; i < 30; i++) testFollow(i); for (let i = 0; i < 30; i++) testFollow(i);
function testSplitJoinAttributionLines(randomSeed) { const testSplitJoinAttributionLines = (randomSeed) => {
const rand = new random(); const rand = new random();
print(`> testSplitJoinAttributionLines#${randomSeed}`); print(`> testSplitJoinAttributionLines#${randomSeed}`);
const doc = `${randomMultiline(10, 20, rand)}\n`; const doc = `${randomMultiline(10, 20, rand)}\n`;
function stringToOps(str) { const stringToOps = (str) => {
const assem = Changeset.mergingOpAssembler(); const assem = Changeset.mergingOpAssembler();
const o = Changeset.newOp('+'); const o = Changeset.newOp('+');
o.chars = 1; o.chars = 1;
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
const c = str.charAt(i); const c = str.charAt(i);
o.lines = (c == '\n' ? 1 : 0); o.lines = (c === '\n' ? 1 : 0);
o.attribs = (c == 'a' || c == 'b' ? `*${c}` : ''); o.attribs = (c === 'a' || c === 'b' ? `*${c}` : '');
assem.append(o); assem.append(o);
} }
return assem.toString(); return assem.toString();
} };
const theJoined = stringToOps(doc); const theJoined = stringToOps(doc);
const theSplit = doc.match(/[^\n]*\n/g).map(stringToOps); const theSplit = doc.match(/[^\n]*\n/g).map(stringToOps);
assertEqualArrays(theSplit, Changeset.splitAttributionLines(theJoined, doc)); assertEqualArrays(theSplit, Changeset.splitAttributionLines(theJoined, doc));
assertEqualStrings(theJoined, Changeset.joinAttributionLines(theSplit)); assertEqualStrings(theJoined, Changeset.joinAttributionLines(theSplit));
} };
for (var i = 0; i < 10; i++) testSplitJoinAttributionLines(i); for (let i = 0; i < 10; i++) testSplitJoinAttributionLines(i);
(function testMoveOpsToNewPool() { (function testMoveOpsToNewPool() {
print('> testMoveOpsToNewPool'); print('> testMoveOpsToNewPool');
@ -685,8 +716,10 @@ function runTests() {
pool2.putAttrib(['foo', 'bar']); pool2.putAttrib(['foo', 'bar']);
assertEqualStrings(Changeset.moveOpsToNewPool('Z:1>2*1+1*0+1$ab', pool1, pool2), 'Z:1>2*0+1*1+1$ab'); assertEqualStrings(
assertEqualStrings(Changeset.moveOpsToNewPool('*1+1*0+1', pool1, pool2), '*0+1*1+1'); Changeset.moveOpsToNewPool('Z:1>2*1+1*0+1$ab', pool1, pool2), 'Z:1>2*0+1*1+1$ab');
assertEqualStrings(
Changeset.moveOpsToNewPool('*1+1*0+1', pool1, pool2), '*0+1*1+1');
})(); })();
@ -709,14 +742,15 @@ function runTests() {
assertEqualArrays(correctSplices, Changeset.toSplices(cs)); assertEqualArrays(correctSplices, Changeset.toSplices(cs));
})(); })();
function testCharacterRangeFollow(testId, cs, oldRange, insertionsAfter, correctNewRange) { const testCharacterRangeFollow = (testId, cs, oldRange, insertionsAfter, correctNewRange) => {
print(`> testCharacterRangeFollow#${testId}`); print(`> testCharacterRangeFollow#${testId}`);
cs = Changeset.checkRep(cs);
assertEqualArrays(correctNewRange, Changeset.characterRangeFollow(
cs, oldRange[0], oldRange[1], insertionsAfter));
};
var cs = Changeset.checkRep(cs); testCharacterRangeFollow(1, 'Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk',
assertEqualArrays(correctNewRange, Changeset.characterRangeFollow(cs, oldRange[0], oldRange[1], insertionsAfter)); [7, 10], false, [14, 15]);
}
testCharacterRangeFollow(1, 'Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk', [7, 10], false, [14, 15]);
testCharacterRangeFollow(2, 'Z:bc<6|x=b4|2-6$', [400, 407], false, [400, 401]); testCharacterRangeFollow(2, 'Z:bc<6|x=b4|2-6$', [400, 407], false, [400, 401]);
testCharacterRangeFollow(3, 'Z:4>0-3+3$abc', [0, 3], false, [3, 3]); testCharacterRangeFollow(3, 'Z:4>0-3+3$abc', [0, 3], false, [3, 3]);
testCharacterRangeFollow(4, 'Z:4>0-3+3$abc', [0, 3], true, [0, 0]); testCharacterRangeFollow(4, 'Z:4>0-3+3$abc', [0, 3], true, [0, 0]);
@ -735,23 +769,31 @@ function runTests() {
p.putAttrib(['name', 'david']); p.putAttrib(['name', 'david']);
p.putAttrib(['color', 'green']); p.putAttrib(['color', 'green']);
assertEqualStrings('david', Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'name', p)); assertEqualStrings('david',
assertEqualStrings('david', Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'name', p)); Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'name', p));
assertEqualStrings('', Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'name', p)); assertEqualStrings('david',
assertEqualStrings('', Changeset.opAttributeValue(Changeset.stringOp('+1'), 'name', p)); Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'name', p));
assertEqualStrings('green', Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'color', p)); assertEqualStrings('',
assertEqualStrings('green', Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'color', p)); Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'name', p));
assertEqualStrings('', Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'color', p)); assertEqualStrings('',
assertEqualStrings('', Changeset.opAttributeValue(Changeset.stringOp('+1'), 'color', p)); Changeset.opAttributeValue(Changeset.stringOp('+1'), 'name', p));
assertEqualStrings('green',
Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'color', p));
assertEqualStrings('green',
Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'color', p));
assertEqualStrings('',
Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'color', p));
assertEqualStrings('',
Changeset.opAttributeValue(Changeset.stringOp('+1'), 'color', p));
})(); })();
function testAppendATextToAssembler(testId, atext, correctOps) { const testAppendATextToAssembler = (testId, atext, correctOps) => {
print(`> testAppendATextToAssembler#${testId}`); print(`> testAppendATextToAssembler#${testId}`);
const assem = Changeset.smartOpAssembler(); const assem = Changeset.smartOpAssembler();
Changeset.appendATextToAssembler(atext, assem); Changeset.appendATextToAssembler(atext, assem);
assertEqualStrings(correctOps, assem.toString()); assertEqualStrings(correctOps, assem.toString());
} };
testAppendATextToAssembler(1, { testAppendATextToAssembler(1, {
text: '\n', text: '\n',
@ -786,13 +828,13 @@ function runTests() {
attribs: '|2+2*x|2+5', attribs: '|2+2*x|2+5',
}, '|2+2*x|1+1*x+3'); }, '|2+2*x|1+1*x+3');
function testMakeAttribsString(testId, pool, opcode, attribs, correctString) { const testMakeAttribsString = (testId, pool, opcode, attribs, correctString) => {
print(`> testMakeAttribsString#${testId}`); print(`> testMakeAttribsString#${testId}`);
const p = poolOrArray(pool); const p = poolOrArray(pool);
const str = Changeset.makeAttribsString(opcode, attribs, p); const str = Changeset.makeAttribsString(opcode, attribs, p);
assertEqualStrings(correctString, str); assertEqualStrings(correctString, str);
} };
testMakeAttribsString(1, ['bold,'], '+', [ testMakeAttribsString(1, ['bold,'], '+', [
['bold', ''], ['bold', ''],
@ -809,12 +851,12 @@ function runTests() {
['abc', 'def'], ['abc', 'def'],
], '*0*1'); ], '*0*1');
function testSubattribution(testId, astr, start, end, correctOutput) { const testSubattribution = (testId, astr, start, end, correctOutput) => {
print(`> testSubattribution#${testId}`); print(`> testSubattribution#${testId}`);
const str = Changeset.subattribution(astr, start, end); const str = Changeset.subattribution(astr, start, end);
assertEqualStrings(correctOutput, str); assertEqualStrings(correctOutput, str);
} };
testSubattribution(1, '+1', 0, 0, ''); testSubattribution(1, '+1', 0, 0, '');
testSubattribution(2, '+1', 0, 1, '+1'); testSubattribution(2, '+1', 0, 1, '+1');
@ -859,39 +901,42 @@ function runTests() {
testSubattribution(41, '*0+2+1*1|1+3', 2, 6, '+1*1|1+3'); testSubattribution(41, '*0+2+1*1|1+3', 2, 6, '+1*1|1+3');
testSubattribution(42, '*0+2+1*1+3', 2, 6, '+1*1+3'); testSubattribution(42, '*0+2+1*1+3', 2, 6, '+1*1+3');
function testFilterAttribNumbers(testId, cs, filter, correctOutput) { const testFilterAttribNumbers = (testId, cs, filter, correctOutput) => {
print(`> testFilterAttribNumbers#${testId}`); print(`> testFilterAttribNumbers#${testId}`);
const str = Changeset.filterAttribNumbers(cs, filter); const str = Changeset.filterAttribNumbers(cs, filter);
assertEqualStrings(correctOutput, str); assertEqualStrings(correctOutput, str);
} };
testFilterAttribNumbers(1, '*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6', (n) => (n % 2) == 0, '*0+1+2+3+4*2+5*0*2*c+6'); testFilterAttribNumbers(1, '*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6',
testFilterAttribNumbers(2, '*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6', (n) => (n % 2) == 1, '*1+1+2+3*1+4+5*1*b+6'); (n) => (n % 2) === 0, '*0+1+2+3+4*2+5*0*2*c+6');
testFilterAttribNumbers(2, '*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6',
(n) => (n % 2) === 1, '*1+1+2+3*1+4+5*1*b+6');
function testInverse(testId, cs, lines, alines, pool, correctOutput) { const testInverse = (testId, cs, lines, alines, pool, correctOutput) => {
print(`> testInverse#${testId}`); print(`> testInverse#${testId}`);
pool = poolOrArray(pool); pool = poolOrArray(pool);
const str = Changeset.inverse(Changeset.checkRep(cs), lines, alines, pool); const str = Changeset.inverse(Changeset.checkRep(cs), lines, alines, pool);
assertEqualStrings(correctOutput, str); assertEqualStrings(correctOutput, str);
} };
// take "FFFFTTTTT" and apply "-FT--FFTT", the inverse of which is "--F--TT--" // take "FFFFTTTTT" and apply "-FT--FFTT", the inverse of which is "--F--TT--"
testInverse(1, 'Z:9>0=1*0=1*1=1=2*0=2*1|1=2$', null, ['+4*1+5'], ['bold,', 'bold,true'], 'Z:9>0=2*0=1=2*1=2$'); testInverse(1, 'Z:9>0=1*0=1*1=1=2*0=2*1|1=2$', null,
['+4*1+5'], ['bold,', 'bold,true'], 'Z:9>0=2*0=1=2*1=2$');
function testMutateTextLines(testId, cs, lines, correctLines) { const testMutateTextLines = (testId, cs, lines, correctLines) => {
print(`> testMutateTextLines#${testId}`); print(`> testMutateTextLines#${testId}`);
const a = lines.slice(); const a = lines.slice();
Changeset.mutateTextLines(cs, a); Changeset.mutateTextLines(cs, a);
assertEqualArrays(correctLines, a); assertEqualArrays(correctLines, a);
} };
testMutateTextLines(1, 'Z:4<1|1-2-1|1+1+1$\nc', ['a\n', 'b\n'], ['\n', 'c\n']); testMutateTextLines(1, 'Z:4<1|1-2-1|1+1+1$\nc', ['a\n', 'b\n'], ['\n', 'c\n']);
testMutateTextLines(2, 'Z:4>0|1-2-1|2+3$\nc\n', ['a\n', 'b\n'], ['\n', 'c\n', '\n']); testMutateTextLines(2, 'Z:4>0|1-2-1|2+3$\nc\n', ['a\n', 'b\n'], ['\n', 'c\n', '\n']);
function testInverseRandom(randomSeed) { const testInverseRandom = (randomSeed) => {
const rand = new random(); const rand = new random();
print(`> testInverseRandom#${randomSeed}`); print(`> testInverseRandom#${randomSeed}`);
@ -928,9 +973,9 @@ function runTests() {
// print(lines.map(function(s) { return '3: '+s.slice(0,-1); }).join('\n')); // print(lines.map(function(s) { return '3: '+s.slice(0,-1); }).join('\n'));
assertEqualArrays(origLines, lines); assertEqualArrays(origLines, lines);
assertEqualArrays(origALines, alines); assertEqualArrays(origALines, alines);
} };
for (var i = 0; i < 30; i++) testInverseRandom(i); for (let i = 0; i < 30; i++) testInverseRandom(i);
} };
runTests(); runTests();

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* The API Handler handles all API http requests * The API Handler handles all API http requests
*/ */
@ -37,7 +38,8 @@ try {
apikey = fs.readFileSync(apikeyFilename, 'utf8'); apikey = fs.readFileSync(apikeyFilename, 'utf8');
apiHandlerLogger.info(`Api key file read from: "${apikeyFilename}"`); apiHandlerLogger.info(`Api key file read from: "${apikeyFilename}"`);
} catch (e) { } catch (e) {
apiHandlerLogger.info(`Api key file "${apikeyFilename}" not found. Creating with random contents.`); apiHandlerLogger.info(
`Api key file "${apikeyFilename}" not found. Creating with random contents.`);
apikey = randomString(32); apikey = randomString(32);
fs.writeFileSync(apikeyFilename, apikey, 'utf8'); fs.writeFileSync(apikeyFilename, apikey, 'utf8');
} }

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* Handles the export requests * Handles the export requests
*/ */
@ -25,7 +26,7 @@ const exportEtherpad = require('../utils/ExportEtherpad');
const fs = require('fs'); const fs = require('fs');
const settings = require('../utils/Settings'); const settings = require('../utils/Settings');
const os = require('os'); const os = require('os');
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); const hooks = require('../../static/js/pluginfw/hooks');
const TidyHtml = require('../utils/TidyHtml'); const TidyHtml = require('../utils/TidyHtml');
const util = require('util'); const util = require('util');
@ -49,7 +50,7 @@ const tempDirectory = os.tmpdir();
/** /**
* do a requested export * do a requested export
*/ */
async function doExport(req, res, padId, readOnlyId, type) { const doExport = async (req, res, padId, readOnlyId, type) => {
// avoid naming the read-only file as the original pad's id // avoid naming the read-only file as the original pad's id
let fileName = readOnlyId ? readOnlyId : padId; let fileName = readOnlyId ? readOnlyId : padId;
@ -104,7 +105,6 @@ async function doExport(req, res, padId, readOnlyId, type) {
const result = await hooks.aCallAll('exportConvert', {srcFile, destFile, req, res}); const result = await hooks.aCallAll('exportConvert', {srcFile, destFile, req, res});
if (result.length > 0) { if (result.length > 0) {
// console.log("export handled by plugin", destFile); // console.log("export handled by plugin", destFile);
handledByPlugin = true;
} else { } else {
// @TODO no Promise interface for convertors (yet) // @TODO no Promise interface for convertors (yet)
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
@ -115,7 +115,6 @@ async function doExport(req, res, padId, readOnlyId, type) {
} }
// send the file // send the file
const sendFile = util.promisify(res.sendFile);
await res.sendFile(destFile, null); await res.sendFile(destFile, null);
// clean up temporary files // clean up temporary files
@ -128,9 +127,9 @@ async function doExport(req, res, padId, readOnlyId, type) {
await fsp_unlink(destFile); await fsp_unlink(destFile);
} }
} };
exports.doExport = function (req, res, padId, readOnlyId, type) { exports.doExport = (req, res, padId, readOnlyId, type) => {
doExport(req, res, padId, readOnlyId, type).catch((err) => { doExport(req, res, padId, readOnlyId, type).catch((err) => {
if (err !== 'stop') { if (err !== 'stop') {
throw err; throw err;

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* Handles the import requests * Handles the import requests
*/ */
@ -30,7 +31,7 @@ const os = require('os');
const importHtml = require('../utils/ImportHtml'); const importHtml = require('../utils/ImportHtml');
const importEtherpad = require('../utils/ImportEtherpad'); const importEtherpad = require('../utils/ImportEtherpad');
const log4js = require('log4js'); const log4js = require('log4js');
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks.js'); const hooks = require('../../static/js/pluginfw/hooks.js');
const util = require('util'); const util = require('util');
const fsp_exists = util.promisify(fs.exists); const fsp_exists = util.promisify(fs.exists);
@ -42,7 +43,7 @@ let convertor = null;
let exportExtension = 'htm'; let exportExtension = 'htm';
// load abiword only if it is enabled and if soffice is disabled // load abiword only if it is enabled and if soffice is disabled
if (settings.abiword != null && settings.soffice === null) { if (settings.abiword != null && settings.soffice == null) {
convertor = require('../utils/Abiword'); convertor = require('../utils/Abiword');
} }
@ -57,7 +58,7 @@ const tmpDirectory = os.tmpdir();
/** /**
* do a requested import * do a requested import
*/ */
async function doImport(req, res, padId) { const doImport = async (req, res, padId) => {
const apiLogger = log4js.getLogger('ImportHandler'); const apiLogger = log4js.getLogger('ImportHandler');
// pipe to a file // pipe to a file
@ -112,7 +113,8 @@ async function doImport(req, res, padId) {
// ensure this is a file ending we know, else we change the file ending to .txt // ensure this is a file ending we know, else we change the file ending to .txt
// this allows us to accept source code files like .c or .java // this allows us to accept source code files like .c or .java
const fileEnding = path.extname(srcFile).toLowerCase(); const fileEnding = path.extname(srcFile).toLowerCase();
const knownFileEndings = ['.txt', '.doc', '.docx', '.pdf', '.odt', '.html', '.htm', '.etherpad', '.rtf']; const knownFileEndings =
['.txt', '.doc', '.docx', '.pdf', '.odt', '.html', '.htm', '.etherpad', '.rtf'];
const fileEndingUnknown = (knownFileEndings.indexOf(fileEnding) < 0); const fileEndingUnknown = (knownFileEndings.indexOf(fileEnding) < 0);
if (fileEndingUnknown) { if (fileEndingUnknown) {
@ -146,7 +148,7 @@ async function doImport(req, res, padId) {
const headCount = _pad.head; const headCount = _pad.head;
if (headCount >= 10) { if (headCount >= 10) {
apiLogger.warn("Direct database Import attempt of a pad that already has content, we won't be doing this"); apiLogger.warn('Aborting direct database import attempt of a pad that already has content');
throw 'padHasData'; throw 'padHasData';
} }
@ -251,9 +253,9 @@ async function doImport(req, res, padId) {
if (await fsp_exists(destFile)) { if (await fsp_exists(destFile)) {
fsp_unlink(destFile); fsp_unlink(destFile);
} }
} };
exports.doImport = function (req, res, padId) { exports.doImport = (req, res, padId) => {
/** /**
* NB: abuse the 'req' object by storing an additional * NB: abuse the 'req' object by storing an additional
* 'directDatabaseAccess' property on it so that it can * 'directDatabaseAccess' property on it so that it can
@ -266,7 +268,10 @@ exports.doImport = function (req, res, padId) {
let status = 'ok'; let status = 'ok';
doImport(req, res, padId).catch((err) => { doImport(req, res, padId).catch((err) => {
// check for known errors and replace the status // check for known errors and replace the status
if (err == 'uploadFailed' || err == 'convertFailed' || err == 'padHasData' || err == 'maxFileSize') { if (err === 'uploadFailed' ||
err === 'convertFailed' ||
err === 'padHasData' ||
err === 'maxFileSize') {
status = err; status = err;
} else { } else {
throw err; throw err;

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* The MessageHandler handles all Messages that comes from Socket.IO and controls the sessions * The MessageHandler handles all Messages that comes from Socket.IO and controls the sessions
*/ */
@ -18,22 +19,20 @@
* limitations under the License. * limitations under the License.
*/ */
/* global exports, process, require */
const padManager = require('../db/PadManager'); const padManager = require('../db/PadManager');
const Changeset = require('ep_etherpad-lite/static/js/Changeset'); const Changeset = require('../../static/js/Changeset');
const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); const AttributePool = require('../../static/js/AttributePool');
const AttributeManager = require('ep_etherpad-lite/static/js/AttributeManager'); const AttributeManager = require('../../static/js/AttributeManager');
const authorManager = require('../db/AuthorManager'); const authorManager = require('../db/AuthorManager');
const readOnlyManager = require('../db/ReadOnlyManager'); const readOnlyManager = require('../db/ReadOnlyManager');
const settings = require('../utils/Settings'); const settings = require('../utils/Settings');
const securityManager = require('../db/SecurityManager'); const securityManager = require('../db/SecurityManager');
const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs.js'); const plugins = require('../../static/js/pluginfw/plugin_defs.js');
const log4js = require('log4js'); const log4js = require('log4js');
const messageLogger = log4js.getLogger('message'); const messageLogger = log4js.getLogger('message');
const accessLogger = log4js.getLogger('access'); const accessLogger = log4js.getLogger('access');
const _ = require('underscore'); const _ = require('underscore');
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks.js'); const hooks = require('../../static/js/pluginfw/hooks.js');
const channels = require('channels'); const channels = require('channels');
const stats = require('../stats'); const stats = require('../stats');
const assert = require('assert').strict; const assert = require('assert').strict;
@ -65,7 +64,9 @@ stats.gauge('totalUsers', () => Object.keys(socketio.sockets.sockets).length);
/** /**
* A changeset queue per pad that is processed by handleUserChanges() * A changeset queue per pad that is processed by handleUserChanges()
*/ */
const padChannels = new channels.channels(({socket, message}, callback) => nodeify(handleUserChanges(socket, message), callback)); const padChannels = new channels.channels(
({socket, message}, callback) => nodeify(handleUserChanges(socket, message), callback)
);
/** /**
* Saves the Socket class we need to send and receive data from the client * Saves the Socket class we need to send and receive data from the client
@ -76,7 +77,7 @@ let socketio;
* This Method is called by server.js to tell the message handler on which socket it should send * This Method is called by server.js to tell the message handler on which socket it should send
* @param socket_io The Socket * @param socket_io The Socket
*/ */
exports.setSocketIO = function (socket_io) { exports.setSocketIO = (socket_io) => {
socketio = socket_io; socketio = socket_io;
}; };
@ -94,7 +95,7 @@ exports.handleConnect = (socket) => {
/** /**
* Kicks all sessions from a pad * Kicks all sessions from a pad
*/ */
exports.kickSessionsFromPad = function (padID) { exports.kickSessionsFromPad = (padID) => {
if (typeof socketio.sockets.clients !== 'function') return; if (typeof socketio.sockets.clients !== 'function') return;
// skip if there is nobody on this pad // skip if there is nobody on this pad
@ -114,7 +115,8 @@ exports.handleDisconnect = async (socket) => {
// save the padname of this session // save the padname of this session
const session = sessioninfos[socket.id]; const session = sessioninfos[socket.id];
// if this connection was already etablished with a handshake, send a disconnect message to the others // if this connection was already etablished with a handshake,
// send a disconnect message to the others
if (session && session.author) { if (session && session.author) {
const {session: {user} = {}} = socket.client.request; const {session: {user} = {}} = socket.client.request;
accessLogger.info(`${'[LEAVE]' + accessLogger.info(`${'[LEAVE]' +
@ -192,7 +194,8 @@ exports.handleMessage = async (socket, message) => {
const auth = thisSession.auth; const auth = thisSession.auth;
if (!auth) { if (!auth) {
console.error('Auth was never applied to a session. If you are using the stress-test tool then restart Etherpad and the Stress test tool.'); console.error('Auth was never applied to a session. If you are using the ' +
'stress-test tool then restart Etherpad and the Stress test tool.');
return; return;
} }
@ -234,7 +237,7 @@ exports.handleMessage = async (socket, message) => {
} }
// Call handleMessage hook. If a plugin returns null, the message will be dropped. // 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 hooks.aCallAll('handleMessage', context)).some((m) => m == null)) {
return; return;
} }
@ -283,11 +286,11 @@ exports.handleMessage = async (socket, message) => {
* @param socket the socket.io Socket object for the client * @param socket the socket.io Socket object for the client
* @param message the message from the client * @param message the message from the client
*/ */
async function handleSaveRevisionMessage(socket, message) { const handleSaveRevisionMessage = async (socket, message) => {
const {padId, author: authorId} = sessioninfos[socket.id]; const {padId, author: authorId} = sessioninfos[socket.id];
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
await pad.addSavedRevision(pad.head, authorId); await pad.addSavedRevision(pad.head, authorId);
} };
/** /**
* Handles a custom message, different to the function below as it handles * Handles a custom message, different to the function below as it handles
@ -296,7 +299,7 @@ async function handleSaveRevisionMessage(socket, message) {
* @param msg {Object} the message we're sending * @param msg {Object} the message we're sending
* @param sessionID {string} the socketIO session to which we're sending this message * @param sessionID {string} the socketIO session to which we're sending this message
*/ */
exports.handleCustomObjectMessage = function (msg, sessionID) { exports.handleCustomObjectMessage = (msg, sessionID) => {
if (msg.data.type === 'CUSTOM') { if (msg.data.type === 'CUSTOM') {
if (sessionID) { if (sessionID) {
// a sessionID is targeted: directly to this sessionID // a sessionID is targeted: directly to this sessionID
@ -314,7 +317,7 @@ exports.handleCustomObjectMessage = function (msg, sessionID) {
* @param padID {Pad} the pad to which we're sending this message * @param padID {Pad} the pad to which we're sending this message
* @param msgString {String} the message we're sending * @param msgString {String} the message we're sending
*/ */
exports.handleCustomMessage = function (padID, msgString) { exports.handleCustomMessage = (padID, msgString) => {
const time = Date.now(); const time = Date.now();
const msg = { const msg = {
type: 'COLLABROOM', type: 'COLLABROOM',
@ -331,12 +334,12 @@ exports.handleCustomMessage = function (padID, msgString) {
* @param socket the socket.io Socket object for the client * @param socket the socket.io Socket object for the client
* @param message the message from the client * @param message the message from the client
*/ */
async function handleChatMessage(socket, message) { const handleChatMessage = async (socket, message) => {
const time = Date.now(); const time = Date.now();
const text = message.data.text; const text = message.data.text;
const {padId, author: authorId} = sessioninfos[socket.id]; const {padId, author: authorId} = sessioninfos[socket.id];
await exports.sendChatMessageToPadClients(time, authorId, text, padId); await exports.sendChatMessageToPadClients(time, authorId, text, padId);
} };
/** /**
* Sends a chat message to all clients of this pad * Sends a chat message to all clients of this pad
@ -345,7 +348,7 @@ async function handleChatMessage(socket, message) {
* @param text the text of the chat message * @param text the text of the chat message
* @param padId the padId to send the chat message to * @param padId the padId to send the chat message to
*/ */
exports.sendChatMessageToPadClients = async function (time, userId, text, padId) { exports.sendChatMessageToPadClients = async (time, userId, text, padId) => {
// get the pad // get the pad
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
@ -371,7 +374,7 @@ exports.sendChatMessageToPadClients = async function (time, userId, text, padId)
* @param socket the socket.io Socket object for the client * @param socket the socket.io Socket object for the client
* @param message the message from the client * @param message the message from the client
*/ */
async function handleGetChatMessages(socket, message) { const handleGetChatMessages = async (socket, message) => {
if (message.data.start == null) { if (message.data.start == null) {
messageLogger.warn('Dropped message, GetChatMessages Message has no start!'); messageLogger.warn('Dropped message, GetChatMessages Message has no start!');
return; return;
@ -387,7 +390,8 @@ async function handleGetChatMessages(socket, message) {
const count = end - start; const count = end - start;
if (count < 0 || count > 100) { if (count < 0 || count > 100) {
messageLogger.warn('Dropped message, GetChatMessages Message, client requested invalid amount of messages!'); messageLogger.warn(
'Dropped message, GetChatMessages Message, client requested invalid amount of messages!');
return; return;
} }
@ -405,14 +409,14 @@ async function handleGetChatMessages(socket, message) {
// send the messages back to the client // send the messages back to the client
socket.json.send(infoMsg); socket.json.send(infoMsg);
} };
/** /**
* Handles a handleSuggestUserName, that means a user have suggest a userName for a other user * Handles a handleSuggestUserName, that means a user have suggest a userName for a other user
* @param socket the socket.io Socket object for the client * @param socket the socket.io Socket object for the client
* @param message the message from the client * @param message the message from the client
*/ */
function handleSuggestUserName(socket, message) { const handleSuggestUserName = (socket, message) => {
// check if all ok // check if all ok
if (message.data.payload.newName == null) { if (message.data.payload.newName == null) {
messageLogger.warn('Dropped message, suggestUserName Message has no newName!'); messageLogger.warn('Dropped message, suggestUserName Message has no newName!');
@ -433,14 +437,15 @@ function handleSuggestUserName(socket, message) {
socket.json.send(message); socket.json.send(message);
} }
}); });
} };
/** /**
* Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations * Handles a USERINFO_UPDATE, that means that a user have changed his color or name.
* Anyway, we get both informations
* @param socket the socket.io Socket object for the client * @param socket the socket.io Socket object for the client
* @param message the message from the client * @param message the message from the client
*/ */
async function handleUserInfoUpdate(socket, message) { const handleUserInfoUpdate = async (socket, message) => {
// check if all ok // check if all ok
if (message.data.userInfo == null) { if (message.data.userInfo == null) {
messageLogger.warn('Dropped message, USERINFO_UPDATE Message has no userInfo!'); messageLogger.warn('Dropped message, USERINFO_UPDATE Message has no userInfo!');
@ -463,7 +468,8 @@ async function handleUserInfoUpdate(socket, message) {
const author = session.author; const author = session.author;
// Check colorId is a Hex color // Check colorId is a Hex color
const isColor = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(message.data.userInfo.colorId); // for #f00 (Thanks Smamatti) // for #f00 (Thanks Smamatti)
const isColor = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(message.data.userInfo.colorId);
if (!isColor) { if (!isColor) {
messageLogger.warn(`Dropped message, USERINFO_UPDATE Color is malformed.${message.data}`); messageLogger.warn(`Dropped message, USERINFO_UPDATE Color is malformed.${message.data}`);
return; return;
@ -496,7 +502,7 @@ async function handleUserInfoUpdate(socket, message) {
// Block until the authorManager has stored the new attributes. // Block until the authorManager has stored the new attributes.
await p; await p;
} };
/** /**
* Handles a USER_CHANGES message, where the client submits its local * Handles a USER_CHANGES message, where the client submits its local
@ -512,7 +518,7 @@ async function handleUserInfoUpdate(socket, message) {
* @param socket the socket.io Socket object for the client * @param socket the socket.io Socket object for the client
* @param message the message from the client * @param message the message from the client
*/ */
async function handleUserChanges(socket, message) { const handleUserChanges = async (socket, message) => {
// This one's no longer pending, as we're gonna process it now // This one's no longer pending, as we're gonna process it now
stats.counter('pendingEdits').dec(); stats.counter('pendingEdits').dec();
@ -578,7 +584,8 @@ async function handleUserChanges(socket, message) {
// + can add text with attribs // + can add text with attribs
// = can change or add attribs // = can change or add attribs
// - can have attribs, but they are discarded and don't show up in the attribs - but do show up in the pool // - can have attribs, but they are discarded and don't show up in the attribs -
// but do show up in the pool
op.attribs.split('*').forEach((attr) => { op.attribs.split('*').forEach((attr) => {
if (!attr) return; if (!attr) return;
@ -586,9 +593,11 @@ async function handleUserChanges(socket, message) {
attr = wireApool.getAttrib(attr); attr = wireApool.getAttrib(attr);
if (!attr) return; if (!attr) return;
// the empty author is used in the clearAuthorship functionality so this should be the only exception // the empty author is used in the clearAuthorship functionality so this
// should be the only exception
if ('author' === attr[0] && (attr[1] !== thisSession.author && attr[1] !== '')) { if ('author' === attr[0] && (attr[1] !== thisSession.author && attr[1] !== '')) {
throw new Error(`Author ${thisSession.author} tried to submit changes as author ${attr[1]} in changeset ${changeset}`); throw new Error(`Author ${thisSession.author} tried to submit changes as author ` +
`${attr[1]} in changeset ${changeset}`);
} }
}); });
} }
@ -628,7 +637,7 @@ async function handleUserChanges(socket, message) {
if (baseRev + 1 === r && c === changeset) { if (baseRev + 1 === r && c === changeset) {
socket.json.send({disconnect: 'badChangeset'}); socket.json.send({disconnect: 'badChangeset'});
stats.meter('failedChangesets').mark(); stats.meter('failedChangesets').mark();
throw new Error("Won't apply USER_CHANGES, because it contains an already accepted changeset"); throw new Error("Won't apply USER_CHANGES, as it contains an already accepted changeset");
} }
changeset = Changeset.follow(c, changeset, false, apool); changeset = Changeset.follow(c, changeset, false, apool);
@ -672,9 +681,9 @@ async function handleUserChanges(socket, message) {
} }
stopWatch.end(); stopWatch.end();
} };
exports.updatePadClients = async function (pad) { exports.updatePadClients = async (pad) => {
// skip this if no-one is on this pad // skip this if no-one is on this pad
const roomSockets = _getRoomSockets(pad.id); const roomSockets = _getRoomSockets(pad.id);
if (roomSockets.length === 0) return; if (roomSockets.length === 0) return;
@ -682,9 +691,12 @@ exports.updatePadClients = async function (pad) {
// since all clients usually get the same set of changesets, store them in local cache // since all clients usually get the same set of changesets, store them in local cache
// to remove unnecessary roundtrip to the datalayer // to remove unnecessary roundtrip to the datalayer
// NB: note below possibly now accommodated via the change to promises/async // NB: note below possibly now accommodated via the change to promises/async
// TODO: in REAL world, if we're working without datalayer cache, all requests to revisions will be fired // TODO: in REAL world, if we're working without datalayer cache,
// BEFORE first result will be landed to our cache object. The solution is to replace parallel processing // all requests to revisions will be fired
// via async.forEach with sequential for() loop. There is no real benefits of running this in parallel, // BEFORE first result will be landed to our cache object.
// The solution is to replace parallel processing
// via async.forEach with sequential for() loop. There is no real
// benefits of running this in parallel,
// but benefit of reusing cached revision object is HUGE // but benefit of reusing cached revision object is HUGE
const revCache = {}; const revCache = {};
@ -737,7 +749,7 @@ exports.updatePadClients = async function (pad) {
/** /**
* Copied from the Etherpad Source Code. Don't know what this method does excatly... * Copied from the Etherpad Source Code. Don't know what this method does excatly...
*/ */
function _correctMarkersInPad(atext, apool) { const _correctMarkersInPad = (atext, apool) => {
const text = atext.text; const text = atext.text;
// collect char positions of line markers (e.g. bullets) in new atext // collect char positions of line markers (e.g. bullets) in new atext
@ -746,9 +758,11 @@ function _correctMarkersInPad(atext, apool) {
const iter = Changeset.opIterator(atext.attribs); const iter = Changeset.opIterator(atext.attribs);
let offset = 0; let offset = 0;
while (iter.hasNext()) { while (iter.hasNext()) {
var op = iter.next(); const op = iter.next();
const hasMarker = _.find(AttributeManager.lineAttributes, (attribute) => Changeset.opAttributeValue(op, attribute, apool)) !== undefined; const hasMarker = _.find(
AttributeManager.lineAttributes,
(attribute) => Changeset.opAttributeValue(op, attribute, apool)) !== undefined;
if (hasMarker) { if (hasMarker) {
for (let i = 0; i < op.chars; i++) { for (let i = 0; i < op.chars; i++) {
@ -778,9 +792,9 @@ function _correctMarkersInPad(atext, apool) {
}); });
return builder.toString(); return builder.toString();
} };
async function handleSwitchToPad(socket, message, _authorID) { const handleSwitchToPad = async (socket, message, _authorID) => {
const currentSessionInfo = sessioninfos[socket.id]; const currentSessionInfo = sessioninfos[socket.id];
const padId = currentSessionInfo.padId; const padId = currentSessionInfo.padId;
@ -816,10 +830,10 @@ async function handleSwitchToPad(socket, message, _authorID) {
const newSessionInfo = sessioninfos[socket.id]; const newSessionInfo = sessioninfos[socket.id];
createSessionInfoAuth(newSessionInfo, message); createSessionInfoAuth(newSessionInfo, message);
await handleClientReady(socket, message, authorID); await handleClientReady(socket, message, authorID);
} };
// Creates/replaces the auth object in the given session info. // Creates/replaces the auth object in the given session info.
function createSessionInfoAuth(sessionInfo, message) { const createSessionInfoAuth = (sessionInfo, message) => {
// Remember this information since we won't // Remember this information since we won't
// have the cookie in further socket.io messages. // have the cookie in further socket.io messages.
// This information will be used to check if // This information will be used to check if
@ -830,15 +844,16 @@ function createSessionInfoAuth(sessionInfo, message) {
padID: message.padId, padID: message.padId,
token: message.token, token: message.token,
}; };
} };
/** /**
* Handles a CLIENT_READY. A CLIENT_READY is the first message from the client to the server. The Client sends his token * Handles a CLIENT_READY. A CLIENT_READY is the first message from the client
* to the server. The Client sends his token
* and the pad it wants to enter. The Server answers with the inital values (clientVars) of the pad * and the pad it wants to enter. The Server answers with the inital values (clientVars) of the pad
* @param socket the socket.io Socket object for the client * @param socket the socket.io Socket object for the client
* @param message the message from the client * @param message the message from the client
*/ */
async function handleClientReady(socket, message, authorID) { const handleClientReady = async (socket, message, authorID) => {
// check if all ok // check if all ok
if (!message.token) { if (!message.token) {
messageLogger.warn('Dropped message, CLIENT_READY Message has no token!'); messageLogger.warn('Dropped message, CLIENT_READY Message has no token!');
@ -884,9 +899,11 @@ async function handleClientReady(socket, message, authorID) {
const historicalAuthorData = {}; const historicalAuthorData = {};
await Promise.all(authors.map((authorId) => authorManager.getAuthor(authorId).then((author) => { await Promise.all(authors.map((authorId) => authorManager.getAuthor(authorId).then((author) => {
if (!author) { if (!author) {
messageLogger.error('There is no author for authorId: ', authorId, '. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802'); messageLogger.error(`There is no author for authorId: ${authorId}. ` +
'This is possibly related to https://github.com/ether/etherpad-lite/issues/2802');
} else { } else {
historicalAuthorData[authorId] = {name: author.name, colorId: author.colorId}; // Filter author attribs (e.g. don't send author's pads to all clients) // Filter author attribs (e.g. don't send author's pads to all clients)
historicalAuthorData[authorId] = {name: author.name, colorId: author.colorId};
} }
}))); })));
@ -931,7 +948,8 @@ async function handleClientReady(socket, message, authorID) {
// Save the revision in sessioninfos, we take the revision from the info the client send to us // Save the revision in sessioninfos, we take the revision from the info the client send to us
sessionInfo.rev = message.client_rev; sessionInfo.rev = message.client_rev;
// During the client reconnect, client might miss some revisions from other clients. By using client revision, // During the client reconnect, client might miss some revisions from other clients.
// By using client revision,
// this below code sends all the revisions missed during the client reconnect // this below code sends all the revisions missed during the client reconnect
const revisionsNeeded = []; const revisionsNeeded = [];
const changesets = {}; const changesets = {};
@ -987,12 +1005,13 @@ async function handleClientReady(socket, message, authorID) {
} }
} else { } else {
// This is a normal first connect // This is a normal first connect
let atext;
let apool;
// prepare all values for the wire, there's a chance that this throws, if the pad is corrupted // prepare all values for the wire, there's a chance that this throws, if the pad is corrupted
try { try {
var atext = Changeset.cloneAText(pad.atext); atext = Changeset.cloneAText(pad.atext);
const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
var apool = attribsForWire.pool.toJsonable(); apool = attribsForWire.pool.toJsonable();
atext.attribs = attribsForWire.translated; atext.attribs = attribsForWire.translated;
} catch (e) { } catch (e) {
console.error(e.stack || e); console.error(e.stack || e);
@ -1147,12 +1166,12 @@ async function handleClientReady(socket, message, authorID) {
socket.json.send(msg); socket.json.send(msg);
})); }));
} }
} };
/** /**
* Handles a request for a rough changeset, the timeslider client needs it * Handles a request for a rough changeset, the timeslider client needs it
*/ */
async function handleChangesetRequest(socket, message) { const handleChangesetRequest = async (socket, message) => {
// check if all ok // check if all ok
if (message.data == null) { if (message.data == null) {
messageLogger.warn('Dropped message, changeset request has no data!'); messageLogger.warn('Dropped message, changeset request has no data!');
@ -1197,15 +1216,16 @@ async function handleChangesetRequest(socket, message) {
data.requestID = message.data.requestID; data.requestID = message.data.requestID;
socket.json.send({type: 'CHANGESET_REQ', data}); socket.json.send({type: 'CHANGESET_REQ', data});
} catch (err) { } catch (err) {
console.error(`Error while handling a changeset request for ${padIds.padId}`, err.toString(), message.data); console.error(`Error while handling a changeset request for ${padIds.padId}`,
} err.toString(), message.data);
} }
};
/** /**
* Tries to rebuild the getChangestInfo function of the original Etherpad * 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 * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144
*/ */
async function getChangesetInfo(padId, startNum, endNum, granularity) { const getChangesetInfo = async (padId, startNum, endNum, granularity) => {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
const head_revision = pad.getHeadRevisionNumber(); const head_revision = pad.getHeadRevisionNumber();
@ -1237,15 +1257,25 @@ async function getChangesetInfo(padId, startNum, endNum, granularity) {
// get all needed composite Changesets // get all needed composite Changesets
const composedChangesets = {}; const composedChangesets = {};
const p1 = Promise.all(compositesChangesetNeeded.map((item) => composePadChangesets(padId, item.start, item.end).then((changeset) => { const p1 = Promise.all(
compositesChangesetNeeded.map(
(item) => composePadChangesets(
padId, item.start, item.end
).then(
(changeset) => {
composedChangesets[`${item.start}/${item.end}`] = changeset; composedChangesets[`${item.start}/${item.end}`] = changeset;
}))); }
)
)
);
// get all needed revision Dates // get all needed revision Dates
const revisionDate = []; const revisionDate = [];
const p2 = Promise.all(revTimesNeeded.map((revNum) => pad.getRevisionDate(revNum).then((revDate) => { const p2 = Promise.all(revTimesNeeded.map((revNum) => pad.getRevisionDate(revNum)
.then((revDate) => {
revisionDate[revNum] = Math.floor(revDate / 1000); revisionDate[revNum] = Math.floor(revDate / 1000);
}))); })
));
// get the lines // get the lines
let lines; let lines;
@ -1288,13 +1318,13 @@ async function getChangesetInfo(padId, startNum, endNum, granularity) {
return {forwardsChangesets, backwardsChangesets, return {forwardsChangesets, backwardsChangesets,
apool: apool.toJsonable(), actualEndNum: endNum, apool: apool.toJsonable(), actualEndNum: endNum,
timeDeltas, start: startNum, granularity}; timeDeltas, start: startNum, granularity};
} };
/** /**
* Tries to rebuild the getPadLines function of the original Etherpad * Tries to rebuild the getPadLines function of the original Etherpad
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263 * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263
*/ */
async function getPadLines(padId, revNum) { const getPadLines = async (padId, revNum) => {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
// get the atext // get the atext
@ -1310,13 +1340,13 @@ async function getPadLines(padId, revNum) {
textlines: Changeset.splitTextLines(atext.text), textlines: Changeset.splitTextLines(atext.text),
alines: Changeset.splitAttributionLines(atext.attribs, atext.text), alines: Changeset.splitAttributionLines(atext.attribs, atext.text),
}; };
} };
/** /**
* Tries to rebuild the composePadChangeset function of the original Etherpad * Tries to rebuild the composePadChangeset function of the original Etherpad
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241 * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241
*/ */
async function composePadChangesets(padId, startNum, endNum) { const composePadChangesets = async (padId, startNum, endNum) => {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
// fetch all changesets we need // fetch all changesets we need
@ -1333,7 +1363,9 @@ async function composePadChangesets(padId, startNum, endNum) {
// get all changesets // get all changesets
const changesets = {}; const changesets = {};
await Promise.all(changesetsNeeded.map((revNum) => pad.getRevisionChangeset(revNum).then((changeset) => changesets[revNum] = changeset))); await Promise.all(changesetsNeeded.map(
(revNum) => pad.getRevisionChangeset(revNum).then((changeset) => changesets[revNum] = changeset)
));
// compose Changesets // compose Changesets
let r; let r;
@ -1351,9 +1383,9 @@ async function composePadChangesets(padId, startNum, endNum) {
console.warn('failed to compose cs in pad:', padId, ' startrev:', startNum, ' current rev:', r); console.warn('failed to compose cs in pad:', padId, ' startrev:', startNum, ' current rev:', r);
throw e; throw e;
} }
} };
function _getRoomSockets(padID) { const _getRoomSockets = (padID) => {
const roomSockets = []; const roomSockets = [];
const room = socketio.sockets.adapter.rooms[padID]; const room = socketio.sockets.adapter.rooms[padID];
@ -1364,21 +1396,19 @@ function _getRoomSockets(padID) {
} }
return roomSockets; return roomSockets;
} };
/** /**
* Get the number of users in a pad * Get the number of users in a pad
*/ */
exports.padUsersCount = function (padID) { exports.padUsersCount = (padID) => ({
return {
padUsersCount: _getRoomSockets(padID).length, padUsersCount: _getRoomSockets(padID).length,
}; });
};
/** /**
* Get the list of users in a pad * Get the list of users in a pad
*/ */
exports.padUsers = async function (padID) { exports.padUsers = async (padID) => {
const padUsers = []; const padUsers = [];
// iterate over all clients (in parallel) // iterate over all clients (in parallel)

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* This is the Socket.IO Router. It routes the Messages between the * This is the Socket.IO Router. It routes the Messages between the
* components of the Server. The components are at the moment: pad and timeslider * components of the Server. The components are at the moment: pad and timeslider
@ -21,9 +22,6 @@
const log4js = require('log4js'); const log4js = require('log4js');
const messageLogger = log4js.getLogger('message'); const messageLogger = log4js.getLogger('message');
const securityManager = require('../db/SecurityManager');
const readOnlyManager = require('../db/ReadOnlyManager');
const settings = require('../utils/Settings');
/** /**
* Saves all components * Saves all components
@ -37,7 +35,7 @@ let socket;
/** /**
* adds a component * adds a component
*/ */
exports.addComponent = function (moduleName, module) { exports.addComponent = (moduleName, module) => {
// save the component // save the component
components[moduleName] = module; components[moduleName] = module;
@ -48,14 +46,14 @@ exports.addComponent = function (moduleName, module) {
/** /**
* sets the socket.io and adds event functions for routing * sets the socket.io and adds event functions for routing
*/ */
exports.setSocketIO = function (_socket) { exports.setSocketIO = (_socket) => {
// save this socket internaly // save this socket internaly
socket = _socket; socket = _socket;
socket.sockets.on('connection', (client) => { socket.sockets.on('connection', (client) => {
// wrap the original send function to log the messages // wrap the original send function to log the messages
client._send = client.send; client._send = client.send;
client.send = function (message) { client.send = (message) => {
messageLogger.debug(`to ${client.id}: ${JSON.stringify(message)}`); messageLogger.debug(`to ${client.id}: ${JSON.stringify(message)}`);
client._send(message); client._send(message);
}; };
@ -66,7 +64,7 @@ exports.setSocketIO = function (_socket) {
} }
client.on('message', async (message) => { client.on('message', async (message) => {
if (message.protocolVersion && message.protocolVersion != 2) { if (message.protocolVersion && message.protocolVersion !== 2) {
messageLogger.warn(`Protocolversion header is not correct: ${JSON.stringify(message)}`); messageLogger.warn(`Protocolversion header is not correct: ${JSON.stringify(message)}`);
return; return;
} }

View file

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

View file

@ -4,7 +4,6 @@ const eejs = require('../../eejs');
const settings = require('../../utils/Settings'); const settings = require('../../utils/Settings');
const installer = require('../../../static/js/pluginfw/installer'); const installer = require('../../../static/js/pluginfw/installer');
const plugins = require('../../../static/js/pluginfw/plugin_defs'); const plugins = require('../../../static/js/pluginfw/plugin_defs');
const _ = require('underscore');
const semver = require('semver'); const semver = require('semver');
const UpdateCheck = require('../../utils/UpdateCheck'); const UpdateCheck = require('../../utils/UpdateCheck');
@ -51,7 +50,7 @@ exports.socketio = (hookName, args, cb) => {
try { try {
const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ 60 * 10); const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ 60 * 10);
const updatable = _(plugins.plugins).keys().filter((plugin) => { const updatable = Object.keys(plugins.plugins).filter((plugin) => {
if (!results[plugin]) return false; if (!results[plugin]) return false;
const latestVersion = results[plugin].version; const latestVersion = results[plugin].version;

View file

@ -1,9 +1,11 @@
'use strict';
const log4js = require('log4js'); const log4js = require('log4js');
const clientLogger = log4js.getLogger('client'); const clientLogger = log4js.getLogger('client');
const formidable = require('formidable'); const formidable = require('formidable');
const apiHandler = require('../../handler/APIHandler'); const apiHandler = require('../../handler/APIHandler');
exports.expressCreateServer = function (hook_name, args, cb) { exports.expressCreateServer = (hookName, args, cb) => {
// The Etherpad client side sends information about how a disconnect happened // The Etherpad client side sends information about how a disconnect happened
args.app.post('/ep/pad/connection-diagnostic-info', (req, res) => { args.app.post('/ep/pad/connection-diagnostic-info', (req, res) => {
new formidable.IncomingForm().parse(req, (err, fields, files) => { new formidable.IncomingForm().parse(req, (err, fields, files) => {
@ -15,8 +17,9 @@ exports.expressCreateServer = function (hook_name, args, cb) {
// The Etherpad client side sends information about client side javscript errors // The Etherpad client side sends information about client side javscript errors
args.app.post('/jserror', (req, res) => { args.app.post('/jserror', (req, res) => {
new formidable.IncomingForm().parse(req, (err, fields, files) => { new formidable.IncomingForm().parse(req, (err, fields, files) => {
let data;
try { try {
var data = JSON.parse(fields.errorInfo); data = JSON.parse(fields.errorInfo);
} catch (e) { } catch (e) {
return res.end(); return res.end();
} }

View file

@ -1,6 +1,8 @@
const stats = require('ep_etherpad-lite/node/stats'); 'use strict';
exports.expressCreateServer = function (hook_name, args, cb) { const stats = require('../../stats');
exports.expressCreateServer = (hook_name, args, cb) => {
exports.app = args.app; exports.app = args.app;
// Handle errors // Handle errors

View file

@ -1,39 +1,43 @@
const assert = require('assert').strict; 'use strict';
const hasPadAccess = require('../../padaccess'); const hasPadAccess = require('../../padaccess');
const settings = require('../../utils/Settings'); const settings = require('../../utils/Settings');
const exportHandler = require('../../handler/ExportHandler'); const exportHandler = require('../../handler/ExportHandler');
const importHandler = require('../../handler/ImportHandler'); const importHandler = require('../../handler/ImportHandler');
const padManager = require('../../db/PadManager'); const padManager = require('../../db/PadManager');
const readOnlyManager = require('../../db/ReadOnlyManager'); const readOnlyManager = require('../../db/ReadOnlyManager');
const authorManager = require('../../db/AuthorManager');
const rateLimit = require('express-rate-limit'); const rateLimit = require('express-rate-limit');
const securityManager = require('../../db/SecurityManager'); const securityManager = require('../../db/SecurityManager');
const webaccess = require('./webaccess'); const webaccess = require('./webaccess');
settings.importExportRateLimiting.onLimitReached = function (req, res, options) { settings.importExportRateLimiting.onLimitReached = (req, res, options) => {
// when the rate limiter triggers, write a warning in the logs // 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}`); console.warn('Import/Export rate limiter triggered on ' +
`"${req.originalUrl}" for IP address ${req.ip}`);
}; };
const limiter = rateLimit(settings.importExportRateLimiting); const limiter = rateLimit(settings.importExportRateLimiting);
exports.expressCreateServer = function (hook_name, args, cb) { exports.expressCreateServer = (hookName, args, cb) => {
// handle export requests // handle export requests
args.app.use('/p/:pad/:rev?/export/:type', limiter); args.app.use('/p/:pad/:rev?/export/:type', limiter);
args.app.get('/p/:pad/:rev?/export/:type', async (req, res, next) => { args.app.get('/p/:pad/:rev?/export/:type', async (req, res, next) => {
const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad']; const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad'];
// send a 404 if we don't support this filetype // send a 404 if we don't support this filetype
if (types.indexOf(req.params.type) == -1) { if (types.indexOf(req.params.type) === -1) {
return next(); return next();
} }
// if abiword is disabled, and this is a format we only support with abiword, output a message // if abiword is disabled, and this is a format we only support with abiword, output a message
if (settings.exportAvailable() == 'no' && if (settings.exportAvailable() === 'no' &&
['odt', 'pdf', 'doc'].indexOf(req.params.type) !== -1) { ['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`); console.error(`Impossible to export pad "${req.params.pad}" in ${req.params.type} format.` +
' There is no converter configured');
// ACHTUNG: do not include req.params.type in res.send() because there is no HTML escaping and it would lead to an XSS // ACHTUNG: do not include req.params.type in res.send() because there is
res.send('This export is not enabled at this Etherpad instance. Set the path to Abiword or soffice (LibreOffice) in settings.json to enable this feature'); // no HTML escaping and it would lead to an XSS
res.send('This export is not enabled at this Etherpad instance. Set the path to Abiword' +
' or soffice (LibreOffice) in settings.json to enable this feature');
return; return;
} }

View file

@ -1,3 +1,5 @@
'use strict';
const RESERVED_WORDS = [ const RESERVED_WORDS = [
'abstract', 'abstract',
'arguments', 'arguments',
@ -65,9 +67,9 @@ const RESERVED_WORDS = [
'yield', 'yield',
]; ];
const regex = /^[a-zA-Z_$][0-9a-zA-Z_$]*(?:\[(?:".+"|\'.+\'|\d+)\])*?$/; const regex = /^[a-zA-Z_$][0-9a-zA-Z_$]*(?:\[(?:".+"|'.+'|\d+)\])*?$/;
module.exports.check = function (inputStr) { module.exports.check = (inputStr) => {
let isValid = true; let isValid = true;
inputStr.split('.').forEach((part) => { inputStr.split('.').forEach((part) => {
if (!regex.test(part)) { if (!regex.test(part)) {

View file

@ -1,3 +1,5 @@
'use strict';
/** /**
* node/hooks/express/openapi.js * node/hooks/express/openapi.js
* *
@ -31,7 +33,9 @@ const OPENAPI_VERSION = '3.0.2'; // Swagger/OAS version
const info = { const info = {
title: 'Etherpad API', title: 'Etherpad API',
description: description:
'Etherpad is a real-time collaborative editor scalable to thousands of simultaneous real time users. It provides full data export capabilities, and runs on your server, under your control.', 'Etherpad is a real-time collaborative editor scalable to thousands of simultaneous ' +
'real time users. It provides full data export capabilities, and runs on your server, ' +
'under your control.',
termsOfService: 'https://etherpad.org/', termsOfService: 'https://etherpad.org/',
contact: { contact: {
name: 'The Etherpad Foundation', name: 'The Etherpad Foundation',
@ -80,7 +84,9 @@ const resources = {
listSessions: { listSessions: {
operationId: 'listSessionsOfGroup', operationId: 'listSessionsOfGroup',
summary: '', summary: '',
responseSchema: {sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}}}, responseSchema: {
sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}},
},
}, },
list: { list: {
operationId: 'listAllGroups', operationId: 'listAllGroups',
@ -109,7 +115,9 @@ const resources = {
listSessions: { listSessions: {
operationId: 'listSessionsOfAuthor', operationId: 'listSessionsOfAuthor',
summary: 'returns all sessions of an author', summary: 'returns all sessions of an author',
responseSchema: {sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}}}, responseSchema: {
sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}},
},
}, },
// We need an operation that return a UserInfo so it can be picked up by the codegen :( // We need an operation that return a UserInfo so it can be picked up by the codegen :(
getName: { getName: {
@ -153,7 +161,8 @@ const resources = {
create: { create: {
operationId: 'createPad', operationId: 'createPad',
description: description:
'creates a new (non-group) pad. Note that if you need to create a group Pad, you should call createGroupPad', 'creates a new (non-group) pad. Note that if you need to create a group Pad, ' +
'you should call createGroupPad',
}, },
getText: { getText: {
operationId: 'getText', operationId: 'getText',
@ -382,9 +391,9 @@ const defaultResponseRefs = {
// convert to a dictionary of operation objects // convert to a dictionary of operation objects
const operations = {}; const operations = {};
for (const resource in resources) { for (const [resource, actions] of Object.entries(resources)) {
for (const action in resources[resource]) { for (const [action, spec] of Object.entries(actions)) {
const {operationId, responseSchema, ...operation} = resources[resource][action]; const {operationId, responseSchema, ...operation} = spec;
// add response objects // add response objects
const responses = {...defaultResponseRefs}; const responses = {...defaultResponseRefs};
@ -607,14 +616,14 @@ exports.expressCreateServer = (hookName, args, cb) => {
if (createHTTPError.isHttpError(err)) { if (createHTTPError.isHttpError(err)) {
// pass http errors thrown by handler forward // pass http errors thrown by handler forward
throw err; throw err;
} else if (err.name == 'apierror') { } else if (err.name === 'apierror') {
// parameters were wrong and the api stopped execution, pass the error // parameters were wrong and the api stopped execution, pass the error
// convert to http error // convert to http error
throw new createHTTPError.BadRequest(err.message); throw new createHTTPError.BadRequest(err.message);
} else { } else {
// an unknown error happened // an unknown error happened
// log it and throw internal error // log it and throw internal error
apiLogger.error(err); apiLogger.error(err.stack || err.toString());
throw new createHTTPError.InternalError('internal error'); throw new createHTTPError.InternalError('internal error');
} }
}); });

View file

@ -1,8 +1,10 @@
'use strict';
const readOnlyManager = require('../../db/ReadOnlyManager'); const readOnlyManager = require('../../db/ReadOnlyManager');
const hasPadAccess = require('../../padaccess'); const hasPadAccess = require('../../padaccess');
const exporthtml = require('../../utils/ExportHtml'); const exporthtml = require('../../utils/ExportHtml');
exports.expressCreateServer = function (hook_name, args, cb) { exports.expressCreateServer = (hookName, args, cb) => {
// serve read only pad // serve read only pad
args.app.get('/ro/:id', async (req, res) => { args.app.get('/ro/:id', async (req, res) => {
// translate the read only pad to a padId // translate the read only pad to a padId

View file

@ -1,7 +1,9 @@
'use strict';
const padManager = require('../../db/PadManager'); const padManager = require('../../db/PadManager');
const url = require('url'); const url = require('url');
exports.expressCreateServer = function (hook_name, args, cb) { exports.expressCreateServer = (hookName, args, cb) => {
// redirects browser to the pad's sanitized url if needed. otherwise, renders the html // redirects browser to the pad's sanitized url if needed. otherwise, renders the html
args.app.param('pad', async (req, res, next, padId) => { args.app.param('pad', async (req, res, next, padId) => {
// ensure the padname is valid and the url doesn't end with a / // ensure the padname is valid and the url doesn't end with a /
@ -17,12 +19,12 @@ exports.expressCreateServer = function (hook_name, args, cb) {
next(); next();
} else { } else {
// the pad id was sanitized, so we redirect to the sanitized version // the pad id was sanitized, so we redirect to the sanitized version
let real_url = sanitizedPadId; let realURL = sanitizedPadId;
real_url = encodeURIComponent(real_url); realURL = encodeURIComponent(realURL);
const query = url.parse(req.url).query; const query = url.parse(req.url).query;
if (query) real_url += `?${query}`; if (query) realURL += `?${query}`;
res.header('Location', real_url); res.header('Location', realURL);
res.status(302).send(`You should be redirected to <a href="${real_url}">${real_url}</a>`); res.status(302).send(`You should be redirected to <a href="${realURL}">${realURL}</a>`);
} }
}); });
return cb(); return cb();

View file

@ -1,14 +1,16 @@
'use strict';
const path = require('path'); const path = require('path');
const eejs = require('ep_etherpad-lite/node/eejs'); const eejs = require('../../eejs');
const toolbar = require('ep_etherpad-lite/node/utils/toolbar'); const toolbar = require('../../utils/toolbar');
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); const hooks = require('../../../static/js/pluginfw/hooks');
const settings = require('../../utils/Settings'); const settings = require('../../utils/Settings');
const webaccess = require('./webaccess'); const webaccess = require('./webaccess');
exports.expressCreateServer = function (hook_name, args, cb) { exports.expressCreateServer = (hookName, args, cb) => {
// expose current stats // expose current stats
args.app.get('/stats', (req, res) => { args.app.get('/stats', (req, res) => {
res.json(require('ep_etherpad-lite/node/stats').toJSON()); res.json(require('../../stats').toJSON());
}); });
// serve index.html under / // serve index.html under /
@ -24,7 +26,14 @@ exports.expressCreateServer = function (hook_name, args, cb) {
// serve robots.txt // serve robots.txt
args.app.get('/robots.txt', (req, res) => { args.app.get('/robots.txt', (req, res) => {
let filePath = path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt'); let filePath = path.join(
settings.root,
'src',
'static',
'skins',
settings.skinName,
'robots.txt'
);
res.sendFile(filePath, (err) => { res.sendFile(filePath, (err) => {
// there is no custom robots.txt, send the default robots.txt which dissallows all // there is no custom robots.txt, send the default robots.txt which dissallows all
if (err) { if (err) {
@ -66,7 +75,14 @@ exports.expressCreateServer = function (hook_name, args, cb) {
// serve favicon.ico from all path levels except as a pad name // serve favicon.ico from all path levels except as a pad name
args.app.get(/\/favicon.ico$/, (req, res) => { args.app.get(/\/favicon.ico$/, (req, res) => {
let filePath = path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'); let filePath = path.join(
settings.root,
'src',
'static',
'skins',
settings.skinName,
'favicon.ico'
);
res.sendFile(filePath, (err) => { res.sendFile(filePath, (err) => {
// there is no custom favicon, send the default favicon // there is no custom favicon, send the default favicon

View file

@ -1,11 +1,12 @@
'use strict';
const minify = require('../../utils/Minify'); const minify = require('../../utils/Minify');
const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs'); const plugins = require('../../../static/js/pluginfw/plugin_defs');
const CachingMiddleware = require('../../utils/caching_middleware'); const CachingMiddleware = require('../../utils/caching_middleware');
const settings = require('../../utils/Settings');
const Yajsml = require('etherpad-yajsml'); const Yajsml = require('etherpad-yajsml');
const _ = require('underscore'); const _ = require('underscore');
exports.expressCreateServer = function (hook_name, args, cb) { exports.expressCreateServer = (hookName, args, cb) => {
// Cache both minified and static. // Cache both minified and static.
const assetCache = new CachingMiddleware(); const assetCache = new CachingMiddleware();
args.app.all(/\/javascripts\/(.*)/, assetCache.handle); args.app.all(/\/javascripts\/(.*)/, assetCache.handle);
@ -34,7 +35,8 @@ exports.expressCreateServer = function (hook_name, args, cb) {
args.app.use(jsServer.handle.bind(jsServer)); args.app.use(jsServer.handle.bind(jsServer));
// serve plugin definitions // serve plugin definitions
// not very static, but served here so that client can do require("pluginfw/static/js/plugin-definitions.js"); // not very static, but served here so that client can do
// require("pluginfw/static/js/plugin-definitions.js");
args.app.get('/pluginfw/plugin-definitions.json', (req, res, next) => { args.app.get('/pluginfw/plugin-definitions.json', (req, res, next) => {
const clientParts = _(plugins.parts) const clientParts = _(plugins.parts)
.filter((part) => _(part).has('client_hooks')); .filter((part) => _(part).has('client_hooks'));

View file

@ -1,9 +1,11 @@
'use strict';
const path = require('path'); const path = require('path');
const npm = require('npm'); const npm = require('npm');
const fs = require('fs'); const fs = require('fs');
const util = require('util'); const util = require('util');
exports.expressCreateServer = function (hook_name, args, cb) { exports.expressCreateServer = (hookName, args, cb) => {
args.app.get('/tests/frontend/specs_list.js', async (req, res) => { args.app.get('/tests/frontend/specs_list.js', async (req, res) => {
const [coreTests, pluginTests] = await Promise.all([ const [coreTests, pluginTests] = await Promise.all([
exports.getCoreTests(), exports.getCoreTests(),
@ -24,9 +26,9 @@ exports.expressCreateServer = function (hook_name, args, cb) {
// path.join seems to normalize by default, but we'll just be explicit // path.join seems to normalize by default, but we'll just be explicit
const rootTestFolder = path.normalize(path.join(npm.root, '../tests/frontend/')); const rootTestFolder = path.normalize(path.join(npm.root, '../tests/frontend/'));
const url2FilePath = function (url) { const url2FilePath = (url) => {
let subPath = url.substr('/tests/frontend'.length); let subPath = url.substr('/tests/frontend'.length);
if (subPath == '') { if (subPath === '') {
subPath = 'index.html'; subPath = 'index.html';
} }
subPath = subPath.split('?')[0]; subPath = subPath.split('?')[0];
@ -49,8 +51,9 @@ exports.expressCreateServer = function (hook_name, args, cb) {
content = `describe(${JSON.stringify(specFileName)}, function(){ ${content} });`; content = `describe(${JSON.stringify(specFileName)}, function(){ ${content} });`;
if(!specFilePath.endsWith('index.html')) res.setHeader('content-type', 'application/javascript'); if (!specFilePath.endsWith('index.html')) {
res.setHeader('content-type', 'application/javascript');
}
res.send(content); res.send(content);
}); });
}); });
@ -69,7 +72,7 @@ exports.expressCreateServer = function (hook_name, args, cb) {
const readdir = util.promisify(fs.readdir); const readdir = util.promisify(fs.readdir);
exports.getPluginTests = async function (callback) { exports.getPluginTests = async (callback) => {
const moduleDir = 'node_modules/'; const moduleDir = 'node_modules/';
const specPath = '/static/tests/frontend/specs/'; const specPath = '/static/tests/frontend/specs/';
const staticDir = '/static/plugins/'; const staticDir = '/static/plugins/';
@ -88,7 +91,4 @@ exports.getPluginTests = async function (callback) {
return Promise.all(promises).then(() => pluginSpecs); return Promise.all(promises).then(() => pluginSpecs);
}; };
exports.getCoreTests = function () { exports.getCoreTests = () => readdir('tests/frontend/specs');
// get the core test specs
return readdir('tests/frontend/specs');
};

View file

@ -1,24 +1,23 @@
'use strict';
const languages = require('languages4translatewiki'); const languages = require('languages4translatewiki');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const _ = require('underscore'); const _ = require('underscore');
const npm = require('npm'); const npm = require('npm');
const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs.js').plugins; const plugins = require('../../static/js/pluginfw/plugin_defs.js').plugins;
const semver = require('semver');
const existsSync = require('../utils/path_exists'); const existsSync = require('../utils/path_exists');
const settings = require('../utils/Settings') const settings = require('../utils/Settings');
;
// returns all existing messages merged together and grouped by langcode // returns all existing messages merged together and grouped by langcode
// {es: {"foo": "string"}, en:...} // {es: {"foo": "string"}, en:...}
function getAllLocales() { const getAllLocales = () => {
const locales2paths = {}; const locales2paths = {};
// Puts the paths of all locale files contained in a given directory // Puts the paths of all locale files contained in a given directory
// into `locales2paths` (files from various dirs are grouped by lang code) // into `locales2paths` (files from various dirs are grouped by lang code)
// (only json files with valid language code as name) // (only json files with valid language code as name)
function extractLangs(dir) { const extractLangs = (dir) => {
if (!existsSync(dir)) return; if (!existsSync(dir)) return;
let stat = fs.lstatSync(dir); let stat = fs.lstatSync(dir);
if (!stat.isDirectory() || stat.isSymbolicLink()) return; if (!stat.isDirectory() || stat.isSymbolicLink()) return;
@ -31,12 +30,12 @@ function getAllLocales() {
const ext = path.extname(file); const ext = path.extname(file);
const locale = path.basename(file, ext).toLowerCase(); const locale = path.basename(file, ext).toLowerCase();
if ((ext == '.json') && languages.isValid(locale)) { if ((ext === '.json') && languages.isValid(locale)) {
if (!locales2paths[locale]) locales2paths[locale] = []; if (!locales2paths[locale]) locales2paths[locale] = [];
locales2paths[locale].push(file); locales2paths[locale].push(file);
} }
}); });
} };
// add core supported languages first // add core supported languages first
extractLangs(`${npm.root}/ep_etherpad-lite/locales`); extractLangs(`${npm.root}/ep_etherpad-lite/locales`);
@ -78,29 +77,29 @@ function getAllLocales() {
} }
return locales; return locales;
} };
// returns a hash of all available languages availables with nativeName and direction // returns a hash of all available languages availables with nativeName and direction
// e.g. { es: {nativeName: "español", direction: "ltr"}, ... } // e.g. { es: {nativeName: "español", direction: "ltr"}, ... }
function getAvailableLangs(locales) { const getAvailableLangs = (locales) => {
const result = {}; const result = {};
_.each(_.keys(locales), (langcode) => { _.each(_.keys(locales), (langcode) => {
result[langcode] = languages.getLanguageInfo(langcode); result[langcode] = languages.getLanguageInfo(langcode);
}); });
return result; return result;
} };
// returns locale index that will be served in /locales.json // returns locale index that will be served in /locales.json
const generateLocaleIndex = function (locales) { const generateLocaleIndex = (locales) => {
const result = _.clone(locales); // keep English strings const result = _.clone(locales); // keep English strings
_.each(_.keys(locales), (langcode) => { _.each(_.keys(locales), (langcode) => {
if (langcode != 'en') result[langcode] = `locales/${langcode}.json`; if (langcode !== 'en') result[langcode] = `locales/${langcode}.json`;
}); });
return JSON.stringify(result); return JSON.stringify(result);
}; };
exports.expressCreateServer = function (n, args, cb) { exports.expressCreateServer = (n, args, cb) => {
// regenerate locales on server restart // regenerate locales on server restart
const locales = getAllLocales(); const locales = getAllLocales();
const localeIndex = generateLocaleIndex(locales); const localeIndex = generateLocaleIndex(locales);

View file

@ -1,3 +1,4 @@
'use strict';
const securityManager = require('./db/SecurityManager'); const securityManager = require('./db/SecurityManager');
// checks for padAccess // checks for padAccess

View file

@ -27,13 +27,17 @@
const log4js = require('log4js'); const log4js = require('log4js');
log4js.replaceConsole(); log4js.replaceConsole();
// wtfnode should be loaded after log4js.replaceConsole() so that it uses log4js for logging, and it
// should be above everything else so that it can hook in before resources are used.
const wtfnode = require('wtfnode');
/* /*
* early check for version compatibility before calling * early check for version compatibility before calling
* any modules that require newer versions of NodeJS * any modules that require newer versions of NodeJS
*/ */
const NodeVersion = require('./utils/NodeVersion'); const NodeVersion = require('./utils/NodeVersion');
NodeVersion.enforceMinNodeVersion('10.13.0'); NodeVersion.enforceMinNodeVersion('10.17.0');
NodeVersion.checkDeprecationStatus('10.13.0', '1.8.3'); NodeVersion.checkDeprecationStatus('10.17.0', '1.8.8');
const UpdateCheck = require('./utils/UpdateCheck'); const UpdateCheck = require('./utils/UpdateCheck');
const db = require('./db/DB'); const db = require('./db/DB');
@ -44,13 +48,45 @@ const plugins = require('../static/js/pluginfw/plugins');
const settings = require('./utils/Settings'); const settings = require('./utils/Settings');
const util = require('util'); const util = require('util');
let started = false; const State = {
let stopped = false; INITIAL: 1,
STARTING: 2,
RUNNING: 3,
STOPPING: 4,
STOPPED: 5,
EXITING: 6,
WAITING_FOR_EXIT: 7,
};
let state = State.INITIAL;
const removeSignalListener = (signal, listener) => {
console.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')}`);
process.off(signal, listener);
};
const runningCallbacks = [];
exports.start = async () => { exports.start = async () => {
if (started) return express.server; switch (state) {
started = true; case State.INITIAL:
if (stopped) throw new Error('restart not supported'); break;
case State.STARTING:
await new Promise((resolve) => runningCallbacks.push(resolve));
// fall through
case State.RUNNING:
return express.server;
case State.STOPPING:
case State.STOPPED:
case State.EXITING:
case State.WAITING_FOR_EXIT:
throw new Error('restart not supported');
default:
throw new Error(`unknown State: ${state.toString()}`);
}
console.log('Starting Etherpad...');
state = State.STARTING;
// Check if Etherpad version is up-to-date // Check if Etherpad version is up-to-date
UpdateCheck.check(); UpdateCheck.check();
@ -60,9 +96,28 @@ exports.start = async () => {
stats.gauge('memoryUsage', () => process.memoryUsage().rss); stats.gauge('memoryUsage', () => process.memoryUsage().rss);
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed); stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
await util.promisify(npm.load)(); process.on('uncaughtException', exports.exit);
// 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) => { throw err; });
try { for (const signal of ['SIGINT', 'SIGTERM']) {
// Forcibly remove other signal listeners to prevent them from terminating node before we are
// 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)) {
removeSignalListener(signal, listener);
}
process.on(signal, exports.exit);
// Prevent signal listeners from being added in the future.
process.on('newListener', (event, listener) => {
if (event !== signal) return;
removeSignalListener(signal, listener);
});
}
await util.promisify(npm.load)();
await db.init(); await db.init();
await plugins.update(); await plugins.update();
console.info(`Installed plugins: ${plugins.formatPluginsWithVersion()}`); console.info(`Installed plugins: ${plugins.formatPluginsWithVersion()}`);
@ -70,67 +125,103 @@ exports.start = async () => {
console.debug(`Installed hooks:\n${plugins.formatHooks()}`); console.debug(`Installed hooks:\n${plugins.formatHooks()}`);
await hooks.aCallAll('loadSettings', {settings}); await hooks.aCallAll('loadSettings', {settings});
await hooks.aCallAll('createServer'); await hooks.aCallAll('createServer');
} catch (e) {
console.error(`exception thrown: ${e.message}`);
if (e.stack) console.log(e.stack);
process.exit(1);
}
process.on('uncaughtException', exports.exit); console.log('Etherpad is running');
state = State.RUNNING;
/* while (runningCallbacks.length > 0) setImmediate(runningCallbacks.pop());
* Connect graceful shutdown with sigint and uncaught exception
*
* Until Etherpad 1.7.5, process.on('SIGTERM') and process.on('SIGINT') were
* not hooked up under Windows, because old nodejs versions did not support
* them.
*
* According to nodejs 6.x documentation, it is now safe to do so. This
* allows to gracefully close the DB connection when hitting CTRL+C under
* Windows, for example.
*
* Source: https://nodejs.org/docs/latest-v6.x/api/process.html#process_signal_events
*
* - SIGTERM is not supported on Windows, it can be listened on.
* - SIGINT from the terminal is supported on all platforms, and can usually
* be generated with <Ctrl>+C (though this may be configurable). It is not
* generated when terminal raw mode is enabled.
*/
process.on('SIGINT', exports.exit);
// When running as PID1 (e.g. in docker container) allow graceful shutdown on SIGTERM c.f. #3265.
// Pass undefined to exports.exit because this is not an abnormal termination.
process.on('SIGTERM', () => exports.exit());
// Return the HTTP server to make it easier to write tests. // Return the HTTP server to make it easier to write tests.
return express.server; return express.server;
}; };
const stoppedCallbacks = [];
exports.stop = async () => { exports.stop = async () => {
if (stopped) return; switch (state) {
stopped = true; case State.STARTING:
await exports.start();
// Don't fall through to State.RUNNING in case another caller is also waiting for startup.
return await exports.stop();
case State.RUNNING:
break;
case State.STOPPING:
await new Promise((resolve) => stoppedCallbacks.push(resolve));
// fall through
case State.INITIAL:
case State.STOPPED:
case State.EXITING:
case State.WAITING_FOR_EXIT:
return;
default:
throw new Error(`unknown State: ${state.toString()}`);
}
console.log('Stopping Etherpad...'); console.log('Stopping Etherpad...');
await new Promise(async (resolve, reject) => { state = State.STOPPING;
const id = setTimeout(() => reject(new Error('Timed out waiting for shutdown tasks')), 3000); let timeout = null;
await hooks.aCallAll('shutdown'); await Promise.race([
clearTimeout(id); hooks.aCallAll('shutdown'),
resolve(); new Promise((resolve, reject) => {
}); timeout = setTimeout(() => reject(new Error('Timed out waiting for shutdown tasks')), 3000);
}),
]);
clearTimeout(timeout);
console.log('Etherpad stopped');
state = State.STOPPED;
while (stoppedCallbacks.length > 0) setImmediate(stoppedCallbacks.pop());
}; };
exports.exit = async (err) => { const exitCallbacks = [];
let exitCode = 0; let exitCalled = false;
if (err) { exports.exit = async (err = null) => {
exitCode = 1; /* eslint-disable no-process-exit */
console.error(err.stack ? err.stack : err); if (err === 'SIGTERM') {
// Termination from SIGTERM is not treated as an abnormal termination.
console.log('Received SIGTERM signal');
err = null;
} else if (err != null) {
console.error(err.stack || err.toString());
process.exitCode = 1;
if (exitCalled) {
console.error('Error occurred while waiting to exit. Forcing an immediate unclean exit...');
process.exit(1);
} }
try { }
exitCalled = true;
switch (state) {
case State.STARTING:
case State.RUNNING:
case State.STOPPING:
await exports.stop(); await exports.stop();
} catch (err) { // Don't fall through to State.STOPPED in case another caller is also waiting for stop().
exitCode = 1; // Don't pass err to exports.exit() because this err has already been processed. (If err is
console.error(err.stack ? err.stack : err); // passed again to exit() then exit() will think that a second error occurred while exiting.)
return await exports.exit();
case State.INITIAL:
case State.STOPPED:
break;
case State.EXITING:
await new Promise((resolve) => exitCallbacks.push(resolve));
// fall through
case State.WAITING_FOR_EXIT:
return;
default:
throw new Error(`unknown State: ${state.toString()}`);
} }
process.exit(exitCode); console.log('Exiting...');
state = State.EXITING;
while (exitCallbacks.length > 0) setImmediate(exitCallbacks.pop());
// Node.js should exit on its own without further action. Add a timeout to force Node.js to exit
// just in case something failed to get cleaned up during the shutdown hook. unref() is called on
// the timeout so that the timeout itself does not prevent Node.js from exiting.
setTimeout(() => {
console.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');
wtfnode.dump();
console.error('Forcing an unclean exit...');
process.exit(1);
}, 5000).unref();
console.log('Waiting for Node.js to exit...');
state = State.WAITING_FOR_EXIT;
/* eslint-enable no-process-exit */
}; };
if (require.main === module) exports.start(); if (require.main === module) exports.start();

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* Controls the communication with the Abiword application * Controls the communication with the Abiword application
*/ */
@ -25,11 +26,12 @@ const os = require('os');
let doConvertTask; let doConvertTask;
// on windows we have to spawn a process for each convertion, cause the plugin abicommand doesn't exist on this platform // 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) { if (os.type().indexOf('Windows') > -1) {
let stdoutBuffer = ''; let stdoutBuffer = '';
doConvertTask = function (task, callback) { doConvertTask = (task, callback) => {
// span an abiword process to perform the conversion // span an abiword process to perform the conversion
const abiword = spawn(settings.abiword, [`--to=${task.destFile}`, task.srcFile]); const abiword = spawn(settings.abiword, [`--to=${task.destFile}`, task.srcFile]);
@ -46,11 +48,11 @@ if (os.type().indexOf('Windows') > -1) {
// throw exceptions if abiword is dieing // throw exceptions if abiword is dieing
abiword.on('exit', (code) => { abiword.on('exit', (code) => {
if (code != 0) { if (code !== 0) {
return callback(`Abiword died with exit code ${code}`); return callback(`Abiword died with exit code ${code}`);
} }
if (stdoutBuffer != '') { if (stdoutBuffer !== '') {
console.log(stdoutBuffer); console.log(stdoutBuffer);
} }
@ -58,17 +60,17 @@ if (os.type().indexOf('Windows') > -1) {
}); });
}; };
exports.convertFile = function (srcFile, destFile, type, callback) { exports.convertFile = (srcFile, destFile, type, callback) => {
doConvertTask({srcFile, destFile, type}, callback); doConvertTask({srcFile, destFile, type}, callback);
}; };
} // on unix operating systems, we can start abiword with abicommand and
// on unix operating systems, we can start abiword with abicommand and communicate with it via stdin/stdout // communicate with it via stdin/stdout
// thats much faster, about factor 10 // thats much faster, about factor 10
else { } else {
// spawn the abiword process // spawn the abiword process
let abiword; let abiword;
let stdoutCallback = null; let stdoutCallback = null;
var spawnAbiword = function () { const spawnAbiword = () => {
abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']); abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']);
let stdoutBuffer = ''; let stdoutBuffer = '';
let firstPrompt = true; let firstPrompt = true;
@ -90,9 +92,9 @@ else {
stdoutBuffer += data.toString(); stdoutBuffer += data.toString();
// we're searching for the prompt, cause this means everything we need is in the buffer // we're searching for the prompt, cause this means everything we need is in the buffer
if (stdoutBuffer.search('AbiWord:>') != -1) { if (stdoutBuffer.search('AbiWord:>') !== -1) {
// filter the feedback message // filter the feedback message
const err = stdoutBuffer.search('OK') != -1 ? null : stdoutBuffer; const err = stdoutBuffer.search('OK') !== -1 ? null : stdoutBuffer;
// reset the buffer // reset the buffer
stdoutBuffer = ''; stdoutBuffer = '';
@ -110,10 +112,10 @@ else {
}; };
spawnAbiword(); spawnAbiword();
doConvertTask = function (task, callback) { doConvertTask = (task, callback) => {
abiword.stdin.write(`convert ${task.srcFile} ${task.destFile} ${task.type}\n`); abiword.stdin.write(`convert ${task.srcFile} ${task.destFile} ${task.type}\n`);
// create a callback that calls the task callback and the caller callback // create a callback that calls the task callback and the caller callback
stdoutCallback = function (err) { stdoutCallback = (err) => {
callback(); callback();
console.log('queue continue'); console.log('queue continue');
try { try {
@ -126,7 +128,7 @@ else {
// Queue with the converts we have to do // Queue with the converts we have to do
const queue = async.queue(doConvertTask, 1); const queue = async.queue(doConvertTask, 1);
exports.convertFile = function (srcFile, destFile, type, callback) { exports.convertFile = (srcFile, destFile, type, callback) => {
queue.push({srcFile, destFile, type, callback}); queue.push({srcFile, destFile, type, callback});
}; };
} }

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* Library for deterministic relative filename expansion for Etherpad. * Library for deterministic relative filename expansion for Etherpad.
*/ */
@ -40,7 +41,7 @@ let etherpadRoot = null;
* @return {string[]|boolean} The shortened array, or false if there was no * @return {string[]|boolean} The shortened array, or false if there was no
* overlap. * overlap.
*/ */
const popIfEndsWith = function (stringArray, lastDesiredElements) { const popIfEndsWith = (stringArray, lastDesiredElements) => {
if (stringArray.length <= lastDesiredElements.length) { if (stringArray.length <= lastDesiredElements.length) {
absPathLogger.debug(`In order to pop "${lastDesiredElements.join(path.sep)}" from "${stringArray.join(path.sep)}", it should contain at least ${lastDesiredElements.length + 1} elements`); absPathLogger.debug(`In order to pop "${lastDesiredElements.join(path.sep)}" from "${stringArray.join(path.sep)}", it should contain at least ${lastDesiredElements.length + 1} elements`);
@ -72,8 +73,8 @@ const popIfEndsWith = function (stringArray, lastDesiredElements) {
* @return {string} The identified absolute base path. If such path cannot be * @return {string} The identified absolute base path. If such path cannot be
* identified, prints a log and exits the application. * identified, prints a log and exits the application.
*/ */
exports.findEtherpadRoot = function () { exports.findEtherpadRoot = () => {
if (etherpadRoot !== null) { if (etherpadRoot != null) {
return etherpadRoot; return etherpadRoot;
} }
@ -126,7 +127,7 @@ exports.findEtherpadRoot = function () {
* it is returned unchanged. Otherwise it is interpreted * it is returned unchanged. Otherwise it is interpreted
* relative to exports.root. * relative to exports.root.
*/ */
exports.makeAbsolute = function (somePath) { exports.makeAbsolute = (somePath) => {
if (path.isAbsolute(somePath)) { if (path.isAbsolute(somePath)) {
return somePath; return somePath;
} }
@ -145,7 +146,7 @@ exports.makeAbsolute = function (somePath) {
* a subdirectory of the base one * a subdirectory of the base one
* @return {boolean} * @return {boolean}
*/ */
exports.isSubdir = function (parent, arbitraryDir) { exports.isSubdir = (parent, arbitraryDir) => {
// modified from: https://stackoverflow.com/questions/37521893/determine-if-a-path-is-subdirectory-of-another-in-node-js#45242825 // modified from: https://stackoverflow.com/questions/37521893/determine-if-a-path-is-subdirectory-of-another-in-node-js#45242825
const relative = path.relative(parent, arbitraryDir); const relative = path.relative(parent, arbitraryDir);
const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* The CLI module handles command line parameters * The CLI module handles command line parameters
*/ */
@ -30,22 +31,22 @@ for (let i = 0; i < argv.length; i++) {
arg = argv[i]; arg = argv[i];
// Override location of settings.json file // Override location of settings.json file
if (prevArg == '--settings' || prevArg == '-s') { if (prevArg === '--settings' || prevArg === '-s') {
exports.argv.settings = arg; exports.argv.settings = arg;
} }
// Override location of credentials.json file // Override location of credentials.json file
if (prevArg == '--credentials') { if (prevArg === '--credentials') {
exports.argv.credentials = arg; exports.argv.credentials = arg;
} }
// Override location of settings.json file // Override location of settings.json file
if (prevArg == '--sessionkey') { if (prevArg === '--sessionkey') {
exports.argv.sessionkey = arg; exports.argv.sessionkey = arg;
} }
// Override location of settings.json file // Override location of settings.json file
if (prevArg == '--apikey') { if (prevArg === '--apikey') {
exports.argv.apikey = arg; exports.argv.apikey = arg;
} }

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* 2014 John McLear (Etherpad Foundation / McLear Ltd) * 2014 John McLear (Etherpad Foundation / McLear Ltd)
* *
@ -16,9 +17,9 @@
const db = require('../db/DB'); const db = require('../db/DB');
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); const hooks = require('../../static/js/pluginfw/hooks');
exports.getPadRaw = async function (padId) { exports.getPadRaw = async (padId) => {
const padKey = `pad:${padId}`; const padKey = `pad:${padId}`;
const padcontent = await db.get(padKey); const padcontent = await db.get(padKey);

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* Helpers for export requests * Helpers for export requests
*/ */
@ -18,9 +19,9 @@
* limitations under the License. * limitations under the License.
*/ */
const Changeset = require('ep_etherpad-lite/static/js/Changeset'); const Changeset = require('../../static/js/Changeset');
exports.getPadPlainText = function (pad, revNum) { exports.getPadPlainText = (pad, revNum) => {
const _analyzeLine = exports._analyzeLine; const _analyzeLine = exports._analyzeLine;
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext); const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext);
const textLines = atext.text.slice(0, -1).split('\n'); const textLines = atext.text.slice(0, -1).split('\n');
@ -43,7 +44,7 @@ exports.getPadPlainText = function (pad, revNum) {
}; };
exports._analyzeLine = function (text, aline, apool) { exports._analyzeLine = (text, aline, apool) => {
const line = {}; const line = {};
// identify list // identify list
@ -81,6 +82,5 @@ exports._analyzeLine = function (text, aline, apool) {
}; };
exports._encodeWhitespace = function (s) { exports._encodeWhitespace =
return s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`); (s) => s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`);
};

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -14,32 +15,29 @@
* limitations under the License. * limitations under the License.
*/ */
const Changeset = require('ep_etherpad-lite/static/js/Changeset'); const Changeset = require('../../static/js/Changeset');
const padManager = require('../db/PadManager'); const padManager = require('../db/PadManager');
const _ = require('underscore'); const _ = require('underscore');
const Security = require('ep_etherpad-lite/static/js/security'); const Security = require('../../static/js/security');
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); const hooks = require('../../static/js/pluginfw/hooks');
const eejs = require('ep_etherpad-lite/node/eejs'); const eejs = require('../eejs');
const _analyzeLine = require('./ExportHelper')._analyzeLine; const _analyzeLine = require('./ExportHelper')._analyzeLine;
const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace; const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace;
const padutils = require('../../static/js/pad_utils').padutils; const padutils = require('../../static/js/pad_utils').padutils;
async function getPadHTML(pad, revNum) { const getPadHTML = async (pad, revNum) => {
let atext = pad.atext; let atext = pad.atext;
// fetch revision atext // fetch revision atext
if (revNum != undefined) { if (revNum !== undefined) {
atext = await pad.getInternalRevisionAText(revNum); atext = await pad.getInternalRevisionAText(revNum);
} }
// convert atext to html // convert atext to html
return await getHTMLFromAtext(pad, atext); return await getHTMLFromAtext(pad, atext);
} };
exports.getPadHTML = getPadHTML; const getHTMLFromAtext = async (pad, atext, authorColors) => {
exports.getHTMLFromAtext = getHTMLFromAtext;
async function getHTMLFromAtext(pad, atext, authorColors) {
const apool = pad.apool(); const apool = pad.apool();
const textLines = atext.text.slice(0, -1).split('\n'); const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
@ -72,9 +70,7 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
const anumMap = {}; const anumMap = {};
let css = ''; let css = '';
const stripDotFromAuthorID = function (id) { const stripDotFromAuthorID = (id) => id.replace(/\./g, '_');
return id.replace(/\./g, '_');
};
if (authorColors) { if (authorColors) {
css += '<style>\n'; css += '<style>\n';
@ -85,15 +81,14 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
// skip non author attributes // skip non author attributes
if (attr[0] === 'author' && attr[1] !== '') { if (attr[0] === 'author' && attr[1] !== '') {
// add to props array // add to props array
var propName = `author${stripDotFromAuthorID(attr[1])}`; const propName = `author${stripDotFromAuthorID(attr[1])}`;
var newLength = props.push(propName); const newLength = props.push(propName);
anumMap[a] = newLength - 1; anumMap[a] = newLength - 1;
css += `.${propName} {background-color: ${authorColors[attr[1]]}}\n`; css += `.${propName} {background-color: ${authorColors[attr[1]]}}\n`;
} else if (attr[0] === 'removed') { } else if (attr[0] === 'removed') {
var propName = 'removed'; const propName = 'removed';
const newLength = props.push(propName);
var newLength = props.push(propName);
anumMap[a] = newLength - 1; anumMap[a] = newLength - 1;
css += '.removed {text-decoration: line-through; ' + css += '.removed {text-decoration: line-through; ' +
@ -122,7 +117,7 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
} }
}); });
function getLineHTML(text, attribs) { const getLineHTML = (text, attribs) => {
// Use order of tags (b/i/u) as order of nesting, for simplicity // Use order of tags (b/i/u) as order of nesting, for simplicity
// and decent nesting. For example, // and decent nesting. For example,
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i> // <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
@ -132,7 +127,7 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
const assem = Changeset.stringAssembler(); const assem = Changeset.stringAssembler();
const openTags = []; const openTags = [];
function getSpanClassFor(i) { const getSpanClassFor = (i) => {
// return if author colors are disabled // return if author colors are disabled
if (!authorColors) return false; if (!authorColors) return false;
@ -153,16 +148,16 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
} }
return false; return false;
} };
// tags added by exportHtmlAdditionalTagsWithData will be exported as <span> with // tags added by exportHtmlAdditionalTagsWithData will be exported as <span> with
// data attributes // data attributes
function isSpanWithData(i) { const isSpanWithData = (i) => {
const property = props[i]; const property = props[i];
return _.isArray(property); return _.isArray(property);
} };
function emitOpenTag(i) { const emitOpenTag = (i) => {
openTags.unshift(i); openTags.unshift(i);
const spanClass = getSpanClassFor(i); const spanClass = getSpanClassFor(i);
@ -175,10 +170,10 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
assem.append(tags[i]); assem.append(tags[i]);
assem.append('>'); assem.append('>');
} }
} };
// this closes an open tag and removes its reference from openTags // this closes an open tag and removes its reference from openTags
function emitCloseTag(i) { const emitCloseTag = (i) => {
openTags.shift(); openTags.shift();
const spanClass = getSpanClassFor(i); const spanClass = getSpanClassFor(i);
const spanWithData = isSpanWithData(i); const spanWithData = isSpanWithData(i);
@ -190,13 +185,13 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
assem.append(tags[i]); assem.append(tags[i]);
assem.append('>'); assem.append('>');
} }
} };
const urls = padutils.findURLs(text); const urls = padutils.findURLs(text);
let idx = 0; let idx = 0;
function processNextChars(numChars) { const processNextChars = (numChars) => {
if (numChars <= 0) { if (numChars <= 0) {
return; return;
} }
@ -208,7 +203,7 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
// based on the attribs used // based on the attribs used
while (iter.hasNext()) { while (iter.hasNext()) {
const o = iter.next(); const o = iter.next();
var usedAttribs = []; const usedAttribs = [];
// mark all attribs as used // mark all attribs as used
Changeset.eachAttribNumber(o.attribs, (a) => { Changeset.eachAttribNumber(o.attribs, (a) => {
@ -218,7 +213,7 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
}); });
let outermostTag = -1; let outermostTag = -1;
// find the outer most open tag that is no longer used // find the outer most open tag that is no longer used
for (var i = openTags.length - 1; i >= 0; i--) { for (let i = openTags.length - 1; i >= 0; i--) {
if (usedAttribs.indexOf(openTags[i]) === -1) { if (usedAttribs.indexOf(openTags[i]) === -1) {
outermostTag = i; outermostTag = i;
break; break;
@ -234,7 +229,7 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
} }
// open all tags that are used but not open // open all tags that are used but not open
for (i = 0; i < usedAttribs.length; i++) { for (let i = 0; i < usedAttribs.length; i++) {
if (openTags.indexOf(usedAttribs[i]) === -1) { if (openTags.indexOf(usedAttribs[i]) === -1) {
emitOpenTag(usedAttribs[i]); emitOpenTag(usedAttribs[i]);
} }
@ -258,14 +253,16 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
while (openTags.length > 0) { while (openTags.length > 0) {
emitCloseTag(openTags[0]); emitCloseTag(openTags[0]);
} }
} // end processNextChars };
// end processNextChars
if (urls) { if (urls) {
urls.forEach((urlData) => { urls.forEach((urlData) => {
const startIndex = urlData[0]; const startIndex = urlData[0];
const url = urlData[1]; const url = urlData[1];
const urlLength = url.length; const urlLength = url.length;
processNextChars(startIndex - idx); processNextChars(startIndex - idx);
// Using rel="noreferrer" stops leaking the URL/location of the exported HTML when clicking links in the document. // Using rel="noreferrer" stops leaking the URL/location of the exported HTML
// when clicking links in the document.
// Not all browsers understand this attribute, but it's part of the HTML5 standard. // Not all browsers understand this attribute, but it's part of the HTML5 standard.
// https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer // https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer
// Additionally, we do rel="noopener" to ensure a higher level of referrer security. // Additionally, we do rel="noopener" to ensure a higher level of referrer security.
@ -280,7 +277,8 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
processNextChars(text.length - idx); processNextChars(text.length - idx);
return _processSpaces(assem.toString()); return _processSpaces(assem.toString());
} // end getLineHTML };
// end getLineHTML
const pieces = [css]; const pieces = [css];
// Need to deal with constraints imposed on HTML lists; can // Need to deal with constraints imposed on HTML lists; can
@ -292,11 +290,11 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
// => keeps track of the parents level of indentation // => keeps track of the parents level of indentation
let openLists = []; let openLists = [];
for (let i = 0; i < textLines.length; i++) { for (let i = 0; i < textLines.length; i++) {
var context; let context;
var line = _analyzeLine(textLines[i], attribLines[i], apool); const line = _analyzeLine(textLines[i], attribLines[i], apool);
const lineContent = getLineHTML(line.text, line.aline); const lineContent = getLineHTML(line.text, line.aline);
if (line.listLevel)// If we are inside a list // If we are inside a list
{ if (line.listLevel) {
context = { context = {
line, line,
lineContent, lineContent,
@ -315,8 +313,11 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
} }
await hooks.aCallAll('getLineHTMLForExport', context); await hooks.aCallAll('getLineHTMLForExport', context);
// To create list parent elements // To create list parent elements
if ((!prevLine || prevLine.listLevel !== line.listLevel) || (prevLine && line.listTypeName !== prevLine.listTypeName)) { if ((!prevLine || prevLine.listLevel !== line.listLevel) ||
const exists = _.find(openLists, (item) => (item.level === line.listLevel && item.type === line.listTypeName)); (prevLine && line.listTypeName !== prevLine.listTypeName)) {
const exists = _.find(openLists, (item) => (
item.level === line.listLevel && item.type === line.listTypeName)
);
if (!exists) { if (!exists) {
let prevLevel = 0; let prevLevel = 0;
if (prevLine && prevLine.listLevel) { if (prevLine && prevLine.listLevel) {
@ -326,23 +327,33 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
prevLevel = 0; prevLevel = 0;
} }
for (var diff = prevLevel; diff < line.listLevel; diff++) { for (let diff = prevLevel; diff < line.listLevel; diff++) {
openLists.push({level: diff, type: line.listTypeName}); openLists.push({level: diff, type: line.listTypeName});
const prevPiece = pieces[pieces.length - 1]; const prevPiece = pieces[pieces.length - 1];
if (prevPiece.indexOf('<ul') === 0 || prevPiece.indexOf('<ol') === 0 || prevPiece.indexOf('</li>') === 0) { if (prevPiece.indexOf('<ul') === 0 ||
prevPiece.indexOf('<ol') === 0 ||
prevPiece.indexOf('</li>') === 0) {
/* /*
uncommenting this breaks nested ols.. uncommenting this breaks nested ols..
if the previous item is NOT a ul, NOT an ol OR closing li then close the list if the previous item is NOT a ul, NOT an ol OR closing li then close the list
so we consider this HTML, I inserted ** where it throws a problem in Example Wrong.. so we consider this HTML,
<ol><li>one</li><li><ol><li>1.1</li><li><ol><li>1.1.1</li></ol></li></ol></li><li>two</li></ol> I inserted ** where it throws a problem in Example Wrong..
<ol><li>one</li><li><ol><li>1.1</li><li><ol><li>1.1.1</li></ol></li></ol>
</li><li>two</li></ol>
Note that closing the li then re-opening for another li item here is wrong. The correct markup is Note that closing the li then re-opening for another li item here is wrong.
The correct markup is
<ol><li>one<ol><li>1.1<ol><li>1.1.1</li></ol></li></ol></li><li>two</li></ol> <ol><li>one<ol><li>1.1<ol><li>1.1.1</li></ol></li></ol></li><li>two</li></ol>
Exmaple Right: <ol class="number"><li>one</li><ol start="2" class="number"><li>1.1</li><ol start="3" class="number"><li>1.1.1</li></ol></li></ol><li>two</li></ol> Exmaple Right: <ol class="number"><li>one</li><ol start="2" class="number">
Example Wrong: <ol class="number"><li>one</li>**</li>**<ol start="2" class="number"><li>1.1</li>**</li>**<ol start="3" class="number"><li>1.1.1</li></ol></li></ol><li>two</li></ol> <li>1.1</li><ol start="3" class="number"><li>1.1.1</li></ol></li></ol>
So it's firing wrong where the current piece is an li and the previous piece is an ol and next piece is an ol <li>two</li></ol>
Example Wrong: <ol class="number"><li>one</li>**</li>**
<ol start="2" class="number"><li>1.1</li>**</li>**<ol start="3" class="number">
<li>1.1.1</li></ol></li></ol><li>two</li></ol>
So it's firing wrong where the current piece is an li and the previous piece is
an ol and next piece is an ol
So to remedy this we can say if next piece is NOT an OL or UL. So to remedy this we can say if next piece is NOT an OL or UL.
// pieces.push("</li>"); // pieces.push("</li>");
@ -350,14 +361,16 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
if ((nextLine.listTypeName === 'number') && (nextLine.text === '')) { if ((nextLine.listTypeName === 'number') && (nextLine.text === '')) {
// is the listTypeName check needed here? null text might be completely fine! // is the listTypeName check needed here? null text might be completely fine!
// TODO Check against Uls // TODO Check against Uls
// don't do anything because the next item is a nested ol openener so we need to keep the li open // don't do anything because the next item is a nested ol openener so
// we need to keep the li open
} else { } else {
pieces.push('<li>'); pieces.push('<li>');
} }
} }
if (line.listTypeName === 'number') { if (line.listTypeName === 'number') {
// We introduce line.start here, this is useful for continuing Ordered list line numbers // We introduce line.start here, this is useful for continuing
// Ordered list line numbers
// in case you have a bullet in a list IE you Want // in case you have a bullet in a list IE you Want
// 1. hello // 1. hello
// * foo // * foo
@ -384,18 +397,24 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
} }
// To close list elements // To close list elements
if (nextLine && nextLine.listLevel === line.listLevel && line.listTypeName === nextLine.listTypeName) { if (nextLine &&
nextLine.listLevel === line.listLevel &&
line.listTypeName === nextLine.listTypeName) {
if (context.lineContent) { if (context.lineContent) {
if ((nextLine.listTypeName === 'number') && (nextLine.text === '')) { if ((nextLine.listTypeName === 'number') && (nextLine.text === '')) {
// is the listTypeName check needed here? null text might be completely fine! // is the listTypeName check needed here? null text might be completely fine!
// TODO Check against Uls // TODO Check against Uls
// don't do anything because the next item is a nested ol openener so we need to keep the li open // don't do anything because the next item is a nested ol openener so we need to
// keep the li open
} else { } else {
pieces.push('</li>'); pieces.push('</li>');
} }
} }
} }
if ((!nextLine || !nextLine.listLevel || nextLine.listLevel < line.listLevel) || (nextLine && line.listTypeName !== nextLine.listTypeName)) { if ((!nextLine ||
!nextLine.listLevel ||
nextLine.listLevel < line.listLevel) ||
(nextLine && line.listTypeName !== nextLine.listTypeName)) {
let nextLevel = 0; let nextLevel = 0;
if (nextLine && nextLine.listLevel) { if (nextLine && nextLine.listLevel) {
nextLevel = nextLine.listLevel; nextLevel = nextLine.listLevel;
@ -404,10 +423,11 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
nextLevel = 0; nextLevel = 0;
} }
for (var diff = nextLevel; diff < line.listLevel; diff++) { for (let diff = nextLevel; diff < line.listLevel; diff++) {
openLists = openLists.filter((el) => el.level !== diff && el.type !== line.listTypeName); openLists = openLists.filter((el) => el.level !== diff && el.type !== line.listTypeName);
if (pieces[pieces.length - 1].indexOf('</ul') === 0 || pieces[pieces.length - 1].indexOf('</ol') === 0) { if (pieces[pieces.length - 1].indexOf('</ul') === 0 ||
pieces[pieces.length - 1].indexOf('</ol') === 0) {
pieces.push('</li>'); pieces.push('</li>');
} }
@ -418,8 +438,8 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
} }
} }
} }
} else// outside any list, need to close line.listLevel of lists } else {
{ // outside any list, need to close line.listLevel of lists
context = { context = {
line, line,
lineContent, lineContent,
@ -435,9 +455,9 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
} }
return pieces.join(''); return pieces.join('');
} };
exports.getPadHTMLDocument = async function (padId, revNum) { exports.getPadHTMLDocument = async (padId, revNum) => {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
// Include some Styles into the Head for Export // Include some Styles into the Head for Export
@ -461,7 +481,7 @@ exports.getPadHTMLDocument = async function (padId, revNum) {
}; };
// copied from ACE // copied from ACE
function _processSpaces(s) { const _processSpaces = (s) => {
const doesWrap = true; const doesWrap = true;
if (s.indexOf('<') < 0 && !doesWrap) { if (s.indexOf('<') < 0 && !doesWrap) {
// short-cut // short-cut
@ -476,34 +496,37 @@ function _processSpaces(s) {
let beforeSpace = false; let beforeSpace = false;
// last space in a run is normal, others are nbsp, // last space in a run is normal, others are nbsp,
// end of line is nbsp // end of line is nbsp
for (var i = parts.length - 1; i >= 0; i--) { for (let i = parts.length - 1; i >= 0; i--) {
var p = parts[i]; const p = parts[i];
if (p == ' ') { if (p === ' ') {
if (endOfLine || beforeSpace) parts[i] = '&nbsp;'; if (endOfLine || beforeSpace) parts[i] = '&nbsp;';
endOfLine = false; endOfLine = false;
beforeSpace = true; beforeSpace = true;
} else if (p.charAt(0) != '<') { } else if (p.charAt(0) !== '<') {
endOfLine = false; endOfLine = false;
beforeSpace = false; beforeSpace = false;
} }
} }
// beginning of line is nbsp // beginning of line is nbsp
for (i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
p = parts[i]; const p = parts[i];
if (p == ' ') { if (p === ' ') {
parts[i] = '&nbsp;'; parts[i] = '&nbsp;';
break; break;
} else if (p.charAt(0) != '<') { } else if (p.charAt(0) !== '<') {
break; break;
} }
} }
} else { } else {
for (i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
p = parts[i]; const p = parts[i];
if (p == ' ') { if (p === ' ') {
parts[i] = '&nbsp;'; parts[i] = '&nbsp;';
} }
} }
} }
return parts.join(''); return parts.join('');
} };
exports.getPadHTML = getPadHTML;
exports.getHTMLFromAtext = getHTMLFromAtext;

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* TXT export * TXT export
*/ */
@ -18,15 +19,15 @@
* limitations under the License. * limitations under the License.
*/ */
const Changeset = require('ep_etherpad-lite/static/js/Changeset'); const Changeset = require('../../static/js/Changeset');
const padManager = require('../db/PadManager'); const padManager = require('../db/PadManager');
const _analyzeLine = require('./ExportHelper')._analyzeLine; const _analyzeLine = require('./ExportHelper')._analyzeLine;
// This is slightly different than the HTML method as it passes the output to getTXTFromAText // This is slightly different than the HTML method as it passes the output to getTXTFromAText
const getPadTXT = async function (pad, revNum) { const getPadTXT = async (pad, revNum) => {
let atext = pad.atext; let atext = pad.atext;
if (revNum != undefined) { if (revNum !== undefined) {
// fetch revision atext // fetch revision atext
atext = await pad.getInternalRevisionAText(revNum); atext = await pad.getInternalRevisionAText(revNum);
} }
@ -37,7 +38,7 @@ const getPadTXT = async function (pad, revNum) {
// This is different than the functionality provided in ExportHtml as it provides formatting // This is different than the functionality provided in ExportHtml as it provides formatting
// functionality that is designed specifically for TXT exports // functionality that is designed specifically for TXT exports
function getTXTFromAtext(pad, atext, authorColors) { const getTXTFromAtext = (pad, atext, authorColors) => {
const apool = pad.apool(); const apool = pad.apool();
const textLines = atext.text.slice(0, -1).split('\n'); const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
@ -53,7 +54,7 @@ function getTXTFromAtext(pad, atext, authorColors) {
} }
}); });
function getLineTXT(text, attribs) { const getLineTXT = (text, attribs) => {
const propVals = [false, false, false]; const propVals = [false, false, false];
const ENTER = 1; const ENTER = 1;
const STAY = 2; const STAY = 2;
@ -69,7 +70,7 @@ function getTXTFromAtext(pad, atext, authorColors) {
let idx = 0; let idx = 0;
function processNextChars(numChars) { const processNextChars = (numChars) => {
if (numChars <= 0) { if (numChars <= 0) {
return; return;
} }
@ -79,7 +80,7 @@ function getTXTFromAtext(pad, atext, authorColors) {
while (iter.hasNext()) { while (iter.hasNext()) {
const o = iter.next(); const o = iter.next();
var propChanged = false; let propChanged = false;
Changeset.eachAttribNumber(o.attribs, (a) => { Changeset.eachAttribNumber(o.attribs, (a) => {
if (a in anumMap) { if (a in anumMap) {
@ -94,7 +95,7 @@ function getTXTFromAtext(pad, atext, authorColors) {
} }
}); });
for (var i = 0; i < propVals.length; i++) { for (let i = 0; i < propVals.length; i++) {
if (propVals[i] === true) { if (propVals[i] === true) {
propVals[i] = LEAVE; propVals[i] = LEAVE;
propChanged = true; propChanged = true;
@ -110,7 +111,7 @@ function getTXTFromAtext(pad, atext, authorColors) {
// leaving bold (e.g.) also leaves italics, etc. // leaving bold (e.g.) also leaves italics, etc.
let left = false; let left = false;
for (var i = 0; i < propVals.length; i++) { for (let i = 0; i < propVals.length; i++) {
const v = propVals[i]; const v = propVals[i];
if (!left) { if (!left) {
@ -123,9 +124,9 @@ function getTXTFromAtext(pad, atext, authorColors) {
} }
} }
var tags2close = []; const tags2close = [];
for (var i = propVals.length - 1; i >= 0; i--) { for (let i = propVals.length - 1; i >= 0; i--) {
if (propVals[i] === LEAVE) { if (propVals[i] === LEAVE) {
// emitCloseTag(i); // emitCloseTag(i);
tags2close.push(i); tags2close.push(i);
@ -136,7 +137,7 @@ function getTXTFromAtext(pad, atext, authorColors) {
} }
} }
for (var i = 0; i < propVals.length; i++) { for (let i = 0; i < propVals.length; i++) {
if (propVals[i] === ENTER || propVals[i] === STAY) { if (propVals[i] === ENTER || propVals[i] === STAY) {
propVals[i] = true; propVals[i] = true;
} }
@ -163,18 +164,20 @@ function getTXTFromAtext(pad, atext, authorColors) {
assem.append(s); assem.append(s);
} // end iteration over spans in line } // end iteration over spans in line
var tags2close = []; const tags2close = [];
for (var i = propVals.length - 1; i >= 0; i--) { for (let i = propVals.length - 1; i >= 0; i--) {
if (propVals[i]) { if (propVals[i]) {
tags2close.push(i); tags2close.push(i);
propVals[i] = false; propVals[i] = false;
} }
} }
} // end processNextChars };
// end processNextChars
processNextChars(text.length - idx); processNextChars(text.length - idx);
return (assem.toString()); return (assem.toString());
} // end getLineHTML };
// end getLineHTML
const pieces = [css]; const pieces = [css];
@ -193,7 +196,7 @@ function getTXTFromAtext(pad, atext, authorColors) {
const line = _analyzeLine(textLines[i], attribLines[i], apool); const line = _analyzeLine(textLines[i], attribLines[i], apool);
let lineContent = getLineTXT(line.text, line.aline); let lineContent = getLineTXT(line.text, line.aline);
if (line.listTypeName == 'bullet') { if (line.listTypeName === 'bullet') {
lineContent = `* ${lineContent}`; // add a bullet lineContent = `* ${lineContent}`; // add a bullet
} }
@ -212,7 +215,7 @@ function getTXTFromAtext(pad, atext, authorColors) {
} }
} }
if (line.listTypeName == 'number') { if (line.listTypeName === 'number') {
/* /*
* listLevel == amount of indentation * listLevel == amount of indentation
* listNumber(s) == item number * listNumber(s) == item number
@ -249,11 +252,11 @@ function getTXTFromAtext(pad, atext, authorColors) {
} }
return pieces.join(''); return pieces.join('');
} };
exports.getTXTFromAtext = getTXTFromAtext; exports.getTXTFromAtext = getTXTFromAtext;
exports.getPadTXTDocument = async function (padId, revNum) { exports.getPadTXTDocument = async (padId, revNum) => {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
return getPadTXT(pad, revNum); return getPadTXT(pad, revNum);
}; };

View file

@ -1,3 +1,5 @@
// 'use strict';
// Uncommenting above breaks tests.
/** /**
* 2014 John McLear (Etherpad Foundation / McLear Ltd) * 2014 John McLear (Etherpad Foundation / McLear Ltd)
* *
@ -14,12 +16,11 @@
* limitations under the License. * limitations under the License.
*/ */
const log4js = require('log4js');
const db = require('../db/DB'); const db = require('../db/DB');
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); const hooks = require('../../static/js/pluginfw/hooks');
exports.setPadRaw = function (padId, records) { exports.setPadRaw = (padId, r) => {
records = JSON.parse(records); const records = JSON.parse(r);
Object.keys(records).forEach(async (key) => { Object.keys(records).forEach(async (key) => {
let value = records[key]; let value = records[key];

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* Copyright Yaco Sistemas S.L. 2011. * Copyright Yaco Sistemas S.L. 2011.
* *
@ -15,8 +16,8 @@
*/ */
const log4js = require('log4js'); const log4js = require('log4js');
const Changeset = require('ep_etherpad-lite/static/js/Changeset'); const Changeset = require('../../static/js/Changeset');
const contentcollector = require('ep_etherpad-lite/static/js/contentcollector'); const contentcollector = require('../../static/js/contentcollector');
const cheerio = require('cheerio'); const cheerio = require('cheerio');
const rehype = require('rehype'); const rehype = require('rehype');
const minifyWhitespace = require('rehype-minify-whitespace'); const minifyWhitespace = require('rehype-minify-whitespace');
@ -69,7 +70,7 @@ exports.setPadHTML = async (pad, html) => {
apiLogger.debug(newText); apiLogger.debug(newText);
const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`; const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`;
function eachAttribRun(attribs, func /* (startInNewText, endInNewText, attribs)*/) { const eachAttribRun = (attribs, func /* (startInNewText, endInNewText, attribs)*/) => {
const attribsIter = Changeset.opIterator(attribs); const attribsIter = Changeset.opIterator(attribs);
let textIndex = 0; let textIndex = 0;
const newTextStart = 0; const newTextStart = 0;
@ -82,7 +83,7 @@ exports.setPadHTML = async (pad, html) => {
} }
textIndex = nextIndex; textIndex = nextIndex;
} }
} };
// create a new changeset with a helper builder object // create a new changeset with a helper builder object
const builder = Changeset.builder(1); const builder = Changeset.builder(1);

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* Controls the communication with LibreOffice * Controls the communication with LibreOffice
*/ */
@ -24,52 +25,9 @@ const path = require('path');
const settings = require('./Settings'); const settings = require('./Settings');
const spawn = require('child_process').spawn; const spawn = require('child_process').spawn;
// Conversion tasks will be queued up, so we don't overload the system
const queue = async.queue(doConvertTask, 1);
const libreOfficeLogger = log4js.getLogger('LibreOffice'); const libreOfficeLogger = log4js.getLogger('LibreOffice');
/** const doConvertTask = (task, callback) => {
* Convert a file from one type to another
*
* @param {String} srcFile The path on disk to convert
* @param {String} destFile The path on disk where the converted file should be stored
* @param {String} type The type to convert into
* @param {Function} callback Standard callback function
*/
exports.convertFile = function (srcFile, destFile, type, callback) {
// Used for the moving of the file, not the conversion
const fileExtension = type;
if (type === 'html') {
// "html:XHTML Writer File:UTF8" does a better job than normal html exports
if (path.extname(srcFile).toLowerCase() === '.doc') {
type = 'html';
}
// PDF files need to be converted with LO Draw ref https://github.com/ether/etherpad-lite/issues/4151
if (path.extname(srcFile).toLowerCase() === '.pdf') {
type = 'html:XHTML Draw File';
}
}
// soffice can't convert from html to doc directly (verified with LO 5 and 6)
// we need to convert to odt first, then to doc
// to avoid `Error: no export filter for /tmp/xxxx.doc` error
if (type === 'doc') {
queue.push({
srcFile,
destFile: destFile.replace(/\.doc$/, '.odt'),
type: 'odt',
callback() {
queue.push({srcFile: srcFile.replace(/\.html$/, '.odt'), destFile, type, callback, fileExtension});
},
});
} else {
queue.push({srcFile, destFile, type, callback, fileExtension});
}
};
function doConvertTask(task, callback) {
const tmpDir = os.tmpdir(); const tmpDir = os.tmpdir();
async.series([ async.series([
@ -77,8 +35,10 @@ function doConvertTask(task, callback) {
* use LibreOffice to convert task.srcFile to another format, given in * use LibreOffice to convert task.srcFile to another format, given in
* task.type * task.type
*/ */
function (callback) { (callback) => {
libreOfficeLogger.debug(`Converting ${task.srcFile} to format ${task.type}. The result will be put in ${tmpDir}`); libreOfficeLogger.debug(
`Converting ${task.srcFile} to format ${task.type}. The result will be put in ${tmpDir}`
);
const soffice = spawn(settings.soffice, [ const soffice = spawn(settings.soffice, [
'--headless', '--headless',
'--invisible', '--invisible',
@ -112,7 +72,7 @@ function doConvertTask(task, callback) {
soffice.on('exit', (code) => { soffice.on('exit', (code) => {
clearTimeout(hangTimeout); clearTimeout(hangTimeout);
if (code != 0) { if (code !== 0) {
// Throw an exception if libreoffice failed // Throw an exception if libreoffice failed
return callback(`LibreOffice died with exit code ${code} and message: ${stdoutBuffer}`); return callback(`LibreOffice died with exit code ${code} and message: ${stdoutBuffer}`);
} }
@ -123,10 +83,10 @@ function doConvertTask(task, callback) {
}, },
// Move the converted file to the correct place // Move the converted file to the correct place
function (callback) { (callback) => {
const filename = path.basename(task.srcFile); const filename = path.basename(task.srcFile);
const sourceFilename = `${filename.substr(0, filename.lastIndexOf('.'))}.${task.fileExtension}`; const sourceFile = `${filename.substr(0, filename.lastIndexOf('.'))}.${task.fileExtension}`;
const sourcePath = path.join(tmpDir, sourceFilename); const sourcePath = path.join(tmpDir, sourceFile);
libreOfficeLogger.debug(`Renaming ${sourcePath} to ${task.destFile}`); libreOfficeLogger.debug(`Renaming ${sourcePath} to ${task.destFile}`);
fs.rename(sourcePath, task.destFile, callback); fs.rename(sourcePath, task.destFile, callback);
}, },
@ -137,4 +97,55 @@ function doConvertTask(task, callback) {
// Invoke the callback for the task // Invoke the callback for the task
task.callback(err); task.callback(err);
}); });
};
// Conversion tasks will be queued up, so we don't overload the system
const queue = async.queue(doConvertTask, 1);
/**
* Convert a file from one type to another
*
* @param {String} srcFile The path on disk to convert
* @param {String} destFile The path on disk where the converted file should be stored
* @param {String} type The type to convert into
* @param {Function} callback Standard callback function
*/
exports.convertFile = (srcFile, destFile, type, callback) => {
// Used for the moving of the file, not the conversion
const fileExtension = type;
if (type === 'html') {
// "html:XHTML Writer File:UTF8" does a better job than normal html exports
if (path.extname(srcFile).toLowerCase() === '.doc') {
type = 'html';
} }
// PDF files need to be converted with LO Draw ref https://github.com/ether/etherpad-lite/issues/4151
if (path.extname(srcFile).toLowerCase() === '.pdf') {
type = 'html:XHTML Draw File';
}
}
// soffice can't convert from html to doc directly (verified with LO 5 and 6)
// we need to convert to odt first, then to doc
// to avoid `Error: no export filter for /tmp/xxxx.doc` error
if (type === 'doc') {
queue.push({
srcFile,
destFile: destFile.replace(/\.doc$/, '.odt'),
type: 'odt',
callback: () => {
queue.push(
{
srcFile: srcFile.replace(/\.html$/, '.odt'),
destFile,
type,
callback,
fileExtension,
}
);
},
});
} else {
queue.push({srcFile, destFile, type, callback, fileExtension});
}
};

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* Worker thread to minify JS & CSS files out of the main NodeJS thread * Worker thread to minify JS & CSS files out of the main NodeJS thread
*/ */
@ -7,12 +8,9 @@ const Terser = require('terser');
const path = require('path'); const path = require('path');
const Threads = require('threads'); const Threads = require('threads');
function compressJS(content) { const compressJS = (content) => Terser.minify(content);
return Terser.minify(content);
}
function compressCSS(filename, ROOT_DIR) { const compressCSS = (filename, ROOT_DIR) => new Promise((res, rej) => {
return new Promise((res, rej) => {
try { try {
const absPath = path.join(ROOT_DIR, filename); const absPath = path.join(ROOT_DIR, filename);
@ -57,7 +55,6 @@ function compressCSS(filename, ROOT_DIR) {
callback(null, content); callback(null, content);
} }
}); });
}
Threads.expose({ Threads.expose({
compressJS, compressJS,

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* Checks related to Node runtime version * Checks related to Node runtime version
*/ */
@ -25,7 +26,7 @@ const semver = require('semver');
* *
* @param {String} minNodeVersion Minimum required Node version * @param {String} minNodeVersion Minimum required Node version
*/ */
exports.enforceMinNodeVersion = function (minNodeVersion) { exports.enforceMinNodeVersion = (minNodeVersion) => {
const currentNodeVersion = process.version; const currentNodeVersion = process.version;
// we cannot use template literals, since we still do not know if we are // we cannot use template literals, since we still do not know if we are
@ -41,10 +42,12 @@ exports.enforceMinNodeVersion = function (minNodeVersion) {
/** /**
* Prints a warning if running on a supported but deprecated Node version * Prints a warning if running on a supported but deprecated Node version
* *
* @param {String} lowestNonDeprecatedNodeVersion all Node version less than this one are deprecated * @param {String} lowestNonDeprecatedNodeVersion all Node version less than this one are
* @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated Node releases * deprecated
* @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated
* Node releases
*/ */
exports.checkDeprecationStatus = function (lowestNonDeprecatedNodeVersion, epRemovalVersion) { exports.checkDeprecationStatus = (lowestNonDeprecatedNodeVersion, epRemovalVersion) => {
const currentNodeVersion = process.version; const currentNodeVersion = process.version;
if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) { if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) {

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* The Settings module reads the settings out of settings.json and provides * The Settings module reads the settings out of settings.json and provides
* this information to the other modules * this information to the other modules
@ -31,7 +32,6 @@ const fs = require('fs');
const os = require('os'); const os = require('os');
const path = require('path'); const path = require('path');
const argv = require('./Cli').argv; const argv = require('./Cli').argv;
const npm = require('npm/lib/npm.js');
const jsonminify = require('jsonminify'); const jsonminify = require('jsonminify');
const log4js = require('log4js'); const log4js = require('log4js');
const randomString = require('./randomstring'); const randomString = require('./randomstring');
@ -381,29 +381,30 @@ exports.commitRateLimiting = {
exports.importMaxFileSize = 50 * 1024 * 1024; exports.importMaxFileSize = 50 * 1024 * 1024;
// checks if abiword is avaiable // checks if abiword is avaiable
exports.abiwordAvailable = function () { exports.abiwordAvailable = () => {
if (exports.abiword != null) { if (exports.abiword != null) {
return os.type().indexOf('Windows') != -1 ? 'withoutPDF' : 'yes'; return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
} else { } else {
return 'no'; return 'no';
} }
}; };
exports.sofficeAvailable = function () { exports.sofficeAvailable = () => {
if (exports.soffice != null) { if (exports.soffice != null) {
return os.type().indexOf('Windows') != -1 ? 'withoutPDF' : 'yes'; return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
} else { } else {
return 'no'; return 'no';
} }
}; };
exports.exportAvailable = function () { exports.exportAvailable = () => {
const abiword = exports.abiwordAvailable(); const abiword = exports.abiwordAvailable();
const soffice = exports.sofficeAvailable(); const soffice = exports.sofficeAvailable();
if (abiword == 'no' && soffice == 'no') { if (abiword === 'no' && soffice === 'no') {
return 'no'; return 'no';
} else if ((abiword == 'withoutPDF' && soffice == 'no') || (abiword == 'no' && soffice == 'withoutPDF')) { } else if ((abiword === 'withoutPDF' && soffice === 'no') ||
(abiword === 'no' && soffice === 'withoutPDF')) {
return 'withoutPDF'; return 'withoutPDF';
} else { } else {
return 'yes'; return 'yes';
@ -411,7 +412,7 @@ exports.exportAvailable = function () {
}; };
// Provide git version if available // Provide git version if available
exports.getGitCommit = function () { exports.getGitCommit = () => {
let version = ''; let version = '';
try { try {
let rootPath = exports.root; let rootPath = exports.root;
@ -436,9 +437,7 @@ exports.getGitCommit = function () {
}; };
// Return etherpad version from package.json // Return etherpad version from package.json
exports.getEpVersion = function () { exports.getEpVersion = () => require('../../package.json').version;
return require('ep_etherpad-lite/package.json').version;
};
/** /**
* Receives a settingsObj and, if the property name is a valid configuration * Receives a settingsObj and, if the property name is a valid configuration
@ -447,7 +446,7 @@ exports.getEpVersion = function () {
* This code refactors a previous version that copied & pasted the same code for * This code refactors a previous version that copied & pasted the same code for
* both "settings.json" and "credentials.json". * both "settings.json" and "credentials.json".
*/ */
function storeSettings(settingsObj) { const storeSettings = (settingsObj) => {
for (const i in settingsObj) { for (const i in settingsObj) {
// test if the setting starts with a lowercase character // test if the setting starts with a lowercase character
if (i.charAt(0).search('[a-z]') !== 0) { if (i.charAt(0).search('[a-z]') !== 0) {
@ -456,7 +455,7 @@ function storeSettings(settingsObj) {
// we know this setting, so we overwrite it // we know this setting, so we overwrite it
// or it's a settings hash, specific to a plugin // or it's a settings hash, specific to a plugin
if (exports[i] !== undefined || i.indexOf('ep_') == 0) { if (exports[i] !== undefined || i.indexOf('ep_') === 0) {
if (_.isObject(settingsObj[i]) && !_.isArray(settingsObj[i])) { if (_.isObject(settingsObj[i]) && !_.isArray(settingsObj[i])) {
exports[i] = _.defaults(settingsObj[i], exports[i]); exports[i] = _.defaults(settingsObj[i], exports[i]);
} else { } else {
@ -467,7 +466,7 @@ function storeSettings(settingsObj) {
console.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); console.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`);
} }
} }
} };
/* /*
* If stringValue is a numeric string, or its value is "true" or "false", coerce * If stringValue is a numeric string, or its value is "true" or "false", coerce
@ -481,7 +480,7 @@ function storeSettings(settingsObj) {
* short syntax "${ABIWORD}", and not "${ABIWORD:null}": the latter would result * short syntax "${ABIWORD}", and not "${ABIWORD:null}": the latter would result
* in the literal string "null", instead. * in the literal string "null", instead.
*/ */
function coerceValue(stringValue) { const coerceValue = (stringValue) => {
// cooked from https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number // 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 isNumeric = !isNaN(stringValue) && !isNaN(parseFloat(stringValue) && isFinite(stringValue));
@ -502,7 +501,7 @@ function coerceValue(stringValue) {
// otherwise, return this value as-is // otherwise, return this value as-is
return stringValue; return stringValue;
} };
/** /**
* Takes a javascript object containing Etherpad's configuration, and returns * Takes a javascript object containing Etherpad's configuration, and returns
@ -540,7 +539,7 @@ function coerceValue(stringValue) {
* *
* see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter * see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter
*/ */
function lookupEnvironmentVariables(obj) { const lookupEnvironmentVariables = (obj) => {
const stringifiedAndReplaced = JSON.stringify(obj, (key, value) => { const stringifiedAndReplaced = JSON.stringify(obj, (key, value) => {
/* /*
* the first invocation of replacer() is with an empty key. Just go on, or * the first invocation of replacer() is with an empty key. Just go on, or
@ -569,7 +568,7 @@ function lookupEnvironmentVariables(obj) {
// MUXATOR 2019-03-21: we could use named capture groups here once we migrate to nodejs v10 // MUXATOR 2019-03-21: we could use named capture groups here once we migrate to nodejs v10
const match = value.match(/^\$\{([^:]*)(:((.|\n)*))?\}$/); const match = value.match(/^\$\{([^:]*)(:((.|\n)*))?\}$/);
if (match === null) { if (match == null) {
// no match: use the value literally, without any substitution // no match: use the value literally, without any substitution
return value; return value;
@ -613,7 +612,7 @@ function lookupEnvironmentVariables(obj) {
const newSettings = JSON.parse(stringifiedAndReplaced); const newSettings = JSON.parse(stringifiedAndReplaced);
return newSettings; return newSettings;
} };
/** /**
* - reads the JSON configuration file settingsFilename from disk * - reads the JSON configuration file settingsFilename from disk
@ -623,7 +622,7 @@ function lookupEnvironmentVariables(obj) {
* *
* The isSettings variable only controls the error logging. * The isSettings variable only controls the error logging.
*/ */
function parseSettings(settingsFilename, isSettings) { const parseSettings = (settingsFilename, isSettings) => {
let settingsStr = ''; let settingsStr = '';
let settingsType, notFoundMessage, notFoundFunction; let settingsType, notFoundMessage, notFoundFunction;
@ -663,9 +662,9 @@ function parseSettings(settingsFilename, isSettings) {
process.exit(1); process.exit(1);
} }
} };
exports.reloadSettings = function reloadSettings() { exports.reloadSettings = () => {
// Discover where the settings file lives // Discover where the settings file lives
const settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json'); const settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json');
@ -695,7 +694,7 @@ exports.reloadSettings = function reloadSettings() {
const skinBasePath = path.join(exports.root, 'src', 'static', 'skins'); const skinBasePath = path.join(exports.root, 'src', 'static', 'skins');
const countPieces = exports.skinName.split(path.sep).length; const countPieces = exports.skinName.split(path.sep).length;
if (countPieces != 1) { if (countPieces !== 1) {
console.error(`skinName must be the name of a directory under "${skinBasePath}". This is not valid: "${exports.skinName}". Falling back to the default "colibris".`); console.error(`skinName must be the name of a directory under "${skinBasePath}". This is not valid: "${exports.skinName}". Falling back to the default "colibris".`);
exports.skinName = 'colibris'; exports.skinName = 'colibris';
@ -766,7 +765,7 @@ exports.reloadSettings = function reloadSettings() {
} }
if (exports.dbType === 'dirty') { if (exports.dbType === 'dirty') {
const dirtyWarning = 'DirtyDB is used. This is fine for testing but not recommended for production.'; const dirtyWarning = 'DirtyDB is used. This is not recommended for production.';
if (!exports.suppressErrorsInPadText) { if (!exports.suppressErrorsInPadText) {
exports.defaultPadText = `${exports.defaultPadText}\nWarning: ${dirtyWarning}${suppressDisableMsg}`; exports.defaultPadText = `${exports.defaultPadText}\nWarning: ${dirtyWarning}${suppressDisableMsg}`;
} }

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* Tidy up the HTML in a given file * Tidy up the HTML in a given file
*/ */
@ -6,7 +7,7 @@ const log4js = require('log4js');
const settings = require('./Settings'); const settings = require('./Settings');
const spawn = require('child_process').spawn; const spawn = require('child_process').spawn;
exports.tidy = function (srcFile) { exports.tidy = (srcFile) => {
const logger = log4js.getLogger('TidyHtml'); const logger = log4js.getLogger('TidyHtml');
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View file

@ -1,11 +1,11 @@
'use strict';
const semver = require('semver'); const semver = require('semver');
const settings = require('./Settings'); const settings = require('./Settings');
const request = require('request'); const request = require('request');
let infos; let infos;
function loadEtherpadInformations() { const loadEtherpadInformations = () => new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
request('https://static.etherpad.org/info.json', (er, response, body) => { request('https://static.etherpad.org/info.json', (er, response, body) => {
if (er) return reject(er); if (er) return reject(er);
@ -17,14 +17,13 @@ function loadEtherpadInformations() {
} }
}); });
}); });
}
exports.getLatestVersion = function () { exports.getLatestVersion = () => {
exports.needsUpdate(); exports.needsUpdate();
return infos.latestVersion; return infos.latestVersion;
}; };
exports.needsUpdate = function (cb) { exports.needsUpdate = (cb) => {
loadEtherpadInformations().then((info) => { loadEtherpadInformations().then((info) => {
if (semver.gt(info.latestVersion, settings.getEpVersion())) { if (semver.gt(info.latestVersion, settings.getEpVersion())) {
if (cb) return cb(true); if (cb) return cb(true);
@ -35,7 +34,7 @@ exports.needsUpdate = function (cb) {
}); });
}; };
exports.check = function () { exports.check = () => {
exports.needsUpdate((needsUpdate) => { exports.needsUpdate((needsUpdate) => {
if (needsUpdate) { if (needsUpdate) {
console.warn(`Update available: Download the actual version ${infos.latestVersion}`); console.warn(`Update available: Download the actual version ${infos.latestVersion}`);

View file

@ -1,3 +1,5 @@
'use strict';
/* /*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd) * 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
* *
@ -47,18 +49,16 @@ CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined;
const responseCache = {}; const responseCache = {};
function djb2Hash(data) { const djb2Hash = (data) => {
const chars = data.split('').map((str) => str.charCodeAt(0)); const chars = data.split('').map((str) => str.charCodeAt(0));
return `${chars.reduce((prev, curr) => ((prev << 5) + prev) + curr, 5381)}`; return `${chars.reduce((prev, curr) => ((prev << 5) + prev) + curr, 5381)}`;
} };
function generateCacheKeyWithSha256(path) { const generateCacheKeyWithSha256 =
return _crypto.createHash('sha256').update(path).digest('hex'); (path) => _crypto.createHash('sha256').update(path).digest('hex');
}
function generateCacheKeyWithDjb2(path) { const generateCacheKeyWithDjb2 =
return Buffer.from(djb2Hash(path)).toString('hex'); (path) => Buffer.from(djb2Hash(path)).toString('hex');
}
let generateCacheKey; let generateCacheKey;
@ -66,7 +66,7 @@ if (_crypto) {
generateCacheKey = generateCacheKeyWithSha256; generateCacheKey = generateCacheKeyWithSha256;
} else { } else {
generateCacheKey = generateCacheKeyWithDjb2; generateCacheKey = generateCacheKeyWithDjb2;
console.warn('No crypto support in this nodejs runtime. A fallback to Djb2 (weaker) will be used.'); console.warn('No crypto support in this nodejs runtime. Djb2 (weaker) will be used.');
} }
// MIMIC https://github.com/microsoft/TypeScript/commit/9677b0641cc5ba7d8b701b4f892ed7e54ceaee9a - END // MIMIC https://github.com/microsoft/TypeScript/commit/9677b0641cc5ba7d8b701b4f892ed7e54ceaee9a - END
@ -80,8 +80,8 @@ if (_crypto) {
function CachingMiddleware() { function CachingMiddleware() {
} }
CachingMiddleware.prototype = new function () { CachingMiddleware.prototype = new function () {
function handle(req, res, next) { const handle = (req, res, next) => {
if (!(req.method == 'GET' || req.method == 'HEAD') || !CACHE_DIR) { if (!(req.method === 'GET' || req.method === 'HEAD') || !CACHE_DIR) {
return next(undefined, req, res); return next(undefined, req, res);
} }
@ -89,7 +89,7 @@ CachingMiddleware.prototype = new function () {
const old_res = {}; const old_res = {};
const supportsGzip = const supportsGzip =
(req.get('Accept-Encoding') || '').indexOf('gzip') != -1; (req.get('Accept-Encoding') || '').indexOf('gzip') !== -1;
const path = require('url').parse(req.url).path; const path = require('url').parse(req.url).path;
const cacheKey = generateCacheKey(path); const cacheKey = generateCacheKey(path);
@ -116,7 +116,7 @@ CachingMiddleware.prototype = new function () {
const _headers = {}; const _headers = {};
old_res.setHeader = res.setHeader; old_res.setHeader = res.setHeader;
res.setHeader = function (key, value) { res.setHeader = (key, value) => {
// Don't set cookies, see issue #707 // Don't set cookies, see issue #707
if (key.toLowerCase() === 'set-cookie') return; if (key.toLowerCase() === 'set-cookie') return;
@ -126,11 +126,8 @@ CachingMiddleware.prototype = new function () {
old_res.writeHead = res.writeHead; old_res.writeHead = res.writeHead;
res.writeHead = function (status, headers) { res.writeHead = function (status, headers) {
const lastModified = (res.getHeader('last-modified') &&
new Date(res.getHeader('last-modified')));
res.writeHead = old_res.writeHead; res.writeHead = old_res.writeHead;
if (status == 200) { if (status === 200) {
// Update cache // Update cache
let buffer = ''; let buffer = '';
@ -169,7 +166,7 @@ CachingMiddleware.prototype = new function () {
respond(); respond();
}); });
}; };
} else if (status == 304) { } else if (status === 304) {
// Nothing new changed from the cached version. // Nothing new changed from the cached version.
old_res.write = res.write; old_res.write = res.write;
old_res.end = res.end; old_res.end = res.end;
@ -204,10 +201,10 @@ CachingMiddleware.prototype = new function () {
const lastModified = (headers['last-modified'] && const lastModified = (headers['last-modified'] &&
new Date(headers['last-modified'])); new Date(headers['last-modified']));
if (statusCode == 200 && lastModified <= modifiedSince) { if (statusCode === 200 && lastModified <= modifiedSince) {
res.writeHead(304, headers); res.writeHead(304, headers);
res.end(); res.end();
} else if (req.method == 'GET') { } else if (req.method === 'GET') {
const readStream = fs.createReadStream(pathStr); const readStream = fs.createReadStream(pathStr);
res.writeHead(statusCode, headers); res.writeHead(statusCode, headers);
readStream.pipe(res); readStream.pipe(res);
@ -217,7 +214,7 @@ CachingMiddleware.prototype = new function () {
} }
} }
}); });
} };
this.handle = handle; this.handle = handle;
}(); }();

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* CustomError * CustomError
* *

View file

@ -1,3 +1,4 @@
'use strict';
const Changeset = require('../../static/js/Changeset'); const Changeset = require('../../static/js/Changeset');
const exportHtml = require('./ExportHtml'); const exportHtml = require('./ExportHtml');
@ -125,7 +126,7 @@ PadDiff.prototype._addAuthors = function (authors) {
// add to array if not in the array // add to array if not in the array
authors.forEach((author) => { authors.forEach((author) => {
if (self._authors.indexOf(author) == -1) { if (self._authors.indexOf(author) === -1) {
self._authors.push(author); self._authors.push(author);
} }
}); });
@ -138,7 +139,6 @@ PadDiff.prototype._createDiffAtext = async function () {
let atext = await this._createClearStartAtext(this._fromRev); let atext = await this._createClearStartAtext(this._fromRev);
let superChangeset = null; let superChangeset = null;
const rev = this._fromRev + 1;
for (let rev = this._fromRev + 1; rev <= this._toRev; rev += bulkSize) { for (let rev = this._fromRev + 1; rev <= this._toRev; rev += bulkSize) {
// get the bulk // get the bulk
@ -161,7 +161,7 @@ PadDiff.prototype._createDiffAtext = async function () {
addedAuthors.push(authors[i]); addedAuthors.push(authors[i]);
// compose it with the superChangset // compose it with the superChangset
if (superChangeset === null) { if (superChangeset == null) {
superChangeset = changeset; superChangeset = changeset;
} else { } else {
superChangeset = Changeset.composeWithDeletions(superChangeset, changeset, this._pad.pool); superChangeset = Changeset.composeWithDeletions(superChangeset, changeset, this._pad.pool);
@ -172,7 +172,8 @@ PadDiff.prototype._createDiffAtext = async function () {
this._addAuthors(addedAuthors); this._addAuthors(addedAuthors);
} }
// if there are only clearAuthorship changesets, we don't get a superChangeset, so we can skip this step // if there are only clearAuthorship changesets, we don't get a superChangeset,
// so we can skip this step
if (superChangeset) { if (superChangeset) {
const deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool); const deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool);
@ -205,7 +206,8 @@ PadDiff.prototype.getHtml = async function () {
}; };
PadDiff.prototype.getAuthors = async function () { PadDiff.prototype.getAuthors = async function () {
// check if html was already produced, if not produce it, this generates the author array at the same time // check if html was already produced, if not produce it, this generates
// the author array at the same time
if (this._html == null) { if (this._html == null) {
await this.getHtml(); await this.getHtml();
} }
@ -213,7 +215,7 @@ PadDiff.prototype.getAuthors = async function () {
return self._authors; return self._authors;
}; };
PadDiff.prototype._extendChangesetWithAuthor = function (changeset, author, apool) { PadDiff.prototype._extendChangesetWithAuthor = (changeset, author, apool) => {
// unpack // unpack
const unpacked = Changeset.unpack(changeset); const unpacked = Changeset.unpack(changeset);
@ -245,7 +247,8 @@ PadDiff.prototype._extendChangesetWithAuthor = function (changeset, author, apoo
return Changeset.pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank); return Changeset.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. // 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) { PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
const lines = Changeset.splitTextLines(startAText.text); const lines = Changeset.splitTextLines(startAText.text);
const alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text); const alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text);
@ -254,21 +257,21 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
// They may be arrays or objects with .get(i) and .length methods. // They may be arrays or objects with .get(i) and .length methods.
// They include final newlines on lines. // They include final newlines on lines.
function lines_get(idx) { const linesGet = (idx) => {
if (lines.get) { if (lines.get) {
return lines.get(idx); return lines.get(idx);
} else { } else {
return lines[idx]; return lines[idx];
} }
} };
function alines_get(idx) { const aLinesGet = (idx) => {
if (alines.get) { if (alines.get) {
return alines.get(idx); return alines.get(idx);
} else { } else {
return alines[idx]; return alines[idx];
} }
} };
let curLine = 0; let curLine = 0;
let curChar = 0; let curChar = 0;
@ -280,10 +283,10 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
const csIter = Changeset.opIterator(unpacked.ops); const csIter = Changeset.opIterator(unpacked.ops);
const builder = Changeset.builder(unpacked.newLen); const builder = Changeset.builder(unpacked.newLen);
function consumeAttribRuns(numChars, func /* (len, attribs, endsLine)*/) { const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => {
if ((!curLineOpIter) || (curLineOpIterLine != curLine)) { if ((!curLineOpIter) || (curLineOpIterLine !== curLine)) {
// create curLineOpIter and advance it to curChar // create curLineOpIter and advance it to curChar
curLineOpIter = Changeset.opIterator(alines_get(curLine)); curLineOpIter = Changeset.opIterator(aLinesGet(curLine));
curLineOpIterLine = curLine; curLineOpIterLine = curLine;
let indexIntoLine = 0; let indexIntoLine = 0;
let done = false; let done = false;
@ -304,7 +307,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
curChar = 0; curChar = 0;
curLineOpIterLine = curLine; curLineOpIterLine = curLine;
curLineNextOp.chars = 0; curLineNextOp.chars = 0;
curLineOpIter = Changeset.opIterator(alines_get(curLine)); curLineOpIter = Changeset.opIterator(aLinesGet(curLine));
} }
if (!curLineNextOp.chars) { if (!curLineNextOp.chars) {
@ -313,7 +316,8 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
const charsToUse = Math.min(numChars, curLineNextOp.chars); const charsToUse = Math.min(numChars, curLineNextOp.chars);
func(charsToUse, curLineNextOp.attribs, charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0); func(charsToUse, curLineNextOp.attribs,
charsToUse === curLineNextOp.chars && curLineNextOp.lines > 0);
numChars -= charsToUse; numChars -= charsToUse;
curLineNextOp.chars -= charsToUse; curLineNextOp.chars -= charsToUse;
curChar += charsToUse; curChar += charsToUse;
@ -323,64 +327,66 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
curLine++; curLine++;
curChar = 0; curChar = 0;
} }
} };
function skip(N, L) { const skip = (N, L) => {
if (L) { if (L) {
curLine += L; curLine += L;
curChar = 0; curChar = 0;
} else if (curLineOpIter && curLineOpIterLine == curLine) { } else if (curLineOpIter && curLineOpIterLine === curLine) {
consumeAttribRuns(N, () => {}); consumeAttribRuns(N, () => {});
} else { } else {
curChar += N; curChar += N;
} }
} };
function nextText(numChars) { const nextText = (numChars) => {
let len = 0; let len = 0;
const assem = Changeset.stringAssembler(); const assem = Changeset.stringAssembler();
const firstString = lines_get(curLine).substring(curChar); const firstString = linesGet(curLine).substring(curChar);
len += firstString.length; len += firstString.length;
assem.append(firstString); assem.append(firstString);
let lineNum = curLine + 1; let lineNum = curLine + 1;
while (len < numChars) { while (len < numChars) {
const nextString = lines_get(lineNum); const nextString = linesGet(lineNum);
len += nextString.length; len += nextString.length;
assem.append(nextString); assem.append(nextString);
lineNum++; lineNum++;
} }
return assem.toString().substring(0, numChars); return assem.toString().substring(0, numChars);
} };
function cachedStrFunc(func) { const cachedStrFunc = (func) => {
const cache = {}; const cache = {};
return function (s) { return (s) => {
if (!cache[s]) { if (!cache[s]) {
cache[s] = func(s); cache[s] = func(s);
} }
return cache[s]; return cache[s];
}; };
} };
const attribKeys = []; const attribKeys = [];
const attribValues = []; const attribValues = [];
// iterate over all operators of this changeset // iterate over all operators of this changeset
while (csIter.hasNext()) { while (csIter.hasNext()) {
var csOp = csIter.next(); const csOp = csIter.next();
if (csOp.opcode == '=') { if (csOp.opcode === '=') {
var textBank = nextText(csOp.chars); const textBank = nextText(csOp.chars);
// decide if this equal operator is an attribution change or not. We can see this by checkinf if attribs is set. // decide if this equal operator is an attribution change or not.
// If the text this operator applies to is only a star, than this is a false positive and should be ignored // We can see this by checkinf if attribs is set.
if (csOp.attribs && textBank != '*') { // If the text this operator applies to is only a star,
// than this is a false positive and should be ignored
if (csOp.attribs && textBank !== '*') {
const deletedAttrib = apool.putAttrib(['removed', true]); const deletedAttrib = apool.putAttrib(['removed', true]);
var authorAttrib = apool.putAttrib(['author', '']); let authorAttrib = apool.putAttrib(['author', '']);
attribKeys.length = 0; attribKeys.length = 0;
attribValues.length = 0; attribValues.length = 0;
@ -393,14 +399,14 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
} }
}); });
var undoBackToAttribs = cachedStrFunc((attribs) => { const undoBackToAttribs = cachedStrFunc((attribs) => {
const backAttribs = []; const backAttribs = [];
for (let i = 0; i < attribKeys.length; i++) { for (let i = 0; i < attribKeys.length; i++) {
const appliedKey = attribKeys[i]; const appliedKey = attribKeys[i];
const appliedValue = attribValues[i]; const appliedValue = attribValues[i];
const oldValue = Changeset.attribsAttributeValue(attribs, appliedKey, apool); const oldValue = Changeset.attribsAttributeValue(attribs, appliedKey, apool);
if (appliedValue != oldValue) { if (appliedValue !== oldValue) {
backAttribs.push([appliedKey, oldValue]); backAttribs.push([appliedKey, oldValue]);
} }
} }
@ -408,7 +414,8 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
return Changeset.makeAttribsString('=', backAttribs, apool); return Changeset.makeAttribsString('=', backAttribs, apool);
}); });
var oldAttribsAddition = `*${Changeset.numToString(deletedAttrib)}*${Changeset.numToString(authorAttrib)}`; const oldAttribsAddition =
`*${Changeset.numToString(deletedAttrib)}*${Changeset.numToString(authorAttrib)}`;
let textLeftToProcess = textBank; let textLeftToProcess = textBank;
@ -427,7 +434,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
} }
// get the text we want to procceed in this step // get the text we want to procceed in this step
var processText = textLeftToProcess.substr(0, lengthToProcess); const processText = textLeftToProcess.substr(0, lengthToProcess);
textLeftToProcess = textLeftToProcess.substr(lengthToProcess); textLeftToProcess = textLeftToProcess.substr(lengthToProcess);
@ -437,13 +444,14 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
// consume the attributes of this linebreak // consume the attributes of this linebreak
consumeAttribRuns(1, () => {}); consumeAttribRuns(1, () => {});
} else { } else {
// add the old text via an insert, but add a deletion attribute + the author attribute of the author who deleted it // add the old text via an insert, but add a deletion attribute +
var textBankIndex = 0; // the author attribute of the author who deleted it
let textBankIndex = 0;
consumeAttribRuns(lengthToProcess, (len, attribs, endsLine) => { consumeAttribRuns(lengthToProcess, (len, attribs, endsLine) => {
// get the old attributes back // get the old attributes back
var attribs = (undoBackToAttribs(attribs) || '') + oldAttribsAddition; const oldAttribs = (undoBackToAttribs(attribs) || '') + oldAttribsAddition;
builder.insert(processText.substr(textBankIndex, len), attribs); builder.insert(processText.substr(textBankIndex, len), oldAttribs);
textBankIndex += len; textBankIndex += len;
}); });
@ -454,11 +462,11 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
skip(csOp.chars, csOp.lines); skip(csOp.chars, csOp.lines);
builder.keep(csOp.chars, csOp.lines); builder.keep(csOp.chars, csOp.lines);
} }
} else if (csOp.opcode == '+') { } else if (csOp.opcode === '+') {
builder.keep(csOp.chars, csOp.lines); builder.keep(csOp.chars, csOp.lines);
} else if (csOp.opcode == '-') { } else if (csOp.opcode === '-') {
var textBank = nextText(csOp.chars); const textBank = nextText(csOp.chars);
var textBankIndex = 0; let textBankIndex = 0;
consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => { consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => {
builder.insert(textBank.substr(textBankIndex, len), attribs + csOp.attribs); builder.insert(textBank.substr(textBankIndex, len), attribs + csOp.attribs);

View file

@ -1,6 +1,7 @@
'use strict';
const fs = require('fs'); const fs = require('fs');
const check = function (path) { const check = (path) => {
const existsSync = fs.statSync || fs.existsSync || path.existsSync; const existsSync = fs.statSync || fs.existsSync || path.existsSync;
let result; let result;

View file

@ -1,3 +1,4 @@
'use strict';
/** /**
* Helpers to manipulate promises (like async but for promises). * Helpers to manipulate promises (like async but for promises).
*/ */

View file

@ -1,10 +1,10 @@
'use strict';
/** /**
* Generates a random String with the given length. Is needed to generate the Author, Group, readonly, session Ids * Generates a random String with the given length. Is needed to generate the
* Author, Group, readonly, session Ids
*/ */
const crypto = require('crypto'); const crypto = require('crypto');
const randomString = function (len) { const randomString = (len) => crypto.randomBytes(len).toString('hex');
return crypto.randomBytes(len).toString('hex');
};
module.exports = randomString; module.exports = randomString;

View file

@ -19,7 +19,6 @@
, "gritter.js" , "gritter.js"
, "$js-cookie/src/js.cookie.js" , "$js-cookie/src/js.cookie.js"
, "$tinycon/tinycon.js" , "$tinycon/tinycon.js"
, "excanvas.js"
, "farbtastic.js" , "farbtastic.js"
, "skin_variants.js" , "skin_variants.js"
, "socketio.js" , "socketio.js"

View file

@ -1,16 +1,10 @@
'use strict';
/** /**
* The Toolbar Module creates and renders the toolbars and buttons * The Toolbar Module creates and renders the toolbars and buttons
*/ */
const _ = require('underscore'); const _ = require('underscore');
let tagAttributes;
let tag;
let Button;
let ButtonsGroup;
let Separator;
let defaultButtonAttributes;
let removeItem;
removeItem = function (array, what) { const removeItem = (array, what) => {
let ax; let ax;
while ((ax = array.indexOf(what)) !== -1) { while ((ax = array.indexOf(what)) !== -1) {
array.splice(ax, 1); array.splice(ax, 1);
@ -18,15 +12,13 @@ removeItem = function (array, what) {
return array; return array;
}; };
defaultButtonAttributes = function (name, overrides) { const defaultButtonAttributes = (name, overrides) => ({
return {
command: name, command: name,
localizationId: `pad.toolbar.${name}.title`, localizationId: `pad.toolbar.${name}.title`,
class: `buttonicon buttonicon-${name}`, class: `buttonicon buttonicon-${name}`,
}; });
};
tag = function (name, attributes, contents) { const tag = (name, attributes, contents) => {
const aStr = tagAttributes(attributes); const aStr = tagAttributes(attributes);
if (_.isString(contents) && contents.length > 0) { if (_.isString(contents) && contents.length > 0) {
@ -36,7 +28,7 @@ tag = function (name, attributes, contents) {
} }
}; };
tagAttributes = function (attributes) { const tagAttributes = (attributes) => {
attributes = _.reduce(attributes || {}, (o, val, name) => { attributes = _.reduce(attributes || {}, (o, val, name) => {
if (!_.isUndefined(val)) { if (!_.isUndefined(val)) {
o[name] = val; o[name] = val;
@ -47,7 +39,7 @@ tagAttributes = function (attributes) {
return ` ${_.map(attributes, (val, name) => `${name}="${_.escape(val)}"`).join(' ')}`; return ` ${_.map(attributes, (val, name) => `${name}="${_.escape(val)}"`).join(' ')}`;
}; };
ButtonsGroup = function () { const ButtonsGroup = function () {
this.buttons = []; this.buttons = [];
}; };
@ -65,9 +57,9 @@ ButtonsGroup.prototype.addButton = function (button) {
}; };
ButtonsGroup.prototype.render = function () { ButtonsGroup.prototype.render = function () {
if (this.buttons && this.buttons.length == 1) { if (this.buttons && this.buttons.length === 1) {
this.buttons[0].grouping = ''; this.buttons[0].grouping = '';
} else { } else if (this.buttons && this.buttons.length > 1) {
_.first(this.buttons).grouping = 'grouped-left'; _.first(this.buttons).grouping = 'grouped-left';
_.last(this.buttons).grouping = 'grouped-right'; _.last(this.buttons).grouping = 'grouped-right';
_.each(this.buttons.slice(1, -1), (btn) => { _.each(this.buttons.slice(1, -1), (btn) => {
@ -80,11 +72,11 @@ ButtonsGroup.prototype.render = function () {
}).join('\n'); }).join('\n');
}; };
Button = function (attributes) { const Button = function (attributes) {
this.attributes = attributes; this.attributes = attributes;
}; };
Button.load = function (btnName) { Button.load = (btnName) => {
const button = module.exports.availableButtons[btnName]; const button = module.exports.availableButtons[btnName];
try { try {
if (button.constructor === Button || button.constructor === SelectButton) { if (button.constructor === Button || button.constructor === SelectButton) {
@ -108,14 +100,17 @@ _.extend(Button.prototype, {
}; };
return tag('li', liAttributes, return tag('li', liAttributes,
tag('a', {'class': this.grouping, 'data-l10n-id': this.attributes.localizationId}, tag('a', {'class': this.grouping, 'data-l10n-id': this.attributes.localizationId},
tag('button', {'class': ` ${this.attributes.class}`, 'data-l10n-id': this.attributes.localizationId}) tag('button', {
'class': ` ${this.attributes.class}`,
'data-l10n-id': this.attributes.localizationId,
})
) )
); );
}, },
}); });
var SelectButton = function (attributes) { const SelectButton = function (attributes) {
this.attributes = attributes; this.attributes = attributes;
this.options = []; this.options = [];
}; };
@ -155,7 +150,7 @@ _.extend(SelectButton.prototype, Button.prototype, {
}, },
}); });
Separator = function () {}; const Separator = function () {};
Separator.prototype.render = function () { Separator.prototype.render = function () {
return tag('li', {class: 'separator'}); return tag('li', {class: 'separator'});
}; };
@ -235,15 +230,11 @@ module.exports = {
this.availableButtons[buttonName] = buttonInfo; this.availableButtons[buttonName] = buttonInfo;
}, },
button(attributes) { button: (attributes) => new Button(attributes),
return new Button(attributes);
}, separator: () => (new Separator()).render(),
separator() {
return (new Separator()).render(); selectButton: (attributes) => new SelectButton(attributes),
},
selectButton(attributes) {
return new SelectButton(attributes);
},
/* /*
* Valid values for whichMenu: 'left' | 'right' | 'timeslider-right' * Valid values for whichMenu: 'left' | 'right' | 'timeslider-right'
@ -271,7 +262,8 @@ module.exports = {
* sufficient to visit a single read only pad to cause the disappearence * sufficient to visit a single read only pad to cause the disappearence
* of the star button from all the pads. * of the star button from all the pads.
*/ */
if ((buttons[0].indexOf('savedrevision') === -1) && (whichMenu === 'right') && (page === 'pad')) { if ((buttons[0].indexOf('savedrevision') === -1) &&
(whichMenu === 'right') && (page === 'pad')) {
buttons[0].push('savedrevision'); buttons[0].push('savedrevision');
} }
} }

2160
src/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -65,31 +65,31 @@
"security": "1.0.0", "security": "1.0.0",
"semver": "5.6.0", "semver": "5.6.0",
"slide": "1.1.6", "slide": "1.1.6",
"socket.io": "^2.3.0", "socket.io": "^2.4.1",
"terser": "^4.7.0", "terser": "^4.7.0",
"threads": "^1.4.0", "threads": "^1.4.0",
"tiny-worker": "^2.3.0", "tiny-worker": "^2.3.0",
"tinycon": "0.0.1", "tinycon": "0.0.1",
"ueberdb2": "^1.2.3", "ueberdb2": "^1.2.5",
"underscore": "1.8.3", "underscore": "1.8.3",
"unorm": "1.4.1" "unorm": "1.4.1",
"wtfnode": "^0.8.4"
}, },
"bin": { "bin": {
"etherpad-lite": "node/server.js" "etherpad-lite": "node/server.js"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^7.15.0", "eslint": "^7.18.0",
"eslint-config-etherpad": "^1.0.20", "eslint-config-etherpad": "^1.0.24",
"eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-mocha": "^8.0.0", "eslint-plugin-mocha": "^8.0.0",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-prefer-arrow": "^1.2.2", "eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0", "eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
"etherpad-cli-client": "0.0.9", "etherpad-cli-client": "0.0.9",
"mocha": "7.1.2", "mocha": "7.1.2",
"mocha-froth": "^0.2.10", "mocha-froth": "^0.2.10",
"nyc": "15.0.1",
"set-cookie-parser": "^2.4.6", "set-cookie-parser": "^2.4.6",
"sinon": "^9.2.0", "sinon": "^9.2.0",
"superagent": "^3.8.3", "superagent": "^3.8.3",
@ -101,7 +101,6 @@
"/static/js/admin/jquery.autosize.js", "/static/js/admin/jquery.autosize.js",
"/static/js/admin/minify.json.js", "/static/js/admin/minify.json.js",
"/static/js/browser.js", "/static/js/browser.js",
"/static/js/excanvas.js",
"/static/js/farbtastic.js", "/static/js/farbtastic.js",
"/static/js/gritter.js", "/static/js/gritter.js",
"/static/js/html10n.js", "/static/js/html10n.js",
@ -140,7 +139,7 @@
"root": true "root": true
}, },
"engines": { "engines": {
"node": ">=10.13.0", "node": "^10.17.0 || >=11.14.0",
"npm": ">=5.5.1" "npm": ">=5.5.1"
}, },
"repository": { "repository": {
@ -149,8 +148,8 @@
}, },
"scripts": { "scripts": {
"lint": "eslint .", "lint": "eslint .",
"test": "nyc mocha --timeout 30000 --recursive ../tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", "test": "mocha --timeout 120000 --recursive ../tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
"test-container": "nyc mocha --timeout 5000 ../tests/container/specs/api" "test-container": "mocha --timeout 5000 ../tests/container/specs/api"
}, },
"version": "1.8.7", "version": "1.8.7",
"license": "Apache-2.0" "license": "Apache-2.0"

View file

@ -1,3 +1,5 @@
'use strict';
const Changeset = require('./Changeset'); const Changeset = require('./Changeset');
const ChangesetUtils = require('./ChangesetUtils'); const ChangesetUtils = require('./ChangesetUtils');
const _ = require('./underscore'); const _ = require('./underscore');
@ -72,10 +74,12 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
const rowChangeset = this._setAttributesOnRangeByLine(row, startCol, endCol, attribs); const rowChangeset = this._setAttributesOnRangeByLine(row, startCol, endCol, attribs);
// compose changesets of all rows into a single changeset, as the range might not be continuous // compose changesets of all rows into a single changeset
// as the range might not be continuous
// due to the presence of line markers on the rows // due to the presence of line markers on the rows
if (allChangesets) { if (allChangesets) {
allChangesets = Changeset.compose(allChangesets.toString(), rowChangeset.toString(), this.rep.apool); allChangesets = Changeset.compose(
allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
} else { } else {
allChangesets = rowChangeset; allChangesets = rowChangeset;
} }
@ -118,7 +122,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
_setAttributesOnRangeByLine(row, startCol, endCol, attribs) { _setAttributesOnRangeByLine(row, startCol, endCol, attribs) {
const builder = Changeset.builder(this.rep.lines.totalWidth()); const builder = Changeset.builder(this.rep.lines.totalWidth());
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]); ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
ChangesetUtils.buildKeepRange(this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool); ChangesetUtils.buildKeepRange(
this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool);
return builder; return builder;
}, },
@ -127,9 +132,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
@param lineNum: the number of the line @param lineNum: the number of the line
*/ */
lineHasMarker(lineNum) { lineHasMarker(lineNum) {
const that = this; return lineAttributes.find(
(attribute) => this.getAttributeOnLine(lineNum, attribute) !== '') !== undefined;
return _.find(lineAttributes, (attribute) => that.getAttributeOnLine(lineNum, attribute) != '') !== undefined;
}, },
/* /*
@ -184,7 +188,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
if (!(rep.selStart && rep.selEnd)) return; if (!(rep.selStart && rep.selEnd)) return;
// If we're looking for the caret attribute not the selection // If we're looking for the caret attribute not the selection
// has the user already got a selection or is this purely a caret location? // has the user already got a selection or is this purely a caret location?
const isNotSelection = (rep.selStart[0] == rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]); const isNotSelection = (rep.selStart[0] === rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]);
if (isNotSelection) { if (isNotSelection) {
if (prevChar) { if (prevChar) {
// If it's not the start of the line // If it's not the start of the line
@ -198,21 +202,18 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
[attributeName, 'true'], [attributeName, 'true'],
], rep.apool); ], rep.apool);
const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`);
function hasIt(attribs) { const hasIt = (attribs) => withItRegex.test(attribs);
return withItRegex.test(attribs);
}
return rangeHasAttrib(rep.selStart, rep.selEnd); const rangeHasAttrib = (selStart, selEnd) => {
function rangeHasAttrib(selStart, selEnd) {
// if range is collapsed -> no attribs in range // if range is collapsed -> no attribs in range
if (selStart[1] == selEnd[1] && selStart[0] == selEnd[0]) return false; if (selStart[1] === selEnd[1] && selStart[0] === selEnd[0]) return false;
if (selStart[0] != selEnd[0]) { // -> More than one line selected if (selStart[0] !== selEnd[0]) { // -> More than one line selected
var hasAttrib = true; let hasAttrib = true;
// from selStart to the end of the first line // from selStart to the end of the first line
hasAttrib = hasAttrib && rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]); hasAttrib = hasAttrib && rangeHasAttrib(
selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]);
// for all lines in between // for all lines in between
for (let n = selStart[0] + 1; n < selEnd[0]; n++) { for (let n = selStart[0] + 1; n < selEnd[0]; n++) {
@ -230,7 +231,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
const lineNum = selStart[0]; const lineNum = selStart[0];
const start = selStart[1]; const start = selStart[1];
const end = selEnd[1]; const end = selEnd[1];
var hasAttrib = true; let hasAttrib = true;
// Iterate over attribs on this line // Iterate over attribs on this line
@ -244,7 +245,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
if (!hasIt(op.attribs)) { if (!hasIt(op.attribs)) {
// does op overlap selection? // does op overlap selection?
if (!(opEndInLine <= start || opStartInLine >= end)) { if (!(opEndInLine <= start || opStartInLine >= end)) {
hasAttrib = false; // since it's overlapping but hasn't got the attrib -> range hasn't got it // since it's overlapping but hasn't got the attrib -> range hasn't got it
hasAttrib = false;
break; break;
} }
} }
@ -252,7 +254,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
} }
return hasAttrib; return hasAttrib;
} };
return rangeHasAttrib(rep.selStart, rep.selEnd);
}, },
/* /*
@ -349,13 +352,13 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
const hasMarker = this.lineHasMarker(lineNum); const hasMarker = this.lineHasMarker(lineNum);
let found = false; let found = false;
const attribs = _(this.getAttributesOnLine(lineNum)).map(function (attrib) { const attribs = this.getAttributesOnLine(lineNum).map((attrib) => {
if (attrib[0] === attributeName && (!attributeValue || attrib[0] === attributeValue)) { if (attrib[0] === attributeName && (!attributeValue || attrib[0] === attributeValue)) {
found = true; found = true;
return [attributeName, '']; return [attrib[0], ''];
} else if (attrib[0] === 'author') { } else if (attrib[0] === 'author') {
// update last author to make changes to line attributes on this line // update last author to make changes to line attributes on this line
return [attributeName, this.author]; return [attrib[0], this.author];
} }
return attrib; return attrib;
}); });
@ -373,7 +376,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
if (hasMarker && !countAttribsWithMarker) { if (hasMarker && !countAttribsWithMarker) {
ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]); ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
} else { } else {
ChangesetUtils.buildKeepRange(this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool); ChangesetUtils.buildKeepRange(
this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool);
} }
return this.applyChangeset(builder); return this.applyChangeset(builder);
@ -394,7 +398,9 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
}, },
hasAttributeOnSelectionOrCaretPosition(attributeName) { hasAttributeOnSelectionOrCaretPosition(attributeName) {
const hasSelection = ((this.rep.selStart[0] !== this.rep.selEnd[0]) || (this.rep.selEnd[1] !== this.rep.selStart[1])); const hasSelection = (
(this.rep.selStart[0] !== this.rep.selEnd[0]) || (this.rep.selEnd[1] !== this.rep.selStart[1])
);
let hasAttrib; let hasAttrib;
if (hasSelection) { if (hasSelection) {
hasAttrib = this.getAttributeOnSelection(attributeName); hasAttrib = this.getAttributeOnSelection(attributeName);

View file

@ -1,3 +1,5 @@
'use strict';
/** /**
* This code is mostly from the old Etherpad. Please help us to comment this code. * This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it. * This helps other people to understand this code better and helps them to improve it.
@ -22,33 +24,23 @@
const Security = require('./security'); const Security = require('./security');
function isNodeText(node) { const isNodeText = (node) => (node.nodeType === 3);
return (node.nodeType == 3);
}
function object(o) { const getAssoc = (obj, name) => obj[`_magicdom_${name}`];
const f = function () {};
f.prototype = o;
return new f();
}
function getAssoc(obj, name) { const setAssoc = (obj, name, value) => {
return obj[`_magicdom_${name}`];
}
function setAssoc(obj, name, value) {
// note that in IE designMode, properties of a node can get // note that in IE designMode, properties of a node can get
// copied to new nodes that are spawned during editing; also, // copied to new nodes that are spawned during editing; also,
// properties representable in HTML text can survive copy-and-paste // properties representable in HTML text can survive copy-and-paste
obj[`_magicdom_${name}`] = value; obj[`_magicdom_${name}`] = value;
} };
// "func" is a function over 0..(numItems-1) that is monotonically // "func" is a function over 0..(numItems-1) that is monotonically
// "increasing" with index (false, then true). Finds the boundary // "increasing" with index (false, then true). Finds the boundary
// between false and true, a number between 0 and numItems inclusive. // between false and true, a number between 0 and numItems inclusive.
function binarySearch(numItems, func) { const binarySearch = (numItems, func) => {
if (numItems < 1) return 0; if (numItems < 1) return 0;
if (func(0)) return 0; if (func(0)) return 0;
if (!func(numItems - 1)) return numItems; if (!func(numItems - 1)) return numItems;
@ -60,22 +52,19 @@ function binarySearch(numItems, func) {
else low = x; else low = x;
} }
return high; return high;
} };
function binarySearchInfinite(expectedLength, func) { const binarySearchInfinite = (expectedLength, func) => {
let i = 0; let i = 0;
while (!func(i)) i += expectedLength; while (!func(i)) i += expectedLength;
return binarySearch(i, func); return binarySearch(i, func);
} };
function htmlPrettyEscape(str) { const htmlPrettyEscape = (str) => Security.escapeHTML(str).replace(/\r?\n/g, '\\n');
return Security.escapeHTML(str).replace(/\r?\n/g, '\\n');
}
const noop = function () {}; const noop = () => {};
exports.isNodeText = isNodeText; exports.isNodeText = isNodeText;
exports.object = object;
exports.getAssoc = getAssoc; exports.getAssoc = getAssoc;
exports.setAssoc = setAssoc; exports.setAssoc = setAssoc;
exports.binarySearch = binarySearch; exports.binarySearch = binarySearch;

View file

@ -1139,7 +1139,7 @@ function Ace2Inner() {
lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode); lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode);
if (firstDirtyNode && lastDirtyNode) { if (firstDirtyNode && lastDirtyNode) {
const cc = makeContentCollector(isStyled, browser, rep.apool, null, className2Author); const cc = makeContentCollector(isStyled, browser, rep.apool, className2Author);
cc.notifySelection(selection); cc.notifySelection(selection);
const dirtyNodes = []; const dirtyNodes = [];
for (let n = firstDirtyNode; n && for (let n = firstDirtyNode; n &&

View file

@ -32,30 +32,31 @@ const hooks = require('./pluginfw/hooks');
const sanitizeUnicode = (s) => UNorm.nfc(s); const sanitizeUnicode = (s) => UNorm.nfc(s);
const makeContentCollector = (collectStyles, abrowser, apool, domInterface, className2Author) => { // This file is used both in browsers and with cheerio in Node.js (for importing HTML). Cheerio's
const dom = domInterface || { // Node-like objects are not 100% API compatible with the DOM specification; the following functions
isNodeText: (n) => n.nodeType === 3, // abstract away the differences.
nodeTagName: (n) => n.tagName,
nodeValue: (n) => n.nodeValue, // .nodeType works with DOM and cheerio 0.22.0, but cheerio 0.22.0 does not provide the Node.*_NODE
nodeNumChildren: (n) => { // constants so they cannot be used here.
if (n.childNodes == null) return 0; const isElementNode = (n) => n.nodeType === 1; // Node.ELEMENT_NODE
return n.childNodes.length; const isTextNode = (n) => n.nodeType === 3; // Node.TEXT_NODE
}, // .tagName works with DOM and cheerio 0.22.0, but:
nodeChild: (n, i) => { // * With DOM, .tagName is an uppercase string.
if (n.childNodes.item == null) { // * With cheerio 0.22.0, .tagName is a lowercase string.
return n.childNodes[i]; // For consistency, this function always returns a lowercase string.
} const tagName = (n) => n.tagName && n.tagName.toLowerCase();
return n.childNodes.item(i); // .childNodes works with DOM and cheerio 0.22.0, except in cheerio the .childNodes property does
}, // not exist on text nodes (and maybe other non-element nodes).
nodeProp: (n, p) => n[p], const childNodes = (n) => n.childNodes || [];
nodeAttr: (n, a) => { const getAttribute = (n, a) => {
// .getAttribute() works with DOM but not with cheerio 0.22.0.
if (n.getAttribute != null) return n.getAttribute(a); if (n.getAttribute != null) return n.getAttribute(a);
// .attribs[] works with cheerio 0.22.0 but not with DOM.
if (n.attribs != null) return n.attribs[a]; if (n.attribs != null) return n.attribs[a];
return null; return null;
},
optNodeInnerHTML: (n) => n.innerHTML,
}; };
const makeContentCollector = (collectStyles, abrowser, apool, className2Author) => {
const _blockElems = { const _blockElems = {
div: 1, div: 1,
p: 1, p: 1,
@ -67,7 +68,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
_blockElems[element] = 1; _blockElems[element] = 1;
}); });
const isBlockElement = (n) => !!_blockElems[(dom.nodeTagName(n) || '').toLowerCase()]; const isBlockElement = (n) => !!_blockElems[tagName(n) || ''];
const textify = (str) => sanitizeUnicode( const textify = (str) => sanitizeUnicode(
str.replace(/(\n | \n)/g, ' ') str.replace(/(\n | \n)/g, ' ')
@ -75,7 +76,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
.replace(/\xa0/g, ' ') .replace(/\xa0/g, ' ')
.replace(/\t/g, ' ')); .replace(/\t/g, ' '));
const getAssoc = (node, name) => dom.nodeProp(node, `_magicdom_${name}`); const getAssoc = (node, name) => node[`_magicdom_${name}`];
const lines = (() => { const lines = (() => {
const textArray = []; const textArray = [];
@ -123,13 +124,17 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
let selEnd = [-1, -1]; let selEnd = [-1, -1];
const _isEmpty = (node, state) => { const _isEmpty = (node, state) => {
// consider clean blank lines pasted in IE to be empty // consider clean blank lines pasted in IE to be empty
if (dom.nodeNumChildren(node) === 0) return true; if (childNodes(node).length === 0) return true;
if (dom.nodeNumChildren(node) === 1 && if (childNodes(node).length === 1 &&
getAssoc(node, 'shouldBeEmpty') && getAssoc(node, 'shouldBeEmpty') &&
dom.optNodeInnerHTML(node) === '&nbsp;' && // Note: The .innerHTML property exists on DOM Element objects but not on cheerio's
// Element-like objects (cheerio v0.22.0) so this equality check will always be false.
// Cheerio's Element-like objects have no equivalent to .innerHTML. (Cheerio objects have an
// .html() method, but that isn't accessible here.)
node.innerHTML === '&nbsp;' &&
!getAssoc(node, 'unpasted')) { !getAssoc(node, 'unpasted')) {
if (state) { if (state) {
const child = dom.nodeChild(node, 0); const child = childNodes(node)[0];
_reachPoint(child, 0, state); _reachPoint(child, 0, state);
_reachPoint(child, 1, state); _reachPoint(child, 1, state);
} }
@ -149,7 +154,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
}; };
const _reachBlockPoint = (nd, idx, state) => { const _reachBlockPoint = (nd, idx, state) => {
if (!dom.isNodeText(nd)) _reachPoint(nd, idx, state); if (!isTextNode(nd)) _reachPoint(nd, idx, state);
}; };
const _reachPoint = (nd, idx, state) => { const _reachPoint = (nd, idx, state) => {
@ -228,8 +233,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
const _recalcAttribString = (state) => { const _recalcAttribString = (state) => {
const lst = []; const lst = [];
for (const a in state.attribs) { for (const [a, count] of Object.entries(state.attribs)) {
if (state.attribs[a]) { if (!count) continue;
// The following splitting of the attribute name is a workaround // The following splitting of the attribute name is a workaround
// to enable the content collector to store key-value attributes // to enable the content collector to store key-value attributes
// see https://github.com/ether/etherpad-lite/issues/2567 for more information // see https://github.com/ether/etherpad-lite/issues/2567 for more information
@ -248,7 +253,6 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
lst.push([a, 'true']); lst.push([a, 'true']);
} }
} }
}
if (state.authorLevel > 0) { if (state.authorLevel > 0) {
const authorAttrib = ['author', state.author]; const authorAttrib = ['author', state.author];
if (apool.putAttrib(authorAttrib, true) >= 0) { if (apool.putAttrib(authorAttrib, true) >= 0) {
@ -316,25 +320,15 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
const startLine = lines.length() - 1; const startLine = lines.length() - 1;
_reachBlockPoint(node, 0, state); _reachBlockPoint(node, 0, state);
if (dom.isNodeText(node)) { if (isTextNode(node)) {
let txt = dom.nodeValue(node); const tname = getAttribute(node.parentNode, 'name');
const tname = dom.nodeAttr(node.parentNode, 'name'); const context = {cc: this, state, tname, node, text: node.nodeValue};
// Hook functions may either return a string (deprecated) or modify context.text. If any hook
const txtFromHook = hooks.callAll('collectContentLineText', { // function modifies context.text then all returned strings are ignored. If no hook functions
cc: this, // modify context.text, the first hook function to return a string wins.
state, const [hookTxt] =
tname, hooks.callAll('collectContentLineText', context).filter((s) => typeof s === 'string');
node, let txt = context.text === node.nodeValue && hookTxt != null ? hookTxt : context.text;
text: txt,
styl: null,
cls: null,
});
if (typeof (txtFromHook) === 'object') {
txt = dom.nodeValue(node);
} else if (txtFromHook) {
txt = txtFromHook;
}
let rest = ''; let rest = '';
let x = 0; // offset into original text let x = 0; // offset into original text
@ -384,8 +378,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
cc.startNewLine(state); cc.startNewLine(state);
} }
} }
} else { } else if (isElementNode(node)) {
const tname = (dom.nodeTagName(node) || '').toLowerCase(); const tname = tagName(node) || '';
if (tname === 'img') { if (tname === 'img') {
hooks.callAll('collectContentImage', { hooks.callAll('collectContentImage', {
@ -403,8 +397,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
if (tname === 'br') { if (tname === 'br') {
this.breakLine = true; this.breakLine = true;
const tvalue = dom.nodeAttr(node, 'value'); const tvalue = getAttribute(node, 'value');
const induceLineBreak = hooks.callAll('collectContentLineBreak', { const [startNewLine = true] = hooks.callAll('collectContentLineBreak', {
cc: this, cc: this,
state, state,
tname, tname,
@ -412,17 +406,14 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
styl: null, styl: null,
cls: null, cls: null,
}); });
const startNewLine = (
typeof (induceLineBreak) === 'object' &&
induceLineBreak.length === 0) ? true : induceLineBreak[0];
if (startNewLine) { if (startNewLine) {
cc.startNewLine(state); cc.startNewLine(state);
} }
} else if (tname === 'script' || tname === 'style') { } else if (tname === 'script' || tname === 'style') {
// ignore // ignore
} else if (!isEmpty) { } else if (!isEmpty) {
let styl = dom.nodeAttr(node, 'style'); let styl = getAttribute(node, 'style');
let cls = dom.nodeAttr(node, 'class'); let cls = getAttribute(node, 'class');
let isPre = (tname === 'pre'); let isPre = (tname === 'pre');
if ((!isPre) && abrowser && abrowser.safari) { if ((!isPre) && abrowser && abrowser.safari) {
isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl)); isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl));
@ -469,26 +460,23 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
cc.doAttrib(state, 'strikethrough'); cc.doAttrib(state, 'strikethrough');
} }
if (tname === 'ul' || tname === 'ol') { if (tname === 'ul' || tname === 'ol') {
let type = node.attribs ? node.attribs.class : null; let type = getAttribute(node, 'class');
const rr = cls && /(?:^| )list-([a-z]+[0-9]+)\b/.exec(cls); const rr = cls && /(?:^| )list-([a-z]+[0-9]+)\b/.exec(cls);
// lists do not need to have a type, so before we make a wrong guess // lists do not need to have a type, so before we make a wrong guess
// check if we find a better hint within the node's children // check if we find a better hint within the node's children
if (!rr && !type) { if (!rr && !type) {
for (const i in node.children) { for (const child of childNodes(node)) {
if (node.children[i] && node.children[i].name === 'ul') { if (tagName(child) !== 'ul') continue;
type = node.children[i].attribs.class; type = getAttribute(child, 'class');
if (type) { if (type) break;
break;
}
}
} }
} }
if (rr && rr[1]) { if (rr && rr[1]) {
type = rr[1]; type = rr[1];
} else { } else {
if (tname === 'ul') { if (tname === 'ul') {
if ((type && type.match('indent')) || const cls = getAttribute(node, 'class');
(node.attribs && node.attribs.class && node.attribs.class.match('indent'))) { if ((type && type.match('indent')) || (cls && cls.match('indent'))) {
type = 'indent'; type = 'indent';
} else { } else {
type = 'bullet'; type = 'bullet';
@ -503,7 +491,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
// This has undesirable behavior in Chrome but is right in other browsers. // This has undesirable behavior in Chrome but is right in other browsers.
// See https://github.com/ether/etherpad-lite/issues/2412 for reasoning // See https://github.com/ether/etherpad-lite/issues/2412 for reasoning
if (!abrowser.chrome) oldListTypeOrNull = (_enterList(state, undefined) || 'none'); if (!abrowser.chrome) oldListTypeOrNull = (_enterList(state, undefined) || 'none');
} else if ((tname === 'li')) { } else if (tname === 'li') {
state.lineAttributes.start = state.start || 0; state.lineAttributes.start = state.start || 0;
_recalcAttribString(state); _recalcAttribString(state);
if (state.lineAttributes.list.indexOf('number') !== -1) { if (state.lineAttributes.list.indexOf('number') !== -1) {
@ -513,7 +501,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
Note how the <ol> item has to be inside a <li> Note how the <ol> item has to be inside a <li>
Because of this we don't increment the start number Because of this we don't increment the start number
*/ */
if (node.parent && node.parent.name !== 'ol') { if (node.parentNode && tagName(node.parentNode) !== 'ol') {
/* /*
TODO: start number has to increment based on indentLevel(numberX) TODO: start number has to increment based on indentLevel(numberX)
This means we have to build an object IE This means we have to build an object IE
@ -530,7 +518,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
} }
} }
// UL list items never modify the start value. // UL list items never modify the start value.
if (node.parent && node.parent.name === 'ul') { if (node.parentNode && tagName(node.parentNode) === 'ul') {
state.start++; state.start++;
// TODO, this is hacky. // TODO, this is hacky.
// Because if the first item is an UL it will increment a list no? // Because if the first item is an UL it will increment a list no?
@ -559,9 +547,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
} }
} }
const nc = dom.nodeNumChildren(node); for (const c of childNodes(node)) {
for (let i = 0; i < nc; i++) {
const c = dom.nodeChild(node, i);
cc.collectContent(c, state); cc.collectContent(c, state);
} }

View file

@ -1,35 +0,0 @@
// Copyright 2006 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
document.createElement("canvas").getContext||(function(){var s=Math,j=s.round,F=s.sin,G=s.cos,V=s.abs,W=s.sqrt,k=10,v=k/2;function X(){return this.context_||(this.context_=new H(this))}var L=Array.prototype.slice;function Y(b,a){var c=L.call(arguments,2);return function(){return b.apply(a,c.concat(L.call(arguments)))}}var M={init:function(b){if(/MSIE/.test(navigator.userAgent)&&!window.opera){var a=b||document;a.createElement("canvas");a.attachEvent("onreadystatechange",Y(this.init_,this,a))}},init_:function(b){b.namespaces.g_vml_||
b.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml","#default#VML");b.namespaces.g_o_||b.namespaces.add("g_o_","urn:schemas-microsoft-com:office:office","#default#VML");if(!b.styleSheets.ex_canvas_){var a=b.createStyleSheet();a.owningElement.id="ex_canvas_";a.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}g_vml_\\:*{behavior:url(#default#VML)}g_o_\\:*{behavior:url(#default#VML)}"}var c=b.getElementsByTagName("canvas"),d=0;for(;d<c.length;d++)this.initElement(c[d])},
initElement:function(b){if(!b.getContext){b.getContext=X;b.innerHTML="";b.attachEvent("onpropertychange",Z);b.attachEvent("onresize",$);var a=b.attributes;if(a.width&&a.width.specified)b.style.width=a.width.nodeValue+"px";else b.width=b.clientWidth;if(a.height&&a.height.specified)b.style.height=a.height.nodeValue+"px";else b.height=b.clientHeight}return b}};function Z(b){var a=b.srcElement;switch(b.propertyName){case "width":a.style.width=a.attributes.width.nodeValue+"px";a.getContext().clearRect();
break;case "height":a.style.height=a.attributes.height.nodeValue+"px";a.getContext().clearRect();break}}function $(b){var a=b.srcElement;if(a.firstChild){a.firstChild.style.width=a.clientWidth+"px";a.firstChild.style.height=a.clientHeight+"px"}}M.init();var N=[],B=0;for(;B<16;B++){var C=0;for(;C<16;C++)N[B*16+C]=B.toString(16)+C.toString(16)}function I(){return[[1,0,0],[0,1,0],[0,0,1]]}function y(b,a){var c=I(),d=0;for(;d<3;d++){var f=0;for(;f<3;f++){var h=0,g=0;for(;g<3;g++)h+=b[d][g]*a[g][f];c[d][f]=
h}}return c}function O(b,a){a.fillStyle=b.fillStyle;a.lineCap=b.lineCap;a.lineJoin=b.lineJoin;a.lineWidth=b.lineWidth;a.miterLimit=b.miterLimit;a.shadowBlur=b.shadowBlur;a.shadowColor=b.shadowColor;a.shadowOffsetX=b.shadowOffsetX;a.shadowOffsetY=b.shadowOffsetY;a.strokeStyle=b.strokeStyle;a.globalAlpha=b.globalAlpha;a.arcScaleX_=b.arcScaleX_;a.arcScaleY_=b.arcScaleY_;a.lineScale_=b.lineScale_}function P(b){var a,c=1;b=String(b);if(b.substring(0,3)=="rgb"){var d=b.indexOf("(",3),f=b.indexOf(")",d+
1),h=b.substring(d+1,f).split(",");a="#";var g=0;for(;g<3;g++)a+=N[Number(h[g])];if(h.length==4&&b.substr(3,1)=="a")c=h[3]}else a=b;return{color:a,alpha:c}}function aa(b){switch(b){case "butt":return"flat";case "round":return"round";case "square":default:return"square"}}function H(b){this.m_=I();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.fillStyle=this.strokeStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=k*1;this.globalAlpha=1;this.canvas=b;
var a=b.ownerDocument.createElement("div");a.style.width=b.clientWidth+"px";a.style.height=b.clientHeight+"px";a.style.overflow="hidden";a.style.position="absolute";b.appendChild(a);this.element_=a;this.lineScale_=this.arcScaleY_=this.arcScaleX_=1}var i=H.prototype;i.clearRect=function(){this.element_.innerHTML=""};i.beginPath=function(){this.currentPath_=[]};i.moveTo=function(b,a){var c=this.getCoords_(b,a);this.currentPath_.push({type:"moveTo",x:c.x,y:c.y});this.currentX_=c.x;this.currentY_=c.y};
i.lineTo=function(b,a){var c=this.getCoords_(b,a);this.currentPath_.push({type:"lineTo",x:c.x,y:c.y});this.currentX_=c.x;this.currentY_=c.y};i.bezierCurveTo=function(b,a,c,d,f,h){var g=this.getCoords_(f,h),l=this.getCoords_(b,a),e=this.getCoords_(c,d);Q(this,l,e,g)};function Q(b,a,c,d){b.currentPath_.push({type:"bezierCurveTo",cp1x:a.x,cp1y:a.y,cp2x:c.x,cp2y:c.y,x:d.x,y:d.y});b.currentX_=d.x;b.currentY_=d.y}i.quadraticCurveTo=function(b,a,c,d){var f=this.getCoords_(b,a),h=this.getCoords_(c,d),g={x:this.currentX_+
0.6666666666666666*(f.x-this.currentX_),y:this.currentY_+0.6666666666666666*(f.y-this.currentY_)};Q(this,g,{x:g.x+(h.x-this.currentX_)/3,y:g.y+(h.y-this.currentY_)/3},h)};i.arc=function(b,a,c,d,f,h){c*=k;var g=h?"at":"wa",l=b+G(d)*c-v,e=a+F(d)*c-v,m=b+G(f)*c-v,r=a+F(f)*c-v;if(l==m&&!h)l+=0.125;var n=this.getCoords_(b,a),o=this.getCoords_(l,e),q=this.getCoords_(m,r);this.currentPath_.push({type:g,x:n.x,y:n.y,radius:c,xStart:o.x,yStart:o.y,xEnd:q.x,yEnd:q.y})};i.rect=function(b,a,c,d){this.moveTo(b,
a);this.lineTo(b+c,a);this.lineTo(b+c,a+d);this.lineTo(b,a+d);this.closePath()};i.strokeRect=function(b,a,c,d){var f=this.currentPath_;this.beginPath();this.moveTo(b,a);this.lineTo(b+c,a);this.lineTo(b+c,a+d);this.lineTo(b,a+d);this.closePath();this.stroke();this.currentPath_=f};i.fillRect=function(b,a,c,d){var f=this.currentPath_;this.beginPath();this.moveTo(b,a);this.lineTo(b+c,a);this.lineTo(b+c,a+d);this.lineTo(b,a+d);this.closePath();this.fill();this.currentPath_=f};i.createLinearGradient=function(b,
a,c,d){var f=new D("gradient");f.x0_=b;f.y0_=a;f.x1_=c;f.y1_=d;return f};i.createRadialGradient=function(b,a,c,d,f,h){var g=new D("gradientradial");g.x0_=b;g.y0_=a;g.r0_=c;g.x1_=d;g.y1_=f;g.r1_=h;return g};i.drawImage=function(b){var a,c,d,f,h,g,l,e,m=b.runtimeStyle.width,r=b.runtimeStyle.height;b.runtimeStyle.width="auto";b.runtimeStyle.height="auto";var n=b.width,o=b.height;b.runtimeStyle.width=m;b.runtimeStyle.height=r;if(arguments.length==3){a=arguments[1];c=arguments[2];h=g=0;l=d=n;e=f=o}else if(arguments.length==
5){a=arguments[1];c=arguments[2];d=arguments[3];f=arguments[4];h=g=0;l=n;e=o}else if(arguments.length==9){h=arguments[1];g=arguments[2];l=arguments[3];e=arguments[4];a=arguments[5];c=arguments[6];d=arguments[7];f=arguments[8]}else throw Error("Invalid number of arguments");var q=this.getCoords_(a,c),t=[];t.push(" <g_vml_:group",' coordsize="',k*10,",",k*10,'"',' coordorigin="0,0"',' style="width:',10,"px;height:",10,"px;position:absolute;");if(this.m_[0][0]!=1||this.m_[0][1]){var E=[];E.push("M11=",
this.m_[0][0],",","M12=",this.m_[1][0],",","M21=",this.m_[0][1],",","M22=",this.m_[1][1],",","Dx=",j(q.x/k),",","Dy=",j(q.y/k),"");var p=q,z=this.getCoords_(a+d,c),w=this.getCoords_(a,c+f),x=this.getCoords_(a+d,c+f);p.x=s.max(p.x,z.x,w.x,x.x);p.y=s.max(p.y,z.y,w.y,x.y);t.push("padding:0 ",j(p.x/k),"px ",j(p.y/k),"px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",E.join(""),", sizingmethod='clip');")}else t.push("top:",j(q.y/k),"px;left:",j(q.x/k),"px;");t.push(' ">','<g_vml_:image src="',b.src,
'"',' style="width:',k*d,"px;"," height:",k*f,'px;"',' cropleft="',h/n,'"',' croptop="',g/o,'"',' cropright="',(n-h-l)/n,'"',' cropbottom="',(o-g-e)/o,'"'," />","</g_vml_:group>");this.element_.insertAdjacentHTML("BeforeEnd",t.join(""))};i.stroke=function(b){var a=[],c=P(b?this.fillStyle:this.strokeStyle),d=c.color,f=c.alpha*this.globalAlpha;a.push("<g_vml_:shape",' filled="',!!b,'"',' style="position:absolute;width:',10,"px;height:",10,'px;"',' coordorigin="0 0" coordsize="',k*10," ",k*10,'"',' stroked="',
!b,'"',' path="');var h={x:null,y:null},g={x:null,y:null},l=0;for(;l<this.currentPath_.length;l++){var e=this.currentPath_[l];switch(e.type){case "moveTo":a.push(" m ",j(e.x),",",j(e.y));break;case "lineTo":a.push(" l ",j(e.x),",",j(e.y));break;case "close":a.push(" x ");e=null;break;case "bezierCurveTo":a.push(" c ",j(e.cp1x),",",j(e.cp1y),",",j(e.cp2x),",",j(e.cp2y),",",j(e.x),",",j(e.y));break;case "at":case "wa":a.push(" ",e.type," ",j(e.x-this.arcScaleX_*e.radius),",",j(e.y-this.arcScaleY_*e.radius),
" ",j(e.x+this.arcScaleX_*e.radius),",",j(e.y+this.arcScaleY_*e.radius)," ",j(e.xStart),",",j(e.yStart)," ",j(e.xEnd),",",j(e.yEnd));break}if(e){if(h.x==null||e.x<h.x)h.x=e.x;if(g.x==null||e.x>g.x)g.x=e.x;if(h.y==null||e.y<h.y)h.y=e.y;if(g.y==null||e.y>g.y)g.y=e.y}}a.push(' ">');if(b)if(typeof this.fillStyle=="object"){var m=this.fillStyle,r=0,n={x:0,y:0},o=0,q=1;if(m.type_=="gradient"){var t=m.x1_/this.arcScaleX_,E=m.y1_/this.arcScaleY_,p=this.getCoords_(m.x0_/this.arcScaleX_,m.y0_/this.arcScaleY_),
z=this.getCoords_(t,E);r=Math.atan2(z.x-p.x,z.y-p.y)*180/Math.PI;if(r<0)r+=360;if(r<1.0E-6)r=0}else{var p=this.getCoords_(m.x0_,m.y0_),w=g.x-h.x,x=g.y-h.y;n={x:(p.x-h.x)/w,y:(p.y-h.y)/x};w/=this.arcScaleX_*k;x/=this.arcScaleY_*k;var R=s.max(w,x);o=2*m.r0_/R;q=2*m.r1_/R-o}var u=m.colors_;u.sort(function(ba,ca){return ba.offset-ca.offset});var J=u.length,da=u[0].color,ea=u[J-1].color,fa=u[0].alpha*this.globalAlpha,ga=u[J-1].alpha*this.globalAlpha,S=[],l=0;for(;l<J;l++){var T=u[l];S.push(T.offset*q+
o+" "+T.color)}a.push('<g_vml_:fill type="',m.type_,'"',' method="none" focus="100%"',' color="',da,'"',' color2="',ea,'"',' colors="',S.join(","),'"',' opacity="',ga,'"',' g_o_:opacity2="',fa,'"',' angle="',r,'"',' focusposition="',n.x,",",n.y,'" />')}else a.push('<g_vml_:fill color="',d,'" opacity="',f,'" />');else{var K=this.lineScale_*this.lineWidth;if(K<1)f*=K;a.push("<g_vml_:stroke",' opacity="',f,'"',' joinstyle="',this.lineJoin,'"',' miterlimit="',this.miterLimit,'"',' endcap="',aa(this.lineCap),
'"',' weight="',K,'px"',' color="',d,'" />')}a.push("</g_vml_:shape>");this.element_.insertAdjacentHTML("beforeEnd",a.join(""))};i.fill=function(){this.stroke(true)};i.closePath=function(){this.currentPath_.push({type:"close"})};i.getCoords_=function(b,a){var c=this.m_;return{x:k*(b*c[0][0]+a*c[1][0]+c[2][0])-v,y:k*(b*c[0][1]+a*c[1][1]+c[2][1])-v}};i.save=function(){var b={};O(this,b);this.aStack_.push(b);this.mStack_.push(this.m_);this.m_=y(I(),this.m_)};i.restore=function(){O(this.aStack_.pop(),
this);this.m_=this.mStack_.pop()};function ha(b){var a=0;for(;a<3;a++){var c=0;for(;c<2;c++)if(!isFinite(b[a][c])||isNaN(b[a][c]))return false}return true}function A(b,a,c){if(!!ha(a)){b.m_=a;if(c)b.lineScale_=W(V(a[0][0]*a[1][1]-a[0][1]*a[1][0]))}}i.translate=function(b,a){A(this,y([[1,0,0],[0,1,0],[b,a,1]],this.m_),false)};i.rotate=function(b){var a=G(b),c=F(b);A(this,y([[a,c,0],[-c,a,0],[0,0,1]],this.m_),false)};i.scale=function(b,a){this.arcScaleX_*=b;this.arcScaleY_*=a;A(this,y([[b,0,0],[0,a,
0],[0,0,1]],this.m_),true)};i.transform=function(b,a,c,d,f,h){A(this,y([[b,a,0],[c,d,0],[f,h,1]],this.m_),true)};i.setTransform=function(b,a,c,d,f,h){A(this,[[b,a,0],[c,d,0],[f,h,1]],true)};i.clip=function(){};i.arcTo=function(){};i.createPattern=function(){return new U};function D(b){this.type_=b;this.r1_=this.y1_=this.x1_=this.r0_=this.y0_=this.x0_=0;this.colors_=[]}D.prototype.addColorStop=function(b,a){a=P(a);this.colors_.push({offset:b,color:a.color,alpha:a.alpha})};function U(){}G_vmlCanvasManager=
M;CanvasRenderingContext2D=H;CanvasGradient=D;CanvasPattern=U})();

View file

@ -1,6 +1,8 @@
'use strict';
// Farbtastic 2.0 alpha // Farbtastic 2.0 alpha
// Original can be found at:
// https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/src/farbtastic.js
// Licensed under the terms of the GNU General Public License v2.0:
// https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/LICENSE.txt
// edited by Sebastian Castro <sebastian.castro@protonmail.com> on 2020-04-06 // edited by Sebastian Castro <sebastian.castro@protonmail.com> on 2020-04-06
(function ($) { (function ($) {
@ -84,16 +86,6 @@ $._farbtastic = function (container, options) {
} }
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
//excanvas-compatible building of canvases
fb._makeCanvas = function(className){
var c = document.createElement('canvas');
if (!c.getContext) { // excanvas hack
c = window.G_vmlCanvasManager.initElement(c);
c.getContext(); //this creates the excanvas children
}
$(c).addClass(className);
return c;
}
/** /**
* Initialize the color picker widget. * Initialize the color picker widget.
@ -109,15 +101,27 @@ $._farbtastic = function (container, options) {
.html( .html(
'<div class="farbtastic" style="position: relative">' + '<div class="farbtastic" style="position: relative">' +
'<div class="farbtastic-solid"></div>' + '<div class="farbtastic-solid"></div>' +
'<canvas class="farbtastic-mask"></canvas>' +
'<canvas class="farbtastic-overlay"></canvas>' +
'</div>' '</div>'
) )
.children('.farbtastic')
.append(fb._makeCanvas('farbtastic-mask'))
.append(fb._makeCanvas('farbtastic-overlay'))
.end()
.find('*').attr(dim).css(dim).end() .find('*').attr(dim).css(dim).end()
.find('div>*').css('position', 'absolute'); .find('div>*').css('position', 'absolute');
// IE Fix: Recreate canvas elements with doc.createElement and excanvas.
browser.msie && $('canvas', container).each(function () {
// Fetch info.
var attr = { 'class': $(this).attr('class'), style: this.getAttribute('style') },
e = document.createElement('canvas');
// Replace element.
$(this).before($(e).attr(attr)).remove();
// Init with explorerCanvas.
G_vmlCanvasManager && G_vmlCanvasManager.initElement(e);
// Set explorerCanvas elements dimensions and absolute positioning.
$(e).attr(dim).css(dim).css('position', 'absolute')
.find('*').attr(dim).css(dim);
});
// Determine layout // Determine layout
fb.radius = (options.width - options.wheelWidth) / 2 - 1; fb.radius = (options.width - options.wheelWidth) / 2 - 1;
fb.square = Math.floor((fb.radius - options.wheelWidth / 2) * 0.7) - 1; fb.square = Math.floor((fb.radius - options.wheelWidth / 2) * 0.7) - 1;
@ -160,12 +164,12 @@ $._farbtastic = function (container, options) {
m.lineWidth = w / r; m.lineWidth = w / r;
m.scale(r, r); m.scale(r, r);
// Each segment goes from angle1 to angle2. // Each segment goes from angle1 to angle2.
for (let i = 0; i <= n; ++i) { for (var i = 0; i <= n; ++i) {
var d2 = i / n, var d2 = i / n,
angle2 = d2 * Math.PI * 2, angle2 = d2 * Math.PI * 2,
// Endpoints // Endpoints
x1 = Math.sin(angle1), y1 = -Math.cos(angle1); x1 = Math.sin(angle1), y1 = -Math.cos(angle1);
const x2 = Math.sin(angle2), y2 = -Math.cos(angle2), x2 = Math.sin(angle2), y2 = -Math.cos(angle2),
// Midpoint chosen so that the endpoints are tangent to the circle. // Midpoint chosen so that the endpoints are tangent to the circle.
am = (angle1 + angle2) / 2, am = (angle1 + angle2) / 2,
tan = 1 / Math.cos((angle2 - angle1) / 2), tan = 1 / Math.cos((angle2 - angle1) / 2),
@ -173,6 +177,26 @@ $._farbtastic = function (container, options) {
// New color // New color
color2 = fb.pack(fb.HSLToRGB([d2, 1, 0.5])); color2 = fb.pack(fb.HSLToRGB([d2, 1, 0.5]));
if (i > 0) { if (i > 0) {
if (browser.msie) {
// IE's gradient calculations mess up the colors. Correct along the diagonals.
var corr = (1 + Math.min(Math.abs(Math.tan(angle1)), Math.abs(Math.tan(Math.PI / 2 - angle1)))) / n;
color1 = fb.pack(fb.HSLToRGB([d1 - 0.15 * corr, 1, 0.5]));
color2 = fb.pack(fb.HSLToRGB([d2 + 0.15 * corr, 1, 0.5]));
// Create gradient fill between the endpoints.
var grad = m.createLinearGradient(x1, y1, x2, y2);
grad.addColorStop(0, color1);
grad.addColorStop(1, color2);
m.fillStyle = grad;
// Draw quadratic curve segment as a fill.
var r1 = (r + w / 2) / r, r2 = (r - w / 2) / r; // inner/outer radius.
m.beginPath();
m.moveTo(x1 * r1, y1 * r1);
m.quadraticCurveTo(xm * r1, ym * r1, x2 * r1, y2 * r1);
m.lineTo(x2 * r2, y2 * r2);
m.quadraticCurveTo(xm * r2, ym * r2, x1 * r2, y1 * r2);
m.fill();
}
else {
// Create gradient fill between the endpoints. // Create gradient fill between the endpoints.
var grad = m.createLinearGradient(x1, y1, x2, y2); var grad = m.createLinearGradient(x1, y1, x2, y2);
grad.addColorStop(0, color1); grad.addColorStop(0, color1);
@ -184,6 +208,7 @@ $._farbtastic = function (container, options) {
m.quadraticCurveTo(xm, ym, x2, y2); m.quadraticCurveTo(xm, ym, x2, y2);
m.stroke(); m.stroke();
} }
}
// Prevent seams where curves join. // Prevent seams where curves join.
angle1 = angle2 - nudge; color1 = color2; d1 = d2; angle1 = angle2 - nudge; color1 = color2; d1 = d2;
} }
@ -226,7 +251,7 @@ $._farbtastic = function (container, options) {
var ctx = buffer.getContext('2d'); var ctx = buffer.getContext('2d');
var frame = ctx.getImageData(0, 0, sz + 1, sz + 1); var frame = ctx.getImageData(0, 0, sz + 1, sz + 1);
let i = 0; var i = 0;
calculateMask(sz, sz, function (x, y, c, a) { calculateMask(sz, sz, function (x, y, c, a) {
frame.data[i++] = frame.data[i++] = frame.data[i++] = c * 255; frame.data[i++] = frame.data[i++] = frame.data[i++] = c * 255;
frame.data[i++] = a * 255; frame.data[i++] = a * 255;
@ -301,7 +326,7 @@ $._farbtastic = function (container, options) {
// Update the overlay canvas. // Update the overlay canvas.
fb.ctxOverlay.clearRect(-fb.mid, -fb.mid, sz, sz); fb.ctxOverlay.clearRect(-fb.mid, -fb.mid, sz, sz);
for (let i in circles) { for (i in circles) {
var c = circles[i]; var c = circles[i];
fb.ctxOverlay.lineWidth = c.lw; fb.ctxOverlay.lineWidth = c.lw;
fb.ctxOverlay.strokeStyle = c.c; fb.ctxOverlay.strokeStyle = c.c;

View file

@ -93,11 +93,8 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
} else if (linestylefilter.ATTRIB_CLASSES[key]) { } else if (linestylefilter.ATTRIB_CLASSES[key]) {
classes += ` ${linestylefilter.ATTRIB_CLASSES[key]}`; classes += ` ${linestylefilter.ATTRIB_CLASSES[key]}`;
} else { } else {
classes += hooks.callAllStr('aceAttribsToClasses', { const results = hooks.callAll('aceAttribsToClasses', {linestylefilter, key, value});
linestylefilter, classes += ` ${results.join(' ')}`;
key,
value,
}, ' ', ' ', '');
} }
} }
} }

View file

@ -28,7 +28,6 @@ let socket;
// assigns to the global `$` and augments it with plugins. // assigns to the global `$` and augments it with plugins.
require('./jquery'); require('./jquery');
require('./farbtastic'); require('./farbtastic');
require('./excanvas');
require('./gritter'); require('./gritter');
const Cookies = require('./pad_utils').Cookies; const Cookies = require('./pad_utils').Cookies;

View file

@ -113,7 +113,7 @@ const reconnectionTries = {
nextTry() { nextTry() {
// double the time to try to reconnect on every time reconnection fails // double the time to try to reconnect on every time reconnection fails
const nextCounterFactor = Math.pow(2, this.counter); const nextCounterFactor = 2 ** this.counter;
this.counter++; this.counter++;
return nextCounterFactor; return nextCounterFactor;

View file

@ -1,6 +1,5 @@
/* global exports, require */ 'use strict';
const _ = require('underscore');
const pluginDefs = require('./plugin_defs'); const pluginDefs = require('./plugin_defs');
// Maps the name of a server-side hook to a string explaining the deprecation // Maps the name of a server-side hook to a string explaining the deprecation
@ -15,66 +14,37 @@ exports.deprecationNotices = {};
const deprecationWarned = {}; const deprecationWarned = {};
function checkDeprecation(hook) { const checkDeprecation = (hook) => {
const notice = exports.deprecationNotices[hook.hook_name]; const notice = exports.deprecationNotices[hook.hook_name];
if (notice == null) return; if (notice == null) return;
if (deprecationWarned[hook.hook_fn_name]) return; if (deprecationWarned[hook.hook_fn_name]) return;
console.warn(`${hook.hook_name} hook used by the ${hook.part.plugin} plugin ` + console.warn(`${hook.hook_name} hook used by the ${hook.part.plugin} plugin ` +
`(${hook.hook_fn_name}) is deprecated: ${notice}`); `(${hook.hook_fn_name}) is deprecated: ${notice}`);
deprecationWarned[hook.hook_fn_name] = true; deprecationWarned[hook.hook_fn_name] = true;
}
exports.bubbleExceptions = true;
const hookCallWrapper = function (hook, hook_name, args, cb) {
if (cb === undefined) cb = function (x) { return x; };
checkDeprecation(hook);
// Normalize output to list for both sync and async cases
const normalize = function (x) {
if (x === undefined) return [];
return x;
};
const normalizedhook = function () {
return normalize(hook.hook_fn(hook_name, args, (x) => cb(normalize(x))));
}; };
if (exports.bubbleExceptions) { // Calls the node-style callback when the Promise settles. Unlike util.callbackify, this takes a
return normalizedhook(); // Promise (rather than a function that returns a Promise), and it returns a Promise (rather than a
} else { // function that returns undefined).
try { const attachCallback = (p, cb) => p.then(
return normalizedhook(); (val) => cb(null, val),
} catch (ex) { // Callbacks often only check the truthiness, not the nullness, of the first parameter. To avoid
console.error([hook_name, hook.part.full_name, ex.stack || ex]); // problems, always pass a truthy value as the first argument if the Promise is rejected.
} (err) => cb(err || new Error(err)));
}
// Normalizes the value provided by hook functions so that it is always an array. `undefined` (but
// not `null`!) becomes an empty array, array values are returned unmodified, and non-array values
// are wrapped in an array (so `null` becomes `[null]`).
const normalizeValue = (val) => {
// `undefined` is treated the same as `[]`. IMPORTANT: `null` is *not* treated the same as `[]`
// because some hooks use `null` as a special value.
if (val === undefined) return [];
if (Array.isArray(val)) return val;
return [val];
}; };
exports.syncMapFirst = function (lst, fn) { // Flattens the array one level.
let i; const flatten1 = (array) => array.reduce((a, b) => a.concat(b), []);
let result;
for (i = 0; i < lst.length; i++) {
result = fn(lst[i]);
if (result.length) return result;
}
return [];
};
exports.mapFirst = function (lst, fn, cb, predicate) {
if (predicate == null) predicate = (x) => (x != null && x.length > 0);
let i = 0;
var next = function () {
if (i >= lst.length) return cb(null, []);
fn(lst[i++], (err, result) => {
if (err) return cb(err);
if (predicate(result)) return cb(null, result);
next();
});
};
next();
};
// Calls the hook function synchronously and returns the value provided by the hook function (via // Calls the hook function synchronously and returns the value provided by the hook function (via
// callback or return value). // callback or return value).
@ -104,7 +74,7 @@ exports.mapFirst = function (lst, fn, cb, predicate) {
// //
// See the tests in tests/backend/specs/hooks.js for examples of supported and prohibited behaviors. // See the tests in tests/backend/specs/hooks.js for examples of supported and prohibited behaviors.
// //
function callHookFnSync(hook, context) { const callHookFnSync = (hook, context) => {
checkDeprecation(hook); checkDeprecation(hook);
// This var is used to keep track of whether the hook function already settled. // This var is used to keep track of whether the hook function already settled.
@ -177,21 +147,36 @@ function callHookFnSync(hook, context) {
// The hook function is assumed to not have a callback parameter, so fall through and accept // The hook function is assumed to not have a callback parameter, so fall through and accept
// `undefined` as the resolved value. // `undefined` as the resolved value.
// //
// IMPORTANT: "Rest" parameters and default parameters are not counted in`Function.length`, so // IMPORTANT: "Rest" parameters and default parameters are not included in `Function.length`,
// the assumption does not hold for wrappers like `(...args) => { real(...args); }`. Such // so the assumption does not hold for wrappers such as:
// functions will still work properly without any logged warnings or errors for now, but: //
// const wrapper = (...args) => real(...args);
//
// ECMAScript does not provide a way to determine whether a function has default or rest
// parameters, so there is no way to be certain that a hook function with `length` < 3 will
// not call the callback. Synchronous hook functions that call the callback even though
// `length` < 3 will still work properly without any logged warnings or errors, but:
//
// * Once the hook is upgraded to support asynchronous hook functions, calling the callback // * Once the hook is upgraded to support asynchronous hook functions, calling the callback
// will (eventually) cause a double settle error, and the function might prematurely // asynchronously will cause a double settle error, and the hook function will prematurely
// resolve to `undefined` instead of the desired value. // resolve to `undefined` instead of the desired value.
//
// * The above "unsettled function" warning is not logged if the function fails to call the // * The above "unsettled function" warning is not logged if the function fails to call the
// callback like it is supposed to. // callback like it is supposed to.
//
// Wrapper functions can avoid problems by setting the wrapper's `length` property to match
// the real function's `length` property:
//
// Object.defineProperty(wrapper, 'length', {value: real.length});
} }
} }
settle(null, val, 'returned value'); settle(null, val, 'returned value');
return outcome.val; return outcome.val;
} };
// DEPRECATED: Use `callAllSerial()` or `aCallAll()` instead.
//
// Invokes all registered hook functions synchronously. // Invokes all registered hook functions synchronously.
// //
// Arguments: // Arguments:
@ -203,15 +188,10 @@ function callHookFnSync(hook, context) {
// 1. Collect all values returned by the hook functions into an array. // 1. Collect all values returned by the hook functions into an array.
// 2. Convert each `undefined` entry into `[]`. // 2. Convert each `undefined` entry into `[]`.
// 3. Flatten one level. // 3. Flatten one level.
exports.callAll = function (hookName, context) { exports.callAll = (hookName, context) => {
if (context == null) context = {}; if (context == null) context = {};
const hooks = pluginDefs.hooks[hookName] || []; const hooks = pluginDefs.hooks[hookName] || [];
return _.flatten(hooks.map((hook) => { return flatten1(hooks.map((hook) => normalizeValue(callHookFnSync(hook, context))));
const ret = callHookFnSync(hook, context);
// `undefined` (but not `null`!) is treated the same as [].
if (ret === undefined) return [];
return ret;
}), 1);
}; };
// Calls the hook function asynchronously and returns a Promise that either resolves to the hook // Calls the hook function asynchronously and returns a Promise that either resolves to the hook
@ -248,7 +228,7 @@ exports.callAll = function (hookName, context) {
// //
// See the tests in tests/backend/specs/hooks.js for examples of supported and prohibited behaviors. // See the tests in tests/backend/specs/hooks.js for examples of supported and prohibited behaviors.
// //
async function callHookFnAsync(hook, context) { const callHookFnAsync = async (hook, context) => {
checkDeprecation(hook); checkDeprecation(hook);
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
// This var is used to keep track of whether the hook function already settled. // This var is used to keep track of whether the hook function already settled.
@ -312,10 +292,21 @@ async function callHookFnAsync(hook, context) {
// The hook function is assumed to not have a callback parameter, so fall through and accept // The hook function is assumed to not have a callback parameter, so fall through and accept
// `undefined` as the resolved value. // `undefined` as the resolved value.
// //
// IMPORTANT: "Rest" parameters and default parameters are not counted in `Function.length`, // IMPORTANT: "Rest" parameters and default parameters are not included in
// so the assumption does not hold for wrappers like `(...args) => { real(...args); }`. For // `Function.length`, so the assumption does not hold for wrappers such as:
// such functions, calling the callback will (eventually) cause a double settle error, and //
// the function might prematurely resolve to `undefined` instead of the desired value. // const wrapper = (...args) => real(...args);
//
// ECMAScript does not provide a way to determine whether a function has default or rest
// parameters, so there is no way to be certain that a hook function with `length` < 3 will
// not call the callback. Hook functions with `length` < 3 that call the callback
// asynchronously will cause a double settle error, and the hook function will prematurely
// resolve to `undefined` instead of the desired value.
//
// Wrapper functions can avoid problems by setting the wrapper's `length` property to match
// the real function's `length` property:
//
// Object.defineProperty(wrapper, 'length', {value: real.length});
} }
} }
@ -326,17 +317,21 @@ async function callHookFnAsync(hook, context) {
(val) => settle(null, val, 'returned value'), (val) => settle(null, val, 'returned value'),
(err) => settle(err, null, 'Promise rejection')); (err) => settle(err, null, 'Promise rejection'));
}); });
} };
// Invokes all registered hook functions asynchronously. // Invokes all registered hook functions asynchronously and concurrently. This is NOT the async
// equivalent of `callAll()`: `callAll()` calls the hook functions serially (one at a time) but this
// function calls them concurrently. Use `callAllSerial()` if the hook functions must be called one
// at a time.
// //
// Arguments: // Arguments:
// * hookName: Name of the hook to invoke. // * hookName: Name of the hook to invoke.
// * context: Passed unmodified to the hook functions, except nullish becomes {}. // * context: Passed unmodified to the hook functions, except nullish becomes {}.
// * cb: Deprecated callback. The following: // * cb: Deprecated. Optional node-style callback. The following:
// const p1 = hooks.aCallAll('myHook', context, cb); // const p1 = hooks.aCallAll('myHook', context, cb);
// is equivalent to: // is equivalent to:
// const p2 = hooks.aCallAll('myHook', context).then((val) => cb(null, val), cb); // const p2 = hooks.aCallAll('myHook', context).then(
// (val) => cb(null, val), (err) => cb(err || new Error(err)));
// //
// Return value: // Return value:
// If cb is nullish, this function resolves to a flattened array of hook results. Specifically, it // If cb is nullish, this function resolves to a flattened array of hook results. Specifically, it
@ -345,57 +340,75 @@ async function callHookFnAsync(hook, context) {
// 2. Convert each `undefined` entry into `[]`. // 2. Convert each `undefined` entry into `[]`.
// 3. Flatten one level. // 3. Flatten one level.
// If cb is non-null, this function resolves to the value returned by cb. // If cb is non-null, this function resolves to the value returned by cb.
exports.aCallAll = async (hookName, context, cb) => { exports.aCallAll = async (hookName, context, cb = null) => {
if (cb != null) return await attachCallback(exports.aCallAll(hookName, context), cb);
if (context == null) context = {}; if (context == null) context = {};
const hooks = pluginDefs.hooks[hookName] || []; const hooks = pluginDefs.hooks[hookName] || [];
let resultsPromise = Promise.all(hooks.map((hook) => callHookFnAsync(hook, context) const results = await Promise.all(
// `undefined` (but not `null`!) is treated the same as []. hooks.map(async (hook) => normalizeValue(await callHookFnAsync(hook, context))));
.then((result) => (result === undefined) ? [] : result))).then((results) => _.flatten(results, 1)); return flatten1(results);
if (cb != null) resultsPromise = resultsPromise.then((val) => cb(null, val), cb);
return await resultsPromise;
}; };
exports.callFirst = function (hook_name, args) { // Like `aCallAll()` except the hook functions are called one at a time instead of concurrently.
if (!args) args = {}; // Only use this function if the hook functions must be called one at a time, otherwise use
if (pluginDefs.hooks[hook_name] === undefined) return []; // `aCallAll()`.
return exports.syncMapFirst(pluginDefs.hooks[hook_name], (hook) => hookCallWrapper(hook, hook_name, args)); exports.callAllSerial = async (hookName, context) => {
if (context == null) context = {};
const hooks = pluginDefs.hooks[hookName] || [];
const results = [];
for (const hook of hooks) {
results.push(normalizeValue(await callHookFnAsync(hook, context)));
}
return flatten1(results);
}; };
function aCallFirst(hook_name, args, cb, predicate) { // DEPRECATED: Use `aCallFirst()` instead.
if (!args) args = {}; //
if (!cb) cb = function () {}; // Like `aCallFirst()`, but synchronous. Hook functions must provide their values synchronously.
if (pluginDefs.hooks[hook_name] === undefined) return cb(null, []); exports.callFirst = (hookName, context) => {
exports.mapFirst( if (context == null) context = {};
pluginDefs.hooks[hook_name], const predicate = (val) => val.length;
(hook, cb) => { const hooks = pluginDefs.hooks[hookName] || [];
hookCallWrapper(hook, hook_name, args, (res) => { cb(null, res); }); for (const hook of hooks) {
}, const val = normalizeValue(callHookFnSync(hook, context));
cb, if (predicate(val)) return val;
predicate
);
}
/* return a Promise if cb is not supplied */
exports.aCallFirst = function (hook_name, args, cb, predicate) {
if (cb === undefined) {
return new Promise((resolve, reject) => {
aCallFirst(hook_name, args, (err, res) => err ? reject(err) : resolve(res), predicate);
});
} else {
return aCallFirst(hook_name, args, cb, predicate);
} }
return [];
}; };
exports.callAllStr = function (hook_name, args, sep, pre, post) { // Invokes the registered hook functions one at a time until one provides a value that meets a
if (sep == undefined) sep = ''; // customizable condition.
if (pre == undefined) pre = ''; //
if (post == undefined) post = ''; // Arguments:
const newCallhooks = []; // * hookName: Name of the hook to invoke.
const callhooks = exports.callAll(hook_name, args); // * context: Passed unmodified to the hook functions, except nullish becomes {}.
for (let i = 0, ii = callhooks.length; i < ii; i++) { // * cb: Deprecated callback. The following:
newCallhooks[i] = pre + callhooks[i] + post; // const p1 = hooks.aCallFirst('myHook', context, cb);
// is equivalent to:
// const p2 = hooks.aCallFirst('myHook', context).then(
// (val) => cb(null, val), (err) => cb(err || new Error(err)));
// * predicate: Optional predicate function that returns true if the hook function provided a
// value that satisfies a desired condition. If nullish, the predicate defaults to a non-empty
// array check. The predicate is invoked each time a hook function returns. It takes one
// argument: the normalized value provided by the hook function. If the predicate returns
// truthy, iteration over the hook functions stops (no more hook functions will be called).
//
// Return value:
// If cb is nullish, resolves to an array that is either the normalized value that satisfied the
// predicate or empty if the predicate was never satisfied. If cb is non-nullish, resolves to the
// value returned from cb().
exports.aCallFirst = async (hookName, context, cb = null, predicate = null) => {
if (cb != null) {
return await attachCallback(exports.aCallFirst(hookName, context, null, predicate), cb);
} }
return newCallhooks.join(sep || ''); if (context == null) context = {};
if (predicate == null) predicate = (val) => val.length;
const hooks = pluginDefs.hooks[hookName] || [];
for (const hook of hooks) {
const val = normalizeValue(await callHookFnAsync(hook, context));
if (predicate(val)) return val;
}
return [];
}; };
exports.exportedForTestingOnly = { exports.exportedForTestingOnly = {

View file

@ -1,6 +1,8 @@
'use strict';
const log4js = require('log4js'); const log4js = require('log4js');
const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); const plugins = require('./plugins');
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); const hooks = require('./hooks');
const npm = require('npm'); const npm = require('npm');
const request = require('request'); const request = require('request');
const util = require('util'); const util = require('util');
@ -13,22 +15,22 @@ const loadNpm = async () => {
npm.on('log', log4js.getLogger('npm').log); npm.on('log', log4js.getLogger('npm').log);
}; };
const onAllTasksFinished = () => {
hooks.aCallAll('restartServer', {}, () => {});
};
let tasks = 0; let tasks = 0;
function wrapTaskCb(cb) { function wrapTaskCb(cb) {
tasks++; tasks++;
return function () { return function (...args) {
cb && cb.apply(this, arguments); cb && cb.apply(this, args);
tasks--; tasks--;
if (tasks == 0) onAllTasksFinished(); if (tasks === 0) onAllTasksFinished();
}; };
} }
function onAllTasksFinished() {
hooks.aCallAll('restartServer', {}, () => {});
}
exports.uninstall = async (pluginName, cb = null) => { exports.uninstall = async (pluginName, cb = null) => {
cb = wrapTaskCb(cb); cb = wrapTaskCb(cb);
try { try {
@ -60,7 +62,7 @@ exports.install = async (pluginName, cb = null) => {
exports.availablePlugins = null; exports.availablePlugins = null;
let cacheTimestamp = 0; let cacheTimestamp = 0;
exports.getAvailablePlugins = function (maxCacheAge) { exports.getAvailablePlugins = (maxCacheAge) => {
const nowTimestamp = Math.round(Date.now() / 1000); const nowTimestamp = Math.round(Date.now() / 1000);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -87,8 +89,8 @@ exports.getAvailablePlugins = function (maxCacheAge) {
}; };
exports.search = function (searchTerm, maxCacheAge) { exports.search = (searchTerm, maxCacheAge) => exports.getAvailablePlugins(maxCacheAge).then(
return exports.getAvailablePlugins(maxCacheAge).then((results) => { (results) => {
const res = {}; const res = {};
if (searchTerm) { if (searchTerm) {
@ -97,10 +99,12 @@ exports.search = function (searchTerm, maxCacheAge) {
for (const pluginName in results) { for (const pluginName in results) {
// for every available plugin // for every available plugin
if (pluginName.indexOf(plugins.prefix) != 0) continue; // TODO: Also search in keywords here! // TODO: Also search in keywords here!
if (pluginName.indexOf(plugins.prefix) !== 0) continue;
if (searchTerm && !~results[pluginName].name.toLowerCase().indexOf(searchTerm) && if (searchTerm && !~results[pluginName].name.toLowerCase().indexOf(searchTerm) &&
(typeof results[pluginName].description !== 'undefined' && !~results[pluginName].description.toLowerCase().indexOf(searchTerm)) (typeof results[pluginName].description !== 'undefined' &&
!~results[pluginName].description.toLowerCase().indexOf(searchTerm))
) { ) {
if (typeof results[pluginName].description === 'undefined') { if (typeof results[pluginName].description === 'undefined') {
console.debug('plugin without Description: %s', results[pluginName].name); console.debug('plugin without Description: %s', results[pluginName].name);
@ -113,5 +117,5 @@ exports.search = function (searchTerm, maxCacheAge) {
} }
return res; return res;
}); }
}; );

View file

@ -1,3 +1,5 @@
'use strict';
// This module contains processed plugin definitions. The data structures in this file are set by // This module contains processed plugin definitions. The data structures in this file are set by
// plugins.js (server) or client_plugins.js (client). // plugins.js (server) or client_plugins.js (client).

View file

@ -1,6 +1,7 @@
'use strict';
const fs = require('fs').promises; const fs = require('fs').promises;
const hooks = require('./hooks'); const hooks = require('./hooks');
const npm = require('npm/lib/npm.js');
const readInstalled = require('./read-installed.js'); const readInstalled = require('./read-installed.js');
const path = require('path'); const path = require('path');
const tsort = require('./tsort'); const tsort = require('./tsort');
@ -13,11 +14,9 @@ const defs = require('./plugin_defs');
exports.prefix = 'ep_'; exports.prefix = 'ep_';
exports.formatPlugins = function () { exports.formatPlugins = () => Object.keys(defs.plugins).join(', ');
return _.keys(defs.plugins).join(', ');
};
exports.formatPluginsWithVersion = function () { exports.formatPluginsWithVersion = () => {
const plugins = []; const plugins = [];
_.forEach(defs.plugins, (plugin) => { _.forEach(defs.plugins, (plugin) => {
if (plugin.package.name !== 'ep_etherpad-lite') { if (plugin.package.name !== 'ep_etherpad-lite') {
@ -28,17 +27,16 @@ exports.formatPluginsWithVersion = function () {
return plugins.join(', '); return plugins.join(', ');
}; };
exports.formatParts = function () { exports.formatParts = () => _.map(defs.parts, (part) => part.full_name).join('\n');
return _.map(defs.parts, (part) => part.full_name).join('\n');
};
exports.formatHooks = function (hook_set_name) { exports.formatHooks = (hook_set_name) => {
const res = []; const res = [];
const hooks = pluginUtils.extractHooks(defs.parts, hook_set_name || 'hooks'); const hooks = pluginUtils.extractHooks(defs.parts, hook_set_name || 'hooks');
_.chain(hooks).keys().forEach((hook_name) => { _.chain(hooks).keys().forEach((hook_name) => {
_.forEach(hooks[hook_name], (hook) => { _.forEach(hooks[hook_name], (hook) => {
res.push(`<dt>${hook.hook_name}</dt><dd>${hook.hook_fn_name} from ${hook.part.full_name}</dd>`); res.push(`<dt>${hook.hook_name}</dt><dd>${hook.hook_fn_name} ` +
`from ${hook.part.full_name}</dd>`);
}); });
}); });
return `<dl>${res.join('\n')}</dl>`; return `<dl>${res.join('\n')}</dl>`;
@ -57,7 +55,7 @@ const callInit = async () => {
})); }));
}; };
exports.pathNormalization = function (part, hook_fn_name, hook_name) { exports.pathNormalization = (part, hook_fn_name, hook_name) => {
const tmp = hook_fn_name.split(':'); // hook_fn_name might be something like 'C:\\foo.js:myFunc'. const tmp = hook_fn_name.split(':'); // hook_fn_name might be something like 'C:\\foo.js:myFunc'.
// If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'. // If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'.
const functionName = (tmp.length > 1 ? tmp.pop() : null) || hook_name; const functionName = (tmp.length > 1 ? tmp.pop() : null) || hook_name;
@ -67,7 +65,7 @@ exports.pathNormalization = function (part, hook_fn_name, hook_name) {
return `${fileName}:${functionName}`; return `${fileName}:${functionName}`;
}; };
exports.update = async function () { exports.update = async () => {
const packages = await exports.getPackages(); const packages = await exports.getPackages();
const parts = {}; // Key is full name. sortParts converts this into a topologically sorted array. const parts = {}; // Key is full name. sortParts converts this into a topologically sorted array.
const plugins = {}; const plugins = {};
@ -83,13 +81,14 @@ exports.update = async function () {
await callInit(); await callInit();
}; };
exports.getPackages = async function () { exports.getPackages = async () => {
// Load list of installed NPM packages, flatten it to a list, and filter out only packages with names that // Load list of installed NPM packages, flatten it to a list,
// and filter out only packages with names that
const dir = settings.root; const dir = settings.root;
const data = await util.promisify(readInstalled)(dir); const data = await util.promisify(readInstalled)(dir);
const packages = {}; const packages = {};
function flatten(deps) { const flatten = (deps) => {
_.chain(deps).keys().each((name) => { _.chain(deps).keys().each((name) => {
if (name.indexOf(exports.prefix) === 0) { if (name.indexOf(exports.prefix) === 0) {
packages[name] = _.clone(deps[name]); packages[name] = _.clone(deps[name]);
@ -102,7 +101,7 @@ exports.getPackages = async function () {
// I don't think we need recursion // I don't think we need recursion
// if (deps[name].dependencies !== undefined) flatten(deps[name].dependencies); // if (deps[name].dependencies !== undefined) flatten(deps[name].dependencies);
}); });
} };
const tmp = {}; const tmp = {};
tmp[data.name] = data; tmp[data.name] = data;
@ -110,7 +109,7 @@ exports.getPackages = async function () {
return packages; return packages;
}; };
async function loadPlugin(packages, plugin_name, plugins, parts) { const loadPlugin = async (packages, plugin_name, plugins, parts) => {
const plugin_path = path.resolve(packages[plugin_name].path, 'ep.json'); const plugin_path = path.resolve(packages[plugin_name].path, 'ep.json');
try { try {
const data = await fs.readFile(plugin_path); const data = await fs.readFile(plugin_path);
@ -129,9 +128,9 @@ async function loadPlugin(packages, plugin_name, plugins, parts) {
} catch (er) { } catch (er) {
console.error(`Unable to load plugin definition file ${plugin_path}`); console.error(`Unable to load plugin definition file ${plugin_path}`);
} }
} };
function partsToParentChildList(parts) { const partsToParentChildList = (parts) => {
const res = []; const res = [];
_.chain(parts).keys().forEach((name) => { _.chain(parts).keys().forEach((name) => {
_.each(parts[name].post || [], (child_name) => { _.each(parts[name].post || [], (child_name) => {
@ -145,15 +144,9 @@ function partsToParentChildList(parts) {
} }
}); });
return res; return res;
} };
// Used only in Node, so no need for _ // Used only in Node, so no need for _
function sortParts(parts) { const sortParts = (parts) => tsort(partsToParentChildList(parts))
return tsort( .filter((name) => parts[name] !== undefined)
partsToParentChildList(parts) .map((name) => parts[name]);
).filter(
(name) => parts[name] !== undefined
).map(
(name) => parts[name]
);
}

View file

@ -1,3 +1,4 @@
'use strict';
const _ = require('underscore'); const _ = require('underscore');
const defs = require('./plugin_defs'); const defs = require('./plugin_defs');
@ -8,13 +9,13 @@ const disabledHookReasons = {
}, },
}; };
function loadFn(path, hookName) { const loadFn = (path, hookName) => {
let functionName; let functionName;
const parts = path.split(':'); const parts = path.split(':');
// on windows: C:\foo\bar:xyz // on windows: C:\foo\bar:xyz
if (parts[0].length == 1) { if (parts[0].length === 1) {
if (parts.length == 3) { if (parts.length === 3) {
functionName = parts.pop(); functionName = parts.pop();
} }
path = parts.join(':'); path = parts.join(':');
@ -30,9 +31,9 @@ function loadFn(path, hookName) {
fn = fn[name]; fn = fn[name];
}); });
return fn; return fn;
} };
function extractHooks(parts, hook_set_name, normalizer) { const extractHooks = (parts, hook_set_name, normalizer) => {
const hooks = {}; const hooks = {};
_.each(parts, (part) => { _.each(parts, (part) => {
_.chain(part[hook_set_name] || {}) _.chain(part[hook_set_name] || {})
@ -50,20 +51,23 @@ function extractHooks(parts, hook_set_name, normalizer) {
const disabledReason = (disabledHookReasons[hook_set_name] || {})[hook_name]; const disabledReason = (disabledHookReasons[hook_set_name] || {})[hook_name];
if (disabledReason) { if (disabledReason) {
console.error(`Hook ${hook_set_name}/${hook_name} is disabled. Reason: ${disabledReason}`); console.error(
`Hook ${hook_set_name}/${hook_name} is disabled. Reason: ${disabledReason}`);
console.error(`The hook function ${hook_fn_name} from plugin ${part.plugin} ` + console.error(`The hook function ${hook_fn_name} from plugin ${part.plugin} ` +
'will never be called, which may cause the plugin to fail'); 'will never be called, which may cause the plugin to fail');
console.error(`Please update the ${part.plugin} plugin to not use the ${hook_name} hook`); console.error(
`Please update the ${part.plugin} plugin to not use the ${hook_name} hook`);
return; return;
} }
let hook_fn;
try { try {
var hook_fn = loadFn(hook_fn_name, hook_name); hook_fn = loadFn(hook_fn_name, hook_name);
if (!hook_fn) { if (!hook_fn) {
throw 'Not a function'; throw new Error('Not a function');
} }
} catch (exc) { } catch (exc) {
console.error(`Failed to load '${hook_fn_name}' for '${part.full_name}/${hook_set_name}/${hook_name}': ${exc.toString()}`); console.error(`Failed to load '${hook_fn_name}' for ` +
`'${part.full_name}/${hook_set_name}/${hook_name}': ${exc.toString()}`);
} }
if (hook_fn) { if (hook_fn) {
if (hooks[hook_name] == null) hooks[hook_name] = []; if (hooks[hook_name] == null) hooks[hook_name] = [];
@ -72,7 +76,7 @@ function extractHooks(parts, hook_set_name, normalizer) {
}); });
}); });
return hooks; return hooks;
} };
exports.extractHooks = extractHooks; exports.extractHooks = extractHooks;
@ -88,10 +92,10 @@ exports.extractHooks = extractHooks;
* No plugins: [] * No plugins: []
* Some plugins: [ 'ep_adminpads', 'ep_add_buttons', 'ep_activepads' ] * Some plugins: [ 'ep_adminpads', 'ep_add_buttons', 'ep_activepads' ]
*/ */
exports.clientPluginNames = function () { exports.clientPluginNames = () => {
const client_plugin_names = _.uniq( const client_plugin_names = _.uniq(
defs.parts defs.parts
.filter((part) => part.hasOwnProperty('client_hooks')) .filter((part) => Object.prototype.hasOwnProperty.call(part, 'client_hooks'))
.map((part) => `plugin-${part.plugin}`) .map((part) => `plugin-${part.plugin}`)
); );

View file

@ -55,7 +55,7 @@ const tsort = (edges) => {
Object.keys(nodes).forEach(visit); Object.keys(nodes).forEach(visit);
return sorted; return sorted;
} };
/** /**
* TEST * TEST

View file

@ -1,19 +1,14 @@
// Specific hash to display the skin variants builder popup 'use strict';
if (window.location.hash.toLowerCase() == '#skinvariantsbuilder') {
$('#skin-variants').addClass('popup-show');
$('.skin-variant').change(() => { // Specific hash to display the skin variants builder popup
updateSkinVariantsClasses(); if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
}); $('#skin-variants').addClass('popup-show');
const containers = ['editor', 'background', 'toolbar']; const containers = ['editor', 'background', 'toolbar'];
const colors = ['super-light', 'light', 'dark', 'super-dark']; const colors = ['super-light', 'light', 'dark', 'super-dark'];
updateCheckboxFromSkinClasses();
updateSkinVariantsClasses();
// add corresponding classes when config change // add corresponding classes when config change
function updateSkinVariantsClasses() { const updateSkinVariantsClasses = () => {
const domsToUpdate = [ const domsToUpdate = [
$('html'), $('html'),
$('iframe[name=ace_outer]').contents().find('html'), $('iframe[name=ace_outer]').contents().find('html'),
@ -27,23 +22,21 @@ if (window.location.hash.toLowerCase() == '#skinvariantsbuilder') {
domsToUpdate.forEach((el) => { el.removeClass('full-width-editor'); }); domsToUpdate.forEach((el) => { el.removeClass('full-width-editor'); });
const new_classes = []; const newClasses = [];
$('select.skin-variant-color').each(function () { $('select.skin-variant-color').each(function () {
new_classes.push(`${$(this).val()}-${$(this).data('container')}`); newClasses.push(`${$(this).val()}-${$(this).data('container')}`);
}); });
if ($('#skin-variant-full-width').is(':checked')) new_classes.push('full-width-editor'); if ($('#skin-variant-full-width').is(':checked')) newClasses.push('full-width-editor');
domsToUpdate.forEach((el) => { el.addClass(new_classes.join(' ')); }); domsToUpdate.forEach((el) => { el.addClass(newClasses.join(' ')); });
$('#skin-variants-result').val(`"skinVariants": "${new_classes.join(' ')}",`); $('#skin-variants-result').val(`"skinVariants": "${newClasses.join(' ')}",`);
} };
// run on init // run on init
function updateCheckboxFromSkinClasses() { const updateCheckboxFromSkinClasses = () => {
$('html').attr('class').split(' ').forEach((classItem) => { $('html').attr('class').split(' ').forEach((classItem) => {
var container = classItem.split('-').slice(-1); const container = classItem.substring(classItem.lastIndexOf('-') + 1, classItem.length);
var container = classItem.substring(classItem.lastIndexOf('-') + 1, classItem.length);
if (containers.indexOf(container) > -1) { if (containers.indexOf(container) > -1) {
const color = classItem.substring(0, classItem.lastIndexOf('-')); const color = classItem.substring(0, classItem.lastIndexOf('-'));
$(`.skin-variant-color[data-container="${container}"`).val(color); $(`.skin-variant-color[data-container="${container}"`).val(color);
@ -51,5 +44,12 @@ if (window.location.hash.toLowerCase() == '#skinvariantsbuilder') {
}); });
$('#skin-variant-full-width').prop('checked', $('html').hasClass('full-width-editor')); $('#skin-variant-full-width').prop('checked', $('html').hasClass('full-width-editor'));
} };
$('.skin-variant').change(() => {
updateSkinVariantsClasses();
});
updateCheckboxFromSkinClasses();
updateSkinVariantsClasses();
} }

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