mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-19 06:03:34 +01:00
Merge branch 'develop' of github.com:ether/etherpad-lite into 3227-tests
This commit is contained in:
commit
4682460b00
150 changed files with 6320 additions and 6944 deletions
26
.github/workflows/backend-tests.yml
vendored
26
.github/workflows/backend-tests.yml
vendored
|
@ -49,12 +49,32 @@ jobs:
|
|||
sudo apt update
|
||||
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
|
||||
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
|
||||
- name: Run the backend tests
|
||||
run: tests/frontend/travis/runnerBackend.sh
|
||||
|
|
26
.github/workflows/frontend-tests.yml
vendored
26
.github/workflows/frontend-tests.yml
vendored
|
@ -55,12 +55,32 @@ jobs:
|
|||
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
|
||||
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
|
||||
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
|
||||
id: environment
|
||||
run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})"
|
||||
|
|
28
.github/workflows/load-test.yml
vendored
28
.github/workflows/load-test.yml
vendored
|
@ -39,14 +39,34 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install all dependencies and symlink for ep_etherpad-lite
|
||||
run: bin/installDeps.sh
|
||||
|
||||
- name: Install etherpad-load-test
|
||||
run: sudo npm install -g etherpad-load-test
|
||||
|
||||
- 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
|
||||
- name: Run load test
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
# Changes for the next release
|
||||
|
||||
### Compatibility changes
|
||||
* Node.js 10.17.0 or newer is now required.
|
||||
|
||||
### Notable new features
|
||||
* Database performance is significantly improved.
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ Etherpad is a real-time collaborative editor [scalable to thousands of simultane
|
|||
# Installation
|
||||
|
||||
## Requirements
|
||||
- `nodejs` >= **10.13.0**.
|
||||
- `nodejs` >= **10.17.0**.
|
||||
|
||||
## 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
|
||||
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):**
|
||||
|
||||
|
|
|
@ -7,89 +7,87 @@
|
|||
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
|
||||
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');
|
||||
|
||||
// load and initialize NPM
|
||||
const npm = require('ep_etherpad-lite/node_modules/npm');
|
||||
npm.load({}, async () => {
|
||||
try {
|
||||
// initialize the database
|
||||
require('ep_etherpad-lite/node/utils/Settings');
|
||||
const db = require('ep_etherpad-lite/node/db/DB');
|
||||
await db.init();
|
||||
(async () => {
|
||||
await util.promisify(npm.load)({});
|
||||
|
||||
// load modules
|
||||
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
|
||||
const padManager = require('ep_etherpad-lite/node/db/PadManager');
|
||||
// initialize the database
|
||||
require('ep_etherpad-lite/node/utils/Settings');
|
||||
const db = require('ep_etherpad-lite/node/db/DB');
|
||||
await db.init();
|
||||
|
||||
let revTestedCount = 0;
|
||||
// load modules
|
||||
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
|
||||
const padManager = require('ep_etherpad-lite/node/db/PadManager');
|
||||
|
||||
// get all pads
|
||||
const res = await padManager.listAllPads();
|
||||
for (const padId of res.padIDs) {
|
||||
const pad = await padManager.getPad(padId);
|
||||
let revTestedCount = 0;
|
||||
|
||||
// check if the pad has a pool
|
||||
if (pad.pool === undefined) {
|
||||
console.error(`[${pad.id}] Missing attribute pool`);
|
||||
// get all pads
|
||||
const res = await padManager.listAllPads();
|
||||
for (const padId of res.padIDs) {
|
||||
const pad = await padManager.getPad(padId);
|
||||
|
||||
// check if the pad has a pool
|
||||
if (pad.pool == null) {
|
||||
console.error(`[${pad.id}] Missing attribute pool`);
|
||||
continue;
|
||||
}
|
||||
// create an array with key kevisions
|
||||
// key revisions always save the full pad atext
|
||||
const head = pad.getHeadRevisionNumber();
|
||||
const keyRevisions = [];
|
||||
for (let rev = 0; rev < head; rev += 100) {
|
||||
keyRevisions.push(rev);
|
||||
}
|
||||
|
||||
// run through all key revisions
|
||||
for (const keyRev of keyRevisions) {
|
||||
// create an array of revisions we need till the next keyRevision or the End
|
||||
const revisionsNeeded = [];
|
||||
for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) {
|
||||
revisionsNeeded.push(rev);
|
||||
}
|
||||
|
||||
// this array will hold all revision changesets
|
||||
const revisions = [];
|
||||
|
||||
// run through all needed revisions and get them from the database
|
||||
for (const revNum of revisionsNeeded) {
|
||||
const revision = await db.get(`pad:${pad.id}:revs:${revNum}`);
|
||||
revisions[revNum] = revision;
|
||||
}
|
||||
|
||||
// check if the revision exists
|
||||
if (revisions[keyRev] == null) {
|
||||
console.error(`[${pad.id}] Missing revision ${keyRev}`);
|
||||
continue;
|
||||
}
|
||||
// create an array with key kevisions
|
||||
// key revisions always save the full pad atext
|
||||
const head = pad.getHeadRevisionNumber();
|
||||
const keyRevisions = [];
|
||||
for (let rev = 0; rev < head; rev += 100) {
|
||||
keyRevisions.push(rev);
|
||||
|
||||
// check if there is a atext in the keyRevisions
|
||||
let {meta: {atext} = {}} = revisions[keyRev];
|
||||
if (atext == null) {
|
||||
console.error(`[${pad.id}] Missing atext in revision ${keyRev}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// run through all key revisions
|
||||
for (const keyRev of keyRevisions) {
|
||||
// create an array of revisions we need till the next keyRevision or the End
|
||||
const revisionsNeeded = [];
|
||||
for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) {
|
||||
revisionsNeeded.push(rev);
|
||||
}
|
||||
|
||||
// this array will hold all revision changesets
|
||||
const revisions = [];
|
||||
|
||||
// run through all needed revisions and get them from the database
|
||||
for (const revNum of revisionsNeeded) {
|
||||
const revision = await db.get(`pad:${pad.id}:revs:${revNum}`);
|
||||
revisions[revNum] = revision;
|
||||
}
|
||||
|
||||
// check if the revision exists
|
||||
if (revisions[keyRev] == null) {
|
||||
console.error(`[${pad.id}] Missing revision ${keyRev}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if there is a atext in the keyRevisions
|
||||
if (revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) {
|
||||
console.error(`[${pad.id}] Missing atext in revision ${keyRev}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const apool = pad.pool;
|
||||
let atext = revisions[keyRev].meta.atext;
|
||||
for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) {
|
||||
try {
|
||||
const cs = revisions[rev].changeset;
|
||||
atext = Changeset.applyToAText(cs, atext, apool);
|
||||
revTestedCount++;
|
||||
} catch (e) {
|
||||
console.error(`[${pad.id}] Bad changeset at revision ${rev} - ${e.message}`);
|
||||
}
|
||||
const apool = pad.pool;
|
||||
for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) {
|
||||
try {
|
||||
const cs = revisions[rev].changeset;
|
||||
atext = Changeset.applyToAText(cs, atext, apool);
|
||||
revTestedCount++;
|
||||
} catch (e) {
|
||||
console.error(`[${pad.id}] Bad changeset at revision ${rev} - ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (revTestedCount === 0) {
|
||||
throw new Error('No revisions tested');
|
||||
}
|
||||
console.log(`Finished: Tested ${revTestedCount} revisions`);
|
||||
} catch (err) {
|
||||
console.trace(err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
if (revTestedCount === 0) {
|
||||
throw new Error('No revisions tested');
|
||||
}
|
||||
console.log(`Finished: Tested ${revTestedCount} revisions`);
|
||||
})();
|
||||
|
|
120
bin/checkPad.js
120
bin/checkPad.js
|
@ -7,85 +7,81 @@
|
|||
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
|
||||
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');
|
||||
|
||||
// get the padID
|
||||
const padId = process.argv[2];
|
||||
let checkRevisionCount = 0;
|
||||
|
||||
// load and initialize NPM;
|
||||
const npm = require('ep_etherpad-lite/node_modules/npm');
|
||||
npm.load({}, async () => {
|
||||
try {
|
||||
// initialize database
|
||||
require('ep_etherpad-lite/node/utils/Settings');
|
||||
const db = require('ep_etherpad-lite/node/db/DB');
|
||||
await db.init();
|
||||
(async () => {
|
||||
await util.promisify(npm.load)({});
|
||||
|
||||
// load modules
|
||||
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
|
||||
const padManager = require('ep_etherpad-lite/node/db/PadManager');
|
||||
// initialize database
|
||||
require('ep_etherpad-lite/node/utils/Settings');
|
||||
const db = require('ep_etherpad-lite/node/db/DB');
|
||||
await db.init();
|
||||
|
||||
const exists = await padManager.doesPadExists(padId);
|
||||
if (!exists) throw new Error('Pad does not exist');
|
||||
// load modules
|
||||
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
|
||||
const padManager = require('ep_etherpad-lite/node/db/PadManager');
|
||||
|
||||
// get the pad
|
||||
const pad = await padManager.getPad(padId);
|
||||
const exists = await padManager.doesPadExists(padId);
|
||||
if (!exists) throw new Error('Pad does not exist');
|
||||
|
||||
// create an array with key revisions
|
||||
// key revisions always save the full pad atext
|
||||
const head = pad.getHeadRevisionNumber();
|
||||
const keyRevisions = [];
|
||||
for (let rev = 0; rev < head; rev += 100) {
|
||||
keyRevisions.push(rev);
|
||||
// get the pad
|
||||
const pad = await padManager.getPad(padId);
|
||||
|
||||
// create an array with key revisions
|
||||
// key revisions always save the full pad atext
|
||||
const head = pad.getHeadRevisionNumber();
|
||||
const keyRevisions = [];
|
||||
for (let rev = 0; rev < head; rev += 100) {
|
||||
keyRevisions.push(rev);
|
||||
}
|
||||
|
||||
// run through all key revisions
|
||||
for (let keyRev of keyRevisions) {
|
||||
keyRev = parseInt(keyRev);
|
||||
// create an array of revisions we need till the next keyRevision or the End
|
||||
const revisionsNeeded = [];
|
||||
for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) {
|
||||
revisionsNeeded.push(rev);
|
||||
}
|
||||
|
||||
// run through all key revisions
|
||||
for (let keyRev of keyRevisions) {
|
||||
keyRev = parseInt(keyRev);
|
||||
// create an array of revisions we need till the next keyRevision or the End
|
||||
const revisionsNeeded = [];
|
||||
for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) {
|
||||
revisionsNeeded.push(rev);
|
||||
}
|
||||
// this array will hold all revision changesets
|
||||
const revisions = [];
|
||||
|
||||
// this array will hold all revision changesets
|
||||
const revisions = [];
|
||||
// run through all needed revisions and get them from the database
|
||||
for (const revNum of revisionsNeeded) {
|
||||
const revision = await db.get(`pad:${padId}:revs:${revNum}`);
|
||||
revisions[revNum] = revision;
|
||||
}
|
||||
|
||||
// run through all needed revisions and get them from the database
|
||||
for (const revNum of revisionsNeeded) {
|
||||
const revision = await db.get(`pad:${padId}:revs:${revNum}`);
|
||||
revisions[revNum] = revision;
|
||||
}
|
||||
// check if the pad has a pool
|
||||
if (pad.pool == null) throw new Error('Attribute pool is missing');
|
||||
|
||||
// check if the pad has a pool
|
||||
if (pad.pool === undefined) throw new Error('Attribute pool is missing');
|
||||
// check if there is an atext in the keyRevisions
|
||||
let {meta: {atext} = {}} = revisions[keyRev] || {};
|
||||
if (atext == null) {
|
||||
console.error(`No atext in key revision ${keyRev}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if there is an atext in the keyRevisions
|
||||
if (revisions[keyRev] === undefined ||
|
||||
revisions[keyRev].meta === undefined ||
|
||||
revisions[keyRev].meta.atext === undefined) {
|
||||
console.error(`No atext in key revision ${keyRev}`);
|
||||
const apool = pad.pool;
|
||||
|
||||
for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) {
|
||||
checkRevisionCount++;
|
||||
try {
|
||||
const cs = revisions[rev].changeset;
|
||||
atext = Changeset.applyToAText(cs, atext, apool);
|
||||
} catch (e) {
|
||||
console.error(`Bad changeset at revision ${rev} - ${e.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const apool = pad.pool;
|
||||
let atext = revisions[keyRev].meta.atext;
|
||||
|
||||
for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) {
|
||||
checkRevisionCount++;
|
||||
try {
|
||||
const cs = revisions[rev].changeset;
|
||||
atext = Changeset.applyToAText(cs, atext, apool);
|
||||
} catch (e) {
|
||||
console.error(`Bad changeset at revision ${rev} - ${e.message}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
console.log(`Finished: Checked ${checkRevisionCount} revisions`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.trace(err);
|
||||
throw err;
|
||||
console.log(`Finished: Checked ${checkRevisionCount} revisions`);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -12,12 +12,14 @@ if (process.argv.length !== 3) throw new Error('Use: node bin/checkPadDeltas.js
|
|||
// get the padID
|
||||
const padId = process.argv[2];
|
||||
|
||||
// load and initialize NPM;
|
||||
const expect = require('../tests/frontend/lib/expect');
|
||||
const diff = require('ep_etherpad-lite/node_modules/diff');
|
||||
const npm = require('ep_etherpad-lite/node_modules/npm');
|
||||
const util = require('util');
|
||||
|
||||
(async () => {
|
||||
await util.promisify(npm.load)({});
|
||||
|
||||
npm.load({}, async () => {
|
||||
// initialize database
|
||||
require('ep_etherpad-lite/node/utils/Settings');
|
||||
const db = require('ep_etherpad-lite/node/db/DB');
|
||||
|
@ -54,10 +56,8 @@ npm.load({}, async () => {
|
|||
// console.log('Fetching', revNum)
|
||||
const revision = await db.get(`pad:${padId}:revs:${revNum}`);
|
||||
// check if there is a atext in the keyRevisions
|
||||
if (~keyRevisions.indexOf(revNum) &&
|
||||
(revision === undefined ||
|
||||
revision.meta === undefined ||
|
||||
revision.meta.atext === undefined)) {
|
||||
const {meta: {atext: revAtext} = {}} = revision || {};
|
||||
if (~keyRevisions.indexOf(revNum) && revAtext == null) {
|
||||
console.error(`No atext in key revision ${revNum}`);
|
||||
continue;
|
||||
}
|
||||
|
@ -104,4 +104,4 @@ npm.load({}, async () => {
|
|||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
391
bin/convert.js
391
bin/convert.js
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
//
|
||||
// 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
|
||||
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
const marked = require('marked');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
|
@ -33,12 +35,12 @@ let template = null;
|
|||
let inputFile = null;
|
||||
|
||||
args.forEach((arg) => {
|
||||
if (!arg.match(/^\-\-/)) {
|
||||
if (!arg.match(/^--/)) {
|
||||
inputFile = arg;
|
||||
} else if (arg.match(/^\-\-format=/)) {
|
||||
format = arg.replace(/^\-\-format=/, '');
|
||||
} else if (arg.match(/^\-\-template=/)) {
|
||||
template = arg.replace(/^\-\-template=/, '');
|
||||
} else if (arg.match(/^--format=/)) {
|
||||
format = arg.replace(/^--format=/, '');
|
||||
} else if (arg.match(/^--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 = {};
|
||||
function processIncludes(inputFile, input, cb) {
|
||||
const processIncludes = (inputFile, input, cb) => {
|
||||
const includes = input.match(includeExpr);
|
||||
if (includes === null) return cb(null, input);
|
||||
if (includes == null) return cb(null, input);
|
||||
let errState = null;
|
||||
console.error(includes);
|
||||
let incCount = includes.length;
|
||||
|
@ -70,7 +72,7 @@ function processIncludes(inputFile, input, cb) {
|
|||
let fname = include.replace(/^@include\s+/, '');
|
||||
if (!fname.match(/\.md$/)) fname += '.md';
|
||||
|
||||
if (includeData.hasOwnProperty(fname)) {
|
||||
if (Object.prototype.hasOwnProperty.call(includeData, fname)) {
|
||||
input = input.split(include).join(includeData[fname]);
|
||||
incCount--;
|
||||
if (incCount === 0) {
|
||||
|
@ -94,10 +96,10 @@ function processIncludes(inputFile, input, cb) {
|
|||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function next(er, input) {
|
||||
const next = (er, input) => {
|
||||
if (er) throw er;
|
||||
switch (format) {
|
||||
case 'json':
|
||||
|
@ -117,4 +119,4 @@ function next(er, input) {
|
|||
default:
|
||||
throw new Error(`Invalid format: ${format}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
//
|
||||
// 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 path = require('path');
|
||||
|
||||
module.exports = toHTML;
|
||||
|
||||
function toHTML(input, filename, template, cb) {
|
||||
const toHTML = (input, filename, template, cb) => {
|
||||
const lexed = marked.lexer(input);
|
||||
fs.readFile(template, 'utf8', (er, template) => {
|
||||
if (er) return cb(er);
|
||||
render(lexed, filename, template, cb);
|
||||
});
|
||||
}
|
||||
};
|
||||
module.exports = toHTML;
|
||||
|
||||
function render(lexed, filename, template, cb) {
|
||||
const render = (lexed, filename, template, cb) => {
|
||||
// get the section
|
||||
const section = getSection(lexed);
|
||||
|
||||
|
@ -52,23 +54,23 @@ function render(lexed, filename, template, cb) {
|
|||
|
||||
// content has to be the last thing we do with
|
||||
// the lexed tokens, because it's destructive.
|
||||
content = marked.parser(lexed);
|
||||
const content = marked.parser(lexed);
|
||||
template = template.replace(/__CONTENT__/g, content);
|
||||
|
||||
cb(null, template);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// just update the list item text in-place.
|
||||
// lists that come right after a heading are what we're after.
|
||||
function parseLists(input) {
|
||||
const parseLists = (input) => {
|
||||
let state = null;
|
||||
let depth = 0;
|
||||
const output = [];
|
||||
output.links = input.links;
|
||||
input.forEach((tok) => {
|
||||
if (state === null) {
|
||||
if (state == null) {
|
||||
if (tok.type === 'heading') {
|
||||
state = 'AFTERHEADING';
|
||||
}
|
||||
|
@ -112,29 +114,27 @@ function parseLists(input) {
|
|||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function parseListItem(text) {
|
||||
text = text.replace(/\{([^\}]+)\}/, '<span class="type">$1</span>');
|
||||
const parseListItem = (text) => {
|
||||
text = text.replace(/\{([^}]+)\}/, '<span class="type">$1</span>');
|
||||
// XXX maybe put more stuff here?
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// section is just the first heading
|
||||
function getSection(lexed) {
|
||||
const section = '';
|
||||
const getSection = (lexed) => {
|
||||
for (let i = 0, l = lexed.length; i < l; i++) {
|
||||
const tok = lexed[i];
|
||||
if (tok.type === 'heading') return tok.text;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function buildToc(lexed, filename, cb) {
|
||||
const indent = 0;
|
||||
const buildToc = (lexed, filename, cb) => {
|
||||
let toc = [];
|
||||
let depth = 0;
|
||||
lexed.forEach((tok) => {
|
||||
|
@ -155,18 +155,18 @@ function buildToc(lexed, filename, cb) {
|
|||
|
||||
toc = marked.parse(toc.join('\n'));
|
||||
cb(null, toc);
|
||||
}
|
||||
};
|
||||
|
||||
const idCounters = {};
|
||||
function getId(text) {
|
||||
const getId = (text) => {
|
||||
text = text.toLowerCase();
|
||||
text = text.replace(/[^a-z0-9]+/g, '_');
|
||||
text = text.replace(/^_+|_+$/, '');
|
||||
text = text.replace(/^([^a-z])/, '_$1');
|
||||
if (idCounters.hasOwnProperty(text)) {
|
||||
if (Object.prototype.hasOwnProperty.call(idCounters, text)) {
|
||||
text += `_${++idCounters[text]}`;
|
||||
} else {
|
||||
idCounters[text] = 0;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
|
@ -26,7 +27,7 @@ module.exports = doJSON;
|
|||
|
||||
const marked = require('marked');
|
||||
|
||||
function doJSON(input, filename, cb) {
|
||||
const doJSON = (input, filename, cb) => {
|
||||
const root = {source: filename};
|
||||
const stack = [root];
|
||||
let depth = 0;
|
||||
|
@ -40,7 +41,7 @@ function doJSON(input, filename, cb) {
|
|||
// <!-- type = module -->
|
||||
// This is for cases where the markdown semantic structure is lacking.
|
||||
if (type === 'paragraph' || type === 'html') {
|
||||
const metaExpr = /<!--([^=]+)=([^\-]+)-->\n*/g;
|
||||
const metaExpr = /<!--([^=]+)=([^-]+)-->\n*/g;
|
||||
text = text.replace(metaExpr, (_0, k, v) => {
|
||||
current[k.trim()] = v.trim();
|
||||
return '';
|
||||
|
@ -146,7 +147,7 @@ function doJSON(input, filename, cb) {
|
|||
}
|
||||
|
||||
return cb(null, root);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 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.',
|
||||
// default: 'false' } ] } ]
|
||||
|
||||
function processList(section) {
|
||||
const processList = (section) => {
|
||||
const list = section.list;
|
||||
const values = [];
|
||||
let current;
|
||||
|
@ -203,13 +204,13 @@ function processList(section) {
|
|||
if (type === 'space') return;
|
||||
if (type === 'list_item_start') {
|
||||
if (!current) {
|
||||
var n = {};
|
||||
const n = {};
|
||||
values.push(n);
|
||||
current = n;
|
||||
} else {
|
||||
current.options = current.options || [];
|
||||
stack.push(current);
|
||||
var n = {};
|
||||
const n = {};
|
||||
current.options.push(n);
|
||||
current = n;
|
||||
}
|
||||
|
@ -247,11 +248,11 @@ function processList(section) {
|
|||
switch (section.type) {
|
||||
case 'ctor':
|
||||
case 'classMethod':
|
||||
case 'method':
|
||||
case 'method': {
|
||||
// each item is an argument, unless the name is 'return',
|
||||
// in which case it's the return value.
|
||||
section.signatures = section.signatures || [];
|
||||
var sig = {};
|
||||
const sig = {};
|
||||
section.signatures.push(sig);
|
||||
sig.params = values.filter((v) => {
|
||||
if (v.name === 'return') {
|
||||
|
@ -262,11 +263,11 @@ function processList(section) {
|
|||
});
|
||||
parseSignature(section.textRaw, sig);
|
||||
break;
|
||||
|
||||
case 'property':
|
||||
}
|
||||
case 'property': {
|
||||
// there should be only one item, which is the value.
|
||||
// copy the data up to the section.
|
||||
var value = values[0] || {};
|
||||
const value = values[0] || {};
|
||||
delete value.name;
|
||||
section.typeof = value.type;
|
||||
delete value.type;
|
||||
|
@ -274,20 +275,21 @@ function processList(section) {
|
|||
section[k] = value[k];
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'event':
|
||||
case 'event': {
|
||||
// event: each item is an argument.
|
||||
section.params = values;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// section.listParsed = values;
|
||||
delete section.list;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// textRaw = "someobject.someMethod(a, [b=100], [c])"
|
||||
function parseSignature(text, sig) {
|
||||
const parseSignature = (text, sig) => {
|
||||
let params = text.match(paramExpr);
|
||||
if (!params) return;
|
||||
params = params[1];
|
||||
|
@ -322,10 +324,10 @@ function parseSignature(text, sig) {
|
|||
if (optional) param.optional = true;
|
||||
if (def !== undefined) param.default = def;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function parseListItem(item) {
|
||||
const parseListItem = (item) => {
|
||||
if (item.options) item.options.forEach(parseListItem);
|
||||
if (!item.textRaw) return;
|
||||
|
||||
|
@ -341,7 +343,7 @@ function parseListItem(item) {
|
|||
item.name = 'return';
|
||||
text = text.replace(retExpr, '');
|
||||
} else {
|
||||
const nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/;
|
||||
const nameExpr = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/;
|
||||
const name = text.match(nameExpr);
|
||||
if (name) {
|
||||
item.name = name[1];
|
||||
|
@ -358,7 +360,7 @@ function parseListItem(item) {
|
|||
}
|
||||
|
||||
text = text.trim();
|
||||
const typeExpr = /^\{([^\}]+)\}/;
|
||||
const typeExpr = /^\{([^}]+)\}/;
|
||||
const type = text.match(typeExpr);
|
||||
if (type) {
|
||||
item.type = type[1];
|
||||
|
@ -376,10 +378,10 @@ function parseListItem(item) {
|
|||
text = text.replace(/^\s*-\s*/, '');
|
||||
text = text.trim();
|
||||
if (text) item.desc = text;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function finishSection(section, parent) {
|
||||
const finishSection = (section, parent) => {
|
||||
if (!section || !parent) {
|
||||
throw new Error(`Invalid finishSection call\n${
|
||||
JSON.stringify(section)}\n${
|
||||
|
@ -416,7 +418,7 @@ function finishSection(section, parent) {
|
|||
ctor.signatures.forEach((sig) => {
|
||||
sig.desc = ctor.desc;
|
||||
});
|
||||
sigs.push.apply(sigs, ctor.signatures);
|
||||
sigs.push(...ctor.signatures);
|
||||
});
|
||||
delete section.ctors;
|
||||
}
|
||||
|
@ -479,50 +481,50 @@ function finishSection(section, parent) {
|
|||
|
||||
parent[plur] = parent[plur] || [];
|
||||
parent[plur].push(section);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Not a general purpose deep copy.
|
||||
// But sufficient for these basic things.
|
||||
function deepCopy(src, dest) {
|
||||
Object.keys(src).filter((k) => !dest.hasOwnProperty(k)).forEach((k) => {
|
||||
const deepCopy = (src, dest) => {
|
||||
Object.keys(src).filter((k) => !Object.prototype.hasOwnProperty.call(dest, k)).forEach((k) => {
|
||||
dest[k] = deepCopy_(src[k]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function deepCopy_(src) {
|
||||
const deepCopy_ = (src) => {
|
||||
if (!src) return src;
|
||||
if (Array.isArray(src)) {
|
||||
var c = new Array(src.length);
|
||||
const c = new Array(src.length);
|
||||
src.forEach((v, i) => {
|
||||
c[i] = deepCopy_(v);
|
||||
});
|
||||
return c;
|
||||
}
|
||||
if (typeof src === 'object') {
|
||||
var c = {};
|
||||
const c = {};
|
||||
Object.keys(src).forEach((k) => {
|
||||
c[k] = deepCopy_(src[k]);
|
||||
});
|
||||
return c;
|
||||
}
|
||||
return src;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// these parse out the contents of an H# tag
|
||||
const eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i;
|
||||
const classExpr = /^Class:\s*([^ ]+).*?$/i;
|
||||
const propExpr = /^(?:property:?\s*)?[^\.]+\.([^ \.\(\)]+)\s*?$/i;
|
||||
const braceExpr = /^(?:property:?\s*)?[^\.\[]+(\[[^\]]+\])\s*?$/i;
|
||||
const propExpr = /^(?:property:?\s*)?[^.]+\.([^ .()]+)\s*?$/i;
|
||||
const braceExpr = /^(?:property:?\s*)?[^.[]+(\[[^\]]+\])\s*?$/i;
|
||||
const classMethExpr =
|
||||
/^class\s*method\s*:?[^\.]+\.([^ \.\(\)]+)\([^\)]*\)\s*?$/i;
|
||||
/^class\s*method\s*:?[^.]+\.([^ .()]+)\([^)]*\)\s*?$/i;
|
||||
const methExpr =
|
||||
/^(?:method:?\s*)?(?:[^\.]+\.)?([^ \.\(\)]+)\([^\)]*\)\s*?$/i;
|
||||
const newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/;
|
||||
var paramExpr = /\((.*)\);?$/;
|
||||
/^(?:method:?\s*)?(?:[^.]+\.)?([^ .()]+)\([^)]*\)\s*?$/i;
|
||||
const newExpr = /^new ([A-Z][a-z]+)\([^)]*\)\s*?$/;
|
||||
const paramExpr = /\((.*)\);?$/;
|
||||
|
||||
function newSection(tok) {
|
||||
const newSection = (tok) => {
|
||||
const section = {};
|
||||
// infer the type from the text.
|
||||
const text = section.textRaw = tok.text;
|
||||
|
@ -551,4 +553,4 @@ function newSection(tok) {
|
|||
section.name = text;
|
||||
}
|
||||
return section;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"description": "Internal tool for generating Node.js API docs",
|
||||
"version": "0.0.0",
|
||||
"engines": {
|
||||
"node": ">=0.6.10"
|
||||
"node": ">=10.17.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"marked": "0.8.2"
|
||||
|
|
|
@ -16,59 +16,54 @@ if (process.argv.length !== 3) throw new Error('Use: node extractPadData.js $PAD
|
|||
const padId = process.argv[2];
|
||||
|
||||
const npm = require('ep_etherpad-lite/node_modules/npm');
|
||||
const util = require('util');
|
||||
|
||||
npm.load({}, async (err) => {
|
||||
if (err) throw err;
|
||||
(async () => {
|
||||
await util.promisify(npm.load)({});
|
||||
|
||||
try {
|
||||
// initialize database
|
||||
require('ep_etherpad-lite/node/utils/Settings');
|
||||
const db = require('ep_etherpad-lite/node/db/DB');
|
||||
await db.init();
|
||||
// initialize database
|
||||
require('ep_etherpad-lite/node/utils/Settings');
|
||||
const db = require('ep_etherpad-lite/node/db/DB');
|
||||
await db.init();
|
||||
|
||||
// load extra modules
|
||||
const dirtyDB = require('ep_etherpad-lite/node_modules/dirty');
|
||||
const padManager = require('ep_etherpad-lite/node/db/PadManager');
|
||||
const util = require('util');
|
||||
// load extra modules
|
||||
const dirtyDB = require('ep_etherpad-lite/node_modules/dirty');
|
||||
const padManager = require('ep_etherpad-lite/node/db/PadManager');
|
||||
|
||||
// initialize output database
|
||||
const dirty = dirtyDB(`${padId}.db`);
|
||||
// initialize output database
|
||||
const dirty = dirtyDB(`${padId}.db`);
|
||||
|
||||
// Promise wrapped get and set function
|
||||
const wrapped = db.db.db.wrappedDB;
|
||||
const get = util.promisify(wrapped.get.bind(wrapped));
|
||||
const set = util.promisify(dirty.set.bind(dirty));
|
||||
// Promise wrapped get and set function
|
||||
const wrapped = db.db.db.wrappedDB;
|
||||
const get = util.promisify(wrapped.get.bind(wrapped));
|
||||
const set = util.promisify(dirty.set.bind(dirty));
|
||||
|
||||
// array in which required key values will be accumulated
|
||||
const neededDBValues = [`pad:${padId}`];
|
||||
// array in which required key values will be accumulated
|
||||
const neededDBValues = [`pad:${padId}`];
|
||||
|
||||
// get the actual pad object
|
||||
const pad = await padManager.getPad(padId);
|
||||
// get the actual pad object
|
||||
const pad = await padManager.getPad(padId);
|
||||
|
||||
// add all authors
|
||||
neededDBValues.push(...pad.getAllAuthors().map((author) => `globalAuthor:${author}`));
|
||||
// add all authors
|
||||
neededDBValues.push(...pad.getAllAuthors().map((author) => `globalAuthor:${author}`));
|
||||
|
||||
// add all revisions
|
||||
for (let rev = 0; rev <= pad.head; ++rev) {
|
||||
neededDBValues.push(`pad:${padId}:revs:${rev}`);
|
||||
}
|
||||
|
||||
// add all chat values
|
||||
for (let chat = 0; chat <= pad.chatHead; ++chat) {
|
||||
neededDBValues.push(`pad:${padId}:chat:${chat}`);
|
||||
}
|
||||
|
||||
for (const dbkey of neededDBValues) {
|
||||
let dbvalue = await get(dbkey);
|
||||
if (dbvalue && typeof dbvalue !== 'object') {
|
||||
dbvalue = JSON.parse(dbvalue);
|
||||
}
|
||||
await set(dbkey, dbvalue);
|
||||
}
|
||||
|
||||
console.log('finished');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
// add all revisions
|
||||
for (let rev = 0; rev <= pad.head; ++rev) {
|
||||
neededDBValues.push(`pad:${padId}:revs:${rev}`);
|
||||
}
|
||||
});
|
||||
|
||||
// add all chat values
|
||||
for (let chat = 0; chat <= pad.chatHead; ++chat) {
|
||||
neededDBValues.push(`pad:${padId}:chat:${chat}`);
|
||||
}
|
||||
|
||||
for (const dbkey of neededDBValues) {
|
||||
let dbvalue = await get(dbkey);
|
||||
if (dbvalue && typeof dbvalue !== 'object') {
|
||||
dbvalue = JSON.parse(dbvalue);
|
||||
}
|
||||
await set(dbkey, dbvalue);
|
||||
}
|
||||
|
||||
console.log('finished');
|
||||
})();
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
|
||||
process.on('unhandledRejection', (err) => { throw err; });
|
||||
|
||||
const npm = require('ep_etherpad-lite/node_modules/npm');
|
||||
const util = require('util');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const log = (str) => {
|
||||
|
@ -43,10 +46,10 @@ const unescape = (val) => {
|
|||
return val;
|
||||
};
|
||||
|
||||
(async () => {
|
||||
await util.promisify(npm.load)({});
|
||||
|
||||
require('ep_etherpad-lite/node_modules/npm').load({}, (er, npm) => {
|
||||
const fs = require('fs');
|
||||
|
||||
const ueberDB = require('ep_etherpad-lite/node_modules/ueberdb2');
|
||||
const settings = require('ep_etherpad-lite/node/utils/Settings');
|
||||
const log4js = require('ep_etherpad-lite/node_modules/log4js');
|
||||
|
@ -68,42 +71,35 @@ require('ep_etherpad-lite/node_modules/npm').load({}, (er, npm) => {
|
|||
if (!sqlFile) throw new Error('Use: node importSqlFile.js $SQLFILE');
|
||||
|
||||
log('initializing db');
|
||||
db.init((err) => {
|
||||
// there was an error while initializing the database, output it and stop
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
log('done');
|
||||
await util.promisify(db.init.bind(db))();
|
||||
log('done');
|
||||
|
||||
log('open output file...');
|
||||
const lines = fs.readFileSync(sqlFile, 'utf8').split('\n');
|
||||
log('open output file...');
|
||||
const lines = fs.readFileSync(sqlFile, 'utf8').split('\n');
|
||||
|
||||
const count = lines.length;
|
||||
let keyNo = 0;
|
||||
const count = lines.length;
|
||||
let keyNo = 0;
|
||||
|
||||
process.stdout.write(`Start importing ${count} keys...\n`);
|
||||
lines.forEach((l) => {
|
||||
if (l.substr(0, 27) === 'REPLACE INTO store VALUES (') {
|
||||
const pos = l.indexOf("', '");
|
||||
const key = l.substr(28, pos - 28);
|
||||
let value = l.substr(pos + 3);
|
||||
value = value.substr(0, value.length - 2);
|
||||
console.log(`key: ${key} val: ${value}`);
|
||||
console.log(`unval: ${unescape(value)}`);
|
||||
db.set(key, unescape(value), null);
|
||||
keyNo++;
|
||||
if (keyNo % 1000 === 0) {
|
||||
process.stdout.write(` ${keyNo}/${count}\n`);
|
||||
}
|
||||
}
|
||||
});
|
||||
process.stdout.write('\n');
|
||||
process.stdout.write('done. waiting for db to finish transaction. ' +
|
||||
'depended on dbms this may take some time..\n');
|
||||
|
||||
db.close(() => {
|
||||
log(`finished, imported ${keyNo} keys.`);
|
||||
});
|
||||
process.stdout.write(`Start importing ${count} keys...\n`);
|
||||
lines.forEach((l) => {
|
||||
if (l.substr(0, 27) === 'REPLACE INTO store VALUES (') {
|
||||
const pos = l.indexOf("', '");
|
||||
const key = l.substr(28, pos - 28);
|
||||
let value = l.substr(pos + 3);
|
||||
value = value.substr(0, value.length - 2);
|
||||
console.log(`key: ${key} val: ${value}`);
|
||||
console.log(`unval: ${unescape(value)}`);
|
||||
db.set(key, unescape(value), null);
|
||||
keyNo++;
|
||||
if (keyNo % 1000 === 0) {
|
||||
process.stdout.write(` ${keyNo}/${count}\n`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
process.stdout.write('\n');
|
||||
process.stdout.write('done. waiting for db to finish transaction. ' +
|
||||
'depended on dbms this may take some time..\n');
|
||||
|
||||
await util.promisify(db.close.bind(db))();
|
||||
log(`finished, imported ${keyNo} keys.`);
|
||||
})();
|
||||
|
|
|
@ -4,9 +4,12 @@
|
|||
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
|
||||
process.on('unhandledRejection', (err) => { throw err; });
|
||||
|
||||
const npm = require('ep_etherpad-lite/node_modules/npm');
|
||||
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}/..`);
|
||||
|
||||
// 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))();
|
||||
console.log('Finished.');
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -220,12 +220,12 @@ fs.readdir(pluginPath, (err, rootFiles) => {
|
|||
}
|
||||
|
||||
updateDeps(parsedPackageJSON, 'devDependencies', {
|
||||
'eslint': '^7.17.0',
|
||||
'eslint-config-etherpad': '^1.0.22',
|
||||
'eslint': '^7.18.0',
|
||||
'eslint-config-etherpad': '^1.0.24',
|
||||
'eslint-plugin-eslint-comments': '^3.2.0',
|
||||
'eslint-plugin-mocha': '^8.0.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-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');
|
||||
if (autoFix) {
|
||||
const engines = {
|
||||
node: '>=10.13.0',
|
||||
node: '^10.17.0 || >=11.14.0',
|
||||
};
|
||||
parsedPackageJSON.engines = engines;
|
||||
writePackageJson(parsedPackageJSON);
|
||||
|
|
|
@ -13,7 +13,6 @@ if (process.argv.length !== 4 && process.argv.length !== 5) {
|
|||
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 util = require('util');
|
||||
|
||||
|
@ -21,95 +20,70 @@ const padId = process.argv[2];
|
|||
const newRevHead = process.argv[3];
|
||||
const newPadId = process.argv[4] || `${padId}-rebuilt`;
|
||||
|
||||
let db, oldPad, newPad;
|
||||
let Pad, PadManager;
|
||||
(async () => {
|
||||
await util.promisify(npm.load)({});
|
||||
|
||||
async.series([
|
||||
(callback) => npm.load({}, callback),
|
||||
(callback) => {
|
||||
// Get a handle into the database
|
||||
db = require('ep_etherpad-lite/node/db/DB');
|
||||
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
|
||||
// not already exist to avoid overwriting it.
|
||||
if (!PadManager.isValidPadId(newPadId)) {
|
||||
throw new Error('Cannot create a pad with that id as it is invalid');
|
||||
const db = require('ep_etherpad-lite/node/db/DB');
|
||||
await db.init();
|
||||
|
||||
const PadManager = require('ep_etherpad-lite/node/db/PadManager');
|
||||
const Pad = require('ep_etherpad-lite/node/db/Pad').Pad;
|
||||
// Validate the newPadId if specified and that a pad with that ID does
|
||||
// not already exist to avoid overwriting it.
|
||||
if (!PadManager.isValidPadId(newPadId)) {
|
||||
throw new Error('Cannot create a pad with that id as it is invalid');
|
||||
}
|
||||
const exists = await PadManager.doesPadExist(newPadId);
|
||||
if (exists) throw new Error('Cannot create a pad with that id as it already exists');
|
||||
|
||||
const oldPad = await PadManager.getPad(padId);
|
||||
const newPad = new Pad(newPadId);
|
||||
|
||||
// Clone all Chat revisions
|
||||
const chatHead = oldPad.chatHead;
|
||||
await Promise.all([...Array(chatHead + 1).keys()].map(async (i) => {
|
||||
const chat = await db.get(`pad:${padId}:chat:${i}`);
|
||||
await db.set(`pad:${newPadId}:chat:${i}`, chat);
|
||||
console.log(`Created: Chat Revision: pad:${newPadId}:chat:${i}`);
|
||||
}));
|
||||
|
||||
// Rebuild Pad from revisions up to and including the new revision head
|
||||
const AuthorManager = require('ep_etherpad-lite/node/db/AuthorManager');
|
||||
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
|
||||
// Author attributes are derived from changesets, but there can also be
|
||||
// non-author attributes with specific mappings that changesets depend on
|
||||
// and, AFAICT, cannot be recreated any other way
|
||||
newPad.pool.numToAttrib = oldPad.pool.numToAttrib;
|
||||
for (let curRevNum = 0; curRevNum <= newRevHead; curRevNum++) {
|
||||
const rev = await db.get(`pad:${padId}:revs:${curRevNum}`);
|
||||
if (!rev || !rev.meta) throw new Error('The specified revision number could not be found.');
|
||||
const newRevNum = ++newPad.head;
|
||||
const newRevId = `pad:${newPad.id}:revs:${newRevNum}`;
|
||||
await Promise.all([
|
||||
db.set(newRevId, rev),
|
||||
AuthorManager.addPad(rev.meta.author, newPad.id),
|
||||
]);
|
||||
newPad.atext = Changeset.applyToAText(rev.changeset, newPad.atext, newPad.pool);
|
||||
console.log(`Created: Revision: pad:${newPad.id}:revs:${newRevNum}`);
|
||||
}
|
||||
|
||||
// Add saved revisions up to the new revision head
|
||||
console.log(newPad.head);
|
||||
const newSavedRevisions = [];
|
||||
for (const savedRev of oldPad.savedRevisions) {
|
||||
if (savedRev.revNum <= newRevHead) {
|
||||
newSavedRevisions.push(savedRev);
|
||||
console.log(`Added: Saved Revision: ${savedRev.revNum}`);
|
||||
}
|
||||
PadManager.doesPadExists(newPadId, (err, exists) => {
|
||||
if (exists) throw new Error('Cannot create a pad with that id as it already exists');
|
||||
});
|
||||
PadManager.getPad(padId, (err, pad) => {
|
||||
oldPad = pad;
|
||||
newPad = new Pad(newPadId);
|
||||
callback();
|
||||
});
|
||||
},
|
||||
(callback) => {
|
||||
// Clone all Chat revisions
|
||||
const chatHead = oldPad.chatHead;
|
||||
for (let i = 0, curHeadNum = 0; i <= chatHead; i++) {
|
||||
db.db.get(`pad:${padId}:chat:${i}`, (err, chat) => {
|
||||
db.db.set(`pad:${newPadId}:chat:${curHeadNum++}`, chat);
|
||||
console.log(`Created: Chat Revision: pad:${newPadId}:chat:${curHeadNum}`);
|
||||
});
|
||||
}
|
||||
callback();
|
||||
},
|
||||
(callback) => {
|
||||
// Rebuild Pad from revisions up to and including the new revision head
|
||||
const AuthorManager = require('ep_etherpad-lite/node/db/AuthorManager');
|
||||
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
|
||||
// Author attributes are derived from changesets, but there can also be
|
||||
// non-author attributes with specific mappings that changesets depend on
|
||||
// and, AFAICT, cannot be recreated any other way
|
||||
newPad.pool.numToAttrib = oldPad.pool.numToAttrib;
|
||||
for (let curRevNum = 0; curRevNum <= newRevHead; curRevNum++) {
|
||||
db.db.get(`pad:${padId}:revs:${curRevNum}`, (err, rev) => {
|
||||
if (rev.meta) {
|
||||
throw new Error('The specified revision number could not be found.');
|
||||
}
|
||||
const newRevNum = ++newPad.head;
|
||||
const newRevId = `pad:${newPad.id}:revs:${newRevNum}`;
|
||||
db.db.set(newRevId, rev);
|
||||
AuthorManager.addPad(rev.meta.author, newPad.id);
|
||||
newPad.atext = Changeset.applyToAText(rev.changeset, newPad.atext, newPad.pool);
|
||||
console.log(`Created: Revision: pad:${newPad.id}:revs:${newRevNum}`);
|
||||
if (newRevNum === newRevHead) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
(callback) => {
|
||||
// Add saved revisions up to the new revision head
|
||||
console.log(newPad.head);
|
||||
const newSavedRevisions = [];
|
||||
for (const savedRev of oldPad.savedRevisions) {
|
||||
if (savedRev.revNum <= newRevHead) {
|
||||
newSavedRevisions.push(savedRev);
|
||||
console.log(`Added: Saved Revision: ${savedRev.revNum}`);
|
||||
}
|
||||
}
|
||||
newPad.savedRevisions = newSavedRevisions;
|
||||
callback();
|
||||
},
|
||||
(callback) => {
|
||||
// Save the source pad
|
||||
db.db.set(`pad:${newPadId}`, newPad, (err) => {
|
||||
console.log(`Created: Source Pad: pad:${newPadId}`);
|
||||
util.callbackify(newPad.saveToDatabase.bind(newPad))(callback);
|
||||
});
|
||||
},
|
||||
], (err) => {
|
||||
if (err) throw err;
|
||||
}
|
||||
newPad.savedRevisions = newSavedRevisions;
|
||||
|
||||
// Save the source pad
|
||||
await db.set(`pad:${newPadId}`, newPad);
|
||||
|
||||
console.log(`Created: Source Pad: pad:${newPadId}`);
|
||||
await newPad.saveToDatabase();
|
||||
|
||||
await db.shutdown();
|
||||
console.info('finished');
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -18,8 +18,10 @@ const padId = process.argv[2];
|
|||
let valueCount = 0;
|
||||
|
||||
const npm = require('ep_etherpad-lite/node_modules/npm');
|
||||
npm.load({}, async (err) => {
|
||||
if (err) throw err;
|
||||
const util = require('util');
|
||||
|
||||
(async () => {
|
||||
await util.promisify(npm.load)({});
|
||||
|
||||
// intialize database
|
||||
require('ep_etherpad-lite/node/utils/Settings');
|
||||
|
@ -56,4 +58,4 @@ npm.load({}, async (err) => {
|
|||
}
|
||||
|
||||
console.info(`Finished: Replaced ${valueCount} values in the database`);
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -421,7 +421,20 @@ Things in context:
|
|||
4. text - the text for that line
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -225,7 +225,7 @@ publish your plugin.
|
|||
"author": "USERNAME (REAL NAME) <MAIL@EXAMPLE.COM>",
|
||||
"contributors": [],
|
||||
"dependencies": {"MODULE": "0.3.20"},
|
||||
"engines": { "node": ">= 10.13.0"}
|
||||
"engines": { "node": "^10.17.0 || >=11.14.0"}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
4294
package-lock.json
generated
4294
package-lock.json
generated
File diff suppressed because it is too large
Load diff
21
package.json
21
package.json
|
@ -3,12 +3,12 @@
|
|||
"ep_etherpad-lite": "file:src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.15.0",
|
||||
"eslint-config-etherpad": "^1.0.20",
|
||||
"eslint": "^7.18.0",
|
||||
"eslint-config-etherpad": "^1.0.24",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-mocha": "^8.0.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-you-dont-need-lodash-underscore": "^6.10.0"
|
||||
},
|
||||
|
@ -39,7 +39,9 @@
|
|||
"tests/**/*"
|
||||
],
|
||||
"excludedFiles": [
|
||||
"**/.eslintrc.js"
|
||||
"**/.eslintrc.js",
|
||||
"tests/frontend/travis/**/*",
|
||||
"tests/ratelimit/**/*"
|
||||
],
|
||||
"extends": "etherpad/tests",
|
||||
"rules": {
|
||||
|
@ -75,7 +77,8 @@
|
|||
"tests/frontend/**/*"
|
||||
],
|
||||
"excludedFiles": [
|
||||
"**/.eslintrc.js"
|
||||
"**/.eslintrc.js",
|
||||
"tests/frontend/travis/**/*"
|
||||
],
|
||||
"extends": "etherpad/tests/frontend",
|
||||
"overrides": [
|
||||
|
@ -92,6 +95,12 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"tests/frontend/travis/**/*"
|
||||
],
|
||||
"extends": "etherpad/node"
|
||||
}
|
||||
],
|
||||
"root": true
|
||||
|
@ -100,6 +109,6 @@
|
|||
"lint": "eslint ."
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
"node": "^10.17.0 || >=11.14.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,24 +11,42 @@
|
|||
"Sebastian Wallroth",
|
||||
"Thargon",
|
||||
"Tim.krieger",
|
||||
"Wikinaut"
|
||||
"Wikinaut",
|
||||
"Zunkelty"
|
||||
]
|
||||
},
|
||||
"admin.page-title": "Admin Dashboard - Etherpad",
|
||||
"admin_plugins": "Plugins verwalten",
|
||||
"admin_plugins.available": "Verfügbare Plugins",
|
||||
"admin_plugins.available_not-found": "Keine Plugins gefunden.",
|
||||
"admin_plugins.available_fetching": "Wird abgerufen...",
|
||||
"admin_plugins.available_install.value": "Installieren",
|
||||
"admin_plugins.available_search.placeholder": "Suche nach Plugins zum Installieren",
|
||||
"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_uninstall.value": "Deinstallieren",
|
||||
"admin_plugins.last-update": "Letze Aktualisierung",
|
||||
"admin_plugins.name": "Name",
|
||||
"admin_plugins.page-title": "Plugin Manager - Etherpad",
|
||||
"admin_plugins.version": "Version",
|
||||
"admin_plugins_info": "Hilfestellung",
|
||||
"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.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_settings": "Einstellungen",
|
||||
"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.page-title": "Einstellungen - Etherpad",
|
||||
"index.newPad": "Neues Pad",
|
||||
"index.createOpenPad": "oder ein Pad mit folgendem Namen erstellen/öffnen:",
|
||||
"index.openPad": "Öffne ein vorhandenes Pad mit folgendem Namen:",
|
||||
|
@ -78,7 +96,7 @@
|
|||
"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.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.reconnecttimer": "Versuche Neuverbindung in",
|
||||
"pad.modals.cancel": "Abbrechen",
|
||||
|
@ -102,6 +120,7 @@
|
|||
"pad.modals.deleted.explanation": "Dieses Pad wurde entfernt.",
|
||||
"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.rejected.explanation": "Der Server hat eine Nachricht abgelehnt, die von deinem Browser gesendet wurde.",
|
||||
"pad.modals.disconnected": "Ihre Verbindung wurde getrennt.",
|
||||
"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.",
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"VezonThunder"
|
||||
]
|
||||
},
|
||||
"admin_plugins.available": "Saatavilla olevat liitännäiset",
|
||||
"admin_plugins.available_install.value": "Lataa",
|
||||
"admin_plugins.available_search.placeholder": "Etsi asennettavia laajennuksia",
|
||||
"admin_plugins.description": "Kuvaus",
|
||||
|
@ -92,7 +93,7 @@
|
|||
"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.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.reconnecttimer": "Yritetään yhdistää uudelleen",
|
||||
"pad.modals.cancel": "Peruuta",
|
||||
|
|
|
@ -2,12 +2,47 @@
|
|||
"@metadata": {
|
||||
"authors": [
|
||||
"Elisardojm",
|
||||
"Ghose",
|
||||
"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.createOpenPad": "ou cree/abra un documento co nome:",
|
||||
"pad.toolbar.bold.title": "Negra (Ctrl-B)",
|
||||
"index.createOpenPad": "ou crea/abre un documento co nome:",
|
||||
"index.openPad": "abrir un Pad existente co nome:",
|
||||
"pad.toolbar.bold.title": "Resaltado (Ctrl-B)",
|
||||
"pad.toolbar.italic.title": "Cursiva (Ctrl-I)",
|
||||
"pad.toolbar.underline.title": "Subliñar (Ctrl-U)",
|
||||
"pad.toolbar.strikethrough.title": "Riscar (Ctrl+5)",
|
||||
|
@ -17,28 +52,30 @@
|
|||
"pad.toolbar.unindent.title": "Sen sangría (Maiús.+TAB)",
|
||||
"pad.toolbar.undo.title": "Desfacer (Ctrl-Z)",
|
||||
"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.timeslider.title": "Liña do tempo",
|
||||
"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.showusers.title": "Mostrar os usuarios deste documento",
|
||||
"pad.toolbar.showusers.title": "Mostrar as usuarias deste documento",
|
||||
"pad.colorpicker.save": "Gardar",
|
||||
"pad.colorpicker.cancel": "Cancelar",
|
||||
"pad.loading": "Cargando...",
|
||||
"pad.noCookie": "Non se puido atopar a cookie. Por favor, habilite as cookies no seu navegador!",
|
||||
"pad.permissionDenied": "Non ten permiso para acceder a este documento",
|
||||
"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 tes permiso para acceder a este documento",
|
||||
"pad.settings.padSettings": "Configuracións do documento",
|
||||
"pad.settings.myView": "A miña vista",
|
||||
"pad.settings.stickychat": "Chat sempre visible",
|
||||
"pad.settings.chatandusers": "Mostrar o chat e os usuarios",
|
||||
"pad.settings.colorcheck": "Cores de identificación",
|
||||
"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.normal": "Normal",
|
||||
"pad.settings.language": "Lingua:",
|
||||
"pad.settings.about": "Acerca de",
|
||||
"pad.settings.poweredBy": "Grazas a",
|
||||
"pad.importExport.import_export": "Importar/Exportar",
|
||||
"pad.importExport.import": "Cargar un ficheiro de texto ou documento",
|
||||
"pad.importExport.importSuccessful": "Correcto!",
|
||||
|
@ -49,9 +86,9 @@
|
|||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"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.reconnecting": "Reconectando co seu documento...",
|
||||
"pad.modals.reconnecting": "Reconectando co teu documento...",
|
||||
"pad.modals.forcereconnect": "Forzar a reconexión",
|
||||
"pad.modals.reconnecttimer": "Intentarase reconectar en",
|
||||
"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.deleted": "Borrado.",
|
||||
"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.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.",
|
||||
|
@ -83,6 +124,9 @@
|
|||
"pad.chat": "Chat",
|
||||
"pad.chat.title": "Abrir o chat deste documento.",
|
||||
"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.toolbar.returnbutton": "Volver ao documento",
|
||||
"timeslider.toolbar.authors": "Autores:",
|
||||
|
@ -112,7 +156,7 @@
|
|||
"pad.savedrevs.timeslider": "Pode consultar as revisións gardadas visitando a liña do tempo",
|
||||
"pad.userlist.entername": "Insira o seu nome",
|
||||
"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.importing": "Importando...",
|
||||
"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.importfailed": "Fallou a importación",
|
||||
"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"
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"Athena in Wonderland",
|
||||
"Cainamarques",
|
||||
"GoEThe",
|
||||
"Guilha",
|
||||
"Hamilton Abreu",
|
||||
"Imperadeiro98",
|
||||
"Luckas",
|
||||
|
@ -16,9 +17,42 @@
|
|||
"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.createOpenPad": "ou crie/abra uma nota com o nome:",
|
||||
"index.openPad": "abrir uma «Nota» existente com o nome:",
|
||||
"index.createOpenPad": "ou cria/abre uma nota com o nome:",
|
||||
"index.openPad": "abrir uma Nota existente com o nome:",
|
||||
"pad.toolbar.bold.title": "Negrito (Ctrl+B)",
|
||||
"pad.toolbar.italic.title": "Itálico (Ctrl+I)",
|
||||
"pad.toolbar.underline.title": "Sublinhado (Ctrl+U)",
|
||||
|
@ -26,7 +60,7 @@
|
|||
"pad.toolbar.ol.title": "Lista ordenada (Ctrl+Shift+N)",
|
||||
"pad.toolbar.ul.title": "Lista desordenada (Ctrl+Shift+L)",
|
||||
"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.redo.title": "Refazer (Ctrl+Y)",
|
||||
"pad.toolbar.clearAuthorship.title": "Limpar cores de autoria (Ctrl+Shift+C)",
|
||||
|
@ -65,7 +99,7 @@
|
|||
"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.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.reconnecttimer": "A tentar restabelecer ligação",
|
||||
"pad.modals.cancel": "Cancelar",
|
||||
|
@ -89,6 +123,8 @@
|
|||
"pad.modals.deleted.explanation": "Esta nota foi removida.",
|
||||
"pad.modals.rateLimited": "Limitado.",
|
||||
"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.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.",
|
||||
|
|
|
@ -18,21 +18,39 @@
|
|||
"Арсен Асхат"
|
||||
]
|
||||
},
|
||||
"admin.page-title": "Панель администратора — Etherpad",
|
||||
"admin_plugins": "Менеджер плагинов",
|
||||
"admin_plugins.available": "Доступные плагины",
|
||||
"admin_plugins.available_not-found": "Плагины не найдены.",
|
||||
"admin_plugins.available_fetching": "Получение…",
|
||||
"admin_plugins.available_install.value": "Установить",
|
||||
"admin_plugins.available_search.placeholder": "Искать плагины для установки",
|
||||
"admin_plugins.description": "Описание",
|
||||
"admin_plugins.installed": "Установленные плагины",
|
||||
"admin_plugins.installed_fetching": "Получение установленных плагинов…",
|
||||
"admin_plugins.installed_nothing": "Вы еще не установили ни одного плагина.",
|
||||
"admin_plugins.installed_uninstall.value": "Удалить",
|
||||
"admin_plugins.last-update": "Последнее обновление",
|
||||
"admin_plugins.name": "Название",
|
||||
"admin_plugins.page-title": "Менеджер плагинов — Etherpad",
|
||||
"admin_plugins.version": "Версия",
|
||||
"admin_plugins_info": "Информация об устранении неполадок",
|
||||
"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_settings": "Настройки",
|
||||
"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.page-title": "Настройки — Etherpad",
|
||||
"index.newPad": "Создать",
|
||||
"index.createOpenPad": "или создать/открыть документ с именем:",
|
||||
"index.openPad": "откройте существующий документ с именем:",
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
"Upwinxp"
|
||||
]
|
||||
},
|
||||
"admin_plugins.last-update": "Zadnja posodobitev",
|
||||
"admin_plugins.name": "Ime",
|
||||
"admin_plugins.version": "Različica",
|
||||
"admin_settings": "Nastavitve",
|
||||
"index.newPad": "Nov dokument",
|
||||
"index.createOpenPad": "ali pa ustvari/odpri dokument z imenom:",
|
||||
"pad.toolbar.bold.title": "Krepko (Ctrl-B)",
|
||||
|
@ -54,7 +58,7 @@
|
|||
"pad.importExport.exportword": "DOC (zapis Microsoft Word)",
|
||||
"pad.importExport.exportpdf": "PDF (zapis Acrobat PDF)",
|
||||
"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.reconnecting": "Poteka povezovanje z dokumentom ...",
|
||||
"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.advice": "Ponovno vzpostavite povezavo in uporabljajte to okno.",
|
||||
"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.cause": "Morda ste se povezali preko neustrezno nastavljenega požarnega zidu ali posredniškega strežnika.",
|
||||
"pad.modals.initsocketfail": "Strežnik je nedosegljiv.",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* This module provides all API functions
|
||||
*/
|
||||
|
@ -18,8 +19,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
|
||||
const customError = require('../utils/customError');
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const CustomError = require('../utils/customError');
|
||||
const padManager = require('./PadManager');
|
||||
const padMessageHandler = require('../handler/PadMessageHandler');
|
||||
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);
|
||||
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
|
||||
if (rev !== undefined) {
|
||||
rev = checkValidRev(rev);
|
||||
|
@ -133,7 +134,7 @@ exports.getRevisionChangeset = async function (padID, rev) {
|
|||
if (rev !== undefined) {
|
||||
// check if this is a valid revision
|
||||
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
|
||||
|
@ -152,7 +153,7 @@ Example returns:
|
|||
{code: 0, message:"ok", data: {text:"Welcome Text"}}
|
||||
{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
|
||||
if (rev !== undefined) {
|
||||
rev = checkValidRev(rev);
|
||||
|
@ -166,7 +167,7 @@ exports.getText = async function (padID, rev) {
|
|||
if (rev !== undefined) {
|
||||
// check if this is a valid revision
|
||||
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
|
||||
|
@ -188,10 +189,10 @@ Example returns:
|
|||
{code: 1, message:"padID does not exist", data: null}
|
||||
{code: 1, message:"text too long", data: null}
|
||||
*/
|
||||
exports.setText = async function (padID, text) {
|
||||
exports.setText = async (padID, text) => {
|
||||
// text is required
|
||||
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
|
||||
|
@ -212,10 +213,10 @@ Example returns:
|
|||
{code: 1, message:"padID does not exist", data: null}
|
||||
{code: 1, message:"text too long", data: null}
|
||||
*/
|
||||
exports.appendText = async function (padID, text) {
|
||||
exports.appendText = async (padID, text) => {
|
||||
// text is required
|
||||
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);
|
||||
|
@ -233,7 +234,7 @@ Example returns:
|
|||
{code: 0, message:"ok", data: {text:"Welcome <strong>Text</strong>"}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
*/
|
||||
exports.getHTML = async function (padID, rev) {
|
||||
exports.getHTML = async (padID, rev) => {
|
||||
if (rev !== undefined) {
|
||||
rev = checkValidRev(rev);
|
||||
}
|
||||
|
@ -245,7 +246,7 @@ exports.getHTML = async function (padID, rev) {
|
|||
// check if this is a valid revision
|
||||
const head = pad.getHeadRevisionNumber();
|
||||
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: 1, message:"padID does not exist", data: null}
|
||||
*/
|
||||
exports.setHTML = async function (padID, html) {
|
||||
exports.setHTML = async (padID, html) => {
|
||||
// html string is required
|
||||
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
|
||||
|
@ -278,7 +279,7 @@ exports.setHTML = async function (padID, html) {
|
|||
try {
|
||||
await importHtml.setPadHTML(pad, cleanText(html));
|
||||
} catch (e) {
|
||||
throw new customError('HTML is malformed', 'apierror');
|
||||
throw new CustomError('HTML is malformed', 'apierror');
|
||||
}
|
||||
|
||||
// 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:
|
||||
|
||||
{"code":0,"message":"ok","data":{"messages":[{"text":"foo","authorID":"a.foo","time":1359199533759,"userName":"test"},
|
||||
{"text":"bar","authorID":"a.foo","time":1359199534622,"userName":"test"}]}}
|
||||
{"code":0,"message":"ok","data":{"messages":[
|
||||
{"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:"padID does not exist", data: null}
|
||||
*/
|
||||
exports.getChatHistory = async function (padID, start, end) {
|
||||
exports.getChatHistory = async (padID, start, end) => {
|
||||
if (start && end) {
|
||||
if (start < 0) {
|
||||
throw new customError('start is below zero', 'apierror');
|
||||
throw new CustomError('start is below zero', 'apierror');
|
||||
}
|
||||
if (end < 0) {
|
||||
throw new customError('end is below zero', 'apierror');
|
||||
throw new CustomError('end is below zero', 'apierror');
|
||||
}
|
||||
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;
|
||||
|
||||
// fall back to getting the whole chat-history if a parameter is missing
|
||||
if (!start || !end) {
|
||||
if (!start || !end) {
|
||||
start = 0;
|
||||
end = pad.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) {
|
||||
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
|
||||
|
@ -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:
|
||||
|
||||
{code: 0, message:"ok", 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
|
||||
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 === undefined || !is_int(time)) {
|
||||
if (time === undefined || !isInt(time)) {
|
||||
time = Date.now();
|
||||
}
|
||||
|
||||
|
@ -375,7 +379,7 @@ Example returns:
|
|||
{code: 0, message:"ok", data: {revisions: 56}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
*/
|
||||
exports.getRevisionsCount = async function (padID) {
|
||||
exports.getRevisionsCount = async (padID) => {
|
||||
// get the pad
|
||||
const pad = await getPadSafe(padID, true);
|
||||
return {revisions: pad.getHeadRevisionNumber()};
|
||||
|
@ -389,7 +393,7 @@ Example returns:
|
|||
{code: 0, message:"ok", data: {savedRevisions: 42}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
*/
|
||||
exports.getSavedRevisionsCount = async function (padID) {
|
||||
exports.getSavedRevisionsCount = async (padID) => {
|
||||
// get the pad
|
||||
const pad = await getPadSafe(padID, true);
|
||||
return {savedRevisions: pad.getSavedRevisionsNumber()};
|
||||
|
@ -403,7 +407,7 @@ Example returns:
|
|||
{code: 0, message:"ok", data: {savedRevisions: [2, 42, 1337]}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
*/
|
||||
exports.listSavedRevisions = async function (padID) {
|
||||
exports.listSavedRevisions = async (padID) => {
|
||||
// get the pad
|
||||
const pad = await getPadSafe(padID, true);
|
||||
return {savedRevisions: pad.getSavedRevisionsList()};
|
||||
|
@ -417,7 +421,7 @@ Example returns:
|
|||
{code: 0, message:"ok", 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
|
||||
if (rev !== undefined) {
|
||||
rev = checkValidRev(rev);
|
||||
|
@ -430,7 +434,7 @@ exports.saveRevision = async function (padID, rev) {
|
|||
// the client asked for a special revision
|
||||
if (rev !== undefined) {
|
||||
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 {
|
||||
rev = pad.getHeadRevisionNumber();
|
||||
|
@ -448,7 +452,7 @@ Example returns:
|
|||
{code: 0, message:"ok", data: {lastEdited: 1340815946602}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
*/
|
||||
exports.getLastEdited = async function (padID) {
|
||||
exports.getLastEdited = async (padID) => {
|
||||
// get the pad
|
||||
const pad = await getPadSafe(padID, true);
|
||||
const lastEdited = await pad.getLastEdit();
|
||||
|
@ -463,16 +467,16 @@ Example returns:
|
|||
{code: 0, message:"ok", data: null}
|
||||
{code: 1, message:"pad does already exist", data: null}
|
||||
*/
|
||||
exports.createPad = async function (padID, text) {
|
||||
exports.createPad = async (padID, text) => {
|
||||
if (padID) {
|
||||
// ensure there is no $ in the padID
|
||||
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
|
||||
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: 1, message:"padID does not exist", data: null}
|
||||
*/
|
||||
exports.deletePad = async function (padID) {
|
||||
exports.deletePad = async (padID) => {
|
||||
const pad = await getPadSafe(padID, true);
|
||||
await pad.remove();
|
||||
};
|
||||
|
@ -501,10 +505,10 @@ exports.deletePad = async function (padID) {
|
|||
{code:0, message:"ok", 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
|
||||
if (rev === undefined) {
|
||||
throw new customError('rev is not defined', 'apierror');
|
||||
throw new CustomError('rev is not defined', 'apierror');
|
||||
}
|
||||
rev = checkValidRev(rev);
|
||||
|
||||
|
@ -513,7 +517,7 @@ exports.restoreRevision = async function (padID, rev) {
|
|||
|
||||
// check if this is a valid revision
|
||||
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);
|
||||
|
@ -521,7 +525,7 @@ exports.restoreRevision = async function (padID, rev) {
|
|||
const oldText = pad.text();
|
||||
atext.text += '\n';
|
||||
|
||||
function eachAttribRun(attribs, func) {
|
||||
const eachAttribRun = (attribs, func) => {
|
||||
const attribsIter = Changeset.opIterator(attribs);
|
||||
let textIndex = 0;
|
||||
const newTextStart = 0;
|
||||
|
@ -534,7 +538,7 @@ exports.restoreRevision = async function (padID, rev) {
|
|||
}
|
||||
textIndex = nextIndex;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// create a new changeset with a helper builder object
|
||||
const builder = Changeset.builder(oldText.length);
|
||||
|
@ -569,7 +573,7 @@ Example returns:
|
|||
{code: 0, message:"ok", data: {padID: destinationID}}
|
||||
{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);
|
||||
await pad.copy(destinationID, force);
|
||||
};
|
||||
|
@ -583,7 +587,7 @@ Example returns:
|
|||
{code: 0, message:"ok", data: {padID: destinationID}}
|
||||
{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);
|
||||
await pad.copyPadWithoutHistory(destinationID, force);
|
||||
};
|
||||
|
@ -597,7 +601,7 @@ Example returns:
|
|||
{code: 0, message:"ok", data: {padID: destinationID}}
|
||||
{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);
|
||||
await pad.copy(destinationID, force);
|
||||
await pad.remove();
|
||||
|
@ -611,7 +615,7 @@ Example returns:
|
|||
{code: 0, message:"ok", 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
|
||||
await getPadSafe(padID, true);
|
||||
|
||||
|
@ -629,11 +633,11 @@ Example returns:
|
|||
{code: 0, message:"ok", data: {padID: padID}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
*/
|
||||
exports.getPadID = async function (roID) {
|
||||
exports.getPadID = async (roID) => {
|
||||
// get the PadId
|
||||
const padID = await readOnlyManager.getPadId(roID);
|
||||
if (padID === null) {
|
||||
throw new customError('padID does not exist', 'apierror');
|
||||
if (padID == null) {
|
||||
throw new CustomError('padID does not exist', 'apierror');
|
||||
}
|
||||
|
||||
return {padID};
|
||||
|
@ -647,7 +651,7 @@ Example returns:
|
|||
{code: 0, message:"ok", 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
|
||||
checkGroupPad(padID, 'publicStatus');
|
||||
|
||||
|
@ -670,7 +674,7 @@ Example returns:
|
|||
{code: 0, message:"ok", data: {publicStatus: true}}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
*/
|
||||
exports.getPublicStatus = async function (padID) {
|
||||
exports.getPublicStatus = async (padID) => {
|
||||
// ensure this is a group pad
|
||||
checkGroupPad(padID, 'publicStatus');
|
||||
|
||||
|
@ -687,7 +691,7 @@ Example returns:
|
|||
{code: 0, message:"ok", data: {authorIDs : ["a.s8oes9dhwrvt0zif", "a.akf8finncvomlqva"]}
|
||||
{code: 1, message:"padID does not exist", data: null}
|
||||
*/
|
||||
exports.listAuthorsOfPad = async function (padID) {
|
||||
exports.listAuthorsOfPad = async (padID) => {
|
||||
// get the pad
|
||||
const pad = await getPadSafe(padID, true);
|
||||
const authorIDs = pad.getAllAuthors();
|
||||
|
@ -717,7 +721,7 @@ Example returns:
|
|||
{code: 1, message:"padID does not exist"}
|
||||
*/
|
||||
|
||||
exports.sendClientsMessage = async function (padID, msg) {
|
||||
exports.sendClientsMessage = async (padID, msg) => {
|
||||
const pad = await getPadSafe(padID, true);
|
||||
padMessageHandler.handleCustomMessage(padID, msg);
|
||||
};
|
||||
|
@ -730,7 +734,7 @@ Example returns:
|
|||
{"code":0,"message":"ok","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: 1, message:"padID does not exist", data: null}
|
||||
*/
|
||||
exports.getChatHead = async function (padID) {
|
||||
exports.getChatHead = async (padID) => {
|
||||
// get the pad
|
||||
const pad = await getPadSafe(padID, true);
|
||||
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
|
||||
|
||||
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://etherpad.org\">http://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}
|
||||
|
||||
*/
|
||||
exports.createDiffHTML = async function (padID, startRev, endRev) {
|
||||
exports.createDiffHTML = async (padID, startRev, endRev) => {
|
||||
// check if startRev is a number
|
||||
if (startRev !== undefined) {
|
||||
startRev = checkValidRev(startRev);
|
||||
|
@ -768,8 +782,9 @@ exports.createDiffHTML = async function (padID, startRev, endRev) {
|
|||
|
||||
// get the pad
|
||||
const pad = await getPadSafe(padID, true);
|
||||
let padDiff;
|
||||
try {
|
||||
var padDiff = new PadDiff(pad, startRev, endRev);
|
||||
padDiff = new PadDiff(pad, startRev, endRev);
|
||||
} catch (e) {
|
||||
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}
|
||||
*/
|
||||
|
||||
exports.getStats = async function () {
|
||||
exports.getStats = async () => {
|
||||
const sessionInfos = padMessageHandler.sessioninfos;
|
||||
|
||||
const sessionKeys = Object.keys(sessionInfos);
|
||||
|
@ -813,20 +828,18 @@ exports.getStats = async function () {
|
|||
**************************** */
|
||||
|
||||
// checks if a number is an int
|
||||
function is_int(value) {
|
||||
return (parseFloat(value) == parseInt(value, 10)) && !isNaN(value);
|
||||
}
|
||||
const isInt = (value) => (parseFloat(value) === parseInt(value, 10)) && !isNaN(value);
|
||||
|
||||
// gets a pad safe
|
||||
async function getPadSafe(padID, shouldExist, text) {
|
||||
// check if padID is a 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
|
||||
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
|
||||
|
@ -834,12 +847,12 @@ async function getPadSafe(padID, shouldExist, text) {
|
|||
|
||||
if (!exists && shouldExist) {
|
||||
// does not exist, but should
|
||||
throw new customError('padID does not exist', 'apierror');
|
||||
throw new CustomError('padID does not exist', 'apierror');
|
||||
}
|
||||
|
||||
if (exists && !shouldExist) {
|
||||
// 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
|
||||
|
@ -848,33 +861,34 @@ async function getPadSafe(padID, shouldExist, text) {
|
|||
|
||||
// checks if a rev is a legal number
|
||||
// pre-condition is that `rev` is not undefined
|
||||
function checkValidRev(rev) {
|
||||
const checkValidRev = (rev) => {
|
||||
if (typeof rev !== 'number') {
|
||||
rev = parseInt(rev, 10);
|
||||
}
|
||||
|
||||
// check if rev is a number
|
||||
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
|
||||
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
|
||||
if (!is_int(rev)) {
|
||||
throw new customError('rev is a float value', 'apierror');
|
||||
if (!isInt(rev)) {
|
||||
throw new CustomError('rev is a float value', 'apierror');
|
||||
}
|
||||
|
||||
return rev;
|
||||
}
|
||||
};
|
||||
|
||||
// checks if a padID is part of a group
|
||||
function checkGroupPad(padID, field) {
|
||||
const checkGroupPad = (padID, field) => {
|
||||
// ensure this is a group pad
|
||||
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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* The AuthorManager controlls all information about the Pad authors
|
||||
*/
|
||||
|
@ -19,85 +20,83 @@
|
|||
*/
|
||||
|
||||
const db = require('./DB');
|
||||
const customError = require('../utils/customError');
|
||||
const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
|
||||
const CustomError = require('../utils/customError');
|
||||
const randomString = require('../../static/js/pad_utils').randomString;
|
||||
|
||||
exports.getColorPalette = function () {
|
||||
return [
|
||||
'#ffc7c7',
|
||||
'#fff1c7',
|
||||
'#e3ffc7',
|
||||
'#c7ffd5',
|
||||
'#c7ffff',
|
||||
'#c7d5ff',
|
||||
'#e3c7ff',
|
||||
'#ffc7f1',
|
||||
'#ffa8a8',
|
||||
'#ffe699',
|
||||
'#cfff9e',
|
||||
'#99ffb3',
|
||||
'#a3ffff',
|
||||
'#99b3ff',
|
||||
'#cc99ff',
|
||||
'#ff99e5',
|
||||
'#e7b1b1',
|
||||
'#e9dcAf',
|
||||
'#cde9af',
|
||||
'#bfedcc',
|
||||
'#b1e7e7',
|
||||
'#c3cdee',
|
||||
'#d2b8ea',
|
||||
'#eec3e6',
|
||||
'#e9cece',
|
||||
'#e7e0ca',
|
||||
'#d3e5c7',
|
||||
'#bce1c5',
|
||||
'#c1e2e2',
|
||||
'#c1c9e2',
|
||||
'#cfc1e2',
|
||||
'#e0bdd9',
|
||||
'#baded3',
|
||||
'#a0f8eb',
|
||||
'#b1e7e0',
|
||||
'#c3c8e4',
|
||||
'#cec5e2',
|
||||
'#b1d5e7',
|
||||
'#cda8f0',
|
||||
'#f0f0a8',
|
||||
'#f2f2a6',
|
||||
'#f5a8eb',
|
||||
'#c5f9a9',
|
||||
'#ececbb',
|
||||
'#e7c4bc',
|
||||
'#daf0b2',
|
||||
'#b0a0fd',
|
||||
'#bce2e7',
|
||||
'#cce2bb',
|
||||
'#ec9afe',
|
||||
'#edabbd',
|
||||
'#aeaeea',
|
||||
'#c4e7b1',
|
||||
'#d722bb',
|
||||
'#f3a5e7',
|
||||
'#ffa8a8',
|
||||
'#d8c0c5',
|
||||
'#eaaedd',
|
||||
'#adc6eb',
|
||||
'#bedad1',
|
||||
'#dee9af',
|
||||
'#e9afc2',
|
||||
'#f8d2a0',
|
||||
'#b3b3e6',
|
||||
];
|
||||
};
|
||||
exports.getColorPalette = () => [
|
||||
'#ffc7c7',
|
||||
'#fff1c7',
|
||||
'#e3ffc7',
|
||||
'#c7ffd5',
|
||||
'#c7ffff',
|
||||
'#c7d5ff',
|
||||
'#e3c7ff',
|
||||
'#ffc7f1',
|
||||
'#ffa8a8',
|
||||
'#ffe699',
|
||||
'#cfff9e',
|
||||
'#99ffb3',
|
||||
'#a3ffff',
|
||||
'#99b3ff',
|
||||
'#cc99ff',
|
||||
'#ff99e5',
|
||||
'#e7b1b1',
|
||||
'#e9dcAf',
|
||||
'#cde9af',
|
||||
'#bfedcc',
|
||||
'#b1e7e7',
|
||||
'#c3cdee',
|
||||
'#d2b8ea',
|
||||
'#eec3e6',
|
||||
'#e9cece',
|
||||
'#e7e0ca',
|
||||
'#d3e5c7',
|
||||
'#bce1c5',
|
||||
'#c1e2e2',
|
||||
'#c1c9e2',
|
||||
'#cfc1e2',
|
||||
'#e0bdd9',
|
||||
'#baded3',
|
||||
'#a0f8eb',
|
||||
'#b1e7e0',
|
||||
'#c3c8e4',
|
||||
'#cec5e2',
|
||||
'#b1d5e7',
|
||||
'#cda8f0',
|
||||
'#f0f0a8',
|
||||
'#f2f2a6',
|
||||
'#f5a8eb',
|
||||
'#c5f9a9',
|
||||
'#ececbb',
|
||||
'#e7c4bc',
|
||||
'#daf0b2',
|
||||
'#b0a0fd',
|
||||
'#bce2e7',
|
||||
'#cce2bb',
|
||||
'#ec9afe',
|
||||
'#edabbd',
|
||||
'#aeaeea',
|
||||
'#c4e7b1',
|
||||
'#d722bb',
|
||||
'#f3a5e7',
|
||||
'#ffa8a8',
|
||||
'#d8c0c5',
|
||||
'#eaaedd',
|
||||
'#adc6eb',
|
||||
'#bedad1',
|
||||
'#dee9af',
|
||||
'#e9afc2',
|
||||
'#f8d2a0',
|
||||
'#b3b3e6',
|
||||
];
|
||||
|
||||
/**
|
||||
* Checks if the author exists
|
||||
*/
|
||||
exports.doesAuthorExist = async function (authorID) {
|
||||
exports.doesAuthorExist = async (authorID) => {
|
||||
const author = await db.get(`globalAuthor:${authorID}`);
|
||||
|
||||
return author !== null;
|
||||
return author != null;
|
||||
};
|
||||
|
||||
/* exported for backwards compatibility */
|
||||
|
@ -107,7 +106,7 @@ exports.doesAuthorExists = exports.doesAuthorExist;
|
|||
* Returns the AuthorID for a token.
|
||||
* @param {String} token The token
|
||||
*/
|
||||
exports.getAuthor4Token = async function (token) {
|
||||
exports.getAuthor4Token = async (token) => {
|
||||
const author = await mapAuthorWithDBKey('token2author', token);
|
||||
|
||||
// return only the sub value authorID
|
||||
|
@ -119,7 +118,7 @@ exports.getAuthor4Token = async function (token) {
|
|||
* @param {String} token The mapper
|
||||
* @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);
|
||||
|
||||
if (name) {
|
||||
|
@ -140,7 +139,7 @@ async function mapAuthorWithDBKey(mapperkey, mapper) {
|
|||
// try to map to an author
|
||||
const author = await db.get(`${mapperkey}:${mapper}`);
|
||||
|
||||
if (author === null) {
|
||||
if (author == null) {
|
||||
// there is no author with this mapper, so create one
|
||||
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
|
||||
* @param {String} name The name of the author
|
||||
*/
|
||||
exports.createAuthor = function (name) {
|
||||
exports.createAuthor = (name) => {
|
||||
// create the new author name
|
||||
const author = `a.${randomString(16)}`;
|
||||
|
||||
|
@ -185,50 +184,40 @@ exports.createAuthor = function (name) {
|
|||
* Returns the Author Obj of the author
|
||||
* @param {String} author The id of the author
|
||||
*/
|
||||
exports.getAuthor = function (author) {
|
||||
// NB: result is already a Promise
|
||||
return db.get(`globalAuthor:${author}`);
|
||||
};
|
||||
exports.getAuthor = (author) => db.get(`globalAuthor:${author}`);
|
||||
|
||||
/**
|
||||
* Returns the color Id of the author
|
||||
* @param {String} author The id of the author
|
||||
*/
|
||||
exports.getAuthorColorId = function (author) {
|
||||
return db.getSub(`globalAuthor:${author}`, ['colorId']);
|
||||
};
|
||||
exports.getAuthorColorId = (author) => db.getSub(`globalAuthor:${author}`, ['colorId']);
|
||||
|
||||
/**
|
||||
* Sets the color Id of the author
|
||||
* @param {String} author The id of the author
|
||||
* @param {String} colorId The color id of the author
|
||||
*/
|
||||
exports.setAuthorColorId = function (author, colorId) {
|
||||
return db.setSub(`globalAuthor:${author}`, ['colorId'], colorId);
|
||||
};
|
||||
exports.setAuthorColorId = (author, colorId) => db.setSub(
|
||||
`globalAuthor:${author}`, ['colorId'], colorId);
|
||||
|
||||
/**
|
||||
* Returns the name of the author
|
||||
* @param {String} author The id of the author
|
||||
*/
|
||||
exports.getAuthorName = function (author) {
|
||||
return db.getSub(`globalAuthor:${author}`, ['name']);
|
||||
};
|
||||
exports.getAuthorName = (author) => db.getSub(`globalAuthor:${author}`, ['name']);
|
||||
|
||||
/**
|
||||
* Sets the name of the author
|
||||
* @param {String} author The id of the author
|
||||
* @param {String} name The name of the author
|
||||
*/
|
||||
exports.setAuthorName = function (author, name) {
|
||||
return db.setSub(`globalAuthor:${author}`, ['name'], name);
|
||||
};
|
||||
exports.setAuthorName = (author, name) => db.setSub(`globalAuthor:${author}`, ['name'], name);
|
||||
|
||||
/**
|
||||
* Returns an array of all pads this author contributed to
|
||||
* @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:
|
||||
* (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
|
||||
|
@ -237,9 +226,9 @@ exports.listPadsOfAuthor = async function (authorID) {
|
|||
// get the globalAuthor
|
||||
const author = await db.get(`globalAuthor:${authorID}`);
|
||||
|
||||
if (author === null) {
|
||||
if (author == null) {
|
||||
// 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
|
||||
|
@ -253,11 +242,11 @@ exports.listPadsOfAuthor = async function (authorID) {
|
|||
* @param {String} author The id of the author
|
||||
* @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
|
||||
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
|
||||
|
@ -280,12 +269,12 @@ exports.addPad = async function (authorID, padID) {
|
|||
* @param {String} author The id of the author
|
||||
* @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}`);
|
||||
|
||||
if (author === null) return;
|
||||
if (author == null) return;
|
||||
|
||||
if (author.padIDs !== null) {
|
||||
if (author.padIDs != null) {
|
||||
// remove pad from author
|
||||
delete author.padIDs[padID];
|
||||
await db.set(`globalAuthor:${authorID}`, author);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* The Group Manager provides functions to manage groups in the database
|
||||
*/
|
||||
|
@ -18,13 +19,13 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const customError = require('../utils/customError');
|
||||
const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
|
||||
const CustomError = require('../utils/customError');
|
||||
const randomString = require('../../static/js/pad_utils').randomString;
|
||||
const db = require('./DB');
|
||||
const padManager = require('./PadManager');
|
||||
const sessionManager = require('./SessionManager');
|
||||
|
||||
exports.listAllGroups = async function () {
|
||||
exports.listAllGroups = async () => {
|
||||
let groups = await db.get('groups');
|
||||
groups = groups || {};
|
||||
|
||||
|
@ -32,17 +33,20 @@ exports.listAllGroups = async function () {
|
|||
return {groupIDs};
|
||||
};
|
||||
|
||||
exports.deleteGroup = async function (groupID) {
|
||||
exports.deleteGroup = async (groupID) => {
|
||||
const group = await db.get(`group:${groupID}`);
|
||||
|
||||
// ensure group exists
|
||||
if (group == null) {
|
||||
// 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)
|
||||
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
|
||||
const group2sessions = await db.get(`group2sessions:${groupID}`);
|
||||
|
@ -76,14 +80,14 @@ exports.deleteGroup = async function (groupID) {
|
|||
await db.set('groups', newGroups);
|
||||
};
|
||||
|
||||
exports.doesGroupExist = async function (groupID) {
|
||||
exports.doesGroupExist = async (groupID) => {
|
||||
// try to get the group entry
|
||||
const group = await db.get(`group:${groupID}`);
|
||||
|
||||
return (group != null);
|
||||
};
|
||||
|
||||
exports.createGroup = async function () {
|
||||
exports.createGroup = async () => {
|
||||
// search for non existing groupID
|
||||
const groupID = `g.${randomString(16)}`;
|
||||
|
||||
|
@ -103,10 +107,10 @@ exports.createGroup = async function () {
|
|||
return {groupID};
|
||||
};
|
||||
|
||||
exports.createGroupIfNotExistsFor = async function (groupMapper) {
|
||||
exports.createGroupIfNotExistsFor = async (groupMapper) => {
|
||||
// ensure mapper is optional
|
||||
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
|
||||
|
@ -128,7 +132,7 @@ exports.createGroupIfNotExistsFor = async function (groupMapper) {
|
|||
return result;
|
||||
};
|
||||
|
||||
exports.createGroupPad = async function (groupID, padName, text) {
|
||||
exports.createGroupPad = async (groupID, padName, text) => {
|
||||
// create the padID
|
||||
const padID = `${groupID}$${padName}`;
|
||||
|
||||
|
@ -136,7 +140,7 @@ exports.createGroupPad = async function (groupID, padName, text) {
|
|||
const groupExists = await exports.doesGroupExist(groupID);
|
||||
|
||||
if (!groupExists) {
|
||||
throw new customError('groupID does not exist', 'apierror');
|
||||
throw new CustomError('groupID does not exist', 'apierror');
|
||||
}
|
||||
|
||||
// ensure pad doesn't exist already
|
||||
|
@ -144,7 +148,7 @@ exports.createGroupPad = async function (groupID, padName, text) {
|
|||
|
||||
if (padExists) {
|
||||
// pad exists already
|
||||
throw new customError('padName does already exist', 'apierror');
|
||||
throw new CustomError('padName does already exist', 'apierror');
|
||||
}
|
||||
|
||||
// create the pad
|
||||
|
@ -156,12 +160,12 @@ exports.createGroupPad = async function (groupID, padName, text) {
|
|||
return {padID};
|
||||
};
|
||||
|
||||
exports.listPads = async function (groupID) {
|
||||
exports.listPads = async (groupID) => {
|
||||
const exists = await exports.doesGroupExist(groupID);
|
||||
|
||||
// ensure the group 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
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
'use strict';
|
||||
/**
|
||||
* The pad object, defined with joose
|
||||
*/
|
||||
|
||||
|
||||
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
|
||||
const AttributePool = require('ep_etherpad-lite/static/js/AttributePool');
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const AttributePool = require('../../static/js/AttributePool');
|
||||
const db = require('./DB');
|
||||
const settings = require('../utils/Settings');
|
||||
const authorManager = require('./AuthorManager');
|
||||
const padManager = require('./PadManager');
|
||||
const padMessageHandler = require('../handler/PadMessageHandler');
|
||||
const groupManager = require('./GroupManager');
|
||||
const customError = require('../utils/customError');
|
||||
const CustomError = require('../utils/customError');
|
||||
const readOnlyManager = require('./ReadOnlyManager');
|
||||
const crypto = require('crypto');
|
||||
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');
|
||||
|
||||
// serialization/deserialization attributes
|
||||
|
@ -23,13 +23,14 @@ const attributeBlackList = ['id'];
|
|||
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
|
||||
*/
|
||||
exports.cleanText = function (txt) {
|
||||
return txt.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\t/g, ' ').replace(/\xa0/g, ' ');
|
||||
};
|
||||
|
||||
exports.cleanText = (txt) => txt.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n')
|
||||
.replace(/\t/g, ' ')
|
||||
.replace(/\xa0/g, ' ');
|
||||
|
||||
const Pad = function Pad(id) {
|
||||
this.atext = Changeset.makeAText('\n');
|
||||
|
@ -56,7 +57,7 @@ Pad.prototype.getSavedRevisionsNumber = function getSavedRevisionsNumber() {
|
|||
};
|
||||
|
||||
Pad.prototype.getSavedRevisionsList = function getSavedRevisionsList() {
|
||||
const savedRev = new Array();
|
||||
const savedRev = [];
|
||||
for (const rev in this.savedRevisions) {
|
||||
savedRev.push(this.savedRevisions[rev].revNum);
|
||||
}
|
||||
|
@ -85,11 +86,11 @@ Pad.prototype.appendRevision = async function appendRevision(aChangeset, author)
|
|||
newRevData.meta.timestamp = Date.now();
|
||||
|
||||
// ex. getNumForAuthor
|
||||
if (author != '') {
|
||||
if (author !== '') {
|
||||
this.pool.putAttrib(['author', author || '']);
|
||||
}
|
||||
|
||||
if (newRev % 100 == 0) {
|
||||
if (newRev % 100 === 0) {
|
||||
newRevData.meta.pool = this.pool;
|
||||
newRevData.meta.atext = this.atext;
|
||||
}
|
||||
|
@ -104,7 +105,7 @@ Pad.prototype.appendRevision = async function appendRevision(aChangeset, author)
|
|||
p.push(authorManager.addPad(author, this.id));
|
||||
}
|
||||
|
||||
if (this.head == 0) {
|
||||
if (this.head === 0) {
|
||||
hooks.callAll('padCreate', {pad: this, author});
|
||||
} else {
|
||||
hooks.callAll('padUpdate', {pad: this, author, revs: newRev, changeset: aChangeset});
|
||||
|
@ -153,7 +154,7 @@ Pad.prototype.getAllAuthors = function getAllAuthors() {
|
|||
const authors = [];
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
@ -177,9 +178,10 @@ Pad.prototype.getInternalRevisionAText = async function getInternalRevisionAText
|
|||
|
||||
// get all needed changesets
|
||||
const changesets = [];
|
||||
await Promise.all(neededChangesets.map((item) => this.getRevisionChangeset(item).then((changeset) => {
|
||||
changesets[item] = changeset;
|
||||
})));
|
||||
await Promise.all(
|
||||
neededChangesets.map((item) => this.getRevisionChangeset(item).then((changeset) => {
|
||||
changesets[item] = changeset;
|
||||
})));
|
||||
|
||||
// we should have the atext by now
|
||||
let atext = await p_atext;
|
||||
|
@ -204,10 +206,11 @@ Pad.prototype.getAllAuthorColors = async function getAllAuthorColors() {
|
|||
const returnTable = {};
|
||||
const colorPalette = authorManager.getColorPalette();
|
||||
|
||||
await Promise.all(authors.map((author) => authorManager.getAuthorColorId(author).then((colorId) => {
|
||||
// colorId might be a hex color or an number out of the palette
|
||||
returnTable[author] = colorPalette[colorId] || 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
|
||||
returnTable[author] = colorPalette[colorId] || colorId;
|
||||
})));
|
||||
|
||||
return returnTable;
|
||||
};
|
||||
|
@ -227,7 +230,7 @@ Pad.prototype.getValidRevisionRange = function getValidRevisionRange(startRev, e
|
|||
endRev = head;
|
||||
}
|
||||
|
||||
if (startRev !== null && endRev !== null) {
|
||||
if (startRev != null && endRev != null) {
|
||||
return {startRev, endRev};
|
||||
}
|
||||
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
|
||||
// getText() and setText() consistent.
|
||||
let changeset;
|
||||
if (newText[newText.length - 1] == '\n') {
|
||||
if (newText[newText.length - 1] === '\n') {
|
||||
changeset = Changeset.makeSplice(oldText, 0, oldText.length, newText);
|
||||
} else {
|
||||
changeset = Changeset.makeSplice(oldText, 0, oldText.length - 1, newText);
|
||||
|
@ -304,9 +307,10 @@ Pad.prototype.getChatMessages = async function getChatMessages(start, end) {
|
|||
|
||||
// get all entries out of the database
|
||||
const entries = [];
|
||||
await Promise.all(neededEntries.map((entryObject) => this.getChatMessage(entryObject.entryNum).then((entry) => {
|
||||
entries[entryObject.order] = entry;
|
||||
})));
|
||||
await Promise.all(
|
||||
neededEntries.map((entryObject) => this.getChatMessage(entryObject.entryNum).then((entry) => {
|
||||
entries[entryObject.order] = entry;
|
||||
})));
|
||||
|
||||
// sort out broken chat entries
|
||||
// it looks like in happened in the past that the chat head was
|
||||
|
@ -384,14 +388,16 @@ Pad.prototype.copy = async function copy(destinationID, force) {
|
|||
// copy all chat messages
|
||||
const chatHead = this.chatHead;
|
||||
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);
|
||||
}
|
||||
|
||||
// copy all revisions
|
||||
const revHead = this.head;
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -412,7 +418,7 @@ Pad.prototype.copy = async function copy(destinationID, force) {
|
|||
await padManager.getPad(destinationID, null); // this runs too early.
|
||||
|
||||
// let the plugins know the pad was copied
|
||||
hooks.callAll('padCopy', {originalPad: this, destinationID});
|
||||
await hooks.aCallAll('padCopy', {originalPad: this, destinationID});
|
||||
|
||||
return {padID: destinationID};
|
||||
};
|
||||
|
@ -426,7 +432,7 @@ Pad.prototype.checkIfGroupExistAndReturnIt = async function checkIfGroupExistAnd
|
|||
|
||||
// group does not exist
|
||||
if (!groupExists) {
|
||||
throw new customError('groupID does not exist for destinationID', 'apierror');
|
||||
throw new CustomError('groupID does not exist for destinationID', 'apierror');
|
||||
}
|
||||
}
|
||||
return destGroupID;
|
||||
|
@ -446,7 +452,7 @@ Pad.prototype.removePadIfForceIsTrueAndAlreadyExist = async function removePadIf
|
|||
if (exists) {
|
||||
if (!force) {
|
||||
console.error('erroring out without force');
|
||||
throw new customError('destinationID already exists', 'apierror');
|
||||
throw new CustomError('destinationID already exists', 'apierror');
|
||||
}
|
||||
|
||||
// exists and forcing
|
||||
|
@ -514,7 +520,7 @@ Pad.prototype.copyPadWithoutHistory = async function copyPadWithoutHistory(desti
|
|||
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
|
||||
newPad.appendRevision(changeset);
|
||||
|
||||
hooks.callAll('padCopy', {originalPad: this, destinationID});
|
||||
await hooks.aCallAll('padCopy', {originalPad: this, destinationID});
|
||||
|
||||
return {padID: destinationID};
|
||||
};
|
||||
|
@ -568,7 +574,7 @@ Pad.prototype.remove = async function remove() {
|
|||
|
||||
// delete the pad entry and delete pad from padManager
|
||||
p.push(padManager.removePad(padID));
|
||||
hooks.callAll('padRemove', {padID});
|
||||
p.push(hooks.aCallAll('padRemove', {padID}));
|
||||
await Promise.all(p);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* The Pad Manager is a Factory for pad Objects
|
||||
*/
|
||||
|
@ -18,7 +19,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const customError = require('../utils/customError');
|
||||
const CustomError = require('../utils/customError');
|
||||
const Pad = require('../db/Pad').Pad;
|
||||
const db = require('./DB');
|
||||
|
||||
|
@ -109,22 +110,22 @@ const padList = {
|
|||
* @param id A String with the id of the pad
|
||||
* @param {Function} callback
|
||||
*/
|
||||
exports.getPad = async function (id, text) {
|
||||
exports.getPad = async (id, text) => {
|
||||
// check if this is a valid padId
|
||||
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
|
||||
if (text != null) {
|
||||
// check if text is a 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
|
||||
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;
|
||||
};
|
||||
|
||||
exports.listAllPads = async function () {
|
||||
exports.listAllPads = async () => {
|
||||
const padIDs = await padList.getPads();
|
||||
|
||||
return {padIDs};
|
||||
};
|
||||
|
||||
// checks if a pad exists
|
||||
exports.doesPadExist = async function (padId) {
|
||||
exports.doesPadExist = async (padId) => {
|
||||
const value = await db.get(`pad:${padId}`);
|
||||
|
||||
return (value != null && value.atext);
|
||||
|
@ -189,9 +190,7 @@ exports.sanitizePadId = async function sanitizePadId(padId) {
|
|||
return padId;
|
||||
};
|
||||
|
||||
exports.isValidPadId = function (padId) {
|
||||
return /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
|
||||
};
|
||||
exports.isValidPadId = (padId) => /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
|
||||
|
||||
/**
|
||||
* Removes the pad from database and unloads it.
|
||||
|
@ -204,6 +203,6 @@ exports.removePad = async (padId) => {
|
|||
};
|
||||
|
||||
// removes a pad from the cache
|
||||
exports.unloadPad = function (padId) {
|
||||
exports.unloadPad = (padId) => {
|
||||
globalPads.remove(padId);
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* 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
|
||||
* @param {String} the pad's id
|
||||
*/
|
||||
exports.isReadOnlyId = function (id) {
|
||||
return id.indexOf('r.') === 0;
|
||||
};
|
||||
exports.isReadOnlyId = (id) => id.indexOf('r.') === 0;
|
||||
|
||||
/**
|
||||
* returns a read only id for a 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
|
||||
let readOnlyId = await db.get(`pad2readonly:${padId}`);
|
||||
|
||||
|
@ -53,15 +52,13 @@ exports.getReadOnlyId = async function (padId) {
|
|||
* returns the padId for a read only id
|
||||
* @param {String} readOnlyId read only id
|
||||
*/
|
||||
exports.getPadId = function (readOnlyId) {
|
||||
return db.get(`readonly2pad:${readOnlyId}`);
|
||||
};
|
||||
exports.getPadId = (readOnlyId) => db.get(`readonly2pad:${readOnlyId}`);
|
||||
|
||||
/**
|
||||
* returns the padId and readonlyPadId in an object for any id
|
||||
* @param {String} padIdOrReadonlyPadId read only id or real pad id
|
||||
*/
|
||||
exports.getIds = async function (id) {
|
||||
exports.getIds = async (id) => {
|
||||
const readonly = (id.indexOf('r.') === 0);
|
||||
|
||||
// Might be null, if this is an unknown read-only id
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Controls the security of pad access
|
||||
*/
|
||||
|
@ -19,7 +20,7 @@
|
|||
*/
|
||||
|
||||
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 sessionManager = require('./SessionManager');
|
||||
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
|
||||
* 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) {
|
||||
authLogger.debug('access denied: missing padID');
|
||||
return DENY;
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
const customError = require('../utils/customError');
|
||||
const CustomError = require('../utils/customError');
|
||||
const promises = require('../utils/promises');
|
||||
const randomString = require('../utils/randomstring');
|
||||
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
|
||||
* 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
|
||||
* just delimiters. This is perfectly legal: Etherpad parsing logic should
|
||||
|
@ -78,26 +81,26 @@ exports.findAuthorID = async (groupID, sessionCookie) => {
|
|||
return sessionInfo.authorID;
|
||||
};
|
||||
|
||||
exports.doesSessionExist = async function (sessionID) {
|
||||
exports.doesSessionExist = async (sessionID) => {
|
||||
// check if the database entry of this session exists
|
||||
const session = await db.get(`session:${sessionID}`);
|
||||
return (session !== null);
|
||||
return (session != null);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
const groupExists = await groupManager.doesGroupExist(groupID);
|
||||
if (!groupExists) {
|
||||
throw new customError('groupID does not exist', 'apierror');
|
||||
throw new CustomError('groupID does not exist', 'apierror');
|
||||
}
|
||||
|
||||
// check if the author exists
|
||||
const authorExists = await authorManager.doesAuthorExist(authorID);
|
||||
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
|
||||
|
@ -107,22 +110,22 @@ exports.createSession = async function (groupID, authorID, validUntil) {
|
|||
|
||||
// check it's a valid number
|
||||
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
|
||||
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
|
||||
if (!is_int(validUntil)) {
|
||||
throw new customError('validUntil is a float value', 'apierror');
|
||||
if (!isInt(validUntil)) {
|
||||
throw new CustomError('validUntil is a float value', 'apierror');
|
||||
}
|
||||
|
||||
// check if validUntil is in the future
|
||||
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
|
||||
|
@ -170,13 +173,13 @@ exports.createSession = async function (groupID, authorID, validUntil) {
|
|||
return {sessionID};
|
||||
};
|
||||
|
||||
exports.getSessionInfo = async function (sessionID) {
|
||||
exports.getSessionInfo = async (sessionID) => {
|
||||
// check if the database entry of this session exists
|
||||
const session = await db.get(`session:${sessionID}`);
|
||||
|
||||
if (session == null) {
|
||||
// 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
|
||||
|
@ -186,11 +189,11 @@ exports.getSessionInfo = async function (sessionID) {
|
|||
/**
|
||||
* Deletes a session
|
||||
*/
|
||||
exports.deleteSession = async function (sessionID) {
|
||||
exports.deleteSession = async (sessionID) => {
|
||||
// ensure that the session exists
|
||||
const session = await db.get(`session:${sessionID}`);
|
||||
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
|
||||
|
@ -217,22 +220,22 @@ exports.deleteSession = async function (sessionID) {
|
|||
}
|
||||
};
|
||||
|
||||
exports.listSessionsOfGroup = async function (groupID) {
|
||||
exports.listSessionsOfGroup = async (groupID) => {
|
||||
// check that the group exists
|
||||
const exists = await groupManager.doesGroupExist(groupID);
|
||||
if (!exists) {
|
||||
throw new customError('groupID does not exist', 'apierror');
|
||||
throw new CustomError('groupID does not exist', 'apierror');
|
||||
}
|
||||
|
||||
const sessions = await listSessionsWithDBKey(`group2sessions:${groupID}`);
|
||||
return sessions;
|
||||
};
|
||||
|
||||
exports.listSessionsOfAuthor = async function (authorID) {
|
||||
exports.listSessionsOfAuthor = async (authorID) => {
|
||||
// check that the author exists
|
||||
const exists = await authorManager.doesAuthorExist(authorID);
|
||||
if (!exists) {
|
||||
throw new customError('authorID does not exist', 'apierror');
|
||||
throw new CustomError('authorID does not exist', 'apierror');
|
||||
}
|
||||
|
||||
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
|
||||
// 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
|
||||
const sessionObject = await db.get(dbkey);
|
||||
const sessions = sessionObject ? sessionObject.sessionIDs : null;
|
||||
|
@ -252,7 +255,7 @@ async function listSessionsWithDBKey(dbkey) {
|
|||
const sessionInfo = await exports.getSessionInfo(sessionID);
|
||||
sessions[sessionID] = sessionInfo;
|
||||
} catch (err) {
|
||||
if (err == 'apierror: sessionID does not exist') {
|
||||
if (err === 'apierror: sessionID does not exist') {
|
||||
console.warn(`Found bad session ${sessionID} in ${dbkey}`);
|
||||
sessions[sessionID] = null;
|
||||
} else {
|
||||
|
@ -262,9 +265,7 @@ async function listSessionsWithDBKey(dbkey) {
|
|||
}
|
||||
|
||||
return sessions;
|
||||
}
|
||||
};
|
||||
|
||||
// checks if a number is an int
|
||||
function is_int(value) {
|
||||
return (parseFloat(value) == parseInt(value)) && !isNaN(value);
|
||||
}
|
||||
const isInt = (value) => (parseFloat(value) === parseInt(value)) && !isNaN(value);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/*
|
||||
* Stores session data in the database
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const DB = require('ep_etherpad-lite/node/db/DB');
|
||||
const Store = require('ep_etherpad-lite/node_modules/express-session').Store;
|
||||
const log4js = require('ep_etherpad-lite/node_modules/log4js');
|
||||
const DB = require('./DB');
|
||||
const Store = require('express-session').Store;
|
||||
const log4js = require('log4js');
|
||||
|
||||
const logger = log4js.getLogger('SessionStore');
|
||||
|
||||
|
|
|
@ -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.
|
||||
* It has no use for ep-lite, but I thought I keep it cause it may help someone to understand the Changeset library
|
||||
* I found this tests in the old Etherpad and used it to test if the Changeset library can be run on
|
||||
* 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
|
||||
*/
|
||||
|
||||
|
@ -21,52 +23,47 @@
|
|||
*/
|
||||
|
||||
|
||||
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
|
||||
const AttributePool = require('ep_etherpad-lite/static/js/AttributePool');
|
||||
const Changeset = require('../static/js/Changeset');
|
||||
const AttributePool = require('../static/js/AttributePool');
|
||||
|
||||
function random() {
|
||||
this.nextInt = function (maxValue) {
|
||||
return Math.floor(Math.random() * maxValue);
|
||||
};
|
||||
|
||||
this.nextDouble = function (maxValue) {
|
||||
return Math.random();
|
||||
};
|
||||
this.nextInt = (maxValue) => Math.floor(Math.random() * maxValue);
|
||||
this.nextDouble = (maxValue) => Math.random();
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
function print(str) {
|
||||
const runTests = () => {
|
||||
const print = (str) => {
|
||||
console.log(str);
|
||||
}
|
||||
};
|
||||
|
||||
function assert(code, optMsg) {
|
||||
if (!eval(code)) throw new Error(`FALSE: ${optMsg || code}`);
|
||||
}
|
||||
const assert = (code, optMsg) => {
|
||||
if (!eval(code)) throw new Error(`FALSE: ${optMsg || code}`); /* eslint-disable-line no-eval */
|
||||
};
|
||||
|
||||
function literal(v) {
|
||||
const literal = (v) => {
|
||||
if ((typeof v) === 'string') {
|
||||
return `"${v.replace(/[\\\"]/g, '\\$1').replace(/\n/g, '\\n')}"`;
|
||||
} else { return JSON.stringify(v); }
|
||||
}
|
||||
};
|
||||
|
||||
function assertEqualArrays(a, b) {
|
||||
const assertEqualArrays = (a, b) => {
|
||||
assert(`JSON.stringify(${literal(a)}) == JSON.stringify(${literal(b)})`);
|
||||
}
|
||||
};
|
||||
|
||||
function assertEqualStrings(a, b) {
|
||||
const assertEqualStrings = (a, b) => {
|
||||
assert(`${literal(a)} == ${literal(b)}`);
|
||||
}
|
||||
};
|
||||
|
||||
function throughIterator(opsStr) {
|
||||
const throughIterator = (opsStr) => {
|
||||
const iter = Changeset.opIterator(opsStr);
|
||||
const assem = Changeset.opAssembler();
|
||||
while (iter.hasNext()) {
|
||||
assem.append(iter.next());
|
||||
}
|
||||
return assem.toString();
|
||||
}
|
||||
};
|
||||
|
||||
function throughSmartAssembler(opsStr) {
|
||||
const throughSmartAssembler = (opsStr) => {
|
||||
const iter = Changeset.opIterator(opsStr);
|
||||
const assem = Changeset.smartOpAssembler();
|
||||
while (iter.hasNext()) {
|
||||
|
@ -74,50 +71,50 @@ function runTests() {
|
|||
}
|
||||
assem.endDocument();
|
||||
return assem.toString();
|
||||
}
|
||||
};
|
||||
|
||||
(function () {
|
||||
(() => {
|
||||
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';
|
||||
assert(`throughIterator(${literal(x)}) == ${literal(x)}`);
|
||||
})();
|
||||
|
||||
(function () {
|
||||
(() => {
|
||||
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';
|
||||
assert(`throughSmartAssembler(${literal(x)}) == ${literal(x)}`);
|
||||
})();
|
||||
|
||||
function applyMutations(mu, arrayOfArrays) {
|
||||
const applyMutations = (mu, arrayOfArrays) => {
|
||||
arrayOfArrays.forEach((a) => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function mutationsToChangeset(oldLen, arrayOfArrays) {
|
||||
const mutationsToChangeset = (oldLen, arrayOfArrays) => {
|
||||
const assem = Changeset.smartOpAssembler();
|
||||
const op = Changeset.newOp();
|
||||
const bank = Changeset.stringAssembler();
|
||||
let oldPos = 0;
|
||||
let newLen = 0;
|
||||
arrayOfArrays.forEach((a) => {
|
||||
if (a[0] == 'skip') {
|
||||
if (a[0] === 'skip') {
|
||||
op.opcode = '=';
|
||||
op.chars = a[1];
|
||||
op.lines = (a[2] || 0);
|
||||
assem.append(op);
|
||||
oldPos += op.chars;
|
||||
newLen += op.chars;
|
||||
} else if (a[0] == 'remove') {
|
||||
} else if (a[0] === 'remove') {
|
||||
op.opcode = '-';
|
||||
op.chars = a[1];
|
||||
op.lines = (a[2] || 0);
|
||||
assem.append(op);
|
||||
oldPos += op.chars;
|
||||
} else if (a[0] == 'insert') {
|
||||
} else if (a[0] === 'insert') {
|
||||
op.opcode = '+';
|
||||
bank.append(a[1]);
|
||||
op.chars = a[1].length;
|
||||
|
@ -129,9 +126,9 @@ function runTests() {
|
|||
newLen += oldLen - oldPos;
|
||||
assem.endDocument();
|
||||
return Changeset.pack(oldLen, newLen, assem.toString(), bank.toString());
|
||||
}
|
||||
};
|
||||
|
||||
function runMutationTest(testId, origLines, muts, correct) {
|
||||
const runMutationTest = (testId, origLines, muts, correct) => {
|
||||
print(`> runMutationTest#${testId}`);
|
||||
let lines = origLines.slice();
|
||||
const mu = Changeset.textLinesMutator(lines);
|
||||
|
@ -149,7 +146,7 @@ function runTests() {
|
|||
// print(literal(cs));
|
||||
const outText = Changeset.applyToText(cs, inText);
|
||||
assertEqualStrings(correctText, outText);
|
||||
}
|
||||
};
|
||||
|
||||
runMutationTest(1, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [
|
||||
['remove', 1, 0, 'a'],
|
||||
|
@ -220,7 +217,7 @@ function runTests() {
|
|||
['skip', 1, 1, true],
|
||||
], ['banana\n', 'cabbage\n', 'duffle\n']);
|
||||
|
||||
function poolOrArray(attribs) {
|
||||
const poolOrArray = (attribs) => {
|
||||
if (attribs.getAttrib) {
|
||||
return attribs; // it's already an attrib pool
|
||||
} else {
|
||||
|
@ -231,23 +228,25 @@ function runTests() {
|
|||
});
|
||||
return p;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function runApplyToAttributionTest(testId, attribs, cs, inAttr, outCorrect) {
|
||||
const runApplyToAttributionTest = (testId, attribs, cs, inAttr, outCorrect) => {
|
||||
print(`> applyToAttribution#${testId}`);
|
||||
const p = poolOrArray(attribs);
|
||||
const result = Changeset.applyToAttribution(
|
||||
Changeset.checkRep(cs), inAttr, p);
|
||||
assertEqualStrings(outCorrect, result);
|
||||
}
|
||||
};
|
||||
|
||||
// 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"
|
||||
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');
|
||||
const lines = ['1\n', '2\n', '3\n', '4\n'];
|
||||
let mu;
|
||||
|
@ -288,7 +287,7 @@ function runTests() {
|
|||
assert(`${mu.hasMore()} == false`);
|
||||
})();
|
||||
|
||||
function runMutateAttributionTest(testId, attribs, cs, alines, outCorrect) {
|
||||
const runMutateAttributionTest = (testId, attribs, cs, alines, outCorrect) => {
|
||||
print(`> runMutateAttributionTest#${testId}`);
|
||||
const p = poolOrArray(attribs);
|
||||
const alines2 = Array.prototype.slice.call(alines);
|
||||
|
@ -298,30 +297,35 @@ function runTests() {
|
|||
|
||||
print(`> runMutateAttributionTest#${testId}.applyToAttribution`);
|
||||
|
||||
function removeQuestionMarks(a) {
|
||||
return a.replace(/\?/g, '');
|
||||
}
|
||||
const removeQuestionMarks = (a) => a.replace(/\?/g, '');
|
||||
const inMerged = Changeset.joinAttributionLines(alines.map(removeQuestionMarks));
|
||||
const correctMerged = Changeset.joinAttributionLines(outCorrect.map(removeQuestionMarks));
|
||||
const mergedResult = Changeset.applyToAttribution(cs, inMerged, p);
|
||||
assertEqualStrings(correctMerged, mergedResult);
|
||||
}
|
||||
};
|
||||
|
||||
// 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
|
||||
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
|
||||
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
|
||||
// the optimization that skips purely-kept lines is working; if any attribution string
|
||||
// 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();
|
||||
p.putAttrib(['char', 'newline']);
|
||||
for (let i = 1; i < 36; i++) {
|
||||
|
@ -332,39 +336,66 @@ function runTests() {
|
|||
})();
|
||||
|
||||
// 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
|
||||
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
|
||||
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
|
||||
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
|
||||
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();
|
||||
for (let i = 0; i < len; i++) {
|
||||
assem.append(String.fromCharCode(rand.nextInt(26) + 97));
|
||||
}
|
||||
return assem.toString();
|
||||
}
|
||||
};
|
||||
|
||||
function randomMultiline(approxMaxLines, approxMaxCols, rand) {
|
||||
const randomMultiline = (approxMaxLines, approxMaxCols, rand) => {
|
||||
const numParts = rand.nextInt(approxMaxLines * 2) + 1;
|
||||
const txt = Changeset.stringAssembler();
|
||||
txt.append(rand.nextInt(2) ? '\n' : '');
|
||||
for (let i = 0; i < numParts; i++) {
|
||||
if ((i % 2) == 0) {
|
||||
if ((i % 2) === 0) {
|
||||
if (rand.nextInt(10)) {
|
||||
txt.append(randomInlineString(rand.nextInt(approxMaxCols) + 1, rand));
|
||||
} else {
|
||||
|
@ -375,9 +406,9 @@ function runTests() {
|
|||
}
|
||||
}
|
||||
return txt.toString();
|
||||
}
|
||||
};
|
||||
|
||||
function randomStringOperation(numCharsLeft, rand) {
|
||||
const randomStringOperation = (numCharsLeft, rand) => {
|
||||
let result;
|
||||
switch (rand.nextInt(9)) {
|
||||
case 0:
|
||||
|
@ -476,26 +507,26 @@ function runTests() {
|
|||
result.skip = Math.min(result.skip, maxOrig);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
function randomTwoPropAttribs(opcode, rand) {
|
||||
const randomTwoPropAttribs = (opcode, rand) => {
|
||||
// assumes attrib pool like ['apple,','apple,true','banana,','banana,true']
|
||||
if (opcode == '-' || rand.nextInt(3)) {
|
||||
if (opcode === '-' || rand.nextInt(3)) {
|
||||
return '';
|
||||
} else if (rand.nextInt(3)) {
|
||||
if (opcode == '+' || rand.nextInt(2)) {
|
||||
if (opcode === '+' || rand.nextInt(2)) {
|
||||
return `*${Changeset.numToString(rand.nextInt(2) * 2 + 1)}`;
|
||||
} else {
|
||||
return `*${Changeset.numToString(rand.nextInt(2) * 2)}`;
|
||||
}
|
||||
} else if (opcode == '+' || rand.nextInt(4) == 0) {
|
||||
} else if (opcode === '+' || rand.nextInt(4) === 0) {
|
||||
return '*1*3';
|
||||
} else {
|
||||
return ['*0*2', '*0*3', '*1*2'][rand.nextInt(3)];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function randomTestChangeset(origText, rand, withAttribs) {
|
||||
const randomTestChangeset = (origText, rand, withAttribs) => {
|
||||
const charBank = Changeset.stringAssembler();
|
||||
let textLeft = origText; // always keep final newline
|
||||
const outTextAssem = Changeset.stringAssembler();
|
||||
|
@ -504,13 +535,13 @@ function runTests() {
|
|||
|
||||
const nextOp = Changeset.newOp();
|
||||
|
||||
function appendMultilineOp(opcode, txt) {
|
||||
const appendMultilineOp = (opcode, txt) => {
|
||||
nextOp.opcode = opcode;
|
||||
if (withAttribs) {
|
||||
nextOp.attribs = randomTwoPropAttribs(opcode, rand);
|
||||
}
|
||||
txt.replace(/\n|[^\n]+/g, (t) => {
|
||||
if (t == '\n') {
|
||||
if (t === '\n') {
|
||||
nextOp.chars = 1;
|
||||
nextOp.lines = 1;
|
||||
opAssem.append(nextOp);
|
||||
|
@ -521,26 +552,26 @@ function runTests() {
|
|||
}
|
||||
return '';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function doOp() {
|
||||
const doOp = () => {
|
||||
const o = randomStringOperation(textLeft.length, rand);
|
||||
if (o.insert) {
|
||||
var txt = o.insert;
|
||||
const txt = o.insert;
|
||||
charBank.append(txt);
|
||||
outTextAssem.append(txt);
|
||||
appendMultilineOp('+', txt);
|
||||
} else if (o.skip) {
|
||||
var txt = textLeft.substring(0, o.skip);
|
||||
const txt = textLeft.substring(0, o.skip);
|
||||
textLeft = textLeft.substring(o.skip);
|
||||
outTextAssem.append(txt);
|
||||
appendMultilineOp('=', txt);
|
||||
} else if (o.remove) {
|
||||
var txt = textLeft.substring(0, o.remove);
|
||||
const txt = textLeft.substring(0, o.remove);
|
||||
textLeft = textLeft.substring(o.remove);
|
||||
appendMultilineOp('-', txt);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while (textLeft.length > 1) doOp();
|
||||
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());
|
||||
Changeset.checkRep(cs);
|
||||
return [cs, outText];
|
||||
}
|
||||
};
|
||||
|
||||
function testCompose(randomSeed) {
|
||||
const testCompose = (randomSeed) => {
|
||||
const rand = new random();
|
||||
print(`> testCompose#${randomSeed}`);
|
||||
|
||||
|
@ -583,9 +614,9 @@ function runTests() {
|
|||
assertEqualStrings(text2, Changeset.applyToText(change12, startText));
|
||||
assertEqualStrings(text3, Changeset.applyToText(change23, text1));
|
||||
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() {
|
||||
print('> simpleComposeAttributesTest');
|
||||
|
@ -607,12 +638,12 @@ function runTests() {
|
|||
p.putAttrib(['y', 'abc']);
|
||||
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(bfa, Changeset.followAttributes(b, a, p));
|
||||
assertEqualStrings(merge, Changeset.composeAttributes(a, afb, true, p));
|
||||
assertEqualStrings(merge, Changeset.composeAttributes(b, bfa, true, p));
|
||||
}
|
||||
};
|
||||
|
||||
testFollow('', '', '', '', '');
|
||||
testFollow('*0', '', '', '*0', '*0');
|
||||
|
@ -624,7 +655,7 @@ function runTests() {
|
|||
testFollow('*0*4', '*2', '', '*0*4', '*0*4');
|
||||
})();
|
||||
|
||||
function testFollow(randomSeed) {
|
||||
const testFollow = (randomSeed) => {
|
||||
const rand = new random();
|
||||
print(`> testFollow#${randomSeed}`);
|
||||
|
||||
|
@ -642,37 +673,37 @@ function runTests() {
|
|||
const merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa));
|
||||
|
||||
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();
|
||||
print(`> testSplitJoinAttributionLines#${randomSeed}`);
|
||||
|
||||
const doc = `${randomMultiline(10, 20, rand)}\n`;
|
||||
|
||||
function stringToOps(str) {
|
||||
const stringToOps = (str) => {
|
||||
const assem = Changeset.mergingOpAssembler();
|
||||
const o = Changeset.newOp('+');
|
||||
o.chars = 1;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const c = str.charAt(i);
|
||||
o.lines = (c == '\n' ? 1 : 0);
|
||||
o.attribs = (c == 'a' || c == 'b' ? `*${c}` : '');
|
||||
o.lines = (c === '\n' ? 1 : 0);
|
||||
o.attribs = (c === 'a' || c === 'b' ? `*${c}` : '');
|
||||
assem.append(o);
|
||||
}
|
||||
return assem.toString();
|
||||
}
|
||||
};
|
||||
|
||||
const theJoined = stringToOps(doc);
|
||||
const theSplit = doc.match(/[^\n]*\n/g).map(stringToOps);
|
||||
|
||||
assertEqualArrays(theSplit, Changeset.splitAttributionLines(theJoined, doc));
|
||||
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() {
|
||||
print('> testMoveOpsToNewPool');
|
||||
|
@ -685,8 +716,10 @@ function runTests() {
|
|||
|
||||
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(Changeset.moveOpsToNewPool('*1+1*0+1', pool1, pool2), '*0+1*1+1');
|
||||
assertEqualStrings(
|
||||
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));
|
||||
})();
|
||||
|
||||
function testCharacterRangeFollow(testId, cs, oldRange, insertionsAfter, correctNewRange) {
|
||||
const testCharacterRangeFollow = (testId, cs, oldRange, insertionsAfter, correctNewRange) => {
|
||||
print(`> testCharacterRangeFollow#${testId}`);
|
||||
cs = Changeset.checkRep(cs);
|
||||
assertEqualArrays(correctNewRange, Changeset.characterRangeFollow(
|
||||
cs, oldRange[0], oldRange[1], insertionsAfter));
|
||||
};
|
||||
|
||||
var cs = Changeset.checkRep(cs);
|
||||
assertEqualArrays(correctNewRange, Changeset.characterRangeFollow(cs, oldRange[0], oldRange[1], insertionsAfter));
|
||||
}
|
||||
|
||||
testCharacterRangeFollow(1, 'Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk', [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(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]);
|
||||
|
@ -735,23 +769,31 @@ function runTests() {
|
|||
p.putAttrib(['name', 'david']);
|
||||
p.putAttrib(['color', 'green']);
|
||||
|
||||
assertEqualStrings('david', Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'name', p));
|
||||
assertEqualStrings('david', Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'name', p));
|
||||
assertEqualStrings('', Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'name', p));
|
||||
assertEqualStrings('', 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));
|
||||
assertEqualStrings('david',
|
||||
Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'name', p));
|
||||
assertEqualStrings('david',
|
||||
Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'name', p));
|
||||
assertEqualStrings('',
|
||||
Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'name', p));
|
||||
assertEqualStrings('',
|
||||
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}`);
|
||||
|
||||
const assem = Changeset.smartOpAssembler();
|
||||
Changeset.appendATextToAssembler(atext, assem);
|
||||
assertEqualStrings(correctOps, assem.toString());
|
||||
}
|
||||
};
|
||||
|
||||
testAppendATextToAssembler(1, {
|
||||
text: '\n',
|
||||
|
@ -786,13 +828,13 @@ function runTests() {
|
|||
attribs: '|2+2*x|2+5',
|
||||
}, '|2+2*x|1+1*x+3');
|
||||
|
||||
function testMakeAttribsString(testId, pool, opcode, attribs, correctString) {
|
||||
const testMakeAttribsString = (testId, pool, opcode, attribs, correctString) => {
|
||||
print(`> testMakeAttribsString#${testId}`);
|
||||
|
||||
const p = poolOrArray(pool);
|
||||
const str = Changeset.makeAttribsString(opcode, attribs, p);
|
||||
assertEqualStrings(correctString, str);
|
||||
}
|
||||
};
|
||||
|
||||
testMakeAttribsString(1, ['bold,'], '+', [
|
||||
['bold', ''],
|
||||
|
@ -809,12 +851,12 @@ function runTests() {
|
|||
['abc', 'def'],
|
||||
], '*0*1');
|
||||
|
||||
function testSubattribution(testId, astr, start, end, correctOutput) {
|
||||
const testSubattribution = (testId, astr, start, end, correctOutput) => {
|
||||
print(`> testSubattribution#${testId}`);
|
||||
|
||||
const str = Changeset.subattribution(astr, start, end);
|
||||
assertEqualStrings(correctOutput, str);
|
||||
}
|
||||
};
|
||||
|
||||
testSubattribution(1, '+1', 0, 0, '');
|
||||
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(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}`);
|
||||
|
||||
const str = Changeset.filterAttribNumbers(cs, filter);
|
||||
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(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');
|
||||
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(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}`);
|
||||
|
||||
pool = poolOrArray(pool);
|
||||
const str = Changeset.inverse(Changeset.checkRep(cs), lines, alines, pool);
|
||||
assertEqualStrings(correctOutput, str);
|
||||
}
|
||||
};
|
||||
|
||||
// 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}`);
|
||||
|
||||
const a = lines.slice();
|
||||
Changeset.mutateTextLines(cs, a);
|
||||
assertEqualArrays(correctLines, a);
|
||||
}
|
||||
};
|
||||
|
||||
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']);
|
||||
|
||||
function testInverseRandom(randomSeed) {
|
||||
const testInverseRandom = (randomSeed) => {
|
||||
const rand = new random();
|
||||
print(`> testInverseRandom#${randomSeed}`);
|
||||
|
||||
|
@ -928,9 +973,9 @@ function runTests() {
|
|||
// print(lines.map(function(s) { return '3: '+s.slice(0,-1); }).join('\n'));
|
||||
assertEqualArrays(origLines, lines);
|
||||
assertEqualArrays(origALines, alines);
|
||||
}
|
||||
};
|
||||
|
||||
for (var i = 0; i < 30; i++) testInverseRandom(i);
|
||||
}
|
||||
for (let i = 0; i < 30; i++) testInverseRandom(i);
|
||||
};
|
||||
|
||||
runTests();
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* The API Handler handles all API http requests
|
||||
*/
|
||||
|
@ -37,7 +38,8 @@ try {
|
|||
apikey = fs.readFileSync(apikeyFilename, 'utf8');
|
||||
apiHandlerLogger.info(`Api key file read from: "${apikeyFilename}"`);
|
||||
} 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);
|
||||
fs.writeFileSync(apikeyFilename, apikey, 'utf8');
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Handles the export requests
|
||||
*/
|
||||
|
@ -25,7 +26,7 @@ const exportEtherpad = require('../utils/ExportEtherpad');
|
|||
const fs = require('fs');
|
||||
const settings = require('../utils/Settings');
|
||||
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 util = require('util');
|
||||
|
||||
|
@ -49,7 +50,7 @@ const tempDirectory = os.tmpdir();
|
|||
/**
|
||||
* 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
|
||||
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});
|
||||
if (result.length > 0) {
|
||||
// console.log("export handled by plugin", destFile);
|
||||
handledByPlugin = true;
|
||||
} else {
|
||||
// @TODO no Promise interface for convertors (yet)
|
||||
await new Promise((resolve, reject) => {
|
||||
|
@ -115,7 +115,6 @@ async function doExport(req, res, padId, readOnlyId, type) {
|
|||
}
|
||||
|
||||
// send the file
|
||||
const sendFile = util.promisify(res.sendFile);
|
||||
await res.sendFile(destFile, null);
|
||||
|
||||
// clean up temporary files
|
||||
|
@ -128,9 +127,9 @@ async function doExport(req, res, padId, readOnlyId, type) {
|
|||
|
||||
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) => {
|
||||
if (err !== 'stop') {
|
||||
throw err;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Handles the import requests
|
||||
*/
|
||||
|
@ -30,7 +31,7 @@ const os = require('os');
|
|||
const importHtml = require('../utils/ImportHtml');
|
||||
const importEtherpad = require('../utils/ImportEtherpad');
|
||||
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 fsp_exists = util.promisify(fs.exists);
|
||||
|
@ -42,7 +43,7 @@ let convertor = null;
|
|||
let exportExtension = 'htm';
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
|
@ -57,7 +58,7 @@ const tmpDirectory = os.tmpdir();
|
|||
/**
|
||||
* do a requested import
|
||||
*/
|
||||
async function doImport(req, res, padId) {
|
||||
const doImport = async (req, res, padId) => {
|
||||
const apiLogger = log4js.getLogger('ImportHandler');
|
||||
|
||||
// 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
|
||||
// this allows us to accept source code files like .c or .java
|
||||
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);
|
||||
|
||||
if (fileEndingUnknown) {
|
||||
|
@ -146,7 +148,7 @@ async function doImport(req, res, padId) {
|
|||
const headCount = _pad.head;
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
|
@ -251,9 +253,9 @@ async function doImport(req, res, padId) {
|
|||
if (await fsp_exists(destFile)) {
|
||||
fsp_unlink(destFile);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.doImport = function (req, res, padId) {
|
||||
exports.doImport = (req, res, padId) => {
|
||||
/**
|
||||
* NB: abuse the 'req' object by storing an additional
|
||||
* 'directDatabaseAccess' property on it so that it can
|
||||
|
@ -266,7 +268,10 @@ exports.doImport = function (req, res, padId) {
|
|||
let status = 'ok';
|
||||
doImport(req, res, padId).catch((err) => {
|
||||
// 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;
|
||||
} else {
|
||||
throw err;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* The MessageHandler handles all Messages that comes from Socket.IO and controls the sessions
|
||||
*/
|
||||
|
@ -18,22 +19,20 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* global exports, process, require */
|
||||
|
||||
const padManager = require('../db/PadManager');
|
||||
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
|
||||
const AttributePool = require('ep_etherpad-lite/static/js/AttributePool');
|
||||
const AttributeManager = require('ep_etherpad-lite/static/js/AttributeManager');
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const AttributePool = require('../../static/js/AttributePool');
|
||||
const AttributeManager = require('../../static/js/AttributeManager');
|
||||
const authorManager = require('../db/AuthorManager');
|
||||
const readOnlyManager = require('../db/ReadOnlyManager');
|
||||
const settings = require('../utils/Settings');
|
||||
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 messageLogger = log4js.getLogger('message');
|
||||
const accessLogger = log4js.getLogger('access');
|
||||
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 stats = require('../stats');
|
||||
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()
|
||||
*/
|
||||
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
|
||||
|
@ -76,7 +77,7 @@ let socketio;
|
|||
* This Method is called by server.js to tell the message handler on which socket it should send
|
||||
* @param socket_io The Socket
|
||||
*/
|
||||
exports.setSocketIO = function (socket_io) {
|
||||
exports.setSocketIO = (socket_io) => {
|
||||
socketio = socket_io;
|
||||
};
|
||||
|
||||
|
@ -94,7 +95,7 @@ exports.handleConnect = (socket) => {
|
|||
/**
|
||||
* Kicks all sessions from a pad
|
||||
*/
|
||||
exports.kickSessionsFromPad = function (padID) {
|
||||
exports.kickSessionsFromPad = (padID) => {
|
||||
if (typeof socketio.sockets.clients !== 'function') return;
|
||||
|
||||
// skip if there is nobody on this pad
|
||||
|
@ -114,7 +115,8 @@ exports.handleDisconnect = async (socket) => {
|
|||
// save the padname of this session
|
||||
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) {
|
||||
const {session: {user} = {}} = socket.client.request;
|
||||
accessLogger.info(`${'[LEAVE]' +
|
||||
|
@ -192,7 +194,8 @@ exports.handleMessage = async (socket, message) => {
|
|||
|
||||
const auth = thisSession.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;
|
||||
}
|
||||
|
||||
|
@ -234,7 +237,7 @@ exports.handleMessage = async (socket, message) => {
|
|||
}
|
||||
|
||||
// Call handleMessage hook. If a plugin returns null, the message will be dropped.
|
||||
if ((await hooks.aCallAll('handleMessage', context)).some((m) => m === null)) {
|
||||
if ((await hooks.aCallAll('handleMessage', context)).some((m) => m == null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -283,11 +286,11 @@ exports.handleMessage = async (socket, message) => {
|
|||
* @param socket the socket.io Socket object for 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 pad = await padManager.getPad(padId);
|
||||
await pad.addSavedRevision(pad.head, authorId);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 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 (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 msgString {String} the message we're sending
|
||||
*/
|
||||
exports.handleCustomMessage = function (padID, msgString) {
|
||||
exports.handleCustomMessage = (padID, msgString) => {
|
||||
const time = Date.now();
|
||||
const msg = {
|
||||
type: 'COLLABROOM',
|
||||
|
@ -331,12 +334,12 @@ exports.handleCustomMessage = function (padID, msgString) {
|
|||
* @param socket the socket.io Socket object for the client
|
||||
* @param message the message from the client
|
||||
*/
|
||||
async function handleChatMessage(socket, message) {
|
||||
const handleChatMessage = async (socket, message) => {
|
||||
const time = Date.now();
|
||||
const text = message.data.text;
|
||||
const {padId, author: authorId} = sessioninfos[socket.id];
|
||||
await exports.sendChatMessageToPadClients(time, authorId, text, padId);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 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
|
||||
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 message the message from the client
|
||||
*/
|
||||
async function handleGetChatMessages(socket, message) {
|
||||
const handleGetChatMessages = async (socket, message) => {
|
||||
if (message.data.start == null) {
|
||||
messageLogger.warn('Dropped message, GetChatMessages Message has no start!');
|
||||
return;
|
||||
|
@ -387,7 +390,8 @@ async function handleGetChatMessages(socket, message) {
|
|||
const count = end - start;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -405,14 +409,14 @@ async function handleGetChatMessages(socket, message) {
|
|||
|
||||
// send the messages back to the client
|
||||
socket.json.send(infoMsg);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 message the message from the client
|
||||
*/
|
||||
function handleSuggestUserName(socket, message) {
|
||||
const handleSuggestUserName = (socket, message) => {
|
||||
// check if all ok
|
||||
if (message.data.payload.newName == null) {
|
||||
messageLogger.warn('Dropped message, suggestUserName Message has no newName!');
|
||||
|
@ -433,14 +437,15 @@ function handleSuggestUserName(socket, 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 message the message from the client
|
||||
*/
|
||||
async function handleUserInfoUpdate(socket, message) {
|
||||
const handleUserInfoUpdate = async (socket, message) => {
|
||||
// check if all ok
|
||||
if (message.data.userInfo == null) {
|
||||
messageLogger.warn('Dropped message, USERINFO_UPDATE Message has no userInfo!');
|
||||
|
@ -463,7 +468,8 @@ async function handleUserInfoUpdate(socket, message) {
|
|||
const author = session.author;
|
||||
|
||||
// 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) {
|
||||
messageLogger.warn(`Dropped message, USERINFO_UPDATE Color is malformed.${message.data}`);
|
||||
return;
|
||||
|
@ -496,7 +502,7 @@ async function handleUserInfoUpdate(socket, message) {
|
|||
|
||||
// Block until the authorManager has stored the new attributes.
|
||||
await p;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 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
|
||||
stats.counter('pendingEdits').dec();
|
||||
|
||||
|
@ -578,7 +584,8 @@ async function handleUserChanges(socket, message) {
|
|||
|
||||
// + can add text with attribs
|
||||
// = can change or add attribs
|
||||
// - can have attribs, but they are discarded and don't show up in the attribs - 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) => {
|
||||
if (!attr) return;
|
||||
|
@ -586,9 +593,11 @@ async function handleUserChanges(socket, message) {
|
|||
attr = wireApool.getAttrib(attr);
|
||||
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] !== '')) {
|
||||
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) {
|
||||
socket.json.send({disconnect: 'badChangeset'});
|
||||
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);
|
||||
|
@ -672,9 +681,9 @@ async function handleUserChanges(socket, message) {
|
|||
}
|
||||
|
||||
stopWatch.end();
|
||||
}
|
||||
};
|
||||
|
||||
exports.updatePadClients = async function (pad) {
|
||||
exports.updatePadClients = async (pad) => {
|
||||
// skip this if no-one is on this pad
|
||||
const roomSockets = _getRoomSockets(pad.id);
|
||||
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
|
||||
// to remove unnecessary roundtrip to the datalayer
|
||||
// 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
|
||||
// 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,
|
||||
// TODO: in REAL world, if we're working without datalayer cache,
|
||||
// all requests to revisions will be fired
|
||||
// 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
|
||||
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...
|
||||
*/
|
||||
function _correctMarkersInPad(atext, apool) {
|
||||
const _correctMarkersInPad = (atext, apool) => {
|
||||
const text = atext.text;
|
||||
|
||||
// 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);
|
||||
let offset = 0;
|
||||
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) {
|
||||
for (let i = 0; i < op.chars; i++) {
|
||||
|
@ -778,9 +792,9 @@ function _correctMarkersInPad(atext, apool) {
|
|||
});
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
};
|
||||
|
||||
async function handleSwitchToPad(socket, message, _authorID) {
|
||||
const handleSwitchToPad = async (socket, message, _authorID) => {
|
||||
const currentSessionInfo = sessioninfos[socket.id];
|
||||
const padId = currentSessionInfo.padId;
|
||||
|
||||
|
@ -816,10 +830,10 @@ async function handleSwitchToPad(socket, message, _authorID) {
|
|||
const newSessionInfo = sessioninfos[socket.id];
|
||||
createSessionInfoAuth(newSessionInfo, message);
|
||||
await handleClientReady(socket, message, authorID);
|
||||
}
|
||||
};
|
||||
|
||||
// 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
|
||||
// have the cookie in further socket.io messages.
|
||||
// This information will be used to check if
|
||||
|
@ -830,15 +844,16 @@ function createSessionInfoAuth(sessionInfo, message) {
|
|||
padID: message.padId,
|
||||
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
|
||||
* @param socket the socket.io Socket object for 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
|
||||
if (!message.token) {
|
||||
messageLogger.warn('Dropped message, CLIENT_READY Message has no token!');
|
||||
|
@ -884,9 +899,11 @@ async function handleClientReady(socket, message, authorID) {
|
|||
const historicalAuthorData = {};
|
||||
await Promise.all(authors.map((authorId) => authorManager.getAuthor(authorId).then((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 {
|
||||
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
|
||||
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
|
||||
const revisionsNeeded = [];
|
||||
const changesets = {};
|
||||
|
@ -987,12 +1005,13 @@ async function handleClientReady(socket, message, authorID) {
|
|||
}
|
||||
} else {
|
||||
// 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
|
||||
try {
|
||||
var atext = Changeset.cloneAText(pad.atext);
|
||||
atext = Changeset.cloneAText(pad.atext);
|
||||
const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
|
||||
var apool = attribsForWire.pool.toJsonable();
|
||||
apool = attribsForWire.pool.toJsonable();
|
||||
atext.attribs = attribsForWire.translated;
|
||||
} catch (e) {
|
||||
console.error(e.stack || e);
|
||||
|
@ -1147,12 +1166,12 @@ async function handleClientReady(socket, message, authorID) {
|
|||
socket.json.send(msg);
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
if (message.data == null) {
|
||||
messageLogger.warn('Dropped message, changeset request has no data!');
|
||||
|
@ -1197,15 +1216,16 @@ async function handleChangesetRequest(socket, message) {
|
|||
data.requestID = message.data.requestID;
|
||||
socket.json.send({type: 'CHANGESET_REQ', data});
|
||||
} 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
|
||||
* 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 head_revision = pad.getHeadRevisionNumber();
|
||||
|
||||
|
@ -1237,15 +1257,25 @@ async function getChangesetInfo(padId, startNum, endNum, granularity) {
|
|||
|
||||
// get all needed composite Changesets
|
||||
const composedChangesets = {};
|
||||
const p1 = Promise.all(compositesChangesetNeeded.map((item) => composePadChangesets(padId, item.start, item.end).then((changeset) => {
|
||||
composedChangesets[`${item.start}/${item.end}`] = changeset;
|
||||
})));
|
||||
const p1 = Promise.all(
|
||||
compositesChangesetNeeded.map(
|
||||
(item) => composePadChangesets(
|
||||
padId, item.start, item.end
|
||||
).then(
|
||||
(changeset) => {
|
||||
composedChangesets[`${item.start}/${item.end}`] = changeset;
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// get all needed revision Dates
|
||||
const revisionDate = [];
|
||||
const p2 = Promise.all(revTimesNeeded.map((revNum) => pad.getRevisionDate(revNum).then((revDate) => {
|
||||
revisionDate[revNum] = Math.floor(revDate / 1000);
|
||||
})));
|
||||
const p2 = Promise.all(revTimesNeeded.map((revNum) => pad.getRevisionDate(revNum)
|
||||
.then((revDate) => {
|
||||
revisionDate[revNum] = Math.floor(revDate / 1000);
|
||||
})
|
||||
));
|
||||
|
||||
// get the lines
|
||||
let lines;
|
||||
|
@ -1288,13 +1318,13 @@ async function getChangesetInfo(padId, startNum, endNum, granularity) {
|
|||
return {forwardsChangesets, backwardsChangesets,
|
||||
apool: apool.toJsonable(), actualEndNum: endNum,
|
||||
timeDeltas, start: startNum, granularity};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
async function getPadLines(padId, revNum) {
|
||||
const getPadLines = async (padId, revNum) => {
|
||||
const pad = await padManager.getPad(padId);
|
||||
|
||||
// get the atext
|
||||
|
@ -1310,13 +1340,13 @@ async function getPadLines(padId, revNum) {
|
|||
textlines: Changeset.splitTextLines(atext.text),
|
||||
alines: Changeset.splitAttributionLines(atext.attribs, atext.text),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
async function composePadChangesets(padId, startNum, endNum) {
|
||||
const composePadChangesets = async (padId, startNum, endNum) => {
|
||||
const pad = await padManager.getPad(padId);
|
||||
|
||||
// fetch all changesets we need
|
||||
|
@ -1333,7 +1363,9 @@ async function composePadChangesets(padId, startNum, endNum) {
|
|||
|
||||
// get all 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
|
||||
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);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function _getRoomSockets(padID) {
|
||||
const _getRoomSockets = (padID) => {
|
||||
const roomSockets = [];
|
||||
const room = socketio.sockets.adapter.rooms[padID];
|
||||
|
||||
|
@ -1364,21 +1396,19 @@ function _getRoomSockets(padID) {
|
|||
}
|
||||
|
||||
return roomSockets;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the number of users in a pad
|
||||
*/
|
||||
exports.padUsersCount = function (padID) {
|
||||
return {
|
||||
padUsersCount: _getRoomSockets(padID).length,
|
||||
};
|
||||
};
|
||||
exports.padUsersCount = (padID) => ({
|
||||
padUsersCount: _getRoomSockets(padID).length,
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the list of users in a pad
|
||||
*/
|
||||
exports.padUsers = async function (padID) {
|
||||
exports.padUsers = async (padID) => {
|
||||
const padUsers = [];
|
||||
|
||||
// iterate over all clients (in parallel)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* 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
|
||||
|
@ -21,9 +22,6 @@
|
|||
|
||||
const log4js = require('log4js');
|
||||
const messageLogger = log4js.getLogger('message');
|
||||
const securityManager = require('../db/SecurityManager');
|
||||
const readOnlyManager = require('../db/ReadOnlyManager');
|
||||
const settings = require('../utils/Settings');
|
||||
|
||||
/**
|
||||
* Saves all components
|
||||
|
@ -37,7 +35,7 @@ let socket;
|
|||
/**
|
||||
* adds a component
|
||||
*/
|
||||
exports.addComponent = function (moduleName, module) {
|
||||
exports.addComponent = (moduleName, module) => {
|
||||
// save the component
|
||||
components[moduleName] = module;
|
||||
|
||||
|
@ -48,14 +46,14 @@ exports.addComponent = function (moduleName, module) {
|
|||
/**
|
||||
* sets the socket.io and adds event functions for routing
|
||||
*/
|
||||
exports.setSocketIO = function (_socket) {
|
||||
exports.setSocketIO = (_socket) => {
|
||||
// save this socket internaly
|
||||
socket = _socket;
|
||||
|
||||
socket.sockets.on('connection', (client) => {
|
||||
// wrap the original send function to log the messages
|
||||
client._send = client.send;
|
||||
client.send = function (message) {
|
||||
client.send = (message) => {
|
||||
messageLogger.debug(`to ${client.id}: ${JSON.stringify(message)}`);
|
||||
client._send(message);
|
||||
};
|
||||
|
@ -66,7 +64,7 @@ exports.setSocketIO = function (_socket) {
|
|||
}
|
||||
|
||||
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)}`);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
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}));
|
||||
});
|
||||
return cb();
|
||||
|
|
|
@ -4,7 +4,6 @@ const eejs = require('../../eejs');
|
|||
const settings = require('../../utils/Settings');
|
||||
const installer = require('../../../static/js/pluginfw/installer');
|
||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const _ = require('underscore');
|
||||
const semver = require('semver');
|
||||
const UpdateCheck = require('../../utils/UpdateCheck');
|
||||
|
||||
|
@ -51,7 +50,7 @@ exports.socketio = (hookName, args, cb) => {
|
|||
try {
|
||||
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;
|
||||
|
||||
const latestVersion = results[plugin].version;
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
const log4js = require('log4js');
|
||||
const clientLogger = log4js.getLogger('client');
|
||||
const formidable = require('formidable');
|
||||
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
|
||||
args.app.post('/ep/pad/connection-diagnostic-info', (req, res) => {
|
||||
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
|
||||
args.app.post('/jserror', (req, res) => {
|
||||
new formidable.IncomingForm().parse(req, (err, fields, files) => {
|
||||
let data;
|
||||
try {
|
||||
var data = JSON.parse(fields.errorInfo);
|
||||
data = JSON.parse(fields.errorInfo);
|
||||
} catch (e) {
|
||||
return res.end();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
// Handle errors
|
||||
|
|
|
@ -1,39 +1,43 @@
|
|||
const assert = require('assert').strict;
|
||||
'use strict';
|
||||
|
||||
const hasPadAccess = require('../../padaccess');
|
||||
const settings = require('../../utils/Settings');
|
||||
const exportHandler = require('../../handler/ExportHandler');
|
||||
const importHandler = require('../../handler/ImportHandler');
|
||||
const padManager = require('../../db/PadManager');
|
||||
const readOnlyManager = require('../../db/ReadOnlyManager');
|
||||
const authorManager = require('../../db/AuthorManager');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const securityManager = require('../../db/SecurityManager');
|
||||
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
|
||||
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);
|
||||
|
||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
||||
exports.expressCreateServer = (hookName, args, cb) => {
|
||||
// handle export requests
|
||||
args.app.use('/p/:pad/:rev?/export/:type', limiter);
|
||||
args.app.get('/p/:pad/:rev?/export/:type', async (req, res, next) => {
|
||||
const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad'];
|
||||
// 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();
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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
|
||||
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');
|
||||
// ACHTUNG: do not include req.params.type in res.send() because there is
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const RESERVED_WORDS = [
|
||||
'abstract',
|
||||
'arguments',
|
||||
|
@ -65,9 +67,9 @@ const RESERVED_WORDS = [
|
|||
'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;
|
||||
inputStr.split('.').forEach((part) => {
|
||||
if (!regex.test(part)) {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* node/hooks/express/openapi.js
|
||||
*
|
||||
|
@ -31,7 +33,9 @@ const OPENAPI_VERSION = '3.0.2'; // Swagger/OAS version
|
|||
const info = {
|
||||
title: 'Etherpad API',
|
||||
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/',
|
||||
contact: {
|
||||
name: 'The Etherpad Foundation',
|
||||
|
@ -80,7 +84,9 @@ const resources = {
|
|||
listSessions: {
|
||||
operationId: 'listSessionsOfGroup',
|
||||
summary: '',
|
||||
responseSchema: {sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}}},
|
||||
responseSchema: {
|
||||
sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}},
|
||||
},
|
||||
},
|
||||
list: {
|
||||
operationId: 'listAllGroups',
|
||||
|
@ -109,7 +115,9 @@ const resources = {
|
|||
listSessions: {
|
||||
operationId: 'listSessionsOfAuthor',
|
||||
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 :(
|
||||
getName: {
|
||||
|
@ -153,7 +161,8 @@ const resources = {
|
|||
create: {
|
||||
operationId: 'createPad',
|
||||
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: {
|
||||
operationId: 'getText',
|
||||
|
@ -382,9 +391,9 @@ const defaultResponseRefs = {
|
|||
|
||||
// convert to a dictionary of operation objects
|
||||
const operations = {};
|
||||
for (const resource in resources) {
|
||||
for (const action in resources[resource]) {
|
||||
const {operationId, responseSchema, ...operation} = resources[resource][action];
|
||||
for (const [resource, actions] of Object.entries(resources)) {
|
||||
for (const [action, spec] of Object.entries(actions)) {
|
||||
const {operationId, responseSchema, ...operation} = spec;
|
||||
|
||||
// add response objects
|
||||
const responses = {...defaultResponseRefs};
|
||||
|
@ -607,14 +616,14 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
if (createHTTPError.isHttpError(err)) {
|
||||
// pass http errors thrown by handler forward
|
||||
throw err;
|
||||
} else if (err.name == 'apierror') {
|
||||
} else if (err.name === 'apierror') {
|
||||
// parameters were wrong and the api stopped execution, pass the error
|
||||
// convert to http error
|
||||
throw new createHTTPError.BadRequest(err.message);
|
||||
} else {
|
||||
// an unknown error happened
|
||||
// log it and throw internal error
|
||||
apiLogger.error(err);
|
||||
apiLogger.error(err.stack || err.toString());
|
||||
throw new createHTTPError.InternalError('internal error');
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
const readOnlyManager = require('../../db/ReadOnlyManager');
|
||||
const hasPadAccess = require('../../padaccess');
|
||||
const exporthtml = require('../../utils/ExportHtml');
|
||||
|
||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
||||
exports.expressCreateServer = (hookName, args, cb) => {
|
||||
// serve read only pad
|
||||
args.app.get('/ro/:id', async (req, res) => {
|
||||
// translate the read only pad to a padId
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
const padManager = require('../../db/PadManager');
|
||||
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
|
||||
args.app.param('pad', async (req, res, next, padId) => {
|
||||
// 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();
|
||||
} else {
|
||||
// the pad id was sanitized, so we redirect to the sanitized version
|
||||
let real_url = sanitizedPadId;
|
||||
real_url = encodeURIComponent(real_url);
|
||||
let realURL = sanitizedPadId;
|
||||
realURL = encodeURIComponent(realURL);
|
||||
const query = url.parse(req.url).query;
|
||||
if (query) real_url += `?${query}`;
|
||||
res.header('Location', real_url);
|
||||
res.status(302).send(`You should be redirected to <a href="${real_url}">${real_url}</a>`);
|
||||
if (query) realURL += `?${query}`;
|
||||
res.header('Location', realURL);
|
||||
res.status(302).send(`You should be redirected to <a href="${realURL}">${realURL}</a>`);
|
||||
}
|
||||
});
|
||||
return cb();
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const eejs = require('ep_etherpad-lite/node/eejs');
|
||||
const toolbar = require('ep_etherpad-lite/node/utils/toolbar');
|
||||
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
|
||||
const eejs = require('../../eejs');
|
||||
const toolbar = require('../../utils/toolbar');
|
||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
||||
const settings = require('../../utils/Settings');
|
||||
const webaccess = require('./webaccess');
|
||||
|
||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
||||
exports.expressCreateServer = (hookName, args, cb) => {
|
||||
// expose current stats
|
||||
args.app.get('/stats', (req, res) => {
|
||||
res.json(require('ep_etherpad-lite/node/stats').toJSON());
|
||||
res.json(require('../../stats').toJSON());
|
||||
});
|
||||
|
||||
// serve index.html under /
|
||||
|
@ -24,7 +26,14 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
|||
|
||||
// serve robots.txt
|
||||
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) => {
|
||||
// there is no custom robots.txt, send the default robots.txt which dissallows all
|
||||
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
|
||||
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) => {
|
||||
// there is no custom favicon, send the default favicon
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
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 settings = require('../../utils/Settings');
|
||||
const Yajsml = require('etherpad-yajsml');
|
||||
const _ = require('underscore');
|
||||
|
||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
||||
exports.expressCreateServer = (hookName, args, cb) => {
|
||||
// Cache both minified and static.
|
||||
const assetCache = new CachingMiddleware();
|
||||
args.app.all(/\/javascripts\/(.*)/, assetCache.handle);
|
||||
|
@ -34,7 +35,8 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
|||
args.app.use(jsServer.handle.bind(jsServer));
|
||||
|
||||
// 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) => {
|
||||
const clientParts = _(plugins.parts)
|
||||
.filter((part) => _(part).has('client_hooks'));
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const npm = require('npm');
|
||||
const fs = require('fs');
|
||||
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) => {
|
||||
const [coreTests, pluginTests] = await Promise.all([
|
||||
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
|
||||
const rootTestFolder = path.normalize(path.join(npm.root, '../tests/frontend/'));
|
||||
|
||||
const url2FilePath = function (url) {
|
||||
const url2FilePath = (url) => {
|
||||
let subPath = url.substr('/tests/frontend'.length);
|
||||
if (subPath == '') {
|
||||
if (subPath === '') {
|
||||
subPath = 'index.html';
|
||||
}
|
||||
subPath = subPath.split('?')[0];
|
||||
|
@ -49,8 +51,9 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
|||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
@ -69,7 +72,7 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
|||
|
||||
const readdir = util.promisify(fs.readdir);
|
||||
|
||||
exports.getPluginTests = async function (callback) {
|
||||
exports.getPluginTests = async (callback) => {
|
||||
const moduleDir = 'node_modules/';
|
||||
const specPath = '/static/tests/frontend/specs/';
|
||||
const staticDir = '/static/plugins/';
|
||||
|
@ -88,7 +91,4 @@ exports.getPluginTests = async function (callback) {
|
|||
return Promise.all(promises).then(() => pluginSpecs);
|
||||
};
|
||||
|
||||
exports.getCoreTests = function () {
|
||||
// get the core test specs
|
||||
return readdir('tests/frontend/specs');
|
||||
};
|
||||
exports.getCoreTests = () => readdir('tests/frontend/specs');
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
'use strict';
|
||||
|
||||
const languages = require('languages4translatewiki');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const _ = require('underscore');
|
||||
const npm = require('npm');
|
||||
const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs.js').plugins;
|
||||
const semver = require('semver');
|
||||
const plugins = require('../../static/js/pluginfw/plugin_defs.js').plugins;
|
||||
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
|
||||
// {es: {"foo": "string"}, en:...}
|
||||
function getAllLocales() {
|
||||
const getAllLocales = () => {
|
||||
const locales2paths = {};
|
||||
|
||||
// Puts the paths of all locale files contained in a given directory
|
||||
// into `locales2paths` (files from various dirs are grouped by lang code)
|
||||
// (only json files with valid language code as name)
|
||||
function extractLangs(dir) {
|
||||
const extractLangs = (dir) => {
|
||||
if (!existsSync(dir)) return;
|
||||
let stat = fs.lstatSync(dir);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return;
|
||||
|
@ -31,12 +30,12 @@ function getAllLocales() {
|
|||
const ext = path.extname(file);
|
||||
const locale = path.basename(file, ext).toLowerCase();
|
||||
|
||||
if ((ext == '.json') && languages.isValid(locale)) {
|
||||
if ((ext === '.json') && languages.isValid(locale)) {
|
||||
if (!locales2paths[locale]) locales2paths[locale] = [];
|
||||
locales2paths[locale].push(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// add core supported languages first
|
||||
extractLangs(`${npm.root}/ep_etherpad-lite/locales`);
|
||||
|
@ -78,29 +77,29 @@ function getAllLocales() {
|
|||
}
|
||||
|
||||
return locales;
|
||||
}
|
||||
};
|
||||
|
||||
// returns a hash of all available languages availables with nativeName and direction
|
||||
// e.g. { es: {nativeName: "español", direction: "ltr"}, ... }
|
||||
function getAvailableLangs(locales) {
|
||||
const getAvailableLangs = (locales) => {
|
||||
const result = {};
|
||||
_.each(_.keys(locales), (langcode) => {
|
||||
result[langcode] = languages.getLanguageInfo(langcode);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
// returns locale index that will be served in /locales.json
|
||||
const generateLocaleIndex = function (locales) {
|
||||
const generateLocaleIndex = (locales) => {
|
||||
const result = _.clone(locales); // keep English strings
|
||||
_.each(_.keys(locales), (langcode) => {
|
||||
if (langcode != 'en') result[langcode] = `locales/${langcode}.json`;
|
||||
if (langcode !== 'en') result[langcode] = `locales/${langcode}.json`;
|
||||
});
|
||||
return JSON.stringify(result);
|
||||
};
|
||||
|
||||
|
||||
exports.expressCreateServer = function (n, args, cb) {
|
||||
exports.expressCreateServer = (n, args, cb) => {
|
||||
// regenerate locales on server restart
|
||||
const locales = getAllLocales();
|
||||
const localeIndex = generateLocaleIndex(locales);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
const securityManager = require('./db/SecurityManager');
|
||||
|
||||
// checks for padAccess
|
||||
|
|
|
@ -27,13 +27,17 @@
|
|||
const log4js = require('log4js');
|
||||
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
|
||||
* any modules that require newer versions of NodeJS
|
||||
*/
|
||||
const NodeVersion = require('./utils/NodeVersion');
|
||||
NodeVersion.enforceMinNodeVersion('10.13.0');
|
||||
NodeVersion.checkDeprecationStatus('10.13.0', '1.8.3');
|
||||
NodeVersion.enforceMinNodeVersion('10.17.0');
|
||||
NodeVersion.checkDeprecationStatus('10.17.0', '1.8.8');
|
||||
|
||||
const UpdateCheck = require('./utils/UpdateCheck');
|
||||
const db = require('./db/DB');
|
||||
|
@ -44,13 +48,45 @@ const plugins = require('../static/js/pluginfw/plugins');
|
|||
const settings = require('./utils/Settings');
|
||||
const util = require('util');
|
||||
|
||||
let started = false;
|
||||
let stopped = false;
|
||||
const State = {
|
||||
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 () => {
|
||||
if (started) return express.server;
|
||||
started = true;
|
||||
if (stopped) throw new Error('restart not supported');
|
||||
switch (state) {
|
||||
case State.INITIAL:
|
||||
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
|
||||
UpdateCheck.check();
|
||||
|
@ -60,77 +96,132 @@ exports.start = async () => {
|
|||
stats.gauge('memoryUsage', () => process.memoryUsage().rss);
|
||||
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 {
|
||||
await db.init();
|
||||
await plugins.update();
|
||||
console.info(`Installed plugins: ${plugins.formatPluginsWithVersion()}`);
|
||||
console.debug(`Installed parts:\n${plugins.formatParts()}`);
|
||||
console.debug(`Installed hooks:\n${plugins.formatHooks()}`);
|
||||
await hooks.aCallAll('loadSettings', {settings});
|
||||
await hooks.aCallAll('createServer');
|
||||
} catch (e) {
|
||||
console.error(`exception thrown: ${e.message}`);
|
||||
if (e.stack) console.log(e.stack);
|
||||
process.exit(1);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
process.on('uncaughtException', exports.exit);
|
||||
await util.promisify(npm.load)();
|
||||
await db.init();
|
||||
await plugins.update();
|
||||
console.info(`Installed plugins: ${plugins.formatPluginsWithVersion()}`);
|
||||
console.debug(`Installed parts:\n${plugins.formatParts()}`);
|
||||
console.debug(`Installed hooks:\n${plugins.formatHooks()}`);
|
||||
await hooks.aCallAll('loadSettings', {settings});
|
||||
await hooks.aCallAll('createServer');
|
||||
|
||||
/*
|
||||
* 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());
|
||||
console.log('Etherpad is running');
|
||||
state = State.RUNNING;
|
||||
while (runningCallbacks.length > 0) setImmediate(runningCallbacks.pop());
|
||||
|
||||
// Return the HTTP server to make it easier to write tests.
|
||||
return express.server;
|
||||
};
|
||||
|
||||
const stoppedCallbacks = [];
|
||||
exports.stop = async () => {
|
||||
if (stopped) return;
|
||||
stopped = true;
|
||||
switch (state) {
|
||||
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...');
|
||||
await new Promise(async (resolve, reject) => {
|
||||
const id = setTimeout(() => reject(new Error('Timed out waiting for shutdown tasks')), 3000);
|
||||
await hooks.aCallAll('shutdown');
|
||||
clearTimeout(id);
|
||||
resolve();
|
||||
});
|
||||
state = State.STOPPING;
|
||||
let timeout = null;
|
||||
await Promise.race([
|
||||
hooks.aCallAll('shutdown'),
|
||||
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) => {
|
||||
let exitCode = 0;
|
||||
if (err) {
|
||||
exitCode = 1;
|
||||
console.error(err.stack ? err.stack : err);
|
||||
const exitCallbacks = [];
|
||||
let exitCalled = false;
|
||||
exports.exit = async (err = null) => {
|
||||
/* eslint-disable no-process-exit */
|
||||
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 {
|
||||
await exports.stop();
|
||||
} catch (err) {
|
||||
exitCode = 1;
|
||||
console.error(err.stack ? err.stack : err);
|
||||
exitCalled = true;
|
||||
switch (state) {
|
||||
case State.STARTING:
|
||||
case State.RUNNING:
|
||||
case State.STOPPING:
|
||||
await exports.stop();
|
||||
// Don't fall through to State.STOPPED in case another caller is also waiting for stop().
|
||||
// Don't pass err to exports.exit() because this err has already been processed. (If err is
|
||||
// passed again to exit() then exit() will think that a second error occurred while exiting.)
|
||||
return await exports.exit();
|
||||
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();
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Controls the communication with the Abiword application
|
||||
*/
|
||||
|
@ -25,11 +26,12 @@ const os = require('os');
|
|||
|
||||
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) {
|
||||
let stdoutBuffer = '';
|
||||
|
||||
doConvertTask = function (task, callback) {
|
||||
doConvertTask = (task, callback) => {
|
||||
// span an abiword process to perform the conversion
|
||||
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
|
||||
abiword.on('exit', (code) => {
|
||||
if (code != 0) {
|
||||
if (code !== 0) {
|
||||
return callback(`Abiword died with exit code ${code}`);
|
||||
}
|
||||
|
||||
if (stdoutBuffer != '') {
|
||||
if (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);
|
||||
};
|
||||
}
|
||||
// on unix operating systems, we can start abiword with abicommand and communicate with it via stdin/stdout
|
||||
// thats much faster, about factor 10
|
||||
else {
|
||||
// on unix operating systems, we can start abiword with abicommand and
|
||||
// communicate with it via stdin/stdout
|
||||
// thats much faster, about factor 10
|
||||
} else {
|
||||
// spawn the abiword process
|
||||
let abiword;
|
||||
let stdoutCallback = null;
|
||||
var spawnAbiword = function () {
|
||||
const spawnAbiword = () => {
|
||||
abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']);
|
||||
let stdoutBuffer = '';
|
||||
let firstPrompt = true;
|
||||
|
@ -90,9 +92,9 @@ else {
|
|||
stdoutBuffer += data.toString();
|
||||
|
||||
// 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
|
||||
const err = stdoutBuffer.search('OK') != -1 ? null : stdoutBuffer;
|
||||
const err = stdoutBuffer.search('OK') !== -1 ? null : stdoutBuffer;
|
||||
|
||||
// reset the buffer
|
||||
stdoutBuffer = '';
|
||||
|
@ -110,10 +112,10 @@ else {
|
|||
};
|
||||
spawnAbiword();
|
||||
|
||||
doConvertTask = function (task, callback) {
|
||||
doConvertTask = (task, callback) => {
|
||||
abiword.stdin.write(`convert ${task.srcFile} ${task.destFile} ${task.type}\n`);
|
||||
// create a callback that calls the task callback and the caller callback
|
||||
stdoutCallback = function (err) {
|
||||
stdoutCallback = (err) => {
|
||||
callback();
|
||||
console.log('queue continue');
|
||||
try {
|
||||
|
@ -126,7 +128,7 @@ else {
|
|||
|
||||
// Queue with the converts we have to do
|
||||
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});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* 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
|
||||
* overlap.
|
||||
*/
|
||||
const popIfEndsWith = function (stringArray, lastDesiredElements) {
|
||||
const popIfEndsWith = (stringArray, lastDesiredElements) => {
|
||||
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`);
|
||||
|
||||
|
@ -72,8 +73,8 @@ const popIfEndsWith = function (stringArray, lastDesiredElements) {
|
|||
* @return {string} The identified absolute base path. If such path cannot be
|
||||
* identified, prints a log and exits the application.
|
||||
*/
|
||||
exports.findEtherpadRoot = function () {
|
||||
if (etherpadRoot !== null) {
|
||||
exports.findEtherpadRoot = () => {
|
||||
if (etherpadRoot != null) {
|
||||
return etherpadRoot;
|
||||
}
|
||||
|
||||
|
@ -126,7 +127,7 @@ exports.findEtherpadRoot = function () {
|
|||
* it is returned unchanged. Otherwise it is interpreted
|
||||
* relative to exports.root.
|
||||
*/
|
||||
exports.makeAbsolute = function (somePath) {
|
||||
exports.makeAbsolute = (somePath) => {
|
||||
if (path.isAbsolute(somePath)) {
|
||||
return somePath;
|
||||
}
|
||||
|
@ -145,7 +146,7 @@ exports.makeAbsolute = function (somePath) {
|
|||
* a subdirectory of the base one
|
||||
* @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
|
||||
const relative = path.relative(parent, arbitraryDir);
|
||||
const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* The CLI module handles command line parameters
|
||||
*/
|
||||
|
@ -30,22 +31,22 @@ for (let i = 0; i < argv.length; i++) {
|
|||
arg = argv[i];
|
||||
|
||||
// Override location of settings.json file
|
||||
if (prevArg == '--settings' || prevArg == '-s') {
|
||||
if (prevArg === '--settings' || prevArg === '-s') {
|
||||
exports.argv.settings = arg;
|
||||
}
|
||||
|
||||
// Override location of credentials.json file
|
||||
if (prevArg == '--credentials') {
|
||||
if (prevArg === '--credentials') {
|
||||
exports.argv.credentials = arg;
|
||||
}
|
||||
|
||||
// Override location of settings.json file
|
||||
if (prevArg == '--sessionkey') {
|
||||
if (prevArg === '--sessionkey') {
|
||||
exports.argv.sessionkey = arg;
|
||||
}
|
||||
|
||||
// Override location of settings.json file
|
||||
if (prevArg == '--apikey') {
|
||||
if (prevArg === '--apikey') {
|
||||
exports.argv.apikey = arg;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* 2014 John McLear (Etherpad Foundation / McLear Ltd)
|
||||
*
|
||||
|
@ -16,9 +17,9 @@
|
|||
|
||||
|
||||
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 padcontent = await db.get(padKey);
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Helpers for export requests
|
||||
*/
|
||||
|
@ -18,9 +19,9 @@
|
|||
* 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 atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext);
|
||||
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 = {};
|
||||
|
||||
// identify list
|
||||
|
@ -81,6 +82,5 @@ exports._analyzeLine = function (text, aline, apool) {
|
|||
};
|
||||
|
||||
|
||||
exports._encodeWhitespace = function (s) {
|
||||
return s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`);
|
||||
};
|
||||
exports._encodeWhitespace =
|
||||
(s) => s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
|
@ -14,32 +15,29 @@
|
|||
* 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 _ = require('underscore');
|
||||
const Security = require('ep_etherpad-lite/static/js/security');
|
||||
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
|
||||
const eejs = require('ep_etherpad-lite/node/eejs');
|
||||
const Security = require('../../static/js/security');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const eejs = require('../eejs');
|
||||
const _analyzeLine = require('./ExportHelper')._analyzeLine;
|
||||
const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace;
|
||||
const padutils = require('../../static/js/pad_utils').padutils;
|
||||
|
||||
async function getPadHTML(pad, revNum) {
|
||||
const getPadHTML = async (pad, revNum) => {
|
||||
let atext = pad.atext;
|
||||
|
||||
// fetch revision atext
|
||||
if (revNum != undefined) {
|
||||
if (revNum !== undefined) {
|
||||
atext = await pad.getInternalRevisionAText(revNum);
|
||||
}
|
||||
|
||||
// convert atext to html
|
||||
return await getHTMLFromAtext(pad, atext);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getPadHTML = getPadHTML;
|
||||
exports.getHTMLFromAtext = getHTMLFromAtext;
|
||||
|
||||
async function getHTMLFromAtext(pad, atext, authorColors) {
|
||||
const getHTMLFromAtext = async (pad, atext, authorColors) => {
|
||||
const apool = pad.apool();
|
||||
const textLines = atext.text.slice(0, -1).split('\n');
|
||||
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
||||
|
@ -72,9 +70,7 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
|
|||
const anumMap = {};
|
||||
let css = '';
|
||||
|
||||
const stripDotFromAuthorID = function (id) {
|
||||
return id.replace(/\./g, '_');
|
||||
};
|
||||
const stripDotFromAuthorID = (id) => id.replace(/\./g, '_');
|
||||
|
||||
if (authorColors) {
|
||||
css += '<style>\n';
|
||||
|
@ -85,15 +81,14 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
|
|||
// skip non author attributes
|
||||
if (attr[0] === 'author' && attr[1] !== '') {
|
||||
// add to props array
|
||||
var propName = `author${stripDotFromAuthorID(attr[1])}`;
|
||||
var newLength = props.push(propName);
|
||||
const propName = `author${stripDotFromAuthorID(attr[1])}`;
|
||||
const newLength = props.push(propName);
|
||||
anumMap[a] = newLength - 1;
|
||||
|
||||
css += `.${propName} {background-color: ${authorColors[attr[1]]}}\n`;
|
||||
} else if (attr[0] === 'removed') {
|
||||
var propName = 'removed';
|
||||
|
||||
var newLength = props.push(propName);
|
||||
const propName = 'removed';
|
||||
const newLength = props.push(propName);
|
||||
anumMap[a] = newLength - 1;
|
||||
|
||||
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
|
||||
// and decent nesting. For example,
|
||||
// <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 openTags = [];
|
||||
|
||||
function getSpanClassFor(i) {
|
||||
const getSpanClassFor = (i) => {
|
||||
// return if author colors are disabled
|
||||
if (!authorColors) return false;
|
||||
|
||||
|
@ -153,16 +148,16 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
|
|||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// tags added by exportHtmlAdditionalTagsWithData will be exported as <span> with
|
||||
// data attributes
|
||||
function isSpanWithData(i) {
|
||||
const isSpanWithData = (i) => {
|
||||
const property = props[i];
|
||||
return _.isArray(property);
|
||||
}
|
||||
};
|
||||
|
||||
function emitOpenTag(i) {
|
||||
const emitOpenTag = (i) => {
|
||||
openTags.unshift(i);
|
||||
const spanClass = getSpanClassFor(i);
|
||||
|
||||
|
@ -175,10 +170,10 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
|
|||
assem.append(tags[i]);
|
||||
assem.append('>');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// this closes an open tag and removes its reference from openTags
|
||||
function emitCloseTag(i) {
|
||||
const emitCloseTag = (i) => {
|
||||
openTags.shift();
|
||||
const spanClass = getSpanClassFor(i);
|
||||
const spanWithData = isSpanWithData(i);
|
||||
|
@ -190,13 +185,13 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
|
|||
assem.append(tags[i]);
|
||||
assem.append('>');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const urls = padutils.findURLs(text);
|
||||
|
||||
let idx = 0;
|
||||
|
||||
function processNextChars(numChars) {
|
||||
const processNextChars = (numChars) => {
|
||||
if (numChars <= 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -208,7 +203,7 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
|
|||
// based on the attribs used
|
||||
while (iter.hasNext()) {
|
||||
const o = iter.next();
|
||||
var usedAttribs = [];
|
||||
const usedAttribs = [];
|
||||
|
||||
// mark all attribs as used
|
||||
Changeset.eachAttribNumber(o.attribs, (a) => {
|
||||
|
@ -218,7 +213,7 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
|
|||
});
|
||||
let outermostTag = -1;
|
||||
// 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) {
|
||||
outermostTag = i;
|
||||
break;
|
||||
|
@ -234,7 +229,7 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
|
|||
}
|
||||
|
||||
// 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) {
|
||||
emitOpenTag(usedAttribs[i]);
|
||||
}
|
||||
|
@ -258,14 +253,16 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
|
|||
while (openTags.length > 0) {
|
||||
emitCloseTag(openTags[0]);
|
||||
}
|
||||
} // end processNextChars
|
||||
};
|
||||
// end processNextChars
|
||||
if (urls) {
|
||||
urls.forEach((urlData) => {
|
||||
const startIndex = urlData[0];
|
||||
const url = urlData[1];
|
||||
const urlLength = url.length;
|
||||
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.
|
||||
// https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer
|
||||
// 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);
|
||||
|
||||
return _processSpaces(assem.toString());
|
||||
} // end getLineHTML
|
||||
};
|
||||
// end getLineHTML
|
||||
const pieces = [css];
|
||||
|
||||
// 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
|
||||
let openLists = [];
|
||||
for (let i = 0; i < textLines.length; i++) {
|
||||
var context;
|
||||
var line = _analyzeLine(textLines[i], attribLines[i], apool);
|
||||
let context;
|
||||
const line = _analyzeLine(textLines[i], attribLines[i], apool);
|
||||
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 = {
|
||||
line,
|
||||
lineContent,
|
||||
|
@ -315,8 +313,11 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
|
|||
}
|
||||
await hooks.aCallAll('getLineHTMLForExport', context);
|
||||
// To create list parent elements
|
||||
if ((!prevLine || prevLine.listLevel !== line.listLevel) || (prevLine && line.listTypeName !== prevLine.listTypeName)) {
|
||||
const exists = _.find(openLists, (item) => (item.level === line.listLevel && item.type === line.listTypeName));
|
||||
if ((!prevLine || prevLine.listLevel !== line.listLevel) ||
|
||||
(prevLine && line.listTypeName !== prevLine.listTypeName)) {
|
||||
const exists = _.find(openLists, (item) => (
|
||||
item.level === line.listLevel && item.type === line.listTypeName)
|
||||
);
|
||||
if (!exists) {
|
||||
let prevLevel = 0;
|
||||
if (prevLine && prevLine.listLevel) {
|
||||
|
@ -326,23 +327,33 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
|
|||
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});
|
||||
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..
|
||||
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..
|
||||
<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>
|
||||
so we consider this HTML,
|
||||
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>
|
||||
|
||||
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>
|
||||
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
|
||||
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>
|
||||
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.
|
||||
// pieces.push("</li>");
|
||||
|
||||
|
@ -350,14 +361,16 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
|
|||
if ((nextLine.listTypeName === 'number') && (nextLine.text === '')) {
|
||||
// is the listTypeName check needed here? null text might be completely fine!
|
||||
// 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 {
|
||||
pieces.push('<li>');
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// 1. hello
|
||||
// * foo
|
||||
|
@ -384,18 +397,24 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
|
|||
}
|
||||
|
||||
// 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 ((nextLine.listTypeName === 'number') && (nextLine.text === '')) {
|
||||
// is the listTypeName check needed here? null text might be completely fine!
|
||||
// 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 {
|
||||
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;
|
||||
if (nextLine && nextLine.listLevel) {
|
||||
nextLevel = nextLine.listLevel;
|
||||
|
@ -404,10 +423,11 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
|
|||
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);
|
||||
|
||||
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>');
|
||||
}
|
||||
|
||||
|
@ -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 = {
|
||||
line,
|
||||
lineContent,
|
||||
|
@ -435,9 +455,9 @@ async function getHTMLFromAtext(pad, atext, authorColors) {
|
|||
}
|
||||
|
||||
return pieces.join('');
|
||||
}
|
||||
};
|
||||
|
||||
exports.getPadHTMLDocument = async function (padId, revNum) {
|
||||
exports.getPadHTMLDocument = async (padId, revNum) => {
|
||||
const pad = await padManager.getPad(padId);
|
||||
|
||||
// Include some Styles into the Head for Export
|
||||
|
@ -461,7 +481,7 @@ exports.getPadHTMLDocument = async function (padId, revNum) {
|
|||
};
|
||||
|
||||
// copied from ACE
|
||||
function _processSpaces(s) {
|
||||
const _processSpaces = (s) => {
|
||||
const doesWrap = true;
|
||||
if (s.indexOf('<') < 0 && !doesWrap) {
|
||||
// short-cut
|
||||
|
@ -476,34 +496,37 @@ function _processSpaces(s) {
|
|||
let beforeSpace = false;
|
||||
// last space in a run is normal, others are nbsp,
|
||||
// end of line is nbsp
|
||||
for (var i = parts.length - 1; i >= 0; i--) {
|
||||
var p = parts[i];
|
||||
if (p == ' ') {
|
||||
for (let i = parts.length - 1; i >= 0; i--) {
|
||||
const p = parts[i];
|
||||
if (p === ' ') {
|
||||
if (endOfLine || beforeSpace) parts[i] = ' ';
|
||||
endOfLine = false;
|
||||
beforeSpace = true;
|
||||
} else if (p.charAt(0) != '<') {
|
||||
} else if (p.charAt(0) !== '<') {
|
||||
endOfLine = false;
|
||||
beforeSpace = false;
|
||||
}
|
||||
}
|
||||
// beginning of line is nbsp
|
||||
for (i = 0; i < parts.length; i++) {
|
||||
p = parts[i];
|
||||
if (p == ' ') {
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const p = parts[i];
|
||||
if (p === ' ') {
|
||||
parts[i] = ' ';
|
||||
break;
|
||||
} else if (p.charAt(0) != '<') {
|
||||
} else if (p.charAt(0) !== '<') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (i = 0; i < parts.length; i++) {
|
||||
p = parts[i];
|
||||
if (p == ' ') {
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const p = parts[i];
|
||||
if (p === ' ') {
|
||||
parts[i] = ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
return parts.join('');
|
||||
}
|
||||
};
|
||||
|
||||
exports.getPadHTML = getPadHTML;
|
||||
exports.getHTMLFromAtext = getHTMLFromAtext;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* TXT export
|
||||
*/
|
||||
|
@ -18,15 +19,15 @@
|
|||
* 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 _analyzeLine = require('./ExportHelper')._analyzeLine;
|
||||
|
||||
// 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;
|
||||
|
||||
if (revNum != undefined) {
|
||||
if (revNum !== undefined) {
|
||||
// fetch revision atext
|
||||
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
|
||||
// functionality that is designed specifically for TXT exports
|
||||
function getTXTFromAtext(pad, atext, authorColors) {
|
||||
const getTXTFromAtext = (pad, atext, authorColors) => {
|
||||
const apool = pad.apool();
|
||||
const textLines = atext.text.slice(0, -1).split('\n');
|
||||
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
||||
|
@ -53,7 +54,7 @@ function getTXTFromAtext(pad, atext, authorColors) {
|
|||
}
|
||||
});
|
||||
|
||||
function getLineTXT(text, attribs) {
|
||||
const getLineTXT = (text, attribs) => {
|
||||
const propVals = [false, false, false];
|
||||
const ENTER = 1;
|
||||
const STAY = 2;
|
||||
|
@ -69,7 +70,7 @@ function getTXTFromAtext(pad, atext, authorColors) {
|
|||
|
||||
let idx = 0;
|
||||
|
||||
function processNextChars(numChars) {
|
||||
const processNextChars = (numChars) => {
|
||||
if (numChars <= 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -79,7 +80,7 @@ function getTXTFromAtext(pad, atext, authorColors) {
|
|||
|
||||
while (iter.hasNext()) {
|
||||
const o = iter.next();
|
||||
var propChanged = false;
|
||||
let propChanged = false;
|
||||
|
||||
Changeset.eachAttribNumber(o.attribs, (a) => {
|
||||
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) {
|
||||
propVals[i] = LEAVE;
|
||||
propChanged = true;
|
||||
|
@ -110,7 +111,7 @@ function getTXTFromAtext(pad, atext, authorColors) {
|
|||
// leaving bold (e.g.) also leaves italics, etc.
|
||||
let left = false;
|
||||
|
||||
for (var i = 0; i < propVals.length; i++) {
|
||||
for (let i = 0; i < propVals.length; i++) {
|
||||
const v = propVals[i];
|
||||
|
||||
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) {
|
||||
// emitCloseTag(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) {
|
||||
propVals[i] = true;
|
||||
}
|
||||
|
@ -163,18 +164,20 @@ function getTXTFromAtext(pad, atext, authorColors) {
|
|||
assem.append(s);
|
||||
} // end iteration over spans in line
|
||||
|
||||
var tags2close = [];
|
||||
for (var i = propVals.length - 1; i >= 0; i--) {
|
||||
const tags2close = [];
|
||||
for (let i = propVals.length - 1; i >= 0; i--) {
|
||||
if (propVals[i]) {
|
||||
tags2close.push(i);
|
||||
propVals[i] = false;
|
||||
}
|
||||
}
|
||||
} // end processNextChars
|
||||
};
|
||||
// end processNextChars
|
||||
|
||||
processNextChars(text.length - idx);
|
||||
return (assem.toString());
|
||||
} // end getLineHTML
|
||||
};
|
||||
// end getLineHTML
|
||||
|
||||
const pieces = [css];
|
||||
|
||||
|
@ -193,7 +196,7 @@ function getTXTFromAtext(pad, atext, authorColors) {
|
|||
const line = _analyzeLine(textLines[i], attribLines[i], apool);
|
||||
let lineContent = getLineTXT(line.text, line.aline);
|
||||
|
||||
if (line.listTypeName == 'bullet') {
|
||||
if (line.listTypeName === '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
|
||||
* listNumber(s) == item number
|
||||
|
@ -249,11 +252,11 @@ function getTXTFromAtext(pad, atext, authorColors) {
|
|||
}
|
||||
|
||||
return pieces.join('');
|
||||
}
|
||||
};
|
||||
|
||||
exports.getTXTFromAtext = getTXTFromAtext;
|
||||
|
||||
exports.getPadTXTDocument = async function (padId, revNum) {
|
||||
exports.getPadTXTDocument = async (padId, revNum) => {
|
||||
const pad = await padManager.getPad(padId);
|
||||
return getPadTXT(pad, revNum);
|
||||
};
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// 'use strict';
|
||||
// Uncommenting above breaks tests.
|
||||
/**
|
||||
* 2014 John McLear (Etherpad Foundation / McLear Ltd)
|
||||
*
|
||||
|
@ -14,12 +16,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const log4js = require('log4js');
|
||||
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) {
|
||||
records = JSON.parse(records);
|
||||
exports.setPadRaw = (padId, r) => {
|
||||
const records = JSON.parse(r);
|
||||
|
||||
Object.keys(records).forEach(async (key) => {
|
||||
let value = records[key];
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Copyright Yaco Sistemas S.L. 2011.
|
||||
*
|
||||
|
@ -15,8 +16,8 @@
|
|||
*/
|
||||
|
||||
const log4js = require('log4js');
|
||||
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
|
||||
const contentcollector = require('ep_etherpad-lite/static/js/contentcollector');
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const contentcollector = require('../../static/js/contentcollector');
|
||||
const cheerio = require('cheerio');
|
||||
const rehype = require('rehype');
|
||||
const minifyWhitespace = require('rehype-minify-whitespace');
|
||||
|
@ -69,7 +70,7 @@ exports.setPadHTML = async (pad, html) => {
|
|||
apiLogger.debug(newText);
|
||||
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);
|
||||
let textIndex = 0;
|
||||
const newTextStart = 0;
|
||||
|
@ -82,7 +83,7 @@ exports.setPadHTML = async (pad, html) => {
|
|||
}
|
||||
textIndex = nextIndex;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// create a new changeset with a helper builder object
|
||||
const builder = Changeset.builder(1);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Controls the communication with LibreOffice
|
||||
*/
|
||||
|
@ -24,52 +25,9 @@ const path = require('path');
|
|||
const settings = require('./Settings');
|
||||
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');
|
||||
|
||||
/**
|
||||
* 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 doConvertTask = (task, callback) => {
|
||||
const tmpDir = os.tmpdir();
|
||||
|
||||
async.series([
|
||||
|
@ -77,8 +35,10 @@ function doConvertTask(task, callback) {
|
|||
* use LibreOffice to convert task.srcFile to another format, given in
|
||||
* task.type
|
||||
*/
|
||||
function (callback) {
|
||||
libreOfficeLogger.debug(`Converting ${task.srcFile} to format ${task.type}. The result will be put in ${tmpDir}`);
|
||||
(callback) => {
|
||||
libreOfficeLogger.debug(
|
||||
`Converting ${task.srcFile} to format ${task.type}. The result will be put in ${tmpDir}`
|
||||
);
|
||||
const soffice = spawn(settings.soffice, [
|
||||
'--headless',
|
||||
'--invisible',
|
||||
|
@ -112,7 +72,7 @@ function doConvertTask(task, callback) {
|
|||
|
||||
soffice.on('exit', (code) => {
|
||||
clearTimeout(hangTimeout);
|
||||
if (code != 0) {
|
||||
if (code !== 0) {
|
||||
// Throw an exception if libreoffice failed
|
||||
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
|
||||
function (callback) {
|
||||
(callback) => {
|
||||
const filename = path.basename(task.srcFile);
|
||||
const sourceFilename = `${filename.substr(0, filename.lastIndexOf('.'))}.${task.fileExtension}`;
|
||||
const sourcePath = path.join(tmpDir, sourceFilename);
|
||||
const sourceFile = `${filename.substr(0, filename.lastIndexOf('.'))}.${task.fileExtension}`;
|
||||
const sourcePath = path.join(tmpDir, sourceFile);
|
||||
libreOfficeLogger.debug(`Renaming ${sourcePath} to ${task.destFile}`);
|
||||
fs.rename(sourcePath, task.destFile, callback);
|
||||
},
|
||||
|
@ -137,4 +97,55 @@ function doConvertTask(task, callback) {
|
|||
// Invoke the callback for the task
|
||||
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});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Worker thread to minify JS & CSS files out of the main NodeJS thread
|
||||
*/
|
||||
|
@ -7,16 +8,13 @@ const Terser = require('terser');
|
|||
const path = require('path');
|
||||
const Threads = require('threads');
|
||||
|
||||
function compressJS(content) {
|
||||
return Terser.minify(content);
|
||||
}
|
||||
const compressJS = (content) => Terser.minify(content);
|
||||
|
||||
function compressCSS(filename, ROOT_DIR) {
|
||||
return new Promise((res, rej) => {
|
||||
try {
|
||||
const absPath = path.join(ROOT_DIR, filename);
|
||||
const compressCSS = (filename, ROOT_DIR) => new Promise((res, rej) => {
|
||||
try {
|
||||
const absPath = path.join(ROOT_DIR, filename);
|
||||
|
||||
/*
|
||||
/*
|
||||
* Changes done to migrate CleanCSS 3.x -> 4.x:
|
||||
*
|
||||
* 1. Rework the rebase logic, because the API was simplified (but we have
|
||||
|
@ -41,23 +39,22 @@ function compressCSS(filename, ROOT_DIR) {
|
|||
* in an array and ask the library to read it by itself.
|
||||
*/
|
||||
|
||||
const basePath = path.dirname(absPath);
|
||||
const basePath = path.dirname(absPath);
|
||||
|
||||
new CleanCSS({
|
||||
rebase: true,
|
||||
rebaseTo: basePath,
|
||||
}).minify([absPath], (errors, minified) => {
|
||||
if (errors) return rej(errors);
|
||||
new CleanCSS({
|
||||
rebase: true,
|
||||
rebaseTo: basePath,
|
||||
}).minify([absPath], (errors, minified) => {
|
||||
if (errors) return rej(errors);
|
||||
|
||||
return res(minified.styles);
|
||||
});
|
||||
} catch (error) {
|
||||
// on error, just yield the un-minified original, but write a log message
|
||||
console.error(`Unexpected error minifying ${filename} (${absPath}): ${error}`);
|
||||
callback(null, content);
|
||||
}
|
||||
});
|
||||
}
|
||||
return res(minified.styles);
|
||||
});
|
||||
} catch (error) {
|
||||
// on error, just yield the un-minified original, but write a log message
|
||||
console.error(`Unexpected error minifying ${filename} (${absPath}): ${error}`);
|
||||
callback(null, content);
|
||||
}
|
||||
});
|
||||
|
||||
Threads.expose({
|
||||
compressJS,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Checks related to Node runtime version
|
||||
*/
|
||||
|
@ -25,7 +26,7 @@ const semver = require('semver');
|
|||
*
|
||||
* @param {String} minNodeVersion Minimum required Node version
|
||||
*/
|
||||
exports.enforceMinNodeVersion = function (minNodeVersion) {
|
||||
exports.enforceMinNodeVersion = (minNodeVersion) => {
|
||||
const currentNodeVersion = process.version;
|
||||
|
||||
// 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
|
||||
*
|
||||
* @param {String} lowestNonDeprecatedNodeVersion all Node version less than this one are deprecated
|
||||
* @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated Node releases
|
||||
* @param {String} lowestNonDeprecatedNodeVersion all Node version less than this one are
|
||||
* 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;
|
||||
|
||||
if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* The Settings module reads the settings out of settings.json and provides
|
||||
* this information to the other modules
|
||||
|
@ -31,7 +32,6 @@ const fs = require('fs');
|
|||
const os = require('os');
|
||||
const path = require('path');
|
||||
const argv = require('./Cli').argv;
|
||||
const npm = require('npm/lib/npm.js');
|
||||
const jsonminify = require('jsonminify');
|
||||
const log4js = require('log4js');
|
||||
const randomString = require('./randomstring');
|
||||
|
@ -381,29 +381,30 @@ exports.commitRateLimiting = {
|
|||
exports.importMaxFileSize = 50 * 1024 * 1024;
|
||||
|
||||
// checks if abiword is avaiable
|
||||
exports.abiwordAvailable = function () {
|
||||
exports.abiwordAvailable = () => {
|
||||
if (exports.abiword != null) {
|
||||
return os.type().indexOf('Windows') != -1 ? 'withoutPDF' : 'yes';
|
||||
return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
|
||||
} else {
|
||||
return 'no';
|
||||
}
|
||||
};
|
||||
|
||||
exports.sofficeAvailable = function () {
|
||||
exports.sofficeAvailable = () => {
|
||||
if (exports.soffice != null) {
|
||||
return os.type().indexOf('Windows') != -1 ? 'withoutPDF' : 'yes';
|
||||
return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
|
||||
} else {
|
||||
return 'no';
|
||||
}
|
||||
};
|
||||
|
||||
exports.exportAvailable = function () {
|
||||
exports.exportAvailable = () => {
|
||||
const abiword = exports.abiwordAvailable();
|
||||
const soffice = exports.sofficeAvailable();
|
||||
|
||||
if (abiword == 'no' && soffice == 'no') {
|
||||
if (abiword === 'no' && soffice === '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';
|
||||
} else {
|
||||
return 'yes';
|
||||
|
@ -411,7 +412,7 @@ exports.exportAvailable = function () {
|
|||
};
|
||||
|
||||
// Provide git version if available
|
||||
exports.getGitCommit = function () {
|
||||
exports.getGitCommit = () => {
|
||||
let version = '';
|
||||
try {
|
||||
let rootPath = exports.root;
|
||||
|
@ -436,9 +437,7 @@ exports.getGitCommit = function () {
|
|||
};
|
||||
|
||||
// Return etherpad version from package.json
|
||||
exports.getEpVersion = function () {
|
||||
return require('ep_etherpad-lite/package.json').version;
|
||||
};
|
||||
exports.getEpVersion = () => require('../../package.json').version;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* both "settings.json" and "credentials.json".
|
||||
*/
|
||||
function storeSettings(settingsObj) {
|
||||
const storeSettings = (settingsObj) => {
|
||||
for (const i in settingsObj) {
|
||||
// test if the setting starts with a lowercase character
|
||||
if (i.charAt(0).search('[a-z]') !== 0) {
|
||||
|
@ -456,7 +455,7 @@ function storeSettings(settingsObj) {
|
|||
|
||||
// we know this setting, so we overwrite it
|
||||
// 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])) {
|
||||
exports[i] = _.defaults(settingsObj[i], exports[i]);
|
||||
} else {
|
||||
|
@ -467,7 +466,7 @@ function storeSettings(settingsObj) {
|
|||
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
|
||||
|
@ -481,7 +480,7 @@ function storeSettings(settingsObj) {
|
|||
* short syntax "${ABIWORD}", and not "${ABIWORD:null}": the latter would result
|
||||
* 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
|
||||
const isNumeric = !isNaN(stringValue) && !isNaN(parseFloat(stringValue) && isFinite(stringValue));
|
||||
|
||||
|
@ -502,7 +501,7 @@ function coerceValue(stringValue) {
|
|||
|
||||
// otherwise, return this value as-is
|
||||
return stringValue;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
function lookupEnvironmentVariables(obj) {
|
||||
const lookupEnvironmentVariables = (obj) => {
|
||||
const stringifiedAndReplaced = JSON.stringify(obj, (key, value) => {
|
||||
/*
|
||||
* 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
|
||||
const match = value.match(/^\$\{([^:]*)(:((.|\n)*))?\}$/);
|
||||
|
||||
if (match === null) {
|
||||
if (match == null) {
|
||||
// no match: use the value literally, without any substitution
|
||||
|
||||
return value;
|
||||
|
@ -613,7 +612,7 @@ function lookupEnvironmentVariables(obj) {
|
|||
const newSettings = JSON.parse(stringifiedAndReplaced);
|
||||
|
||||
return newSettings;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* - reads the JSON configuration file settingsFilename from disk
|
||||
|
@ -623,7 +622,7 @@ function lookupEnvironmentVariables(obj) {
|
|||
*
|
||||
* The isSettings variable only controls the error logging.
|
||||
*/
|
||||
function parseSettings(settingsFilename, isSettings) {
|
||||
const parseSettings = (settingsFilename, isSettings) => {
|
||||
let settingsStr = '';
|
||||
|
||||
let settingsType, notFoundMessage, notFoundFunction;
|
||||
|
@ -663,9 +662,9 @@ function parseSettings(settingsFilename, isSettings) {
|
|||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.reloadSettings = function reloadSettings() {
|
||||
exports.reloadSettings = () => {
|
||||
// Discover where the settings file lives
|
||||
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 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".`);
|
||||
|
||||
exports.skinName = 'colibris';
|
||||
|
@ -766,7 +765,7 @@ exports.reloadSettings = function reloadSettings() {
|
|||
}
|
||||
|
||||
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) {
|
||||
exports.defaultPadText = `${exports.defaultPadText}\nWarning: ${dirtyWarning}${suppressDisableMsg}`;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Tidy up the HTML in a given file
|
||||
*/
|
||||
|
@ -6,7 +7,7 @@ const log4js = require('log4js');
|
|||
const settings = require('./Settings');
|
||||
const spawn = require('child_process').spawn;
|
||||
|
||||
exports.tidy = function (srcFile) {
|
||||
exports.tidy = (srcFile) => {
|
||||
const logger = log4js.getLogger('TidyHtml');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
|
@ -1,30 +1,29 @@
|
|||
'use strict';
|
||||
const semver = require('semver');
|
||||
const settings = require('./Settings');
|
||||
const request = require('request');
|
||||
|
||||
let infos;
|
||||
|
||||
function loadEtherpadInformations() {
|
||||
return new Promise((resolve, reject) => {
|
||||
request('https://static.etherpad.org/info.json', (er, response, body) => {
|
||||
if (er) return reject(er);
|
||||
const loadEtherpadInformations = () => new Promise((resolve, reject) => {
|
||||
request('https://static.etherpad.org/info.json', (er, response, body) => {
|
||||
if (er) return reject(er);
|
||||
|
||||
try {
|
||||
infos = JSON.parse(body);
|
||||
return resolve(infos);
|
||||
} catch (err) {
|
||||
return reject(err);
|
||||
}
|
||||
});
|
||||
try {
|
||||
infos = JSON.parse(body);
|
||||
return resolve(infos);
|
||||
} catch (err) {
|
||||
return reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
exports.getLatestVersion = function () {
|
||||
exports.getLatestVersion = () => {
|
||||
exports.needsUpdate();
|
||||
return infos.latestVersion;
|
||||
};
|
||||
|
||||
exports.needsUpdate = function (cb) {
|
||||
exports.needsUpdate = (cb) => {
|
||||
loadEtherpadInformations().then((info) => {
|
||||
if (semver.gt(info.latestVersion, settings.getEpVersion())) {
|
||||
if (cb) return cb(true);
|
||||
|
@ -35,7 +34,7 @@ exports.needsUpdate = function (cb) {
|
|||
});
|
||||
};
|
||||
|
||||
exports.check = function () {
|
||||
exports.check = () => {
|
||||
exports.needsUpdate((needsUpdate) => {
|
||||
if (needsUpdate) {
|
||||
console.warn(`Update available: Download the actual version ${infos.latestVersion}`);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
/*
|
||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
||||
*
|
||||
|
@ -47,18 +49,16 @@ CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined;
|
|||
|
||||
const responseCache = {};
|
||||
|
||||
function djb2Hash(data) {
|
||||
const djb2Hash = (data) => {
|
||||
const chars = data.split('').map((str) => str.charCodeAt(0));
|
||||
return `${chars.reduce((prev, curr) => ((prev << 5) + prev) + curr, 5381)}`;
|
||||
}
|
||||
};
|
||||
|
||||
function generateCacheKeyWithSha256(path) {
|
||||
return _crypto.createHash('sha256').update(path).digest('hex');
|
||||
}
|
||||
const generateCacheKeyWithSha256 =
|
||||
(path) => _crypto.createHash('sha256').update(path).digest('hex');
|
||||
|
||||
function generateCacheKeyWithDjb2(path) {
|
||||
return Buffer.from(djb2Hash(path)).toString('hex');
|
||||
}
|
||||
const generateCacheKeyWithDjb2 =
|
||||
(path) => Buffer.from(djb2Hash(path)).toString('hex');
|
||||
|
||||
let generateCacheKey;
|
||||
|
||||
|
@ -66,7 +66,7 @@ if (_crypto) {
|
|||
generateCacheKey = generateCacheKeyWithSha256;
|
||||
} else {
|
||||
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
|
||||
|
@ -80,8 +80,8 @@ if (_crypto) {
|
|||
function CachingMiddleware() {
|
||||
}
|
||||
CachingMiddleware.prototype = new function () {
|
||||
function handle(req, res, next) {
|
||||
if (!(req.method == 'GET' || req.method == 'HEAD') || !CACHE_DIR) {
|
||||
const handle = (req, res, next) => {
|
||||
if (!(req.method === 'GET' || req.method === 'HEAD') || !CACHE_DIR) {
|
||||
return next(undefined, req, res);
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,7 @@ CachingMiddleware.prototype = new function () {
|
|||
const old_res = {};
|
||||
|
||||
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 cacheKey = generateCacheKey(path);
|
||||
|
@ -116,7 +116,7 @@ CachingMiddleware.prototype = new function () {
|
|||
|
||||
const _headers = {};
|
||||
old_res.setHeader = res.setHeader;
|
||||
res.setHeader = function (key, value) {
|
||||
res.setHeader = (key, value) => {
|
||||
// Don't set cookies, see issue #707
|
||||
if (key.toLowerCase() === 'set-cookie') return;
|
||||
|
||||
|
@ -126,11 +126,8 @@ CachingMiddleware.prototype = new function () {
|
|||
|
||||
old_res.writeHead = res.writeHead;
|
||||
res.writeHead = function (status, headers) {
|
||||
const lastModified = (res.getHeader('last-modified') &&
|
||||
new Date(res.getHeader('last-modified')));
|
||||
|
||||
res.writeHead = old_res.writeHead;
|
||||
if (status == 200) {
|
||||
if (status === 200) {
|
||||
// Update cache
|
||||
let buffer = '';
|
||||
|
||||
|
@ -169,7 +166,7 @@ CachingMiddleware.prototype = new function () {
|
|||
respond();
|
||||
});
|
||||
};
|
||||
} else if (status == 304) {
|
||||
} else if (status === 304) {
|
||||
// Nothing new changed from the cached version.
|
||||
old_res.write = res.write;
|
||||
old_res.end = res.end;
|
||||
|
@ -204,10 +201,10 @@ CachingMiddleware.prototype = new function () {
|
|||
const lastModified = (headers['last-modified'] &&
|
||||
new Date(headers['last-modified']));
|
||||
|
||||
if (statusCode == 200 && lastModified <= modifiedSince) {
|
||||
if (statusCode === 200 && lastModified <= modifiedSince) {
|
||||
res.writeHead(304, headers);
|
||||
res.end();
|
||||
} else if (req.method == 'GET') {
|
||||
} else if (req.method === 'GET') {
|
||||
const readStream = fs.createReadStream(pathStr);
|
||||
res.writeHead(statusCode, headers);
|
||||
readStream.pipe(res);
|
||||
|
@ -217,7 +214,7 @@ CachingMiddleware.prototype = new function () {
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.handle = handle;
|
||||
}();
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* CustomError
|
||||
*
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const exportHtml = require('./ExportHtml');
|
||||
|
||||
|
@ -125,7 +126,7 @@ PadDiff.prototype._addAuthors = function (authors) {
|
|||
|
||||
// add to array if not in the array
|
||||
authors.forEach((author) => {
|
||||
if (self._authors.indexOf(author) == -1) {
|
||||
if (self._authors.indexOf(author) === -1) {
|
||||
self._authors.push(author);
|
||||
}
|
||||
});
|
||||
|
@ -138,7 +139,6 @@ PadDiff.prototype._createDiffAtext = async function () {
|
|||
let atext = await this._createClearStartAtext(this._fromRev);
|
||||
|
||||
let superChangeset = null;
|
||||
const rev = this._fromRev + 1;
|
||||
|
||||
for (let rev = this._fromRev + 1; rev <= this._toRev; rev += bulkSize) {
|
||||
// get the bulk
|
||||
|
@ -161,7 +161,7 @@ PadDiff.prototype._createDiffAtext = async function () {
|
|||
addedAuthors.push(authors[i]);
|
||||
|
||||
// compose it with the superChangset
|
||||
if (superChangeset === null) {
|
||||
if (superChangeset == null) {
|
||||
superChangeset = changeset;
|
||||
} else {
|
||||
superChangeset = Changeset.composeWithDeletions(superChangeset, changeset, this._pad.pool);
|
||||
|
@ -172,7 +172,8 @@ PadDiff.prototype._createDiffAtext = async function () {
|
|||
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) {
|
||||
const deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool);
|
||||
|
||||
|
@ -205,7 +206,8 @@ PadDiff.prototype.getHtml = 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) {
|
||||
await this.getHtml();
|
||||
}
|
||||
|
@ -213,7 +215,7 @@ PadDiff.prototype.getAuthors = async function () {
|
|||
return self._authors;
|
||||
};
|
||||
|
||||
PadDiff.prototype._extendChangesetWithAuthor = function (changeset, author, apool) {
|
||||
PadDiff.prototype._extendChangesetWithAuthor = (changeset, author, apool) => {
|
||||
// unpack
|
||||
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);
|
||||
};
|
||||
|
||||
// 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) {
|
||||
const lines = Changeset.splitTextLines(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 include final newlines on lines.
|
||||
|
||||
function lines_get(idx) {
|
||||
const linesGet = (idx) => {
|
||||
if (lines.get) {
|
||||
return lines.get(idx);
|
||||
} else {
|
||||
return lines[idx];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function alines_get(idx) {
|
||||
const aLinesGet = (idx) => {
|
||||
if (alines.get) {
|
||||
return alines.get(idx);
|
||||
} else {
|
||||
return alines[idx];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let curLine = 0;
|
||||
let curChar = 0;
|
||||
|
@ -280,10 +283,10 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
|
|||
const csIter = Changeset.opIterator(unpacked.ops);
|
||||
const builder = Changeset.builder(unpacked.newLen);
|
||||
|
||||
function consumeAttribRuns(numChars, func /* (len, attribs, endsLine)*/) {
|
||||
if ((!curLineOpIter) || (curLineOpIterLine != curLine)) {
|
||||
const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => {
|
||||
if ((!curLineOpIter) || (curLineOpIterLine !== curLine)) {
|
||||
// create curLineOpIter and advance it to curChar
|
||||
curLineOpIter = Changeset.opIterator(alines_get(curLine));
|
||||
curLineOpIter = Changeset.opIterator(aLinesGet(curLine));
|
||||
curLineOpIterLine = curLine;
|
||||
let indexIntoLine = 0;
|
||||
let done = false;
|
||||
|
@ -304,7 +307,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
|
|||
curChar = 0;
|
||||
curLineOpIterLine = curLine;
|
||||
curLineNextOp.chars = 0;
|
||||
curLineOpIter = Changeset.opIterator(alines_get(curLine));
|
||||
curLineOpIter = Changeset.opIterator(aLinesGet(curLine));
|
||||
}
|
||||
|
||||
if (!curLineNextOp.chars) {
|
||||
|
@ -313,7 +316,8 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
|
|||
|
||||
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;
|
||||
curLineNextOp.chars -= charsToUse;
|
||||
curChar += charsToUse;
|
||||
|
@ -323,64 +327,66 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
|
|||
curLine++;
|
||||
curChar = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function skip(N, L) {
|
||||
const skip = (N, L) => {
|
||||
if (L) {
|
||||
curLine += L;
|
||||
curChar = 0;
|
||||
} else if (curLineOpIter && curLineOpIterLine == curLine) {
|
||||
} else if (curLineOpIter && curLineOpIterLine === curLine) {
|
||||
consumeAttribRuns(N, () => {});
|
||||
} else {
|
||||
curChar += N;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function nextText(numChars) {
|
||||
const nextText = (numChars) => {
|
||||
let len = 0;
|
||||
const assem = Changeset.stringAssembler();
|
||||
const firstString = lines_get(curLine).substring(curChar);
|
||||
const firstString = linesGet(curLine).substring(curChar);
|
||||
len += firstString.length;
|
||||
assem.append(firstString);
|
||||
|
||||
let lineNum = curLine + 1;
|
||||
|
||||
while (len < numChars) {
|
||||
const nextString = lines_get(lineNum);
|
||||
const nextString = linesGet(lineNum);
|
||||
len += nextString.length;
|
||||
assem.append(nextString);
|
||||
lineNum++;
|
||||
}
|
||||
|
||||
return assem.toString().substring(0, numChars);
|
||||
}
|
||||
};
|
||||
|
||||
function cachedStrFunc(func) {
|
||||
const cachedStrFunc = (func) => {
|
||||
const cache = {};
|
||||
|
||||
return function (s) {
|
||||
return (s) => {
|
||||
if (!cache[s]) {
|
||||
cache[s] = func(s);
|
||||
}
|
||||
return cache[s];
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const attribKeys = [];
|
||||
const attribValues = [];
|
||||
|
||||
// iterate over all operators of this changeset
|
||||
while (csIter.hasNext()) {
|
||||
var csOp = csIter.next();
|
||||
const csOp = csIter.next();
|
||||
|
||||
if (csOp.opcode == '=') {
|
||||
var textBank = nextText(csOp.chars);
|
||||
if (csOp.opcode === '=') {
|
||||
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.
|
||||
// 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 != '*') {
|
||||
// decide if this equal operator is an attribution change or not.
|
||||
// We can see this by checkinf if attribs is set.
|
||||
// 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]);
|
||||
var authorAttrib = apool.putAttrib(['author', '']);
|
||||
let authorAttrib = apool.putAttrib(['author', '']);
|
||||
|
||||
attribKeys.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 = [];
|
||||
for (let i = 0; i < attribKeys.length; i++) {
|
||||
const appliedKey = attribKeys[i];
|
||||
const appliedValue = attribValues[i];
|
||||
const oldValue = Changeset.attribsAttributeValue(attribs, appliedKey, apool);
|
||||
|
||||
if (appliedValue != oldValue) {
|
||||
if (appliedValue !== oldValue) {
|
||||
backAttribs.push([appliedKey, oldValue]);
|
||||
}
|
||||
}
|
||||
|
@ -408,7 +414,8 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, 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;
|
||||
|
||||
|
@ -427,7 +434,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
|
|||
}
|
||||
|
||||
// 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);
|
||||
|
||||
|
@ -437,13 +444,14 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
|
|||
// consume the attributes of this linebreak
|
||||
consumeAttribRuns(1, () => {});
|
||||
} else {
|
||||
// add the old text via an insert, but add a deletion attribute + the author attribute of the author who deleted it
|
||||
var textBankIndex = 0;
|
||||
// add the old text via an insert, but add a deletion attribute +
|
||||
// the author attribute of the author who deleted it
|
||||
let textBankIndex = 0;
|
||||
consumeAttribRuns(lengthToProcess, (len, attribs, endsLine) => {
|
||||
// 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;
|
||||
});
|
||||
|
||||
|
@ -454,11 +462,11 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
|
|||
skip(csOp.chars, csOp.lines);
|
||||
builder.keep(csOp.chars, csOp.lines);
|
||||
}
|
||||
} else if (csOp.opcode == '+') {
|
||||
} else if (csOp.opcode === '+') {
|
||||
builder.keep(csOp.chars, csOp.lines);
|
||||
} else if (csOp.opcode == '-') {
|
||||
var textBank = nextText(csOp.chars);
|
||||
var textBankIndex = 0;
|
||||
} else if (csOp.opcode === '-') {
|
||||
const textBank = nextText(csOp.chars);
|
||||
let textBankIndex = 0;
|
||||
|
||||
consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => {
|
||||
builder.insert(textBank.substr(textBankIndex, len), attribs + csOp.attribs);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'use strict';
|
||||
const fs = require('fs');
|
||||
|
||||
const check = function (path) {
|
||||
const check = (path) => {
|
||||
const existsSync = fs.statSync || fs.existsSync || path.existsSync;
|
||||
|
||||
let result;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Helpers to manipulate promises (like async but for promises).
|
||||
*/
|
||||
|
|
|
@ -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 randomString = function (len) {
|
||||
return crypto.randomBytes(len).toString('hex');
|
||||
};
|
||||
const randomString = (len) => crypto.randomBytes(len).toString('hex');
|
||||
|
||||
module.exports = randomString;
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
, "gritter.js"
|
||||
, "$js-cookie/src/js.cookie.js"
|
||||
, "$tinycon/tinycon.js"
|
||||
, "excanvas.js"
|
||||
, "farbtastic.js"
|
||||
, "skin_variants.js"
|
||||
, "socketio.js"
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
'use strict';
|
||||
/**
|
||||
* The Toolbar Module creates and renders the toolbars and buttons
|
||||
*/
|
||||
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;
|
||||
while ((ax = array.indexOf(what)) !== -1) {
|
||||
array.splice(ax, 1);
|
||||
|
@ -18,15 +12,13 @@ removeItem = function (array, what) {
|
|||
return array;
|
||||
};
|
||||
|
||||
defaultButtonAttributes = function (name, overrides) {
|
||||
return {
|
||||
command: name,
|
||||
localizationId: `pad.toolbar.${name}.title`,
|
||||
class: `buttonicon buttonicon-${name}`,
|
||||
};
|
||||
};
|
||||
const defaultButtonAttributes = (name, overrides) => ({
|
||||
command: name,
|
||||
localizationId: `pad.toolbar.${name}.title`,
|
||||
class: `buttonicon buttonicon-${name}`,
|
||||
});
|
||||
|
||||
tag = function (name, attributes, contents) {
|
||||
const tag = (name, attributes, contents) => {
|
||||
const aStr = tagAttributes(attributes);
|
||||
|
||||
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) => {
|
||||
if (!_.isUndefined(val)) {
|
||||
o[name] = val;
|
||||
|
@ -47,7 +39,7 @@ tagAttributes = function (attributes) {
|
|||
return ` ${_.map(attributes, (val, name) => `${name}="${_.escape(val)}"`).join(' ')}`;
|
||||
};
|
||||
|
||||
ButtonsGroup = function () {
|
||||
const ButtonsGroup = function () {
|
||||
this.buttons = [];
|
||||
};
|
||||
|
||||
|
@ -65,9 +57,9 @@ ButtonsGroup.prototype.addButton = function (button) {
|
|||
};
|
||||
|
||||
ButtonsGroup.prototype.render = function () {
|
||||
if (this.buttons && this.buttons.length == 1) {
|
||||
if (this.buttons && this.buttons.length === 1) {
|
||||
this.buttons[0].grouping = '';
|
||||
} else {
|
||||
} else if (this.buttons && this.buttons.length > 1) {
|
||||
_.first(this.buttons).grouping = 'grouped-left';
|
||||
_.last(this.buttons).grouping = 'grouped-right';
|
||||
_.each(this.buttons.slice(1, -1), (btn) => {
|
||||
|
@ -80,11 +72,11 @@ ButtonsGroup.prototype.render = function () {
|
|||
}).join('\n');
|
||||
};
|
||||
|
||||
Button = function (attributes) {
|
||||
const Button = function (attributes) {
|
||||
this.attributes = attributes;
|
||||
};
|
||||
|
||||
Button.load = function (btnName) {
|
||||
Button.load = (btnName) => {
|
||||
const button = module.exports.availableButtons[btnName];
|
||||
try {
|
||||
if (button.constructor === Button || button.constructor === SelectButton) {
|
||||
|
@ -108,14 +100,17 @@ _.extend(Button.prototype, {
|
|||
};
|
||||
return tag('li', liAttributes,
|
||||
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.options = [];
|
||||
};
|
||||
|
@ -155,7 +150,7 @@ _.extend(SelectButton.prototype, Button.prototype, {
|
|||
},
|
||||
});
|
||||
|
||||
Separator = function () {};
|
||||
const Separator = function () {};
|
||||
Separator.prototype.render = function () {
|
||||
return tag('li', {class: 'separator'});
|
||||
};
|
||||
|
@ -235,15 +230,11 @@ module.exports = {
|
|||
this.availableButtons[buttonName] = buttonInfo;
|
||||
},
|
||||
|
||||
button(attributes) {
|
||||
return new Button(attributes);
|
||||
},
|
||||
separator() {
|
||||
return (new Separator()).render();
|
||||
},
|
||||
selectButton(attributes) {
|
||||
return new SelectButton(attributes);
|
||||
},
|
||||
button: (attributes) => new Button(attributes),
|
||||
|
||||
separator: () => (new Separator()).render(),
|
||||
|
||||
selectButton: (attributes) => new SelectButton(attributes),
|
||||
|
||||
/*
|
||||
* 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
|
||||
* 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');
|
||||
}
|
||||
}
|
||||
|
|
2150
src/package-lock.json
generated
2150
src/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -65,31 +65,31 @@
|
|||
"security": "1.0.0",
|
||||
"semver": "5.6.0",
|
||||
"slide": "1.1.6",
|
||||
"socket.io": "^2.3.0",
|
||||
"socket.io": "^2.4.1",
|
||||
"terser": "^4.7.0",
|
||||
"threads": "^1.4.0",
|
||||
"tiny-worker": "^2.3.0",
|
||||
"tinycon": "0.0.1",
|
||||
"ueberdb2": "^1.2.3",
|
||||
"ueberdb2": "^1.2.5",
|
||||
"underscore": "1.8.3",
|
||||
"unorm": "1.4.1"
|
||||
"unorm": "1.4.1",
|
||||
"wtfnode": "^0.8.4"
|
||||
},
|
||||
"bin": {
|
||||
"etherpad-lite": "node/server.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.15.0",
|
||||
"eslint-config-etherpad": "^1.0.20",
|
||||
"eslint": "^7.18.0",
|
||||
"eslint-config-etherpad": "^1.0.24",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-mocha": "^8.0.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-you-dont-need-lodash-underscore": "^6.10.0",
|
||||
"etherpad-cli-client": "0.0.9",
|
||||
"mocha": "7.1.2",
|
||||
"mocha-froth": "^0.2.10",
|
||||
"nyc": "15.0.1",
|
||||
"set-cookie-parser": "^2.4.6",
|
||||
"sinon": "^9.2.0",
|
||||
"superagent": "^3.8.3",
|
||||
|
@ -101,7 +101,6 @@
|
|||
"/static/js/admin/jquery.autosize.js",
|
||||
"/static/js/admin/minify.json.js",
|
||||
"/static/js/browser.js",
|
||||
"/static/js/excanvas.js",
|
||||
"/static/js/farbtastic.js",
|
||||
"/static/js/gritter.js",
|
||||
"/static/js/html10n.js",
|
||||
|
@ -140,7 +139,7 @@
|
|||
"root": true
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0",
|
||||
"node": "^10.17.0 || >=11.14.0",
|
||||
"npm": ">=5.5.1"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -149,8 +148,8 @@
|
|||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"test": "nyc mocha --timeout 30000 --recursive ../tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
|
||||
"test-container": "nyc mocha --timeout 5000 ../tests/container/specs/api"
|
||||
"test": "mocha --timeout 120000 --recursive ../tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
|
||||
"test-container": "mocha --timeout 5000 ../tests/container/specs/api"
|
||||
},
|
||||
"version": "1.8.7",
|
||||
"license": "Apache-2.0"
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const Changeset = require('./Changeset');
|
||||
const ChangesetUtils = require('./ChangesetUtils');
|
||||
const _ = require('./underscore');
|
||||
|
@ -72,10 +74,12 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
|||
|
||||
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
|
||||
if (allChangesets) {
|
||||
allChangesets = Changeset.compose(allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
|
||||
allChangesets = Changeset.compose(
|
||||
allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
|
||||
} else {
|
||||
allChangesets = rowChangeset;
|
||||
}
|
||||
|
@ -118,7 +122,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
|||
_setAttributesOnRangeByLine(row, startCol, endCol, attribs) {
|
||||
const builder = Changeset.builder(this.rep.lines.totalWidth());
|
||||
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;
|
||||
},
|
||||
|
||||
|
@ -127,9 +132,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
|||
@param lineNum: the number of the line
|
||||
*/
|
||||
lineHasMarker(lineNum) {
|
||||
const that = this;
|
||||
|
||||
return _.find(lineAttributes, (attribute) => that.getAttributeOnLine(lineNum, attribute) != '') !== undefined;
|
||||
return lineAttributes.find(
|
||||
(attribute) => this.getAttributeOnLine(lineNum, attribute) !== '') !== undefined;
|
||||
},
|
||||
|
||||
/*
|
||||
|
@ -184,7 +188,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
|||
if (!(rep.selStart && rep.selEnd)) return;
|
||||
// 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?
|
||||
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 (prevChar) {
|
||||
// If it's not the start of the line
|
||||
|
@ -198,21 +202,18 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
|||
[attributeName, 'true'],
|
||||
], rep.apool);
|
||||
const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`);
|
||||
function hasIt(attribs) {
|
||||
return withItRegex.test(attribs);
|
||||
}
|
||||
const hasIt = (attribs) => withItRegex.test(attribs);
|
||||
|
||||
return rangeHasAttrib(rep.selStart, rep.selEnd);
|
||||
|
||||
function rangeHasAttrib(selStart, selEnd) {
|
||||
const rangeHasAttrib = (selStart, selEnd) => {
|
||||
// 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
|
||||
var hasAttrib = true;
|
||||
if (selStart[0] !== selEnd[0]) { // -> More than one line selected
|
||||
let hasAttrib = true;
|
||||
|
||||
// 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 (let n = selStart[0] + 1; n < selEnd[0]; n++) {
|
||||
|
@ -230,7 +231,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
|||
const lineNum = selStart[0];
|
||||
const start = selStart[1];
|
||||
const end = selEnd[1];
|
||||
var hasAttrib = true;
|
||||
let hasAttrib = true;
|
||||
|
||||
// Iterate over attribs on this line
|
||||
|
||||
|
@ -244,7 +245,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
|||
if (!hasIt(op.attribs)) {
|
||||
// does op overlap selection?
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -252,7 +254,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
|||
}
|
||||
|
||||
return hasAttrib;
|
||||
}
|
||||
};
|
||||
return rangeHasAttrib(rep.selStart, rep.selEnd);
|
||||
},
|
||||
|
||||
/*
|
||||
|
@ -349,13 +352,13 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
|||
const hasMarker = this.lineHasMarker(lineNum);
|
||||
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)) {
|
||||
found = true;
|
||||
return [attributeName, ''];
|
||||
return [attrib[0], ''];
|
||||
} else if (attrib[0] === 'author') {
|
||||
// update last author to make changes to line attributes on this line
|
||||
return [attributeName, this.author];
|
||||
return [attrib[0], this.author];
|
||||
}
|
||||
return attrib;
|
||||
});
|
||||
|
@ -373,7 +376,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
|||
if (hasMarker && !countAttribsWithMarker) {
|
||||
ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
|
||||
} 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);
|
||||
|
@ -394,7 +398,9 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
|||
},
|
||||
|
||||
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;
|
||||
if (hasSelection) {
|
||||
hasAttrib = this.getAttributeOnSelection(attributeName);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -22,33 +24,23 @@
|
|||
|
||||
const Security = require('./security');
|
||||
|
||||
function isNodeText(node) {
|
||||
return (node.nodeType == 3);
|
||||
}
|
||||
const isNodeText = (node) => (node.nodeType === 3);
|
||||
|
||||
function object(o) {
|
||||
const f = function () {};
|
||||
f.prototype = o;
|
||||
return new f();
|
||||
}
|
||||
const getAssoc = (obj, name) => obj[`_magicdom_${name}`];
|
||||
|
||||
function getAssoc(obj, name) {
|
||||
return obj[`_magicdom_${name}`];
|
||||
}
|
||||
|
||||
function setAssoc(obj, name, value) {
|
||||
const setAssoc = (obj, name, value) => {
|
||||
// note that in IE designMode, properties of a node can get
|
||||
// copied to new nodes that are spawned during editing; also,
|
||||
// properties representable in HTML text can survive copy-and-paste
|
||||
obj[`_magicdom_${name}`] = value;
|
||||
}
|
||||
};
|
||||
|
||||
// "func" is a function over 0..(numItems-1) that is monotonically
|
||||
// "increasing" with index (false, then true). Finds the boundary
|
||||
// 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 (func(0)) return 0;
|
||||
if (!func(numItems - 1)) return numItems;
|
||||
|
@ -60,22 +52,19 @@ function binarySearch(numItems, func) {
|
|||
else low = x;
|
||||
}
|
||||
return high;
|
||||
}
|
||||
};
|
||||
|
||||
function binarySearchInfinite(expectedLength, func) {
|
||||
const binarySearchInfinite = (expectedLength, func) => {
|
||||
let i = 0;
|
||||
while (!func(i)) i += expectedLength;
|
||||
return binarySearch(i, func);
|
||||
}
|
||||
};
|
||||
|
||||
function htmlPrettyEscape(str) {
|
||||
return Security.escapeHTML(str).replace(/\r?\n/g, '\\n');
|
||||
}
|
||||
const htmlPrettyEscape = (str) => Security.escapeHTML(str).replace(/\r?\n/g, '\\n');
|
||||
|
||||
const noop = function () {};
|
||||
const noop = () => {};
|
||||
|
||||
exports.isNodeText = isNodeText;
|
||||
exports.object = object;
|
||||
exports.getAssoc = getAssoc;
|
||||
exports.setAssoc = setAssoc;
|
||||
exports.binarySearch = binarySearch;
|
||||
|
|
|
@ -1139,7 +1139,7 @@ function Ace2Inner() {
|
|||
|
||||
lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode);
|
||||
if (firstDirtyNode && lastDirtyNode) {
|
||||
const cc = makeContentCollector(isStyled, browser, rep.apool, null, className2Author);
|
||||
const cc = makeContentCollector(isStyled, browser, rep.apool, className2Author);
|
||||
cc.notifySelection(selection);
|
||||
const dirtyNodes = [];
|
||||
for (let n = firstDirtyNode; n &&
|
||||
|
|
|
@ -32,30 +32,31 @@ const hooks = require('./pluginfw/hooks');
|
|||
|
||||
const sanitizeUnicode = (s) => UNorm.nfc(s);
|
||||
|
||||
const makeContentCollector = (collectStyles, abrowser, apool, domInterface, className2Author) => {
|
||||
const dom = domInterface || {
|
||||
isNodeText: (n) => n.nodeType === 3,
|
||||
nodeTagName: (n) => n.tagName,
|
||||
nodeValue: (n) => n.nodeValue,
|
||||
nodeNumChildren: (n) => {
|
||||
if (n.childNodes == null) return 0;
|
||||
return n.childNodes.length;
|
||||
},
|
||||
nodeChild: (n, i) => {
|
||||
if (n.childNodes.item == null) {
|
||||
return n.childNodes[i];
|
||||
}
|
||||
return n.childNodes.item(i);
|
||||
},
|
||||
nodeProp: (n, p) => n[p],
|
||||
nodeAttr: (n, a) => {
|
||||
if (n.getAttribute != null) return n.getAttribute(a);
|
||||
if (n.attribs != null) return n.attribs[a];
|
||||
return null;
|
||||
},
|
||||
optNodeInnerHTML: (n) => n.innerHTML,
|
||||
};
|
||||
// This file is used both in browsers and with cheerio in Node.js (for importing HTML). Cheerio's
|
||||
// Node-like objects are not 100% API compatible with the DOM specification; the following functions
|
||||
// abstract away the differences.
|
||||
|
||||
// .nodeType works with DOM and cheerio 0.22.0, but cheerio 0.22.0 does not provide the Node.*_NODE
|
||||
// constants so they cannot be used here.
|
||||
const isElementNode = (n) => n.nodeType === 1; // Node.ELEMENT_NODE
|
||||
const isTextNode = (n) => n.nodeType === 3; // Node.TEXT_NODE
|
||||
// .tagName works with DOM and cheerio 0.22.0, but:
|
||||
// * With DOM, .tagName is an uppercase string.
|
||||
// * With cheerio 0.22.0, .tagName is a lowercase string.
|
||||
// For consistency, this function always returns a lowercase string.
|
||||
const tagName = (n) => n.tagName && n.tagName.toLowerCase();
|
||||
// .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).
|
||||
const childNodes = (n) => n.childNodes || [];
|
||||
const getAttribute = (n, a) => {
|
||||
// .getAttribute() works with DOM but not with cheerio 0.22.0.
|
||||
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];
|
||||
return null;
|
||||
};
|
||||
|
||||
const makeContentCollector = (collectStyles, abrowser, apool, className2Author) => {
|
||||
const _blockElems = {
|
||||
div: 1,
|
||||
p: 1,
|
||||
|
@ -67,7 +68,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
|
|||
_blockElems[element] = 1;
|
||||
});
|
||||
|
||||
const isBlockElement = (n) => !!_blockElems[(dom.nodeTagName(n) || '').toLowerCase()];
|
||||
const isBlockElement = (n) => !!_blockElems[tagName(n) || ''];
|
||||
|
||||
const textify = (str) => sanitizeUnicode(
|
||||
str.replace(/(\n | \n)/g, ' ')
|
||||
|
@ -75,7 +76,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
|
|||
.replace(/\xa0/g, ' ')
|
||||
.replace(/\t/g, ' '));
|
||||
|
||||
const getAssoc = (node, name) => dom.nodeProp(node, `_magicdom_${name}`);
|
||||
const getAssoc = (node, name) => node[`_magicdom_${name}`];
|
||||
|
||||
const lines = (() => {
|
||||
const textArray = [];
|
||||
|
@ -123,13 +124,17 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
|
|||
let selEnd = [-1, -1];
|
||||
const _isEmpty = (node, state) => {
|
||||
// consider clean blank lines pasted in IE to be empty
|
||||
if (dom.nodeNumChildren(node) === 0) return true;
|
||||
if (dom.nodeNumChildren(node) === 1 &&
|
||||
if (childNodes(node).length === 0) return true;
|
||||
if (childNodes(node).length === 1 &&
|
||||
getAssoc(node, 'shouldBeEmpty') &&
|
||||
dom.optNodeInnerHTML(node) === ' ' &&
|
||||
// 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 === ' ' &&
|
||||
!getAssoc(node, 'unpasted')) {
|
||||
if (state) {
|
||||
const child = dom.nodeChild(node, 0);
|
||||
const child = childNodes(node)[0];
|
||||
_reachPoint(child, 0, state);
|
||||
_reachPoint(child, 1, state);
|
||||
}
|
||||
|
@ -149,7 +154,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
|
|||
};
|
||||
|
||||
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) => {
|
||||
|
@ -228,25 +233,24 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
|
|||
|
||||
const _recalcAttribString = (state) => {
|
||||
const lst = [];
|
||||
for (const a in state.attribs) {
|
||||
if (state.attribs[a]) {
|
||||
// The following splitting of the attribute name is a workaround
|
||||
// to enable the content collector to store key-value attributes
|
||||
// see https://github.com/ether/etherpad-lite/issues/2567 for more information
|
||||
// in long term the contentcollector should be refactored to get rid of this workaround
|
||||
const ATTRIBUTE_SPLIT_STRING = '::';
|
||||
for (const [a, count] of Object.entries(state.attribs)) {
|
||||
if (!count) continue;
|
||||
// The following splitting of the attribute name is a workaround
|
||||
// to enable the content collector to store key-value attributes
|
||||
// see https://github.com/ether/etherpad-lite/issues/2567 for more information
|
||||
// in long term the contentcollector should be refactored to get rid of this workaround
|
||||
const ATTRIBUTE_SPLIT_STRING = '::';
|
||||
|
||||
// see if attributeString is splittable
|
||||
const attributeSplits = a.split(ATTRIBUTE_SPLIT_STRING);
|
||||
if (attributeSplits.length > 1) {
|
||||
// the attribute name follows the convention key::value
|
||||
// so save it as a key value attribute
|
||||
lst.push([attributeSplits[0], attributeSplits[1]]);
|
||||
} else {
|
||||
// the "normal" case, the attribute is just a switch
|
||||
// so set it true
|
||||
lst.push([a, 'true']);
|
||||
}
|
||||
// see if attributeString is splittable
|
||||
const attributeSplits = a.split(ATTRIBUTE_SPLIT_STRING);
|
||||
if (attributeSplits.length > 1) {
|
||||
// the attribute name follows the convention key::value
|
||||
// so save it as a key value attribute
|
||||
lst.push([attributeSplits[0], attributeSplits[1]]);
|
||||
} else {
|
||||
// the "normal" case, the attribute is just a switch
|
||||
// so set it true
|
||||
lst.push([a, 'true']);
|
||||
}
|
||||
}
|
||||
if (state.authorLevel > 0) {
|
||||
|
@ -316,25 +320,15 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
|
|||
const startLine = lines.length() - 1;
|
||||
_reachBlockPoint(node, 0, state);
|
||||
|
||||
if (dom.isNodeText(node)) {
|
||||
let txt = dom.nodeValue(node);
|
||||
const tname = dom.nodeAttr(node.parentNode, 'name');
|
||||
|
||||
const txtFromHook = hooks.callAll('collectContentLineText', {
|
||||
cc: this,
|
||||
state,
|
||||
tname,
|
||||
node,
|
||||
text: txt,
|
||||
styl: null,
|
||||
cls: null,
|
||||
});
|
||||
|
||||
if (typeof (txtFromHook) === 'object') {
|
||||
txt = dom.nodeValue(node);
|
||||
} else if (txtFromHook) {
|
||||
txt = txtFromHook;
|
||||
}
|
||||
if (isTextNode(node)) {
|
||||
const tname = getAttribute(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
|
||||
// function modifies context.text then all returned strings are ignored. If no hook functions
|
||||
// modify context.text, the first hook function to return a string wins.
|
||||
const [hookTxt] =
|
||||
hooks.callAll('collectContentLineText', context).filter((s) => typeof s === 'string');
|
||||
let txt = context.text === node.nodeValue && hookTxt != null ? hookTxt : context.text;
|
||||
|
||||
let rest = '';
|
||||
let x = 0; // offset into original text
|
||||
|
@ -384,8 +378,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
|
|||
cc.startNewLine(state);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const tname = (dom.nodeTagName(node) || '').toLowerCase();
|
||||
} else if (isElementNode(node)) {
|
||||
const tname = tagName(node) || '';
|
||||
|
||||
if (tname === 'img') {
|
||||
hooks.callAll('collectContentImage', {
|
||||
|
@ -403,8 +397,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
|
|||
|
||||
if (tname === 'br') {
|
||||
this.breakLine = true;
|
||||
const tvalue = dom.nodeAttr(node, 'value');
|
||||
const induceLineBreak = hooks.callAll('collectContentLineBreak', {
|
||||
const tvalue = getAttribute(node, 'value');
|
||||
const [startNewLine = true] = hooks.callAll('collectContentLineBreak', {
|
||||
cc: this,
|
||||
state,
|
||||
tname,
|
||||
|
@ -412,17 +406,14 @@ const makeContentCollector = (collectStyles, abrowser, apool, domInterface, clas
|
|||
styl: null,
|
||||
cls: null,
|
||||
});
|
||||
const startNewLine = (
|
||||
typeof (induceLineBreak) === 'object' &&
|
||||
induceLineBreak.length === 0) ? true : induceLineBreak[0];
|
||||
if (startNewLine) {
|
||||
cc.startNewLine(state);
|
||||
}
|
||||
} else if (tname === 'script' || tname === 'style') {
|
||||
// ignore
|
||||
} else if (!isEmpty) {
|
||||
let styl = dom.nodeAttr(node, 'style');
|
||||
let cls = dom.nodeAttr(node, 'class');
|
||||
let styl = getAttribute(node, 'style');
|
||||
let cls = getAttribute(node, 'class');
|
||||
let isPre = (tname === 'pre');
|
||||
if ((!isPre) && abrowser && abrowser.safari) {
|
||||
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');
|
||||
}
|
||||
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);
|
||||
// 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
|
||||
if (!rr && !type) {
|
||||
for (const i in node.children) {
|
||||
if (node.children[i] && node.children[i].name === 'ul') {
|
||||
type = node.children[i].attribs.class;
|
||||
if (type) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const child of childNodes(node)) {
|
||||
if (tagName(child) !== 'ul') continue;
|
||||
type = getAttribute(child, 'class');
|
||||
if (type) break;
|
||||
}
|
||||
}
|
||||
if (rr && rr[1]) {
|
||||
type = rr[1];
|
||||
} else {
|
||||
if (tname === 'ul') {
|
||||
if ((type && type.match('indent')) ||
|
||||
(node.attribs && node.attribs.class && node.attribs.class.match('indent'))) {
|
||||
const cls = getAttribute(node, 'class');
|
||||
if ((type && type.match('indent')) || (cls && cls.match('indent'))) {
|
||||
type = 'indent';
|
||||
} else {
|
||||
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.
|
||||
// See https://github.com/ether/etherpad-lite/issues/2412 for reasoning
|
||||
if (!abrowser.chrome) oldListTypeOrNull = (_enterList(state, undefined) || 'none');
|
||||
} else if ((tname === 'li')) {
|
||||
} else if (tname === 'li') {
|
||||
state.lineAttributes.start = state.start || 0;
|
||||
_recalcAttribString(state);
|
||||
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>
|
||||
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)
|
||||
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.
|
||||
if (node.parent && node.parent.name === 'ul') {
|
||||
if (node.parentNode && tagName(node.parentNode) === 'ul') {
|
||||
state.start++;
|
||||
// TODO, this is hacky.
|
||||
// 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 (let i = 0; i < nc; i++) {
|
||||
const c = dom.nodeChild(node, i);
|
||||
for (const c of childNodes(node)) {
|
||||
cc.collectContent(c, state);
|
||||
}
|
||||
|
||||
|
|
|
@ -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})();
|
|
@ -1,9 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
// 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
|
||||
(function ($) {
|
||||
|
||||
|
||||
var __debug = false;
|
||||
var __factor = 0.8;
|
||||
|
||||
|
@ -19,7 +21,7 @@ $.farbtastic = function (container, options) {
|
|||
|
||||
$._farbtastic = function (container, options) {
|
||||
var fb = this;
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
|
@ -109,15 +101,27 @@ $._farbtastic = function (container, options) {
|
|||
.html(
|
||||
'<div class="farbtastic" style="position: relative">' +
|
||||
'<div class="farbtastic-solid"></div>' +
|
||||
'<canvas class="farbtastic-mask"></canvas>' +
|
||||
'<canvas class="farbtastic-overlay"></canvas>' +
|
||||
'</div>'
|
||||
)
|
||||
.children('.farbtastic')
|
||||
.append(fb._makeCanvas('farbtastic-mask'))
|
||||
.append(fb._makeCanvas('farbtastic-overlay'))
|
||||
.end()
|
||||
.find('*').attr(dim).css(dim).end()
|
||||
.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
|
||||
fb.radius = (options.width - options.wheelWidth) / 2 - 1;
|
||||
fb.square = Math.floor((fb.radius - options.wheelWidth / 2) * 0.7) - 1;
|
||||
|
@ -137,7 +141,7 @@ $._farbtastic = function (container, options) {
|
|||
fb.ctxOverlay = fb.cnvOverlay[0].getContext('2d');
|
||||
fb.ctxMask.translate(fb.mid, fb.mid);
|
||||
fb.ctxOverlay.translate(fb.mid, fb.mid);
|
||||
|
||||
|
||||
// Draw widget base layers.
|
||||
fb.drawCircle();
|
||||
fb.drawMask();
|
||||
|
@ -160,12 +164,12 @@ $._farbtastic = function (container, options) {
|
|||
m.lineWidth = w / r;
|
||||
m.scale(r, r);
|
||||
// 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,
|
||||
angle2 = d2 * Math.PI * 2,
|
||||
// Endpoints
|
||||
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.
|
||||
am = (angle1 + angle2) / 2,
|
||||
tan = 1 / Math.cos((angle2 - angle1) / 2),
|
||||
|
@ -173,16 +177,37 @@ $._farbtastic = function (container, options) {
|
|||
// New color
|
||||
color2 = fb.pack(fb.HSLToRGB([d2, 1, 0.5]));
|
||||
if (i > 0) {
|
||||
// Create gradient fill between the endpoints.
|
||||
var grad = m.createLinearGradient(x1, y1, x2, y2);
|
||||
grad.addColorStop(0, color1);
|
||||
grad.addColorStop(1, color2);
|
||||
m.strokeStyle = grad;
|
||||
// Draw quadratic curve segment.
|
||||
m.beginPath();
|
||||
m.moveTo(x1, y1);
|
||||
m.quadraticCurveTo(xm, ym, x2, y2);
|
||||
m.stroke();
|
||||
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.
|
||||
var grad = m.createLinearGradient(x1, y1, x2, y2);
|
||||
grad.addColorStop(0, color1);
|
||||
grad.addColorStop(1, color2);
|
||||
m.strokeStyle = grad;
|
||||
// Draw quadratic curve segment.
|
||||
m.beginPath();
|
||||
m.moveTo(x1, y1);
|
||||
m.quadraticCurveTo(xm, ym, x2, y2);
|
||||
m.stroke();
|
||||
}
|
||||
}
|
||||
// Prevent seams where curves join.
|
||||
angle1 = angle2 - nudge; color1 = color2; d1 = d2;
|
||||
|
@ -190,7 +215,7 @@ $._farbtastic = function (container, options) {
|
|||
m.restore();
|
||||
__debug && $('body').append('<div>drawCircle '+ (+(new Date()) - tm) +'ms');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Draw the saturation/luminance mask.
|
||||
*/
|
||||
|
@ -214,9 +239,9 @@ $._farbtastic = function (container, options) {
|
|||
|
||||
outputPixel(x, y, c, a);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Method #1: direct pixel access (new Canvas).
|
||||
if (fb.ctxMask.getImageData) {
|
||||
// Create half-resolution buffer.
|
||||
|
@ -226,7 +251,7 @@ $._farbtastic = function (container, options) {
|
|||
var ctx = buffer.getContext('2d');
|
||||
var frame = ctx.getImageData(0, 0, sz + 1, sz + 1);
|
||||
|
||||
let i = 0;
|
||||
var i = 0;
|
||||
calculateMask(sz, sz, function (x, y, c, a) {
|
||||
frame.data[i++] = frame.data[i++] = frame.data[i++] = c * 255;
|
||||
frame.data[i++] = a * 255;
|
||||
|
@ -277,7 +302,7 @@ $._farbtastic = function (container, options) {
|
|||
}
|
||||
cache.push([c, a]);
|
||||
});
|
||||
}
|
||||
}
|
||||
__debug && $('body').append('<div>drawMask '+ (+(new Date()) - tm) +'ms');
|
||||
}
|
||||
|
||||
|
@ -301,7 +326,7 @@ $._farbtastic = function (container, options) {
|
|||
|
||||
// Update the overlay canvas.
|
||||
fb.ctxOverlay.clearRect(-fb.mid, -fb.mid, sz, sz);
|
||||
for (let i in circles) {
|
||||
for (i in circles) {
|
||||
var c = circles[i];
|
||||
fb.ctxOverlay.lineWidth = c.lw;
|
||||
fb.ctxOverlay.strokeStyle = c.c;
|
||||
|
@ -323,7 +348,7 @@ $._farbtastic = function (container, options) {
|
|||
|
||||
// Draw markers
|
||||
fb.drawMarkers();
|
||||
|
||||
|
||||
// Linked elements or callback
|
||||
if (typeof fb.callback == 'object') {
|
||||
// Set background/foreground color
|
||||
|
@ -343,15 +368,15 @@ $._farbtastic = function (container, options) {
|
|||
fb.callback.call(fb, fb.color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper for returning coordinates relative to the center.
|
||||
*/
|
||||
fb.widgetCoords = function (event) {
|
||||
return {
|
||||
x: event.pageX - fb.offset.left - fb.mid,
|
||||
x: event.pageX - fb.offset.left - fb.mid,
|
||||
y: event.pageY - fb.offset.top - fb.mid
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -414,7 +439,7 @@ $._farbtastic = function (container, options) {
|
|||
fb.packDX = function (c, a) {
|
||||
return '#' + fb.dec2hex(a) + fb.dec2hex(c) + fb.dec2hex(c) + fb.dec2hex(c);
|
||||
};
|
||||
|
||||
|
||||
fb.pack = function (rgb) {
|
||||
var r = Math.round(rgb[0] * 255);
|
||||
var g = Math.round(rgb[1] * 255);
|
||||
|
|
|
@ -93,11 +93,8 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
|
|||
} else if (linestylefilter.ATTRIB_CLASSES[key]) {
|
||||
classes += ` ${linestylefilter.ATTRIB_CLASSES[key]}`;
|
||||
} else {
|
||||
classes += hooks.callAllStr('aceAttribsToClasses', {
|
||||
linestylefilter,
|
||||
key,
|
||||
value,
|
||||
}, ' ', ' ', '');
|
||||
const results = hooks.callAll('aceAttribsToClasses', {linestylefilter, key, value});
|
||||
classes += ` ${results.join(' ')}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ let socket;
|
|||
// assigns to the global `$` and augments it with plugins.
|
||||
require('./jquery');
|
||||
require('./farbtastic');
|
||||
require('./excanvas');
|
||||
require('./gritter');
|
||||
|
||||
const Cookies = require('./pad_utils').Cookies;
|
||||
|
|
|
@ -113,7 +113,7 @@ const reconnectionTries = {
|
|||
|
||||
nextTry() {
|
||||
// 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++;
|
||||
|
||||
return nextCounterFactor;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/* global exports, require */
|
||||
'use strict';
|
||||
|
||||
const _ = require('underscore');
|
||||
const pluginDefs = require('./plugin_defs');
|
||||
|
||||
// Maps the name of a server-side hook to a string explaining the deprecation
|
||||
|
@ -15,66 +14,37 @@ exports.deprecationNotices = {};
|
|||
|
||||
const deprecationWarned = {};
|
||||
|
||||
function checkDeprecation(hook) {
|
||||
const checkDeprecation = (hook) => {
|
||||
const notice = exports.deprecationNotices[hook.hook_name];
|
||||
if (notice == null) return;
|
||||
if (deprecationWarned[hook.hook_fn_name]) return;
|
||||
console.warn(`${hook.hook_name} hook used by the ${hook.part.plugin} plugin ` +
|
||||
`(${hook.hook_fn_name}) is deprecated: ${notice}`);
|
||||
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) {
|
||||
return normalizedhook();
|
||||
} else {
|
||||
try {
|
||||
return normalizedhook();
|
||||
} catch (ex) {
|
||||
console.error([hook_name, hook.part.full_name, ex.stack || ex]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.syncMapFirst = function (lst, fn) {
|
||||
let i;
|
||||
let result;
|
||||
for (i = 0; i < lst.length; i++) {
|
||||
result = fn(lst[i]);
|
||||
if (result.length) return result;
|
||||
}
|
||||
return [];
|
||||
// Calls the node-style callback when the Promise settles. Unlike util.callbackify, this takes a
|
||||
// Promise (rather than a function that returns a Promise), and it returns a Promise (rather than a
|
||||
// function that returns undefined).
|
||||
const attachCallback = (p, cb) => p.then(
|
||||
(val) => cb(null, val),
|
||||
// Callbacks often only check the truthiness, not the nullness, of the first parameter. To avoid
|
||||
// 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.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();
|
||||
};
|
||||
// Flattens the array one level.
|
||||
const flatten1 = (array) => array.reduce((a, b) => a.concat(b), []);
|
||||
|
||||
// Calls the hook function synchronously and returns the value provided by the hook function (via
|
||||
// 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.
|
||||
//
|
||||
function callHookFnSync(hook, context) {
|
||||
const callHookFnSync = (hook, context) => {
|
||||
checkDeprecation(hook);
|
||||
|
||||
// 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
|
||||
// `undefined` as the resolved value.
|
||||
//
|
||||
// IMPORTANT: "Rest" parameters and default parameters are not counted in`Function.length`, so
|
||||
// the assumption does not hold for wrappers like `(...args) => { real(...args); }`. Such
|
||||
// functions will still work properly without any logged warnings or errors for now, but:
|
||||
// IMPORTANT: "Rest" parameters and default parameters are not included in `Function.length`,
|
||||
// so the assumption does not hold for wrappers such as:
|
||||
//
|
||||
// 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
|
||||
// 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.
|
||||
//
|
||||
// * The above "unsettled function" warning is not logged if the function fails to call the
|
||||
// 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');
|
||||
return outcome.val;
|
||||
}
|
||||
};
|
||||
|
||||
// DEPRECATED: Use `callAllSerial()` or `aCallAll()` instead.
|
||||
//
|
||||
// Invokes all registered hook functions synchronously.
|
||||
//
|
||||
// Arguments:
|
||||
|
@ -203,15 +188,10 @@ function callHookFnSync(hook, context) {
|
|||
// 1. Collect all values returned by the hook functions into an array.
|
||||
// 2. Convert each `undefined` entry into `[]`.
|
||||
// 3. Flatten one level.
|
||||
exports.callAll = function (hookName, context) {
|
||||
exports.callAll = (hookName, context) => {
|
||||
if (context == null) context = {};
|
||||
const hooks = pluginDefs.hooks[hookName] || [];
|
||||
return _.flatten(hooks.map((hook) => {
|
||||
const ret = callHookFnSync(hook, context);
|
||||
// `undefined` (but not `null`!) is treated the same as [].
|
||||
if (ret === undefined) return [];
|
||||
return ret;
|
||||
}), 1);
|
||||
return flatten1(hooks.map((hook) => normalizeValue(callHookFnSync(hook, context))));
|
||||
};
|
||||
|
||||
// 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.
|
||||
//
|
||||
async function callHookFnAsync(hook, context) {
|
||||
const callHookFnAsync = async (hook, context) => {
|
||||
checkDeprecation(hook);
|
||||
return await new Promise((resolve, reject) => {
|
||||
// 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
|
||||
// `undefined` as the resolved value.
|
||||
//
|
||||
// IMPORTANT: "Rest" parameters and default parameters are not counted in `Function.length`,
|
||||
// so the assumption does not hold for wrappers like `(...args) => { real(...args); }`. For
|
||||
// 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.
|
||||
// IMPORTANT: "Rest" parameters and default parameters are not included in
|
||||
// `Function.length`, so the assumption does not hold for wrappers such as:
|
||||
//
|
||||
// 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'),
|
||||
(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:
|
||||
// * hookName: Name of the hook to invoke.
|
||||
// * 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);
|
||||
// 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:
|
||||
// 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 `[]`.
|
||||
// 3. Flatten one level.
|
||||
// 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 = {};
|
||||
const hooks = pluginDefs.hooks[hookName] || [];
|
||||
let resultsPromise = Promise.all(hooks.map((hook) => callHookFnAsync(hook, context)
|
||||
// `undefined` (but not `null`!) is treated the same as [].
|
||||
.then((result) => (result === undefined) ? [] : result))).then((results) => _.flatten(results, 1));
|
||||
if (cb != null) resultsPromise = resultsPromise.then((val) => cb(null, val), cb);
|
||||
return await resultsPromise;
|
||||
const results = await Promise.all(
|
||||
hooks.map(async (hook) => normalizeValue(await callHookFnAsync(hook, context))));
|
||||
return flatten1(results);
|
||||
};
|
||||
|
||||
exports.callFirst = function (hook_name, args) {
|
||||
if (!args) args = {};
|
||||
if (pluginDefs.hooks[hook_name] === undefined) return [];
|
||||
return exports.syncMapFirst(pluginDefs.hooks[hook_name], (hook) => hookCallWrapper(hook, hook_name, args));
|
||||
};
|
||||
|
||||
function aCallFirst(hook_name, args, cb, predicate) {
|
||||
if (!args) args = {};
|
||||
if (!cb) cb = function () {};
|
||||
if (pluginDefs.hooks[hook_name] === undefined) return cb(null, []);
|
||||
exports.mapFirst(
|
||||
pluginDefs.hooks[hook_name],
|
||||
(hook, cb) => {
|
||||
hookCallWrapper(hook, hook_name, args, (res) => { cb(null, res); });
|
||||
},
|
||||
cb,
|
||||
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);
|
||||
// Like `aCallAll()` except the hook functions are called one at a time instead of concurrently.
|
||||
// Only use this function if the hook functions must be called one at a time, otherwise use
|
||||
// `aCallAll()`.
|
||||
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);
|
||||
};
|
||||
|
||||
exports.callAllStr = function (hook_name, args, sep, pre, post) {
|
||||
if (sep == undefined) sep = '';
|
||||
if (pre == undefined) pre = '';
|
||||
if (post == undefined) post = '';
|
||||
const newCallhooks = [];
|
||||
const callhooks = exports.callAll(hook_name, args);
|
||||
for (let i = 0, ii = callhooks.length; i < ii; i++) {
|
||||
newCallhooks[i] = pre + callhooks[i] + post;
|
||||
// DEPRECATED: Use `aCallFirst()` instead.
|
||||
//
|
||||
// Like `aCallFirst()`, but synchronous. Hook functions must provide their values synchronously.
|
||||
exports.callFirst = (hookName, context) => {
|
||||
if (context == null) context = {};
|
||||
const predicate = (val) => val.length;
|
||||
const hooks = pluginDefs.hooks[hookName] || [];
|
||||
for (const hook of hooks) {
|
||||
const val = normalizeValue(callHookFnSync(hook, context));
|
||||
if (predicate(val)) return val;
|
||||
}
|
||||
return newCallhooks.join(sep || '');
|
||||
return [];
|
||||
};
|
||||
|
||||
// Invokes the registered hook functions one at a time until one provides a value that meets a
|
||||
// customizable condition.
|
||||
//
|
||||
// Arguments:
|
||||
// * hookName: Name of the hook to invoke.
|
||||
// * context: Passed unmodified to the hook functions, except nullish becomes {}.
|
||||
// * cb: Deprecated callback. The following:
|
||||
// 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);
|
||||
}
|
||||
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 = {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
'use strict';
|
||||
|
||||
const log4js = require('log4js');
|
||||
const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins');
|
||||
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
|
||||
const plugins = require('./plugins');
|
||||
const hooks = require('./hooks');
|
||||
const npm = require('npm');
|
||||
const request = require('request');
|
||||
const util = require('util');
|
||||
|
@ -13,22 +15,22 @@ const loadNpm = async () => {
|
|||
npm.on('log', log4js.getLogger('npm').log);
|
||||
};
|
||||
|
||||
const onAllTasksFinished = () => {
|
||||
hooks.aCallAll('restartServer', {}, () => {});
|
||||
};
|
||||
|
||||
let tasks = 0;
|
||||
|
||||
function wrapTaskCb(cb) {
|
||||
tasks++;
|
||||
|
||||
return function () {
|
||||
cb && cb.apply(this, arguments);
|
||||
return function (...args) {
|
||||
cb && cb.apply(this, args);
|
||||
tasks--;
|
||||
if (tasks == 0) onAllTasksFinished();
|
||||
if (tasks === 0) onAllTasksFinished();
|
||||
};
|
||||
}
|
||||
|
||||
function onAllTasksFinished() {
|
||||
hooks.aCallAll('restartServer', {}, () => {});
|
||||
}
|
||||
|
||||
exports.uninstall = async (pluginName, cb = null) => {
|
||||
cb = wrapTaskCb(cb);
|
||||
try {
|
||||
|
@ -60,7 +62,7 @@ exports.install = async (pluginName, cb = null) => {
|
|||
exports.availablePlugins = null;
|
||||
let cacheTimestamp = 0;
|
||||
|
||||
exports.getAvailablePlugins = function (maxCacheAge) {
|
||||
exports.getAvailablePlugins = (maxCacheAge) => {
|
||||
const nowTimestamp = Math.round(Date.now() / 1000);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -87,31 +89,33 @@ exports.getAvailablePlugins = function (maxCacheAge) {
|
|||
};
|
||||
|
||||
|
||||
exports.search = function (searchTerm, maxCacheAge) {
|
||||
return exports.getAvailablePlugins(maxCacheAge).then((results) => {
|
||||
const res = {};
|
||||
exports.search = (searchTerm, maxCacheAge) => exports.getAvailablePlugins(maxCacheAge).then(
|
||||
(results) => {
|
||||
const res = {};
|
||||
|
||||
if (searchTerm) {
|
||||
searchTerm = searchTerm.toLowerCase();
|
||||
}
|
||||
|
||||
for (const pluginName in results) {
|
||||
// for every available plugin
|
||||
if (pluginName.indexOf(plugins.prefix) != 0) continue; // TODO: Also search in keywords here!
|
||||
|
||||
if (searchTerm && !~results[pluginName].name.toLowerCase().indexOf(searchTerm) &&
|
||||
(typeof results[pluginName].description !== 'undefined' && !~results[pluginName].description.toLowerCase().indexOf(searchTerm))
|
||||
) {
|
||||
if (typeof results[pluginName].description === 'undefined') {
|
||||
console.debug('plugin without Description: %s', results[pluginName].name);
|
||||
}
|
||||
|
||||
continue;
|
||||
if (searchTerm) {
|
||||
searchTerm = searchTerm.toLowerCase();
|
||||
}
|
||||
|
||||
res[pluginName] = results[pluginName];
|
||||
}
|
||||
for (const pluginName in results) {
|
||||
// for every available plugin
|
||||
// TODO: Also search in keywords here!
|
||||
if (pluginName.indexOf(plugins.prefix) !== 0) continue;
|
||||
|
||||
return res;
|
||||
});
|
||||
};
|
||||
if (searchTerm && !~results[pluginName].name.toLowerCase().indexOf(searchTerm) &&
|
||||
(typeof results[pluginName].description !== 'undefined' &&
|
||||
!~results[pluginName].description.toLowerCase().indexOf(searchTerm))
|
||||
) {
|
||||
if (typeof results[pluginName].description === 'undefined') {
|
||||
console.debug('plugin without Description: %s', results[pluginName].name);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
res[pluginName] = results[pluginName];
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
// This module contains processed plugin definitions. The data structures in this file are set by
|
||||
// plugins.js (server) or client_plugins.js (client).
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const hooks = require('./hooks');
|
||||
const npm = require('npm/lib/npm.js');
|
||||
const readInstalled = require('./read-installed.js');
|
||||
const path = require('path');
|
||||
const tsort = require('./tsort');
|
||||
|
@ -13,11 +14,9 @@ const defs = require('./plugin_defs');
|
|||
|
||||
exports.prefix = 'ep_';
|
||||
|
||||
exports.formatPlugins = function () {
|
||||
return _.keys(defs.plugins).join(', ');
|
||||
};
|
||||
exports.formatPlugins = () => Object.keys(defs.plugins).join(', ');
|
||||
|
||||
exports.formatPluginsWithVersion = function () {
|
||||
exports.formatPluginsWithVersion = () => {
|
||||
const plugins = [];
|
||||
_.forEach(defs.plugins, (plugin) => {
|
||||
if (plugin.package.name !== 'ep_etherpad-lite') {
|
||||
|
@ -28,17 +27,16 @@ exports.formatPluginsWithVersion = function () {
|
|||
return plugins.join(', ');
|
||||
};
|
||||
|
||||
exports.formatParts = function () {
|
||||
return _.map(defs.parts, (part) => part.full_name).join('\n');
|
||||
};
|
||||
exports.formatParts = () => _.map(defs.parts, (part) => part.full_name).join('\n');
|
||||
|
||||
exports.formatHooks = function (hook_set_name) {
|
||||
exports.formatHooks = (hook_set_name) => {
|
||||
const res = [];
|
||||
const hooks = pluginUtils.extractHooks(defs.parts, hook_set_name || 'hooks');
|
||||
|
||||
_.chain(hooks).keys().forEach((hook_name) => {
|
||||
_.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>`;
|
||||
|
@ -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'.
|
||||
// If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'.
|
||||
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}`;
|
||||
};
|
||||
|
||||
exports.update = async function () {
|
||||
exports.update = async () => {
|
||||
const packages = await exports.getPackages();
|
||||
const parts = {}; // Key is full name. sortParts converts this into a topologically sorted array.
|
||||
const plugins = {};
|
||||
|
@ -83,13 +81,14 @@ exports.update = async function () {
|
|||
await callInit();
|
||||
};
|
||||
|
||||
exports.getPackages = async function () {
|
||||
// Load list of installed NPM packages, flatten it to a list, and filter out only packages with names that
|
||||
exports.getPackages = async () => {
|
||||
// Load list of installed NPM packages, flatten it to a list,
|
||||
// and filter out only packages with names that
|
||||
const dir = settings.root;
|
||||
const data = await util.promisify(readInstalled)(dir);
|
||||
|
||||
const packages = {};
|
||||
function flatten(deps) {
|
||||
const flatten = (deps) => {
|
||||
_.chain(deps).keys().each((name) => {
|
||||
if (name.indexOf(exports.prefix) === 0) {
|
||||
packages[name] = _.clone(deps[name]);
|
||||
|
@ -102,7 +101,7 @@ exports.getPackages = async function () {
|
|||
// I don't think we need recursion
|
||||
// if (deps[name].dependencies !== undefined) flatten(deps[name].dependencies);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const tmp = {};
|
||||
tmp[data.name] = data;
|
||||
|
@ -110,7 +109,7 @@ exports.getPackages = async function () {
|
|||
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');
|
||||
try {
|
||||
const data = await fs.readFile(plugin_path);
|
||||
|
@ -129,9 +128,9 @@ async function loadPlugin(packages, plugin_name, plugins, parts) {
|
|||
} catch (er) {
|
||||
console.error(`Unable to load plugin definition file ${plugin_path}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function partsToParentChildList(parts) {
|
||||
const partsToParentChildList = (parts) => {
|
||||
const res = [];
|
||||
_.chain(parts).keys().forEach((name) => {
|
||||
_.each(parts[name].post || [], (child_name) => {
|
||||
|
@ -145,15 +144,9 @@ function partsToParentChildList(parts) {
|
|||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
// Used only in Node, so no need for _
|
||||
function sortParts(parts) {
|
||||
return tsort(
|
||||
partsToParentChildList(parts)
|
||||
).filter(
|
||||
(name) => parts[name] !== undefined
|
||||
).map(
|
||||
(name) => parts[name]
|
||||
);
|
||||
}
|
||||
const sortParts = (parts) => tsort(partsToParentChildList(parts))
|
||||
.filter((name) => parts[name] !== undefined)
|
||||
.map((name) => parts[name]);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
const _ = require('underscore');
|
||||
const defs = require('./plugin_defs');
|
||||
|
||||
|
@ -8,13 +9,13 @@ const disabledHookReasons = {
|
|||
},
|
||||
};
|
||||
|
||||
function loadFn(path, hookName) {
|
||||
const loadFn = (path, hookName) => {
|
||||
let functionName;
|
||||
const parts = path.split(':');
|
||||
|
||||
// on windows: C:\foo\bar:xyz
|
||||
if (parts[0].length == 1) {
|
||||
if (parts.length == 3) {
|
||||
if (parts[0].length === 1) {
|
||||
if (parts.length === 3) {
|
||||
functionName = parts.pop();
|
||||
}
|
||||
path = parts.join(':');
|
||||
|
@ -30,9 +31,9 @@ function loadFn(path, hookName) {
|
|||
fn = fn[name];
|
||||
});
|
||||
return fn;
|
||||
}
|
||||
};
|
||||
|
||||
function extractHooks(parts, hook_set_name, normalizer) {
|
||||
const extractHooks = (parts, hook_set_name, normalizer) => {
|
||||
const hooks = {};
|
||||
_.each(parts, (part) => {
|
||||
_.chain(part[hook_set_name] || {})
|
||||
|
@ -50,20 +51,23 @@ function extractHooks(parts, hook_set_name, normalizer) {
|
|||
|
||||
const disabledReason = (disabledHookReasons[hook_set_name] || {})[hook_name];
|
||||
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} ` +
|
||||
'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`);
|
||||
'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`);
|
||||
return;
|
||||
}
|
||||
|
||||
let hook_fn;
|
||||
try {
|
||||
var hook_fn = loadFn(hook_fn_name, hook_name);
|
||||
hook_fn = loadFn(hook_fn_name, hook_name);
|
||||
if (!hook_fn) {
|
||||
throw 'Not a function';
|
||||
throw new Error('Not a function');
|
||||
}
|
||||
} 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 (hooks[hook_name] == null) hooks[hook_name] = [];
|
||||
|
@ -72,7 +76,7 @@ function extractHooks(parts, hook_set_name, normalizer) {
|
|||
});
|
||||
});
|
||||
return hooks;
|
||||
}
|
||||
};
|
||||
|
||||
exports.extractHooks = extractHooks;
|
||||
|
||||
|
@ -88,10 +92,10 @@ exports.extractHooks = extractHooks;
|
|||
* No plugins: []
|
||||
* Some plugins: [ 'ep_adminpads', 'ep_add_buttons', 'ep_activepads' ]
|
||||
*/
|
||||
exports.clientPluginNames = function () {
|
||||
exports.clientPluginNames = () => {
|
||||
const client_plugin_names = _.uniq(
|
||||
defs.parts
|
||||
.filter((part) => part.hasOwnProperty('client_hooks'))
|
||||
.filter((part) => Object.prototype.hasOwnProperty.call(part, 'client_hooks'))
|
||||
.map((part) => `plugin-${part.plugin}`)
|
||||
);
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ const tsort = (edges) => {
|
|||
Object.keys(nodes).forEach(visit);
|
||||
|
||||
return sorted;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* TEST
|
||||
|
|
|
@ -1,19 +1,14 @@
|
|||
// Specific hash to display the skin variants builder popup
|
||||
if (window.location.hash.toLowerCase() == '#skinvariantsbuilder') {
|
||||
$('#skin-variants').addClass('popup-show');
|
||||
'use strict';
|
||||
|
||||
$('.skin-variant').change(() => {
|
||||
updateSkinVariantsClasses();
|
||||
});
|
||||
// Specific hash to display the skin variants builder popup
|
||||
if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
|
||||
$('#skin-variants').addClass('popup-show');
|
||||
|
||||
const containers = ['editor', 'background', 'toolbar'];
|
||||
const colors = ['super-light', 'light', 'dark', 'super-dark'];
|
||||
|
||||
updateCheckboxFromSkinClasses();
|
||||
updateSkinVariantsClasses();
|
||||
|
||||
// add corresponding classes when config change
|
||||
function updateSkinVariantsClasses() {
|
||||
const updateSkinVariantsClasses = () => {
|
||||
const domsToUpdate = [
|
||||
$('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'); });
|
||||
|
||||
const new_classes = [];
|
||||
const newClasses = [];
|
||||
$('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
|
||||
function updateCheckboxFromSkinClasses() {
|
||||
const updateCheckboxFromSkinClasses = () => {
|
||||
$('html').attr('class').split(' ').forEach((classItem) => {
|
||||
var container = classItem.split('-').slice(-1);
|
||||
|
||||
var container = classItem.substring(classItem.lastIndexOf('-') + 1, classItem.length);
|
||||
const container = classItem.substring(classItem.lastIndexOf('-') + 1, classItem.length);
|
||||
if (containers.indexOf(container) > -1) {
|
||||
const color = classItem.substring(0, classItem.lastIndexOf('-'));
|
||||
$(`.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').change(() => {
|
||||
updateSkinVariantsClasses();
|
||||
});
|
||||
|
||||
updateCheckboxFromSkinClasses();
|
||||
updateSkinVariantsClasses();
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue