From 12bd617f511de1da4f4511617ffed6e2804ac9d7 Mon Sep 17 00:00:00 2001 From: Sebastian Castro Date: Sat, 19 Sep 2020 20:09:30 +0200 Subject: [PATCH 001/315] css: Improve toolbar responsiveness for small screen (#4322) Until now, the "mobile layout" (with right toolbar on bottom of the screen) was displayed only when screen was smaller than 800px. It made the toolbar break for screen about 1000px when a lot of plugins are in the toolbar. Now instead, we detect with javascript when the toolbar icons overflow the natural space available, and we switch in "mobile layout" in such case --- src/static/css/pad/layout.css | 6 +- src/static/css/pad/popup.css | 14 ++-- src/static/css/pad/popup_users.css | 16 +++-- src/static/css/pad/toolbar.css | 71 ++++++++++--------- src/static/js/pad_editbar.js | 10 +-- .../skins/colibris/src/components/toolbar.css | 39 +++++----- src/static/skins/colibris/src/layout.css | 7 -- 7 files changed, 82 insertions(+), 81 deletions(-) diff --git a/src/static/css/pad/layout.css b/src/static/css/pad/layout.css index 7b64936d7..e5b79c268 100644 --- a/src/static/css/pad/layout.css +++ b/src/static/css/pad/layout.css @@ -47,8 +47,6 @@ body { width: 0; /* hide when the container is empty */ } -@media only screen and (max-width: 800px) { - #editorcontainerbox { - margin-bottom: 39px; /* Leave space for the bottom toolbar on mobile */ - } +.mobile-layout #editorcontainerbox { + margin-bottom: 39px; /* Leave space for the bottom toolbar on mobile */ } diff --git a/src/static/css/pad/popup.css b/src/static/css/pad/popup.css index 00fc8ca51..0eb000996 100644 --- a/src/static/css/pad/popup.css +++ b/src/static/css/pad/popup.css @@ -78,9 +78,11 @@ .popup#users .popup-content { overflow: visible; } - /* Move popup to the bottom, except popup linked to left toolbar, like hyperklink popup */ - .popup:not(.toolbar-popup) { - top: auto; - bottom: 1rem; - } -} \ No newline at end of file +} +/* Move popup to the bottom, except popup linked to left toolbar, like hyperklink popup */ +.mobile-layout .popup:not(.toolbar-popup) { + top: auto; + left: 1rem; + right: auto; + bottom: 1rem; +} diff --git a/src/static/css/pad/popup_users.css b/src/static/css/pad/popup_users.css index 8b6ba82bc..ce4d14365 100644 --- a/src/static/css/pad/popup_users.css +++ b/src/static/css/pad/popup_users.css @@ -98,13 +98,15 @@ input#myusernameedit:not(.editable) { right: calc(100% + 15px); z-index: 101; } -@media (max-width: 800px) { - #mycolorpicker.popup { - top: auto; - bottom: 0; - left: auto !important; - right: 0 !important; - } +.mobile-layout #users.popup { + right: 1rem; + left: auto; +} +.mobile-layout #mycolorpicker.popup { + top: auto; + bottom: 0; + left: auto !important; + right: 0 !important; } #mycolorpicker.popup .btn-container { margin-top: 10px; diff --git a/src/static/css/pad/toolbar.css b/src/static/css/pad/toolbar.css index bc258510a..1a398b199 100644 --- a/src/static/css/pad/toolbar.css +++ b/src/static/css/pad/toolbar.css @@ -139,37 +139,40 @@ .toolbar ul li.separator { width: 5px; } - /* menu_right act like a new toolbar on the bottom of the screen */ - .toolbar .menu_right { - position: fixed; - bottom: 0; - right: 0; - left: 0; - border-top: 1px solid #ccc; - background-color: #f4f4f4; - padding: 0 5px 5px 5px; - } - .toolbar ul.menu_right > li { - margin-right: 8px; - } - .toolbar ul.menu_right > li.separator { - display: none; - } - .toolbar ul.menu_right > li a { - border: none; - background-color: transparent; - margin-left: 5px; - } - .toolbar ul.menu_right > li[data-key="showusers"] { - position: absolute; - right: 0; - top: 0; - bottom: 0; - margin: 0; - } - .toolbar ul.menu_right > li[data-key="showusers"] a { - height: 100%; - width: 40px; - border-radius: 0; - } -} \ No newline at end of file +} + +/* menu_right act like a new toolbar on the bottom of the screen */ +.mobile-layout .toolbar .menu_right { + position: fixed; + bottom: 0; + right: 0; + left: 0; + border-top: 1px solid #ccc; + background-color: #f4f4f4; + padding: 0 5px 5px 5px; +} +.mobile-layout .toolbar ul.menu_right > li { + margin-right: 8px; +} +.mobile-layout .toolbar ul.menu_right > li[data-key="showusers"] { + position: absolute; + right: 0; + top: 0; + bottom: 0; + margin: 0; +} +.mobile-layout .toolbar ul.menu_right > li[data-key="showusers"] a { + height: 100%; + width: 40px; + border-radius: 0; +} +.mobile-layout .toolbar ul.menu_right > li.separator { + display: none; +} +.mobile-layout .toolbar ul.menu_right > li a { + border: none; + margin-left: 5px; +} +.mobile-layout .toolbar ul.menu_right > li a:not(.selected) { + background-color: transparent; +} diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 30d223059..0e40bb990 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -317,12 +317,14 @@ var padeditbar = (function() { // reset style $('.toolbar').removeClass('cropped') + $('body').removeClass('mobile-layout'); var menu_left = $('.toolbar .menu_left')[0]; - // on mobile the menu_right get displayed at the bottom of the screen - var isMobileLayout = $('.toolbar .menu_right').css('position') === 'fixed'; - - if (menu_left && menu_left.scrollWidth > $('.toolbar').width() && isMobileLayout) { + var menuRightWidth = 280; // this is approximate, we cannot measure it because on mobileLayour it takes the full width on the bottom of the page + if (menu_left && menu_left.scrollWidth > $('.toolbar').width() - menuRightWidth || $('.toolbar').width() < 1000) { + $('body').addClass('mobile-layout'); + } + if (menu_left && menu_left.scrollWidth > $('.toolbar').width()) { $('.toolbar').addClass('cropped'); } } diff --git a/src/static/skins/colibris/src/components/toolbar.css b/src/static/skins/colibris/src/components/toolbar.css index 91e9991ed..7f3e71403 100644 --- a/src/static/skins/colibris/src/components/toolbar.css +++ b/src/static/skins/colibris/src/components/toolbar.css @@ -131,23 +131,24 @@ } } -@media (max-width: 800px) { - - .toolbar ul li { - margin: 5px 2px; - } - - .toolbar .menu_right { - border-top: 1px solid #d2d2d2; - border-top: var(--toolbar-border); - background-color: #ffffff; - background-color: var(--bg-color); - padding: 0; - } - - .toolbar ul li a:hover { background-color: transparent; } - - .toolbar ul li.separator { margin: 0; display: none; } +.mobile-layout .toolbar ul li { + margin: 5px 2px; +} +.mobile-layout .toolbar ul li.separator { + margin: 0 5px; +} +@media (max-width: 800px) { + .mobile-layout .toolbar ul li.separator { + display: none; + } +} +.mobile-layout .toolbar .menu_right { + border-top: 1px solid #d2d2d2; + border-top: var(--toolbar-border); + background-color: #ffffff; + background-color: var(--bg-color); + padding: 0; +} +.mobile-layout .toolbar ul li a:hover { + /* background-color: transparent; */ } - - diff --git a/src/static/skins/colibris/src/layout.css b/src/static/skins/colibris/src/layout.css index 6385cf140..1ec3886c8 100644 --- a/src/static/skins/colibris/src/layout.css +++ b/src/static/skins/colibris/src/layout.css @@ -46,10 +46,3 @@ border-radius: 0; } } - -@media only screen and (max-width: 800px) { - #editorcontainerbox { - margin-bottom: 39px; /* margin for bottom toolbar */ - } -} - From 3886e95c83af106f812da39987a304201689081d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 19 Sep 2020 15:51:55 -0400 Subject: [PATCH 002/315] SessionManager: Fix session expiration check This bug was introduced in 8b0baa96797718985b0557d25d4696c19220c309. --- src/node/db/SessionManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index 5f7df1e24..5ba43c462 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -72,7 +72,7 @@ exports.findAuthorID = async (groupID, sessionCookie) => { return undefined; }); const now = Math.floor(Date.now() / 1000); - const isMatch = (si) => (si != null && si.groupID === groupID && si.validUntil <= now); + const isMatch = (si) => (si != null && si.groupID === groupID && now < si.validUntil); const sessionInfo = await promises.firstSatisfies(sessionInfoPromises, isMatch); if (sessionInfo == null) return undefined; return sessionInfo.authorID; From 65942691b6b8bdc22891624d69eff8d4933fa97f Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 21 Sep 2020 16:02:42 +0200 Subject: [PATCH 003/315] Localisation updates from https://translatewiki.net. --- src/locales/de.json | 2 +- src/locales/is.json | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/locales/de.json b/src/locales/de.json index bcee159f2..7be360596 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -51,7 +51,7 @@ "pad.settings.fontType.normal": "Normal", "pad.settings.language": "Sprache:", "pad.settings.about": "Über", - "pad.settings.poweredBy": "Powered by $1", + "pad.settings.poweredBy": "Powered by", "pad.importExport.import_export": "Import/Export", "pad.importExport.import": "Textdatei oder Dokument hochladen", "pad.importExport.importSuccessful": "Erfolgreich!", diff --git a/src/locales/is.json b/src/locales/is.json index 0d7fd5afa..394503f1a 100644 --- a/src/locales/is.json +++ b/src/locales/is.json @@ -41,6 +41,8 @@ "pad.settings.fontType": "Leturgerð:", "pad.settings.fontType.normal": "Venjulegt", "pad.settings.language": "Tungumál:", + "pad.settings.about": "Um hugbúnaðinn", + "pad.settings.poweredBy": "Keyrt með", "pad.importExport.import_export": "Flytja inn/út", "pad.importExport.import": "Settu inn hverskyns texta eða skjal", "pad.importExport.importSuccessful": "Heppnaðist!", @@ -51,7 +53,7 @@ "pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Open Document Format)", - "pad.importExport.abiword.innerHTML": "Þú getur aðeins flutt inn úr hreinum texta eða HTML sniðum. Til að geta nýtt \nfleiri þróaðri innflutningssnið settu þá upp AbiWord forritið.", + "pad.importExport.abiword.innerHTML": "Þú getur aðeins flutt inn úr hreinum texta eða HTML sniðum. Til að geta nýtt \nfleiri þróaðri innflutningssnið settu þá upp AbiWord forritið eða LibreOffice.", "pad.modals.connected": "Tengt.", "pad.modals.reconnecting": "Endurtengist skrifblokkinni þinni...", "pad.modals.forcereconnect": "Þvinga endurtengingu", @@ -119,7 +121,7 @@ "pad.userlist.guest": "Gestur", "pad.userlist.deny": "Hafna", "pad.userlist.approve": "Samþykkja", - "pad.editbar.clearcolors": "Hreinsa liti höfunda á öllu skjalinu?", + "pad.editbar.clearcolors": "Hreinsa liti höfunda á öllu skjalinu? Þetta er ekki hægt að afturkalla", "pad.impexp.importbutton": "Flytja inn núna", "pad.impexp.importing": "Flyt inn...", "pad.impexp.confirmimport": "Innflutningur á skrá mun skrifa yfir þann texta sem er á skrifblokkinni núna. \nErtu viss um að þú viljir halda áfram?", From 346111250e87fa82b3f8e955a6fb24cd219870d0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 16 Sep 2020 16:59:00 -0400 Subject: [PATCH 004/315] utils: Fix promise creation accounting bug in promises.timesLimit Before this change, `promises.timesLimit()` created `concurrency - 1` too many promises. The only users of this function use a concurrency of 500, so this meant that 499 extra promises were created each time it was used. The bug didn't affect correctness, but it did result in a large number of unnecessary database operations whenever a pad was deleted. This change fixes that bug. Also: * Convert the function to async and have it resolve after all of the created promises are resolved. * Reject concurrency of 0 (unless total is 0). * Document the function. * Add tests. --- src/node/utils/promises.js | 38 +++++++-------- tests/backend/specs/promises.js | 85 +++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 22 deletions(-) create mode 100644 tests/backend/specs/promises.js diff --git a/src/node/utils/promises.js b/src/node/utils/promises.js index a754823a0..bb973befa 100644 --- a/src/node/utils/promises.js +++ b/src/node/utils/promises.js @@ -35,27 +35,21 @@ exports.firstSatisfies = (promises, predicate) => { return Promise.race(newPromises); }; -exports.timesLimit = function(ltMax, concurrency, promiseCreator) { - var done = 0 - var current = 0 - - function addAnother () { - function _internalRun () { - done++ - - if (done < ltMax) { - addAnother() - } - } - - promiseCreator(current) - .then(_internalRun) - .catch(_internalRun) - - current++ - } - - for (var i = 0; i < concurrency && i < ltMax; i++) { - addAnother() +// Calls `promiseCreator(i)` a total number of `total` times, where `i` is 0 through `total - 1` (in +// order). The `concurrency` argument specifies the maximum number of Promises returned by +// `promiseCreator` that are allowed to be active (unresolved) simultaneously. (In other words: If +// `total` is greater than `concurrency`, then `concurrency` Promises will be created right away, +// and each remaining Promise will be created once one of the earlier Promises resolves.) This async +// function resolves once all `total` Promises have resolved. +exports.timesLimit = async (total, concurrency, promiseCreator) => { + if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive'); + let next = 0; + const addAnother = () => promiseCreator(next++).finally(() => { + if (next < total) return addAnother(); + }); + const promises = []; + for (var i = 0; i < concurrency && i < total; i++) { + promises.push(addAnother()); } + await Promise.all(promises); } diff --git a/tests/backend/specs/promises.js b/tests/backend/specs/promises.js new file mode 100644 index 000000000..13a8c532a --- /dev/null +++ b/tests/backend/specs/promises.js @@ -0,0 +1,85 @@ +function m(mod) { return __dirname + '/../../../src/' + mod; } + +const assert = require('assert').strict; +const promises = require(m('node/utils/promises')); + +describe('promises.timesLimit', async () => { + let wantIndex = 0; + const testPromises = []; + const makePromise = (index) => { + // Make sure index increases by one each time. + assert.equal(index, wantIndex++); + // Save the resolve callback (so the test can trigger resolution) + // and the promise itself (to wait for resolve to take effect). + const p = {}; + const promise = new Promise((resolve) => { + p.resolve = resolve; + }); + p.promise = promise; + testPromises.push(p); + return p.promise; + }; + + const total = 11; + const concurrency = 7; + const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise); + + it('honors concurrency', async () => { + assert.equal(wantIndex, concurrency); + }); + + it('creates another when one completes', async () => { + const {promise, resolve} = testPromises.shift(); + resolve(); + await promise; + assert.equal(wantIndex, concurrency + 1); + }); + + it('creates the expected total number of promises', async () => { + while (testPromises.length > 0) { + // Resolve them in random order to ensure that the resolution order doesn't matter. + const i = Math.floor(Math.random() * Math.floor(testPromises.length)); + const {promise, resolve} = testPromises.splice(i, 1)[0]; + resolve(); + await promise; + } + assert.equal(wantIndex, total); + }); + + it('resolves', async () => { + await timesLimitPromise; + }); + + it('does not create too many promises if total < concurrency', async () => { + wantIndex = 0; + assert.equal(testPromises.length, 0); + const total = 7; + const concurrency = 11; + const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise); + while (testPromises.length > 0) { + const {promise, resolve} = testPromises.pop(); + resolve(); + await promise; + } + await timesLimitPromise; + assert.equal(wantIndex, total); + }); + + it('accepts total === 0, concurrency > 0', async () => { + wantIndex = 0; + assert.equal(testPromises.length, 0); + await promises.timesLimit(0, concurrency, makePromise); + assert.equal(wantIndex, 0); + }); + + it('accepts total === 0, concurrency === 0', async () => { + wantIndex = 0; + assert.equal(testPromises.length, 0); + await promises.timesLimit(0, 0, makePromise); + assert.equal(wantIndex, 0); + }); + + it('rejects total > 0, concurrency === 0', async () => { + await assert.rejects(promises.timesLimit(total, 0, makePromise), RangeError); + }); +}); From de98852da6a152a7c485ec9cd792a0740b06205a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 21 Sep 2020 16:50:42 -0400 Subject: [PATCH 005/315] SessionStore: Delete unused methods `all`, `clear`, `length` --- src/node/db/SessionStore.js | 44 ------------------------------------- 1 file changed, 44 deletions(-) diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index 647cbbc8d..9803c5672 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -52,47 +52,3 @@ SessionStore.prototype.destroy = function(sid, fn) { process.nextTick(fn); } }; - -/* - * RPB: the following methods are optional requirements for a compatible session - * store for express-session, but in any case appear to depend on a - * non-existent feature of ueberdb2 - */ -if (db.forEach) { - SessionStore.prototype.all = function(fn) { - messageLogger.debug('ALL'); - - var sessions = []; - - db.forEach(function(key, value) { - if (key.substr(0,15) === "sessionstorage:") { - sessions.push(value); - } - }); - fn(null, sessions); - }; - - SessionStore.prototype.clear = function(fn) { - messageLogger.debug('CLEAR'); - - db.forEach(function(key, value) { - if (key.substr(0,15) === "sessionstorage:") { - db.remove("session:" + key); - } - }); - if (fn) fn(); - }; - - SessionStore.prototype.length = function(fn) { - messageLogger.debug('LENGTH'); - - var i = 0; - - db.forEach(function(key, value) { - if (key.substr(0,15) === "sessionstorage:") { - i++; - } - }); - fn(null, i); - } -}; From 5d2c438e3eac4c82f002b4ebf687c8f5345ccdd1 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 21 Sep 2020 16:41:45 -0400 Subject: [PATCH 006/315] SessionStore: Use an arrow function to avoid `this` juggling --- src/node/db/SessionStore.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index 9803c5672..cf52b4af4 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -19,15 +19,13 @@ SessionStore.prototype.__proto__ = Store.prototype; SessionStore.prototype.get = function(sid, fn) { messageLogger.debug('GET ' + sid); - var self = this; - - db.get("sessionstorage:" + sid, function(err, sess) { + db.get('sessionstorage:' + sid, (err, sess) => { if (sess) { sess.cookie.expires = 'string' == typeof sess.cookie.expires ? new Date(sess.cookie.expires) : sess.cookie.expires; if (!sess.cookie.expires || new Date() < sess.cookie.expires) { fn(null, sess); } else { - self.destroy(sid, fn); + this.destroy(sid, fn); } } else { fn(); From 012449101d930ea130161e8eb317d101e6697649 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 21 Sep 2020 16:45:25 -0400 Subject: [PATCH 007/315] SessionStore: Use `const` instead of `var` --- src/node/db/SessionStore.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index cf52b4af4..040f108c5 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -7,12 +7,13 @@ * express-session, which can't actually use promises anyway. */ -var Store = require('ep_etherpad-lite/node_modules/express-session').Store, - db = require('ep_etherpad-lite/node/db/DB').db, - log4js = require('ep_etherpad-lite/node_modules/log4js'), - messageLogger = log4js.getLogger("SessionStore"); +const Store = require('ep_etherpad-lite/node_modules/express-session').Store; +const db = require('ep_etherpad-lite/node/db/DB').db; +const log4js = require('ep_etherpad-lite/node_modules/log4js'); -var SessionStore = module.exports = function SessionStore() {}; +const messageLogger = log4js.getLogger('SessionStore'); + +const SessionStore = module.exports = function SessionStore() {}; SessionStore.prototype.__proto__ = Store.prototype; From 5fb6bc193875a34e1dea4881b91827cdb2a829c4 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 21 Sep 2020 16:52:57 -0400 Subject: [PATCH 008/315] SessionStore: Use single quotes everywhere --- src/node/db/SessionStore.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index 040f108c5..0e01b319c 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -37,7 +37,7 @@ SessionStore.prototype.get = function(sid, fn) { SessionStore.prototype.set = function(sid, sess, fn) { messageLogger.debug('SET ' + sid); - db.set("sessionstorage:" + sid, sess); + db.set('sessionstorage:' + sid, sess); if (fn) { process.nextTick(fn); } @@ -46,7 +46,7 @@ SessionStore.prototype.set = function(sid, sess, fn) { SessionStore.prototype.destroy = function(sid, fn) { messageLogger.debug('DESTROY ' + sid); - db.remove("sessionstorage:" + sid); + db.remove('sessionstorage:' + sid); if (fn) { process.nextTick(fn); } From 4060db0daff35a20472096500a46a311cd192a0a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 21 Sep 2020 16:55:27 -0400 Subject: [PATCH 009/315] SessionStore: Reduce unnecessary vertical space --- src/node/db/SessionStore.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index 0e01b319c..9ac0bb402 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -19,7 +19,6 @@ SessionStore.prototype.__proto__ = Store.prototype; SessionStore.prototype.get = function(sid, fn) { messageLogger.debug('GET ' + sid); - db.get('sessionstorage:' + sid, (err, sess) => { if (sess) { sess.cookie.expires = 'string' == typeof sess.cookie.expires ? new Date(sess.cookie.expires) : sess.cookie.expires; @@ -36,18 +35,12 @@ SessionStore.prototype.get = function(sid, fn) { SessionStore.prototype.set = function(sid, sess, fn) { messageLogger.debug('SET ' + sid); - db.set('sessionstorage:' + sid, sess); - if (fn) { - process.nextTick(fn); - } + if (fn) process.nextTick(fn); }; SessionStore.prototype.destroy = function(sid, fn) { messageLogger.debug('DESTROY ' + sid); - db.remove('sessionstorage:' + sid); - if (fn) { - process.nextTick(fn); - } + if (fn) process.nextTick(fn); }; From 90775cec0dbc92d718245c4fdfc16ce402d8421c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 21 Sep 2020 16:57:10 -0400 Subject: [PATCH 010/315] SessionStore: Rename `messageLogger` to `logger` --- src/node/db/SessionStore.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index 9ac0bb402..419da8744 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -11,14 +11,14 @@ const Store = require('ep_etherpad-lite/node_modules/express-session').Store; const db = require('ep_etherpad-lite/node/db/DB').db; const log4js = require('ep_etherpad-lite/node_modules/log4js'); -const messageLogger = log4js.getLogger('SessionStore'); +const logger = log4js.getLogger('SessionStore'); const SessionStore = module.exports = function SessionStore() {}; SessionStore.prototype.__proto__ = Store.prototype; SessionStore.prototype.get = function(sid, fn) { - messageLogger.debug('GET ' + sid); + logger.debug('GET ' + sid); db.get('sessionstorage:' + sid, (err, sess) => { if (sess) { sess.cookie.expires = 'string' == typeof sess.cookie.expires ? new Date(sess.cookie.expires) : sess.cookie.expires; @@ -34,13 +34,13 @@ SessionStore.prototype.get = function(sid, fn) { }; SessionStore.prototype.set = function(sid, sess, fn) { - messageLogger.debug('SET ' + sid); + logger.debug('SET ' + sid); db.set('sessionstorage:' + sid, sess); if (fn) process.nextTick(fn); }; SessionStore.prototype.destroy = function(sid, fn) { - messageLogger.debug('DESTROY ' + sid); + logger.debug('DESTROY ' + sid); db.remove('sessionstorage:' + sid); if (fn) process.nextTick(fn); }; From 0504e07eb4c60a71b633766d17ecaa5c2b97a0b4 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 21 Sep 2020 16:58:54 -0400 Subject: [PATCH 011/315] SessionStore: Wrap long line --- src/node/db/SessionStore.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index 419da8744..e84def10a 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -21,7 +21,8 @@ SessionStore.prototype.get = function(sid, fn) { logger.debug('GET ' + sid); db.get('sessionstorage:' + sid, (err, sess) => { if (sess) { - sess.cookie.expires = 'string' == typeof sess.cookie.expires ? new Date(sess.cookie.expires) : sess.cookie.expires; + sess.cookie.expires = ('string' == typeof sess.cookie.expires + ? new Date(sess.cookie.expires) : sess.cookie.expires); if (!sess.cookie.expires || new Date() < sess.cookie.expires) { fn(null, sess); } else { From bee91a0bd19a97eadaa3495007b0d2c32dfef4a1 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 21 Sep 2020 17:05:29 -0400 Subject: [PATCH 012/315] SessionStore: Use EC6 class syntax This fixes a minor bug where the SessionStore constructor did not call the base class constructor. --- src/node/db/SessionStore.js | 52 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index e84def10a..bf278b48b 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -13,35 +13,33 @@ const log4js = require('ep_etherpad-lite/node_modules/log4js'); const logger = log4js.getLogger('SessionStore'); -const SessionStore = module.exports = function SessionStore() {}; - -SessionStore.prototype.__proto__ = Store.prototype; - -SessionStore.prototype.get = function(sid, fn) { - logger.debug('GET ' + sid); - db.get('sessionstorage:' + sid, (err, sess) => { - if (sess) { - sess.cookie.expires = ('string' == typeof sess.cookie.expires - ? new Date(sess.cookie.expires) : sess.cookie.expires); - if (!sess.cookie.expires || new Date() < sess.cookie.expires) { - fn(null, sess); +module.exports = class SessionStore extends Store { + get(sid, fn) { + logger.debug('GET ' + sid); + db.get('sessionstorage:' + sid, (err, sess) => { + if (sess) { + sess.cookie.expires = ('string' == typeof sess.cookie.expires + ? new Date(sess.cookie.expires) : sess.cookie.expires); + if (!sess.cookie.expires || new Date() < sess.cookie.expires) { + fn(null, sess); + } else { + this.destroy(sid, fn); + } } else { - this.destroy(sid, fn); + fn(); } - } else { - fn(); - } - }); -}; + }); + } -SessionStore.prototype.set = function(sid, sess, fn) { - logger.debug('SET ' + sid); - db.set('sessionstorage:' + sid, sess); - if (fn) process.nextTick(fn); -}; + set(sid, sess, fn) { + logger.debug('SET ' + sid); + db.set('sessionstorage:' + sid, sess); + if (fn) process.nextTick(fn); + } -SessionStore.prototype.destroy = function(sid, fn) { - logger.debug('DESTROY ' + sid); - db.remove('sessionstorage:' + sid); - if (fn) process.nextTick(fn); + destroy(sid, fn) { + logger.debug('DESTROY ' + sid); + db.remove('sessionstorage:' + sid); + if (fn) process.nextTick(fn); + } }; From 436cbb031d95998595a98d796186a327a109745b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 21 Sep 2020 17:07:56 -0400 Subject: [PATCH 013/315] SessionStore: Avoid early DB.db dereference Avoid dereferencing `DB.db` until it is used so that it is possible to `require('SessionStore')` before calling `DB.init()`. (This is useful when writing tests.) --- src/node/db/SessionStore.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index bf278b48b..601b73b06 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -7,8 +7,8 @@ * 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 db = require('ep_etherpad-lite/node/db/DB').db; const log4js = require('ep_etherpad-lite/node_modules/log4js'); const logger = log4js.getLogger('SessionStore'); @@ -16,7 +16,7 @@ const logger = log4js.getLogger('SessionStore'); module.exports = class SessionStore extends Store { get(sid, fn) { logger.debug('GET ' + sid); - db.get('sessionstorage:' + sid, (err, sess) => { + DB.db.get('sessionstorage:' + sid, (err, sess) => { if (sess) { sess.cookie.expires = ('string' == typeof sess.cookie.expires ? new Date(sess.cookie.expires) : sess.cookie.expires); @@ -33,13 +33,13 @@ module.exports = class SessionStore extends Store { set(sid, sess, fn) { logger.debug('SET ' + sid); - db.set('sessionstorage:' + sid, sess); + DB.db.set('sessionstorage:' + sid, sess); if (fn) process.nextTick(fn); } destroy(sid, fn) { logger.debug('DESTROY ' + sid); - db.remove('sessionstorage:' + sid); + DB.db.remove('sessionstorage:' + sid); if (fn) process.nextTick(fn); } }; From a4be577ed1be33a4c2097d70aaf92d8f42712ea0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 21 Sep 2020 17:10:16 -0400 Subject: [PATCH 014/315] SessionStore: Don't call callback until cached in DB layer --- src/node/db/SessionStore.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index 601b73b06..e265ee68e 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -33,13 +33,11 @@ module.exports = class SessionStore extends Store { set(sid, sess, fn) { logger.debug('SET ' + sid); - DB.db.set('sessionstorage:' + sid, sess); - if (fn) process.nextTick(fn); + DB.db.set('sessionstorage:' + sid, sess, fn); } destroy(sid, fn) { logger.debug('DESTROY ' + sid); - DB.db.remove('sessionstorage:' + sid); - if (fn) process.nextTick(fn); + DB.db.remove('sessionstorage:' + sid, fn); } }; From a000a93dc644aa32f5ad9a0385837981f57edc44 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 21 Sep 2020 00:42:29 -0400 Subject: [PATCH 015/315] Refactor startup/shutdown for tests * `src/node/server.js` can now be run as a script (for normal operation) or imported as a module (for tests). * Move shutdown actions to `src/node/server.js` to be close to the startup actions. * Put startup and shutdown in functions so that tests can call them. * Use `await` instead of callbacks. * Block until the HTTP server is listening to avoid races during test startup. * Add a new `shutdown` hook. * Use the `shutdown` hook to: * close the HTTP server * call `end()` on the stats collection to cancel its timers * call `terminate()` on the Threads.Pool to stop the workers * Exit with exit code 0 (instead of 1) on SIGTERM. * Export the HTTP server so that tests can get the HTTP server's port via `server.address().port` when `settings.port` is 0. --- doc/api/hooks_server-side.md | 22 ++++ src/ep.json | 8 +- src/node/db/DB.js | 5 + src/node/hooks/express.js | 29 +++-- src/node/hooks/express/adminsettings.js | 5 +- src/node/hooks/express/errorhandling.js | 58 ---------- src/node/server.js | 141 ++++++++++++++++-------- src/node/stats.js | 4 + src/node/utils/Minify.js | 4 + src/package.json | 2 +- tests/backend/specs/socketio.js | 44 +++----- 11 files changed, 171 insertions(+), 151 deletions(-) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index b4ef1e525..0a4f95181 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -10,6 +10,28 @@ Things in context: Use this hook to receive the global settings in your plugin. +## shutdown +Called from: src/node/server.js + +Things in context: None + +This hook runs before shutdown. Use it to stop timers, close sockets and files, +flush buffers, etc. The database is not available while this hook is running. +The shutdown function must not block for long because there is a short timeout +before the process is forcibly terminated. + +The shutdown function must return a Promise, which must resolve to `undefined`. +Returning `callback(value)` will return a Promise that is resolved to `value`. + +Example: + +``` +// using an async function +exports.shutdown = async (hookName, context) => { + await flushBuffers(); +}; +``` + ## pluginUninstall Called from: src/static/js/pluginfw/installer.js diff --git a/src/ep.json b/src/ep.json index 428f57269..b436a63be 100644 --- a/src/ep.json +++ b/src/ep.json @@ -1,10 +1,14 @@ { "parts": [ + { "name": "DB", "hooks": { "shutdown": "ep_etherpad-lite/node/db/DB" } }, + { "name": "Minify", "hooks": { "shutdown": "ep_etherpad-lite/node/utils/Minify" } }, { "name": "express", "hooks": { - "createServer": "ep_etherpad-lite/node/hooks/express:createServer", - "restartServer": "ep_etherpad-lite/node/hooks/express:restartServer" + "createServer": "ep_etherpad-lite/node/hooks/express", + "restartServer": "ep_etherpad-lite/node/hooks/express", + "shutdown": "ep_etherpad-lite/node/hooks/express" } }, { "name": "static", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/static:expressCreateServer" } }, + { "name": "stats", "hooks": { "shutdown": "ep_etherpad-lite/node/stats" } }, { "name": "i18n", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/i18n:expressCreateServer" } }, { "name": "specialpages", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages:expressCreateServer" } }, { "name": "padurlsanitize", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize:expressCreateServer" } }, diff --git a/src/node/db/DB.js b/src/node/db/DB.js index 4150743de..22c3635ea 100644 --- a/src/node/db/DB.js +++ b/src/node/db/DB.js @@ -71,3 +71,8 @@ exports.init = function() { }); }); } + +exports.shutdown = async (hookName, context) => { + await exports.doShutdown(); + console.log('Database closed'); +}; diff --git a/src/node/hooks/express.js b/src/node/hooks/express.js index 7ff7d4ffc..cb536ef4d 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.js @@ -5,18 +5,20 @@ var fs = require('fs'); var path = require('path'); var npm = require("npm/lib/npm.js"); var _ = require("underscore"); +const util = require('util'); -var server; var serverName; -exports.createServer = function () { +exports.server = null; + +exports.createServer = async () => { console.log("Report bugs at https://github.com/ether/etherpad-lite/issues") serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`; console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`); - exports.restartServer(); + await exports.restartServer(); if (settings.ip === "") { // using Unix socket for connectivity @@ -38,10 +40,10 @@ exports.createServer = function () { } } -exports.restartServer = function () { - if (server) { +exports.restartServer = async () => { + if (exports.server) { console.log("Restarting express server"); - server.close(); + await util.promisify(exports.server.close).bind(exports.server)(); } var app = express(); // New syntax for express v3 @@ -65,10 +67,10 @@ exports.restartServer = function () { } var https = require('https'); - server = https.createServer(options, app); + exports.server = https.createServer(options, app); } else { var http = require('http'); - server = http.createServer(app); + exports.server = http.createServer(app); } app.use(function(req, res, next) { @@ -110,7 +112,12 @@ exports.restartServer = function () { } hooks.callAll("expressConfigure", {"app": app}); - hooks.callAll("expressCreateServer", {"app": app, "server": server}); + hooks.callAll('expressCreateServer', {app, server: exports.server}); - server.listen(settings.port, settings.ip); -} + await util.promisify(exports.server.listen).bind(exports.server)(settings.port, settings.ip); +}; + +exports.shutdown = async (hookName, context) => { + if (!exports.server) return; + await util.promisify(exports.server.close).bind(exports.server)(); +}; diff --git a/src/node/hooks/express/adminsettings.js b/src/node/hooks/express/adminsettings.js index 1e0d6004f..1ad48b111 100644 --- a/src/node/hooks/express/adminsettings.js +++ b/src/node/hooks/express/adminsettings.js @@ -46,11 +46,10 @@ exports.socketio = function (hook_name, args, cb) { }); }); - socket.on("restartServer", function () { + socket.on('restartServer', async () => { console.log("Admin request to restart server through a socket on /admin/settings"); settings.reloadSettings(); - hooks.aCallAll("restartServer", {}, function () {}); - + await hooks.aCallAll('restartServer'); }); }); diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js index 66553621c..d4b8b89dd 100644 --- a/src/node/hooks/express/errorhandling.js +++ b/src/node/hooks/express/errorhandling.js @@ -1,39 +1,5 @@ -var os = require("os"); -var db = require('../../db/DB'); var stats = require('ep_etherpad-lite/node/stats') - -exports.onShutdown = false; -exports.gracefulShutdown = function(err) { - if(err && err.stack) { - console.error(err.stack); - } else if(err) { - console.error(err); - } - - // ensure there is only one graceful shutdown running - if (exports.onShutdown) { - return; - } - - exports.onShutdown = true; - - console.log("graceful shutdown..."); - - // do the db shutdown - db.doShutdown().then(function() { - console.log("db sucessfully closed."); - - process.exit(0); - }); - - setTimeout(function() { - process.exit(1); - }, 3000); -} - -process.on('uncaughtException', exports.gracefulShutdown); - exports.expressCreateServer = function (hook_name, args, cb) { exports.app = args.app; @@ -46,28 +12,4 @@ exports.expressCreateServer = function (hook_name, args, cb) { console.error(err.stack? err.stack : err.toString()); stats.meter('http500').mark() }); - - /* - * 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 +C (though this may be configurable). It is not - * generated when terminal raw mode is enabled. - */ - process.on('SIGINT', exports.gracefulShutdown); - - // when running as PID1 (e.g. in docker container) - // allow graceful shutdown on SIGTERM c.f. #3265 - process.on('SIGTERM', exports.gracefulShutdown); } diff --git a/src/node/server.js b/src/node/server.js index a1f62df4f..c9ef33cc9 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -21,65 +21,112 @@ * limitations under the License. */ -const log4js = require('log4js') - , NodeVersion = require('./utils/NodeVersion') - , UpdateCheck = require('./utils/UpdateCheck') - ; - +const log4js = require('log4js'); log4js.replaceConsole(); /* * 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'); - -/* - * Etherpad 1.8.3 will require at least nodejs 10.13.0. - */ NodeVersion.checkDeprecationStatus('10.13.0', '1.8.3'); -// Check if Etherpad version is up-to-date -UpdateCheck.check(); +const UpdateCheck = require('./utils/UpdateCheck'); +const db = require('./db/DB'); +const express = require('./hooks/express'); +const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); +const npm = require('npm/lib/npm.js'); +const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); +const settings = require('./utils/Settings'); +const util = require('util'); -/* - * start up stats counting system - */ -var stats = require('./stats'); -stats.gauge('memoryUsage', function() { - return process.memoryUsage().rss; -}); +let started = false; +let stopped = false; -/* - * no use of let or await here because it would cause startup - * to fail completely on very early versions of NodeJS - */ -var npm = require("npm/lib/npm.js"); +exports.start = async () => { + if (started) return; + started = true; + if (stopped) throw new Error('restart not supported'); -npm.load({}, function() { - var settings = require('./utils/Settings'); - var db = require('./db/DB'); - var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); - var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); + // Check if Etherpad version is up-to-date + UpdateCheck.check(); - db.init() - .then(plugins.update) - .then(function() { - console.info("Installed plugins: " + plugins.formatPluginsWithVersion()); - console.debug("Installed parts:\n" + plugins.formatParts()); - console.debug("Installed hooks:\n" + plugins.formatHooks()); + // start up stats counting system + const stats = require('./stats'); + stats.gauge('memoryUsage', () => process.memoryUsage().rss); - // Call loadSettings hook - hooks.aCallAll("loadSettings", { settings: settings }); + await util.promisify(npm.load)(); - // initalize the http server - hooks.callAll("createServer", {}); - }) - .catch(function(e) { - console.error("exception thrown: " + e.message); - if (e.stack) { - console.log(e.stack); - } - process.exit(1); - }); -}); + 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); + } + + process.on('uncaughtException', exports.exit); + + /* + * 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 +C (though this may be configurable). It is not + * generated when terminal raw mode is enabled. + */ + process.on('SIGINT', exports.exit); + + // When running as PID1 (e.g. in docker container) allow graceful shutdown on SIGTERM c.f. #3265. + // Pass undefined to exports.exit because this is not an abnormal termination. + process.on('SIGTERM', () => exports.exit()); + + // Return the HTTP server to make it easier to write tests. + return express.server; +}; + +exports.stop = async () => { + if (stopped) return; + stopped = true; + 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(); + }); +}; + +exports.exit = async (err) => { + let exitCode = 0; + if (err) { + exitCode = 1; + console.error(err.stack ? err.stack : err); + } + try { + await exports.stop(); + } catch (err) { + exitCode = 1; + console.error(err.stack ? err.stack : err); + } + process.exit(exitCode); +}; + +if (require.main === module) exports.start(); diff --git a/src/node/stats.js b/src/node/stats.js index ff1752fe9..13654bb7d 100644 --- a/src/node/stats.js +++ b/src/node/stats.js @@ -1,3 +1,7 @@ var measured = require('measured-core') module.exports = measured.createCollection(); + +module.exports.shutdown = async (hookName, context) => { + module.exports.end(); +}; diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index a4194eb9e..e94d34b04 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -418,3 +418,7 @@ exports.minify = minify; exports.requestURI = requestURI; exports.requestURIs = requestURIs; + +exports.shutdown = async (hookName, context) => { + await threadsPool.terminate(); +}; diff --git a/src/package.json b/src/package.json index b01e07a02..b2d87f40a 100644 --- a/src/package.json +++ b/src/package.json @@ -92,7 +92,7 @@ "url": "https://github.com/ether/etherpad-lite.git" }, "scripts": { - "test": "nyc wtfnode node_modules/.bin/_mocha --timeout 5000 --recursive ../tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", + "test": "nyc wtfnode node_modules/.bin/_mocha --timeout 30000 --recursive ../tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", "test-container": "nyc mocha --timeout 5000 ../tests/container/specs/api" }, "version": "1.8.6", diff --git a/tests/backend/specs/socketio.js b/tests/backend/specs/socketio.js index 9db6bdbeb..ba639a79c 100644 --- a/tests/backend/specs/socketio.js +++ b/tests/backend/specs/socketio.js @@ -1,51 +1,30 @@ function m(mod) { return __dirname + '/../../../src/' + mod; } const assert = require('assert').strict; -const db = require(m('node/db/DB')); -const express = require(m('node_modules/express')); -const http = require('http'); +const io = require(m('node_modules/socket.io-client')); const log4js = require(m('node_modules/log4js')); -let padManager; +const padManager = require(m('node/db/PadManager')); const plugins = require(m('static/js/pluginfw/plugin_defs')); +const server = require(m('node/server')); const setCookieParser = require(m('node_modules/set-cookie-parser')); const settings = require(m('node/utils/Settings')); -const io = require(m('node_modules/socket.io-client')); -const stats = require(m('node/stats')); const supertest = require(m('node_modules/supertest')); -const util = require('util'); const logger = log4js.getLogger('test'); -const app = express(); -const server = http.createServer(app); let client; let baseUrl; before(async () => { - await util.promisify(server.listen).bind(server)(0, 'localhost'); - baseUrl = `http://localhost:${server.address().port}`; + settings.port = 0; + settings.ip = 'localhost'; + const httpServer = await server.start(); + baseUrl = `http://localhost:${httpServer.address().port}`; logger.debug(`HTTP server at ${baseUrl}`); client = supertest(baseUrl); - const npm = require(m('node_modules/npm/lib/npm.js')); - await util.promisify(npm.load)(); - settings.users = { - admin: {password: 'admin-password', is_admin: true}, - user: {password: 'user-password'}, - }; - await db.init(); - padManager = require(m('node/db/PadManager')); - const webaccess = require(m('node/hooks/express/webaccess')); - webaccess.expressConfigure('expressConfigure', {app}); - const socketio = require(m('node/hooks/express/socketio')); - socketio.expressCreateServer('expressCreateServer', {app, server}); - app.get(/./, (req, res) => { res.status(200).send('OK'); }); }); after(async () => { - stats.end(); - await Promise.all([ - db.doShutdown(), - util.promisify(server.close).bind(server)(), - ]); + await server.stop(); }); // Waits for and returns the next named socket.io event. Rejects if there is any error while waiting @@ -129,16 +108,23 @@ const handshake = async (socket, padID) => { }; describe('socket.io access checks', () => { + const settingsBackup = {}; let socket; beforeEach(async () => { + Object.assign(settingsBackup, settings); assert(socket == null); settings.requireAuthentication = false; settings.requireAuthorization = false; + settings.users = { + admin: {password: 'admin-password', is_admin: true}, + user: {password: 'user-password'}, + }; Promise.all(['pad', 'other-pad'].map(async (pad) => { if (await padManager.doesPadExist(pad)) (await padManager.getPad(pad)).remove(); })); }); afterEach(async () => { + Object.assign(settings, settingsBackup); if (socket) socket.close(); socket = null; }); From 13252c955c1aafe2f860c4875f8f6c06b19b4301 Mon Sep 17 00:00:00 2001 From: John McLear Date: Wed, 16 Sep 2020 19:58:44 +0100 Subject: [PATCH 016/315] include hash auth in tests --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 7b0ed03ae..1622e0afe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ before_install: install: - "bin/installDeps.sh" - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" + - "npm install ep_hash_auth" script: - "tests/frontend/travis/runner.sh" From 45ec8326f0361dd1a5d0086fb58ce4feacd4272a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 12 Sep 2020 23:51:32 -0400 Subject: [PATCH 017/315] Add a new 'rejected' disconnect reason This reason will be used in a future commit that will reject erroneous messages. --- src/locales/en.json | 3 +++ src/static/js/pad_connectionstatus.js | 23 ++++++++++++++++++++--- src/templates/pad.html | 5 +++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index 33c30b50a..4e9fb33de 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -90,6 +90,9 @@ "pad.modals.rateLimited": "Rate Limited.", "pad.modals.rateLimited.explanation": "You sent too many messages to this pad so it disconnected you.", + "pad.modals.rejected.explanation": "The server rejected a message that was sent by your browser.", + "pad.modals.rejected.cause": "The server may have been updated while you were viewing the pad, or maybe there is a bug in Etherpad. Try reloading the page.", + "pad.modals.disconnected": "You have been disconnected.", "pad.modals.disconnected.explanation": "The connection to the server was lost", "pad.modals.disconnected.cause": "The server may be unavailable. Please notify the service administrator if this continues to happen.", diff --git a/src/static/js/pad_connectionstatus.js b/src/static/js/pad_connectionstatus.js index ad9fe0855..a27243322 100644 --- a/src/static/js/pad_connectionstatus.js +++ b/src/static/js/pad_connectionstatus.js @@ -63,9 +63,26 @@ var padconnectionstatus = (function() what: 'disconnected', why: msg }; - var k = String(msg); // known reason why - if (!(k == 'userdup' || k == 'deleted' || k == 'looping' || k == 'slowcommit' || k == 'initsocketfail' || k == 'unauth' || k == 'rateLimited' || k == 'badChangeset' || k == 'corruptPad')) - { + + // These message IDs correspond to localized strings that are presented to the user. If a new + // message ID is added here then a new div must be added to src/templates/pad.html and the + // corresponding l10n IDs must be added to the language files in src/locales. + const knownReasons = [ + 'badChangeset', + 'corruptPad', + 'deleted', + 'disconnected', + 'initsocketfail', + 'looping', + 'rateLimited', + 'rejected', + 'slowcommit', + 'unauth', + 'userdup', + ]; + let k = String(msg); + if (knownReasons.indexOf(k) === -1) { + // Fall back to a generic message. k = 'disconnected'; } diff --git a/src/templates/pad.html b/src/templates/pad.html index bd718c77d..49aa20204 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -283,6 +283,11 @@

+
+

+

+

+
<% e.begin_block("disconnected"); %>

From 3365e944bf61f1e4e0a29623f0c42554ee269149 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 16 Sep 2020 21:06:15 -0400 Subject: [PATCH 018/315] async-ify more functions, and await completion Where feasible I put the await at the end of the function to minimize the impact on latency. My motivation for this change: Eliminate a race condition in tests I am writing. --- bin/rebuildPad.js | 3 +- src/node/db/API.js | 37 ++++++------- src/node/db/AuthorManager.js | 2 +- src/node/db/Pad.js | 76 +++++++++++++++------------ src/node/db/PadManager.js | 5 +- src/node/handler/ImportHandler.js | 4 +- src/node/handler/PadMessageHandler.js | 12 +++-- src/node/utils/ImportHtml.js | 9 ++-- 8 files changed, 78 insertions(+), 70 deletions(-) diff --git a/bin/rebuildPad.js b/bin/rebuildPad.js index 0013718a9..313510ccd 100644 --- a/bin/rebuildPad.js +++ b/bin/rebuildPad.js @@ -110,8 +110,7 @@ async.series([ // Save the source pad db.db.set("pad:"+newPadId, newPad, function(err) { console.log("Created: Source Pad: pad:" + newPadId); - newPad.saveToDatabase(); - callback(); + newPad.saveToDatabase().then(() => callback(), callback); }); } ], function (err) { diff --git a/src/node/db/API.js b/src/node/db/API.js index 209461114..9f5b786f9 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -203,11 +203,10 @@ exports.setText = async function(padID, text) // get the pad let pad = await getPadSafe(padID, true); - // set the text - pad.setText(text); - - // update the clients on the pad - padMessageHandler.updatePadClients(pad); + await Promise.all([ + pad.setText(text), + padMessageHandler.updatePadClients(pad), + ]); } /** @@ -226,12 +225,11 @@ exports.appendText = async function(padID, text) throw new customError("text is not a string", "apierror"); } - // get and update the pad let pad = await getPadSafe(padID, true); - pad.appendText(text); - - // update the clients on the pad - padMessageHandler.updatePadClients(pad); + await Promise.all([ + pad.appendText(text), + padMessageHandler.updatePadClients(pad), + ]); } /** @@ -287,7 +285,7 @@ exports.setHTML = async function(padID, html) // add a new changeset with the new html to the pad try { - importHtml.setPadHTML(pad, cleanText(html)); + await importHtml.setPadHTML(pad, cleanText(html)); } catch (e) { throw new customError("HTML is malformed", "apierror"); } @@ -373,7 +371,7 @@ exports.appendChatMessage = async function(padID, text, authorID, time) // @TODO - missing getPadSafe() call ? // save chat message to database and send message to all connected clients - padMessageHandler.sendChatMessageToPadClients(time, authorID, text, padID); + await padMessageHandler.sendChatMessageToPadClients(time, authorID, text, padID); } /*****************/ @@ -454,7 +452,7 @@ exports.saveRevision = async function(padID, rev) } let author = await authorManager.createAuthor('API'); - pad.addSavedRevision(rev, author.authorID, 'Saved through API call'); + await pad.addSavedRevision(rev, author.authorID, 'Saved through API call'); } /** @@ -575,11 +573,10 @@ exports.restoreRevision = async function(padID, rev) var changeset = builder.toString(); - // append the changeset - pad.appendRevision(changeset); - - // update the clients on the pad - padMessageHandler.updatePadClients(pad); + await Promise.all([ + pad.appendRevision(changeset), + padMessageHandler.updatePadClients(pad), + ]); } /** @@ -688,7 +685,7 @@ exports.setPublicStatus = async function(padID, publicStatus) } // set the password - pad.setPublicStatus(publicStatus); + await pad.setPublicStatus(publicStatus); } /** @@ -726,7 +723,7 @@ exports.setPassword = async function(padID, password) let pad = await getPadSafe(padID, true); // set the password - pad.setPassword(password == "" ? null : password); + await pad.setPassword(password === '' ? null : password); } /** diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index a17952248..2723717db 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -245,6 +245,6 @@ exports.removePad = async function(authorID, padID) if (author.padIDs !== null) { // remove pad from author delete author.padIDs[padID]; - db.set("globalAuthor:" + authorID, author); + await db.set('globalAuthor:' + authorID, author); } } diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index 33cc38bcb..85188d2b6 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -71,7 +71,7 @@ Pad.prototype.getPublicStatus = function getPublicStatus() { return this.publicStatus; }; -Pad.prototype.appendRevision = function appendRevision(aChangeset, author) { +Pad.prototype.appendRevision = async function appendRevision(aChangeset, author) { if (!author) { author = ''; } @@ -97,12 +97,14 @@ Pad.prototype.appendRevision = function appendRevision(aChangeset, author) { newRevData.meta.atext = this.atext; } - db.set("pad:" + this.id + ":revs:" + newRev, newRevData); - this.saveToDatabase(); + const p = [ + db.set('pad:' + this.id + ':revs:' + newRev, newRevData), + this.saveToDatabase(), + ]; // set the author to pad if (author) { - authorManager.addPad(author, this.id); + p.push(authorManager.addPad(author, this.id)); } if (this.head == 0) { @@ -110,10 +112,12 @@ Pad.prototype.appendRevision = function appendRevision(aChangeset, author) { } else { hooks.callAll("padUpdate", {'pad':this, 'author': author}); } + + await Promise.all(p); }; // save all attributes to the database -Pad.prototype.saveToDatabase = function saveToDatabase() { +Pad.prototype.saveToDatabase = async function saveToDatabase() { var dbObject = {}; for (var attr in this) { @@ -127,7 +131,7 @@ Pad.prototype.saveToDatabase = function saveToDatabase() { } } - db.set("pad:" + this.id, dbObject); + await db.set('pad:' + this.id, dbObject); } // get time of last edit (changeset application) @@ -244,7 +248,7 @@ Pad.prototype.text = function text() { return this.atext.text; }; -Pad.prototype.setText = function setText(newText) { +Pad.prototype.setText = async function setText(newText) { // clean the new text newText = exports.cleanText(newText); @@ -261,10 +265,10 @@ Pad.prototype.setText = function setText(newText) { } // append the changeset - this.appendRevision(changeset); + await this.appendRevision(changeset); }; -Pad.prototype.appendText = function appendText(newText) { +Pad.prototype.appendText = async function appendText(newText) { // clean the new text newText = exports.cleanText(newText); @@ -274,14 +278,16 @@ Pad.prototype.appendText = function appendText(newText) { var changeset = Changeset.makeSplice(oldText, oldText.length, 0, newText); // append the changeset - this.appendRevision(changeset); + await this.appendRevision(changeset); }; -Pad.prototype.appendChatMessage = function appendChatMessage(text, userId, time) { +Pad.prototype.appendChatMessage = async function appendChatMessage(text, userId, time) { this.chatHead++; // save the chat entry in the database - db.set("pad:" + this.id + ":chat:" + this.chatHead, { "text": text, "userId": userId, "time": time }); - this.saveToDatabase(); + await Promise.all([ + db.set('pad:' + this.id + ':chat:' + this.chatHead, {text, userId, time}), + this.saveToDatabase(), + ]); }; Pad.prototype.getChatMessage = async function getChatMessage(entryNum) { @@ -350,7 +356,7 @@ Pad.prototype.init = async function init(text) { // this pad doesn't exist, so create it let firstChangeset = Changeset.makeSplice("\n", 0, 0, exports.cleanText(text)); - this.appendRevision(firstChangeset, ''); + await this.appendRevision(firstChangeset, ''); } hooks.callAll("padLoad", { 'pad': this }); @@ -366,7 +372,7 @@ Pad.prototype.copy = async function copy(destinationID, force) { // padMessageHandler.kickSessionsFromPad(sourceID); // flush the source pad: - this.saveToDatabase(); + await this.saveToDatabase(); try { @@ -532,6 +538,7 @@ Pad.prototype.copyPadWithoutHistory = async function copyPadWithoutHistory(desti Pad.prototype.remove = async function remove() { var padID = this.id; + const p = []; // kick everyone from this pad padMessageHandler.kickSessionsFromPad(padID); @@ -552,44 +559,45 @@ Pad.prototype.remove = async function remove() { delete group.pads[padID]; // set the new value - db.set("group:" + groupID, group); + p.push(db.set('group:' + groupID, group)); } // remove the readonly entries - let readonlyID = readOnlyManager.getReadOnlyId(padID); - - db.remove("pad2readonly:" + padID); - db.remove("readonly2pad:" + readonlyID); + p.push(readOnlyManager.getReadOnlyId(padID).then(async (readonlyID) => { + await db.remove('readonly2pad:' + readonlyID); + })); + p.push(db.remove('pad2readonly:' + padID)); // delete all chat messages - promises.timesLimit(this.chatHead + 1, 500, function (i) { - return db.remove("pad:" + padID + ":chat:" + i, null); - }) + p.push(promises.timesLimit(this.chatHead + 1, 500, async (i) => { + await db.remove('pad:' + padID + ':chat:' + i, null); + })); // delete all revisions - promises.timesLimit(this.head + 1, 500, function (i) { - return db.remove("pad:" + padID + ":revs:" + i, null); - }) + p.push(promises.timesLimit(this.head + 1, 500, async (i) => { + await db.remove('pad:' + padID + ':revs:' + i, null); + })); // remove pad from all authors who contributed this.getAllAuthors().forEach(authorID => { - authorManager.removePad(authorID, padID); + p.push(authorManager.removePad(authorID, padID)); }); // delete the pad entry and delete pad from padManager - padManager.removePad(padID); + p.push(padManager.removePad(padID)); hooks.callAll("padRemove", { padID }); + await Promise.all(p); } // set in db -Pad.prototype.setPublicStatus = function setPublicStatus(publicStatus) { +Pad.prototype.setPublicStatus = async function setPublicStatus(publicStatus) { this.publicStatus = publicStatus; - this.saveToDatabase(); + await this.saveToDatabase(); }; -Pad.prototype.setPassword = function setPassword(password) { +Pad.prototype.setPassword = async function setPassword(password) { this.passwordHash = password == null ? null : hash(password, generateSalt()); - this.saveToDatabase(); + await this.saveToDatabase(); }; Pad.prototype.isCorrectPassword = function isCorrectPassword(password) { @@ -600,7 +608,7 @@ Pad.prototype.isPasswordProtected = function isPasswordProtected() { return this.passwordHash != null; }; -Pad.prototype.addSavedRevision = function addSavedRevision(revNum, savedById, label) { +Pad.prototype.addSavedRevision = async function addSavedRevision(revNum, savedById, label) { // if this revision is already saved, return silently for (var i in this.savedRevisions) { if (this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum) { @@ -618,7 +626,7 @@ Pad.prototype.addSavedRevision = function addSavedRevision(revNum, savedById, la // save this new saved revision this.savedRevisions.push(savedRevision); - this.saveToDatabase(); + await this.saveToDatabase(); }; Pad.prototype.getSavedRevisions = function getSavedRevisions() { diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index 89e6a84ab..adbbb79e2 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -200,10 +200,11 @@ exports.isValidPadId = function(padId) /** * Removes the pad from database and unloads it. */ -exports.removePad = function(padId) { - db.remove("pad:" + padId); +exports.removePad = async (padId) => { + const p = db.remove('pad:' + padId); exports.unloadPad(padId); padList.removePad(padId); + await p; } // removes a pad from the cache diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index 6ea76cac3..0f076f68f 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -226,12 +226,12 @@ async function doImport(req, res, padId) if (!req.directDatabaseAccess) { if (importHandledByPlugin || useConvertor || fileIsHTML) { try { - importHtml.setPadHTML(pad, text); + await importHtml.setPadHTML(pad, text); } catch (e) { apiLogger.warn("Error importing, possibly caused by malformed HTML"); } } else { - pad.setText(text); + await pad.setText(text); } } diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 6f3cab8a5..879d40587 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -294,7 +294,7 @@ async function handleSaveRevisionMessage(client, message) var userId = sessioninfos[client.id].author; let pad = await padManager.getPad(padId); - pad.addSavedRevision(pad.head, userId); + await pad.addSavedRevision(pad.head, userId); } /** @@ -365,7 +365,7 @@ exports.sendChatMessageToPadClients = async function(time, userId, text, padId) let userName = await authorManager.getAuthorName(userId); // save the chat message - pad.appendChatMessage(text, userId, time); + const promise = pad.appendChatMessage(text, userId, time); let msg = { type: "COLLABROOM", @@ -374,6 +374,8 @@ exports.sendChatMessageToPadClients = async function(time, userId, text, padId) // broadcast the chat message to everyone on the pad socketio.sockets.in(padId).json.send(msg); + + await promise; } /** @@ -664,7 +666,7 @@ async function handleUserChanges(data) } try { - pad.appendRevision(changeset, thisSession.author); + await pad.appendRevision(changeset, thisSession.author); } catch(e) { client.json.send({ disconnect: "badChangeset" }); stats.meter('failedChangesets').mark(); @@ -673,13 +675,13 @@ async function handleUserChanges(data) let correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool); if (correctionChangeset) { - pad.appendRevision(correctionChangeset); + await pad.appendRevision(correctionChangeset); } // Make sure the pad always ends with an empty line. if (pad.text().lastIndexOf("\n") !== pad.text().length-1) { var nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length - 1, 0, "\n"); - pad.appendRevision(nlChangeset); + await pad.appendRevision(nlChangeset); } await exports.updatePadClients(pad); diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index 28ce4f689..8cbfdbab9 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -22,8 +22,7 @@ const rehype = require("rehype") const format = require("rehype-format") -exports.setPadHTML = function(pad, html) -{ +exports.setPadHTML = async (pad, html) => { var apiLogger = log4js.getLogger("ImportHtml"); var opts = { @@ -103,6 +102,8 @@ exports.setPadHTML = function(pad, html) var theChangeset = builder.toString(); apiLogger.debug('The changeset: ' + theChangeset); - pad.setText("\n"); - pad.appendRevision(theChangeset); + await Promise.all([ + pad.setText('\n'), + pad.appendRevision(theChangeset), + ]); } From 6011ef426f1207b0adcc909172e9ba679033e679 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 2 Sep 2020 20:00:14 -0400 Subject: [PATCH 019/315] PadMessageHandler: Make sessioninfo tracking more robust A session's sessioninfo could go away asynchronously due to a disconnect. Grab a reference once and use it throughout the function to avoid dereferencing a null sessioninfo object. --- src/node/handler/PadMessageHandler.js | 81 ++++++++++++--------------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 879d40587..f6d72764b 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -192,7 +192,7 @@ exports.handleMessage = async function(client, message) return; } - let thisSession = sessioninfos[client.id]; + const thisSession = sessioninfos[client.id]; if (!thisSession) { messageLogger.warn("Dropped message from an unknown connection.") @@ -210,25 +210,23 @@ exports.handleMessage = async function(client, message) return; } - if (message.type === "CLIENT_READY") { - // client tried to auth for the first time (first msg from the client) - createSessionInfoAuth(client, message); - } - - // the session may have been dropped during earlier processing - if (!sessioninfos[client.id]) { + // Drop the message if the client disconnected while the hooks were running. + if (sessioninfos[client.id] !== thisSession) { messageLogger.warn("Dropping message from a connection that has gone away.") return; } - // Simulate using the load testing tool - if (!sessioninfos[client.id].auth) { + if (message.type === "CLIENT_READY") { + // client tried to auth for the first time (first msg from the client) + createSessionInfoAuth(thisSession, 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.") return; } - let auth = sessioninfos[client.id].auth; - // check if pad is requested via readOnly let padId = auth.padID; @@ -549,10 +547,14 @@ async function handleUserChanges(data) return; } + // The client might disconnect between our callbacks. We should still + // finish processing the changeset, so keep a reference to the session. + const thisSession = sessioninfos[client.id]; + // TODO: this might happen with other messages too => find one place to copy the session // and always use the copy. atm a message will be ignored if the session is gone even // if the session was valid when the message arrived in the first place - if (!sessioninfos[client.id]) { + if (!thisSession) { messageLogger.warn("Dropped message, disconnect happened in the mean time"); return; } @@ -562,10 +564,6 @@ async function handleUserChanges(data) var wireApool = (new AttributePool()).fromJsonable(message.data.apool); var changeset = message.data.changeset; - // The client might disconnect between our callbacks. We should still - // finish processing the changeset, so keep a reference to the session. - var thisSession = sessioninfos[client.id]; - // Measure time to process edit var stopWatch = stats.timer('edits').start(); @@ -808,13 +806,11 @@ function _correctMarkersInPad(atext, apool) { function handleSwitchToPad(client, message) { // clear the session and leave the room - let currentSession = sessioninfos[client.id]; - let padId = currentSession.padId; - let roomClients = _getRoomClients(padId); - - roomClients.forEach(client => { + const currentSessionInfo = sessioninfos[client.id]; + const padId = currentSessionInfo.padId; + _getRoomClients(padId).forEach(client => { let sinfo = sessioninfos[client.id]; - if (sinfo && sinfo.author === currentSession.author) { + if (sinfo && sinfo.author === currentSessionInfo.author) { // fix user's counter, works on page refresh or if user closes browser window and then rejoins sessioninfos[client.id] = {}; client.leave(padId); @@ -822,25 +818,24 @@ function handleSwitchToPad(client, message) }); // start up the new pad - createSessionInfoAuth(client, message); + const newSessionInfo = sessioninfos[client.id]; + createSessionInfoAuth(newSessionInfo, message); handleClientReady(client, message); } -// Creates/replaces the auth object in the client's session info. Session info for the client must -// already exist. -function createSessionInfoAuth(client, message) +// Creates/replaces the auth object in the given session info. +function 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 // the sessionId of this connection is still valid // since it could have been deleted by the API. - sessioninfos[client.id].auth = - { + sessionInfo.auth = { sessionID: message.sessionID, padID: message.padId, - token : message.token, - password: message.password + token: message.token, + password: message.password, }; } @@ -929,9 +924,8 @@ async function handleClientReady(client, message) // glue the clientVars together, send them and tell the other clients that a new one is there // Check that the client is still here. It might have disconnected between callbacks. - if (sessioninfos[client.id] === undefined) { - return; - } + const sessionInfo = sessioninfos[client.id]; + if (sessionInfo == null) return; // Check if this author is already on the pad, if yes, kick the other sessions! let roomClients = _getRoomClients(pad.id); @@ -947,9 +941,9 @@ async function handleClientReady(client, message) } // Save in sessioninfos that this session belonges to this pad - sessioninfos[client.id].padId = padIds.padId; - sessioninfos[client.id].readOnlyPadId = padIds.readOnlyPadId; - sessioninfos[client.id].readonly = padIds.readonly; + sessionInfo.padId = padIds.padId; + sessionInfo.readOnlyPadId = padIds.readOnlyPadId; + sessionInfo.readonly = padIds.readonly; // Log creation/(re-)entering of a pad let ip = remoteAddress[client.id]; @@ -971,7 +965,7 @@ async function handleClientReady(client, message) client.join(padIds.padId); // Save the revision in sessioninfos, we take the revision from the info the client send to us - sessioninfos[client.id].rev = message.client_rev; + sessionInfo.rev = message.client_rev; // During the client reconnect, client might miss some revisions from other clients. By using client revision, // this below code sends all the revisions missed during the client reconnect @@ -1131,9 +1125,9 @@ async function handleClientReady(client, message) client.json.send({type: "CLIENT_VARS", data: clientVars}); // Save the current revision in sessioninfos, should be the same as in clientVars - sessioninfos[client.id].rev = pad.getHeadRevisionNumber(); + sessionInfo.rev = pad.getHeadRevisionNumber(); - sessioninfos[client.id].author = authorID; + sessionInfo.author = authorID; // prepare the notification for the other users on the pad, that this user joined let messageToTheOtherUsers = { @@ -1168,12 +1162,11 @@ async function handleClientReady(client, message) // Since sessioninfos might change while being enumerated, check if the // sessionID is still assigned to a valid session - if (sessioninfos[roomClient.id] === undefined) { - return; - } + const sessionInfo = sessioninfos[roomClient.id]; + if (sessionInfo == null) return; // get the authorname & colorId - let author = sessioninfos[roomClient.id].author; + let author = sessionInfo.author; let cached = historicalAuthorData[author]; // reuse previously created cache of author's data From ca7b8e278fd8edfede9e1b75d023629ce7557ffb Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 22 Sep 2020 15:12:25 +0100 Subject: [PATCH 020/315] allow slower for Safari --- tests/frontend/specs/responsiveness.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/frontend/specs/responsiveness.js b/tests/frontend/specs/responsiveness.js index e6552642f..e8eb4c604 100644 --- a/tests/frontend/specs/responsiveness.js +++ b/tests/frontend/specs/responsiveness.js @@ -35,7 +35,7 @@ describe('Responsiveness of Editor', function() { var amount = 200000; //number of blocks of chars we will insert var length = (amount * (chars.length) +1); // include a counter for each space var text = ''; // the text we're gonna insert - this.timeout(amount * 100); + this.timeout(amount * 120); // Changed from 100 to 120 to allow Mac OSX Safari to be slow. // get keys to send var keyMultiplier = 10; // multiplier * 10 == total number of key events From 0f6baac7b55022efba309433344d9539108ea43b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 22 Sep 2020 17:47:26 -0400 Subject: [PATCH 021/315] Revert "tests: Use wtfnode to determine why mocha isn't exiting" (#4315) This reverts commit ae1142a799b91f20c9a54c233b8c1a80ca99d0a5. According to https://github.com/ether/etherpad-lite/pull/4304#issuecomment-694833456 wtfnode always seems to exit with 0 even if the tests fail. --- src/package-lock.json | 6 ------ src/package.json | 5 ++--- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 3c6a019d8..6b9b9b0c8 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -8835,12 +8835,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz", "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==" }, - "wtfnode": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.8.3.tgz", - "integrity": "sha512-Ll7iH8MbRQTE+QTw20Xax/0PM5VeSVSOhsmoR3+knWuJkEWTV5d9yPO6Sb+IDbt9I4UCrKpvHuF9T9zteRNOuA==", - "dev": true - }, "xml2js": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", diff --git a/src/package.json b/src/package.json index b2d87f40a..9575ab117 100644 --- a/src/package.json +++ b/src/package.json @@ -80,8 +80,7 @@ "nyc": "15.0.1", "set-cookie-parser": "^2.4.6", "supertest": "4.0.2", - "wd": "1.12.1", - "wtfnode": "^0.8.3" + "wd": "1.12.1" }, "engines": { "node": ">=10.13.0", @@ -92,7 +91,7 @@ "url": "https://github.com/ether/etherpad-lite.git" }, "scripts": { - "test": "nyc wtfnode node_modules/.bin/_mocha --timeout 30000 --recursive ../tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", + "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" }, "version": "1.8.6", From 1bb44098df5b1292e058b6dba5a695946805990a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 10 Sep 2020 13:27:59 -0400 Subject: [PATCH 022/315] PadMessageHandler: Move handleMessage hooks after access check Move the handleMessageSecurity and handleMessage hooks after the call to securityManager.checkAccess. Benefits: * A handleMessage plugin can safely assume the message will be handled unless the plugin itself drops the message, so it doesn't need to repeat the access checks done by the `handleMessage` function. * This paves the way for a future enhancement: pass the author ID to the hooks. Note: The handleMessageSecurity hook is broken in several ways: * The hook result is ignored for `CLIENT_READY` and `SWITCH_TO_PAD` messages because the `handleClientReady` function overwrites the hook result. This causes the client to receive client vars with `readonly` set to true, which causes the client to display an immutable pad even though the pad is technically writable. * The formatting toolbar buttons are removed for read-only pads before the handleMessageSecurity hook even runs. * It is awkwardly named: Without reading the documentation, how is one supposed to know that "handle message security" actually means "grant one-time write access to a read-only pad"? * It is called for every message even though calls after a `CLIENT_READY` or `SWITCH_TO_PAD` are mostly pointless. * Why would anyone want to grant write access when the user visits a read-only pad URL? The user should just visit the writable pad URL instead. * Why would anyone want to grant write access that only lasts for a single socket.io connection? * There are better ways to temporarily grant write access (e.g., the authorize hook). * This hook is inviting bugs because it breaks a core assumption about `/p/r.*` URLs. I think the hook should be deprecated and eventually removed. --- doc/api/hooks_server-side.md | 7 ------ src/node/handler/PadMessageHandler.js | 33 ++++++++++++--------------- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 0a4f95181..dd0b8599e 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -399,9 +399,6 @@ The handleMessage function must return a Promise. If the Promise resolves to `null`, the message is dropped. Returning `callback(value)` will return a Promise that is resolved to `value`. -**WARNING:** handleMessage is called for every message, even if the client is -not authorized to send the message. It is up to the plugin to check permissions. - Examples: ``` @@ -444,10 +441,6 @@ The handleMessageSecurity function must return a Promise. If the Promise resolves to `true`, write access is granted as described above. Returning `callback(value)` will return a Promise that is resolved to `value`. -**WARNING:** handleMessageSecurity is called for every message, even if the -client is not authorized to send the message. It is up to the plugin to check -permissions. - Examples: ``` diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index f6d72764b..e311e9dbf 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -199,23 +199,6 @@ exports.handleMessage = async function(client, message) return; } - // Allow plugins to bypass the readonly message blocker - if ((await hooks.aCallAll('handleMessageSecurity', {client, message})).some((w) => w === true)) { - thisSession.readonly = false; - } - - // Call handleMessage hook. If a plugin returns null, the message will be dropped. Note that for - // all messages handleMessage will be called, even if the client is not authorized - if ((await hooks.aCallAll('handleMessage', {client, message})).some((m) => m === null)) { - return; - } - - // Drop the message if the client disconnected while the hooks were running. - if (sessioninfos[client.id] !== thisSession) { - messageLogger.warn("Dropping message from a connection that has gone away.") - return; - } - if (message.type === "CLIENT_READY") { // client tried to auth for the first time (first msg from the client) createSessionInfoAuth(thisSession, message); @@ -245,7 +228,21 @@ exports.handleMessage = async function(client, message) return; } - // access was granted + // Allow plugins to bypass the readonly message blocker + if ((await hooks.aCallAll('handleMessageSecurity', {client, message})).some((w) => w === true)) { + thisSession.readonly = false; + } + + // Call handleMessage hook. If a plugin returns null, the message will be dropped. + if ((await hooks.aCallAll('handleMessage', {client, message})).some((m) => m === null)) { + return; + } + + // Drop the message if the client disconnected during the above processing. + if (sessioninfos[client.id] !== thisSession) { + messageLogger.warn('Dropping message from a connection that has gone away.') + return; + } // Check what type of message we get and delegate to the other methods if (message.type === "CLIENT_READY") { From ff4da04907f0ab63b61a89d8f1ebbf0392207505 Mon Sep 17 00:00:00 2001 From: John McLear Date: Wed, 23 Sep 2020 09:22:08 +0100 Subject: [PATCH 023/315] no need to ask for translations if no template files are included --- bin/plugins/checkPlugin.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/plugins/checkPlugin.js b/bin/plugins/checkPlugin.js index 0fccb4f12..826dcbc11 100755 --- a/bin/plugins/checkPlugin.js +++ b/bin/plugins/checkPlugin.js @@ -182,7 +182,8 @@ fs.readdir(pluginPath, function (err, rootFiles) { } } - if(files.indexOf("locales") === -1){ + // if we include templates but don't have translations... + if(files.indexOf("templates") !== -1 && files.indexOf("locales") === -1){ console.warn("Translations not found, please create. Translation files help with Etherpad accessibility."); } From 53fd0b4f98bacbd075c4b8a1315d45dc3769c727 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 26 Aug 2020 22:08:07 -0400 Subject: [PATCH 024/315] webaccess: Return 401 for authn failure, 403 for authz failure This makes it possible for reverse proxies to transform 403 errors into something like "upgrade to a premium account to access this pad". Also add some webaccess tests. --- doc/api/hooks_server-side.md | 3 +- src/node/hooks/express/webaccess.js | 34 +++--- src/node/server.js | 2 +- tests/backend/specs/webaccess.js | 166 ++++++++++++++++++++++++++++ 4 files changed, 188 insertions(+), 17 deletions(-) create mode 100644 tests/backend/specs/webaccess.js diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index dd0b8599e..370e782ed 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -370,7 +370,8 @@ A plugin's authFailure function is only called if all of the following are true: Calling the provided callback with `[true]` tells Etherpad that the failure was handled and no further error handling is required. Calling the callback with `[]` or `undefined` defers error handling to the next authFailure plugin (if -any, otherwise fall back to HTTP basic authentication). +any, otherwise fall back to HTTP basic authentication for an authentication +failure or a generic 403 page for an authorization failure). Example: diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index b83fbbd00..fe5a40535 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -58,19 +58,6 @@ exports.checkAccess = (req, res, next) => { hooks.aCallFirst('authorize', {req, res, next, resource: req.path}, hookResultMangle(grant)); }; - /* Authentication OR authorization failed. */ - const failure = () => { - return hooks.aCallFirst('authFailure', {req, res, next}, hookResultMangle((ok) => { - if (ok) return; - // No plugin handled the authn/authz failure. Fall back to basic authentication. - res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); - // Delay the error response for 1s to slow down brute force attacks. - setTimeout(() => { - res.status(401).send('Authentication Required'); - }, 1000); - })); - }; - // Access checking is done in three steps: // // 1) Try to just access the thing. If access fails (perhaps authentication has not yet completed, @@ -78,7 +65,7 @@ exports.checkAccess = (req, res, next) => { // 2) Try to authenticate. (Or, if already logged in, reauthenticate with different credentials if // supported by the authn scheme.) If authentication fails, give the user a 401 error to // request new credentials. Otherwise, go to the next step. - // 3) Try to access the thing again. If this fails, give the user a 401 error. + // 3) Try to access the thing again. If this fails, give the user a 403 error. // // Plugins can use the 'next' callback (from the hook's context) to break out at any point (e.g., // to process an OAuth callback). Plugins can use the authFailure hook to override the default @@ -103,6 +90,17 @@ exports.checkAccess = (req, res, next) => { } hooks.aCallFirst('authenticate', ctx, hookResultMangle((ok) => { if (!ok) { + const failure = () => { + return hooks.aCallFirst('authFailure', {req, res, next}, hookResultMangle((ok) => { + if (ok) return; + // No plugin handled the authentication failure. Fall back to basic authentication. + res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); + // Delay the error response for 1s to slow down brute force attacks. + setTimeout(() => { + res.status(401).send('Authentication Required'); + }, 1000); + })); + }; // Fall back to HTTP basic auth. if (!httpBasicAuth) return failure(); if (!(ctx.username in settings.users)) { @@ -126,7 +124,13 @@ exports.checkAccess = (req, res, next) => { })); }; - step3Authorize = () => authorize(failure); + step3Authorize = () => authorize(() => { + return hooks.aCallFirst('authFailure', {req, res, next}, hookResultMangle((ok) => { + if (ok) return; + // No plugin handled the authorization failure. + res.status(403).send('Forbidden'); + })); + }); step1PreAuthenticate(); }; diff --git a/src/node/server.js b/src/node/server.js index c9ef33cc9..a8a567179 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -45,7 +45,7 @@ let started = false; let stopped = false; exports.start = async () => { - if (started) return; + if (started) return express.server; started = true; if (stopped) throw new Error('restart not supported'); diff --git a/tests/backend/specs/webaccess.js b/tests/backend/specs/webaccess.js new file mode 100644 index 000000000..8367429f0 --- /dev/null +++ b/tests/backend/specs/webaccess.js @@ -0,0 +1,166 @@ +function m(mod) { return __dirname + '/../../../src/' + mod; } + +const assert = require('assert').strict; +const log4js = require(m('node_modules/log4js')); +const plugins = require(m('static/js/pluginfw/plugin_defs')); +const server = require(m('node/server')); +const settings = require(m('node/utils/Settings')); +const supertest = require(m('node_modules/supertest')); + +let agent; +const logger = log4js.getLogger('test'); + +before(async function() { + settings.port = 0; + settings.ip = 'localhost'; + const httpServer = await server.start(); + const baseUrl = `http://localhost:${httpServer.address().port}`; + logger.debug(`HTTP server at ${baseUrl}`); + agent = supertest(baseUrl); +}); + +after(async function() { + await server.stop(); +}); + +describe('webaccess without any plugins', function() { + const backup = {}; + + before(async function() { + Object.assign(backup, settings); + settings.users = { + admin: {password: 'admin-password', is_admin: true}, + user: {password: 'user-password'}, + }; + }); + + after(async function() { + Object.assign(settings, backup); + }); + + it('!authn !authz anonymous / -> 200', async function() { + settings.requireAuthentication = false; + settings.requireAuthorization = false; + await agent.get('/').expect(200); + }); + it('!authn !authz anonymous /admin/ -> 401', async function() { + settings.requireAuthentication = false; + settings.requireAuthorization = false; + await agent.get('/admin/').expect(401); + }); + it('authn !authz anonymous / -> 401', async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + await agent.get('/').expect(401); + }); + it('authn !authz user / -> 200', async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + await agent.get('/').auth('user', 'user-password').expect(200); + }); + it('authn !authz user /admin/ -> 403', async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + await agent.get('/admin/').auth('user', 'user-password').expect(403); + }); + it('authn !authz admin / -> 200', async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + await agent.get('/').auth('admin', 'admin-password').expect(200); + }); + it('authn !authz admin /admin/ -> 200', async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + await agent.get('/admin/').auth('admin', 'admin-password').expect(200); + }); + it('authn authz user / -> 403', async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + await agent.get('/').auth('user', 'user-password').expect(403); + }); + it('authn authz user /admin/ -> 403', async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + await agent.get('/admin/').auth('user', 'user-password').expect(403); + }); + it('authn authz admin / -> 200', async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + await agent.get('/').auth('admin', 'admin-password').expect(200); + }); + it('authn authz admin /admin/ -> 200', async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + await agent.get('/admin/').auth('admin', 'admin-password').expect(200); + }); +}); + +describe('webaccess with authFailure plugin', function() { + let handle, returnUndef, status, called; + const authFailure = (hookName, context, cb) => { + assert.equal(hookName, 'authFailure'); + assert(context != null); + assert(context.req != null); + assert(context.res != null); + assert(context.next != null); + assert(!called); + called = true; + if (handle) { + context.res.status(status).send('injected content'); + return cb([true]); + } + if (returnUndef) return cb(); + return cb([]); + }; + + const settingsBackup = {}; + let authFailureHooksBackup; + before(function() { + Object.assign(settingsBackup, settings); + authFailureHooksBackup = plugins.hooks.authFailure; + plugins.hooks.authFailure = [{hook_fn: authFailure}]; + settings.requireAuthentication = true; + settings.requireAuthorization = true; + settings.users = { + admin: {password: 'admin-password', is_admin: true}, + user: {password: 'user-password'}, + }; + }); + after(function() { + Object.assign(settings, settingsBackup); + plugins.hooks.authFailure = authFailureHooksBackup; + }); + + beforeEach(function() { + handle = false; + returnUndef = false; + status = 200; + called = false; + }); + afterEach(function() { + assert(called); + }); + + it('authn fail, hook handles -> 200', async function() { + handle = true; + await agent.get('/').expect(200, /injected content/); + }); + it('authn fail, hook defers (undefined) -> 401', async function() { + returnUndef = true; + await agent.get('/').expect(401); + }); + it('authn fail, hook defers (empty list) -> 401', async function() { + await agent.get('/').expect(401); + }); + it('authz fail, hook handles -> 200', async function() { + handle = true; + await agent.get('/').auth('user', 'user-password').expect(200, /injected content/); + }); + it('authz fail, hook defers (undefined) -> 403', async function() { + returnUndef = true; + await agent.get('/').auth('user', 'user-password').expect(403); + }); + it('authz fail, hook defers (empty list) -> 403', async function() { + await agent.get('/').auth('user', 'user-password').expect(403); + }); +}); From 94f944160dcf46d161f44aee264656d77bfb169c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 23 Sep 2020 17:13:07 -0400 Subject: [PATCH 025/315] security: Don't require express_sid if authn not required This should make it possible to embed a pad in an iframe from another site as long as `settings.requireAuthentication` is false. --- src/node/hooks/express/socketio.js | 43 ++++++++++++++++-------------- tests/backend/specs/socketio.js | 10 +++++-- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index b1406afd2..ffc280b5c 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -8,6 +8,7 @@ var padMessageHandler = require("../../handler/PadMessageHandler"); var cookieParser = require('cookie-parser'); var sessionModule = require('express-session'); +const util = require('util'); exports.expressCreateServer = function (hook_name, args, cb) { //init socket.io and redirect all requests to the MessageHandler @@ -48,32 +49,34 @@ exports.expressCreateServer = function (hook_name, args, cb) { // check whether the user has authenticated, then any random person on the Internet can read, // modify, or create any pad (unless the pad is password protected or an HTTP API session is // required). - var cookieParserFn = cookieParser(webaccess.secret, {}); - io.use((socket, next) => { - var data = socket.request; - if (!data.headers.cookie) { + const cookieParserFn = util.promisify(cookieParser(webaccess.secret, {})); + const getSession = util.promisify(args.app.sessionStore.get).bind(args.app.sessionStore); + io.use(async (socket, next) => { + const req = socket.request; + if (!req.headers.cookie) { // socketio.js-client on node.js doesn't support cookies (see https://git.io/JU8u9), so the // token and express_sid cookies have to be passed via a query parameter for unit tests. - data.headers.cookie = socket.handshake.query.cookie; + req.headers.cookie = socket.handshake.query.cookie; } - if (!data.headers.cookie && settings.loadTest) { + if (!req.headers.cookie && settings.loadTest) { console.warn('bypassing socket.io authentication check due to settings.loadTest'); return next(null, true); } - const fail = (msg) => { return next(new Error(msg), false); }; - cookieParserFn(data, {}, function(err) { - if (err) return fail('access denied: unable to parse express_sid cookie'); - const expressSid = data.signedCookies.express_sid; - if (!expressSid) return fail ('access denied: signed express_sid cookie is required'); - args.app.sessionStore.get(expressSid, (err, session) => { - if (err || !session) return fail('access denied: bad session or session has expired'); - data.session = new sessionModule.Session(data, session); - if (settings.requireAuthentication && data.session.user == null) { - return fail('access denied: authentication required'); - } - next(null, true); - }); - }); + try { + await cookieParserFn(req, {}); + const expressSid = req.signedCookies.express_sid; + const needAuthn = settings.requireAuthentication; + if (needAuthn && !expressSid) throw new Error('signed express_sid cookie is required'); + if (expressSid) { + const session = await getSession(expressSid); + if (!session) throw new Error('bad session or session has expired'); + req.session = new sessionModule.Session(req, session); + if (needAuthn && req.session.user == null) throw new Error('authentication required'); + } + } catch (err) { + return next(new Error(`access denied: ${err}`), false); + } + return next(null, true); }); // var socketIOLogger = log4js.getLogger("socket.io"); diff --git a/tests/backend/specs/socketio.js b/tests/backend/specs/socketio.js index ba639a79c..8dfd8aecc 100644 --- a/tests/backend/specs/socketio.js +++ b/tests/backend/specs/socketio.js @@ -130,13 +130,19 @@ describe('socket.io access checks', () => { }); // Normal accesses. - it('!authn anonymous /p/pad -> 200, ok', async () => { + it('!authn anonymous cookie /p/pad -> 200, ok', async () => { const res = await client.get('/p/pad').expect(200); // Should not throw. socket = await connect(res); const clientVars = await handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); + it('!authn !cookie -> ok', async () => { + // Should not throw. + socket = await connect(null); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); it('!authn user /p/pad -> 200, ok', async () => { const res = await client.get('/p/pad').auth('user', 'user-password').expect(200); // Should not throw. @@ -160,7 +166,7 @@ describe('socket.io access checks', () => { // Despite the 401, try to create the pad via a socket.io connection anyway. await assert.rejects(connect(res), {message: /authentication required/i}); }); - it('socket.io connection without express-session cookie -> error', async () => { + it('authn !cookie -> error', async () => { settings.requireAuthentication = true; await assert.rejects(connect(null), {message: /signed express_sid cookie is required/i}); }); From 6cde6f5a9897fb210e68b7d833154804abf51b17 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 24 Sep 2020 15:54:49 +0200 Subject: [PATCH 026/315] Localisation updates from https://translatewiki.net. --- src/locales/diq.json | 2 ++ src/locales/fr.json | 2 ++ src/locales/mk.json | 2 ++ src/locales/pms.json | 2 ++ src/locales/pt-br.json | 2 ++ src/locales/sv.json | 2 ++ src/locales/tr.json | 3 +++ src/locales/zh-hant.json | 2 ++ 8 files changed, 17 insertions(+) diff --git a/src/locales/diq.json b/src/locales/diq.json index 8ba96f358..c21ffb62a 100644 --- a/src/locales/diq.json +++ b/src/locales/diq.json @@ -85,6 +85,8 @@ "pad.modals.deleted.explanation": "Ena ped wedariye", "pad.modals.rateLimited": "Nısbeto kemeyeyın", "pad.modals.rateLimited.explanation": "Na pad re ßıma vêşi mesac rışto, coki ra irtibat bıriyayo.", + "pad.modals.rejected.explanation": "Server, terefê browseri ra rışiyaye yew mesac red kerdo.", + "pad.modals.rejected.cause": "Şıma wexto ke ped weyniyayış de server belka biyo rocane ya ziEtherpad de yew xeta bena. Pela reyna bar kerê.", "pad.modals.disconnected": "İrtibata şıma reyê", "pad.modals.disconnected.explanation": "Rovıteri ya irtibata şıma reyyê", "pad.modals.disconnected.cause": "Qay rovıtero nêkarên o. Ena xerpey deqam kena se idarekaranê sistemiya irtibat kewê", diff --git a/src/locales/fr.json b/src/locales/fr.json index 6b567dcbb..e8c9bd910 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -104,6 +104,8 @@ "pad.modals.deleted.explanation": "Ce bloc-notes a été supprimé.", "pad.modals.rateLimited": "Taux limité.", "pad.modals.rateLimited.explanation": "Vous avez envoyé trop de messages à ce bloc, il vous a donc déconnecté.", + "pad.modals.rejected.explanation": "Le serveur a rejeté un message qui a été envoyé par votre navigateur.", + "pad.modals.rejected.cause": "Le serveur peut avoir été mis à jour pendant que vous regardiez le bloc, ou il y a peut-être un bogue dans Etherpad. Essayez de recharger la page.", "pad.modals.disconnected": "Vous avez été déconnecté.", "pad.modals.disconnected.explanation": "La connexion au serveur a échoué.", "pad.modals.disconnected.cause": "Il se peut que le serveur soit indisponible. Si le problème persiste, veuillez en informer l’administrateur du service.", diff --git a/src/locales/mk.json b/src/locales/mk.json index b5d79d1f8..3f492c7d0 100644 --- a/src/locales/mk.json +++ b/src/locales/mk.json @@ -81,6 +81,8 @@ "pad.modals.deleted.explanation": "Оваа тетратка е отстранета.", "pad.modals.rateLimited": "Ограничено по стапка.", "pad.modals.rateLimited.explanation": "Испративте премногу пораки на тетраткава, па затоа таа ве исклучи.", + "pad.modals.rejected.explanation": "Опслужувачот ја отфрли пораката што му беше испратена од вашиот прелистувач.", + "pad.modals.rejected.cause": "Опслужувачот може да бил подновен додека ја гледавте тетратката, или пак Etherpad има некоја грешка. Пробајте со превчитување на страницата.", "pad.modals.disconnected": "Врската е прекината.", "pad.modals.disconnected.explanation": "Врската со опслужувачот е прекината", "pad.modals.disconnected.cause": "Опслужувачот може да е недостапен. Известете го администраторот ако ова продолжи да ви се случува.", diff --git a/src/locales/pms.json b/src/locales/pms.json index 05677597a..8ca0b3ff7 100644 --- a/src/locales/pms.json +++ b/src/locales/pms.json @@ -78,6 +78,8 @@ "pad.modals.deleted.explanation": "Ës feuj a l'é stàit eliminà.", "pad.modals.rateLimited": "Tass limità.", "pad.modals.rateLimited.explanation": "A l'ha mandà tròpi mëssagi a 's blòch-sì, antlora a l'ha dëscolegalo.", + "pad.modals.rejected.explanation": "Ël servent a l'ha arpossà un mëssagi mandà da sò navigador.", + "pad.modals.rejected.cause": "Ël servent a podrìa esse stàit agiornà antramentre che chiel a beicava ël blòch, o peul desse ch'a-i é un givo an Etherpad. Ch'a preuva a carié torna la pàgina.", "pad.modals.disconnected": "A l'é stàit dëscolegà", "pad.modals.disconnected.explanation": "La conession al servent a l'é perdusse", "pad.modals.disconnected.cause": "Ël servent a podrìa esse indisponìbil. Për piasì, ch'a anforma l'aministrator dël servissi si ël problema a persist.", diff --git a/src/locales/pt-br.json b/src/locales/pt-br.json index ebcb3be44..a458de3d0 100644 --- a/src/locales/pt-br.json +++ b/src/locales/pt-br.json @@ -9,6 +9,7 @@ "Lpagliari", "Luckas", "Macofe", + "Nsharkey", "Prilopes", "Rafaelff", "Rodrigo codignoli", @@ -94,6 +95,7 @@ "pad.modals.deleted.explanation": "Esta nota foi removida.", "pad.modals.rateLimited": "Limitado.", "pad.modals.rateLimited.explanation": "Você enviou muitas mensagens para este pad por isso será desconectado.", + "pad.modals.rejected.explanation": "O servidor rejeitou uma mensagem que foi enviada pelo seu navegador.", "pad.modals.disconnected": "Você foi desconectado.", "pad.modals.disconnected.explanation": "A conexão com o servidor foi perdida", "pad.modals.disconnected.cause": "O servidor pode estar indisponível. Por favor, notifique o administrador caso isso continue.", diff --git a/src/locales/sv.json b/src/locales/sv.json index fcef0a10c..3065ac079 100644 --- a/src/locales/sv.json +++ b/src/locales/sv.json @@ -83,6 +83,8 @@ "pad.modals.deleted.explanation": "Detta block har tagits bort.", "pad.modals.rateLimited": "Begränsad frekvens.", "pad.modals.rateLimited.explanation": "Du skickade för många meddelanden till detta block så du blev frånkopplad.", + "pad.modals.rejected.explanation": "Servern avvisade ett meddelande som skickades av din webbläsare.", + "pad.modals.rejected.cause": "Servern kan ha uppdaterats medan du visade blocket, eller så finns det kanske en bugg i Etherpad. Försök att ladda om sidan.", "pad.modals.disconnected": "Du har blivit frånkopplad.", "pad.modals.disconnected.explanation": "Anslutningen till servern avbröts", "pad.modals.disconnected.cause": "Servern kanske är otillgänglig. Var god meddela tjänstadministratören om detta fortsätter att hända.", diff --git a/src/locales/tr.json b/src/locales/tr.json index 6031cfd2c..7b5dfac0a 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -9,6 +9,7 @@ "Joseph", "McAang", "Meelo", + "MuratTheTurkish", "Trockya", "Vito Genovese" ] @@ -88,6 +89,8 @@ "pad.modals.deleted.explanation": "Bu bloknot kaldırılmış.", "pad.modals.rateLimited": "Oran Sınırlı.", "pad.modals.rateLimited.explanation": "Bu pad'e çok fazla mesaj gönderdiniz, böylece bağlantı kesildi.", + "pad.modals.rejected.explanation": "Sunucu, tarayıcınız tarafından gönderilen bir mesajı reddetti.", + "pad.modals.rejected.cause": "Siz pedi görüntülerken sunucu güncellenmiş olabilir veya Etherpad'de bir hata olabilir. Sayfayı yeniden yüklemeyi deneyin.", "pad.modals.disconnected": "Bağlantınız koptu.", "pad.modals.disconnected.explanation": "Sunucu bağlantısı kaybedildi", "pad.modals.disconnected.cause": "Sunucu kullanılamıyor olabilir. Bunun devam etmesi durumunda servis yöneticisine bildirin.", diff --git a/src/locales/zh-hant.json b/src/locales/zh-hant.json index 79adb64f0..bd4eb6d8d 100644 --- a/src/locales/zh-hant.json +++ b/src/locales/zh-hant.json @@ -87,6 +87,8 @@ "pad.modals.deleted.explanation": "此記事本已被移除。", "pad.modals.rateLimited": "比例限制。", "pad.modals.rateLimited.explanation": "您發送太多訊息到此記事本,因此中斷了您的連結。", + "pad.modals.rejected.explanation": "伺服器拒絕了由您的瀏覽器發送的訊息。", + "pad.modals.rejected.cause": "當您在檢視記事本時伺服器可能正在更新,或是在 Etherpad 裡有臭蟲。請嘗試重新載入頁面。", "pad.modals.disconnected": "您已中斷連線。", "pad.modals.disconnected.explanation": "伺服器連接曾中斷", "pad.modals.disconnected.cause": "伺服器可能無法使用。若此情況持續發生,請通知伺服器管理員。", From 0bb8d73ba2969bc2c871c5095de19188a75b2b54 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 12 Sep 2020 23:35:41 -0400 Subject: [PATCH 027/315] PadMessageHandler: Always save the author ID in the session info Before, the author ID was only saved in the session info during the initial CLIENT_READY, not when the client sent a CLIENT_READY due to a reconnect. This caused the handling of subsequent messages to use an undefined author ID. --- src/node/handler/PadMessageHandler.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index e311e9dbf..8f3547920 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -53,7 +53,7 @@ const rateLimiter = new RateLimiterMemory({ * readonlyPadId = The readonly pad id of the pad * readonly = Wether the client has only read access (true) or read/write access (false) * rev = That last revision that was send to this client - * author = the author name of this session + * author = the author ID used for this session */ var sessioninfos = {}; exports.sessioninfos = sessioninfos; @@ -219,7 +219,7 @@ exports.handleMessage = async function(client, message) } const {session: {user} = {}} = client.client.request; - const {accessStatus} = + const {accessStatus, authorID} = await securityManager.checkAccess(padId, auth.sessionID, auth.token, auth.password, user); if (accessStatus !== "grant") { @@ -227,6 +227,19 @@ exports.handleMessage = async function(client, message) client.json.send({ accessStatus }); return; } + if (thisSession.author != null && thisSession.author !== authorID) { + messageLogger.warn( + 'Rejecting message from client because the author ID changed mid-session.' + + ' Bad or missing token or sessionID?' + + ` socket:${client.id}` + + ` IP:${settings.disableIPlogging ? ANONYMOUS : remoteAddress[client.id]}` + + ` originalAuthorID:${thisSession.author}` + + ` newAuthorID:${authorID}` + + ` message:${message}`); + client.json.send({disconnect: 'rejected'}); + return; + } + thisSession.author = authorID; // Allow plugins to bypass the readonly message blocker if ((await hooks.aCallAll('handleMessageSecurity', {client, message})).some((w) => w === true)) { @@ -1124,8 +1137,6 @@ async function handleClientReady(client, message) // Save the current revision in sessioninfos, should be the same as in clientVars sessionInfo.rev = pad.getHeadRevisionNumber(); - sessionInfo.author = authorID; - // prepare the notification for the other users on the pad, that this user joined let messageToTheOtherUsers = { "type": "COLLABROOM", From 89de03795a928450159234147b994f2f4a90df12 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 24 Sep 2020 19:14:10 -0400 Subject: [PATCH 028/315] tests: Delete unused imports and code --- .../backend/specs/api/importexportGetPost.js | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/tests/backend/specs/api/importexportGetPost.js b/tests/backend/specs/api/importexportGetPost.js index f678f7de7..693295f85 100644 --- a/tests/backend/specs/api/importexportGetPost.js +++ b/tests/backend/specs/api/importexportGetPost.js @@ -9,7 +9,6 @@ const settings = require(__dirname+'/../../../../src/node/utils/Settings'); const host = 'http://127.0.0.1:'+settings.port; const api = supertest('http://'+settings.ip+":"+settings.port); const path = require('path'); -const async = require(__dirname+'/../../../../src/node_modules/async'); const request = require(__dirname+'/../../../../src/node_modules/request'); const padText = fs.readFileSync("../tests/backend/specs/api/test.txt"); const etherpadDoc = fs.readFileSync("../tests/backend/specs/api/test.etherpad"); @@ -23,8 +22,6 @@ var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); apiKey = apiKey.replace(/\n$/, ""); var apiVersion = 1; var testPadId = makeid(); -var lastEdited = ""; -var text = generateLongText(); describe('Connectivity', function(){ it('can connect', function(done) { @@ -376,35 +373,3 @@ function makeid() } return text; } - -function generateLongText(){ - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - for( var i=0; i < 80000; i++ ){ - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} - -// Need this to compare arrays (listSavedRevisions test) -Array.prototype.equals = function (array) { - // if the other array is a falsy value, return - if (!array) - return false; - // compare lengths - can save a lot of time - if (this.length != array.length) - return false; - for (var i = 0, l=this.length; i < l; i++) { - // Check if we have nested arrays - if (this[i] instanceof Array && array[i] instanceof Array) { - // recurse into the nested arrays - if (!this[i].equals(array[i])) - return false; - } else if (this[i] != array[i]) { - // Warning - two different object instances will never be equal: {x:20} != {x:20} - return false; - } - } - return true; -} From 668373b80f1d0c89b64e22ef2ee4a302b02d74b8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 24 Sep 2020 19:21:07 -0400 Subject: [PATCH 029/315] tests: Fix abiword/soffice check --- .../backend/specs/api/importexportGetPost.js | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/tests/backend/specs/api/importexportGetPost.js b/tests/backend/specs/api/importexportGetPost.js index 693295f85..368359449 100644 --- a/tests/backend/specs/api/importexportGetPost.js +++ b/tests/backend/specs/api/importexportGetPost.js @@ -109,7 +109,10 @@ describe('Imports and Exports', function(){ // TODO: fix support for .doc files.. it('Tries to import .doc that uses soffice or abiword', function(done) { if(!settings.allowAnyoneToImport) return done(); - if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done(); + if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && + (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + return done(); + } var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { if (err) { @@ -132,7 +135,10 @@ describe('Imports and Exports', function(){ it('exports DOC', function(done) { if(!settings.allowAnyoneToImport) return done(); - if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done(); + if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && + (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + return done(); + } try{ request(host + '/p/'+testPadId+'/export/doc', function (err, res, body) { // TODO: At some point checking that the contents is correct would be suitable @@ -149,7 +155,10 @@ describe('Imports and Exports', function(){ it('Tries to import .docx that uses soffice or abiword', function(done) { if(!settings.allowAnyoneToImport) return done(); - if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done(); + if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && + (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + return done(); + } var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { if (err) { @@ -172,7 +181,10 @@ describe('Imports and Exports', function(){ it('exports DOC from imported DOCX', function(done) { if(!settings.allowAnyoneToImport) return done(); - if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done(); + if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && + (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + return done(); + } request(host + '/p/'+testPadId+'/export/doc', function (err, res, body) { // TODO: At some point checking that the contents is correct would be suitable if(body.length >= 9100){ @@ -185,7 +197,10 @@ describe('Imports and Exports', function(){ it('Tries to import .pdf that uses soffice or abiword', function(done) { if(!settings.allowAnyoneToImport) return done(); - if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done(); + if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && + (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + return done(); + } var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { if (err) { @@ -208,7 +223,10 @@ describe('Imports and Exports', function(){ it('exports PDF', function(done) { if(!settings.allowAnyoneToImport) return done(); - if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done(); + if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && + (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + return done(); + } request(host + '/p/'+testPadId+'/export/pdf', function (err, res, body) { // TODO: At some point checking that the contents is correct would be suitable if(body.length >= 1000){ @@ -221,7 +239,10 @@ describe('Imports and Exports', function(){ it('Tries to import .odt that uses soffice or abiword', function(done) { if(!settings.allowAnyoneToImport) return done(); - if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done(); + if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && + (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + return done(); + } var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { if (err) { @@ -244,7 +265,10 @@ describe('Imports and Exports', function(){ it('exports ODT', function(done) { if(!settings.allowAnyoneToImport) return done(); - if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done(); + if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && + (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + return done(); + } request(host + '/p/'+testPadId+'/export/odt', function (err, res, body) { // TODO: At some point checking that the contents is correct would be suitable if(body.length >= 7000){ From c148e673a8a6fefcc6a85dd556c9a911a048f888 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 24 Sep 2020 19:25:51 -0400 Subject: [PATCH 030/315] tests: Use `this.skip()` when skipping tests --- .../backend/specs/api/importexportGetPost.js | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/tests/backend/specs/api/importexportGetPost.js b/tests/backend/specs/api/importexportGetPost.js index 368359449..831266ec0 100644 --- a/tests/backend/specs/api/importexportGetPost.js +++ b/tests/backend/specs/api/importexportGetPost.js @@ -73,6 +73,7 @@ describe('Imports and Exports', function(){ it('creates a new Pad, imports content to it, checks that content', function(done) { if(!settings.allowAnyoneToImport){ console.warn("not anyone can import so not testing -- to include this test set allowAnyoneToImport to true in settings.json"); + this.skip(); done(); }else{ api.get(endPoint('createPad')+"&padID="+testPadId) @@ -108,9 +109,10 @@ describe('Imports and Exports', function(){ // For some reason word import does not work in testing.. // TODO: fix support for .doc files.. it('Tries to import .doc that uses soffice or abiword', function(done) { - if(!settings.allowAnyoneToImport) return done(); + if (!settings.allowAnyoneToImport) { this.skip(); return done(); } if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + this.skip(); return done(); } @@ -134,9 +136,10 @@ describe('Imports and Exports', function(){ }); it('exports DOC', function(done) { - if(!settings.allowAnyoneToImport) return done(); + if (!settings.allowAnyoneToImport) { this.skip(); return done(); } if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + this.skip(); return done(); } try{ @@ -154,9 +157,10 @@ describe('Imports and Exports', function(){ }) it('Tries to import .docx that uses soffice or abiword', function(done) { - if(!settings.allowAnyoneToImport) return done(); + if (!settings.allowAnyoneToImport) { this.skip(); return done(); } if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + this.skip(); return done(); } @@ -180,9 +184,10 @@ describe('Imports and Exports', function(){ }); it('exports DOC from imported DOCX', function(done) { - if(!settings.allowAnyoneToImport) return done(); + if (!settings.allowAnyoneToImport) { this.skip(); return done(); } if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + this.skip(); return done(); } request(host + '/p/'+testPadId+'/export/doc', function (err, res, body) { @@ -196,9 +201,10 @@ describe('Imports and Exports', function(){ }) it('Tries to import .pdf that uses soffice or abiword', function(done) { - if(!settings.allowAnyoneToImport) return done(); + if (!settings.allowAnyoneToImport) { this.skip(); return done(); } if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + this.skip(); return done(); } @@ -222,9 +228,10 @@ describe('Imports and Exports', function(){ }); it('exports PDF', function(done) { - if(!settings.allowAnyoneToImport) return done(); + if (!settings.allowAnyoneToImport) { this.skip(); return done(); } if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + this.skip(); return done(); } request(host + '/p/'+testPadId+'/export/pdf', function (err, res, body) { @@ -238,9 +245,10 @@ describe('Imports and Exports', function(){ }) it('Tries to import .odt that uses soffice or abiword', function(done) { - if(!settings.allowAnyoneToImport) return done(); + if (!settings.allowAnyoneToImport) { this.skip(); return done(); } if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + this.skip(); return done(); } @@ -264,9 +272,10 @@ describe('Imports and Exports', function(){ }); it('exports ODT', function(done) { - if(!settings.allowAnyoneToImport) return done(); + if (!settings.allowAnyoneToImport) { this.skip(); return done(); } if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + this.skip(); return done(); } request(host + '/p/'+testPadId+'/export/odt', function (err, res, body) { @@ -280,7 +289,7 @@ describe('Imports and Exports', function(){ }) it('Tries to import .etherpad', function(done) { - if(!settings.allowAnyoneToImport) return done(); + if (!settings.allowAnyoneToImport) { this.skip(); return done(); } var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { if (err) { @@ -353,6 +362,7 @@ describe('Imports and Exports', function(){ it('Tries to import unsupported file type', function(done) { if(settings.allowUnknownFileEnds === true){ console.log("allowing unknown file ends so skipping this test"); + this.skip(); return done(); } From 1c3c5b744cff66ac6d1906770d032fc838ba8349 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 24 Sep 2020 19:28:32 -0400 Subject: [PATCH 031/315] tests: Skip all import/export tests if `!allowAnyoneToImport` Three of the four tests fail if `settings.allowAnyoneToImport` is false. The fourth ("tries to import Plain Text to a pad that does not exist") isn't particularly useful when `settings.allowAnyoneToImport` is false: That test tests an import failure mode, and when `settings.allowAnyoneToImport` is false the failure could be caused by that instead of the expected cause. --- tests/backend/specs/api/importexportGetPost.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/backend/specs/api/importexportGetPost.js b/tests/backend/specs/api/importexportGetPost.js index 831266ec0..e372a9a85 100644 --- a/tests/backend/specs/api/importexportGetPost.js +++ b/tests/backend/specs/api/importexportGetPost.js @@ -311,6 +311,7 @@ describe('Imports and Exports', function(){ }); it('exports Etherpad', function(done) { + if (!settings.allowAnyoneToImport) { this.skip(); return done(); } request(host + '/p/'+testPadId+'/export/etherpad', function (err, res, body) { // TODO: At some point checking that the contents is correct would be suitable if(body.indexOf("hello") !== -1){ @@ -323,6 +324,7 @@ describe('Imports and Exports', function(){ }) it('exports HTML for this Etherpad file', function(done) { + if (!settings.allowAnyoneToImport) { this.skip(); return done(); } request(host + '/p/'+testPadId+'/export/html', function (err, res, body) { // broken pre fix export --
    @@ -338,6 +340,7 @@ describe('Imports and Exports', function(){ }) it('tries to import Plain Text to a pad that does not exist', function(done) { + if (!settings.allowAnyoneToImport) { this.skip(); return done(); } var req = request.post(host + '/p/'+testPadId+testPadId+testPadId+'/import', function (err, res, body) { if (res.statusCode === 200) { throw new Error("Was able to import to a pad that doesn't exist"); @@ -360,6 +363,7 @@ describe('Imports and Exports', function(){ }); it('Tries to import unsupported file type', function(done) { + if (!settings.allowAnyoneToImport) { this.skip(); return done(); } if(settings.allowUnknownFileEnds === true){ console.log("allowing unknown file ends so skipping this test"); this.skip(); From 54c999fe83700487be1b3228c7186716a140f0df Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 25 Sep 2020 13:55:19 -0400 Subject: [PATCH 032/315] tests: Factor out common skip checks --- .../backend/specs/api/importexportGetPost.js | 352 ++++++++---------- 1 file changed, 153 insertions(+), 199 deletions(-) diff --git a/tests/backend/specs/api/importexportGetPost.js b/tests/backend/specs/api/importexportGetPost.js index e372a9a85..7d84bc71a 100644 --- a/tests/backend/specs/api/importexportGetPost.js +++ b/tests/backend/specs/api/importexportGetPost.js @@ -70,227 +70,185 @@ Example Curl command for testing import URI: */ describe('Imports and Exports', function(){ - it('creates a new Pad, imports content to it, checks that content', function(done) { - if(!settings.allowAnyoneToImport){ - console.warn("not anyone can import so not testing -- to include this test set allowAnyoneToImport to true in settings.json"); + before(function() { + if (!settings.allowAnyoneToImport) { + console.warn('not anyone can import so not testing -- ' + + 'to include this test set allowAnyoneToImport to true in settings.json'); this.skip(); - done(); - }else{ - api.get(endPoint('createPad')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Unable to create new Pad"); - - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - api.get(endPoint('getText')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.data.text !== padText.toString()){ - throw new Error("text is wrong on export"); - } - }) - } - }); - - let form = req.form(); - - form.append('file', padText, { - filename: '/test.txt', - contentType: 'text/plain' - }); - - }) - .expect('Content-Type', /json/) - .expect(200, done) } }); - // For some reason word import does not work in testing.. - // TODO: fix support for .doc files.. - it('Tries to import .doc that uses soffice or abiword', function(done) { - if (!settings.allowAnyoneToImport) { this.skip(); return done(); } - if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && - (!settings.soffice || settings.soffice.indexOf('/') === -1)) { - this.skip(); - return done(); - } + it('creates a new Pad, imports content to it, checks that content', function(done) { + api.get(endPoint('createPad') + "&padID=" + testPadId) + .expect(function(res) { + if (res.body.code !== 0) throw new Error("Unable to create new Pad"); - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){ - throw new Error("Failed DOC import", testPadId); - }else{ - done(); - } + var req = request.post(host + '/p/' + testPadId + '/import', function(err, res, body) { + if (err) { + throw new Error("Failed to import", err); + } else { + api.get(endPoint('getText')+"&padID="+testPadId) + .expect(function(res){ + if(res.body.data.text !== padText.toString()){ + throw new Error("text is wrong on export"); + } + }) + } + }); + + let form = req.form(); + + form.append('file', padText, { + filename: '/test.txt', + contentType: 'text/plain' + }); + + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); + + describe('Import/Export tests requiring AbiWord/LibreOffice', function() { + before(function() { + if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && + (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + this.skip(); } }); - let form = req.form(); - form.append('file', wordDoc, { - filename: '/test.doc', - contentType: 'application/msword' - }); - }); + // For some reason word import does not work in testing.. + // TODO: fix support for .doc files.. + it('Tries to import .doc that uses soffice or abiword', function(done) { + var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { + if (err) { + throw new Error("Failed to import", err); + } else { + if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){ + throw new Error("Failed DOC import", testPadId); + }else{ + done(); + } + } + }); - it('exports DOC', function(done) { - if (!settings.allowAnyoneToImport) { this.skip(); return done(); } - if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && - (!settings.soffice || settings.soffice.indexOf('/') === -1)) { - this.skip(); - return done(); - } - try{ + let form = req.form(); + form.append('file', wordDoc, { + filename: '/test.doc', + contentType: 'application/msword' + }); + }); + + it('exports DOC', function(done) { + try{ + request(host + '/p/'+testPadId+'/export/doc', function (err, res, body) { + // TODO: At some point checking that the contents is correct would be suitable + if(body.length >= 9000){ + done(); + }else{ + throw new Error("Word Document export length is not right"); + } + }) + }catch(e){ + throw new Error(e); + } + }); + + it('Tries to import .docx that uses soffice or abiword', function(done) { + var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { + if (err) { + throw new Error("Failed to import", err); + } else { + if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){ + throw new Error("Failed DOCX import"); + }else{ + done(); + } + } + }); + + let form = req.form(); + form.append('file', wordXDoc, { + filename: '/test.docx', + contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + }); + }); + + it('exports DOC from imported DOCX', function(done) { request(host + '/p/'+testPadId+'/export/doc', function (err, res, body) { // TODO: At some point checking that the contents is correct would be suitable - if(body.length >= 9000){ + if(body.length >= 9100){ done(); }else{ throw new Error("Word Document export length is not right"); } }) - }catch(e){ - throw new Error(e); - } - }) + }); - it('Tries to import .docx that uses soffice or abiword', function(done) { - if (!settings.allowAnyoneToImport) { this.skip(); return done(); } - if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && - (!settings.soffice || settings.soffice.indexOf('/') === -1)) { - this.skip(); - return done(); - } - - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){ - throw new Error("Failed DOCX import"); - }else{ - done(); + it('Tries to import .pdf that uses soffice or abiword', function(done) { + var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { + if (err) { + throw new Error("Failed to import", err); + } else { + if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){ + throw new Error("Failed PDF import"); + }else{ + done(); + } } - } + }); + + let form = req.form(); + form.append('file', pdfDoc, { + filename: '/test.pdf', + contentType: 'application/pdf' + }); }); - let form = req.form(); - form.append('file', wordXDoc, { - filename: '/test.docx', - contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' - }); - }); - - it('exports DOC from imported DOCX', function(done) { - if (!settings.allowAnyoneToImport) { this.skip(); return done(); } - if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && - (!settings.soffice || settings.soffice.indexOf('/') === -1)) { - this.skip(); - return done(); - } - request(host + '/p/'+testPadId+'/export/doc', function (err, res, body) { - // TODO: At some point checking that the contents is correct would be suitable - if(body.length >= 9100){ - done(); - }else{ - throw new Error("Word Document export length is not right"); - } - }) - }) - - it('Tries to import .pdf that uses soffice or abiword', function(done) { - if (!settings.allowAnyoneToImport) { this.skip(); return done(); } - if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && - (!settings.soffice || settings.soffice.indexOf('/') === -1)) { - this.skip(); - return done(); - } - - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){ - throw new Error("Failed PDF import"); - }else{ + it('exports PDF', function(done) { + request(host + '/p/'+testPadId+'/export/pdf', function (err, res, body) { + // TODO: At some point checking that the contents is correct would be suitable + if(body.length >= 1000){ done(); - } - } - }); - - let form = req.form(); - form.append('file', pdfDoc, { - filename: '/test.pdf', - contentType: 'application/pdf' - }); - }); - - it('exports PDF', function(done) { - if (!settings.allowAnyoneToImport) { this.skip(); return done(); } - if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && - (!settings.soffice || settings.soffice.indexOf('/') === -1)) { - this.skip(); - return done(); - } - request(host + '/p/'+testPadId+'/export/pdf', function (err, res, body) { - // TODO: At some point checking that the contents is correct would be suitable - if(body.length >= 1000){ - done(); - }else{ - throw new Error("PDF Document export length is not right"); - } - }) - }) - - it('Tries to import .odt that uses soffice or abiword', function(done) { - if (!settings.allowAnyoneToImport) { this.skip(); return done(); } - if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && - (!settings.soffice || settings.soffice.indexOf('/') === -1)) { - this.skip(); - return done(); - } - - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){ - throw new Error("Failed ODT import", testPadId); }else{ - done(); + throw new Error("PDF Document export length is not right"); } - } + }) }); - let form = req.form(); - form.append('file', odtDoc, { - filename: '/test.odt', - contentType: 'application/odt' - }); - }); + it('Tries to import .odt that uses soffice or abiword', function(done) { + var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { + if (err) { + throw new Error("Failed to import", err); + } else { + if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){ + throw new Error("Failed ODT import", testPadId); + }else{ + done(); + } + } + }); - it('exports ODT', function(done) { - if (!settings.allowAnyoneToImport) { this.skip(); return done(); } - if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && - (!settings.soffice || settings.soffice.indexOf('/') === -1)) { - this.skip(); - return done(); - } - request(host + '/p/'+testPadId+'/export/odt', function (err, res, body) { - // TODO: At some point checking that the contents is correct would be suitable - if(body.length >= 7000){ - done(); - }else{ - throw new Error("ODT Document export length is not right"); - } - }) - }) + let form = req.form(); + form.append('file', odtDoc, { + filename: '/test.odt', + contentType: 'application/odt' + }); + }); + + it('exports ODT', function(done) { + request(host + '/p/'+testPadId+'/export/odt', function (err, res, body) { + // TODO: At some point checking that the contents is correct would be suitable + if(body.length >= 7000){ + done(); + }else{ + throw new Error("ODT Document export length is not right"); + } + }) + }); + + }); // End of AbiWord/LibreOffice tests. it('Tries to import .etherpad', function(done) { - if (!settings.allowAnyoneToImport) { this.skip(); return done(); } - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { if (err) { throw new Error("Failed to import", err); @@ -311,7 +269,6 @@ describe('Imports and Exports', function(){ }); it('exports Etherpad', function(done) { - if (!settings.allowAnyoneToImport) { this.skip(); return done(); } request(host + '/p/'+testPadId+'/export/etherpad', function (err, res, body) { // TODO: At some point checking that the contents is correct would be suitable if(body.indexOf("hello") !== -1){ @@ -321,10 +278,9 @@ describe('Imports and Exports', function(){ throw new Error("Etherpad Document does not include hello"); } }) - }) + }); it('exports HTML for this Etherpad file', function(done) { - if (!settings.allowAnyoneToImport) { this.skip(); return done(); } request(host + '/p/'+testPadId+'/export/html', function (err, res, body) { // broken pre fix export --
      @@ -337,10 +293,9 @@ describe('Imports and Exports', function(){ throw new Error("Exported HTML nested list items is not right", body); } }) - }) + }); it('tries to import Plain Text to a pad that does not exist', function(done) { - if (!settings.allowAnyoneToImport) { this.skip(); return done(); } var req = request.post(host + '/p/'+testPadId+testPadId+testPadId+'/import', function (err, res, body) { if (res.statusCode === 200) { throw new Error("Was able to import to a pad that doesn't exist"); @@ -363,7 +318,6 @@ describe('Imports and Exports', function(){ }); it('Tries to import unsupported file type', function(done) { - if (!settings.allowAnyoneToImport) { this.skip(); return done(); } if(settings.allowUnknownFileEnds === true){ console.log("allowing unknown file ends so skipping this test"); this.skip(); From 23131a501c35e192d8c71bdb775390522da1037b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 24 Sep 2020 19:32:02 -0400 Subject: [PATCH 033/315] tests: Rewrite import/export tests to use async and supertest --- src/package-lock.json | 71 ++++ src/package.json | 1 + .../backend/specs/api/importexportGetPost.js | 341 +++++------------- 3 files changed, 172 insertions(+), 241 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 6b9b9b0c8..7ff64c3b1 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -8235,6 +8235,77 @@ "requires": { "methods": "^1.1.2", "superagent": "^3.8.3" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + } + } } }, "supports-color": { diff --git a/src/package.json b/src/package.json index 9575ab117..ee45fa9a6 100644 --- a/src/package.json +++ b/src/package.json @@ -79,6 +79,7 @@ "mocha-froth": "^0.2.10", "nyc": "15.0.1", "set-cookie-parser": "^2.4.6", + "superagent": "^3.8.3", "supertest": "4.0.2", "wd": "1.12.1" }, diff --git a/tests/backend/specs/api/importexportGetPost.js b/tests/backend/specs/api/importexportGetPost.js index 7d84bc71a..aa72f7072 100644 --- a/tests/backend/specs/api/importexportGetPost.js +++ b/tests/backend/specs/api/importexportGetPost.js @@ -2,14 +2,14 @@ * Import and Export tests for the /p/whateverPadId/import and /p/whateverPadId/export endpoints. */ -const assert = require('assert'); +const assert = require('assert').strict; +const superagent = require(__dirname+'/../../../../src/node_modules/superagent'); const supertest = require(__dirname+'/../../../../src/node_modules/supertest'); const fs = require('fs'); const settings = require(__dirname+'/../../../../src/node/utils/Settings'); const host = 'http://127.0.0.1:'+settings.port; -const api = supertest('http://'+settings.ip+":"+settings.port); +const agent = supertest(`http://${settings.ip}:${settings.port}`); const path = require('path'); -const request = require(__dirname+'/../../../../src/node_modules/request'); const padText = fs.readFileSync("../tests/backend/specs/api/test.txt"); const etherpadDoc = fs.readFileSync("../tests/backend/specs/api/test.etherpad"); const wordDoc = fs.readFileSync("../tests/backend/specs/api/test.doc"); @@ -24,22 +24,18 @@ var apiVersion = 1; var testPadId = makeid(); describe('Connectivity', function(){ - it('can connect', function(done) { - api.get('/api/') - .expect('Content-Type', /json/) - .expect(200, done) + it('can connect', async function() { + await agent.get('/api/') + .expect(200) + .expect('Content-Type', /json/); }); }) describe('API Versioning', function(){ - it('finds the version tag', function(done) { - api.get('/api/') - .expect(function(res){ - apiVersion = res.body.currentVersion; - if (!res.body.currentVersion) throw new Error("No version set in API"); - return; - }) - .expect(200, done) + it('finds the version tag', async function() { + await agent.get('/api/') + .expect(200) + .expect((res) => assert(res.body.currentVersion)); }); }) @@ -78,34 +74,17 @@ describe('Imports and Exports', function(){ } }); - it('creates a new Pad, imports content to it, checks that content', function(done) { - api.get(endPoint('createPad') + "&padID=" + testPadId) - .expect(function(res) { - if (res.body.code !== 0) throw new Error("Unable to create new Pad"); - - var req = request.post(host + '/p/' + testPadId + '/import', function(err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - api.get(endPoint('getText')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.data.text !== padText.toString()){ - throw new Error("text is wrong on export"); - } - }) - } - }); - - let form = req.form(); - - form.append('file', padText, { - filename: '/test.txt', - contentType: 'text/plain' - }); - - }) + it('creates a new Pad, imports content to it, checks that content', async function() { + await agent.get(endPoint('createPad') + `&padID=${testPadId}`) + .expect(200) .expect('Content-Type', /json/) - .expect(200, done) + .expect((res) => assert.equal(res.body.code, 0)); + await agent.post(`/p/${testPadId}/import`) + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(200); + await agent.get(endPoint('getText') + `&padID=${testPadId}`) + .expect(200) + .expect((res) => assert.equal(res.body.data.text, padText.toString())); }); describe('Import/Export tests requiring AbiWord/LibreOffice', function() { @@ -118,233 +97,113 @@ describe('Imports and Exports', function(){ // For some reason word import does not work in testing.. // TODO: fix support for .doc files.. - it('Tries to import .doc that uses soffice or abiword', function(done) { - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){ - throw new Error("Failed DOC import", testPadId); - }else{ - done(); - } - } - }); - - let form = req.form(); - form.append('file', wordDoc, { - filename: '/test.doc', - contentType: 'application/msword' - }); + it('Tries to import .doc that uses soffice or abiword', async function() { + await agent.post(`/p/${testPadId}/import`) + .attach('file', wordDoc, {filename: '/test.doc', contentType: 'application/msword'}) + .expect(200) + .expect(/FrameCall\('undefined', 'ok'\);/); }); - it('exports DOC', function(done) { - try{ - request(host + '/p/'+testPadId+'/export/doc', function (err, res, body) { - // TODO: At some point checking that the contents is correct would be suitable - if(body.length >= 9000){ - done(); - }else{ - throw new Error("Word Document export length is not right"); - } - }) - }catch(e){ - throw new Error(e); - } + it('exports DOC', async function() { + await agent.get(`/p/${testPadId}/export/doc`) + .buffer(true).parse(superagent.parse['application/octet-stream']) + .expect(200) + .expect((res) => assert(res.body.length >= 9000)); }); - it('Tries to import .docx that uses soffice or abiword', function(done) { - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){ - throw new Error("Failed DOCX import"); - }else{ - done(); - } - } - }); - - let form = req.form(); - form.append('file', wordXDoc, { - filename: '/test.docx', - contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' - }); + it('Tries to import .docx that uses soffice or abiword', async function() { + await agent.post(`/p/${testPadId}/import`) + .attach('file', wordXDoc, { + filename: '/test.docx', + contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }) + .expect(200) + .expect(/FrameCall\('undefined', 'ok'\);/); }); - it('exports DOC from imported DOCX', function(done) { - request(host + '/p/'+testPadId+'/export/doc', function (err, res, body) { - // TODO: At some point checking that the contents is correct would be suitable - if(body.length >= 9100){ - done(); - }else{ - throw new Error("Word Document export length is not right"); - } - }) + it('exports DOC from imported DOCX', async function() { + await agent.get(`/p/${testPadId}/export/doc`) + .buffer(true).parse(superagent.parse['application/octet-stream']) + .expect(200) + .expect((res) => assert(res.body.length >= 9100)); }); - it('Tries to import .pdf that uses soffice or abiword', function(done) { - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){ - throw new Error("Failed PDF import"); - }else{ - done(); - } - } - }); - - let form = req.form(); - form.append('file', pdfDoc, { - filename: '/test.pdf', - contentType: 'application/pdf' - }); + it('Tries to import .pdf that uses soffice or abiword', async function() { + await agent.post(`/p/${testPadId}/import`) + .attach('file', pdfDoc, {filename: '/test.pdf', contentType: 'application/pdf'}) + .expect(200) + .expect(/FrameCall\('undefined', 'ok'\);/); }); - it('exports PDF', function(done) { - request(host + '/p/'+testPadId+'/export/pdf', function (err, res, body) { - // TODO: At some point checking that the contents is correct would be suitable - if(body.length >= 1000){ - done(); - }else{ - throw new Error("PDF Document export length is not right"); - } - }) + it('exports PDF', async function() { + await agent.get(`/p/${testPadId}/export/pdf`) + .buffer(true).parse(superagent.parse['application/octet-stream']) + .expect(200) + .expect((res) => assert(res.body.length >= 1000)); }); - it('Tries to import .odt that uses soffice or abiword', function(done) { - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){ - throw new Error("Failed ODT import", testPadId); - }else{ - done(); - } - } - }); - - let form = req.form(); - form.append('file', odtDoc, { - filename: '/test.odt', - contentType: 'application/odt' - }); + it('Tries to import .odt that uses soffice or abiword', async function() { + await agent.post(`/p/${testPadId}/import`) + .attach('file', odtDoc, {filename: '/test.odt', contentType: 'application/odt'}) + .expect(200) + .expect(/FrameCall\('undefined', 'ok'\);/); }); - it('exports ODT', function(done) { - request(host + '/p/'+testPadId+'/export/odt', function (err, res, body) { - // TODO: At some point checking that the contents is correct would be suitable - if(body.length >= 7000){ - done(); - }else{ - throw new Error("ODT Document export length is not right"); - } - }) + it('exports ODT', async function() { + await agent.get(`/p/${testPadId}/export/odt`) + .buffer(true).parse(superagent.parse['application/octet-stream']) + .expect(200) + .expect((res) => assert(res.body.length >= 7000)); }); }); // End of AbiWord/LibreOffice tests. - it('Tries to import .etherpad', function(done) { - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - if(res.body.indexOf("FrameCall(\'true\', \'ok\');") === -1){ - throw new Error("Failed Etherpad import", err, testPadId); - }else{ - done(); - } - } - }); - - let form = req.form(); - form.append('file', etherpadDoc, { - filename: '/test.etherpad', - contentType: 'application/etherpad' - }); + it('Tries to import .etherpad', async function() { + await agent.post(`/p/${testPadId}/import`) + .attach('file', etherpadDoc, { + filename: '/test.etherpad', + contentType: 'application/etherpad', + }) + .expect(200) + .expect(/FrameCall\('true', 'ok'\);/); }); - it('exports Etherpad', function(done) { - request(host + '/p/'+testPadId+'/export/etherpad', function (err, res, body) { - // TODO: At some point checking that the contents is correct would be suitable - if(body.indexOf("hello") !== -1){ - done(); - }else{ - console.error("body"); - throw new Error("Etherpad Document does not include hello"); - } - }) + it('exports Etherpad', async function() { + await agent.get(`/p/${testPadId}/export/etherpad`) + .buffer(true).parse(superagent.parse.text) + .expect(200) + .expect(/hello/); }); - it('exports HTML for this Etherpad file', function(done) { - request(host + '/p/'+testPadId+'/export/html', function (err, res, body) { - - // broken pre fix export --
        - var expectedHTML = '
          • hello
        '; - // expect body to include - if(body.indexOf(expectedHTML) !== -1){ - done(); - }else{ - console.error(body); - throw new Error("Exported HTML nested list items is not right", body); - } - }) + it('exports HTML for this Etherpad file', async function() { + await agent.get(`/p/${testPadId}/export/html`) + .expect(200) + .expect('content-type', 'text/html; charset=utf-8') + .expect(/
          • hello<\/ul><\/li><\/ul>/); }); - it('tries to import Plain Text to a pad that does not exist', function(done) { - var req = request.post(host + '/p/'+testPadId+testPadId+testPadId+'/import', function (err, res, body) { - if (res.statusCode === 200) { - throw new Error("Was able to import to a pad that doesn't exist"); - }else{ - // Wasn't able to write to a pad that doesn't exist, this is expected behavior - api.get(endPoint('getText')+"&padID="+testPadId+testPadId+testPadId) - .expect(function(res){ - if(res.body.code !== 1) throw new Error("Pad Exists"); - }) - .expect(200, done) - } - - let form = req.form(); - - form.append('file', padText, { - filename: '/test.txt', - contentType: 'text/plain' - }); - }) + it('tries to import Plain Text to a pad that does not exist', async function() { + const padId = testPadId + testPadId + testPadId; + await agent.post(`/p/${padId}/import`) + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(405); + await agent.get(endPoint('getText') + `&padID=${padId}`) + .expect(200) + .expect((res) => assert.equal(res.body.code, 1)); }); - it('Tries to import unsupported file type', function(done) { - if(settings.allowUnknownFileEnds === true){ - console.log("allowing unknown file ends so skipping this test"); - this.skip(); - return done(); + it('Tries to import unsupported file type', async function() { + if (settings.allowUnknownFileEnds === true) { + console.log('skipping test because allowUnknownFileEnds is true'); + return this.skip(); } - - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - if(res.body.indexOf("FrameCall('undefined', 'ok');") !== -1){ - console.log("worked"); - throw new Error("You shouldn't be able to import this file", testPadId); - } - return done(); - } - }); - - let form = req.form(); - form.append('file', padText, { - filename: '/test.xasdasdxx', - contentType: 'weirdness/jobby' - }); + await agent.post(`/p/${testPadId}/import`) + .attach('file', padText, {filename: '/test.xasdasdxx', contentType: 'weirdness/jobby'}) + .expect(200) + .expect((res) => assert.doesNotMatch(res.text, /FrameCall\('undefined', 'ok'\);/)); }); -// end of tests -}) +}); // End of tests. @@ -352,7 +211,7 @@ describe('Imports and Exports', function(){ var endPoint = function(point, version){ version = version || apiVersion; - return '/api/'+version+'/'+point+'?apikey='+apiKey; + return `/api/${version}/${point}?apikey=${apiKey}`; } function makeid() From 3c9ae57bb33fbf047aa4872b33b1493d2ebc1109 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 4 Sep 2020 17:35:42 -0400 Subject: [PATCH 034/315] PadMessageHandler: Block Promise resolution until message is handled Benefits: * More functions are now async which makes it possible for future changes to use await in those functions. * This will help keep the server from drowning in too many messages if we ever add acknowledgements or if WebSocket backpressure ever becomes reality. * This might make tests less flaky because changes triggered by a message will complete before the Promise resolves. --- src/node/handler/PadMessageHandler.js | 33 +++++++++++++++------------ src/node/handler/SocketIORouter.js | 8 +++---- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 8f3547920..a106bc252 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -259,9 +259,9 @@ exports.handleMessage = async function(client, message) // Check what type of message we get and delegate to the other methods if (message.type === "CLIENT_READY") { - handleClientReady(client, message); + await handleClientReady(client, message); } else if (message.type === "CHANGESET_REQ") { - handleChangesetRequest(client, message); + await handleChangesetRequest(client, message); } else if(message.type === "COLLABROOM") { if (thisSession.readonly) { messageLogger.warn("Dropped message, COLLABROOM for readonly pad"); @@ -269,13 +269,13 @@ exports.handleMessage = async function(client, message) stats.counter('pendingEdits').inc() padChannels.emit(message.padId, {client: client, message: message}); // add to pad queue } else if (message.data.type === "USERINFO_UPDATE") { - handleUserInfoUpdate(client, message); + await handleUserInfoUpdate(client, message); } else if (message.data.type === "CHAT_MESSAGE") { - handleChatMessage(client, message); + await handleChatMessage(client, message); } else if (message.data.type === "GET_CHAT_MESSAGES") { - handleGetChatMessages(client, message); + await handleGetChatMessages(client, message); } else if (message.data.type === "SAVE_REVISION") { - handleSaveRevisionMessage(client, message); + await handleSaveRevisionMessage(client, message); } else if (message.data.type === "CLIENT_MESSAGE" && message.data.payload != null && message.data.payload.type === "suggestUserName") { @@ -284,7 +284,7 @@ exports.handleMessage = async function(client, message) messageLogger.warn("Dropped message, unknown COLLABROOM Data Type " + message.data.type); } } else if(message.type === "SWITCH_TO_PAD") { - handleSwitchToPad(client, message); + await handleSwitchToPad(client, message); } else { messageLogger.warn("Dropped message, unknown Message Type " + message.type); } @@ -347,14 +347,14 @@ exports.handleCustomMessage = function(padID, msgString) { * @param client the client that send this message * @param message the message from the client */ -function handleChatMessage(client, message) +async function handleChatMessage(client, message) { var time = Date.now(); var userId = sessioninfos[client.id].author; var text = message.data.text; var padId = sessioninfos[client.id].padId; - exports.sendChatMessageToPadClients(time, userId, text, padId); + await exports.sendChatMessageToPadClients(time, userId, text, padId); } /** @@ -463,7 +463,7 @@ function handleSuggestUserName(client, message) * @param client the client that send this message * @param message the message from the client */ -function handleUserInfoUpdate(client, message) +async function handleUserInfoUpdate(client, message) { // check if all ok if (message.data.userInfo == null) { @@ -494,8 +494,10 @@ function handleUserInfoUpdate(client, message) } // Tell the authorManager about the new attributes - authorManager.setAuthorColorId(author, message.data.userInfo.colorId); - authorManager.setAuthorName(author, message.data.userInfo.name); + const p = Promise.all([ + authorManager.setAuthorColorId(author, message.data.userInfo.colorId), + authorManager.setAuthorName(author, message.data.userInfo.name), + ]); var padId = session.padId; @@ -517,6 +519,9 @@ function handleUserInfoUpdate(client, message) // Send the other clients on the pad the update message client.broadcast.to(padId).json.send(infoMsg); + + // Block until the authorManager has stored the new attributes. + await p; } /** @@ -813,7 +818,7 @@ function _correctMarkersInPad(atext, apool) { return builder.toString(); } -function handleSwitchToPad(client, message) +async function handleSwitchToPad(client, message) { // clear the session and leave the room const currentSessionInfo = sessioninfos[client.id]; @@ -830,7 +835,7 @@ function handleSwitchToPad(client, message) // start up the new pad const newSessionInfo = sessioninfos[client.id]; createSessionInfoAuth(newSessionInfo, message); - handleClientReady(client, message); + await handleClientReady(client, message); } // Creates/replaces the auth object in the given session info. diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index a5220d2f4..23f3e459c 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -87,7 +87,7 @@ exports.setSocketIO = function(_socket) { if (clientAuthorized) { // client is authorized, everything ok - handleMessage(client, message); + await handleMessage(client, message); } else { // try to authorize the client if (message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) { @@ -104,7 +104,7 @@ exports.setSocketIO = function(_socket) { if (accessStatus === "grant") { // access was granted, mark the client as authorized and handle the message clientAuthorized = true; - handleMessage(client, message); + await handleMessage(client, message); } else { // no access, send the client a message that tells him why messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message)); @@ -127,13 +127,13 @@ exports.setSocketIO = function(_socket) { } // try to handle the message of this client -function handleMessage(client, message) +async function handleMessage(client, message) { if (message.component && components[message.component]) { // check if component is registered in the components array if (components[message.component]) { messageLogger.debug("from " + client.id + ": " + stringifyWithoutPassword(message)); - components[message.component].handleMessage(client, message); + await components[message.component].handleMessage(client, message); } } else { messageLogger.error("Can't route the message:" + stringifyWithoutPassword(message)); From 72ed1816ecae16e442a15f68fbbb1a73ccf1bb81 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 24 Sep 2020 13:59:07 -0400 Subject: [PATCH 035/315] security: Fix authz check for pad names with encoded characters Also: * Minor test cleanups (`function` instead of arrow functions, etc.). * Add a test for a case that was previously not covered. --- src/node/hooks/express/webaccess.js | 7 ++- tests/backend/specs/socketio.js | 92 ++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 30 deletions(-) diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index fe5a40535..fd8c935e6 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -39,12 +39,13 @@ exports.checkAccess = (req, res, next) => { if (!level) return fail(); const user = req.session.user; if (user == null) return next(); // This will happen if authentication is not required. - const padID = (req.path.match(/^\/p\/(.*)$/) || [])[1]; - if (padID == null) return next(); + const encodedPadId = (req.path.match(/^\/p\/(.*)$/) || [])[1]; + if (encodedPadId == null) return next(); + const padId = decodeURIComponent(encodedPadId); // The user was granted access to a pad. Remember the authorization level in the user's // settings so that SecurityManager can approve or deny specific actions. if (user.padAuthorizations == null) user.padAuthorizations = {}; - user.padAuthorizations[padID] = level; + user.padAuthorizations[padId] = level; return next(); }; diff --git a/tests/backend/specs/socketio.js b/tests/backend/specs/socketio.js index 8dfd8aecc..b685e1f51 100644 --- a/tests/backend/specs/socketio.js +++ b/tests/backend/specs/socketio.js @@ -11,19 +11,19 @@ const settings = require(m('node/utils/Settings')); const supertest = require(m('node_modules/supertest')); const logger = log4js.getLogger('test'); -let client; +let agent; let baseUrl; -before(async () => { +before(async function() { settings.port = 0; settings.ip = 'localhost'; const httpServer = await server.start(); baseUrl = `http://localhost:${httpServer.address().port}`; logger.debug(`HTTP server at ${baseUrl}`); - client = supertest(baseUrl); + agent = supertest(baseUrl); }); -after(async () => { +after(async function() { await server.stop(); }); @@ -107,10 +107,22 @@ const handshake = async (socket, padID) => { return msg; }; -describe('socket.io access checks', () => { +describe('socket.io access checks', function() { + let authorize; + let authorizeHooksBackup; + const cleanUpPads = async () => { + const padIds = ['pad', 'other-pad', 'päd']; + await Promise.all(padIds.map(async (padId) => { + if (await padManager.doesPadExist(padId)) { + const pad = await padManager.getPad(padId); + await pad.remove(); + } + })); + }; const settingsBackup = {}; let socket; - beforeEach(async () => { + + beforeEach(async function() { Object.assign(settingsBackup, settings); assert(socket == null); settings.requireAuthentication = false; @@ -119,67 +131,93 @@ describe('socket.io access checks', () => { admin: {password: 'admin-password', is_admin: true}, user: {password: 'user-password'}, }; - Promise.all(['pad', 'other-pad'].map(async (pad) => { - if (await padManager.doesPadExist(pad)) (await padManager.getPad(pad)).remove(); - })); + authorize = () => true; + authorizeHooksBackup = plugins.hooks.authorize; + plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => { + if (req.session.user == null) return cb([]); // Hasn't authenticated yet. + return cb([authorize(req)]); + }}]; + await cleanUpPads(); }); - afterEach(async () => { + afterEach(async function() { Object.assign(settings, settingsBackup); if (socket) socket.close(); socket = null; + plugins.hooks.authorize = authorizeHooksBackup; + await cleanUpPads(); }); // Normal accesses. - it('!authn anonymous cookie /p/pad -> 200, ok', async () => { - const res = await client.get('/p/pad').expect(200); + it('!authn anonymous cookie /p/pad -> 200, ok', async function() { + const res = await agent.get('/p/pad').expect(200); // Should not throw. socket = await connect(res); const clientVars = await handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); - it('!authn !cookie -> ok', async () => { + it('!authn !cookie -> ok', async function() { // Should not throw. socket = await connect(null); const clientVars = await handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); - it('!authn user /p/pad -> 200, ok', async () => { - const res = await client.get('/p/pad').auth('user', 'user-password').expect(200); + it('!authn user /p/pad -> 200, ok', async function() { + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); // Should not throw. socket = await connect(res); const clientVars = await handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); - it('authn user /p/pad -> 200, ok', async () => { + it('authn user /p/pad -> 200, ok', async function() { settings.requireAuthentication = true; - const res = await client.get('/p/pad').auth('user', 'user-password').expect(200); + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); // Should not throw. socket = await connect(res); const clientVars = await handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); + it('authz user /p/pad -> 200, ok', async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + // Should not throw. + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); + it('supports pad names with characters that must be percent-encoded', async function() { + settings.requireAuthentication = true; + // requireAuthorization is set to true here to guarantee that the user's padAuthorizations + // object is populated. Technically this isn't necessary because the user's padAuthorizations is + // currently populated even if requireAuthorization is false, but setting this to true ensures + // the test remains useful if the implementation ever changes. + settings.requireAuthorization = true; + const encodedPadId = encodeURIComponent('päd'); + const res = await agent.get(`/p/${encodedPadId}`).auth('user', 'user-password').expect(200); + // Should not throw. + socket = await connect(res); + const clientVars = await handshake(socket, 'päd'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); // Abnormal access attempts. - it('authn anonymous /p/pad -> 401, error', async () => { + it('authn anonymous /p/pad -> 401, error', async function() { settings.requireAuthentication = true; - const res = await client.get('/p/pad').expect(401); + const res = await agent.get('/p/pad').expect(401); // Despite the 401, try to create the pad via a socket.io connection anyway. await assert.rejects(connect(res), {message: /authentication required/i}); }); - it('authn !cookie -> error', async () => { + it('authn !cookie -> error', async function() { settings.requireAuthentication = true; await assert.rejects(connect(null), {message: /signed express_sid cookie is required/i}); }); - it('authorization bypass attempt -> error', async () => { - plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => { - if (req.session.user == null) return cb([]); // Hasn't authenticated yet. - // Only allowed to access /p/pad. - return cb([req.path === '/p/pad']); - }}]; + it('authorization bypass attempt -> error', async function() { + // Only allowed to access /p/pad. + authorize = (req) => req.path === '/p/pad'; settings.requireAuthentication = true; settings.requireAuthorization = true; // First authenticate and establish a session. - const res = await client.get('/p/pad').auth('user', 'user-password').expect(200); + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); // Connecting should work because the user successfully authenticated. socket = await connect(res); // Accessing /p/other-pad should fail, despite the successful fetch of /p/pad. From 1e3aa9edffca91557aa438e154be909cba6c22a6 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 13 Sep 2020 00:43:38 -0400 Subject: [PATCH 036/315] pad: Revert back to sending `CLIENT_READY` on reconnect Commit 0bb8d73ba2969bc2c871c5095de19188a75b2b54 fixed the author ID that is saved in the socket.io sessioninfo when the client sends a `CLIENT_READY` with `reconnect` set to true, so it is now safe to undo the workaround from PR #3868. Fixes #4331. --- src/static/js/pad.js | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/static/js/pad.js b/src/static/js/pad.js index a96dc4f1b..37bc0e009 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -170,23 +170,10 @@ function sendClientReady(isReconnect, messageType) "protocolVersion": 2 }; - //this is a reconnect, lets tell the server our revisionnumber - if(isReconnect == true) - { - // Hammer approach for now. This is obviously wrong and needs a proper fix - // TODO: See https://github.com/ether/etherpad-lite/issues/3830 - document.location=document.location; - - // Switching to pad should work but doesn't... - // return pad.switchToPad(padId); // hacky but whatever. - // It might be related to Auth because failure logs... - // [ERROR] console - Auth was never applied to a session. - // If you are using the stress-test tool then restart Etherpad - // and the Stress test tool. - - msg.client_rev=pad.collabClient.getCurrentRevisionNumber(); - msg.reconnect=true; - + // this is a reconnect, lets tell the server our revisionnumber + if (isReconnect) { + msg.client_rev = pad.collabClient.getCurrentRevisionNumber(); + msg.reconnect = true; } socket.json.send(msg); From 6ed11b7605aaa2f917adcea57415efbbee8d59fd Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 2 Sep 2020 21:21:40 -0400 Subject: [PATCH 037/315] PadMessageHandler: Avoid redundant access checks --- src/node/handler/PadMessageHandler.js | 50 +++++++++++++++----------- src/node/handler/SocketIORouter.js | 51 +++------------------------ 2 files changed, 34 insertions(+), 67 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index a106bc252..1ebbc0419 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -218,13 +218,13 @@ exports.handleMessage = async function(client, message) padId = await readOnlyManager.getPadId(padId); } + // FIXME: Allow to override readwrite access with readonly const {session: {user} = {}} = client.client.request; const {accessStatus, authorID} = await securityManager.checkAccess(padId, auth.sessionID, auth.token, auth.password, user); - - if (accessStatus !== "grant") { - // no access, send the client a message that tells him why - client.json.send({ accessStatus }); + if (accessStatus !== 'grant') { + // Access denied. Send the reason to the user. + client.json.send({accessStatus}); return; } if (thisSession.author != null && thisSession.author !== authorID) { @@ -259,7 +259,7 @@ exports.handleMessage = async function(client, message) // Check what type of message we get and delegate to the other methods if (message.type === "CLIENT_READY") { - await handleClientReady(client, message); + await handleClientReady(client, message, authorID); } else if (message.type === "CHANGESET_REQ") { await handleChangesetRequest(client, message); } else if(message.type === "COLLABROOM") { @@ -284,7 +284,7 @@ exports.handleMessage = async function(client, message) messageLogger.warn("Dropped message, unknown COLLABROOM Data Type " + message.data.type); } } else if(message.type === "SWITCH_TO_PAD") { - await handleSwitchToPad(client, message); + await handleSwitchToPad(client, message, authorID); } else { messageLogger.warn("Dropped message, unknown Message Type " + message.type); } @@ -818,11 +818,30 @@ function _correctMarkersInPad(atext, apool) { return builder.toString(); } -async function handleSwitchToPad(client, message) +async function handleSwitchToPad(client, message, _authorID) { - // clear the session and leave the room const currentSessionInfo = sessioninfos[client.id]; const padId = currentSessionInfo.padId; + + // Check permissions for the new pad. + const newPadIds = await readOnlyManager.getIds(message.padId); + const {session: {user} = {}} = client.client.request; + const {accessStatus, authorID} = await securityManager.checkAccess( + newPadIds.padId, message.sessionID, message.token, message.password, user); + if (accessStatus !== 'grant') { + // Access denied. Send the reason to the user. + client.json.send({accessStatus}); + return; + } + // The same token and session ID were passed to checkAccess in handleMessage, so this second call + // to checkAccess should return the same author ID. + assert(authorID === _authorID); + assert(authorID === currentSessionInfo.author); + + // Check if the connection dropped during the access check. + if (sessioninfos[client.id] !== currentSessionInfo) return; + + // clear the session and leave the room _getRoomClients(padId).forEach(client => { let sinfo = sessioninfos[client.id]; if (sinfo && sinfo.author === currentSessionInfo.author) { @@ -835,7 +854,7 @@ async function handleSwitchToPad(client, message) // start up the new pad const newSessionInfo = sessioninfos[client.id]; createSessionInfoAuth(newSessionInfo, message); - await handleClientReady(client, message); + await handleClientReady(client, message, authorID); } // Creates/replaces the auth object in the given session info. @@ -860,7 +879,7 @@ function createSessionInfoAuth(sessionInfo, message) * @param client the client that send this message * @param message the message from the client */ -async function handleClientReady(client, message) +async function handleClientReady(client, message, authorID) { // check if all ok if (!message.token) { @@ -888,17 +907,6 @@ async function handleClientReady(client, message) // Get ro/rw id:s let padIds = await readOnlyManager.getIds(message.padId); - // FIXME: Allow to override readwrite access with readonly - const {session: {user} = {}} = client.client.request; - const {accessStatus, authorID} = await securityManager.checkAccess( - padIds.padId, message.sessionID, message.token, message.password, user); - - // no access, send the client a message that tells him why - if (accessStatus !== "grant") { - client.json.send({ accessStatus }); - return; - } - // get all authordata of this new user assert(authorID); let value = await authorManager.getAuthor(authorID); diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index 23f3e459c..a52393634 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -65,8 +65,6 @@ exports.setSocketIO = function(_socket) { remoteAddress[client.id] = client.handshake.address; } - var clientAuthorized = false; - // wrap the original send function to log the messages client._send = client.send; client.send = function(message) { @@ -84,37 +82,12 @@ exports.setSocketIO = function(_socket) { messageLogger.warn("Protocolversion header is not correct:" + stringifyWithoutPassword(message)); return; } - - if (clientAuthorized) { - // client is authorized, everything ok - await handleMessage(client, message); - } else { - // try to authorize the client - if (message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) { - // check for read-only pads - let padId = message.padId; - if (padId.indexOf("r.") === 0) { - padId = await readOnlyManager.getPadId(message.padId); - } - - const {session: {user} = {}} = client.client.request; - const {accessStatus} = await securityManager.checkAccess( - padId, message.sessionID, message.token, message.password, user); - - if (accessStatus === "grant") { - // access was granted, mark the client as authorized and handle the message - clientAuthorized = true; - await handleMessage(client, message); - } else { - // no access, send the client a message that tells him why - messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message)); - client.json.send({ accessStatus }); - } - } else { - // drop message - messageLogger.warn("Dropped message because of bad permissions:" + stringifyWithoutPassword(message)); - } + if (!message.component || !components[message.component]) { + messageLogger.error("Can't route the message:" + stringifyWithoutPassword(message)); + return; } + messageLogger.debug("from " + client.id + ": " + stringifyWithoutPassword(message)); + await components[message.component].handleMessage(client, message); }); client.on('disconnect', function() { @@ -126,20 +99,6 @@ exports.setSocketIO = function(_socket) { }); } -// try to handle the message of this client -async function handleMessage(client, message) -{ - if (message.component && components[message.component]) { - // check if component is registered in the components array - if (components[message.component]) { - messageLogger.debug("from " + client.id + ": " + stringifyWithoutPassword(message)); - await components[message.component].handleMessage(client, message); - } - } else { - messageLogger.error("Can't route the message:" + stringifyWithoutPassword(message)); - } -} - // returns a stringified representation of a message, removes the password // this ensures there are no passwords in the log function stringifyWithoutPassword(message) From 02757079c0c2d19a612b61a11a054a86faa66ab4 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 11 Sep 2020 19:46:47 -0400 Subject: [PATCH 038/315] security: Enable authorize plugins to grant modify-only access --- doc/api/hooks_server-side.md | 10 ++++-- src/node/db/SecurityManager.js | 5 ++- src/node/hooks/express/webaccess.js | 1 + tests/backend/specs/socketio.js | 54 +++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 370e782ed..95ad6c1c9 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -253,8 +253,8 @@ following are true: For pre-authentication invocations of your authorize function, you can pass the following values to the provided callback: -* `[true]` or `['create']` will immediately grant access without requiring the - user to authenticate. +* `[true]`, `['create']`, or `['modify']` will immediately grant access without + requiring the user to authenticate. * `[false]` will trigger authentication unless authentication is not required. * `[]` or `undefined` will defer the decision to the next authorization plugin (if any, otherwise it is the same as calling with `[false]`). @@ -267,7 +267,11 @@ public. For post-authentication invocations of your authorize function, you can pass the following values to the provided callback: -* `[true]` or `['create']` will grant access. +* `[true]` or `['create']` will grant access to modify or create the pad if the + request is for a pad, otherwise access is simply granted. (Access will be + downgraded to modify-only if `settings.editOnly` is true.) +* `['modify']` will grant access to modify but not create the pad if the + request is for a pad, otherwise access is simply granted. * `[false]` will deny access. * `[]` or `undefined` will defer the authorization decision to the next authorization plugin (if any, otherwise deny). diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 781f47e85..19fc23c26 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -58,6 +58,8 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user return DENY; } + let canCreate = !settings.editOnly; + if (settings.requireAuthentication) { // Make sure the user has authenticated if authentication is required. The caller should have // already performed this check, but it is repeated here just in case. @@ -73,6 +75,7 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user authLogger.debug('access denied: unauthorized'); return DENY; } + if (level !== 'create') canCreate = false; } // allow plugins to deny access @@ -88,7 +91,7 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user const p_padExists = padManager.doesPadExist(padID); const padExists = await p_padExists; - if (!padExists && settings.editOnly) { + if (!padExists && !canCreate) { authLogger.debug('access denied: user attempted to create a pad, which is prohibited'); return DENY; } diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index fd8c935e6..01ba8a926 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -13,6 +13,7 @@ exports.normalizeAuthzLevel = (level) => { switch (level) { case true: return 'create'; + case 'modify': case 'create': return level; default: diff --git a/tests/backend/specs/socketio.js b/tests/backend/specs/socketio.js index b685e1f51..f6cd25a63 100644 --- a/tests/backend/specs/socketio.js +++ b/tests/backend/specs/socketio.js @@ -125,6 +125,7 @@ describe('socket.io access checks', function() { beforeEach(async function() { Object.assign(settingsBackup, settings); assert(socket == null); + settings.editOnly = false; settings.requireAuthentication = false; settings.requireAuthorization = false; settings.users = { @@ -224,4 +225,57 @@ describe('socket.io access checks', function() { const message = await handshake(socket, 'other-pad'); assert.equal(message.accessStatus, 'deny'); }); + + // Authorization levels via authorize hook + it("level='create' -> can create", async () => { + authorize = () => 'create'; + settings.requireAuthentication = true; + settings.requireAuthorization = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + }); + it('level=true -> can create', async () => { + authorize = () => true; + settings.requireAuthentication = true; + settings.requireAuthorization = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + }); + it("level='modify' -> can modify", async () => { + const pad = await padManager.getPad('pad'); // Create the pad. + authorize = () => 'modify'; + settings.requireAuthentication = true; + settings.requireAuthorization = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + }); + it("level='create' settings.editOnly=true -> unable to create", async () => { + authorize = () => 'create'; + settings.requireAuthentication = true; + settings.requireAuthorization = true; + settings.editOnly = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it("level='modify' settings.editOnly=false -> unable to create", async () => { + authorize = () => 'modify'; + settings.requireAuthentication = true; + settings.requireAuthorization = true; + settings.editOnly = false; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); }); From 4332affba6264cc886878b36873266f9e1dbc457 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 26 Sep 2020 14:04:17 -0400 Subject: [PATCH 039/315] Fix typo in session check (sesion -> session) --- src/node/db/SecurityManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 19fc23c26..ff28ff8fe 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -129,7 +129,7 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user return DENY; } - const passwordExempt = settings.sessionNoPassword && sesionAuthorID != null; + const passwordExempt = settings.sessionNoPassword && sessionAuthorID != null; const requirePassword = pad.isPasswordProtected() && !passwordExempt; if (requirePassword) { if (password == null) { From 3bb71e14d10b0a6e2b825603df063f9cb5e7ddbd Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 13 Sep 2020 00:00:50 -0400 Subject: [PATCH 040/315] PadMessageHandler: Logging improvements --- src/node/handler/PadMessageHandler.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 1ebbc0419..56e506023 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -134,7 +134,13 @@ exports.handleDisconnect = async function(client) ip = 'ANONYMOUS'; } - accessLogger.info('[LEAVE] Pad "' + session.padId + '": Author "' + session.author + '" on client ' + client.id + ' with IP "' + ip + '" left the pad'); + const {session: {user} = {}} = client.client.request; + accessLogger.info('[LEAVE]' + + ` pad:${session.padId}` + + ` socket:${client.id}` + + ` IP:${ip}` + + ` authorID:${session.author}` + + ((user && user.username) ? ` username:${user.username}` : '')); // get the author color out of the db let color = await authorManager.getAuthorColorId(session.author); @@ -235,6 +241,7 @@ exports.handleMessage = async function(client, message) ` IP:${settings.disableIPlogging ? ANONYMOUS : remoteAddress[client.id]}` + ` originalAuthorID:${thisSession.author}` + ` newAuthorID:${authorID}` + + ((user && user.username) ? ` username:${user.username}` : '') + ` message:${message}`); client.json.send({disconnect: 'rejected'}); return; @@ -618,7 +625,7 @@ async function handleUserChanges(data) // 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("Trying to submit changes as another author in changeset " + changeset); + throw new Error(`Author ${thisSession.author} tried to submit changes as author ${attr[1]} in changeset ${changeset}`); } }); } @@ -632,7 +639,7 @@ async function handleUserChanges(data) // There is an error in this changeset, so just refuse it client.json.send({ disconnect: "badChangeset" }); stats.meter('failedChangesets').mark(); - throw new Error("Can't apply USER_CHANGES, because " + e.message); + throw new Error(`Can't apply USER_CHANGES from Socket ${client.id} because: ${e.message}`); } // ex. applyUserChanges @@ -976,11 +983,13 @@ async function handleClientReady(client, message, authorID) ip = 'ANONYMOUS'; } - if (pad.head > 0) { - accessLogger.info('[ENTER] Pad "' + padIds.padId + '": Client ' + client.id + ' with IP "' + ip + '" entered the pad'); - } else if (pad.head === 0) { - accessLogger.info('[CREATE] Pad "' + padIds.padId + '": Client ' + client.id + ' with IP "' + ip + '" created the pad'); - } + const {session: {user} = {}} = client.client.request; + accessLogger.info(`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` + + ` pad:${padIds.padId}` + + ` socket:${client.id}` + + ` IP:${ip}` + + ` authorID:${authorID}` + + ((user && user.username) ? ` username:${user.username}` : '')); if (message.reconnect) { // If this is a reconnect, we don't have to send the client the ClientVars again From 889a3f72618276a8cbafe9985b4fca02d0525980 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 24 Sep 2020 19:09:57 -0400 Subject: [PATCH 041/315] Bump Etherpad version in src/package-lock.json --- src/package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/package-lock.json b/src/package-lock.json index 7ff64c3b1..884b67644 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1,6 +1,6 @@ { "name": "ep_etherpad-lite", - "version": "1.8.5", + "version": "1.8.6", "lockfileVersion": 1, "requires": true, "dependencies": { From ab5934cbdae00440759edc81077e35966f0867d2 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 27 Aug 2020 14:33:58 -0400 Subject: [PATCH 042/315] webaccess: Split authFailure hook into authnFailure and authzFailure This makes it possible for plugins to return different pages to the user depending on whether the auth failure was authn or authz. --- doc/api/hooks_server-side.md | 74 +++++++++++++++- src/node/hooks/express/webaccess.js | 32 ++++--- tests/backend/specs/webaccess.js | 132 ++++++++++++++++++---------- 3 files changed, 177 insertions(+), 61 deletions(-) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 95ad6c1c9..18de6b036 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -308,7 +308,7 @@ Things in context: This hook is called to handle authentication. Plugins that supply an authenticate function should probably also supply an -authFailure function unless falling back to HTTP basic authentication is +authnFailure function unless falling back to HTTP basic authentication is appropriate upon authentication failure. This hook is only called if either the `requireAuthentication` setting is true @@ -359,10 +359,12 @@ Things in context: 2. res - the response object 3. next - ? +**DEPRECATED:** Use authnFailure or authzFailure instead. + This hook is called to handle an authentication or authorization failure. Plugins that supply an authenticate function should probably also supply an -authFailure function unless falling back to HTTP basic authentication is +authnFailure function unless falling back to HTTP basic authentication is appropriate upon authentication failure. A plugin's authFailure function is only called if all of the following are true: @@ -370,6 +372,10 @@ A plugin's authFailure function is only called if all of the following are true: * There was an authentication or authorization failure. * The failure was not already handled by an authFailure function from another plugin. +* For authentication failures: The failure was not already handled by the + authnFailure hook. +* For authorization failures: The failure was not already handled by the + authzFailure hook. Calling the provided callback with `[true]` tells Etherpad that the failure was handled and no further error handling is required. Calling the callback with @@ -389,6 +395,70 @@ exports.authFailure = (hookName, context, cb) => { }; ``` +## authnFailure +Called from: src/node/hooks/express/webaccess.js + +Things in context: + +1. req - the request object +2. res - the response object + +This hook is called to handle an authentication failure. + +Plugins that supply an authenticate function should probably also supply an +authnFailure function unless falling back to HTTP basic authentication is +appropriate upon authentication failure. + +A plugin's authnFailure function is only called if the authentication failure +was not already handled by an authnFailure function from another plugin. + +Calling the provided callback with `[true]` tells Etherpad that the failure was +handled and no further error handling is required. Calling the callback with +`[]` or `undefined` defers error handling to an authnFailure function from +another plugin (if any, otherwise fall back to the deprecated authFailure hook). + +Example: + +``` +exports.authnFailure = (hookName, context, cb) => { + if (notApplicableToThisPlugin(context)) return cb([]); + context.res.redirect(makeLoginURL(context.req)); + return cb([true]); +}; +``` + +## authzFailure +Called from: src/node/hooks/express/webaccess.js + +Things in context: + +1. req - the request object +2. res - the response object + +This hook is called to handle an authorization failure. + +A plugin's authzFailure function is only called if the authorization failure was +not already handled by an authzFailure function from another plugin. + +Calling the provided callback with `[true]` tells Etherpad that the failure was +handled and no further error handling is required. Calling the callback with +`[]` or `undefined` defers error handling to an authzFailure function from +another plugin (if any, otherwise fall back to the deprecated authFailure hook). + +Example: + +``` +exports.authzFailure = (hookName, context, cb) => { + if (notApplicableToThisPlugin(context)) return cb([]); + if (needsPremiumAccount(context.req) && !context.req.session.user.premium) { + context.res.status(200).send(makeUpgradeToPremiumAccountPage(context.req)); + return cb([true]); + } + // Use the generic 403 forbidden response. + return cb([]); +}; +``` + ## handleMessage Called from: src/node/handler/PadMessageHandler.js diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 01ba8a926..1dfa24127 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -8,6 +8,8 @@ const stats = require('ep_etherpad-lite/node/stats'); const sessionModule = require('express-session'); const cookieParser = require('cookie-parser'); +hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead'; + exports.normalizeAuthzLevel = (level) => { if (!level) return false; switch (level) { @@ -70,8 +72,8 @@ exports.checkAccess = (req, res, next) => { // 3) Try to access the thing again. If this fails, give the user a 403 error. // // Plugins can use the 'next' callback (from the hook's context) to break out at any point (e.g., - // to process an OAuth callback). Plugins can use the authFailure hook to override the default - // error handling behavior (e.g., to redirect to a login page). + // to process an OAuth callback). Plugins can use the authnFailure and authzFailure hooks to + // override the default error handling behavior (e.g., to redirect to a login page). let step1PreAuthenticate, step2Authenticate, step3Authorize; @@ -93,14 +95,17 @@ exports.checkAccess = (req, res, next) => { hooks.aCallFirst('authenticate', ctx, hookResultMangle((ok) => { if (!ok) { const failure = () => { - return hooks.aCallFirst('authFailure', {req, res, next}, hookResultMangle((ok) => { + return hooks.aCallFirst('authnFailure', {req, res}, hookResultMangle((ok) => { if (ok) return; - // No plugin handled the authentication failure. Fall back to basic authentication. - res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); - // Delay the error response for 1s to slow down brute force attacks. - setTimeout(() => { - res.status(401).send('Authentication Required'); - }, 1000); + return hooks.aCallFirst('authFailure', {req, res, next}, hookResultMangle((ok) => { + if (ok) return; + // No plugin handled the authentication failure. Fall back to basic authentication. + res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); + // Delay the error response for 1s to slow down brute force attacks. + setTimeout(() => { + res.status(401).send('Authentication Required'); + }, 1000); + })); })); }; // Fall back to HTTP basic auth. @@ -127,10 +132,13 @@ exports.checkAccess = (req, res, next) => { }; step3Authorize = () => authorize(() => { - return hooks.aCallFirst('authFailure', {req, res, next}, hookResultMangle((ok) => { + return hooks.aCallFirst('authzFailure', {req, res}, hookResultMangle((ok) => { if (ok) return; - // No plugin handled the authorization failure. - res.status(403).send('Forbidden'); + return hooks.aCallFirst('authFailure', {req, res, next}, hookResultMangle((ok) => { + if (ok) return; + // No plugin handled the authorization failure. + res.status(403).send('Forbidden'); + })); })); }); diff --git a/tests/backend/specs/webaccess.js b/tests/backend/specs/webaccess.js index 8367429f0..029b41ddb 100644 --- a/tests/backend/specs/webaccess.js +++ b/tests/backend/specs/webaccess.js @@ -95,30 +95,43 @@ describe('webaccess without any plugins', function() { }); }); -describe('webaccess with authFailure plugin', function() { - let handle, returnUndef, status, called; - const authFailure = (hookName, context, cb) => { - assert.equal(hookName, 'authFailure'); - assert(context != null); - assert(context.req != null); - assert(context.res != null); - assert(context.next != null); - assert(!called); - called = true; - if (handle) { - context.res.status(status).send('injected content'); - return cb([true]); +describe('webaccess with authnFailure, authzFailure, authFailure hooks', function() { + const Handler = class { + constructor(hookName) { + this.hookName = hookName; + this.shouldHandle = false; + this.called = false; + } + handle(hookName, context, cb) { + assert.equal(hookName, this.hookName); + assert(context != null); + assert(context.req != null); + assert(context.res != null); + assert(!this.called); + this.called = true; + if (this.shouldHandle) { + context.res.status(200).send(this.hookName); + return cb([true]); + } + return cb([]); } - if (returnUndef) return cb(); - return cb([]); }; - + const handlers = {}; + const hookNames = ['authnFailure', 'authzFailure', 'authFailure']; const settingsBackup = {}; - let authFailureHooksBackup; - before(function() { + const hooksBackup = {}; + + beforeEach(function() { Object.assign(settingsBackup, settings); - authFailureHooksBackup = plugins.hooks.authFailure; - plugins.hooks.authFailure = [{hook_fn: authFailure}]; + hookNames.forEach((hookName) => { + if (plugins.hooks[hookName] == null) plugins.hooks[hookName] = []; + }); + Object.assign(hooksBackup, plugins.hooks); + hookNames.forEach((hookName) => { + const handler = new Handler(hookName); + handlers[hookName] = handler; + plugins.hooks[hookName] = [{hook_fn: handler.handle.bind(handler)}]; + }); settings.requireAuthentication = true; settings.requireAuthorization = true; settings.users = { @@ -126,41 +139,66 @@ describe('webaccess with authFailure plugin', function() { user: {password: 'user-password'}, }; }); - after(function() { - Object.assign(settings, settingsBackup); - plugins.hooks.authFailure = authFailureHooksBackup; - }); - - beforeEach(function() { - handle = false; - returnUndef = false; - status = 200; - called = false; - }); afterEach(function() { - assert(called); + Object.assign(settings, settingsBackup); + Object.assign(plugins.hooks, hooksBackup); }); - it('authn fail, hook handles -> 200', async function() { - handle = true; - await agent.get('/').expect(200, /injected content/); - }); - it('authn fail, hook defers (undefined) -> 401', async function() { - returnUndef = true; + // authn failure tests + it('authn fail, no hooks handle -> 401', async function() { await agent.get('/').expect(401); + assert(handlers['authnFailure'].called); + assert(!handlers['authzFailure'].called); + assert(handlers['authFailure'].called); }); - it('authn fail, hook defers (empty list) -> 401', async function() { - await agent.get('/').expect(401); + it('authn fail, authnFailure handles', async function() { + handlers['authnFailure'].shouldHandle = true; + await agent.get('/').expect(200, 'authnFailure'); + assert(handlers['authnFailure'].called); + assert(!handlers['authzFailure'].called); + assert(!handlers['authFailure'].called); }); - it('authz fail, hook handles -> 200', async function() { - handle = true; - await agent.get('/').auth('user', 'user-password').expect(200, /injected content/); + it('authn fail, authFailure handles', async function() { + handlers['authFailure'].shouldHandle = true; + await agent.get('/').expect(200, 'authFailure'); + assert(handlers['authnFailure'].called); + assert(!handlers['authzFailure'].called); + assert(handlers['authFailure'].called); }); - it('authz fail, hook defers (undefined) -> 403', async function() { - returnUndef = true; + it('authnFailure trumps authFailure', async function() { + handlers['authnFailure'].shouldHandle = true; + handlers['authFailure'].shouldHandle = true; + await agent.get('/').expect(200, 'authnFailure'); + assert(handlers['authnFailure'].called); + assert(!handlers['authFailure'].called); + }); + + // authz failure tests + it('authz fail, no hooks handle -> 403', async function() { await agent.get('/').auth('user', 'user-password').expect(403); + assert(!handlers['authnFailure'].called); + assert(handlers['authzFailure'].called); + assert(handlers['authFailure'].called); }); - it('authz fail, hook defers (empty list) -> 403', async function() { - await agent.get('/').auth('user', 'user-password').expect(403); + it('authz fail, authzFailure handles', async function() { + handlers['authzFailure'].shouldHandle = true; + await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure'); + assert(!handlers['authnFailure'].called); + assert(handlers['authzFailure'].called); + assert(!handlers['authFailure'].called); + }); + it('authz fail, authFailure handles', async function() { + handlers['authFailure'].shouldHandle = true; + await agent.get('/').auth('user', 'user-password').expect(200, 'authFailure'); + assert(!handlers['authnFailure'].called); + assert(handlers['authzFailure'].called); + assert(handlers['authFailure'].called); + }); + it('authzFailure trumps authFailure', async function() { + handlers['authzFailure'].shouldHandle = true; + handlers['authFailure'].shouldHandle = true; + await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure'); + assert(handlers['authzFailure'].called); + assert(!handlers['authFailure'].called); }); }); From 90adc50289264a9d30cfa38ae9a724a369bee9f2 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sat, 26 Sep 2020 19:41:33 +0100 Subject: [PATCH 043/315] tests: Include some plugins in tests (#4339) This PR introduces testing of plugins from the ether/ organization on Github. Each plugin is added into ``.travis``. Frontend plugins tests are run exclusive to core tests. Backend runs both core and plugins with core. Including frontend core tests with plugin tests caused the session to overrun causing errors. --- .travis.yml | 45 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1622e0afe..cf7307501 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ before_install: install: - "bin/installDeps.sh" - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" - - "npm install ep_hash_auth" + # Installing some plugins script: - "tests/frontend/travis/runner.sh" @@ -32,7 +32,7 @@ jobs: # we can only frontend tests from the ether/ organization and not from forks. # To request tests to be run ask a maintainer to fork your repo to ether/ - if: fork = false - name: "Test the Frontend" + name: "Test the Frontend without Plugins" install: #FIXME - "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/g' settings.json.template > settings.json" @@ -41,7 +41,7 @@ jobs: - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" script: - "tests/frontend/travis/runner.sh" - - name: "Run the Backend tests" + - name: "Run the Backend tests without Plugins" install: - "bin/installDeps.sh" - "cd src && npm install && cd -" @@ -55,13 +55,50 @@ jobs: # - "docker build -t etherpad:test ." # - "docker run -d -p 9001:9001 etherpad:test && sleep 3" # - "cd src && npm run test-container" - - name: "Load test Etherpad" + - name: "Load test Etherpad without Plugins" install: - "bin/installDeps.sh" - "cd src && npm install && cd -" - "npm install -g etherpad-load-test" script: - "tests/frontend/travis/runnerLoadTest.sh" + # we can only frontend tests from the ether/ organization and not from forks. + # To request tests to be run ask a maintainer to fork your repo to ether/ + - if: fork = false + name: "Test the Frontend Plugins only" + install: + #FIXME + - "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/g' settings.json.template > settings.json" + - "tests/frontend/travis/sauce_tunnel.sh" + - "bin/installDeps.sh" + - "rm tests/frontend/specs/*" + - "npm install ep_hash_auth ep_cursortrace ep_subscript_and_superscript ep_headings2 ep_author_hover ep_align ep_webrtc ep_table_of_contents ep_font_size ep_markdown ep_spellcheck" + - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" + script: + - "tests/frontend/travis/runner.sh" + - name: "Run the Backend tests with Plugins" + install: + - "bin/installDeps.sh" + - "npm install ep_hash_auth ep_cursortrace ep_subscript_and_superscript ep_headings2 ep_author_hover ep_align ep_webrtc ep_table_of_contents ep_font_size ep_markdown ep_spellcheck" + - "cd src && npm install && cd -" + script: + - "tests/frontend/travis/runnerBackend.sh" +## Temporarily commented out the Dockerfile tests +# - name: "Test the Dockerfile" +# install: +# - "cd src && npm install && cd -" +# script: +# - "docker build -t etherpad:test ." +# - "docker run -d -p 9001:9001 etherpad:test && sleep 3" +# - "cd src && npm run test-container" + - name: "Load test Etherpad with Plugins" + install: + - "bin/installDeps.sh" + - "npm install ep_hash_auth ep_cursortrace ep_subscript_and_superscript ep_headings2 ep_comments_page ep_author_hover ep_align ep_webrtc ep_table_of_contents ep_font_size ep_markdown ep_spellcheck" + - "cd src && npm install && cd -" + - "npm install -g etherpad-load-test" + script: + - "tests/frontend/travis/runnerLoadTest.sh" notifications: irc: From e88c532172bf36c6eb2a6c656b7458ad18592c6f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 26 Sep 2020 14:36:36 -0400 Subject: [PATCH 044/315] tests: Delete unused variable --- tests/backend/specs/api/sessionsAndGroups.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/backend/specs/api/sessionsAndGroups.js b/tests/backend/specs/api/sessionsAndGroups.js index cbc6e0a1a..06e816994 100644 --- a/tests/backend/specs/api/sessionsAndGroups.js +++ b/tests/backend/specs/api/sessionsAndGroups.js @@ -10,7 +10,6 @@ var filePath = path.join(__dirname, '../../../../APIKEY.txt'); var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); apiKey = apiKey.replace(/\n$/, ""); var apiVersion = 1; -var testPadId = makeid(); var groupID = ""; var authorID = ""; var sessionID = ""; From 4527254bcc69fd651cd6d216b8125cb0e29fa13e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 26 Sep 2020 14:38:24 -0400 Subject: [PATCH 045/315] tests: Use `let` and `const` instead of `var` --- tests/backend/specs/api/sessionsAndGroups.js | 35 ++++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/tests/backend/specs/api/sessionsAndGroups.js b/tests/backend/specs/api/sessionsAndGroups.js index 06e816994..9a5970be4 100644 --- a/tests/backend/specs/api/sessionsAndGroups.js +++ b/tests/backend/specs/api/sessionsAndGroups.js @@ -1,19 +1,18 @@ -var assert = require('assert') - supertest = require(__dirname+'/../../../../src/node_modules/supertest'), - fs = require('fs'), - settings = require(__dirname + '/../../../../src/node/utils/Settings'), - api = supertest('http://'+settings.ip+":"+settings.port), - path = require('path'); +const assert = require('assert'); +const supertest = require(__dirname + '/../../../../src/node_modules/supertest'); +const fs = require('fs'); +const settings = require(__dirname + '/../../../../src/node/utils/Settings'); +const api = supertest(`http://${settings.ip}:${settings.port}`); +const path = require('path'); -var filePath = path.join(__dirname, '../../../../APIKEY.txt'); +const filePath = path.join(__dirname, '../../../../APIKEY.txt'); -var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); -apiKey = apiKey.replace(/\n$/, ""); -var apiVersion = 1; -var groupID = ""; -var authorID = ""; -var sessionID = ""; -var padID = makeid(); +const apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}).replace(/\n$/, ''); +let apiVersion = 1; +let groupID = ''; +let authorID = ''; +let sessionID = ''; +let padID = makeid(); describe('API Versioning', function(){ it('errors if can not connect', function(done) { @@ -348,16 +347,16 @@ describe('listPadsOfAuthor', function(){ -var endPoint = function(point){ +const endPoint = function(point) { return '/api/'+apiVersion+'/'+point+'?apikey='+apiKey; } function makeid() { - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for( var i=0; i < 5; i++ ){ + for (let i = 0; i < 5; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; From 24345bf9a8ccb4cf560bfdb09f3847f2af432af0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 26 Sep 2020 15:20:47 -0400 Subject: [PATCH 046/315] tests: Group session and group tests to improve readability --- tests/backend/specs/api/sessionsAndGroups.js | 121 ++++++------------- 1 file changed, 36 insertions(+), 85 deletions(-) diff --git a/tests/backend/specs/api/sessionsAndGroups.js b/tests/backend/specs/api/sessionsAndGroups.js index 9a5970be4..1608f6f09 100644 --- a/tests/backend/specs/api/sessionsAndGroups.js +++ b/tests/backend/specs/api/sessionsAndGroups.js @@ -59,8 +59,8 @@ describe('API Versioning', function(){ -> listPadsOfAuthor(authorID) */ -describe('createGroup', function(){ - it('creates a new group', function(done) { +describe('API: Group creation and deletion', function() { + it('createGroup', function(done) { api.get(endPoint('createGroup')) .expect(function(res){ if(res.body.code !== 0 || !res.body.data.groupID) throw new Error("Unable to create new Pad"); @@ -69,10 +69,8 @@ describe('createGroup', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('listSessionsOfGroup', function(){ - it('Lists the session of a group', function(done) { + it('listSessionsOfGroup for empty group', function(done) { api.get(endPoint('listSessionsOfGroup')+"&groupID="+groupID) .expect(function(res){ if(res.body.code !== 0 || res.body.data !== null) throw new Error("Sessions show as existing for this group"); @@ -80,10 +78,8 @@ describe('listSessionsOfGroup', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('deleteGroup', function(){ - it('Deletes a group', function(done) { + it('deleteGroup', function(done) { api.get(endPoint('deleteGroup')+"&groupID="+groupID) .expect(function(res){ if(res.body.code !== 0) throw new Error("Group failed to be deleted"); @@ -91,10 +87,8 @@ describe('deleteGroup', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('createGroupIfNotExistsFor', function(){ - it('Creates a group if one doesnt exist for mapper 0', function(done) { + it('createGroupIfNotExistsFor', function(done) { api.get(endPoint('createGroupIfNotExistsFor')+"&groupMapper=management") .expect(function(res){ if(res.body.code !== 0 || !res.body.data.groupID) throw new Error("Sessions show as existing for this group"); @@ -102,10 +96,10 @@ describe('createGroupIfNotExistsFor', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) +}); -describe('createGroup', function(){ - it('creates a new group', function(done) { +describe('API: Author creation', function() { + it('createGroup', function(done) { api.get(endPoint('createGroup')) .expect(function(res){ if(res.body.code !== 0 || !res.body.data.groupID) throw new Error("Unable to create new Pad"); @@ -114,10 +108,8 @@ describe('createGroup', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('createAuthor', function(){ - it('Creates an author with a name set', function(done) { + it('createAuthor', function(done) { api.get(endPoint('createAuthor')) .expect(function(res){ if(res.body.code !== 0 || !res.body.data.authorID) throw new Error("Unable to create author"); @@ -125,10 +117,8 @@ describe('createAuthor', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('createAuthor', function(){ - it('Creates an author with a name set', function(done) { + it('createAuthor with name', function(done) { api.get(endPoint('createAuthor')+"&name=john") .expect(function(res){ if(res.body.code !== 0 || !res.body.data.authorID) throw new Error("Unable to create user with name set"); @@ -137,10 +127,8 @@ describe('createAuthor', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('createAuthorIfNotExistsFor', function(){ - it('Creates an author if it doesnt exist already and provides mapping', function(done) { + it('createAuthorIfNotExistsFor', function(done) { api.get(endPoint('createAuthorIfNotExistsFor')+"&authorMapper=chris") .expect(function(res){ if(res.body.code !== 0 || !res.body.data.authorID) throw new Error("Unable to create author with mapper"); @@ -148,10 +136,8 @@ describe('createAuthorIfNotExistsFor', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('getAuthorName', function(){ - it('Gets the author name', function(done) { + it('getAuthorName', function(done) { api.get(endPoint('getAuthorName')+"&authorID="+authorID) .expect(function(res){ if(res.body.code !== 0 || res.body.data !== "john") throw new Error("Unable to get Author Name from Author ID"); @@ -159,14 +145,10 @@ describe('getAuthorName', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) +}); -// BEGIN SESSION TESTS -/////////////////////////////////////// -/////////////////////////////////////// - -describe('createSession', function(){ - it('Creates a session for an Author', function(done) { +describe('API: Sessions', function() { + it('createSession', function(done) { api.get(endPoint('createSession')+"&authorID="+authorID+"&groupID="+groupID+"&validUntil=999999999999") .expect(function(res){ if(res.body.code !== 0 || !res.body.data.sessionID) throw new Error("Unable to create Session"); @@ -175,10 +157,8 @@ describe('createSession', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('getSessionInfo', function(){ - it('Gets session inf', function(done) { + it('getSessionInfo', function(done) { api.get(endPoint('getSessionInfo')+"&sessionID="+sessionID) .expect(function(res){ if(res.body.code !== 0 || !res.body.data.groupID || !res.body.data.authorID || !res.body.data.validUntil) throw new Error("Unable to get Session info"); @@ -186,10 +166,8 @@ describe('getSessionInfo', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('listSessionsOfGroup', function(){ - it('Gets sessions of a group', function(done) { + it('listSessionsOfGroup', function(done) { api.get(endPoint('listSessionsOfGroup')+"&groupID="+groupID) .expect(function(res){ if(res.body.code !== 0 || typeof res.body.data !== "object") throw new Error("Unable to get sessions of a group"); @@ -197,10 +175,8 @@ describe('listSessionsOfGroup', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('deleteSession', function(){ - it('Deletes a session', function(done) { + it('deleteSession', function(done) { api.get(endPoint('deleteSession')+"&sessionID="+sessionID) .expect(function(res){ if(res.body.code !== 0) throw new Error("Unable to delete a session"); @@ -208,10 +184,8 @@ describe('deleteSession', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('getSessionInfo', function(){ - it('Gets session info', function(done) { + it('getSessionInfo of deleted session', function(done) { api.get(endPoint('getSessionInfo')+"&sessionID="+sessionID) .expect(function(res){ if(res.body.code !== 1) throw new Error("Session was not properly deleted"); @@ -219,14 +193,10 @@ describe('getSessionInfo', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) +}); -// GROUP PAD MANAGEMENT -/////////////////////////////////////// -/////////////////////////////////////// - -describe('listPads', function(){ - it('Lists Pads of a Group', function(done) { +describe('API: Group pad management', function() { + it('listPads', function(done) { api.get(endPoint('listPads')+"&groupID="+groupID) .expect(function(res){ if(res.body.code !== 0 || res.body.data.padIDs.length !== 0) throw new Error("Group already had pads for some reason"+res.body.data.padIDs); @@ -234,10 +204,8 @@ describe('listPads', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('createGroupPad', function(){ - it('Creates a Group Pad', function(done) { + it('createGroupPad', function(done) { api.get(endPoint('createGroupPad')+"&groupID="+groupID+"&padName="+padID) .expect(function(res){ if(res.body.code !== 0) throw new Error("Unable to create group pad"); @@ -246,10 +214,8 @@ describe('createGroupPad', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('listPads', function(){ - it('Lists Pads of a Group', function(done) { + it('listPads after creating a group pad', function(done) { api.get(endPoint('listPads')+"&groupID="+groupID) .expect(function(res){ if(res.body.code !== 0 || res.body.data.padIDs.length !== 1) throw new Error("Group isnt listing this pad"); @@ -257,14 +223,10 @@ describe('listPads', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) +}); -// PAD SECURITY /-_-\ -/////////////////////////////////////// -/////////////////////////////////////// - -describe('getPublicStatus', function(){ - it('Gets the public status of a pad', function(done) { +describe('API: Pad security', function() { + it('getPublicStatus', function(done) { api.get(endPoint('getPublicStatus')+"&padID="+padID) .expect(function(res){ if(res.body.code !== 0 || res.body.data.publicstatus) throw new Error("Unable to get public status of this pad"); @@ -272,10 +234,8 @@ describe('getPublicStatus', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('setPublicStatus', function(){ - it('Sets the public status of a pad', function(done) { + it('setPublicStatus', function(done) { api.get(endPoint('setPublicStatus')+"&padID="+padID+"&publicStatus=true") .expect(function(res){ if(res.body.code !== 0) throw new Error("Setting status did not work"); @@ -283,10 +243,8 @@ describe('setPublicStatus', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('getPublicStatus', function(){ - it('Gets the public status of a pad', function(done) { + it('getPublicStatus after changing public status', function(done) { api.get(endPoint('getPublicStatus')+"&padID="+padID) .expect(function(res){ if(res.body.code !== 0 || !res.body.data.publicStatus) throw new Error("Setting public status of this pad did not work"); @@ -294,10 +252,8 @@ describe('getPublicStatus', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('isPasswordProtected', function(){ - it('Gets the public status of a pad', function(done) { + it('isPasswordProtected', function(done) { api.get(endPoint('isPasswordProtected')+"&padID="+padID) .expect(function(res){ if(res.body.code !== 0 || res.body.data.isPasswordProtected) throw new Error("Pad is password protected by default"); @@ -305,10 +261,8 @@ describe('isPasswordProtected', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('setPassword', function(){ - it('Gets the public status of a pad', function(done) { + it('setPassword', function(done) { api.get(endPoint('setPassword')+"&padID="+padID+"&password=test") .expect(function(res){ if(res.body.code !== 0) throw new Error("Unabe to set password"); @@ -316,10 +270,8 @@ describe('setPassword', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) -describe('isPasswordProtected', function(){ - it('Gets the public status of a pad', function(done) { + it('isPasswordProtected after setting password', function(done) { api.get(endPoint('isPasswordProtected')+"&padID="+padID) .expect(function(res){ if(res.body.code !== 0 || !res.body.data.isPasswordProtected) throw new Error("Pad password protection has not applied"); @@ -327,15 +279,14 @@ describe('isPasswordProtected', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) - +}); // NOT SURE HOW TO POPULAT THIS /-_-\ /////////////////////////////////////// /////////////////////////////////////// -describe('listPadsOfAuthor', function(){ - it('Gets the Pads of an Author', function(done) { +describe('API: Misc', function() { + it('listPadsOfAuthor', function(done) { api.get(endPoint('listPadsOfAuthor')+"&authorID="+authorID) .expect(function(res){ if(res.body.code !== 0 || res.body.data.padIDs.length !== 0) throw new Error("Pad password protection has not applied"); @@ -343,7 +294,7 @@ describe('listPadsOfAuthor', function(){ .expect('Content-Type', /json/) .expect(200, done) }); -}) +}); From e01e575c86d92b8607e5da74852a6ee53cae9050 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 26 Sep 2020 15:57:51 -0400 Subject: [PATCH 047/315] tests: Use async/await instead of callbacks, use assert --- tests/backend/specs/api/sessionsAndGroups.js | 392 ++++++++++--------- 1 file changed, 206 insertions(+), 186 deletions(-) diff --git a/tests/backend/specs/api/sessionsAndGroups.js b/tests/backend/specs/api/sessionsAndGroups.js index 1608f6f09..03df340bc 100644 --- a/tests/backend/specs/api/sessionsAndGroups.js +++ b/tests/backend/specs/api/sessionsAndGroups.js @@ -1,4 +1,4 @@ -const assert = require('assert'); +const assert = require('assert').strict; const supertest = require(__dirname + '/../../../../src/node_modules/supertest'); const fs = require('fs'); const settings = require(__dirname + '/../../../../src/node/utils/Settings'); @@ -14,17 +14,16 @@ let authorID = ''; let sessionID = ''; let padID = makeid(); -describe('API Versioning', function(){ - it('errors if can not connect', function(done) { - api.get('/api/') - .expect(function(res){ - apiVersion = res.body.currentVersion; - if (!res.body.currentVersion) throw new Error("No version set in API"); - return; - }) - .expect(200, done) +describe('API Versioning', function() { + it('errors if can not connect', async function() { + await api.get('/api/') + .expect(200) + .expect((res) => { + assert(res.body.currentVersion); + apiVersion = res.body.currentVersion; + }); }); -}) +}); // BEGIN GROUP AND AUTHOR TESTS ///////////////////////////////////// @@ -60,224 +59,244 @@ describe('API Versioning', function(){ */ describe('API: Group creation and deletion', function() { - it('createGroup', function(done) { - api.get(endPoint('createGroup')) - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.groupID) throw new Error("Unable to create new Pad"); - groupID = res.body.data.groupID; - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('createGroup', async function() { + await api.get(endPoint('createGroup')) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.groupID); + groupID = res.body.data.groupID; + }); }); - it('listSessionsOfGroup for empty group', function(done) { - api.get(endPoint('listSessionsOfGroup')+"&groupID="+groupID) - .expect(function(res){ - if(res.body.code !== 0 || res.body.data !== null) throw new Error("Sessions show as existing for this group"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('listSessionsOfGroup for empty group', async function() { + await api.get(endPoint('listSessionsOfGroup') + `&groupID=${groupID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert.equal(res.body.data, null); + }); }); - it('deleteGroup', function(done) { - api.get(endPoint('deleteGroup')+"&groupID="+groupID) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Group failed to be deleted"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('deleteGroup', async function() { + await api.get(endPoint('deleteGroup') + `&groupID=${groupID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + }); }); - it('createGroupIfNotExistsFor', function(done) { - api.get(endPoint('createGroupIfNotExistsFor')+"&groupMapper=management") - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.groupID) throw new Error("Sessions show as existing for this group"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('createGroupIfNotExistsFor', async function() { + await api.get(endPoint('createGroupIfNotExistsFor') + '&groupMapper=management') + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.groupID); + }); }); }); describe('API: Author creation', function() { - it('createGroup', function(done) { - api.get(endPoint('createGroup')) - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.groupID) throw new Error("Unable to create new Pad"); - groupID = res.body.data.groupID; - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('createGroup', async function() { + await api.get(endPoint('createGroup')) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.groupID); + groupID = res.body.data.groupID; + }); }); - it('createAuthor', function(done) { - api.get(endPoint('createAuthor')) - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.authorID) throw new Error("Unable to create author"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('createAuthor', async function() { + await api.get(endPoint('createAuthor')) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.authorID); + }); }); - it('createAuthor with name', function(done) { - api.get(endPoint('createAuthor')+"&name=john") - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.authorID) throw new Error("Unable to create user with name set"); - authorID = res.body.data.authorID; // we will be this author for the rest of the tests - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('createAuthor with name', async function() { + await api.get(endPoint('createAuthor') + '&name=john') + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.authorID); + authorID = res.body.data.authorID; // we will be this author for the rest of the tests + }); }); - it('createAuthorIfNotExistsFor', function(done) { - api.get(endPoint('createAuthorIfNotExistsFor')+"&authorMapper=chris") - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.authorID) throw new Error("Unable to create author with mapper"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('createAuthorIfNotExistsFor', async function() { + await api.get(endPoint('createAuthorIfNotExistsFor') + '&authorMapper=chris') + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.authorID); + }); }); - it('getAuthorName', function(done) { - api.get(endPoint('getAuthorName')+"&authorID="+authorID) - .expect(function(res){ - if(res.body.code !== 0 || res.body.data !== "john") throw new Error("Unable to get Author Name from Author ID"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('getAuthorName', async function() { + await api.get(endPoint('getAuthorName') + `&authorID=${authorID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert.equal(res.body.data, 'john'); + }); }); }); describe('API: Sessions', function() { - it('createSession', function(done) { - api.get(endPoint('createSession')+"&authorID="+authorID+"&groupID="+groupID+"&validUntil=999999999999") - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.sessionID) throw new Error("Unable to create Session"); - sessionID = res.body.data.sessionID; - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('createSession', async function() { + await api.get(endPoint('createSession') + + `&authorID=${authorID}&groupID=${groupID}&validUntil=999999999999`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.sessionID); + sessionID = res.body.data.sessionID; + }); }); - it('getSessionInfo', function(done) { - api.get(endPoint('getSessionInfo')+"&sessionID="+sessionID) - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.groupID || !res.body.data.authorID || !res.body.data.validUntil) throw new Error("Unable to get Session info"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('getSessionInfo', async function() { + await api.get(endPoint('getSessionInfo') + `&sessionID=${sessionID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.groupID); + assert(res.body.data.authorID); + assert(res.body.data.validUntil); + }); }); - it('listSessionsOfGroup', function(done) { - api.get(endPoint('listSessionsOfGroup')+"&groupID="+groupID) - .expect(function(res){ - if(res.body.code !== 0 || typeof res.body.data !== "object") throw new Error("Unable to get sessions of a group"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('listSessionsOfGroup', async function() { + await api.get(endPoint('listSessionsOfGroup') + `&groupID=${groupID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert.equal(typeof res.body.data, 'object'); + }); }); - it('deleteSession', function(done) { - api.get(endPoint('deleteSession')+"&sessionID="+sessionID) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Unable to delete a session"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('deleteSession', async function() { + await api.get(endPoint('deleteSession') + `&sessionID=${sessionID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + }); }); - it('getSessionInfo of deleted session', function(done) { - api.get(endPoint('getSessionInfo')+"&sessionID="+sessionID) - .expect(function(res){ - if(res.body.code !== 1) throw new Error("Session was not properly deleted"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('getSessionInfo of deleted session', async function() { + await api.get(endPoint('getSessionInfo') + `&sessionID=${sessionID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 1); + }); }); }); describe('API: Group pad management', function() { - it('listPads', function(done) { - api.get(endPoint('listPads')+"&groupID="+groupID) - .expect(function(res){ - if(res.body.code !== 0 || res.body.data.padIDs.length !== 0) throw new Error("Group already had pads for some reason"+res.body.data.padIDs); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('listPads', async function() { + await api.get(endPoint('listPads') + `&groupID=${groupID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert.equal(res.body.data.padIDs.length, 0); + }); }); - it('createGroupPad', function(done) { - api.get(endPoint('createGroupPad')+"&groupID="+groupID+"&padName="+padID) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Unable to create group pad"); - padID = res.body.data.padID; - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('createGroupPad', async function() { + await api.get(endPoint('createGroupPad') + `&groupID=${groupID}&padName=${padID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + padID = res.body.data.padID; + }); }); - it('listPads after creating a group pad', function(done) { - api.get(endPoint('listPads')+"&groupID="+groupID) - .expect(function(res){ - if(res.body.code !== 0 || res.body.data.padIDs.length !== 1) throw new Error("Group isnt listing this pad"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('listPads after creating a group pad', async function() { + await api.get(endPoint('listPads') + `&groupID=${groupID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert.equal(res.body.data.padIDs.length, 1); + }); }); }); describe('API: Pad security', function() { - it('getPublicStatus', function(done) { - api.get(endPoint('getPublicStatus')+"&padID="+padID) - .expect(function(res){ - if(res.body.code !== 0 || res.body.data.publicstatus) throw new Error("Unable to get public status of this pad"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('getPublicStatus', async function() { + await api.get(endPoint('getPublicStatus') + `&padID=${padID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(!res.body.data.publicstatus); + }); }); - it('setPublicStatus', function(done) { - api.get(endPoint('setPublicStatus')+"&padID="+padID+"&publicStatus=true") - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Setting status did not work"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('setPublicStatus', async function() { + await api.get(endPoint('setPublicStatus') + `&padID=${padID}&publicStatus=true`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + }); }); - it('getPublicStatus after changing public status', function(done) { - api.get(endPoint('getPublicStatus')+"&padID="+padID) - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.publicStatus) throw new Error("Setting public status of this pad did not work"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('getPublicStatus after changing public status', async function() { + await api.get(endPoint('getPublicStatus') + `&padID=${padID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.publicStatus); + }); }); - it('isPasswordProtected', function(done) { - api.get(endPoint('isPasswordProtected')+"&padID="+padID) - .expect(function(res){ - if(res.body.code !== 0 || res.body.data.isPasswordProtected) throw new Error("Pad is password protected by default"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('isPasswordProtected', async function() { + await api.get(endPoint('isPasswordProtected') + `&padID=${padID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(!res.body.data.isPasswordProtected); + }); }); - it('setPassword', function(done) { - api.get(endPoint('setPassword')+"&padID="+padID+"&password=test") - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Unabe to set password"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('setPassword', async function() { + await api.get(endPoint('setPassword') + `&padID=${padID}&password=test`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + }); }); - it('isPasswordProtected after setting password', function(done) { - api.get(endPoint('isPasswordProtected')+"&padID="+padID) - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.isPasswordProtected) throw new Error("Pad password protection has not applied"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('isPasswordProtected after setting password', async function() { + await api.get(endPoint('isPasswordProtected') + `&padID=${padID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.isPasswordProtected); + }); }); }); @@ -286,21 +305,22 @@ describe('API: Pad security', function() { /////////////////////////////////////// describe('API: Misc', function() { - it('listPadsOfAuthor', function(done) { - api.get(endPoint('listPadsOfAuthor')+"&authorID="+authorID) - .expect(function(res){ - if(res.body.code !== 0 || res.body.data.padIDs.length !== 0) throw new Error("Pad password protection has not applied"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + it('listPadsOfAuthor', async function() { + await api.get(endPoint('listPadsOfAuthor') + `&authorID=${authorID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert.equal(res.body.data.padIDs.length, 0); + }); }); }); const endPoint = function(point) { - return '/api/'+apiVersion+'/'+point+'?apikey='+apiKey; -} + return `/api/${apiVersion}/${point}?apikey=${apiKey}`; +}; function makeid() { From c18831c33378296985495f7dc7b73b241fa0bc47 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 26 Sep 2020 16:11:50 -0400 Subject: [PATCH 048/315] tests: Fix typo (publicstatus -> publicStatus) --- tests/backend/specs/api/sessionsAndGroups.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/backend/specs/api/sessionsAndGroups.js b/tests/backend/specs/api/sessionsAndGroups.js index 03df340bc..870146d64 100644 --- a/tests/backend/specs/api/sessionsAndGroups.js +++ b/tests/backend/specs/api/sessionsAndGroups.js @@ -247,7 +247,7 @@ describe('API: Pad security', function() { .expect('Content-Type', /json/) .expect((res) => { assert.equal(res.body.code, 0); - assert(!res.body.data.publicstatus); + assert(!res.body.data.publicStatus); }); }); From 9f63d9b76a08ec67c046b91bb1eec12188d9ac8d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 26 Sep 2020 16:12:21 -0400 Subject: [PATCH 049/315] tests: Check for true/false, not truthiness --- tests/backend/specs/api/sessionsAndGroups.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/backend/specs/api/sessionsAndGroups.js b/tests/backend/specs/api/sessionsAndGroups.js index 870146d64..1e11ff0a1 100644 --- a/tests/backend/specs/api/sessionsAndGroups.js +++ b/tests/backend/specs/api/sessionsAndGroups.js @@ -247,7 +247,7 @@ describe('API: Pad security', function() { .expect('Content-Type', /json/) .expect((res) => { assert.equal(res.body.code, 0); - assert(!res.body.data.publicStatus); + assert.equal(res.body.data.publicStatus, false); }); }); @@ -266,7 +266,7 @@ describe('API: Pad security', function() { .expect('Content-Type', /json/) .expect((res) => { assert.equal(res.body.code, 0); - assert(res.body.data.publicStatus); + assert.equal(res.body.data.publicStatus, true); }); }); @@ -276,7 +276,7 @@ describe('API: Pad security', function() { .expect('Content-Type', /json/) .expect((res) => { assert.equal(res.body.code, 0); - assert(!res.body.data.isPasswordProtected); + assert.equal(res.body.data.isPasswordProtected, false); }); }); @@ -295,7 +295,7 @@ describe('API: Pad security', function() { .expect('Content-Type', /json/) .expect((res) => { assert.equal(res.body.code, 0); - assert(res.body.data.isPasswordProtected); + assert.equal(res.body.data.isPasswordProtected, true); }); }); }); From c56973ce74f997c4693fce70dd0aaafcf212d56c Mon Sep 17 00:00:00 2001 From: Pedro Beschorner Marin Date: Wed, 16 Sep 2020 14:57:27 -0300 Subject: [PATCH 050/315] Fix readOnly pad export The export request hook wasn't testing if the pad's id was from a read-only pad before validating with the pad manager. This includes an extra step that makes the read-only id verification and also avoids setting the original pad's id as the file's name. --- src/node/db/ReadOnlyManager.js | 10 ++++++++++ src/node/handler/ExportHandler.js | 9 +++++---- src/node/hooks/express/importexport.js | 15 ++++++++++++--- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/node/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.js index 96a52d479..615dbf82c 100644 --- a/src/node/db/ReadOnlyManager.js +++ b/src/node/db/ReadOnlyManager.js @@ -22,6 +22,16 @@ var db = require("./DB"); var 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; +} + /** * returns a read only id for a pad * @param {String} padId the id of the pad diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js index 39638c222..7d9b9c1e7 100644 --- a/src/node/handler/ExportHandler.js +++ b/src/node/handler/ExportHandler.js @@ -49,9 +49,10 @@ const tempDirectory = os.tmpdir(); /** * do a requested export */ -async function doExport(req, res, padId, type) +async function doExport(req, res, padId, readOnlyId, type) { - var fileName = padId; + // avoid naming the read-only file as the original pad's id + var fileName = readOnlyId ? readOnlyId : padId; // allow fileName to be overwritten by a hook, the type type is kept static for security reasons let hookFileName = await hooks.aCallFirst("exportFileName", padId); @@ -130,9 +131,9 @@ async function doExport(req, res, padId, type) } } -exports.doExport = function(req, res, padId, type) +exports.doExport = function(req, res, padId, readOnlyId, type) { - doExport(req, res, padId, type).catch(err => { + doExport(req, res, padId, readOnlyId, type).catch(err => { if (err !== "stop") { throw err; } diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js index 4aa06ecb8..f5c3c34cb 100644 --- a/src/node/hooks/express/importexport.js +++ b/src/node/hooks/express/importexport.js @@ -4,6 +4,7 @@ var settings = require('../../utils/Settings'); var exportHandler = require('../../handler/ExportHandler'); var importHandler = require('../../handler/ImportHandler'); var padManager = require("../../db/PadManager"); +var readOnlyManager = require("../../db/ReadOnlyManager"); var authorManager = require("../../db/AuthorManager"); const rateLimit = require("express-rate-limit"); const securityManager = require("../../db/SecurityManager"); @@ -39,14 +40,22 @@ exports.expressCreateServer = function (hook_name, args, cb) { res.header("Access-Control-Allow-Origin", "*"); if (await hasPadAccess(req, res)) { - let exists = await padManager.doesPadExists(req.params.pad); + let padId = req.params.pad; + + let readOnlyId = null; + if (readOnlyManager.isReadOnlyId(padId)) { + readOnlyId = padId; + padId = await readOnlyManager.getPadId(readOnlyId); + } + + let exists = await padManager.doesPadExists(padId); if (!exists) { - console.warn(`Someone tried to export a pad that doesn't exist (${req.params.pad})`); + console.warn(`Someone tried to export a pad that doesn't exist (${padId})`); return next(); } console.log(`Exporting pad "${req.params.pad}" in ${req.params.type} format`); - exportHandler.doExport(req, res, req.params.pad, req.params.type); + exportHandler.doExport(req, res, padId, readOnlyId, req.params.type); } }); From ea4b9bf7d79d4c27862f0133a7e641c831dd2de9 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sat, 26 Sep 2020 21:57:21 +0100 Subject: [PATCH 051/315] tests: support even slower safari (#4361) --- tests/frontend/specs/responsiveness.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/frontend/specs/responsiveness.js b/tests/frontend/specs/responsiveness.js index e8eb4c604..8938ff41b 100644 --- a/tests/frontend/specs/responsiveness.js +++ b/tests/frontend/specs/responsiveness.js @@ -35,7 +35,7 @@ describe('Responsiveness of Editor', function() { var amount = 200000; //number of blocks of chars we will insert var length = (amount * (chars.length) +1); // include a counter for each space var text = ''; // the text we're gonna insert - this.timeout(amount * 120); // Changed from 100 to 120 to allow Mac OSX Safari to be slow. + this.timeout(amount * 150); // Changed from 100 to 150 to allow Mac OSX Safari to be slow. // get keys to send var keyMultiplier = 10; // multiplier * 10 == total number of key events From 411b278881e0511209dc43f0eebcac01da8a35f8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 27 Aug 2020 21:41:31 -0400 Subject: [PATCH 052/315] webaccess: Log all authentication successes/failures This loses some of the granularity of the default HTTP basic auth (unknown username vs. bad password), but there is considerable value in having logging that is consistent no matter what authentication plugins are installed. --- src/node/hooks/express/webaccess.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 1dfa24127..c9fd4e013 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -94,7 +94,10 @@ exports.checkAccess = (req, res, next) => { } hooks.aCallFirst('authenticate', ctx, hookResultMangle((ok) => { if (!ok) { - const failure = () => { + // Fall back to HTTP basic auth. + if (!httpBasicAuth || !(ctx.username in settings.users) || + settings.users[ctx.username].password !== ctx.password) { + httpLogger.info(`Failed authentication from IP ${req.ip}`); return hooks.aCallFirst('authnFailure', {req, res}, hookResultMangle((ok) => { if (ok) return; return hooks.aCallFirst('authFailure', {req, res, next}, hookResultMangle((ok) => { @@ -107,18 +110,7 @@ exports.checkAccess = (req, res, next) => { }, 1000); })); })); - }; - // Fall back to HTTP basic auth. - if (!httpBasicAuth) return failure(); - if (!(ctx.username in settings.users)) { - httpLogger.info(`Failed authentication from IP ${req.ip} - no such user`); - return failure(); } - if (settings.users[ctx.username].password !== ctx.password) { - httpLogger.info(`Failed authentication from IP ${req.ip} for user ${ctx.username} - incorrect password`); - return failure(); - } - httpLogger.info(`Successful authentication from IP ${req.ip} for user ${ctx.username}`); settings.users[ctx.username].username = ctx.username; req.session.user = settings.users[ctx.username]; } @@ -127,6 +119,9 @@ exports.checkAccess = (req, res, next) => { res.status(500).send('Internal Server Error'); return; } + let username = req.session.user.username; + username = (username != null) ? username : ''; + httpLogger.info(`Successful authentication from IP ${req.ip} for username ${username}`); step3Authorize(); })); }; From 53b80d628029991879b82dd87ade0b89360f1553 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 27 Sep 2020 15:13:55 +0100 Subject: [PATCH 053/315] tests: adding a check before finishing responsiveness test - allowing load test to run for 25 instead of 30 seconds to facilitate travis performance. (#4363) The goal of this PR is to make tests break less frequently. It is yet confirmed if this has worked but time will tell. --- tests/frontend/specs/responsiveness.js | 7 ++++--- tests/frontend/travis/runnerLoadTest.sh | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/frontend/specs/responsiveness.js b/tests/frontend/specs/responsiveness.js index 8938ff41b..121851c25 100644 --- a/tests/frontend/specs/responsiveness.js +++ b/tests/frontend/specs/responsiveness.js @@ -72,14 +72,15 @@ describe('Responsiveness of Editor', function() { } helper.waitFor(function(){ // Wait for the ability to process - return true; // Ghetto but works for now + var el = inner$('body'); + if(el[0].textContent.length > amount) return true; }).done(function(){ var end = Date.now(); // get the current time var delay = end - start; // get the delay as the current time minus the start time - expect(delay).to.be.below(400); + expect(delay).to.be.below(600); done(); - }, 1000); + }, 5000); }, 10000); }); diff --git a/tests/frontend/travis/runnerLoadTest.sh b/tests/frontend/travis/runnerLoadTest.sh index 5ac447758..3f7b6d9ac 100755 --- a/tests/frontend/travis/runnerLoadTest.sh +++ b/tests/frontend/travis/runnerLoadTest.sh @@ -41,8 +41,8 @@ curl http://localhost:9001/p/minifyme -f -s > /dev/null sleep 10 # run the backend tests -echo "Now run the load tests for 30 seconds and if it stalls before 100 then error" -etherpad-loadtest -d 30 +echo "Now run the load tests for 25 seconds and if it stalls before 100 then error" +etherpad-loadtest -d 25 exit_code=$? kill $! From a51132d7124442500e76e30d10c316fb704d9d17 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 27 Sep 2020 19:12:11 +0100 Subject: [PATCH 054/315] tests: test coverage for read only pad ids (#4364) --- tests/backend/specs/api/importexportGetPost.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/backend/specs/api/importexportGetPost.js b/tests/backend/specs/api/importexportGetPost.js index aa72f7072..3515d7776 100644 --- a/tests/backend/specs/api/importexportGetPost.js +++ b/tests/backend/specs/api/importexportGetPost.js @@ -87,6 +87,22 @@ describe('Imports and Exports', function(){ .expect((res) => assert.equal(res.body.data.text, padText.toString())); }); + it('gets read only pad Id and exports the html and text for this pad', async function(){ + let ro = await agent.get(endPoint('getReadOnlyID')+"&padID="+testPadId) + .expect(200) + .expect((res) => assert.ok(JSON.parse(res.text).data.readOnlyID)); + let readOnlyId = JSON.parse(ro.text).data.readOnlyID; + + await agent.get(`/p/${readOnlyId}/export/html`) + .expect(200) + .expect((res) => assert(res.text.indexOf("This is the") !== -1)); + + await agent.get(`/p/${readOnlyId}/export/txt`) + .expect(200) + .expect((res) => assert(res.text.indexOf("This is the") !== -1)); + }); + + describe('Import/Export tests requiring AbiWord/LibreOffice', function() { before(function() { if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && From 304318b6187b0451af0ee5d2bf2935b525a644ad Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 23 Aug 2020 16:56:28 -0400 Subject: [PATCH 055/315] webaccess: Move pre-authn authz check to a separate hook Before this change, the authorize hook was invoked twice: once before authentication and again after (if settings.requireAuthorization is true). Now pre-authentication authorization is instead handled by a new preAuthorize hook, and the authorize hook is only invoked after the user has authenticated. Rationale: Without this change it is too easy to write an authorization plugin that is too permissive. Specifically: * If the plugin does not check the path for /admin then a non-admin user might be able to access /admin pages. * If the plugin assumes that the user has already been authenticated by the time the authorize function is called then unauthenticated users might be able to gain access to restricted resources. This change also avoids calling the plugin's authorize function twice per access, which makes it easier for plugin authors to write an authorization plugin that is easy to understand. This change may break existing authorization plugins: After this change, the authorize hook will no longer be able to authorize non-admin access to /admin pages. This is intentional. Access to admin pages should instead be controlled via the `is_admin` user setting, which can be set in the config file or by an authentication plugin. Also: * Add tests for the authenticate and authorize hooks. * Disable the authentication failure delay when testing. --- doc/api/hooks_server-side.md | 128 +++++++++----- src/node/hooks/express/webaccess.js | 85 ++++++--- src/static/js/pluginfw/hooks.js | 16 +- tests/backend/specs/socketio.js | 6 +- tests/backend/specs/webaccess.js | 263 +++++++++++++++++++++++++++- 5 files changed, 422 insertions(+), 76 deletions(-) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 18de6b036..021f633f3 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -212,6 +212,50 @@ Things in context: I have no idea what this is useful for, someone else will have to add this description. +## preAuthorize +Called from: src/node/hooks/express/webaccess.js + +Things in context: + +1. req - the request object +2. res - the response object +3. next - bypass callback. If this is called instead of the normal callback then + all remaining access checks are skipped. + +This hook is called for each HTTP request before any authentication checks are +performed. Example uses: + +* Always grant access to static content. +* Process an OAuth callback. +* Drop requests from IP addresses that have failed N authentication checks + within the past X minutes. + +A preAuthorize function is always called for each request unless a preAuthorize +function from another plugin (if any) has already explicitly granted or denied +the request. + +You can pass the following values to the provided callback: + +* `[]` defers the access decision to the normal authentication and authorization + checks (or to a preAuthorize function from another plugin, if one exists). +* `[true]` immediately grants access to the requested resource, unless the + request is for an `/admin` page in which case it is treated the same as `[]`. + (This prevents buggy plugins from accidentally granting admin access to the + general public.) +* `[false]` immediately denies the request. The preAuthnFailure hook will be + called to handle the failure. + +Example: + +``` +exports.preAuthorize = (hookName, context, cb) => { + if (ipAddressIsFirewalled(context.req)) return cb([false]); + if (requestIsForStaticContent(context.req)) return cb([true]); + if (requestIsForOAuthCallback(context.req)) return cb([true]); + return cb([]); +}; +``` + ## authorize Called from: src/node/hooks/express/webaccess.js @@ -225,47 +269,23 @@ Things in context: This hook is called to handle authorization. It is especially useful for controlling access to specific paths. -A plugin's authorize function is typically called twice for each access: once -before authentication and again after. Specifically, it is called if all of the -following are true: +A plugin's authorize function is only called if all of the following are true: * The request is not for static content or an API endpoint. (Requests for static content and API endpoints are always authorized, even if unauthenticated.) -* Either authentication has not yet been performed (`context.req.session.user` - is undefined) or the user has successfully authenticated - (`context.req.session.user` is an object containing user-specific settings). -* If the user has successfully authenticated, the user is not an admin. (Admin - users are always authorized.) -* Either the request is for an `/admin` page or the `requireAuthentication` - setting is true. -* Either the request is for an `/admin` page, or the user has not yet - authenticated, or the user has authenticated and the `requireAuthorization` - setting is true. -* For pre-authentication invocations of a plugin's authorize function - (`context.req.session.user` is undefined), an authorize function from a - different plugin has not already caused the pre-authentication authorization - to pass or fail. -* For post-authentication invocations of a plugin's authorize function - (`context.req.session.user` is an object), an authorize function from a - different plugin has not already caused the post-authentication authorization - to pass or fail. +* The `requireAuthentication` and `requireAuthorization` settings are both true. +* The user has already successfully authenticated. +* The user is not an admin (admin users are always authorized). +* The path being accessed is not an `/admin` path (`/admin` paths can only be + accessed by admin users, and admin users are always authorized). +* An authorize function from a different plugin has not already caused + authorization to pass or fail. -For pre-authentication invocations of your authorize function, you can pass the -following values to the provided callback: +Note that the authorize hook cannot grant access to `/admin` pages. If admin +access is desired, the `is_admin` user setting must be set to true. This can be +set in the settings file or by the authenticate hook. -* `[true]`, `['create']`, or `['modify']` will immediately grant access without - requiring the user to authenticate. -* `[false]` will trigger authentication unless authentication is not required. -* `[]` or `undefined` will defer the decision to the next authorization plugin - (if any, otherwise it is the same as calling with `[false]`). - -**WARNING:** Your authorize function can be called for an `/admin` page even if -the user has not yet authenticated. It is your responsibility to fail or defer -authorization if you do not want to grant admin privileges to the general -public. - -For post-authentication invocations of your authorize function, you can pass the -following values to the provided callback: +You can pass the following values to the provided callback: * `[true]` or `['create']` will grant access to modify or create the pad if the request is for a pad, otherwise access is simply granted. (Access will be @@ -281,11 +301,6 @@ Example: ``` exports.authorize = (hookName, context, cb) => { const user = context.req.session.user; - if (!user) { - // The user has not yet authenticated so defer the pre-authentication - // authorization decision to the next plugin. - return cb([]); - } const path = context.req.path; // or context.resource if (isExplicitlyProhibited(user, path)) return cb([false]); if (isExplicitlyAllowed(user, path)) return cb([true]); @@ -395,6 +410,35 @@ exports.authFailure = (hookName, context, cb) => { }; ``` +## preAuthzFailure +Called from: src/node/hooks/express/webaccess.js + +Things in context: + +1. req - the request object +2. res - the response object + +This hook is called to handle a pre-authentication authorization failure. + +A plugin's preAuthzFailure function is only called if the pre-authentication +authorization failure was not already handled by a preAuthzFailure function from +another plugin. + +Calling the provided callback with `[true]` tells Etherpad that the failure was +handled and no further error handling is required. Calling the callback with +`[]` or `undefined` defers error handling to a preAuthzFailure function from +another plugin (if any, otherwise fall back to a generic 403 error page). + +Example: + +``` +exports.preAuthzFailure = (hookName, context, cb) => { + if (notApplicableToThisPlugin(context)) return cb([]); + context.res.status(403).send(renderFancy403Page(context.req)); + return cb([true]); +}; +``` + ## authnFailure Called from: src/node/hooks/express/webaccess.js @@ -435,7 +479,7 @@ Things in context: 1. req - the request object 2. res - the response object -This hook is called to handle an authorization failure. +This hook is called to handle a post-authentication authorization failure. A plugin's authzFailure function is only called if the authorization failure was not already handled by an authzFailure function from another plugin. diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index c9fd4e013..0f3a01ee7 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -24,6 +24,9 @@ exports.normalizeAuthzLevel = (level) => { return false; }; +// Exported so that tests can set this to 0 to avoid unnecessary test slowness. +exports.authnFailureDelayMs = 1000; + exports.checkAccess = (req, res, next) => { const hookResultMangle = (cb) => { return (err, data) => { @@ -31,12 +34,11 @@ exports.checkAccess = (req, res, next) => { }; }; + const requireAdmin = req.path.toLowerCase().indexOf('/admin') === 0; + // This may be called twice per access: once before authentication is checked and once after (if // settings.requireAuthorization is true). const authorize = (fail) => { - // Do not require auth for static paths and the API...this could be a bit brittle - if (req.path.match(/^\/(static|javascripts|pluginfw|api)/)) return next(); - const grant = (level) => { level = exports.normalizeAuthzLevel(level); if (!level) return fail(); @@ -51,35 +53,70 @@ exports.checkAccess = (req, res, next) => { user.padAuthorizations[padId] = level; return next(); }; - - if (req.path.toLowerCase().indexOf('/admin') !== 0) { - if (!settings.requireAuthentication) return grant('create'); - if (!settings.requireAuthorization && req.session && req.session.user) return grant('create'); - } - - if (req.session && req.session.user && req.session.user.is_admin) return grant('create'); - + const isAuthenticated = req.session && req.session.user; + if (isAuthenticated && req.session.user.is_admin) return grant('create'); + const requireAuthn = requireAdmin || settings.requireAuthentication; + if (!requireAuthn) return grant('create'); + if (!isAuthenticated) return grant(false); + if (requireAdmin && !req.session.user.is_admin) return grant(false); + if (!settings.requireAuthorization) return grant('create'); hooks.aCallFirst('authorize', {req, res, next, resource: req.path}, hookResultMangle(grant)); }; - // Access checking is done in three steps: + // Access checking is done in four steps: // - // 1) Try to just access the thing. If access fails (perhaps authentication has not yet completed, + // 1) Check the preAuthorize hook for early permit/deny (permit is only allowed for non-admin + // pages). If any plugin explicitly grants or denies access, skip the remaining steps. + // 2) Try to just access the thing. If access fails (perhaps authentication has not yet completed, // or maybe different credentials are required), go to the next step. - // 2) Try to authenticate. (Or, if already logged in, reauthenticate with different credentials if + // 3) Try to authenticate. (Or, if already logged in, reauthenticate with different credentials if // supported by the authn scheme.) If authentication fails, give the user a 401 error to // request new credentials. Otherwise, go to the next step. - // 3) Try to access the thing again. If this fails, give the user a 403 error. + // 4) Try to access the thing again. If this fails, give the user a 403 error. // // Plugins can use the 'next' callback (from the hook's context) to break out at any point (e.g., - // to process an OAuth callback). Plugins can use the authnFailure and authzFailure hooks to - // override the default error handling behavior (e.g., to redirect to a login page). + // to process an OAuth callback). Plugins can use the preAuthzFailure, authnFailure, and + // authzFailure hooks to override the default error handling behavior (e.g., to redirect to a + // login page). - let step1PreAuthenticate, step2Authenticate, step3Authorize; + let step1PreAuthorize, step2PreAuthenticate, step3Authenticate, step4Authorize; - step1PreAuthenticate = () => authorize(step2Authenticate); + step1PreAuthorize = () => { + // This aCallFirst predicate will cause aCallFirst to call the hook functions one at a time + // until one of them returns a non-empty list, with an exception: If the request is for an + // /admin page, truthy entries are filtered out before checking to see whether the list is + // empty. This prevents plugin authors from accidentally granting admin privileges to the + // general public. + const predicate = (results) => (results != null && + results.filter((x) => (!requireAdmin || !x)).length > 0); + hooks.aCallFirst('preAuthorize', {req, res, next}, (err, results) => { + if (err != null) { + httpLogger.error('Error in preAuthorize hook:', err); + return res.status(500).send('Internal Server Error'); + } + // Do not require auth for static paths and the API...this could be a bit brittle + if (req.path.match(/^\/(static|javascripts|pluginfw|api)/)) results.push(true); + if (requireAdmin) { + // Filter out all 'true' entries to prevent plugin authors from accidentally granting admin + // privileges to the general public. + results = results.filter((x) => !x); + } + if (results.length > 0) { + // Access was explicitly granted or denied. If any value is false then access is denied. + if (results.every((x) => x)) return next(); + return hooks.aCallFirst('preAuthzFailure', {req, res}, hookResultMangle((ok) => { + if (ok) return; + // No plugin handled the pre-authentication authorization failure. + res.status(403).send('Forbidden'); + })); + } + step2PreAuthenticate(); + }, predicate); + }; - step2Authenticate = () => { + step2PreAuthenticate = () => authorize(step3Authenticate); + + step3Authenticate = () => { if (settings.users == null) settings.users = {}; const ctx = {req, res, users: settings.users, next}; // If the HTTP basic auth header is present, extract the username and password so it can be @@ -107,7 +144,7 @@ exports.checkAccess = (req, res, next) => { // Delay the error response for 1s to slow down brute force attacks. setTimeout(() => { res.status(401).send('Authentication Required'); - }, 1000); + }, exports.authnFailureDelayMs); })); })); } @@ -122,11 +159,11 @@ exports.checkAccess = (req, res, next) => { let username = req.session.user.username; username = (username != null) ? username : ''; httpLogger.info(`Successful authentication from IP ${req.ip} for username ${username}`); - step3Authorize(); + step4Authorize(); })); }; - step3Authorize = () => authorize(() => { + step4Authorize = () => authorize(() => { return hooks.aCallFirst('authzFailure', {req, res}, hookResultMangle((ok) => { if (ok) return; return hooks.aCallFirst('authFailure', {req, res, next}, hookResultMangle((ok) => { @@ -137,7 +174,7 @@ exports.checkAccess = (req, res, next) => { })); }); - step1PreAuthenticate(); + step1PreAuthorize(); }; exports.secret = null; diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 3b6be4d9a..13e36a645 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -61,14 +61,15 @@ exports.syncMapFirst = function (lst, fn) { return []; } -exports.mapFirst = function (lst, fn, cb) { +exports.mapFirst = function (lst, fn, cb, predicate) { + if (predicate == null) predicate = (x) => (x != null && x.length > 0); var i = 0; var next = function () { if (i >= lst.length) return cb(null, []); fn(lst[i++], function (err, result) { if (err) return cb(err); - if (result.length) return cb(null, result); + if (predicate(result)) return cb(null, result); next(); }); } @@ -142,7 +143,7 @@ exports.callFirst = function (hook_name, args) { }); } -function aCallFirst(hook_name, args, cb) { +function aCallFirst(hook_name, args, cb, predicate) { if (!args) args = {}; if (!cb) cb = function () {}; if (pluginDefs.hooks[hook_name] === undefined) return cb(null, []); @@ -151,20 +152,21 @@ function aCallFirst(hook_name, args, cb) { function (hook, cb) { hookCallWrapper(hook, hook_name, args, function (res) { cb(null, res); }); }, - cb + cb, + predicate ); } /* return a Promise if cb is not supplied */ -exports.aCallFirst = function (hook_name, args, cb) { +exports.aCallFirst = function (hook_name, args, cb, predicate) { if (cb === undefined) { return new Promise(function(resolve, reject) { aCallFirst(hook_name, args, function(err, res) { return err ? reject(err) : resolve(res); - }); + }, predicate); }); } else { - return aCallFirst(hook_name, args, cb); + return aCallFirst(hook_name, args, cb, predicate); } } diff --git a/tests/backend/specs/socketio.js b/tests/backend/specs/socketio.js index f6cd25a63..717c4f7b8 100644 --- a/tests/backend/specs/socketio.js +++ b/tests/backend/specs/socketio.js @@ -9,12 +9,16 @@ const server = require(m('node/server')); const setCookieParser = require(m('node_modules/set-cookie-parser')); const settings = require(m('node/utils/Settings')); const supertest = require(m('node_modules/supertest')); +const webaccess = require(m('node/hooks/express/webaccess')); const logger = log4js.getLogger('test'); let agent; let baseUrl; +let authnFailureDelayMsBackup; before(async function() { + authnFailureDelayMsBackup = webaccess.authnFailureDelayMs; + webaccess.authnFailureDelayMs = 0; // Speed up tests. settings.port = 0; settings.ip = 'localhost'; const httpServer = await server.start(); @@ -24,6 +28,7 @@ before(async function() { }); after(async function() { + webaccess.authnFailureDelayMs = authnFailureDelayMsBackup; await server.stop(); }); @@ -135,7 +140,6 @@ describe('socket.io access checks', function() { authorize = () => true; authorizeHooksBackup = plugins.hooks.authorize; plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => { - if (req.session.user == null) return cb([]); // Hasn't authenticated yet. return cb([authorize(req)]); }}]; await cleanUpPads(); diff --git a/tests/backend/specs/webaccess.js b/tests/backend/specs/webaccess.js index 029b41ddb..7bce670c4 100644 --- a/tests/backend/specs/webaccess.js +++ b/tests/backend/specs/webaccess.js @@ -6,11 +6,15 @@ const plugins = require(m('static/js/pluginfw/plugin_defs')); const server = require(m('node/server')); const settings = require(m('node/utils/Settings')); const supertest = require(m('node_modules/supertest')); +const webaccess = require(m('node/hooks/express/webaccess')); let agent; const logger = log4js.getLogger('test'); +let authnFailureDelayMsBackup; before(async function() { + authnFailureDelayMsBackup = webaccess.authnFailureDelayMs; + webaccess.authnFailureDelayMs = 0; // Speed up tests. settings.port = 0; settings.ip = 'localhost'; const httpServer = await server.start(); @@ -20,10 +24,11 @@ before(async function() { }); after(async function() { + webaccess.authnFailureDelayMs = authnFailureDelayMsBackup; await server.stop(); }); -describe('webaccess without any plugins', function() { +describe('webaccess: without plugins', function() { const backup = {}; before(async function() { @@ -95,7 +100,261 @@ describe('webaccess without any plugins', function() { }); }); -describe('webaccess with authnFailure, authzFailure, authFailure hooks', function() { +describe('webaccess: preAuthorize, authenticate, and authorize hooks', function() { + let callOrder; + const Handler = class { + constructor(hookName, suffix) { + this.called = false; + this.hookName = hookName; + this.innerHandle = () => []; + this.id = hookName + suffix; + this.checkContext = () => {}; + } + handle(hookName, context, cb) { + assert.equal(hookName, this.hookName); + assert(context != null); + assert(context.req != null); + assert(context.res != null); + assert(context.next != null); + this.checkContext(context); + assert(!this.called); + this.called = true; + callOrder.push(this.id); + return cb(this.innerHandle(context.req)); + } + }; + const handlers = {}; + const hookNames = ['preAuthorize', 'authenticate', 'authorize']; + const hooksBackup = {}; + const settingsBackup = {}; + + beforeEach(async function() { + callOrder = []; + hookNames.forEach((hookName) => { + // Create two handlers for each hook to test deferral to the next function. + const h0 = new Handler(hookName, '_0'); + const h1 = new Handler(hookName, '_1'); + handlers[hookName] = [h0, h1]; + hooksBackup[hookName] = plugins.hooks[hookName] || []; + plugins.hooks[hookName] = [{hook_fn: h0.handle.bind(h0)}, {hook_fn: h1.handle.bind(h1)}]; + }); + hooksBackup.preAuthzFailure = plugins.hooks.preAuthzFailure || []; + Object.assign(settingsBackup, settings); + settings.users = { + admin: {password: 'admin-password', is_admin: true}, + user: {password: 'user-password'}, + }; + }); + afterEach(async function() { + Object.assign(plugins.hooks, hooksBackup); + Object.assign(settings, settingsBackup); + }); + + describe('preAuthorize', function() { + beforeEach(async function() { + settings.requireAuthentication = false; + settings.requireAuthorization = false; + }); + + it('defers if it returns []', async function() { + await agent.get('/').expect(200); + // Note: The preAuthorize hook always runs even if requireAuthorization is false. + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); + }); + it('bypasses authenticate and authorize hooks when true is returned', async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + handlers.preAuthorize[0].innerHandle = () => [true]; + await agent.get('/').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0']); + }); + it('bypasses authenticate and authorize hooks when false is returned', async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + handlers.preAuthorize[0].innerHandle = () => [false]; + await agent.get('/').expect(403); + assert.deepEqual(callOrder, ['preAuthorize_0']); + }); + it('bypasses authenticate and authorize hooks for static content, defers', async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + await agent.get('/static/robots.txt').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); + }); + it('cannot grant access to /admin', async function() { + handlers.preAuthorize[0].innerHandle = () => [true]; + await agent.get('/admin/').expect(401); + // Notes: + // * preAuthorize[1] is called despite preAuthorize[0] returning a non-empty list because + // 'true' entries are ignored for /admin/* requests. + // * The authenticate hook always runs for /admin/* requests even if + // settings.requireAuthentication is false. + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', + 'authenticate_0', 'authenticate_1']); + }); + it('can deny access to /admin', async function() { + handlers.preAuthorize[0].innerHandle = () => [false]; + await agent.get('/admin/').auth('admin', 'admin-password').expect(403); + assert.deepEqual(callOrder, ['preAuthorize_0']); + }); + it('runs preAuthzFailure hook when access is denied', async function() { + handlers.preAuthorize[0].innerHandle = () => [false]; + let called = false; + plugins.hooks.preAuthzFailure = [{hook_fn: (hookName, {req, res}, cb) => { + assert.equal(hookName, 'preAuthzFailure'); + assert(req != null); + assert(res != null); + assert(!called); + called = true; + res.status(200).send('injected'); + return cb([true]); + }}]; + await agent.get('/admin/').auth('admin', 'admin-password').expect(200, 'injected'); + assert(called); + }); + it('returns 500 if an exception is thrown', async function() { + handlers.preAuthorize[0].innerHandle = () => { throw new Error('exception test'); }; + await agent.get('/').expect(500); + }); + }); + + describe('authenticate', function() { + beforeEach(async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + }); + + it('is not called if !requireAuthentication and not /admin/*', async function() { + settings.requireAuthentication = false; + await agent.get('/').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); + }); + it('is called if !requireAuthentication and /admin/*', async function() { + settings.requireAuthentication = false; + await agent.get('/admin/').expect(401); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', + 'authenticate_0', 'authenticate_1']); + }); + it('defers if empty list returned', async function() { + await agent.get('/').expect(401); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', + 'authenticate_0', 'authenticate_1']); + }); + it('does not defer if return [true], 200', async function() { + handlers.authenticate[0].innerHandle = (req) => { req.session.user = {}; return [true]; }; + await agent.get('/').expect(200); + // Note: authenticate_1 was not called because authenticate_0 handled it. + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); + }); + it('does not defer if return [false], 401', async function() { + handlers.authenticate[0].innerHandle = (req) => [false]; + await agent.get('/').expect(401); + // Note: authenticate_1 was not called because authenticate_0 handled it. + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); + }); + it('falls back to HTTP basic auth', async function() { + await agent.get('/').auth('user', 'user-password').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', + 'authenticate_0', 'authenticate_1']); + }); + it('passes settings.users in context', async function() { + handlers.authenticate[0].checkContext = ({users}) => { + assert.equal(users, settings.users); + }; + await agent.get('/').expect(401); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', + 'authenticate_0', 'authenticate_1']); + }); + it('passes user, password in context if provided', async function() { + handlers.authenticate[0].checkContext = ({username, password}) => { + assert.equal(username, 'user'); + assert.equal(password, 'user-password'); + }; + await agent.get('/').auth('user', 'user-password').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', + 'authenticate_0', 'authenticate_1']); + }); + it('does not pass user, password in context if not provided', async function() { + handlers.authenticate[0].checkContext = ({username, password}) => { + assert(username == null); + assert(password == null); + }; + await agent.get('/').expect(401); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', + 'authenticate_0', 'authenticate_1']); + }); + it('errors if req.session.user is not created', async function() { + handlers.authenticate[0].innerHandle = () => [true]; + await agent.get('/').expect(500); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); + }); + it('returns 500 if an exception is thrown', async function() { + handlers.authenticate[0].innerHandle = () => { throw new Error('exception test'); }; + await agent.get('/').expect(500); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); + }); + }); + + describe('authorize', function() { + beforeEach(async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + }); + + it('is not called if !requireAuthorization (non-/admin)', async function() { + settings.requireAuthorization = false; + await agent.get('/').auth('user', 'user-password').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', + 'authenticate_0', 'authenticate_1']); + }); + it('is not called if !requireAuthorization (/admin)', async function() { + settings.requireAuthorization = false; + await agent.get('/admin/').auth('admin', 'admin-password').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', + 'authenticate_0', 'authenticate_1']); + }); + it('defers if empty list returned', async function() { + await agent.get('/').auth('user', 'user-password').expect(403); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', + 'authenticate_0', 'authenticate_1', + 'authorize_0', 'authorize_1']); + }); + it('does not defer if return [true], 200', async function() { + handlers.authorize[0].innerHandle = () => [true]; + await agent.get('/').auth('user', 'user-password').expect(200); + // Note: authorize_1 was not called because authorize_0 handled it. + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', + 'authenticate_0', 'authenticate_1', + 'authorize_0']); + }); + it('does not defer if return [false], 403', async function() { + handlers.authorize[0].innerHandle = (req) => [false]; + await agent.get('/').auth('user', 'user-password').expect(403); + // Note: authorize_1 was not called because authorize_0 handled it. + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', + 'authenticate_0', 'authenticate_1', + 'authorize_0']); + }); + it('passes req.path in context', async function() { + handlers.authorize[0].checkContext = ({resource}) => { + assert.equal(resource, '/'); + }; + await agent.get('/').auth('user', 'user-password').expect(403); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', + 'authenticate_0', 'authenticate_1', + 'authorize_0', 'authorize_1']); + }); + it('returns 500 if an exception is thrown', async function() { + handlers.authorize[0].innerHandle = () => { throw new Error('exception test'); }; + await agent.get('/').auth('user', 'user-password').expect(500); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', + 'authenticate_0', 'authenticate_1', + 'authorize_0']); + }); + }); +}); + +describe('webaccess: authnFailure, authzFailure, authFailure hooks', function() { const Handler = class { constructor(hookName) { this.hookName = hookName; From 505d67ed1c5ab2a4df67b3f41387dcdb4a5638f2 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 27 Sep 2020 21:18:13 +0100 Subject: [PATCH 056/315] allowing longer for FF to do timeslider rev test --- tests/frontend/specs/timeslider_revisions.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/frontend/specs/timeslider_revisions.js b/tests/frontend/specs/timeslider_revisions.js index 67123344b..8fa7f5514 100644 --- a/tests/frontend/specs/timeslider_revisions.js +++ b/tests/frontend/specs/timeslider_revisions.js @@ -2,7 +2,7 @@ describe("timeslider", function(){ //create a new pad before each test run beforeEach(function(cb){ helper.newPad(cb); - this.timeout(6000); + this.timeout(60000); }); it("loads adds a hundred revisions", function(done) { // passes @@ -109,12 +109,12 @@ describe("timeslider", function(){ it("jumps to a revision given in the url", function(done) { var inner$ = helper.padInner$; var chrome$ = helper.padChrome$; - this.timeout(20000); + this.timeout(40000); // wait for the text to be loaded helper.waitFor(function(){ return inner$('body').text().length != 0; - }, 6000).always(function() { + }, 10000).always(function() { var newLines = inner$('body div').length; var oldLength = inner$('body').text().length + newLines / 2; expect( oldLength ).to.not.eql( 0 ); @@ -130,7 +130,7 @@ describe("timeslider", function(){ // was accepted by the server. var colorOkay = inner$('span').first().attr('class').indexOf("author-") == 0; return lenOkay && colorOkay; - }, 6000).always(function() { + }, 10000).always(function() { // go to timeslider with a specific revision set $('#iframe-container iframe').attr('src', $('#iframe-container iframe').attr('src')+'/timeslider#0'); @@ -142,7 +142,7 @@ describe("timeslider", function(){ if(timeslider$){ return timeslider$('#innerdocbody').text().length == oldLength; } - }, 6000).always(function(){ + }, 10000).always(function(){ expect( timeslider$('#innerdocbody').text().length ).to.eql( oldLength ); done(); }); From 180983736d7065d4dace6b20805d57baf0b363ef Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 19 Sep 2020 15:30:04 -0400 Subject: [PATCH 057/315] security: Enable authorize plugins to grant read-only access --- doc/api/hooks_server-side.md | 1 + src/node/handler/PadMessageHandler.js | 7 ++++--- src/node/hooks/express/specialpages.js | 4 +++- src/node/hooks/express/webaccess.js | 12 ++++++++++++ tests/backend/specs/socketio.js | 22 +++++++++++++++++++++- 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 021f633f3..fb7b706df 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -292,6 +292,7 @@ You can pass the following values to the provided callback: downgraded to modify-only if `settings.editOnly` is true.) * `['modify']` will grant access to modify but not create the pad if the request is for a pad, otherwise access is simply granted. +* `['readOnly']` will grant read-only access. * `[false]` will deny access. * `[]` or `undefined` will defer the authorization decision to the next authorization plugin (if any, otherwise deny). diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 56e506023..1094fe837 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -39,6 +39,7 @@ var remoteAddress = require("../utils/RemoteAddress").remoteAddress; const assert = require('assert').strict; const nodeify = require("nodeify"); const { RateLimiterMemory } = require('rate-limiter-flexible'); +const webaccess = require('../hooks/express/webaccess'); const rateLimiter = new RateLimiterMemory({ points: settings.commitRateLimiting.points, @@ -224,7 +225,6 @@ exports.handleMessage = async function(client, message) padId = await readOnlyManager.getPadId(padId); } - // FIXME: Allow to override readwrite access with readonly const {session: {user} = {}} = client.client.request; const {accessStatus, authorID} = await securityManager.checkAccess(padId, auth.sessionID, auth.token, auth.password, user); @@ -973,7 +973,8 @@ async function handleClientReady(client, message, authorID) // Save in sessioninfos that this session belonges to this pad sessionInfo.padId = padIds.padId; sessionInfo.readOnlyPadId = padIds.readOnlyPadId; - sessionInfo.readonly = padIds.readonly; + sessionInfo.readonly = + padIds.readonly || !webaccess.userCanModify(message.padId, client.client.request); // Log creation/(re-)entering of a pad let ip = remoteAddress[client.id]; @@ -1112,7 +1113,7 @@ async function handleClientReady(client, message, authorID) "chatHead": pad.chatHead, "numConnectedUsers": roomClients.length, "readOnlyId": padIds.readOnlyPadId, - "readonly": padIds.readonly, + "readonly": sessionInfo.readonly, "serverTimestamp": Date.now(), "userId": authorID, "abiwordAvailable": settings.abiwordAvailable(), diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js index b11f77a00..2b5ea231c 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.js @@ -3,6 +3,7 @@ var eejs = require('ep_etherpad-lite/node/eejs'); var toolbar = require("ep_etherpad-lite/node/utils/toolbar"); var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); var settings = require('../../utils/Settings'); +const webaccess = require('./webaccess'); exports.expressCreateServer = function (hook_name, args, cb) { // expose current stats @@ -42,7 +43,8 @@ exports.expressCreateServer = function (hook_name, args, cb) { args.app.get('/p/:pad', function(req, res, next) { // The below might break for pads being rewritten - var isReadOnly = req.url.indexOf("/p/r.") === 0; + const isReadOnly = + req.url.indexOf("/p/r.") === 0 || !webaccess.userCanModify(req.params.pad, req); hooks.callAll("padInitToolbar", { toolbar: toolbar, diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 0f3a01ee7..2be42ceab 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -1,3 +1,4 @@ +const assert = require('assert').strict; const express = require('express'); const log4js = require('log4js'); const httpLogger = log4js.getLogger('http'); @@ -15,6 +16,7 @@ exports.normalizeAuthzLevel = (level) => { switch (level) { case true: return 'create'; + case 'readOnly': case 'modify': case 'create': return level; @@ -24,6 +26,16 @@ exports.normalizeAuthzLevel = (level) => { return false; }; +exports.userCanModify = (padId, req) => { + if (!settings.requireAuthentication) return true; + const {session: {user} = {}} = req; + assert(user); // If authn required and user == null, the request should have already been denied. + assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization. + const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]); + assert(level); // If !level, the request should have already been denied. + return level !== 'readOnly'; +}; + // Exported so that tests can set this to 0 to avoid unnecessary test slowness. exports.authnFailureDelayMs = 1000; diff --git a/tests/backend/specs/socketio.js b/tests/backend/specs/socketio.js index 717c4f7b8..b22d8cd5a 100644 --- a/tests/backend/specs/socketio.js +++ b/tests/backend/specs/socketio.js @@ -252,7 +252,7 @@ describe('socket.io access checks', function() { assert.equal(clientVars.data.readonly, false); }); it("level='modify' -> can modify", async () => { - const pad = await padManager.getPad('pad'); // Create the pad. + await padManager.getPad('pad'); // Create the pad. authorize = () => 'modify'; settings.requireAuthentication = true; settings.requireAuthorization = true; @@ -282,4 +282,24 @@ describe('socket.io access checks', function() { const message = await handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); + it("level='readOnly' -> unable to create", async () => { + authorize = () => 'readOnly'; + settings.requireAuthentication = true; + settings.requireAuthorization = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it("level='readOnly' -> unable to modify", async () => { + await padManager.getPad('pad'); // Create the pad. + authorize = () => 'readOnly'; + settings.requireAuthentication = true; + settings.requireAuthorization = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, true); + }); }); From 8919608d4567caccab6f9823cb625b2f7645f9ed Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 27 Sep 2020 23:12:37 +0100 Subject: [PATCH 058/315] tests: disable a version of safari for now as its too buggy on sauce labs --- tests/frontend/travis/remote_runner.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/frontend/travis/remote_runner.js b/tests/frontend/travis/remote_runner.js index 0779a43c4..569b121f5 100644 --- a/tests/frontend/travis/remote_runner.js +++ b/tests/frontend/travis/remote_runner.js @@ -101,12 +101,14 @@ sauceTestWorker.push({ , 'args' : ['--use-fake-device-for-media-stream'] }); +/* // 3) Safari on OSX 10.15 sauceTestWorker.push({ 'platform' : 'OS X 10.15' , 'browserName' : 'safari' , 'version' : '13.1' }); +*/ // 4) Safari on OSX 10.14 sauceTestWorker.push({ From 7bd5435f50e3a9ac12871fba6f4c7de795900e8f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 29 Aug 2020 00:24:58 -0400 Subject: [PATCH 059/315] webaccess: Log hook errors --- src/node/hooks/express/webaccess.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 2be42ceab..a2662a8cc 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -42,6 +42,7 @@ exports.authnFailureDelayMs = 1000; exports.checkAccess = (req, res, next) => { const hookResultMangle = (cb) => { return (err, data) => { + if (err != null) httpLogger.error(`Error during access check: ${err}`); return cb(!err && data.length && data[0]); }; }; From bf9d613e9593a8593a630aa2fa7c1b135eee0bd7 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 28 Sep 2020 06:22:06 -0400 Subject: [PATCH 060/315] feature: New user-specific `readOnly` and `canCreate` settings (#4370) Also: * Group the tests for readability. * Factor out some common test setup. --- doc/api/hooks_server-side.md | 11 +- settings.json.docker | 18 +- settings.json.template | 18 +- src/node/db/SecurityManager.js | 8 +- src/node/hooks/express/webaccess.js | 1 + tests/backend/specs/socketio.js | 359 +++++++++++++++++----------- 6 files changed, 260 insertions(+), 155 deletions(-) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index fb7b706df..57ae1c117 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -288,10 +288,13 @@ set in the settings file or by the authenticate hook. You can pass the following values to the provided callback: * `[true]` or `['create']` will grant access to modify or create the pad if the - request is for a pad, otherwise access is simply granted. (Access will be - downgraded to modify-only if `settings.editOnly` is true.) -* `['modify']` will grant access to modify but not create the pad if the - request is for a pad, otherwise access is simply granted. + request is for a pad, otherwise access is simply granted. Access to a pad will + be downgraded to modify-only if `settings.editOnly` is true or the user's + `canCreate` setting is set to `false`, and downgraded to read-only if the + user's `readOnly` setting is `true`. +* `['modify']` will grant access to modify but not create the pad if the request + is for a pad, otherwise access is simply granted. Access to a pad will be + downgraded to read-only if the user's `readOnly` setting is `true`. * `['readOnly']` will grant read-only access. * `[false]` will deny access. * `[]` or `undefined` will defer the authorization decision to the next diff --git a/settings.json.docker b/settings.json.docker index 332e06ac4..8bbdedb40 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -391,10 +391,22 @@ }, /* - * Users for basic authentication. + * User accounts. These accounts are used by: + * - default HTTP basic authentication if no plugin handles authentication + * - some but not all authentication plugins + * - some but not all authorization plugins * - * is_admin = true gives access to /admin. - * If you do not uncomment this, /admin will not be available! + * User properties: + * - password: The user's password. Some authentication plugins will ignore + * this. + * - is_admin: true gives access to /admin. Defaults to false. If you do not + * uncomment this, /admin will not be available! + * - readOnly: If true, this user will not be able to create new pads or + * modify existing pads. Defaults to false. + * - canCreate: If this is true and readOnly is false, this user can create + * new pads. Defaults to true. + * + * Authentication and authorization plugins may define additional properties. * * WARNING: passwords should not be stored in plaintext in this file. * If you want to mitigate this, please install ep_hash_auth and diff --git a/settings.json.template b/settings.json.template index c6b7f394f..880310919 100644 --- a/settings.json.template +++ b/settings.json.template @@ -394,10 +394,22 @@ }, /* - * Users for basic authentication. + * User accounts. These accounts are used by: + * - default HTTP basic authentication if no plugin handles authentication + * - some but not all authentication plugins + * - some but not all authorization plugins * - * is_admin = true gives access to /admin. - * If you do not uncomment this, /admin will not be available! + * User properties: + * - password: The user's password. Some authentication plugins will ignore + * this. + * - is_admin: true gives access to /admin. Defaults to false. If you do not + * uncomment this, /admin will not be available! + * - readOnly: If true, this user will not be able to create new pads or + * modify existing pads. Defaults to false. + * - canCreate: If this is true and readOnly is false, this user can create + * new pads. Defaults to true. + * + * Authentication and authorization plugins may define additional properties. * * WARNING: passwords should not be stored in plaintext in this file. * If you want to mitigate this, please install ep_hash_auth and diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index ff28ff8fe..ad802f327 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -67,8 +67,12 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user authLogger.debug('access denied: authentication is required'); return DENY; } - // Check whether the user is authorized. Note that userSettings.padAuthorizations will still be - // populated even if settings.requireAuthorization is false. + + // Check whether the user is authorized to create the pad if it doesn't exist. + if (userSettings.canCreate != null && !userSettings.canCreate) canCreate = false; + if (userSettings.readOnly) canCreate = false; + // Note: userSettings.padAuthorizations should still be populated even if + // settings.requireAuthorization is false. const padAuthzs = userSettings.padAuthorizations || {}; const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]); if (!level) { diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index a2662a8cc..9cb5f4570 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -30,6 +30,7 @@ exports.userCanModify = (padId, req) => { if (!settings.requireAuthentication) return true; const {session: {user} = {}} = req; assert(user); // If authn required and user == null, the request should have already been denied. + if (user.readOnly) return false; assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization. const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]); assert(level); // If !level, the request should have already been denied. diff --git a/tests/backend/specs/socketio.js b/tests/backend/specs/socketio.js index b22d8cd5a..e89d48725 100644 --- a/tests/backend/specs/socketio.js +++ b/tests/backend/specs/socketio.js @@ -152,154 +152,227 @@ describe('socket.io access checks', function() { await cleanUpPads(); }); - // Normal accesses. - it('!authn anonymous cookie /p/pad -> 200, ok', async function() { - const res = await agent.get('/p/pad').expect(200); - // Should not throw. - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - }); - it('!authn !cookie -> ok', async function() { - // Should not throw. - socket = await connect(null); - const clientVars = await handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - }); - it('!authn user /p/pad -> 200, ok', async function() { - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - // Should not throw. - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - }); - it('authn user /p/pad -> 200, ok', async function() { - settings.requireAuthentication = true; - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - // Should not throw. - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - }); - it('authz user /p/pad -> 200, ok', async function() { - settings.requireAuthentication = true; - settings.requireAuthorization = true; - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - // Should not throw. - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - }); - it('supports pad names with characters that must be percent-encoded', async function() { - settings.requireAuthentication = true; - // requireAuthorization is set to true here to guarantee that the user's padAuthorizations - // object is populated. Technically this isn't necessary because the user's padAuthorizations is - // currently populated even if requireAuthorization is false, but setting this to true ensures - // the test remains useful if the implementation ever changes. - settings.requireAuthorization = true; - const encodedPadId = encodeURIComponent('päd'); - const res = await agent.get(`/p/${encodedPadId}`).auth('user', 'user-password').expect(200); - // Should not throw. - socket = await connect(res); - const clientVars = await handshake(socket, 'päd'); - assert.equal(clientVars.type, 'CLIENT_VARS'); + describe('Normal accesses', function() { + it('!authn anonymous cookie /p/pad -> 200, ok', async function() { + const res = await agent.get('/p/pad').expect(200); + // Should not throw. + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); + it('!authn !cookie -> ok', async function() { + // Should not throw. + socket = await connect(null); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); + it('!authn user /p/pad -> 200, ok', async function() { + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + // Should not throw. + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); + it('authn user /p/pad -> 200, ok', async function() { + settings.requireAuthentication = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + // Should not throw. + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); + it('authz user /p/pad -> 200, ok', async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + // Should not throw. + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); + it('supports pad names with characters that must be percent-encoded', async function() { + settings.requireAuthentication = true; + // requireAuthorization is set to true here to guarantee that the user's padAuthorizations + // object is populated. Technically this isn't necessary because the user's padAuthorizations + // is currently populated even if requireAuthorization is false, but setting this to true + // ensures the test remains useful if the implementation ever changes. + settings.requireAuthorization = true; + const encodedPadId = encodeURIComponent('päd'); + const res = await agent.get(`/p/${encodedPadId}`).auth('user', 'user-password').expect(200); + // Should not throw. + socket = await connect(res); + const clientVars = await handshake(socket, 'päd'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); }); - // Abnormal access attempts. - it('authn anonymous /p/pad -> 401, error', async function() { - settings.requireAuthentication = true; - const res = await agent.get('/p/pad').expect(401); - // Despite the 401, try to create the pad via a socket.io connection anyway. - await assert.rejects(connect(res), {message: /authentication required/i}); - }); - it('authn !cookie -> error', async function() { - settings.requireAuthentication = true; - await assert.rejects(connect(null), {message: /signed express_sid cookie is required/i}); - }); - it('authorization bypass attempt -> error', async function() { - // Only allowed to access /p/pad. - authorize = (req) => req.path === '/p/pad'; - settings.requireAuthentication = true; - settings.requireAuthorization = true; - // First authenticate and establish a session. - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - // Connecting should work because the user successfully authenticated. - socket = await connect(res); - // Accessing /p/other-pad should fail, despite the successful fetch of /p/pad. - const message = await handshake(socket, 'other-pad'); - assert.equal(message.accessStatus, 'deny'); + describe('Abnormal access attempts', function() { + it('authn anonymous /p/pad -> 401, error', async function() { + settings.requireAuthentication = true; + const res = await agent.get('/p/pad').expect(401); + // Despite the 401, try to create the pad via a socket.io connection anyway. + await assert.rejects(connect(res), {message: /authentication required/i}); + }); + it('authn !cookie -> error', async function() { + settings.requireAuthentication = true; + await assert.rejects(connect(null), {message: /signed express_sid cookie is required/i}); + }); + it('authorization bypass attempt -> error', async function() { + // Only allowed to access /p/pad. + authorize = (req) => req.path === '/p/pad'; + settings.requireAuthentication = true; + settings.requireAuthorization = true; + // First authenticate and establish a session. + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + // Connecting should work because the user successfully authenticated. + socket = await connect(res); + // Accessing /p/other-pad should fail, despite the successful fetch of /p/pad. + const message = await handshake(socket, 'other-pad'); + assert.equal(message.accessStatus, 'deny'); + }); }); - // Authorization levels via authorize hook - it("level='create' -> can create", async () => { - authorize = () => 'create'; - settings.requireAuthentication = true; - settings.requireAuthorization = true; - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - assert.equal(clientVars.data.readonly, false); + describe('Authorization levels via authorize hook', function() { + beforeEach(async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + }); + + it("level='create' -> can create", async function() { + authorize = () => 'create'; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + }); + it('level=true -> can create', async function() { + authorize = () => true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + }); + it("level='modify' -> can modify", async function() { + await padManager.getPad('pad'); // Create the pad. + authorize = () => 'modify'; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + }); + it("level='create' settings.editOnly=true -> unable to create", async function() { + authorize = () => 'create'; + settings.editOnly = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it("level='modify' settings.editOnly=false -> unable to create", async function() { + authorize = () => 'modify'; + settings.editOnly = false; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it("level='readOnly' -> unable to create", async function() { + authorize = () => 'readOnly'; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it("level='readOnly' -> unable to modify", async function() { + await padManager.getPad('pad'); // Create the pad. + authorize = () => 'readOnly'; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, true); + }); }); - it('level=true -> can create', async () => { - authorize = () => true; - settings.requireAuthentication = true; - settings.requireAuthorization = true; - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - assert.equal(clientVars.data.readonly, false); + + describe('Authorization levels via user settings', function() { + beforeEach(async function() { + settings.requireAuthentication = true; + }); + + it('user.canCreate = true -> can create and modify', async function() { + settings.users.user.canCreate = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + }); + it('user.canCreate = false -> unable to create', async function() { + settings.users.user.canCreate = false; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it('user.readOnly = true -> unable to create', async function() { + settings.users.user.readOnly = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it('user.readOnly = true -> unable to modify', async function() { + await padManager.getPad('pad'); // Create the pad. + settings.users.user.readOnly = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, true); + }); + it('user.readOnly = false -> can create and modify', async function() { + settings.users.user.readOnly = false; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + }); + it('user.readOnly = true, user.canCreate = true -> unable to create', async function() { + settings.users.user.canCreate = true; + settings.users.user.readOnly = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); }); - it("level='modify' -> can modify", async () => { - await padManager.getPad('pad'); // Create the pad. - authorize = () => 'modify'; - settings.requireAuthentication = true; - settings.requireAuthorization = true; - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - assert.equal(clientVars.data.readonly, false); - }); - it("level='create' settings.editOnly=true -> unable to create", async () => { - authorize = () => 'create'; - settings.requireAuthentication = true; - settings.requireAuthorization = true; - settings.editOnly = true; - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); - assert.equal(message.accessStatus, 'deny'); - }); - it("level='modify' settings.editOnly=false -> unable to create", async () => { - authorize = () => 'modify'; - settings.requireAuthentication = true; - settings.requireAuthorization = true; - settings.editOnly = false; - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); - assert.equal(message.accessStatus, 'deny'); - }); - it("level='readOnly' -> unable to create", async () => { - authorize = () => 'readOnly'; - settings.requireAuthentication = true; - settings.requireAuthorization = true; - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); - assert.equal(message.accessStatus, 'deny'); - }); - it("level='readOnly' -> unable to modify", async () => { - await padManager.getPad('pad'); // Create the pad. - authorize = () => 'readOnly'; - settings.requireAuthentication = true; - settings.requireAuthorization = true; - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - assert.equal(clientVars.data.readonly, true); + + describe('Authorization level interaction between authorize hook and user settings', function() { + beforeEach(async function() { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + }); + + it('authorize hook does not elevate level from user settings', async function() { + settings.users.user.readOnly = true; + authorize = () => 'create'; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it('user settings does not elevate level from authorize hook', async function() { + settings.users.user.readOnly = false; + settings.users.user.canCreate = true; + authorize = () => 'readOnly'; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); }); }); From 837ca6ec1e1c5ca299eb1b4bae2dbcfb442429ea Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 28 Sep 2020 17:15:23 +0200 Subject: [PATCH 061/315] Localisation updates from https://translatewiki.net. --- src/locales/ca.json | 2 ++ src/locales/ko.json | 1 + src/locales/ru.json | 2 ++ 3 files changed, 5 insertions(+) diff --git a/src/locales/ca.json b/src/locales/ca.json index 151f3ab2e..b46d4c8a2 100644 --- a/src/locales/ca.json +++ b/src/locales/ca.json @@ -88,6 +88,8 @@ "pad.modals.deleted.explanation": "S'ha suprimit el pad.", "pad.modals.rateLimited": "Tarifa limitada.", "pad.modals.rateLimited.explanation": "Heu enviat massa missatges a aquest pad per això us han desconnectat.", + "pad.modals.rejected.explanation": "El servidor ha rebutjat un missatge enviat pel seu navegador.", + "pad.modals.rejected.cause": "Pot ser que el servidor s'hagi actualitzat mentre estàveu veient la plataforma, o potser hi ha un error a Etherpad. Intenta tornar a carregar la pàgina.", "pad.modals.disconnected": "Heu estat desconnectat.", "pad.modals.disconnected.explanation": "S'ha perdut la connexió amb el servidor", "pad.modals.disconnected.cause": "El servidor sembla que no està disponible. Notifiqueu a l'administrador del servei si continua passant.", diff --git a/src/locales/ko.json b/src/locales/ko.json index 200f0e832..b6e9978ad 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -84,6 +84,7 @@ "pad.modals.corruptPad.cause": "잘못된 서버 구성 또는 다른 예기치 않은 오류 때문에 발생했을 수 있습니다. 서버 관리자와 연락하세요.", "pad.modals.deleted": "삭제되었습니다.", "pad.modals.deleted.explanation": "이 패드를 제거했습니다.", + "pad.modals.rejected.explanation": "브라우저가 보낸 메시지를 서버가 거부했습니다.", "pad.modals.disconnected": "연결이 끊어졌습니다.", "pad.modals.disconnected.explanation": "서버에서 연결을 잃었습니다", "pad.modals.disconnected.cause": "서버를 사용할 수 없습니다. 이 문제가 계속 발생하면 서비스 관리자에게 알려주시기 바랍니다.", diff --git a/src/locales/ru.json b/src/locales/ru.json index 0ae5a000b..e617f7cef 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -90,6 +90,8 @@ "pad.modals.deleted.explanation": "Этот документ был удалён.", "pad.modals.rateLimited": "Скорость ограничена.", "pad.modals.rateLimited.explanation": "Вы отправили слишком много сообщений в этот документ, поэтому вы были отключены.", + "pad.modals.rejected.explanation": "Сервер отклонил сообщение, посланное вашим браузером.", + "pad.modals.rejected.cause": "Возможно, сервер обновился, пока вы просматривали документ, а, может, это ошибка в Etherpad. Попробуйте перезагрузить страницу.", "pad.modals.disconnected": "Соединение разорвано.", "pad.modals.disconnected.explanation": "Подключение к серверу потеряно", "pad.modals.disconnected.cause": "Сервер, возможно, недоступен. Пожалуйста, сообщите администратору службы, если проблема будет повторятся.", From 5964055dec6091cc81c54ddd73585bd79104904c Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 29 Sep 2020 13:21:35 +0100 Subject: [PATCH 062/315] package updates: update deps and resolve some potential security issues (#4369) --- src/package-lock.json | 1679 +++++++++++++++-------------------------- src/package.json | 4 +- 2 files changed, 613 insertions(+), 1070 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 884b67644..59f124154 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -435,11 +435,11 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -471,9 +471,9 @@ "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/node": { - "version": "14.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.10.1.tgz", - "integrity": "sha512-aYNbO+FZ/3KGeQCEkNhHFRIzBOUgc7QvcVNKXbfnhDkSfwUv91JsQQa10rDgKSTSLkXZ1UIyPe4FJJNVgw1xWQ==" + "version": "14.11.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz", + "integrity": "sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA==" }, "@types/readable-stream": { "version": "2.3.9", @@ -551,9 +551,9 @@ }, "dependencies": { "@types/node": { - "version": "8.10.63", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.63.tgz", - "integrity": "sha512-g+nSkeHFDd2WOQChfmy9SAXLywT47WZBrGS/NC5ym5PJ8c8RC6l4pbGaUW/X0+eZJnXw6/AVNEouXWhV4iz72Q==" + "version": "8.10.64", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.64.tgz", + "integrity": "sha512-/EwBIb+imu8Qi/A3NF9sJ9iuKo7yV+pryqjmeRqaU0C4wBAOhas5mdvoYeJ5PCKrh6thRSJHdoasFqh3BQGILA==" } } }, @@ -892,9 +892,9 @@ "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==" }, "bl": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", - "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", "dev": true, "requires": { "buffer": "^5.5.0", @@ -1220,11 +1220,11 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -2808,9 +2808,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash.assignin": { "version": "4.2.0", @@ -3148,9 +3148,9 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "mssql": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/mssql/-/mssql-6.2.1.tgz", - "integrity": "sha512-erINJ9EUPvPuWXifZfhum0CVEVrdvnFYlpgU6WKkQW69W4W7DWqJS2FHdedHnuJWlJ8x1WW1NcD8GFfF15O2aA==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-6.2.3.tgz", + "integrity": "sha512-4TW/fA9UgzmVTNgjl65r6ISr6aL5QHnlptEt1A3jIpdzkNbFPIkRbUNz90324HIdE+5pKc3VqikOImcTrhd4og==", "requires": { "debug": "^4", "tarn": "^1.1.5", @@ -3158,11 +3158,11 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -3230,11 +3230,11 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -3294,9 +3294,9 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, "npm": { - "version": "6.14.5", - "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.5.tgz", - "integrity": "sha512-CDwa3FJd0XJpKDbWCST484H+mCNjF26dPrU+xnREW+upR0UODjMEfXPl3bxWuAwZIX6c2ASg1plLO7jP8ehWeA==", + "version": "6.14.8", + "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.8.tgz", + "integrity": "sha512-HBZVBMYs5blsj94GTeQZel7s9odVuuSUHy1+AlZh7rPVux1os2ashvEGLy/STNK7vUjbrCg5Kq9/GXisJgdf6A==", "requires": { "JSONStream": "^1.3.5", "abbrev": "~1.1.1", @@ -3304,7 +3304,7 @@ "ansistyles": "~0.1.3", "aproba": "^2.0.0", "archy": "~1.0.0", - "bin-links": "^1.1.7", + "bin-links": "^1.1.8", "bluebird": "^3.5.5", "byte-size": "^5.0.1", "cacache": "^12.0.3", @@ -3325,7 +3325,7 @@ "find-npm-prefix": "^1.0.2", "fs-vacuum": "~1.2.10", "fs-write-stream-atomic": "~1.0.10", - "gentle-fs": "^2.3.0", + "gentle-fs": "^2.3.1", "glob": "^7.1.6", "graceful-fs": "^4.2.4", "has-unicode": "~2.0.1", @@ -3340,14 +3340,14 @@ "is-cidr": "^3.0.0", "json-parse-better-errors": "^1.0.2", "lazy-property": "~1.0.0", - "libcipm": "^4.0.7", + "libcipm": "^4.0.8", "libnpm": "^3.0.1", "libnpmaccess": "^3.0.2", "libnpmhook": "^5.0.3", "libnpmorg": "^1.0.1", "libnpmsearch": "^2.0.2", "libnpmteam": "^1.0.2", - "libnpx": "^10.2.2", + "libnpx": "^10.2.4", "lock-verify": "^2.1.0", "lockfile": "^1.0.4", "lodash._baseindexof": "*", @@ -3362,22 +3362,22 @@ "lodash.uniq": "~4.5.0", "lodash.without": "~4.4.0", "lru-cache": "^5.1.1", - "meant": "~1.0.1", + "meant": "^1.0.2", "mississippi": "^3.0.0", "mkdirp": "^0.5.5", "move-concurrently": "^1.0.1", "node-gyp": "^5.1.0", "nopt": "^4.0.3", "normalize-package-data": "^2.5.0", - "npm-audit-report": "^1.3.2", + "npm-audit-report": "^1.3.3", "npm-cache-filename": "~1.0.2", "npm-install-checks": "^3.0.2", - "npm-lifecycle": "^3.1.4", + "npm-lifecycle": "^3.1.5", "npm-package-arg": "^6.1.1", "npm-packlist": "^1.4.8", "npm-pick-manifest": "^3.0.2", "npm-profile": "^4.0.4", - "npm-registry-fetch": "^4.0.4", + "npm-registry-fetch": "^4.0.7", "npm-user-validate": "~1.0.0", "npmlog": "~4.1.2", "once": "~1.4.0", @@ -3425,8 +3425,7 @@ "dependencies": { "JSONStream": { "version": "1.3.5", - "resolved": "", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "bundled": true, "requires": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" @@ -3434,29 +3433,25 @@ }, "abbrev": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "bundled": true }, "agent-base": { "version": "4.3.0", - "resolved": "", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "bundled": true, "requires": { "es6-promisify": "^5.0.0" } }, "agentkeepalive": { "version": "3.5.2", - "resolved": "", - "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==", + "bundled": true, "requires": { "humanize-ms": "^1.2.1" } }, "ajv": { "version": "5.5.2", - "resolved": "", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "bundled": true, "requires": { "co": "^4.6.0", "fast-deep-equal": "^1.0.0", @@ -3466,49 +3461,41 @@ }, "ansi-align": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "bundled": true, "requires": { "string-width": "^2.0.0" } }, "ansi-regex": { "version": "2.1.1", - "resolved": "", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "bundled": true }, "ansi-styles": { "version": "3.2.1", - "resolved": "", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "bundled": true, "requires": { "color-convert": "^1.9.0" } }, "ansicolors": { "version": "0.3.2", - "resolved": "", - "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=" + "bundled": true }, "ansistyles": { "version": "0.1.3", - "resolved": "", - "integrity": "sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk=" + "bundled": true }, "aproba": { "version": "2.0.0", - "resolved": "", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + "bundled": true }, "archy": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=" + "bundled": true }, "are-we-there-yet": { "version": "1.1.4", - "resolved": "", - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "bundled": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -3516,8 +3503,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3530,8 +3516,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -3540,55 +3525,46 @@ }, "asap": { "version": "2.0.6", - "resolved": "", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + "bundled": true }, "asn1": { "version": "0.2.4", - "resolved": "", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "bundled": true, "requires": { "safer-buffer": "~2.1.0" } }, "assert-plus": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "bundled": true }, "asynckit": { "version": "0.4.0", - "resolved": "", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "bundled": true }, "aws-sign2": { "version": "0.7.0", - "resolved": "", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "bundled": true }, "aws4": { "version": "1.8.0", - "resolved": "", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + "bundled": true }, "balanced-match": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "bundled": true }, "bcrypt-pbkdf": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "bundled": true, "optional": true, "requires": { "tweetnacl": "^0.14.3" } }, "bin-links": { - "version": "1.1.7", - "resolved": "", - "integrity": "sha512-/eaLaTu7G7/o7PV04QPy1HRT65zf+1tFkPGv0sPTV0tRwufooYBQO3zrcyGgm+ja+ZtBf2GEuKjDRJ2pPG+yqA==", + "version": "1.1.8", + "bundled": true, "requires": { "bluebird": "^3.5.3", "cmd-shim": "^3.0.0", @@ -3600,13 +3576,11 @@ }, "bluebird": { "version": "3.5.5", - "resolved": "", - "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==" + "bundled": true }, "boxen": { "version": "1.3.0", - "resolved": "", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "bundled": true, "requires": { "ansi-align": "^2.0.0", "camelcase": "^4.0.0", @@ -3619,8 +3593,7 @@ }, "brace-expansion": { "version": "1.1.11", - "resolved": "", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "bundled": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3628,28 +3601,23 @@ }, "buffer-from": { "version": "1.0.0", - "resolved": "", - "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==" + "bundled": true }, "builtins": { "version": "1.0.3", - "resolved": "", - "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=" + "bundled": true }, "byline": { "version": "5.0.0", - "resolved": "", - "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=" + "bundled": true }, "byte-size": { "version": "5.0.1", - "resolved": "", - "integrity": "sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw==" + "bundled": true }, "cacache": { "version": "12.0.3", - "resolved": "", - "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", + "bundled": true, "requires": { "bluebird": "^3.5.5", "chownr": "^1.1.1", @@ -3670,28 +3638,23 @@ }, "call-limit": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-5twvci5b9eRBw2wCfPtN0GmlR2/gadZqyFpPhOK6CvMFoFgA+USnZ6Jpu1lhG9h85pQ3Ouil3PfXWRD4EUaRiQ==" + "bundled": true }, "camelcase": { "version": "4.1.0", - "resolved": "", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + "bundled": true }, "capture-stack-trace": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=" + "bundled": true }, "caseless": { "version": "0.12.0", - "resolved": "", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "bundled": true }, "chalk": { "version": "2.4.1", - "resolved": "", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "bundled": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -3700,31 +3663,26 @@ }, "chownr": { "version": "1.1.4", - "resolved": "", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "bundled": true }, "ci-info": { "version": "2.0.0", - "resolved": "", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + "bundled": true }, "cidr-regex": { "version": "2.0.10", - "resolved": "", - "integrity": "sha512-sB3ogMQXWvreNPbJUZMRApxuRYd+KoIo4RGQ81VatjmMW6WJPo+IJZ2846FGItr9VzKo5w7DXzijPLGtSd0N3Q==", + "bundled": true, "requires": { "ip-regex": "^2.1.0" } }, "cli-boxes": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" + "bundled": true }, "cli-columns": { "version": "3.1.2", - "resolved": "", - "integrity": "sha1-ZzLZcpee/CrkRKHwjgj6E5yWoY4=", + "bundled": true, "requires": { "string-width": "^2.0.0", "strip-ansi": "^3.0.1" @@ -3732,8 +3690,7 @@ }, "cli-table3": { "version": "0.5.1", - "resolved": "", - "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "bundled": true, "requires": { "colors": "^1.1.2", "object-assign": "^4.1.0", @@ -3741,39 +3698,47 @@ } }, "cliui": { - "version": "4.1.0", - "resolved": "", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "version": "5.0.0", + "bundled": true, "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "version": "4.1.0", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true + }, + "string-width": { + "version": "3.1.0", + "bundled": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } }, "strip-ansi": { - "version": "4.0.0", - "resolved": "", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "5.2.0", + "bundled": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^4.1.0" } } } }, "clone": { "version": "1.0.4", - "resolved": "", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + "bundled": true }, "cmd-shim": { "version": "3.0.3", - "resolved": "", - "integrity": "sha512-DtGg+0xiFhQIntSBRzL2fRQBnmtAVwXIDo4Qq46HPpObYquxMaZS4sb82U9nH91qJrlosC1wa9gwr0QyL/HypA==", + "bundled": true, "requires": { "graceful-fs": "^4.1.2", "mkdirp": "~0.5.0" @@ -3781,37 +3746,31 @@ }, "co": { "version": "4.6.0", - "resolved": "", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + "bundled": true }, "code-point-at": { "version": "1.1.0", - "resolved": "", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "bundled": true }, "color-convert": { "version": "1.9.1", - "resolved": "", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "bundled": true, "requires": { "color-name": "^1.1.1" } }, "color-name": { "version": "1.1.3", - "resolved": "", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "bundled": true }, "colors": { "version": "1.3.3", - "resolved": "", - "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==", + "bundled": true, "optional": true }, "columnify": { "version": "1.5.4", - "resolved": "", - "integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=", + "bundled": true, "requires": { "strip-ansi": "^3.0.0", "wcwidth": "^1.0.0" @@ -3819,21 +3778,18 @@ }, "combined-stream": { "version": "1.0.6", - "resolved": "", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "bundled": true, "requires": { "delayed-stream": "~1.0.0" } }, "concat-map": { "version": "0.0.1", - "resolved": "", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "bundled": true }, "concat-stream": { "version": "1.6.2", - "resolved": "", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "bundled": true, "requires": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -3843,8 +3799,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3857,8 +3812,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -3867,19 +3821,17 @@ }, "config-chain": { "version": "1.1.12", - "resolved": "", - "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", + "bundled": true, "requires": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "configstore": { - "version": "3.1.2", - "resolved": "", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "version": "3.1.5", + "bundled": true, "requires": { - "dot-prop": "^4.1.0", + "dot-prop": "^4.2.1", "graceful-fs": "^4.1.2", "make-dir": "^1.0.0", "unique-string": "^1.0.0", @@ -3889,13 +3841,11 @@ }, "console-control-strings": { "version": "1.1.0", - "resolved": "", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "bundled": true }, "copy-concurrently": { "version": "1.0.5", - "resolved": "", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "bundled": true, "requires": { "aproba": "^1.1.1", "fs-write-stream-atomic": "^1.0.8", @@ -3907,33 +3857,28 @@ "dependencies": { "aproba": { "version": "1.2.0", - "resolved": "", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "bundled": true }, "iferr": { "version": "0.1.5", - "resolved": "", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" + "bundled": true } } }, "core-util-is": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "bundled": true }, "create-error-class": { "version": "3.0.2", - "resolved": "", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "bundled": true, "requires": { "capture-stack-trace": "^1.0.0" } }, "cross-spawn": { "version": "5.1.0", - "resolved": "", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "bundled": true, "requires": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", @@ -3942,8 +3887,7 @@ "dependencies": { "lru-cache": { "version": "4.1.5", - "resolved": "", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "bundled": true, "requires": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" @@ -3951,131 +3895,110 @@ }, "yallist": { "version": "2.1.2", - "resolved": "", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + "bundled": true } } }, "crypto-random-string": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" + "bundled": true }, "cyclist": { "version": "0.2.2", - "resolved": "", - "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=" + "bundled": true }, "dashdash": { "version": "1.14.1", - "resolved": "", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "bundled": true, "requires": { "assert-plus": "^1.0.0" } }, "debug": { "version": "3.1.0", - "resolved": "", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "bundled": true, "requires": { "ms": "2.0.0" }, "dependencies": { "ms": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "bundled": true } } }, "debuglog": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=" + "bundled": true }, "decamelize": { "version": "1.2.0", - "resolved": "", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + "bundled": true }, "decode-uri-component": { "version": "0.2.0", - "resolved": "", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + "bundled": true }, "deep-extend": { "version": "0.6.0", - "resolved": "", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + "bundled": true }, "defaults": { "version": "1.0.3", - "resolved": "", - "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "bundled": true, "requires": { "clone": "^1.0.2" } }, "define-properties": { "version": "1.1.3", - "resolved": "", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "bundled": true, "requires": { "object-keys": "^1.0.12" } }, "delayed-stream": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "bundled": true }, "delegates": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + "bundled": true }, "detect-indent": { "version": "5.0.0", - "resolved": "", - "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=" + "bundled": true }, "detect-newline": { "version": "2.1.0", - "resolved": "", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=" + "bundled": true }, "dezalgo": { "version": "1.0.3", - "resolved": "", - "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", + "bundled": true, "requires": { "asap": "^2.0.0", "wrappy": "1" } }, "dot-prop": { - "version": "4.2.0", - "resolved": "", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "version": "4.2.1", + "bundled": true, "requires": { "is-obj": "^1.0.0" } }, "dotenv": { "version": "5.0.1", - "resolved": "", - "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==" + "bundled": true }, "duplexer3": { "version": "0.1.4", - "resolved": "", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + "bundled": true }, "duplexify": { "version": "3.6.0", - "resolved": "", - "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", + "bundled": true, "requires": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", @@ -4085,8 +4008,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4099,8 +4021,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -4109,8 +4030,7 @@ }, "ecc-jsbn": { "version": "0.1.2", - "resolved": "", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "bundled": true, "optional": true, "requires": { "jsbn": "~0.1.0", @@ -4119,47 +4039,44 @@ }, "editor": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-YMf4e9YrzGqJT6jM1q+3gjok90I=" + "bundled": true + }, + "emoji-regex": { + "version": "7.0.3", + "bundled": true }, "encoding": { "version": "0.1.12", - "resolved": "", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "bundled": true, "requires": { "iconv-lite": "~0.4.13" } }, "end-of-stream": { "version": "1.4.1", - "resolved": "", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "bundled": true, "requires": { "once": "^1.4.0" } }, "env-paths": { "version": "2.2.0", - "resolved": "", - "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==" + "bundled": true }, "err-code": { "version": "1.1.2", - "resolved": "", - "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=" + "bundled": true }, "errno": { "version": "0.1.7", - "resolved": "", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "bundled": true, "requires": { "prr": "~1.0.1" } }, "es-abstract": { "version": "1.12.0", - "resolved": "", - "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", + "bundled": true, "requires": { "es-to-primitive": "^1.1.1", "function-bind": "^1.1.1", @@ -4170,8 +4087,7 @@ }, "es-to-primitive": { "version": "1.2.0", - "resolved": "", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "bundled": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -4180,26 +4096,22 @@ }, "es6-promise": { "version": "4.2.8", - "resolved": "", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + "bundled": true }, "es6-promisify": { "version": "5.0.0", - "resolved": "", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "bundled": true, "requires": { "es6-promise": "^4.0.3" } }, "escape-string-regexp": { "version": "1.0.5", - "resolved": "", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "bundled": true }, "execa": { "version": "0.7.0", - "resolved": "", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "bundled": true, "requires": { "cross-spawn": "^5.0.1", "get-stream": "^3.0.0", @@ -4212,53 +4124,37 @@ "dependencies": { "get-stream": { "version": "3.0.0", - "resolved": "", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + "bundled": true } } }, "extend": { "version": "3.0.2", - "resolved": "", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "bundled": true }, "extsprintf": { "version": "1.3.0", - "resolved": "", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "bundled": true }, "fast-deep-equal": { "version": "1.1.0", - "resolved": "", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + "bundled": true }, "fast-json-stable-stringify": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + "bundled": true }, "figgy-pudding": { "version": "3.5.1", - "resolved": "", - "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==" + "bundled": true }, "find-npm-prefix": { "version": "1.0.2", - "resolved": "", - "integrity": "sha512-KEftzJ+H90x6pcKtdXZEPsQse8/y/UnvzRKrOSQFprnrGaFuJ62fVkP34Iu2IYuMvyauCyoLTNkJZgrrGA2wkA==" - }, - "find-up": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "^2.0.0" - } + "bundled": true }, "flush-write-stream": { "version": "1.0.3", - "resolved": "", - "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", + "bundled": true, "requires": { "inherits": "^2.0.1", "readable-stream": "^2.0.4" @@ -4266,8 +4162,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4280,8 +4175,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -4290,13 +4184,11 @@ }, "forever-agent": { "version": "0.6.1", - "resolved": "", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "bundled": true }, "form-data": { "version": "2.3.2", - "resolved": "", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "bundled": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "1.0.6", @@ -4305,8 +4197,7 @@ }, "from2": { "version": "2.3.0", - "resolved": "", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "bundled": true, "requires": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" @@ -4314,8 +4205,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4328,8 +4218,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -4338,16 +4227,14 @@ }, "fs-minipass": { "version": "1.2.7", - "resolved": "", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "bundled": true, "requires": { "minipass": "^2.6.0" }, "dependencies": { "minipass": { "version": "2.9.0", - "resolved": "", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "bundled": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4357,8 +4244,7 @@ }, "fs-vacuum": { "version": "1.2.10", - "resolved": "", - "integrity": "sha1-t2Kb7AekAxolSP35n17PHMizHjY=", + "bundled": true, "requires": { "graceful-fs": "^4.1.2", "path-is-inside": "^1.0.1", @@ -4367,8 +4253,7 @@ }, "fs-write-stream-atomic": { "version": "1.0.10", - "resolved": "", - "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "bundled": true, "requires": { "graceful-fs": "^4.1.2", "iferr": "^0.1.5", @@ -4378,13 +4263,11 @@ "dependencies": { "iferr": { "version": "0.1.5", - "resolved": "", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" + "bundled": true }, "readable-stream": { "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4397,8 +4280,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -4407,18 +4289,15 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "bundled": true }, "function-bind": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "bundled": true }, "gauge": { "version": "2.7.4", - "resolved": "", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "bundled": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -4432,13 +4311,11 @@ "dependencies": { "aproba": { "version": "1.2.0", - "resolved": "", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "bundled": true }, "string-width": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "bundled": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4449,13 +4326,11 @@ }, "genfun": { "version": "5.0.0", - "resolved": "", - "integrity": "sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==" + "bundled": true }, "gentle-fs": { - "version": "2.3.0", - "resolved": "", - "integrity": "sha512-3k2CgAmPxuz7S6nKK+AqFE2AdM1QuwqKLPKzIET3VRwK++3q96MsNFobScDjlCrq97ZJ8y5R725MOlm6ffUCjg==", + "version": "2.3.1", + "bundled": true, "requires": { "aproba": "^1.1.2", "chownr": "^1.1.2", @@ -4472,41 +4347,35 @@ "dependencies": { "aproba": { "version": "1.2.0", - "resolved": "", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "bundled": true }, "iferr": { "version": "0.1.5", - "resolved": "", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" + "bundled": true } } }, "get-caller-file": { - "version": "1.0.3", - "resolved": "", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" + "version": "2.0.5", + "bundled": true }, "get-stream": { "version": "4.1.0", - "resolved": "", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "bundled": true, "requires": { "pump": "^3.0.0" } }, "getpass": { "version": "0.1.7", - "resolved": "", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "bundled": true, "requires": { "assert-plus": "^1.0.0" } }, "glob": { "version": "7.1.6", - "resolved": "", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "bundled": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -4518,16 +4387,14 @@ }, "global-dirs": { "version": "0.1.1", - "resolved": "", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "bundled": true, "requires": { "ini": "^1.3.4" } }, "got": { "version": "6.7.1", - "resolved": "", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "bundled": true, "requires": { "create-error-class": "^3.0.0", "duplexer3": "^0.1.4", @@ -4544,25 +4411,21 @@ "dependencies": { "get-stream": { "version": "3.0.0", - "resolved": "", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + "bundled": true } } }, "graceful-fs": { "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + "bundled": true }, "har-schema": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "bundled": true }, "har-validator": { "version": "5.1.0", - "resolved": "", - "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", + "bundled": true, "requires": { "ajv": "^5.3.0", "har-schema": "^2.0.0" @@ -4570,41 +4433,34 @@ }, "has": { "version": "1.0.3", - "resolved": "", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "bundled": true, "requires": { "function-bind": "^1.1.1" } }, "has-flag": { "version": "3.0.0", - "resolved": "", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "bundled": true }, "has-symbols": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" + "bundled": true }, "has-unicode": { "version": "2.0.1", - "resolved": "", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + "bundled": true }, "hosted-git-info": { "version": "2.8.8", - "resolved": "", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" + "bundled": true }, "http-cache-semantics": { "version": "3.8.1", - "resolved": "", - "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==" + "bundled": true }, "http-proxy-agent": { "version": "2.1.0", - "resolved": "", - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "bundled": true, "requires": { "agent-base": "4", "debug": "3.1.0" @@ -4612,8 +4468,7 @@ }, "http-signature": { "version": "1.2.0", - "resolved": "", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "bundled": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -4622,8 +4477,7 @@ }, "https-proxy-agent": { "version": "2.2.4", - "resolved": "", - "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "bundled": true, "requires": { "agent-base": "^4.3.0", "debug": "^3.1.0" @@ -4631,52 +4485,44 @@ }, "humanize-ms": { "version": "1.2.1", - "resolved": "", - "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "bundled": true, "requires": { "ms": "^2.0.0" } }, "iconv-lite": { "version": "0.4.23", - "resolved": "", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "bundled": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } }, "iferr": { "version": "1.0.2", - "resolved": "", - "integrity": "sha512-9AfeLfji44r5TKInjhz3W9DyZI1zR1JAf2hVBMGhddAKPqBsupb89jGfbCTHIGZd6fGZl9WlHdn4AObygyMKwg==" + "bundled": true }, "ignore-walk": { "version": "3.0.3", - "resolved": "", - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "bundled": true, "requires": { "minimatch": "^3.0.4" } }, "import-lazy": { "version": "2.1.0", - "resolved": "", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" + "bundled": true }, "imurmurhash": { "version": "0.1.4", - "resolved": "", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + "bundled": true }, "infer-owner": { "version": "1.0.4", - "resolved": "", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" + "bundled": true }, "inflight": { "version": "1.0.6", - "resolved": "", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "bundled": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -4684,18 +4530,15 @@ }, "inherits": { "version": "2.0.4", - "resolved": "", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "bundled": true }, "ini": { "version": "1.3.5", - "resolved": "", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + "bundled": true }, "init-package-json": { "version": "1.10.3", - "resolved": "", - "integrity": "sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw==", + "bundled": true, "requires": { "glob": "^7.1.1", "npm-package-arg": "^4.0.0 || ^5.0.0 || ^6.0.0", @@ -4707,66 +4550,52 @@ "validate-npm-package-name": "^3.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" - }, "ip": { "version": "1.1.5", - "resolved": "", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + "bundled": true }, "ip-regex": { "version": "2.1.0", - "resolved": "", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" + "bundled": true }, "is-callable": { "version": "1.1.4", - "resolved": "", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" + "bundled": true }, "is-ci": { "version": "1.2.1", - "resolved": "", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "bundled": true, "requires": { "ci-info": "^1.5.0" }, "dependencies": { "ci-info": { "version": "1.6.0", - "resolved": "", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==" + "bundled": true } } }, "is-cidr": { "version": "3.0.0", - "resolved": "", - "integrity": "sha512-8Xnnbjsb0x462VoYiGlhEi+drY8SFwrHiSYuzc/CEwco55vkehTaxAyIjEdpi3EMvLPPJAJi9FlzP+h+03gp0Q==", + "bundled": true, "requires": { "cidr-regex": "^2.0.10" } }, "is-date-object": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + "bundled": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "bundled": true, "requires": { "number-is-nan": "^1.0.0" } }, "is-installed-globally": { "version": "0.1.0", - "resolved": "", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "bundled": true, "requires": { "global-dirs": "^0.1.0", "is-path-inside": "^1.0.0" @@ -4774,108 +4603,89 @@ }, "is-npm": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=" + "bundled": true }, "is-obj": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + "bundled": true }, "is-path-inside": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "bundled": true, "requires": { "path-is-inside": "^1.0.1" } }, "is-redirect": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" + "bundled": true }, "is-regex": { "version": "1.0.4", - "resolved": "", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "bundled": true, "requires": { "has": "^1.0.1" } }, "is-retry-allowed": { "version": "1.2.0", - "resolved": "", - "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==" + "bundled": true }, "is-stream": { "version": "1.1.0", - "resolved": "", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + "bundled": true }, "is-symbol": { "version": "1.0.2", - "resolved": "", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "bundled": true, "requires": { "has-symbols": "^1.0.0" } }, "is-typedarray": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "bundled": true }, "isarray": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "bundled": true }, "isexe": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "bundled": true }, "isstream": { "version": "0.1.2", - "resolved": "", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "bundled": true }, "jsbn": { "version": "0.1.1", - "resolved": "", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "bundled": true, "optional": true }, "json-parse-better-errors": { "version": "1.0.2", - "resolved": "", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + "bundled": true }, "json-schema": { "version": "0.2.3", - "resolved": "", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + "bundled": true }, "json-schema-traverse": { "version": "0.3.1", - "resolved": "", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + "bundled": true }, "json-stringify-safe": { "version": "5.0.1", - "resolved": "", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "bundled": true }, "jsonparse": { "version": "1.3.1", - "resolved": "", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + "bundled": true }, "jsprim": { "version": "1.4.1", - "resolved": "", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "bundled": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -4885,29 +4695,18 @@ }, "latest-version": { "version": "3.1.0", - "resolved": "", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "bundled": true, "requires": { "package-json": "^4.0.0" } }, "lazy-property": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-hN3Es3Bnm6i9TNz6TAa0PVcREUc=" - }, - "lcid": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "requires": { - "invert-kv": "^2.0.0" - } + "bundled": true }, "libcipm": { - "version": "4.0.7", - "resolved": "", - "integrity": "sha512-fTq33otU3PNXxxCTCYCYe7V96o59v/o7bvtspmbORXpgFk+wcWrGf5x6tBgui5gCed/45/wtPomBsZBYm5KbIw==", + "version": "4.0.8", + "bundled": true, "requires": { "bin-links": "^1.1.2", "bluebird": "^3.5.1", @@ -4915,7 +4714,7 @@ "find-npm-prefix": "^1.0.2", "graceful-fs": "^4.1.11", "ini": "^1.3.5", - "lock-verify": "^2.0.2", + "lock-verify": "^2.1.0", "mkdirp": "^0.5.1", "npm-lifecycle": "^3.0.0", "npm-logical-tree": "^1.2.1", @@ -4928,8 +4727,7 @@ }, "libnpm": { "version": "3.0.1", - "resolved": "", - "integrity": "sha512-d7jU5ZcMiTfBqTUJVZ3xid44fE5ERBm9vBnmhp2ECD2Ls+FNXWxHSkO7gtvrnbLO78gwPdNPz1HpsF3W4rjkBQ==", + "bundled": true, "requires": { "bin-links": "^1.1.2", "bluebird": "^3.5.3", @@ -4955,8 +4753,7 @@ }, "libnpmaccess": { "version": "3.0.2", - "resolved": "", - "integrity": "sha512-01512AK7MqByrI2mfC7h5j8N9V4I7MHJuk9buo8Gv+5QgThpOgpjB7sQBDDkeZqRteFb1QM/6YNdHfG7cDvfAQ==", + "bundled": true, "requires": { "aproba": "^2.0.0", "get-stream": "^4.0.0", @@ -4966,8 +4763,7 @@ }, "libnpmconfig": { "version": "1.2.1", - "resolved": "", - "integrity": "sha512-9esX8rTQAHqarx6qeZqmGQKBNZR5OIbl/Ayr0qQDy3oXja2iFVQQI81R6GZ2a02bSNZ9p3YOGX1O6HHCb1X7kA==", + "bundled": true, "requires": { "figgy-pudding": "^3.5.1", "find-up": "^3.0.0", @@ -4976,16 +4772,14 @@ "dependencies": { "find-up": { "version": "3.0.0", - "resolved": "", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "bundled": true, "requires": { "locate-path": "^3.0.0" } }, "locate-path": { "version": "3.0.0", - "resolved": "", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "bundled": true, "requires": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" @@ -4993,31 +4787,27 @@ }, "p-limit": { "version": "2.2.0", - "resolved": "", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "bundled": true, "requires": { "p-try": "^2.0.0" } }, "p-locate": { "version": "3.0.0", - "resolved": "", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "bundled": true, "requires": { "p-limit": "^2.0.0" } }, "p-try": { "version": "2.2.0", - "resolved": "", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + "bundled": true } } }, "libnpmhook": { "version": "5.0.3", - "resolved": "", - "integrity": "sha512-UdNLMuefVZra/wbnBXECZPefHMGsVDTq5zaM/LgKNE9Keyl5YXQTnGAzEo+nFOpdRqTWI9LYi4ApqF9uVCCtuA==", + "bundled": true, "requires": { "aproba": "^2.0.0", "figgy-pudding": "^3.4.1", @@ -5027,8 +4817,7 @@ }, "libnpmorg": { "version": "1.0.1", - "resolved": "", - "integrity": "sha512-0sRUXLh+PLBgZmARvthhYXQAWn0fOsa6T5l3JSe2n9vKG/lCVK4nuG7pDsa7uMq+uTt2epdPK+a2g6btcY11Ww==", + "bundled": true, "requires": { "aproba": "^2.0.0", "figgy-pudding": "^3.4.1", @@ -5038,8 +4827,7 @@ }, "libnpmpublish": { "version": "1.1.2", - "resolved": "", - "integrity": "sha512-2yIwaXrhTTcF7bkJKIKmaCV9wZOALf/gsTDxVSu/Gu/6wiG3fA8ce8YKstiWKTxSFNC0R7isPUb6tXTVFZHt2g==", + "bundled": true, "requires": { "aproba": "^2.0.0", "figgy-pudding": "^3.5.1", @@ -5054,8 +4842,7 @@ }, "libnpmsearch": { "version": "2.0.2", - "resolved": "", - "integrity": "sha512-VTBbV55Q6fRzTdzziYCr64+f8AopQ1YZ+BdPOv16UegIEaE8C0Kch01wo4s3kRTFV64P121WZJwgmBwrq68zYg==", + "bundled": true, "requires": { "figgy-pudding": "^3.5.1", "get-stream": "^4.0.0", @@ -5064,8 +4851,7 @@ }, "libnpmteam": { "version": "1.0.2", - "resolved": "", - "integrity": "sha512-p420vM28Us04NAcg1rzgGW63LMM6rwe+6rtZpfDxCcXxM0zUTLl7nPFEnRF3JfFBF5skF/yuZDUthTsHgde8QA==", + "bundled": true, "requires": { "aproba": "^2.0.0", "figgy-pudding": "^3.4.1", @@ -5074,9 +4860,8 @@ } }, "libnpx": { - "version": "10.2.2", - "resolved": "", - "integrity": "sha512-ujaYToga1SAX5r7FU5ShMFi88CWpY75meNZtr6RtEyv4l2ZK3+Wgvxq2IqlwWBiDZOqhumdeiocPS1aKrCMe3A==", + "version": "10.2.4", + "bundled": true, "requires": { "dotenv": "^5.0.1", "npm-package-arg": "^6.0.0", @@ -5085,22 +4870,12 @@ "update-notifier": "^2.3.0", "which": "^1.3.0", "y18n": "^4.0.0", - "yargs": "^11.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" + "yargs": "^14.2.3" } }, "lock-verify": { "version": "2.1.0", - "resolved": "", - "integrity": "sha512-vcLpxnGvrqisKvLQ2C2v0/u7LVly17ak2YSgoK4PrdsYBXQIax19vhKiLfvKNFx7FRrpTnitrpzF/uuCMuorIg==", + "bundled": true, "requires": { "npm-package-arg": "^6.1.0", "semver": "^5.4.1" @@ -5108,21 +4883,18 @@ }, "lockfile": { "version": "1.0.4", - "resolved": "", - "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", + "bundled": true, "requires": { "signal-exit": "^3.0.2" } }, "lodash._baseindexof": { "version": "3.1.0", - "resolved": "", - "integrity": "sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=" + "bundled": true }, "lodash._baseuniq": { "version": "4.6.0", - "resolved": "", - "integrity": "sha1-DrtE5FaBSveQXGIS+iybLVG4Qeg=", + "bundled": true, "requires": { "lodash._createset": "~4.0.0", "lodash._root": "~3.0.0" @@ -5130,87 +4902,72 @@ }, "lodash._bindcallback": { "version": "3.0.1", - "resolved": "", - "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=" + "bundled": true }, "lodash._cacheindexof": { "version": "3.0.2", - "resolved": "", - "integrity": "sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=" + "bundled": true }, "lodash._createcache": { "version": "3.1.2", - "resolved": "", - "integrity": "sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=", + "bundled": true, "requires": { "lodash._getnative": "^3.0.0" } }, "lodash._createset": { "version": "4.0.3", - "resolved": "", - "integrity": "sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=" + "bundled": true }, "lodash._getnative": { "version": "3.9.1", - "resolved": "", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" + "bundled": true }, "lodash._root": { "version": "3.0.1", - "resolved": "", - "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=" + "bundled": true }, "lodash.clonedeep": { "version": "4.5.0", - "resolved": "", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + "bundled": true }, "lodash.restparam": { "version": "3.6.1", - "resolved": "", - "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=" + "bundled": true }, "lodash.union": { "version": "4.6.0", - "resolved": "", - "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" + "bundled": true }, "lodash.uniq": { "version": "4.5.0", - "resolved": "", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + "bundled": true }, "lodash.without": { "version": "4.4.0", - "resolved": "", - "integrity": "sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=" + "bundled": true }, "lowercase-keys": { "version": "1.0.1", - "resolved": "", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + "bundled": true }, "lru-cache": { "version": "5.1.1", - "resolved": "", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "bundled": true, "requires": { "yallist": "^3.0.2" } }, "make-dir": { "version": "1.3.0", - "resolved": "", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "bundled": true, "requires": { "pify": "^3.0.0" } }, "make-fetch-happen": { "version": "5.0.2", - "resolved": "", - "integrity": "sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag==", + "bundled": true, "requires": { "agentkeepalive": "^3.4.1", "cacache": "^12.0.0", @@ -5225,69 +4982,42 @@ "ssri": "^6.0.0" } }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "requires": { - "p-defer": "^1.0.0" - } - }, "meant": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-UakVLFjKkbbUwNWJ2frVLnnAtbb7D7DsloxRd3s/gDpI8rdv8W5Hp3NaDb+POBI1fQdeussER6NB8vpcRURvlg==" - }, - "mem": { - "version": "4.3.0", - "resolved": "", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - }, - "dependencies": { - "mimic-fn": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" - } - } + "version": "1.0.2", + "bundled": true }, "mime-db": { "version": "1.35.0", - "resolved": "", - "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" + "bundled": true }, "mime-types": { "version": "2.1.19", - "resolved": "", - "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", + "bundled": true, "requires": { "mime-db": "~1.35.0" } }, "minimatch": { "version": "3.0.4", - "resolved": "", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "bundled": true, "requires": { "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.5", + "bundled": true + }, "minizlib": { "version": "1.3.3", - "resolved": "", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "bundled": true, "requires": { "minipass": "^2.9.0" }, "dependencies": { "minipass": { "version": "2.9.0", - "resolved": "", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "bundled": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5297,8 +5027,7 @@ }, "mississippi": { "version": "3.0.0", - "resolved": "", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "bundled": true, "requires": { "concat-stream": "^1.5.0", "duplexify": "^3.4.2", @@ -5314,23 +5043,20 @@ }, "mkdirp": { "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "bundled": true, "requires": { "minimist": "^1.2.5" }, "dependencies": { "minimist": { "version": "1.2.5", - "resolved": "", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "bundled": true } } }, "move-concurrently": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "bundled": true, "requires": { "aproba": "^1.1.1", "copy-concurrently": "^1.0.0", @@ -5342,30 +5068,21 @@ "dependencies": { "aproba": { "version": "1.2.0", - "resolved": "", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "bundled": true } } }, "ms": { "version": "2.1.1", - "resolved": "", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + "bundled": true }, "mute-stream": { "version": "0.0.7", - "resolved": "", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" - }, - "nice-try": { - "version": "1.0.5", - "resolved": "", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + "bundled": true }, "node-fetch-npm": { "version": "2.0.2", - "resolved": "", - "integrity": "sha512-nJIxm1QmAj4v3nfCvEeCrYSoVwXyxLnaPBK5W1W5DGEJwjlKuC2VEUycGw5oxk+4zZahRrB84PUJJgEmhFTDFw==", + "bundled": true, "requires": { "encoding": "^0.1.11", "json-parse-better-errors": "^1.0.0", @@ -5374,8 +5091,7 @@ }, "node-gyp": { "version": "5.1.0", - "resolved": "", - "integrity": "sha512-OUTryc5bt/P8zVgNUmC6xdXiDJxLMAW8cF5tLQOT9E5sOQj+UeQxnnPy74K3CLCa/SOjjBlbuzDLR8ANwA+wmw==", + "bundled": true, "requires": { "env-paths": "^2.2.0", "glob": "^7.1.4", @@ -5392,8 +5108,7 @@ }, "nopt": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "bundled": true, "requires": { "abbrev": "1", "osenv": "^0.1.4" @@ -5401,8 +5116,7 @@ }, "normalize-package-data": { "version": "2.5.0", - "resolved": "", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "bundled": true, "requires": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -5412,8 +5126,7 @@ "dependencies": { "resolve": { "version": "1.10.0", - "resolved": "", - "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "bundled": true, "requires": { "path-parse": "^1.0.6" } @@ -5421,9 +5134,8 @@ } }, "npm-audit-report": { - "version": "1.3.2", - "resolved": "", - "integrity": "sha512-abeqS5ONyXNaZJPGAf6TOUMNdSe1Y6cpc9MLBRn+CuUoYbfdca6AxOyXVlfIv9OgKX+cacblbG5w7A6ccwoTPw==", + "version": "1.3.3", + "bundled": true, "requires": { "cli-table3": "^0.5.0", "console-control-strings": "^1.1.0" @@ -5431,29 +5143,25 @@ }, "npm-bundled": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "bundled": true, "requires": { "npm-normalize-package-bin": "^1.0.1" } }, "npm-cache-filename": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-3tMGxbC/yHCp6fr4I7xfKD4FrhE=" + "bundled": true }, "npm-install-checks": { "version": "3.0.2", - "resolved": "", - "integrity": "sha512-E4kzkyZDIWoin6uT5howP8VDvkM+E8IQDcHAycaAxMbwkqhIg5eEYALnXOl3Hq9MrkdQB/2/g1xwBINXdKSRkg==", + "bundled": true, "requires": { "semver": "^2.3.0 || 3.x || 4 || 5" } }, "npm-lifecycle": { - "version": "3.1.4", - "resolved": "", - "integrity": "sha512-tgs1PaucZwkxECGKhC/stbEgFyc3TGh2TJcg2CDr6jbvQRdteHNhmMeljRzpe4wgFAXQADoy1cSqqi7mtiAa5A==", + "version": "3.1.5", + "bundled": true, "requires": { "byline": "^5.0.0", "graceful-fs": "^4.1.15", @@ -5467,18 +5175,15 @@ }, "npm-logical-tree": { "version": "1.2.1", - "resolved": "", - "integrity": "sha512-AJI/qxDB2PWI4LG1CYN579AY1vCiNyWfkiquCsJWqntRu/WwimVrC8yXeILBFHDwxfOejxewlmnvW9XXjMlYIg==" + "bundled": true }, "npm-normalize-package-bin": { "version": "1.0.1", - "resolved": "", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" + "bundled": true }, "npm-package-arg": { "version": "6.1.1", - "resolved": "", - "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==", + "bundled": true, "requires": { "hosted-git-info": "^2.7.1", "osenv": "^0.1.5", @@ -5488,8 +5193,7 @@ }, "npm-packlist": { "version": "1.4.8", - "resolved": "", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "bundled": true, "requires": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1", @@ -5498,8 +5202,7 @@ }, "npm-pick-manifest": { "version": "3.0.2", - "resolved": "", - "integrity": "sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw==", + "bundled": true, "requires": { "figgy-pudding": "^3.5.1", "npm-package-arg": "^6.0.0", @@ -5508,8 +5211,7 @@ }, "npm-profile": { "version": "4.0.4", - "resolved": "", - "integrity": "sha512-Ta8xq8TLMpqssF0H60BXS1A90iMoM6GeKwsmravJ6wYjWwSzcYBTdyWa3DZCYqPutacBMEm7cxiOkiIeCUAHDQ==", + "bundled": true, "requires": { "aproba": "^1.1.2 || 2", "figgy-pudding": "^3.4.1", @@ -5517,9 +5219,8 @@ } }, "npm-registry-fetch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.4.tgz", - "integrity": "sha512-6jb34hX/iYNQebqWUHtU8YF6Cjb1H6ouTFPClYsyiW6lpFkljTpdeftm53rRojtja1rKAvKNIIiTS5Sjpw4wsA==", + "version": "4.0.7", + "bundled": true, "requires": { "JSONStream": "^1.3.4", "bluebird": "^3.5.1", @@ -5531,29 +5232,25 @@ }, "dependencies": { "safe-buffer": { - "version": "5.2.0", - "resolved": "", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + "version": "5.2.1", + "bundled": true } } }, "npm-run-path": { "version": "2.0.2", - "resolved": "", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "bundled": true, "requires": { "path-key": "^2.0.0" } }, "npm-user-validate": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-jOyg9c6gTU6TUZ73LQVXp1Ei6VE=" + "bundled": true }, "npmlog": { "version": "4.1.2", - "resolved": "", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "bundled": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -5563,28 +5260,23 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "bundled": true }, "oauth-sign": { "version": "0.9.0", - "resolved": "", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + "bundled": true }, "object-assign": { "version": "4.1.1", - "resolved": "", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "bundled": true }, "object-keys": { "version": "1.0.12", - "resolved": "", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==" + "bundled": true }, "object.getownpropertydescriptors": { "version": "2.0.3", - "resolved": "", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "bundled": true, "requires": { "define-properties": "^1.1.2", "es-abstract": "^1.5.1" @@ -5592,114 +5284,38 @@ }, "once": { "version": "1.4.0", - "resolved": "", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "bundled": true, "requires": { "wrappy": "1" } }, "opener": { "version": "1.5.1", - "resolved": "", - "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==" + "bundled": true }, "os-homedir": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-locale": { - "version": "3.1.0", - "resolved": "", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - } - } + "bundled": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + "bundled": true }, "osenv": { "version": "0.1.5", - "resolved": "", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "bundled": true, "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" } }, - "p-defer": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" - }, "p-finally": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" - }, - "p-limit": { - "version": "1.2.0", - "resolved": "", - "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + "bundled": true }, "package-json": { "version": "4.0.1", - "resolved": "", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "bundled": true, "requires": { "got": "^6.7.1", "registry-auth-token": "^3.0.1", @@ -5709,8 +5325,7 @@ }, "pacote": { "version": "9.5.12", - "resolved": "", - "integrity": "sha512-BUIj/4kKbwWg4RtnBncXPJd15piFSVNpTzY0rysSr3VnMowTYgkGKcaHrbReepAkjTr8lH2CVWRi58Spg2CicQ==", + "bundled": true, "requires": { "bluebird": "^3.5.3", "cacache": "^12.0.2", @@ -5746,8 +5361,7 @@ "dependencies": { "minipass": { "version": "2.9.0", - "resolved": "", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "bundled": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5757,8 +5371,7 @@ }, "parallel-transform": { "version": "1.1.0", - "resolved": "", - "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "bundled": true, "requires": { "cyclist": "~0.2.2", "inherits": "^2.0.3", @@ -5767,8 +5380,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -5781,8 +5393,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -5791,58 +5402,47 @@ }, "path-exists": { "version": "3.0.0", - "resolved": "", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + "bundled": true }, "path-is-absolute": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "bundled": true }, "path-is-inside": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" + "bundled": true }, "path-key": { "version": "2.0.1", - "resolved": "", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + "bundled": true }, "path-parse": { "version": "1.0.6", - "resolved": "", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "bundled": true }, "performance-now": { "version": "2.1.0", - "resolved": "", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "bundled": true }, "pify": { "version": "3.0.0", - "resolved": "", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + "bundled": true }, "prepend-http": { "version": "1.0.4", - "resolved": "", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" + "bundled": true }, "process-nextick-args": { "version": "2.0.0", - "resolved": "", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + "bundled": true }, "promise-inflight": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + "bundled": true }, "promise-retry": { "version": "1.1.1", - "resolved": "", - "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", + "bundled": true, "requires": { "err-code": "^1.0.0", "retry": "^0.10.0" @@ -5850,51 +5450,43 @@ "dependencies": { "retry": { "version": "0.10.1", - "resolved": "", - "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=" + "bundled": true } } }, "promzard": { "version": "0.3.0", - "resolved": "", - "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=", + "bundled": true, "requires": { "read": "1" } }, "proto-list": { "version": "1.2.4", - "resolved": "", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" + "bundled": true }, "protoduck": { "version": "5.0.1", - "resolved": "", - "integrity": "sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg==", + "bundled": true, "requires": { "genfun": "^5.0.0" } }, "prr": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" + "bundled": true }, "pseudomap": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + "bundled": true }, "psl": { "version": "1.1.29", - "resolved": "", - "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + "bundled": true }, "pump": { "version": "3.0.0", - "resolved": "", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "bundled": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -5902,8 +5494,7 @@ }, "pumpify": { "version": "1.5.1", - "resolved": "", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "bundled": true, "requires": { "duplexify": "^3.6.0", "inherits": "^2.0.3", @@ -5912,8 +5503,7 @@ "dependencies": { "pump": { "version": "2.0.1", - "resolved": "", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "bundled": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -5923,23 +5513,19 @@ }, "punycode": { "version": "1.4.1", - "resolved": "", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + "bundled": true }, "qrcode-terminal": { "version": "0.12.0", - "resolved": "", - "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==" + "bundled": true }, "qs": { "version": "6.5.2", - "resolved": "", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + "bundled": true }, "query-string": { "version": "6.8.2", - "resolved": "", - "integrity": "sha512-J3Qi8XZJXh93t2FiKyd/7Ec6GNifsjKXUsVFkSBj/kjLsDylWhnCz4NT1bkPcKotttPW+QbKGqqPH8OoI2pdqw==", + "bundled": true, "requires": { "decode-uri-component": "^0.2.0", "split-on-first": "^1.0.0", @@ -5948,47 +5534,35 @@ }, "qw": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-77/cdA+a0FQwRCassYNBLMi5ltQ=" + "bundled": true }, "rc": { "version": "1.2.8", - "resolved": "", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "bundled": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - } } }, "read": { "version": "1.0.7", - "resolved": "", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "bundled": true, "requires": { "mute-stream": "~0.0.4" } }, "read-cmd-shim": { "version": "1.0.5", - "resolved": "", - "integrity": "sha512-v5yCqQ/7okKoZZkBQUAfTsQ3sVJtXdNfbPnI5cceppoxEVLYA3k+VtV2omkeo8MS94JCy4fSiUwlRBAwCVRPUA==", + "bundled": true, "requires": { "graceful-fs": "^4.1.2" } }, "read-installed": { "version": "4.0.3", - "resolved": "", - "integrity": "sha1-/5uLZ/GH0eTCm5/rMfayI6zRkGc=", + "bundled": true, "requires": { "debuglog": "^1.0.1", "graceful-fs": "^4.1.2", @@ -6001,8 +5575,7 @@ }, "read-package-json": { "version": "2.1.1", - "resolved": "", - "integrity": "sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A==", + "bundled": true, "requires": { "glob": "^7.1.1", "graceful-fs": "^4.1.2", @@ -6013,8 +5586,7 @@ }, "read-package-tree": { "version": "5.3.1", - "resolved": "", - "integrity": "sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==", + "bundled": true, "requires": { "read-package-json": "^2.0.0", "readdir-scoped-modules": "^1.0.0", @@ -6023,8 +5595,7 @@ }, "readable-stream": { "version": "3.6.0", - "resolved": "", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "bundled": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -6033,8 +5604,7 @@ }, "readdir-scoped-modules": { "version": "1.1.0", - "resolved": "", - "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "bundled": true, "requires": { "debuglog": "^1.0.1", "dezalgo": "^1.0.0", @@ -6044,8 +5614,7 @@ }, "registry-auth-token": { "version": "3.4.0", - "resolved": "", - "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", + "bundled": true, "requires": { "rc": "^1.1.6", "safe-buffer": "^5.0.1" @@ -6053,16 +5622,14 @@ }, "registry-url": { "version": "3.1.0", - "resolved": "", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "bundled": true, "requires": { "rc": "^1.0.1" } }, "request": { "version": "2.88.0", - "resolved": "", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "bundled": true, "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -6088,115 +5655,96 @@ }, "require-directory": { "version": "2.1.1", - "resolved": "", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + "bundled": true }, "require-main-filename": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + "version": "2.0.0", + "bundled": true }, "resolve-from": { "version": "4.0.0", - "resolved": "", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + "bundled": true }, "retry": { "version": "0.12.0", - "resolved": "", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + "bundled": true }, "rimraf": { "version": "2.7.1", - "resolved": "", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "bundled": true, "requires": { "glob": "^7.1.3" } }, "run-queue": { "version": "1.0.3", - "resolved": "", - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "bundled": true, "requires": { "aproba": "^1.1.1" }, "dependencies": { "aproba": { "version": "1.2.0", - "resolved": "", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "bundled": true } } }, "safe-buffer": { "version": "5.1.2", - "resolved": "", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "bundled": true }, "safer-buffer": { "version": "2.1.2", - "resolved": "", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "bundled": true }, "semver": { "version": "5.7.1", - "resolved": "", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "bundled": true }, "semver-diff": { "version": "2.1.0", - "resolved": "", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "bundled": true, "requires": { "semver": "^5.0.3" } }, "set-blocking": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "bundled": true }, "sha": { "version": "3.0.0", - "resolved": "", - "integrity": "sha512-DOYnM37cNsLNSGIG/zZWch5CKIRNoLdYUQTQlcgkRkoYIUwDYjqDyye16YcDZg/OPdcbUgTKMjc4SY6TB7ZAPw==", + "bundled": true, "requires": { "graceful-fs": "^4.1.2" } }, "shebang-command": { "version": "1.2.0", - "resolved": "", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "bundled": true, "requires": { "shebang-regex": "^1.0.0" } }, "shebang-regex": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + "bundled": true }, "signal-exit": { "version": "3.0.2", - "resolved": "", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + "bundled": true }, "slide": { "version": "1.1.6", - "resolved": "", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=" + "bundled": true }, "smart-buffer": { "version": "4.1.0", - "resolved": "", - "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==" + "bundled": true }, "socks": { "version": "2.3.3", - "resolved": "", - "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", + "bundled": true, "requires": { "ip": "1.1.5", "smart-buffer": "^4.1.0" @@ -6204,8 +5752,7 @@ }, "socks-proxy-agent": { "version": "4.0.2", - "resolved": "", - "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", + "bundled": true, "requires": { "agent-base": "~4.2.1", "socks": "~2.3.2" @@ -6213,8 +5760,7 @@ "dependencies": { "agent-base": { "version": "4.2.1", - "resolved": "", - "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "bundled": true, "requires": { "es6-promisify": "^5.0.0" } @@ -6223,13 +5769,11 @@ }, "sorted-object": { "version": "2.0.1", - "resolved": "", - "integrity": "sha1-fWMfS9OnmKJK8d/8+/6DM3pd9fw=" + "bundled": true }, "sorted-union-stream": { "version": "2.1.3", - "resolved": "", - "integrity": "sha1-x3lMfgd4gAUv9xqNSi27Sppjisc=", + "bundled": true, "requires": { "from2": "^1.3.0", "stream-iterate": "^1.1.0" @@ -6237,8 +5781,7 @@ "dependencies": { "from2": { "version": "1.3.0", - "resolved": "", - "integrity": "sha1-iEE7qqX5pZfP3pIh2GmGzTwGHf0=", + "bundled": true, "requires": { "inherits": "~2.0.1", "readable-stream": "~1.1.10" @@ -6246,13 +5789,11 @@ }, "isarray": { "version": "0.0.1", - "resolved": "", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + "bundled": true }, "readable-stream": { "version": "1.1.14", - "resolved": "", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -6262,15 +5803,13 @@ }, "string_decoder": { "version": "0.10.31", - "resolved": "", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + "bundled": true } } }, "spdx-correct": { "version": "3.0.0", - "resolved": "", - "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "bundled": true, "requires": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -6278,32 +5817,27 @@ }, "spdx-exceptions": { "version": "2.1.0", - "resolved": "", - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==" + "bundled": true }, "spdx-expression-parse": { "version": "3.0.0", - "resolved": "", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "bundled": true, "requires": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "spdx-license-ids": { - "version": "3.0.3", - "resolved": "", - "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==" + "version": "3.0.5", + "bundled": true }, "split-on-first": { "version": "1.1.0", - "resolved": "", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" + "bundled": true }, "sshpk": { "version": "1.14.2", - "resolved": "", - "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "bundled": true, "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -6318,16 +5852,14 @@ }, "ssri": { "version": "6.0.1", - "resolved": "", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "bundled": true, "requires": { "figgy-pudding": "^3.5.1" } }, "stream-each": { "version": "1.2.2", - "resolved": "", - "integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==", + "bundled": true, "requires": { "end-of-stream": "^1.1.0", "stream-shift": "^1.0.0" @@ -6335,8 +5867,7 @@ }, "stream-iterate": { "version": "1.2.0", - "resolved": "", - "integrity": "sha1-K9fHcpbBcCpGSIuK1B95hl7s1OE=", + "bundled": true, "requires": { "readable-stream": "^2.1.5", "stream-shift": "^1.0.0" @@ -6344,8 +5875,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -6358,8 +5888,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -6368,18 +5897,15 @@ }, "stream-shift": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + "bundled": true }, "strict-uri-encode": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" + "bundled": true }, "string-width": { "version": "2.1.1", - "resolved": "", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "bundled": true, "requires": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" @@ -6387,18 +5913,15 @@ "dependencies": { "ansi-regex": { "version": "3.0.0", - "resolved": "", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "bundled": true }, "is-fullwidth-code-point": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "bundled": true }, "strip-ansi": { "version": "4.0.0", - "resolved": "", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "bundled": true, "requires": { "ansi-regex": "^3.0.0" } @@ -6407,54 +5930,46 @@ }, "string_decoder": { "version": "1.3.0", - "resolved": "", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "bundled": true, "requires": { "safe-buffer": "~5.2.0" }, "dependencies": { "safe-buffer": { "version": "5.2.0", - "resolved": "", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + "bundled": true } } }, "stringify-package": { "version": "1.0.1", - "resolved": "", - "integrity": "sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==" + "bundled": true }, "strip-ansi": { "version": "3.0.1", - "resolved": "", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "bundled": true, "requires": { "ansi-regex": "^2.0.0" } }, "strip-eof": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + "bundled": true }, "strip-json-comments": { "version": "2.0.1", - "resolved": "", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + "bundled": true }, "supports-color": { "version": "5.4.0", - "resolved": "", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "bundled": true, "requires": { "has-flag": "^3.0.0" } }, "tar": { "version": "4.4.13", - "resolved": "", - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "bundled": true, "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", @@ -6467,8 +5982,7 @@ "dependencies": { "minipass": { "version": "2.9.0", - "resolved": "", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "bundled": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -6478,26 +5992,22 @@ }, "term-size": { "version": "1.2.0", - "resolved": "", - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "bundled": true, "requires": { "execa": "^0.7.0" } }, "text-table": { "version": "0.2.0", - "resolved": "", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" + "bundled": true }, "through": { "version": "2.3.8", - "resolved": "", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + "bundled": true }, "through2": { "version": "2.0.3", - "resolved": "", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "bundled": true, "requires": { "readable-stream": "^2.1.5", "xtend": "~4.0.1" @@ -6505,8 +6015,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -6519,8 +6028,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -6529,18 +6037,15 @@ }, "timed-out": { "version": "4.0.1", - "resolved": "", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" + "bundled": true }, "tiny-relative-date": { "version": "1.3.0", - "resolved": "", - "integrity": "sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A==" + "bundled": true }, "tough-cookie": { "version": "2.4.3", - "resolved": "", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "bundled": true, "requires": { "psl": "^1.1.24", "punycode": "^1.4.1" @@ -6548,71 +6053,60 @@ }, "tunnel-agent": { "version": "0.6.0", - "resolved": "", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "bundled": true, "requires": { "safe-buffer": "^5.0.1" } }, "tweetnacl": { "version": "0.14.5", - "resolved": "", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "bundled": true, "optional": true }, "typedarray": { "version": "0.0.6", - "resolved": "", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + "bundled": true }, "uid-number": { "version": "0.0.6", - "resolved": "", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" + "bundled": true }, "umask": { "version": "1.1.0", - "resolved": "", - "integrity": "sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=" + "bundled": true }, "unique-filename": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "bundled": true, "requires": { "unique-slug": "^2.0.0" } }, "unique-slug": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=", + "bundled": true, "requires": { "imurmurhash": "^0.1.4" } }, "unique-string": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "bundled": true, "requires": { "crypto-random-string": "^1.0.0" } }, "unpipe": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "bundled": true }, "unzip-response": { "version": "2.0.1", - "resolved": "", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=" + "bundled": true }, "update-notifier": { "version": "2.5.0", - "resolved": "", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "bundled": true, "requires": { "boxen": "^1.2.1", "chalk": "^2.0.1", @@ -6628,39 +6122,33 @@ }, "url-parse-lax": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "bundled": true, "requires": { "prepend-http": "^1.0.1" } }, "util-deprecate": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "bundled": true }, "util-extend": { "version": "1.0.3", - "resolved": "", - "integrity": "sha1-p8IW0mdUUWljeztu3GypEZ4v+T8=" + "bundled": true }, "util-promisify": { "version": "2.1.0", - "resolved": "", - "integrity": "sha1-PCI2R2xNMsX/PEcAKt18E7moKlM=", + "bundled": true, "requires": { "object.getownpropertydescriptors": "^2.0.3" } }, "uuid": { "version": "3.3.3", - "resolved": "", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + "bundled": true }, "validate-npm-package-license": { "version": "3.0.4", - "resolved": "", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "bundled": true, "requires": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -6668,16 +6156,14 @@ }, "validate-npm-package-name": { "version": "3.0.0", - "resolved": "", - "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "bundled": true, "requires": { "builtins": "^1.0.3" } }, "verror": { "version": "1.10.0", - "resolved": "", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "bundled": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -6686,37 +6172,32 @@ }, "wcwidth": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "bundled": true, "requires": { "defaults": "^1.0.3" } }, "which": { "version": "1.3.1", - "resolved": "", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "bundled": true, "requires": { "isexe": "^2.0.0" } }, "which-module": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + "bundled": true }, "wide-align": { "version": "1.1.2", - "resolved": "", - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "bundled": true, "requires": { "string-width": "^1.0.2" }, "dependencies": { "string-width": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "bundled": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6727,50 +6208,60 @@ }, "widest-line": { "version": "2.0.1", - "resolved": "", - "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "bundled": true, "requires": { "string-width": "^2.1.1" } }, "worker-farm": { "version": "1.7.0", - "resolved": "", - "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "bundled": true, "requires": { "errno": "~0.1.7" } }, "wrap-ansi": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "version": "5.1.0", + "bundled": true, "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" }, "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true + }, "string-width": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "version": "3.1.0", + "bundled": true, "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "bundled": true, + "requires": { + "ansi-regex": "^4.1.0" } } } }, "wrappy": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "bundled": true }, "write-file-atomic": { "version": "2.4.3", - "resolved": "", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "bundled": true, "requires": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", @@ -6779,56 +6270,108 @@ }, "xdg-basedir": { "version": "3.0.0", - "resolved": "", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=" + "bundled": true }, "xtend": { "version": "4.0.1", - "resolved": "", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + "bundled": true }, "y18n": { "version": "4.0.0", - "resolved": "", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + "bundled": true }, "yallist": { "version": "3.0.3", - "resolved": "", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + "bundled": true }, "yargs": { - "version": "11.1.1", - "resolved": "", - "integrity": "sha512-PRU7gJrJaXv3q3yQZ/+/X6KBswZiaQ+zOmdprZcouPYtQgvNU35i+68M4b1ZHLZtYFT5QObFLV+ZkmJYcwKdiw==", + "version": "14.2.3", + "bundled": true, "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.1.1", - "find-up": "^2.1.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.1.0", + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", + "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^2.0.0", + "string-width": "^3.0.0", "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^9.0.2" + "y18n": "^4.0.0", + "yargs-parser": "^15.0.1" }, "dependencies": { - "y18n": { - "version": "3.2.1", - "resolved": "", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + "ansi-regex": { + "version": "4.1.0", + "bundled": true + }, + "find-up": { + "version": "3.0.0", + "bundled": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true + }, + "locate-path": { + "version": "3.0.0", + "bundled": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "bundled": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "bundled": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "bundled": true + }, + "string-width": { + "version": "3.1.0", + "bundled": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "bundled": true, + "requires": { + "ansi-regex": "^4.1.0" + } } } }, "yargs-parser": { - "version": "9.0.2", - "resolved": "", - "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", + "version": "15.0.1", + "bundled": true, "requires": { - "camelcase": "^4.1.0" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "bundled": true + } } } } @@ -7824,11 +7367,11 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -8368,9 +7911,9 @@ }, "dependencies": { "@types/node": { - "version": "12.12.58", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.58.tgz", - "integrity": "sha512-Be46CNIHWAagEfINOjmriSxuv7IVcqbGe+sDSg2SYCEz/0CRBy7LRASGfRbD8KZkqoePU73Wsx3UvOSFcq/9hA==" + "version": "12.12.62", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.62.tgz", + "integrity": "sha512-qAfo81CsD7yQIM9mVyh6B/U47li5g7cfpVQEDMfQeF8pSZVwzbhwU3crc0qG4DmpsebpJPR49AKOExQyJ05Cpg==" }, "bl": { "version": "3.0.1", @@ -8573,9 +8116,9 @@ } }, "ueberdb2": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-0.5.3.tgz", - "integrity": "sha512-812rKRIkJD+2XX7boh7vsexzemj9/oT1KnAR7O/bAbIZIZBs2zZegEJr/pdJvzF+AIem+CVNHp1ankOpqKCKkA==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-0.5.4.tgz", + "integrity": "sha512-zrmsQruUxh6JgYi6gmwKk5Jey47lvTW3iOvxtf9zGyaKPZFOdNvvcYQK8gODTnOpoSZxz0ewD52mCMPo6g0edQ==", "requires": { "async": "^3.2.0", "cassandra-driver": "^4.5.1", @@ -8585,7 +8128,7 @@ "dirty": "^1.1.0", "elasticsearch": "^16.7.1", "mocha": "^7.1.2", - "mssql": "^6.2.1", + "mssql": "^6.2.3", "mysql": "2.18.1", "nano": "^8.2.2", "pg": "^8.0.3", diff --git a/src/package.json b/src/package.json index ee45fa9a6..24eeb53de 100644 --- a/src/package.json +++ b/src/package.json @@ -52,7 +52,7 @@ "log4js": "0.6.35", "measured-core": "1.11.2", "nodeify": "1.0.1", - "npm": "6.14.5", + "npm": "6.14.8", "openapi-backend": "2.4.1", "rate-limiter-flexible": "^2.1.4", "rehype": "^10.0.0", @@ -67,7 +67,7 @@ "threads": "^1.4.0", "tiny-worker": "^2.3.0", "tinycon": "0.0.1", - "ueberdb2": "^0.5.3", + "ueberdb2": "^0.5.4", "underscore": "1.8.3", "unorm": "1.4.1" }, From 554eef77708fcc5b597d5864395bd4ba61dfde5e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 29 Aug 2020 20:28:08 -0400 Subject: [PATCH 063/315] webaccess: Exempt `/favicon.ico` and `/locales.json` from auth checks --- src/node/hooks/express/webaccess.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 9cb5f4570..822abc428 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -11,6 +11,15 @@ const cookieParser = require('cookie-parser'); hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead'; +const staticPathsRE = new RegExp('^/(' + [ + 'api/.*', + 'favicon\\.ico', + 'javascripts/.*', + 'locales\\.json', + 'pluginfw/.*', + 'static/.*', +].join('|') + ')$'); + exports.normalizeAuthzLevel = (level) => { if (!level) return false; switch (level) { @@ -108,8 +117,7 @@ exports.checkAccess = (req, res, next) => { httpLogger.error('Error in preAuthorize hook:', err); return res.status(500).send('Internal Server Error'); } - // Do not require auth for static paths and the API...this could be a bit brittle - if (req.path.match(/^\/(static|javascripts|pluginfw|api)/)) results.push(true); + if (req.path.match(staticPathsRE)) results.push(true); if (requireAdmin) { // Filter out all 'true' entries to prevent plugin authors from accidentally granting admin // privileges to the general public. From dbef630f44f76b63eb5343a06f8df1ef7e71b539 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 1 Oct 2020 05:15:27 -0400 Subject: [PATCH 064/315] i18n: Localize `/admin` pages (#4380) Not every string was localized: * `/admin/plugins` has some CSS magic to draw the tables of plugins differently on narrow (mobile) screens, and the l10n library we use does not support that particular magic. The strings that were not localized are "Name", "Description", "Version", and "Time". These strings are only stuck in English when the page is viewed on a narrow screen; normal desktop users will see translated strings. The CSS magic ought to be replaced with something more robust (lots of nested `div`s); those remaining strings can be localized whenever that happens. * Strings from external sources such as plugin descriptions, error messages, and `settings.json` comments are not localized. --- src/locales/en.json | 34 +++++++++++++++++++++ src/templates/admin/index.html | 11 ++++--- src/templates/admin/plugins-info.html | 27 +++++++++-------- src/templates/admin/plugins.html | 43 ++++++++++++++------------- src/templates/admin/settings.html | 22 +++++++------- 5 files changed, 91 insertions(+), 46 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index 4e9fb33de..ec861b772 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1,4 +1,38 @@ { + "admin.page-title": "Admin Dashboard - Etherpad", + "admin_plugins": "Plugin manager", + "admin_plugins.available": "Available plugins", + "admin_plugins.available_not-found": "No plugins found.", + "admin_plugins.available_fetching": "Fetching…", + "admin_plugins.available_install.value": "Install", + "admin_plugins.available_search.placeholder": "Search for plugins to install", + "admin_plugins.description": "Description", + "admin_plugins.installed": "Installed plugins", + "admin_plugins.installed_fetching": "Fetching installed plugins…", + "admin_plugins.installed_nothing": "You haven't installed any plugins yet.", + "admin_plugins.installed_uninstall.value": "Uninstall", + "admin_plugins.last-update": "Last update", + "admin_plugins.name": "Name", + "admin_plugins.page-title": "Plugin manager - Etherpad", + "admin_plugins.version": "Version", + "admin_plugins_info": "Troubleshooting information", + "admin_plugins_info.hooks": "Installed hooks", + "admin_plugins_info.hooks_client": "Client-side hooks", + "admin_plugins_info.hooks_server": "Server-side hooks", + "admin_plugins_info.parts": "Installed parts", + "admin_plugins_info.plugins": "Installed plugins", + "admin_plugins_info.page-title": "Plugin information - Etherpad", + "admin_plugins_info.version": "Etherpad version", + "admin_plugins_info.version_latest": "Latest available version", + "admin_plugins_info.version_number": "Version number", + "admin_settings": "Settings", + "admin_settings.current": "Current configuration", + "admin_settings.current_example-devel": "Example development settings template", + "admin_settings.current_example-prod": "Example production settings template", + "admin_settings.current_restart.value": "Restart Etherpad", + "admin_settings.current_save.value": "Save Settings", + "admin_settings.page-title": "Settings - Etherpad", + "index.newPad": "New Pad", "index.createOpenPad": "or create/open a Pad with the name:", "index.openPad": "open an existing Pad with the name:", diff --git a/src/templates/admin/index.html b/src/templates/admin/index.html index 01724f500..efed7f801 100644 --- a/src/templates/admin/index.html +++ b/src/templates/admin/index.html @@ -1,11 +1,14 @@ - Admin Dashboard - Etherpad + Admin Dashboard - Etherpad + + +
            @@ -13,9 +16,9 @@

            Etherpad

            diff --git a/src/templates/admin/plugins-info.html b/src/templates/admin/plugins-info.html index fe0f69f7b..b960ff32f 100644 --- a/src/templates/admin/plugins-info.html +++ b/src/templates/admin/plugins-info.html @@ -4,9 +4,12 @@ - Plugin information - Etherpad + Plugin information - Etherpad + + +
            @@ -14,30 +17,30 @@

            Etherpad

            -

            Etherpad version

            -

            Version number: <%= epVersion %>

            -

            Latest available version: <%= latestVersion %>

            +

            Etherpad version

            +

            Version number: <%= epVersion %>

            +

            Latest available version: <%= latestVersion %>

            Git sha: <%= gitCommit %>

            -

            Installed plugins

            +

            Installed plugins

            <%- plugins.formatPlugins().replace(/, /g,"\n") %>
            -

            Installed parts

            +

            Installed parts

            <%= plugins.formatParts() %>
            -

            Installed hooks

            -

            Server side hooks

            +

            Installed hooks

            +

            Server-side hooks

            <%- plugins.formatHooks() %>
            -

            Client side hooks

            +

            Client-side hooks

            <%- plugins.formatHooks("client_hooks") %>
            diff --git a/src/templates/admin/plugins.html b/src/templates/admin/plugins.html index 0fff78437..204e74735 100644 --- a/src/templates/admin/plugins.html +++ b/src/templates/admin/plugins.html @@ -1,12 +1,15 @@ - Plugin manager - Etherpad + Plugin manager - Etherpad + + +
            @@ -23,21 +26,21 @@

            Etherpad

            -

            Installed plugins

            +

            Installed plugins

            - - - + + + @@ -48,7 +51,7 @@ @@ -58,8 +61,8 @@
            NameDescriptionVersionNameDescriptionVersion
            - +

            -

            You haven't installed any plugins yet.

            -


            Fetching installed plugins...

            +

            You haven't installed any plugins yet.

            +


            Fetching installed plugins…

            @@ -67,18 +70,18 @@
            -

            Available plugins

            +

            Available plugins

            - +
            - - - - + + + + @@ -90,7 +93,7 @@ @@ -102,8 +105,8 @@ diff --git a/src/templates/admin/settings.html b/src/templates/admin/settings.html index 74f35521a..74e259ffb 100644 --- a/src/templates/admin/settings.html +++ b/src/templates/admin/settings.html @@ -1,7 +1,7 @@ - Settings - Etherpad + Settings - Etherpad @@ -9,7 +9,9 @@ - + + +
            @@ -27,22 +29,22 @@

            Etherpad

            From ceb09ce99ad080ae804d322804b22e3e8f0be005 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Thu, 1 Oct 2020 11:39:01 +0200 Subject: [PATCH 065/315] security: Support proxy with rate limiting and include CI test coverage for nginx rev proxy (#4373) Previously Etherpad would not pass the correct client IP address through and this caused the rate limiter to limit users behind reverse proxies. This change allows Etherpad to use a client IP passed from a reverse proxy. Note to devs: This header can be spoofed and spoofing the header could be used in an attack. To mitigate additional *steps should be taken by Etherpad site admins IE doing rate limiting at proxy.* This only really applies to large scale deployments but it's worth noting. --- .travis.yml | 12 +++++++++ src/node/handler/PadMessageHandler.js | 5 ++-- src/package-lock.json | 10 ++++++++ src/package.json | 3 ++- tests/ratelimit/Dockerfile.anotherip | 4 +++ tests/ratelimit/Dockerfile.nginx | 2 ++ tests/ratelimit/nginx.conf | 26 +++++++++++++++++++ tests/ratelimit/send_changesets.js | 25 +++++++++++++++++++ tests/ratelimit/testlimits.sh | 36 +++++++++++++++++++++++++++ 9 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 tests/ratelimit/Dockerfile.anotherip create mode 100644 tests/ratelimit/Dockerfile.nginx create mode 100644 tests/ratelimit/nginx.conf create mode 100644 tests/ratelimit/send_changesets.js create mode 100644 tests/ratelimit/testlimits.sh diff --git a/.travis.yml b/.travis.yml index cf7307501..2fab80ceb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -99,6 +99,18 @@ jobs: - "npm install -g etherpad-load-test" script: - "tests/frontend/travis/runnerLoadTest.sh" + - name: "Test rate limit" + install: + - "docker network create --subnet=172.23.42.0/16 ep_net" + - "docker build -f Dockerfile -t epl-debian-slim ." + - "docker build -f tests/ratelimit/Dockerfile.nginx -t nginx-latest ." + - "docker build -f tests/ratelimit/Dockerfile.anotherip -t anotherip ." + - "docker run -p 8081:80 --rm --network ep_net --ip 172.23.42.1 -d nginx-latest" + - "docker run --name etherpad-docker -p 9000:9001 --rm --network ep_net --ip 172.23.42.2 -e 'TRUST_PROXY=true' epl-debian-slim &" + - "docker run --rm --network ep_net --ip 172.23.42.3 --name anotherip -dt anotherip" + - "./bin/installDeps.sh" + script: + - "cd tests/ratelimit && bash testlimits.sh" notifications: irc: diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 1094fe837..9e0e349ce 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -181,10 +181,11 @@ exports.handleMessage = async function(client, message) var env = process.env.NODE_ENV || 'development'; if (env === 'production') { + const clientIPAddress = remoteAddress[client.id]; try { - await rateLimiter.consume(client.handshake.address); // consume 1 point per event from IP + await rateLimiter.consume(clientIPAddress); // consume 1 point per event from IP }catch(e){ - console.warn("Rate limited: ", client.handshake.address, " to reduce the amount of rate limiting that happens edit the rateLimit values in settings.json"); + console.warn("Rate limited: ", clientIPAddress, " to reduce the amount of rate limiting that happens edit the rateLimit values in settings.json"); stats.meter('rateLimited').mark(); client.json.send({disconnect:"rateLimited"}); return; diff --git a/src/package-lock.json b/src/package-lock.json index 59f124154..356b7455e 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1810,6 +1810,16 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "etherpad-cli-client": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/etherpad-cli-client/-/etherpad-cli-client-0.0.9.tgz", + "integrity": "sha1-A+5+fNzA4EZLTu/djn7gzwUaVDs=", + "dev": true, + "requires": { + "async": "*", + "socket.io-client": "*" + } + }, "etherpad-require-kernel": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/etherpad-require-kernel/-/etherpad-require-kernel-1.0.9.tgz", diff --git a/src/package.json b/src/package.json index 24eeb53de..18f42c395 100644 --- a/src/package.json +++ b/src/package.json @@ -81,7 +81,8 @@ "set-cookie-parser": "^2.4.6", "superagent": "^3.8.3", "supertest": "4.0.2", - "wd": "1.12.1" + "wd": "1.12.1", + "etherpad-cli-client": "0.0.9" }, "engines": { "node": ">=10.13.0", diff --git a/tests/ratelimit/Dockerfile.anotherip b/tests/ratelimit/Dockerfile.anotherip new file mode 100644 index 000000000..5b9d1d21a --- /dev/null +++ b/tests/ratelimit/Dockerfile.anotherip @@ -0,0 +1,4 @@ +FROM node:alpine3.12 +WORKDIR /tmp +RUN npm i etherpad-cli-client +COPY ./tests/ratelimit/send_changesets.js /tmp/send_changesets.js diff --git a/tests/ratelimit/Dockerfile.nginx b/tests/ratelimit/Dockerfile.nginx new file mode 100644 index 000000000..ba8dd358f --- /dev/null +++ b/tests/ratelimit/Dockerfile.nginx @@ -0,0 +1,2 @@ +FROM nginx +COPY ./tests/ratelimit/nginx.conf /etc/nginx/nginx.conf diff --git a/tests/ratelimit/nginx.conf b/tests/ratelimit/nginx.conf new file mode 100644 index 000000000..97f0a9e00 --- /dev/null +++ b/tests/ratelimit/nginx.conf @@ -0,0 +1,26 @@ +events {} +http { + server { + access_log /dev/fd/1; + error_log /dev/fd/2; + location / { + proxy_pass http://172.23.42.2:9001/; + proxy_set_header Host $host; + proxy_pass_header Server; + # be careful, this line doesn't override any proxy_buffering on set in a conf.d/file.conf + proxy_buffering off; + proxy_set_header X-Real-IP $remote_addr; # http://wiki.nginx.org/HttpProxyModule + proxy_set_header X-Forwarded-For $remote_addr; # EP logs to show the actual remote IP + proxy_set_header X-Forwarded-Proto $scheme; # for EP to set secure cookie flag when https is used + proxy_set_header Host $host; # pass the host header + proxy_http_version 1.1; # recommended with keepalive connections + # WebSocket proxying - from http://nginx.org/en/docs/http/websocket.html + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + } + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } +} diff --git a/tests/ratelimit/send_changesets.js b/tests/ratelimit/send_changesets.js new file mode 100644 index 000000000..cf0377cae --- /dev/null +++ b/tests/ratelimit/send_changesets.js @@ -0,0 +1,25 @@ +try{ + var etherpad = require("../../src/node_modules/etherpad-cli-client"); + //ugly +} catch { + var etherpad = require("etherpad-cli-client") +} +var pad = etherpad.connect(process.argv[2]); +pad.on("connected", function(){ + + setTimeout(function(){ + setInterval(function(){ + pad.append("1"); + }, process.argv[3]); + },500); // wait because CLIENT_READY message is included in ratelimit + + setTimeout(function(){ + process.exit(0); + },11000) +}); +// in case of disconnect exit code 1 +pad.on("message", function(message){ + if(message.disconnect == 'rateLimited'){ + process.exit(1); + } +}) diff --git a/tests/ratelimit/testlimits.sh b/tests/ratelimit/testlimits.sh new file mode 100644 index 000000000..778348dcc --- /dev/null +++ b/tests/ratelimit/testlimits.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +#sending changesets every 101ms should not trigger ratelimit +node send_changesets.js http://127.0.0.1:8081/p/BACKEND_TEST_ratelimit_101ms 101 +if [[ $? -ne 0 ]];then + echo "FAILED: ratelimit was triggered when sending every 101 ms" + exit 1 +fi + +#sending changesets every 99ms should trigger ratelimit +node send_changesets.js http://127.0.0.1:8081/p/BACKEND_TEST_ratelimit_99ms 99 +if [[ $? -ne 1 ]];then + echo "FAILED: ratelimit was not triggered when sending every 99 ms" + exit 1 +fi + +#sending changesets every 101ms via proxy +node send_changesets.js http://127.0.0.1:8081/p/BACKEND_TEST_ratelimit_101ms 101 & +pid1=$! + +#sending changesets every 101ms via second IP and proxy +docker exec anotherip node /tmp/send_changesets.js http://172.23.42.1:80/p/BACKEND_TEST_ratelimit_101ms_via_second_ip 101 & +pid2=$! + +wait $pid1 +exit1=$? +wait $pid2 +exit2=$? + +echo "101ms with proxy returned with ${exit1}" +echo "101ms via another ip returned with ${exit2}" + +if [[ $exit1 -eq 1 || $exit2 -eq 1 ]];then + echo "FAILED: ratelimit was triggered during proxy and requests via second ip" + exit 1 +fi From 891d2600fa4659d6183bb3ac524b88e504835ff3 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Fri, 2 Oct 2020 09:05:33 +0200 Subject: [PATCH 066/315] Localisation updates from https://translatewiki.net. --- src/locales/ar.json | 5 +++++ src/locales/da.json | 2 ++ src/locales/de.json | 16 ++++++++++++++++ src/locales/es.json | 32 ++++++++++++++++++++++++++++++++ src/locales/hy.json | 1 + src/locales/ko.json | 31 +++++++++++++++++++++++++++++++ src/locales/mk.json | 33 +++++++++++++++++++++++++++++++++ src/locales/pt-br.json | 3 +++ src/locales/tr.json | 33 +++++++++++++++++++++++++++++++++ 9 files changed, 156 insertions(+) diff --git a/src/locales/ar.json b/src/locales/ar.json index 6d4f3a1d3..c9e3ff909 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -14,6 +14,11 @@ "محمد أحمد عبد الفتاح" ] }, + "admin_plugins.description": "الوصف", + "admin_plugins.name": "الاسم", + "admin_plugins.version": "الإصدار", + "admin_plugins_info.version_number": "رقم الإصدار", + "admin_settings": "إعدادات", "index.newPad": "باد جديد", "index.createOpenPad": "أو صنع/فتح باد بوضع اسمه:", "index.openPad": "افتح باد موجودة بالاسم:", diff --git a/src/locales/da.json b/src/locales/da.json index dbea05471..f948f6612 100644 --- a/src/locales/da.json +++ b/src/locales/da.json @@ -8,6 +8,8 @@ "Steenth" ] }, + "admin_plugins.description": "Beskrivelse", + "admin_plugins.name": "Navn", "index.newPad": "Ny Pad", "index.createOpenPad": "eller opret/åbn en Pad med navnet:", "pad.toolbar.bold.title": "Fed (Ctrl-B)", diff --git a/src/locales/de.json b/src/locales/de.json index 7be360596..79f7d8dfa 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -3,6 +3,7 @@ "authors": [ "Bjarncraft", "Dom", + "Killarnee", "Metalhead64", "Mklehr", "Nipsky", @@ -13,6 +14,21 @@ "Wikinaut" ] }, + "admin_plugins": "Plugins verwalten", + "admin_plugins.available": "Verfügbare Plugins", + "admin_plugins.available_not-found": "Keine Plugins gefunden.", + "admin_plugins.available_install.value": "Installieren", + "admin_plugins.description": "Beschreibung", + "admin_plugins.installed_nothing": "Du hast bisher noch keine Plugins installiert.", + "admin_plugins.last-update": "Letze Aktualisierung", + "admin_plugins.name": "Name", + "admin_plugins.version": "Version", + "admin_plugins_info.hooks": "Installierte Hooks", + "admin_plugins_info.plugins": "Installierte Plugins", + "admin_plugins_info.version_number": "Versionsnummer", + "admin_settings": "Einstellungen", + "admin_settings.current": "Derzeitige Konfiguration", + "admin_settings.current_save.value": "Einstellungen speichern", "index.newPad": "Neues Pad", "index.createOpenPad": "oder ein Pad mit folgendem Namen erstellen/öffnen:", "index.openPad": "Öffne ein vorhandenes Pad mit folgendem Namen:", diff --git a/src/locales/es.json b/src/locales/es.json index 8066ad326..406cb0082 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -18,6 +18,38 @@ "Xuacu" ] }, + "admin.page-title": "Panel administrativo. Etherpad", + "admin_plugins": "Gestor de complementos", + "admin_plugins.available": "Complementos disponibles", + "admin_plugins.available_not-found": "No se encontró ningún complemento.", + "admin_plugins.available_fetching": "Recuperando…", + "admin_plugins.available_install.value": "Instalar", + "admin_plugins.available_search.placeholder": "Buscar complementos para instalar", + "admin_plugins.description": "Descripción", + "admin_plugins.installed": "Complementos instalados", + "admin_plugins.installed_fetching": "Recuperando los complementos instalados…", + "admin_plugins.installed_nothing": "No se ha instalado ningún complemento aún.", + "admin_plugins.installed_uninstall.value": "Desinstalar", + "admin_plugins.last-update": "Última actualización", + "admin_plugins.name": "Nombre", + "admin_plugins.page-title": "Gestor de complementos. Etherpad", + "admin_plugins.version": "Versión", + "admin_plugins_info": "Información para solucionar problemas", + "admin_plugins_info.hooks": "Actuadores instalados", + "admin_plugins_info.hooks_client": "Actuadores del lado cliente", + "admin_plugins_info.hooks_server": "Actuadores del lado servidor", + "admin_plugins_info.parts": "Partes instaladas", + "admin_plugins_info.plugins": "Complementos instalados", + "admin_plugins_info.page-title": "Información del complemento. Etherpad", + "admin_plugins_info.version": "Versión de Etherpad", + "admin_plugins_info.version_latest": "Versión más reciente disponible", + "admin_plugins_info.version_number": "Número de versión", + "admin_settings": "Configuración", + "admin_settings.current": "Configuración actual", + "admin_settings.current_example-devel": "Plantilla de ejemplo de configuración de desarrollo", + "admin_settings.current_restart.value": "Reiniciar Etherpad", + "admin_settings.current_save.value": "Guardar configuración", + "admin_settings.page-title": "Configuración. Etherpad", "index.newPad": "Nuevo pad", "index.createOpenPad": "o crea/abre un pad con el nombre:", "pad.toolbar.bold.title": "Negrita (Ctrl-B)", diff --git a/src/locales/hy.json b/src/locales/hy.json index 7eaa58a48..7ee1481a5 100644 --- a/src/locales/hy.json +++ b/src/locales/hy.json @@ -4,6 +4,7 @@ "Kareyac" ] }, + "index.newPad": "Ստեղծել", "pad.toolbar.underline.title": "ընդգծելով (Ctrl-U)", "pad.toolbar.undo.title": "Չեղարկել (Ctrl-Z)", "pad.toolbar.redo.title": "Վերադարձնել (Ctrl-Y)", diff --git a/src/locales/ko.json b/src/locales/ko.json index b6e9978ad..f2214ee47 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -12,6 +12,37 @@ "아라" ] }, + "admin.page-title": "관리 대시보드 - 이더패드", + "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": "플러그인 관리자 - 이더패드", + "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": "플러그인 정보 - 이더패드", + "admin_plugins_info.version": "이더패드 버전", + "admin_plugins_info.version_latest": "사용 가능한 최신 버전", + "admin_plugins_info.version_number": "버전 번호", + "admin_settings": "설정", + "admin_settings.current": "현재 구성", + "admin_settings.current_restart.value": "이더패드 다시 시작", + "admin_settings.current_save.value": "설정 저장", + "admin_settings.page-title": "설정 - 이더패드", "index.newPad": "새 패드", "index.createOpenPad": "또는 다음 이름으로 패드 만들기/열기:", "pad.toolbar.bold.title": "굵게 (Ctrl+B)", diff --git a/src/locales/mk.json b/src/locales/mk.json index 3f492c7d0..29cce35fa 100644 --- a/src/locales/mk.json +++ b/src/locales/mk.json @@ -6,6 +6,39 @@ "Vlad5250" ] }, + "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": "отвори постоечка тетратка наречена:", diff --git a/src/locales/pt-br.json b/src/locales/pt-br.json index a458de3d0..5af5eeaa2 100644 --- a/src/locales/pt-br.json +++ b/src/locales/pt-br.json @@ -20,6 +20,9 @@ "Webysther" ] }, + "admin_plugins.available_install.value": "Instalar", + "admin_plugins.installed_uninstall.value": "Desinstalar", + "admin_plugins.version": "Versão", "index.newPad": "Nova Nota", "index.createOpenPad": "ou criar-abrir uma Nota com o nome:", "index.openPad": "abra um bloco existente com o nome:", diff --git a/src/locales/tr.json b/src/locales/tr.json index 7b5dfac0a..b11146406 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -14,6 +14,39 @@ "Vito Genovese" ] }, + "admin.page-title": "Yönetim Panosu - Etherpad", + "admin_plugins": "Eklenti yöneticisi", + "admin_plugins.available": "Mevcut eklentiler", + "admin_plugins.available_not-found": "Eklenti bulunamadı.", + "admin_plugins.available_fetching": "Getiriliyor…", + "admin_plugins.available_install.value": "Yükle", + "admin_plugins.available_search.placeholder": "Yüklenecek eklentileri arayın", + "admin_plugins.description": "Açıklama", + "admin_plugins.installed": "Yüklü eklentiler", + "admin_plugins.installed_fetching": "Yüklü eklentiler alınıyor…", + "admin_plugins.installed_nothing": "Henüz herhangi bir eklenti yüklemediniz.", + "admin_plugins.installed_uninstall.value": "Kaldır", + "admin_plugins.last-update": "Son güncelleme", + "admin_plugins.name": "Ad", + "admin_plugins.page-title": "Eklenti yöneticisi - Etherpad", + "admin_plugins.version": "Sürüm", + "admin_plugins_info": "Sorun giderme bilgisi", + "admin_plugins_info.hooks": "Yüklü kancalar", + "admin_plugins_info.hooks_client": "İstemci taraf kancaları", + "admin_plugins_info.hooks_server": "Sunucu taraf kancaları", + "admin_plugins_info.parts": "Yüklü parçalar", + "admin_plugins_info.plugins": "Yüklü eklentiler", + "admin_plugins_info.page-title": "Eklenti bilgisi - Etherpad", + "admin_plugins_info.version": "Etherpad sürümü", + "admin_plugins_info.version_latest": "Mevcut en son sürümü", + "admin_plugins_info.version_number": "Sürüm numarası", + "admin_settings": "Ayarlar", + "admin_settings.current": "Geçerli yapılandırma", + "admin_settings.current_example-devel": "Örnek geliştirme ayarları şablonu", + "admin_settings.current_example-prod": "Örnek üretim ayarları şablonu", + "admin_settings.current_restart.value": "Etherpad'ı sıfırla", + "admin_settings.current_save.value": "Ayarları Kaydet", + "admin_settings.page-title": "Ayarlar - Etherpad", "index.newPad": "Yeni Bloknot", "index.createOpenPad": "veya şu adla bir Bloknot oluşturun/açın:", "index.openPad": "şu adla varolan bir Pad'i açın:", From d55edebddd6db2e46d3d2290ba4c7bcef08c18a7 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 2 Oct 2020 23:46:41 -0400 Subject: [PATCH 067/315] cookies: Refactor `pad_cookie.js` * Use the cookie functions from `pad_utils.js`. * Delete unused methods, variables, and parameters. * Simplify the logic. * Use an ES6 class instead of a weird literal thingy. * Use `const` instead of `var`. --- src/static/js/pad_cookie.js | 155 ++++++++---------------------------- 1 file changed, 35 insertions(+), 120 deletions(-) diff --git a/src/static/js/pad_cookie.js b/src/static/js/pad_cookie.js index 2e0b70950..0075691f6 100644 --- a/src/static/js/pad_cookie.js +++ b/src/static/js/pad_cookie.js @@ -1,9 +1,3 @@ -/** - * 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. - * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED - */ - /** * Copyright 2009 Google Inc. * @@ -20,130 +14,51 @@ * limitations under the License. */ +const createCookie = require('./pad_utils').createCookie; +const readCookie = require('./pad_utils').readCookie; -var padcookie = (function() -{ - var cookieName = isHttpsScheme() ? "prefs" : "prefsHttp"; - - function getRawCookie() - { - // returns null if can't get cookie text - if (!document.cookie) - { - return null; - } - // look for (start of string OR semicolon) followed by whitespace followed by prefs=(something); - var regexResult = document.cookie.match(new RegExp("(?:^|;)\\s*" + cookieName + "=([^;]*)(?:;|$)")); - if ((!regexResult) || (!regexResult[1])) - { - return null; - } - return regexResult[1]; +exports.padcookie = new class { + constructor() { + this.cookieName_ = window.location.protocol === 'https:' ? 'prefs' : 'prefsHttp'; + const prefs = this.readPrefs_() || {}; + delete prefs.userId; + delete prefs.name; + delete prefs.colorId; + this.prefs_ = prefs; + this.savePrefs_(); } - function setRawCookie(safeText) - { - var expiresDate = new Date(); - expiresDate.setFullYear(3000); - var secure = isHttpsScheme() ? ";secure" : ""; - var sameSite = isHttpsScheme() ? ";sameSite=Strict": ";sameSite=Lax"; - document.cookie = (cookieName + "=" + safeText + ";expires=" + expiresDate.toGMTString() + secure + sameSite); - } - - function parseCookie(text) - { - // returns null if can't parse cookie. - try - { - var cookieData = JSON.parse(unescape(text)); - return cookieData; - } - catch (e) - { - return null; - } - } - - function stringifyCookie(data) - { - return escape(JSON.stringify(data)); - } - - function saveCookie() - { - if (!inited) - { - return; - } - setRawCookie(stringifyCookie(cookieData)); - - if ((!getRawCookie()) && (!alreadyWarnedAboutNoCookies)) - { + init() { + if (this.readPrefs_() == null) { $.gritter.add({ - title: "Error", - text: html10n.get("pad.noCookie"), + title: 'Error', + text: html10n.get('pad.noCookie'), sticky: true, - class_name: "error" + class_name: 'error', }); - alreadyWarnedAboutNoCookies = true; } } - function isHttpsScheme() { - return window.location.protocol == "https:"; + readPrefs_() { + const jsonEsc = readCookie(this.cookieName_); + if (jsonEsc == null) return null; + try { + return JSON.parse(unescape(jsonEsc)); + } catch (e) { + return null; + } } - var wasNoCookie = true; - var cookieData = {}; - var alreadyWarnedAboutNoCookies = false; - var inited = false; + savePrefs_() { + createCookie(this.cookieName_, escape(JSON.stringify(this.prefs_)), 365 * 100); + } - var pad = undefined; - var self = { - init: function(prefsToSet, _pad) - { - pad = _pad; + getPref(prefName) { + return this.prefs_[prefName]; + } - var rawCookie = getRawCookie(); - if (rawCookie) - { - var cookieObj = parseCookie(rawCookie); - if (cookieObj) - { - wasNoCookie = false; // there was a cookie - delete cookieObj.userId; - delete cookieObj.name; - delete cookieObj.colorId; - cookieData = cookieObj; - } - } - - for (var k in prefsToSet) - { - cookieData[k] = prefsToSet[k]; - } - - inited = true; - saveCookie(); - }, - wasNoCookie: function() - { - return wasNoCookie; - }, - isCookiesEnabled: function() { - return !!getRawCookie(); - }, - getPref: function(prefName) - { - return cookieData[prefName]; - }, - setPref: function(prefName, value) - { - cookieData[prefName] = value; - saveCookie(); - } - }; - return self; -}()); - -exports.padcookie = padcookie; + setPref(prefName, value) { + this.prefs_[prefName] = value; + this.savePrefs_(); + } +}(); From 3ab0f30ac84e0e9a66a172e92d9dedbaed2dd8b3 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 2 Oct 2020 18:43:12 -0400 Subject: [PATCH 068/315] cookies: Use js-cookie to read and write cookies Rather than reinvent the wheel, use a well-tested library to parse and write cookies. This should also help prevent XSS vulnerabilities because the library handles special characters such as semicolon. --- src/node/utils/Minify.js | 13 +++++----- src/node/utils/tar.json | 1 + src/package-lock.json | 5 ++++ src/package.json | 1 + src/static/js/pad.js | 34 ++++++++++-------------- src/static/js/pad_cookie.js | 11 ++++---- src/static/js/pad_editor.js | 3 ++- src/static/js/pad_utils.js | 52 +++++-------------------------------- src/static/js/timeslider.js | 30 ++++++++++----------- 9 files changed, 54 insertions(+), 96 deletions(-) diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index e94d34b04..750aac007 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -44,12 +44,13 @@ var threadsPool = Threads.Pool(function () { }, 2) var LIBRARY_WHITELIST = [ - 'async' - , 'security' - , 'tinycon' - , 'underscore' - , 'unorm' - ]; + 'async', + 'js-cookie', + 'security', + 'tinycon', + 'underscore', + 'unorm', +]; // Rewrite tar to include modules with no extensions and proper rooted paths. var LIBRARY_PREFIX = 'ep_etherpad-lite/static/js'; diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json index 80d645dd0..7d0913425 100644 --- a/src/node/utils/tar.json +++ b/src/node/utils/tar.json @@ -17,6 +17,7 @@ , "pad_connectionstatus.js" , "chat.js" , "gritter.js" + , "$js-cookie/src/js.cookie.js" , "$tinycon/tinycon.js" , "excanvas.js" , "farbtastic.js" diff --git a/src/package-lock.json b/src/package-lock.json index 356b7455e..60ec0887f 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -2656,6 +2656,11 @@ "istanbul-lib-report": "^3.0.0" } }, + "js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/src/package.json b/src/package.json index 18f42c395..155638161 100644 --- a/src/package.json +++ b/src/package.json @@ -46,6 +46,7 @@ "formidable": "1.2.1", "graceful-fs": "4.2.4", "http-errors": "1.7.3", + "js-cookie": "^2.2.1", "jsonminify": "0.4.1", "languages4translatewiki": "0.1.3", "lodash.clonedeep": "4.5.0", diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 37bc0e009..416d7897e 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -30,6 +30,7 @@ require('./jquery'); require('./farbtastic'); require('./excanvas'); +const Cookies = require('./pad_utils').Cookies; var chat = require('./chat').chat; var getCollabClient = require('./collab_client').getCollabClient; var padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus; @@ -42,8 +43,6 @@ var padsavedrevs = require('./pad_savedrevs'); var paduserlist = require('./pad_userlist').paduserlist; var padutils = require('./pad_utils').padutils; var colorutils = require('./colorutils').colorutils; -var createCookie = require('./pad_utils').createCookie; -var readCookie = require('./pad_utils').readCookie; var randomString = require('./pad_utils').randomString; var gritter = require('./gritter').gritter; @@ -83,7 +82,7 @@ var getParameters = [ { name: "rtl", checkVal: "true", callback: function(val) { settings.rtlIsTrue = true } }, { name: "alwaysShowChat", checkVal: "true", callback: function(val) { if(!settings.hideChat) chat.stickToScreen(); } }, { name: "chatAndUsers", checkVal: "true", callback: function(val) { chat.chatAndUsers(); } }, - { name: "lang", checkVal: null, callback: function(val) { window.html10n.localize([val, 'en']); createCookie('language', val); } } + { name: "lang", checkVal: null, callback: function(val) { window.html10n.localize([val, 'en']); Cookies.set('language', val); } }, ]; function getParams() @@ -130,7 +129,7 @@ function getUrlVars() function savePassword() { //set the password cookie - createCookie("password",$("#passwordinput").val(),null,document.location.pathname); + Cookies.set('password', $('#passwordinput').val(), {path: document.location.pathname}); //reload document.location=document.location; return false; @@ -149,25 +148,21 @@ function sendClientReady(isReconnect, messageType) document.title = padId.replace(/_+/g, ' ') + " | " + title; } - var token = readCookie("token"); + let token = Cookies.get('token'); if (token == null) { token = "t." + randomString(); - createCookie("token", token, 60); + Cookies.set('token', token, {expires: 60}); } - var encodedSessionID = readCookie('sessionID'); - var sessionID = encodedSessionID == null ? null : decodeURIComponent(encodedSessionID); - var password = readCookie("password"); - - var msg = { - "component": "pad", - "type": messageType, - "padId": padId, - "sessionID": sessionID, - "password": password, - "token": token, - "protocolVersion": 2 + const msg = { + component: 'pad', + type: messageType, + padId: padId, + sessionID: Cookies.get('sessionID'), + password: Cookies.get('password'), + token: token, + protocolVersion: 2 }; // this is a reconnect, lets tell the server our revisionnumber @@ -456,7 +451,6 @@ var pad = { { pad.collabClient.sendClientMessage(msg); }, - createCookie: createCookie, init: function() { @@ -957,8 +951,6 @@ var settings = { pad.settings = settings; exports.baseURL = ''; exports.settings = settings; -exports.createCookie = createCookie; -exports.readCookie = readCookie; exports.randomString = randomString; exports.getParams = getParams; exports.getUrlVars = getUrlVars; diff --git a/src/static/js/pad_cookie.js b/src/static/js/pad_cookie.js index 0075691f6..f3dedf03c 100644 --- a/src/static/js/pad_cookie.js +++ b/src/static/js/pad_cookie.js @@ -14,8 +14,7 @@ * limitations under the License. */ -const createCookie = require('./pad_utils').createCookie; -const readCookie = require('./pad_utils').readCookie; +const Cookies = require('./pad_utils').Cookies; exports.padcookie = new class { constructor() { @@ -40,17 +39,17 @@ exports.padcookie = new class { } readPrefs_() { - const jsonEsc = readCookie(this.cookieName_); - if (jsonEsc == null) return null; try { - return JSON.parse(unescape(jsonEsc)); + const json = Cookies.get(this.cookieName_); + if (json == null) return null; + return JSON.parse(json); } catch (e) { return null; } } savePrefs_() { - createCookie(this.cookieName_, escape(JSON.stringify(this.prefs_)), 365 * 100); + Cookies.set(this.cookieName_, JSON.stringify(this.prefs_), {expires: 365 * 100}); } getPref(prefName) { diff --git a/src/static/js/pad_editor.js b/src/static/js/pad_editor.js index 6c53e4014..edd394064 100644 --- a/src/static/js/pad_editor.js +++ b/src/static/js/pad_editor.js @@ -20,6 +20,7 @@ * limitations under the License. */ +const Cookies = require('./pad_utils').Cookies; var padcookie = require('./pad_cookie').padcookie; var padutils = require('./pad_utils').padutils; @@ -108,7 +109,7 @@ var padeditor = (function() }) $("#languagemenu").val(html10n.getLanguage()); $("#languagemenu").change(function() { - pad.createCookie("language",$("#languagemenu").val(),null,'/'); + Cookies.set('language', $('#languagemenu').val()); window.html10n.localize([$("#languagemenu").val(), 'en']); }); }, diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index a7feb59ae..e9bc58cb4 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -39,49 +39,6 @@ function randomString(len) return randomstring; } -function createCookie(name, value, days, path){ /* Used by IE */ - if (days) - { - var date = new Date(); - date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); - var expires = "; expires=" + date.toGMTString(); - } - else{ - var expires = ""; - } - - if(!path){ // IF the Path of the cookie isn't set then just create it on root - path = "/"; - } - - //Check if we accessed the pad over https - var secure = window.location.protocol == "https:" ? ";secure" : ""; - var isHttpsScheme = window.location.protocol === "https:"; - var sameSite = isHttpsScheme ? ";sameSite=Strict": ";sameSite=Lax"; - - //Check if the browser is IE and if so make sure the full path is set in the cookie - if((navigator.appName == 'Microsoft Internet Explorer') || ((navigator.appName == 'Netscape') && (new RegExp("Trident/.*rv:([0-9]{1,}[\.0-9]{0,})").exec(navigator.userAgent) != null))){ - document.cookie = name + "=" + value + expires + "; path=/" + secure + sameSite; /* Note this bodge fix for IE is temporary until auth is rewritten */ - } - else{ - document.cookie = name + "=" + value + expires + "; path=" + path + secure + sameSite; - } - -} - -function readCookie(name) -{ - var nameEQ = name + "="; - var ca = document.cookie.split(';'); - for (var i = 0; i < ca.length; i++) - { - var c = ca[i]; - while (c.charAt(0) == ' ') c = c.substring(1, c.length); - if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); - } - return null; -} - var padutils = { escapeHtml: function(x) { @@ -571,7 +528,12 @@ padutils.setupGlobalExceptionHandler = setupGlobalExceptionHandler; padutils.binarySearch = require('./ace2_common').binarySearch; +// This file is included from Node so that it can reuse randomString, but Node doesn't have a global +// window object. +if (typeof window !== 'undefined') { + exports.Cookies = require('js-cookie/src/js.cookie'); + exports.Cookies.defaults.sameSite = window.location.protocol === 'https:' ? 'Strict' : 'Lax'; + exports.Cookies.defaults.secure = window.location.protocol === 'https:'; +} exports.randomString = randomString; -exports.createCookie = createCookie; -exports.readCookie = readCookie; exports.padutils = padutils; diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js index d8d3e5964..2721bd0e5 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -24,8 +24,7 @@ // assigns to the global `$` and augments it with plugins. require('./jquery'); -var createCookie = require('./pad_utils').createCookie; -var readCookie = require('./pad_utils').readCookie; +const Cookies = require('./pad_utils').Cookies; var randomString = require('./pad_utils').randomString; var hooks = require('./pluginfw/hooks'); @@ -45,11 +44,11 @@ function init() { document.title = padId.replace(/_+/g, ' ') + " | " + document.title; //ensure we have a token - token = readCookie("token"); + token = Cookies.get('token'); if(token == null) { token = "t." + randomString(); - createCookie("token", token, 60); + Cookies.set('token', token, {expires: 60}); } var loc = document.location; @@ -107,19 +106,16 @@ function init() { //sends a message over the socket function sendSocketMsg(type, data) { - var sessionID = decodeURIComponent(readCookie("sessionID")); - var password = readCookie("password"); - - var msg = { "component" : "pad", // FIXME: Remove this stupidity! - "type": type, - "data": data, - "padId": padId, - "token": token, - "sessionID": sessionID, - "password": password, - "protocolVersion": 2}; - - socket.json.send(msg); + socket.json.send({ + component: 'pad', // FIXME: Remove this stupidity! + type, + data, + padId, + token, + sessionID: Cookies.get('sessionID'), + password: Cookies.get('password'), + protocolVersion: 2, + }); } var fireWhenAllScriptsAreLoaded = []; From bf53162cdd33399fef9cae5ca0bf6378de113cfb Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 2 Oct 2020 22:32:44 -0400 Subject: [PATCH 069/315] cookies: Use `Lax` instead of `Strict` for `SameSite` --- src/node/hooks/express/webaccess.js | 11 +++-------- src/static/js/pad_utils.js | 4 +++- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 822abc428..b43542ddc 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -228,8 +228,6 @@ exports.expressConfigure = (hook_name, args, cb) => { exports.secret = settings.sessionKey; } - const sameSite = settings.ssl ? 'Strict' : 'Lax'; - args.app.sessionStore = exports.sessionStore; args.app.use(sessionModule({ secret: exports.secret, @@ -239,12 +237,9 @@ exports.expressConfigure = (hook_name, args, cb) => { name: 'express_sid', proxy: true, cookie: { - /* - * Firefox started enforcing sameSite, see https://github.com/ether/etherpad-lite/issues/3989 - * for details. In response we set it based on if SSL certs are set in Etherpad. Note that if - * You use Nginx or so for reverse proxy this may cause problems. Use Certificate pinning to remedy. - */ - sameSite: sameSite, + // `Strict` is not used because it has few security benefits but significant usability + // drawbacks vs. `Lax`. See https://stackoverflow.com/q/41841880 for discussion. + sameSite: 'Lax', /* * The automatic express-session mechanism for determining if the * application is being served over ssl is similar to the one used for diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index e9bc58cb4..9e22951e1 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -532,7 +532,9 @@ padutils.binarySearch = require('./ace2_common').binarySearch; // window object. if (typeof window !== 'undefined') { exports.Cookies = require('js-cookie/src/js.cookie'); - exports.Cookies.defaults.sameSite = window.location.protocol === 'https:' ? 'Strict' : 'Lax'; + // `Strict` is not used because it has few security benefits but significant usability drawbacks + // vs. `Lax`. See https://stackoverflow.com/q/41841880 for discussion. + exports.Cookies.defaults.sameSite = 'Lax'; exports.Cookies.defaults.secure = window.location.protocol === 'https:'; } exports.randomString = randomString; From 2db4b04af3fbb01d6c70ed012836fc30907ca015 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 2 Oct 2020 23:53:05 -0400 Subject: [PATCH 070/315] cookies: Use `SameSite=None` if in an iframe from another site --- settings.json.docker | 18 ++++++++++++++++++ settings.json.template | 18 ++++++++++++++++++ src/node/hooks/express/webaccess.js | 4 +--- src/node/utils/Settings.js | 18 ++++++++++++++++++ src/static/js/pad_utils.js | 17 ++++++++++++++++- 5 files changed, 71 insertions(+), 4 deletions(-) diff --git a/settings.json.docker b/settings.json.docker index 8bbdedb40..5e5d806fa 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -336,6 +336,24 @@ */ "trustProxy": "${TRUST_PROXY:false}", + /* + * Settings controlling the session cookie issued by Etherpad. + */ + "cookie": { + /* + * Value of the SameSite cookie property. "Lax" is recommended unless + * Etherpad will be embedded in an iframe from another site, in which case + * this must be set to "None". Note: "None" will not work (the browser will + * not send the cookie to Etherpad) unless https is used to access Etherpad + * (either directly or via a reverse proxy with "trustProxy" set to true). + * + * "Strict" is not recommended because it has few security benefits but + * significant usability drawbacks vs. "Lax". See + * https://stackoverflow.com/q/41841880 for discussion. + */ + "sameSite": "${COOKIE_SAME_SITE:Lax}" + }, + /* * Privacy: disable IP logging */ diff --git a/settings.json.template b/settings.json.template index 880310919..61d6db117 100644 --- a/settings.json.template +++ b/settings.json.template @@ -339,6 +339,24 @@ */ "trustProxy": false, + /* + * Settings controlling the session cookie issued by Etherpad. + */ + "cookie": { + /* + * Value of the SameSite cookie property. "Lax" is recommended unless + * Etherpad will be embedded in an iframe from another site, in which case + * this must be set to "None". Note: "None" will not work (the browser will + * not send the cookie to Etherpad) unless https is used to access Etherpad + * (either directly or via a reverse proxy with "trustProxy" set to true). + * + * "Strict" is not recommended because it has few security benefits but + * significant usability drawbacks vs. "Lax". See + * https://stackoverflow.com/q/41841880 for discussion. + */ + "sameSite": "Lax" + }, + /* * Privacy: disable IP logging */ diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index b43542ddc..744a6d316 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -237,9 +237,7 @@ exports.expressConfigure = (hook_name, args, cb) => { name: 'express_sid', proxy: true, cookie: { - // `Strict` is not used because it has few security benefits but significant usability - // drawbacks vs. `Lax`. See https://stackoverflow.com/q/41841880 for discussion. - sameSite: 'Lax', + sameSite: settings.cookie.sameSite, /* * The automatic express-session mechanism for determining if the * application is being served over ssl is similar to the one used for diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 6a03668c6..6f3ccea89 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -268,6 +268,24 @@ exports.sessionKey = false; */ exports.trustProxy = false; +/* + * Settings controlling the session cookie issued by Etherpad. + */ +exports.cookie = { + /* + * Value of the SameSite cookie property. "Lax" is recommended unless + * Etherpad will be embedded in an iframe from another site, in which case + * this must be set to "None". Note: "None" will not work (the browser will + * not send the cookie to Etherpad) unless https is used to access Etherpad + * (either directly or via a reverse proxy with "trustProxy" set to true). + * + * "Strict" is not recommended because it has few security benefits but + * significant usability drawbacks vs. "Lax". See + * https://stackoverflow.com/q/41841880 for discussion. + */ + sameSite: 'Lax', +}; + /* * This setting is used if you need authentication and/or * authorization. Note: /admin always requires authentication, and diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index 9e22951e1..c6620f077 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -528,13 +528,28 @@ padutils.setupGlobalExceptionHandler = setupGlobalExceptionHandler; padutils.binarySearch = require('./ace2_common').binarySearch; +// https://stackoverflow.com/a/42660748 +function inThirdPartyIframe() { + try { + return (!window.top.location.hostname); + } catch (e) { + return true; + } +} + // This file is included from Node so that it can reuse randomString, but Node doesn't have a global // window object. if (typeof window !== 'undefined') { exports.Cookies = require('js-cookie/src/js.cookie'); + // Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case + // use `SameSite=None`. For iframes from another site, only `None` has a chance of working + // because the cookies are third-party (not same-site). Many browsers/users block third-party + // cookies, but maybe blocked is better than definitely blocked (which would happen with `Lax` + // or `Strict`). Note: `None` will not work unless secure is true. + // // `Strict` is not used because it has few security benefits but significant usability drawbacks // vs. `Lax`. See https://stackoverflow.com/q/41841880 for discussion. - exports.Cookies.defaults.sameSite = 'Lax'; + exports.Cookies.defaults.sameSite = inThirdPartyIframe() ? 'None' : 'Lax'; exports.Cookies.defaults.secure = window.location.protocol === 'https:'; } exports.randomString = randomString; From 29ee63f2ba57f883c5b541c6da5d3b6f96e33351 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 5 Oct 2020 15:56:29 +0200 Subject: [PATCH 071/315] Localisation updates from https://translatewiki.net. --- src/locales/da.json | 2 ++ src/locales/diq.json | 33 +++++++++++++++++++++++++++++++++ src/locales/fr.json | 33 +++++++++++++++++++++++++++++++++ src/locales/hu.json | 21 +++++++++++++++++++++ src/locales/hy.json | 5 +++++ src/locales/lb.json | 3 +++ src/locales/pl.json | 4 ++++ src/locales/pms.json | 33 +++++++++++++++++++++++++++++++++ src/locales/pt-br.json | 11 +++++++++++ src/locales/qqq.json | 2 ++ src/locales/ru.json | 8 ++++++++ src/locales/sv.json | 30 ++++++++++++++++++++++++++++++ src/locales/zh-hant.json | 33 +++++++++++++++++++++++++++++++++ 13 files changed, 218 insertions(+) diff --git a/src/locales/da.json b/src/locales/da.json index f948f6612..8e3ebad5a 100644 --- a/src/locales/da.json +++ b/src/locales/da.json @@ -8,8 +8,10 @@ "Steenth" ] }, + "admin_plugins.available_fetching": "Henter...", "admin_plugins.description": "Beskrivelse", "admin_plugins.name": "Navn", + "admin_settings": "Indstillinger", "index.newPad": "Ny Pad", "index.createOpenPad": "eller opret/åbn en Pad med navnet:", "pad.toolbar.bold.title": "Fed (Ctrl-B)", diff --git a/src/locales/diq.json b/src/locales/diq.json index c21ffb62a..68ca9db0a 100644 --- a/src/locales/diq.json +++ b/src/locales/diq.json @@ -10,6 +10,39 @@ "Orbot707" ] }, + "admin.page-title": "Panoyê İdarekari - Etherpad", + "admin_plugins": "İdarekarê Dekerdeki", + "admin_plugins.available": "Mewcud Dekerdeki", + "admin_plugins.available_not-found": "Dekerdek nevineya", + "admin_plugins.available_fetching": "Aniyeno...", + "admin_plugins.available_install.value": "Bar ke", + "admin_plugins.available_search.placeholder": "Barbıyaye dekerdeka bıvinê", + "admin_plugins.description": "Şınasnayış", + "admin_plugins.installed": "Dekerdekê bariyayê", + "admin_plugins.installed_fetching": "Bariyayê dekerdeki gêriyenê...", + "admin_plugins.installed_nothing": "Heena şıma qet yew dekerdek bar nêkerdo", + "admin_plugins.installed_uninstall.value": "Wedarnê", + "admin_plugins.last-update": "Resnayışo Peyên", + "admin_plugins.name": "Name", + "admin_plugins.page-title": "İdarekarê dekerdeka - Etherpad", + "admin_plugins.version": "Versiyon", + "admin_plugins_info": "Melumatê xetay timari", + "admin_plugins_info.hooks": "Bariyaye qancey", + "admin_plugins_info.hooks_client": "Qancey kışta waşteri", + "admin_plugins_info.hooks_server": "Qancey kışta serveri", + "admin_plugins_info.parts": "Barbıyaye letey", + "admin_plugins_info.plugins": "Dekerdekê bariyayê", + "admin_plugins_info.page-title": "Melumatê dekerdeki - Etherpad", + "admin_plugins_info.version": "Versiyonê Etherpadi", + "admin_plugins_info.version_latest": "Mewcud versiyono peyên", + "admin_plugins_info.version_number": "Numrey versiyoni", + "admin_settings": "Eyari", + "admin_settings.current": "Konfigurasyono ravêrde", + "admin_settings.current_example-devel": "Şablonê emsalê ravêrberdışi eyari", + "admin_settings.current_example-prod": "Şablonê emsalê vıraştışê eyari", + "admin_settings.current_restart.value": "Etherpad'i reyna ake", + "admin_settings.current_save.value": "Eyaran qeyd ke", + "admin_settings.page-title": "Eyari - Etherpad", "index.newPad": "Bloknoto newe", "index.createOpenPad": "ya zi be nê nameyi ra yew bloknot vıraze/ake:", "index.openPad": "yew Padê biyayeyi be nê nameyi ra ake:", diff --git a/src/locales/fr.json b/src/locales/fr.json index e8c9bd910..fb2ef1d76 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -29,6 +29,39 @@ "Wladek92" ] }, + "admin.page-title": "Tableau de bord administrateur — Etherpad", + "admin_plugins": "Gestionnaire de compléments", + "admin_plugins.available": "Compléments disponibles", + "admin_plugins.available_not-found": "Aucun complément trouvé.", + "admin_plugins.available_fetching": "Récupération…", + "admin_plugins.available_install.value": "Installer", + "admin_plugins.available_search.placeholder": "Rechercher des compléments à installer", + "admin_plugins.description": "Description", + "admin_plugins.installed": "Compléments installés", + "admin_plugins.installed_fetching": "Récupération des compléments installés…", + "admin_plugins.installed_nothing": "Vous n’avez pas encore installé de complément.", + "admin_plugins.installed_uninstall.value": "Désinstaller", + "admin_plugins.last-update": "Dernière mise à jour", + "admin_plugins.name": "Nom", + "admin_plugins.page-title": "Gestionnaire de compléments — Etherpad", + "admin_plugins.version": "Version", + "admin_plugins_info": "Information de résolution de problème", + "admin_plugins_info.hooks": "Crochets installés", + "admin_plugins_info.hooks_client": "Crochets côté client", + "admin_plugins_info.hooks_server": "Crochets côté serveur", + "admin_plugins_info.parts": "Parties installées", + "admin_plugins_info.plugins": "Compléments installés", + "admin_plugins_info.page-title": "Information de complément — Etherpad", + "admin_plugins_info.version": "Version Etherpad", + "admin_plugins_info.version_latest": "Dernière version disponible", + "admin_plugins_info.version_number": "Numéro de version", + "admin_settings": "Paramètres", + "admin_settings.current": "Configuration actuelle", + "admin_settings.current_example-devel": "Exemple de modèle de paramètres de développement", + "admin_settings.current_example-prod": "Exemple de modèle de paramètres de production", + "admin_settings.current_restart.value": "Redémarrer Etherpad", + "admin_settings.current_save.value": "Enregistrer les paramètres", + "admin_settings.page-title": "Paramètres — Etherpad", "index.newPad": "Nouveau bloc-notes", "index.createOpenPad": "ou créer/ouvrir un bloc-notes intitulé :", "index.openPad": "ouvrir un Pad existant avec le nom :", diff --git a/src/locales/hu.json b/src/locales/hu.json index 324563116..141c951e7 100644 --- a/src/locales/hu.json +++ b/src/locales/hu.json @@ -5,6 +5,7 @@ "Bencemac", "Csega", "Dj", + "Hanna Tardos", "Misibacsi", "Notramo", "Ovari", @@ -12,6 +13,26 @@ "Tgr" ] }, + "admin_plugins": "Bővítménykezelő", + "admin_plugins.available": "Elérhető bővítmények", + "admin_plugins.available_not-found": "Nem található bővítmény.", + "admin_plugins.available_install.value": "Telepítés", + "admin_plugins.available_search.placeholder": "Telepíthető bővítmények keresése", + "admin_plugins.description": "Leírás", + "admin_plugins.installed": "Telepített bővítmények", + "admin_plugins.installed_nothing": "Még nem telepítettél bővítményeket.", + "admin_plugins.installed_uninstall.value": "Eltávolítás", + "admin_plugins.last-update": "Utolsó frissítés", + "admin_plugins.name": "Név", + "admin_plugins.page-title": "Bővítménykezelő - Etherpad", + "admin_plugins_info": "Hibaelhárításra vonatkozó információ", + "admin_plugins_info.plugins": "Telepített bővítmények", + "admin_plugins_info.page-title": "Információ bővítményről - Etherpad", + "admin_plugins_info.version_latest": "Legfrissebb elérhető verzió", + "admin_settings": "Beállítások", + "admin_settings.current": "Jelenlegi beállítások", + "admin_settings.current_restart.value": "Etherpad újraindítása", + "admin_settings.current_save.value": "Beállítások mentése", "index.newPad": "Új jegyzetfüzet", "index.createOpenPad": "vagy jegyzetfüzet létrehozása/megnyitása ezzel a névvel:", "index.openPad": "nyisson meg egy meglévő jegyzetfüzetet névvel:", diff --git a/src/locales/hy.json b/src/locales/hy.json index 7ee1481a5..c284d1be5 100644 --- a/src/locales/hy.json +++ b/src/locales/hy.json @@ -4,7 +4,12 @@ "Kareyac" ] }, + "admin_plugins.available_install.value": "Տեղադրել", + "admin_plugins.description": "Նկարագրություն", + "admin_plugins.version": "Տարբերակ", + "admin_settings": "Կարգավորումներ", "index.newPad": "Ստեղծել", + "pad.toolbar.bold.title": "Թավատառ (Ctrl+B)", "pad.toolbar.underline.title": "ընդգծելով (Ctrl-U)", "pad.toolbar.undo.title": "Չեղարկել (Ctrl-Z)", "pad.toolbar.redo.title": "Վերադարձնել (Ctrl-Y)", diff --git a/src/locales/lb.json b/src/locales/lb.json index e9acca6d8..62c1621fa 100644 --- a/src/locales/lb.json +++ b/src/locales/lb.json @@ -6,6 +6,9 @@ "Soued031" ] }, + "admin_plugins_info.version": "Etherpad Versioun", + "admin_plugins_info.version_latest": "Lescht disponibel Versioun", + "admin_settings": "Astellungen", "index.newPad": "Neie Pad", "index.createOpenPad": "oder maacht ee Pad mat dësem Numm op:", "pad.toolbar.bold.title": "Fett (Strg-B)", diff --git a/src/locales/pl.json b/src/locales/pl.json index 68f293708..e637c1b11 100644 --- a/src/locales/pl.json +++ b/src/locales/pl.json @@ -2,6 +2,7 @@ "@metadata": { "authors": [ "DeRudySoulStorm", + "Kareyac", "Macofe", "Mateon1", "Matlin", @@ -14,6 +15,9 @@ "Woytecr" ] }, + "admin_plugins.description": "Opis", + "admin_plugins.version": "Wersja", + "admin_settings": "Ustawienia", "index.newPad": "Nowy dokument", "index.createOpenPad": "lub stwórz/otwórz dokument o nazwie:", "index.openPad": "otwórz istniejący dokument o nazwie:", diff --git a/src/locales/pms.json b/src/locales/pms.json index 8ca0b3ff7..6411091ea 100644 --- a/src/locales/pms.json +++ b/src/locales/pms.json @@ -4,6 +4,39 @@ "Borichèt" ] }, + "admin.page-title": "Cruscòt d'aministrator - Etherpad", + "admin_plugins": "Mansé dj'anstalassion", + "admin_plugins.available": "Anstalassion disponìbij", + "admin_plugins.available_not-found": "Gnun-e anstalassion trovà.", + "admin_plugins.available_fetching": "Arcuperassion…", + "admin_plugins.available_install.value": "Anstalé", + "admin_plugins.available_search.placeholder": "Arserca d'aplicassion da anstalé", + "admin_plugins.description": "Descrission", + "admin_plugins.installed": "Aplicassion anstalà", + "admin_plugins.installed_fetching": "Arcuperassion dj'aplicassion anstalà…", + "admin_plugins.installed_nothing": "A l'ha ancor nen anstalà d'aplicassion.", + "admin_plugins.installed_uninstall.value": "Disanstalé", + "admin_plugins.last-update": "Ùltim agiornament", + "admin_plugins.name": "Nòm", + "admin_plugins.page-title": "Mansé d'aplicassion - Etherpad", + "admin_plugins.version": "Version", + "admin_plugins_info": "Anformassion an sij problema", + "admin_plugins_info.hooks": "Gancio anstalà", + "admin_plugins_info.hooks_client": "Gancio da la part dël client", + "admin_plugins_info.hooks_server": "Gancio da la part dël servent", + "admin_plugins_info.parts": "Part anstalà", + "admin_plugins_info.plugins": "Aplicassion anstalà", + "admin_plugins_info.page-title": "Anformassion d'aplicassion - Etherpad", + "admin_plugins_info.version": "Version d'Etherpad", + "admin_plugins_info.version_latest": "Ùltima version disponìbil", + "admin_plugins_info.version_number": "Nùmer ëd version", + "admin_settings": "Paràmeter", + "admin_settings.current": "Configurassion atual", + "admin_settings.current_example-devel": "Stamp d'esempi dij paràmeter ëd dësvlup", + "admin_settings.current_example-prod": "Stamp d'esempi dij paràmeter ëd produssion", + "admin_settings.current_restart.value": "Fé torna parte Etherpad", + "admin_settings.current_save.value": "Argistré ij paràmeter", + "admin_settings.page-title": "Paràmeter - Etherpad", "index.newPad": "Feuj neuv", "index.createOpenPad": "o creé/duverté un feuj antitolà:", "index.openPad": "duverté un Pad esistent con ël nòm:", diff --git a/src/locales/pt-br.json b/src/locales/pt-br.json index 5af5eeaa2..31c4ca0dd 100644 --- a/src/locales/pt-br.json +++ b/src/locales/pt-br.json @@ -20,9 +20,20 @@ "Webysther" ] }, + "admin_plugins": "Gerente de complementos", + "admin_plugins.available_not-found": "Nenhuma complementos encontrado.", "admin_plugins.available_install.value": "Instalar", + "admin_plugins.description": "Descrição", + "admin_plugins.installed": "Complementos instalados", "admin_plugins.installed_uninstall.value": "Desinstalar", + "admin_plugins.last-update": "Última atualização", + "admin_plugins.name": "Nome", "admin_plugins.version": "Versão", + "admin_plugins_info.version_number": "Número de versão", + "admin_settings": "Configurações", + "admin_settings.current": "Configuração atual", + "admin_settings.current_save.value": "Salvar configurações", + "admin_settings.page-title": "Configurações - Etherpad", "index.newPad": "Nova Nota", "index.createOpenPad": "ou criar-abrir uma Nota com o nome:", "index.openPad": "abra um bloco existente com o nome:", diff --git a/src/locales/qqq.json b/src/locales/qqq.json index c680bf3dc..327bd8e18 100644 --- a/src/locales/qqq.json +++ b/src/locales/qqq.json @@ -4,11 +4,13 @@ "Liuxinyu970226", "Mklehr", "Nemo bis", + "Robby", "Shirayuki", "Siebrand", "Tim.krieger" ] }, + "admin_settings": "{{identical|Settings}}", "index.newPad": "Used as button text.\nA pad, in the context of Etherpad, is a notepad, something to write on.", "index.createOpenPad": "label for an input field that allows the user to choose a custom name for his new pad. In case the pad already exists the user will be redirected to its url.", "pad.toolbar.bold.title": "Used as tooltip of button", diff --git a/src/locales/ru.json b/src/locales/ru.json index e617f7cef..6f25f6968 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -6,6 +6,7 @@ "Diralik", "Eleferen", "Facenapalm", + "Kareyac", "MSClaudiu", "Movses", "Nzeemin", @@ -15,6 +16,13 @@ "Volkov" ] }, + "admin_plugins.available": "Доступные плагины", + "admin_plugins.available_install.value": "Установить", + "admin_plugins.description": "Описание", + "admin_plugins.installed_uninstall.value": "Удалить", + "admin_plugins.version": "Версия", + "admin_settings": "Настройки", + "admin_settings.current_save.value": "Сохранить настройки", "index.newPad": "Создать", "index.createOpenPad": "или создать/открыть документ с именем:", "index.openPad": "откройте существующий документ с именем:", diff --git a/src/locales/sv.json b/src/locales/sv.json index 3065ac079..44bae568a 100644 --- a/src/locales/sv.json +++ b/src/locales/sv.json @@ -8,6 +8,36 @@ "WikiPhoenix" ] }, + "admin_plugins": "Insticksprogramhanterare", + "admin_plugins.available": "Tillgängliga insticksprogram", + "admin_plugins.available_not-found": "Inga insticksmoduler hittades.", + "admin_plugins.available_fetching": "Hämtar…", + "admin_plugins.available_install.value": "Installera", + "admin_plugins.available_search.placeholder": "Sök efter insticksprogram att installera", + "admin_plugins.description": "Beskrivning", + "admin_plugins.installed": "Installerade insticksprogram", + "admin_plugins.installed_fetching": "Hämtar installerade insticksprogram…", + "admin_plugins.installed_nothing": "Du har ännu inte installerat några insticksprogram.", + "admin_plugins.installed_uninstall.value": "Avinstallera", + "admin_plugins.last-update": "Senast uppdaterat", + "admin_plugins.name": "Namn", + "admin_plugins.page-title": "Insticksprogramhanterare - Etherpad", + "admin_plugins.version": "Version", + "admin_plugins_info": "Felsökningsinformation", + "admin_plugins_info.hooks": "Installerade hooks", + "admin_plugins_info.hooks_client": "Klient-hooks", + "admin_plugins_info.hooks_server": "Server-hooks", + "admin_plugins_info.parts": "Installerade delar", + "admin_plugins_info.plugins": "Installerade insticksprogram", + "admin_plugins_info.page-title": "Insticksprograminformation - Etherpad", + "admin_plugins_info.version": "Etherpad-version", + "admin_plugins_info.version_latest": "Senast tillgängliga version", + "admin_plugins_info.version_number": "Versionsnummer", + "admin_settings": "Inställningar", + "admin_settings.current": "Nuvarande konfiguration", + "admin_settings.current_restart.value": "Starta om Etherpad", + "admin_settings.current_save.value": "Spara inställningar", + "admin_settings.page-title": "Inställningar - Etherpad", "index.newPad": "Nytt block", "index.createOpenPad": "eller skapa/öppna ett block med namnet:", "index.openPad": "öppna ett befintligt block med namnet:", diff --git a/src/locales/zh-hant.json b/src/locales/zh-hant.json index bd4eb6d8d..da75b7827 100644 --- a/src/locales/zh-hant.json +++ b/src/locales/zh-hant.json @@ -12,6 +12,39 @@ "Wehwei" ] }, + "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": "開啟一個現有的記事本,名稱為:", From 275e5c31c8f90b257430b08eb48e0e38c37a6109 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 3 Oct 2020 15:13:48 -0400 Subject: [PATCH 072/315] webaccess: Wrap long lines --- src/node/hooks/express/webaccess.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 744a6d316..1fc9c8f71 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -213,10 +213,16 @@ exports.expressConfigure = (hook_name, args, cb) => { next(); }); - // If the log level specified in the config file is WARN or ERROR the application server never starts listening to requests as reported in issue #158. - // Not installing the log4js connect logger when the log level has a higher severity than INFO since it would not log at that level anyway. - if (!(settings.loglevel === 'WARN' || settings.loglevel === 'ERROR')) - args.app.use(log4js.connectLogger(httpLogger, {level: log4js.levels.DEBUG, format: ':status, :method :url'})); + // If the log level specified in the config file is WARN or ERROR the application server never + // starts listening to requests as reported in issue #158. Not installing the log4js connect + // logger when the log level has a higher severity than INFO since it would not log at that level + // anyway. + if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) { + args.app.use(log4js.connectLogger(httpLogger, { + level: log4js.levels.DEBUG, + format: ':status, :method :url', + })); + } /* Do not let express create the session, so that we can retain a * reference to it for socket.io to use. Also, set the key (cookie From b68969fbac0d8f02229af41a465e89d11a487d84 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 3 Oct 2020 15:21:50 -0400 Subject: [PATCH 073/315] webaccess: Simplify Express and express-session setup --- src/node/hooks/express/socketio.js | 4 ++-- src/node/hooks/express/webaccess.js | 28 +++++++++------------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index ffc280b5c..8ec3b25e3 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -49,8 +49,8 @@ exports.expressCreateServer = function (hook_name, args, cb) { // check whether the user has authenticated, then any random person on the Internet can read, // modify, or create any pad (unless the pad is password protected or an HTTP API session is // required). - const cookieParserFn = util.promisify(cookieParser(webaccess.secret, {})); - const getSession = util.promisify(args.app.sessionStore.get).bind(args.app.sessionStore); + const cookieParserFn = util.promisify(cookieParser(settings.sessionKey, {})); + const getSession = util.promisify(webaccess.sessionStore.get).bind(webaccess.sessionStore); io.use(async (socket, next) => { const req = socket.request; if (!req.headers.cookie) { diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 1fc9c8f71..09e672791 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -199,17 +199,12 @@ exports.checkAccess = (req, res, next) => { step1PreAuthorize(); }; -exports.secret = null; - exports.expressConfigure = (hook_name, args, cb) => { // Measure response time args.app.use((req, res, next) => { const stopWatch = stats.timer('httpRequests').start(); - const sendFn = res.send; - res.send = function() { // function, not arrow, due to use of 'arguments' - stopWatch.end(); - sendFn.apply(res, arguments); - }; + const sendFn = res.send.bind(res); + res.send = (...args) => { stopWatch.end(); sendFn(...args); }; next(); }); @@ -224,22 +219,17 @@ exports.expressConfigure = (hook_name, args, cb) => { })); } - /* Do not let express create the session, so that we can retain a - * reference to it for socket.io to use. Also, set the key (cookie - * name) to a javascript identifier compatible string. Makes code - * handling it cleaner :) */ + // Do not let express create the session, so that we can retain a reference to it for socket.io to + // use. + exports.sessionStore = new ueberStore(); - if (!exports.sessionStore) { - exports.sessionStore = new ueberStore(); - exports.secret = settings.sessionKey; - } - - args.app.sessionStore = exports.sessionStore; args.app.use(sessionModule({ - secret: exports.secret, - store: args.app.sessionStore, + secret: settings.sessionKey, + store: exports.sessionStore, resave: false, saveUninitialized: true, + // Set the cookie name to a javascript identifier compatible string. Makes code handling it + // cleaner :) name: 'express_sid', proxy: true, cookie: { From 3f8365a995299f50deec59e6040e4d82b4e912e9 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 3 Oct 2020 16:52:01 -0400 Subject: [PATCH 074/315] express: Use `const` and `let` instead of `var` Also: * Sort imports. * Use single quotes. * Abbreviate module names. --- src/node/hooks/express.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/node/hooks/express.js b/src/node/hooks/express.js index cb536ef4d..5363d331e 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.js @@ -1,13 +1,13 @@ -var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); -var express = require('express'); -var settings = require('../utils/Settings'); -var fs = require('fs'); -var path = require('path'); -var npm = require("npm/lib/npm.js"); -var _ = require("underscore"); +const _ = require('underscore'); +const express = require('express'); +const fs = require('fs'); +const hooks = require('../../static/js/pluginfw/hooks'); +const npm = require('npm/lib/npm'); +const path = require('path'); +const settings = require('../utils/Settings'); const util = require('util'); -var serverName; +let serverName; exports.server = null; @@ -33,7 +33,7 @@ exports.createServer = async () => { console.warn("Admin username and password not set in settings.json. To access admin please uncomment and edit 'users' in settings.json"); } - var env = process.env.NODE_ENV || 'development'; + const env = process.env.NODE_ENV || 'development'; if (env !== 'production') { console.warn("Etherpad is running in Development mode. This mode is slower for users and less secure than production mode. You should set the NODE_ENV environment variable to production by using: export NODE_ENV=production"); @@ -46,30 +46,30 @@ exports.restartServer = async () => { await util.promisify(exports.server.close).bind(exports.server)(); } - var app = express(); // New syntax for express v3 + const app = express(); // New syntax for express v3 if (settings.ssl) { console.log("SSL -- enabled"); console.log(`SSL -- server key file: ${settings.ssl.key}`); console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`); - var options = { + const options = { key: fs.readFileSync( settings.ssl.key ), cert: fs.readFileSync( settings.ssl.cert ) }; if (settings.ssl.ca) { options.ca = []; - for (var i = 0; i < settings.ssl.ca.length; i++) { - var caFileName = settings.ssl.ca[i]; + for (let i = 0; i < settings.ssl.ca.length; i++) { + const caFileName = settings.ssl.ca[i]; options.ca.push(fs.readFileSync(caFileName)); } } - var https = require('https'); + const https = require('https'); exports.server = https.createServer(options, app); } else { - var http = require('http'); + const http = require('http'); exports.server = http.createServer(app); } From f7953ece8557cfa81685048899f4aa44418859d7 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 3 Oct 2020 17:52:10 -0400 Subject: [PATCH 075/315] socketio: Delete redundant authentication check There's no need to perform an authentication check in the socket.io middleware because `PadMessageHandler.handleMessage` calls `SecurityMananger.checkAccess` and that now performs authentication and authorization checks. This change also improves the user experience: Before, access denials caused socket.io error events in the client, which `pad.js` mostly ignores (the user doesn't see anything). Now a deny message is sent back to the client, which causes `pad.js` to display an obvious permission denied message. This also fixes a minor bug: `settings.loadTest` is supposed to bypass authentication and authorization checks, but they weren't bypassed because `SecurityManager.checkAccess` did not check `settings.loadTest`. --- src/node/db/SecurityManager.js | 10 ++++----- src/node/hooks/express/socketio.js | 33 +++++++----------------------- tests/backend/specs/socketio.js | 15 ++++++-------- 3 files changed, 18 insertions(+), 40 deletions(-) diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index ad802f327..818f04d9d 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -60,15 +60,15 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user let canCreate = !settings.editOnly; - if (settings.requireAuthentication) { - // Make sure the user has authenticated if authentication is required. The caller should have - // already performed this check, but it is repeated here just in case. + // Authentication and authorization checks. + if (settings.loadTest) { + console.warn( + 'bypassing socket.io authentication and authorization checks due to settings.loadTest'); + } else if (settings.requireAuthentication) { if (userSettings == null) { authLogger.debug('access denied: authentication is required'); return DENY; } - - // Check whether the user is authorized to create the pad if it doesn't exist. if (userSettings.canCreate != null && !userSettings.canCreate) canCreate = false; if (userSettings.readOnly) canCreate = false; // Note: userSettings.padAuthorizations should still be populated even if diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index 8ec3b25e3..b1e07ba91 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -40,15 +40,6 @@ exports.expressCreateServer = function (hook_name, args, cb) { cookie: false, }); - // REQUIRE a signed express-session cookie to be present, then load the session. See - // http://www.danielbaulig.de/socket-ioexpress for more info. After the session is loaded, ensure - // that the user has authenticated (if authentication is required). - // - // !!!WARNING!!! Requests to /socket.io are NOT subject to the checkAccess middleware in - // webaccess.js. If this handler fails to check for a signed express-session cookie or fails to - // check whether the user has authenticated, then any random person on the Internet can read, - // modify, or create any pad (unless the pad is password protected or an HTTP API session is - // required). const cookieParserFn = util.promisify(cookieParser(settings.sessionKey, {})); const getSession = util.promisify(webaccess.sessionStore.get).bind(webaccess.sessionStore); io.use(async (socket, next) => { @@ -58,24 +49,14 @@ exports.expressCreateServer = function (hook_name, args, cb) { // token and express_sid cookies have to be passed via a query parameter for unit tests. req.headers.cookie = socket.handshake.query.cookie; } - if (!req.headers.cookie && settings.loadTest) { - console.warn('bypassing socket.io authentication check due to settings.loadTest'); - return next(null, true); - } - try { - await cookieParserFn(req, {}); - const expressSid = req.signedCookies.express_sid; - const needAuthn = settings.requireAuthentication; - if (needAuthn && !expressSid) throw new Error('signed express_sid cookie is required'); - if (expressSid) { - const session = await getSession(expressSid); - if (!session) throw new Error('bad session or session has expired'); - req.session = new sessionModule.Session(req, session); - if (needAuthn && req.session.user == null) throw new Error('authentication required'); - } - } catch (err) { - return next(new Error(`access denied: ${err}`), false); + await cookieParserFn(req, {}); + const expressSid = req.signedCookies.express_sid; + if (expressSid) { + const session = await getSession(expressSid); + if (session) req.session = new sessionModule.Session(req, session); } + // Note: PadMessageHandler.handleMessage calls SecurityMananger.checkAccess which will perform + // authentication and authorization checks. return next(null, true); }); diff --git a/tests/backend/specs/socketio.js b/tests/backend/specs/socketio.js index e89d48725..6a4541d74 100644 --- a/tests/backend/specs/socketio.js +++ b/tests/backend/specs/socketio.js @@ -155,20 +155,17 @@ describe('socket.io access checks', function() { describe('Normal accesses', function() { it('!authn anonymous cookie /p/pad -> 200, ok', async function() { const res = await agent.get('/p/pad').expect(200); - // Should not throw. socket = await connect(res); const clientVars = await handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); it('!authn !cookie -> ok', async function() { - // Should not throw. socket = await connect(null); const clientVars = await handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); it('!authn user /p/pad -> 200, ok', async function() { const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - // Should not throw. socket = await connect(res); const clientVars = await handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); @@ -176,7 +173,6 @@ describe('socket.io access checks', function() { it('authn user /p/pad -> 200, ok', async function() { settings.requireAuthentication = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - // Should not throw. socket = await connect(res); const clientVars = await handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); @@ -185,7 +181,6 @@ describe('socket.io access checks', function() { settings.requireAuthentication = true; settings.requireAuthorization = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - // Should not throw. socket = await connect(res); const clientVars = await handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); @@ -199,7 +194,6 @@ describe('socket.io access checks', function() { settings.requireAuthorization = true; const encodedPadId = encodeURIComponent('päd'); const res = await agent.get(`/p/${encodedPadId}`).auth('user', 'user-password').expect(200); - // Should not throw. socket = await connect(res); const clientVars = await handshake(socket, 'päd'); assert.equal(clientVars.type, 'CLIENT_VARS'); @@ -211,11 +205,15 @@ describe('socket.io access checks', function() { settings.requireAuthentication = true; const res = await agent.get('/p/pad').expect(401); // Despite the 401, try to create the pad via a socket.io connection anyway. - await assert.rejects(connect(res), {message: /authentication required/i}); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); }); it('authn !cookie -> error', async function() { settings.requireAuthentication = true; - await assert.rejects(connect(null), {message: /signed express_sid cookie is required/i}); + socket = await connect(null); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); }); it('authorization bypass attempt -> error', async function() { // Only allowed to access /p/pad. @@ -224,7 +222,6 @@ describe('socket.io access checks', function() { settings.requireAuthorization = true; // First authenticate and establish a session. const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - // Connecting should work because the user successfully authenticated. socket = await connect(res); // Accessing /p/other-pad should fail, despite the successful fetch of /p/pad. const message = await handshake(socket, 'other-pad'); From 821c06cc3a0f42426f02344955e8c1c976830b2e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 3 Oct 2020 18:00:04 -0400 Subject: [PATCH 076/315] socketio: Reuse the `express-session` middleware --- src/node/hooks/express/socketio.js | 19 +++---------------- src/node/hooks/express/webaccess.js | 11 ++++------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index b1e07ba91..3ceae1ddc 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -6,10 +6,6 @@ var webaccess = require("ep_etherpad-lite/node/hooks/express/webaccess"); var padMessageHandler = require("../../handler/PadMessageHandler"); -var cookieParser = require('cookie-parser'); -var sessionModule = require('express-session'); -const util = require('util'); - exports.expressCreateServer = function (hook_name, args, cb) { //init socket.io and redirect all requests to the MessageHandler // there shouldn't be a browser that isn't compatible to all @@ -40,24 +36,15 @@ exports.expressCreateServer = function (hook_name, args, cb) { cookie: false, }); - const cookieParserFn = util.promisify(cookieParser(settings.sessionKey, {})); - const getSession = util.promisify(webaccess.sessionStore.get).bind(webaccess.sessionStore); - io.use(async (socket, next) => { + io.use((socket, next) => { const req = socket.request; if (!req.headers.cookie) { // socketio.js-client on node.js doesn't support cookies (see https://git.io/JU8u9), so the // token and express_sid cookies have to be passed via a query parameter for unit tests. req.headers.cookie = socket.handshake.query.cookie; } - await cookieParserFn(req, {}); - const expressSid = req.signedCookies.express_sid; - if (expressSid) { - const session = await getSession(expressSid); - if (session) req.session = new sessionModule.Session(req, session); - } - // Note: PadMessageHandler.handleMessage calls SecurityMananger.checkAccess which will perform - // authentication and authorization checks. - return next(null, true); + // See: https://socket.io/docs/faq/#Usage-with-express-session + webaccess.sessionMiddleware(req, {}, next); }); // var socketIOLogger = log4js.getLogger("socket.io"); diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 09e672791..843cf1478 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -219,13 +219,9 @@ exports.expressConfigure = (hook_name, args, cb) => { })); } - // Do not let express create the session, so that we can retain a reference to it for socket.io to - // use. - exports.sessionStore = new ueberStore(); - - args.app.use(sessionModule({ + exports.sessionMiddleware = sessionModule({ secret: settings.sessionKey, - store: exports.sessionStore, + store: new ueberStore(), resave: false, saveUninitialized: true, // Set the cookie name to a javascript identifier compatible string. Makes code handling it @@ -256,7 +252,8 @@ exports.expressConfigure = (hook_name, args, cb) => { */ secure: 'auto', } - })); + }); + args.app.use(exports.sessionMiddleware); args.app.use(cookieParser(settings.sessionKey, {})); From 377560eb517a2d3caf77f2abe128b152b526ce0b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 3 Oct 2020 18:10:00 -0400 Subject: [PATCH 077/315] express: Move general Express setup from `webaccess.js` The `express-session`, `cookie-parser`, etc. middleware is not specific to access checks. --- src/node/hooks/express.js | 60 ++++++++++++++++++++++++++++ src/node/hooks/express/socketio.js | 4 +- src/node/hooks/express/webaccess.js | 62 ----------------------------- 3 files changed, 62 insertions(+), 64 deletions(-) diff --git a/src/node/hooks/express.js b/src/node/hooks/express.js index 5363d331e..9bb4f2239 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.js @@ -1,12 +1,18 @@ const _ = require('underscore'); +const cookieParser = require('cookie-parser'); const express = require('express'); +const expressSession = require('express-session'); const fs = require('fs'); const hooks = require('../../static/js/pluginfw/hooks'); +const log4js = require('log4js'); const npm = require('npm/lib/npm'); const path = require('path'); +const sessionStore = require('../db/SessionStore'); const settings = require('../utils/Settings'); +const stats = require('../stats'); const util = require('util'); +const logger = log4js.getLogger('http'); let serverName; exports.server = null; @@ -111,6 +117,60 @@ exports.restartServer = async () => { app.enable('trust proxy'); } + // Measure response time + app.use((req, res, next) => { + const stopWatch = stats.timer('httpRequests').start(); + const sendFn = res.send.bind(res); + res.send = (...args) => { stopWatch.end(); sendFn(...args); }; + next(); + }); + + // If the log level specified in the config file is WARN or ERROR the application server never + // starts listening to requests as reported in issue #158. Not installing the log4js connect + // logger when the log level has a higher severity than INFO since it would not log at that level + // anyway. + if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) { + app.use(log4js.connectLogger(logger, { + level: log4js.levels.DEBUG, + format: ':status, :method :url', + })); + } + + exports.sessionMiddleware = expressSession({ + secret: settings.sessionKey, + store: new sessionStore(), + resave: false, + saveUninitialized: true, + // Set the cookie name to a javascript identifier compatible string. Makes code handling it + // cleaner :) + name: 'express_sid', + proxy: true, + cookie: { + sameSite: settings.cookie.sameSite, + + // The automatic express-session mechanism for determining if the application is being served + // over ssl is similar to the one used for setting the language cookie, which check if one of + // these conditions is true: + // + // 1. we are directly serving the nodejs application over SSL, using the "ssl" options in + // settings.json + // + // 2. we are serving the nodejs application in plaintext, but we are using a reverse proxy + // that terminates SSL for us. In this case, the user has to set trustProxy = true in + // settings.json, and the information wheter the application is over SSL or not will be + // extracted from the X-Forwarded-Proto HTTP header + // + // Please note that this will not be compatible with applications being served over http and + // https at the same time. + // + // reference: https://github.com/expressjs/session/blob/v1.17.0/README.md#cookiesecure + secure: 'auto', + } + }); + app.use(exports.sessionMiddleware); + + app.use(cookieParser(settings.sessionKey, {})); + hooks.callAll("expressConfigure", {"app": app}); hooks.callAll('expressCreateServer', {app, server: exports.server}); diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index 3ceae1ddc..f5964f18a 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -1,8 +1,8 @@ +const express = require("../express"); var settings = require('../../utils/Settings'); var socketio = require('socket.io'); var socketIORouter = require("../../handler/SocketIORouter"); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); -var webaccess = require("ep_etherpad-lite/node/hooks/express/webaccess"); var padMessageHandler = require("../../handler/PadMessageHandler"); @@ -44,7 +44,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { req.headers.cookie = socket.handshake.query.cookie; } // See: https://socket.io/docs/faq/#Usage-with-express-session - webaccess.sessionMiddleware(req, {}, next); + express.sessionMiddleware(req, {}, next); }); // var socketIOLogger = log4js.getLogger("socket.io"); diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 843cf1478..64943dda3 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -1,13 +1,8 @@ const assert = require('assert').strict; -const express = require('express'); const log4js = require('log4js'); const httpLogger = log4js.getLogger('http'); const settings = require('../../utils/Settings'); const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -const ueberStore = require('../../db/SessionStore'); -const stats = require('ep_etherpad-lite/node/stats'); -const sessionModule = require('express-session'); -const cookieParser = require('cookie-parser'); hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead'; @@ -200,62 +195,5 @@ exports.checkAccess = (req, res, next) => { }; exports.expressConfigure = (hook_name, args, cb) => { - // Measure response time - args.app.use((req, res, next) => { - const stopWatch = stats.timer('httpRequests').start(); - const sendFn = res.send.bind(res); - res.send = (...args) => { stopWatch.end(); sendFn(...args); }; - next(); - }); - - // If the log level specified in the config file is WARN or ERROR the application server never - // starts listening to requests as reported in issue #158. Not installing the log4js connect - // logger when the log level has a higher severity than INFO since it would not log at that level - // anyway. - if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) { - args.app.use(log4js.connectLogger(httpLogger, { - level: log4js.levels.DEBUG, - format: ':status, :method :url', - })); - } - - exports.sessionMiddleware = sessionModule({ - secret: settings.sessionKey, - store: new ueberStore(), - resave: false, - saveUninitialized: true, - // Set the cookie name to a javascript identifier compatible string. Makes code handling it - // cleaner :) - name: 'express_sid', - proxy: true, - cookie: { - sameSite: settings.cookie.sameSite, - /* - * The automatic express-session mechanism for determining if the - * application is being served over ssl is similar to the one used for - * setting the language cookie, which check if one of these conditions is - * true: - * - * 1. we are directly serving the nodejs application over SSL, using the - * "ssl" options in settings.json - * - * 2. we are serving the nodejs application in plaintext, but we are using - * a reverse proxy that terminates SSL for us. In this case, the user - * has to set trustProxy = true in settings.json, and the information - * wheter the application is over SSL or not will be extracted from the - * X-Forwarded-Proto HTTP header - * - * Please note that this will not be compatible with applications being - * served over http and https at the same time. - * - * reference: https://github.com/expressjs/session/blob/v1.17.0/README.md#cookiesecure - */ - secure: 'auto', - } - }); - args.app.use(exports.sessionMiddleware); - - args.app.use(cookieParser(settings.sessionKey, {})); - args.app.use(exports.checkAccess); }; From 32b6d8e37f43d22e527b7e1ae53bb0b2b8d874ed Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 1 Oct 2020 16:23:32 -0400 Subject: [PATCH 078/315] tests: Factor out common server setup/teardown --- tests/backend/common.js | 43 ++++++++++++++++++++++++++++++++ tests/backend/specs/socketio.js | 27 +++----------------- tests/backend/specs/webaccess.js | 23 ++--------------- 3 files changed, 49 insertions(+), 44 deletions(-) create mode 100644 tests/backend/common.js diff --git a/tests/backend/common.js b/tests/backend/common.js new file mode 100644 index 000000000..666d69eca --- /dev/null +++ b/tests/backend/common.js @@ -0,0 +1,43 @@ +function m(mod) { return __dirname + '/../../src/' + mod; } + +const log4js = require(m('node_modules/log4js')); +const server = require(m('node/server')); +const settings = require(m('node/utils/Settings')); +const supertest = require(m('node_modules/supertest')); +const webaccess = require(m('node/hooks/express/webaccess')); + +const backups = {}; +let inited = false; + +exports.agent = null; +exports.baseUrl = null; +exports.httpServer = null; +exports.logger = log4js.getLogger('test'); + +exports.init = async function() { + if (inited) return exports.agent; + inited = true; + + // Note: This is only a shallow backup. + backups.settings = Object.assign({}, settings); + // Start the Etherpad server on a random unused port. + settings.port = 0; + settings.ip = 'localhost'; + exports.httpServer = await server.start(); + exports.baseUrl = `http://localhost:${exports.httpServer.address().port}`; + exports.logger.debug(`HTTP server at ${exports.baseUrl}`); + // Create a supertest user agent for the HTTP server. + exports.agent = supertest(exports.baseUrl); + // Speed up authn tests. + backups.authnFailureDelayMs = webaccess.authnFailureDelayMs; + webaccess.authnFailureDelayMs = 0; + + after(async function() { + webaccess.authnFailureDelayMs = backups.authnFailureDelayMs; + await server.stop(); + // Note: This does not unset settings that were added. + Object.assign(settings, backups.settings); + }); + + return exports.agent; +}; diff --git a/tests/backend/specs/socketio.js b/tests/backend/specs/socketio.js index 6a4541d74..e17af09fc 100644 --- a/tests/backend/specs/socketio.js +++ b/tests/backend/specs/socketio.js @@ -1,36 +1,17 @@ function m(mod) { return __dirname + '/../../../src/' + mod; } const assert = require('assert').strict; +const common = require('../common'); const io = require(m('node_modules/socket.io-client')); -const log4js = require(m('node_modules/log4js')); const padManager = require(m('node/db/PadManager')); const plugins = require(m('static/js/pluginfw/plugin_defs')); -const server = require(m('node/server')); const setCookieParser = require(m('node_modules/set-cookie-parser')); const settings = require(m('node/utils/Settings')); -const supertest = require(m('node_modules/supertest')); -const webaccess = require(m('node/hooks/express/webaccess')); -const logger = log4js.getLogger('test'); +const logger = common.logger; let agent; -let baseUrl; -let authnFailureDelayMsBackup; -before(async function() { - authnFailureDelayMsBackup = webaccess.authnFailureDelayMs; - webaccess.authnFailureDelayMs = 0; // Speed up tests. - settings.port = 0; - settings.ip = 'localhost'; - const httpServer = await server.start(); - baseUrl = `http://localhost:${httpServer.address().port}`; - logger.debug(`HTTP server at ${baseUrl}`); - agent = supertest(baseUrl); -}); - -after(async function() { - webaccess.authnFailureDelayMs = authnFailureDelayMsBackup; - await server.stop(); -}); +before(async function() { agent = await common.init(); }); // Waits for and returns the next named socket.io event. Rejects if there is any error while waiting // (unless waiting for that error event). @@ -75,7 +56,7 @@ const connect = async (res) => { }).join('; '); logger.debug('socket.io connecting...'); - const socket = io(`${baseUrl}/`, { + const socket = io(`${common.baseUrl}/`, { forceNew: true, // Different tests will have different query parameters. path: '/socket.io', // socketio.js-client on node.js doesn't support cookies (see https://git.io/JU8u9), so the diff --git a/tests/backend/specs/webaccess.js b/tests/backend/specs/webaccess.js index 7bce670c4..68d98a7c5 100644 --- a/tests/backend/specs/webaccess.js +++ b/tests/backend/specs/webaccess.js @@ -1,32 +1,13 @@ function m(mod) { return __dirname + '/../../../src/' + mod; } const assert = require('assert').strict; -const log4js = require(m('node_modules/log4js')); +const common = require('../common'); const plugins = require(m('static/js/pluginfw/plugin_defs')); -const server = require(m('node/server')); const settings = require(m('node/utils/Settings')); -const supertest = require(m('node_modules/supertest')); -const webaccess = require(m('node/hooks/express/webaccess')); let agent; -const logger = log4js.getLogger('test'); -let authnFailureDelayMsBackup; -before(async function() { - authnFailureDelayMsBackup = webaccess.authnFailureDelayMs; - webaccess.authnFailureDelayMs = 0; // Speed up tests. - settings.port = 0; - settings.ip = 'localhost'; - const httpServer = await server.start(); - const baseUrl = `http://localhost:${httpServer.address().port}`; - logger.debug(`HTTP server at ${baseUrl}`); - agent = supertest(baseUrl); -}); - -after(async function() { - webaccess.authnFailureDelayMs = authnFailureDelayMsBackup; - await server.stop(); -}); +before(async function() { agent = await common.init(); }); describe('webaccess: without plugins', function() { const backup = {}; From 2f17849b7bcd85623ac47a0fe3da9b67ddf03566 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 1 Oct 2020 17:32:45 -0400 Subject: [PATCH 079/315] tests: Switch import/export tests to self-contained server This makes it possible to test various settings combinations and examine internal state to confirm correct behavior. Also, the user doesn't need to start an Etherpad server before running these tests. --- tests/backend/specs/api/importexportGetPost.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/backend/specs/api/importexportGetPost.js b/tests/backend/specs/api/importexportGetPost.js index 3515d7776..065f6150a 100644 --- a/tests/backend/specs/api/importexportGetPost.js +++ b/tests/backend/specs/api/importexportGetPost.js @@ -3,12 +3,10 @@ */ const assert = require('assert').strict; +const common = require('../../common'); const superagent = require(__dirname+'/../../../../src/node_modules/superagent'); -const supertest = require(__dirname+'/../../../../src/node_modules/supertest'); const fs = require('fs'); const settings = require(__dirname+'/../../../../src/node/utils/Settings'); -const host = 'http://127.0.0.1:'+settings.port; -const agent = supertest(`http://${settings.ip}:${settings.port}`); const path = require('path'); const padText = fs.readFileSync("../tests/backend/specs/api/test.txt"); const etherpadDoc = fs.readFileSync("../tests/backend/specs/api/test.etherpad"); @@ -18,11 +16,14 @@ const odtDoc = fs.readFileSync("../tests/backend/specs/api/test.odt"); const pdfDoc = fs.readFileSync("../tests/backend/specs/api/test.pdf"); var filePath = path.join(__dirname, '../../../../APIKEY.txt'); +let agent; var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); apiKey = apiKey.replace(/\n$/, ""); var apiVersion = 1; var testPadId = makeid(); +before(async function() { agent = await common.init(); }); + describe('Connectivity', function(){ it('can connect', async function() { await agent.get('/api/') From 9a6f2864417d17de1a95c725aaa315b0bec15f40 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 1 Oct 2020 22:24:28 -0400 Subject: [PATCH 080/315] tests: Always run the import unsupported file type test --- tests/backend/specs/api/importexportGetPost.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/backend/specs/api/importexportGetPost.js b/tests/backend/specs/api/importexportGetPost.js index 065f6150a..95995cbcb 100644 --- a/tests/backend/specs/api/importexportGetPost.js +++ b/tests/backend/specs/api/importexportGetPost.js @@ -75,6 +75,18 @@ describe('Imports and Exports', function(){ } }); + const backups = {}; + + beforeEach(async function() { + // Note: This is a shallow copy. + backups.settings = Object.assign({}, settings); + }); + + afterEach(async function() { + // Note: This does not unset settings that were added. + Object.assign(settings, backups.settings); + }); + it('creates a new Pad, imports content to it, checks that content', async function() { await agent.get(endPoint('createPad') + `&padID=${testPadId}`) .expect(200) @@ -210,10 +222,7 @@ describe('Imports and Exports', function(){ }); it('Tries to import unsupported file type', async function() { - if (settings.allowUnknownFileEnds === true) { - console.log('skipping test because allowUnknownFileEnds is true'); - return this.skip(); - } + settings.allowUnknownFileEnds = false; await agent.post(`/p/${testPadId}/import`) .attach('file', padText, {filename: '/test.xasdasdxx', contentType: 'weirdness/jobby'}) .expect(200) From f4eae40c6b27238f2c6440e4ff7047e28e56446e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 1 Oct 2020 15:44:24 -0400 Subject: [PATCH 081/315] webaccess: Check for read-only pad ID in `userCanModify` This currently isn't absolutely necessary because all current callers of `userCanModify` already check for a read-only pad ID themselves. However: * This adds defense in depth. * This makes it possible to simply replace the import handler's `allowAnyoneToImport` check with a call to `userCanModify`. --- src/node/hooks/express/webaccess.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 64943dda3..410fb4e5f 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -3,6 +3,7 @@ const log4js = require('log4js'); const httpLogger = log4js.getLogger('http'); const settings = require('../../utils/Settings'); const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); +const readOnlyManager = require('../../db/ReadOnlyManager'); hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead'; @@ -31,6 +32,7 @@ exports.normalizeAuthzLevel = (level) => { }; exports.userCanModify = (padId, req) => { + if (readOnlyManager.isReadOnlyId(padId)) return false; if (!settings.requireAuthentication) return true; const {session: {user} = {}} = req; assert(user); // If authn required and user == null, the request should have already been denied. From ed6fcefb67f5d28c779a7a6a9d63f4e5d650c2ab Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 1 Oct 2020 21:29:38 -0400 Subject: [PATCH 082/315] webaccess: Fix pad ID extraction for import and export paths --- src/node/hooks/express/webaccess.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 410fb4e5f..2ee28f55f 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -64,7 +64,7 @@ exports.checkAccess = (req, res, next) => { if (!level) return fail(); const user = req.session.user; if (user == null) return next(); // This will happen if authentication is not required. - const encodedPadId = (req.path.match(/^\/p\/(.*)$/) || [])[1]; + const encodedPadId = (req.path.match(/^\/p\/([^/]*)/) || [])[1]; if (encodedPadId == null) return next(); const padId = decodeURIComponent(encodedPadId); // The user was granted access to a pad. Remember the authorization level in the user's From 831528e8bc1d47b0178402c145895d7c7edea14f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 1 Oct 2020 21:27:23 -0400 Subject: [PATCH 083/315] import: Allow import if pad does not yet exist --- src/node/hooks/express/importexport.js | 5 ----- tests/backend/specs/api/importexportGetPost.js | 10 ---------- 2 files changed, 15 deletions(-) diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js index f5c3c34cb..efb89b3c9 100644 --- a/src/node/hooks/express/importexport.js +++ b/src/node/hooks/express/importexport.js @@ -62,11 +62,6 @@ exports.expressCreateServer = function (hook_name, args, cb) { // handle import requests args.app.use('/p/:pad/import', limiter); args.app.post('/p/:pad/import', async function(req, res, next) { - if (!(await padManager.doesPadExists(req.params.pad))) { - console.warn(`Someone tried to import into a pad that doesn't exist (${req.params.pad})`); - return next(); - } - const {session: {user} = {}} = req; const {accessStatus, authorID} = await securityManager.checkAccess( req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password, user); diff --git a/tests/backend/specs/api/importexportGetPost.js b/tests/backend/specs/api/importexportGetPost.js index 95995cbcb..27f4758a0 100644 --- a/tests/backend/specs/api/importexportGetPost.js +++ b/tests/backend/specs/api/importexportGetPost.js @@ -211,16 +211,6 @@ describe('Imports and Exports', function(){ .expect(/
              • hello<\/ul><\/li><\/ul>/); }); - it('tries to import Plain Text to a pad that does not exist', async function() { - const padId = testPadId + testPadId + testPadId; - await agent.post(`/p/${padId}/import`) - .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) - .expect(405); - await agent.get(endPoint('getText') + `&padID=${padId}`) - .expect(200) - .expect((res) => assert.equal(res.body.code, 1)); - }); - it('Tries to import unsupported file type', async function() { settings.allowUnknownFileEnds = false; await agent.post(`/p/${testPadId}/import`) From a8cf434d1dcf651db6e3536e46bddf6f24bd4c78 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 1 Oct 2020 15:49:04 -0400 Subject: [PATCH 084/315] import: Replace the `allowAnyoneToImport` check with `userCanModify` This reduces the number of hoops a user or tool must jump through to import. --- settings.json.template | 13 -- src/locales/en.json | 3 +- src/node/handler/PadMessageHandler.js | 12 -- src/node/hooks/express/importexport.js | 34 +--- src/node/utils/Settings.js | 14 -- src/static/css/pad.css | 4 - src/static/js/collab_client.js | 10 -- src/static/js/pad_editbar.js | 11 -- src/templates/pad.html | 1 - .../backend/specs/api/importexportGetPost.js | 145 ++++++++++++++++-- tests/frontend/travis/runnerBackend.sh | 5 +- 11 files changed, 143 insertions(+), 109 deletions(-) diff --git a/settings.json.template b/settings.json.template index 61d6db117..e4b2b3d3c 100644 --- a/settings.json.template +++ b/settings.json.template @@ -497,19 +497,6 @@ */ "importMaxFileSize": 52428800, // 50 * 1024 * 1024 - - /* - * From Etherpad 1.8.3 onwards import was restricted to authors who had - * content within the pad. - * - * This setting will override that restriction and allow any user to import - * without the requirement to add content to a pad. - * - * This setting is useful for when you use a plugin for authentication so you - * can already trust each user. - */ - "allowAnyoneToImport": false, - /* * From Etherpad 1.9.0 onwards, when Etherpad is in production mode commits from individual users are rate limited * diff --git a/src/locales/en.json b/src/locales/en.json index ec861b772..a65e62885 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -188,6 +188,5 @@ "pad.impexp.importfailed": "Import failed", "pad.impexp.copypaste": "Please copy paste", "pad.impexp.exportdisabled": "Exporting as {{type}} format is disabled. Please contact your system administrator for details.", - "pad.impexp.maxFileSize": "File too big. Contact your site administrator to increase the allowed file size for import", - "pad.impexp.permission": "Import is disabled because you never contributed to this pad. Please contribute at least once before importing" + "pad.impexp.maxFileSize": "File too big. Contact your site administrator to increase the allowed file size for import" } diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 9e0e349ce..0794a7b1d 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -942,16 +942,6 @@ async function handleClientReady(client, message, authorID) }); })); - let thisUserHasEditedThisPad = false; - if (historicalAuthorData[authorID]) { - /* - * This flag is set to true when a user contributes to a specific pad for - * the first time. It is used for deciding if importing to that pad is - * allowed or not. - */ - thisUserHasEditedThisPad = true; - } - // glue the clientVars together, send them and tell the other clients that a new one is there // Check that the client is still here. It might have disconnected between callbacks. @@ -1135,8 +1125,6 @@ async function handleClientReady(client, message, authorID) "percentageToScrollWhenUserPressesArrowUp": settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp, }, "initialChangesets": [], // FIXME: REMOVE THIS SHIT - "thisUserHasEditedThisPad": thisUserHasEditedThisPad, - "allowAnyoneToImport": settings.allowAnyoneToImport } // Add a username to the clientVars if one avaiable diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js index efb89b3c9..d5e6c3bbd 100644 --- a/src/node/hooks/express/importexport.js +++ b/src/node/hooks/express/importexport.js @@ -8,6 +8,7 @@ var readOnlyManager = require("../../db/ReadOnlyManager"); var 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) { // when the rate limiter triggers, write a warning in the logs @@ -63,36 +64,11 @@ exports.expressCreateServer = function (hook_name, args, cb) { args.app.use('/p/:pad/import', limiter); args.app.post('/p/:pad/import', async function(req, res, next) { const {session: {user} = {}} = req; - const {accessStatus, authorID} = await securityManager.checkAccess( + const {accessStatus} = await securityManager.checkAccess( req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password, user); - if (accessStatus !== 'grant') return res.status(403).send('Forbidden'); - assert(authorID); - - /* - * Starting from Etherpad 1.8.3 onwards, importing into a pad is allowed - * only if a user has his browser opened and connected to the pad (i.e. a - * Socket.IO session is estabilished for him) and he has already - * contributed to that specific pad. - * - * Note that this does not have anything to do with the "session", used - * for logging into "group pads". That kind of session is not needed here. - * - * This behaviour does not apply to API requests, only to /p/$PAD$/import - * - * See: https://github.com/ether/etherpad-lite/pull/3833#discussion_r407490205 - */ - if (!settings.allowAnyoneToImport) { - const authorsPads = await authorManager.listPadsOfAuthor(authorID); - if (!authorsPads) { - console.warn(`Unable to import file into "${req.params.pad}". Author "${authorID}" exists but he never contributed to any pad`); - return next(); - } - if (authorsPads.padIDs.indexOf(req.params.pad) === -1) { - console.warn(`Unable to import file into "${req.params.pad}". Author "${authorID}" exists but he never contributed to this pad`); - return next(); - } + if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) { + return res.status(403).send('Forbidden'); } - - importHandler.doImport(req, res, req.params.pad); + await importHandler.doImport(req, res, req.params.pad); }); } diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 6f3ccea89..baf4d539e 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -385,20 +385,6 @@ exports.commitRateLimiting = { */ exports.importMaxFileSize = 50 * 1024 * 1024; - -/* - * From Etherpad 1.8.3 onwards import was restricted to authors who had - * content within the pad. - * - * This setting will override that restriction and allow any user to import - * without the requirement to add content to a pad. - * - * This setting is useful for when you use a plugin for authentication so you - * can already trust each user. - */ -exports.allowAnyoneToImport = false, - - // checks if abiword is avaiable exports.abiwordAvailable = function() { diff --git a/src/static/css/pad.css b/src/static/css/pad.css index 2043c3fff..9455e657c 100644 --- a/src/static/css/pad.css +++ b/src/static/css/pad.css @@ -71,7 +71,3 @@ input { @media (max-width: 800px) { .hide-for-mobile { display: none; } } - -#importmessagepermission { - display: none; -} diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.js index a5620f7b5..b04e6aa2c 100644 --- a/src/static/js/collab_client.js +++ b/src/static/js/collab_client.js @@ -312,16 +312,6 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad) } else if (msg.type == "ACCEPT_COMMIT") { - /* - * this is the first time this user contributed to this pad. Let's record - * it, because it will be used for allowing import. - * - * TODO: here, we are changing this variable on the client side only. The - * server has all the informations to make the same deduction, and - * broadcast to the client. - */ - clientVars.thisUserHasEditedThisPad = true; - var newRev = msg.newRev; if (msgQueue.length > 0) { diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 0e40bb990..0035b0023 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -415,17 +415,6 @@ var padeditbar = (function() toolbar.registerCommand("import_export", function () { toolbar.toggleDropDown("import_export", function(){ - - if (clientVars.thisUserHasEditedThisPad || clientVars.allowAnyoneToImport) { - // the user has edited this pad historically or in this session - $('#importform').show(); - $('#importmessagepermission').hide(); - } else { - // this is the first time this user visits this pad - $('#importform').hide(); - $('#importmessagepermission').show(); - } - // If Import file input exists then focus on it.. if($('#importfileinput').length !== 0){ setTimeout(function(){ diff --git a/src/templates/pad.html b/src/templates/pad.html index 49aa20204..0ba811095 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -195,7 +195,6 @@
            -
            <% e.end_block(); %>
            diff --git a/tests/backend/specs/api/importexportGetPost.js b/tests/backend/specs/api/importexportGetPost.js index 27f4758a0..5c716ea0c 100644 --- a/tests/backend/specs/api/importexportGetPost.js +++ b/tests/backend/specs/api/importexportGetPost.js @@ -7,7 +7,10 @@ const common = require('../../common'); const superagent = require(__dirname+'/../../../../src/node_modules/superagent'); const fs = require('fs'); const settings = require(__dirname+'/../../../../src/node/utils/Settings'); +const padManager = require(__dirname+'/../../../../src/node/db/PadManager'); const path = require('path'); +const plugins = require(__dirname+'/../../../../src/static/js/pluginfw/plugin_defs'); + const padText = fs.readFileSync("../tests/backend/specs/api/test.txt"); const etherpadDoc = fs.readFileSync("../tests/backend/specs/api/test.etherpad"); const wordDoc = fs.readFileSync("../tests/backend/specs/api/test.doc"); @@ -20,7 +23,8 @@ let agent; var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); apiKey = apiKey.replace(/\n$/, ""); var apiVersion = 1; -var testPadId = makeid(); +const testPadId = makeid(); +const testPadIdEnc = encodeURIComponent(testPadId); before(async function() { agent = await common.init(); }); @@ -67,14 +71,6 @@ Example Curl command for testing import URI: */ describe('Imports and Exports', function(){ - before(function() { - if (!settings.allowAnyoneToImport) { - console.warn('not anyone can import so not testing -- ' + - 'to include this test set allowAnyoneToImport to true in settings.json'); - this.skip(); - } - }); - const backups = {}; beforeEach(async function() { @@ -219,6 +215,137 @@ describe('Imports and Exports', function(){ .expect((res) => assert.doesNotMatch(res.text, /FrameCall\('undefined', 'ok'\);/)); }); + describe('Import authorization checks', function() { + let authorize; + + const deleteTestPad = async () => { + if (await padManager.doesPadExist(testPadId)) { + const pad = await padManager.getPad(testPadId); + await pad.remove(); + } + }; + + const createTestPad = async (text) => { + const pad = await padManager.getPad(testPadId); + if (text) await pad.setText(text); + return pad; + }; + + beforeEach(async function() { + await deleteTestPad(); + settings.requireAuthentication = false; + settings.requireAuthorization = true; + settings.users = {user: {password: 'user-password'}}; + authorize = () => true; + backups.hooks = {}; + backups.hooks.authorize = plugins.hooks.authorize || []; + plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => cb([authorize(req)])}]; + }); + + afterEach(async function() { + await deleteTestPad(); + Object.assign(plugins.hooks, backups.hooks); + }); + + it('!authn !exist -> create', async function() { + await agent.post(`/p/${testPadIdEnc}/import`) + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(200); + assert(await padManager.doesPadExist(testPadId)); + const pad = await padManager.getPad(testPadId); + assert.equal(pad.text(), padText.toString()); + }); + + it('!authn exist -> replace', async function() { + const pad = await createTestPad('before import'); + await agent.post(`/p/${testPadIdEnc}/import`) + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(200); + assert(await padManager.doesPadExist(testPadId)); + assert.equal(pad.text(), padText.toString()); + }); + + it('authn anonymous !exist -> fail', async function() { + settings.requireAuthentication = true; + await agent.post(`/p/${testPadIdEnc}/import`) + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(401); + assert(!(await padManager.doesPadExist(testPadId))); + }); + + it('authn anonymous exist -> fail', async function() { + settings.requireAuthentication = true; + const pad = await createTestPad('before import\n'); + await agent.post(`/p/${testPadIdEnc}/import`) + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(401); + assert.equal(pad.text(), 'before import\n'); + }); + + it('authn user create !exist -> create', async function() { + settings.requireAuthentication = true; + await agent.post(`/p/${testPadIdEnc}/import`) + .auth('user', 'user-password') + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(200); + assert(await padManager.doesPadExist(testPadId)); + const pad = await padManager.getPad(testPadId); + assert.equal(pad.text(), padText.toString()); + }); + + it('authn user modify !exist -> fail', async function() { + settings.requireAuthentication = true; + authorize = () => 'modify'; + await agent.post(`/p/${testPadIdEnc}/import`) + .auth('user', 'user-password') + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(403); + assert(!(await padManager.doesPadExist(testPadId))); + }); + + it('authn user readonly !exist -> fail', async function() { + settings.requireAuthentication = true; + authorize = () => 'readOnly'; + await agent.post(`/p/${testPadIdEnc}/import`) + .auth('user', 'user-password') + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(403); + assert(!(await padManager.doesPadExist(testPadId))); + }); + + it('authn user create exist -> replace', async function() { + settings.requireAuthentication = true; + const pad = await createTestPad('before import\n'); + await agent.post(`/p/${testPadIdEnc}/import`) + .auth('user', 'user-password') + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(200); + assert.equal(pad.text(), padText.toString()); + }); + + it('authn user modify exist -> replace', async function() { + settings.requireAuthentication = true; + authorize = () => 'modify'; + const pad = await createTestPad('before import\n'); + await agent.post(`/p/${testPadIdEnc}/import`) + .auth('user', 'user-password') + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(200); + assert.equal(pad.text(), padText.toString()); + }); + + it('authn user readonly exist -> fail', async function() { + const pad = await createTestPad('before import\n'); + settings.requireAuthentication = true; + authorize = () => 'readOnly'; + await agent.post(`/p/${testPadIdEnc}/import`) + .auth('user', 'user-password') + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(403); + assert.equal(pad.text(), 'before import\n'); + }); + }); + }); // End of tests. diff --git a/tests/frontend/travis/runnerBackend.sh b/tests/frontend/travis/runnerBackend.sh index c595dce02..ff92667d3 100755 --- a/tests/frontend/travis/runnerBackend.sh +++ b/tests/frontend/travis/runnerBackend.sh @@ -12,11 +12,8 @@ cd "${MY_DIR}/../../../" # Set soffice to /usr/bin/soffice sed 's#\"soffice\": null,#\"soffice\":\"/usr/bin/soffice\",#g' settings.json.template > settings.json.soffice -# Set allowAnyoneToImport to true -sed 's/\"allowAnyoneToImport\": false,/\"allowAnyoneToImport\": true,/g' settings.json.soffice > settings.json.allowImport - # Set "max": 10 to 100 to not agressively rate limit -sed 's/\"max\": 10/\"max\": 100/g' settings.json.allowImport > settings.json.rateLimit +sed 's/\"max\": 10/\"max\": 100/g' settings.json.soffice > settings.json.rateLimit # Set "points": 10 to 1000 to not agressively rate limit commits sed 's/\"points\": 10/\"points\": 1000/g' settings.json.rateLimit > settings.json From 34b232d6588575321455a94208a128909a9c4267 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 6 Oct 2020 03:16:21 -0400 Subject: [PATCH 085/315] Update `CHANGELOG.md` with the changes so far (#4393) --- CHANGELOG.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 093150284..12c5ea66e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Develop -- TODO Change to 1.8.x. -* ... +### Compatibility-breaking changes +* Authorization failures now return 403 by default instead of 401 +* The `authorize` hook is now only called after successful + authentication. Use the new `preAuthorize` hook if you need to bypass + authentication +* The `authFailure` hook is deprecated; use the new + `authnFailure` and `authzFailure` hooks instead +* The format of `ENTER`, `CREATE`, and `LEAVE` log messages has changed +### Notable new features +* Users can now import without creating and editing the pad first +* Added a new `readOnly` user setting that makes it possible to create users in + `settings.json` that can read pads but not create or modify them +* Added a new `canCreate` user setting that makes it possible to create users in + `settings.json` that can modify pads but not create them +* The `authorize` hook now accepts `readOnly` to grant read-only access to a pad +* The `authorize` hook now accepts `modify` to grant modify-only (creation + prohibited) access to a pad +* All authentication successes and failures are now logged +* Added a new `cookie.sameSite` setting that makes it possible to enable + authentication when Etherpad is embedded in an iframe from another site +### Notable fixes +* Fixed rate limit accounting when Etherpad is behind a reverse proxy +* Fixed typos that prevented access to pads via an HTTP API session +* Fixed authorization failures for pad URLs containing a percent-encoded + character +* Fixed exporting of read-only pads +### Minor changes +* Temporary disconnections no longer force a full page refresh +* Toolbar layout for narrow screens is improved +* Fixed `SameSite` cookie attribute for the `language`, + `token`, and `pref` cookies +* Fixed superfluous database accesses when deleting a pad +* Expanded test coverage. # 1.8.6 * IMPORTANT: This fixes a severe problem with postgresql in 1.8.5 From c74b2543343b6166a7c52960c814d133565b9206 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 4 Oct 2020 17:29:45 -0400 Subject: [PATCH 086/315] tests: Disable non-test logging unless level <= DEBUG This makes it easier to see the test results, and it hides some scary-looking but intentional error messages. This code will likely have to be updated if/when we change the logging library (see issue #1922). --- tests/backend/common.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/backend/common.js b/tests/backend/common.js index 666d69eca..0b4f85205 100644 --- a/tests/backend/common.js +++ b/tests/backend/common.js @@ -14,10 +14,19 @@ exports.baseUrl = null; exports.httpServer = null; exports.logger = log4js.getLogger('test'); +const logLevel = exports.logger.level; + exports.init = async function() { if (inited) return exports.agent; inited = true; + if (!logLevel.isLessThanOrEqualTo(log4js.levels.DEBUG)) { + exports.logger.warn('Disabling non-test logging for the duration of the test. ' + + 'To enable non-test logging, change the loglevel setting to DEBUG.'); + log4js.setGlobalLogLevel(log4js.levels.OFF); + exports.logger.setLevel(logLevel); + } + // Note: This is only a shallow backup. backups.settings = Object.assign({}, settings); // Start the Etherpad server on a random unused port. @@ -37,6 +46,7 @@ exports.init = async function() { await server.stop(); // Note: This does not unset settings that were added. Object.assign(settings, backups.settings); + log4js.setGlobalLogLevel(logLevel); }); return exports.agent; From 52f8fc9ba31a12d461c5dedf05dbea28952052c4 Mon Sep 17 00:00:00 2001 From: jeanfabrice Date: Tue, 6 Oct 2020 14:28:11 +0200 Subject: [PATCH 087/315] legacySupport: Run node 10 with '--experimental_worker' flags (#4392) * Run node 10 with '--experimental_worker' flags * Use dedicated function to retrieve node/npm program version The goal of this commit is to ensure that any linux based node 10 deployments run with the experimental_worker flag. This flag is required for workers to "work" in node 10. This will not affect other versions of node. This resolves #4335 where Docker would fail due to being based on node 10. --- Dockerfile | 2 +- bin/cleanRun.sh | 9 ++++-- bin/debugRun.sh | 5 +++- bin/fastRun.sh | 5 +++- bin/functions.sh | 74 ++++++++++++++++++++++++++++++++++++++++++++++ bin/installDeps.sh | 56 ++++------------------------------- bin/run.sh | 10 +++---- 7 files changed, 98 insertions(+), 63 deletions(-) create mode 100644 bin/functions.sh diff --git a/Dockerfile b/Dockerfile index 45601c876..aa6091a59 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,4 +51,4 @@ COPY --chown=etherpad:0 ./settings.json.docker /opt/etherpad-lite/settings.json RUN chmod -R g=u . EXPOSE 9001 -CMD ["node", "node_modules/ep_etherpad-lite/node/server.js"] +CMD ["node", "--experimental-worker", "node_modules/ep_etherpad-lite/node/server.js"] diff --git a/bin/cleanRun.sh b/bin/cleanRun.sh index 379b770a5..57de27e5c 100755 --- a/bin/cleanRun.sh +++ b/bin/cleanRun.sh @@ -1,7 +1,10 @@ #!/bin/sh -#Move to the folder where ep-lite is installed -cd $(dirname $0) +# Move to the folder where ep-lite is installed +cd "$(dirname "$0")"/.. + +# Source constants and usefull functions +. bin/functions.sh #Was this script started in the bin folder? if yes move out if [ -d "../bin" ]; then @@ -38,4 +41,4 @@ bin/installDeps.sh "$@" || exit 1 echo "Started Etherpad..." SCRIPTPATH=$(pwd -P) -node "${SCRIPTPATH}/node_modules/ep_etherpad-lite/node/server.js" "$@" +node $(compute_node_args) "${SCRIPTPATH}/node_modules/ep_etherpad-lite/node/server.js" "$@" diff --git a/bin/debugRun.sh b/bin/debugRun.sh index d9b18aaa2..9b2fff9bd 100755 --- a/bin/debugRun.sh +++ b/bin/debugRun.sh @@ -3,6 +3,9 @@ # Move to the folder where ep-lite is installed cd "$(dirname "$0")"/.. +# Source constants and usefull functions +. bin/functions.sh + # Prepare the environment bin/installDeps.sh || exit 1 @@ -12,4 +15,4 @@ echo "Open 'chrome://inspect' on Chrome to start debugging." # Use 0.0.0.0 to allow external connections to the debugger # (ex: running Etherpad on a docker container). Use default port # (9229) -node --inspect=0.0.0.0:9229 node_modules/ep_etherpad-lite/node/server.js "$@" +node $(compute_node_args) --inspect=0.0.0.0:9229 node_modules/ep_etherpad-lite/node/server.js "$@" diff --git a/bin/fastRun.sh b/bin/fastRun.sh index e00bb8c72..90d83dc8e 100755 --- a/bin/fastRun.sh +++ b/bin/fastRun.sh @@ -12,6 +12,9 @@ set -eu # source: https://stackoverflow.com/questions/59895/how-to-get-the-source-directory-of-a-bash-script-from-within-the-script-itself#246128 DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +# Source constants and usefull functions +. ${DIR}/../bin/functions.sh + echo "Running directly, without checking/installing dependencies" # move to the base Etherpad directory. This will be necessary until Etherpad @@ -19,4 +22,4 @@ echo "Running directly, without checking/installing dependencies" cd "${DIR}/.." # run Etherpad main class -node "${DIR}/../node_modules/ep_etherpad-lite/node/server.js" "${@}" +node $(compute_node_args) "${DIR}/../node_modules/ep_etherpad-lite/node/server.js" "$@" diff --git a/bin/functions.sh b/bin/functions.sh new file mode 100644 index 000000000..c7f3c8556 --- /dev/null +++ b/bin/functions.sh @@ -0,0 +1,74 @@ +# minimum required node version +REQUIRED_NODE_MAJOR=10 +REQUIRED_NODE_MINOR=13 + +# minimum required npm version +REQUIRED_NPM_MAJOR=5 +REQUIRED_NPM_MINOR=5 + +pecho() { printf %s\\n "$*"; } +log() { pecho "$@"; } +error() { log "ERROR: $@" >&2; } +fatal() { error "$@"; exit 1; } +is_cmd() { command -v "$@" >/dev/null 2>&1; } + + +get_program_version() { + PROGRAM="$1" + KIND="${2:-full}" + PROGRAM_VERSION_STRING=$($PROGRAM --version) + PROGRAM_VERSION_STRING=${PROGRAM_VERSION_STRING#"v"} + + DETECTED_MAJOR=$(pecho "$PROGRAM_VERSION_STRING" | cut -s -d "." -f 1) + [ -n "$DETECTED_MAJOR" ] || fatal "Cannot extract $PROGRAM major version from version string \"$PROGRAM_VERSION_STRING\"" + case "$DETECTED_MAJOR" in + ''|*[!0-9]*) + fatal "$PROGRAM_LABEL major version from \"$VERSION_STRING\" is not a number. Detected: \"$DETECTED_MAJOR\"" + ;; + esac + + DETECTED_MINOR=$(pecho "$PROGRAM_VERSION_STRING" | cut -s -d "." -f 2) + [ -n "$DETECTED_MINOR" ] || fatal "Cannot extract $PROGRAM minor version from version string \"$PROGRAM_VERSION_STRING\"" + case "$DETECTED_MINOR" in + ''|*[!0-9]*) + fatal "$PROGRAM_LABEL minor version from \"$VERSION_STRING\" is not a number. Detected: \"$DETECTED_MINOR\"" + esac + + case $KIND in + major) + echo $DETECTED_MAJOR + exit;; + minor) + echo $DETECTED_MINOR + exit;; + *) + echo $DETECTED_MAJOR.$DETECTED_MINOR + exit;; + esac + + echo $VERSION +} + + +compute_node_args() { + ARGS="" + + NODE_MAJOR=$(get_program_version "node" "major") + [ "$NODE_MAJOR" -eq "10" ] && ARGS="$ARGS --experimental-worker" + + echo $ARGS +} + + +require_minimal_version() { + PROGRAM_LABEL="$1" + VERSION="$2" + REQUIRED_MAJOR="$3" + REQUIRED_MINOR="$4" + + VERSION_MAJOR=$(pecho "$VERSION" | cut -s -d "." -f 1) + VERSION_MINOR=$(pecho "$VERSION" | cut -s -d "." -f 2) + + [ "$VERSION_MAJOR" -gt "$REQUIRED_MAJOR" ] || ([ "$VERSION_MAJOR" -eq "$REQUIRED_MAJOR" ] && [ "$VERSION_MINOR" -ge "$REQUIRED_MINOR" ]) \ + || fatal "Your $PROGRAM_LABEL version \"$VERSION_MAJOR.$VERSION_MINOR\" is too old. $PROGRAM_LABEL $REQUIRED_MAJOR.$REQUIRED_MINOR.x or higher is required." +} diff --git a/bin/installDeps.sh b/bin/installDeps.sh index 5e0bbb931..bdce38fc7 100755 --- a/bin/installDeps.sh +++ b/bin/installDeps.sh @@ -1,52 +1,11 @@ #!/bin/sh -# minimum required node version -REQUIRED_NODE_MAJOR=10 -REQUIRED_NODE_MINOR=13 - -# minimum required npm version -REQUIRED_NPM_MAJOR=5 -REQUIRED_NPM_MINOR=5 - -pecho() { printf %s\\n "$*"; } -log() { pecho "$@"; } -error() { log "ERROR: $@" >&2; } -fatal() { error "$@"; exit 1; } -is_cmd() { command -v "$@" >/dev/null 2>&1; } - -require_minimal_version() { - PROGRAM_LABEL="$1" - VERSION_STRING="$2" - REQUIRED_MAJOR="$3" - REQUIRED_MINOR="$4" - - # Flag -s (--only-delimited on GNU cut) ensures no string is returned - # when there is no match - DETECTED_MAJOR=$(pecho "$VERSION_STRING" | cut -s -d "." -f 1) - DETECTED_MINOR=$(pecho "$VERSION_STRING" | cut -s -d "." -f 2) - - [ -n "$DETECTED_MAJOR" ] || fatal "Cannot extract $PROGRAM_LABEL major version from version string \"$VERSION_STRING\"" - - [ -n "$DETECTED_MINOR" ] || fatal "Cannot extract $PROGRAM_LABEL minor version from version string \"$VERSION_STRING\"" - - case "$DETECTED_MAJOR" in - ''|*[!0-9]*) - fatal "$PROGRAM_LABEL major version from \"$VERSION_STRING\" is not a number. Detected: \"$DETECTED_MAJOR\"" - ;; - esac - - case "$DETECTED_MINOR" in - ''|*[!0-9]*) - fatal "$PROGRAM_LABEL minor version from \"$VERSION_STRING\" is not a number. Detected: \"$DETECTED_MINOR\"" - esac - - [ "$DETECTED_MAJOR" -gt "$REQUIRED_MAJOR" ] || ([ "$DETECTED_MAJOR" -eq "$REQUIRED_MAJOR" ] && [ "$DETECTED_MINOR" -ge "$REQUIRED_MINOR" ]) \ - || fatal "Your $PROGRAM_LABEL version \"$VERSION_STRING\" is too old. $PROGRAM_LABEL $REQUIRED_MAJOR.$REQUIRED_MINOR.x or higher is required." -} - # Move to the folder where ep-lite is installed cd "$(dirname "$0")"/.. +# Source constants and usefull functions +. bin/functions.sh + # Is node installed? # Not checking io.js, default installation creates a symbolic link to node is_cmd node || fatal "Please install node.js ( https://nodejs.org )" @@ -55,15 +14,10 @@ is_cmd node || fatal "Please install node.js ( https://nodejs.org )" is_cmd npm || fatal "Please install npm ( https://npmjs.org )" # Check npm version -NPM_VERSION_STRING=$(npm --version) - -require_minimal_version "npm" "$NPM_VERSION_STRING" "$REQUIRED_NPM_MAJOR" "$REQUIRED_NPM_MINOR" +require_minimal_version "npm" $(get_program_version "npm") "$REQUIRED_NPM_MAJOR" "$REQUIRED_NPM_MINOR" # Check node version -NODE_VERSION_STRING=$(node --version) -NODE_VERSION_STRING=${NODE_VERSION_STRING#"v"} - -require_minimal_version "nodejs" "$NODE_VERSION_STRING" "$REQUIRED_NODE_MAJOR" "$REQUIRED_NODE_MINOR" +require_minimal_version "nodejs" $(get_program_version "node") "$REQUIRED_NODE_MAJOR" "$REQUIRED_NODE_MINOR" # Get the name of the settings file settings="settings.json" diff --git a/bin/run.sh b/bin/run.sh index ff6b3de09..50bce4bdd 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -1,13 +1,11 @@ #!/bin/sh -pecho() { printf %s\\n "$*"; } -log() { pecho "$@"; } -error() { log "ERROR: $@" >&2; } -fatal() { error "$@"; exit 1; } - # Move to the folder where ep-lite is installed cd "$(dirname "$0")"/.. +# Source constants and usefull functions +. bin/functions.sh + ignoreRoot=0 for ARG in "$@"; do if [ "$ARG" = "--root" ]; then @@ -34,4 +32,4 @@ bin/installDeps.sh "$@" || exit 1 log "Starting Etherpad..." SCRIPTPATH=$(pwd -P) -exec node "$SCRIPTPATH/node_modules/ep_etherpad-lite/node/server.js" "$@" +exec node $(compute_node_args) "$SCRIPTPATH/node_modules/ep_etherpad-lite/node/server.js" "$@" From 92d4b8b649fadc54e9781c8f332f96a3b7588d1c Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 6 Oct 2020 14:21:09 +0100 Subject: [PATCH 088/315] tests: re-enable docker tests in travis (#4395) Thanks to node10 having better support we can re-enable these tests. --- .travis.yml | 30 ++++++++++++++---------------- tests/frontend/travis/runner.sh | 2 +- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2fab80ceb..4891db8fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,14 +47,13 @@ jobs: - "cd src && npm install && cd -" script: - "tests/frontend/travis/runnerBackend.sh" -## Temporarily commented out the Dockerfile tests -# - name: "Test the Dockerfile" -# install: -# - "cd src && npm install && cd -" -# script: -# - "docker build -t etherpad:test ." -# - "docker run -d -p 9001:9001 etherpad:test && sleep 3" -# - "cd src && npm run test-container" + - name: "Test the Dockerfile" + install: + - "cd src && npm install && cd -" + script: + - "docker build -t etherpad:test ." + - "docker run -d -p 9001:9001 etherpad:test && sleep 3" + - "cd src && npm run test-container" - name: "Load test Etherpad without Plugins" install: - "bin/installDeps.sh" @@ -83,14 +82,13 @@ jobs: - "cd src && npm install && cd -" script: - "tests/frontend/travis/runnerBackend.sh" -## Temporarily commented out the Dockerfile tests -# - name: "Test the Dockerfile" -# install: -# - "cd src && npm install && cd -" -# script: -# - "docker build -t etherpad:test ." -# - "docker run -d -p 9001:9001 etherpad:test && sleep 3" -# - "cd src && npm run test-container" + - name: "Test the Dockerfile" + install: + - "cd src && npm install && cd -" + script: + - "docker build -t etherpad:test ." + - "docker run -d -p 9001:9001 etherpad:test && sleep 3" + - "cd src && npm run test-container" - name: "Load test Etherpad with Plugins" install: - "bin/installDeps.sh" diff --git a/tests/frontend/travis/runner.sh b/tests/frontend/travis/runner.sh index ffc6bbd5b..bbb4680d1 100755 --- a/tests/frontend/travis/runner.sh +++ b/tests/frontend/travis/runner.sh @@ -16,7 +16,7 @@ cd "${MY_DIR}/../../../" # This is possible because the "install" section of .travis.yml already contains # a call to bin/installDeps.sh echo "Running Etherpad directly, assuming bin/installDeps.sh has already been run" -node node_modules/ep_etherpad-lite/node/server.js "${@}" & +node node_modules/ep_etherpad-lite/node/server.js --experimental-worker "${@}" & echo "Now I will try for 15 seconds to connect to Etherpad on http://localhost:9001" From 5aa318a09b4595eee1da715e10d7fe69cbd1d44b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 6 Oct 2020 17:37:26 -0400 Subject: [PATCH 089/315] Call the aceAttribClasses hook synchronously We could instead await the results of the hook, but then all callers and their callers recursively would have to be converted to async, and that's a huge change. --- src/static/js/linestylefilter.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index 85e69b2be..a3cf68433 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -59,11 +59,8 @@ linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFun { // Plugin Hook to add more Attrib Classes - hooks.aCallAll('aceAttribClasses', linestylefilter.ATTRIB_CLASSES, function(err, ATTRIB_CLASSES){ - if(ATTRIB_CLASSES.length >= 1){ - linestylefilter.ATTRIB_CLASSES = ATTRIB_CLASSES[0]; - } - }); + const results = hooks.callAll('aceAttribClasses', linestylefilter.ATTRIB_CLASSES); + if (results.length >= 1) linestylefilter.ATTRIB_CLASSES = results[0]; if (lineLength == 0) return textAndClassFunc; From ba6bdf35be1d35ca3a7f57e6066c92eb228f6fc7 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 6 Oct 2020 17:47:03 -0400 Subject: [PATCH 090/315] Make the aceAttribClasses hook harder to misuse --- doc/api/hooks_server-side.md | 17 ++++++++++------- src/static/js/linestylefilter.js | 5 +++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 57ae1c117..9c8630b4d 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -679,18 +679,21 @@ exports.stylesForExport = function(hook, padId, cb){ ## aceAttribClasses Called from: src/static/js/linestylefilter.js -Things in context: -1. Attributes - Object of Attributes +This hook is called when attributes are investigated on a line. It is useful if +you want to add another attribute type or property type to a pad. -This hook is called when attributes are investigated on a line. It is useful if you want to add another attribute type or property type to a pad. +An attributes object is passed to the aceAttribClasses hook functions instead of +the usual context object. A hook function can either modify this object directly +or provide an object whose properties will be assigned to the attributes object. Example: ``` -exports.aceAttribClasses = function(hook_name, attr, cb){ - attr.sub = 'tag:sub'; - cb(attr); -} +exports.aceAttribClasses = (hookName, attrs, cb) => { + return cb([{ + sub: 'tag:sub', + }]); +}; ``` ## exportFileName diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index a3cf68433..b3496a818 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -59,8 +59,9 @@ linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFun { // Plugin Hook to add more Attrib Classes - const results = hooks.callAll('aceAttribClasses', linestylefilter.ATTRIB_CLASSES); - if (results.length >= 1) linestylefilter.ATTRIB_CLASSES = results[0]; + for (const attribClasses of hooks.callAll('aceAttribClasses', linestylefilter.ATTRIB_CLASSES)) { + Object.assign(linestylefilter.ATTRIB_CLASSES, attribClasses); + } if (lineLength == 0) return textAndClassFunc; From 661a89355f1d355f410feffcfe234b8974a125b7 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 6 Oct 2020 19:44:34 -0400 Subject: [PATCH 091/315] socketio: Mimic what Express does to get client IP address This also makes it easier for plugins to get the client IP address. --- src/node/handler/PadMessageHandler.js | 31 ++++++--------------------- src/node/handler/SocketIORouter.js | 10 --------- src/node/hooks/express/socketio.js | 9 ++++++++ src/node/utils/RemoteAddress.js | 1 - src/package.json | 1 + 5 files changed, 17 insertions(+), 35 deletions(-) delete mode 100644 src/node/utils/RemoteAddress.js diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 0794a7b1d..78a8b9858 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -35,7 +35,6 @@ var _ = require('underscore'); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); var channels = require("channels"); var stats = require('../stats'); -var remoteAddress = require("../utils/RemoteAddress").remoteAddress; const assert = require('assert').strict; const nodeify = require("nodeify"); const { RateLimiterMemory } = require('rate-limiter-flexible'); @@ -127,19 +126,11 @@ exports.handleDisconnect = async function(client) // if this connection was already etablished with a handshake, send a disconnect message to the others if (session && session.author) { - // Get the IP address from our persistant object - let ip = remoteAddress[client.id]; - - // Anonymize the IP address if IP logging is disabled - if (settings.disableIPlogging) { - ip = 'ANONYMOUS'; - } - const {session: {user} = {}} = client.client.request; accessLogger.info('[LEAVE]' + ` pad:${session.padId}` + ` socket:${client.id}` + - ` IP:${ip}` + + ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : client.request.ip}` + ` authorID:${session.author}` + ((user && user.username) ? ` username:${user.username}` : '')); @@ -181,11 +172,11 @@ exports.handleMessage = async function(client, message) var env = process.env.NODE_ENV || 'development'; if (env === 'production') { - const clientIPAddress = remoteAddress[client.id]; try { - await rateLimiter.consume(clientIPAddress); // consume 1 point per event from IP - }catch(e){ - console.warn("Rate limited: ", clientIPAddress, " to reduce the amount of rate limiting that happens edit the rateLimit values in settings.json"); + await rateLimiter.consume(client.request.ip); // consume 1 point per event from IP + } catch (e) { + console.warn(`Rate limited: ${client.request.ip} to reduce the amount of rate limiting ` + + 'that happens edit the rateLimit values in settings.json'); stats.meter('rateLimited').mark(); client.json.send({disconnect:"rateLimited"}); return; @@ -239,7 +230,7 @@ exports.handleMessage = async function(client, message) 'Rejecting message from client because the author ID changed mid-session.' + ' Bad or missing token or sessionID?' + ` socket:${client.id}` + - ` IP:${settings.disableIPlogging ? ANONYMOUS : remoteAddress[client.id]}` + + ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : client.request.ip}` + ` originalAuthorID:${thisSession.author}` + ` newAuthorID:${authorID}` + ((user && user.username) ? ` username:${user.username}` : '') + @@ -967,19 +958,11 @@ async function handleClientReady(client, message, authorID) sessionInfo.readonly = padIds.readonly || !webaccess.userCanModify(message.padId, client.client.request); - // Log creation/(re-)entering of a pad - let ip = remoteAddress[client.id]; - - // Anonymize the IP address if IP logging is disabled - if (settings.disableIPlogging) { - ip = 'ANONYMOUS'; - } - const {session: {user} = {}} = client.client.request; accessLogger.info(`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` + ` pad:${padIds.padId}` + ` socket:${client.id}` + - ` IP:${ip}` + + ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : client.request.ip}` + ` authorID:${authorID}` + ((user && user.username) ? ` username:${user.username}` : '')); diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index a52393634..d7712b8ee 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -23,7 +23,6 @@ var log4js = require('log4js'); var messageLogger = log4js.getLogger("message"); var securityManager = require("../db/SecurityManager"); var readOnlyManager = require("../db/ReadOnlyManager"); -var remoteAddress = require("../utils/RemoteAddress").remoteAddress; var settings = require('../utils/Settings'); /** @@ -56,15 +55,6 @@ exports.setSocketIO = function(_socket) { socket.sockets.on('connection', function(client) { - // Broken: See http://stackoverflow.com/questions/4647348/send-message-to-specific-client-with-socket-io-and-node-js - // Fixed by having a persistant object, ideally this would actually be in the database layer - // TODO move to database layer - if (settings.trustProxy && client.handshake.headers['x-forwarded-for'] !== undefined) { - remoteAddress[client.id] = client.handshake.headers['x-forwarded-for']; - } else { - remoteAddress[client.id] = client.handshake.address; - } - // wrap the original send function to log the messages client._send = client.send; client.send = function(message) { diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index f5964f18a..aae450da3 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -1,4 +1,5 @@ const express = require("../express"); +const proxyaddr = require('proxy-addr'); var settings = require('../../utils/Settings'); var socketio = require('socket.io'); var socketIORouter = require("../../handler/SocketIORouter"); @@ -38,6 +39,14 @@ exports.expressCreateServer = function (hook_name, args, cb) { io.use((socket, next) => { const req = socket.request; + // Express sets req.ip but socket.io does not. Replicate Express's behavior here. + if (req.ip == null) { + if (settings.trustProxy) { + req.ip = proxyaddr(req, args.app.get('trust proxy fn')); + } else { + req.ip = socket.handshake.address; + } + } if (!req.headers.cookie) { // socketio.js-client on node.js doesn't support cookies (see https://git.io/JU8u9), so the // token and express_sid cookies have to be passed via a query parameter for unit tests. diff --git a/src/node/utils/RemoteAddress.js b/src/node/utils/RemoteAddress.js deleted file mode 100644 index 86a4a5b26..000000000 --- a/src/node/utils/RemoteAddress.js +++ /dev/null @@ -1 +0,0 @@ -exports.remoteAddress = {}; diff --git a/src/package.json b/src/package.json index 155638161..1d57e6116 100644 --- a/src/package.json +++ b/src/package.json @@ -55,6 +55,7 @@ "nodeify": "1.0.1", "npm": "6.14.8", "openapi-backend": "2.4.1", + "proxy-addr": "^2.0.6", "rate-limiter-flexible": "^2.1.4", "rehype": "^10.0.0", "rehype-format": "^3.0.1", From 45bee54aa0579c463e9f11f32036ffb802b767dd Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 6 Oct 2020 15:53:14 -0400 Subject: [PATCH 092/315] HTML export: Await async hook completion before processing results --- src/node/utils/ExportHtml.js | 43 ++++++++++++++++++------------------ src/node/utils/padDiff.js | 2 +- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js index d0ebf20de..2c5a2c213 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.js @@ -33,13 +33,13 @@ async function getPadHTML(pad, revNum) } // convert atext to html - return getHTMLFromAtext(pad, atext); + return await getHTMLFromAtext(pad, atext); } exports.getPadHTML = getPadHTML; exports.getHTMLFromAtext = getHTMLFromAtext; -function getHTMLFromAtext(pad, atext, authorColors) +async function getHTMLFromAtext(pad, atext, authorColors) { var apool = pad.apool(); var textLines = atext.text.slice(0, -1).split('\n'); @@ -48,22 +48,23 @@ function getHTMLFromAtext(pad, atext, authorColors) var tags = ['h1', 'h2', 'strong', 'em', 'u', 's']; var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; - // prepare tags stored as ['tag', true] to be exported - hooks.aCallAll("exportHtmlAdditionalTags", pad, function(err, newProps){ - newProps.forEach(function (propName, i) { - tags.push(propName); - props.push(propName); - }); - }); - - // prepare tags stored as ['tag', 'value'] to be exported. This will generate HTML - // with tags like - hooks.aCallAll("exportHtmlAdditionalTagsWithData", pad, function(err, newProps){ - newProps.forEach(function (propName, i) { - tags.push('span data-' + propName[0] + '="' + propName[1] + '"'); - props.push(propName); - }); - }); + await Promise.all([ + // prepare tags stored as ['tag', true] to be exported + hooks.aCallAll('exportHtmlAdditionalTags', pad).then((newProps) => { + newProps.forEach((prop) => { + tags.push(prop); + props.push(prop); + }); + }), + // prepare tags stored as ['tag', 'value'] to be exported. This will generate HTML with tags + // like + hooks.aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps) => { + newProps.forEach((prop) => { + tags.push(`span data-${prop[0]}="${prop[1]}"`); + props.push(prop); + }); + }), + ]); // holds a map of used styling attributes (*1, *2, etc) in the apool // and maps them to an index in props @@ -336,7 +337,7 @@ function getHTMLFromAtext(pad, atext, authorColors) { nextLine = _analyzeLine(textLines[i + 1], attribLines[i + 1], apool); } - hooks.aCallAll('getLineHTMLForExport', context); + await hooks.aCallAll('getLineHTMLForExport', context); //To create list parent elements if ((!prevLine || prevLine.listLevel !== line.listLevel) || (prevLine && line.listTypeName !== prevLine.listTypeName)) { @@ -476,8 +477,8 @@ function getHTMLFromAtext(pad, atext, authorColors) padId: pad.id }; - hooks.aCallAll("getLineHTMLForExport", context); - pieces.push(context.lineContent, "
            "); + await hooks.aCallAll('getLineHTMLForExport', context); + pieces.push(context.lineContent, '
            '); } } diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js index 7cf29aba4..3018813f7 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.js @@ -206,7 +206,7 @@ PadDiff.prototype.getHtml = async function() { let authorColors = await this._pad.getAllAuthorColors(); // convert the atext to html - this._html = exportHtml.getHTMLFromAtext(this._pad, atext, authorColors); + this._html = await exportHtml.getHTMLFromAtext(this._pad, atext, authorColors); return this._html; } From 66df0a572f03d1c9cbe7364ea8a81a3c05c17518 Mon Sep 17 00:00:00 2001 From: John McLear Date: Wed, 7 Oct 2020 13:43:54 +0100 Subject: [PATCH 093/315] Security: FEATURE REMOVAL: Remove all plain text password logic and ui (#4178) This will be a breaking change for some people. We removed all internal password control logic. If this affects you, you have two options: 1. Use a plugin for authentication and use session based pad access (recommended). 1. Use a plugin for password setting. The reasoning for removing this feature is to reduce the overall security footprint of Etherpad. It is unnecessary and cumbersome to keep this feature and with the thousands of available authentication methods available in the world our focus should be on supporting those and allowing more granual access based on their implementations (instead of half assed baking our own). --- CHANGELOG.md | 5 +++ doc/api/hooks_server-side.md | 5 +-- doc/api/http_api.md | 18 --------- doc/docker.md | 1 - settings.json.docker | 6 --- settings.json.template | 6 --- src/locales/en.json | 2 - src/node/db/API.js | 39 -------------------- src/node/db/Pad.js | 30 --------------- src/node/db/SecurityManager.js | 30 +++------------ src/node/handler/APIHandler.js | 2 - src/node/handler/PadMessageHandler.js | 5 +-- src/node/handler/SocketIORouter.js | 21 ++--------- src/node/hooks/express/importexport.js | 2 +- src/node/hooks/express/openapi.js | 9 ----- src/node/padaccess.js | 2 +- src/node/utils/Settings.js | 5 --- src/static/css/pad/loadingbox.css | 8 +--- src/static/js/pad.js | 28 -------------- src/static/js/timeslider.js | 1 - src/static/tests.html | 2 - src/templates/pad.html | 9 ----- tests/backend/specs/api/sessionsAndGroups.js | 32 ---------------- tests/backend/specs/socketio.js | 1 - 24 files changed, 23 insertions(+), 246 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12c5ea66e..4a797552c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Develop -- TODO Change to 1.8.x. ### Compatibility-breaking changes +* **IMPORTANT:** It is no longer possible to protect a group pad with a + password. All API calls to `setPassword` or `isPasswordProtected` will fail. + Existing group pads that were previously password protected will no longer be + password protected. If you need fine-grained access control, you can restrict + API session creation in your frontend service, or you can use plugins. * Authorization failures now return 403 by default instead of 401 * The `authorize` hook is now only called after successful authentication. Use the new `preAuthorize` hook if you need to bypass diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 9c8630b4d..10d738634 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -139,9 +139,8 @@ Called from: src/node/db/SecurityManager.js Things in context: 1. padID - the pad the user wants to access -2. password - the password the user has given to access the pad -3. token - the token of the author -4. sessionCookie - the session the use has +2. token - the token of the author +3. sessionCookie - the session the use has This hook gets called when the access to the concrete pad is being checked. Return `false` to deny access. diff --git a/doc/api/http_api.md b/doc/api/http_api.md index 7cdadefcc..fb570a393 100644 --- a/doc/api/http_api.md +++ b/doc/api/http_api.md @@ -581,24 +581,6 @@ return true of false * `{code: 0, message:"ok", data: {publicStatus: true}}` * `{code: 1, message:"padID does not exist", data: null}` -#### setPassword(padID, password) - * API >= 1 - -returns ok or an error message - -*Example returns:* - * `{code: 0, message:"ok", data: null}` - * `{code: 1, message:"padID does not exist", data: null}` - -#### isPasswordProtected(padID) - * API >= 1 - -returns true or false - -*Example returns:* - * `{code: 0, message:"ok", data: {passwordProtection: true}}` - * `{code: 1, message:"padID does not exist", data: null}` - #### listAuthorsOfPad(padID) * API >= 1 diff --git a/doc/docker.md b/doc/docker.md index 5eb0a16ad..e12a8fb12 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -171,7 +171,6 @@ For the editor container, you can also make it full width by adding `full-width- | `SUPPRESS_ERRORS_IN_PAD_TEXT` | Should we suppress errors from being visible in the default Pad Text? | `false` | | `REQUIRE_SESSION` | If this option is enabled, a user must have a session to access pads. This effectively allows only group pads to be accessed. | `false` | | `EDIT_ONLY` | Users may edit pads but not create new ones. Pad creation is only via the API. This applies both to group pads and regular pads. | `false` | -| `SESSION_NO_PASSWORD` | If set to true, those users who have a valid session will automatically be granted access to password protected pads. | `false` | | `MINIFY` | If true, all css & js will be minified before sending to the client. This will improve the loading performance massively, but makes it difficult to debug the javascript/css | `true` | | `MAX_AGE` | How long may clients use served javascript code (in seconds)? Not setting this may cause problems during deployment. Set to 0 to disable caching. | `21600` (6 hours) | | `ABIWORD` | Absolute path to the Abiword executable. Abiword is needed to get advanced import/export features of pads. Setting it to null disables Abiword and will only allow plain text and HTML import/exports. | `null` | diff --git a/settings.json.docker b/settings.json.docker index 5e5d806fa..081af38b8 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -260,12 +260,6 @@ */ "editOnly": "${EDIT_ONLY:false}", - /* - * If set to true, those users who have a valid session will automatically be - * granted access to password protected pads. - */ - "sessionNoPassword": "${SESSION_NO_PASSWORD:false}", - /* * If true, all css & js will be minified before sending to the client. * diff --git a/settings.json.template b/settings.json.template index e4b2b3d3c..f4cfdea62 100644 --- a/settings.json.template +++ b/settings.json.template @@ -263,12 +263,6 @@ */ "editOnly": false, - /* - * If set to true, those users who have a valid session will automatically be - * granted access to password protected pads. - */ - "sessionNoPassword": false, - /* * If true, all css & js will be minified before sending to the client. * diff --git a/src/locales/en.json b/src/locales/en.json index a65e62885..319746e74 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -60,9 +60,7 @@ "pad.loading": "Loading...", "pad.noCookie": "Cookie could not be found. Please allow cookies in your browser! Your session and settings will not be saved between visits. This may be due to Etherpad being included in an iFrame in some Browsers. Please ensure Etherpad is on the same subdomain/domain as the parent iFrame", - "pad.passwordRequired": "You need a password to access this pad", "pad.permissionDenied": "You do not have permission to access this pad", - "pad.wrongPassword": "Your password was wrong", "pad.settings.padSettings": "Pad Settings", "pad.settings.myView": "My View", diff --git a/src/node/db/API.js b/src/node/db/API.js index 9f5b786f9..1b5136b61 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -684,7 +684,6 @@ exports.setPublicStatus = async function(padID, publicStatus) publicStatus = (publicStatus.toLowerCase() === "true"); } - // set the password await pad.setPublicStatus(publicStatus); } @@ -706,44 +705,6 @@ exports.getPublicStatus = async function(padID) return { publicStatus: pad.getPublicStatus() }; } -/** -setPassword(padID, password) returns ok or a error message - -Example returns: - -{code: 0, message:"ok", data: null} -{code: 1, message:"padID does not exist", data: null} -*/ -exports.setPassword = async function(padID, password) -{ - // ensure this is a group pad - checkGroupPad(padID, "password"); - - // get the pad - let pad = await getPadSafe(padID, true); - - // set the password - await pad.setPassword(password === '' ? null : password); -} - -/** -isPasswordProtected(padID) returns true or false - -Example returns: - -{code: 0, message:"ok", data: {passwordProtection: true}} -{code: 1, message:"padID does not exist", data: null} -*/ -exports.isPasswordProtected = async function(padID) -{ - // ensure this is a group pad - checkGroupPad(padID, "password"); - - // get the pad - let pad = await getPadSafe(padID, true); - return { isPasswordProtected: pad.isPasswordProtected() }; -} - /** listAuthorsOfPad(padID) returns an array of authors who contributed to this pad diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index 85188d2b6..f41e64a4c 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -37,7 +37,6 @@ let Pad = function Pad(id) { this.head = -1; this.chatHead = -1; this.publicStatus = false; - this.passwordHash = null; this.id = id; this.savedRevisions = []; }; @@ -595,19 +594,6 @@ Pad.prototype.setPublicStatus = async function setPublicStatus(publicStatus) { await this.saveToDatabase(); }; -Pad.prototype.setPassword = async function setPassword(password) { - this.passwordHash = password == null ? null : hash(password, generateSalt()); - await this.saveToDatabase(); -}; - -Pad.prototype.isCorrectPassword = function isCorrectPassword(password) { - return compare(this.passwordHash, password); -}; - -Pad.prototype.isPasswordProtected = function isPasswordProtected() { - return this.passwordHash != null; -}; - Pad.prototype.addSavedRevision = async function addSavedRevision(revNum, savedById, label) { // if this revision is already saved, return silently for (var i in this.savedRevisions) { @@ -633,19 +619,3 @@ Pad.prototype.getSavedRevisions = function getSavedRevisions() { return this.savedRevisions; }; -/* Crypto helper methods */ - -function hash(password, salt) { - var shasum = crypto.createHash('sha512'); - shasum.update(password + salt); - - return shasum.digest("hex") + "$" + salt; -} - -function generateSalt() { - return randomString(86); -} - -function compare(hashStr, password) { - return hash(password, hashStr.split("$")[1]) === hashStr; -} diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 818f04d9d..073dcdf8c 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -28,8 +28,6 @@ var log4js = require('log4js'); var authLogger = log4js.getLogger("auth"); const DENY = Object.freeze({accessStatus: 'deny'}); -const WRONG_PASSWORD = Object.freeze({accessStatus: 'wrongPassword'}); -const NEED_PASSWORD = Object.freeze({accessStatus: 'needPassword'}); /** * Determines whether the user can access a pad. @@ -42,16 +40,14 @@ const NEED_PASSWORD = Object.freeze({accessStatus: 'needPassword'}); * is false and the user is accessing a public pad. If there is not an author already associated * with this token then a new author object is created (including generating an author ID) and * associated with this token. - * @param password is the password the user has given to access this pad. It can be null. * @param userSettings is the settings.users[username] object (or equivalent from an authn plugin). - * @return {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx}. The caller - * must use the author ID returned in this object when making any changes associated with the - * author. + * @return {accessStatus: grant|deny, authorID: a.xxxxxx}. The caller must use the author ID + * returned in this object when making any changes associated with the author. * * 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, password, userSettings) +exports.checkAccess = async function(padID, sessionCookie, token, userSettings) { if (!padID) { authLogger.debug('access denied: missing padID'); @@ -84,7 +80,7 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user // allow plugins to deny access const isFalse = (x) => x === false; - if (hooks.callAll('onAccessCheck', {padID, password, token, sessionCookie}).some(isFalse)) { + if (hooks.callAll('onAccessCheck', {padID, token, sessionCookie}).some(isFalse)) { authLogger.debug('access denied: an onAccessCheck hook function returned false'); return DENY; } @@ -112,8 +108,7 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user }; if (!padID.includes('$')) { - // Only group pads can be private or have passwords, so there is nothing more to check for this - // non-group pad. + // Only group pads can be private, so there is nothing more to check for this non-group pad. return grant; } @@ -122,7 +117,7 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user authLogger.debug('access denied: must have an HTTP API session to create a group pad'); return DENY; } - // Creating a group pad, so there is no password or public status to check. + // Creating a group pad, so there is no public status to check. return grant; } @@ -133,18 +128,5 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user return DENY; } - const passwordExempt = settings.sessionNoPassword && sessionAuthorID != null; - const requirePassword = pad.isPasswordProtected() && !passwordExempt; - if (requirePassword) { - if (password == null) { - authLogger.debug('access denied: password required'); - return NEED_PASSWORD; - } - if (!password || !pad.isCorrectPassword(password)) { - authLogger.debug('access denied: wrong password'); - return WRONG_PASSWORD; - } - } - return grant; }; diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index cb714460f..f4e0be38e 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -70,8 +70,6 @@ version["1"] = Object.assign({}, , "getReadOnlyID" : ["padID"] , "setPublicStatus" : ["padID", "publicStatus"] , "getPublicStatus" : ["padID"] - , "setPassword" : ["padID", "password"] - , "isPasswordProtected" : ["padID"] , "listAuthorsOfPad" : ["padID"] , "padUsersCount" : ["padID"] } diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 78a8b9858..e068df98f 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -219,7 +219,7 @@ exports.handleMessage = async function(client, message) const {session: {user} = {}} = client.client.request; const {accessStatus, authorID} = - await securityManager.checkAccess(padId, auth.sessionID, auth.token, auth.password, user); + await securityManager.checkAccess(padId, auth.sessionID, auth.token, user); if (accessStatus !== 'grant') { // Access denied. Send the reason to the user. client.json.send({accessStatus}); @@ -826,7 +826,7 @@ async function handleSwitchToPad(client, message, _authorID) const newPadIds = await readOnlyManager.getIds(message.padId); const {session: {user} = {}} = client.client.request; const {accessStatus, authorID} = await securityManager.checkAccess( - newPadIds.padId, message.sessionID, message.token, message.password, user); + newPadIds.padId, message.sessionID, message.token, user); if (accessStatus !== 'grant') { // Access denied. Send the reason to the user. client.json.send({accessStatus}); @@ -868,7 +868,6 @@ function createSessionInfoAuth(sessionInfo, message) sessionID: message.sessionID, padID: message.padId, token: message.token, - password: message.password, }; } diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index d7712b8ee..4eb8b1cca 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -58,7 +58,7 @@ exports.setSocketIO = function(_socket) { // wrap the original send function to log the messages client._send = client.send; client.send = function(message) { - messageLogger.debug("to " + client.id + ": " + stringifyWithoutPassword(message)); + messageLogger.debug(`to ${client.id}: ${JSON.stringify(message)}`); client._send(message); } @@ -69,14 +69,14 @@ exports.setSocketIO = function(_socket) { client.on('message', async function(message) { if (message.protocolVersion && message.protocolVersion != 2) { - messageLogger.warn("Protocolversion header is not correct:" + stringifyWithoutPassword(message)); + messageLogger.warn(`Protocolversion header is not correct: ${JSON.stringify(message)}`); return; } if (!message.component || !components[message.component]) { - messageLogger.error("Can't route the message:" + stringifyWithoutPassword(message)); + messageLogger.error(`Can't route the message: ${JSON.stringify(message)}`); return; } - messageLogger.debug("from " + client.id + ": " + stringifyWithoutPassword(message)); + messageLogger.debug(`from ${client.id}: ${JSON.stringify(message)}`); await components[message.component].handleMessage(client, message); }); @@ -88,16 +88,3 @@ exports.setSocketIO = function(_socket) { }); }); } - -// returns a stringified representation of a message, removes the password -// this ensures there are no passwords in the log -function stringifyWithoutPassword(message) -{ - let newMessage = Object.assign({}, message); - - if (newMessage.password != null) { - newMessage.password = "xxx"; - } - - return JSON.stringify(newMessage); -} diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js index d5e6c3bbd..6b376c0e2 100644 --- a/src/node/hooks/express/importexport.js +++ b/src/node/hooks/express/importexport.js @@ -65,7 +65,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { args.app.post('/p/:pad/import', async function(req, res, next) { const {session: {user} = {}} = req; const {accessStatus} = await securityManager.checkAccess( - req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password, user); + req.params.pad, req.cookies.sessionID, req.cookies.token, user); if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) { return res.status(403).send('Forbidden'); } diff --git a/src/node/hooks/express/openapi.js b/src/node/hooks/express/openapi.js index 76ed66932..bc7473b9c 100644 --- a/src/node/hooks/express/openapi.js +++ b/src/node/hooks/express/openapi.js @@ -201,15 +201,6 @@ const resources = { summary: 'return true of false', responseSchema: { publicStatus: { type: 'boolean' } }, }, - setPassword: { - operationId: 'setPassword', - summary: 'returns ok or a error message', - }, - isPasswordProtected: { - operationId: 'isPasswordProtected', - summary: 'returns true or false', - responseSchema: { passwordProtection: { type: 'boolean' } }, - }, authors: { operationId: 'listAuthorsOfPad', summary: 'returns an array of authors who contributed to this pad', diff --git a/src/node/padaccess.js b/src/node/padaccess.js index 6e294403e..b30c43b40 100644 --- a/src/node/padaccess.js +++ b/src/node/padaccess.js @@ -5,7 +5,7 @@ module.exports = async function (req, res) { try { const {session: {user} = {}} = req; const accessObj = await securityManager.checkAccess( - req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password, user); + req.params.pad, req.cookies.sessionID, req.cookies.token, user); if (accessObj.accessStatus === "grant") { // there is access, continue diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index baf4d539e..ee84f4b8b 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -193,11 +193,6 @@ exports.requireSession = false; */ exports.editOnly = false; -/** - * A flag that bypasses password prompts for users with valid sessions - */ -exports.sessionNoPassword = false; - /** * Max age that responses will have (affects caching layer). */ diff --git a/src/static/css/pad/loadingbox.css b/src/static/css/pad/loadingbox.css index 5e990cd72..dcd00f469 100644 --- a/src/static/css/pad/loadingbox.css +++ b/src/static/css/pad/loadingbox.css @@ -10,10 +10,6 @@ z-index: 100; } -#editorloadingbox .passForm{ - padding:10px; -} - #editorloadingbox input{ padding:10px; } @@ -22,6 +18,6 @@ padding:10px; } -#passwordRequired, #permissionDenied, #wrongPassword, #noCookie { +#permissionDenied, #noCookie { display:none; -} \ No newline at end of file +} diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 416d7897e..9040cc1dd 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -126,15 +126,6 @@ function getUrlVars() return vars; } -function savePassword() -{ - //set the password cookie - Cookies.set('password', $('#passwordinput').val(), {path: document.location.pathname}); - //reload - document.location=document.location; - return false; -} - function sendClientReady(isReconnect, messageType) { messageType = typeof messageType !== 'undefined' ? messageType : 'CLIENT_READY'; @@ -160,7 +151,6 @@ function sendClientReady(isReconnect, messageType) type: messageType, padId: padId, sessionID: Cookies.get('sessionID'), - password: Cookies.get('password'), token: token, protocolVersion: 2 }; @@ -225,10 +215,6 @@ function handshake() //the access was not granted, give the user a message if(obj.accessStatus) { - if(!receivedClientVars){ - $('.passForm').submit(require(module.id).savePassword); - } - if(obj.accessStatus == "deny") { $('#loading').hide(); @@ -241,19 +227,6 @@ function handshake() $("#editorloadingbox").show(); } } - else if(obj.accessStatus == "needPassword") - { - $('#loading').hide(); - $('#passwordRequired').show(); - $("#passwordinput").focus(); - } - else if(obj.accessStatus == "wrongPassword") - { - $('#loading').hide(); - $('#wrongPassword').show(); - $('#passwordRequired').show(); - $("#passwordinput").focus(); - } } //if we haven't recieved the clientVars yet, then this message should it be @@ -954,7 +927,6 @@ exports.settings = settings; exports.randomString = randomString; exports.getParams = getParams; exports.getUrlVars = getUrlVars; -exports.savePassword = savePassword; exports.handshake = handshake; exports.pad = pad; exports.init = init; diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js index 2721bd0e5..86f5f6137 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -113,7 +113,6 @@ function sendSocketMsg(type, data) padId, token, sessionID: Cookies.get('sessionID'), - password: Cookies.get('password'), protocolVersion: 2, }); } diff --git a/src/static/tests.html b/src/static/tests.html index cbcd90af4..3c1f71c10 100644 --- a/src/static/tests.html +++ b/src/static/tests.html @@ -161,7 +161,5 @@
            getReadOnlyID(padID)
            setPublicStatus(padID,publicStatus)
            getPublicStatus(padID)
            -
            setPassword(padID,password)
            -
            isPasswordProtected(padID)
            diff --git a/src/templates/pad.html b/src/templates/pad.html index 0ba811095..9624975ab 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -91,18 +91,9 @@
            -
            -

            You need a password to access this pad

            -
            - - -

            You do not have permission to access this pad

            -
            -

            Your password was wrong

            -
            <% e.begin_block("loading"); %>

            Loading...

            <% e.end_block(); %> diff --git a/tests/backend/specs/api/sessionsAndGroups.js b/tests/backend/specs/api/sessionsAndGroups.js index 1e11ff0a1..61aa3ceb7 100644 --- a/tests/backend/specs/api/sessionsAndGroups.js +++ b/tests/backend/specs/api/sessionsAndGroups.js @@ -51,9 +51,6 @@ describe('API Versioning', function() { -> getPublicStatus(padId) -> setPublicStatus(padId, status) -> getPublicStatus(padId) - -> isPasswordProtected(padID) -- should be false - -> setPassword(padID, password) - -> isPasswordProtected(padID) -- should be true -> listPadsOfAuthor(authorID) */ @@ -269,35 +266,6 @@ describe('API: Pad security', function() { assert.equal(res.body.data.publicStatus, true); }); }); - - it('isPasswordProtected', async function() { - await api.get(endPoint('isPasswordProtected') + `&padID=${padID}`) - .expect(200) - .expect('Content-Type', /json/) - .expect((res) => { - assert.equal(res.body.code, 0); - assert.equal(res.body.data.isPasswordProtected, false); - }); - }); - - it('setPassword', async function() { - await api.get(endPoint('setPassword') + `&padID=${padID}&password=test`) - .expect(200) - .expect('Content-Type', /json/) - .expect((res) => { - assert.equal(res.body.code, 0); - }); - }); - - it('isPasswordProtected after setting password', async function() { - await api.get(endPoint('isPasswordProtected') + `&padID=${padID}`) - .expect(200) - .expect('Content-Type', /json/) - .expect((res) => { - assert.equal(res.body.code, 0); - assert.equal(res.body.data.isPasswordProtected, true); - }); - }); }); // NOT SURE HOW TO POPULAT THIS /-_-\ diff --git a/tests/backend/specs/socketio.js b/tests/backend/specs/socketio.js index e17af09fc..ca3ddc4de 100644 --- a/tests/backend/specs/socketio.js +++ b/tests/backend/specs/socketio.js @@ -83,7 +83,6 @@ const handshake = async (socket, padID) => { type: 'CLIENT_READY', padId: padID, sessionID: null, - password: null, token: 't.12345', protocolVersion: 2, }); From ce0b1511591340385d7f293db71f6ae6c4b82379 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 8 Oct 2020 15:53:01 +0200 Subject: [PATCH 094/315] Localisation updates from https://translatewiki.net. --- src/locales/ar.json | 5 +--- src/locales/ast.json | 5 +--- src/locales/awa.json | 1 - src/locales/az.json | 2 -- src/locales/azb.json | 2 -- src/locales/bcc.json | 2 -- src/locales/be-tarask.json | 5 +--- src/locales/bg.json | 1 - src/locales/bgn.json | 1 - src/locales/bn.json | 2 -- src/locales/br.json | 5 +--- src/locales/bs.json | 2 -- src/locales/ca.json | 5 +--- src/locales/cs.json | 2 -- src/locales/da.json | 2 -- src/locales/de.json | 5 +--- src/locales/diq.json | 5 +--- src/locales/dsb.json | 2 -- src/locales/dty.json | 2 -- src/locales/el.json | 5 +--- src/locales/en-gb.json | 2 -- src/locales/eo.json | 2 -- src/locales/es.json | 2 -- src/locales/et.json | 2 -- src/locales/eu.json | 2 -- src/locales/fa.json | 2 -- src/locales/fi.json | 2 -- src/locales/fo.json | 2 -- src/locales/fr.json | 5 +--- src/locales/gl.json | 2 -- src/locales/gu.json | 2 -- src/locales/he.json | 50 ++++++++++++++++++++++++++++++++------ src/locales/hr.json | 5 +--- src/locales/hrx.json | 2 -- src/locales/hsb.json | 2 -- src/locales/hu.json | 20 ++++++++++++--- src/locales/hy.json | 1 - src/locales/ia.json | 2 -- src/locales/id.json | 2 -- src/locales/is.json | 2 -- src/locales/it.json | 5 +--- src/locales/ja.json | 5 +--- src/locales/kab.json | 2 -- src/locales/km.json | 2 -- src/locales/ko.json | 5 +--- src/locales/ksh.json | 2 -- src/locales/lb.json | 2 -- src/locales/lki.json | 2 -- src/locales/lrc.json | 1 - src/locales/lt.json | 2 -- src/locales/lv.json | 2 -- src/locales/map-bms.json | 2 -- src/locales/mg.json | 1 - src/locales/mk.json | 5 +--- src/locales/ml.json | 2 -- src/locales/mn.json | 1 - src/locales/mnw.json | 1 - src/locales/mr.json | 1 - src/locales/ms.json | 2 -- src/locales/nap.json | 2 -- src/locales/nb.json | 5 +--- src/locales/nds.json | 2 -- src/locales/ne.json | 2 -- src/locales/nl.json | 2 -- src/locales/nn.json | 2 -- src/locales/oc.json | 2 -- src/locales/olo.json | 1 - src/locales/os.json | 2 -- src/locales/pa.json | 2 -- src/locales/pl.json | 5 +--- src/locales/pms.json | 5 +--- src/locales/ps.json | 2 -- src/locales/pt-br.json | 5 +--- src/locales/pt.json | 5 +--- src/locales/qqq.json | 2 -- src/locales/ro.json | 5 +--- src/locales/ru.json | 13 ++++++---- src/locales/sco.json | 2 -- src/locales/sd.json | 3 ++- src/locales/sh.json | 2 -- src/locales/shn.json | 2 -- src/locales/sk.json | 2 -- src/locales/skr-arab.json | 1 - src/locales/sl.json | 2 -- src/locales/sq.json | 2 -- src/locales/sr-ec.json | 2 -- src/locales/sr-el.json | 2 -- src/locales/sv.json | 8 +++--- src/locales/ta.json | 2 -- src/locales/te.json | 1 - src/locales/th.json | 5 +--- src/locales/tr.json | 5 +--- src/locales/uk.json | 5 +--- src/locales/vi.json | 2 -- src/locales/zh-hans.json | 5 +--- src/locales/zh-hant.json | 5 +--- 96 files changed, 98 insertions(+), 241 deletions(-) diff --git a/src/locales/ar.json b/src/locales/ar.json index c9e3ff909..ef5cbf753 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -43,9 +43,7 @@ "pad.colorpicker.cancel": "إلغاء", "pad.loading": "جارٍ التحميل...", "pad.noCookie": "الكوكيز غير متاحة. الرجاء السماح بتحميل الكوكيز على متصفحك!", - "pad.passwordRequired": "تحتاج إلى كلمة سر للوصول إلى هذا الباد", "pad.permissionDenied": "ليس لديك إذن لدخول هذا الباد", - "pad.wrongPassword": "كانت كلمة السر خاطئة", "pad.settings.padSettings": "إعدادات الباد", "pad.settings.myView": "رؤيتي", "pad.settings.stickychat": "الدردشة دائما على الشاشة", @@ -146,6 +144,5 @@ "pad.impexp.importfailed": "فشل الاستيراد", "pad.impexp.copypaste": "الرجاء نسخ/لصق", "pad.impexp.exportdisabled": "تصدير التنسيق {{type}} معطل. يرجى الاتصال بمسؤول النظام الخاص بك للحصول على التفاصيل.", - "pad.impexp.maxFileSize": "الملف كبير جدا. اتصل بإداري الموقع الخاص بك لزيادة حجم الملف المسموح به للاستيراد", - "pad.impexp.permission": "الاستيراد معطل لأنك لم تساهم مسبقا لهذه الباد. من فضلك ساهم على الأقل مرة واحدة قبل الاستيراد" + "pad.impexp.maxFileSize": "الملف كبير جدا. اتصل بإداري الموقع الخاص بك لزيادة حجم الملف المسموح به للاستيراد" } diff --git a/src/locales/ast.json b/src/locales/ast.json index bb18a8b89..f74319be2 100644 --- a/src/locales/ast.json +++ b/src/locales/ast.json @@ -28,9 +28,7 @@ "pad.colorpicker.cancel": "Encaboxar", "pad.loading": "Cargando...", "pad.noCookie": "Nun pudo alcontrase la cookie. ¡Por favor, permite les cookies nel navegador! La sesión y preferencies nun se guarden ente visites. Esto pué debese a qu'Etherpad inclúyese nun iFrame en dalgunos restoladores. Asegúrate de qu'Etherpad tea nel mesmu subdominiu/dominiu que la iFrame padre", - "pad.passwordRequired": "Necesites una contraseña pa entrar a esti bloc", "pad.permissionDenied": "Nun tienes permisu pa entrar a esti bloc", - "pad.wrongPassword": "La contraseña era incorreuta", "pad.settings.padSettings": "Configuración del bloc", "pad.settings.myView": "la mio vista", "pad.settings.stickychat": "Alderique en pantalla siempres", @@ -131,6 +129,5 @@ "pad.impexp.importfailed": "Falló la importación", "pad.impexp.copypaste": "Por favor, copia y apega", "pad.impexp.exportdisabled": "La esportación en formatu {{type}} ta desactivada. Por favor, comunica col alministrador del sistema pa más detalles.", - "pad.impexp.maxFileSize": "El ficheru ye demasiao grande. Comunícate col alministrador del sitiu p'aumentar el tamañu de ficheru permitíu na importación", - "pad.impexp.permission": "La importación ta desactivada porque nunca contribuisti nesti bloc. Contribuye polo menos una vez antes d'importar" + "pad.impexp.maxFileSize": "El ficheru ye demasiao grande. Comunícate col alministrador del sitiu p'aumentar el tamañu de ficheru permitíu na importación" } diff --git a/src/locales/awa.json b/src/locales/awa.json index 40f66e142..31b8b5c4c 100644 --- a/src/locales/awa.json +++ b/src/locales/awa.json @@ -18,7 +18,6 @@ "pad.colorpicker.save": "सहेजा जाय", "pad.colorpicker.cancel": "रद्द करा जाय", "pad.loading": "लोड होत है...", - "pad.wrongPassword": "आप कय पासवर्ड गलत रहा", "pad.settings.padSettings": "प्याड सेटिङ्ग", "pad.settings.myView": "हमार दृष्य", "pad.settings.colorcheck": "लेखकीय रङ्ग", diff --git a/src/locales/az.json b/src/locales/az.json index 52a27df92..a1bcb00ee 100644 --- a/src/locales/az.json +++ b/src/locales/az.json @@ -34,9 +34,7 @@ "pad.colorpicker.cancel": "İmtina", "pad.loading": "Yüklənir...", "pad.noCookie": "Çərəz tapıla bilmədi. Lütfən səyyahınızda çərəzlərə icazə verinǃ", - "pad.passwordRequired": "Bu lövhəyə daxil olmaq üçün parol lazımdır", "pad.permissionDenied": "Bu lövhəyə daxil olmaq üçün icazəniz yoxdur", - "pad.wrongPassword": "Sizin parolunuz səhvdir", "pad.settings.padSettings": "Lövhə nizamlamaları", "pad.settings.myView": "Mənim Görüntüm", "pad.settings.stickychat": "Söhbət həmişə ekranda", diff --git a/src/locales/azb.json b/src/locales/azb.json index 7da2c6122..b92a4bcdb 100644 --- a/src/locales/azb.json +++ b/src/locales/azb.json @@ -31,9 +31,7 @@ "pad.colorpicker.cancel": "وازگئچ", "pad.loading": "یوکلنیر...", "pad.noCookie": "کوکی تاپیلمادی. لوطفن براوزرینیزده کوکیلره ایجازه وئرین!", - "pad.passwordRequired": "بو نوت دفترچه سینه ال تاپماق اوچون بیر رمزه احتیاجینیز واردیر.", "pad.permissionDenied": "بو نوت دفترچه سینه ال تاپماق اوچون ایجازه نیز یوخدور.", - "pad.wrongPassword": "سیزین رمزینیز دوز دئییل", "pad.settings.padSettings": "یادداشت دفترچه سینین تنظیملر", "pad.settings.myView": "منیم گورنتوم", "pad.settings.stickychat": "نمایش صفحه سینده همیشه چت اولسون", diff --git a/src/locales/bcc.json b/src/locales/bcc.json index a91048951..1daef8ec2 100644 --- a/src/locales/bcc.json +++ b/src/locales/bcc.json @@ -27,9 +27,7 @@ "pad.colorpicker.save": "زاپاس کورتین", "pad.colorpicker.cancel": "کنسیل", "pad.loading": "...بار بیت", - "pad.passwordRequired": "برای دسترسی به این دفترچه یادداشت نیاز به یک گذرواژه دارید", "pad.permissionDenied": "شرمنده، شما را اجازت په دسترسی ای صفحه نیست.", - "pad.wrongPassword": "گذرواژه‌ی شما درست نیست", "pad.settings.padSettings": "تنظیمات دفترچه یادداشت", "pad.settings.myView": "منی سۏج", "pad.settings.stickychat": "گفتگو همیشه روی صفحه نمایش باشد", diff --git a/src/locales/be-tarask.json b/src/locales/be-tarask.json index 6f159969f..a18cee359 100644 --- a/src/locales/be-tarask.json +++ b/src/locales/be-tarask.json @@ -31,9 +31,7 @@ "pad.colorpicker.cancel": "Скасаваць", "pad.loading": "Загрузка...", "pad.noCookie": "Кукі ня знойдзеныя. Калі ласка, дазвольце кукі ў вашым браўзэры! Паміж наведваньнямі вашая сэсія і налады ня будуць захаваныя. Гэта можа адбывацца таму, што ў некаторых броўзэрах Etherpad заключаны ўнутры iFrame. Праверце, калі ласка, што Etherpad знаходзіцца ў тым жа паддамэне/дамэне, што і бацькоўскі iFrame", - "pad.passwordRequired": "Для доступу да гэтага дакумэнта патрэбны пароль", "pad.permissionDenied": "Вы ня маеце дазволу на доступ да гэтага дакумэнта", - "pad.wrongPassword": "Вы ўвялі няслушны пароль", "pad.settings.padSettings": "Налады дакумэнта", "pad.settings.myView": "Мой выгляд", "pad.settings.stickychat": "Заўсёды паказваць чат", @@ -132,6 +130,5 @@ "pad.impexp.importfailed": "Памылка імпарту", "pad.impexp.copypaste": "Калі ласка, скапіюйце і ўстаўце", "pad.impexp.exportdisabled": "Экспарт у фармаце {{type}} адключаны. Калі ласка, зьвярніцеся да вашага сыстэмнага адміністратара па падрабязнасьці.", - "pad.impexp.maxFileSize": "Файл завялікі. Зьвярніцеся да адміністратара сайту, каб павялічыць дазволены памер файлаў для імпарту", - "pad.impexp.permission": "Імпарт адключаны, бо вы ніколі не працавалі з гэтым нататнікам. Калі ласка, перад імпартам зрабеце хоць бы адзін унёсак" + "pad.impexp.maxFileSize": "Файл завялікі. Зьвярніцеся да адміністратара сайту, каб павялічыць дазволены памер файлаў для імпарту" } diff --git a/src/locales/bg.json b/src/locales/bg.json index 5caf2bdff..c66c9b2e6 100644 --- a/src/locales/bg.json +++ b/src/locales/bg.json @@ -22,7 +22,6 @@ "pad.colorpicker.save": "Съхраняване", "pad.colorpicker.cancel": "Отказ", "pad.loading": "Зареждане...", - "pad.wrongPassword": "Неправилна парола", "pad.settings.language": "Език:", "pad.importExport.exportetherpad": "Etherpad", "pad.importExport.exporthtml": "HTML", diff --git a/src/locales/bgn.json b/src/locales/bgn.json index 148d981c1..72668755f 100644 --- a/src/locales/bgn.json +++ b/src/locales/bgn.json @@ -24,7 +24,6 @@ "pad.colorpicker.save": "ذخیره", "pad.colorpicker.cancel": "کنسیل", "pad.loading": "لودینگ...", - "pad.wrongPassword": "شمی پاسورد جووان نه اینت", "pad.settings.padSettings": "یاداشتئ دفترچه ئی تنظیمات", "pad.settings.myView": "نئ دیست", "pad.settings.stickychat": "هبر موچین وختا بی دیستئ تاکدیمئ سرا بیئت", diff --git a/src/locales/bn.json b/src/locales/bn.json index 5a43fb233..48ecede6d 100644 --- a/src/locales/bn.json +++ b/src/locales/bn.json @@ -33,9 +33,7 @@ "pad.colorpicker.cancel": "বাতিল", "pad.loading": "লোড হচ্ছে...", "pad.noCookie": "কুকি পাওয়া যায়নি। দয়া করে আপনার ব্রাউজারে কুকি অনুমতি দিন!", - "pad.passwordRequired": "এই প্যাড-টি দেখার জন্য আপনাকে পাসওয়ার্ড ব্যবহার করতে হবে", "pad.permissionDenied": "দুঃখিত, এ প্যাড-টি দেখার অধিকার আপনার নেই", - "pad.wrongPassword": "আপনার পাসওয়ার্ড সঠিক নয়", "pad.settings.padSettings": "প্যাডের স্থাপন", "pad.settings.myView": "আমার দৃশ্য", "pad.settings.stickychat": "চ্যাট সক্রীনে প্রদর্শন করা হবে", diff --git a/src/locales/br.json b/src/locales/br.json index a9790e3e7..b8f74ffc3 100644 --- a/src/locales/br.json +++ b/src/locales/br.json @@ -30,9 +30,7 @@ "pad.colorpicker.cancel": "Nullañ", "pad.loading": "O kargañ...", "pad.noCookie": "N'eus ket gallet kavout an toupin. Aotreit an toupinoù en ho merdeer, mar plij !", - "pad.passwordRequired": "Ezhomm ho peus ur ger-tremen evit mont d'ar Pad-se", "pad.permissionDenied": "\nN'oc'h ket aotreet da vont d'ar pad-mañ", - "pad.wrongPassword": "Fazius e oa ho ker-tremen", "pad.settings.padSettings": "Arventennoù Pad", "pad.settings.myView": "Ma diskwel", "pad.settings.stickychat": "Diskwel ar flap bepred", @@ -131,6 +129,5 @@ "pad.impexp.importfailed": "C'hwitet eo an enporzhiadenn", "pad.impexp.copypaste": "Eilit/pegit, mar plij", "pad.impexp.exportdisabled": "Diweredekaet eo ezporzhiañ d'ar furmad {{type}}. Kit e darempred gant merour ar reizhiad evit gouzout hiroc'h.", - "pad.impexp.maxFileSize": "Re vras eo ar restr. Kit e daremrepd gant merour ho lec'hienn evit kreskiñ ment aoteet ar restroù evit enporzhiañ", - "pad.impexp.permission": "Diweredekaet eo an enporzhiañ peogwir n'hoc'h eus ket kemeret perzh gwech ebet er bloc'had-se. Kemerit perzh ur wech da nebeutañ a-raok enporzhiañ." + "pad.impexp.maxFileSize": "Re vras eo ar restr. Kit e daremrepd gant merour ho lec'hienn evit kreskiñ ment aoteet ar restroù evit enporzhiañ" } diff --git a/src/locales/bs.json b/src/locales/bs.json index 75587e7d8..74ea04963 100644 --- a/src/locales/bs.json +++ b/src/locales/bs.json @@ -29,9 +29,7 @@ "pad.colorpicker.cancel": "Otkaži", "pad.loading": "Učitavam...", "pad.noCookie": "Kolačić nije pronađen. Molimo Vas dozvolite kolačiće u Vašem pregledniku!", - "pad.passwordRequired": "Treba Vam lozinka da bi ste pristupili ovom padu", "pad.permissionDenied": "Nemate dopuštenje da pistupite ovom padu", - "pad.wrongPassword": "Vaša lozinka je pogrešna", "pad.settings.padSettings": "Postavke stranice", "pad.settings.myView": "Moj prikaz", "pad.settings.stickychat": "Ćaskanje uvijek na ekranu", diff --git a/src/locales/ca.json b/src/locales/ca.json index b46d4c8a2..a2907f204 100644 --- a/src/locales/ca.json +++ b/src/locales/ca.json @@ -37,9 +37,7 @@ "pad.colorpicker.cancel": "Cancel·la", "pad.loading": "S'està carregant...", "pad.noCookie": "No s'ha trobat la galeta. Permeteu les galetes en el navegador!", - "pad.passwordRequired": "Us cal una contrasenya per a accedir a aquest pad", "pad.permissionDenied": "No teniu permisos per a accedir a aquest pad", - "pad.wrongPassword": "La contrasenya és incorrecta", "pad.settings.padSettings": "Paràmetres del pad", "pad.settings.myView": "La meva vista", "pad.settings.stickychat": "Xateja sempre a la pantalla", @@ -145,6 +143,5 @@ "pad.impexp.importfailed": "Ha fallat la importació", "pad.impexp.copypaste": "Si us plau, copieu i enganxeu", "pad.impexp.exportdisabled": "Està inhabilitada l'exportació com a {{type}}. Contacteu amb el vostre administrador de sistemes per a més informació.", - "pad.impexp.maxFileSize": "Arxiu massa gran. Poseu-vos en contacte amb l'administrador del vostre lloc per augmentar la mida màxima dels fitxers importats", - "pad.impexp.permission": "La importació està desactivada perquè mai heu contribuït a aquest bloc. Si us plau, contribuïu almenys un cop abans d'importar" + "pad.impexp.maxFileSize": "Arxiu massa gran. Poseu-vos en contacte amb l'administrador del vostre lloc per augmentar la mida màxima dels fitxers importats" } diff --git a/src/locales/cs.json b/src/locales/cs.json index 2896f91a6..7cdec000c 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -36,9 +36,7 @@ "pad.colorpicker.cancel": "Zrušit", "pad.loading": "Načítání...", "pad.noCookie": "Nelze nalézt cookie. Povolte prosím cookie ve Vašem prohlížeči.", - "pad.passwordRequired": "Pro přístup k tomuto Padu je třeba znát heslo", "pad.permissionDenied": "Nemáte oprávnění pro přístup k tomuto Padu", - "pad.wrongPassword": "Nesprávné heslo", "pad.settings.padSettings": "Nastavení Padu", "pad.settings.myView": "Vlastní pohled", "pad.settings.stickychat": "Chat vždy na obrazovce", diff --git a/src/locales/da.json b/src/locales/da.json index 8e3ebad5a..6d6e630ce 100644 --- a/src/locales/da.json +++ b/src/locales/da.json @@ -35,9 +35,7 @@ "pad.colorpicker.cancel": "Afbryd", "pad.loading": "Indlæser ...", "pad.noCookie": "Cookie kunne ikke findes. Tillad venligst cookier i din browser!", - "pad.passwordRequired": "Du skal bruge en adgangskode for at få adgang til denne pad", "pad.permissionDenied": "Du har ikke tilladelse til at få adgang til denne pad.", - "pad.wrongPassword": "Din adgangskode er forkert", "pad.settings.padSettings": "Pad indstillinger", "pad.settings.myView": "Min visning", "pad.settings.stickychat": "Chat altid på skærmen", diff --git a/src/locales/de.json b/src/locales/de.json index 79f7d8dfa..2b46c0f7a 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -53,9 +53,7 @@ "pad.colorpicker.cancel": "Abbrechen", "pad.loading": "Lade …", "pad.noCookie": "Das Cookie konnte nicht gefunden werden. Bitte erlaube Cookies in deinem Browser! Deine Sitzung und Einstellungen werden zwischen den Besuchen nicht gespeichert. Dies kann darauf zurückzuführen sein, dass Etherpad in einigen Browsern in einem iFrame enthalten ist. Bitte stelle sicher, dass sich Etherpad auf der gleichen Subdomain/Domain wie der übergeordnete iFrame befindet.", - "pad.passwordRequired": "Du benötigst ein Kennwort, um auf dieses Pad zuzugreifen", "pad.permissionDenied": "Du hast keine Berechtigung, um auf dieses Pad zuzugreifen", - "pad.wrongPassword": "Dein eingegebenes Kennwort war falsch", "pad.settings.padSettings": "Pad-Einstellungen", "pad.settings.myView": "Eigene Ansicht", "pad.settings.stickychat": "Unterhaltung immer anzeigen", @@ -159,6 +157,5 @@ "pad.impexp.importfailed": "Import fehlgeschlagen", "pad.impexp.copypaste": "Bitte kopieren und einfügen", "pad.impexp.exportdisabled": "Der Export im {{type}}-Format ist deaktiviert. Für Einzelheiten kontaktiere bitte deinen Systemadministrator.", - "pad.impexp.maxFileSize": "Die Datei ist zu groß. Kontaktiere bitte deinen Administrator, um das Limit für den Dateiimport zu erhöhen.", - "pad.impexp.permission": "Die Importfunktion ist deaktiviert, da du noch nichts zu diesem Pad beigetragen hast. Bitte trage etwas zu diesem Pad bei, bevor du importierst." + "pad.impexp.maxFileSize": "Die Datei ist zu groß. Kontaktiere bitte deinen Administrator, um das Limit für den Dateiimport zu erhöhen." } diff --git a/src/locales/diq.json b/src/locales/diq.json index 68ca9db0a..bf8914365 100644 --- a/src/locales/diq.json +++ b/src/locales/diq.json @@ -67,9 +67,7 @@ "pad.colorpicker.cancel": "Bıtexelne", "pad.loading": "Bar beno...", "pad.noCookie": "Çerez nêvineya. Rovıter de çereza aktiv kerê.Ronıştısê u eyarê şıma mabênê ziyareti qeyd nêbenê.Çıkı, Etherpad tay rovıteran de tewrê yew iFrame belka biyo. Kerem ke Etherpad corên iFrame ya wa eyni bınca/ca de zey pê bo.", - "pad.passwordRequired": "Ena bloknot resayışi rê parola icab krna", "pad.permissionDenied": "Ena bloknot resayışi rê icazeta şıma çıni ya", - "pad.wrongPassword": "Parola şıma ğeleta", "pad.settings.padSettings": "Sazkerdışê Pedi", "pad.settings.myView": "Asayışê mı", "pad.settings.stickychat": "Ekran de tım mıhebet bıkerê", @@ -175,6 +173,5 @@ "pad.impexp.importfailed": "Zer kerdış mıwafaq nebı", "pad.impexp.copypaste": "Reca keme kopya pronayış bıkeri", "pad.impexp.exportdisabled": "Formatta {{type}} ya ateber kerdış dewra vıciya yo. Qandé teferruati idarekarana irtibat kewê", - "pad.impexp.maxFileSize": "Dosya zêde gırsa, azere kerdışi rê mısade deyaye ebatê dosyay zeydınayışi rê idarekarê siteya irtibat kewê", - "pad.impexp.permission": "Şıma ena ped rê qet iştirak nêkerdo coki ra azere kerdış dewre ra veto. Vêre azere kerdışi minimum yû iştirak bıkerê" + "pad.impexp.maxFileSize": "Dosya zêde gırsa, azere kerdışi rê mısade deyaye ebatê dosyay zeydınayışi rê idarekarê siteya irtibat kewê" } diff --git a/src/locales/dsb.json b/src/locales/dsb.json index 4e042c314..4e5f1f632 100644 --- a/src/locales/dsb.json +++ b/src/locales/dsb.json @@ -26,9 +26,7 @@ "pad.colorpicker.save": "Składowaś", "pad.colorpicker.cancel": "Pśetergnuś", "pad.loading": "Zacytujo se...", - "pad.passwordRequired": "Trjebaš gronidło, aby na toś ten zapisnik pśistup měł", "pad.permissionDenied": "Njamaš pśistupne pšawo za toś ten zapisnik.", - "pad.wrongPassword": "Twójo gronidło jo wopaki było", "pad.settings.padSettings": "Nastajenja zapisnika", "pad.settings.myView": "Mój naglěd", "pad.settings.stickychat": "Chat pśecej na wobrazowce pokazaś", diff --git a/src/locales/dty.json b/src/locales/dty.json index 8c07fc0c3..c01143cc1 100644 --- a/src/locales/dty.json +++ b/src/locales/dty.json @@ -29,9 +29,7 @@ "pad.colorpicker.cancel": "खारेजी", "pad.loading": "लोड हुन्नाछ़....", "pad.noCookie": "कुकी पाउन नाइ सकियो। तमरा ब्राउजरमी कुकी राख्दाइ अनुमति दिय!", - "pad.passwordRequired": "यो प्याड खोल्लाकी पासवर्ड चाहिन्छ", "pad.permissionDenied": "तमलाईँ यै प्याड खोल्लाकी अनुमति नाइथिन", - "pad.wrongPassword": "तमरो पासवर्ड गलत थ्यो", "pad.settings.padSettings": "प्याड सेटिङ्गअन", "pad.settings.myView": "मेरि हेराइ", "pad.settings.stickychat": "जबलई पर्दामी कुरडी गद्य्या", diff --git a/src/locales/el.json b/src/locales/el.json index d551ba8fa..b8da354ce 100644 --- a/src/locales/el.json +++ b/src/locales/el.json @@ -33,9 +33,7 @@ "pad.colorpicker.cancel": "Άκυρο", "pad.loading": "Φόρτωση...", "pad.noCookie": "Το cookie δεν βρέθηκε. Παρακαλώ επιτρέψτε τα cookies στον περιηγητή σας! Η περίοδος σύνδεσης και οι ρυθμίσεις σας δεν θα αποθηκευτούν μεταξύ των επισκέψεων. Αυτό μπορεί να οφείλεται στο ότι το Etherpad περιλαμβάνεται σε ένα iFrame σε ορισμένα προγράμματα περιήγησης. Βεβαιωθείτε ότι το Etherpad βρίσκεται στον ίδιο υποτομέα/τομέα με το γονικό iFrame", - "pad.passwordRequired": "Χρειάζεστε συνθηματικό για πρόσβαση σε αυτό το pad", "pad.permissionDenied": "Δεν έχετε δικαίωμα πρόσβασης σε αυτό το pad", - "pad.wrongPassword": "Το συνθηματικό σας ήταν λανθασμένο", "pad.settings.padSettings": "Ρυθμίσεις Pad", "pad.settings.myView": "Η προβολή μου", "pad.settings.stickychat": "Να είναι πάντα ορατή η συνομιλία", @@ -136,6 +134,5 @@ "pad.impexp.importfailed": "Η εισαγωγή απέτυχε", "pad.impexp.copypaste": "Παρακαλώ αντιγράψτε και επικολλήστε", "pad.impexp.exportdisabled": "Η εξαγωγή σε μορφή {{type}} έχει απενεργοποιηθεί. Επικοινωνήστε με τον διαχειριστή του συστήματός σας για λεπτομέρειες.", - "pad.impexp.maxFileSize": "Πολύ μεγάλο αρχείο. Επικοινωνήστε με τον διαχειριστή για να αυξήσετε το επιτρεπόμενο μέγεθος αρχείου", - "pad.impexp.permission": "Η εισαγωγή είναι απενεργοποιημένη επειδή δεν συνεισφέρατε ποτέ σε αυτό το pad. Συνεισφέρετε τουλάχιστον μία φορά πριν από την εισαγωγή" + "pad.impexp.maxFileSize": "Πολύ μεγάλο αρχείο. Επικοινωνήστε με τον διαχειριστή για να αυξήσετε το επιτρεπόμενο μέγεθος αρχείου" } diff --git a/src/locales/en-gb.json b/src/locales/en-gb.json index 1b1938a06..271c50a5b 100644 --- a/src/locales/en-gb.json +++ b/src/locales/en-gb.json @@ -32,9 +32,7 @@ "pad.colorpicker.cancel": "Cancel", "pad.loading": "Loading...", "pad.noCookie": "Cookie could not be found. Please allow cookies in your browser! Your session and settings will not be saved between visits. This may be due to Etherpad being included in an iFrame in some Browsers. Please ensure Etherpad is on the same subdomain/domain as the parent iFrame", - "pad.passwordRequired": "You need a password to access this pad", "pad.permissionDenied": "You do not have permission to access this pad", - "pad.wrongPassword": "Your password was wrong", "pad.settings.padSettings": "Pad Settings", "pad.settings.myView": "My View", "pad.settings.stickychat": "Chat always on screen", diff --git a/src/locales/eo.json b/src/locales/eo.json index 6f8428a97..220825bb9 100644 --- a/src/locales/eo.json +++ b/src/locales/eo.json @@ -31,9 +31,7 @@ "pad.colorpicker.cancel": "Nuligi", "pad.loading": "Ŝargante...", "pad.noCookie": "Kuketo ne estis trovigebla. Bonvolu permesi kuketojn en via retumilo!", - "pad.passwordRequired": "Vi bezonas pasvorton por aliri ĉi tiun tekston", "pad.permissionDenied": "Vi ne havas permeson por aliri ĉi tiun tekston", - "pad.wrongPassword": "Via pasvorto estis malĝusta", "pad.settings.padSettings": "Redaktilaj Agordoj", "pad.settings.myView": "Mia vido", "pad.settings.stickychat": "Babilejo ĉiam videbla", diff --git a/src/locales/es.json b/src/locales/es.json index 406cb0082..b0d7501df 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -73,9 +73,7 @@ "pad.colorpicker.cancel": "Cancelar", "pad.loading": "Cargando...", "pad.noCookie": "No se pudo encontrar la «cookie». Permite la utilización de «cookies» en el navegador.", - "pad.passwordRequired": "Necesitas una contraseña para acceder a este pad", "pad.permissionDenied": "No tienes permiso para acceder a este pad", - "pad.wrongPassword": "La contraseña era incorrecta", "pad.settings.padSettings": "Configuración del pad", "pad.settings.myView": "Preferencias personales", "pad.settings.stickychat": "Chat siempre en pantalla", diff --git a/src/locales/et.json b/src/locales/et.json index 4170f65b3..45b839a86 100644 --- a/src/locales/et.json +++ b/src/locales/et.json @@ -27,9 +27,7 @@ "pad.colorpicker.save": "Salvesta", "pad.colorpicker.cancel": "Loobu", "pad.loading": "Laadimine...", - "pad.passwordRequired": "Sul peab olema parool selle klade rööptoimetamiseks", "pad.permissionDenied": "Sul puuduvad ligipääsuõigused selle klade rööptoimetamiseks", - "pad.wrongPassword": "Vigane parool", "pad.settings.padSettings": "Klade seadistused", "pad.settings.myView": "Minu vaade", "pad.settings.stickychat": "Näita vestlust alatiselt ekraanil", diff --git a/src/locales/eu.json b/src/locales/eu.json index 630589572..51a226a2d 100644 --- a/src/locales/eu.json +++ b/src/locales/eu.json @@ -32,9 +32,7 @@ "pad.colorpicker.cancel": "Utzi", "pad.loading": "Kargatzen...", "pad.noCookie": "Cookiea ez da aurkitu. Mesedez, gaitu cookieak zure nabigatzailean!", - "pad.passwordRequired": "Pasahitza behar duzu pad honetara sartzeko", "pad.permissionDenied": "Ez duzu bamienik pad honetara sartzeko", - "pad.wrongPassword": "Zure pasahitza oker zegoen", "pad.settings.padSettings": "Pad hobespenak", "pad.settings.myView": "Nire ikusmoldea", "pad.settings.stickychat": "Txata beti pantailan", diff --git a/src/locales/fa.json b/src/locales/fa.json index 20dddb009..095eee3b3 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -34,9 +34,7 @@ "pad.colorpicker.cancel": "لغو", "pad.loading": "در حال بارگذاری...", "pad.noCookie": "کوکی یافت نشد. لطفاً اجازهٔ اجرای کوکی در مروگرتان را بدهید!", - "pad.passwordRequired": "برای دسترسی به این دفترچه یادداشت نیاز به یک گذرواژه دارید", "pad.permissionDenied": "شما اجازه‌ی دسترسی به این دفترچه یادداشت را ندارید", - "pad.wrongPassword": "گذرواژه‌ی شما درست نیست", "pad.settings.padSettings": "تنظیمات دفترچه یادداشت", "pad.settings.myView": "نمای من", "pad.settings.stickychat": "گفتگو همیشه روی صفحه نمایش باشد", diff --git a/src/locales/fi.json b/src/locales/fi.json index e89949d37..8a5af8c9d 100644 --- a/src/locales/fi.json +++ b/src/locales/fi.json @@ -40,9 +40,7 @@ "pad.colorpicker.cancel": "Peru", "pad.loading": "Ladataan…", "pad.noCookie": "Evästettä ei löytynyt. Ole hyvä, ja salli evästeet selaimessasi!", - "pad.passwordRequired": "Tämä muistio on suojattu salasanalla.", "pad.permissionDenied": "Käyttöoikeutesi eivät riitä tämän muistion käyttämiseen.", - "pad.wrongPassword": "Väärä salasana", "pad.settings.padSettings": "Muistion asetukset", "pad.settings.myView": "Oma näkymä", "pad.settings.stickychat": "Keskustelu aina näkyvissä", diff --git a/src/locales/fo.json b/src/locales/fo.json index 197d8902f..86a5c9fdd 100644 --- a/src/locales/fo.json +++ b/src/locales/fo.json @@ -20,9 +20,7 @@ "pad.colorpicker.save": "Goym", "pad.colorpicker.cancel": "Ógilda", "pad.loading": "Løðir...", - "pad.passwordRequired": "Tú hevur brúk fyri einum loyniorði fyri at fáa atgongd til henda paddin", "pad.permissionDenied": "Tú hevur ikki loyvi til at fáa atgongd til henda paddin", - "pad.wrongPassword": "Títt loyniorð var skeivt", "pad.settings.padSettings": "Pad innstillingar", "pad.settings.myView": "Mín sýning", "pad.settings.stickychat": "Kjatta altíð á skerminum", diff --git a/src/locales/fr.json b/src/locales/fr.json index fb2ef1d76..8ea26f3c0 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -86,9 +86,7 @@ "pad.colorpicker.cancel": "Annuler", "pad.loading": "Chargement...", "pad.noCookie": "Un cookie n’a pas pu être trouvé. Veuillez autoriser les fichiers témoins (ou cookies) dans votre navigateur ! Votre session et vos paramètres ne seront pas enregistrés entre les visites. Cela peut être dû au fait qu’Etehrpad est inclus dans un iFrame dans certains navigateurs. Veuillez vous assurer que Etherpad est dans le même sous-domaine/domaine que son iFrame parent", - "pad.passwordRequired": "Vous avez besoin d'un mot de passe pour accéder à ce bloc-note", "pad.permissionDenied": "Vous n’êtes pas autorisé à accéder à ce bloc-notes", - "pad.wrongPassword": "Votre mot de passe est incorrect", "pad.settings.padSettings": "Paramètres du bloc-notes", "pad.settings.myView": "Ma vue", "pad.settings.stickychat": "Toujours afficher le clavardage", @@ -194,6 +192,5 @@ "pad.impexp.importfailed": "Échec de l’importation", "pad.impexp.copypaste": "Veuillez copier-coller", "pad.impexp.exportdisabled": "L’exportation au format {{type}} est désactivée. Veuillez contacter votre administrateur système pour plus de détails.", - "pad.impexp.maxFileSize": "Fichier trop gros. Contactez votre administrateur de site pour augmenter la taille maximale des fichiers importés", - "pad.impexp.permission": "L’importation est désactivée parce que vous n’avez jamais contribué à ce bloc. Veuillez contribuer au moins une fois avant d’importer" + "pad.impexp.maxFileSize": "Fichier trop gros. Contactez votre administrateur de site pour augmenter la taille maximale des fichiers importés" } diff --git a/src/locales/gl.json b/src/locales/gl.json index 214059b39..673efc913 100644 --- a/src/locales/gl.json +++ b/src/locales/gl.json @@ -28,9 +28,7 @@ "pad.colorpicker.cancel": "Cancelar", "pad.loading": "Cargando...", "pad.noCookie": "Non se puido atopar a cookie. Por favor, habilite as cookies no seu navegador!", - "pad.passwordRequired": "Cómpre un contrasinal para acceder a este documento", "pad.permissionDenied": "Non ten permiso para acceder a este documento", - "pad.wrongPassword": "O contrasinal era incorrecto", "pad.settings.padSettings": "Configuracións do documento", "pad.settings.myView": "A miña vista", "pad.settings.stickychat": "Chat sempre visible", diff --git a/src/locales/gu.json b/src/locales/gu.json index b9aee98e6..894a5e3f8 100644 --- a/src/locales/gu.json +++ b/src/locales/gu.json @@ -13,9 +13,7 @@ "pad.colorpicker.cancel": "રદ્દ કરો", "pad.loading": "લાવે છે...", "pad.noCookie": "કુકી મળી નહી. આપના બ્રાઉઝર સેટિંગમાં જઇ કુકી સક્રિય કરો!", - "pad.passwordRequired": "તમારે આ પેડના ઉપયોગ માટે ગુપ્તસંજ્ઞાની જરુર પડશે", "pad.permissionDenied": "આ પેડના ઉપયોગની આપને પરવાનગી નથી", - "pad.wrongPassword": "આપની ગુપ્તસંજ્ઞા ખોટી છે", "pad.settings.padSettings": "પેડ ગોઠવણીઓ", "pad.settings.myView": "મારા મતે", "pad.settings.fontType.normal": "સામાન્ય", diff --git a/src/locales/he.json b/src/locales/he.json index ab0d3004e..8a1b135dd 100644 --- a/src/locales/he.json +++ b/src/locales/he.json @@ -8,6 +8,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": "פתיחת פנקס קיים עם השם:", @@ -31,10 +64,8 @@ "pad.colorpicker.save": "שמירה", "pad.colorpicker.cancel": "ביטול", "pad.loading": "טעינה...", - "pad.noCookie": "העוגייה לא נמצאה. נא לאפשר עוגיות בדפדפן שלך!", - "pad.passwordRequired": "דרושה ססמה כדי לגשת לפנקס הזה", + "pad.noCookie": "העוגייה לא נמצאה. נא לאפשר עוגיות בדפדפן שלך! ההפעלה וההגדרות שלך לא יישמרו בין ביקורים. זה יכול לקרות עם Etherpad נכלל בתוך חלונית פנימית (iframe) בחלק מהדפדפנים. נא לוודא ש־Etherpad הוא תחת אותו שם תחום/תת־שם תחום כמו החלונית הפנימית של ההורה", "pad.permissionDenied": "אין לך הרשאה לגשת לפנקס הזה", - "pad.wrongPassword": "ססמתך הייתה שגויה", "pad.settings.padSettings": "הגדרות פנקס", "pad.settings.myView": "התצוגה שלי", "pad.settings.stickychat": "השיחה תמיד על המסך", @@ -46,6 +77,7 @@ "pad.settings.fontType.normal": "רגיל", "pad.settings.language": "שפה:", "pad.settings.about": "על אודות", + "pad.settings.poweredBy": "מופעל על גבי", "pad.importExport.import_export": "ייבוא/ייצוא", "pad.importExport.import": "העלאת כל קובץ טקסט או מסמך", "pad.importExport.importSuccessful": "זה עבד!", @@ -80,6 +112,10 @@ "pad.modals.corruptPad.cause": "ייתכן שזה קרה בגלל הגדרות שרת שגויות או התנהגות בלתי־צפויה כלשהי. נא ליצור קשר עם המנהל של השירות אם נראה לך שזאת שגיאה.", "pad.modals.deleted": "נמחק.", "pad.modals.deleted.explanation": "הפנקס הזה הוסר.", + "pad.modals.rateLimited": "מוגבל קצב.", + "pad.modals.rateLimited.explanation": "שלחת יותר מדי הודעות לפנקס הזה ולכן הוא ניתק אותך.", + "pad.modals.rejected.explanation": "השרת דחה את ההודעה שנשלחה על ידי הדפדפן שלך.", + "pad.modals.rejected.cause": "יכול להיות שהשרת עודכן בזמן שצפית בפנקס או שיש תקלה ב־Etherpad. מומלץ לנסות לרענן את העמוד.", "pad.modals.disconnected": "נותקת.", "pad.modals.disconnected.explanation": "התקשורת לשרת אבדה", "pad.modals.disconnected.cause": "ייתכן שהשרת אינו זמין. נא להודיע למנהל השירות אם זה ממשיך לקרות.", @@ -92,6 +128,7 @@ "pad.chat.loadmessages": "טעינת הודעות נוספות", "pad.chat.stick.title": "הצמדת צ׳אט למסך", "pad.chat.writeMessage.placeholder": "מקום לכתיבת ההודעה שלך", + "timeslider.followContents": "לעקוב אחר עדכוני תוכן פנקס", "timeslider.pageTitle": "גולל זמן של {{appTitle}}", "timeslider.toolbar.returnbutton": "חזרה אל הפנקס", "timeslider.toolbar.authors": "כותבים:", @@ -126,14 +163,13 @@ "pad.userlist.approve": "לאשר", "pad.editbar.clearcolors": "לנקות צבעים לסימון כותבים בכל המסמך? זו פעולה בלתי הפיכה", "pad.impexp.importbutton": "לייבא כעת", - "pad.impexp.importing": "ייבוא...", - "pad.impexp.confirmimport": "ייבוא של קובץ יבטל את הטקסט הנוכחי בפנקס. האם ברצונך להמשיך?", + "pad.impexp.importing": "מתבצע ייבוא…", + "pad.impexp.confirmimport": "ייבוא של קובץ יבטל את הטקסט הנוכחי בפנקס. להמשיך?", "pad.impexp.convertFailed": "לא הצלחנו לייבא את הקובץ הזה. נא להשתמש בתסדיר מסמך שונה או להעתיק ולהדביק ידנית", "pad.impexp.padHasData": "לא הצלחנו לייבא את הקובץ הזה, כי בפנקס הזה כבר יש שינויים. נא לייבא לפנקס חדש.", "pad.impexp.uploadFailed": "ההעלאה נכשלה, נא לנסות שוב", "pad.impexp.importfailed": "הייבוא נכשל", "pad.impexp.copypaste": "נא להעתיק ולהדביק", "pad.impexp.exportdisabled": "ייצוא בתסדיר {{type}} אינו פעיל. מנהל המערכת שלך יוכל לספר לך על זה עוד פרטים.", - "pad.impexp.maxFileSize": "הקובץ גדול מדי. נא ליצור קשר עם הנהלת האתר כדי להגדיל את הגודל המרבי שמותר לייבא.", - "pad.impexp.permission": "הייבוא מושבת כיוון שמעולם לא תרמת לפנקס הזה. נא לתרום לפחות פעם אחת בטרם ביצוע ניסיון ייבוא" + "pad.impexp.maxFileSize": "הקובץ גדול מדי. נא ליצור קשר עם הנהלת האתר כדי להגדיל את הגודל המרבי שמותר לייבא." } diff --git a/src/locales/hr.json b/src/locales/hr.json index 7845fa7a7..4ba0e09d1 100644 --- a/src/locales/hr.json +++ b/src/locales/hr.json @@ -29,9 +29,7 @@ "pad.colorpicker.cancel": "Otkaži", "pad.loading": "Učitavanje...", "pad.noCookie": "Kolačić nije pronađen. Molimo Vas, omogućite kolačiće u Vašem pregledniku! Sesija i postavke neće biti sačuvane između Vaših posjećivanja. Razlog može biti uključenost Etherpada u iFrame u nekim preglednicima. Molimo Vas, osigurajte da je Etherpad na istoj poddomeni/domeni kao i ''roditeljski'' iFrame.", - "pad.passwordRequired": "Potrebna Vam je zaporka za pristup ovomu blokiću", "pad.permissionDenied": "Nemate dopuštenje za pristup ovome blokiću", - "pad.wrongPassword": "Vaša zaporka nije valjana", "pad.settings.padSettings": "Postavke blokića", "pad.settings.myView": "Vaš prikaz", "pad.settings.stickychat": "Stavi čavrljanje uvijek na ekranu", @@ -135,6 +133,5 @@ "pad.impexp.importfailed": "Uvoz nije uspio", "pad.impexp.copypaste": "Molimo preslikajte/zalijepite", "pad.impexp.exportdisabled": "Izvoz u formatu {{type}} nije omogućen. Molimo Vas, kontaktirajte Vašega administratora sustava za više pojedinosti.", - "pad.impexp.maxFileSize": "Datoteka je prevelika. Kontaktirajte administratora Vašega mrežnoga sjedišta kako biste zatražili povećanje dopuštene veličine datoteke za uvoz", - "pad.impexp.permission": "Uvoz je onemogućen jer niste doprinosili ovom blokiću Etherpada. Molimo Vas, doprinosite barem jednom prije radnje uvoza" + "pad.impexp.maxFileSize": "Datoteka je prevelika. Kontaktirajte administratora Vašega mrežnoga sjedišta kako biste zatražili povećanje dopuštene veličine datoteke za uvoz" } diff --git a/src/locales/hrx.json b/src/locales/hrx.json index 73849a836..7413abe16 100644 --- a/src/locales/hrx.json +++ b/src/locales/hrx.json @@ -26,9 +26,7 @@ "pad.colorpicker.save": "Speichre", "pad.colorpicker.cancel": "Abbreche", "pad.loading": "Loode …", - "pad.passwordRequired": "Sie benötiche en Passwort, um uff das Pad zuzugreife", "pad.permissionDenied": "Du host ken Berechtichung, um uff das Pad zuzugreif", - "pad.wrongPassword": "Dein Passwort woor falsch", "pad.settings.padSettings": "Pad Einstellunge", "pad.settings.myView": "Eichne Oonsicht", "pad.settings.stickychat": "Chat immer oonzeiche", diff --git a/src/locales/hsb.json b/src/locales/hsb.json index e5e86d468..fe680c23a 100644 --- a/src/locales/hsb.json +++ b/src/locales/hsb.json @@ -27,9 +27,7 @@ "pad.colorpicker.cancel": "Přetorhnyć", "pad.loading": "Začituje so...", "pad.noCookie": "Plack njeje so namakał. Prošu dopušćće placki w swojim wobhladowaku!", - "pad.passwordRequired": "Trjebaš hesło, zo by na tutón zapisnik přistup měł", "pad.permissionDenied": "Nimaće prawo za přistup na tutón zapisnik.", - "pad.wrongPassword": "Twoje hesło bě wopak", "pad.settings.padSettings": "Nastajenja zapisnika", "pad.settings.myView": "Mój napohlad", "pad.settings.stickychat": "Chat přeco na wobrazowce pokazać", diff --git a/src/locales/hu.json b/src/locales/hu.json index 141c951e7..45ac4b261 100644 --- a/src/locales/hu.json +++ b/src/locales/hu.json @@ -13,26 +13,39 @@ "Tgr" ] }, + "admin.page-title": "Admin irányítópult - Etherpad", "admin_plugins": "Bővítménykezelő", "admin_plugins.available": "Elérhető bővítmények", "admin_plugins.available_not-found": "Nem található bővítmény.", + "admin_plugins.available_fetching": "Lehívás...", "admin_plugins.available_install.value": "Telepítés", "admin_plugins.available_search.placeholder": "Telepíthető bővítmények keresése", "admin_plugins.description": "Leírás", "admin_plugins.installed": "Telepített bővítmények", + "admin_plugins.installed_fetching": "Telepített bővítmények lehívása...", "admin_plugins.installed_nothing": "Még nem telepítettél bővítményeket.", "admin_plugins.installed_uninstall.value": "Eltávolítás", "admin_plugins.last-update": "Utolsó frissítés", "admin_plugins.name": "Név", "admin_plugins.page-title": "Bővítménykezelő - Etherpad", + "admin_plugins.version": "Verzió", "admin_plugins_info": "Hibaelhárításra vonatkozó információ", + "admin_plugins_info.hooks": "Telepített hookok", + "admin_plugins_info.hooks_client": "Kliensoldali hookok", + "admin_plugins_info.hooks_server": "Szerveroldali hookok", + "admin_plugins_info.parts": "Telepített elemek", "admin_plugins_info.plugins": "Telepített bővítmények", "admin_plugins_info.page-title": "Információ bővítményről - Etherpad", + "admin_plugins_info.version": "Etherpad verzió", "admin_plugins_info.version_latest": "Legfrissebb elérhető verzió", + "admin_plugins_info.version_number": "Verziószám", "admin_settings": "Beállítások", "admin_settings.current": "Jelenlegi beállítások", + "admin_settings.current_example-devel": "Fejlesztés beállítások sablon minta", + "admin_settings.current_example-prod": "Gyártás beállítások sablon minta", "admin_settings.current_restart.value": "Etherpad újraindítása", "admin_settings.current_save.value": "Beállítások mentése", + "admin_settings.page-title": "Beállítások - Etherpad", "index.newPad": "Új jegyzetfüzet", "index.createOpenPad": "vagy jegyzetfüzet létrehozása/megnyitása ezzel a névvel:", "index.openPad": "nyisson meg egy meglévő jegyzetfüzetet névvel:", @@ -57,9 +70,7 @@ "pad.colorpicker.cancel": "Mégsem", "pad.loading": "Betöltés…", "pad.noCookie": "Nem található a süti. Engedélyezd a böngésződben a sütik használatát! A munkamenet és a beállítások nem kerülnek mentésre a látogatások között. Ennek oka lehet az, hogy az Etherpad egyes böngészőkben szerepel az iFrame-ben. Ellenőrizze, hogy az Etherpad ugyanabban az altartomány / tartományban van-e, mint a szülő iFrame", - "pad.passwordRequired": "Jelszóra van szükséged ezen jegyzetfüzet eléréséhez", "pad.permissionDenied": "Nincs engedélyed ezen jegyzetfüzet eléréséhez", - "pad.wrongPassword": "A jelszó rossz volt", "pad.settings.padSettings": "Jegyzetfüzet beállításai", "pad.settings.myView": "Az én nézetem", "pad.settings.stickychat": "Mindig mutasd a csevegés-dobozt", @@ -108,6 +119,8 @@ "pad.modals.deleted.explanation": "Ez a jegyzetfüzet el lett távolítva.", "pad.modals.rateLimited": "Korlátozott.", "pad.modals.rateLimited.explanation": "Túl sok üzenetet küldött erre a jegyzetfüzetre, így a kapcsolat bontva lett.", + "pad.modals.rejected.explanation": "A szerver elutasított egy üzenetet, amit a keresőd küldött.", + "pad.modals.rejected.cause": "Lehet, hogy a szerveren frissítés történt, miközben a padet nézted, vagy bugos az Etherpad. Próbáld meg frissíteni az oldalt.", "pad.modals.disconnected": "Kapcsolat bontva.", "pad.modals.disconnected.explanation": "A szerverrel való kapcsolat megszűnt", "pad.modals.disconnected.cause": "Lehet, hogy a szerver nem elérhető. Kérlek, értesítsd a szolgáltatás adminisztrátorát, ha a probléma tartósan fennáll.", @@ -163,6 +176,5 @@ "pad.impexp.importfailed": "Az importálás nem sikerült", "pad.impexp.copypaste": "Kérjük másold be", "pad.impexp.exportdisabled": "{{type}} formátumba az exportálás nem engedélyezett. Kérjük, a részletekért fordulj a rendszeradminisztrátorhoz.", - "pad.impexp.maxFileSize": "Túl nagy a fájl. Vegye fel a kapcsolatot a webhelygazdájával, hogy növelje az importálható fájl méretét", - "pad.impexp.permission": "Az importálás le van tiltva, mert soha nem járult hozzá ehhez a jegyzetfüzethez. Kérjük, járuljon hozzá legalább egyszer az importálás előtt" + "pad.impexp.maxFileSize": "Túl nagy a fájl. Vegye fel a kapcsolatot a webhelygazdájával, hogy növelje az importálható fájl méretét" } diff --git a/src/locales/hy.json b/src/locales/hy.json index c284d1be5..d18e5308f 100644 --- a/src/locales/hy.json +++ b/src/locales/hy.json @@ -21,7 +21,6 @@ "pad.colorpicker.save": "Պահպանել", "pad.colorpicker.cancel": "Չեղարկել", "pad.loading": "Բեռնվում է…", - "pad.wrongPassword": "Սխալ գաղտնաբառ", "pad.settings.myView": "Իմ տեսարան", "pad.settings.rtlcheck": "Կարդալ բովանդակությունը աջից ձախ", "pad.settings.fontType": "Տառատեսակի տեսակը", diff --git a/src/locales/ia.json b/src/locales/ia.json index 4356f0680..871835ed2 100644 --- a/src/locales/ia.json +++ b/src/locales/ia.json @@ -27,9 +27,7 @@ "pad.colorpicker.cancel": "Cancellar", "pad.loading": "Cargamento…", "pad.noCookie": "Le cookie non pote esser trovate. Per favor permitte le cookies in tu navigator!", - "pad.passwordRequired": "Un contrasigno es necessari pro acceder a iste pad", "pad.permissionDenied": "Tu non ha le permission de acceder a iste pad", - "pad.wrongPassword": "Le contrasigno es incorrecte", "pad.settings.padSettings": "Configuration del pad", "pad.settings.myView": "Mi vista", "pad.settings.stickychat": "Chat sempre visibile", diff --git a/src/locales/id.json b/src/locales/id.json index 738b5a3c3..9cb0dd39d 100644 --- a/src/locales/id.json +++ b/src/locales/id.json @@ -29,9 +29,7 @@ "pad.colorpicker.cancel": "Batalkan", "pad.loading": "Memuat...", "pad.noCookie": "Kuki tidak dapat ditemukan. Izinkan kuki di peramban Anda!", - "pad.passwordRequired": "Anda memerlukan kata sandi untuk mengakses pad ini", "pad.permissionDenied": "Anda tidak memiliki izin untuk mengakses pad ini", - "pad.wrongPassword": "Kata sandi Anda salah", "pad.settings.padSettings": "Pengaturan Pad", "pad.settings.myView": "Tampilan Saya", "pad.settings.stickychat": "Chat selalu di layar", diff --git a/src/locales/is.json b/src/locales/is.json index 394503f1a..f452bf345 100644 --- a/src/locales/is.json +++ b/src/locales/is.json @@ -28,9 +28,7 @@ "pad.colorpicker.cancel": "Hætta við", "pad.loading": "Hleð inn...", "pad.noCookie": "Smákaka fannst ekki. Þú verður að leyfa smákökur í vafranum þínum!", - "pad.passwordRequired": "Þú þarft að gefa upp lykilorð til að komast á þessa skrifblokk", "pad.permissionDenied": "Þú hefur ekki réttindi til að nota þessa skrifblokk", - "pad.wrongPassword": "Lykilorðinu þínu var hafnað", "pad.settings.padSettings": "Stillingar skrifblokkar", "pad.settings.myView": "Mitt yfirlit", "pad.settings.stickychat": "Spjall alltaf á skjánum", diff --git a/src/locales/it.json b/src/locales/it.json index 974234422..31639722a 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -34,9 +34,7 @@ "pad.colorpicker.cancel": "Annulla", "pad.loading": "Caricamento in corso…", "pad.noCookie": "Il cookie non è stato trovato. Consenti i cookie nel tuo browser! La sessione e le impostazioni non verranno salvate tra le diverse visite. Ciò può essere dovuto al fatto che Etherpad è stato incluso in un iFrame in alcuni browser. Assicurati che Etherpad si trovi sullo stesso sottodominio/dominio dell'iFrame principale", - "pad.passwordRequired": "Per accedere a questo Pad è necessaria una password", "pad.permissionDenied": "Non si dispone dei permessi necessari per accedere a questo Pad", - "pad.wrongPassword": "La password è sbagliata", "pad.settings.padSettings": "Impostazioni del Pad", "pad.settings.myView": "Mia visualizzazione", "pad.settings.stickychat": "Chat sempre sullo schermo", @@ -137,6 +135,5 @@ "pad.impexp.importfailed": "Importazione fallita", "pad.impexp.copypaste": "Si prega di copiare e incollare", "pad.impexp.exportdisabled": "L'esportazione come {{type}} è disabilitata. Contattare l'amministratore per i dettagli.", - "pad.impexp.maxFileSize": "File troppo grande. Contatta l'amministratore del sito per incrementare la dimensione consentita per l'importazione", - "pad.impexp.permission": "L'importazione è disabilitata perché non hai mai contribuito a questo pad. Per favore, contribuisci almeno una volta, prima di fare importazioni" + "pad.impexp.maxFileSize": "File troppo grande. Contatta l'amministratore del sito per incrementare la dimensione consentita per l'importazione" } diff --git a/src/locales/ja.json b/src/locales/ja.json index 8e766b2a2..5f5b5fabd 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -32,9 +32,7 @@ "pad.colorpicker.cancel": "キャンセル", "pad.loading": "読み込み中...", "pad.noCookie": "Cookie could not be found. Please allow cookies in your browser! Your session and settings will not be saved between visits. \n\nクッキーが見つかりません。ブラウザの設定でクッキーの使用を許可するまで、アクセスの記録や設定は引き継がれません。原因はブラウザによって Etherpad が iFrame に組み込まれたからと考えられます。親ドメインの iFrame と同じドメイン/サブドメインに置かれているかどうか、Etherpad の設定を確認してください。", - "pad.passwordRequired": "このパッドにアクセスするにはパスワードが必要です", "pad.permissionDenied": "あなたにはこのパッドへのアクセス許可がありません", - "pad.wrongPassword": "パスワードが間違っています", "pad.settings.padSettings": "パッドの設定", "pad.settings.myView": "個人設定", "pad.settings.stickychat": "画面にチャットを常に表示", @@ -138,6 +136,5 @@ "pad.impexp.importfailed": "インポートに失敗しました", "pad.impexp.copypaste": "コピー & ペーストしてください", "pad.impexp.exportdisabled": "{{type}}形式でのエクスポートは無効になっています。詳細はシステム管理者にお問い合わせください。", - "pad.impexp.maxFileSize": "ファイルが重すぎます。サイト管理者に連絡してインポート可能なファイルサイズの上限を引き上げてもらう必要があります", - "pad.impexp.permission": "このパッドに寄稿した実績がないため、インポートを無効にしました。まず寄稿をしてから再度、インポートしてください" + "pad.impexp.maxFileSize": "ファイルが重すぎます。サイト管理者に連絡してインポート可能なファイルサイズの上限を引き上げてもらう必要があります" } diff --git a/src/locales/kab.json b/src/locales/kab.json index ed9263a67..1bce482ad 100644 --- a/src/locales/kab.json +++ b/src/locales/kab.json @@ -27,9 +27,7 @@ "pad.colorpicker.cancel": "Sefsex", "pad.loading": "Asali...", "pad.noCookie": "Anagi n tuqqna ulac-it. Sireg inagan n tuqqna deg iminig-ik!", - "pad.passwordRequired": "Tesriḍ awal uffir akken ad tkecmeḍ ar upad-agi", "pad.permissionDenied": "Ur ɣur-k ara tasiregt akken ad tkecmeḍ ar upad-agi", - "pad.wrongPassword": "Awal-uhhir mačči d ameɣtu", "pad.settings.padSettings": "Iɣewwaṛen n upad", "pad.settings.myView": "Timeẓri-iw", "pad.settings.stickychat": "Asqerdec yezga deg ugdil", diff --git a/src/locales/km.json b/src/locales/km.json index 3129e9dab..d620d9694 100644 --- a/src/locales/km.json +++ b/src/locales/km.json @@ -26,9 +26,7 @@ "pad.colorpicker.save": "រក្សាទុក", "pad.colorpicker.cancel": "បោះបង់", "pad.loading": "កំពុងផ្ទុក…", - "pad.passwordRequired": "អ្នក​ត្រូវ​មាន​ពាក្យ​សម្ងាត់ ដើម្បី​ចូល​ផេត​នេះ", "pad.permissionDenied": "អ្នក​មិន​មាន​សិទ្ធិ​ចូល​ផេត​នេះ​ទេ", - "pad.wrongPassword": "ពាក្យ​សម្ងាត់​របស់​អ្នក ខុស​ហើយ", "pad.settings.padSettings": "ការ​កំណត់​ផេត", "pad.settings.myView": "គំហើញរបស់ខ្ញុំ", "pad.settings.stickychat": "តែង​បង្ហាញ​ការ​ជជែក​លើ​អេក្រង់", diff --git a/src/locales/ko.json b/src/locales/ko.json index f2214ee47..fea62797a 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -66,9 +66,7 @@ "pad.colorpicker.cancel": "취소", "pad.loading": "불러오는 중...", "pad.noCookie": "쿠키를 찾을 수 없습니다. 브라우저에서 쿠키를 허용해주세요!", - "pad.passwordRequired": "이 패드에 접근하려면 비밀번호가 필요합니다", "pad.permissionDenied": "이 패드에 접근할 권한이 없습니다", - "pad.wrongPassword": "비밀번호가 잘못되었습니다", "pad.settings.padSettings": "패드 설정", "pad.settings.myView": "내 보기", "pad.settings.stickychat": "화면에 항상 대화 보기", @@ -170,6 +168,5 @@ "pad.impexp.importfailed": "가져오기를 실패했습니다", "pad.impexp.copypaste": "복사하여 붙여넣으세요", "pad.impexp.exportdisabled": "{{type}} 형식으로 내보내기가 비활성화되어 있습니다. 자세한 내용은 시스템 관리자에게 문의하시기 바랍니다.", - "pad.impexp.maxFileSize": "파일의 용량이 너무 큽니다. 가져올 파일의 허용 크기를 늘리려면 사이트 관리자에게 문의하십시오", - "pad.impexp.permission": "이 패드로 기여한 적이 없으므로 가져오기가 비활성화됩니다. 가져오기 전에 적어도 한 번은 기여해 주십시오" + "pad.impexp.maxFileSize": "파일의 용량이 너무 큽니다. 가져올 파일의 허용 크기를 늘리려면 사이트 관리자에게 문의하십시오" } diff --git a/src/locales/ksh.json b/src/locales/ksh.json index 28ebf44ed..4eb1c5c74 100644 --- a/src/locales/ksh.json +++ b/src/locales/ksh.json @@ -27,9 +27,7 @@ "pad.colorpicker.cancel": "Ophüre", "pad.loading": "Ben aam Lahde …", "pad.noCookie": "Dat Pläzje wood nit jevonge. Don dat en Dingem Brauser zohlohße!", - "pad.passwordRequired": "Do bruchs e Paßwoot för heh dat Pädd.", "pad.permissionDenied": "Do häs nit dat Rääsch, op heh dat Pädd zohzejriife.", - "pad.wrongPassword": "Ding Paßwoot wohr verkeht.", "pad.settings.padSettings": "Däm Pädd sing Enschtällonge", "pad.settings.myView": "Aanseesch", "pad.settings.stickychat": "Donn der Klaaf emmer aanzeije", diff --git a/src/locales/lb.json b/src/locales/lb.json index 62c1621fa..22c07e2a3 100644 --- a/src/locales/lb.json +++ b/src/locales/lb.json @@ -28,9 +28,7 @@ "pad.colorpicker.cancel": "Ofbriechen", "pad.loading": "Lueden...", "pad.noCookie": "Cookie gouf net fonnt. Erlaabt wgl. Cookien an Ärem Browser!", - "pad.passwordRequired": "Dir braucht ee Passwuert fir dëse Pad opzemaachen", "pad.permissionDenied": "Dir hutt net déi néideg Rechter fir dëse Pad opzemaachen", - "pad.wrongPassword": "Äert Passwuert ass falsch", "pad.settings.myView": "Méng Usiicht", "pad.settings.linenocheck": "Zeilennummeren", "pad.settings.rtlcheck": "Inhalt vu riets no lénks liesen?", diff --git a/src/locales/lki.json b/src/locales/lki.json index 5559aca07..ad6adb828 100644 --- a/src/locales/lki.json +++ b/src/locales/lki.json @@ -29,9 +29,7 @@ "pad.colorpicker.cancel": "ئآهووسانن/لغو", "pad.loading": "...(loading)بارنیائن", "pad.noCookie": "کوکی یافت نشد. لطفاً اجازهٔ اجرای کوکی در مروگرتان را بدهید!", - "pad.passwordRequired": "هؤمۀ رمزتؤنه گرۀکۀ-لازمۀ ئۀرا اتصال ئئ \npad", "pad.permissionDenied": "شما اجازه‌ی دسترسی به این دفترچه یادداشت را ندارید", - "pad.wrongPassword": "رمزۀ تؤن دؤرس نیۀ", "pad.settings.padSettings": "pad تنظیمۀل", "pad.settings.myView": "نمایش-سئرکردن مه", "pad.settings.stickychat": "گەپ(قسە)هەمۆیشە وە وەڵگە نمایش بوو", diff --git a/src/locales/lrc.json b/src/locales/lrc.json index 104a9ba7b..e61fb027e 100644 --- a/src/locales/lrc.json +++ b/src/locales/lrc.json @@ -20,7 +20,6 @@ "pad.colorpicker.save": "زٱخیرٱ كردن", "pad.colorpicker.cancel": "ٱنجوم شؽڤ كردن", "pad.loading": "د هالٱت سڤار كرد...", - "pad.wrongPassword": "پٱسڤردتو اْشتبائٱ", "pad.settings.padSettings": "میزوکاری دٱفتٱرچٱ", "pad.settings.myView": "نٱزٱرگٱ ماْ", "pad.settings.stickychat": "همیشٱ د بٱلگٱ چٱک چنٱ بٱکؽت", diff --git a/src/locales/lt.json b/src/locales/lt.json index 21fc583ad..58a522785 100644 --- a/src/locales/lt.json +++ b/src/locales/lt.json @@ -32,9 +32,7 @@ "pad.colorpicker.cancel": "Atšaukti", "pad.loading": "Įkraunama...", "pad.noCookie": "Slapuko nepavyko rasti. Prašome leisti slapukus interneto naršyklėje!", - "pad.passwordRequired": "Jums reikia slaptažodžio kad galėtumėte matyti šį bloknotą", "pad.permissionDenied": "Jūs neturite leidimo patekti į šį bloknotą", - "pad.wrongPassword": "Jūsų slaptažodis neteisingas", "pad.settings.padSettings": "Bloknoto nustatymai", "pad.settings.myView": "Mano Vaizdas", "pad.settings.stickychat": "Pokalbiai visada viršuje", diff --git a/src/locales/lv.json b/src/locales/lv.json index 5dd6a9511..2b8ac452e 100644 --- a/src/locales/lv.json +++ b/src/locales/lv.json @@ -30,9 +30,7 @@ "pad.colorpicker.save": "Saglabāt", "pad.colorpicker.cancel": "Atcelt", "pad.loading": "Ielādē…", - "pad.passwordRequired": "Ir nepieciešama parole, lai piekļūtu šim pad", "pad.permissionDenied": "Atvaino, bet tev nav pieejas šim pad.", - "pad.wrongPassword": "Jūsu parole bija nepareiza", "pad.settings.padSettings": "Pad Iestatijumi", "pad.settings.myView": "Mans viedoklis", "pad.settings.stickychat": "Čats vienmēr ekrānā", diff --git a/src/locales/map-bms.json b/src/locales/map-bms.json index da02a51c0..781f3e15d 100644 --- a/src/locales/map-bms.json +++ b/src/locales/map-bms.json @@ -27,9 +27,7 @@ "pad.colorpicker.save": "Simpen", "pad.colorpicker.cancel": "Batalna", "pad.loading": "Muatna...", - "pad.passwordRequired": "Rika perlu tembung sandhi kanggo ngakses pad kiye", "pad.permissionDenied": "Rika ora duwe idin kanggo ngakses pad kiye", - "pad.wrongPassword": "Tembung sandhine Rika salah", "pad.settings.padSettings": "Pangaturan Pad", "pad.settings.myView": "Delengané Inyong", "pad.settings.stickychat": "Dopokan mesti nang layar", diff --git a/src/locales/mg.json b/src/locales/mg.json index 510db8433..d731335ac 100644 --- a/src/locales/mg.json +++ b/src/locales/mg.json @@ -21,7 +21,6 @@ "pad.colorpicker.cancel": "Aoka ihany", "pad.loading": "Am-pakàna…", "pad.permissionDenied": "Tsy manana lalalana mijery ity pad ity ianao", - "pad.wrongPassword": "Diso ny tenimiafinao", "pad.settings.padSettings": "Safidin'ny ped", "pad.settings.myView": "Ny jeriko", "pad.settings.linenocheck": "Laharan'ny andalana", diff --git a/src/locales/mk.json b/src/locales/mk.json index 29cce35fa..b8e03f546 100644 --- a/src/locales/mk.json +++ b/src/locales/mk.json @@ -63,9 +63,7 @@ "pad.colorpicker.cancel": "Откажи", "pad.loading": "Вчитувам...", "pad.noCookie": "Не можев да го најдам колачето. Овозможете колачиња во вашиот прелистувач! Вашата седница и нагодувања нема да бидат зачувани за следната посета. Ова можеби се должи на тоа што Etherpad е вклучен во iFrame во некои прелистувачи. Проверете дали Etherpad е на истиот поддомен/домен како матичниот iFrame", - "pad.passwordRequired": "Потребна е лозинка за пристап", "pad.permissionDenied": "За овде не е потребна дозвола за пристап", - "pad.wrongPassword": "Погрешна лозинка", "pad.settings.padSettings": "Поставки на тетратката", "pad.settings.myView": "Мој поглед", "pad.settings.stickychat": "Разговорите секогаш на екранот", @@ -171,6 +169,5 @@ "pad.impexp.importfailed": "Увозот не успеа", "pad.impexp.copypaste": "Прекопирајте", "pad.impexp.exportdisabled": "Извозот во форматот {{type}} е оневозможен. Ако сакате да дознаете повеќе за ова, обратете се кај системскиот администратор.", - "pad.impexp.maxFileSize": "Податотеката е преголема. Обратете се кај администраторот за да ви ја зголеми допуштената големина за увоз на податотеки", - "pad.impexp.permission": "Увозот е оневозможен бидејќи досега немате придонеси во тетраткава. Направете барем еден придонес пред да увезувате" + "pad.impexp.maxFileSize": "Податотеката е преголема. Обратете се кај администраторот за да ви ја зголеми допуштената големина за увоз на податотеки" } diff --git a/src/locales/ml.json b/src/locales/ml.json index b2069d6ac..27518f2b3 100644 --- a/src/locales/ml.json +++ b/src/locales/ml.json @@ -35,9 +35,7 @@ "pad.colorpicker.cancel": "റദ്ദാക്കുക", "pad.loading": "ശേഖരിക്കുന്നു...", "pad.noCookie": "കുക്കി കണ്ടെത്താനായില്ല. ദയവായി താങ്കളുടെ ബ്രൗസറിൽ കുക്കികൾ അനുവദിക്കുക! തിരുത്തലുകൾക്കിടയിൽ താങ്കളുടെ സെഷനോ സജ്ജീകരണങ്ങളോ സേവ് ചെയ്യപ്പെടുകയില്ല. ചില ബ്രൗസറുകളിൽ ഈതർപാഡ് ഐഫ്രെയിമിന്റെ കൂടെ ഉൾപ്പെടുത്തിയത്കൊണ്ടാവാം ഈ പ്രശ്നം. പാരന്റ് ഐഫ്രെയിമിന്റെ അതെ ഡൊമെയ്‌നിൽ/ഉപഡൊമെയ്‌നിൽ തന്നെയാണ് ഈതർപാഡ് ഉള്ളത് എന്ന കാര്യം ഉറപ്പ് വരുത്തുക.", - "pad.passwordRequired": "ഈ പാഡ് ഉപയോഗിക്കുന്നതിനായി ഒരു രഹസ്യവാക്ക് നൽകേണ്ടതാണ്", "pad.permissionDenied": "ഈ പാഡ് കാണുവാൻ താങ്കൾക്ക് അനുമതിയില്ല", - "pad.wrongPassword": "താങ്കൾ നല്കിയ രഹസ്യവാക്ക് തെറ്റായിരുന്നു", "pad.settings.padSettings": "പാഡ് സജ്ജീകരണങ്ങൾ", "pad.settings.myView": "എന്റെ കാഴ്ച", "pad.settings.stickychat": "തത്സമയസംവാദം എപ്പോഴും സ്ക്രീനിൽ കാണിക്കുക", diff --git a/src/locales/mn.json b/src/locales/mn.json index 24998c893..d4722100a 100644 --- a/src/locales/mn.json +++ b/src/locales/mn.json @@ -23,7 +23,6 @@ "pad.colorpicker.save": "Хадгалах", "pad.colorpicker.cancel": "Цуцлах", "pad.loading": "Уншиж байна...", - "pad.wrongPassword": "Таны оруулсан нууц үг буруу байна", "pad.settings.padSettings": "Падын тохиргоо", "pad.settings.myView": "Өөрийн харагдац", "pad.settings.linenocheck": "Мөрийн дугаар", diff --git a/src/locales/mnw.json b/src/locales/mnw.json index ae9f18c3c..09c44aa08 100644 --- a/src/locales/mnw.json +++ b/src/locales/mnw.json @@ -12,7 +12,6 @@ "pad.colorpicker.save": "ဂိုင်သိပ်", "pad.colorpicker.cancel": "တးပဲါ", "pad.loading": "ပတိုန်ဒၟံၚ်", - "pad.wrongPassword": "ကောန်ဍေၚ်မၞးဂှ် ဗၠေတ်မံၚ်", "pad.settings.language": "အရေဝ်ဘာသာ", "pad.importExport.import_export": "ပလုပ်/ပတိတ်", "pad.importExport.importSuccessful": "ဍိုက်ပေၚ်စိုပ်ဒတုဲ", diff --git a/src/locales/mr.json b/src/locales/mr.json index 536b578c0..3b9763cd3 100644 --- a/src/locales/mr.json +++ b/src/locales/mr.json @@ -16,7 +16,6 @@ "pad.colorpicker.save": "जतन करा", "pad.colorpicker.cancel": "रद्द करा", "pad.loading": "प्रभारण करीत आहे", - "pad.wrongPassword": "आपला परवलीचा शब्द चूक होता", "pad.settings.myView": "माझे दृश्य", "pad.settings.linenocheck": "रेषांचे क्रमांक", "pad.settings.language": "भाषा", diff --git a/src/locales/ms.json b/src/locales/ms.json index 3546f4ae5..7b80c1e4f 100644 --- a/src/locales/ms.json +++ b/src/locales/ms.json @@ -28,9 +28,7 @@ "pad.colorpicker.cancel": "Batalkan", "pad.loading": "Sedang dimuatkan...", "pad.noCookie": "Cookie tidak dapat dijumpai. Tolong benarkan cookie dalam pelayar anda!", - "pad.passwordRequired": "Anda memerlukan kata laluan untuk mengakses pad ini", "pad.permissionDenied": "Anda tiada kebenaran untuk mengakses pad ini", - "pad.wrongPassword": "Kata laluan anda salah", "pad.settings.padSettings": "Tetapan Pad", "pad.settings.myView": "Paparan Saya", "pad.settings.stickychat": "Sentiasa bersembang pada skrin", diff --git a/src/locales/nap.json b/src/locales/nap.json index 8e508b4c0..017bc7a45 100644 --- a/src/locales/nap.json +++ b/src/locales/nap.json @@ -29,9 +29,7 @@ "pad.colorpicker.cancel": "Canciella", "pad.loading": "Carecamiento 'n curso…", "pad.noCookie": "Cookie nun truvata. Pe' piacere premmettete 'e cookies dint' 'o navigatóre vuosto!", - "pad.passwordRequired": "Pe' accede a chisto Pad è necessaria 'na password", "pad.permissionDenied": "Nun se dispunne d\"e permisse necessare pe' accede a chisto Pad", - "pad.wrongPassword": "'A password è sbagliata", "pad.settings.padSettings": "Mpostazzione d\"o pad", "pad.settings.myView": "Mia Veruta", "pad.settings.stickychat": "Chat sempe ncopp' 'o schermo", diff --git a/src/locales/nb.json b/src/locales/nb.json index 1eac430e2..25e57f98d 100644 --- a/src/locales/nb.json +++ b/src/locales/nb.json @@ -33,9 +33,7 @@ "pad.colorpicker.cancel": "Avbryt", "pad.loading": "Laster …", "pad.noCookie": "Kunne ikke finne informasjonskapselen. Vennligst tillat informasjonskapsler (cookies) i din nettleser! Informasjonskapsler brukes til å lagre innstillinger o.l. Om feilen gjentar seg, kan det skyldes feil i nettsidens bruk av iFrame.", - "pad.passwordRequired": "Pad-en er låst med et passord", "pad.permissionDenied": "Du har ikke tilgang til denne pad-en", - "pad.wrongPassword": "Feil passord", "pad.settings.padSettings": "Padinnstillinger", "pad.settings.myView": "Min visning", "pad.settings.stickychat": "Chat alltid synlig", @@ -136,6 +134,5 @@ "pad.impexp.importfailed": "Import feilet", "pad.impexp.copypaste": "Vennligst kopier og lim inn", "pad.impexp.exportdisabled": "Eksportering som {{type}} er deaktivert. Vennligst kontakt systemadministratoren din for detaljer.", - "pad.impexp.maxFileSize": "Filen er for stor. Kontakt systemansvarlig for å øke filstørrelse for import", - "pad.impexp.permission": "Import er deaktivert fordi du aldri har bidratt til denne padden. Vennligst bidra minst en gang før du importerer" + "pad.impexp.maxFileSize": "Filen er for stor. Kontakt systemansvarlig for å øke filstørrelse for import" } diff --git a/src/locales/nds.json b/src/locales/nds.json index 7e3024033..e18be8763 100644 --- a/src/locales/nds.json +++ b/src/locales/nds.json @@ -27,9 +27,7 @@ "pad.colorpicker.save": "Spiekern", "pad.colorpicker.cancel": "Afbreken", "pad.loading": "Läädt…", - "pad.passwordRequired": "Du bruukst en Passwoort, wenn du in düt Pad rinwullt", "pad.permissionDenied": "In düt Pad dröffst du nich rin", - "pad.wrongPassword": "Dien Passwoort weer nich richtig", "pad.settings.padSettings": "So is dat Pad instellt", "pad.settings.myView": "So heff ik dat instellt", "pad.settings.stickychat": "Chat jümmers wiesen", diff --git a/src/locales/ne.json b/src/locales/ne.json index 182dc6c8a..30f4b8046 100644 --- a/src/locales/ne.json +++ b/src/locales/ne.json @@ -29,9 +29,7 @@ "pad.colorpicker.save": "सङ्ग्रह गर्ने", "pad.colorpicker.cancel": "रद्द", "pad.loading": "खुल्दै छ…", - "pad.passwordRequired": "यो प्यड खोल्न पासवर्ड चाहिन्छ", "pad.permissionDenied": "तपाईंलाई यो प्याड खोल्न अनुमति छैन", - "pad.wrongPassword": "तपाईंको पासवर्ड गलत थियो", "pad.settings.padSettings": "प्याड सेटिङ्गहरू", "pad.settings.myView": "मेरो दृष्य", "pad.settings.stickychat": "पर्दामा सधै च्याट गर्ने", diff --git a/src/locales/nl.json b/src/locales/nl.json index 4c7a4aa19..7030c41b8 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -37,9 +37,7 @@ "pad.colorpicker.cancel": "Annuleren", "pad.loading": "Bezig met laden…", "pad.noCookie": "Er kon geen cookie gevonden worden. Zorg ervoor dat uw browser cookies accepteert.", - "pad.passwordRequired": "U hebt een wachtwoord nodig om toegang te krijgen tot deze pad", "pad.permissionDenied": "U hebt geen rechten om deze pad te bekijken", - "pad.wrongPassword": "U hebt een onjuist wachtwoord ingevoerd", "pad.settings.padSettings": "Padinstellingen", "pad.settings.myView": "Mijn overzicht", "pad.settings.stickychat": "Chat altijd zichtbaar", diff --git a/src/locales/nn.json b/src/locales/nn.json index 2acbd46fe..e607ecd3a 100644 --- a/src/locales/nn.json +++ b/src/locales/nn.json @@ -26,9 +26,7 @@ "pad.colorpicker.save": "Lagra", "pad.colorpicker.cancel": "Avbryt", "pad.loading": "Lastar …", - "pad.passwordRequired": "Du treng eit passord for å opna denne blokka", "pad.permissionDenied": "Du har ikkje tilgang til denne blokka", - "pad.wrongPassword": "Feil passord", "pad.settings.padSettings": "Blokkinnstillingar", "pad.settings.myView": "Mi visning", "pad.settings.stickychat": "Prat alltid synleg", diff --git a/src/locales/oc.json b/src/locales/oc.json index 1c5a01207..b61833cab 100644 --- a/src/locales/oc.json +++ b/src/locales/oc.json @@ -28,9 +28,7 @@ "pad.colorpicker.cancel": "Anullar", "pad.loading": "Cargament...", "pad.noCookie": "Lo cookie a pas pogut èsser trobat. Autorizatz los cookies dins vòstre navigador !", - "pad.passwordRequired": "Avètz besonh d'un senhal per accedir a aqueste Pad", "pad.permissionDenied": "Vos es pas permés d’accedir a aqueste Pad.", - "pad.wrongPassword": "Senhal incorrècte", "pad.settings.padSettings": "Paramètres del Pad", "pad.settings.myView": "Ma vista", "pad.settings.stickychat": "Afichar totjorn lo chat", diff --git a/src/locales/olo.json b/src/locales/olo.json index 331ddf877..6ee8f430b 100644 --- a/src/locales/olo.json +++ b/src/locales/olo.json @@ -10,7 +10,6 @@ "pad.toolbar.settings.title": "Azetukset", "pad.colorpicker.save": "Tallenda", "pad.colorpicker.cancel": "Hylgiä", - "pad.wrongPassword": "Peittosana oli viärin", "pad.settings.linenocheck": "Riädynoumerot", "pad.settings.rtlcheck": "Luve syväindö oigielpäi huruale?", "pad.settings.language": "Kieli:", diff --git a/src/locales/os.json b/src/locales/os.json index 8d33b6810..e341ae065 100644 --- a/src/locales/os.json +++ b/src/locales/os.json @@ -26,9 +26,7 @@ "pad.colorpicker.save": "Нывæрын", "pad.colorpicker.cancel": "Ныууадзын", "pad.loading": "Æвгæд цæуы...", - "pad.passwordRequired": "Ацы документмӕ рывналынӕн дӕ хъӕуы пароль", "pad.permissionDenied": "Дӕуӕн нӕй бар ацы документмӕ рывналын", - "pad.wrongPassword": "Дӕ пароль раст нӕу", "pad.settings.padSettings": "Документы уагӕвӕрдтытӕ", "pad.settings.myView": "Мӕ уынд", "pad.settings.stickychat": "Ныхас алкуыдӕр ӕвдисын", diff --git a/src/locales/pa.json b/src/locales/pa.json index 30661eb7c..1d532884a 100644 --- a/src/locales/pa.json +++ b/src/locales/pa.json @@ -31,9 +31,7 @@ "pad.colorpicker.cancel": "ਰੱਦ ਕਰੋ", "pad.loading": "…ਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ", "pad.noCookie": "ਕੂਕੀਜ਼ ਨਹੀਂ ਲੱਭੀਅਾਂ। ਕਿਰਪਾ ਕਰਕੇ ਬ੍ਰਾੳੂਜ਼ਰ ਵਿੱਚ ਕੂਕੀਜ਼ ਲਾਗੂ ਕਰੋ।", - "pad.passwordRequired": "ਇਸ ਪੈਡ ਤੱਕ ਅਪੜਨ ਲਈ ਤੁਹਾਨੂੰ ਇੱਕ ਲੰਘ-ਸ਼ਬਦ ਦੀ ਲੋੜ ਹੈ", "pad.permissionDenied": "ਇਹ ਪੈਡ ਵਰਤਨ ਲਈ ਤੁਹਾਨੂੰ ਅਧਿਕਾਰ ਨਹੀਂ ਹਨ", - "pad.wrongPassword": "ਤੁਹਾਡਾ ਲੰਘ-ਸ਼ਬਦ ਗਲਤ ਸੀ", "pad.settings.padSettings": "ਪੈਡ ਸੈਟਿੰਗ", "pad.settings.myView": "ਮੇਰੀ ਝਲਕ", "pad.settings.stickychat": "ਹਮੇਸ਼ਾ ਸਕਰੀਨ ਉੱਤੇ ਗੱਲ ਕਰੋ", diff --git a/src/locales/pl.json b/src/locales/pl.json index e637c1b11..34cb1bf2d 100644 --- a/src/locales/pl.json +++ b/src/locales/pl.json @@ -42,9 +42,7 @@ "pad.colorpicker.cancel": "Anuluj", "pad.loading": "Ładowanie...", "pad.noCookie": "Nie można znaleźć pliku cookie. Proszę zezwolić na pliki cookie w przeglądarce! Twoja sesja i ustawienia nie zostaną zapisane między wizytami. Może to wynikać z włączenia Etherpad do ramki iFrame w niektórych przeglądarkach. Upewnij się, że Etherpad jest w tej samej subdomenie/domenie, co nadrzędna ramka iFrame", - "pad.passwordRequired": "Musisz podać hasło aby uzyskać dostęp do tego dokumentu", "pad.permissionDenied": "Nie masz uprawnień dostępu do tego dokumentu", - "pad.wrongPassword": "Nieprawidłowe hasło", "pad.settings.padSettings": "Ustawienia dokumentu", "pad.settings.myView": "Mój widok", "pad.settings.stickychat": "Czat zawsze na ekranie", @@ -145,6 +143,5 @@ "pad.impexp.importfailed": "Importowanie nie powiodło się", "pad.impexp.copypaste": "Proszę skopiować i wkleić", "pad.impexp.exportdisabled": "Eksport do formatu {{type}} jest wyłączony. Proszę skontaktować się z administratorem aby uzyskać więcej szczegółów.", - "pad.impexp.maxFileSize": "Plik jest zbyt duży. Skontaktuj się z administratorem aby zwiększył maksymalny dopuszczalny rozmiar importowanych plików", - "pad.impexp.permission": "Importowanie jest zablokowane, ponieważ nie edytowałeś jeszcze tego dokumentu. Wykonaj przynajmniej jedną zmianę zanim zaczniesz importować" + "pad.impexp.maxFileSize": "Plik jest zbyt duży. Skontaktuj się z administratorem aby zwiększył maksymalny dopuszczalny rozmiar importowanych plików" } diff --git a/src/locales/pms.json b/src/locales/pms.json index 6411091ea..5b91c5e4d 100644 --- a/src/locales/pms.json +++ b/src/locales/pms.json @@ -61,9 +61,7 @@ "pad.colorpicker.cancel": "Anulé", "pad.loading": "Antramentr ch'as caria…", "pad.noCookie": "Ël bëscotin a l'é nen ëstàit trovà. Për piasì, ch'a autorisa ij bëscotin su sò navigador! Soa session e sò paràmeter a saran pa argistrà antra na vìsita e l'àutra. Sòn a peul esse dovù al fàit che Etherpad a l'é contnù an n'iFrame an chèich navigador. Ch'a contròla che Etherpad resta ant l'istess sot-domini/domini ëd sò ce iFrame", - "pad.passwordRequired": "A l'ha da manca ëd na ciav për acede a cost feuj-sì", "pad.permissionDenied": "A l'ha nen ël përmess d'acede a 's feuj-sì", - "pad.wrongPassword": "Soa ciav a l'era nen giusta", "pad.settings.padSettings": "Paràmeter dël feuj", "pad.settings.myView": "Mia vista", "pad.settings.stickychat": "Ciaciarade sempe an slë scren", @@ -168,6 +166,5 @@ "pad.impexp.importfailed": "Amportassion falìa", "pad.impexp.copypaste": "Për piasì, ch'a còpia e ancòla", "pad.impexp.exportdisabled": "L'esportassion an formà {{type}} a l'é disativà. Për piasì, ch'a contata sò aministrator ëd sistema për ij detaj.", - "pad.impexp.maxFileSize": "Archivi tròp gròss. Ch'a contata sò aministrator ëd sit për sumenté la taja dj'archivi consentìa për j'amportassion", - "pad.impexp.permission": "L'amportassion a l'é disativà përchè chiel a l'ha pa mai contribuì a cost blòch-sì. Për piasì, ch'a contribuissa almanch na vira prima d'amporté" + "pad.impexp.maxFileSize": "Archivi tròp gròss. Ch'a contata sò aministrator ëd sit për sumenté la taja dj'archivi consentìa për j'amportassion" } diff --git a/src/locales/ps.json b/src/locales/ps.json index 135e1354e..35391e41a 100644 --- a/src/locales/ps.json +++ b/src/locales/ps.json @@ -20,8 +20,6 @@ "pad.colorpicker.save": "خوندي کول", "pad.colorpicker.cancel": "ناگارل", "pad.loading": "رابرسېرېږي...", - "pad.passwordRequired": "دې ليکچې ته د لاسرسي لپاره تاسې يو پټنوم ته اړتيا لرۍ", - "pad.wrongPassword": "پټنوم مو سم نه و", "pad.settings.padSettings": "د ليکچې امستنې", "pad.settings.myView": "زما کتنه", "pad.settings.stickychat": "تل په پردې بانډار کول", diff --git a/src/locales/pt-br.json b/src/locales/pt-br.json index 31c4ca0dd..277bbec6a 100644 --- a/src/locales/pt-br.json +++ b/src/locales/pt-br.json @@ -58,9 +58,7 @@ "pad.colorpicker.cancel": "Cancelar", "pad.loading": "Carregando...", "pad.noCookie": "Não foi possível encontrar o cookie. Por favor, permita cookies no seu navegador! Sua sessão e configurações não serão salvas entre as visitas. Isso pode ser devido ao fato de o Etherpad ser incluído em um iFrame em alguns navegadores. Verifique se o Etherpad está no mesmo subdomínio/domínio que o iFrame pai", - "pad.passwordRequired": "Você precisa de uma senha para acessar esta Nota", "pad.permissionDenied": "Você não tem permissão para acessar esta Nota", - "pad.wrongPassword": "Senha incorreta", "pad.settings.padSettings": "Configurações da Nota", "pad.settings.myView": "Minha Visão", "pad.settings.stickychat": "Bate-papo sempre visível", @@ -165,6 +163,5 @@ "pad.impexp.importfailed": "A importação falhou", "pad.impexp.copypaste": "Copie e cole", "pad.impexp.exportdisabled": "A exportação em formato {{type}} está desativada. Comunique-se com o administrador do sistema para detalhes.", - "pad.impexp.maxFileSize": "Arquivo muito grande. Entre em contato com o administrador do site para aumentar o tamanho do arquivo permitido para importação", - "pad.impexp.permission": "A importação está desativada porque você nunca contribuiu para este bloco. Contribua pelo menos uma vez antes de importar" + "pad.impexp.maxFileSize": "Arquivo muito grande. Entre em contato com o administrador do site para aumentar o tamanho do arquivo permitido para importação" } diff --git a/src/locales/pt.json b/src/locales/pt.json index b770bd15d..03bfcb92c 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -40,9 +40,7 @@ "pad.colorpicker.cancel": "Cancelar", "pad.loading": "A carregar…", "pad.noCookie": "Não foi possível encontrar o ''cookie''. Por favor, permita ''cookies'' no seu navegador! A sua sessão e as definições não foram guardadas entre as visitas. Isto poderá ter ocorrido porque Etherpad foi incluído numa iFrame em alguns «Navegadores». Por favor, certifique-se que Etherpad está no mesmo subdomínio / domínio que a iFrame fonte", - "pad.passwordRequired": "Precisa de uma palavra-passe para aceder a esta nota", "pad.permissionDenied": "Não tem permissão para aceder a esta nota", - "pad.wrongPassword": "A sua palavra-passe estava errada", "pad.settings.padSettings": "Configurações da nota", "pad.settings.myView": "A minha vista", "pad.settings.stickychat": "Conversação sempre no ecrã", @@ -146,6 +144,5 @@ "pad.impexp.importfailed": "A importação falhou", "pad.impexp.copypaste": "Copie e insira, por favor", "pad.impexp.exportdisabled": "A exportação no formato {{type}} está desativada. Por favor, contacte o administrador do sistema para mais informações.", - "pad.impexp.maxFileSize": "Ficheiro demasiado grande. Contacte o administrador do ''site'' para aumentar o tamanho máximo dos ficheiros importados", - "pad.impexp.permission": "A importação está desativada porque nunca contribuiu para esta nota. Contribua pelo menos uma vez antes de importar, por favor" + "pad.impexp.maxFileSize": "Ficheiro demasiado grande. Contacte o administrador do ''site'' para aumentar o tamanho máximo dos ficheiros importados" } diff --git a/src/locales/qqq.json b/src/locales/qqq.json index 327bd8e18..7dfd56bc3 100644 --- a/src/locales/qqq.json +++ b/src/locales/qqq.json @@ -33,9 +33,7 @@ "pad.colorpicker.save": "Used as button text in the \"Color picker\" window.\n\nSee also:\n* {{msg-etherpadlite|Pad.colorpicker.cancel}}\n{{Identical|Save}}", "pad.colorpicker.cancel": "Used as button text in the \"Color picker\" window.\n\nSee also:\n* {{msg-etherpadlite|Pad.colorpicker.save}}\n{{Identical|Cancel}}", "pad.loading": "Used to indicate the pad is being loaded.\n{{Identical|Loading}}", - "pad.passwordRequired": "Followed by the \"Password\" input box.", "pad.permissionDenied": "Used as error message.", - "pad.wrongPassword": "Used as error message if the specified password is wrong.", "pad.settings.padSettings": "Used as heading of settings window", "pad.settings.myView": "Section heading for a users personal settings, meaning changes to the settings in this section will only affect the current view (this browser window) of the pad.", "pad.settings.stickychat": "Used as checkbox label", diff --git a/src/locales/ro.json b/src/locales/ro.json index 9f4cb8e3d..2c5167b12 100644 --- a/src/locales/ro.json +++ b/src/locales/ro.json @@ -34,9 +34,7 @@ "pad.colorpicker.cancel": "Anulează", "pad.loading": "Se încarcă...", "pad.noCookie": "Cookie-ul nu a putut fi găsit. Vă rugăm să permiteți cookie-urile în browser! Sesiunea și setările nu vor fi salvate între vizite. Aceasta se poate datora faptului că Etherpad este inclus într-un iFrame în unele browsere. Vă rugăm să vă asigurați că Etherpad este pe același subdomeniu/domeniu ca iFrame părinte", - "pad.passwordRequired": "Ai nevoie de o parolă pentru a accesa acest pad", "pad.permissionDenied": "Nu ai permisiunea să accesezi acest pad", - "pad.wrongPassword": "Parola ta este greșită", "pad.settings.padSettings": "Setări pentru Pad", "pad.settings.myView": "Perspectiva mea", "pad.settings.stickychat": "Chat-ul întotdeauna pe ecran", @@ -135,6 +133,5 @@ "pad.impexp.importfailed": "Import eșuat", "pad.impexp.copypaste": "Vă rugăm să copiați și să lipiți", "pad.impexp.exportdisabled": "Exportul ca format {{type}} este dezactivat. Vă rugăm să contactați administratorul de sistem pentru detalii.", - "pad.impexp.maxFileSize": "Fișier prea mare. Contactați administratorul site-ului pentru a crește dimensiunea permisă a fișierului pentru import", - "pad.impexp.permission": "Importul este dezactivat deoarece nu ați contribuit niciodată la această placă. Vă rugăm să contribuiți cel puțin o dată înainte de import" + "pad.impexp.maxFileSize": "Fișier prea mare. Contactați administratorul site-ului pentru a crește dimensiunea permisă a fișierului pentru import" } diff --git a/src/locales/ru.json b/src/locales/ru.json index 6f25f6968..71ee5eacc 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -13,14 +13,20 @@ "Okras", "Patrick Star", "Teretalexev", - "Volkov" + "Volkov", + "Арсен Асхат" ] }, + "admin_plugins": "Менеджер плагинов", "admin_plugins.available": "Доступные плагины", + "admin_plugins.available_not-found": "Плагины не найдены.", "admin_plugins.available_install.value": "Установить", "admin_plugins.description": "Описание", + "admin_plugins.installed_nothing": "Вы еще не установили ни одного плагина.", "admin_plugins.installed_uninstall.value": "Удалить", "admin_plugins.version": "Версия", + "admin_plugins_info.hooks": "Установленные крючки", + "admin_plugins_info.version_number": "Номер версии", "admin_settings": "Настройки", "admin_settings.current_save.value": "Сохранить настройки", "index.newPad": "Создать", @@ -47,9 +53,7 @@ "pad.colorpicker.cancel": "Отмена", "pad.loading": "Загрузка...", "pad.noCookie": "Куки не найдены. Пожалуйста, включите куки в вашем браузере! Ваш сеанс и настройки не будут сохранены между посещениями. Это может быть связано с тем, что Etherpad включен в iFrame в некоторых браузерах. Убедитесь, что Etherpad находится в том же поддомене/домене, что и родительский iFrame.", - "pad.passwordRequired": "Вам нужен пароль для доступа", "pad.permissionDenied": "У вас нет разрешения на доступ", - "pad.wrongPassword": "Неправильный пароль", "pad.settings.padSettings": "Настройки документа", "pad.settings.myView": "Мой вид", "pad.settings.stickychat": "Всегда отображать чат", @@ -155,6 +159,5 @@ "pad.impexp.importfailed": "Ошибка при импорте", "pad.impexp.copypaste": "Пожалуйста, скопируйте", "pad.impexp.exportdisabled": "Экспорт в формате {{type}} отключён. Для подробной информации обратитесь к системному администратору.", - "pad.impexp.maxFileSize": "Файл слишком большой. Обратитесь к администратору сайта, чтобы увеличить разрешённый размер файла для импорта", - "pad.impexp.permission": "Импорт отключен, поскольку вы никогда не вносили вклад в этот документ. Пожалуйста, внесите свой вклад хотя бы один раз перед импортом" + "pad.impexp.maxFileSize": "Файл слишком большой. Обратитесь к администратору сайта, чтобы увеличить разрешённый размер файла для импорта" } diff --git a/src/locales/sco.json b/src/locales/sco.json index eaad6147f..75437be92 100644 --- a/src/locales/sco.json +++ b/src/locales/sco.json @@ -28,9 +28,7 @@ "pad.colorpicker.cancel": "Cancel", "pad.loading": "Laidin...", "pad.noCookie": "Cookie could nae be foond. Please allae cookies in yer brouser!", - "pad.passwordRequired": "Ye need ae passwaird fer tae access this pad", "pad.permissionDenied": "Ye dinna hae permeession tae access this pad", - "pad.wrongPassword": "Yer password wis wrang", "pad.settings.padSettings": "Pad Settins", "pad.settings.myView": "Ma Luik", "pad.settings.stickychat": "Tauk aye oan screen", diff --git a/src/locales/sd.json b/src/locales/sd.json index 14abc6537..1aad42778 100644 --- a/src/locales/sd.json +++ b/src/locales/sd.json @@ -2,10 +2,12 @@ "@metadata": { "authors": [ "BaRaN6161 TURK", + "Kaleem Bhatti", "Mehtab ahmed", "Tweety" ] }, + "admin_settings": "ترتيبون", "index.newPad": "نئين پٽي", "index.createOpenPad": "يا نالي سان ڪا پٽي تخليق ڪريو\\کوليو:", "pad.toolbar.bold.title": "وزني (Ctrl+B)", @@ -24,7 +26,6 @@ "pad.colorpicker.save": "سانڍيو", "pad.colorpicker.cancel": "رد", "pad.loading": "لاهيندي...", - "pad.wrongPassword": "توهان جو ڳجھو لفظ غلط هيو", "pad.settings.padSettings": "پٽي جو ترتيبون", "pad.settings.myView": "منهنجو نظارو", "pad.settings.stickychat": "ڳالھ ٻولھ هميشه پردي تي ڪريو", diff --git a/src/locales/sh.json b/src/locales/sh.json index db60b3108..a17f6ae0d 100644 --- a/src/locales/sh.json +++ b/src/locales/sh.json @@ -28,9 +28,7 @@ "pad.colorpicker.cancel": "Otkaži", "pad.loading": "Učitavam...", "pad.noCookie": "Nisam mogao pronaći kolačić. Omogućite kolačiće u vašem pregledniku!", - "pad.passwordRequired": "Potrebna je lozinka za pristup", "pad.permissionDenied": "Za ovdje nije potrebna dozvola za pristup", - "pad.wrongPassword": "Pogrešna lozinka", "pad.settings.padSettings": "Postavke blokića", "pad.settings.myView": "Moj prikaz", "pad.settings.stickychat": "Ćaskanje uvijek na ekranu", diff --git a/src/locales/shn.json b/src/locales/shn.json index 67cf557b4..5f4b6218d 100644 --- a/src/locales/shn.json +++ b/src/locales/shn.json @@ -29,9 +29,7 @@ "pad.colorpicker.cancel": "ဢမ်ႇႁဵတ်း", "pad.loading": "တိုၵ်ႉလူတ်ႇ", "pad.noCookie": "ၶုၵ်းၶီး ဢမ်ႇႁၼ်လႆႈ။ ၶႅၼ်းတေႃႈ ၶႂၢင်းပၼ် ၶုၵ်းၶီး တီႈၼႂ်း ပရၢဝ်ႇသႃႇၸဝ်ႈၵဝ်ႇ", - "pad.passwordRequired": "တႃႇၶဝ်ႈတီႈၽႅတ်ႉၼႆႉ ၸဝ်ႈၵဝ်ႇ လူဝ်ႇမီး မၢႆလပ်ႉ", "pad.permissionDenied": "ၸဝ်ႈၵဝ်ႇ ဢမ်ႇမီးၶေႃႈၶႂၢင်ႉ တႃႇၶဝ်ႈၼႂ်းၽႅတ်ႉၼႆႉ", - "pad.wrongPassword": "မၢႆလပ်ႉၸဝ်ႈၵဝ်ႇ ၽိတ်း", "pad.settings.padSettings": "ပိူင်သၢင်ႈၽႅတ်ႉ", "pad.settings.myView": "ဝိဝ်းၵဝ်", "pad.settings.stickychat": "ၶျၢတ်ႉၼိူဝ်ၼႃႈၽိဝ် တႃႇသေႇ", diff --git a/src/locales/sk.json b/src/locales/sk.json index 67ac5ff31..9f4a714c8 100644 --- a/src/locales/sk.json +++ b/src/locales/sk.json @@ -31,9 +31,7 @@ "pad.colorpicker.cancel": "Zrušiť", "pad.loading": "Načítava sa...", "pad.noCookie": "Cookie nebolo možné nájsť. Povoľte prosím cookies vo vašom prehliadači.", - "pad.passwordRequired": "Prístup k tomuto Padu je chránený heslom", "pad.permissionDenied": "Ľutujeme, nemáte oprávnenie pristupovať k tomuto Padu", - "pad.wrongPassword": "Nesprávne heslo", "pad.settings.padSettings": "Nastavenia Padu", "pad.settings.myView": "Vlastný pohľad", "pad.settings.stickychat": "Chat stále na obrazovke", diff --git a/src/locales/skr-arab.json b/src/locales/skr-arab.json index 9941b6926..826148952 100644 --- a/src/locales/skr-arab.json +++ b/src/locales/skr-arab.json @@ -13,7 +13,6 @@ "pad.colorpicker.save": "بچاؤ", "pad.colorpicker.cancel": "منسوخ", "pad.loading": "لوڈ تھیندا پئے۔۔۔", - "pad.wrongPassword": "تہاݙ پاسورڈ غلط ہے", "pad.settings.padSettings": "پیڈ ترتیباں", "pad.settings.fontType": "فونٹ قسم:", "pad.settings.language": "زبان:", diff --git a/src/locales/sl.json b/src/locales/sl.json index 7eb021e07..6fa5b1bd3 100644 --- a/src/locales/sl.json +++ b/src/locales/sl.json @@ -31,9 +31,7 @@ "pad.colorpicker.cancel": "Prekliči", "pad.loading": "Nalaganje ...", "pad.noCookie": "Piškotka ni bilo mogoče najti. Prosimo, dovolite piškotke v vašem brskalniku!", - "pad.passwordRequired": "Za dostop do dokumenta potrebujete geslo.", "pad.permissionDenied": "Nimate dovoljenja za dostop do tega dokumenta.", - "pad.wrongPassword": "Vpisano geslo je napačno", "pad.settings.padSettings": "Nastavitve dokumenta", "pad.settings.myView": "Moj pogled", "pad.settings.stickychat": "Vsebina klepeta je vedno na zaslonu", diff --git a/src/locales/sq.json b/src/locales/sq.json index 0eb2c2bf4..4e6a8cd3b 100644 --- a/src/locales/sq.json +++ b/src/locales/sq.json @@ -29,9 +29,7 @@ "pad.colorpicker.cancel": "Anuloje", "pad.loading": "Po ngarkohet…", "pad.noCookie": "S’u gjet dot cookie. Ju lutemi, lejoni cookie-t te shfletuesi juaj!", - "pad.passwordRequired": "Ju duhet një fjalëkalim që të mund të përdorni këtë bllok", "pad.permissionDenied": "S’keni leje të hyni në këtë bllok", - "pad.wrongPassword": "Fjalëkalimi juaj qe gabim", "pad.settings.padSettings": "Rregullime Blloku", "pad.settings.myView": "Pamja ime", "pad.settings.stickychat": "Fjalosje përherë në ekran", diff --git a/src/locales/sr-ec.json b/src/locales/sr-ec.json index 230cbdf6b..e77dfc83f 100644 --- a/src/locales/sr-ec.json +++ b/src/locales/sr-ec.json @@ -34,9 +34,7 @@ "pad.colorpicker.cancel": "Откажи", "pad.loading": "Учитавам…", "pad.noCookie": "Колачић није пронађен. Молимо да укључите колачиће у вашем прегледавачу!", - "pad.passwordRequired": "Требате имати лозинку како бисте приступили овом паду", "pad.permissionDenied": "Немате дозволу да приступите овом паду", - "pad.wrongPassword": "Ваша лозинка није исправна", "pad.settings.padSettings": "Подешавања пада", "pad.settings.myView": "Мој приказ", "pad.settings.stickychat": "Ћаскање увек на екрану", diff --git a/src/locales/sr-el.json b/src/locales/sr-el.json index 3c4bff17a..d961a0b41 100644 --- a/src/locales/sr-el.json +++ b/src/locales/sr-el.json @@ -25,9 +25,7 @@ "pad.colorpicker.cancel": "Otkaži", "pad.loading": "Učitavam…", "pad.noCookie": "Kolačić nije pronađen. Molimo da uključite kolačiće u vašem pregledavaču!", - "pad.passwordRequired": "Trebate imati lozinku kako biste pristupili ovom padu", "pad.permissionDenied": "Nemate dozvolu da pristupite ovom padu", - "pad.wrongPassword": "Vaša lozinka nije ispravna", "pad.settings.padSettings": "Podešavanja pada", "pad.settings.myView": "Moj prikaz", "pad.settings.stickychat": "Ćaskanje uvek na ekranu", diff --git a/src/locales/sv.json b/src/locales/sv.json index 44bae568a..ca117642e 100644 --- a/src/locales/sv.json +++ b/src/locales/sv.json @@ -8,6 +8,7 @@ "WikiPhoenix" ] }, + "admin.page-title": "Administratörernas instrumentpanel - Etherpad", "admin_plugins": "Insticksprogramhanterare", "admin_plugins.available": "Tillgängliga insticksprogram", "admin_plugins.available_not-found": "Inga insticksmoduler hittades.", @@ -35,6 +36,8 @@ "admin_plugins_info.version_number": "Versionsnummer", "admin_settings": "Inställningar", "admin_settings.current": "Nuvarande konfiguration", + "admin_settings.current_example-devel": "Exempelmall för utvecklingsinställningar", + "admin_settings.current_example-prod": "Exempelmall för produktionsinställningar", "admin_settings.current_restart.value": "Starta om Etherpad", "admin_settings.current_save.value": "Spara inställningar", "admin_settings.page-title": "Inställningar - Etherpad", @@ -62,9 +65,7 @@ "pad.colorpicker.cancel": "Avbryt", "pad.loading": "Läser in …", "pad.noCookie": "Kunde inte hitta några kakor. Var god tillåt kakor i din webbläsare! Din session och inställningar kommer inte sparas mellan dina besök. Detta kan bero på att Etherpad inte ligger inuti en iFrame i vissa webbläsare. Se till att Etherpad är i samma underdomän/domän som det överordnade iFrame-elementet.", - "pad.passwordRequired": "Du behöver ett lösenord för att få tillgång till detta block", "pad.permissionDenied": "Du har inte åtkomstbehörighet för detta block", - "pad.wrongPassword": "Ditt lösenord var fel", "pad.settings.padSettings": "Blockinställningar", "pad.settings.myView": "Min vy", "pad.settings.stickychat": "Chatten alltid på skärmen", @@ -170,6 +171,5 @@ "pad.impexp.importfailed": "Importering misslyckades", "pad.impexp.copypaste": "Var god kopiera och klistra in", "pad.impexp.exportdisabled": "Exportering av formatet {{type}} är inaktiverad. Var god kontakta din systemadministratör för mer information.", - "pad.impexp.maxFileSize": "Filen är för stor. Kontakta din systemadministratör för att öka den tillåtna filstorleken för importering", - "pad.impexp.permission": "Importering är inaktiverat eftersom du aldrig har bidragit till detta block. Var god bidra minst en gång innan du importerar" + "pad.impexp.maxFileSize": "Filen är för stor. Kontakta din systemadministratör för att öka den tillåtna filstorleken för importering" } diff --git a/src/locales/ta.json b/src/locales/ta.json index 6f08c3bc6..7fbd151f9 100644 --- a/src/locales/ta.json +++ b/src/locales/ta.json @@ -19,9 +19,7 @@ "pad.colorpicker.save": "சேமி", "pad.colorpicker.cancel": "இரத்து செய்", "pad.loading": "ஏற்றப்படுகிறது...", - "pad.passwordRequired": "இவ்வட்டையை அணுக தங்களுக்கு ஒரு கடவுச்சொல் தேவைப்படும்", "pad.permissionDenied": "இவ்வட்டையை அணுக தங்களுக்கு அனுமதி இல்லை", - "pad.wrongPassword": "தங்கள் கடவுச்சொல் தவறானது", "pad.settings.padSettings": "அட்டை அமைவுகள்", "pad.settings.myView": "என் பார்வை", "pad.settings.stickychat": "திரையில் எப்பொழுதும் அரட்டை", diff --git a/src/locales/te.json b/src/locales/te.json index 2e136cae7..035fbf91e 100644 --- a/src/locales/te.json +++ b/src/locales/te.json @@ -30,7 +30,6 @@ "pad.colorpicker.cancel": "రద్దుచేయి", "pad.loading": "లోడవుతోంది...", "pad.permissionDenied": "ఈ పేజీని చూడడానికి మీరు అనుమతి లేదు.", - "pad.wrongPassword": "మీ సంకేతపదం తప్పు", "pad.settings.padSettings": "పలక అమరికలు", "pad.settings.myView": "నా ఉద్దేశ్యము", "pad.settings.stickychat": "తెరపైనే మాటామంతిని ఎల్లపుడు చేయుము", diff --git a/src/locales/th.json b/src/locales/th.json index db17e2321..84d3fc56a 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -29,9 +29,7 @@ "pad.colorpicker.cancel": "ยกเลิก", "pad.loading": "กำลังโหลด...", "pad.noCookie": "ไม่พบคุกกี้ กรุณาอนุญาติคุกกี้บนเบราว์เซอร์ของคุณ การเข้าสู่ระบบและการตั้งค่าจะไม่ถูกบันทึกขณะเยี่ยมชม อาจเกิดปัญหาจากอีเทอร์แพดถูกฝังไว้ในหน้าผ่าน iFrame ในบางเบราว์เซอร์ กรุณาตรวจสอบว่าอีเทอร์แพดอยู่ในโดเมนหรือโดเมนรองเดียวกันกับหน้าที่ฝัง iFrame", - "pad.passwordRequired": "คุณต้องใช้รหัสผ่านเพื่อเข้าถึงแผ่นจดบันทึกนี้", "pad.permissionDenied": "คุณไม่มีสิทธิ์เข้าถึงแผ่นจดบันทึกนี้", - "pad.wrongPassword": "รหัสผ่านไม่ถูกต้อง", "pad.settings.padSettings": "การตั้งค่าแผ่นจดบันทึก", "pad.settings.myView": "มุมมองของฉัน", "pad.settings.stickychat": "แสดงการแชทบนหน้าจอเสมอ", @@ -134,6 +132,5 @@ "pad.impexp.importfailed": "การนำเข้าล้มเหลว", "pad.impexp.copypaste": "โปรดคัดลอกแล้ววาง", "pad.impexp.exportdisabled": "การส่งออกเป็นรูปแบบ {{type}} ถูกปิดใช้งาน โปรดติดต่อผู้ดูแลระบบของคุณสำหรับรายละเอียดเพิ่มเติม", - "pad.impexp.maxFileSize": "ไฟล์ใหญ่เกินไป ติดต่อผู้ดูแลไซต์เพื่อให้ขยายขนาดไฟล์ที่นำเข้าได้", - "pad.impexp.permission": "การนำเข้าถูกปิดเนื่องจากคุณไม่เคยมีส่วนร่วมในแพดนี้ กรุณามีส่วนร่วมซักครั้งหนึ่งก่อนนำเข้า" + "pad.impexp.maxFileSize": "ไฟล์ใหญ่เกินไป ติดต่อผู้ดูแลไซต์เพื่อให้ขยายขนาดไฟล์ที่นำเข้าได้" } diff --git a/src/locales/tr.json b/src/locales/tr.json index b11146406..add15393f 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -71,9 +71,7 @@ "pad.colorpicker.cancel": "İptal", "pad.loading": "Yükleniyor...", "pad.noCookie": "Çerez bulunamadı. Lütfen tarayıcınızda çerezlere izin veriniz! Lütfen tarayıcınızda çerezlere izin verin! Oturumunuz ve ayarlarınız ziyaretler arasında kaydedilmez. Bunun nedeni, Etherpad'in bazı Tarayıcılarda bir iFrame'e dahil edilmiş olması olabilir. Lütfen Etherpad'in üst iFrame ile aynı alt alanda/alanda olduğundan emin olun", - "pad.passwordRequired": "Bu bloknota erişebilmeniz için parolaya ihtiyacınız var", "pad.permissionDenied": "Bu bloknota erişmeye izniniz yok", - "pad.wrongPassword": "Parolanız yanlış", "pad.settings.padSettings": "Bloknot Ayarları", "pad.settings.myView": "Görünümüm", "pad.settings.stickychat": "Sohbeti her zaman ekranda yap", @@ -179,6 +177,5 @@ "pad.impexp.importfailed": "İçe aktarım başarısız oldu", "pad.impexp.copypaste": "Lütfen kopyala yapıştır yapın", "pad.impexp.exportdisabled": "{{type}} biçimiyle dışa aktarma devre dışı bırakıldı. Ayrıntılar için sistem yöneticinizle iletişime geçiniz.", - "pad.impexp.maxFileSize": "Dosya çok büyük. İçe aktarma için izin verilen dosya boyutunu artırmak için site yöneticinize başvurun", - "pad.impexp.permission": "Bu ped'e hiç katkıda bulunmadığınız için içe aktarma devre dışı. Lütfen içe aktarmadan önce en az bir kez katkıda bulunun" + "pad.impexp.maxFileSize": "Dosya çok büyük. İçe aktarma için izin verilen dosya boyutunu artırmak için site yöneticinize başvurun" } diff --git a/src/locales/uk.json b/src/locales/uk.json index 19042c963..44713a71b 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -36,9 +36,7 @@ "pad.colorpicker.cancel": "Скасувати", "pad.loading": "Завантаження…", "pad.noCookie": "Реп'яшки не знайдено. Будь-ласка, увімкніть реп'яшки у вашому браузері!", - "pad.passwordRequired": "Вам необхідний пароль для доступу до цього документа", "pad.permissionDenied": "У Вас немає дозволу для доступу до цього документа", - "pad.wrongPassword": "Неправильний пароль", "pad.settings.padSettings": "Налаштування документа", "pad.settings.myView": "Мій Вигляд", "pad.settings.stickychat": "Завжди відображувати чат", @@ -137,6 +135,5 @@ "pad.impexp.importfailed": "Помилка при імпортуванні", "pad.impexp.copypaste": "Будь ласка, скопіюйте та вставте", "pad.impexp.exportdisabled": "Експорт у формат {{type}} вимкнено. Будь ласка, зв'яжіться із Вашим системним адміністратором за деталями.", - "pad.impexp.maxFileSize": "Файл завеликий. Зверніться до адміністратора сайту для збільшення максимально дозволеного розміру файлів для імпорту", - "pad.impexp.permission": "Імпорт є вимкненим, оскільки ви ніколи не працювали із цим документом. Будь ласка, відредагуйте хоча б раз перед тим, як зробити імпорт" + "pad.impexp.maxFileSize": "Файл завеликий. Зверніться до адміністратора сайту для збільшення максимально дозволеного розміру файлів для імпорту" } diff --git a/src/locales/vi.json b/src/locales/vi.json index 42bc84806..c9fb1aca5 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -29,9 +29,7 @@ "pad.colorpicker.save": "Lưu", "pad.colorpicker.cancel": "Hủy bỏ", "pad.loading": "Đang tải…", - "pad.passwordRequired": "Bạn cần mật khẩu để truy cập pad này", "pad.permissionDenied": "Bạn không có quyền truy cập pad này.", - "pad.wrongPassword": "Bạn đã nhập sai mật khẩu", "pad.settings.padSettings": "Tùy chọn Pad", "pad.settings.myView": "Chỉ có tôi", "pad.settings.stickychat": "Luân hiện cửa sổ trò chuyện trên màn hình", diff --git a/src/locales/zh-hans.json b/src/locales/zh-hans.json index c7fa2a268..70bc75ed8 100644 --- a/src/locales/zh-hans.json +++ b/src/locales/zh-hans.json @@ -42,9 +42,7 @@ "pad.colorpicker.cancel": "取消", "pad.loading": "载入中……", "pad.noCookie": "无法找到Cookie。请在您的浏览器中允许Cookie!您的会话和设置不会在访问时保存。这可能是由于Etherpad包含在某些浏览器中的iFrame中。请确保Etherpad与父iFrame位于同一子域/域上", - "pad.passwordRequired": "您需要密码才能访问这个记事本", "pad.permissionDenied": "您没有访问这个记事本的权限", - "pad.wrongPassword": "您的密码错了", "pad.settings.padSettings": "记事本设置", "pad.settings.myView": "我的视窗", "pad.settings.stickychat": "总是显示聊天屏幕", @@ -148,6 +146,5 @@ "pad.impexp.importfailed": "导入失败", "pad.impexp.copypaste": "请复制粘贴", "pad.impexp.exportdisabled": "{{type}} 格式的导出被禁用。有关详情,请与您的系统管理员联系。", - "pad.impexp.maxFileSize": "文件太大。 请与您的站点管理员联系以增加允许导入的文件大小", - "pad.impexp.permission": "因为你从未为在此记事本做出任何贡献,导入已被禁用。在导入之前,请至少贡献一次" + "pad.impexp.maxFileSize": "文件太大。 请与您的站点管理员联系以增加允许导入的文件大小" } diff --git a/src/locales/zh-hant.json b/src/locales/zh-hant.json index da75b7827..aab670762 100644 --- a/src/locales/zh-hant.json +++ b/src/locales/zh-hant.json @@ -69,9 +69,7 @@ "pad.colorpicker.cancel": "取消", "pad.loading": "載入中...", "pad.noCookie": "找不到 Cookie。請允許瀏覽器使用 Cookie!您的 session 與設定沒有在訪問期間被儲存下來,這可能是因為在某些瀏覽器裡 Etherpad 被包在 iFrame 中,請確認 Etherpad 是在父層級的 iFrame 相同的網域/子網域。", - "pad.passwordRequired": "您需要密碼才能存取這個記事本", "pad.permissionDenied": "你沒有存取這個記事本的權限", - "pad.wrongPassword": "密碼錯誤", "pad.settings.padSettings": "記事本設定", "pad.settings.myView": "我的視窗", "pad.settings.stickychat": "永遠在螢幕上顯示聊天", @@ -177,6 +175,5 @@ "pad.impexp.importfailed": "匯入失敗", "pad.impexp.copypaste": "請複製貼上", "pad.impexp.exportdisabled": "{{type}}格式的匯出被禁用。有關詳情,請與您的系統管理員聯繫。", - "pad.impexp.maxFileSize": "檔案太大。請聯絡您的網站管理員來增加用於匯入的允許檔案大小。", - "pad.impexp.permission": "因為您已沒在此記事本上貢獻,匯入功能已停用。在進行匯入之前,至少要做出一次貢獻" + "pad.impexp.maxFileSize": "檔案太大。請聯絡您的網站管理員來增加用於匯入的允許檔案大小。" } From 048bd0f50d6458f2fca6d9ea1737f169c523da03 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 8 Oct 2020 01:37:17 -0400 Subject: [PATCH 095/315] tests: Simplify API key reading Also delete unused imports. --- src/node/handler/APIHandler.js | 4 ++++ tests/backend/common.js | 2 ++ tests/backend/fuzzImportTest.js | 8 ++------ tests/backend/specs/api/api.js | 9 ++------- tests/backend/specs/api/characterEncoding.js | 9 ++------- tests/backend/specs/api/chat.js | 15 ++++++--------- tests/backend/specs/api/fuzzImportTest.js | 8 ++------ tests/backend/specs/api/importexport.js | 10 ++-------- tests/backend/specs/api/importexportGetPost.js | 5 +---- tests/backend/specs/api/instance.js | 10 ++-------- tests/backend/specs/api/pad.js | 9 ++------- tests/backend/specs/api/sessionsAndGroups.js | 7 ++----- 12 files changed, 29 insertions(+), 67 deletions(-) diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index f4e0be38e..000e46c3a 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -196,3 +196,7 @@ exports.handle = async function(apiVersion, functionName, fields, req, res) // call the api function return api[functionName].apply(this, functionParams); } + +exports.exportedForTestingOnly = { + apiKey: apikey, +}; diff --git a/tests/backend/common.js b/tests/backend/common.js index 0b4f85205..6c780219c 100644 --- a/tests/backend/common.js +++ b/tests/backend/common.js @@ -1,5 +1,6 @@ function m(mod) { return __dirname + '/../../src/' + mod; } +const apiHandler = require(m('node/handler/APIHandler')); const log4js = require(m('node_modules/log4js')); const server = require(m('node/server')); const settings = require(m('node/utils/Settings')); @@ -9,6 +10,7 @@ const webaccess = require(m('node/hooks/express/webaccess')); const backups = {}; let inited = false; +exports.apiKey = apiHandler.exportedForTestingOnly.apiKey; exports.agent = null; exports.baseUrl = null; exports.httpServer = null; diff --git a/tests/backend/fuzzImportTest.js b/tests/backend/fuzzImportTest.js index 1c82e6c1c..f8eac55eb 100644 --- a/tests/backend/fuzzImportTest.js +++ b/tests/backend/fuzzImportTest.js @@ -2,17 +2,13 @@ * Fuzz testing the import endpoint * Usage: node fuzzImportTest.js */ -const fs = require('fs'); +const common = require('./common'); const settings = require(__dirname+'/loadSettings').loadSettings(); const host = "http://" + settings.ip + ":" + settings.port; -const path = require('path'); -const async = require(__dirname+'/../../src/node_modules/async'); const request = require('request'); const froth = require('mocha-froth'); -var filePath = path.join(__dirname, '/../../APIKEY.txt'); -var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); -apiKey = apiKey.replace(/\n$/, ""); +const apiKey = common.apiKey; var apiVersion = 1; var testPadId = "TEST_fuzz" + makeid(); diff --git a/tests/backend/specs/api/api.js b/tests/backend/specs/api/api.js index 0b372de85..e5fca6bca 100644 --- a/tests/backend/specs/api/api.js +++ b/tests/backend/specs/api/api.js @@ -6,19 +6,14 @@ * and openapi definitions. */ -const assert = require('assert'); +const common = require('../../common'); const supertest = require(__dirname + '/../../../../src/node_modules/supertest'); -const fs = require('fs'); const settings = require(__dirname + '/../../../../src/node/utils/Settings'); const api = supertest('http://' + settings.ip + ':' + settings.port); -const path = require('path'); var validateOpenAPI = require(__dirname + '/../../../../src/node_modules/openapi-schema-validation').validate; -var filePath = path.join(__dirname, '../../../../APIKEY.txt'); - -var apiKey = fs.readFileSync(filePath, { encoding: 'utf-8' }); -apiKey = apiKey.replace(/\n$/, ''); +const apiKey = common.apiKey; var apiVersion = 1; var testPadId = makeid(); diff --git a/tests/backend/specs/api/characterEncoding.js b/tests/backend/specs/api/characterEncoding.js index 83145089f..ed192d325 100644 --- a/tests/backend/specs/api/characterEncoding.js +++ b/tests/backend/specs/api/characterEncoding.js @@ -4,18 +4,13 @@ * TODO: maybe unify those two files and merge in a single one. */ -const assert = require('assert'); +const common = require('../../common'); const supertest = require(__dirname+'/../../../../src/node_modules/supertest'); const fs = require('fs'); const settings = require(__dirname + '/../../../../src/node/utils/Settings'); const api = supertest('http://'+settings.ip+":"+settings.port); -const path = require('path'); -const async = require(__dirname+'/../../../../src/node_modules/async'); -var filePath = path.join(__dirname, '../../../../APIKEY.txt'); - -var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); -apiKey = apiKey.replace(/\n$/, ""); +const apiKey = common.apiKey; var apiVersion = 1; var testPadId = makeid(); diff --git a/tests/backend/specs/api/chat.js b/tests/backend/specs/api/chat.js index e2ce59b1c..293b4a145 100644 --- a/tests/backend/specs/api/chat.js +++ b/tests/backend/specs/api/chat.js @@ -1,14 +1,11 @@ -var assert = require('assert') - supertest = require(__dirname+'/../../../../src/node_modules/supertest'), - fs = require('fs'), - settings = require(__dirname + '/../../../../src/node/utils/Settings'), - api = supertest('http://'+settings.ip+":"+settings.port), - path = require('path'); +function m(mod) { return __dirname + '/../../../../src/' + mod; } -var filePath = path.join(__dirname, '../../../../APIKEY.txt'); +const common = require('../../common'); +const settings = require(m('node/utils/Settings')); +const supertest = require(m('node_modules/supertest')); -var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); -apiKey = apiKey.replace(/\n$/, ""); +const api = supertest(`http://${settings.ip}:${settings.port}`); +const apiKey = common.apiKey; var apiVersion = 1; var authorID = ""; var padID = makeid(); diff --git a/tests/backend/specs/api/fuzzImportTest.js b/tests/backend/specs/api/fuzzImportTest.js index ca62a1bee..461d25423 100644 --- a/tests/backend/specs/api/fuzzImportTest.js +++ b/tests/backend/specs/api/fuzzImportTest.js @@ -2,17 +2,13 @@ * Fuzz testing the import endpoint */ /* -const fs = require('fs'); +const common = require('../../common'); const settings = require(__dirname+'/../../../../tests/container/loadSettings.js').loadSettings(); const host = "http://" + settings.ip + ":" + settings.port; -const path = require('path'); -const async = require(__dirname+'/../../../../src/node_modules/async'); const request = require(__dirname+'/../../../../src/node_modules/request'); const froth = require(__dirname+'/../../../../src/node_modules/mocha-froth'); -var filePath = path.join(__dirname, '../../../../APIKEY.txt'); -var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); -apiKey = apiKey.replace(/\n$/, ""); +const apiKey = common.apiKey; var apiVersion = 1; var testPadId = "TEST_fuzz" + makeid(); diff --git a/tests/backend/specs/api/importexport.js b/tests/backend/specs/api/importexport.js index 685bb7241..67ced55db 100644 --- a/tests/backend/specs/api/importexport.js +++ b/tests/backend/specs/api/importexport.js @@ -5,18 +5,12 @@ * TODO: unify those two files, and merge in a single one. */ -const assert = require('assert'); +const common = require('../../common'); const supertest = require(__dirname+'/../../../../src/node_modules/supertest'); -const fs = require('fs'); const settings = require(__dirname+'/../../../../tests/container/loadSettings.js').loadSettings(); const api = supertest('http://'+settings.ip+":"+settings.port); -const path = require('path'); -const async = require(__dirname+'/../../../../src/node_modules/async'); -var filePath = path.join(__dirname, '../../../../APIKEY.txt'); - -var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); -apiKey = apiKey.replace(/\n$/, ""); +const apiKey = common.apiKey; var apiVersion = 1; var lastEdited = ""; diff --git a/tests/backend/specs/api/importexportGetPost.js b/tests/backend/specs/api/importexportGetPost.js index 5c716ea0c..edc51f92d 100644 --- a/tests/backend/specs/api/importexportGetPost.js +++ b/tests/backend/specs/api/importexportGetPost.js @@ -8,7 +8,6 @@ const superagent = require(__dirname+'/../../../../src/node_modules/superagent') const fs = require('fs'); const settings = require(__dirname+'/../../../../src/node/utils/Settings'); const padManager = require(__dirname+'/../../../../src/node/db/PadManager'); -const path = require('path'); const plugins = require(__dirname+'/../../../../src/static/js/pluginfw/plugin_defs'); const padText = fs.readFileSync("../tests/backend/specs/api/test.txt"); @@ -17,11 +16,9 @@ const wordDoc = fs.readFileSync("../tests/backend/specs/api/test.doc"); const wordXDoc = fs.readFileSync("../tests/backend/specs/api/test.docx"); const odtDoc = fs.readFileSync("../tests/backend/specs/api/test.odt"); const pdfDoc = fs.readFileSync("../tests/backend/specs/api/test.pdf"); -var filePath = path.join(__dirname, '../../../../APIKEY.txt'); let agent; -var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); -apiKey = apiKey.replace(/\n$/, ""); +const apiKey = common.apiKey; var apiVersion = 1; const testPadId = makeid(); const testPadIdEnc = encodeURIComponent(testPadId); diff --git a/tests/backend/specs/api/instance.js b/tests/backend/specs/api/instance.js index 4849c6507..c7b25e8d5 100644 --- a/tests/backend/specs/api/instance.js +++ b/tests/backend/specs/api/instance.js @@ -3,18 +3,12 @@ * * Section "GLOBAL FUNCTIONS" in src/node/db/API.js */ -const assert = require('assert'); +const common = require('../../common'); const supertest = require(__dirname+'/../../../../src/node_modules/supertest'); -const fs = require('fs'); const settings = require(__dirname+'/../../../../src/node/utils/Settings'); const api = supertest('http://'+settings.ip+":"+settings.port); -const path = require('path'); - -var filePath = path.join(__dirname, '../../../../APIKEY.txt'); - -var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); -apiKey = apiKey.replace(/\n$/, ""); +const apiKey = common.apiKey; var apiVersion = '1.2.14'; describe('Connectivity for instance-level API tests', function() { diff --git a/tests/backend/specs/api/pad.js b/tests/backend/specs/api/pad.js index 6de05c8b5..5440856e9 100644 --- a/tests/backend/specs/api/pad.js +++ b/tests/backend/specs/api/pad.js @@ -5,18 +5,13 @@ * TODO: unify those two files, and merge in a single one. */ -const assert = require('assert'); +const common = require('../../common'); const supertest = require(__dirname+'/../../../../src/node_modules/supertest'); -const fs = require('fs'); const settings = require(__dirname + '/../../../../src/node/utils/Settings'); const api = supertest('http://'+settings.ip+":"+settings.port); -const path = require('path'); const async = require(__dirname+'/../../../../src/node_modules/async'); -var filePath = path.join(__dirname, '../../../../APIKEY.txt'); - -var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); -apiKey = apiKey.replace(/\n$/, ""); +const apiKey = common.apiKey; var apiVersion = 1; var testPadId = makeid(); var lastEdited = ""; diff --git a/tests/backend/specs/api/sessionsAndGroups.js b/tests/backend/specs/api/sessionsAndGroups.js index 61aa3ceb7..597b05d70 100644 --- a/tests/backend/specs/api/sessionsAndGroups.js +++ b/tests/backend/specs/api/sessionsAndGroups.js @@ -1,13 +1,10 @@ const assert = require('assert').strict; +const common = require('../../common'); const supertest = require(__dirname + '/../../../../src/node_modules/supertest'); -const fs = require('fs'); const settings = require(__dirname + '/../../../../src/node/utils/Settings'); const api = supertest(`http://${settings.ip}:${settings.port}`); -const path = require('path'); -const filePath = path.join(__dirname, '../../../../APIKEY.txt'); - -const apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}).replace(/\n$/, ''); +const apiKey = common.apiKey; let apiVersion = 1; let groupID = ''; let authorID = ''; From a2328cd7f0037f402c346cda4b690e07ab9ba818 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Sat, 10 Oct 2020 17:57:22 +0200 Subject: [PATCH 096/315] timeslider: bugfix: follow pad contents - only goToLineNumber if it exists (#4390) --- src/static/js/broadcast.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js index 0ae71d86b..f14bea495 100644 --- a/src/static/js/broadcast.js +++ b/src/static/js/broadcast.js @@ -255,9 +255,15 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro debugLog(e); } - var lineNumber = Changeset.opIterator(Changeset.unpack(changeset).ops).next().lines; + // we want to scroll to the area that is changed before the lines are mutated and we can't go + // to a line that is not there yet + let lineNumber = Changeset.opIterator(Changeset.unpack(changeset).ops).next().lines; if($('#options-followContents').is(":checked") || $('#options-followContents').prop("checked")){ - goToLineNumber(lineNumber); + if(padContents.currentLines.length <= lineNumber){ + goToLineNumber(padContents.currentLines.length-1) + } else { + goToLineNumber(lineNumber); + } } Changeset.mutateTextLines(changeset, padContents); From 052fbb944f9c0c37613fcb01fbfd1f0f6ec556ea Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 11 Oct 2020 04:51:53 -0400 Subject: [PATCH 097/315] plugins: Delete noisy and useless debug message (#4409) The debug statement mostly printed the following useless message over and over, causing Travis CI logs to become truncated: [DEBUG] pluginfw - [ undefined ] returning --- src/static/js/pluginfw/read-installed.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/static/js/pluginfw/read-installed.js b/src/static/js/pluginfw/read-installed.js index 800ee32cd..a4a05660b 100644 --- a/src/static/js/pluginfw/read-installed.js +++ b/src/static/js/pluginfw/read-installed.js @@ -287,7 +287,6 @@ function findUnmet (obj) { } }) - log.debug([obj._id], "returning") return obj } From a4927095ae44a183353ae0993e426e930d8a9139 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 4 Oct 2020 18:26:05 -0400 Subject: [PATCH 098/315] CSP: Disable the indexCustomInlineScripts hook --- doc/api/hooks_server-side.md | 1 - src/static/js/pluginfw/shared.js | 19 +++++++++++++++++-- src/templates/index.html | 2 -- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 10d738634..4dbfd38b5 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -118,7 +118,6 @@ Available blocks in `pad.html` are: * `indexCustomStyles` - contains the `index.css` `` tag, allows you to add your own or to customize the one provided by the active skin * `indexWrapper` - contains the form for creating new pads * `indexCustomScripts` - contains the `index.js` ` - @@ -37,6 +13,8 @@ + + Welcome to Etherpad!

            This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!

            Get involved with Etherpad at http://etherpad.org
            aw

            ","authors":["a.HKIv23mEbachFYfH",""]}} {"code":4,"message":"no or wrong API Key","data":null} */ -exports.createDiffHTML = async function(padID, startRev, endRev) { - +exports.createDiffHTML = async function (padID, startRev, endRev) { // check if startRev is a number if (startRev !== undefined) { startRev = checkValidRev(startRev); @@ -770,18 +767,18 @@ exports.createDiffHTML = async function(padID, startRev, endRev) { } // get the pad - let pad = await getPadSafe(padID, true); + const pad = await getPadSafe(padID, true); try { var padDiff = new PadDiff(pad, startRev, endRev); } catch (e) { - throw { stop: e.message }; + throw {stop: e.message}; } - let html = await padDiff.getHtml(); - let authors = await padDiff.getAuthors(); + const html = await padDiff.getHtml(); + const authors = await padDiff.getAuthors(); - return { html, authors }; -} + return {html, authors}; +}; /* ******************** ** GLOBAL FUNCTIONS ** @@ -796,20 +793,20 @@ exports.createDiffHTML = async function(padID, startRev, endRev) { {"code":4,"message":"no or wrong API Key","data":null} */ -exports.getStats = async function() { +exports.getStats = async function () { const sessionInfos = padMessageHandler.sessioninfos; const sessionKeys = Object.keys(sessionInfos); - const activePads = new Set(Object.entries(sessionInfos).map(k => k[1].padId)); + const activePads = new Set(Object.entries(sessionInfos).map((k) => k[1].padId)); - const { padIDs } = await padManager.listAllPads(); + const {padIDs} = await padManager.listAllPads(); return { totalPads: padIDs.length, totalSessions: sessionKeys.length, totalActivePads: activePads.size, - } -} + }; +}; /* **************************** ** INTERNAL HELPER FUNCTIONS * @@ -817,32 +814,32 @@ exports.getStats = async function() { // checks if a number is an int function is_int(value) { - return (parseFloat(value) == parseInt(value, 10)) && !isNaN(value) + return (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"); + if (typeof padID !== 'string') { + 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 - let exists = await padManager.doesPadExists(padID); + const exists = await padManager.doesPadExists(padID); 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 @@ -852,23 +849,23 @@ 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) { - if (typeof rev !== "number") { + 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"); + throw new customError('rev is a float value', 'apierror'); } return rev; @@ -877,7 +874,7 @@ function checkValidRev(rev) { // checks if a padID is part of a group function 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"); + if (padID && padID.indexOf('$') === -1) { + throw new customError(`You can only get/set the ${field} of pads that belong to a group`, 'apierror'); } } diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index 382cf3ee5..f4ef903cf 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -18,31 +18,87 @@ * limitations under the License. */ -var db = require("./DB"); -var customError = require("../utils/customError"); -var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +const db = require('./DB'); +const customError = require('../utils/customError'); +const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; -exports.getColorPalette = function() { +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" + '#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) { - let author = await db.get("globalAuthor:" + authorID); +exports.doesAuthorExist = async function (authorID) { + const author = await db.get(`globalAuthor:${authorID}`); return author !== null; -} +}; /* exported for backwards compatibility */ exports.doesAuthorExists = exports.doesAuthorExist; @@ -51,20 +107,20 @@ exports.doesAuthorExists = exports.doesAuthorExist; * Returns the AuthorID for a token. * @param {String} token The token */ -exports.getAuthor4Token = async function(token) { - let author = await mapAuthorWithDBKey("token2author", token); +exports.getAuthor4Token = async function (token) { + const author = await mapAuthorWithDBKey('token2author', token); // return only the sub value authorID return author ? author.authorID : author; -} +}; /** * Returns the AuthorID for a mapper. * @param {String} token The mapper * @param {String} name The name of the author (optional) */ -exports.createAuthorIfNotExistsFor = async function(authorMapper, name) { - let author = await mapAuthorWithDBKey("mapper2author", authorMapper); +exports.createAuthorIfNotExistsFor = async function (authorMapper, name) { + const author = await mapAuthorWithDBKey('mapper2author', authorMapper); if (name) { // set the name of this author @@ -80,16 +136,16 @@ exports.createAuthorIfNotExistsFor = async function(authorMapper, name) { * @param {String} mapperkey The database key name for this mapper * @param {String} mapper The mapper */ -async function mapAuthorWithDBKey (mapperkey, mapper) { +async function mapAuthorWithDBKey(mapperkey, mapper) { // try to map to an author - let author = await db.get(mapperkey + ":" + mapper); + const author = await db.get(`${mapperkey}:${mapper}`); if (author === null) { // there is no author with this mapper, so create one - let author = await exports.createAuthor(null); + const author = await exports.createAuthor(null); // create the token2author relation - await db.set(mapperkey + ":" + mapper, author.authorID); + await db.set(`${mapperkey}:${mapper}`, author.authorID); // return the author return author; @@ -97,109 +153,109 @@ async function mapAuthorWithDBKey (mapperkey, mapper) { // there is an author with this mapper // update the timestamp of this author - await db.setSub("globalAuthor:" + author, ["timestamp"], Date.now()); + await db.setSub(`globalAuthor:${author}`, ['timestamp'], Date.now()); // return the author - return { authorID: author}; + return {authorID: author}; } /** * Internal function that creates the database entry for an author * @param {String} name The name of the author */ -exports.createAuthor = function(name) { +exports.createAuthor = function (name) { // create the new author name - let author = "a." + randomString(16); + const author = `a.${randomString(16)}`; // create the globalAuthors db entry - let authorObj = { - "colorId": Math.floor(Math.random() * (exports.getColorPalette().length)), - "name": name, - "timestamp": Date.now() + const authorObj = { + colorId: Math.floor(Math.random() * (exports.getColorPalette().length)), + name, + timestamp: Date.now(), }; // set the global author db entry // NB: no await, since we're not waiting for the DB set to finish - db.set("globalAuthor:" + author, authorObj); + db.set(`globalAuthor:${author}`, authorObj); - return { authorID: author }; -} + return {authorID: author}; +}; /** * Returns the Author Obj of the author * @param {String} author The id of the author */ -exports.getAuthor = function(author) { +exports.getAuthor = function (author) { // NB: result is already a Promise - return db.get("globalAuthor:" + author); -} + return 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 = function (author) { + return 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 = function (author, colorId) { + return 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 = function (author) { + return 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 = function (author, name) { + return 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 function (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 */ // get the globalAuthor - let author = await db.get("globalAuthor:" + authorID); + const author = await db.get(`globalAuthor:${authorID}`); 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 - let padIDs = Object.keys(author.padIDs || {}); + const padIDs = Object.keys(author.padIDs || {}); - return { padIDs }; -} + return {padIDs}; +}; /** * Adds a new pad to the list of contributions * @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 function (authorID, padID) { // get the entry - let author = await db.get("globalAuthor:" + authorID); + const author = await db.get(`globalAuthor:${authorID}`); if (author === null) return; @@ -216,22 +272,22 @@ exports.addPad = async function(authorID, padID) { author.padIDs[padID] = 1; // anything, because value is not used // save the new element back - db.set("globalAuthor:" + authorID, author); -} + db.set(`globalAuthor:${authorID}`, author); +}; /** * Removes a pad from the list of contributions * @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) { - let author = await db.get("globalAuthor:" + authorID); +exports.removePad = async function (authorID, padID) { + const author = await db.get(`globalAuthor:${authorID}`); if (author === null) return; if (author.padIDs !== null) { // remove pad from author delete author.padIDs[padID]; - await db.set('globalAuthor:' + authorID, author); + await db.set(`globalAuthor:${authorID}`, author); } -} +}; diff --git a/src/node/db/DB.js b/src/node/db/DB.js index 22c3635ea..aca8d7432 100644 --- a/src/node/db/DB.js +++ b/src/node/db/DB.js @@ -19,13 +19,13 @@ * limitations under the License. */ -var ueberDB = require("ueberdb2"); -var settings = require("../utils/Settings"); -var log4js = require('log4js'); -const util = require("util"); +const ueberDB = require('ueberdb2'); +const settings = require('../utils/Settings'); +const log4js = require('log4js'); +const util = require('util'); // set database settings -let db = new ueberDB.database(settings.dbType, settings.dbSettings, null, log4js.getLogger("ueberDB")); +const db = new ueberDB.database(settings.dbType, settings.dbSettings, null, log4js.getLogger('ueberDB')); /** * The UeberDB Object that provides the database functions @@ -36,32 +36,32 @@ exports.db = null; * Initalizes the database with the settings provided by the settings module * @param {Function} callback */ -exports.init = function() { +exports.init = function () { // initalize the database async return new Promise((resolve, reject) => { - db.init(function(err) { + db.init((err) => { if (err) { // there was an error while initializing the database, output it and stop - console.error("ERROR: Problem while initalizing the database"); + console.error('ERROR: Problem while initalizing the database'); console.error(err.stack ? err.stack : err); process.exit(1); } // everything ok, set up Promise-based methods - ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove', 'doShutdown'].forEach(fn => { + ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove', 'doShutdown'].forEach((fn) => { exports[fn] = util.promisify(db[fn].bind(db)); }); // set up wrappers for get and getSub that can't return "undefined" - let get = exports.get; - exports.get = async function(key) { - let result = await get(key); + const get = exports.get; + exports.get = async function (key) { + const result = await get(key); return (result === undefined) ? null : result; }; - let getSub = exports.getSub; - exports.getSub = async function(key, sub) { - let result = await getSub(key, sub); + const getSub = exports.getSub; + exports.getSub = async function (key, sub) { + const result = await getSub(key, sub); return (result === undefined) ? null : result; }; @@ -70,7 +70,7 @@ exports.init = function() { resolve(); }); }); -} +}; exports.shutdown = async (hookName, context) => { await exports.doShutdown(); diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.js index 3e9d11676..1330acc43 100644 --- a/src/node/db/GroupManager.js +++ b/src/node/db/GroupManager.js @@ -18,52 +18,48 @@ * limitations under the License. */ -var customError = require("../utils/customError"); -var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; -var db = require("./DB"); -var padManager = require("./PadManager"); -var sessionManager = require("./SessionManager"); +const customError = require('../utils/customError'); +const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +const db = require('./DB'); +const padManager = require('./PadManager'); +const sessionManager = require('./SessionManager'); -exports.listAllGroups = async function() { - let groups = await db.get("groups"); +exports.listAllGroups = async function () { + let groups = await db.get('groups'); groups = groups || {}; - let groupIDs = Object.keys(groups); - return { groupIDs }; -} + const groupIDs = Object.keys(groups); + return {groupIDs}; +}; -exports.deleteGroup = async function(groupID) { - let group = await db.get("group:" + groupID); +exports.deleteGroup = async function (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 => { - return 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 - let group2sessions = await db.get("group2sessions:" + groupID); - let sessions = group2sessions ? group2sessions.sessionIDs : {}; + const group2sessions = await db.get(`group2sessions:${groupID}`); + const sessions = group2sessions ? group2sessions.sessionIDs : {}; // loop through all sessions and delete them (in parallel) - await Promise.all(Object.keys(sessions).map(session => { - return sessionManager.deleteSession(session); - })); + await Promise.all(Object.keys(sessions).map((session) => sessionManager.deleteSession(session))); // remove group and group2sessions entry - await db.remove("group2sessions:" + groupID); - await db.remove("group:" + groupID); + await db.remove(`group2sessions:${groupID}`); + await db.remove(`group:${groupID}`); // unlist the group let groups = await exports.listAllGroups(); groups = groups ? groups.groupIDs : []; - let index = groups.indexOf(groupID); + const index = groups.indexOf(groupID); if (index === -1) { // it's not listed @@ -75,102 +71,102 @@ exports.deleteGroup = async function(groupID) { groups.splice(index, 1); // regenerate group list - var newGroups = {}; - groups.forEach(group => newGroups[group] = 1); - await db.set("groups", newGroups); -} + const newGroups = {}; + groups.forEach((group) => newGroups[group] = 1); + await db.set('groups', newGroups); +}; -exports.doesGroupExist = async function(groupID) { +exports.doesGroupExist = async function (groupID) { // try to get the group entry - let group = await db.get("group:" + groupID); + const group = await db.get(`group:${groupID}`); return (group != null); -} +}; -exports.createGroup = async function() { +exports.createGroup = async function () { // search for non existing groupID - var groupID = "g." + randomString(16); + const groupID = `g.${randomString(16)}`; // create the group - await db.set("group:" + groupID, {pads: {}}); + await db.set(`group:${groupID}`, {pads: {}}); // list the group let groups = await exports.listAllGroups(); - groups = groups? groups.groupIDs : []; + groups = groups ? groups.groupIDs : []; groups.push(groupID); // regenerate group list - var newGroups = {}; - groups.forEach(group => newGroups[group] = 1); - await db.set("groups", newGroups); + const newGroups = {}; + groups.forEach((group) => newGroups[group] = 1); + await db.set('groups', newGroups); - return { groupID }; -} + return {groupID}; +}; -exports.createGroupIfNotExistsFor = async function(groupMapper) { +exports.createGroupIfNotExistsFor = async function (groupMapper) { // ensure mapper is optional - if (typeof groupMapper !== "string") { - throw new customError("groupMapper is not a string", "apierror"); + if (typeof groupMapper !== 'string') { + throw new customError('groupMapper is not a string', 'apierror'); } // try to get a group for this mapper - let groupID = await db.get("mapper2group:" + groupMapper); + const groupID = await db.get(`mapper2group:${groupMapper}`); if (groupID) { // there is a group for this mapper - let exists = await exports.doesGroupExist(groupID); + const exists = await exports.doesGroupExist(groupID); - if (exists) return { groupID }; + if (exists) return {groupID}; } // hah, the returned group doesn't exist, let's create one - let result = await exports.createGroup(); + const result = await exports.createGroup(); // create the mapper entry for this group - await db.set("mapper2group:" + groupMapper, result.groupID); + await db.set(`mapper2group:${groupMapper}`, result.groupID); return result; -} +}; -exports.createGroupPad = async function(groupID, padName, text) { +exports.createGroupPad = async function (groupID, padName, text) { // create the padID - let padID = groupID + "$" + padName; + const padID = `${groupID}$${padName}`; // ensure group exists - let groupExists = await exports.doesGroupExist(groupID); + 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 - let padExists = await padManager.doesPadExists(padID); + const padExists = await padManager.doesPadExists(padID); if (padExists) { // pad exists already - throw new customError("padName does already exist", "apierror"); + throw new customError('padName does already exist', 'apierror'); } // create the pad await padManager.getPad(padID, text); - //create an entry in the group for this pad - await db.setSub("group:" + groupID, ["pads", padID], 1); + // create an entry in the group for this pad + await db.setSub(`group:${groupID}`, ['pads', padID], 1); - return { padID }; -} + return {padID}; +}; -exports.listPads = async function(groupID) { - let exists = await exports.doesGroupExist(groupID); +exports.listPads = async function (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 - let result = await db.getSub("group:" + groupID, ["pads"]); - let padIDs = Object.keys(result); + const result = await db.getSub(`group:${groupID}`, ['pads']); + const padIDs = Object.keys(result); - return { padIDs }; -} + return {padIDs}; +}; diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index b147f40e0..c39b3c69e 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -3,36 +3,36 @@ */ -var Changeset = require("ep_etherpad-lite/static/js/Changeset"); -var AttributePool = require("ep_etherpad-lite/static/js/AttributePool"); -var db = require("./DB"); -var settings = require('../utils/Settings'); -var authorManager = require("./AuthorManager"); -var padManager = require("./PadManager"); -var padMessageHandler = require("../handler/PadMessageHandler"); -var groupManager = require("./GroupManager"); -var customError = require("../utils/customError"); -var readOnlyManager = require("./ReadOnlyManager"); -var crypto = require("crypto"); -var randomString = require("../utils/randomstring"); -var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -var promises = require('../utils/promises') +const Changeset = require('ep_etherpad-lite/static/js/Changeset'); +const AttributePool = require('ep_etherpad-lite/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 readOnlyManager = require('./ReadOnlyManager'); +const crypto = require('crypto'); +const randomString = require('../utils/randomstring'); +const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); +const promises = require('../utils/promises'); // serialization/deserialization attributes -var attributeBlackList = ["id"]; -var jsonableList = ["pool"]; +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 * @param txt */ exports.cleanText = function (txt) { - return txt.replace(/\r\n/g,'\n').replace(/\r/g,'\n').replace(/\t/g, ' ').replace(/\xa0/g, ' '); + return txt.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\t/g, ' ').replace(/\xa0/g, ' '); }; -let Pad = function Pad(id) { - this.atext = Changeset.makeAText("\n"); +const Pad = function Pad(id) { + this.atext = Changeset.makeAText('\n'); this.pool = new AttributePool(); this.head = -1; this.chatHead = -1; @@ -56,13 +56,11 @@ Pad.prototype.getSavedRevisionsNumber = function getSavedRevisionsNumber() { }; Pad.prototype.getSavedRevisionsList = function getSavedRevisionsList() { - var savedRev = new Array(); - for (var rev in this.savedRevisions) { + const savedRev = new Array(); + for (const rev in this.savedRevisions) { savedRev.push(this.savedRevisions[rev].revNum); } - savedRev.sort(function(a, b) { - return a - b; - }); + savedRev.sort((a, b) => a - b); return savedRev; }; @@ -75,12 +73,12 @@ Pad.prototype.appendRevision = async function appendRevision(aChangeset, author) author = ''; } - var newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool); + const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool); Changeset.copyAText(newAText, this.atext); - var newRev = ++this.head; + const newRev = ++this.head; - var newRevData = {}; + const newRevData = {}; newRevData.changeset = aChangeset; newRevData.meta = {}; newRevData.meta.author = author; @@ -97,7 +95,7 @@ Pad.prototype.appendRevision = async function appendRevision(aChangeset, author) } const p = [ - db.set('pad:' + this.id + ':revs:' + newRev, newRevData), + db.set(`pad:${this.id}:revs:${newRev}`, newRevData), this.saveToDatabase(), ]; @@ -107,9 +105,9 @@ Pad.prototype.appendRevision = async function appendRevision(aChangeset, author) } if (this.head == 0) { - hooks.callAll("padCreate", {'pad':this, 'author': author}); + hooks.callAll('padCreate', {pad: this, author}); } else { - hooks.callAll("padUpdate", {'pad':this, 'author': author, 'revs': newRev, 'changeset': aChangeset}); + hooks.callAll('padUpdate', {pad: this, author, revs: newRev, changeset: aChangeset}); } await Promise.all(p); @@ -117,10 +115,10 @@ Pad.prototype.appendRevision = async function appendRevision(aChangeset, author) // save all attributes to the database Pad.prototype.saveToDatabase = async function saveToDatabase() { - var dbObject = {}; + const dbObject = {}; - for (var attr in this) { - if (typeof this[attr] === "function") continue; + for (const attr in this) { + if (typeof this[attr] === 'function') continue; if (attributeBlackList.indexOf(attr) !== -1) continue; dbObject[attr] = this[attr]; @@ -130,32 +128,32 @@ Pad.prototype.saveToDatabase = async function saveToDatabase() { } } - await db.set('pad:' + this.id, dbObject); -} + await db.set(`pad:${this.id}`, dbObject); +}; // get time of last edit (changeset application) Pad.prototype.getLastEdit = function getLastEdit() { - var revNum = this.getHeadRevisionNumber(); - return db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "timestamp"]); -} + const revNum = this.getHeadRevisionNumber(); + return db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']); +}; Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum) { - return db.getSub("pad:" + this.id + ":revs:" + revNum, ["changeset"]); -} + return db.getSub(`pad:${this.id}:revs:${revNum}`, ['changeset']); +}; Pad.prototype.getRevisionAuthor = function getRevisionAuthor(revNum) { - return db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "author"]); -} + return db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'author']); +}; Pad.prototype.getRevisionDate = function getRevisionDate(revNum) { - return db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "timestamp"]); -} + return db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']); +}; Pad.prototype.getAllAuthors = function getAllAuthors() { - var authors = []; + const authors = []; - for(var key in this.pool.numToAttrib) { - if (this.pool.numToAttrib[key][0] == "author" && this.pool.numToAttrib[key][1] != "") { + for (const key in this.pool.numToAttrib) { + if (this.pool.numToAttrib[key][0] == 'author' && this.pool.numToAttrib[key][1] != '') { authors.push(this.pool.numToAttrib[key][1]); } } @@ -164,63 +162,59 @@ Pad.prototype.getAllAuthors = function getAllAuthors() { }; Pad.prototype.getInternalRevisionAText = async function getInternalRevisionAText(targetRev) { - let keyRev = this.getKeyRevisionNumber(targetRev); + const keyRev = this.getKeyRevisionNumber(targetRev); // find out which changesets are needed - let neededChangesets = []; - for (let curRev = keyRev; curRev < targetRev; ) { + const neededChangesets = []; + for (let curRev = keyRev; curRev < targetRev;) { neededChangesets.push(++curRev); } // get all needed data out of the database // start to get the atext of the key revision - let p_atext = db.getSub("pad:" + this.id + ":revs:" + keyRev, ["meta", "atext"]); + const p_atext = db.getSub(`pad:${this.id}:revs:${keyRev}`, ['meta', 'atext']); // get all needed changesets - let changesets = []; - await Promise.all(neededChangesets.map(item => { - return this.getRevisionChangeset(item).then(changeset => { - changesets[item] = changeset; - }); - })); + const changesets = []; + 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; atext = Changeset.cloneAText(atext); // apply all changesets to the key changeset - let apool = this.apool(); - for (let curRev = keyRev; curRev < targetRev; ) { - let cs = changesets[++curRev]; + const apool = this.apool(); + for (let curRev = keyRev; curRev < targetRev;) { + const cs = changesets[++curRev]; atext = Changeset.applyToAText(cs, atext, apool); } return atext; -} +}; Pad.prototype.getRevision = function getRevisionChangeset(revNum) { - return db.get("pad:" + this.id + ":revs:" + revNum); -} + return db.get(`pad:${this.id}:revs:${revNum}`); +}; Pad.prototype.getAllAuthorColors = async function getAllAuthorColors() { - let authors = this.getAllAuthors(); - let returnTable = {}; - let colorPalette = authorManager.getColorPalette(); + const authors = this.getAllAuthors(); + const returnTable = {}; + const colorPalette = authorManager.getColorPalette(); - await Promise.all(authors.map(author => { - return 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; -} +}; Pad.prototype.getValidRevisionRange = function getValidRevisionRange(startRev, endRev) { startRev = parseInt(startRev, 10); - var head = this.getHeadRevisionNumber(); + const head = this.getHeadRevisionNumber(); endRev = endRev ? parseInt(endRev, 10) : head; if (isNaN(startRev) || startRev < 0 || startRev > head) { @@ -234,7 +228,7 @@ Pad.prototype.getValidRevisionRange = function getValidRevisionRange(startRev, e } if (startRev !== null && endRev !== null) { - return { startRev: startRev , endRev: endRev } + return {startRev, endRev}; } return null; }; @@ -251,16 +245,16 @@ Pad.prototype.setText = async function setText(newText) { // clean the new text newText = exports.cleanText(newText); - var oldText = this.text(); + const oldText = this.text(); // create the changeset // We want to ensure the pad still ends with a \n, but otherwise keep // getText() and setText() consistent. - var changeset; + let changeset; if (newText[newText.length - 1] == '\n') { changeset = Changeset.makeSplice(oldText, 0, oldText.length, newText); } else { - changeset = Changeset.makeSplice(oldText, 0, oldText.length-1, newText); + changeset = Changeset.makeSplice(oldText, 0, oldText.length - 1, newText); } // append the changeset @@ -271,10 +265,10 @@ Pad.prototype.appendText = async function appendText(newText) { // clean the new text newText = exports.cleanText(newText); - var oldText = this.text(); + const oldText = this.text(); // create the changeset - var changeset = Changeset.makeSplice(oldText, oldText.length, 0, newText); + const changeset = Changeset.makeSplice(oldText, oldText.length, 0, newText); // append the changeset await this.appendRevision(changeset); @@ -284,14 +278,14 @@ Pad.prototype.appendChatMessage = async function appendChatMessage(text, userId, this.chatHead++; // save the chat entry in the database await Promise.all([ - db.set('pad:' + this.id + ':chat:' + this.chatHead, {text, userId, time}), + db.set(`pad:${this.id}:chat:${this.chatHead}`, {text, userId, time}), this.saveToDatabase(), ]); }; Pad.prototype.getChatMessage = async function getChatMessage(entryNum) { // get the chat entry - let entry = await db.get("pad:" + this.id + ":chat:" + entryNum); + const entry = await db.get(`pad:${this.id}:chat:${entryNum}`); // get the authorName if the entry exists if (entry != null) { @@ -302,49 +296,45 @@ Pad.prototype.getChatMessage = async function getChatMessage(entryNum) { }; Pad.prototype.getChatMessages = async function getChatMessages(start, end) { - // collect the numbers of chat entries and in which order we need them - let neededEntries = []; + const neededEntries = []; for (let order = 0, entryNum = start; entryNum <= end; ++order, ++entryNum) { - neededEntries.push({ entryNum, order }); + neededEntries.push({entryNum, order}); } // get all entries out of the database - let entries = []; - await Promise.all(neededEntries.map(entryObject => { - return this.getChatMessage(entryObject.entryNum).then(entry => { - entries[entryObject.order] = entry; - }); - })); + const entries = []; + 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 // incremented, but the chat message wasn't added - let cleanedEntries = entries.filter(entry => { - let pass = (entry != null); + const cleanedEntries = entries.filter((entry) => { + const pass = (entry != null); if (!pass) { - console.warn("WARNING: Found broken chat entry in pad " + this.id); + console.warn(`WARNING: Found broken chat entry in pad ${this.id}`); } return pass; }); return cleanedEntries; -} +}; Pad.prototype.init = async function init(text) { - // replace text with default text if text isn't set if (text == null) { text = settings.defaultPadText; } // try to load the pad - let value = await db.get("pad:" + this.id); + const value = await db.get(`pad:${this.id}`); // if this pad exists, load it if (value != null) { // copy all attr. To a transfrom via fromJsonable if necassary - for (var attr in value) { + for (const attr in value) { if (jsonableList.indexOf(attr) !== -1) { this[attr] = this[attr].fromJsonable(value[attr]); } else { @@ -353,17 +343,17 @@ Pad.prototype.init = async function init(text) { } } else { // this pad doesn't exist, so create it - let firstChangeset = Changeset.makeSplice("\n", 0, 0, exports.cleanText(text)); + const firstChangeset = Changeset.makeSplice('\n', 0, 0, exports.cleanText(text)); await this.appendRevision(firstChangeset, ''); } - hooks.callAll("padLoad", { 'pad': this }); -} + hooks.callAll('padLoad', {pad: this}); +}; Pad.prototype.copy = async function copy(destinationID, force) { let destGroupID; - let sourceID = this.id; + const sourceID = this.id; // Kick everyone from this pad. // This was commented due to https://github.com/ether/etherpad-lite/issues/3183. @@ -380,32 +370,28 @@ Pad.prototype.copy = async function copy(destinationID, force) { // if force is true and already exists a Pad with the same id, remove that Pad await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force); - } catch(err) { + } catch (err) { throw err; } // copy the 'pad' entry - let pad = await db.get("pad:" + sourceID); - db.set("pad:" + destinationID, pad); + const pad = await db.get(`pad:${sourceID}`); + db.set(`pad:${destinationID}`, pad); // copy all relations in parallel - let promises = []; + const promises = []; // copy all chat messages - let chatHead = this.chatHead; + const chatHead = this.chatHead; for (let i = 0; i <= chatHead; ++i) { - let p = db.get("pad:" + sourceID + ":chat:" + i).then(chat => { - return 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 - let revHead = this.head; + const revHead = this.head; for (let i = 0; i <= revHead; ++i) { - let p = db.get("pad:" + sourceID + ":revs:" + i).then(rev => { - return 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); } @@ -416,70 +402,69 @@ Pad.prototype.copy = async function copy(destinationID, force) { // Group pad? Add it to the group's list if (destGroupID) { - await db.setSub("group:" + destGroupID, ["pads", destinationID], 1); + await db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1); } // delay still necessary? - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 10)); // Initialize the new pad (will update the listAllPads cache) await padManager.getPad(destinationID, null); // this runs too early. // let the plugins know the pad was copied - hooks.callAll('padCopy', { 'originalPad': this, 'destinationID': destinationID }); + hooks.callAll('padCopy', {originalPad: this, destinationID}); - return { padID: destinationID }; -} + return {padID: destinationID}; +}; Pad.prototype.checkIfGroupExistAndReturnIt = async function checkIfGroupExistAndReturnIt(destinationID) { let destGroupID = false; - if (destinationID.indexOf("$") >= 0) { - - destGroupID = destinationID.split("$")[0] - let groupExists = await groupManager.doesGroupExist(destGroupID); + if (destinationID.indexOf('$') >= 0) { + destGroupID = destinationID.split('$')[0]; + const groupExists = await groupManager.doesGroupExist(destGroupID); // 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; -} +}; Pad.prototype.removePadIfForceIsTrueAndAlreadyExist = async function removePadIfForceIsTrueAndAlreadyExist(destinationID, force) { // if the pad exists, we should abort, unless forced. - let exists = await padManager.doesPadExist(destinationID); + const exists = await padManager.doesPadExist(destinationID); // allow force to be a string - if (typeof force === "string") { - force = (force.toLowerCase() === "true"); + if (typeof force === 'string') { + force = (force.toLowerCase() === 'true'); } else { force = !!force; } if (exists) { if (!force) { - console.error("erroring out without force"); - throw new customError("destinationID already exists", "apierror"); + console.error('erroring out without force'); + throw new customError('destinationID already exists', 'apierror'); } // exists and forcing - let pad = await padManager.getPad(destinationID); + const pad = await padManager.getPad(destinationID); await pad.remove(); } -} +}; Pad.prototype.copyAuthorInfoToDestinationPad = function copyAuthorInfoToDestinationPad(destinationID) { // add the new sourcePad to all authors who contributed to the old one - this.getAllAuthors().forEach(authorID => { + this.getAllAuthors().forEach((authorID) => { authorManager.addPad(authorID, destinationID); }); -} +}; Pad.prototype.copyPadWithoutHistory = async function copyPadWithoutHistory(destinationID, force) { let destGroupID; - let sourceID = this.id; + const sourceID = this.id; // flush the source pad this.saveToDatabase(); @@ -490,53 +475,53 @@ Pad.prototype.copyPadWithoutHistory = async function copyPadWithoutHistory(desti // if force is true and already exists a Pad with the same id, remove that Pad await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force); - } catch(err) { + } catch (err) { throw err; } - let sourcePad = await padManager.getPad(sourceID); + const sourcePad = await padManager.getPad(sourceID); // add the new sourcePad to all authors who contributed to the old one this.copyAuthorInfoToDestinationPad(destinationID); // Group pad? Add it to the group's list if (destGroupID) { - await db.setSub("group:" + destGroupID, ["pads", destinationID], 1); + await db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1); } // initialize the pad with a new line to avoid getting the defaultText - let newPad = await padManager.getPad(destinationID, '\n'); + const newPad = await padManager.getPad(destinationID, '\n'); - let oldAText = this.atext; - let newPool = newPad.pool; + const oldAText = this.atext; + const newPool = newPad.pool; newPool.fromJsonable(sourcePad.pool.toJsonable()); // copy that sourceId pool to the new pad // based on Changeset.makeSplice - let assem = Changeset.smartOpAssembler(); + const assem = Changeset.smartOpAssembler(); assem.appendOpWithText('=', ''); Changeset.appendATextToAssembler(oldAText, assem); assem.endDocument(); // although we have instantiated the newPad with '\n', an additional '\n' is // added internally, so the pad text on the revision 0 is "\n\n" - let oldLength = 2; + const oldLength = 2; - let newLength = assem.getLengthChange(); - let newText = oldAText.text; + const newLength = assem.getLengthChange(); + const newText = oldAText.text; // create a changeset that removes the previous text and add the newText with // all atributes present on the source pad - let changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText); + const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText); newPad.appendRevision(changeset); - hooks.callAll('padCopy', { 'originalPad': this, 'destinationID': destinationID }); + hooks.callAll('padCopy', {originalPad: this, destinationID}); - return { padID: destinationID }; -} + return {padID: destinationID}; +}; Pad.prototype.remove = async function remove() { - var padID = this.id; + const padID = this.id; const p = []; // kick everyone from this pad @@ -548,45 +533,44 @@ Pad.prototype.remove = async function remove() { // run to completion // is it a group pad? -> delete the entry of this pad in the group - if (padID.indexOf("$") >= 0) { - + if (padID.indexOf('$') >= 0) { // it is a group pad - let groupID = padID.substring(0, padID.indexOf("$")); - let group = await db.get("group:" + groupID); + const groupID = padID.substring(0, padID.indexOf('$')); + const group = await db.get(`group:${groupID}`); // remove the pad entry delete group.pads[padID]; // set the new value - p.push(db.set('group:' + groupID, group)); + p.push(db.set(`group:${groupID}`, group)); } // remove the readonly entries p.push(readOnlyManager.getReadOnlyId(padID).then(async (readonlyID) => { - await db.remove('readonly2pad:' + readonlyID); + await db.remove(`readonly2pad:${readonlyID}`); })); - p.push(db.remove('pad2readonly:' + padID)); + p.push(db.remove(`pad2readonly:${padID}`)); // delete all chat messages p.push(promises.timesLimit(this.chatHead + 1, 500, async (i) => { - await db.remove('pad:' + padID + ':chat:' + i, null); + await db.remove(`pad:${padID}:chat:${i}`, null); })); // delete all revisions p.push(promises.timesLimit(this.head + 1, 500, async (i) => { - await db.remove('pad:' + padID + ':revs:' + i, null); + await db.remove(`pad:${padID}:revs:${i}`, null); })); // remove pad from all authors who contributed - this.getAllAuthors().forEach(authorID => { + this.getAllAuthors().forEach((authorID) => { p.push(authorManager.removePad(authorID, padID)); }); // delete the pad entry and delete pad from padManager p.push(padManager.removePad(padID)); - hooks.callAll("padRemove", { padID }); + hooks.callAll('padRemove', {padID}); await Promise.all(p); -} +}; // set in db Pad.prototype.setPublicStatus = async function setPublicStatus(publicStatus) { @@ -596,17 +580,17 @@ Pad.prototype.setPublicStatus = async function setPublicStatus(publicStatus) { Pad.prototype.addSavedRevision = async function addSavedRevision(revNum, savedById, label) { // if this revision is already saved, return silently - for (var i in this.savedRevisions) { + for (const i in this.savedRevisions) { if (this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum) { return; } } // build the saved revision object - var savedRevision = {}; + const savedRevision = {}; savedRevision.revNum = revNum; savedRevision.savedById = savedById; - savedRevision.label = label || "Revision " + revNum; + savedRevision.label = label || `Revision ${revNum}`; savedRevision.timestamp = Date.now(); savedRevision.id = randomString(10); @@ -618,4 +602,3 @@ Pad.prototype.addSavedRevision = async function addSavedRevision(revNum, savedBy Pad.prototype.getSavedRevisions = function getSavedRevisions() { return this.savedRevisions; }; - diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index ec198b36a..9334b92a4 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -18,9 +18,9 @@ * limitations under the License. */ -var customError = require("../utils/customError"); -var Pad = require("../db/Pad").Pad; -var db = require("./DB"); +const customError = require('../utils/customError'); +const Pad = require('../db/Pad').Pad; +const db = require('./DB'); /** * A cache of all loaded Pads. @@ -33,14 +33,14 @@ var db = require("./DB"); * If this is needed in other places, it would be wise to make this a prototype * that's defined somewhere more sensible. */ -var globalPads = { - get: function(name) { return this[':'+name]; }, - set: function(name, value) { - this[':'+name] = value; - }, - remove: function(name) { - delete this[':'+name]; - } +const globalPads = { + get(name) { return this[`:${name}`]; }, + set(name, value) { + this[`:${name}`] = value; + }, + remove(name) { + delete this[`:${name}`]; + }, }; /** @@ -48,24 +48,24 @@ var globalPads = { * * Updated without db access as new pads are created/old ones removed. */ -let padList = { +const padList = { list: new Set(), cachedList: undefined, initiated: false, - init: async function() { - let dbData = await db.findKeys("pad:*", "*:*:*"); + async init() { + const dbData = await db.findKeys('pad:*', '*:*:*'); if (dbData != null) { this.initiated = true; - for (let val of dbData) { - this.addPad(val.replace(/^pad:/,""), false); + for (const val of dbData) { + this.addPad(val.replace(/^pad:/, ''), false); } } return this; }, - load: async function() { + async load() { if (!this.initiated) { return this.init(); } @@ -75,7 +75,7 @@ let padList = { /** * Returns all pads in alphabetical order as array. */ - getPads: async function() { + async getPads() { await this.load(); if (!this.cachedList) { @@ -84,7 +84,7 @@ let padList = { return this.cachedList; }, - addPad: function(name) { + addPad(name) { if (!this.initiated) return; if (!this.list.has(name)) { @@ -92,14 +92,14 @@ let padList = { this.cachedList = undefined; } }, - removePad: function(name) { + removePad(name) { if (!this.initiated) return; if (this.list.has(name)) { this.list.delete(name); this.cachedList = undefined; } - } + }, }; // initialises the all-knowing data structure @@ -109,22 +109,22 @@ let padList = { * @param id A String with the id of the pad * @param {Function} callback */ -exports.getPad = async function(id, text) { +exports.getPad = async function (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"); + if (typeof text !== 'string') { + 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'); } } @@ -144,20 +144,20 @@ exports.getPad = async function(id, text) { padList.addPad(id); return pad; -} +}; -exports.listAllPads = async function() { - let padIDs = await padList.getPads(); +exports.listAllPads = async function () { + const padIDs = await padList.getPads(); - return { padIDs }; -} + return {padIDs}; +}; // checks if a pad exists -exports.doesPadExist = async function(padId) { - let value = await db.get("pad:" + padId); +exports.doesPadExist = async function (padId) { + const value = await db.get(`pad:${padId}`); return (value != null && value.atext); -} +}; // alias for backwards compatibility exports.doesPadExists = exports.doesPadExist; @@ -168,42 +168,42 @@ exports.doesPadExists = exports.doesPadExist; */ const padIdTransforms = [ [/\s+/g, '_'], - [/:+/g, '_'] + [/:+/g, '_'], ]; // returns a sanitized padId, respecting legacy pad id formats exports.sanitizePadId = async function sanitizePadId(padId) { for (let i = 0, n = padIdTransforms.length; i < n; ++i) { - let exists = await exports.doesPadExist(padId); + const exists = await exports.doesPadExist(padId); if (exists) { return padId; } - let [from, to] = padIdTransforms[i]; + const [from, to] = padIdTransforms[i]; padId = padId.replace(from, to); } // we're out of possible transformations, so just return it return padId; -} +}; -exports.isValidPadId = function(padId) { +exports.isValidPadId = function (padId) { return /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId); -} +}; /** * Removes the pad from database and unloads it. */ exports.removePad = async (padId) => { - const p = db.remove('pad:' + padId); + const p = db.remove(`pad:${padId}`); exports.unloadPad(padId); padList.removePad(padId); await p; -} +}; // removes a pad from the cache -exports.unloadPad = function(padId) { +exports.unloadPad = function (padId) { globalPads.remove(padId); -} +}; diff --git a/src/node/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.js index 094f996cd..54f8c592e 100644 --- a/src/node/db/ReadOnlyManager.js +++ b/src/node/db/ReadOnlyManager.js @@ -19,17 +19,17 @@ */ -var db = require("./DB"); -var randomString = require("../utils/randomstring"); +const db = require('./DB'); +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 = function (id) { + return id.indexOf('r.') === 0; +}; /** * returns a read only id for a pad @@ -37,36 +37,36 @@ exports.isReadOnlyId = function(id) { */ exports.getReadOnlyId = async function (padId) { // check if there is a pad2readonly entry - let readOnlyId = await db.get("pad2readonly:" + padId); + let readOnlyId = await db.get(`pad2readonly:${padId}`); // there is no readOnly Entry in the database, let's create one if (readOnlyId == null) { - readOnlyId = "r." + randomString(16); - db.set("pad2readonly:" + padId, readOnlyId); - db.set("readonly2pad:" + readOnlyId, padId); + readOnlyId = `r.${randomString(16)}`; + db.set(`pad2readonly:${padId}`, readOnlyId); + db.set(`readonly2pad:${readOnlyId}`, padId); } return readOnlyId; -} +}; /** * 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 = function (readOnlyId) { + return 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) { - let readonly = (id.indexOf("r.") === 0); +exports.getIds = async function (id) { + const readonly = (id.indexOf('r.') === 0); // Might be null, if this is an unknown read-only id - let readOnlyPadId = readonly ? id : await exports.getReadOnlyId(id); - let padId = readonly ? await exports.getPadId(id) : id; + const readOnlyPadId = readonly ? id : await exports.getReadOnlyId(id); + const padId = readonly ? await exports.getPadId(id) : id; - return { readOnlyPadId, padId, readonly }; -} + return {readOnlyPadId, padId, readonly}; +}; diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 331b395a6..64091dbdc 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -18,14 +18,14 @@ * limitations under the License. */ -var authorManager = require("./AuthorManager"); -var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); -var padManager = require("./PadManager"); -var sessionManager = require("./SessionManager"); -var settings = require("../utils/Settings"); +const authorManager = require('./AuthorManager'); +const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks.js'); +const padManager = require('./PadManager'); +const sessionManager = require('./SessionManager'); +const settings = require('../utils/Settings'); const webaccess = require('../hooks/express/webaccess'); -var log4js = require('log4js'); -var authLogger = log4js.getLogger("auth"); +const log4js = require('log4js'); +const authLogger = log4js.getLogger('auth'); const DENY = Object.freeze({accessStatus: 'deny'}); @@ -47,7 +47,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 function (padID, sessionCookie, token, userSettings) { if (!padID) { authLogger.debug('access denied: missing padID'); return DENY; diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index c43ce91bf..8f1daab52 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -18,12 +18,12 @@ * limitations under the License. */ -var customError = require("../utils/customError"); +const customError = require('../utils/customError'); const promises = require('../utils/promises'); -var randomString = require("../utils/randomstring"); -var db = require("./DB"); -var groupManager = require("./GroupManager"); -var authorManager = require("./AuthorManager"); +const randomString = require('../utils/randomstring'); +const db = require('./DB'); +const groupManager = require('./GroupManager'); +const authorManager = require('./AuthorManager'); /** * Finds the author ID for a session with matching ID and group. @@ -78,61 +78,61 @@ exports.findAuthorID = async (groupID, sessionCookie) => { return sessionInfo.authorID; }; -exports.doesSessionExist = async function(sessionID) { - //check if the database entry of this session exists - let session = await db.get("session:" + sessionID); +exports.doesSessionExist = async function (sessionID) { + // check if the database entry of this session exists + const session = await db.get(`session:${sessionID}`); return (session !== null); -} +}; /** * Creates a new session between an author and a group */ -exports.createSession = async function(groupID, authorID, validUntil) { +exports.createSession = async function (groupID, authorID, validUntil) { // check if the group exists - let groupExists = await groupManager.doesGroupExist(groupID); + 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 - let authorExists = await authorManager.doesAuthorExist(authorID); + 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 - if (typeof validUntil !== "number") { + if (typeof validUntil !== 'number') { validUntil = parseInt(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"); + 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 - let sessionID = "s." + randomString(16); + const sessionID = `s.${randomString(16)}`; // set the session into the database - await db.set("session:" + sessionID, {"groupID": groupID, "authorID": authorID, "validUntil": validUntil}); + await db.set(`session:${sessionID}`, {groupID, authorID, validUntil}); // get the entry - let group2sessions = await db.get("group2sessions:" + groupID); + let group2sessions = await db.get(`group2sessions:${groupID}`); /* * In some cases, the db layer could return "undefined" as well as "null". @@ -144,115 +144,115 @@ exports.createSession = async function(groupID, authorID, validUntil) { */ if (!group2sessions || !group2sessions.sessionIDs) { // the entry doesn't exist so far, let's create it - group2sessions = {sessionIDs : {}}; + group2sessions = {sessionIDs: {}}; } // add the entry for this session group2sessions.sessionIDs[sessionID] = 1; // save the new element back - await db.set("group2sessions:" + groupID, group2sessions); + await db.set(`group2sessions:${groupID}`, group2sessions); // get the author2sessions entry - let author2sessions = await db.get("author2sessions:" + authorID); + let author2sessions = await db.get(`author2sessions:${authorID}`); if (author2sessions == null || author2sessions.sessionIDs == null) { // the entry doesn't exist so far, let's create it - author2sessions = {sessionIDs : {}}; + author2sessions = {sessionIDs: {}}; } // add the entry for this session author2sessions.sessionIDs[sessionID] = 1; - //save the new element back - await db.set("author2sessions:" + authorID, author2sessions); + // save the new element back + await db.set(`author2sessions:${authorID}`, author2sessions); - return { sessionID }; -} + return {sessionID}; +}; -exports.getSessionInfo = async function(sessionID) { +exports.getSessionInfo = async function (sessionID) { // check if the database entry of this session exists - let session = await db.get("session:" + sessionID); + 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 return session; -} +}; /** * Deletes a session */ -exports.deleteSession = async function(sessionID) { +exports.deleteSession = async function (sessionID) { // ensure that the session exists - let session = await db.get("session:" + sessionID); + 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 - let groupID = session.groupID; - let authorID = session.authorID; + const groupID = session.groupID; + const authorID = session.authorID; // get the group2sessions and author2sessions entries - let group2sessions = await db.get("group2sessions:" + groupID); - let author2sessions = await db.get("author2sessions:" + authorID); + const group2sessions = await db.get(`group2sessions:${groupID}`); + const author2sessions = await db.get(`author2sessions:${authorID}`); // remove the session - await db.remove("session:" + sessionID); + await db.remove(`session:${sessionID}`); // remove session from group2sessions if (group2sessions != null) { // Maybe the group was already deleted delete group2sessions.sessionIDs[sessionID]; - await db.set("group2sessions:" + groupID, group2sessions); + await db.set(`group2sessions:${groupID}`, group2sessions); } // remove session from author2sessions if (author2sessions != null) { // Maybe the author was already deleted delete author2sessions.sessionIDs[sessionID]; - await db.set("author2sessions:" + authorID, author2sessions); + await db.set(`author2sessions:${authorID}`, author2sessions); } -} +}; -exports.listSessionsOfGroup = async function(groupID) { +exports.listSessionsOfGroup = async function (groupID) { // check that the group exists - let exists = await groupManager.doesGroupExist(groupID); + const exists = await groupManager.doesGroupExist(groupID); if (!exists) { - throw new customError("groupID does not exist", "apierror"); + throw new customError('groupID does not exist', 'apierror'); } - let sessions = await listSessionsWithDBKey("group2sessions:" + groupID); + const sessions = await listSessionsWithDBKey(`group2sessions:${groupID}`); return sessions; -} +}; -exports.listSessionsOfAuthor = async function(authorID) { +exports.listSessionsOfAuthor = async function (authorID) { // check that the author exists - let exists = await authorManager.doesAuthorExist(authorID) + const exists = await authorManager.doesAuthorExist(authorID); if (!exists) { - throw new customError("authorID does not exist", "apierror"); + throw new customError('authorID does not exist', 'apierror'); } - let sessions = await listSessionsWithDBKey("author2sessions:" + authorID); + const sessions = await listSessionsWithDBKey(`author2sessions:${authorID}`); return sessions; -} +}; // 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) { // get the group2sessions entry - let sessionObject = await db.get(dbkey); - let sessions = sessionObject ? sessionObject.sessionIDs : null; + const sessionObject = await db.get(dbkey); + const sessions = sessionObject ? sessionObject.sessionIDs : null; // iterate through the sessions and get the sessioninfos - for (let sessionID in sessions) { + for (const sessionID in sessions) { try { - let sessionInfo = await exports.getSessionInfo(sessionID); + 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 { diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index e265ee68e..91bd75561 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -15,11 +15,11 @@ const logger = log4js.getLogger('SessionStore'); module.exports = class SessionStore extends Store { get(sid, fn) { - logger.debug('GET ' + sid); - DB.db.get('sessionstorage:' + sid, (err, sess) => { + logger.debug(`GET ${sid}`); + DB.db.get(`sessionstorage:${sid}`, (err, sess) => { if (sess) { - sess.cookie.expires = ('string' == typeof sess.cookie.expires - ? new Date(sess.cookie.expires) : sess.cookie.expires); + sess.cookie.expires = ('string' === typeof sess.cookie.expires + ? new Date(sess.cookie.expires) : sess.cookie.expires); if (!sess.cookie.expires || new Date() < sess.cookie.expires) { fn(null, sess); } else { @@ -32,12 +32,12 @@ module.exports = class SessionStore extends Store { } set(sid, sess, fn) { - logger.debug('SET ' + sid); - DB.db.set('sessionstorage:' + sid, sess, fn); + logger.debug(`SET ${sid}`); + DB.db.set(`sessionstorage:${sid}`, sess, fn); } destroy(sid, fn) { - logger.debug('DESTROY ' + sid); - DB.db.remove('sessionstorage:' + sid, fn); + logger.debug(`DESTROY ${sid}`); + DB.db.remove(`sessionstorage:${sid}`, fn); } }; diff --git a/src/node/easysync_tests.js b/src/node/easysync_tests.js index 374e949fd..c8a5c9853 100644 --- a/src/node/easysync_tests.js +++ b/src/node/easysync_tests.js @@ -21,47 +21,45 @@ */ -var Changeset = require("ep_etherpad-lite/static/js/Changeset"); -var AttributePool = require("ep_etherpad-lite/static/js/AttributePool"); +const Changeset = require('ep_etherpad-lite/static/js/Changeset'); +const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); function random() { this.nextInt = function (maxValue) { return Math.floor(Math.random() * maxValue); - } + }; this.nextDouble = function (maxValue) { return Math.random(); - } + }; } function runTests() { - function print(str) { console.log(str); } function assert(code, optMsg) { - if (!eval(code)) throw new Error("FALSE: " + (optMsg || code)); + if (!eval(code)) throw new Error(`FALSE: ${optMsg || code}`); } function literal(v) { - if ((typeof v) == "string") { - return '"' + v.replace(/[\\\"]/g, '\\$1').replace(/\n/g, '\\n') + '"'; - } else - return JSON.stringify(v); + if ((typeof v) === 'string') { + return `"${v.replace(/[\\\"]/g, '\\$1').replace(/\n/g, '\\n')}"`; + } else { return JSON.stringify(v); } } function assertEqualArrays(a, b) { - assert("JSON.stringify(" + literal(a) + ") == JSON.stringify(" + literal(b) + ")"); + assert(`JSON.stringify(${literal(a)}) == JSON.stringify(${literal(b)})`); } function assertEqualStrings(a, b) { - assert(literal(a) + " == " + literal(b)); + assert(`${literal(a)} == ${literal(b)}`); } function throughIterator(opsStr) { - var iter = Changeset.opIterator(opsStr); - var assem = Changeset.opAssembler(); + const iter = Changeset.opIterator(opsStr); + const assem = Changeset.opAssembler(); while (iter.hasNext()) { assem.append(iter.next()); } @@ -69,8 +67,8 @@ function runTests() { } function throughSmartAssembler(opsStr) { - var iter = Changeset.opIterator(opsStr); - var assem = Changeset.smartOpAssembler(); + const iter = Changeset.opIterator(opsStr); + const assem = Changeset.smartOpAssembler(); while (iter.hasNext()) { assem.append(iter.next()); } @@ -79,20 +77,20 @@ function runTests() { } (function () { - print("> throughIterator"); - var 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)); + 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"); - var 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)); + 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) { - arrayOfArrays.forEach(function (a) { - var result = mu[a[0]].apply(mu, a.slice(1)); + arrayOfArrays.forEach((a) => { + const result = mu[a[0]].apply(mu, a.slice(1)); if (a[0] == 'remove' && a[3]) { assertEqualStrings(a[3], result); } @@ -100,12 +98,12 @@ function runTests() { } function mutationsToChangeset(oldLen, arrayOfArrays) { - var assem = Changeset.smartOpAssembler(); - var op = Changeset.newOp(); - var bank = Changeset.stringAssembler(); - var oldPos = 0; - var newLen = 0; - arrayOfArrays.forEach(function (a) { + 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') { op.opcode = '='; op.chars = a[1]; @@ -134,101 +132,101 @@ function runTests() { } function runMutationTest(testId, origLines, muts, correct) { - print("> runMutationTest#" + testId); - var lines = origLines.slice(); - var mu = Changeset.textLinesMutator(lines); + print(`> runMutationTest#${testId}`); + let lines = origLines.slice(); + const mu = Changeset.textLinesMutator(lines); applyMutations(mu, muts); mu.close(); assertEqualArrays(correct, lines); - var inText = origLines.join(''); - var cs = mutationsToChangeset(inText.length, muts); + const inText = origLines.join(''); + const cs = mutationsToChangeset(inText.length, muts); lines = origLines.slice(); Changeset.mutateTextLines(cs, lines); assertEqualArrays(correct, lines); - var correctText = correct.join(''); - //print(literal(cs)); - var outText = Changeset.applyToText(cs, inText); + const correctText = correct.join(''); + // 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"], - ['insert', "tu"], - ['remove', 1, 0, "p"], + runMutationTest(1, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [ + ['remove', 1, 0, 'a'], + ['insert', 'tu'], + ['remove', 1, 0, 'p'], ['skip', 4, 1], ['skip', 7, 1], - ['insert', "cream\npie\n", 2], + ['insert', 'cream\npie\n', 2], ['skip', 2], - ['insert', "bot"], - ['insert', "\n", 1], - ['insert', "bu"], + ['insert', 'bot'], + ['insert', '\n', 1], + ['insert', 'bu'], ['skip', 3], - ['remove', 3, 1, "ge\n"], - ['remove', 6, 0, "duffle"] - ], ["tuple\n", "banana\n", "cream\n", "pie\n", "cabot\n", "bubba\n", "eggplant\n"]); + ['remove', 3, 1, 'ge\n'], + ['remove', 6, 0, 'duffle'], + ], ['tuple\n', 'banana\n', 'cream\n', 'pie\n', 'cabot\n', 'bubba\n', 'eggplant\n']); - runMutationTest(2, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [ - ['remove', 1, 0, "a"], - ['remove', 1, 0, "p"], - ['insert', "tu"], + runMutationTest(2, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [ + ['remove', 1, 0, 'a'], + ['remove', 1, 0, 'p'], + ['insert', 'tu'], ['skip', 11, 2], - ['insert', "cream\npie\n", 2], + ['insert', 'cream\npie\n', 2], ['skip', 2], - ['insert', "bot"], - ['insert', "\n", 1], - ['insert', "bu"], + ['insert', 'bot'], + ['insert', '\n', 1], + ['insert', 'bu'], ['skip', 3], - ['remove', 3, 1, "ge\n"], - ['remove', 6, 0, "duffle"] - ], ["tuple\n", "banana\n", "cream\n", "pie\n", "cabot\n", "bubba\n", "eggplant\n"]); + ['remove', 3, 1, 'ge\n'], + ['remove', 6, 0, 'duffle'], + ], ['tuple\n', 'banana\n', 'cream\n', 'pie\n', 'cabot\n', 'bubba\n', 'eggplant\n']); - runMutationTest(3, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [ - ['remove', 6, 1, "apple\n"], + runMutationTest(3, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [ + ['remove', 6, 1, 'apple\n'], ['skip', 15, 2], ['skip', 6], - ['remove', 1, 1, "\n"], - ['remove', 8, 0, "eggplant"], - ['skip', 1, 1] - ], ["banana\n", "cabbage\n", "duffle\n"]); + ['remove', 1, 1, '\n'], + ['remove', 8, 0, 'eggplant'], + ['skip', 1, 1], + ], ['banana\n', 'cabbage\n', 'duffle\n']); - runMutationTest(4, ["15\n"], [ + runMutationTest(4, ['15\n'], [ ['skip', 1], - ['insert', "\n2\n3\n4\n", 4], - ['skip', 2, 1] - ], ["1\n", "2\n", "3\n", "4\n", "5\n"]); + ['insert', '\n2\n3\n4\n', 4], + ['skip', 2, 1], + ], ['1\n', '2\n', '3\n', '4\n', '5\n']); - runMutationTest(5, ["1\n", "2\n", "3\n", "4\n", "5\n"], [ + runMutationTest(5, ['1\n', '2\n', '3\n', '4\n', '5\n'], [ ['skip', 1], - ['remove', 7, 4, "\n2\n3\n4\n"], - ['skip', 2, 1] - ], ["15\n"]); + ['remove', 7, 4, '\n2\n3\n4\n'], + ['skip', 2, 1], + ], ['15\n']); - runMutationTest(6, ["123\n", "abc\n", "def\n", "ghi\n", "xyz\n"], [ - ['insert', "0"], + runMutationTest(6, ['123\n', 'abc\n', 'def\n', 'ghi\n', 'xyz\n'], [ + ['insert', '0'], ['skip', 4, 1], ['skip', 4, 1], - ['remove', 8, 2, "def\nghi\n"], - ['skip', 4, 1] - ], ["0123\n", "abc\n", "xyz\n"]); + ['remove', 8, 2, 'def\nghi\n'], + ['skip', 4, 1], + ], ['0123\n', 'abc\n', 'xyz\n']); - runMutationTest(7, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [ - ['remove', 6, 1, "apple\n"], + runMutationTest(7, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [ + ['remove', 6, 1, 'apple\n'], ['skip', 15, 2, true], ['skip', 6, 0, true], - ['remove', 1, 1, "\n"], - ['remove', 8, 0, "eggplant"], - ['skip', 1, 1, true] - ], ["banana\n", "cabbage\n", "duffle\n"]); + ['remove', 1, 1, '\n'], + ['remove', 8, 0, 'eggplant'], + ['skip', 1, 1, true], + ], ['banana\n', 'cabbage\n', 'duffle\n']); function poolOrArray(attribs) { if (attribs.getAttrib) { return attribs; // it's already an attrib pool } else { // assume it's an array of attrib strings to be split and added - var p = new AttributePool(); - attribs.forEach(function (kv) { + const p = new AttributePool(); + attribs.forEach((kv) => { p.putAttrib(kv.split(',')); }); return p; @@ -236,98 +234,97 @@ function runTests() { } function runApplyToAttributionTest(testId, attribs, cs, inAttr, outCorrect) { - print("> applyToAttribution#" + testId); - var p = poolOrArray(attribs); - var result = Changeset.applyToAttribution( - Changeset.checkRep(cs), inAttr, p); + print(`> applyToAttribution#${testId}`); + const p = poolOrArray(attribs); + const result = Changeset.applyToAttribution( + Changeset.checkRep(cs), inAttr, p); assertEqualStrings(outCorrect, result); } // turn cactus\n into actusabcd\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 "david\ngreen\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"); - var lines = ["1\n", "2\n", "3\n", "4\n"]; - var mu; + print('> mutatorHasMore'); + const lines = ['1\n', '2\n', '3\n', '4\n']; + let mu; mu = Changeset.textLinesMutator(lines); - assert(mu.hasMore() + ' == true'); + assert(`${mu.hasMore()} == true`); mu.skip(8, 4); - assert(mu.hasMore() + ' == false'); + assert(`${mu.hasMore()} == false`); mu.close(); - assert(mu.hasMore() + ' == false'); + assert(`${mu.hasMore()} == false`); // still 1,2,3,4 mu = Changeset.textLinesMutator(lines); - assert(mu.hasMore() + ' == true'); + assert(`${mu.hasMore()} == true`); mu.remove(2, 1); - assert(mu.hasMore() + ' == true'); + assert(`${mu.hasMore()} == true`); mu.skip(2, 1); - assert(mu.hasMore() + ' == true'); + assert(`${mu.hasMore()} == true`); mu.skip(2, 1); - assert(mu.hasMore() + ' == true'); + assert(`${mu.hasMore()} == true`); mu.skip(2, 1); - assert(mu.hasMore() + ' == false'); - mu.insert("5\n", 1); - assert(mu.hasMore() + ' == false'); + assert(`${mu.hasMore()} == false`); + mu.insert('5\n', 1); + assert(`${mu.hasMore()} == false`); mu.close(); - assert(mu.hasMore() + ' == false'); + assert(`${mu.hasMore()} == false`); // 2,3,4,5 now mu = Changeset.textLinesMutator(lines); - assert(mu.hasMore() + ' == true'); + assert(`${mu.hasMore()} == true`); mu.remove(6, 3); - assert(mu.hasMore() + ' == true'); + assert(`${mu.hasMore()} == true`); mu.remove(2, 1); - assert(mu.hasMore() + ' == false'); - mu.insert("hello\n", 1); - assert(mu.hasMore() + ' == false'); + assert(`${mu.hasMore()} == false`); + mu.insert('hello\n', 1); + assert(`${mu.hasMore()} == false`); mu.close(); - assert(mu.hasMore() + ' == false'); - + assert(`${mu.hasMore()} == false`); })(); function runMutateAttributionTest(testId, attribs, cs, alines, outCorrect) { - print("> runMutateAttributionTest#" + testId); - var p = poolOrArray(attribs); - var alines2 = Array.prototype.slice.call(alines); - var result = Changeset.mutateAttributionLines( - Changeset.checkRep(cs), alines2, p); + print(`> runMutateAttributionTest#${testId}`); + const p = poolOrArray(attribs); + const alines2 = Array.prototype.slice.call(alines); + const result = Changeset.mutateAttributionLines( + Changeset.checkRep(cs), alines2, p); assertEqualArrays(outCorrect, alines2); - print("> runMutateAttributionTest#" + testId + ".applyToAttribution"); + print(`> runMutateAttributionTest#${testId}.applyToAttribution`); function removeQuestionMarks(a) { return a.replace(/\?/g, ''); } - var inMerged = Changeset.joinAttributionLines(alines.map(removeQuestionMarks)); - var correctMerged = Changeset.joinAttributionLines(outCorrect.map(removeQuestionMarks)); - var mergedResult = Changeset.applyToAttribution(cs, inMerged, p); + 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 456\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']); - var testPoolWithChars = (function () { - var p = new AttributePool(); + const testPoolWithChars = (function () { + const p = new AttributePool(); p.putAttrib(['char', 'newline']); - for (var i = 1; i < 36; i++) { + for (let i = 1; i < 36; i++) { p.putAttrib(['char', Changeset.numToString(i)]); } p.putAttrib(['char', '']); @@ -335,38 +332,38 @@ 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:117=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) { - var assem = Changeset.stringAssembler(); - for (var i = 0; i < len; i++) { + 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) { - var numParts = rand.nextInt(approxMaxLines * 2) + 1; - var txt = Changeset.stringAssembler(); + const numParts = rand.nextInt(approxMaxLines * 2) + 1; + const txt = Changeset.stringAssembler(); txt.append(rand.nextInt(2) ? '\n' : ''); - for (var i = 0; i < numParts; i++) { + for (let i = 0; i < numParts; i++) { if ((i % 2) == 0) { if (rand.nextInt(10)) { txt.append(randomInlineString(rand.nextInt(approxMaxCols) + 1, rand)); @@ -381,98 +378,98 @@ function runTests() { } function randomStringOperation(numCharsLeft, rand) { - var result; + let result; switch (rand.nextInt(9)) { - case 0: + case 0: { // insert char result = { - insert: randomInlineString(1, rand) + insert: randomInlineString(1, rand), }; break; } - case 1: + case 1: { // delete char result = { - remove: 1 + remove: 1, }; break; } - case 2: + case 2: { // skip char result = { - skip: 1 + skip: 1, }; break; } - case 3: + case 3: { // insert small result = { - insert: randomInlineString(rand.nextInt(4) + 1, rand) + insert: randomInlineString(rand.nextInt(4) + 1, rand), }; break; } - case 4: + case 4: { // delete small result = { - remove: rand.nextInt(4) + 1 + remove: rand.nextInt(4) + 1, }; break; } - case 5: + case 5: { // skip small result = { - skip: rand.nextInt(4) + 1 + skip: rand.nextInt(4) + 1, }; break; } - case 6: + case 6: { // insert multiline; result = { - insert: randomMultiline(5, 20, rand) + insert: randomMultiline(5, 20, rand), }; break; } - case 7: + case 7: { // delete multiline result = { - remove: Math.round(numCharsLeft * rand.nextDouble() * rand.nextDouble()) + remove: Math.round(numCharsLeft * rand.nextDouble() * rand.nextDouble()), }; break; } - case 8: + case 8: { // skip multiline result = { - skip: Math.round(numCharsLeft * rand.nextDouble() * rand.nextDouble()) + skip: Math.round(numCharsLeft * rand.nextDouble() * rand.nextDouble()), }; break; } - case 9: + case 9: { // delete to end result = { - remove: numCharsLeft + remove: numCharsLeft, }; break; } - case 10: + case 10: { // skip to end result = { - skip: numCharsLeft + skip: numCharsLeft, }; break; } } - var maxOrig = numCharsLeft - 1; + const maxOrig = numCharsLeft - 1; if ('remove' in result) { result.remove = Math.min(result.remove, maxOrig); } else if ('skip' in result) { @@ -487,34 +484,32 @@ function runTests() { return ''; } else if (rand.nextInt(3)) { if (opcode == '+' || rand.nextInt(2)) { - return '*' + Changeset.numToString(rand.nextInt(2) * 2 + 1); + return `*${Changeset.numToString(rand.nextInt(2) * 2 + 1)}`; } else { - return '*' + Changeset.numToString(rand.nextInt(2) * 2); + return `*${Changeset.numToString(rand.nextInt(2) * 2)}`; } + } else if (opcode == '+' || rand.nextInt(4) == 0) { + return '*1*3'; } else { - if (opcode == '+' || rand.nextInt(4) == 0) { - return '*1*3'; - } else { - return ['*0*2', '*0*3', '*1*2'][rand.nextInt(3)]; - } + return ['*0*2', '*0*3', '*1*2'][rand.nextInt(3)]; } } function randomTestChangeset(origText, rand, withAttribs) { - var charBank = Changeset.stringAssembler(); - var textLeft = origText; // always keep final newline - var outTextAssem = Changeset.stringAssembler(); - var opAssem = Changeset.smartOpAssembler(); - var oldLen = origText.length; + const charBank = Changeset.stringAssembler(); + let textLeft = origText; // always keep final newline + const outTextAssem = Changeset.stringAssembler(); + const opAssem = Changeset.smartOpAssembler(); + const oldLen = origText.length; - var nextOp = Changeset.newOp(); + const nextOp = Changeset.newOp(); function appendMultilineOp(opcode, txt) { nextOp.opcode = opcode; if (withAttribs) { nextOp.attribs = randomTwoPropAttribs(opcode, rand); } - txt.replace(/\n|[^\n]+/g, function (t) { + txt.replace(/\n|[^\n]+/g, (t) => { if (t == '\n') { nextOp.chars = 1; nextOp.lines = 1; @@ -529,7 +524,7 @@ function runTests() { } function doOp() { - var o = randomStringOperation(textLeft.length, rand); + const o = randomStringOperation(textLeft.length, rand); if (o.insert) { var txt = o.insert; charBank.append(txt); @@ -548,41 +543,41 @@ function runTests() { } while (textLeft.length > 1) doOp(); - for (var i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen) - var outText = outTextAssem.toString() + '\n'; + for (let i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen) + const outText = `${outTextAssem.toString()}\n`; opAssem.endDocument(); - var cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString()); + const cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString()); Changeset.checkRep(cs); return [cs, outText]; } function testCompose(randomSeed) { - var rand = new random(); - print("> testCompose#" + randomSeed); + const rand = new random(); + print(`> testCompose#${randomSeed}`); - var p = new AttributePool(); + const p = new AttributePool(); - var startText = randomMultiline(10, 20, rand) + '\n'; + const startText = `${randomMultiline(10, 20, rand)}\n`; - var x1 = randomTestChangeset(startText, rand); - var change1 = x1[0]; - var text1 = x1[1]; + const x1 = randomTestChangeset(startText, rand); + const change1 = x1[0]; + const text1 = x1[1]; - var x2 = randomTestChangeset(text1, rand); - var change2 = x2[0]; - var text2 = x2[1]; + const x2 = randomTestChangeset(text1, rand); + const change2 = x2[0]; + const text2 = x2[1]; - var x3 = randomTestChangeset(text2, rand); - var change3 = x3[0]; - var text3 = x3[1]; + const x3 = randomTestChangeset(text2, rand); + const change3 = x3[0]; + const text3 = x3[1]; - //print(literal(Changeset.toBaseTen(startText))); - //print(literal(Changeset.toBaseTen(change1))); - //print(literal(Changeset.toBaseTen(change2))); - var change12 = Changeset.checkRep(Changeset.compose(change1, change2, p)); - var change23 = Changeset.checkRep(Changeset.compose(change2, change3, p)); - var change123 = Changeset.checkRep(Changeset.compose(change12, change3, p)); - var change123a = Changeset.checkRep(Changeset.compose(change1, change23, p)); + // print(literal(Changeset.toBaseTen(startText))); + // print(literal(Changeset.toBaseTen(change1))); + // print(literal(Changeset.toBaseTen(change2))); + const change12 = Changeset.checkRep(Changeset.compose(change1, change2, p)); + const change23 = Changeset.checkRep(Changeset.compose(change2, change3, p)); + const change123 = Changeset.checkRep(Changeset.compose(change12, change3, p)); + const change123a = Changeset.checkRep(Changeset.compose(change1, change23, p)); assertEqualStrings(change123, change123a); assertEqualStrings(text2, Changeset.applyToText(change12, startText)); @@ -593,18 +588,18 @@ function runTests() { for (var i = 0; i < 30; i++) testCompose(i); (function simpleComposeAttributesTest() { - print("> simpleComposeAttributesTest"); - var p = new AttributePool(); + print('> simpleComposeAttributesTest'); + const p = new AttributePool(); p.putAttrib(['bold', '']); p.putAttrib(['bold', 'true']); - var cs1 = Changeset.checkRep("Z:2>1*1+1*1=1$x"); - var cs2 = Changeset.checkRep("Z:3>0*0|1=3$"); - var cs12 = Changeset.checkRep(Changeset.compose(cs1, cs2, p)); - assertEqualStrings("Z:2>1+1*0|1=2$x", cs12); + const cs1 = Changeset.checkRep('Z:2>1*1+1*1=1$x'); + const cs2 = Changeset.checkRep('Z:3>0*0|1=3$'); + const cs12 = Changeset.checkRep(Changeset.compose(cs1, cs2, p)); + assertEqualStrings('Z:2>1+1*0|1=2$x', cs12); })(); (function followAttributesTest() { - var p = new AttributePool(); + const p = new AttributePool(); p.putAttrib(['x', '']); p.putAttrib(['x', 'abc']); p.putAttrib(['x', 'def']); @@ -630,21 +625,21 @@ function runTests() { })(); function testFollow(randomSeed) { - var rand = new random(); - print("> testFollow#" + randomSeed); + const rand = new random(); + print(`> testFollow#${randomSeed}`); - var p = new AttributePool(); + const p = new AttributePool(); - var startText = randomMultiline(10, 20, rand) + '\n'; + const startText = `${randomMultiline(10, 20, rand)}\n`; - var cs1 = randomTestChangeset(startText, rand)[0]; - var cs2 = randomTestChangeset(startText, rand)[0]; + const cs1 = randomTestChangeset(startText, rand)[0]; + const cs2 = randomTestChangeset(startText, rand)[0]; - var afb = Changeset.checkRep(Changeset.follow(cs1, cs2, false, p)); - var bfa = Changeset.checkRep(Changeset.follow(cs2, cs1, true, p)); + const afb = Changeset.checkRep(Changeset.follow(cs1, cs2, false, p)); + const bfa = Changeset.checkRep(Changeset.follow(cs2, cs1, true, p)); - var merge1 = Changeset.checkRep(Changeset.compose(cs1, afb)); - var merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa)); + const merge1 = Changeset.checkRep(Changeset.compose(cs1, afb)); + const merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa)); assertEqualStrings(merge1, merge2); } @@ -652,26 +647,26 @@ function runTests() { for (var i = 0; i < 30; i++) testFollow(i); function testSplitJoinAttributionLines(randomSeed) { - var rand = new random(); - print("> testSplitJoinAttributionLines#" + randomSeed); + const rand = new random(); + print(`> testSplitJoinAttributionLines#${randomSeed}`); - var doc = randomMultiline(10, 20, rand) + '\n'; + const doc = `${randomMultiline(10, 20, rand)}\n`; function stringToOps(str) { - var assem = Changeset.mergingOpAssembler(); - var o = Changeset.newOp('+'); + const assem = Changeset.mergingOpAssembler(); + const o = Changeset.newOp('+'); o.chars = 1; - for (var i = 0; i < str.length; i++) { - var c = str.charAt(i); + 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.attribs = (c == 'a' || c == 'b' ? `*${c}` : ''); assem.append(o); } return assem.toString(); } - var theJoined = stringToOps(doc); - var theSplit = doc.match(/[^\n]*\n/g).map(stringToOps); + const theJoined = stringToOps(doc); + const theSplit = doc.match(/[^\n]*\n/g).map(stringToOps); assertEqualArrays(theSplit, Changeset.splitAttributionLines(theJoined, doc)); assertEqualStrings(theJoined, Changeset.joinAttributionLines(theSplit)); @@ -680,10 +675,10 @@ function runTests() { for (var i = 0; i < 10; i++) testSplitJoinAttributionLines(i); (function testMoveOpsToNewPool() { - print("> testMoveOpsToNewPool"); + print('> testMoveOpsToNewPool'); - var pool1 = new AttributePool(); - var pool2 = new AttributePool(); + const pool1 = new AttributePool(); + const pool2 = new AttributePool(); pool1.putAttrib(['baz', 'qux']); pool1.putAttrib(['foo', 'bar']); @@ -696,249 +691,241 @@ function runTests() { (function testMakeSplice() { - print("> testMakeSplice"); - - var t = "a\nb\nc\n"; - var t2 = Changeset.applyToText(Changeset.makeSplice(t, 5, 0, "def"), t); - assertEqualStrings("a\nb\ncdef\n", t2); + print('> testMakeSplice'); + const t = 'a\nb\nc\n'; + const t2 = Changeset.applyToText(Changeset.makeSplice(t, 5, 0, 'def'), t); + assertEqualStrings('a\nb\ncdef\n', t2); })(); (function testToSplices() { - print("> testToSplices"); + print('> testToSplices'); - var cs = Changeset.checkRep('Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk'); - var correctSplices = [ - [5, 8, "123456789"], - [9, 17, "abcdefghijk"] + const cs = Changeset.checkRep('Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk'); + const correctSplices = [ + [5, 8, '123456789'], + [9, 17, 'abcdefghijk'], ]; assertEqualArrays(correctSplices, Changeset.toSplices(cs)); })(); function testCharacterRangeFollow(testId, cs, oldRange, insertionsAfter, correctNewRange) { - print("> testCharacterRangeFollow#" + testId); + print(`> testCharacterRangeFollow#${testId}`); 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(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]); - testCharacterRangeFollow(5, "Z:5>1+1=1-3+3$abcd", [1, 4], false, [5, 5]); - testCharacterRangeFollow(6, "Z:5>1+1=1-3+3$abcd", [1, 4], true, [2, 2]); - testCharacterRangeFollow(7, "Z:5>1+1=1-3+3$abcd", [0, 6], false, [1, 7]); - testCharacterRangeFollow(8, "Z:5>1+1=1-3+3$abcd", [0, 3], false, [1, 2]); - testCharacterRangeFollow(9, "Z:5>1+1=1-3+3$abcd", [2, 5], false, [5, 6]); - testCharacterRangeFollow(10, "Z:2>1+1$a", [0, 0], false, [1, 1]); - testCharacterRangeFollow(11, "Z:2>1+1$a", [0, 0], true, [0, 0]); + 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]); + testCharacterRangeFollow(5, 'Z:5>1+1=1-3+3$abcd', [1, 4], false, [5, 5]); + testCharacterRangeFollow(6, 'Z:5>1+1=1-3+3$abcd', [1, 4], true, [2, 2]); + testCharacterRangeFollow(7, 'Z:5>1+1=1-3+3$abcd', [0, 6], false, [1, 7]); + testCharacterRangeFollow(8, 'Z:5>1+1=1-3+3$abcd', [0, 3], false, [1, 2]); + testCharacterRangeFollow(9, 'Z:5>1+1=1-3+3$abcd', [2, 5], false, [5, 6]); + testCharacterRangeFollow(10, 'Z:2>1+1$a', [0, 0], false, [1, 1]); + testCharacterRangeFollow(11, 'Z:2>1+1$a', [0, 0], true, [0, 0]); (function testOpAttributeValue() { - print("> testOpAttributeValue"); + print('> testOpAttributeValue'); - var p = new AttributePool(); + const p = new AttributePool(); 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) { - print("> testAppendATextToAssembler#" + testId); + print(`> testAppendATextToAssembler#${testId}`); - var assem = Changeset.smartOpAssembler(); + const assem = Changeset.smartOpAssembler(); Changeset.appendATextToAssembler(atext, assem); assertEqualStrings(correctOps, assem.toString()); } testAppendATextToAssembler(1, { - text: "\n", - attribs: "|1+1" - }, ""); + text: '\n', + attribs: '|1+1', + }, ''); testAppendATextToAssembler(2, { - text: "\n\n", - attribs: "|2+2" - }, "|1+1"); + text: '\n\n', + attribs: '|2+2', + }, '|1+1'); testAppendATextToAssembler(3, { - text: "\n\n", - attribs: "*x|2+2" - }, "*x|1+1"); + text: '\n\n', + attribs: '*x|2+2', + }, '*x|1+1'); testAppendATextToAssembler(4, { - text: "\n\n", - attribs: "*x|1+1|1+1" - }, "*x|1+1"); + text: '\n\n', + attribs: '*x|1+1|1+1', + }, '*x|1+1'); testAppendATextToAssembler(5, { - text: "foo\n", - attribs: "|1+4" - }, "+3"); + text: 'foo\n', + attribs: '|1+4', + }, '+3'); testAppendATextToAssembler(6, { - text: "\nfoo\n", - attribs: "|2+5" - }, "|1+1+3"); + text: '\nfoo\n', + attribs: '|2+5', + }, '|1+1+3'); testAppendATextToAssembler(7, { - text: "\nfoo\n", - attribs: "*x|2+5" - }, "*x|1+1*x+3"); + text: '\nfoo\n', + attribs: '*x|2+5', + }, '*x|1+1*x+3'); testAppendATextToAssembler(8, { - text: "\n\n\nfoo\n", - attribs: "|2+2*x|2+5" - }, "|2+2*x|1+1*x+3"); + text: '\n\n\nfoo\n', + attribs: '|2+2*x|2+5', + }, '|2+2*x|1+1*x+3'); function testMakeAttribsString(testId, pool, opcode, attribs, correctString) { - print("> testMakeAttribsString#" + testId); + print(`> testMakeAttribsString#${testId}`); - var p = poolOrArray(pool); - var str = Changeset.makeAttribsString(opcode, attribs, p); + const p = poolOrArray(pool); + const str = Changeset.makeAttribsString(opcode, attribs, p); assertEqualStrings(correctString, str); } testMakeAttribsString(1, ['bold,'], '+', [ - ['bold', ''] + ['bold', ''], ], ''); testMakeAttribsString(2, ['abc,def', 'bold,'], '=', [ - ['bold', ''] + ['bold', ''], ], '*1'); testMakeAttribsString(3, ['abc,def', 'bold,true'], '+', [ ['abc', 'def'], - ['bold', 'true'] + ['bold', 'true'], ], '*0*1'); testMakeAttribsString(4, ['abc,def', 'bold,true'], '+', [ ['bold', 'true'], - ['abc', 'def'] + ['abc', 'def'], ], '*0*1'); function testSubattribution(testId, astr, start, end, correctOutput) { - print("> testSubattribution#" + testId); + print(`> testSubattribution#${testId}`); - var str = Changeset.subattribution(astr, start, end); + const str = Changeset.subattribution(astr, start, end); assertEqualStrings(correctOutput, str); } - testSubattribution(1, "+1", 0, 0, ""); - testSubattribution(2, "+1", 0, 1, "+1"); - testSubattribution(3, "+1", 0, undefined, "+1"); - testSubattribution(4, "|1+1", 0, 0, ""); - testSubattribution(5, "|1+1", 0, 1, "|1+1"); - testSubattribution(6, "|1+1", 0, undefined, "|1+1"); - testSubattribution(7, "*0+1", 0, 0, ""); - testSubattribution(8, "*0+1", 0, 1, "*0+1"); - testSubattribution(9, "*0+1", 0, undefined, "*0+1"); - testSubattribution(10, "*0|1+1", 0, 0, ""); - testSubattribution(11, "*0|1+1", 0, 1, "*0|1+1"); - testSubattribution(12, "*0|1+1", 0, undefined, "*0|1+1"); - testSubattribution(13, "*0+2+1*1+3", 0, 1, "*0+1"); - testSubattribution(14, "*0+2+1*1+3", 0, 2, "*0+2"); - testSubattribution(15, "*0+2+1*1+3", 0, 3, "*0+2+1"); - testSubattribution(16, "*0+2+1*1+3", 0, 4, "*0+2+1*1+1"); - testSubattribution(17, "*0+2+1*1+3", 0, 5, "*0+2+1*1+2"); - testSubattribution(18, "*0+2+1*1+3", 0, 6, "*0+2+1*1+3"); - testSubattribution(19, "*0+2+1*1+3", 0, 7, "*0+2+1*1+3"); - testSubattribution(20, "*0+2+1*1+3", 0, undefined, "*0+2+1*1+3"); - testSubattribution(21, "*0+2+1*1+3", 1, undefined, "*0+1+1*1+3"); - testSubattribution(22, "*0+2+1*1+3", 2, undefined, "+1*1+3"); - testSubattribution(23, "*0+2+1*1+3", 3, undefined, "*1+3"); - testSubattribution(24, "*0+2+1*1+3", 4, undefined, "*1+2"); - testSubattribution(25, "*0+2+1*1+3", 5, undefined, "*1+1"); - testSubattribution(26, "*0+2+1*1+3", 6, undefined, ""); - testSubattribution(27, "*0+2+1*1|1+3", 0, 1, "*0+1"); - testSubattribution(28, "*0+2+1*1|1+3", 0, 2, "*0+2"); - testSubattribution(29, "*0+2+1*1|1+3", 0, 3, "*0+2+1"); - testSubattribution(30, "*0+2+1*1|1+3", 0, 4, "*0+2+1*1+1"); - testSubattribution(31, "*0+2+1*1|1+3", 0, 5, "*0+2+1*1+2"); - testSubattribution(32, "*0+2+1*1|1+3", 0, 6, "*0+2+1*1|1+3"); - testSubattribution(33, "*0+2+1*1|1+3", 0, 7, "*0+2+1*1|1+3"); - testSubattribution(34, "*0+2+1*1|1+3", 0, undefined, "*0+2+1*1|1+3"); - testSubattribution(35, "*0+2+1*1|1+3", 1, undefined, "*0+1+1*1|1+3"); - testSubattribution(36, "*0+2+1*1|1+3", 2, undefined, "+1*1|1+3"); - testSubattribution(37, "*0+2+1*1|1+3", 3, undefined, "*1|1+3"); - testSubattribution(38, "*0+2+1*1|1+3", 4, undefined, "*1|1+2"); - testSubattribution(39, "*0+2+1*1|1+3", 5, undefined, "*1|1+1"); - testSubattribution(40, "*0+2+1*1|1+3", 1, 5, "*0+1+1*1+2"); - testSubattribution(41, "*0+2+1*1|1+3", 2, 6, "+1*1|1+3"); - testSubattribution(42, "*0+2+1*1+3", 2, 6, "+1*1+3"); + testSubattribution(1, '+1', 0, 0, ''); + testSubattribution(2, '+1', 0, 1, '+1'); + testSubattribution(3, '+1', 0, undefined, '+1'); + testSubattribution(4, '|1+1', 0, 0, ''); + testSubattribution(5, '|1+1', 0, 1, '|1+1'); + testSubattribution(6, '|1+1', 0, undefined, '|1+1'); + testSubattribution(7, '*0+1', 0, 0, ''); + testSubattribution(8, '*0+1', 0, 1, '*0+1'); + testSubattribution(9, '*0+1', 0, undefined, '*0+1'); + testSubattribution(10, '*0|1+1', 0, 0, ''); + testSubattribution(11, '*0|1+1', 0, 1, '*0|1+1'); + testSubattribution(12, '*0|1+1', 0, undefined, '*0|1+1'); + testSubattribution(13, '*0+2+1*1+3', 0, 1, '*0+1'); + testSubattribution(14, '*0+2+1*1+3', 0, 2, '*0+2'); + testSubattribution(15, '*0+2+1*1+3', 0, 3, '*0+2+1'); + testSubattribution(16, '*0+2+1*1+3', 0, 4, '*0+2+1*1+1'); + testSubattribution(17, '*0+2+1*1+3', 0, 5, '*0+2+1*1+2'); + testSubattribution(18, '*0+2+1*1+3', 0, 6, '*0+2+1*1+3'); + testSubattribution(19, '*0+2+1*1+3', 0, 7, '*0+2+1*1+3'); + testSubattribution(20, '*0+2+1*1+3', 0, undefined, '*0+2+1*1+3'); + testSubattribution(21, '*0+2+1*1+3', 1, undefined, '*0+1+1*1+3'); + testSubattribution(22, '*0+2+1*1+3', 2, undefined, '+1*1+3'); + testSubattribution(23, '*0+2+1*1+3', 3, undefined, '*1+3'); + testSubattribution(24, '*0+2+1*1+3', 4, undefined, '*1+2'); + testSubattribution(25, '*0+2+1*1+3', 5, undefined, '*1+1'); + testSubattribution(26, '*0+2+1*1+3', 6, undefined, ''); + testSubattribution(27, '*0+2+1*1|1+3', 0, 1, '*0+1'); + testSubattribution(28, '*0+2+1*1|1+3', 0, 2, '*0+2'); + testSubattribution(29, '*0+2+1*1|1+3', 0, 3, '*0+2+1'); + testSubattribution(30, '*0+2+1*1|1+3', 0, 4, '*0+2+1*1+1'); + testSubattribution(31, '*0+2+1*1|1+3', 0, 5, '*0+2+1*1+2'); + testSubattribution(32, '*0+2+1*1|1+3', 0, 6, '*0+2+1*1|1+3'); + testSubattribution(33, '*0+2+1*1|1+3', 0, 7, '*0+2+1*1|1+3'); + testSubattribution(34, '*0+2+1*1|1+3', 0, undefined, '*0+2+1*1|1+3'); + testSubattribution(35, '*0+2+1*1|1+3', 1, undefined, '*0+1+1*1|1+3'); + testSubattribution(36, '*0+2+1*1|1+3', 2, undefined, '+1*1|1+3'); + testSubattribution(37, '*0+2+1*1|1+3', 3, undefined, '*1|1+3'); + testSubattribution(38, '*0+2+1*1|1+3', 4, undefined, '*1|1+2'); + testSubattribution(39, '*0+2+1*1|1+3', 5, undefined, '*1|1+1'); + testSubattribution(40, '*0+2+1*1|1+3', 1, 5, '*0+1+1*1+2'); + 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) { - print("> testFilterAttribNumbers#" + testId); + print(`> testFilterAttribNumbers#${testId}`); - var str = Changeset.filterAttribNumbers(cs, filter); + 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", function (n) { - return (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", function (n) { - return (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) { - print("> testInverse#" + testId); + print(`> testInverse#${testId}`); pool = poolOrArray(pool); - var str = Changeset.inverse(Changeset.checkRep(cs), lines, alines, 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) { - print("> testMutateTextLines#" + testId); + print(`> testMutateTextLines#${testId}`); - var a = lines.slice(); + 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"]); + 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) { - var rand = new random(); - print("> testInverseRandom#" + randomSeed); + const rand = new random(); + print(`> testInverseRandom#${randomSeed}`); - var p = poolOrArray(['apple,', 'apple,true', 'banana,', 'banana,true']); + const p = poolOrArray(['apple,', 'apple,true', 'banana,', 'banana,true']); - var startText = randomMultiline(10, 20, rand) + '\n'; - var alines = Changeset.splitAttributionLines(Changeset.makeAttribution(startText), startText); - var lines = startText.slice(0, -1).split('\n').map(function (s) { - return s + '\n'; - }); + const startText = `${randomMultiline(10, 20, rand)}\n`; + const alines = Changeset.splitAttributionLines(Changeset.makeAttribution(startText), startText); + const lines = startText.slice(0, -1).split('\n').map((s) => `${s}\n`); - var stylifier = randomTestChangeset(startText, rand, true)[0]; + const stylifier = randomTestChangeset(startText, rand, true)[0]; - //print(alines.join('\n')); + // print(alines.join('\n')); Changeset.mutateAttributionLines(stylifier, alines, p); - //print(stylifier); - //print(alines.join('\n')); + // print(stylifier); + // print(alines.join('\n')); Changeset.mutateTextLines(stylifier, lines); - var changeset = randomTestChangeset(lines.join(''), rand, true)[0]; - var inverseChangeset = Changeset.inverse(changeset, lines, alines, p); + const changeset = randomTestChangeset(lines.join(''), rand, true)[0]; + const inverseChangeset = Changeset.inverse(changeset, lines, alines, p); - var origLines = lines.slice(); - var origALines = alines.slice(); + const origLines = lines.slice(); + const origALines = alines.slice(); Changeset.mutateTextLines(changeset, lines); Changeset.mutateAttributionLines(changeset, alines, p); - //print(origALines.join('\n')); - //print(changeset); - //print(inverseChangeset); - //print(origLines.map(function(s) { return '1: '+s.slice(0,-1); }).join('\n')); - //print(lines.map(function(s) { return '2: '+s.slice(0,-1); }).join('\n')); - //print(alines.join('\n')); + // print(origALines.join('\n')); + // print(changeset); + // print(inverseChangeset); + // print(origLines.map(function(s) { return '1: '+s.slice(0,-1); }).join('\n')); + // print(lines.map(function(s) { return '2: '+s.slice(0,-1); }).join('\n')); + // print(alines.join('\n')); Changeset.mutateTextLines(inverseChangeset, lines); Changeset.mutateAttributionLines(inverseChangeset, alines, p); - //print(lines.map(function(s) { return '3: '+s.slice(0,-1); }).join('\n')); + // print(lines.map(function(s) { return '3: '+s.slice(0,-1); }).join('\n')); assertEqualArrays(origLines, lines); assertEqualArrays(origALines, alines); } diff --git a/src/node/eejs/index.js b/src/node/eejs/index.js index 057c24c75..3e30200f2 100644 --- a/src/node/eejs/index.js +++ b/src/node/eejs/index.js @@ -19,84 +19,84 @@ * require("./index").require("./examples/foo.ejs") */ -var ejs = require("ejs"); -var fs = require("fs"); -var path = require("path"); -var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); -var resolve = require("resolve"); -var settings = require('../utils/Settings'); +const ejs = require('ejs'); +const fs = require('fs'); +const path = require('path'); +const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks.js'); +const resolve = require('resolve'); +const settings = require('../utils/Settings'); -const templateCache = new Map() +const templateCache = new Map(); exports.info = { __output_stack: [], block_stack: [], file_stack: [], - args: [] + args: [], }; function getCurrentFile() { - return exports.info.file_stack[exports.info.file_stack.length-1]; + return exports.info.file_stack[exports.info.file_stack.length - 1]; } function createBlockId(name) { - return getCurrentFile().path + '|' + name; + return `${getCurrentFile().path}|${name}`; } exports._init = function (b, recursive) { exports.info.__output_stack.push(exports.info.__output); exports.info.__output = b; -} +}; exports._exit = function (b, recursive) { - getCurrentFile().inherit.forEach(function (item) { + getCurrentFile().inherit.forEach((item) => { exports._require(item.name, item.args); }); exports.info.__output = exports.info.__output_stack.pop(); -} +}; -exports.begin_capture = function() { +exports.begin_capture = function () { exports.info.__output_stack.push(exports.info.__output.concat()); exports.info.__output.splice(0, exports.info.__output.length); -} +}; exports.end_capture = function () { - var res = exports.info.__output.join(""); + const res = exports.info.__output.join(''); exports.info.__output.splice.apply( - exports.info.__output, - [0, exports.info.__output.length].concat(exports.info.__output_stack.pop())); + exports.info.__output, + [0, exports.info.__output.length].concat(exports.info.__output_stack.pop())); return res; -} +}; exports.begin_define_block = function (name) { exports.info.block_stack.push(name); exports.begin_capture(); -} +}; exports.end_define_block = function () { - var content = exports.end_capture(); + const content = exports.end_capture(); return content; -} +}; exports.end_block = function () { - var name = exports.info.block_stack.pop(); - var renderContext = exports.info.args[exports.info.args.length-1]; - var args = {content: exports.end_define_block(), renderContext: renderContext}; - hooks.callAll("eejsBlock_" + name, args); + const name = exports.info.block_stack.pop(); + const renderContext = exports.info.args[exports.info.args.length - 1]; + const args = {content: exports.end_define_block(), renderContext}; + hooks.callAll(`eejsBlock_${name}`, args); exports.info.__output.push(args.content); -} +}; exports.begin_block = exports.begin_define_block; exports.inherit = function (name, args) { - getCurrentFile().inherit.push({name:name, args:args}); -} + getCurrentFile().inherit.push({name, args}); +}; exports.require = function (name, args, mod) { if (args == undefined) args = {}; - var basedir = __dirname; - var paths = []; + let basedir = __dirname; + let paths = []; if (exports.info.file_stack.length) { basedir = path.dirname(getCurrentFile().path); @@ -106,43 +106,43 @@ exports.require = function (name, args, mod) { paths = mod.paths; } - var ejspath = resolve.sync( - name, - { - paths : paths, - basedir : basedir, - extensions : [ '.html', '.ejs' ], - } - ) + const ejspath = resolve.sync( + name, + { + paths, + basedir, + extensions: ['.html', '.ejs'], + }, + ); args.e = exports; args.require = require; - let template - if (settings.maxAge !== 0){ // don't cache if maxAge is 0 + let template; + if (settings.maxAge !== 0) { // don't cache if maxAge is 0 if (!templateCache.has(ejspath)) { - template = '<% e._init(__output); %>' + fs.readFileSync(ejspath).toString() + '<% e._exit(); %>'; - templateCache.set(ejspath, template) + template = `<% e._init(__output); %>${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`; + templateCache.set(ejspath, template); } else { - template = templateCache.get(ejspath) + template = templateCache.get(ejspath); } - }else{ - template = '<% e._init(__output); %>' + fs.readFileSync(ejspath).toString() + '<% e._exit(); %>'; + } else { + template = `<% e._init(__output); %>${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`; } exports.info.args.push(args); exports.info.file_stack.push({path: ejspath, inherit: []}); - if(settings.maxAge !== 0){ - var res = ejs.render(template, args, { cache: true, filename: ejspath }); - }else{ - var res = ejs.render(template, args, { cache: false, filename: ejspath }); + if (settings.maxAge !== 0) { + var res = ejs.render(template, args, {cache: true, filename: ejspath}); + } else { + var res = ejs.render(template, args, {cache: false, filename: ejspath}); } exports.info.file_stack.pop(); exports.info.args.pop(); return res; -} +}; exports._require = function (name, args) { exports.info.__output.push(exports.require(name, args)); -} +}; diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index 6a134553a..242e4e873 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -18,131 +18,118 @@ * limitations under the License. */ -var absolutePaths = require('../utils/AbsolutePaths'); -var fs = require("fs"); -var api = require("../db/API"); -var log4js = require('log4js'); -var padManager = require("../db/PadManager"); -var randomString = require("../utils/randomstring"); -var argv = require('../utils/Cli').argv; -var createHTTPError = require('http-errors'); +const absolutePaths = require('../utils/AbsolutePaths'); +const fs = require('fs'); +const api = require('../db/API'); +const log4js = require('log4js'); +const padManager = require('../db/PadManager'); +const randomString = require('../utils/randomstring'); +const argv = require('../utils/Cli').argv; +const createHTTPError = require('http-errors'); -var apiHandlerLogger = log4js.getLogger('APIHandler'); +const apiHandlerLogger = log4js.getLogger('APIHandler'); -//ensure we have an apikey -var apikey = null; -var apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || "./APIKEY.txt"); +// ensure we have an apikey +let apikey = null; +const apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || './APIKEY.txt'); try { - apikey = fs.readFileSync(apikeyFilename,"utf8"); + apikey = fs.readFileSync(apikeyFilename, 'utf8'); apiHandlerLogger.info(`Api key file read from: "${apikeyFilename}"`); -} catch(e) { +} catch (e) { apiHandlerLogger.info(`Api key file "${apikeyFilename}" not found. Creating with random contents.`); apikey = randomString(32); - fs.writeFileSync(apikeyFilename,apikey,"utf8"); + fs.writeFileSync(apikeyFilename, apikey, 'utf8'); } // a list of all functions -var version = {}; +const version = {}; -version["1"] = Object.assign({}, - { "createGroup" : [] - , "createGroupIfNotExistsFor" : ["groupMapper"] - , "deleteGroup" : ["groupID"] - , "listPads" : ["groupID"] - , "createPad" : ["padID", "text"] - , "createGroupPad" : ["groupID", "padName", "text"] - , "createAuthor" : ["name"] - , "createAuthorIfNotExistsFor": ["authorMapper" , "name"] - , "listPadsOfAuthor" : ["authorID"] - , "createSession" : ["groupID", "authorID", "validUntil"] - , "deleteSession" : ["sessionID"] - , "getSessionInfo" : ["sessionID"] - , "listSessionsOfGroup" : ["groupID"] - , "listSessionsOfAuthor" : ["authorID"] - , "getText" : ["padID", "rev"] - , "setText" : ["padID", "text"] - , "getHTML" : ["padID", "rev"] - , "setHTML" : ["padID", "html"] - , "getRevisionsCount" : ["padID"] - , "getLastEdited" : ["padID"] - , "deletePad" : ["padID"] - , "getReadOnlyID" : ["padID"] - , "setPublicStatus" : ["padID", "publicStatus"] - , "getPublicStatus" : ["padID"] - , "listAuthorsOfPad" : ["padID"] - , "padUsersCount" : ["padID"] - } +version['1'] = Object.assign({}, + {createGroup: [], + createGroupIfNotExistsFor: ['groupMapper'], + deleteGroup: ['groupID'], + listPads: ['groupID'], + createPad: ['padID', 'text'], + createGroupPad: ['groupID', 'padName', 'text'], + createAuthor: ['name'], + createAuthorIfNotExistsFor: ['authorMapper', 'name'], + listPadsOfAuthor: ['authorID'], + createSession: ['groupID', 'authorID', 'validUntil'], + deleteSession: ['sessionID'], + getSessionInfo: ['sessionID'], + listSessionsOfGroup: ['groupID'], + listSessionsOfAuthor: ['authorID'], + getText: ['padID', 'rev'], + setText: ['padID', 'text'], + getHTML: ['padID', 'rev'], + setHTML: ['padID', 'html'], + getRevisionsCount: ['padID'], + getLastEdited: ['padID'], + deletePad: ['padID'], + getReadOnlyID: ['padID'], + setPublicStatus: ['padID', 'publicStatus'], + getPublicStatus: ['padID'], + listAuthorsOfPad: ['padID'], + padUsersCount: ['padID']}, ); -version["1.1"] = Object.assign({}, version["1"], - { "getAuthorName" : ["authorID"] - , "padUsers" : ["padID"] - , "sendClientsMessage" : ["padID", "msg"] - , "listAllGroups" : [] - } +version['1.1'] = Object.assign({}, version['1'], + {getAuthorName: ['authorID'], + padUsers: ['padID'], + sendClientsMessage: ['padID', 'msg'], + listAllGroups: []}, ); -version["1.2"] = Object.assign({}, version["1.1"], - { "checkToken" : [] - } +version['1.2'] = Object.assign({}, version['1.1'], + {checkToken: []}, ); -version["1.2.1"] = Object.assign({}, version["1.2"], - { "listAllPads" : [] - } +version['1.2.1'] = Object.assign({}, version['1.2'], + {listAllPads: []}, ); -version["1.2.7"] = Object.assign({}, version["1.2.1"], - { "createDiffHTML" : ["padID", "startRev", "endRev"] - , "getChatHistory" : ["padID", "start", "end"] - , "getChatHead" : ["padID"] - } +version['1.2.7'] = Object.assign({}, version['1.2.1'], + {createDiffHTML: ['padID', 'startRev', 'endRev'], + getChatHistory: ['padID', 'start', 'end'], + getChatHead: ['padID']}, ); -version["1.2.8"] = Object.assign({}, version["1.2.7"], - { "getAttributePool" : ["padID"] - , "getRevisionChangeset" : ["padID", "rev"] - } +version['1.2.8'] = Object.assign({}, version['1.2.7'], + {getAttributePool: ['padID'], + getRevisionChangeset: ['padID', 'rev']}, ); -version["1.2.9"] = Object.assign({}, version["1.2.8"], - { "copyPad" : ["sourceID", "destinationID", "force"] - , "movePad" : ["sourceID", "destinationID", "force"] - } +version['1.2.9'] = Object.assign({}, version['1.2.8'], + {copyPad: ['sourceID', 'destinationID', 'force'], + movePad: ['sourceID', 'destinationID', 'force']}, ); -version["1.2.10"] = Object.assign({}, version["1.2.9"], - { "getPadID" : ["roID"] - } +version['1.2.10'] = Object.assign({}, version['1.2.9'], + {getPadID: ['roID']}, ); -version["1.2.11"] = Object.assign({}, version["1.2.10"], - { "getSavedRevisionsCount" : ["padID"] - , "listSavedRevisions" : ["padID"] - , "saveRevision" : ["padID", "rev"] - , "restoreRevision" : ["padID", "rev"] - } +version['1.2.11'] = Object.assign({}, version['1.2.10'], + {getSavedRevisionsCount: ['padID'], + listSavedRevisions: ['padID'], + saveRevision: ['padID', 'rev'], + restoreRevision: ['padID', 'rev']}, ); -version["1.2.12"] = Object.assign({}, version["1.2.11"], - { "appendChatMessage" : ["padID", "text", "authorID", "time"] - } +version['1.2.12'] = Object.assign({}, version['1.2.11'], + {appendChatMessage: ['padID', 'text', 'authorID', 'time']}, ); -version["1.2.13"] = Object.assign({}, version["1.2.12"], - { "appendText" : ["padID", "text"] - } +version['1.2.13'] = Object.assign({}, version['1.2.12'], + {appendText: ['padID', 'text']}, ); -version["1.2.14"] = Object.assign({}, version["1.2.13"], - { "getStats" : [] - } +version['1.2.14'] = Object.assign({}, version['1.2.13'], + {getStats: []}, ); -version["1.2.15"] = Object.assign({}, version["1.2.14"], - { "copyPadWithoutHistory" : ["sourceID", "destinationID", "force"] - } +version['1.2.15'] = Object.assign({}, version['1.2.14'], + {copyPadWithoutHistory: ['sourceID', 'destinationID', 'force']}, ); // set the latest available API version here @@ -158,7 +145,7 @@ exports.version = version; * @req express request object * @res express response object */ -exports.handle = async function(apiVersion, functionName, fields, req, res) { +exports.handle = async function (apiVersion, functionName, fields, req, res) { // say goodbye if this is an unknown API version if (!(apiVersion in version)) { throw new createHTTPError.NotFound('no such api version'); @@ -170,31 +157,29 @@ exports.handle = async function(apiVersion, functionName, fields, req, res) { } // check the api key! - fields["apikey"] = fields["apikey"] || fields["api_key"]; + fields.apikey = fields.apikey || fields.api_key; - if (fields["apikey"] !== apikey.trim()) { + if (fields.apikey !== apikey.trim()) { throw new createHTTPError.Unauthorized('no or wrong API Key'); } // sanitize any padIDs before continuing - if (fields["padID"]) { - fields["padID"] = await padManager.sanitizePadId(fields["padID"]); + if (fields.padID) { + fields.padID = await padManager.sanitizePadId(fields.padID); } // there was an 'else' here before - removed it to ensure // that this sanitize step can't be circumvented by forcing // the first branch to be taken - if (fields["padName"]) { - fields["padName"] = await padManager.sanitizePadId(fields["padName"]); + if (fields.padName) { + fields.padName = await padManager.sanitizePadId(fields.padName); } // put the function parameters in an array - var functionParams = version[apiVersion][functionName].map(function (field) { - return fields[field] - }); + const functionParams = version[apiVersion][functionName].map((field) => fields[field]); // call the api function return api[functionName].apply(this, functionParams); -} +}; exports.exportedForTestingOnly = { apiKey: apikey, diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js index 990426dbb..0a92633f7 100644 --- a/src/node/handler/ExportHandler.js +++ b/src/node/handler/ExportHandler.js @@ -19,15 +19,15 @@ * limitations under the License. */ -var exporthtml = require("../utils/ExportHtml"); -var exporttxt = require("../utils/ExportTxt"); -var exportEtherpad = require("../utils/ExportEtherpad"); -var fs = require("fs"); -var settings = require('../utils/Settings'); -var os = require('os'); -var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); -var TidyHtml = require('../utils/TidyHtml'); -const util = require("util"); +const exporthtml = require('../utils/ExportHtml'); +const exporttxt = require('../utils/ExportTxt'); +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 TidyHtml = require('../utils/TidyHtml'); +const util = require('util'); const fsp_writeFile = util.promisify(fs.writeFile); const fsp_unlink = util.promisify(fs.unlink); @@ -36,12 +36,12 @@ let convertor = null; // load abiword only if it is enabled if (settings.abiword != null) { - convertor = require("../utils/Abiword"); + convertor = require('../utils/Abiword'); } // Use LibreOffice if an executable has been defined in the settings if (settings.soffice != null) { - convertor = require("../utils/LibreOffice"); + convertor = require('../utils/LibreOffice'); } const tempDirectory = os.tmpdir(); @@ -51,10 +51,10 @@ const tempDirectory = os.tmpdir(); */ async function doExport(req, res, padId, readOnlyId, type) { // avoid naming the read-only file as the original pad's id - var fileName = readOnlyId ? readOnlyId : padId; + let fileName = readOnlyId ? readOnlyId : padId; // allow fileName to be overwritten by a hook, the type type is kept static for security reasons - let hookFileName = await hooks.aCallFirst("exportFileName", padId); + const hookFileName = await hooks.aCallFirst('exportFileName', padId); // if fileName is set then set it to the padId, note that fileName is returned as an array. if (hookFileName.length) { @@ -62,15 +62,15 @@ async function doExport(req, res, padId, readOnlyId, type) { } // tell the browser that this is a downloadable file - res.attachment(fileName + "." + type); + res.attachment(`${fileName}.${type}`); // if this is a plain text export, we can do this directly // We have to over engineer this because tabs are stored as attributes and not plain text - if (type === "etherpad") { - let pad = await exportEtherpad.getPadRaw(padId); + if (type === 'etherpad') { + const pad = await exportEtherpad.getPadRaw(padId); res.send(pad); - } else if (type === "txt") { - let txt = await exporttxt.getPadTXTDocument(padId, req.params.rev); + } else if (type === 'txt') { + const txt = await exporttxt.getPadTXTDocument(padId, req.params.rev); res.send(txt); } else { // render the html document @@ -79,17 +79,17 @@ async function doExport(req, res, padId, readOnlyId, type) { // decide what to do with the html export // if this is a html export, we can send this from here directly - if (type === "html") { + if (type === 'html') { // do any final changes the plugin might want to make - let newHTML = await hooks.aCallFirst("exportHTMLSend", html); + const newHTML = await hooks.aCallFirst('exportHTMLSend', html); if (newHTML.length) html = newHTML; res.send(html); - throw "stop"; + throw 'stop'; } // else write the html export to a file - let randNum = Math.floor(Math.random()*0xFFFFFFFF); - let srcFile = tempDirectory + "/etherpad_export_" + randNum + ".html"; + const randNum = Math.floor(Math.random() * 0xFFFFFFFF); + const srcFile = `${tempDirectory}/etherpad_export_${randNum}.html`; await fsp_writeFile(srcFile, html); // Tidy up the exported HTML @@ -98,42 +98,42 @@ async function doExport(req, res, padId, readOnlyId, type) { await TidyHtml.tidy(srcFile); // send the convert job to the convertor (abiword, libreoffice, ..) - let destFile = tempDirectory + "/etherpad_export_" + randNum + "." + type; + const destFile = `${tempDirectory}/etherpad_export_${randNum}.${type}`; // Allow plugins to overwrite the convert in export process - let result = await hooks.aCallAll("exportConvert", { srcFile, destFile, req, res }); + 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) => { - convertor.convertFile(srcFile, destFile, type, function(err) { - err ? reject("convertFailed") : resolve(); + convertor.convertFile(srcFile, destFile, type, (err) => { + err ? reject('convertFailed') : resolve(); }); }); } // send the file - let sendFile = util.promisify(res.sendFile); + const sendFile = util.promisify(res.sendFile); await res.sendFile(destFile, null); // clean up temporary files await fsp_unlink(srcFile); // 100ms delay to accommodate for slow windows fs - if (os.type().indexOf("Windows") > -1) { - await new Promise(resolve => setTimeout(resolve, 100)); + if (os.type().indexOf('Windows') > -1) { + await new Promise((resolve) => setTimeout(resolve, 100)); } await fsp_unlink(destFile); } } -exports.doExport = function(req, res, padId, readOnlyId, type) { - doExport(req, res, padId, readOnlyId, type).catch(err => { - if (err !== "stop") { +exports.doExport = function (req, res, padId, readOnlyId, type) { + doExport(req, res, padId, readOnlyId, type).catch((err) => { + if (err !== 'stop') { throw err; } }); -} +}; diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index c7af6a99b..719cb9f88 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -20,36 +20,36 @@ * limitations under the License. */ -var padManager = require("../db/PadManager") - , padMessageHandler = require("./PadMessageHandler") - , fs = require("fs") - , path = require("path") - , settings = require('../utils/Settings') - , formidable = require('formidable') - , os = require("os") - , importHtml = require("../utils/ImportHtml") - , importEtherpad = require("../utils/ImportEtherpad") - , log4js = require("log4js") - , hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js") - , util = require("util"); +const padManager = require('../db/PadManager'); +const padMessageHandler = require('./PadMessageHandler'); +const fs = require('fs'); +const path = require('path'); +const settings = require('../utils/Settings'); +const formidable = require('formidable'); +const os = require('os'); +const importHtml = require('../utils/ImportHtml'); +const importEtherpad = require('../utils/ImportEtherpad'); +const log4js = require('log4js'); +const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks.js'); +const util = require('util'); -let fsp_exists = util.promisify(fs.exists); -let fsp_rename = util.promisify(fs.rename); -let fsp_readFile = util.promisify(fs.readFile); -let fsp_unlink = util.promisify(fs.unlink) +const fsp_exists = util.promisify(fs.exists); +const fsp_rename = util.promisify(fs.rename); +const fsp_readFile = util.promisify(fs.readFile); +const fsp_unlink = util.promisify(fs.unlink); let convertor = null; -let exportExtension = "htm"; +let exportExtension = 'htm'; // load abiword only if it is enabled and if soffice is disabled if (settings.abiword != null && settings.soffice === null) { - convertor = require("../utils/Abiword"); + convertor = require('../utils/Abiword'); } // load soffice only if it is enabled if (settings.soffice != null) { - convertor = require("../utils/LibreOffice"); - exportExtension = "html"; + convertor = require('../utils/LibreOffice'); + exportExtension = 'html'; } const tmpDirectory = os.tmpdir(); @@ -58,28 +58,28 @@ const tmpDirectory = os.tmpdir(); * do a requested import */ async function doImport(req, res, padId) { - var apiLogger = log4js.getLogger("ImportHandler"); + const apiLogger = log4js.getLogger('ImportHandler'); // pipe to a file // convert file to html via abiword or soffice // set html in the pad - var randNum = Math.floor(Math.random()*0xFFFFFFFF); + const randNum = Math.floor(Math.random() * 0xFFFFFFFF); // setting flag for whether to use convertor or not let useConvertor = (convertor != null); - let form = new formidable.IncomingForm(); + const form = new formidable.IncomingForm(); form.keepExtensions = true; form.uploadDir = tmpDirectory; form.maxFileSize = settings.importMaxFileSize; - + // Ref: https://github.com/node-formidable/formidable/issues/469 // Crash in Etherpad was Uploading Error: Error: Request aborted // [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed - form.onPart = part => { + form.onPart = (part) => { form.handlePart(part); if (part.filename !== undefined) { - form.openedFiles[form.openedFiles.length - 1]._writeStream.on('error', err => { + form.openedFiles[form.openedFiles.length - 1]._writeStream.on('error', (err) => { form.emit('error', err); }); } @@ -87,23 +87,23 @@ async function doImport(req, res, padId) { // locally wrapped Promise, since form.parse requires a callback let srcFile = await new Promise((resolve, reject) => { - form.parse(req, function(err, fields, files) { + form.parse(req, (err, fields, files) => { if (err || files.file === undefined) { // the upload failed, stop at this point if (err) { - console.warn("Uploading Error: " + err.stack); + console.warn(`Uploading Error: ${err.stack}`); } // I hate doing indexOf here but I can't see anything to use... - if (err && err.stack && err.stack.indexOf("maxFileSize") !== -1) { - reject("maxFileSize"); + if (err && err.stack && err.stack.indexOf('maxFileSize') !== -1) { + reject('maxFileSize'); } - reject("uploadFailed"); + reject('uploadFailed'); } - if(!files.file){ // might not be a graceful fix but it works - reject("uploadFailed"); - }else{ + if (!files.file) { // might not be a graceful fix but it works + reject('uploadFailed'); + } else { resolve(files.file.path); } }); @@ -111,47 +111,47 @@ 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 - let fileEnding = path.extname(srcFile).toLowerCase() - , knownFileEndings = [".txt", ".doc", ".docx", ".pdf", ".odt", ".html", ".htm", ".etherpad", ".rtf"] - , fileEndingUnknown = (knownFileEndings.indexOf(fileEnding) < 0); + const fileEnding = path.extname(srcFile).toLowerCase(); + const knownFileEndings = ['.txt', '.doc', '.docx', '.pdf', '.odt', '.html', '.htm', '.etherpad', '.rtf']; + const fileEndingUnknown = (knownFileEndings.indexOf(fileEnding) < 0); if (fileEndingUnknown) { // the file ending is not known if (settings.allowUnknownFileEnds === true) { // we need to rename this file with a .txt ending - let oldSrcFile = srcFile; + const oldSrcFile = srcFile; - srcFile = path.join(path.dirname(srcFile), path.basename(srcFile, fileEnding) + ".txt"); + srcFile = path.join(path.dirname(srcFile), `${path.basename(srcFile, fileEnding)}.txt`); await fsp_rename(oldSrcFile, srcFile); } else { - console.warn("Not allowing unknown file type to be imported", fileEnding); - throw "uploadFailed"; + console.warn('Not allowing unknown file type to be imported', fileEnding); + throw 'uploadFailed'; } } - let destFile = path.join(tmpDirectory, "etherpad_import_" + randNum + "." + exportExtension); + const destFile = path.join(tmpDirectory, `etherpad_import_${randNum}.${exportExtension}`); // Logic for allowing external Import Plugins - let result = await hooks.aCallAll("import", { srcFile, destFile, fileEnding }); - let importHandledByPlugin = (result.length > 0); // This feels hacky and wrong.. + const result = await hooks.aCallAll('import', {srcFile, destFile, fileEnding}); + const importHandledByPlugin = (result.length > 0); // This feels hacky and wrong.. - let fileIsEtherpad = (fileEnding === ".etherpad"); - let fileIsHTML = (fileEnding === ".html" || fileEnding === ".htm"); - let fileIsTXT = (fileEnding === ".txt"); + const fileIsEtherpad = (fileEnding === '.etherpad'); + const fileIsHTML = (fileEnding === '.html' || fileEnding === '.htm'); + const fileIsTXT = (fileEnding === '.txt'); if (fileIsEtherpad) { // we do this here so we can see if the pad has quite a few edits - let _pad = await padManager.getPad(padId); - let headCount = _pad.head; + const _pad = await padManager.getPad(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"); - throw "padHasData"; + throw 'padHasData'; } const fsp_readFile = util.promisify(fs.readFile); - let _text = await fsp_readFile(srcFile, "utf8"); + const _text = await fsp_readFile(srcFile, 'utf8'); req.directDatabaseAccess = true; await importEtherpad.setPadRaw(padId, _text); } @@ -170,11 +170,11 @@ async function doImport(req, res, padId) { } else { // @TODO - no Promise interface for convertors (yet) await new Promise((resolve, reject) => { - convertor.convertFile(srcFile, destFile, exportExtension, function(err) { + convertor.convertFile(srcFile, destFile, exportExtension, (err) => { // catch convert errors if (err) { - console.warn("Converting Error:", err); - reject("convertFailed"); + console.warn('Converting Error:', err); + reject('convertFailed'); } resolve(); }); @@ -184,13 +184,13 @@ async function doImport(req, res, padId) { if (!useConvertor && !req.directDatabaseAccess) { // Read the file with no encoding for raw buffer access. - let buf = await fsp_readFile(destFile); + const buf = await fsp_readFile(destFile); // Check if there are only ascii chars in the uploaded file - let isAscii = ! Array.prototype.some.call(buf, c => (c > 240)); + const isAscii = !Array.prototype.some.call(buf, (c) => (c > 240)); if (!isAscii) { - throw "uploadFailed"; + throw 'uploadFailed'; } } @@ -201,7 +201,7 @@ async function doImport(req, res, padId) { let text; if (!req.directDatabaseAccess) { - text = await fsp_readFile(destFile, "utf8"); + text = await fsp_readFile(destFile, 'utf8'); /* * The tag needs to be stripped out, otherwise it is appended to the @@ -211,13 +211,13 @@ async function doImport(req, res, padId) { * added to the <title> tag. This is a quick & dirty way of matching the * title and comment it out independently on the classes that are set on it. */ - text = text.replace("<title", "<!-- <title"); - text = text.replace("","-->"); + text = text.replace('', '-->'); // node on windows has a delay on releasing of the file lock. // We add a 100ms delay to work around this - if (os.type().indexOf("Windows") > -1){ - await new Promise(resolve => setTimeout(resolve, 100)); + if (os.type().indexOf('Windows') > -1) { + await new Promise((resolve) => setTimeout(resolve, 100)); } } @@ -227,7 +227,7 @@ async function doImport(req, res, padId) { try { await importHtml.setPadHTML(pad, text); } catch (e) { - apiLogger.warn("Error importing, possibly caused by malformed HTML"); + apiLogger.warn('Error importing, possibly caused by malformed HTML'); } } else { await pad.setText(text); @@ -274,16 +274,16 @@ exports.doImport = function (req, res, padId) { * the function above there's no other way to return * a value to the caller. */ - let status = "ok"; - doImport(req, res, padId).catch(err => { + 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; } }).then(() => { // close the connection - res.send(""); + res.send(``); }); -} +}; diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index c3e3ac272..214571044 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -20,30 +20,30 @@ /* global exports, process, require */ -var padManager = require("../db/PadManager"); -var Changeset = require("ep_etherpad-lite/static/js/Changeset"); -var AttributePool = require("ep_etherpad-lite/static/js/AttributePool"); -var AttributeManager = require("ep_etherpad-lite/static/js/AttributeManager"); -var authorManager = require("../db/AuthorManager"); -var readOnlyManager = require("../db/ReadOnlyManager"); -var settings = require('../utils/Settings'); -var securityManager = require("../db/SecurityManager"); -var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugin_defs.js"); -var log4js = require('log4js'); -var messageLogger = log4js.getLogger("message"); -var accessLogger = log4js.getLogger("access"); -var _ = require('underscore'); -var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); -var channels = require("channels"); -var stats = require('../stats'); +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 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 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 channels = require('channels'); +const stats = require('../stats'); const assert = require('assert').strict; -const nodeify = require("nodeify"); -const { RateLimiterMemory } = require('rate-limiter-flexible'); +const nodeify = require('nodeify'); +const {RateLimiterMemory} = require('rate-limiter-flexible'); const webaccess = require('../hooks/express/webaccess'); const rateLimiter = new RateLimiterMemory({ points: settings.commitRateLimiting.points, - duration: settings.commitRateLimiting.duration + duration: settings.commitRateLimiting.duration, }); /** @@ -56,20 +56,16 @@ const rateLimiter = new RateLimiterMemory({ * rev = That last revision that was send to this client * author = the author ID used for this session */ -var sessioninfos = {}; +const sessioninfos = {}; exports.sessioninfos = sessioninfos; // Measure total amount of users -stats.gauge('totalUsers', function() { - return Object.keys(socketio.sockets.sockets).length; -}); +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) => { - return 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 @@ -80,9 +76,9 @@ 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) { - socketio=socket_io; -} +exports.setSocketIO = function (socket_io) { + socketio = socket_io; +}; /** * Handles the connection of a new user @@ -98,17 +94,15 @@ exports.handleConnect = (socket) => { /** * Kicks all sessions from a pad */ -exports.kickSessionsFromPad = function(padID) { - if(typeof socketio.sockets['clients'] !== 'function') - return; +exports.kickSessionsFromPad = function (padID) { + if (typeof socketio.sockets.clients !== 'function') return; // skip if there is nobody on this pad - if(_getRoomSockets(padID).length === 0) - return; + if (_getRoomSockets(padID).length === 0) return; // disconnect everyone from this pad - socketio.sockets.in(padID).json.send({disconnect:"deleted"}); -} + socketio.sockets.in(padID).json.send({disconnect: 'deleted'}); +}; /** * Handles the disconnection of a user @@ -123,35 +117,35 @@ exports.handleDisconnect = async (socket) => { // 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]' + + accessLogger.info(`${'[LEAVE]' + ` pad:${session.padId}` + ` socket:${socket.id}` + ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` + - ` authorID:${session.author}` + - ((user && user.username) ? ` username:${user.username}` : '')); + ` authorID:${session.author}`}${ + (user && user.username) ? ` username:${user.username}` : ''}`); // get the author color out of the db - let color = await authorManager.getAuthorColorId(session.author); + const color = await authorManager.getAuthorColorId(session.author); // prepare the notification for the other users on the pad, that this user left - let messageToTheOtherUsers = { - "type": "COLLABROOM", - "data": { - type: "USER_LEAVE", + const messageToTheOtherUsers = { + type: 'COLLABROOM', + data: { + type: 'USER_LEAVE', userInfo: { - "ip": "127.0.0.1", - "colorId": color, - "userAgent": "Anonymous", - "userId": session.author - } - } + ip: '127.0.0.1', + colorId: color, + userAgent: 'Anonymous', + userId: session.author, + }, + }, }; // Go through all user that are still on the pad, and send them the USER_LEAVE message socket.broadcast.to(session.padId).json.send(messageToTheOtherUsers); // Allow plugins to hook into users leaving the pad - hooks.callAll("userLeave", session); + hooks.callAll('userLeave', session); } // Delete the sessioninfos entrys of this session @@ -164,7 +158,7 @@ exports.handleDisconnect = async (socket) => { * @param message the message from the client */ exports.handleMessage = async (socket, message) => { - var env = process.env.NODE_ENV || 'development'; + const env = process.env.NODE_ENV || 'development'; if (env === 'production') { try { @@ -189,25 +183,25 @@ exports.handleMessage = async (socket, message) => { const thisSession = sessioninfos[socket.id]; if (!thisSession) { - messageLogger.warn("Dropped message from an unknown connection.") + messageLogger.warn('Dropped message from an unknown connection.'); return; } - if (message.type === "CLIENT_READY") { + if (message.type === 'CLIENT_READY') { // client tried to auth for the first time (first msg from the client) createSessionInfoAuth(thisSession, 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; } // check if pad is requested via readOnly let padId = auth.padID; - if (padId.indexOf("r.") === 0) { + if (padId.indexOf('r.') === 0) { // Pad is readOnly, first get the real Pad ID padId = await readOnlyManager.getPadId(padId); } @@ -222,14 +216,14 @@ exports.handleMessage = async (socket, message) => { } if (thisSession.author != null && thisSession.author !== authorID) { messageLogger.warn( - 'Rejecting message from client because the author ID changed mid-session.' + + `${'Rejecting message from client because the author ID changed mid-session.' + ' Bad or missing token or sessionID?' + ` socket:${socket.id}` + ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` + ` originalAuthorID:${thisSession.author}` + - ` newAuthorID:${authorID}` + - ((user && user.username) ? ` username:${user.username}` : '') + - ` message:${message}`); + ` newAuthorID:${authorID}`}${ + (user && user.username) ? ` username:${user.username}` : '' + } message:${message}`); socket.json.send({disconnect: 'rejected'}); return; } @@ -248,42 +242,42 @@ exports.handleMessage = async (socket, message) => { // Drop the message if the client disconnected during the above processing. if (sessioninfos[socket.id] !== thisSession) { - messageLogger.warn('Dropping message from a connection that has gone away.') + messageLogger.warn('Dropping message from a connection that has gone away.'); return; } // Check what type of message we get and delegate to the other methods - if (message.type === "CLIENT_READY") { + if (message.type === 'CLIENT_READY') { await handleClientReady(socket, message, authorID); - } else if (message.type === "CHANGESET_REQ") { + } else if (message.type === 'CHANGESET_REQ') { await handleChangesetRequest(socket, message); - } else if(message.type === "COLLABROOM") { + } else if (message.type === 'COLLABROOM') { if (thisSession.readonly) { - messageLogger.warn("Dropped message, COLLABROOM for readonly pad"); - } else if (message.data.type === "USER_CHANGES") { - stats.counter('pendingEdits').inc() + messageLogger.warn('Dropped message, COLLABROOM for readonly pad'); + } else if (message.data.type === 'USER_CHANGES') { + stats.counter('pendingEdits').inc(); padChannels.emit(message.padId, {socket, message}); // add to pad queue - } else if (message.data.type === "USERINFO_UPDATE") { + } else if (message.data.type === 'USERINFO_UPDATE') { await handleUserInfoUpdate(socket, message); - } else if (message.data.type === "CHAT_MESSAGE") { + } else if (message.data.type === 'CHAT_MESSAGE') { await handleChatMessage(socket, message); - } else if (message.data.type === "GET_CHAT_MESSAGES") { + } else if (message.data.type === 'GET_CHAT_MESSAGES') { await handleGetChatMessages(socket, message); - } else if (message.data.type === "SAVE_REVISION") { + } else if (message.data.type === 'SAVE_REVISION') { await handleSaveRevisionMessage(socket, message); - } else if (message.data.type === "CLIENT_MESSAGE" && + } else if (message.data.type === 'CLIENT_MESSAGE' && message.data.payload != null && - message.data.payload.type === "suggestUserName") { + message.data.payload.type === 'suggestUserName') { handleSuggestUserName(socket, message); } else { - messageLogger.warn("Dropped message, unknown COLLABROOM Data Type " + message.data.type); + messageLogger.warn(`Dropped message, unknown COLLABROOM Data Type ${message.data.type}`); } - } else if(message.type === "SWITCH_TO_PAD") { + } else if (message.type === 'SWITCH_TO_PAD') { await handleSwitchToPad(socket, message, authorID); } else { - messageLogger.warn("Dropped message, unknown Message Type " + message.type); + messageLogger.warn(`Dropped message, unknown Message Type ${message.type}`); } -} +}; /** @@ -304,9 +298,9 @@ 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) { - if (msg.data.type === "CUSTOM") { - if (sessionID){ +exports.handleCustomObjectMessage = function (msg, sessionID) { + if (msg.data.type === 'CUSTOM') { + if (sessionID) { // a sessionID is targeted: directly to this sessionID socketio.sockets.socket(sessionID).json.send(msg); } else { @@ -314,7 +308,7 @@ exports.handleCustomObjectMessage = function(msg, sessionID) { socketio.sockets.in(msg.data.payload.padId).json.send(msg); } } -} +}; /** * Handles a custom message (sent via HTTP API request) @@ -322,17 +316,17 @@ 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) { - let time = Date.now(); - let msg = { +exports.handleCustomMessage = function (padID, msgString) { + const time = Date.now(); + const msg = { type: 'COLLABROOM', data: { type: msgString, - time: time - } + time, + }, }; socketio.sockets.in(padID).json.send(msg); -} +}; /** * Handles a Chat Message @@ -340,8 +334,8 @@ exports.handleCustomMessage = function(padID, msgString) { * @param message the message from the client */ async function handleChatMessage(socket, message) { - var time = Date.now(); - var text = message.data.text; + const time = Date.now(); + const text = message.data.text; const {padId, author: authorId} = sessioninfos[socket.id]; await exports.sendChatMessageToPadClients(time, authorId, text, padId); } @@ -353,26 +347,26 @@ 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 function (time, userId, text, padId) { // get the pad - let pad = await padManager.getPad(padId); + const pad = await padManager.getPad(padId); // get the author - let userName = await authorManager.getAuthorName(userId); + const userName = await authorManager.getAuthorName(userId); // save the chat message const promise = pad.appendChatMessage(text, userId, time); - let msg = { - type: "COLLABROOM", - data: { type: "CHAT_MESSAGE", userId, userName, time, text } + const msg = { + type: 'COLLABROOM', + data: {type: 'CHAT_MESSAGE', userId, userName, time, text}, }; // broadcast the chat message to everyone on the pad socketio.sockets.in(padId).json.send(msg); await promise; -} +}; /** * Handles the clients request for more chat-messages @@ -381,34 +375,34 @@ exports.sendChatMessageToPadClients = async function(time, userId, text, padId) */ async function handleGetChatMessages(socket, message) { if (message.data.start == null) { - messageLogger.warn("Dropped message, GetChatMessages Message has no start!"); + messageLogger.warn('Dropped message, GetChatMessages Message has no start!'); return; } if (message.data.end == null) { - messageLogger.warn("Dropped message, GetChatMessages Message has no start!"); + messageLogger.warn('Dropped message, GetChatMessages Message has no start!'); return; } - let start = message.data.start; - let end = message.data.end; - let count = end - start; + const start = message.data.start; + const end = message.data.end; + 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; } const padId = sessioninfos[socket.id].padId; - let pad = await padManager.getPad(padId); + const pad = await padManager.getPad(padId); - let chatMessages = await pad.getChatMessages(start, end); - let infoMsg = { - type: "COLLABROOM", + const chatMessages = await pad.getChatMessages(start, end); + const infoMsg = { + type: 'COLLABROOM', data: { - type: "CHAT_MESSAGES", - messages: chatMessages - } + type: 'CHAT_MESSAGES', + messages: chatMessages, + }, }; // send the messages back to the client @@ -423,12 +417,12 @@ async function handleGetChatMessages(socket, message) { function handleSuggestUserName(socket, message) { // check if all ok if (message.data.payload.newName == null) { - messageLogger.warn("Dropped message, suggestUserName Message has no newName!"); + messageLogger.warn('Dropped message, suggestUserName Message has no newName!'); return; } if (message.data.payload.unnamedId == null) { - messageLogger.warn("Dropped message, suggestUserName Message has no unnamedId!"); + messageLogger.warn('Dropped message, suggestUserName Message has no unnamedId!'); return; } @@ -451,29 +445,29 @@ function handleSuggestUserName(socket, message) { async function handleUserInfoUpdate(socket, message) { // check if all ok if (message.data.userInfo == null) { - messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no userInfo!"); + messageLogger.warn('Dropped message, USERINFO_UPDATE Message has no userInfo!'); return; } if (message.data.userInfo.colorId == null) { - messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no colorId!"); + messageLogger.warn('Dropped message, USERINFO_UPDATE Message has no colorId!'); return; } // Check that we have a valid session and author to update. const session = sessioninfos[socket.id]; if (!session || !session.author || !session.padId) { - messageLogger.warn("Dropped message, USERINFO_UPDATE Session not ready." + message.data); + messageLogger.warn(`Dropped message, USERINFO_UPDATE Session not ready.${message.data}`); return; } // Find out the author name of this session - var author = session.author; + const author = session.author; // Check colorId is a Hex color - var isColor = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(message.data.userInfo.colorId) // for #f00 (Thanks Smamatti) + const isColor = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(message.data.userInfo.colorId); // for #f00 (Thanks Smamatti) if (!isColor) { - messageLogger.warn("Dropped message, USERINFO_UPDATE Color is malformed." + message.data); + messageLogger.warn(`Dropped message, USERINFO_UPDATE Color is malformed.${message.data}`); return; } @@ -483,22 +477,22 @@ async function handleUserInfoUpdate(socket, message) { authorManager.setAuthorName(author, message.data.userInfo.name), ]); - var padId = session.padId; + const padId = session.padId; - var infoMsg = { - type: "COLLABROOM", + const infoMsg = { + type: 'COLLABROOM', data: { // The Client doesn't know about USERINFO_UPDATE, use USER_NEWINFO - type: "USER_NEWINFO", + type: 'USER_NEWINFO', userInfo: { userId: author, // set a null name, when there is no name set. cause the client wants it null name: message.data.userInfo.name || null, colorId: message.data.userInfo.colorId, - userAgent: "Anonymous", - ip: "127.0.0.1", - } - } + userAgent: 'Anonymous', + ip: '127.0.0.1', + }, + }, }; // Send the other clients on the pad the update message @@ -524,21 +518,21 @@ async function handleUserInfoUpdate(socket, message) { */ async function handleUserChanges(socket, message) { // This one's no longer pending, as we're gonna process it now - stats.counter('pendingEdits').dec() + stats.counter('pendingEdits').dec(); // Make sure all required fields are present if (message.data.baseRev == null) { - messageLogger.warn("Dropped message, USER_CHANGES Message has no baseRev!"); + messageLogger.warn('Dropped message, USER_CHANGES Message has no baseRev!'); return; } if (message.data.apool == null) { - messageLogger.warn("Dropped message, USER_CHANGES Message has no apool!"); + messageLogger.warn('Dropped message, USER_CHANGES Message has no apool!'); return; } if (message.data.changeset == null) { - messageLogger.warn("Dropped message, USER_CHANGES Message has no changeset!"); + messageLogger.warn('Dropped message, USER_CHANGES Message has no changeset!'); return; } @@ -550,20 +544,20 @@ async function handleUserChanges(socket, message) { // and always use the copy. atm a message will be ignored if the session is gone even // if the session was valid when the message arrived in the first place if (!thisSession) { - messageLogger.warn("Dropped message, disconnect happened in the mean time"); + messageLogger.warn('Dropped message, disconnect happened in the mean time'); return; } // get all Vars we need - var baseRev = message.data.baseRev; - var wireApool = (new AttributePool()).fromJsonable(message.data.apool); - var changeset = message.data.changeset; + const baseRev = message.data.baseRev; + const wireApool = (new AttributePool()).fromJsonable(message.data.apool); + let changeset = message.data.changeset; // Measure time to process edit - var stopWatch = stats.timer('edits').start(); + const stopWatch = stats.timer('edits').start(); // get the pad - let pad = await padManager.getPad(thisSession.padId); + const pad = await padManager.getPad(thisSession.padId); // create the changeset try { @@ -573,24 +567,24 @@ async function handleUserChanges(socket, message) { // Verify that the attribute indexes used in the changeset are all // defined in the accompanying attribute pool. - Changeset.eachAttribNumber(changeset, function(n) { + Changeset.eachAttribNumber(changeset, (n) => { if (!wireApool.getAttrib(n)) { - throw new Error("Attribute pool is missing attribute " + n + " for changeset " + changeset); + throw new Error(`Attribute pool is missing attribute ${n} for changeset ${changeset}`); } }); // Validate all added 'author' attribs to be the same value as the current user - var iterator = Changeset.opIterator(Changeset.unpack(changeset).ops) - , op; + const iterator = Changeset.opIterator(Changeset.unpack(changeset).ops); + let op; while (iterator.hasNext()) { - op = iterator.next() + op = iterator.next(); // + 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 - op.attribs.split('*').forEach(function(attr) { + op.attribs.split('*').forEach((attr) => { if (!attr) return; attr = wireApool.getAttrib(attr); @@ -607,8 +601,7 @@ async function handleUserChanges(socket, message) { // Afaik, it copies the new attributes from the changeset, to the global Attribute Pool changeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool); - - } catch(e) { + } catch (e) { // There is an error in this changeset, so just refuse it socket.json.send({disconnect: 'badChangeset'}); stats.meter('failedChangesets').mark(); @@ -616,7 +609,7 @@ async function handleUserChanges(socket, message) { } // ex. applyUserChanges - let apool = pad.pool; + const apool = pad.pool; let r = baseRev; // The client's changeset might not be based on the latest revision, @@ -625,7 +618,7 @@ async function handleUserChanges(socket, message) { while (r < pad.getHeadRevisionNumber()) { r++; - let c = await pad.getRevisionChangeset(r); + const c = await pad.getRevisionChangeset(r); // At this point, both "c" (from the pad) and "changeset" (from the // client) are relative to revision r - 1. The follow function @@ -643,37 +636,37 @@ async function handleUserChanges(socket, message) { } changeset = Changeset.follow(c, changeset, false, apool); - } catch(e) { + } catch (e) { socket.json.send({disconnect: 'badChangeset'}); stats.meter('failedChangesets').mark(); - throw new Error("Can't apply USER_CHANGES, because " + e.message); + throw new Error(`Can't apply USER_CHANGES, because ${e.message}`); } } - let prevText = pad.text(); + const prevText = pad.text(); if (Changeset.oldLen(changeset) !== prevText.length) { socket.json.send({disconnect: 'badChangeset'}); stats.meter('failedChangesets').mark(); - throw new Error("Can't apply USER_CHANGES "+changeset+" with oldLen " + Changeset.oldLen(changeset) + " to document of length " + prevText.length); + throw new Error(`Can't apply USER_CHANGES ${changeset} with oldLen ${Changeset.oldLen(changeset)} to document of length ${prevText.length}`); } try { await pad.appendRevision(changeset, thisSession.author); - } catch(e) { + } catch (e) { socket.json.send({disconnect: 'badChangeset'}); stats.meter('failedChangesets').mark(); throw e; } - let correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool); + const correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool); if (correctionChangeset) { await pad.appendRevision(correctionChangeset); } // Make sure the pad always ends with an empty line. - if (pad.text().lastIndexOf("\n") !== pad.text().length-1) { - var nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length - 1, 0, "\n"); + if (pad.text().lastIndexOf('\n') !== pad.text().length - 1) { + const nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length - 1, 0, '\n'); await pad.appendRevision(nlChangeset); } @@ -685,7 +678,7 @@ async function handleUserChanges(socket, message) { stopWatch.end(); } -exports.updatePadClients = async function(pad) { +exports.updatePadClients = async function (pad) { // skip this if no-one is on this pad const roomSockets = _getRoomSockets(pad.id); if (roomSockets.length === 0) return; @@ -697,7 +690,7 @@ exports.updatePadClients = async function(pad) { // 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 - let revCache = {}; + const revCache = {}; // go through all sessions on this pad for (const socket of roomSockets) { @@ -705,16 +698,16 @@ exports.updatePadClients = async function(pad) { // send them all new changesets while (sessioninfos[sid] && sessioninfos[sid].rev < pad.getHeadRevisionNumber()) { - let r = sessioninfos[sid].rev + 1; + const r = sessioninfos[sid].rev + 1; let revision = revCache[r]; if (!revision) { revision = await pad.getRevision(r); revCache[r] = revision; } - let author = revision.meta.author, - revChangeset = revision.changeset, - currentTime = revision.meta.timestamp; + const author = revision.meta.author; + const revChangeset = revision.changeset; + const currentTime = revision.meta.timestamp; // next if session has not been deleted if (sessioninfos[sid] == null) { @@ -724,16 +717,15 @@ exports.updatePadClients = async function(pad) { if (author === sessioninfos[sid].author) { socket.json.send({type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev: r}}); } else { - let forWire = Changeset.prepareForWire(revChangeset, pad.pool); - let wireMsg = {"type": "COLLABROOM", - "data": { type:"NEW_CHANGES", - newRev:r, - changeset: forWire.translated, - apool: forWire.pool, - author: author, - currentTime: currentTime, - timeDelta: currentTime - sessioninfos[sid].time - }}; + const forWire = Changeset.prepareForWire(revChangeset, pad.pool); + const wireMsg = {type: 'COLLABROOM', + data: {type: 'NEW_CHANGES', + newRev: r, + changeset: forWire.translated, + apool: forWire.pool, + author, + currentTime, + timeDelta: currentTime - sessioninfos[sid].time}}; socket.json.send(wireMsg); } @@ -744,29 +736,27 @@ exports.updatePadClients = async function(pad) { } } } -} +}; /** * Copied from the Etherpad Source Code. Don't know what this method does excatly... */ function _correctMarkersInPad(atext, apool) { - var text = atext.text; + const text = atext.text; // collect char positions of line markers (e.g. bullets) in new atext // that aren't at the start of a line - var badMarkers = []; - var iter = Changeset.opIterator(atext.attribs); - var offset = 0; + const badMarkers = []; + const iter = Changeset.opIterator(atext.attribs); + let offset = 0; while (iter.hasNext()) { var op = iter.next(); - var hasMarker = _.find(AttributeManager.lineAttributes, function(attribute) { - return Changeset.opAttributeValue(op, attribute, apool); - }) !== undefined; + const hasMarker = _.find(AttributeManager.lineAttributes, (attribute) => Changeset.opAttributeValue(op, attribute, apool)) !== undefined; if (hasMarker) { - for (var i = 0; i < op.chars; i++) { - if (offset > 0 && text.charAt(offset-1) !== '\n') { + for (let i = 0; i < op.chars; i++) { + if (offset > 0 && text.charAt(offset - 1) !== '\n') { badMarkers.push(offset); } offset++; @@ -783,12 +773,12 @@ function _correctMarkersInPad(atext, apool) { // create changeset that removes these bad markers offset = 0; - var builder = Changeset.builder(text.length); + const builder = Changeset.builder(text.length); - badMarkers.forEach(function(pos) { + badMarkers.forEach((pos) => { builder.keepText(text.substring(offset, pos)); builder.remove(1); - offset = pos+1; + offset = pos + 1; }); return builder.toString(); @@ -855,56 +845,54 @@ function createSessionInfoAuth(sessionInfo, message) { async function handleClientReady(socket, message, authorID) { // check if all ok if (!message.token) { - messageLogger.warn("Dropped message, CLIENT_READY Message has no token!"); + messageLogger.warn('Dropped message, CLIENT_READY Message has no token!'); return; } if (!message.padId) { - messageLogger.warn("Dropped message, CLIENT_READY Message has no padId!"); + messageLogger.warn('Dropped message, CLIENT_READY Message has no padId!'); return; } if (!message.protocolVersion) { - messageLogger.warn("Dropped message, CLIENT_READY Message has no protocolVersion!"); + messageLogger.warn('Dropped message, CLIENT_READY Message has no protocolVersion!'); return; } if (message.protocolVersion !== 2) { - messageLogger.warn("Dropped message, CLIENT_READY Message has a unknown protocolVersion '" + message.protocolVersion + "'!"); + messageLogger.warn(`Dropped message, CLIENT_READY Message has a unknown protocolVersion '${message.protocolVersion}'!`); return; } - hooks.callAll("clientReady", message); + hooks.callAll('clientReady', message); // Get ro/rw id:s - let padIds = await readOnlyManager.getIds(message.padId); + const padIds = await readOnlyManager.getIds(message.padId); // get all authordata of this new user assert(authorID); - let value = await authorManager.getAuthor(authorID); - let authorColorId = value.colorId; - let authorName = value.name; + const value = await authorManager.getAuthor(authorID); + const authorColorId = value.colorId; + const authorName = value.name; // load the pad-object from the database - let pad = await padManager.getPad(padIds.padId); + const pad = await padManager.getPad(padIds.padId); // these db requests all need the pad object (timestamp of latest revision, author data) - let authors = pad.getAllAuthors(); + const authors = pad.getAllAuthors(); // get timestamp of latest revision needed for timeslider - let currentTime = await pad.getRevisionDate(pad.getHeadRevisionNumber()); + const currentTime = await pad.getRevisionDate(pad.getHeadRevisionNumber()); // get all author data out of the database (in parallel) - let historicalAuthorData = {}; - await Promise.all(authors.map(authorId => { - return 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"); - } else { - historicalAuthorData[authorId] = { name: author.name, colorId: author.colorId }; // Filter author attribs (e.g. don't send author's pads to all clients) - } - }); - })); + 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'); + } else { + historicalAuthorData[authorId] = {name: author.name, colorId: author.colorId}; // Filter author attribs (e.g. don't send author's pads to all clients) + } + }))); // glue the clientVars together, send them and tell the other clients that a new one is there @@ -932,12 +920,12 @@ async function handleClientReady(socket, message, authorID) { padIds.readonly || !webaccess.userCanModify(message.padId, socket.client.request); const {session: {user} = {}} = socket.client.request; - accessLogger.info(`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` + + accessLogger.info(`${`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` + ` pad:${padIds.padId}` + ` socket:${socket.id}` + ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` + - ` authorID:${authorID}` + - ((user && user.username) ? ` username:${user.username}` : '')); + ` authorID:${authorID}`}${ + (user && user.username) ? ` username:${user.username}` : ''}`); if (message.reconnect) { // If this is a reconnect, we don't have to send the client the ClientVars again @@ -949,13 +937,13 @@ async function handleClientReady(socket, message, authorID) { // 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 - var revisionsNeeded = []; - var changesets = {}; + const revisionsNeeded = []; + const changesets = {}; - var startNum = message.client_rev + 1; - var endNum = pad.getHeadRevisionNumber() + 1; + let startNum = message.client_rev + 1; + let endNum = pad.getHeadRevisionNumber() + 1; - var headNum = pad.getHeadRevisionNumber(); + const headNum = pad.getHeadRevisionNumber(); if (endNum > headNum + 1) { endNum = headNum + 1; @@ -971,51 +959,47 @@ async function handleClientReady(socket, message, authorID) { } // get changesets, author and timestamp needed for pending revisions (in parallel) - let promises = []; - for (let revNum of revisionsNeeded) { - let cs = changesets[revNum]; - promises.push( pad.getRevisionChangeset(revNum).then(result => cs.changeset = result )); - promises.push( pad.getRevisionAuthor(revNum).then(result => cs.author = result )); - promises.push( pad.getRevisionDate(revNum).then(result => cs.timestamp = result )); + const promises = []; + for (const revNum of revisionsNeeded) { + const cs = changesets[revNum]; + promises.push(pad.getRevisionChangeset(revNum).then((result) => cs.changeset = result)); + promises.push(pad.getRevisionAuthor(revNum).then((result) => cs.author = result)); + promises.push(pad.getRevisionDate(revNum).then((result) => cs.timestamp = result)); } await Promise.all(promises); // return pending changesets - for (let r of revisionsNeeded) { - - let forWire = Changeset.prepareForWire(changesets[r]['changeset'], pad.pool); - let wireMsg = {"type":"COLLABROOM", - "data":{type:"CLIENT_RECONNECT", - headRev:pad.getHeadRevisionNumber(), - newRev:r, - changeset:forWire.translated, - apool: forWire.pool, - author: changesets[r]['author'], - currentTime: changesets[r]['timestamp'] - }}; + for (const r of revisionsNeeded) { + const forWire = Changeset.prepareForWire(changesets[r].changeset, pad.pool); + const wireMsg = {type: 'COLLABROOM', + data: {type: 'CLIENT_RECONNECT', + headRev: pad.getHeadRevisionNumber(), + newRev: r, + changeset: forWire.translated, + apool: forWire.pool, + author: changesets[r].author, + currentTime: changesets[r].timestamp}}; socket.json.send(wireMsg); } if (startNum === endNum) { - var Msg = {"type":"COLLABROOM", - "data":{type:"CLIENT_RECONNECT", - noChanges: true, - newRev: pad.getHeadRevisionNumber() - }}; + const Msg = {type: 'COLLABROOM', + data: {type: 'CLIENT_RECONNECT', + noChanges: true, + newRev: pad.getHeadRevisionNumber()}}; socket.json.send(Msg); } - } else { // This is a normal first connect // 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); - var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); + const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); var apool = attribsForWire.pool.toJsonable(); atext.attribs = attribsForWire.translated; - } catch(e) { - console.error(e.stack || e) + } catch (e) { + console.error(e.stack || e); socket.json.send({disconnect: 'corruptPad'}); // pull the brakes return; @@ -1024,64 +1008,64 @@ async function handleClientReady(socket, message, authorID) { // Warning: never ever send padIds.padId to the client. If the // client is read only you would open a security hole 1 swedish // mile wide... - var clientVars = { - "skinName": settings.skinName, - "skinVariants": settings.skinVariants, - "randomVersionString": settings.randomVersionString, - "accountPrivs": { - "maxRevisions": 100 + const clientVars = { + skinName: settings.skinName, + skinVariants: settings.skinVariants, + randomVersionString: settings.randomVersionString, + accountPrivs: { + maxRevisions: 100, }, - "automaticReconnectionTimeout": settings.automaticReconnectionTimeout, - "initialRevisionList": [], - "initialOptions": { - "guestPolicy": "deny" + automaticReconnectionTimeout: settings.automaticReconnectionTimeout, + initialRevisionList: [], + initialOptions: { + guestPolicy: 'deny', }, - "savedRevisions": pad.getSavedRevisions(), - "collab_client_vars": { - "initialAttributedText": atext, - "clientIp": "127.0.0.1", - "padId": message.padId, - "historicalAuthorData": historicalAuthorData, - "apool": apool, - "rev": pad.getHeadRevisionNumber(), - "time": currentTime, + savedRevisions: pad.getSavedRevisions(), + collab_client_vars: { + initialAttributedText: atext, + clientIp: '127.0.0.1', + padId: message.padId, + historicalAuthorData, + apool, + rev: pad.getHeadRevisionNumber(), + time: currentTime, }, - "colorPalette": authorManager.getColorPalette(), - "clientIp": "127.0.0.1", - "userIsGuest": true, - "userColor": authorColorId, - "padId": message.padId, - "padOptions": settings.padOptions, - "padShortcutEnabled": settings.padShortcutEnabled, - "initialTitle": "Pad: " + message.padId, - "opts": {}, + colorPalette: authorManager.getColorPalette(), + clientIp: '127.0.0.1', + userIsGuest: true, + userColor: authorColorId, + padId: message.padId, + padOptions: settings.padOptions, + padShortcutEnabled: settings.padShortcutEnabled, + initialTitle: `Pad: ${message.padId}`, + opts: {}, // tell the client the number of the latest chat-message, which will be // used to request the latest 100 chat-messages later (GET_CHAT_MESSAGES) - "chatHead": pad.chatHead, - "numConnectedUsers": roomSockets.length, - "readOnlyId": padIds.readOnlyPadId, - "readonly": sessionInfo.readonly, - "serverTimestamp": Date.now(), - "userId": authorID, - "abiwordAvailable": settings.abiwordAvailable(), - "sofficeAvailable": settings.sofficeAvailable(), - "exportAvailable": settings.exportAvailable(), - "plugins": { - "plugins": plugins.plugins, - "parts": plugins.parts, + chatHead: pad.chatHead, + numConnectedUsers: roomSockets.length, + readOnlyId: padIds.readOnlyPadId, + readonly: sessionInfo.readonly, + serverTimestamp: Date.now(), + userId: authorID, + abiwordAvailable: settings.abiwordAvailable(), + sofficeAvailable: settings.sofficeAvailable(), + exportAvailable: settings.exportAvailable(), + plugins: { + plugins: plugins.plugins, + parts: plugins.parts, }, - "indentationOnNewLine": settings.indentationOnNewLine, - "scrollWhenFocusLineIsOutOfViewport": { - "percentage" : { - "editionAboveViewport": settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport, - "editionBelowViewport": settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport, + indentationOnNewLine: settings.indentationOnNewLine, + scrollWhenFocusLineIsOutOfViewport: { + percentage: { + editionAboveViewport: settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport, + editionBelowViewport: settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport, }, - "duration": settings.scrollWhenFocusLineIsOutOfViewport.duration, - "scrollWhenCaretIsInTheLastLineOfViewport": settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport, - "percentageToScrollWhenUserPressesArrowUp": settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp, + duration: settings.scrollWhenFocusLineIsOutOfViewport.duration, + scrollWhenCaretIsInTheLastLineOfViewport: settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport, + percentageToScrollWhenUserPressesArrowUp: settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp, }, - "initialChangesets": [], // FIXME: REMOVE THIS SHIT - } + initialChangesets: [], // FIXME: REMOVE THIS SHIT + }; // Add a username to the clientVars if one avaiable if (authorName != null) { @@ -1092,7 +1076,7 @@ async function handleClientReady(socket, message, authorID) { const messages = await hooks.aCallAll('clientVars', {clientVars, pad, socket}); // combine our old object with the new attributes from the hook - for (let msg of messages) { + for (const msg of messages) { Object.assign(clientVars, msg); } @@ -1106,17 +1090,17 @@ async function handleClientReady(socket, message, authorID) { sessionInfo.rev = pad.getHeadRevisionNumber(); // prepare the notification for the other users on the pad, that this user joined - let messageToTheOtherUsers = { - "type": "COLLABROOM", - "data": { - type: "USER_NEWINFO", + const messageToTheOtherUsers = { + type: 'COLLABROOM', + data: { + type: 'USER_NEWINFO', userInfo: { - "ip": "127.0.0.1", - "colorId": authorColorId, - "userAgent": "Anonymous", - "userId": authorID, - } - } + ip: '127.0.0.1', + colorId: authorColorId, + userAgent: 'Anonymous', + userId: authorID, + }, + }, }; // Add the authorname of this new User, if avaiable @@ -1129,7 +1113,6 @@ async function handleClientReady(socket, message, authorID) { // Get sessions for this pad and update them (in parallel) await Promise.all(_getRoomSockets(pad.id).map(async (roomSocket) => { - // Jump over, if this session is the connection session if (roomSocket.id === socket.id) { return; @@ -1141,25 +1124,25 @@ async function handleClientReady(socket, message, authorID) { if (sessionInfo == null) return; // get the authorname & colorId - let author = sessionInfo.author; - let cached = historicalAuthorData[author]; + const author = sessionInfo.author; + const cached = historicalAuthorData[author]; // reuse previously created cache of author's data let authorInfo = cached ? cached : (await authorManager.getAuthor(author)); // default fallback color to use if authorInfo.colorId is null - const defaultColor = "#daf0b2"; + const defaultColor = '#daf0b2'; if (!authorInfo) { - console.warn(`handleClientReady(): no authorInfo parameter was received. Default values are going to be used. See issue #3612. This can be caused by a user clicking undo after clearing all authorship colors see #2802`); + console.warn('handleClientReady(): no authorInfo parameter was received. Default values are going to be used. See issue #3612. This can be caused by a user clicking undo after clearing all authorship colors see #2802'); authorInfo = {}; } // For some reason sometimes name isn't set // Catch this issue here and use a fixed name. if (!authorInfo.name) { - console.warn(`handleClientReady(): client submitted no author name. Using "Anonymous". See: issue #3612`); - authorInfo.name = "Anonymous"; + console.warn('handleClientReady(): client submitted no author name. Using "Anonymous". See: issue #3612'); + authorInfo.name = 'Anonymous'; } // For some reason sometimes colorId isn't set @@ -1170,18 +1153,18 @@ async function handleClientReady(socket, message, authorID) { } // Send the new User a Notification about this other user - let msg = { - "type": "COLLABROOM", - "data": { - type: "USER_NEWINFO", + const msg = { + type: 'COLLABROOM', + data: { + type: 'USER_NEWINFO', userInfo: { - "ip": "127.0.0.1", - "colorId": authorInfo.colorId, - "name": authorInfo.name, - "userAgent": "Anonymous", - "userId": author - } - } + ip: '127.0.0.1', + colorId: authorInfo.colorId, + name: authorInfo.name, + userAgent: 'Anonymous', + userId: author, + }, + }, }; socket.json.send(msg); @@ -1195,49 +1178,49 @@ async function handleClientReady(socket, message, authorID) { async function handleChangesetRequest(socket, message) { // check if all ok if (message.data == null) { - messageLogger.warn("Dropped message, changeset request has no data!"); + messageLogger.warn('Dropped message, changeset request has no data!'); return; } if (message.padId == null) { - messageLogger.warn("Dropped message, changeset request has no padId!"); + messageLogger.warn('Dropped message, changeset request has no padId!'); return; } if (message.data.granularity == null) { - messageLogger.warn("Dropped message, changeset request has no granularity!"); + messageLogger.warn('Dropped message, changeset request has no granularity!'); return; } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill if (Math.floor(message.data.granularity) !== message.data.granularity) { - messageLogger.warn("Dropped message, changeset request granularity is not an integer!"); + messageLogger.warn('Dropped message, changeset request granularity is not an integer!'); return; } if (message.data.start == null) { - messageLogger.warn("Dropped message, changeset request has no start!"); + messageLogger.warn('Dropped message, changeset request has no start!'); return; } if (message.data.requestID == null) { - messageLogger.warn("Dropped message, changeset request has no requestID!"); + messageLogger.warn('Dropped message, changeset request has no requestID!'); return; } - let granularity = message.data.granularity; - let start = message.data.start; - let end = start + (100 * granularity); + const granularity = message.data.granularity; + const start = message.data.start; + const end = start + (100 * granularity); - let padIds = await readOnlyManager.getIds(message.padId); + const padIds = await readOnlyManager.getIds(message.padId); // build the requested rough changesets and send them back try { - let data = await getChangesetInfo(padIds.padId, start, end, granularity); + const data = await getChangesetInfo(padIds.padId, start, end, granularity); 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); } } @@ -1246,8 +1229,8 @@ async function handleChangesetRequest(socket, message) { * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144 */ async function getChangesetInfo(padId, startNum, endNum, granularity) { - let pad = await padManager.getPad(padId); - let head_revision = pad.getHeadRevisionNumber(); + const pad = await padManager.getPad(padId); + const head_revision = pad.getHeadRevisionNumber(); // calculate the last full endnum if (endNum > head_revision + 1) { @@ -1255,15 +1238,15 @@ async function getChangesetInfo(padId, startNum, endNum, granularity) { } endNum = Math.floor(endNum / granularity) * granularity; - let compositesChangesetNeeded = []; - let revTimesNeeded = []; + const compositesChangesetNeeded = []; + const revTimesNeeded = []; // figure out which composite Changeset and revTimes we need, to load them in bulk for (let start = startNum; start < endNum; start += granularity) { - let end = start + granularity; + const end = start + granularity; // add the composite Changeset we needed - compositesChangesetNeeded.push({ start, end }); + compositesChangesetNeeded.push({start, end}); // add the t1 time we need revTimesNeeded.push(start === 0 ? 0 : start - 1); @@ -1276,24 +1259,20 @@ async function getChangesetInfo(padId, startNum, endNum, granularity) { // it would make all the lookups run in series // get all needed composite Changesets - let composedChangesets = {}; - let p1 = Promise.all(compositesChangesetNeeded.map(item => { - return composePadChangesets(padId, item.start, item.end).then(changeset => { - composedChangesets[item.start + "/" + item.end] = changeset; - }); - })); + const composedChangesets = {}; + 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 - let revisionDate = []; - let p2 = Promise.all(revTimesNeeded.map(revNum => { - return pad.getRevisionDate(revNum).then(revDate => { - revisionDate[revNum] = Math.floor(revDate / 1000); - }); - })); + const revisionDate = []; + const p2 = Promise.all(revTimesNeeded.map((revNum) => pad.getRevisionDate(revNum).then((revDate) => { + revisionDate[revNum] = Math.floor(revDate / 1000); + }))); // get the lines let lines; - let p3 = getPadLines(padId, startNum - 1).then(_lines => { + const p3 = getPadLines(padId, startNum - 1).then((_lines) => { lines = _lines; }); @@ -1301,37 +1280,37 @@ async function getChangesetInfo(padId, startNum, endNum, granularity) { await Promise.all([p1, p2, p3]); // doesn't know what happens here exactly :/ - let timeDeltas = []; - let forwardsChangesets = []; - let backwardsChangesets = []; - let apool = new AttributePool(); + const timeDeltas = []; + const forwardsChangesets = []; + const backwardsChangesets = []; + const apool = new AttributePool(); for (let compositeStart = startNum; compositeStart < endNum; compositeStart += granularity) { - let compositeEnd = compositeStart + granularity; + const compositeEnd = compositeStart + granularity; if (compositeEnd > endNum || compositeEnd > head_revision + 1) { break; } - let forwards = composedChangesets[compositeStart + "/" + compositeEnd]; - let backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool()); + const forwards = composedChangesets[`${compositeStart}/${compositeEnd}`]; + const backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool()); Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool()); Changeset.mutateTextLines(forwards, lines.textlines); - let forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool); - let backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool); + const forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool); + const backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool); - let t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 1]; - let t2 = revisionDate[compositeEnd - 1]; + const t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 1]; + const t2 = revisionDate[compositeEnd - 1]; timeDeltas.push(t2 - t1); forwardsChangesets.push(forwards2); backwardsChangesets.push(backwards2); } - return { forwardsChangesets, backwardsChangesets, - apool: apool.toJsonable(), actualEndNum: endNum, - timeDeltas, start: startNum, granularity }; + return {forwardsChangesets, backwardsChangesets, + apool: apool.toJsonable(), actualEndNum: endNum, + timeDeltas, start: startNum, granularity}; } /** @@ -1339,7 +1318,7 @@ async function getChangesetInfo(padId, startNum, endNum, granularity) { * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263 */ async function getPadLines(padId, revNum) { - let pad = await padManager.getPad(padId); + const pad = await padManager.getPad(padId); // get the atext let atext; @@ -1347,12 +1326,12 @@ async function getPadLines(padId, revNum) { if (revNum >= 0) { atext = await pad.getInternalRevisionAText(revNum); } else { - atext = Changeset.makeAText("\n"); + atext = Changeset.makeAText('\n'); } return { textlines: Changeset.splitTextLines(atext.text), - alines: Changeset.splitAttributionLines(atext.attribs, atext.text) + alines: Changeset.splitAttributionLines(atext.attribs, atext.text), }; } @@ -1360,52 +1339,49 @@ async function getPadLines(padId, revNum) { * 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) { - let pad = await padManager.getPad(padId); +async function composePadChangesets(padId, startNum, endNum) { + const pad = await padManager.getPad(padId); // fetch all changesets we need - let headNum = pad.getHeadRevisionNumber(); + const headNum = pad.getHeadRevisionNumber(); endNum = Math.min(endNum, headNum + 1); startNum = Math.max(startNum, 0); // create an array for all changesets, we will // replace the values with the changeset later - let changesetsNeeded = []; - for (let r = startNum ; r < endNum; r++) { + const changesetsNeeded = []; + for (let r = startNum; r < endNum; r++) { changesetsNeeded.push(r); } // get all changesets - let changesets = {}; - await Promise.all(changesetsNeeded.map(revNum => { - return pad.getRevisionChangeset(revNum).then(changeset => changesets[revNum] = changeset); - })); + const changesets = {}; + await Promise.all(changesetsNeeded.map((revNum) => pad.getRevisionChangeset(revNum).then((changeset) => changesets[revNum] = changeset))); // compose Changesets let r; try { let changeset = changesets[startNum]; - let pool = pad.apool(); + const pool = pad.apool(); for (r = startNum + 1; r < endNum; r++) { - let cs = changesets[r]; + const cs = changesets[r]; changeset = Changeset.compose(changeset, cs, pool); } return changeset; - } catch (e) { // r-1 indicates the rev that was build starting with startNum, applying startNum+1, +2, +3 - console.warn("failed to compose cs in pad:", padId, " startrev:", startNum," current rev:", r); + console.warn('failed to compose cs in pad:', padId, ' startrev:', startNum, ' current rev:', r); throw e; } } function _getRoomSockets(padID) { const roomSockets = []; - var room = socketio.sockets.adapter.rooms[padID]; + const room = socketio.sockets.adapter.rooms[padID]; if (room) { - for (var id in room.sockets) { + for (const id in room.sockets) { roomSockets.push(socketio.sockets.sockets[id]); } } @@ -1416,27 +1392,26 @@ function _getRoomSockets(padID) { /** * Get the number of users in a pad */ -exports.padUsersCount = function(padID) { +exports.padUsersCount = function (padID) { return { - padUsersCount: _getRoomSockets(padID).length - } -} + padUsersCount: _getRoomSockets(padID).length, + }; +}; /** * Get the list of users in a pad */ -exports.padUsers = async function(padID) { - - let padUsers = []; +exports.padUsers = async function (padID) { + const padUsers = []; // iterate over all clients (in parallel) await Promise.all(_getRoomSockets(padID).map(async (roomSocket) => { const s = sessioninfos[roomSocket.id]; if (s) { - return authorManager.getAuthor(s.author).then(author => { + return authorManager.getAuthor(s.author).then((author) => { // Fixes: https://github.com/ether/etherpad-lite/issues/4120 // On restart author might not be populated? - if(author){ + if (author) { author.id = s.author; padUsers.push(author); } @@ -1444,7 +1419,7 @@ exports.padUsers = async function(padID) { } })); - return { padUsers }; -} + return {padUsers}; +}; exports.sessioninfos = sessioninfos; diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index 9b3c6f2dc..56e5c5be4 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -19,53 +19,53 @@ * limitations under the License. */ -var log4js = require('log4js'); -var messageLogger = log4js.getLogger("message"); -var securityManager = require("../db/SecurityManager"); -var readOnlyManager = require("../db/ReadOnlyManager"); -var settings = require('../utils/Settings'); +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 * key is the component name * value is the component module */ -var components = {}; +const components = {}; -var socket; +let socket; /** * adds a component */ -exports.addComponent = function(moduleName, module) { +exports.addComponent = function (moduleName, module) { // save the component components[moduleName] = module; // give the module the socket module.setSocketIO(socket); -} +}; /** * sets the socket.io and adds event functions for routing */ -exports.setSocketIO = function(_socket) { +exports.setSocketIO = function (_socket) { // save this socket internaly socket = _socket; - socket.sockets.on('connection', function(client) { + socket.sockets.on('connection', (client) => { // wrap the original send function to log the messages client._send = client.send; - client.send = function(message) { + client.send = function (message) { messageLogger.debug(`to ${client.id}: ${JSON.stringify(message)}`); client._send(message); - } + }; // tell all components about this connect - for (let i in components) { + for (const i in components) { components[i].handleConnect(client); } - client.on('message', async function(message) { + client.on('message', async (message) => { if (message.protocolVersion && message.protocolVersion != 2) { messageLogger.warn(`Protocolversion header is not correct: ${JSON.stringify(message)}`); return; @@ -78,11 +78,11 @@ exports.setSocketIO = function(_socket) { await components[message.component].handleMessage(client, message); }); - client.on('disconnect', function() { + client.on('disconnect', () => { // tell all components about this disconnect - for (let i in components) { + for (const i in components) { components[i].handleDisconnect(client); } }); }); -} +}; diff --git a/src/node/hooks/express.js b/src/node/hooks/express.js index 9bb4f2239..05283b5cb 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.js @@ -18,7 +18,7 @@ let serverName; exports.server = null; exports.createServer = async () => { - console.log("Report bugs at https://github.com/ether/etherpad-lite/issues") + console.log('Report bugs at https://github.com/ether/etherpad-lite/issues'); serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`; @@ -26,7 +26,7 @@ exports.createServer = async () => { await exports.restartServer(); - if (settings.ip === "") { + if (settings.ip === '') { // using Unix socket for connectivity console.log(`You can access your Etherpad instance using the Unix socket at ${settings.port}`); } else { @@ -42,26 +42,26 @@ exports.createServer = async () => { const env = process.env.NODE_ENV || 'development'; if (env !== 'production') { - console.warn("Etherpad is running in Development mode. This mode is slower for users and less secure than production mode. You should set the NODE_ENV environment variable to production by using: export NODE_ENV=production"); + console.warn('Etherpad is running in Development mode. This mode is slower for users and less secure than production mode. You should set the NODE_ENV environment variable to production by using: export NODE_ENV=production'); } -} +}; exports.restartServer = async () => { if (exports.server) { - console.log("Restarting express server"); + console.log('Restarting express server'); await util.promisify(exports.server.close).bind(exports.server)(); } const app = express(); // New syntax for express v3 if (settings.ssl) { - console.log("SSL -- enabled"); + console.log('SSL -- enabled'); console.log(`SSL -- server key file: ${settings.ssl.key}`); console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`); const options = { - key: fs.readFileSync( settings.ssl.key ), - cert: fs.readFileSync( settings.ssl.cert ) + key: fs.readFileSync(settings.ssl.key), + cert: fs.readFileSync(settings.ssl.cert), }; if (settings.ssl.ca) { @@ -79,16 +79,16 @@ exports.restartServer = async () => { exports.server = http.createServer(app); } - app.use(function(req, res, next) { + app.use((req, res, next) => { // res.header("X-Frame-Options", "deny"); // breaks embedded pads if (settings.ssl) { // we use SSL - res.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); + res.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); } // Stop IE going into compatability mode // https://github.com/ether/etherpad-lite/issues/2547 - res.header("X-UA-Compatible", "IE=Edge,chrome=1"); + res.header('X-UA-Compatible', 'IE=Edge,chrome=1'); // Enable a strong referrer policy. Same-origin won't drop Referers when // loading local resources, but it will drop them when loading foreign resources. @@ -97,11 +97,11 @@ exports.restartServer = async () => { // marked with // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy // https://github.com/ether/etherpad-lite/pull/3636 - res.header("Referrer-Policy", "same-origin"); + res.header('Referrer-Policy', 'same-origin'); // send git version in the Server response header if exposeVersion is true. if (settings.exposeVersion) { - res.header("Server", serverName); + res.header('Server', serverName); } next(); @@ -165,13 +165,13 @@ exports.restartServer = async () => { // // reference: https://github.com/expressjs/session/blob/v1.17.0/README.md#cookiesecure secure: 'auto', - } + }, }); app.use(exports.sessionMiddleware); app.use(cookieParser(settings.sessionKey, {})); - hooks.callAll("expressConfigure", {"app": app}); + hooks.callAll('expressConfigure', {app}); hooks.callAll('expressCreateServer', {app, server: exports.server}); await util.promisify(exports.server.listen).bind(exports.server)(settings.port, settings.ip); diff --git a/src/node/hooks/express/admin.js b/src/node/hooks/express/admin.js index 3971d1a32..417939600 100644 --- a/src/node/hooks/express/admin.js +++ b/src/node/hooks/express/admin.js @@ -1,9 +1,9 @@ -var eejs = require('ep_etherpad-lite/node/eejs'); +const eejs = require('ep_etherpad-lite/node/eejs'); exports.expressCreateServer = function (hook_name, args, cb) { - args.app.get('/admin', function(req, res) { - if('/' != req.path[req.path.length-1]) return res.redirect('./admin/'); + args.app.get('/admin', (req, res) => { + if ('/' != req.path[req.path.length - 1]) return res.redirect('./admin/'); res.send(eejs.require('ep_etherpad-lite/templates/admin/index.html', {req})); }); return cb(); -} +}; diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js index d1c6b389e..e01fc998d 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.js @@ -1,13 +1,13 @@ -var eejs = require('ep_etherpad-lite/node/eejs'); -var settings = require('ep_etherpad-lite/node/utils/Settings'); -var installer = require('ep_etherpad-lite/static/js/pluginfw/installer'); -var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs'); -var _ = require('underscore'); -var semver = require('semver'); +const eejs = require('ep_etherpad-lite/node/eejs'); +const settings = require('ep_etherpad-lite/node/utils/Settings'); +const installer = require('ep_etherpad-lite/static/js/pluginfw/installer'); +const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs'); +const _ = require('underscore'); +const semver = require('semver'); const UpdateCheck = require('ep_etherpad-lite/node/utils/UpdateCheck'); -exports.expressCreateServer = function(hook_name, args, cb) { - args.app.get('/admin/plugins', function(req, res) { +exports.expressCreateServer = function (hook_name, args, cb) { + args.app.get('/admin/plugins', (req, res) => { res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins.html', { plugins: plugins.plugins, req, @@ -16,114 +16,108 @@ exports.expressCreateServer = function(hook_name, args, cb) { })); }); - args.app.get('/admin/plugins/info', function(req, res) { - var gitCommit = settings.getGitCommit(); - var epVersion = settings.getEpVersion(); + args.app.get('/admin/plugins/info', (req, res) => { + const gitCommit = settings.getGitCommit(); + const epVersion = settings.getEpVersion(); - res.send(eejs.require("ep_etherpad-lite/templates/admin/plugins-info.html", { - gitCommit: gitCommit, - epVersion: epVersion, + res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins-info.html', { + gitCommit, + epVersion, latestVersion: UpdateCheck.getLatestVersion(), req, })); }); return cb(); -} +}; -exports.socketio = function(hook_name, args, cb) { - var io = args.io.of("/pluginfw/installer"); - io.on('connection', function(socket) { +exports.socketio = function (hook_name, args, cb) { + const io = args.io.of('/pluginfw/installer'); + io.on('connection', (socket) => { if (!socket.conn.request.session || !socket.conn.request.session.user || !socket.conn.request.session.user.is_admin) return; - socket.on("getInstalled", function(query) { + socket.on('getInstalled', (query) => { // send currently installed plugins - var installed = Object.keys(plugins.plugins).map(function(plugin) { - return plugins.plugins[plugin].package - }); + const installed = Object.keys(plugins.plugins).map((plugin) => plugins.plugins[plugin].package); - socket.emit("results:installed", {installed: installed}); + socket.emit('results:installed', {installed}); }); - socket.on("checkUpdates", async function() { + socket.on('checkUpdates', async () => { // Check plugins for updates try { - let results = await installer.getAvailablePlugins(/*maxCacheAge:*/ 60 * 10); + const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ 60 * 10); - var updatable = _(plugins.plugins).keys().filter(function(plugin) { + const updatable = _(plugins.plugins).keys().filter((plugin) => { if (!results[plugin]) return false; - var latestVersion = results[plugin].version; - var currentVersion = plugins.plugins[plugin].package.version; + const latestVersion = results[plugin].version; + const currentVersion = plugins.plugins[plugin].package.version; return semver.gt(latestVersion, currentVersion); }); - socket.emit("results:updatable", {updatable: updatable}); + socket.emit('results:updatable', {updatable}); } catch (er) { console.warn(er); - socket.emit("results:updatable", {updatable: {}}); + socket.emit('results:updatable', {updatable: {}}); } }); - socket.on("getAvailable", async function(query) { + socket.on('getAvailable', async (query) => { try { - let results = await installer.getAvailablePlugins(/*maxCacheAge:*/ false); - socket.emit("results:available", results); + const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ false); + socket.emit('results:available', results); } catch (er) { console.error(er); - socket.emit("results:available", {}); + socket.emit('results:available', {}); } }); - socket.on("search", async function(query) { + socket.on('search', async (query) => { try { - let results = await installer.search(query.searchTerm, /*maxCacheAge:*/ 60 * 10); - var res = Object.keys(results) - .map(function(pluginName) { - return results[pluginName]; - }) - .filter(function(plugin) { - return !plugins.plugins[plugin.name]; - }); + const results = await installer.search(query.searchTerm, /* maxCacheAge:*/ 60 * 10); + let res = Object.keys(results) + .map((pluginName) => results[pluginName]) + .filter((plugin) => !plugins.plugins[plugin.name]); res = sortPluginList(res, query.sortBy, query.sortDir) - .slice(query.offset, query.offset+query.limit); - socket.emit("results:search", {results: res, query: query}); + .slice(query.offset, query.offset + query.limit); + socket.emit('results:search', {results: res, query}); } catch (er) { console.error(er); - socket.emit("results:search", {results: {}, query: query}); + socket.emit('results:search', {results: {}, query}); } }); - socket.on("install", function(plugin_name) { - installer.install(plugin_name, function(er) { + socket.on('install', (plugin_name) => { + installer.install(plugin_name, (er) => { if (er) console.warn(er); - socket.emit("finished:install", {plugin: plugin_name, code: er? er.code : null, error: er? er.message : null}); + socket.emit('finished:install', {plugin: plugin_name, code: er ? er.code : null, error: er ? er.message : null}); }); }); - socket.on("uninstall", function(plugin_name) { - installer.uninstall(plugin_name, function(er) { + socket.on('uninstall', (plugin_name) => { + installer.uninstall(plugin_name, (er) => { if (er) console.warn(er); - socket.emit("finished:uninstall", {plugin: plugin_name, error: er? er.message : null}); + socket.emit('finished:uninstall', {plugin: plugin_name, error: er ? er.message : null}); }); }); }); return cb(); -} +}; -function sortPluginList(plugins, property, /*ASC?*/dir) { - return plugins.sort(function(a, b) { +function sortPluginList(plugins, property, /* ASC?*/dir) { + return plugins.sort((a, b) => { if (a[property] < b[property]) { - return dir? -1 : 1; + return dir ? -1 : 1; } if (a[property] > b[property]) { - return dir? 1 : -1; + return dir ? 1 : -1; } // a must be equal to b diff --git a/src/node/hooks/express/adminsettings.js b/src/node/hooks/express/adminsettings.js index 201c7533f..8cfe41ca6 100644 --- a/src/node/hooks/express/adminsettings.js +++ b/src/node/hooks/express/adminsettings.js @@ -1,55 +1,52 @@ -var eejs = require('ep_etherpad-lite/node/eejs'); -var settings = require('ep_etherpad-lite/node/utils/Settings'); -var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); -var fs = require('fs'); +const eejs = require('ep_etherpad-lite/node/eejs'); +const settings = require('ep_etherpad-lite/node/utils/Settings'); +const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); +const fs = require('fs'); exports.expressCreateServer = function (hook_name, args, cb) { - args.app.get('/admin/settings', function(req, res) { + args.app.get('/admin/settings', (req, res) => { res.send(eejs.require('ep_etherpad-lite/templates/admin/settings.html', { req, - settings: "", + settings: '', search_results: {}, - errors: [] + errors: [], })); }); return cb(); -} +}; exports.socketio = function (hook_name, args, cb) { - var io = args.io.of("/settings"); - io.on('connection', function (socket) { - + const io = args.io.of('/settings'); + io.on('connection', (socket) => { if (!socket.conn.request.session || !socket.conn.request.session.user || !socket.conn.request.session.user.is_admin) return; - socket.on("load", function (query) { - fs.readFile('settings.json', 'utf8', function (err,data) { + socket.on('load', (query) => { + fs.readFile('settings.json', 'utf8', (err, data) => { if (err) { return console.log(err); } // if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result - if(settings.showSettingsInAdminPage === false) { - socket.emit("settings", {results: 'NOT_ALLOWED'}); - } - else { - socket.emit("settings", {results: data}); + if (settings.showSettingsInAdminPage === false) { + socket.emit('settings', {results: 'NOT_ALLOWED'}); + } else { + socket.emit('settings', {results: data}); } }); }); - socket.on("saveSettings", function (settings) { - fs.writeFile('settings.json', settings, function (err) { + socket.on('saveSettings', (settings) => { + fs.writeFile('settings.json', settings, (err) => { if (err) throw err; - socket.emit("saveprogress", "saved"); + socket.emit('saveprogress', 'saved'); }); }); socket.on('restartServer', async () => { - console.log("Admin request to restart server through a socket on /admin/settings"); + console.log('Admin request to restart server through a socket on /admin/settings'); settings.reloadSettings(); await hooks.aCallAll('restartServer'); }); - }); return cb(); -} +}; diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.js index ed9f5a987..c87998e94 100644 --- a/src/node/hooks/express/apicalls.js +++ b/src/node/hooks/express/apicalls.js @@ -1,34 +1,34 @@ -var log4js = require('log4js'); -var clientLogger = log4js.getLogger("client"); -var formidable = require('formidable'); -var apiHandler = require('../../handler/APIHandler'); +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) { - //The Etherpad client side sends information about how a disconnect happened - args.app.post('/ep/pad/connection-diagnostic-info', function(req, res) { - new formidable.IncomingForm().parse(req, function(err, fields, files) { - clientLogger.info("DIAGNOSTIC-INFO: " + fields.diagnosticInfo); - res.end("OK"); + // 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) => { + clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`); + res.end('OK'); }); }); - //The Etherpad client side sends information about client side javscript errors - args.app.post('/jserror', function(req, res) { - new formidable.IncomingForm().parse(req, function(err, fields, files) { + // 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) => { try { - var data = JSON.parse(fields.errorInfo) - }catch(e){ - return res.end() + var data = JSON.parse(fields.errorInfo); + } catch (e) { + return res.end(); } - clientLogger.warn(data.msg+' --', data); - res.end("OK"); + clientLogger.warn(`${data.msg} --`, data); + res.end('OK'); }); }); - //Provide a possibility to query the latest available API version - args.app.get('/api', function (req, res) { - res.json({"currentVersion" : apiHandler.latestApiVersion}); + // Provide a possibility to query the latest available API version + args.app.get('/api', (req, res) => { + res.json({currentVersion: apiHandler.latestApiVersion}); }); return cb(); -} +}; diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js index 9863c6a5b..4a20b70d2 100644 --- a/src/node/hooks/express/errorhandling.js +++ b/src/node/hooks/express/errorhandling.js @@ -1,17 +1,17 @@ -var stats = require('ep_etherpad-lite/node/stats') +const stats = require('ep_etherpad-lite/node/stats'); exports.expressCreateServer = function (hook_name, args, cb) { exports.app = args.app; // Handle errors - args.app.use(function(err, req, res, next) { + args.app.use((err, req, res, next) => { // if an error occurs Connect will pass it down // through these "error-handling" middleware // allowing you to respond however you like - res.status(500).send({ error: 'Sorry, something bad happened!' }); - console.error(err.stack? err.stack : err.toString()); - stats.meter('http500').mark() + res.status(500).send({error: 'Sorry, something bad happened!'}); + console.error(err.stack ? err.stack : err.toString()); + stats.meter('http500').mark(); }); return cb(); -} +}; diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js index 5a0b72420..7a6c38655 100644 --- a/src/node/hooks/express/importexport.js +++ b/src/node/hooks/express/importexport.js @@ -1,44 +1,43 @@ const assert = require('assert').strict; -var hasPadAccess = require("../../padaccess"); -var settings = require('../../utils/Settings'); -var exportHandler = require('../../handler/ExportHandler'); -var importHandler = require('../../handler/ImportHandler'); -var padManager = require("../../db/PadManager"); -var readOnlyManager = require("../../db/ReadOnlyManager"); -var authorManager = require("../../db/AuthorManager"); -const rateLimit = require("express-rate-limit"); -const securityManager = require("../../db/SecurityManager"); -const webaccess = require("./webaccess"); +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 = function (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}`); -} +}; -var limiter = rateLimit(settings.importExportRateLimiting); +const limiter = rateLimit(settings.importExportRateLimiting); exports.expressCreateServer = function (hook_name, args, cb) { - // handle export requests args.app.use('/p/:pad/:rev?/export/:type', limiter); - args.app.get('/p/:pad/:rev?/export/:type', async function(req, res, next) { - var types = ["pdf", "doc", "txt", "html", "odt", "etherpad"]; - //send a 404 if we don't support this filetype + 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) { return next(); } // if abiword is disabled, and this is a format we only support with abiword, output a message - if (settings.exportAvailable() == "no" && - ["odt", "pdf", "doc"].indexOf(req.params.type) !== -1) { + 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`); // 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"); + 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; } - res.header("Access-Control-Allow-Origin", "*"); + res.header('Access-Control-Allow-Origin', '*'); if (await hasPadAccess(req, res)) { let padId = req.params.pad; @@ -49,7 +48,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { padId = await readOnlyManager.getPadId(readOnlyId); } - let exists = await padManager.doesPadExists(padId); + const exists = await padManager.doesPadExists(padId); if (!exists) { console.warn(`Someone tried to export a pad that doesn't exist (${padId})`); return next(); @@ -62,7 +61,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { // handle import requests args.app.use('/p/:pad/import', limiter); - args.app.post('/p/:pad/import', async function(req, res, next) { + args.app.post('/p/:pad/import', async (req, res, next) => { const {session: {user} = {}} = req; const {accessStatus} = await securityManager.checkAccess( req.params.pad, req.cookies.sessionID, req.cookies.token, user); @@ -73,4 +72,4 @@ exports.expressCreateServer = function (hook_name, args, cb) { }); return cb(); -} +}; diff --git a/src/node/hooks/express/isValidJSONPName.js b/src/node/hooks/express/isValidJSONPName.js index 47755ef86..442c963e9 100644 --- a/src/node/hooks/express/isValidJSONPName.js +++ b/src/node/hooks/express/isValidJSONPName.js @@ -62,14 +62,14 @@ const RESERVED_WORDS = [ 'volatile', 'while', 'with', - 'yield' + 'yield', ]; const regex = /^[a-zA-Z_$][0-9a-zA-Z_$]*(?:\[(?:".+"|\'.+\'|\d+)\])*?$/; -module.exports.check = function(inputStr) { - var isValid = true; - inputStr.split(".").forEach(function(part) { +module.exports.check = function (inputStr) { + let isValid = true; + inputStr.split('.').forEach((part) => { if (!regex.test(part)) { isValid = false; } @@ -80,4 +80,4 @@ module.exports.check = function(inputStr) { }); return isValid; -} +}; diff --git a/src/node/hooks/express/openapi.js b/src/node/hooks/express/openapi.js index 0bfa871f1..8ea9529c7 100644 --- a/src/node/hooks/express/openapi.js +++ b/src/node/hooks/express/openapi.js @@ -14,7 +14,7 @@ const OpenAPIBackend = require('openapi-backend').default; const formidable = require('formidable'); -const { promisify } = require('util'); +const {promisify} = require('util'); const cloneDeep = require('lodash.clonedeep'); const createHTTPError = require('http-errors'); @@ -57,12 +57,12 @@ const resources = { create: { operationId: 'createGroup', summary: 'creates a new group', - responseSchema: { groupID: { type: 'string' } }, + responseSchema: {groupID: {type: 'string'}}, }, createIfNotExistsFor: { operationId: 'createGroupIfNotExistsFor', summary: 'this functions helps you to map your application group ids to Etherpad group ids', - responseSchema: { groupID: { type: 'string' } }, + responseSchema: {groupID: {type: 'string'}}, }, delete: { operationId: 'deleteGroup', @@ -71,7 +71,7 @@ const resources = { listPads: { operationId: 'listPads', summary: 'returns all pads of this group', - responseSchema: { padIDs: { type: 'array', items: { type: 'string' } } }, + responseSchema: {padIDs: {type: 'array', items: {type: 'string'}}}, }, createPad: { operationId: 'createGroupPad', @@ -80,12 +80,12 @@ 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', summary: '', - responseSchema: { groupIDs: { type: 'array', items: { type: 'string' } } }, + responseSchema: {groupIDs: {type: 'array', items: {type: 'string'}}}, }, }, @@ -94,28 +94,28 @@ const resources = { create: { operationId: 'createAuthor', summary: 'creates a new author', - responseSchema: { authorID: { type: 'string' } }, + responseSchema: {authorID: {type: 'string'}}, }, createIfNotExistsFor: { operationId: 'createAuthorIfNotExistsFor', summary: 'this functions helps you to map your application author ids to Etherpad author ids', - responseSchema: { authorID: { type: 'string' } }, + responseSchema: {authorID: {type: 'string'}}, }, listPads: { operationId: 'listPadsOfAuthor', summary: 'returns an array of all pads this author contributed to', - responseSchema: { padIDs: { type: 'array', items: { type: 'string' } } }, + responseSchema: {padIDs: {type: 'array', items: {type: 'string'}}}, }, 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: { operationId: 'getAuthorName', summary: 'Returns the Author Name of the author', - responseSchema: { info: { $ref: '#/components/schemas/UserInfo' } }, + responseSchema: {info: {$ref: '#/components/schemas/UserInfo'}}, }, }, @@ -124,7 +124,7 @@ const resources = { create: { operationId: 'createSession', summary: 'creates a new session. validUntil is an unix timestamp in seconds', - responseSchema: { sessionID: { type: 'string' } }, + responseSchema: {sessionID: {type: 'string'}}, }, delete: { operationId: 'deleteSession', @@ -134,7 +134,7 @@ const resources = { info: { operationId: 'getSessionInfo', summary: 'returns informations about a session', - responseSchema: { info: { $ref: '#/components/schemas/SessionInfo' } }, + responseSchema: {info: {$ref: '#/components/schemas/SessionInfo'}}, }, }, @@ -143,7 +143,7 @@ const resources = { listAll: { operationId: 'listAllPads', summary: 'list all the pads', - responseSchema: { padIDs: { type: 'array', items: { type: 'string' } } }, + responseSchema: {padIDs: {type: 'array', items: {type: 'string'}}}, }, createDiffHTML: { operationId: 'createDiffHTML', @@ -158,7 +158,7 @@ const resources = { getText: { operationId: 'getText', summary: 'returns the text of a pad', - responseSchema: { text: { type: 'string' } }, + responseSchema: {text: {type: 'string'}}, }, setText: { operationId: 'setText', @@ -167,7 +167,7 @@ const resources = { getHTML: { operationId: 'getHTML', summary: 'returns the text of a pad formatted as HTML', - responseSchema: { html: { type: 'string' } }, + responseSchema: {html: {type: 'string'}}, }, setHTML: { operationId: 'setHTML', @@ -176,12 +176,12 @@ const resources = { getRevisionsCount: { operationId: 'getRevisionsCount', summary: 'returns the number of revisions of this pad', - responseSchema: { revisions: { type: 'integer' } }, + responseSchema: {revisions: {type: 'integer'}}, }, getLastEdited: { operationId: 'getLastEdited', summary: 'returns the timestamp of the last revision of the pad', - responseSchema: { lastEdited: { type: 'integer' } }, + responseSchema: {lastEdited: {type: 'integer'}}, }, delete: { operationId: 'deletePad', @@ -190,7 +190,7 @@ const resources = { getReadOnlyID: { operationId: 'getReadOnlyID', summary: 'returns the read only link of a pad', - responseSchema: { readOnlyID: { type: 'string' } }, + responseSchema: {readOnlyID: {type: 'string'}}, }, setPublicStatus: { operationId: 'setPublicStatus', @@ -199,22 +199,22 @@ const resources = { getPublicStatus: { operationId: 'getPublicStatus', summary: 'return true of false', - responseSchema: { publicStatus: { type: 'boolean' } }, + responseSchema: {publicStatus: {type: 'boolean'}}, }, authors: { operationId: 'listAuthorsOfPad', summary: 'returns an array of authors who contributed to this pad', - responseSchema: { authorIDs: { type: 'array', items: { type: 'string' } } }, + responseSchema: {authorIDs: {type: 'array', items: {type: 'string'}}}, }, usersCount: { operationId: 'padUsersCount', summary: 'returns the number of user that are currently editing this pad', - responseSchema: { padUsersCount: { type: 'integer' } }, + responseSchema: {padUsersCount: {type: 'integer'}}, }, users: { operationId: 'padUsers', summary: 'returns the list of users that are currently editing this pad', - responseSchema: { padUsers: { type: 'array', items: { $ref: '#/components/schemas/UserInfo' } } }, + responseSchema: {padUsers: {type: 'array', items: {$ref: '#/components/schemas/UserInfo'}}}, }, sendClientsMessage: { operationId: 'sendClientsMessage', @@ -227,13 +227,13 @@ const resources = { getChatHistory: { operationId: 'getChatHistory', summary: 'returns the chat history', - responseSchema: { messages: { type: 'array', items: { $ref: '#/components/schemas/Message' } } }, + responseSchema: {messages: {type: 'array', items: {$ref: '#/components/schemas/Message'}}}, }, // We need an operation that returns a Message so it can be picked up by the codegen :( getChatHead: { operationId: 'getChatHead', summary: 'returns the chatHead (chat-message) of the pad', - responseSchema: { chatHead: { $ref: '#/components/schemas/Message' } }, + responseSchema: {chatHead: {$ref: '#/components/schemas/Message'}}, }, appendChatMessage: { operationId: 'appendChatMessage', @@ -384,10 +384,10 @@ const defaultResponseRefs = { const operations = {}; for (const resource in resources) { for (const action in resources[resource]) { - const { operationId, responseSchema, ...operation } = resources[resource][action]; + const {operationId, responseSchema, ...operation} = resources[resource][action]; // add response objects - const responses = { ...defaultResponseRefs }; + const responses = {...defaultResponseRefs}; if (responseSchema) { responses[200] = cloneDeep(defaultResponses.Success); responses[200].content['application/json'].schema.properties.data = { @@ -478,14 +478,14 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { }, }, }, - security: [{ ApiKey: [] }], + security: [{ApiKey: []}], }; // build operations for (const funcName in apiHandler.version[version]) { let operation = {}; if (operations[funcName]) { - operation = { ...operations[funcName] }; + operation = {...operations[funcName]}; } else { // console.warn(`No operation found for function: ${funcName}`); operation = { @@ -497,7 +497,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { // set parameters operation.parameters = operation.parameters || []; for (const paramName of apiHandler.version[version][funcName]) { - operation.parameters.push({ $ref: `#/components/parameters/${paramName}` }); + operation.parameters.push({$ref: `#/components/parameters/${paramName}`}); if (!definition.components.parameters[paramName]) { definition.components.parameters[paramName] = { name: paramName, @@ -533,7 +533,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { }; exports.expressCreateServer = (hookName, args, cb) => { - const { app } = args; + const {app} = args; // create openapi-backend handlers for each api version under /api/{version}/* for (const version in apiHandler.version) { @@ -550,7 +550,7 @@ exports.expressCreateServer = (hookName, args, cb) => { app.get(`${apiRoot}/openapi.json`, (req, res) => { // For openapi definitions, wide CORS is probably fine res.header('Access-Control-Allow-Origin', '*'); - res.json({ ...definition, servers: [generateServerForApiVersion(apiRoot, req)] }); + res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]}); }); // serve latest openapi definition file under /api/openapi.json @@ -558,7 +558,7 @@ exports.expressCreateServer = (hookName, args, cb) => { if (isLatestAPIVersion) { app.get(`/${style}/openapi.json`, (req, res) => { res.header('Access-Control-Allow-Origin', '*'); - res.json({ ...definition, servers: [generateServerForApiVersion(apiRoot, req)] }); + res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]}); }); } @@ -586,7 +586,7 @@ exports.expressCreateServer = (hookName, args, cb) => { for (const funcName in apiHandler.version[version]) { const handler = async (c, req, res) => { // parse fields from request - const { header, params, query } = c.request; + const {header, params, query} = c.request; // read form data if method was POST let formData = {}; @@ -602,7 +602,7 @@ exports.expressCreateServer = (hookName, args, cb) => { apiLogger.info(`REQUEST, v${version}:${funcName}, ${JSON.stringify(fields)}`); // pass to api handler - let data = await apiHandler.handle(version, funcName, fields, req, res).catch((err) => { + const data = await apiHandler.handle(version, funcName, fields, req, res).catch((err) => { // convert all errors to http errors if (createHTTPError.isHttpError(err)) { // pass http errors thrown by handler forward @@ -620,7 +620,7 @@ exports.expressCreateServer = (hookName, args, cb) => { }); // return in common format - let response = { code: 0, message: 'ok', data: data || null }; + const response = {code: 0, message: 'ok', data: data || null}; // log response apiLogger.info(`RESPONSE, ${funcName}, ${JSON.stringify(response)}`); @@ -654,24 +654,24 @@ exports.expressCreateServer = (hookName, args, cb) => { // https://github.com/ether/etherpad-lite/tree/master/doc/api/http_api.md#response-format switch (res.statusCode) { case 403: // forbidden - response = { code: 4, message: err.message, data: null }; + response = {code: 4, message: err.message, data: null}; break; case 401: // unauthorized (no or wrong api key) - response = { code: 4, message: err.message, data: null }; + response = {code: 4, message: err.message, data: null}; break; case 404: // not found (no such function) - response = { code: 3, message: err.message, data: null }; + response = {code: 3, message: err.message, data: null}; break; case 500: // server error (internal error) - response = { code: 2, message: err.message, data: null }; + response = {code: 2, message: err.message, data: null}; break; case 400: // bad request (wrong parameters) // respond with 200 OK to keep old behavior and pass tests res.statusCode = 200; // @TODO: this is bad api design - response = { code: 1, message: err.message, data: null }; + response = {code: 1, message: err.message, data: null}; break; default: - response = { code: 1, message: err.message, data: null }; + response = {code: 1, message: err.message, data: null}; break; } } diff --git a/src/node/hooks/express/padreadonly.js b/src/node/hooks/express/padreadonly.js index 486806d80..f17f7f0d6 100644 --- a/src/node/hooks/express/padreadonly.js +++ b/src/node/hooks/express/padreadonly.js @@ -1,13 +1,12 @@ -var readOnlyManager = require("../../db/ReadOnlyManager"); -var hasPadAccess = require("../../padaccess"); -var exporthtml = require("../../utils/ExportHtml"); +const readOnlyManager = require('../../db/ReadOnlyManager'); +const hasPadAccess = require('../../padaccess'); +const exporthtml = require('../../utils/ExportHtml'); exports.expressCreateServer = function (hook_name, args, cb) { // serve read only pad - args.app.get('/ro/:id', async function(req, res) { - + args.app.get('/ro/:id', async (req, res) => { // translate the read only pad to a padId - let padId = await readOnlyManager.getPadId(req.params.id); + const padId = await readOnlyManager.getPadId(req.params.id); if (padId == null) { res.status(404).send('404 - Not Found'); return; @@ -18,9 +17,9 @@ exports.expressCreateServer = function (hook_name, args, cb) { if (await hasPadAccess(req, res)) { // render the html document - let html = await exporthtml.getPadHTMLDocument(padId, null); + const html = await exporthtml.getPadHTMLDocument(padId, null); res.send(html); } }); return cb(); -} +}; diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js index 7c603b2b2..8a287a961 100644 --- a/src/node/hooks/express/padurlsanitize.js +++ b/src/node/hooks/express/padurlsanitize.js @@ -1,30 +1,29 @@ -var padManager = require('../../db/PadManager'); -var url = require('url'); +const padManager = require('../../db/PadManager'); +const url = require('url'); exports.expressCreateServer = function (hook_name, args, cb) { - // redirects browser to the pad's sanitized url if needed. otherwise, renders the html - args.app.param('pad', async function (req, res, next, padId) { + args.app.param('pad', async (req, res, next, padId) => { // ensure the padname is valid and the url doesn't end with a / if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) { res.status(404).send('Such a padname is forbidden'); return; } - let sanitizedPadId = await padManager.sanitizePadId(padId); + const sanitizedPadId = await padManager.sanitizePadId(padId); if (sanitizedPadId === padId) { // the pad id was fine, so just render it next(); } else { // the pad id was sanitized, so we redirect to the sanitized version - var real_url = sanitizedPadId; + let real_url = sanitizedPadId; real_url = encodeURIComponent(real_url); - var query = url.parse(req.url).query; - if ( query ) real_url += '?' + query; + 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 ' + real_url + ''); + res.status(302).send(`You should be redirected to ${real_url}`); } }); return cb(); -} +}; diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index fbfea0989..3a0cd1bb0 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -1,19 +1,19 @@ -const express = require("../express"); +const express = require('../express'); const proxyaddr = require('proxy-addr'); -var settings = require('../../utils/Settings'); -var socketio = require('socket.io'); -var socketIORouter = require("../../handler/SocketIORouter"); -var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); +const settings = require('../../utils/Settings'); +const socketio = require('socket.io'); +const socketIORouter = require('../../handler/SocketIORouter'); +const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -var padMessageHandler = require("../../handler/PadMessageHandler"); +const padMessageHandler = require('../../handler/PadMessageHandler'); exports.expressCreateServer = function (hook_name, args, cb) { - //init socket.io and redirect all requests to the MessageHandler + // init socket.io and redirect all requests to the MessageHandler // there shouldn't be a browser that isn't compatible to all // transports in this list at once // e.g. XHR is disabled in IE by default, so in IE it should use jsonp-polling - var io = socketio({ - transports: settings.socketTransportProtocols + const io = socketio({ + transports: settings.socketTransportProtocols, }).listen(args.server, { /* * Do not set the "io" cookie. @@ -61,17 +61,17 @@ exports.expressCreateServer = function (hook_name, args, cb) { // https://github.com/Automattic/socket.io/wiki/Migrating-to-1.0 // This debug logging environment is set in Settings.js - //minify socket.io javascript + // minify socket.io javascript // Due to a shitty decision by the SocketIO team minification is // no longer available, details available at: // http://stackoverflow.com/questions/23981741/minify-socket-io-socket-io-js-with-1-0 // if(settings.minify) io.enable('browser client minification'); - //Initalize the Socket.IO Router + // Initalize the Socket.IO Router socketIORouter.setSocketIO(io); - socketIORouter.addComponent("pad", padMessageHandler); + socketIORouter.addComponent('pad', padMessageHandler); - hooks.callAll("socketio", {"app": args.app, "io": io, "server": args.server}); + hooks.callAll('socketio', {app: args.app, io, server: args.server}); return cb(); -} +}; diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js index b21d3910b..f53ce1ac7 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.js @@ -1,83 +1,81 @@ -var path = require('path'); -var eejs = require('ep_etherpad-lite/node/eejs'); -var toolbar = require("ep_etherpad-lite/node/utils/toolbar"); -var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -var settings = require('../../utils/Settings'); +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 settings = require('../../utils/Settings'); const webaccess = require('./webaccess'); exports.expressCreateServer = function (hook_name, args, cb) { // expose current stats - args.app.get('/stats', function(req, res) { - res.json(require('ep_etherpad-lite/node/stats').toJSON()) - }) + args.app.get('/stats', (req, res) => { + res.json(require('ep_etherpad-lite/node/stats').toJSON()); + }); - //serve index.html under / - args.app.get('/', function(req, res) { + // serve index.html under / + args.app.get('/', (req, res) => { res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req})); }); - //serve javascript.html - args.app.get('/javascript', function(req, res) { + // serve javascript.html + args.app.get('/javascript', (req, res) => { res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req})); }); - //serve robots.txt - args.app.get('/robots.txt', function(req, res) { - var filePath = path.join(settings.root, "src", "static", "skins", settings.skinName, "robots.txt"); - res.sendFile(filePath, function(err) { - //there is no custom robots.txt, send the default robots.txt which dissallows all - if(err) - { - filePath = path.join(settings.root, "src", "static", "robots.txt"); + // serve robots.txt + args.app.get('/robots.txt', (req, res) => { + 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) { + filePath = path.join(settings.root, 'src', 'static', 'robots.txt'); res.sendFile(filePath); } }); }); - //serve pad.html under /p - args.app.get('/p/:pad', function(req, res, next) { + // serve pad.html under /p + args.app.get('/p/:pad', (req, res, next) => { // The below might break for pads being rewritten const isReadOnly = - req.url.indexOf("/p/r.") === 0 || !webaccess.userCanModify(req.params.pad, req); + req.url.indexOf('/p/r.') === 0 || !webaccess.userCanModify(req.params.pad, req); - hooks.callAll("padInitToolbar", { - toolbar: toolbar, - isReadOnly: isReadOnly + hooks.callAll('padInitToolbar', { + toolbar, + isReadOnly, }); - res.send(eejs.require("ep_etherpad-lite/templates/pad.html", { - req: req, - toolbar: toolbar, - isReadOnly: isReadOnly + res.send(eejs.require('ep_etherpad-lite/templates/pad.html', { + req, + toolbar, + isReadOnly, })); }); - //serve timeslider.html under /p/$padname/timeslider - args.app.get('/p/:pad/timeslider', function(req, res, next) { - hooks.callAll("padInitToolbar", { - toolbar: toolbar + // serve timeslider.html under /p/$padname/timeslider + args.app.get('/p/:pad/timeslider', (req, res, next) => { + hooks.callAll('padInitToolbar', { + toolbar, }); - res.send(eejs.require("ep_etherpad-lite/templates/timeslider.html", { - req: req, - toolbar: toolbar + res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', { + req, + toolbar, })); }); - //serve favicon.ico from all path levels except as a pad name - args.app.get( /\/favicon.ico$/, function(req, res) { - var filePath = path.join(settings.root, "src", "static", "skins", settings.skinName, "favicon.ico"); + // 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'); - res.sendFile(filePath, function(err) { - //there is no custom favicon, send the default favicon - if(err) - { - filePath = path.join(settings.root, "src", "static", "favicon.ico"); + res.sendFile(filePath, (err) => { + // there is no custom favicon, send the default favicon + if (err) { + filePath = path.join(settings.root, 'src', 'static', 'favicon.ico'); res.sendFile(filePath); } }); }); return cb(); -} +}; diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.js index 2a925b26b..2df757e64 100644 --- a/src/node/hooks/express/static.js +++ b/src/node/hooks/express/static.js @@ -1,14 +1,13 @@ -var minify = require('../../utils/Minify'); -var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugin_defs"); -var CachingMiddleware = require('../../utils/caching_middleware'); -var settings = require("../../utils/Settings"); -var Yajsml = require('etherpad-yajsml'); -var _ = require("underscore"); +const minify = require('../../utils/Minify'); +const plugins = require('ep_etherpad-lite/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) { - // Cache both minified and static. - var assetCache = new CachingMiddleware; + const assetCache = new CachingMiddleware(); args.app.all(/\/javascripts\/(.*)/, assetCache.handle); // Minify will serve static files compressed (minify enabled). It also has @@ -18,43 +17,42 @@ exports.expressCreateServer = function (hook_name, args, cb) { // Setup middleware that will package JavaScript files served by minify for // CommonJS loader on the client-side. // Hostname "invalid.invalid" is a dummy value to allow parsing as a URI. - var jsServer = new (Yajsml.Server)({ - rootPath: 'javascripts/src/' - , rootURI: 'http://invalid.invalid/static/js/' - , libraryPath: 'javascripts/lib/' - , libraryURI: 'http://invalid.invalid/static/plugins/' - , requestURIs: minify.requestURIs // Loop-back is causing problems, this is a workaround. + const jsServer = new (Yajsml.Server)({ + rootPath: 'javascripts/src/', + rootURI: 'http://invalid.invalid/static/js/', + libraryPath: 'javascripts/lib/', + libraryURI: 'http://invalid.invalid/static/plugins/', + requestURIs: minify.requestURIs, // Loop-back is causing problems, this is a workaround. }); - var StaticAssociator = Yajsml.associators.StaticAssociator; - var associations = + const StaticAssociator = Yajsml.associators.StaticAssociator; + const associations = Yajsml.associators.associationsForSimpleMapping(minify.tar); - var associator = new StaticAssociator(associations); + const associator = new StaticAssociator(associations); jsServer.setAssociator(associator); 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"); - args.app.get('/pluginfw/plugin-definitions.json', function (req, res, next) { + args.app.get('/pluginfw/plugin-definitions.json', (req, res, next) => { + const clientParts = _(plugins.parts) + .filter((part) => _(part).has('client_hooks')); - var clientParts = _(plugins.parts) - .filter(function(part){ return _(part).has('client_hooks') }); - - var clientPlugins = {}; + const clientPlugins = {}; _(clientParts).chain() - .map(function(part){ return part.plugin }) - .uniq() - .each(function(name){ - clientPlugins[name] = _(plugins.plugins[name]).clone(); - delete clientPlugins[name]['package']; - }); + .map((part) => part.plugin) + .uniq() + .each((name) => { + clientPlugins[name] = _(plugins.plugins[name]).clone(); + delete clientPlugins[name].package; + }); - res.header("Content-Type","application/json; charset=utf-8"); - res.write(JSON.stringify({"plugins": clientPlugins, "parts": clientParts})); + res.header('Content-Type', 'application/json; charset=utf-8'); + res.write(JSON.stringify({plugins: clientPlugins, parts: clientParts})); res.end(); }); return cb(); -} +}; diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index 1ed67a5ca..7b32a322d 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -1,13 +1,13 @@ -var path = require("path") - , npm = require("npm") - , fs = require("fs") - , util = require("util"); +const path = require('path'); +const npm = require('npm'); +const fs = require('fs'); +const util = require('util'); exports.expressCreateServer = function (hook_name, args, cb) { - args.app.get('/tests/frontend/specs_list.js', async function(req, res) { - let [coreTests, pluginTests] = await Promise.all([ + args.app.get('/tests/frontend/specs_list.js', async (req, res) => { + const [coreTests, pluginTests] = await Promise.all([ exports.getCoreTests(), - exports.getPluginTests() + exports.getPluginTests(), ]); // merge the two sets of results @@ -16,79 +16,77 @@ exports.expressCreateServer = function (hook_name, args, cb) { // Keep only *.js files files = files.filter((f) => f.endsWith('.js')); - console.debug("Sent browser the following test specs:", files); + console.debug('Sent browser the following test specs:', files); res.setHeader('content-type', 'text/javascript'); - res.end("var specs_list = " + JSON.stringify(files) + ";\n"); + res.end(`var specs_list = ${JSON.stringify(files)};\n`); }); // path.join seems to normalize by default, but we'll just be explicit - var rootTestFolder = path.normalize(path.join(npm.root, "../tests/frontend/")); + const rootTestFolder = path.normalize(path.join(npm.root, '../tests/frontend/')); - var url2FilePath = function(url) { - var subPath = url.substr("/tests/frontend".length); - if (subPath == "") { - subPath = "index.html" + const url2FilePath = function (url) { + let subPath = url.substr('/tests/frontend'.length); + if (subPath == '') { + subPath = 'index.html'; } - subPath = subPath.split("?")[0]; + subPath = subPath.split('?')[0]; - var filePath = path.normalize(path.join(rootTestFolder, subPath)); + let filePath = path.normalize(path.join(rootTestFolder, subPath)); // make sure we jail the paths to the test folder, otherwise serve index if (filePath.indexOf(rootTestFolder) !== 0) { - filePath = path.join(rootTestFolder, "index.html"); + filePath = path.join(rootTestFolder, 'index.html'); } return filePath; - } + }; - args.app.get('/tests/frontend/specs/*', function (req, res) { - var specFilePath = url2FilePath(req.url); - var specFileName = path.basename(specFilePath); + args.app.get('/tests/frontend/specs/*', (req, res) => { + const specFilePath = url2FilePath(req.url); + const specFileName = path.basename(specFilePath); - fs.readFile(specFilePath, function(err, content) { + fs.readFile(specFilePath, (err, content) => { if (err) { return res.send(500); } - content = "describe(" + JSON.stringify(specFileName) + ", function(){ " + content + " });"; + content = `describe(${JSON.stringify(specFileName)}, function(){ ${content} });`; res.send(content); }); }); - args.app.get('/tests/frontend/*', function (req, res) { - var filePath = url2FilePath(req.url); + args.app.get('/tests/frontend/*', (req, res) => { + const filePath = url2FilePath(req.url); res.sendFile(filePath); }); - args.app.get('/tests/frontend', function (req, res) { + args.app.get('/tests/frontend', (req, res) => { res.redirect('/tests/frontend/index.html'); }); return cb(); -} +}; const readdir = util.promisify(fs.readdir); -exports.getPluginTests = async function(callback) { - const moduleDir = "node_modules/"; - const specPath = "/static/tests/frontend/specs/"; - const staticDir = "/static/plugins/"; +exports.getPluginTests = async function (callback) { + const moduleDir = 'node_modules/'; + const specPath = '/static/tests/frontend/specs/'; + const staticDir = '/static/plugins/'; - let pluginSpecs = []; + const pluginSpecs = []; - let plugins = await readdir(moduleDir); - let promises = plugins - .map(plugin => [ plugin, moduleDir + plugin + specPath] ) - .filter(([plugin, specDir]) => fs.existsSync(specDir)) // check plugin exists - .map(([plugin, specDir]) => { - return readdir(specDir) - .then(specFiles => specFiles.map(spec => { - pluginSpecs.push(staticDir + plugin + specPath + spec); - })); - }); + const plugins = await readdir(moduleDir); + const promises = plugins + .map((plugin) => [plugin, moduleDir + plugin + specPath]) + .filter(([plugin, specDir]) => fs.existsSync(specDir)) // check plugin exists + .map(([plugin, specDir]) => readdir(specDir) + .then((specFiles) => specFiles.map((spec) => { + pluginSpecs.push(staticDir + plugin + specPath + spec); + }))); return Promise.all(promises).then(() => pluginSpecs); -} +}; -exports.getCoreTests = function() { +exports.getCoreTests = function () { // get the core test specs return readdir('tests/frontend/specs'); -} +}; diff --git a/src/node/hooks/i18n.js b/src/node/hooks/i18n.js index 98acb9de7..610c3f68f 100644 --- a/src/node/hooks/i18n.js +++ b/src/node/hooks/i18n.js @@ -1,58 +1,58 @@ -var languages = require('languages4translatewiki') - , fs = require('fs') - , path = require('path') - , _ = require('underscore') - , npm = require('npm') - , plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs.js').plugins - , semver = require('semver') - , existsSync = require('../utils/path_exists') - , settings = require('../utils/Settings') +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 existsSync = require('../utils/path_exists'); +const settings = require('../utils/Settings') ; // returns all existing messages merged together and grouped by langcode // {es: {"foo": "string"}, en:...} function getAllLocales() { - var locales2paths = {}; + 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) { - if(!existsSync(dir)) return; - var stat = fs.lstatSync(dir); + if (!existsSync(dir)) return; + let stat = fs.lstatSync(dir); if (!stat.isDirectory() || stat.isSymbolicLink()) return; - fs.readdirSync(dir).forEach(function(file) { + fs.readdirSync(dir).forEach((file) => { file = path.resolve(dir, file); stat = fs.lstatSync(file); if (stat.isDirectory() || stat.isSymbolicLink()) return; - var ext = path.extname(file) - , locale = path.basename(file, ext).toLowerCase(); + const ext = path.extname(file); + const locale = path.basename(file, ext).toLowerCase(); if ((ext == '.json') && languages.isValid(locale)) { - if(!locales2paths[locale]) locales2paths[locale] = []; + if (!locales2paths[locale]) locales2paths[locale] = []; locales2paths[locale].push(file); } }); } - //add core supported languages first - extractLangs(npm.root+"/ep_etherpad-lite/locales"); + // add core supported languages first + extractLangs(`${npm.root}/ep_etherpad-lite/locales`); - //add plugins languages (if any) - for(var pluginName in plugins) extractLangs(path.join(npm.root, pluginName, 'locales')); + // add plugins languages (if any) + for (const pluginName in plugins) extractLangs(path.join(npm.root, pluginName, 'locales')); // Build a locale index (merge all locale data other than user-supplied overrides) - var locales = {} - _.each (locales2paths, function(files, langcode) { - locales[langcode]={}; + const locales = {}; + _.each(locales2paths, (files, langcode) => { + locales[langcode] = {}; - files.forEach(function(file) { + files.forEach((file) => { let fileContents; try { - fileContents = JSON.parse(fs.readFileSync(file,'utf8')); + fileContents = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (err) { console.error(`failed to read JSON file ${file}: ${err}`); throw err; @@ -64,17 +64,17 @@ function getAllLocales() { // Add custom strings from settings.json // Since this is user-supplied, we'll do some extra sanity checks const wrongFormatErr = Error( - "customLocaleStrings in wrong format. See documentation " + - "for Customization for Administrators, under Localization.") + 'customLocaleStrings in wrong format. See documentation ' + + 'for Customization for Administrators, under Localization.'); if (settings.customLocaleStrings) { - if (typeof settings.customLocaleStrings !== "object") throw wrongFormatErr - _.each(settings.customLocaleStrings, function(overrides, langcode) { - if (typeof overrides !== "object") throw wrongFormatErr - _.each(overrides, function(localeString, key) { - if (typeof localeString !== "string") throw wrongFormatErr - locales[langcode][key] = localeString - }) - }) + if (typeof settings.customLocaleStrings !== 'object') throw wrongFormatErr; + _.each(settings.customLocaleStrings, (overrides, langcode) => { + if (typeof overrides !== 'object') throw wrongFormatErr; + _.each(overrides, (localeString, key) => { + if (typeof localeString !== 'string') throw wrongFormatErr; + locales[langcode][key] = localeString; + }); + }); } return locales; @@ -83,45 +83,44 @@ function getAllLocales() { // returns a hash of all available languages availables with nativeName and direction // e.g. { es: {nativeName: "español", direction: "ltr"}, ... } function getAvailableLangs(locales) { - var result = {}; - _.each(_.keys(locales), function(langcode) { + const result = {}; + _.each(_.keys(locales), (langcode) => { result[langcode] = languages.getLanguageInfo(langcode); }); return result; } // returns locale index that will be served in /locales.json -var generateLocaleIndex = function (locales) { - var result = _.clone(locales) // keep English strings - _.each(_.keys(locales), function(langcode) { - if (langcode != 'en') result[langcode]='locales/'+langcode+'.json'; +const generateLocaleIndex = function (locales) { + const result = _.clone(locales); // keep English strings + _.each(_.keys(locales), (langcode) => { + if (langcode != 'en') result[langcode] = `locales/${langcode}.json`; }); return JSON.stringify(result); -} +}; -exports.expressCreateServer = function(n, args, cb) { - - //regenerate locales on server restart - var locales = getAllLocales(); - var localeIndex = generateLocaleIndex(locales); +exports.expressCreateServer = function (n, args, cb) { + // regenerate locales on server restart + const locales = getAllLocales(); + const localeIndex = generateLocaleIndex(locales); exports.availableLangs = getAvailableLangs(locales); - args.app.get ('/locales/:locale', function(req, res) { - //works with /locale/en and /locale/en.json requests - var locale = req.params.locale.split('.')[0]; + args.app.get('/locales/:locale', (req, res) => { + // works with /locale/en and /locale/en.json requests + const locale = req.params.locale.split('.')[0]; if (exports.availableLangs.hasOwnProperty(locale)) { res.setHeader('Content-Type', 'application/json; charset=utf-8'); - res.send('{"'+locale+'":'+JSON.stringify(locales[locale])+'}'); + res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`); } else { res.status(404).send('Language not available'); } - }) + }); - args.app.get('/locales.json', function(req, res) { + args.app.get('/locales.json', (req, res) => { res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.send(localeIndex); - }) + }); return cb(); -} +}; diff --git a/src/node/padaccess.js b/src/node/padaccess.js index b30c43b40..617056a97 100644 --- a/src/node/padaccess.js +++ b/src/node/padaccess.js @@ -1,4 +1,4 @@ -var securityManager = require('./db/SecurityManager'); +const securityManager = require('./db/SecurityManager'); // checks for padAccess module.exports = async function (req, res) { @@ -7,7 +7,7 @@ module.exports = async function (req, res) { const accessObj = await securityManager.checkAccess( req.params.pad, req.cookies.sessionID, req.cookies.token, user); - if (accessObj.accessStatus === "grant") { + if (accessObj.accessStatus === 'grant') { // there is access, continue return true; } else { @@ -19,4 +19,4 @@ module.exports = async function (req, res) { // @TODO - send internal server error here? throw err; } -} +}; diff --git a/src/node/server.js b/src/node/server.js index a8a567179..5b3040bfc 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -61,13 +61,13 @@ exports.start = async () => { 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()); + 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); + console.error(`exception thrown: ${e.message}`); if (e.stack) console.log(e.stack); process.exit(1); } diff --git a/src/node/stats.js b/src/node/stats.js index 13654bb7d..8f0d1f180 100644 --- a/src/node/stats.js +++ b/src/node/stats.js @@ -1,4 +1,4 @@ -var measured = require('measured-core') +const measured = require('measured-core'); module.exports = measured.createCollection(); diff --git a/src/node/utils/Abiword.js b/src/node/utils/Abiword.js index ee6b50781..b75487d75 100644 --- a/src/node/utils/Abiword.js +++ b/src/node/utils/Abiword.js @@ -18,41 +18,39 @@ * limitations under the License. */ -var spawn = require('child_process').spawn; -var async = require("async"); -var settings = require("./Settings"); -var os = require('os'); +const spawn = require('child_process').spawn; +const async = require('async'); +const settings = require('./Settings'); +const os = require('os'); -var doConvertTask; +let doConvertTask; -//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) -{ - var stdoutBuffer = ""; +// 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) { - //span an abiword process to perform the conversion - var abiword = spawn(settings.abiword, ["--to=" + task.destFile, task.srcFile]); + doConvertTask = function (task, callback) { + // span an abiword process to perform the conversion + const abiword = spawn(settings.abiword, [`--to=${task.destFile}`, task.srcFile]); - //delegate the processing of stdout to another function - abiword.stdout.on('data', function (data) { - //add data to buffer - stdoutBuffer+=data.toString(); - }); - - //append error messages to the buffer - abiword.stderr.on('data', function (data) { + // delegate the processing of stdout to another function + abiword.stdout.on('data', (data) => { + // add data to buffer stdoutBuffer += data.toString(); }); - //throw exceptions if abiword is dieing - abiword.on('exit', function (code) { - if(code != 0) { + // append error messages to the buffer + abiword.stderr.on('data', (data) => { + stdoutBuffer += data.toString(); + }); + + // throw exceptions if abiword is dieing + abiword.on('exit', (code) => { + if (code != 0) { return callback(`Abiword died with exit code ${code}`); } - if(stdoutBuffer != "") - { + if (stdoutBuffer != '') { console.log(stdoutBuffer); } @@ -60,51 +58,48 @@ if(os.type().indexOf("Windows") > -1) }); }; - exports.convertFile = function(srcFile, destFile, type, callback) { - doConvertTask({"srcFile": srcFile, "destFile": destFile, "type": type}, callback); + exports.convertFile = function (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 -{ - //spawn the abiword process - var abiword; - var stdoutCallback = null; - var spawnAbiword = function (){ - abiword = spawn(settings.abiword, ["--plugin", "AbiCommand"]); - var stdoutBuffer = ""; - var firstPrompt = true; +// 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 () { + abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']); + let stdoutBuffer = ''; + let firstPrompt = true; - //append error messages to the buffer - abiword.stderr.on('data', function (data) { + // append error messages to the buffer + abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); - //abiword died, let's restart abiword and return an error with the callback - abiword.on('exit', function (code) { + // abiword died, let's restart abiword and return an error with the callback + abiword.on('exit', (code) => { spawnAbiword(); stdoutCallback(`Abiword died with exit code ${code}`); }); - //delegate the processing of stdout to a other function - abiword.stdout.on('data',function (data) { - //add data to buffer - stdoutBuffer+=data.toString(); + // delegate the processing of stdout to a other function + abiword.stdout.on('data', (data) => { + // add data to buffer + stdoutBuffer += data.toString(); - //we're searching for the prompt, cause this means everything we need is in the buffer - if(stdoutBuffer.search("AbiWord:>") != -1) - { - //filter the feedback message - var err = stdoutBuffer.search("OK") != -1 ? null : stdoutBuffer; + // we're searching for the prompt, cause this means everything we need is in the buffer + if (stdoutBuffer.search('AbiWord:>') != -1) { + // filter the feedback message + const err = stdoutBuffer.search('OK') != -1 ? null : stdoutBuffer; - //reset the buffer - stdoutBuffer = ""; + // reset the buffer + stdoutBuffer = ''; - //call the callback with the error message - //skip the first prompt - if(stdoutCallback != null && !firstPrompt) - { + // call the callback with the error message + // skip the first prompt + if (stdoutCallback != null && !firstPrompt) { stdoutCallback(err); stdoutCallback = null; } @@ -115,23 +110,23 @@ else }; spawnAbiword(); - doConvertTask = function(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 + doConvertTask = function (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) { callback(); - console.log("queue continue"); - try{ + console.log('queue continue'); + try { task.callback(err); - }catch(e){ - console.error("Abiword File failed to convert", e); + } catch (e) { + console.error('Abiword File failed to convert', e); } }; }; - //Queue with the converts we have to do - var queue = async.queue(doConvertTask, 1); - exports.convertFile = function(srcFile, destFile, type, callback) { - queue.push({"srcFile": srcFile, "destFile": destFile, "type": type, "callback": callback}); + // Queue with the converts we have to do + const queue = async.queue(doConvertTask, 1); + exports.convertFile = function (srcFile, destFile, type, callback) { + queue.push({srcFile, destFile, type, callback}); }; } diff --git a/src/node/utils/AbsolutePaths.js b/src/node/utils/AbsolutePaths.js index 9d864c474..22294cfe2 100644 --- a/src/node/utils/AbsolutePaths.js +++ b/src/node/utils/AbsolutePaths.js @@ -18,17 +18,17 @@ * limitations under the License. */ -var log4js = require('log4js'); -var path = require('path'); -var _ = require('underscore'); +const log4js = require('log4js'); +const path = require('path'); +const _ = require('underscore'); -var absPathLogger = log4js.getLogger('AbsolutePaths'); +const absPathLogger = log4js.getLogger('AbsolutePaths'); /* * findEtherpadRoot() computes its value only on first invocation. * Subsequent invocations are served from this variable. */ -var etherpadRoot = null; +let etherpadRoot = null; /** * If stringArray's last elements are exactly equal to lastDesiredElements, @@ -40,9 +40,9 @@ var etherpadRoot = null; * @return {string[]|boolean} The shortened array, or false if there was no * overlap. */ -var popIfEndsWith = function(stringArray, lastDesiredElements) { +const popIfEndsWith = function (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`); + absPathLogger.debug(`In order to pop "${lastDesiredElements.join(path.sep)}" from "${stringArray.join(path.sep)}", it should contain at least ${lastDesiredElements.length + 1} elements`); return false; } @@ -72,7 +72,7 @@ var 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() { +exports.findEtherpadRoot = function () { if (etherpadRoot !== null) { return etherpadRoot; } @@ -87,7 +87,7 @@ exports.findEtherpadRoot = function() { * * \src */ - var maybeEtherpadRoot = popIfEndsWith(splitFoundRoot, ['src']); + let maybeEtherpadRoot = popIfEndsWith(splitFoundRoot, ['src']); if ((maybeEtherpadRoot === false) && (process.platform === 'win32')) { /* @@ -126,7 +126,7 @@ exports.findEtherpadRoot = function() { * it is returned unchanged. Otherwise it is interpreted * relative to exports.root. */ -exports.makeAbsolute = function(somePath) { +exports.makeAbsolute = function (somePath) { if (path.isAbsolute(somePath)) { return somePath; } @@ -145,7 +145,7 @@ exports.makeAbsolute = function(somePath) { * a subdirectory of the base one * @return {boolean} */ -exports.isSubdir = function(parent, arbitraryDir) { +exports.isSubdir = function (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); diff --git a/src/node/utils/Cli.js b/src/node/utils/Cli.js index 04c532fa0..6297a4f8c 100644 --- a/src/node/utils/Cli.js +++ b/src/node/utils/Cli.js @@ -22,30 +22,30 @@ // An object containing the parsed command-line options exports.argv = {}; -var argv = process.argv.slice(2); -var arg, prevArg; +const argv = process.argv.slice(2); +let arg, prevArg; // Loop through args -for ( var i = 0; i < argv.length; i++ ) { +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; } diff --git a/src/node/utils/ExportEtherpad.js b/src/node/utils/ExportEtherpad.js index a92a91928..ace298ab7 100644 --- a/src/node/utils/ExportEtherpad.js +++ b/src/node/utils/ExportEtherpad.js @@ -15,41 +15,39 @@ */ -let db = require("../db/DB"); -let hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); +const db = require('../db/DB'); +const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -exports.getPadRaw = async function(padId) { +exports.getPadRaw = async function (padId) { + const padKey = `pad:${padId}`; + const padcontent = await db.get(padKey); - let padKey = "pad:" + padId; - let padcontent = await db.get(padKey); - - let records = [ padKey ]; + const records = [padKey]; for (let i = 0; i <= padcontent.head; i++) { - records.push(padKey + ":revs:" + i); + records.push(`${padKey}:revs:${i}`); } for (let i = 0; i <= padcontent.chatHead; i++) { - records.push(padKey + ":chat:" + i); + records.push(`${padKey}:chat:${i}`); } - let data = {}; - for (let key of records) { - + const data = {}; + for (const key of records) { // For each piece of info about a pad. - let entry = data[key] = await db.get(key); + const entry = data[key] = await db.get(key); // Get the Pad Authors if (entry.pool && entry.pool.numToAttrib) { - let authors = entry.pool.numToAttrib; + const authors = entry.pool.numToAttrib; - for (let k of Object.keys(authors)) { - if (authors[k][0] === "author") { - let authorId = authors[k][1]; + for (const k of Object.keys(authors)) { + if (authors[k][0] === 'author') { + const authorId = authors[k][1]; // Get the author info - let authorEntry = await db.get("globalAuthor:" + authorId); + const authorEntry = await db.get(`globalAuthor:${authorId}`); if (authorEntry) { - data["globalAuthor:" + authorId] = authorEntry; + data[`globalAuthor:${authorId}`] = authorEntry; if (authorEntry.padIDs) { authorEntry.padIDs = padId; } @@ -68,4 +66,4 @@ exports.getPadRaw = async function(padId) { })); return data; -} +}; diff --git a/src/node/utils/ExportHelper.js b/src/node/utils/ExportHelper.js index f6ec4486e..e498d4c42 100644 --- a/src/node/utils/ExportHelper.js +++ b/src/node/utils/ExportHelper.js @@ -18,24 +18,23 @@ * limitations under the License. */ -var Changeset = require("ep_etherpad-lite/static/js/Changeset"); +const Changeset = require('ep_etherpad-lite/static/js/Changeset'); -exports.getPadPlainText = function(pad, revNum){ - var _analyzeLine = exports._analyzeLine; - var atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext); - var textLines = atext.text.slice(0, -1).split('\n'); - var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); - var apool = pad.pool; +exports.getPadPlainText = function (pad, revNum) { + const _analyzeLine = exports._analyzeLine; + const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext); + const textLines = atext.text.slice(0, -1).split('\n'); + const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); + const apool = pad.pool; - var pieces = []; - for (var i = 0; i < textLines.length; i++){ - var line = _analyzeLine(textLines[i], attribLines[i], apool); - if (line.listLevel){ - var numSpaces = line.listLevel * 2 - 1; - var bullet = '*'; + const pieces = []; + for (let i = 0; i < textLines.length; i++) { + const line = _analyzeLine(textLines[i], attribLines[i], apool); + if (line.listLevel) { + const numSpaces = line.listLevel * 2 - 1; + const bullet = '*'; pieces.push(new Array(numSpaces + 1).join(' '), bullet, ' ', line.text, '\n'); - } - else{ + } else { pieces.push(line.text, '\n'); } } @@ -44,38 +43,37 @@ exports.getPadPlainText = function(pad, revNum){ }; -exports._analyzeLine = function(text, aline, apool){ - var line = {}; +exports._analyzeLine = function (text, aline, apool) { + const line = {}; // identify list - var lineMarker = 0; + let lineMarker = 0; line.listLevel = 0; - if (aline){ - var opIter = Changeset.opIterator(aline); - if (opIter.hasNext()){ - var listType = Changeset.opAttributeValue(opIter.next(), 'list', apool); - if (listType){ + if (aline) { + const opIter = Changeset.opIterator(aline); + if (opIter.hasNext()) { + let listType = Changeset.opAttributeValue(opIter.next(), 'list', apool); + if (listType) { lineMarker = 1; listType = /([a-z]+)([0-9]+)/.exec(listType); - if (listType){ + if (listType) { line.listTypeName = listType[1]; line.listLevel = Number(listType[2]); } } } - var opIter2 = Changeset.opIterator(aline); - if (opIter2.hasNext()){ - var start = Changeset.opAttributeValue(opIter2.next(), 'start', apool); - if (start){ - line.start = start; + const opIter2 = Changeset.opIterator(aline); + if (opIter2.hasNext()) { + const start = Changeset.opAttributeValue(opIter2.next(), 'start', apool); + if (start) { + line.start = start; } } } - if (lineMarker){ + if (lineMarker) { line.text = text.substring(1); line.aline = Changeset.subattribution(aline, 1); - } - else{ + } else { line.text = text; line.aline = aline; } @@ -83,8 +81,6 @@ exports._analyzeLine = function(text, aline, apool){ }; -exports._encodeWhitespace = function(s){ - return s.replace(/[^\x21-\x7E\s\t\n\r]/gu, function(c){ - return "&#" +c.codePointAt(0) + ";"; - }); +exports._encodeWhitespace = function (s) { + return s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`); }; diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js index 93637dc60..cfb294138 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.js @@ -14,14 +14,14 @@ * limitations under the License. */ -var Changeset = require("ep_etherpad-lite/static/js/Changeset"); -var padManager = require("../db/PadManager"); -var _ = require('underscore'); -var Security = require('ep_etherpad-lite/static/js/security'); -var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -var eejs = require('ep_etherpad-lite/node/eejs'); -var _analyzeLine = require('./ExportHelper')._analyzeLine; -var _encodeWhitespace = require('./ExportHelper')._encodeWhitespace; +const Changeset = require('ep_etherpad-lite/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 _analyzeLine = require('./ExportHelper')._analyzeLine; +const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace; async function getPadHTML(pad, revNum) { let atext = pad.atext; @@ -39,12 +39,12 @@ exports.getPadHTML = getPadHTML; exports.getHTMLFromAtext = getHTMLFromAtext; async function getHTMLFromAtext(pad, atext, authorColors) { - var apool = pad.apool(); - var textLines = atext.text.slice(0, -1).split('\n'); - var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); + const apool = pad.apool(); + const textLines = atext.text.slice(0, -1).split('\n'); + const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); - var tags = ['h1', 'h2', 'strong', 'em', 'u', 's']; - var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; + const tags = ['h1', 'h2', 'strong', 'em', 'u', 's']; + const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; await Promise.all([ // prepare tags stored as ['tag', true] to be exported @@ -68,56 +68,55 @@ async function getHTMLFromAtext(pad, atext, authorColors) { // and maps them to an index in props // *3:2 -> the attribute *3 means strong // *2:5 -> the attribute *2 means s(trikethrough) - var anumMap = {}; - var css = ""; + const anumMap = {}; + let css = ''; - var stripDotFromAuthorID = function(id){ - return id.replace(/\./g,'_'); + const stripDotFromAuthorID = function (id) { + return id.replace(/\./g, '_'); }; - if(authorColors){ - css+=""; + css += ''; } // iterates over all props(h1,h2,strong,...), checks if it is used in // this pad, and if yes puts its attrib id->props value into anumMap - props.forEach(function (propName, i) { - var attrib = [propName, true]; + props.forEach((propName, i) => { + let attrib = [propName, true]; if (_.isArray(propName)) { // propName can be in the form of ['color', 'red'], // see hook exportHtmlAdditionalTagsWithData attrib = propName; } - var propTrueNum = apool.putAttrib(attrib, true); - if (propTrueNum >= 0) - { + const propTrueNum = apool.putAttrib(attrib, true); + if (propTrueNum >= 0) { anumMap[propTrueNum] = i; } }); @@ -128,15 +127,15 @@ async function getHTMLFromAtext(pad, atext, authorColors) { // Just bold Bold and italics Just italics // becomes // Just bold Bold and italics Just italics - var taker = Changeset.stringIterator(text); - var assem = Changeset.stringAssembler(); - var openTags = []; + const taker = Changeset.stringIterator(text); + const assem = Changeset.stringAssembler(); + const openTags = []; - function getSpanClassFor(i){ - //return if author colors are disabled + function getSpanClassFor(i) { + // return if author colors are disabled if (!authorColors) return false; - var property = props[i]; + const property = props[i]; // we are not insterested on properties in the form of ['color', 'red'], // see hook exportHtmlAdditionalTagsWithData @@ -144,12 +143,12 @@ async function getHTMLFromAtext(pad, atext, authorColors) { return false; } - if(property.substr(0,6) === "author"){ + if (property.substr(0, 6) === 'author') { return stripDotFromAuthorID(property); } - if(property === "removed"){ - return "removed"; + if (property === 'removed') { + return 'removed'; } return false; @@ -157,16 +156,16 @@ async function getHTMLFromAtext(pad, atext, authorColors) { // tags added by exportHtmlAdditionalTagsWithData will be exported as with // data attributes - function isSpanWithData(i){ - var property = props[i]; + function isSpanWithData(i) { + const property = props[i]; return _.isArray(property); } function emitOpenTag(i) { openTags.unshift(i); - var spanClass = getSpanClassFor(i); + const spanClass = getSpanClassFor(i); - if(spanClass){ + if (spanClass) { assem.append(''); @@ -180,10 +179,10 @@ async function getHTMLFromAtext(pad, atext, authorColors) { // this closes an open tag and removes its reference from openTags function emitCloseTag(i) { openTags.shift(); - var spanClass = getSpanClassFor(i); - var spanWithData = isSpanWithData(i); + const spanClass = getSpanClassFor(i); + const spanWithData = isSpanWithData(i); - if(spanClass || spanWithData){ + if (spanClass || spanWithData) { assem.append(''); } else { assem.append(' { + if (a in anumMap) { usedAttribs.push(anumMap[a]); // i = 0 => bold, etc. } }); - var outermostTag = -1; + let outermostTag = -1; // find the outer most open tag that is no longer used - for (var i = openTags.length - 1; i >= 0; i--) - { - if (usedAttribs.indexOf(openTags[i]) === -1) - { + for (var i = openTags.length - 1; i >= 0; i--) { + if (usedAttribs.indexOf(openTags[i]) === -1) { outermostTag = i; break; } } // close all tags upto the outer most - if (outermostTag !== -1) - { - while ( outermostTag >= 0 ) - { + if (outermostTag !== -1) { + while (outermostTag >= 0) { emitCloseTag(openTags[0]); outermostTag--; } } // open all tags that are used but not open - for (i=0; i < usedAttribs.length; i++) - { - if (openTags.indexOf(usedAttribs[i]) === -1) - { + for (i = 0; i < usedAttribs.length; i++) { + if (openTags.indexOf(usedAttribs[i]) === -1) { emitOpenTag(usedAttribs[i]); } } - var chars = o.chars; - if (o.lines) - { + let chars = o.chars; + if (o.lines) { chars--; // exclude newline at end of line, if present } - var s = taker.take(chars); + let s = taker.take(chars); - //removes the characters with the code 12. Don't know where they come - //from but they break the abiword parser and are completly useless - s = s.replace(String.fromCharCode(12), ""); + // removes the characters with the code 12. Don't know where they come + // from but they break the abiword parser and are completly useless + s = s.replace(String.fromCharCode(12), ''); assem.append(_encodeWhitespace(Security.escapeHTML(s))); } // end iteration over spans in line // close all the tags that are open after the last op - while (openTags.length > 0) - { + while (openTags.length > 0) { emitCloseTag(openTags[0]); } } // end processNextChars - if (urls) - { - urls.forEach(function (urlData) { - var startIndex = urlData[0]; - var url = urlData[1]; - var urlLength = url.length; + 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. // Not all browsers understand this attribute, but it's part of the HTML5 standard. @@ -284,16 +271,16 @@ async function getHTMLFromAtext(pad, atext, authorColors) { // https://html.spec.whatwg.org/multipage/links.html#link-type-noopener // https://mathiasbynens.github.io/rel-noopener/ // https://github.com/ether/etherpad-lite/pull/3636 - assem.append(''); + assem.append(``); processNextChars(urlLength); assem.append(''); }); } processNextChars(text.length - idx); - + return _processSpaces(assem.toString()); } // end getLineHTML - var pieces = [css]; + const pieces = [css]; // Need to deal with constraints imposed on HTML lists; can // only gain one level of nesting at once, can't change type @@ -302,56 +289,48 @@ async function getHTMLFromAtext(pad, atext, authorColors) { // so we want to do something reasonable there. We also // want to deal gracefully with blank lines. // => keeps track of the parents level of indentation - var openLists = []; - for (var i = 0; i < textLines.length; i++) - { + let openLists = []; + for (let i = 0; i < textLines.length; i++) { var context; var line = _analyzeLine(textLines[i], attribLines[i], apool); - var lineContent = getLineHTML(line.text, line.aline); - if (line.listLevel)//If we are inside a list + const lineContent = getLineHTML(line.text, line.aline); + if (line.listLevel)// If we are inside a list { context = { - line: line, - lineContent: lineContent, - apool: apool, + line, + lineContent, + apool, attribLine: attribLines[i], text: textLines[i], - padId: pad.id + padId: pad.id, }; - var prevLine = null; - var nextLine = null; - if (i > 0) - { - prevLine = _analyzeLine(textLines[i -1], attribLines[i -1], apool); + let prevLine = null; + let nextLine = null; + if (i > 0) { + prevLine = _analyzeLine(textLines[i - 1], attribLines[i - 1], apool); } - if (i < textLines.length) - { + if (i < textLines.length) { nextLine = _analyzeLine(textLines[i + 1], attribLines[i + 1], apool); } await hooks.aCallAll('getLineHTMLForExport', context); - //To create list parent elements - if ((!prevLine || prevLine.listLevel !== line.listLevel) || (prevLine && line.listTypeName !== prevLine.listTypeName)) - { - var exists = _.find(openLists, function (item) { - return (item.level === line.listLevel && item.type === line.listTypeName); - }); + // 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 (!exists) { - var prevLevel = 0; + let prevLevel = 0; if (prevLine && prevLine.listLevel) { prevLevel = prevLine.listLevel; } - if (prevLine && line.listTypeName !== prevLine.listTypeName) - { + if (prevLine && line.listTypeName !== prevLine.listTypeName) { prevLevel = 0; } for (var diff = prevLevel; diff < line.listLevel; diff++) { openLists.push({level: diff, type: line.listTypeName}); - var prevPiece = pieces[pieces.length - 1]; + const prevPiece = pieces[pieces.length - 1]; - if (prevPiece.indexOf("") === 0) - { - /* + if (prevPiece.indexOf('') === 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.. @@ -367,19 +346,16 @@ async function getHTMLFromAtext(pad, atext, authorColors) { // pieces.push(""); */ - if( (nextLine.listTypeName === 'number') && (nextLine.text === '') ){ + 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 - }else{ - pieces.push("
          • "); + } else { + pieces.push('
          • '); } - - } - if (line.listTypeName === "number") - { + if (line.listTypeName === 'number') { // 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 @@ -390,80 +366,66 @@ async function getHTMLFromAtext(pad, atext, authorColors) { // TODO: This logic could also be used to continue OL with indented content // but that's a job for another day.... - if(line.start){ - pieces.push("
              "); - }else{ - pieces.push("
                "); + if (line.start) { + pieces.push(`
                  `); + } else { + pieces.push(`
                    `); } - } - else - { - pieces.push("
                      "); + } else { + pieces.push(`
                        `); } } } } // if we're going up a level we shouldn't be adding.. - if(context.lineContent){ - pieces.push("
                      • ", context.lineContent); + if (context.lineContent) { + pieces.push('
                      • ', context.lineContent); } // To close list elements - if (nextLine && nextLine.listLevel === line.listLevel && line.listTypeName === nextLine.listTypeName) - { - if(context.lineContent){ - if( (nextLine.listTypeName === 'number') && (nextLine.text === '') ){ + 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 - }else{ - pieces.push("
                      • "); + } else { + pieces.push(''); } - } } - if ((!nextLine || !nextLine.listLevel || nextLine.listLevel < line.listLevel) || (nextLine && line.listTypeName !== nextLine.listTypeName)) - { - var nextLevel = 0; + if ((!nextLine || !nextLine.listLevel || nextLine.listLevel < line.listLevel) || (nextLine && line.listTypeName !== nextLine.listTypeName)) { + let nextLevel = 0; if (nextLine && nextLine.listLevel) { nextLevel = nextLine.listLevel; } - if (nextLine && line.listTypeName !== nextLine.listTypeName) - { + if (nextLine && line.listTypeName !== nextLine.listTypeName) { nextLevel = 0; } - for (var diff = nextLevel; diff < line.listLevel; diff++) - { - openLists = openLists.filter(function(el) { - return el.level !== diff && el.type !== line.listTypeName; - }); + for (var diff = nextLevel; diff < line.listLevel; diff++) { + openLists = openLists.filter((el) => el.level !== diff && el.type !== line.listTypeName); - if (pieces[pieces.length - 1].indexOf(""); + if (pieces[pieces.length - 1].indexOf(''); } - if (line.listTypeName === "number") - { - pieces.push("
                  "); - } - else - { - pieces.push(""); + if (line.listTypeName === 'number') { + pieces.push('
                '); + } else { + pieces.push(''); } } } - } - else//outside any list, need to close line.listLevel of lists + } else// outside any list, need to close line.listLevel of lists { context = { - line: line, - lineContent: lineContent, - apool: apool, + line, + lineContent, + apool, attribLine: attribLines[i], text: textLines[i], - padId: pad.id + padId: pad.id, }; await hooks.aCallAll('getLineHTMLForExport', context); @@ -475,46 +437,45 @@ async function getHTMLFromAtext(pad, atext, authorColors) { } exports.getPadHTMLDocument = async function (padId, revNum) { - let pad = await padManager.getPad(padId); + const pad = await padManager.getPad(padId); // Include some Styles into the Head for Export - let stylesForExportCSS = ""; - let stylesForExport = await hooks.aCallAll("stylesForExport", padId); - stylesForExport.forEach(function(css){ + let stylesForExportCSS = ''; + const stylesForExport = await hooks.aCallAll('stylesForExport', padId); + stylesForExport.forEach((css) => { stylesForExportCSS += css; }); let html = await getPadHTML(pad, revNum); - for (const hookHtml of await hooks.aCallAll("exportHTMLAdditionalContent", {padId})) { + for (const hookHtml of await hooks.aCallAll('exportHTMLAdditionalContent', {padId})) { html += hookHtml; } - return eejs.require("ep_etherpad-lite/templates/export_html.html", { + return eejs.require('ep_etherpad-lite/templates/export_html.html', { body: html, padId: Security.escapeHTML(padId), - extraCSS: stylesForExportCSS + extraCSS: stylesForExportCSS, }); -} +}; // copied from ACE -var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; -var _REGEX_SPACE = /\s/; -var _REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source + '|' + _REGEX_WORDCHAR.source + ')'); -var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source + _REGEX_URLCHAR.source + '*(?![:.,;])' + _REGEX_URLCHAR.source, 'g'); +const _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; +const _REGEX_SPACE = /\s/; +const _REGEX_URLCHAR = new RegExp(`(${/[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source}|${_REGEX_WORDCHAR.source})`); +const _REGEX_URL = new RegExp(`${/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source + _REGEX_URLCHAR.source}*(?![:.,;])${_REGEX_URLCHAR.source}`, 'g'); // returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...] function _findURLs(text) { _REGEX_URL.lastIndex = 0; - var urls = null; - var execResult; - while ((execResult = _REGEX_URL.exec(text))) - { + let urls = null; + let execResult; + while ((execResult = _REGEX_URL.exec(text))) { urls = (urls || []); - var startIndex = execResult.index; - var url = execResult[0]; + const startIndex = execResult.index; + const url = execResult[0]; urls.push([startIndex, url]); } @@ -523,50 +484,46 @@ function _findURLs(text) { // copied from ACE -function _processSpaces(s){ - var doesWrap = true; - if (s.indexOf("<") < 0 && !doesWrap){ +function _processSpaces(s) { + const doesWrap = true; + if (s.indexOf('<') < 0 && !doesWrap) { // short-cut return s.replace(/ /g, ' '); } - var parts = []; - s.replace(/<[^>]*>?| |[^ <]+/g, function (m){ + const parts = []; + s.replace(/<[^>]*>?| |[^ <]+/g, (m) => { parts.push(m); }); - if (doesWrap){ - var endOfLine = true; - var beforeSpace = false; + if (doesWrap) { + let endOfLine = true; + 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--){ + for (var i = parts.length - 1; i >= 0; i--) { var p = parts[i]; - if (p == " "){ + 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++){ + for (i = 0; i < parts.length; i++) { p = parts[i]; - if (p == " "){ + if (p == ' ') { parts[i] = ' '; break; - } - else if (p.charAt(0) != "<"){ + } else if (p.charAt(0) != '<') { break; } } - } - else - { - for (i = 0; i < parts.length; i++){ + } else { + for (i = 0; i < parts.length; i++) { p = parts[i]; - if (p == " "){ + if (p == ' ') { parts[i] = ' '; } } diff --git a/src/node/utils/ExportTxt.js b/src/node/utils/ExportTxt.js index 7390e2948..9d47896bc 100644 --- a/src/node/utils/ExportTxt.js +++ b/src/node/utils/ExportTxt.js @@ -18,12 +18,12 @@ * limitations under the License. */ -var Changeset = require("ep_etherpad-lite/static/js/Changeset"); -var padManager = require("../db/PadManager"); -var _analyzeLine = require('./ExportHelper')._analyzeLine; +const Changeset = require('ep_etherpad-lite/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 -var getPadTXT = async function(pad, revNum) { +const getPadTXT = async function (pad, revNum) { let atext = pad.atext; if (revNum != undefined) { @@ -33,57 +33,57 @@ var getPadTXT = async function(pad, revNum) { // convert atext to html return getTXTFromAtext(pad, atext); -} +}; // 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) { - var apool = pad.apool(); - var textLines = atext.text.slice(0, -1).split('\n'); - var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); + const apool = pad.apool(); + const textLines = atext.text.slice(0, -1).split('\n'); + const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); - var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; - var anumMap = {}; - var css = ""; + const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; + const anumMap = {}; + const css = ''; - props.forEach(function(propName, i) { - var propTrueNum = apool.putAttrib([propName, true], true); + props.forEach((propName, i) => { + const propTrueNum = apool.putAttrib([propName, true], true); if (propTrueNum >= 0) { anumMap[propTrueNum] = i; } }); function getLineTXT(text, attribs) { - var propVals = [false, false, false]; - var ENTER = 1; - var STAY = 2; - var LEAVE = 0; + const propVals = [false, false, false]; + const ENTER = 1; + const STAY = 2; + const LEAVE = 0; // Use order of tags (b/i/u) as order of nesting, for simplicity // and decent nesting. For example, // Just bold Bold and italics Just italics // becomes // Just bold Bold and italics Just italics - var taker = Changeset.stringIterator(text); - var assem = Changeset.stringAssembler(); + const taker = Changeset.stringIterator(text); + const assem = Changeset.stringAssembler(); - var idx = 0; + let idx = 0; function processNextChars(numChars) { if (numChars <= 0) { return; } - var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars)); + const iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars)); idx += numChars; while (iter.hasNext()) { - var o = iter.next(); + const o = iter.next(); var propChanged = false; - Changeset.eachAttribNumber(o.attribs, function(a) { + Changeset.eachAttribNumber(o.attribs, (a) => { if (a in anumMap) { - var i = anumMap[a]; // i = 0 => bold, etc. + const i = anumMap[a]; // i = 0 => bold, etc. if (!propVals[i]) { propVals[i] = ENTER; @@ -108,20 +108,18 @@ function getTXTFromAtext(pad, atext, authorColors) { // according to what happens at start of span if (propChanged) { // leaving bold (e.g.) also leaves italics, etc. - var left = false; + let left = false; for (var i = 0; i < propVals.length; i++) { - var v = propVals[i]; + const v = propVals[i]; if (!left) { if (v === LEAVE) { left = true; } - } else { - if (v === true) { - // tag will be closed and re-opened - propVals[i] = STAY; - } + } else if (v === true) { + // tag will be closed and re-opened + propVals[i] = STAY; } } @@ -129,11 +127,11 @@ function getTXTFromAtext(pad, atext, authorColors) { for (var i = propVals.length - 1; i >= 0; i--) { if (propVals[i] === LEAVE) { - //emitCloseTag(i); + // emitCloseTag(i); tags2close.push(i); propVals[i] = false; } else if (propVals[i] === STAY) { - //emitCloseTag(i); + // emitCloseTag(i); tags2close.push(i); } } @@ -146,13 +144,13 @@ function getTXTFromAtext(pad, atext, authorColors) { // propVals is now all {true,false} again } // end if (propChanged) - var chars = o.chars; + let chars = o.chars; if (o.lines) { // exclude newline at end of line, if present chars--; } - var s = taker.take(chars); + const s = taker.take(chars); // removes the characters with the code 12. Don't know where they come // from but they break the abiword parser and are completly useless @@ -172,14 +170,13 @@ function getTXTFromAtext(pad, atext, authorColors) { propVals[i] = false; } } - } // end processNextChars processNextChars(text.length - idx); - return(assem.toString()); + return (assem.toString()); } // end getLineHTML - var pieces = [css]; + const pieces = [css]; // Need to deal with constraints imposed on HTML lists; can // only gain one level of nesting at once, can't change type @@ -189,34 +186,33 @@ function getTXTFromAtext(pad, atext, authorColors) { // want to deal gracefully with blank lines. // => keeps track of the parents level of indentation - var listNumbers = {}; - var prevListLevel; + const listNumbers = {}; + let prevListLevel; - for (var i = 0; i < textLines.length; i++) { + for (let i = 0; i < textLines.length; i++) { + const line = _analyzeLine(textLines[i], attribLines[i], apool); + let lineContent = getLineTXT(line.text, line.aline); - var line = _analyzeLine(textLines[i], attribLines[i], apool); - var lineContent = getLineTXT(line.text, line.aline); - - if (line.listTypeName == "bullet") { - lineContent = "* " + lineContent; // add a bullet + if (line.listTypeName == 'bullet') { + lineContent = `* ${lineContent}`; // add a bullet } - if (line.listTypeName !== "number") { + if (line.listTypeName !== 'number') { // We're no longer in an OL so we can reset counting - for (var key in listNumbers) { + for (const key in listNumbers) { delete listNumbers[key]; } } if (line.listLevel > 0) { - for (var j = line.listLevel - 1; j >= 0; j--) { + for (let j = line.listLevel - 1; j >= 0; j--) { pieces.push('\t'); // tab indent list numbers.. - if(!listNumbers[line.listLevel]){ + if (!listNumbers[line.listLevel]) { listNumbers[line.listLevel] = 0; } } - if (line.listTypeName == "number") { + if (line.listTypeName == 'number') { /* * listLevel == amount of indentation * listNumber(s) == item number @@ -230,19 +226,19 @@ function getTXTFromAtext(pad, atext, authorColors) { * To handle going back to 2.1 when prevListLevel is lower number * than current line.listLevel then reset the object value */ - if(line.listLevel < prevListLevel){ + if (line.listLevel < prevListLevel) { delete listNumbers[prevListLevel]; } listNumbers[line.listLevel]++; - if(line.listLevel > 1){ - var x = 1; - while(x <= line.listLevel-1){ - pieces.push(listNumbers[x]+".") + if (line.listLevel > 1) { + let x = 1; + while (x <= line.listLevel - 1) { + pieces.push(`${listNumbers[x]}.`); x++; } } - pieces.push(listNumbers[line.listLevel]+". ") + pieces.push(`${listNumbers[line.listLevel]}. `); prevListLevel = line.listLevel; } @@ -257,7 +253,7 @@ function getTXTFromAtext(pad, atext, authorColors) { exports.getTXTFromAtext = getTXTFromAtext; -exports.getPadTXTDocument = async function(padId, revNum) { - let pad = await padManager.getPad(padId); +exports.getPadTXTDocument = async function (padId, revNum) { + const pad = await padManager.getPad(padId); return getPadTXT(pad, revNum); -} +}; diff --git a/src/node/utils/ImportEtherpad.js b/src/node/utils/ImportEtherpad.js index d2a58c2c9..0c0dbcc7a 100644 --- a/src/node/utils/ImportEtherpad.js +++ b/src/node/utils/ImportEtherpad.js @@ -14,14 +14,14 @@ * limitations under the License. */ -var log4js = require('log4js'); -const db = require("../db/DB"); +const log4js = require('log4js'); +const db = require('../db/DB'); const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -exports.setPadRaw = function(padId, records) { +exports.setPadRaw = function (padId, records) { records = JSON.parse(records); - Object.keys(records).forEach(async function(key) { + Object.keys(records).forEach(async (key) => { let value = records[key]; if (!value) { @@ -36,7 +36,7 @@ exports.setPadRaw = function(padId, records) { newKey = key; // Does this author already exist? - let author = await db.get(key); + const author = await db.get(key); if (author) { // Yes, add the padID to the author @@ -47,20 +47,20 @@ exports.setPadRaw = function(padId, records) { value = author; } else { // No, create a new array with the author info in - value.padIDs = [ padId ]; + value.padIDs = [padId]; } } else { // Not author data, probably pad data // we can split it to look to see if it's pad data - let oldPadId = key.split(":"); + const oldPadId = key.split(':'); // we know it's pad data - if (oldPadId[0] === "pad") { + if (oldPadId[0] === 'pad') { // so set the new pad id for the author oldPadId[1] = padId; // and create the value - newKey = oldPadId.join(":"); // create the new key + newKey = oldPadId.join(':'); // create the new key } // is this a key that is supported through a plugin? @@ -74,4 +74,4 @@ exports.setPadRaw = function(padId, records) { // Write the value to the server await db.set(newKey, value); }); -} +}; diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index 8cbfdbab9..9032eda77 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -14,75 +14,75 @@ * limitations under the License. */ -const log4js = require('log4js'); -const Changeset = require("ep_etherpad-lite/static/js/Changeset"); -const contentcollector = require("ep_etherpad-lite/static/js/contentcollector"); -const cheerio = require("cheerio"); -const rehype = require("rehype") -const format = require("rehype-format") +const log4js = require('log4js'); +const Changeset = require('ep_etherpad-lite/static/js/Changeset'); +const contentcollector = require('ep_etherpad-lite/static/js/contentcollector'); +const cheerio = require('cheerio'); +const rehype = require('rehype'); +const format = require('rehype-format'); exports.setPadHTML = async (pad, html) => { - var apiLogger = log4js.getLogger("ImportHtml"); + const apiLogger = log4js.getLogger('ImportHtml'); - var opts = { + const opts = { indentInitial: false, - indent: -1 - } + indent: -1, + }; rehype() - .use(format, opts) - .process(html, function(err, output){ - html = String(output).replace(/(\r\n|\n|\r)/gm,""); - }) + .use(format, opts) + .process(html, (err, output) => { + html = String(output).replace(/(\r\n|\n|\r)/gm, ''); + }); - var $ = cheerio.load(html); + const $ = cheerio.load(html); // Appends a line break, used by Etherpad to ensure a caret is available // below the last line of an import - $('body').append("

                "); + $('body').append('

                '); - var doc = $('html')[0]; + const doc = $('html')[0]; apiLogger.debug('html:'); apiLogger.debug(html); // Convert a dom tree into a list of lines and attribute liens // using the content collector object - var cc = contentcollector.makeContentCollector(true, null, pad.pool); + const cc = contentcollector.makeContentCollector(true, null, pad.pool); try { // we use a try here because if the HTML is bad it will blow up cc.collectContent(doc); - } catch(e) { - apiLogger.warn("HTML was not properly formed", e); + } catch (e) { + apiLogger.warn('HTML was not properly formed', e); // don't process the HTML because it was bad throw e; } - var result = cc.finish(); + const result = cc.finish(); apiLogger.debug('Lines:'); - var i; + let i; for (i = 0; i < result.lines.length; i++) { - apiLogger.debug('Line ' + (i + 1) + ' text: ' + result.lines[i]); - apiLogger.debug('Line ' + (i + 1) + ' attributes: ' + result.lineAttribs[i]); + apiLogger.debug(`Line ${i + 1} text: ${result.lines[i]}`); + apiLogger.debug(`Line ${i + 1} attributes: ${result.lineAttribs[i]}`); } // Get the new plain text and its attributes - var newText = result.lines.join('\n'); + const newText = result.lines.join('\n'); apiLogger.debug('newText:'); apiLogger.debug(newText); - var newAttribs = result.lineAttribs.join('|1+1') + '|1+1'; + const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`; - function eachAttribRun(attribs, func /*(startInNewText, endInNewText, attribs)*/ ) { - var attribsIter = Changeset.opIterator(attribs); - var textIndex = 0; - var newTextStart = 0; - var newTextEnd = newText.length; + function eachAttribRun(attribs, func /* (startInNewText, endInNewText, attribs)*/) { + const attribsIter = Changeset.opIterator(attribs); + let textIndex = 0; + const newTextStart = 0; + const newTextEnd = newText.length; while (attribsIter.hasNext()) { - var op = attribsIter.next(); - var nextIndex = textIndex + op.chars; + const op = attribsIter.next(); + const nextIndex = textIndex + op.chars; if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); } @@ -91,19 +91,19 @@ exports.setPadHTML = async (pad, html) => { } // create a new changeset with a helper builder object - var builder = Changeset.builder(1); + const builder = Changeset.builder(1); // assemble each line into the builder - eachAttribRun(newAttribs, function(start, end, attribs) { + eachAttribRun(newAttribs, (start, end, attribs) => { builder.insert(newText.substring(start, end), attribs); }); // the changeset is ready! - var theChangeset = builder.toString(); + const theChangeset = builder.toString(); - apiLogger.debug('The changeset: ' + theChangeset); + apiLogger.debug(`The changeset: ${theChangeset}`); await Promise.all([ pad.setText('\n'), pad.appendRevision(theChangeset), ]); -} +}; diff --git a/src/node/utils/LibreOffice.js b/src/node/utils/LibreOffice.js index 6ed28210e..74496cabd 100644 --- a/src/node/utils/LibreOffice.js +++ b/src/node/utils/LibreOffice.js @@ -16,18 +16,18 @@ * limitations under the License. */ -var async = require("async"); -var fs = require("fs"); -var log4js = require('log4js'); -var os = require("os"); -var path = require("path"); -var settings = require("./Settings"); -var spawn = require("child_process").spawn; +const async = require('async'); +const fs = require('fs'); +const log4js = require('log4js'); +const os = require('os'); +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 -var queue = async.queue(doConvertTask, 1); +const queue = async.queue(doConvertTask, 1); -var libreOfficeLogger = log4js.getLogger('LibreOffice'); +const libreOfficeLogger = log4js.getLogger('LibreOffice'); /** * Convert a file from one type to another @@ -37,18 +37,18 @@ var libreOfficeLogger = log4js.getLogger('LibreOffice'); * @param {String} type The type to convert into * @param {Function} callback Standard callback function */ -exports.convertFile = function(srcFile, destFile, type, callback) { +exports.convertFile = function (srcFile, destFile, type, callback) { // Used for the moving of the file, not the conversion - var fileExtension = type; + const fileExtension = type; - if (type === "html") { + if (type === 'html') { // "html:XHTML Writer File:UTF8" does a better job than normal html exports - if (path.extname(srcFile).toLowerCase() === ".doc") { - type = "html"; + 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" + if (path.extname(srcFile).toLowerCase() === '.pdf') { + type = 'html:XHTML Draw File'; } } @@ -57,58 +57,60 @@ exports.convertFile = function(srcFile, destFile, type, callback) { // to avoid `Error: no export filter for /tmp/xxxx.doc` error if (type === 'doc') { queue.push({ - "srcFile": srcFile, - "destFile": destFile.replace(/\.doc$/, '.odt'), - "type": 'odt', - "callback": function () { - queue.push({"srcFile": srcFile.replace(/\.html$/, '.odt'), "destFile": destFile, "type": type, "callback": callback, "fileExtension": fileExtension }); - } + srcFile, + destFile: destFile.replace(/\.doc$/, '.odt'), + type: 'odt', + callback() { + queue.push({srcFile: srcFile.replace(/\.html$/, '.odt'), destFile, type, callback, fileExtension}); + }, }); } else { - queue.push({"srcFile": srcFile, "destFile": destFile, "type": type, "callback": callback, "fileExtension": fileExtension}); + queue.push({srcFile, destFile, type, callback, fileExtension}); } }; function doConvertTask(task, callback) { - var tmpDir = os.tmpdir(); + const tmpDir = os.tmpdir(); async.series([ /* * use LibreOffice to convert task.srcFile to another format, given in * task.type */ - function(callback) { + function (callback) { libreOfficeLogger.debug(`Converting ${task.srcFile} to format ${task.type}. The result will be put in ${tmpDir}`); - var soffice = spawn(settings.soffice, [ + const soffice = spawn(settings.soffice, [ '--headless', '--invisible', '--nologo', '--nolockcheck', '--writer', - '--convert-to', task.type, + '--convert-to', + task.type, task.srcFile, - '--outdir', tmpDir + '--outdir', + tmpDir, ]); // Soffice/libreoffice is buggy and often hangs. // To remedy this we kill the spawned process after a while. const hangTimeout = setTimeout(() => { soffice.stdin.pause(); // required to kill hanging threads soffice.kill(); - }, 120000); - - var stdoutBuffer = ''; + }, 120000); + + let stdoutBuffer = ''; // Delegate the processing of stdout to another function - soffice.stdout.on('data', function(data) { + soffice.stdout.on('data', (data) => { stdoutBuffer += data.toString(); }); // Append error messages to the buffer - soffice.stderr.on('data', function(data) { + soffice.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); - soffice.on('exit', function(code) { + soffice.on('exit', (code) => { clearTimeout(hangTimeout); if (code != 0) { // Throw an exception if libreoffice failed @@ -117,18 +119,18 @@ function doConvertTask(task, callback) { // if LibreOffice exited succesfully, go on with processing callback(); - }) + }); }, // Move the converted file to the correct place - function(callback) { - var filename = path.basename(task.srcFile); - var sourceFilename = filename.substr(0, filename.lastIndexOf('.')) + '.' + task.fileExtension; - var sourcePath = path.join(tmpDir, sourceFilename); + function (callback) { + const filename = path.basename(task.srcFile); + const sourceFilename = `${filename.substr(0, filename.lastIndexOf('.'))}.${task.fileExtension}`; + const sourcePath = path.join(tmpDir, sourceFilename); libreOfficeLogger.debug(`Renaming ${sourcePath} to ${task.destFile}`); fs.rename(sourcePath, task.destFile, callback); - } - ], function(err) { + }, + ], (err) => { // Invoke the callback for the local queue callback(); diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index 8951d5159..0e5d66abc 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -19,31 +19,29 @@ * limitations under the License. */ -var ERR = require("async-stacktrace"); -var settings = require('./Settings'); -var async = require('async'); -var fs = require('fs'); -var StringDecoder = require('string_decoder').StringDecoder; -var CleanCSS = require('clean-css'); -var path = require('path'); -var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugin_defs"); -var RequireKernel = require('etherpad-require-kernel'); -var urlutil = require('url'); -var mime = require('mime-types') -var Threads = require('threads') -var log4js = require('log4js'); +const ERR = require('async-stacktrace'); +const settings = require('./Settings'); +const async = require('async'); +const fs = require('fs'); +const StringDecoder = require('string_decoder').StringDecoder; +const CleanCSS = require('clean-css'); +const path = require('path'); +const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs'); +const RequireKernel = require('etherpad-require-kernel'); +const urlutil = require('url'); +const mime = require('mime-types'); +const Threads = require('threads'); +const log4js = require('log4js'); -var logger = log4js.getLogger("Minify"); +const logger = log4js.getLogger('Minify'); -var ROOT_DIR = path.normalize(__dirname + "/../../static/"); -var TAR_PATH = path.join(__dirname, 'tar.json'); -var tar = JSON.parse(fs.readFileSync(TAR_PATH, 'utf8')); +const ROOT_DIR = path.normalize(`${__dirname}/../../static/`); +const TAR_PATH = path.join(__dirname, 'tar.json'); +const tar = JSON.parse(fs.readFileSync(TAR_PATH, 'utf8')); -var threadsPool = Threads.Pool(function () { - return Threads.spawn(new Threads.Worker("./MinifyWorker")) -}, 2) +const threadsPool = Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2); -var LIBRARY_WHITELIST = [ +const LIBRARY_WHITELIST = [ 'async', 'js-cookie', 'security', @@ -53,71 +51,68 @@ var LIBRARY_WHITELIST = [ ]; // Rewrite tar to include modules with no extensions and proper rooted paths. -var LIBRARY_PREFIX = 'ep_etherpad-lite/static/js'; +const LIBRARY_PREFIX = 'ep_etherpad-lite/static/js'; exports.tar = {}; function prefixLocalLibraryPath(path) { if (path.charAt(0) == '$') { return path.slice(1); } else { - return LIBRARY_PREFIX + '/' + path; + return `${LIBRARY_PREFIX}/${path}`; } } -for (var key in tar) { +for (const key in tar) { exports.tar[prefixLocalLibraryPath(key)] = tar[key].map(prefixLocalLibraryPath).concat( - tar[key].map(prefixLocalLibraryPath).map(function (p) { - return p.replace(/\.js$/, ''); - }) + tar[key].map(prefixLocalLibraryPath).map((p) => p.replace(/\.js$/, '')), ).concat( - tar[key].map(prefixLocalLibraryPath).map(function (p) { - return p.replace(/\.js$/, '') + '/index.js'; - }) + tar[key].map(prefixLocalLibraryPath).map((p) => `${p.replace(/\.js$/, '')}/index.js`), ); } // What follows is a terrible hack to avoid loop-back within the server. // TODO: Serve files from another service, or directly from the file system. function requestURI(url, method, headers, callback) { - var parsedURL = urlutil.parse(url); + const parsedURL = urlutil.parse(url); - var status = 500, headers = {}, content = []; + let status = 500; var headers = {}; const + content = []; - var mockRequest = { - url: url - , method: method - , params: {filename: parsedURL.path.replace(/^\/static\//, '')} - , headers: headers + const mockRequest = { + url, + method, + params: {filename: parsedURL.path.replace(/^\/static\//, '')}, + headers, }; - var mockResponse = { - writeHead: function (_status, _headers) { + const mockResponse = { + writeHead(_status, _headers) { status = _status; - for (var header in _headers) { + for (const header in _headers) { if (Object.prototype.hasOwnProperty.call(_headers, header)) { headers[header] = _headers[header]; } } - } - , setHeader: function (header, value) { + }, + setHeader(header, value) { headers[header.toLowerCase()] = value.toString(); - } - , header: function (header, value) { + }, + header(header, value) { headers[header.toLowerCase()] = value.toString(); - } - , write: function (_content) { - _content && content.push(_content); - } - , end: function (_content) { + }, + write(_content) { + _content && content.push(_content); + }, + end(_content) { _content && content.push(_content); callback(status, headers, content.join('')); - } + }, }; minify(mockRequest, mockResponse); } function requestURIs(locations, method, headers, callback) { - var pendingRequests = locations.length; - var responses = []; + let pendingRequests = locations.length; + const responses = []; function respondFor(i) { return function (status, headers, content) { @@ -128,14 +123,14 @@ function requestURIs(locations, method, headers, callback) { }; } - for (var i = 0, ii = locations.length; i < ii; i++) { + for (let i = 0, ii = locations.length; i < ii; i++) { requestURI(locations[i], method, headers, respondFor(i)); } function completed() { - var statuss = responses.map(function (x) {return x[0];}); - var headerss = responses.map(function (x) {return x[1];}); - var contentss = responses.map(function (x) {return x[2];}); + const statuss = responses.map((x) => x[0]); + const headerss = responses.map((x) => x[1]); + const contentss = responses.map((x) => x[2]); callback(statuss, headerss, contentss); } } @@ -146,15 +141,15 @@ function requestURIs(locations, method, headers, callback) { * @param res the Express response */ function minify(req, res) { - var filename = req.params['filename']; + let filename = req.params.filename; // No relative paths, especially if they may go up the file hierarchy. filename = path.normalize(path.join(ROOT_DIR, filename)); - filename = filename.replace(/\.\./g, '') + filename = filename.replace(/\.\./g, ''); if (filename.indexOf(ROOT_DIR) == 0) { filename = filename.slice(ROOT_DIR.length); - filename = filename.replace(/\\/g, '/') + filename = filename.replace(/\\/g, '/'); } else { res.writeHead(404, {}); res.end(); @@ -166,36 +161,36 @@ function minify(req, res) { are rewritten into ROOT_PATH_OF_MYPLUGIN/static/js/test.js, commonly ETHERPAD_ROOT/node_modules/ep_myplugin/static/js/test.js */ - var match = filename.match(/^plugins\/([^\/]+)(\/(?:(static\/.*)|.*))?$/); + const match = filename.match(/^plugins\/([^\/]+)(\/(?:(static\/.*)|.*))?$/); if (match) { - var library = match[1]; - var libraryPath = match[2] || ''; + const library = match[1]; + const libraryPath = match[2] || ''; if (plugins.plugins[library] && match[3]) { - var plugin = plugins.plugins[library]; - var pluginPath = plugin.package.realPath; + const plugin = plugins.plugins[library]; + const pluginPath = plugin.package.realPath; filename = path.relative(ROOT_DIR, pluginPath + libraryPath); filename = filename.replace(/\\/g, '/'); // windows path fix } else if (LIBRARY_WHITELIST.indexOf(library) != -1) { // Go straight into node_modules // Avoid `require.resolve()`, since 'mustache' and 'mustache/index.js' // would end up resolving to logically distinct resources. - filename = '../node_modules/' + library + libraryPath; + filename = `../node_modules/${library}${libraryPath}`; } } - var contentType = mime.lookup(filename); + const contentType = mime.lookup(filename); - statFile(filename, function (error, date, exists) { + statFile(filename, (error, date, exists) => { if (date) { date = new Date(date); date.setMilliseconds(0); res.setHeader('last-modified', date.toUTCString()); res.setHeader('date', (new Date()).toUTCString()); if (settings.maxAge !== undefined) { - var expiresDate = new Date(Date.now()+settings.maxAge*1000); + const expiresDate = new Date(Date.now() + settings.maxAge * 1000); res.setHeader('expires', expiresDate.toUTCString()); - res.setHeader('cache-control', 'max-age=' + settings.maxAge); + res.setHeader('cache-control', `max-age=${settings.maxAge}`); } } @@ -208,37 +203,35 @@ function minify(req, res) { } else if (new Date(req.headers['if-modified-since']) >= date) { res.writeHead(304, {}); res.end(); - } else { - if (req.method == 'HEAD') { - res.header("Content-Type", contentType); - res.writeHead(200, {}); - res.end(); - } else if (req.method == 'GET') { - getFileCompressed(filename, contentType, function (error, content) { - if(ERR(error, function(){ - res.writeHead(500, {}); - res.end(); - })) return; - res.header("Content-Type", contentType); - res.writeHead(200, {}); - res.write(content); + } else if (req.method == 'HEAD') { + res.header('Content-Type', contentType); + res.writeHead(200, {}); + res.end(); + } else if (req.method == 'GET') { + getFileCompressed(filename, contentType, (error, content) => { + if (ERR(error, () => { + res.writeHead(500, {}); res.end(); - }); - } else { - res.writeHead(405, {'allow': 'HEAD, GET'}); + })) return; + res.header('Content-Type', contentType); + res.writeHead(200, {}); + res.write(content); res.end(); - } + }); + } else { + res.writeHead(405, {allow: 'HEAD, GET'}); + res.end(); } }, 3); } // find all includes in ace.js and embed them. function getAceFile(callback) { - fs.readFile(ROOT_DIR + 'js/ace.js', "utf8", function(err, data) { - if(ERR(err, callback)) return; + fs.readFile(`${ROOT_DIR}js/ace.js`, 'utf8', (err, data) => { + if (ERR(err, callback)) return; // Find all includes in ace.js and embed them - var founds = data.match(/\$\$INCLUDE_[a-zA-Z_]+\("[^"]*"\)/gi); + let founds = data.match(/\$\$INCLUDE_[a-zA-Z_]+\("[^"]*"\)/gi); if (!settings.minify) { founds = []; } @@ -250,25 +243,25 @@ function getAceFile(callback) { // Request the contents of the included file on the server-side and write // them into the file. - async.forEach(founds, function (item, callback) { - var filename = item.match(/"([^"]*)"/)[1]; + async.forEach(founds, (item, callback) => { + const filename = item.match(/"([^"]*)"/)[1]; // Hostname "invalid.invalid" is a dummy value to allow parsing as a URI. - var baseURI = 'http://invalid.invalid'; - var resourceURI = baseURI + path.normalize(path.join('/static/', filename)); + const baseURI = 'http://invalid.invalid'; + let resourceURI = baseURI + path.normalize(path.join('/static/', filename)); resourceURI = resourceURI.replace(/\\/g, '/'); // Windows (safe generally?) - requestURI(resourceURI, 'GET', {}, function (status, headers, body) { - var error = !(status == 200 || status == 404); + requestURI(resourceURI, 'GET', {}, (status, headers, body) => { + const error = !(status == 200 || status == 404); if (!error) { - data += 'Ace2Editor.EMBEDED[' + JSON.stringify(filename) + '] = ' - + JSON.stringify(status == 200 ? body || '' : null) + ';\n'; + data += `Ace2Editor.EMBEDED[${JSON.stringify(filename)}] = ${ + JSON.stringify(status == 200 ? body || '' : null)};\n`; } else { console.error(`getAceFile(): error getting ${resourceURI}. Status code: ${status}`); } callback(); }); - }, function(error) { + }, (error) => { callback(error, data); }); }); @@ -289,19 +282,19 @@ function statFile(filename, callback, dirStatLimit) { } else if (filename == 'js/ace.js') { // Sometimes static assets are inlined into this file, so we have to stat // everything. - lastModifiedDateOfEverything(function (error, date) { + lastModifiedDateOfEverything((error, date) => { callback(error, date, !error); }); } else if (filename == 'js/require-kernel.js') { callback(null, requireLastModified(), true); } else { - fs.stat(ROOT_DIR + filename, function (error, stats) { + fs.stat(ROOT_DIR + filename, (error, stats) => { if (error) { - if (error.code == "ENOENT") { + if (error.code == 'ENOENT') { // Stat the directory instead. - statFile(path.dirname(filename), function (error, date, exists) { + statFile(path.dirname(filename), (error, date, exists) => { callback(error, date, false); - }, dirStatLimit-1); + }, dirStatLimit - 1); } else { callback(error); } @@ -314,29 +307,28 @@ function statFile(filename, callback, dirStatLimit) { } } function lastModifiedDateOfEverything(callback) { - var folders2check = [ROOT_DIR + 'js/', ROOT_DIR + 'css/']; - var latestModification = 0; - //go trough this two folders - async.forEach(folders2check, function(path, callback) { - //read the files in the folder - fs.readdir(path, function(err, files) { - if(ERR(err, callback)) return; + const folders2check = [`${ROOT_DIR}js/`, `${ROOT_DIR}css/`]; + let latestModification = 0; + // go trough this two folders + async.forEach(folders2check, (path, callback) => { + // read the files in the folder + fs.readdir(path, (err, files) => { + if (ERR(err, callback)) return; - //we wanna check the directory itself for changes too - files.push("."); + // we wanna check the directory itself for changes too + files.push('.'); - //go trough all files in this folder - async.forEach(files, function(filename, callback) { - //get the stat data of this file - fs.stat(path + "/" + filename, function(err, stats) { - if(ERR(err, callback)) return; + // go trough all files in this folder + async.forEach(files, (filename, callback) => { + // get the stat data of this file + fs.stat(`${path}/${filename}`, (err, stats) => { + if (ERR(err, callback)) return; - //get the modification time - var modificationTime = stats.mtime.getTime(); + // get the modification time + const modificationTime = stats.mtime.getTime(); - //compare the modification time to the highest found - if(modificationTime > latestModification) - { + // compare the modification time to the highest found + if (modificationTime > latestModification) { latestModification = modificationTime; } @@ -344,29 +336,29 @@ function lastModifiedDateOfEverything(callback) { }); }, callback); }); - }, function () { + }, () => { callback(null, latestModification); }); } // This should be provided by the module, but until then, just use startup // time. -var _requireLastModified = new Date(); +const _requireLastModified = new Date(); function requireLastModified() { return _requireLastModified.toUTCString(); } function requireDefinition() { - return 'var require = ' + RequireKernel.kernelSource + ';\n'; + return `var require = ${RequireKernel.kernelSource};\n`; } function getFileCompressed(filename, contentType, callback) { - getFile(filename, function (error, content) { + getFile(filename, (error, content) => { if (error || !content || !settings.minify) { callback(error, content); } else if (contentType == 'application/javascript') { - threadsPool.queue(async ({ compressJS }) => { + threadsPool.queue(async ({compressJS}) => { try { - logger.info('Compress JS file %s.', filename) + logger.info('Compress JS file %s.', filename); content = content.toString(); const compressResult = await compressJS(content); @@ -381,11 +373,11 @@ function getFileCompressed(filename, contentType, callback) { } callback(null, content); - }) + }); } else if (contentType == 'text/css') { - threadsPool.queue(async ({ compressCSS }) => { + threadsPool.queue(async ({compressCSS}) => { try { - logger.info('Compress CSS file %s.', filename) + logger.info('Compress CSS file %s.', filename); content = await compressCSS(filename, ROOT_DIR); } catch (error) { @@ -393,7 +385,7 @@ function getFileCompressed(filename, contentType, callback) { } callback(null, content); - }) + }); } else { callback(null, content); } diff --git a/src/node/utils/MinifyWorker.js b/src/node/utils/MinifyWorker.js index 6fbbd24fc..c8bc09eb1 100644 --- a/src/node/utils/MinifyWorker.js +++ b/src/node/utils/MinifyWorker.js @@ -2,10 +2,10 @@ * Worker thread to minify JS & CSS files out of the main NodeJS thread */ -var CleanCSS = require('clean-css'); -var Terser = require("terser"); -var path = require('path'); -var Threads = require('threads') +const CleanCSS = require('clean-css'); +const Terser = require('terser'); +const path = require('path'); +const Threads = require('threads'); function compressJS(content) { return Terser.minify(content); @@ -46,20 +46,20 @@ function compressCSS(filename, ROOT_DIR) { new CleanCSS({ rebase: true, rebaseTo: basePath, - }).minify([absPath], function (errors, minified) { - if (errors) return rej(errors) + }).minify([absPath], (errors, minified) => { + if (errors) return rej(errors); - return res(minified.styles) + 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, - compressCSS -}) + compressCSS, +}); diff --git a/src/node/utils/NodeVersion.js b/src/node/utils/NodeVersion.js index 1ebbcbca0..f237e6637 100644 --- a/src/node/utils/NodeVersion.js +++ b/src/node/utils/NodeVersion.js @@ -25,17 +25,17 @@ const semver = require('semver'); * * @param {String} minNodeVersion Minimum required Node version */ -exports.enforceMinNodeVersion = function(minNodeVersion) { +exports.enforceMinNodeVersion = function (minNodeVersion) { const currentNodeVersion = process.version; // we cannot use template literals, since we still do not know if we are // running under Node >= 4.0 if (semver.lt(currentNodeVersion, minNodeVersion)) { - console.error('Running Etherpad on Node ' + currentNodeVersion + ' is not supported. Please upgrade at least to Node ' + minNodeVersion); + console.error(`Running Etherpad on Node ${currentNodeVersion} is not supported. Please upgrade at least to Node ${minNodeVersion}`); process.exit(1); } - console.debug('Running on Node ' + currentNodeVersion + ' (minimum required Node version: ' + minNodeVersion + ')'); + console.debug(`Running on Node ${currentNodeVersion} (minimum required Node version: ${minNodeVersion})`); }; /** @@ -44,7 +44,7 @@ exports.enforceMinNodeVersion = function(minNodeVersion) { * @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 = function (lowestNonDeprecatedNodeVersion, epRemovalVersion) { const currentNodeVersion = process.version; if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) { diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 143d873a5..c4245b77c 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -26,17 +26,17 @@ * limitations under the License. */ -var absolutePaths = require('./AbsolutePaths'); -var fs = require("fs"); -var os = require("os"); -var path = require('path'); -var argv = require('./Cli').argv; -var npm = require("npm/lib/npm.js"); -var jsonminify = require("jsonminify"); -var log4js = require("log4js"); -var randomString = require("./randomstring"); -var suppressDisableMsg = " -- To suppress these warning messages change suppressErrorsInPadText to true in your settings.json\n"; -var _ = require("underscore"); +const absolutePaths = require('./AbsolutePaths'); +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'); +const suppressDisableMsg = ' -- To suppress these warning messages change suppressErrorsInPadText to true in your settings.json\n'; +const _ = require('underscore'); /* Root path of the installation */ exports.root = absolutePaths.findEtherpadRoot(); @@ -59,14 +59,14 @@ console.log(`Random string used for versioning assets: ${exports.randomVersionSt /** * The app title, visible e.g. in the browser window */ -exports.title = "Etherpad"; +exports.title = 'Etherpad'; /** * The app favicon fully specified url, visible e.g. in the browser window */ -exports.favicon = "favicon.ico"; -exports.faviconPad = "../" + exports.favicon; -exports.faviconTimeslider = "../../" + exports.favicon; +exports.favicon = 'favicon.ico'; +exports.faviconPad = `../${exports.favicon}`; +exports.faviconTimeslider = `../../${exports.favicon}`; /* * Skin name. @@ -76,12 +76,12 @@ exports.faviconTimeslider = "../../" + exports.favicon; */ exports.skinName = null; -exports.skinVariants = "super-light-toolbar super-light-editor light-background"; +exports.skinVariants = 'super-light-toolbar super-light-editor light-background'; /** * The IP ep-lite should listen to */ -exports.ip = "0.0.0.0"; +exports.ip = '0.0.0.0'; /** * The Port ep-lite should listen to @@ -107,60 +107,60 @@ exports.socketTransportProtocols = ['xhr-polling', 'jsonp-polling', 'htmlfile']; /* * The Type of the database */ -exports.dbType = "dirty"; +exports.dbType = 'dirty'; /** * This setting is passed with dbType to ueberDB to set up the database */ -exports.dbSettings = { "filename" : path.join(exports.root, "var/dirty.db") }; +exports.dbSettings = {filename: path.join(exports.root, 'var/dirty.db')}; /** * The default Text of a new pad */ -exports.defaultPadText = "Welcome to Etherpad!\n\nThis 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!\n\nEtherpad on Github: https:\/\/github.com\/ether\/etherpad-lite\n"; +exports.defaultPadText = 'Welcome to Etherpad!\n\nThis 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!\n\nEtherpad on Github: https:\/\/github.com\/ether\/etherpad-lite\n'; /** * The default Pad Settings for a user (Can be overridden by changing the setting */ exports.padOptions = { - "noColors": false, - "showControls": true, - "showChat": true, - "showLineNumbers": true, - "useMonospaceFont": false, - "userName": false, - "userColor": false, - "rtl": false, - "alwaysShowChat": false, - "chatAndUsers": false, - "lang": "en-gb" + noColors: false, + showControls: true, + showChat: true, + showLineNumbers: true, + useMonospaceFont: false, + userName: false, + userColor: false, + rtl: false, + alwaysShowChat: false, + chatAndUsers: false, + lang: 'en-gb', }, /** * Whether certain shortcut keys are enabled for a user in the pad */ exports.padShortcutEnabled = { - "altF9" : true, - "altC" : true, - "delete" : true, - "cmdShift2" : true, - "return" : true, - "esc" : true, - "cmdS" : true, - "tab" : true, - "cmdZ" : true, - "cmdY" : true, - "cmdB" : true, - "cmdI" : true, - "cmdU" : true, - "cmd5" : true, - "cmdShiftL" : true, - "cmdShiftN" : true, - "cmdShift1" : true, - "cmdShiftC" : true, - "cmdH" : true, - "ctrlHome" : true, - "pageUp" : true, - "pageDown" : true, + altF9: true, + altC: true, + delete: true, + cmdShift2: true, + return: true, + esc: true, + cmdS: true, + tab: true, + cmdZ: true, + cmdY: true, + cmdB: true, + cmdI: true, + cmdU: true, + cmd5: true, + cmdShiftL: true, + cmdShiftN: true, + cmdShift1: true, + cmdShiftC: true, + cmdH: true, + ctrlHome: true, + pageUp: true, + pageDown: true, }, /** @@ -168,20 +168,20 @@ exports.padShortcutEnabled = { */ exports.toolbar = { left: [ - ["bold", "italic", "underline", "strikethrough"], - ["orderedlist", "unorderedlist", "indent", "outdent"], - ["undo", "redo"], - ["clearauthorship"] + ['bold', 'italic', 'underline', 'strikethrough'], + ['orderedlist', 'unorderedlist', 'indent', 'outdent'], + ['undo', 'redo'], + ['clearauthorship'], ], right: [ - ["importexport", "timeslider", "savedrevision"], - ["settings", "embed"], - ["showusers"] + ['importexport', 'timeslider', 'savedrevision'], + ['settings', 'embed'], + ['showusers'], ], timeslider: [ - ["timeslider_export", "timeslider_settings", "timeslider_returnToPad"] - ] -} + ['timeslider_export', 'timeslider_settings', 'timeslider_returnToPad'], + ], +}; /** * A flag that requires any user to have a valid session (via the api) before accessing a pad @@ -196,7 +196,7 @@ exports.editOnly = false; /** * Max age that responses will have (affects caching layer). */ -exports.maxAge = 1000*60*60*6; // 6 hours +exports.maxAge = 1000 * 60 * 60 * 6; // 6 hours /** * A flag that shows if minification is enabled or not @@ -226,7 +226,7 @@ exports.allowUnknownFileEnds = true; /** * The log level of log4js */ -exports.loglevel = "INFO"; +exports.loglevel = 'INFO'; /** * Disable IP logging @@ -251,7 +251,7 @@ exports.indentationOnNewLine = true; /* * log4js appender configuration */ -exports.logconfig = { appenders: [{ type: "console" }]}; +exports.logconfig = {appenders: [{type: 'console'}]}; /* * Session Key, do not sure this. @@ -303,28 +303,28 @@ exports.scrollWhenFocusLineIsOutOfViewport = { /* * Percentage of viewport height to be additionally scrolled. */ - "percentage": { - "editionAboveViewport": 0, - "editionBelowViewport": 0 + percentage: { + editionAboveViewport: 0, + editionBelowViewport: 0, }, /* * Time (in milliseconds) used to animate the scroll transition. Set to 0 to * disable animation */ - "duration": 0, + duration: 0, /* * Percentage of viewport height to be additionally scrolled when user presses arrow up * in the line of the top of the viewport. */ - "percentageToScrollWhenUserPressesArrowUp": 0, + percentageToScrollWhenUserPressesArrowUp: 0, /* * Flag to control if it should scroll when user places the caret in the last * line of the viewport */ - "scrollWhenCaretIsInTheLastLineOfViewport": false + scrollWhenCaretIsInTheLastLineOfViewport: false, }; /* @@ -350,10 +350,10 @@ exports.customLocaleStrings = {}; */ exports.importExportRateLimiting = { // duration of the rate limit window (milliseconds) - "windowMs": 90000, + windowMs: 90000, // maximum number of requests per IP to allow during the rate limit window - "max": 10 + max: 10, }; /* @@ -366,10 +366,10 @@ exports.importExportRateLimiting = { */ exports.commitRateLimiting = { // duration of the rate limit window (seconds) - "duration": 1, + duration: 1, // maximum number of chanes per IP to allow during the rate limit window - "points": 10 + points: 10, }; /* @@ -381,64 +381,64 @@ exports.commitRateLimiting = { exports.importMaxFileSize = 50 * 1024 * 1024; // checks if abiword is avaiable -exports.abiwordAvailable = function() { +exports.abiwordAvailable = function () { if (exports.abiword != null) { - return os.type().indexOf("Windows") != -1 ? "withoutPDF" : "yes"; + return os.type().indexOf('Windows') != -1 ? 'withoutPDF' : 'yes'; } else { - return "no"; + return 'no'; } }; -exports.sofficeAvailable = function() { +exports.sofficeAvailable = function () { if (exports.soffice != null) { - return os.type().indexOf("Windows") != -1 ? "withoutPDF": "yes"; + return os.type().indexOf('Windows') != -1 ? 'withoutPDF' : 'yes'; } else { - return "no"; + return 'no'; } }; -exports.exportAvailable = function() { - var abiword = exports.abiwordAvailable(); - var soffice = exports.sofficeAvailable(); +exports.exportAvailable = function () { + const abiword = exports.abiwordAvailable(); + const soffice = exports.sofficeAvailable(); - if (abiword == "no" && soffice == "no") { - return "no"; - } else if ((abiword == "withoutPDF" && soffice == "no") || (abiword == "no" && soffice == "withoutPDF")) { - return "withoutPDF"; + if (abiword == 'no' && soffice == 'no') { + return 'no'; + } else if ((abiword == 'withoutPDF' && soffice == 'no') || (abiword == 'no' && soffice == 'withoutPDF')) { + return 'withoutPDF'; } else { - return "yes"; + return 'yes'; } }; // Provide git version if available -exports.getGitCommit = function() { - var version = ""; +exports.getGitCommit = function () { + let version = ''; try { - var rootPath = exports.root; - if (fs.lstatSync(rootPath + '/.git').isFile()) { - rootPath = fs.readFileSync(rootPath + '/.git', "utf8"); + let rootPath = exports.root; + if (fs.lstatSync(`${rootPath}/.git`).isFile()) { + rootPath = fs.readFileSync(`${rootPath}/.git`, 'utf8'); rootPath = rootPath.split(' ').pop().trim(); } else { rootPath += '/.git'; } - var ref = fs.readFileSync(rootPath + "/HEAD", "utf-8"); - if (ref.startsWith("ref: ")) { - var refPath = rootPath + "/" + ref.substring(5, ref.indexOf("\n")); - version = fs.readFileSync(refPath, "utf-8"); + const ref = fs.readFileSync(`${rootPath}/HEAD`, 'utf-8'); + if (ref.startsWith('ref: ')) { + const refPath = `${rootPath}/${ref.substring(5, ref.indexOf('\n'))}`; + version = fs.readFileSync(refPath, 'utf-8'); } else { version = ref; } version = version.substring(0, 7); - } catch(e) { - console.warn("Can't get git version for server header\n" + e.message) + } catch (e) { + console.warn(`Can't get git version for server header\n${e.message}`); } return version; -} +}; // Return etherpad version from package.json -exports.getEpVersion = function() { +exports.getEpVersion = function () { return require('ep_etherpad-lite/package.json').version; -} +}; /** * Receives a settingsObj and, if the property name is a valid configuration @@ -448,9 +448,9 @@ exports.getEpVersion = function() { * both "settings.json" and "credentials.json". */ function storeSettings(settingsObj) { - for (var i in settingsObj) { + for (const i in settingsObj) { // test if the setting starts with a lowercase character - if (i.charAt(0).search("[a-z]") !== 0) { + if (i.charAt(0).search('[a-z]') !== 0) { console.warn(`Settings should start with a lowercase character: '${i}'`); } @@ -482,26 +482,26 @@ function storeSettings(settingsObj) { * in the literal string "null", instead. */ function 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)); + // 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)); - if (isNumeric) { - // detected numeric string. Coerce to a number + if (isNumeric) { + // detected numeric string. Coerce to a number - return +stringValue; - } + return +stringValue; + } - // the boolean literal case is easy. - if (stringValue === "true" ) { - return true; - } + // the boolean literal case is easy. + if (stringValue === 'true') { + return true; + } - if (stringValue === "false") { - return false; - } + if (stringValue === 'false') { + return false; + } - // otherwise, return this value as-is - return stringValue; + // otherwise, return this value as-is + return stringValue; } /** @@ -624,24 +624,24 @@ function lookupEnvironmentVariables(obj) { * The isSettings variable only controls the error logging. */ function parseSettings(settingsFilename, isSettings) { - let settingsStr = ""; + let settingsStr = ''; let settingsType, notFoundMessage, notFoundFunction; if (isSettings) { - settingsType = "settings"; - notFoundMessage = "Continuing using defaults!"; + settingsType = 'settings'; + notFoundMessage = 'Continuing using defaults!'; notFoundFunction = console.warn; } else { - settingsType = "credentials"; - notFoundMessage = "Ignoring."; + settingsType = 'credentials'; + notFoundMessage = 'Ignoring.'; notFoundFunction = console.info; } try { - //read the settings file + // read the settings file settingsStr = fs.readFileSync(settingsFilename).toString(); - } catch(e) { + } catch (e) { notFoundFunction(`No ${settingsType} file found in ${settingsFilename}. ${notFoundMessage}`); // or maybe undefined! @@ -649,7 +649,7 @@ function parseSettings(settingsFilename, isSettings) { } try { - settingsStr = jsonminify(settingsStr).replace(",]","]").replace(",}","}"); + settingsStr = jsonminify(settingsStr).replace(',]', ']').replace(',}', '}'); const settings = JSON.parse(settingsStr); @@ -658,7 +658,7 @@ function parseSettings(settingsFilename, isSettings) { const replacedSettings = lookupEnvironmentVariables(settings); return replacedSettings; - } catch(e) { + } catch (e) { console.error(`There was an error processing your ${settingsType} file from ${settingsFilename}: ${e.message}`); process.exit(1); @@ -667,55 +667,55 @@ function parseSettings(settingsFilename, isSettings) { exports.reloadSettings = function reloadSettings() { // Discover where the settings file lives - var settingsFilename = absolutePaths.makeAbsolute(argv.settings || "settings.json"); + const settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json'); // Discover if a credential file exists - var credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || "credentials.json"); + const credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json'); // try to parse the settings - var settings = parseSettings(settingsFilename, true); + const settings = parseSettings(settingsFilename, true); // try to parse the credentials - var credentials = parseSettings(credentialsFilename, false); + const credentials = parseSettings(credentialsFilename, false); storeSettings(settings); storeSettings(credentials); - log4js.configure(exports.logconfig);//Configure the logging appenders - log4js.setGlobalLogLevel(exports.loglevel);//set loglevel - process.env['DEBUG'] = 'socket.io:' + exports.loglevel; // Used by SocketIO for Debug + log4js.configure(exports.logconfig);// Configure the logging appenders + log4js.setGlobalLogLevel(exports.loglevel);// set loglevel + process.env.DEBUG = `socket.io:${exports.loglevel}`; // Used by SocketIO for Debug log4js.replaceConsole(); if (!exports.skinName) { - console.warn(`No "skinName" parameter found. Please check out settings.json.template and update your settings.json. Falling back to the default "colibris".`); - exports.skinName = "colibris"; + console.warn('No "skinName" parameter found. Please check out settings.json.template and update your settings.json. Falling back to the default "colibris".'); + exports.skinName = 'colibris'; } // checks if skinName has an acceptable value, otherwise falls back to "colibris" if (exports.skinName) { - const skinBasePath = path.join(exports.root, "src", "static", "skins"); + const skinBasePath = path.join(exports.root, 'src', 'static', 'skins'); const countPieces = exports.skinName.split(path.sep).length; 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"; + exports.skinName = 'colibris'; } // informative variable, just for the log messages - var skinPath = path.normalize(path.join(skinBasePath, exports.skinName)); + let skinPath = path.normalize(path.join(skinBasePath, exports.skinName)); // what if someone sets skinName == ".." or "."? We catch him! if (absolutePaths.isSubdir(skinBasePath, skinPath) === false) { console.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. Falling back to the default "colibris".`); - exports.skinName = "colibris"; + exports.skinName = 'colibris'; skinPath = path.join(skinBasePath, exports.skinName); } if (fs.existsSync(skinPath) === false) { console.error(`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`); - exports.skinName = "colibris"; + exports.skinName = 'colibris'; skinPath = path.join(skinBasePath, exports.skinName); } @@ -725,13 +725,13 @@ exports.reloadSettings = function reloadSettings() { if (exports.abiword) { // Check abiword actually exists if (exports.abiword != null) { - fs.exists(exports.abiword, function(exists) { + fs.exists(exports.abiword, (exists) => { if (!exists) { - var abiwordError = "Abiword does not exist at this path, check your settings file."; + const abiwordError = 'Abiword does not exist at this path, check your settings file.'; if (!exports.suppressErrorsInPadText) { - exports.defaultPadText = exports.defaultPadText + "\nError: " + abiwordError + suppressDisableMsg; + exports.defaultPadText = `${exports.defaultPadText}\nError: ${abiwordError}${suppressDisableMsg}`; } - console.error(abiwordError + ` File location: ${exports.abiword}`); + console.error(`${abiwordError} File location: ${exports.abiword}`); exports.abiword = null; } }); @@ -739,46 +739,46 @@ exports.reloadSettings = function reloadSettings() { } if (exports.soffice) { - fs.exists(exports.soffice, function(exists) { + fs.exists(exports.soffice, (exists) => { if (!exists) { - var sofficeError = "soffice (libreoffice) does not exist at this path, check your settings file."; + const sofficeError = 'soffice (libreoffice) does not exist at this path, check your settings file.'; if (!exports.suppressErrorsInPadText) { - exports.defaultPadText = exports.defaultPadText + "\nError: " + sofficeError + suppressDisableMsg; + exports.defaultPadText = `${exports.defaultPadText}\nError: ${sofficeError}${suppressDisableMsg}`; } - console.error(sofficeError + ` File location: ${exports.soffice}`); + console.error(`${sofficeError} File location: ${exports.soffice}`); exports.soffice = null; } }); } if (!exports.sessionKey) { - var sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || "./SESSIONKEY.txt"); + const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt'); try { - exports.sessionKey = fs.readFileSync(sessionkeyFilename,"utf8"); + exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8'); console.info(`Session key loaded from: ${sessionkeyFilename}`); - } catch(e) { + } catch (e) { console.info(`Session key file "${sessionkeyFilename}" not found. Creating with random contents.`); exports.sessionKey = randomString(32); - fs.writeFileSync(sessionkeyFilename,exports.sessionKey,"utf8"); + fs.writeFileSync(sessionkeyFilename, exports.sessionKey, 'utf8'); } } else { - console.warn("Declaring the sessionKey in the settings.json is deprecated. This value is auto-generated now. Please remove the setting from the file. -- If you are seeing this error after restarting using the Admin User Interface then you can ignore this message."); + console.warn('Declaring the sessionKey in the settings.json is deprecated. This value is auto-generated now. Please remove the setting from the file. -- If you are seeing this error after restarting using the Admin User Interface then you can ignore this message.'); } - if (exports.dbType === "dirty") { - var dirtyWarning = "DirtyDB is used. This is fine for testing but not recommended for production."; + if (exports.dbType === 'dirty') { + const dirtyWarning = 'DirtyDB is used. This is fine for testing but not recommended for production.'; if (!exports.suppressErrorsInPadText) { - exports.defaultPadText = exports.defaultPadText + "\nWarning: " + dirtyWarning + suppressDisableMsg; + exports.defaultPadText = `${exports.defaultPadText}\nWarning: ${dirtyWarning}${suppressDisableMsg}`; } exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename); - console.warn(dirtyWarning + ` File location: ${exports.dbSettings.filename}`); + console.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`); } - if (exports.ip === "") { + if (exports.ip === '') { // using Unix socket for connectivity - console.warn(`The settings file contains an empty string ("") for the "ip" parameter. The "port" parameter will be interpreted as the path to a Unix socket to bind at.`); + console.warn('The settings file contains an empty string ("") for the "ip" parameter. The "port" parameter will be interpreted as the path to a Unix socket to bind at.'); } }; diff --git a/src/node/utils/TidyHtml.js b/src/node/utils/TidyHtml.js index 26d48a62f..42e3e3547 100644 --- a/src/node/utils/TidyHtml.js +++ b/src/node/utils/TidyHtml.js @@ -2,42 +2,41 @@ * Tidy up the HTML in a given file */ -var log4js = require('log4js'); -var settings = require('./Settings'); -var spawn = require('child_process').spawn; +const log4js = require('log4js'); +const settings = require('./Settings'); +const spawn = require('child_process').spawn; -exports.tidy = function(srcFile) { - var logger = log4js.getLogger('TidyHtml'); +exports.tidy = function (srcFile) { + const logger = log4js.getLogger('TidyHtml'); return new Promise((resolve, reject) => { - // Don't do anything if Tidy hasn't been enabled if (!settings.tidyHtml) { logger.debug('tidyHtml has not been configured yet, ignoring tidy request'); return resolve(null); } - var errMessage = ''; + let errMessage = ''; // Spawn a new tidy instance that cleans up the file inline - logger.debug('Tidying ' + srcFile); - var tidy = spawn(settings.tidyHtml, ['-modify', srcFile]); + logger.debug(`Tidying ${srcFile}`); + const tidy = spawn(settings.tidyHtml, ['-modify', srcFile]); // Keep track of any error messages - tidy.stderr.on('data', function (data) { + tidy.stderr.on('data', (data) => { errMessage += data.toString(); }); - tidy.on('close', function(code) { + tidy.on('close', (code) => { // Tidy returns a 0 when no errors occur and a 1 exit code when // the file could be tidied but a few warnings were generated if (code === 0 || code === 1) { - logger.debug('Tidied ' + srcFile + ' successfully'); + logger.debug(`Tidied ${srcFile} successfully`); resolve(null); } else { - logger.error('Failed to tidy ' + srcFile + '\n' + errMessage); - reject('Tidy died with exit code ' + code); + logger.error(`Failed to tidy ${srcFile}\n${errMessage}`); + reject(`Tidy died with exit code ${code}`); } }); }); -} +}; diff --git a/src/node/utils/UpdateCheck.js b/src/node/utils/UpdateCheck.js index 640aab719..8332ff204 100644 --- a/src/node/utils/UpdateCheck.js +++ b/src/node/utils/UpdateCheck.js @@ -5,8 +5,8 @@ const request = require('request'); let infos; function loadEtherpadInformations() { - return new Promise(function(resolve, reject) { - request('https://static.etherpad.org/info.json', function (er, response, body) { + return new Promise((resolve, reject) => { + request('https://static.etherpad.org/info.json', (er, response, body) => { if (er) return reject(er); try { @@ -16,29 +16,29 @@ function loadEtherpadInformations() { return reject(err); } }); - }) + }); } -exports.getLatestVersion = function() { +exports.getLatestVersion = function () { exports.needsUpdate(); return infos.latestVersion; -} +}; -exports.needsUpdate = function(cb) { - loadEtherpadInformations().then(function(info) { +exports.needsUpdate = function (cb) { + loadEtherpadInformations().then((info) => { if (semver.gt(info.latestVersion, settings.getEpVersion())) { if (cb) return cb(true); } - }).catch(function (err) { - console.error('Can not perform Etherpad update check: ' + err) + }).catch((err) => { + console.error(`Can not perform Etherpad update check: ${err}`); if (cb) return cb(false); - }) -} + }); +}; -exports.check = function() { - exports.needsUpdate(function (needsUpdate) { +exports.check = function () { + exports.needsUpdate((needsUpdate) => { if (needsUpdate) { - console.warn('Update available: Download the actual version ' + infos.latestVersion) + console.warn(`Update available: Download the actual version ${infos.latestVersion}`); } - }) -} + }); +}; diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.js index 6df4faa97..8bb3ab00d 100644 --- a/src/node/utils/caching_middleware.js +++ b/src/node/utils/caching_middleware.js @@ -14,13 +14,13 @@ * limitations under the License. */ -var async = require('async'); -var Buffer = require('buffer').Buffer; -var fs = require('fs'); -var path = require('path'); -var zlib = require('zlib'); -var settings = require('./Settings'); -var existsSync = require('./path_exists'); +const async = require('async'); +const Buffer = require('buffer').Buffer; +const fs = require('fs'); +const path = require('path'); +const zlib = require('zlib'); +const settings = require('./Settings'); +const existsSync = require('./path_exists'); /* * The crypto module can be absent on reduced node installations. @@ -42,13 +42,13 @@ try { _crypto = undefined; } -var CACHE_DIR = path.normalize(path.join(settings.root, 'var/')); +let CACHE_DIR = path.normalize(path.join(settings.root, 'var/')); CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined; -var responseCache = {}; +const responseCache = {}; function djb2Hash(data) { - const chars = data.split("").map(str => str.charCodeAt(0)); + const chars = data.split('').map((str) => str.charCodeAt(0)); return `${chars.reduce((prev, curr) => ((prev << 5) + prev) + curr, 5381)}`; } @@ -81,23 +81,23 @@ function CachingMiddleware() { } CachingMiddleware.prototype = new function () { function handle(req, res, next) { - if (!(req.method == "GET" || req.method == "HEAD") || !CACHE_DIR) { + if (!(req.method == 'GET' || req.method == 'HEAD') || !CACHE_DIR) { return next(undefined, req, res); } - var old_req = {}; - var old_res = {}; + const old_req = {}; + const old_res = {}; - var supportsGzip = + const supportsGzip = (req.get('Accept-Encoding') || '').indexOf('gzip') != -1; - var path = require('url').parse(req.url).path; - var cacheKey = generateCacheKey(path); + const path = require('url').parse(req.url).path; + const cacheKey = generateCacheKey(path); - fs.stat(CACHE_DIR + 'minified_' + cacheKey, function (error, stats) { - var modifiedSince = (req.headers['if-modified-since'] - && new Date(req.headers['if-modified-since'])); - var lastModifiedCache = !error && stats.mtime; + fs.stat(`${CACHE_DIR}minified_${cacheKey}`, (error, stats) => { + const modifiedSince = (req.headers['if-modified-since'] && + new Date(req.headers['if-modified-since'])); + const lastModifiedCache = !error && stats.mtime; if (lastModifiedCache && responseCache[cacheKey]) { req.headers['if-modified-since'] = lastModifiedCache.toUTCString(); } else { @@ -108,13 +108,13 @@ CachingMiddleware.prototype = new function () { old_req.method = req.method; req.method = 'GET'; - var expirationDate = new Date(((responseCache[cacheKey] || {}).headers || {})['expires']); + const expirationDate = new Date(((responseCache[cacheKey] || {}).headers || {}).expires); if (expirationDate > new Date()) { // Our cached version is still valid. return respond(); } - var _headers = {}; + const _headers = {}; old_res.setHeader = res.setHeader; res.setHeader = function (key, value) { // Don't set cookies, see issue #707 @@ -126,46 +126,46 @@ CachingMiddleware.prototype = new function () { old_res.writeHead = res.writeHead; res.writeHead = function (status, headers) { - var lastModified = (res.getHeader('last-modified') - && new Date(res.getHeader('last-modified'))); + const lastModified = (res.getHeader('last-modified') && + new Date(res.getHeader('last-modified'))); res.writeHead = old_res.writeHead; if (status == 200) { // Update cache - var buffer = ''; + let buffer = ''; - Object.keys(headers || {}).forEach(function (key) { + Object.keys(headers || {}).forEach((key) => { res.setHeader(key, headers[key]); }); headers = _headers; old_res.write = res.write; old_res.end = res.end; - res.write = function(data, encoding) { + res.write = function (data, encoding) { buffer += data.toString(encoding); }; - res.end = function(data, encoding) { + res.end = function (data, encoding) { async.parallel([ function (callback) { - var path = CACHE_DIR + 'minified_' + cacheKey; - fs.writeFile(path, buffer, function (error, stats) { + const path = `${CACHE_DIR}minified_${cacheKey}`; + fs.writeFile(path, buffer, (error, stats) => { callback(); }); - } - , function (callback) { - var path = CACHE_DIR + 'minified_' + cacheKey + '.gz'; - zlib.gzip(buffer, function(error, content) { + }, + function (callback) { + const path = `${CACHE_DIR}minified_${cacheKey}.gz`; + zlib.gzip(buffer, (error, content) => { if (error) { callback(); } else { - fs.writeFile(path, content, function (error, stats) { + fs.writeFile(path, content, (error, stats) => { callback(); }); } }); - } - ], function () { - responseCache[cacheKey] = {statusCode: status, headers: headers}; + }, + ], () => { + responseCache[cacheKey] = {statusCode: status, headers}; respond(); }); }; @@ -173,8 +173,8 @@ CachingMiddleware.prototype = new function () { // Nothing new changed from the cached version. old_res.write = res.write; old_res.end = res.end; - res.write = function(data, encoding) {}; - res.end = function(data, encoding) { respond(); }; + res.write = function (data, encoding) {}; + res.end = function (data, encoding) { respond(); }; } else { res.writeHead(status, headers); } @@ -191,24 +191,24 @@ CachingMiddleware.prototype = new function () { res.write = old_res.write || res.write; res.end = old_res.end || res.end; - let headers = {}; + const headers = {}; Object.assign(headers, (responseCache[cacheKey].headers || {})); - var statusCode = responseCache[cacheKey].statusCode; + const statusCode = responseCache[cacheKey].statusCode; - var pathStr = CACHE_DIR + 'minified_' + cacheKey; + let pathStr = `${CACHE_DIR}minified_${cacheKey}`; if (supportsGzip && /application\/javascript/.test(headers['content-type'])) { - pathStr = pathStr + '.gz'; + pathStr += '.gz'; headers['content-encoding'] = 'gzip'; } - var lastModified = (headers['last-modified'] - && new Date(headers['last-modified'])); + const lastModified = (headers['last-modified'] && + new Date(headers['last-modified'])); if (statusCode == 200 && lastModified <= modifiedSince) { res.writeHead(304, headers); res.end(); } else if (req.method == 'GET') { - var readStream = fs.createReadStream(pathStr); + const readStream = fs.createReadStream(pathStr); res.writeHead(statusCode, headers); readStream.pipe(res); } else { diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js index 3018813f7..2fc36660e 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.js @@ -1,17 +1,17 @@ -var Changeset = require("../../static/js/Changeset"); -var exportHtml = require('./ExportHtml'); +const Changeset = require('../../static/js/Changeset'); +const exportHtml = require('./ExportHtml'); -function PadDiff (pad, fromRev, toRev) { +function PadDiff(pad, fromRev, toRev) { // check parameters if (!pad || !pad.id || !pad.atext || !pad.pool) { throw new Error('Invalid pad'); } - var range = pad.getValidRevisionRange(fromRev, toRev); + const range = pad.getValidRevisionRange(fromRev, toRev); if (!range) { - throw new Error('Invalid revision range.' + - ' startRev: ' + fromRev + - ' endRev: ' + toRev); + throw new Error(`${'Invalid revision range.' + + ' startRev: '}${fromRev + } endRev: ${toRev}`); } this._pad = pad; @@ -21,12 +21,12 @@ function PadDiff (pad, fromRev, toRev) { this._authors = []; } -PadDiff.prototype._isClearAuthorship = function(changeset) { +PadDiff.prototype._isClearAuthorship = function (changeset) { // unpack - var unpacked = Changeset.unpack(changeset); + const unpacked = Changeset.unpack(changeset); // check if there is nothing in the charBank - if (unpacked.charBank !== "") { + if (unpacked.charBank !== '') { return false; } @@ -36,10 +36,10 @@ PadDiff.prototype._isClearAuthorship = function(changeset) { } // lets iterator over the operators - var iterator = Changeset.opIterator(unpacked.ops); + const iterator = Changeset.opIterator(unpacked.ops); // get the first operator, this should be a clear operator - var clearOperator = iterator.next(); + const clearOperator = iterator.next(); // check if there is only one operator if (iterator.hasNext() === true) { @@ -47,18 +47,18 @@ PadDiff.prototype._isClearAuthorship = function(changeset) { } // check if this operator doesn't change text - if (clearOperator.opcode !== "=") { + if (clearOperator.opcode !== '=') { return false; } // check that this operator applys to the complete text // if the text ends with a new line, its exactly one character less, else it has the same length - if (clearOperator.chars !== unpacked.oldLen-1 && clearOperator.chars !== unpacked.oldLen) { + if (clearOperator.chars !== unpacked.oldLen - 1 && clearOperator.chars !== unpacked.oldLen) { return false; } - var attributes = []; - Changeset.eachAttribNumber(changeset, function(attrNum) { + const attributes = []; + Changeset.eachAttribNumber(changeset, (attrNum) => { attributes.push(attrNum); }); @@ -67,90 +67,84 @@ PadDiff.prototype._isClearAuthorship = function(changeset) { return false; } - var appliedAttribute = this._pad.pool.getAttrib(attributes[0]); + const appliedAttribute = this._pad.pool.getAttrib(attributes[0]); // check if the applied attribute is an anonymous author attribute - if (appliedAttribute[0] !== "author" || appliedAttribute[1] !== "") { + if (appliedAttribute[0] !== 'author' || appliedAttribute[1] !== '') { return false; } return true; }; -PadDiff.prototype._createClearAuthorship = async function(rev) { - - let atext = await this._pad.getInternalRevisionAText(rev); +PadDiff.prototype._createClearAuthorship = async function (rev) { + const atext = await this._pad.getInternalRevisionAText(rev); // build clearAuthorship changeset - var builder = Changeset.builder(atext.text.length); - builder.keepText(atext.text, [['author','']], this._pad.pool); - var changeset = builder.toString(); + const builder = Changeset.builder(atext.text.length); + builder.keepText(atext.text, [['author', '']], this._pad.pool); + const changeset = builder.toString(); return changeset; -} - -PadDiff.prototype._createClearStartAtext = async function(rev) { +}; +PadDiff.prototype._createClearStartAtext = async function (rev) { // get the atext of this revision - let atext = this._pad.getInternalRevisionAText(rev); + const atext = this._pad.getInternalRevisionAText(rev); // create the clearAuthorship changeset - let changeset = await this._createClearAuthorship(rev); + const changeset = await this._createClearAuthorship(rev); // apply the clearAuthorship changeset - let newAText = Changeset.applyToAText(changeset, atext, this._pad.pool); + const newAText = Changeset.applyToAText(changeset, atext, this._pad.pool); return newAText; -} - -PadDiff.prototype._getChangesetsInBulk = async function(startRev, count) { +}; +PadDiff.prototype._getChangesetsInBulk = async function (startRev, count) { // find out which revisions we need - let revisions = []; + const revisions = []; for (let i = startRev; i < (startRev + count) && i <= this._pad.head; i++) { revisions.push(i); } // get all needed revisions (in parallel) - let changesets = [], authors = []; - await Promise.all(revisions.map(rev => { - return this._pad.getRevision(rev).then(revision => { - let arrayNum = rev - startRev; - changesets[arrayNum] = revision.changeset; - authors[arrayNum] = revision.meta.author; - }); - })); + const changesets = []; const + authors = []; + await Promise.all(revisions.map((rev) => this._pad.getRevision(rev).then((revision) => { + const arrayNum = rev - startRev; + changesets[arrayNum] = revision.changeset; + authors[arrayNum] = revision.meta.author; + }))); - return { changesets, authors }; -} + return {changesets, authors}; +}; -PadDiff.prototype._addAuthors = function(authors) { - var self = this; +PadDiff.prototype._addAuthors = function (authors) { + const self = this; // add to array if not in the array - authors.forEach(function(author) { + authors.forEach((author) => { if (self._authors.indexOf(author) == -1) { self._authors.push(author); } }); }; -PadDiff.prototype._createDiffAtext = async function() { - - let bulkSize = 100; +PadDiff.prototype._createDiffAtext = async function () { + const bulkSize = 100; // get the cleaned startAText let atext = await this._createClearStartAtext(this._fromRev); let superChangeset = null; - let rev = this._fromRev + 1; + const rev = this._fromRev + 1; for (let rev = this._fromRev + 1; rev <= this._toRev; rev += bulkSize) { - // get the bulk - let { changesets, authors } = await this._getChangesetsInBulk(rev, bulkSize); + const {changesets, authors} = await this._getChangesetsInBulk(rev, bulkSize); - let addedAuthors = []; + const addedAuthors = []; // run through all changesets for (let i = 0; i < changesets.length && (rev + i) <= this._toRev; ++i) { @@ -180,7 +174,7 @@ PadDiff.prototype._createDiffAtext = async function() { // if there are only clearAuthorship changesets, we don't get a superChangeset, so we can skip this step if (superChangeset) { - let deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool); + const deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool); // apply the superChangeset, which includes all addings atext = Changeset.applyToAText(superChangeset, atext, this._pad.pool); @@ -190,59 +184,57 @@ PadDiff.prototype._createDiffAtext = async function() { } return atext; -} - -PadDiff.prototype.getHtml = async function() { +}; +PadDiff.prototype.getHtml = async function () { // cache the html if (this._html != null) { return this._html; } // get the diff atext - let atext = await this._createDiffAtext(); + const atext = await this._createDiffAtext(); // get the authorColor table - let authorColors = await this._pad.getAllAuthorColors(); + const authorColors = await this._pad.getAllAuthorColors(); // convert the atext to html this._html = await exportHtml.getHTMLFromAtext(this._pad, atext, authorColors); return this._html; -} - -PadDiff.prototype.getAuthors = async function() { +}; +PadDiff.prototype.getAuthors = async function () { // check if html was already produced, if not produce it, this generates the author array at the same time if (this._html == null) { await this.getHtml(); } return self._authors; -} +}; -PadDiff.prototype._extendChangesetWithAuthor = function(changeset, author, apool) { +PadDiff.prototype._extendChangesetWithAuthor = function (changeset, author, apool) { // unpack - var unpacked = Changeset.unpack(changeset); + const unpacked = Changeset.unpack(changeset); - var iterator = Changeset.opIterator(unpacked.ops); - var assem = Changeset.opAssembler(); + const iterator = Changeset.opIterator(unpacked.ops); + const assem = Changeset.opAssembler(); // create deleted attribs - var authorAttrib = apool.putAttrib(["author", author || ""]); - var deletedAttrib = apool.putAttrib(["removed", true]); - var attribs = "*" + Changeset.numToString(authorAttrib) + "*" + Changeset.numToString(deletedAttrib); + const authorAttrib = apool.putAttrib(['author', author || '']); + const deletedAttrib = apool.putAttrib(['removed', true]); + const attribs = `*${Changeset.numToString(authorAttrib)}*${Changeset.numToString(deletedAttrib)}`; // iteratore over the operators of the changeset - while(iterator.hasNext()) { - var operator = iterator.next(); + while (iterator.hasNext()) { + const operator = iterator.next(); - if (operator.opcode === "-") { + if (operator.opcode === '-') { // this is a delete operator, extend it with the author operator.attribs = attribs; - } else if (operator.opcode === "=" && operator.attribs) { + } else if (operator.opcode === '=' && operator.attribs) { // this is operator changes only attributes, let's mark which author did that - operator.attribs+="*"+Changeset.numToString(authorAttrib); + operator.attribs += `*${Changeset.numToString(authorAttrib)}`; } // append the new operator to our assembler @@ -254,9 +246,9 @@ PadDiff.prototype._extendChangesetWithAuthor = function(changeset, author, apool }; // 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) { - var lines = Changeset.splitTextLines(startAText.text); - var alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text); +PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { + const lines = Changeset.splitTextLines(startAText.text); + const alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text); // lines and alines are what the exports is meant to apply to. // They may be arrays or objects with .get(i) and .length methods. @@ -278,24 +270,23 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { } } - var curLine = 0; - var curChar = 0; - var curLineOpIter = null; - var curLineOpIterLine; - var curLineNextOp = Changeset.newOp('+'); + let curLine = 0; + let curChar = 0; + let curLineOpIter = null; + let curLineOpIterLine; + const curLineNextOp = Changeset.newOp('+'); - var unpacked = Changeset.unpack(cs); - var csIter = Changeset.opIterator(unpacked.ops); - var builder = Changeset.builder(unpacked.newLen); - - function consumeAttribRuns(numChars, func /*(len, attribs, endsLine)*/ ) { + const unpacked = Changeset.unpack(cs); + const csIter = Changeset.opIterator(unpacked.ops); + const builder = Changeset.builder(unpacked.newLen); + function consumeAttribRuns(numChars, func /* (len, attribs, endsLine)*/) { if ((!curLineOpIter) || (curLineOpIterLine != curLine)) { // create curLineOpIter and advance it to curChar curLineOpIter = Changeset.opIterator(alines_get(curLine)); curLineOpIterLine = curLine; - var indexIntoLine = 0; - var done = false; + let indexIntoLine = 0; + let done = false; while (!done) { curLineOpIter.next(curLineNextOp); if (indexIntoLine + curLineNextOp.chars >= curChar) { @@ -320,7 +311,7 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { curLineOpIter.next(curLineNextOp); } - var charsToUse = Math.min(numChars, curLineNextOp.chars); + const charsToUse = Math.min(numChars, curLineNextOp.chars); func(charsToUse, curLineNextOp.attribs, charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0); numChars -= charsToUse; @@ -338,26 +329,24 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { if (L) { curLine += L; curChar = 0; + } else if (curLineOpIter && curLineOpIterLine == curLine) { + consumeAttribRuns(N, () => {}); } else { - if (curLineOpIter && curLineOpIterLine == curLine) { - consumeAttribRuns(N, function () {}); - } else { - curChar += N; - } + curChar += N; } } function nextText(numChars) { - var len = 0; - var assem = Changeset.stringAssembler(); - var firstString = lines_get(curLine).substring(curChar); + let len = 0; + const assem = Changeset.stringAssembler(); + const firstString = lines_get(curLine).substring(curChar); len += firstString.length; assem.append(firstString); - var lineNum = curLine + 1; + let lineNum = curLine + 1; while (len < numChars) { - var nextString = lines_get(lineNum); + const nextString = lines_get(lineNum); len += nextString.length; assem.append(nextString); lineNum++; @@ -367,7 +356,7 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { } function cachedStrFunc(func) { - var cache = {}; + const cache = {}; return function (s) { if (!cache[s]) { @@ -377,8 +366,8 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { }; } - var attribKeys = []; - var attribValues = []; + const attribKeys = []; + const attribValues = []; // iterate over all operators of this changeset while (csIter.hasNext()) { @@ -389,27 +378,27 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { // 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 != "*") { - var deletedAttrib = apool.putAttrib(["removed", true]); - var authorAttrib = apool.putAttrib(["author", ""]); + if (csOp.attribs && textBank != '*') { + const deletedAttrib = apool.putAttrib(['removed', true]); + var authorAttrib = apool.putAttrib(['author', '']); attribKeys.length = 0; attribValues.length = 0; - Changeset.eachAttribNumber(csOp.attribs, function (n) { + Changeset.eachAttribNumber(csOp.attribs, (n) => { attribKeys.push(apool.getAttribKey(n)); attribValues.push(apool.getAttribValue(n)); - if (apool.getAttribKey(n) === "author") { + if (apool.getAttribKey(n) === 'author') { authorAttrib = n; } }); - var undoBackToAttribs = cachedStrFunc(function (attribs) { - var backAttribs = []; - for (var i = 0; i < attribKeys.length; i++) { - var appliedKey = attribKeys[i]; - var appliedValue = attribValues[i]; - var oldValue = Changeset.attribsAttributeValue(attribs, appliedKey, apool); + var 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) { backAttribs.push([appliedKey, oldValue]); @@ -419,21 +408,21 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { return Changeset.makeAttribsString('=', backAttribs, apool); }); - var oldAttribsAddition = "*" + Changeset.numToString(deletedAttrib) + "*" + Changeset.numToString(authorAttrib); + var oldAttribsAddition = `*${Changeset.numToString(deletedAttrib)}*${Changeset.numToString(authorAttrib)}`; - var textLeftToProcess = textBank; + let textLeftToProcess = textBank; - while(textLeftToProcess.length > 0) { + while (textLeftToProcess.length > 0) { // process till the next line break or process only one line break - var lengthToProcess = textLeftToProcess.indexOf("\n"); - var lineBreak = false; - switch(lengthToProcess) { + let lengthToProcess = textLeftToProcess.indexOf('\n'); + let lineBreak = false; + switch (lengthToProcess) { case -1: - lengthToProcess=textLeftToProcess.length; + lengthToProcess = textLeftToProcess.length; break; case 0: lineBreak = true; - lengthToProcess=1; + lengthToProcess = 1; break; } @@ -446,13 +435,13 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { builder.keep(1, 1); // just skip linebreaks, don't do a insert + keep for a linebreak // consume the attributes of this linebreak - consumeAttribRuns(1, function() {}); + 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; - consumeAttribRuns(lengthToProcess, function (len, attribs, endsLine) { + consumeAttribRuns(lengthToProcess, (len, attribs, endsLine) => { // get the old attributes back - var attribs = (undoBackToAttribs(attribs) || "") + oldAttribsAddition; + var attribs = (undoBackToAttribs(attribs) || '') + oldAttribsAddition; builder.insert(processText.substr(textBankIndex, len), attribs); textBankIndex += len; @@ -471,7 +460,7 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { var textBank = nextText(csOp.chars); var textBankIndex = 0; - consumeAttribRuns(csOp.chars, function (len, attribs, endsLine) { + consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => { builder.insert(textBank.substr(textBankIndex, len), attribs + csOp.attribs); textBankIndex += len; }); diff --git a/src/node/utils/path_exists.js b/src/node/utils/path_exists.js index c2d43f6c2..18dc35270 100644 --- a/src/node/utils/path_exists.js +++ b/src/node/utils/path_exists.js @@ -1,15 +1,15 @@ -var fs = require('fs'); +const fs = require('fs'); -var check = function(path) { - var existsSync = fs.statSync || fs.existsSync || path.existsSync; +const check = function (path) { + const existsSync = fs.statSync || fs.existsSync || path.existsSync; - var result; + let result; try { result = existsSync(path); } catch (e) { result = false; } return result; -} +}; module.exports = check; diff --git a/src/node/utils/promises.js b/src/node/utils/promises.js index bb973befa..60c1cff2a 100644 --- a/src/node/utils/promises.js +++ b/src/node/utils/promises.js @@ -13,7 +13,7 @@ exports.firstSatisfies = (promises, predicate) => { // value does not satisfy `predicate`. These transformed Promises will be passed to Promise.race, // yielding the first resolved value that satisfies `predicate`. const newPromises = promises.map( - (p) => new Promise((resolve, reject) => p.then((v) => predicate(v) && resolve(v), reject))); + (p) => new Promise((resolve, reject) => p.then((v) => predicate(v) && resolve(v), reject))); // If `promises` is an empty array or if none of them resolve to a value that satisfies // `predicate`, then `Promise.race(newPromises)` will never resolve. To handle that, add another @@ -48,8 +48,8 @@ exports.timesLimit = async (total, concurrency, promiseCreator) => { if (next < total) return addAnother(); }); const promises = []; - for (var i = 0; i < concurrency && i < total; i++) { + for (let i = 0; i < concurrency && i < total; i++) { promises.push(addAnother()); } await Promise.all(promises); -} +}; diff --git a/src/node/utils/randomstring.js b/src/node/utils/randomstring.js index f12bd7f78..622b0082d 100644 --- a/src/node/utils/randomstring.js +++ b/src/node/utils/randomstring.js @@ -1,10 +1,10 @@ /** * Generates a random String with the given length. Is needed to generate the Author, Group, readonly, session Ids */ -var crypto = require('crypto'); +const crypto = require('crypto'); -var randomString = function(len) { - return crypto.randomBytes(len).toString('hex') +const randomString = function (len) { + return crypto.randomBytes(len).toString('hex'); }; module.exports = randomString; diff --git a/src/node/utils/toolbar.js b/src/node/utils/toolbar.js index a2b8f5579..9e036ab47 100644 --- a/src/node/utils/toolbar.js +++ b/src/node/utils/toolbar.js @@ -1,53 +1,50 @@ /** * The Toolbar Module creates and renders the toolbars and buttons */ -var _ = require("underscore") - , tagAttributes - , tag - , Button - , ButtonsGroup - , Separator - , defaultButtonAttributes - , removeItem; +const _ = require('underscore'); +let tagAttributes; +let tag; +let Button; +let ButtonsGroup; +let Separator; +let defaultButtonAttributes; +let removeItem; -removeItem = function(array,what) { - var ax; +removeItem = function (array, what) { + let ax; while ((ax = array.indexOf(what)) !== -1) { array.splice(ax, 1); } - return array; + return array; }; defaultButtonAttributes = function (name, overrides) { return { command: name, - localizationId: "pad.toolbar." + name + ".title", - class: "buttonicon buttonicon-" + name + localizationId: `pad.toolbar.${name}.title`, + class: `buttonicon buttonicon-${name}`, }; }; tag = function (name, attributes, contents) { - var aStr = tagAttributes(attributes); + const aStr = tagAttributes(attributes); if (_.isString(contents) && contents.length > 0) { - return '<' + name + aStr + '>' + contents + ''; - } - else { - return '<' + name + aStr + '>'; + return `<${name}${aStr}>${contents}`; + } else { + return `<${name}${aStr}>`; } }; tagAttributes = function (attributes) { - attributes = _.reduce(attributes || {}, function (o, val, name) { + attributes = _.reduce(attributes || {}, (o, val, name) => { if (!_.isUndefined(val)) { o[name] = val; } return o; }, {}); - return " " + _.map(attributes, function (val, name) { - return "" + name + '="' + _.escape(val) + '"'; - }).join(" "); + return ` ${_.map(attributes, (val, name) => `${name}="${_.escape(val)}"`).join(' ')}`; }; ButtonsGroup = function () { @@ -55,8 +52,8 @@ ButtonsGroup = function () { }; ButtonsGroup.fromArray = function (array) { - var btnGroup = new this; - _.each(array, function (btnName) { + const btnGroup = new this(); + _.each(array, (btnName) => { btnGroup.addButton(Button.load(btnName)); }); return btnGroup; @@ -69,19 +66,18 @@ ButtonsGroup.prototype.addButton = function (button) { ButtonsGroup.prototype.render = function () { if (this.buttons && this.buttons.length == 1) { - this.buttons[0].grouping = ""; - } - else { - _.first(this.buttons).grouping = "grouped-left"; - _.last(this.buttons).grouping = "grouped-right"; - _.each(this.buttons.slice(1, -1), function (btn) { - btn.grouping = "grouped-middle" + this.buttons[0].grouping = ''; + } else { + _.first(this.buttons).grouping = 'grouped-left'; + _.last(this.buttons).grouping = 'grouped-right'; + _.each(this.buttons.slice(1, -1), (btn) => { + btn.grouping = 'grouped-middle'; }); } - return _.map(this.buttons, function (btn) { - if(btn) return btn.render(); - }).join("\n"); + return _.map(this.buttons, (btn) => { + if (btn) return btn.render(); + }).join('\n'); }; Button = function (attributes) { @@ -89,165 +85,163 @@ Button = function (attributes) { }; Button.load = function (btnName) { - var button = module.exports.availableButtons[btnName]; - try{ + const button = module.exports.availableButtons[btnName]; + try { if (button.constructor === Button || button.constructor === SelectButton) { return button; - } - else { + } else { return new Button(button); } - }catch(e){ - console.warn("Error loading button", btnName); + } catch (e) { + console.warn('Error loading button', btnName); return false; } }; _.extend(Button.prototype, { - grouping: "", + grouping: '', - render: function () { - var liAttributes = { - "data-type": "button", - "data-key": this.attributes.command, + render() { + const liAttributes = { + 'data-type': 'button', + 'data-key': this.attributes.command, }; - 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 }) - ) + 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}), + ), ); - } + }, }); - var SelectButton = function (attributes) { this.attributes = attributes; this.options = []; }; _.extend(SelectButton.prototype, Button.prototype, { - addOption: function (value, text, attributes) { + addOption(value, text, attributes) { this.options.push({ - value: value, - text: text, - attributes: attributes + value, + text, + attributes, }); return this; }, - select: function (attributes) { - var options = []; + select(attributes) { + const options = []; - _.each(this.options, function (opt) { - var a = _.extend({ - value: opt.value + _.each(this.options, (opt) => { + const a = _.extend({ + value: opt.value, }, opt.attributes); - options.push( tag("option", a, opt.text) ); + options.push(tag('option', a, opt.text)); }); - return tag("select", attributes, options.join("")); + return tag('select', attributes, options.join('')); }, - render: function () { - var attributes = { - id: this.attributes.id, - "data-key": this.attributes.command, - "data-type": "select" + render() { + const attributes = { + 'id': this.attributes.id, + 'data-key': this.attributes.command, + 'data-type': 'select', }; - return tag("li", attributes, - this.select({ id: this.attributes.selectId }) + return tag('li', attributes, + this.select({id: this.attributes.selectId}), ); - } + }, }); Separator = function () {}; Separator.prototype.render = function () { - return tag("li", { "class": "separator" }); + return tag('li', {class: 'separator'}); }; module.exports = { availableButtons: { - bold: defaultButtonAttributes("bold"), - italic: defaultButtonAttributes("italic"), - underline: defaultButtonAttributes("underline"), - strikethrough: defaultButtonAttributes("strikethrough"), + bold: defaultButtonAttributes('bold'), + italic: defaultButtonAttributes('italic'), + underline: defaultButtonAttributes('underline'), + strikethrough: defaultButtonAttributes('strikethrough'), orderedlist: { - command: "insertorderedlist", - localizationId: "pad.toolbar.ol.title", - class: "buttonicon buttonicon-insertorderedlist" + command: 'insertorderedlist', + localizationId: 'pad.toolbar.ol.title', + class: 'buttonicon buttonicon-insertorderedlist', }, unorderedlist: { - command: "insertunorderedlist", - localizationId: "pad.toolbar.ul.title", - class: "buttonicon buttonicon-insertunorderedlist" + command: 'insertunorderedlist', + localizationId: 'pad.toolbar.ul.title', + class: 'buttonicon buttonicon-insertunorderedlist', }, - indent: defaultButtonAttributes("indent"), + indent: defaultButtonAttributes('indent'), outdent: { - command: "outdent", - localizationId: "pad.toolbar.unindent.title", - class: "buttonicon buttonicon-outdent" + command: 'outdent', + localizationId: 'pad.toolbar.unindent.title', + class: 'buttonicon buttonicon-outdent', }, - undo: defaultButtonAttributes("undo"), - redo: defaultButtonAttributes("redo"), + undo: defaultButtonAttributes('undo'), + redo: defaultButtonAttributes('redo'), clearauthorship: { - command: "clearauthorship", - localizationId: "pad.toolbar.clearAuthorship.title", - class: "buttonicon buttonicon-clearauthorship" + command: 'clearauthorship', + localizationId: 'pad.toolbar.clearAuthorship.title', + class: 'buttonicon buttonicon-clearauthorship', }, importexport: { - command: "import_export", - localizationId: "pad.toolbar.import_export.title", - class: "buttonicon buttonicon-import_export" + command: 'import_export', + localizationId: 'pad.toolbar.import_export.title', + class: 'buttonicon buttonicon-import_export', }, timeslider: { - command: "showTimeSlider", - localizationId: "pad.toolbar.timeslider.title", - class: "buttonicon buttonicon-history" + command: 'showTimeSlider', + localizationId: 'pad.toolbar.timeslider.title', + class: 'buttonicon buttonicon-history', }, - savedrevision: defaultButtonAttributes("savedRevision"), - settings: defaultButtonAttributes("settings"), - embed: defaultButtonAttributes("embed"), - showusers: defaultButtonAttributes("showusers"), + savedrevision: defaultButtonAttributes('savedRevision'), + settings: defaultButtonAttributes('settings'), + embed: defaultButtonAttributes('embed'), + showusers: defaultButtonAttributes('showusers'), timeslider_export: { - command: "import_export", - localizationId: "timeslider.toolbar.exportlink.title", - class: "buttonicon buttonicon-import_export" + command: 'import_export', + localizationId: 'timeslider.toolbar.exportlink.title', + class: 'buttonicon buttonicon-import_export', }, timeslider_settings: { - command: "settings", - localizationId: "pad.toolbar.settings.title", - class: "buttonicon buttonicon-settings" + command: 'settings', + localizationId: 'pad.toolbar.settings.title', + class: 'buttonicon buttonicon-settings', }, timeslider_returnToPad: { - command: "timeslider_returnToPad", - localizationId: "timeslider.toolbar.returnbutton", - class: "buttontext" - } + command: 'timeslider_returnToPad', + localizationId: 'timeslider.toolbar.returnbutton', + class: 'buttontext', + }, }, - registerButton: function (buttonName, buttonInfo) { + registerButton(buttonName, buttonInfo) { this.availableButtons[buttonName] = buttonInfo; }, - button: function (attributes) { + button(attributes) { return new Button(attributes); }, - separator: function () { - return (new Separator).render(); + separator() { + return (new Separator()).render(); }, - selectButton: function (attributes) { + selectButton(attributes) { return new SelectButton(attributes); }, @@ -255,15 +249,15 @@ module.exports = { * Valid values for whichMenu: 'left' | 'right' | 'timeslider-right' * Valid values for page: 'pad' | 'timeslider' */ - menu: function (buttons, isReadOnly, whichMenu, page) { + menu(buttons, isReadOnly, whichMenu, page) { if (isReadOnly) { // The best way to detect if it's the left editbar is to check for a bold button - if (buttons[0].indexOf("bold") !== -1) { + if (buttons[0].indexOf('bold') !== -1) { // Clear all formatting buttons buttons = []; } else { // Remove Save Revision from the right menu - removeItem(buttons[0],"savedrevision"); + removeItem(buttons[0], 'savedrevision'); } } else { /* @@ -277,14 +271,12 @@ 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")) { - buttons[0].push("savedrevision"); + if ((buttons[0].indexOf('savedrevision') === -1) && (whichMenu === 'right') && (page === 'pad')) { + buttons[0].push('savedrevision'); } } - var groups = _.map(buttons, function (group) { - return ButtonsGroup.fromArray(group).render(); - }); + const groups = _.map(buttons, (group) => ButtonsGroup.fromArray(group).render()); return groups.join(this.separator()); - } + }, }; diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js index 9dcf89fc3..c1257a9bc 100644 --- a/src/static/js/AttributeManager.js +++ b/src/static/js/AttributeManager.js @@ -1,18 +1,18 @@ -var Changeset = require('./Changeset'); -var ChangesetUtils = require('./ChangesetUtils'); -var _ = require('./underscore'); +const Changeset = require('./Changeset'); +const ChangesetUtils = require('./ChangesetUtils'); +const _ = require('./underscore'); -var lineMarkerAttribute = 'lmkr'; +const lineMarkerAttribute = 'lmkr'; // Some of these attributes are kept for compatibility purposes. // Not sure if we need all of them -var DEFAULT_LINE_ATTRIBUTES = ['author', 'lmkr', 'insertorder', 'start']; +const DEFAULT_LINE_ATTRIBUTES = ['author', 'lmkr', 'insertorder', 'start']; // If one of these attributes are set to the first character of a // line it is considered as a line attribute marker i.e. attributes // set on this marker are applied to the whole line. // The list attribute is only maintained for compatibility reasons -var lineAttributes = [lineMarkerAttribute,'list']; +const lineAttributes = [lineMarkerAttribute, 'list']; /* The Attribute manager builds changesets based on a document @@ -29,7 +29,7 @@ var lineAttributes = [lineMarkerAttribute,'list']; - a SkipList `lines` containing the text lines of the document. */ -var AttributeManager = function(rep, applyChangesetCallback) { +const AttributeManager = function (rep, applyChangesetCallback) { this.rep = rep; this.applyChangesetCallback = applyChangesetCallback; this.author = ''; @@ -43,12 +43,11 @@ AttributeManager.lineAttributes = lineAttributes; AttributeManager.prototype = _(AttributeManager.prototype).extend({ - applyChangeset: function(changeset){ - if(!this.applyChangesetCallback) return changeset; + applyChangeset(changeset) { + if (!this.applyChangesetCallback) return changeset; - var cs = changeset.toString(); - if (!Changeset.isIdentity(cs)) - { + const cs = changeset.toString(); + if (!Changeset.isIdentity(cs)) { this.applyChangesetCallback(cs); } @@ -61,17 +60,17 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ @param end [row, col] tuple pointing to the end of the range @param attribs: an array of attributes */ - setAttributesOnRange: function(start, end, attribs) { + setAttributesOnRange(start, end, attribs) { // instead of applying the attributes to the whole range at once, we need to apply them // line by line, to be able to disregard the "*" used as line marker. For more details, // see https://github.com/ether/etherpad-lite/issues/2772 - var allChangesets; - for(var row = start[0]; row <= end[0]; row++) { - var rowRange = this._findRowRange(row, start, end); - var startCol = rowRange[0]; - var endCol = rowRange[1]; + let allChangesets; + for (let row = start[0]; row <= end[0]; row++) { + const rowRange = this._findRowRange(row, start, end); + const startCol = rowRange[0]; + const endCol = rowRange[1]; - var rowChangeset = this._setAttributesOnRangeByLine(row, startCol, endCol, attribs); + const rowChangeset = this._setAttributesOnRangeByLine(row, startCol, endCol, attribs); // compose changesets of all rows into a single changeset, as the range might not be continuous // due to the presence of line markers on the rows @@ -85,12 +84,12 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ return this.applyChangeset(allChangesets); }, - _findRowRange: function(row, start, end) { - var startCol, endCol; + _findRowRange(row, start, end) { + let startCol, endCol; - var startLineOffset = this.rep.lines.offsetOfIndex(row); - var endLineOffset = this.rep.lines.offsetOfIndex(row+1); - var lineLength = endLineOffset - startLineOffset; + const startLineOffset = this.rep.lines.offsetOfIndex(row); + const endLineOffset = this.rep.lines.offsetOfIndex(row + 1); + const lineLength = endLineOffset - startLineOffset; // find column where range on this row starts if (row === start[0]) { // are we on the first row of range? @@ -116,8 +115,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ @param endCol column where range ends @param attribs: an array of attributes */ - _setAttributesOnRangeByLine: function(row, startCol, endCol, attribs) { - var builder = Changeset.builder(this.rep.lines.totalWidth()); + _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); return builder; @@ -127,12 +126,10 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ Returns if the line already has a line marker @param lineNum: the number of the line */ - lineHasMarker: function(lineNum){ - var that = this; + lineHasMarker(lineNum) { + const that = this; - return _.find(lineAttributes, function(attribute){ - return that.getAttributeOnLine(lineNum, attribute) != ''; - }) !== undefined; + return _.find(lineAttributes, (attribute) => that.getAttributeOnLine(lineNum, attribute) != '') !== undefined; }, /* @@ -140,14 +137,12 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ @param lineNum: the number of the line to set the attribute for @param attributeKey: the name of the attribute to get, e.g. list */ - getAttributeOnLine: function(lineNum, attributeName){ + getAttributeOnLine(lineNum, attributeName) { // get `attributeName` attribute of first char of line - var aline = this.rep.alines[lineNum]; - if (aline) - { - var opIter = Changeset.opIterator(aline); - if (opIter.hasNext()) - { + const aline = this.rep.alines[lineNum]; + if (aline) { + const opIter = Changeset.opIterator(aline); + if (opIter.hasNext()) { return Changeset.opAttributeValue(opIter.next(), attributeName, this.rep.apool) || ''; } } @@ -158,96 +153,94 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ Gets all attributes on a line @param lineNum: the number of the line to get the attribute for */ - getAttributesOnLine: function(lineNum){ + getAttributesOnLine(lineNum) { // get attributes of first char of line - var aline = this.rep.alines[lineNum]; - var attributes = [] - if (aline) - { - var opIter = Changeset.opIterator(aline) - , op - if (opIter.hasNext()) - { - op = opIter.next() - if(!op.attribs) return [] + const aline = this.rep.alines[lineNum]; + const attributes = []; + if (aline) { + const opIter = Changeset.opIterator(aline); + let op; + if (opIter.hasNext()) { + op = opIter.next(); + if (!op.attribs) return []; - Changeset.eachAttribNumber(op.attribs, function(n) { - attributes.push([this.rep.apool.getAttribKey(n), this.rep.apool.getAttribValue(n)]) - }.bind(this)) + Changeset.eachAttribNumber(op.attribs, (n) => { + attributes.push([this.rep.apool.getAttribKey(n), this.rep.apool.getAttribValue(n)]); + }); return attributes; } } return []; }, - /* + /* Gets a given attribute on a selection @param attributeName @param prevChar returns true or false if an attribute is visible in range */ - getAttributeOnSelection: function(attributeName, prevChar){ - var rep = this.rep; - if (!(rep.selStart && rep.selEnd)) return + getAttributeOnSelection(attributeName, prevChar) { + const rep = this.rep; + 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? - var isNotSelection = (rep.selStart[0] == rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]); - if(isNotSelection){ - if(prevChar){ + 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 - if(rep.selStart[1] !== 0){ + if (rep.selStart[1] !== 0) { rep.selStart[1]--; } } } - var withIt = Changeset.makeAttribsString('+', [ - [attributeName, 'true'] + const withIt = Changeset.makeAttribsString('+', [ + [attributeName, 'true'], ], rep.apool); - var withItRegex = new RegExp(withIt.replace(/\*/g, '\\*') + "(\\*|$)"); + const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); function hasIt(attribs) { return withItRegex.test(attribs); } - return rangeHasAttrib(rep.selStart, rep.selEnd) + return rangeHasAttrib(rep.selStart, rep.selEnd); function 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 + var 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(var n=selStart[0]+1; n < selEnd[0]; n++) { - hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length]) + for (let n = selStart[0] + 1; n < selEnd[0]; n++) { + hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length]); } // for the last, potentially partial, line - hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]]) + hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]]); - return hasAttrib + return hasAttrib; } // Logic tells us we now have a range on a single line - var lineNum = selStart[0] - , start = selStart[1] - , end = selEnd[1] - , hasAttrib = true + const lineNum = selStart[0]; + const start = selStart[1]; + const end = selEnd[1]; + var hasAttrib = true; // Iterate over attribs on this line - var opIter = Changeset.opIterator(rep.alines[lineNum]) - , indexIntoLine = 0 + const opIter = Changeset.opIterator(rep.alines[lineNum]); + let indexIntoLine = 0; while (opIter.hasNext()) { - var op = opIter.next(); - var opStartInLine = indexIntoLine; - var opEndInLine = opStartInLine + op.chars; + const op = opIter.next(); + const opStartInLine = indexIntoLine; + const opEndInLine = opStartInLine + op.chars; if (!hasIt(op.attribs)) { // does op overlap selection? if (!(opEndInLine <= start || opStartInLine >= end)) { @@ -258,7 +251,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ indexIntoLine = opEndInLine; } - return hasAttrib + return hasAttrib; } }, @@ -269,40 +262,39 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ returns a list of attributes in the format [ ["key","value"], ["key","value"], ... ] */ - getAttributesOnPosition: function(lineNumber, column){ + getAttributesOnPosition(lineNumber, column) { // get all attributes of the line - var aline = this.rep.alines[lineNumber]; + const aline = this.rep.alines[lineNumber]; if (!aline) { - return []; + return []; } // iterate through all operations of a line - var opIter = Changeset.opIterator(aline); + const opIter = Changeset.opIterator(aline); // we need to sum up how much characters each operations take until the wanted position - var currentPointer = 0; - var attributes = []; - var currentOperation; + let currentPointer = 0; + const attributes = []; + let currentOperation; while (opIter.hasNext()) { currentOperation = opIter.next(); - currentPointer = currentPointer + currentOperation.chars; + currentPointer += currentOperation.chars; if (currentPointer > column) { // we got the operation of the wanted position, now collect all its attributes - Changeset.eachAttribNumber(currentOperation.attribs, function (n) { + Changeset.eachAttribNumber(currentOperation.attribs, (n) => { attributes.push([ this.rep.apool.getAttribKey(n), - this.rep.apool.getAttribValue(n) + this.rep.apool.getAttribValue(n), ]); - }.bind(this)); + }); // skip the loop return attributes; } } return attributes; - }, /* @@ -311,7 +303,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ returns a list of attributes in the format [ ["key","value"], ["key","value"], ... ] */ - getAttributesOnCaret: function(){ + getAttributesOnCaret() { return this.getAttributesOnPosition(this.rep.selStart[0], this.rep.selStart[1]); }, @@ -322,72 +314,72 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ @param attributeValue: an optional parameter to pass to the attribute (e.g. indention level) */ - setAttributeOnLine: function(lineNum, attributeName, attributeValue){ - var loc = [0,0]; - var builder = Changeset.builder(this.rep.lines.totalWidth()); - var hasMarker = this.lineHasMarker(lineNum); + setAttributeOnLine(lineNum, attributeName, attributeValue) { + let loc = [0, 0]; + const builder = Changeset.builder(this.rep.lines.totalWidth()); + const hasMarker = this.lineHasMarker(lineNum); ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0])); - if(hasMarker){ + if (hasMarker) { ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [ - [attributeName, attributeValue] + [attributeName, attributeValue], + ], this.rep.apool); + } else { + // add a line marker + builder.insert('*', [ + ['author', this.author], + ['insertorder', 'first'], + [lineMarkerAttribute, '1'], + [attributeName, attributeValue], ], this.rep.apool); - }else{ - // add a line marker - builder.insert('*', [ - ['author', this.author], - ['insertorder', 'first'], - [lineMarkerAttribute, '1'], - [attributeName, attributeValue] - ], this.rep.apool); } return this.applyChangeset(builder); }, - /** + /** * Removes a specified attribute on a line * @param lineNum the number of the affected line * @param attributeName the name of the attribute to remove, e.g. list * @param attributeValue if given only attributes with equal value will be removed */ - removeAttributeOnLine: function(lineNum, attributeName, attributeValue){ - var builder = Changeset.builder(this.rep.lines.totalWidth()); - var hasMarker = this.lineHasMarker(lineNum); - var found = false; + removeAttributeOnLine(lineNum, attributeName, attributeValue) { + const builder = Changeset.builder(this.rep.lines.totalWidth()); + const hasMarker = this.lineHasMarker(lineNum); + let found = false; - var attribs = _(this.getAttributesOnLine(lineNum)).map(function (attrib) { - if (attrib[0] === attributeName && (!attributeValue || attrib[0] === attributeValue)){ - found = true; - return [attributeName, '']; - }else if (attrib[0] === 'author'){ - // update last author to make changes to line attributes on this line - return [attributeName, this.author]; - } - return attrib; - }); + const attribs = _(this.getAttributesOnLine(lineNum)).map(function (attrib) { + if (attrib[0] === attributeName && (!attributeValue || attrib[0] === attributeValue)) { + found = true; + return [attributeName, '']; + } else if (attrib[0] === 'author') { + // update last author to make changes to line attributes on this line + return [attributeName, this.author]; + } + return attrib; + }); - if (!found) { - return; - } + if (!found) { + return; + } - ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]); + ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]); - var countAttribsWithMarker = _.chain(attribs).filter(function(a){return !!a[1];}) - .map(function(a){return a[0];}).difference(DEFAULT_LINE_ATTRIBUTES).size().value(); + const countAttribsWithMarker = _.chain(attribs).filter((a) => !!a[1]) + .map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value(); - //if we have marker and any of attributes don't need to have marker. we need delete it - 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); - } + // if we have marker and any of attributes don't need to have marker. we need delete it + 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); + } - return this.applyChangeset(builder); - }, + return this.applyChangeset(builder); + }, - /* + /* Toggles a line attribute for the specified line number If a line attribute with the specified name exists with any value it will be removed Otherwise it will be set to the given value @@ -395,20 +387,19 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ @param attributeKey: the name of the attribute to toggle, e.g. list @param attributeValue: the value to pass to the attribute (e.g. indention level) */ - toggleAttributeOnLine: function(lineNum, attributeName, attributeValue) { - return this.getAttributeOnLine(lineNum, attributeName) ? - this.removeAttributeOnLine(lineNum, attributeName) : - this.setAttributeOnLine(lineNum, attributeName, attributeValue); - + toggleAttributeOnLine(lineNum, attributeName, attributeValue) { + return this.getAttributeOnLine(lineNum, attributeName) + ? this.removeAttributeOnLine(lineNum, attributeName) + : this.setAttributeOnLine(lineNum, attributeName, attributeValue); }, - hasAttributeOnSelectionOrCaretPosition: function(attributeName) { - var hasSelection = ((this.rep.selStart[0] !== this.rep.selEnd[0]) || (this.rep.selEnd[1] !== this.rep.selStart[1])); - var hasAttrib; + hasAttributeOnSelectionOrCaretPosition(attributeName) { + 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); - }else { - var attributesOnCaretPosition = this.getAttributesOnCaret(); + } else { + const attributesOnCaretPosition = this.getAttributesOnCaret(); hasAttrib = _.contains(_.flatten(attributesOnCaretPosition), attributeName); } return hasAttrib; diff --git a/src/static/js/AttributePool.js b/src/static/js/AttributePool.js index 7e7634e42..78d3e7c5b 100644 --- a/src/static/js/AttributePool.js +++ b/src/static/js/AttributePool.js @@ -28,28 +28,28 @@ used to reference Attributes in Changesets. */ -var AttributePool = function () { +const AttributePool = function () { this.numToAttrib = {}; // e.g. {0: ['foo','bar']} this.attribToNum = {}; // e.g. {'foo,bar': 0} this.nextNum = 0; }; AttributePool.prototype.putAttrib = function (attrib, dontAddIfAbsent) { - var str = String(attrib); + const str = String(attrib); if (str in this.attribToNum) { return this.attribToNum[str]; } if (dontAddIfAbsent) { return -1; } - var num = this.nextNum++; + const num = this.nextNum++; this.attribToNum[str] = num; this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')]; return num; }; AttributePool.prototype.getAttrib = function (num) { - var pair = this.numToAttrib[num]; + const pair = this.numToAttrib[num]; if (!pair) { return pair; } @@ -57,20 +57,20 @@ AttributePool.prototype.getAttrib = function (num) { }; AttributePool.prototype.getAttribKey = function (num) { - var pair = this.numToAttrib[num]; + const pair = this.numToAttrib[num]; if (!pair) return ''; return pair[0]; }; AttributePool.prototype.getAttribValue = function (num) { - var pair = this.numToAttrib[num]; + const pair = this.numToAttrib[num]; if (!pair) return ''; return pair[1]; }; AttributePool.prototype.eachAttrib = function (func) { - for (var n in this.numToAttrib) { - var pair = this.numToAttrib[n]; + for (const n in this.numToAttrib) { + const pair = this.numToAttrib[n]; func(pair[0], pair[1]); } }; @@ -78,7 +78,7 @@ AttributePool.prototype.eachAttrib = function (func) { AttributePool.prototype.toJsonable = function () { return { numToAttrib: this.numToAttrib, - nextNum: this.nextNum + nextNum: this.nextNum, }; }; @@ -86,7 +86,7 @@ AttributePool.prototype.fromJsonable = function (obj) { this.numToAttrib = obj.numToAttrib; this.nextNum = obj.nextNum; this.attribToNum = {}; - for (var n in this.numToAttrib) { + for (const n in this.numToAttrib) { this.attribToNum[String(this.numToAttrib[n])] = Number(n); } return this; diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index e4e6d2d63..422c7ede6 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -25,7 +25,7 @@ * limitations under the License. */ -var AttributePool = require("./AttributePool"); +const AttributePool = require('./AttributePool'); /** * ==================== General Util Functions ======================= @@ -36,7 +36,7 @@ var AttributePool = require("./AttributePool"); * @param msg {string} Just some message */ exports.error = function error(msg) { - var e = new Error(msg); + const e = new Error(msg); e.easysync = true; throw e; }; @@ -49,8 +49,8 @@ exports.error = function error(msg) { */ exports.assert = function assert(b, msgParts) { if (!b) { - var msg = Array.prototype.slice.call(arguments, 1).join(''); - exports.error("Failed assertion: " + msg); + const msg = Array.prototype.slice.call(arguments, 1).join(''); + exports.error(`Failed assertion: ${msg}`); } }; @@ -79,12 +79,10 @@ exports.numToString = function (num) { * @return integer */ exports.toBaseTen = function (cs) { - var dollarIndex = cs.indexOf('$'); - var beforeDollar = cs.substring(0, dollarIndex); - var fromDollar = cs.substring(dollarIndex); - return beforeDollar.replace(/[0-9a-z]+/g, function (s) { - return String(exports.parseNum(s)); - }) + fromDollar; + const dollarIndex = cs.indexOf('$'); + const beforeDollar = cs.substring(0, dollarIndex); + const fromDollar = cs.substring(dollarIndex); + return beforeDollar.replace(/[0-9a-z]+/g, (s) => String(exports.parseNum(s))) + fromDollar; }; @@ -116,29 +114,29 @@ exports.newLen = function (cs) { * @return {Op} type object iterator */ exports.opIterator = function (opsStr, optStartIndex) { - //print(opsStr); - var regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g; - var startIndex = (optStartIndex || 0); - var curIndex = startIndex; - var prevIndex = curIndex; + // print(opsStr); + const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g; + const startIndex = (optStartIndex || 0); + let curIndex = startIndex; + let prevIndex = curIndex; function nextRegexMatch() { prevIndex = curIndex; - var result; + let result; regex.lastIndex = curIndex; result = regex.exec(opsStr); curIndex = regex.lastIndex; if (result[0] == '?') { - exports.error("Hit error opcode in op stream"); + exports.error('Hit error opcode in op stream'); } return result; } - var regexResult = nextRegexMatch(); - var obj = exports.newOp(); + let regexResult = nextRegexMatch(); + const obj = exports.newOp(); function next(optObj) { - var op = (optObj || obj); + const op = (optObj || obj); if (regexResult[0]) { op.attribs = regexResult[1]; op.lines = exports.parseNum(regexResult[2] || 0); @@ -159,9 +157,9 @@ exports.opIterator = function (opsStr, optStartIndex) { return prevIndex; } return { - next: next, - hasNext: hasNext, - lastIndex: lastIndex + next, + hasNext, + lastIndex, }; }; @@ -185,7 +183,7 @@ exports.newOp = function (optOpcode) { opcode: (optOpcode || ''), chars: 0, lines: 0, - attribs: '' + attribs: '', }; }; @@ -198,7 +196,7 @@ exports.cloneOp = function (op) { opcode: op.opcode, chars: op.chars, lines: op.lines, - attribs: op.attribs + attribs: op.attribs, }; }; @@ -220,7 +218,7 @@ exports.copyOp = function (op1, op2) { exports.opString = function (op) { // just for debugging if (!op.opcode) return 'null'; - var assem = exports.opAssembler(); + const assem = exports.opAssembler(); assem.append(op); return assem.toString(); }; @@ -240,33 +238,33 @@ exports.stringOp = function (str) { exports.checkRep = function (cs) { // doesn't check things that require access to attrib pool (e.g. attribute order) // or original string (e.g. newline positions) - var unpacked = exports.unpack(cs); - var oldLen = unpacked.oldLen; - var newLen = unpacked.newLen; - var ops = unpacked.ops; - var charBank = unpacked.charBank; + const unpacked = exports.unpack(cs); + const oldLen = unpacked.oldLen; + const newLen = unpacked.newLen; + const ops = unpacked.ops; + let charBank = unpacked.charBank; - var assem = exports.smartOpAssembler(); - var oldPos = 0; - var calcNewLen = 0; - var numInserted = 0; - var iter = exports.opIterator(ops); + const assem = exports.smartOpAssembler(); + let oldPos = 0; + let calcNewLen = 0; + let numInserted = 0; + const iter = exports.opIterator(ops); while (iter.hasNext()) { - var o = iter.next(); + const o = iter.next(); switch (o.opcode) { - case '=': - oldPos += o.chars; - calcNewLen += o.chars; - break; - case '-': - oldPos += o.chars; - exports.assert(oldPos <= oldLen, oldPos, " > ", oldLen, " in ", cs); - break; - case '+': + case '=': + oldPos += o.chars; + calcNewLen += o.chars; + break; + case '-': + oldPos += o.chars; + exports.assert(oldPos <= oldLen, oldPos, ' > ', oldLen, ' in ', cs); + break; + case '+': { calcNewLen += o.chars; numInserted += o.chars; - exports.assert(calcNewLen <= newLen, calcNewLen, " > ", newLen, " in ", cs); + exports.assert(calcNewLen <= newLen, calcNewLen, ' > ', newLen, ' in ', cs); break; } } @@ -276,15 +274,15 @@ exports.checkRep = function (cs) { calcNewLen += oldLen - oldPos; charBank = charBank.substring(0, numInserted); while (charBank.length < numInserted) { - charBank += "?"; + charBank += '?'; } assem.endDocument(); - var normalized = exports.pack(oldLen, calcNewLen, assem.toString(), charBank); + const normalized = exports.pack(oldLen, calcNewLen, assem.toString(), charBank); exports.assert(normalized == cs, 'Invalid changeset (checkRep failed)'); return cs; -} +}; /** @@ -303,12 +301,12 @@ exports.smartOpAssembler = function () { // - strips final "=" // - ignores 0-length changes // - reorders consecutive + and - (which margingOpAssembler doesn't do) - var minusAssem = exports.mergingOpAssembler(); - var plusAssem = exports.mergingOpAssembler(); - var keepAssem = exports.mergingOpAssembler(); - var assem = exports.stringAssembler(); - var lastOpcode = ''; - var lengthChange = 0; + const minusAssem = exports.mergingOpAssembler(); + const plusAssem = exports.mergingOpAssembler(); + const keepAssem = exports.mergingOpAssembler(); + const assem = exports.stringAssembler(); + let lastOpcode = ''; + let lengthChange = 0; function flushKeeps() { assem.append(keepAssem.toString()); @@ -348,9 +346,9 @@ exports.smartOpAssembler = function () { } function appendOpWithText(opcode, text, attribs, pool) { - var op = exports.newOp(opcode); + const op = exports.newOp(opcode); op.attribs = exports.makeAttribsString(opcode, attribs, pool); - var lastNewlinePos = text.lastIndexOf('\n'); + const lastNewlinePos = text.lastIndexOf('\n'); if (lastNewlinePos < 0) { op.chars = text.length; op.lines = 0; @@ -388,12 +386,12 @@ exports.smartOpAssembler = function () { } return { - append: append, - toString: toString, - clear: clear, - endDocument: endDocument, - appendOpWithText: appendOpWithText, - getLengthChange: getLengthChange + append, + toString, + clear, + endDocument, + appendOpWithText, + getLengthChange, }; }; @@ -403,14 +401,14 @@ exports.mergingOpAssembler = function () { // merges consecutive operations that are mergeable, ignores // no-ops, and drops final pure "keeps". It does not re-order // operations. - var assem = exports.opAssembler(); - var bufOp = exports.newOp(); + const assem = exports.opAssembler(); + const bufOp = exports.newOp(); // If we get, for example, insertions [xxx\n,yyy], those don't merge, // but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n]. // This variable stores the length of yyy and any other newline-less // ops immediately after it. - var bufOpAdditionalCharsAfterNewline = 0; + let bufOpAdditionalCharsAfterNewline = 0; function flush(isEndDocument) { if (bufOp.opcode) { @@ -465,17 +463,16 @@ exports.mergingOpAssembler = function () { exports.clearOp(bufOp); } return { - append: append, - toString: toString, - clear: clear, - endDocument: endDocument + append, + toString, + clear, + endDocument, }; }; - exports.opAssembler = function () { - var pieces = []; + const pieces = []; // this function allows op to be mutated later (doesn't keep a ref) function append(op) { @@ -495,9 +492,9 @@ exports.opAssembler = function () { pieces.length = 0; } return { - append: append, - toString: toString, - clear: clear + append, + toString, + clear, }; }; @@ -506,28 +503,28 @@ exports.opAssembler = function () { * @param str {string} String to be iterated over */ exports.stringIterator = function (str) { - var curIndex = 0; + let curIndex = 0; // newLines is the number of \n between curIndex and str.length - var newLines = str.split("\n").length - 1 - function getnewLines(){ - return newLines + let newLines = str.split('\n').length - 1; + function getnewLines() { + return newLines; } function assertRemaining(n) { - exports.assert(n <= remaining(), "!(", n, " <= ", remaining(), ")"); + exports.assert(n <= remaining(), '!(', n, ' <= ', remaining(), ')'); } function take(n) { assertRemaining(n); - var s = str.substr(curIndex, n); - newLines -= s.split("\n").length - 1 + const s = str.substr(curIndex, n); + newLines -= s.split('\n').length - 1; curIndex += n; return s; } function peek(n) { assertRemaining(n); - var s = str.substr(curIndex, n); + const s = str.substr(curIndex, n); return s; } @@ -540,11 +537,11 @@ exports.stringIterator = function (str) { return str.length - curIndex; } return { - take: take, - skip: skip, - remaining: remaining, - peek: peek, - newlines: getnewLines + take, + skip, + remaining, + peek, + newlines: getnewLines, }; }; @@ -552,7 +549,7 @@ exports.stringIterator = function (str) { * A custom made StringBuffer */ exports.stringAssembler = function () { - var pieces = []; + const pieces = []; function append(x) { pieces.push(String(x)); @@ -562,8 +559,8 @@ exports.stringAssembler = function () { return pieces.join(''); } return { - append: append, - toString: toString + append, + toString, }; }; @@ -581,11 +578,11 @@ exports.textLinesMutator = function (lines) { // is not actually a newline, but for the purposes of N and L values, // the caller should pretend it is, and for things to work right in that case, the input // to insert() should be a single line with no newlines. - var curSplice = [0, 0]; - var inSplice = false; + const curSplice = [0, 0]; + let inSplice = false; // position in document after curSplice is applied: - var curLine = 0, - curCol = 0; + let curLine = 0; + let curCol = 0; // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) && // curLine >= curSplice[0] // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then @@ -617,7 +614,7 @@ exports.textLinesMutator = function (lines) { } function lines_length() { - if ((typeof lines.length) == "number") { + if ((typeof lines.length) === 'number') { return lines.length; } else { return lines.length(); @@ -645,7 +642,7 @@ exports.textLinesMutator = function (lines) { } function debugPrint(typ) { - print(typ + ": " + curSplice.toSource() + " / " + curLine + "," + curCol + " / " + lines_toSource()); + print(`${typ}: ${curSplice.toSource()} / ${curLine},${curCol} / ${lines_toSource()}`); } function putCurLineInSplice() { @@ -662,7 +659,7 @@ exports.textLinesMutator = function (lines) { if (!inSplice) { enterSplice(); } - for (var i = 0; i < L; i++) { + for (let i = 0; i < L; i++) { curCol = 0; putCurLineInSplice(); curLine++; @@ -678,14 +675,14 @@ exports.textLinesMutator = function (lines) { curLine += L; curCol = 0; } - //print(inSplice+" / "+isCurLineInSplice()+" / "+curSplice[0]+" / "+curSplice[1]+" / "+lines.length); -/*if (inSplice && (! isCurLineInSplice()) && (curSplice[0] + curSplice[1] < lines.length)) { + // print(inSplice+" / "+isCurLineInSplice()+" / "+curSplice[0]+" / "+curSplice[1]+" / "+lines.length); + /* if (inSplice && (! isCurLineInSplice()) && (curSplice[0] + curSplice[1] < lines.length)) { print("BLAH"); putCurLineInSplice(); }*/ // tests case foo in remove(), which isn't otherwise covered in current impl } - //debugPrint("skip"); + // debugPrint("skip"); } function skip(N, L, includeInSplice) { @@ -700,24 +697,24 @@ exports.textLinesMutator = function (lines) { putCurLineInSplice(); } curCol += N; - //debugPrint("skip"); + // debugPrint("skip"); } } } function removeLines(L) { - var removed = ''; + let removed = ''; if (L) { if (!inSplice) { enterSplice(); } function nextKLinesText(k) { - var m = curSplice[0] + curSplice[1]; + const m = curSplice[0] + curSplice[1]; return lines_slice(m, m + k).join(''); } if (isCurLineInSplice()) { - //print(curCol); + // print(curCol); if (curCol == 0) { removed = curSplice[curSplice.length - 1]; // print("FOO"); // case foo @@ -727,7 +724,7 @@ exports.textLinesMutator = function (lines) { } else { removed = nextKLinesText(L - 1); curSplice[1] += L - 1; - var sline = curSplice.length - 1; + const sline = curSplice.length - 1; removed = curSplice[sline].substring(curCol) + removed; curSplice[sline] = curSplice[sline].substring(0, curCol) + lines_get(curSplice[0] + curSplice[1]); curSplice[1] += 1; @@ -736,13 +733,13 @@ exports.textLinesMutator = function (lines) { removed = nextKLinesText(L); curSplice[1] += L; } - //debugPrint("remove"); + // debugPrint("remove"); } return removed; } function remove(N, L) { - var removed = ''; + let removed = ''; if (N) { if (L) { return removeLines(L); @@ -750,10 +747,10 @@ exports.textLinesMutator = function (lines) { if (!inSplice) { enterSplice(); } - var sline = putCurLineInSplice(); + const sline = putCurLineInSplice(); removed = curSplice[sline].substring(curCol, curCol + N); curSplice[sline] = curSplice[sline].substring(0, curCol) + curSplice[sline].substring(curCol + N); - //debugPrint("remove"); + // debugPrint("remove"); } } return removed; @@ -765,18 +762,18 @@ exports.textLinesMutator = function (lines) { enterSplice(); } if (L) { - var newLines = exports.splitTextLines(text); + const newLines = exports.splitTextLines(text); if (isCurLineInSplice()) { - //if (curCol == 0) { - //curSplice.length--; - //curSplice[1]--; - //Array.prototype.push.apply(curSplice, newLines); - //curLine += newLines.length; - //} - //else { + // if (curCol == 0) { + // curSplice.length--; + // curSplice[1]--; + // Array.prototype.push.apply(curSplice, newLines); + // curLine += newLines.length; + // } + // else { var sline = curSplice.length - 1; - var theLine = curSplice[sline]; - var lineCol = curCol; + const theLine = curSplice[sline]; + const lineCol = curCol; curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; curLine++; newLines.splice(0, 1); @@ -784,7 +781,7 @@ exports.textLinesMutator = function (lines) { curLine += newLines.length; curSplice.push(theLine.substring(lineCol)); curCol = 0; - //} + // } } else { Array.prototype.push.apply(curSplice, newLines); curLine += newLines.length; @@ -792,18 +789,18 @@ exports.textLinesMutator = function (lines) { } else { var sline = putCurLineInSplice(); if (!curSplice[sline]) { - console.error("curSplice[sline] not populated, actual curSplice contents is ", curSplice, ". Possibly related to https://github.com/ether/etherpad-lite/issues/2802"); + console.error('curSplice[sline] not populated, actual curSplice contents is ', curSplice, '. Possibly related to https://github.com/ether/etherpad-lite/issues/2802'); } curSplice[sline] = curSplice[sline].substring(0, curCol) + text + curSplice[sline].substring(curCol); curCol += text.length; } - //debugPrint("insert"); + // debugPrint("insert"); } } function hasMore() { - //print(lines.length+" / "+inSplice+" / "+(curSplice.length - 2)+" / "+curSplice[1]); - var docLines = lines_length(); + // print(lines.length+" / "+inSplice+" / "+(curSplice.length - 2)+" / "+curSplice[1]); + let docLines = lines_length(); if (inSplice) { docLines += curSplice.length - 2 - curSplice[1]; } @@ -814,17 +811,17 @@ exports.textLinesMutator = function (lines) { if (inSplice) { leaveSplice(); } - //debugPrint("close"); + // debugPrint("close"); } - var self = { - skip: skip, - remove: remove, - insert: insert, - close: close, - hasMore: hasMore, - removeLines: removeLines, - skipLines: skipLines + const self = { + skip, + remove, + insert, + close, + hasMore, + removeLines, + skipLines, }; return self; }; @@ -845,18 +842,18 @@ exports.textLinesMutator = function (lines) { * @return {string} the integrated changeset */ exports.applyZip = function (in1, idx1, in2, idx2, func) { - var iter1 = exports.opIterator(in1, idx1); - var iter2 = exports.opIterator(in2, idx2); - var assem = exports.smartOpAssembler(); - var op1 = exports.newOp(); - var op2 = exports.newOp(); - var opOut = exports.newOp(); + const iter1 = exports.opIterator(in1, idx1); + const iter2 = exports.opIterator(in2, idx2); + const assem = exports.smartOpAssembler(); + const op1 = exports.newOp(); + const op2 = exports.newOp(); + const opOut = exports.newOp(); while (op1.opcode || iter1.hasNext() || op2.opcode || iter2.hasNext()) { if ((!op1.opcode) && iter1.hasNext()) iter1.next(op1); if ((!op2.opcode) && iter2.hasNext()) iter2.next(op2); func(op1, op2, opOut); if (opOut.opcode) { - //print(opOut.toSource()); + // print(opOut.toSource()); assem.append(opOut); opOut.opcode = ''; } @@ -871,23 +868,23 @@ exports.applyZip = function (in1, idx1, in2, idx2, func) { * @returns {Changeset} a Changeset class */ exports.unpack = function (cs) { - var headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; - var headerMatch = headerRegex.exec(cs); + const headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; + const headerMatch = headerRegex.exec(cs); if ((!headerMatch) || (!headerMatch[0])) { - exports.error("Not a exports: " + cs); + exports.error(`Not a exports: ${cs}`); } - var oldLen = exports.parseNum(headerMatch[1]); - var changeSign = (headerMatch[2] == '>') ? 1 : -1; - var changeMag = exports.parseNum(headerMatch[3]); - var newLen = oldLen + changeSign * changeMag; - var opsStart = headerMatch[0].length; - var opsEnd = cs.indexOf("$"); + const oldLen = exports.parseNum(headerMatch[1]); + const changeSign = (headerMatch[2] == '>') ? 1 : -1; + const changeMag = exports.parseNum(headerMatch[3]); + const newLen = oldLen + changeSign * changeMag; + const opsStart = headerMatch[0].length; + let opsEnd = cs.indexOf('$'); if (opsEnd < 0) opsEnd = cs.length; return { - oldLen: oldLen, - newLen: newLen, + oldLen, + newLen, ops: cs.substring(opsStart, opsEnd), - charBank: cs.substring(opsEnd + 1) + charBank: cs.substring(opsEnd + 1), }; }; @@ -900,9 +897,9 @@ exports.unpack = function (cs) { * @returns {Changeset} a Changeset class */ exports.pack = function (oldLen, newLen, opsStr, bank) { - var lenDiff = newLen - oldLen; - var lenDiffStr = (lenDiff >= 0 ? '>' + exports.numToString(lenDiff) : '<' + exports.numToString(-lenDiff)); - var a = []; + const lenDiff = newLen - oldLen; + const lenDiffStr = (lenDiff >= 0 ? `>${exports.numToString(lenDiff)}` : `<${exports.numToString(-lenDiff)}`); + const a = []; a.push('Z:', exports.numToString(oldLen), lenDiffStr, opsStr, '$', bank); return a.join(''); }; @@ -913,39 +910,39 @@ exports.pack = function (oldLen, newLen, opsStr, bank) { * @params str {string} String to which a Changeset should be applied */ exports.applyToText = function (cs, str) { - var unpacked = exports.unpack(cs); - exports.assert(str.length == unpacked.oldLen, "mismatched apply: ", str.length, " / ", unpacked.oldLen); - var csIter = exports.opIterator(unpacked.ops); - var bankIter = exports.stringIterator(unpacked.charBank); - var strIter = exports.stringIterator(str); - var assem = exports.stringAssembler(); + const unpacked = exports.unpack(cs); + exports.assert(str.length == unpacked.oldLen, 'mismatched apply: ', str.length, ' / ', unpacked.oldLen); + const csIter = exports.opIterator(unpacked.ops); + const bankIter = exports.stringIterator(unpacked.charBank); + const strIter = exports.stringIterator(str); + const assem = exports.stringAssembler(); while (csIter.hasNext()) { - var op = csIter.next(); + const op = csIter.next(); switch (op.opcode) { - case '+': - //op is + and op.lines 0: no newlines must be in op.chars - //op is + and op.lines >0: op.chars must include op.lines newlines - if(op.lines != bankIter.peek(op.chars).split("\n").length - 1){ - throw new Error("newline count is wrong in op +; cs:"+cs+" and text:"+str); - } - assem.append(bankIter.take(op.chars)); - break; - case '-': - //op is - and op.lines 0: no newlines must be in the deleted string - //op is - and op.lines >0: op.lines newlines must be in the deleted string - if(op.lines != strIter.peek(op.chars).split("\n").length - 1){ - throw new Error("newline count is wrong in op -; cs:"+cs+" and text:"+str); - } - strIter.skip(op.chars); - break; - case '=': - //op is = and op.lines 0: no newlines must be in the copied string - //op is = and op.lines >0: op.lines newlines must be in the copied string - if(op.lines != strIter.peek(op.chars).split("\n").length - 1){ - throw new Error("newline count is wrong in op =; cs:"+cs+" and text:"+str); - } - assem.append(strIter.take(op.chars)); - break; + case '+': + // op is + and op.lines 0: no newlines must be in op.chars + // op is + and op.lines >0: op.chars must include op.lines newlines + if (op.lines != bankIter.peek(op.chars).split('\n').length - 1) { + throw new Error(`newline count is wrong in op +; cs:${cs} and text:${str}`); + } + assem.append(bankIter.take(op.chars)); + break; + case '-': + // op is - and op.lines 0: no newlines must be in the deleted string + // op is - and op.lines >0: op.lines newlines must be in the deleted string + if (op.lines != strIter.peek(op.chars).split('\n').length - 1) { + throw new Error(`newline count is wrong in op -; cs:${cs} and text:${str}`); + } + strIter.skip(op.chars); + break; + case '=': + // op is = and op.lines 0: no newlines must be in the copied string + // op is = and op.lines >0: op.lines newlines must be in the copied string + if (op.lines != strIter.peek(op.chars).split('\n').length - 1) { + throw new Error(`newline count is wrong in op =; cs:${cs} and text:${str}`); + } + assem.append(strIter.take(op.chars)); + break; } } assem.append(strIter.take(strIter.remaining())); @@ -958,22 +955,22 @@ exports.applyToText = function (cs, str) { * @param lines The lines to which the changeset needs to be applied */ exports.mutateTextLines = function (cs, lines) { - var unpacked = exports.unpack(cs); - var csIter = exports.opIterator(unpacked.ops); - var bankIter = exports.stringIterator(unpacked.charBank); - var mut = exports.textLinesMutator(lines); + const unpacked = exports.unpack(cs); + const csIter = exports.opIterator(unpacked.ops); + const bankIter = exports.stringIterator(unpacked.charBank); + const mut = exports.textLinesMutator(lines); while (csIter.hasNext()) { - var op = csIter.next(); + const op = csIter.next(); switch (op.opcode) { - case '+': - mut.insert(bankIter.take(op.chars), op.lines); - break; - case '-': - mut.remove(op.chars, op.lines); - break; - case '=': - mut.skip(op.chars, op.lines, ( !! op.attribs)); - break; + case '+': + mut.insert(bankIter.take(op.chars), op.lines); + break; + case '-': + mut.remove(op.chars, op.lines); + break; + case '=': + mut.skip(op.chars, op.lines, (!!op.attribs)); + break; } } mut.close(); @@ -1008,16 +1005,16 @@ exports.composeAttributes = function (att1, att2, resultIsMutation, pool) { return att2; } if (!att2) return att1; - var atts = []; - att1.replace(/\*([0-9a-z]+)/g, function (_, a) { + const atts = []; + att1.replace(/\*([0-9a-z]+)/g, (_, a) => { atts.push(pool.getAttrib(exports.parseNum(a))); return ''; }); - att2.replace(/\*([0-9a-z]+)/g, function (_, a) { - var pair = pool.getAttrib(exports.parseNum(a)); - var found = false; - for (var i = 0; i < atts.length; i++) { - var oldPair = atts[i]; + att2.replace(/\*([0-9a-z]+)/g, (_, a) => { + const pair = pool.getAttrib(exports.parseNum(a)); + let found = false; + for (let i = 0; i < atts.length; i++) { + const oldPair = atts[i]; if (oldPair[0] == pair[0]) { if (pair[1] || resultIsMutation) { oldPair[1] = pair[1]; @@ -1034,12 +1031,12 @@ exports.composeAttributes = function (att1, att2, resultIsMutation, pool) { return ''; }); atts.sort(); - var buf = exports.stringAssembler(); - for (var i = 0; i < atts.length; i++) { + const buf = exports.stringAssembler(); + for (let i = 0; i < atts.length; i++) { buf.append('*'); buf.append(exports.numToString(pool.putAttrib(atts[i]))); } - //print(att1+" / "+att2+" / "+buf.toString()); + // print(att1+" / "+att2+" / "+buf.toString()); return buf.toString(); }; @@ -1051,7 +1048,7 @@ exports._slicerZipperFunc = function (attOp, csOp, opOut, pool) { // attOp is the op from the sequence that is being operated on, either an // attribution string or the earlier of two exportss being composed. // pool can be null if definitely not needed. - //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); + // print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); if (attOp.opcode == '-') { exports.copyOp(attOp, opOut); attOp.opcode = ''; @@ -1060,7 +1057,7 @@ exports._slicerZipperFunc = function (attOp, csOp, opOut, pool) { csOp.opcode = ''; } else { switch (csOp.opcode) { - case '-': + case '-': { if (csOp.chars <= attOp.chars) { // delete or delete part @@ -1090,14 +1087,14 @@ exports._slicerZipperFunc = function (attOp, csOp, opOut, pool) { } break; } - case '+': + case '+': { // insert exports.copyOp(csOp, opOut); csOp.opcode = ''; break; } - case '=': + case '=': { if (csOp.chars <= attOp.chars) { // keep or keep part @@ -1123,7 +1120,7 @@ exports._slicerZipperFunc = function (attOp, csOp, opOut, pool) { } break; } - case '': + case '': { exports.copyOp(attOp, opOut); attOp.opcode = ''; @@ -1140,30 +1137,28 @@ exports._slicerZipperFunc = function (attOp, csOp, opOut, pool) { * @param pool {AttribsPool} the attibutes pool */ exports.applyToAttribution = function (cs, astr, pool) { - var unpacked = exports.unpack(cs); + const unpacked = exports.unpack(cs); - return exports.applyZip(astr, 0, unpacked.ops, 0, function (op1, op2, opOut) { - return exports._slicerZipperFunc(op1, op2, opOut, pool); - }); + return exports.applyZip(astr, 0, unpacked.ops, 0, (op1, op2, opOut) => exports._slicerZipperFunc(op1, op2, opOut, pool)); }; -/*exports.oneInsertedLineAtATimeOpIterator = function(opsStr, optStartIndex, charBank) { +/* exports.oneInsertedLineAtATimeOpIterator = function(opsStr, optStartIndex, charBank) { var iter = exports.opIterator(opsStr, optStartIndex); var bankIndex = 0; };*/ exports.mutateAttributionLines = function (cs, lines, pool) { - //dmesg(cs); - //dmesg(lines.toSource()+" ->"); - var unpacked = exports.unpack(cs); - var csIter = exports.opIterator(unpacked.ops); - var csBank = unpacked.charBank; - var csBankIndex = 0; + // dmesg(cs); + // dmesg(lines.toSource()+" ->"); + const unpacked = exports.unpack(cs); + const csIter = exports.opIterator(unpacked.ops); + const csBank = unpacked.charBank; + let csBankIndex = 0; // treat the attribution lines as text lines, mutating a line at a time - var mut = exports.textLinesMutator(lines); + const mut = exports.textLinesMutator(lines); - var lineIter = null; + let lineIter = null; function isNextMutOp() { return (lineIter && lineIter.hasNext()) || mut.hasMore(); @@ -1171,7 +1166,7 @@ exports.mutateAttributionLines = function (cs, lines, pool) { function nextMutOp(destOp) { if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) { - var line = mut.removeLines(1); + const line = mut.removeLines(1); lineIter = exports.opIterator(line); } if (lineIter && lineIter.hasNext()) { @@ -1180,42 +1175,42 @@ exports.mutateAttributionLines = function (cs, lines, pool) { destOp.opcode = ''; } } - var lineAssem = null; + let lineAssem = null; function outputMutOp(op) { - //print("outputMutOp: "+op.toSource()); + // print("outputMutOp: "+op.toSource()); if (!lineAssem) { lineAssem = exports.mergingOpAssembler(); } lineAssem.append(op); if (op.lines > 0) { - exports.assert(op.lines == 1, "Can't have op.lines of ", op.lines, " in attribution lines"); + exports.assert(op.lines == 1, "Can't have op.lines of ", op.lines, ' in attribution lines'); // ship it to the mut mut.insert(lineAssem.toString(), 1); lineAssem = null; } } - var csOp = exports.newOp(); - var attOp = exports.newOp(); - var opOut = exports.newOp(); + const csOp = exports.newOp(); + const attOp = exports.newOp(); + const opOut = exports.newOp(); while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) { if ((!csOp.opcode) && csIter.hasNext()) { csIter.next(csOp); } - //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); - //print(csOp.opcode+"/"+csOp.lines+"/"+csOp.attribs+"/"+lineAssem+"/"+lineIter+"/"+(lineIter?lineIter.hasNext():null)); - //print("csOp: "+csOp.toSource()); + // print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); + // print(csOp.opcode+"/"+csOp.lines+"/"+csOp.attribs+"/"+lineAssem+"/"+lineIter+"/"+(lineIter?lineIter.hasNext():null)); + // print("csOp: "+csOp.toSource()); if ((!csOp.opcode) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) { break; // done } else if (csOp.opcode == '=' && csOp.lines > 0 && (!csOp.attribs) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) { // skip multiple lines; this is what makes small changes not order of the document size mut.skipLines(csOp.lines); - //print("skipped: "+csOp.lines); + // print("skipped: "+csOp.lines); csOp.opcode = ''; } else if (csOp.opcode == '+') { if (csOp.lines > 1) { - var firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; + const firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; exports.copyOp(csOp, opOut); csOp.chars -= firstLineLen; csOp.lines--; @@ -1232,7 +1227,7 @@ exports.mutateAttributionLines = function (cs, lines, pool) { if ((!attOp.opcode) && isNextMutOp()) { nextMutOp(attOp); } - //print("attOp: "+attOp.toSource()); + // print("attOp: "+attOp.toSource()); exports._slicerZipperFunc(attOp, csOp, opOut, pool); if (opOut.opcode) { outputMutOp(opOut); @@ -1241,10 +1236,10 @@ exports.mutateAttributionLines = function (cs, lines, pool) { } } - exports.assert(!lineAssem, "line assembler not finished:"+cs); + exports.assert(!lineAssem, `line assembler not finished:${cs}`); mut.close(); - //dmesg("-> "+lines.toSource()); + // dmesg("-> "+lines.toSource()); }; /** @@ -1253,10 +1248,10 @@ exports.mutateAttributionLines = function (cs, lines, pool) { * @returns {string} joined Attribution lines */ exports.joinAttributionLines = function (theAlines) { - var assem = exports.mergingOpAssembler(); - for (var i = 0; i < theAlines.length; i++) { - var aline = theAlines[i]; - var iter = exports.opIterator(aline); + const assem = exports.mergingOpAssembler(); + for (let i = 0; i < theAlines.length; i++) { + const aline = theAlines[i]; + const iter = exports.opIterator(aline); while (iter.hasNext()) { assem.append(iter.next()); } @@ -1265,10 +1260,10 @@ exports.joinAttributionLines = function (theAlines) { }; exports.splitAttributionLines = function (attrOps, text) { - var iter = exports.opIterator(attrOps); - var assem = exports.mergingOpAssembler(); - var lines = []; - var pos = 0; + const iter = exports.opIterator(attrOps); + const assem = exports.mergingOpAssembler(); + const lines = []; + let pos = 0; function appendOp(op) { assem.append(op); @@ -1280,12 +1275,12 @@ exports.splitAttributionLines = function (attrOps, text) { } while (iter.hasNext()) { - var op = iter.next(); - var numChars = op.chars; - var numLines = op.lines; + const op = iter.next(); + let numChars = op.chars; + let numLines = op.lines; while (numLines > 1) { - var newlineEnd = text.indexOf('\n', pos) + 1; - exports.assert(newlineEnd > 0, "newlineEnd <= 0 in splitAttributionLines"); + const newlineEnd = text.indexOf('\n', pos) + 1; + exports.assert(newlineEnd > 0, 'newlineEnd <= 0 in splitAttributionLines'); op.chars = newlineEnd - pos; op.lines = 1; appendOp(op); @@ -1317,24 +1312,24 @@ exports.splitTextLines = function (text) { * @param pool {AtribsPool} Attribs pool */ exports.compose = function (cs1, cs2, pool) { - var unpacked1 = exports.unpack(cs1); - var unpacked2 = exports.unpack(cs2); - var len1 = unpacked1.oldLen; - var len2 = unpacked1.newLen; - exports.assert(len2 == unpacked2.oldLen, "mismatched composition of two changesets"); - var len3 = unpacked2.newLen; - var bankIter1 = exports.stringIterator(unpacked1.charBank); - var bankIter2 = exports.stringIterator(unpacked2.charBank); - var bankAssem = exports.stringAssembler(); + const unpacked1 = exports.unpack(cs1); + const unpacked2 = exports.unpack(cs2); + const len1 = unpacked1.oldLen; + const len2 = unpacked1.newLen; + exports.assert(len2 == unpacked2.oldLen, 'mismatched composition of two changesets'); + const len3 = unpacked2.newLen; + const bankIter1 = exports.stringIterator(unpacked1.charBank); + const bankIter2 = exports.stringIterator(unpacked2.charBank); + const bankAssem = exports.stringAssembler(); - var newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function (op1, op2, opOut) { - //var debugBuilder = exports.stringAssembler(); - //debugBuilder.append(exports.opString(op1)); - //debugBuilder.append(','); - //debugBuilder.append(exports.opString(op2)); - //debugBuilder.append(' / '); - var op1code = op1.opcode; - var op2code = op2.opcode; + const newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, (op1, op2, opOut) => { + // var debugBuilder = exports.stringAssembler(); + // debugBuilder.append(exports.opString(op1)); + // debugBuilder.append(','); + // debugBuilder.append(exports.opString(op2)); + // debugBuilder.append(' / '); + const op1code = op1.opcode; + const op2code = op2.opcode; if (op1code == '+' && op2code == '-') { bankIter1.skip(Math.min(op1.chars, op2.chars)); } @@ -1347,12 +1342,12 @@ exports.compose = function (cs1, cs2, pool) { } } - //debugBuilder.append(exports.opString(op1)); - //debugBuilder.append(','); - //debugBuilder.append(exports.opString(op2)); - //debugBuilder.append(' -> '); - //debugBuilder.append(exports.opString(opOut)); - //print(debugBuilder.toString()); + // debugBuilder.append(exports.opString(op1)); + // debugBuilder.append(','); + // debugBuilder.append(exports.opString(op2)); + // debugBuilder.append(' -> '); + // debugBuilder.append(exports.opString(opOut)); + // print(debugBuilder.toString()); }); return exports.pack(len1, len3, newOps, bankAssem.toString()); @@ -1369,11 +1364,11 @@ exports.attributeTester = function (attribPair, pool) { if (!pool) { return never; } - var attribNum = pool.putAttrib(attribPair, true); + const attribNum = pool.putAttrib(attribPair, true); if (attribNum < 0) { return never; } else { - var re = new RegExp('\\*' + exports.numToString(attribNum) + '(?!\\w)'); + const re = new RegExp(`\\*${exports.numToString(attribNum)}(?!\\w)`); return function (attribs) { return re.test(attribs); }; @@ -1389,7 +1384,7 @@ exports.attributeTester = function (attribPair, pool) { * @param N {int} length of the identity changeset */ exports.identity = function (N) { - return exports.pack(N, N, "", ""); + return exports.pack(N, N, '', ''); }; @@ -1406,7 +1401,7 @@ exports.identity = function (N) { * @param pool {AttribPool} Attribution Pool */ exports.makeSplice = function (oldFullText, spliceStart, numRemoved, newText, optNewTextAPairs, pool) { - var oldLen = oldFullText.length; + const oldLen = oldFullText.length; if (spliceStart >= oldLen) { spliceStart = oldLen - 1; @@ -1414,10 +1409,10 @@ exports.makeSplice = function (oldFullText, spliceStart, numRemoved, newText, op if (numRemoved > oldFullText.length - spliceStart) { numRemoved = oldFullText.length - spliceStart; } - var oldText = oldFullText.substring(spliceStart, spliceStart + numRemoved); - var newLen = oldLen + newText.length - oldText.length; + const oldText = oldFullText.substring(spliceStart, spliceStart + numRemoved); + const newLen = oldLen + newText.length - oldText.length; - var assem = exports.smartOpAssembler(); + const assem = exports.smartOpAssembler(); assem.appendOpWithText('=', oldFullText.substring(0, spliceStart)); assem.appendOpWithText('-', oldText); assem.appendOpWithText('+', newText, optNewTextAPairs, pool); @@ -1433,21 +1428,21 @@ exports.makeSplice = function (oldFullText, spliceStart, numRemoved, newText, op */ exports.toSplices = function (cs) { // - var unpacked = exports.unpack(cs); - var splices = []; + const unpacked = exports.unpack(cs); + const splices = []; - var oldPos = 0; - var iter = exports.opIterator(unpacked.ops); - var charIter = exports.stringIterator(unpacked.charBank); - var inSplice = false; + let oldPos = 0; + const iter = exports.opIterator(unpacked.ops); + const charIter = exports.stringIterator(unpacked.charBank); + let inSplice = false; while (iter.hasNext()) { - var op = iter.next(); + const op = iter.next(); if (op.opcode == '=') { oldPos += op.chars; inSplice = false; } else { if (!inSplice) { - splices.push([oldPos, oldPos, ""]); + splices.push([oldPos, oldPos, '']); inSplice = true; } if (op.opcode == '-') { @@ -1466,16 +1461,16 @@ exports.toSplices = function (cs) { * */ exports.characterRangeFollow = function (cs, startChar, endChar, insertionsAfter) { - var newStartChar = startChar; - var newEndChar = endChar; - var splices = exports.toSplices(cs); - var lengthChangeSoFar = 0; - for (var i = 0; i < splices.length; i++) { - var splice = splices[i]; - var spliceStart = splice[0] + lengthChangeSoFar; - var spliceEnd = splice[1] + lengthChangeSoFar; - var newTextLength = splice[2].length; - var thisLengthChange = newTextLength - (spliceEnd - spliceStart); + let newStartChar = startChar; + let newEndChar = endChar; + const splices = exports.toSplices(cs); + let lengthChangeSoFar = 0; + for (let i = 0; i < splices.length; i++) { + const splice = splices[i]; + const spliceStart = splice[0] + lengthChangeSoFar; + const spliceEnd = splice[1] + lengthChangeSoFar; + const newTextLength = splice[2].length; + const thisLengthChange = newTextLength - (spliceEnd - spliceStart); if (spliceStart <= newStartChar && spliceEnd >= newEndChar) { // splice fully replaces/deletes range @@ -1519,16 +1514,16 @@ exports.characterRangeFollow = function (cs, startChar, endChar, insertionsAfter */ exports.moveOpsToNewPool = function (cs, oldPool, newPool) { // works on exports or attribution string - var dollarPos = cs.indexOf('$'); + let dollarPos = cs.indexOf('$'); if (dollarPos < 0) { dollarPos = cs.length; } - var upToDollar = cs.substring(0, dollarPos); - var fromDollar = cs.substring(dollarPos); + const upToDollar = cs.substring(0, dollarPos); + const fromDollar = cs.substring(dollarPos); // order of attribs stays the same - return upToDollar.replace(/\*([0-9a-z]+)/g, function (_, a) { - var oldNum = exports.parseNum(a); - var pair = oldPool.getAttrib(oldNum); + return upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => { + const oldNum = exports.parseNum(a); + let pair = oldPool.getAttrib(oldNum); /* * Setting an empty pair. Required for when delete pad contents / attributes @@ -1540,8 +1535,8 @@ exports.moveOpsToNewPool = function (cs, oldPool, newPool) { pair = []; } - var newNum = newPool.putAttrib(pair); - return '*' + exports.numToString(newNum); + const newNum = newPool.putAttrib(pair); + return `*${exports.numToString(newNum)}`; }) + fromDollar; }; @@ -1550,7 +1545,7 @@ exports.moveOpsToNewPool = function (cs, oldPool, newPool) { * @param text {string} text to be inserted */ exports.makeAttribution = function (text) { - var assem = exports.smartOpAssembler(); + const assem = exports.smartOpAssembler(); assem.appendOpWithText('+', text); return assem.toString(); }; @@ -1562,13 +1557,13 @@ exports.makeAttribution = function (text) { * @param func {function} function to be called */ exports.eachAttribNumber = function (cs, func) { - var dollarPos = cs.indexOf('$'); + let dollarPos = cs.indexOf('$'); if (dollarPos < 0) { dollarPos = cs.length; } - var upToDollar = cs.substring(0, dollarPos); + const upToDollar = cs.substring(0, dollarPos); - upToDollar.replace(/\*([0-9a-z]+)/g, function (_, a) { + upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => { func(exports.parseNum(a)); return ''; }); @@ -1590,18 +1585,18 @@ exports.filterAttribNumbers = function (cs, filter) { * does exactly the same as exports.filterAttribNumbers */ exports.mapAttribNumbers = function (cs, func) { - var dollarPos = cs.indexOf('$'); + let dollarPos = cs.indexOf('$'); if (dollarPos < 0) { dollarPos = cs.length; } - var upToDollar = cs.substring(0, dollarPos); + const upToDollar = cs.substring(0, dollarPos); - var newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, function (s, a) { - var n = func(exports.parseNum(a)); + const newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, (s, a) => { + const n = func(exports.parseNum(a)); if (n === true) { return s; - } else if ((typeof n) === "number") { - return '*' + exports.numToString(n); + } else if ((typeof n) === 'number') { + return `*${exports.numToString(n)}`; } else { return ''; } @@ -1618,8 +1613,8 @@ exports.mapAttribNumbers = function (cs, func) { */ exports.makeAText = function (text, attribs) { return { - text: text, - attribs: (attribs || exports.makeAttribution(text)) + text, + attribs: (attribs || exports.makeAttribution(text)), }; }; @@ -1632,7 +1627,7 @@ exports.makeAText = function (text, attribs) { exports.applyToAText = function (cs, atext, pool) { return { text: exports.applyToText(cs, atext.text), - attribs: exports.applyToAttribution(cs, atext.attribs, pool) + attribs: exports.applyToAttribution(cs, atext.attribs, pool), }; }; @@ -1644,9 +1639,9 @@ exports.cloneAText = function (atext) { if (atext) { return { text: atext.text, - attribs: atext.attribs - } - } else exports.error("atext is null"); + attribs: atext.attribs, + }; + } else { exports.error('atext is null'); } }; /** @@ -1665,8 +1660,8 @@ exports.copyAText = function (atext1, atext2) { */ exports.appendATextToAssembler = function (atext, assem) { // intentionally skips last newline char of atext - var iter = exports.opIterator(atext.attribs); - var op = exports.newOp(); + const iter = exports.opIterator(atext.attribs); + const op = exports.newOp(); while (iter.hasNext()) { iter.next(op); if (!iter.hasNext()) { @@ -1678,9 +1673,9 @@ exports.appendATextToAssembler = function (atext, assem) { assem.append(op); } } else { - var nextToLastNewlineEnd = + const nextToLastNewlineEnd = atext.text.lastIndexOf('\n', atext.text.length - 2) + 1; - var lastLineLength = atext.text.length - nextToLastNewlineEnd - 1; + const lastLineLength = atext.text.length - nextToLastNewlineEnd - 1; op.lines--; op.chars -= (lastLineLength + 1); assem.append(op); @@ -1702,11 +1697,11 @@ exports.appendATextToAssembler = function (atext, assem) { * @param pool {AtributePool} */ exports.prepareForWire = function (cs, pool) { - var newPool = new AttributePool(); - var newCs = exports.moveOpsToNewPool(cs, pool, newPool); + const newPool = new AttributePool(); + const newCs = exports.moveOpsToNewPool(cs, pool, newPool); return { translated: newCs, - pool: newPool + pool: newPool, }; }; @@ -1714,8 +1709,8 @@ exports.prepareForWire = function (cs, pool) { * Checks if a changeset s the identity changeset */ exports.isIdentity = function (cs) { - var unpacked = exports.unpack(cs); - return unpacked.ops == "" && unpacked.oldLen == unpacked.newLen; + const unpacked = exports.unpack(cs); + return unpacked.ops == '' && unpacked.oldLen == unpacked.newLen; }; /** @@ -1737,9 +1732,9 @@ exports.opAttributeValue = function (op, key, pool) { * @param pool {AttribPool} attribute pool */ exports.attribsAttributeValue = function (attribs, key, pool) { - var value = ''; + let value = ''; if (attribs) { - exports.eachAttribNumber(attribs, function (n) { + exports.eachAttribNumber(attribs, (n) => { if (pool.getAttribKey(n) == key) { value = pool.getAttribValue(n); } @@ -1754,13 +1749,13 @@ exports.attribsAttributeValue = function (attribs, key, pool) { * @param oldLen {int} Old length */ exports.builder = function (oldLen) { - var assem = exports.smartOpAssembler(); - var o = exports.newOp(); - var charBank = exports.stringAssembler(); + const assem = exports.smartOpAssembler(); + const o = exports.newOp(); + const charBank = exports.stringAssembler(); var self = { // attribs are [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in latter case) - keep: function (N, L, attribs, pool) { + keep(N, L, attribs, pool) { o.opcode = '='; o.attribs = (attribs && exports.makeAttribsString('=', attribs, pool)) || ''; o.chars = N; @@ -1768,16 +1763,16 @@ exports.builder = function (oldLen) { assem.append(o); return self; }, - keepText: function (text, attribs, pool) { + keepText(text, attribs, pool) { assem.appendOpWithText('=', text, attribs, pool); return self; }, - insert: function (text, attribs, pool) { + insert(text, attribs, pool) { assem.appendOpWithText('+', text, attribs, pool); charBank.append(text); return self; }, - remove: function (N, L) { + remove(N, L) { o.opcode = '-'; o.attribs = ''; o.chars = N; @@ -1785,11 +1780,11 @@ exports.builder = function (oldLen) { assem.append(o); return self; }, - toString: function () { + toString() { assem.endDocument(); - var newLen = oldLen + assem.getLengthChange(); + const newLen = oldLen + assem.getLengthChange(); return exports.pack(oldLen, newLen, assem.toString(), charBank.toString()); - } + }, }; return self; @@ -1799,18 +1794,18 @@ exports.makeAttribsString = function (opcode, attribs, pool) { // makeAttribsString(opcode, '*3') or makeAttribsString(opcode, [['foo','bar']], myPool) work if (!attribs) { return ''; - } else if ((typeof attribs) == "string") { + } else if ((typeof attribs) === 'string') { return attribs; } else if (pool && attribs && attribs.length) { if (attribs.length > 1) { attribs = attribs.slice(); attribs.sort(); } - var result = []; - for (var i = 0; i < attribs.length; i++) { - var pair = attribs[i]; + const result = []; + for (let i = 0; i < attribs.length; i++) { + const pair = attribs[i]; if (opcode == '=' || (opcode == '+' && pair[1])) { - result.push('*' + exports.numToString(pool.putAttrib(pair))); + result.push(`*${exports.numToString(pool.putAttrib(pair))}`); } } return result.join(''); @@ -1819,11 +1814,11 @@ exports.makeAttribsString = function (opcode, attribs, pool) { // like "substring" but on a single-line attribution string exports.subattribution = function (astr, start, optEnd) { - var iter = exports.opIterator(astr, 0); - var assem = exports.smartOpAssembler(); - var attOp = exports.newOp(); - var csOp = exports.newOp(); - var opOut = exports.newOp(); + const iter = exports.opIterator(astr, 0); + const assem = exports.smartOpAssembler(); + const attOp = exports.newOp(); + const csOp = exports.newOp(); + const opOut = exports.newOp(); function doCsOp() { if (csOp.chars) { @@ -1886,24 +1881,23 @@ exports.inverse = function (cs, lines, alines, pool) { } } - var curLine = 0; - var curChar = 0; - var curLineOpIter = null; - var curLineOpIterLine; - var curLineNextOp = exports.newOp('+'); + let curLine = 0; + let curChar = 0; + let curLineOpIter = null; + let curLineOpIterLine; + const curLineNextOp = exports.newOp('+'); - var unpacked = exports.unpack(cs); - var csIter = exports.opIterator(unpacked.ops); - var builder = exports.builder(unpacked.newLen); - - function consumeAttribRuns(numChars, func /*(len, attribs, endsLine)*/ ) { + const unpacked = exports.unpack(cs); + const csIter = exports.opIterator(unpacked.ops); + const builder = exports.builder(unpacked.newLen); + function consumeAttribRuns(numChars, func /* (len, attribs, endsLine)*/) { if ((!curLineOpIter) || (curLineOpIterLine != curLine)) { // create curLineOpIter and advance it to curChar curLineOpIter = exports.opIterator(alines_get(curLine)); curLineOpIterLine = curLine; - var indexIntoLine = 0; - var done = false; + let indexIntoLine = 0; + let done = false; while (!done && curLineOpIter.hasNext()) { curLineOpIter.next(curLineNextOp); if (indexIntoLine + curLineNextOp.chars >= curChar) { @@ -1926,7 +1920,7 @@ exports.inverse = function (cs, lines, alines, pool) { if (!curLineNextOp.chars) { curLineOpIter.next(curLineNextOp); } - var charsToUse = Math.min(numChars, curLineNextOp.chars); + const charsToUse = Math.min(numChars, curLineNextOp.chars); func(charsToUse, curLineNextOp.attribs, charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0); numChars -= charsToUse; curLineNextOp.chars -= charsToUse; @@ -1943,25 +1937,23 @@ exports.inverse = function (cs, lines, alines, pool) { if (L) { curLine += L; curChar = 0; + } else if (curLineOpIter && curLineOpIterLine == curLine) { + consumeAttribRuns(N, () => {}); } else { - if (curLineOpIter && curLineOpIterLine == curLine) { - consumeAttribRuns(N, function () {}); - } else { - curChar += N; - } + curChar += N; } } function nextText(numChars) { - var len = 0; - var assem = exports.stringAssembler(); - var firstString = lines_get(curLine).substring(curChar); + let len = 0; + const assem = exports.stringAssembler(); + const firstString = lines_get(curLine).substring(curChar); len += firstString.length; assem.append(firstString); - var lineNum = curLine + 1; + let lineNum = curLine + 1; while (len < numChars) { - var nextString = lines_get(lineNum); + const nextString = lines_get(lineNum); len += nextString.length; assem.append(nextString); lineNum++; @@ -1971,7 +1963,7 @@ exports.inverse = function (cs, lines, alines, pool) { } function cachedStrFunc(func) { - var cache = {}; + const cache = {}; return function (s) { if (!cache[s]) { cache[s] = func(s); @@ -1980,31 +1972,31 @@ exports.inverse = function (cs, lines, alines, pool) { }; } - var attribKeys = []; - var attribValues = []; + const attribKeys = []; + const attribValues = []; while (csIter.hasNext()) { - var csOp = csIter.next(); + const csOp = csIter.next(); if (csOp.opcode == '=') { if (csOp.attribs) { attribKeys.length = 0; attribValues.length = 0; - exports.eachAttribNumber(csOp.attribs, function (n) { + exports.eachAttribNumber(csOp.attribs, (n) => { attribKeys.push(pool.getAttribKey(n)); attribValues.push(pool.getAttribValue(n)); }); - var undoBackToAttribs = cachedStrFunc(function (attribs) { - var backAttribs = []; - for (var i = 0; i < attribKeys.length; i++) { - var appliedKey = attribKeys[i]; - var appliedValue = attribValues[i]; - var oldValue = exports.attribsAttributeValue(attribs, appliedKey, pool); + var undoBackToAttribs = cachedStrFunc((attribs) => { + const backAttribs = []; + for (let i = 0; i < attribKeys.length; i++) { + const appliedKey = attribKeys[i]; + const appliedValue = attribValues[i]; + const oldValue = exports.attribsAttributeValue(attribs, appliedKey, pool); if (appliedValue != oldValue) { backAttribs.push([appliedKey, oldValue]); } } return exports.makeAttribsString('=', backAttribs, pool); }); - consumeAttribRuns(csOp.chars, function (len, attribs, endsLine) { + consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => { builder.keep(len, endsLine ? 1 : 0, undoBackToAttribs(attribs)); }); } else { @@ -2016,7 +2008,7 @@ exports.inverse = function (cs, lines, alines, pool) { } else if (csOp.opcode == '-') { var textBank = nextText(csOp.chars); var textBankIndex = 0; - consumeAttribRuns(csOp.chars, function (len, attribs, endsLine) { + consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => { builder.insert(textBank.substr(textBankIndex, len), attribs); textBankIndex += len; }); @@ -2028,33 +2020,33 @@ exports.inverse = function (cs, lines, alines, pool) { // %CLIENT FILE ENDS HERE% exports.follow = function (cs1, cs2, reverseInsertOrder, pool) { - var unpacked1 = exports.unpack(cs1); - var unpacked2 = exports.unpack(cs2); - var len1 = unpacked1.oldLen; - var len2 = unpacked2.oldLen; - exports.assert(len1 == len2, "mismatched follow - cannot transform cs1 on top of cs2"); - var chars1 = exports.stringIterator(unpacked1.charBank); - var chars2 = exports.stringIterator(unpacked2.charBank); + const unpacked1 = exports.unpack(cs1); + const unpacked2 = exports.unpack(cs2); + const len1 = unpacked1.oldLen; + const len2 = unpacked2.oldLen; + exports.assert(len1 == len2, 'mismatched follow - cannot transform cs1 on top of cs2'); + const chars1 = exports.stringIterator(unpacked1.charBank); + const chars2 = exports.stringIterator(unpacked2.charBank); - var oldLen = unpacked1.newLen; - var oldPos = 0; - var newLen = 0; + const oldLen = unpacked1.newLen; + let oldPos = 0; + let newLen = 0; - var hasInsertFirst = exports.attributeTester(['insertorder', 'first'], pool); + const hasInsertFirst = exports.attributeTester(['insertorder', 'first'], pool); - var newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function (op1, op2, opOut) { + const newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, (op1, op2, opOut) => { if (op1.opcode == '+' || op2.opcode == '+') { - var whichToDo; + let whichToDo; if (op2.opcode != '+') { whichToDo = 1; } else if (op1.opcode != '+') { whichToDo = 2; } else { // both + - var firstChar1 = chars1.peek(1); - var firstChar2 = chars2.peek(1); - var insertFirst1 = hasInsertFirst(op1.attribs); - var insertFirst2 = hasInsertFirst(op2.attribs); + const firstChar1 = chars1.peek(1); + const firstChar2 = chars2.peek(1); + const insertFirst1 = hasInsertFirst(op1.attribs); + const insertFirst2 = hasInsertFirst(op2.attribs); if (insertFirst1 && !insertFirst2) { whichToDo = 1; } else if (insertFirst2 && !insertFirst1) { @@ -2089,19 +2081,17 @@ exports.follow = function (cs1, cs2, reverseInsertOrder, pool) { } else if (op1.opcode == '-') { if (!op2.opcode) { op1.opcode = ''; - } else { - if (op1.chars <= op2.chars) { - op2.chars -= op1.chars; - op2.lines -= op1.lines; - op1.opcode = ''; - if (!op2.chars) { - op2.opcode = ''; - } - } else { - op1.chars -= op2.chars; - op1.lines -= op2.lines; + } else if (op1.chars <= op2.chars) { + op2.chars -= op1.chars; + op2.lines -= op1.lines; + op1.opcode = ''; + if (!op2.chars) { op2.opcode = ''; } + } else { + op1.chars -= op2.chars; + op1.lines -= op2.lines; + op2.opcode = ''; } } else if (op2.opcode == '-') { exports.copyOp(op2, opOut); @@ -2153,16 +2143,16 @@ exports.follow = function (cs1, cs2, reverseInsertOrder, pool) { } } switch (opOut.opcode) { - case '=': - oldPos += opOut.chars; - newLen += opOut.chars; - break; - case '-': - oldPos += opOut.chars; - break; - case '+': - newLen += opOut.chars; - break; + case '=': + oldPos += opOut.chars; + newLen += opOut.chars; + break; + case '-': + oldPos += opOut.chars; + break; + case '+': + newLen += opOut.chars; + break; } }); newLen += oldLen - oldPos; @@ -2179,15 +2169,15 @@ exports.followAttributes = function (att1, att2, pool) { // to produce the merged set. if ((!att2) || (!pool)) return ''; if (!att1) return att2; - var atts = []; - att2.replace(/\*([0-9a-z]+)/g, function (_, a) { + const atts = []; + att2.replace(/\*([0-9a-z]+)/g, (_, a) => { atts.push(pool.getAttrib(exports.parseNum(a))); return ''; }); - att1.replace(/\*([0-9a-z]+)/g, function (_, a) { - var pair1 = pool.getAttrib(exports.parseNum(a)); - for (var i = 0; i < atts.length; i++) { - var pair2 = atts[i]; + att1.replace(/\*([0-9a-z]+)/g, (_, a) => { + const pair1 = pool.getAttrib(exports.parseNum(a)); + for (let i = 0; i < atts.length; i++) { + const pair2 = atts[i]; if (pair1[0] == pair2[0]) { if (pair1[1] <= pair2[1]) { // winner of merge is pair1, delete this attribute @@ -2199,8 +2189,8 @@ exports.followAttributes = function (att1, att2, pool) { return ''; }); // we've only removed attributes, so they're already sorted - var buf = exports.stringAssembler(); - for (var i = 0; i < atts.length; i++) { + const buf = exports.stringAssembler(); + for (let i = 0; i < atts.length; i++) { buf.append('*'); buf.append(exports.numToString(pool.putAttrib(atts[i]))); } @@ -2208,19 +2198,19 @@ exports.followAttributes = function (att1, att2, pool) { }; exports.composeWithDeletions = function (cs1, cs2, pool) { - var unpacked1 = exports.unpack(cs1); - var unpacked2 = exports.unpack(cs2); - var len1 = unpacked1.oldLen; - var len2 = unpacked1.newLen; - exports.assert(len2 == unpacked2.oldLen, "mismatched composition of two changesets"); - var len3 = unpacked2.newLen; - var bankIter1 = exports.stringIterator(unpacked1.charBank); - var bankIter2 = exports.stringIterator(unpacked2.charBank); - var bankAssem = exports.stringAssembler(); + const unpacked1 = exports.unpack(cs1); + const unpacked2 = exports.unpack(cs2); + const len1 = unpacked1.oldLen; + const len2 = unpacked1.newLen; + exports.assert(len2 == unpacked2.oldLen, 'mismatched composition of two changesets'); + const len3 = unpacked2.newLen; + const bankIter1 = exports.stringIterator(unpacked1.charBank); + const bankIter2 = exports.stringIterator(unpacked2.charBank); + const bankAssem = exports.stringAssembler(); - var newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function (op1, op2, opOut) { - var op1code = op1.opcode; - var op2code = op2.opcode; + const newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, (op1, op2, opOut) => { + const op1code = op1.opcode; + const op2code = op2.opcode; if (op1code == '+' && op2code == '-') { bankIter1.skip(Math.min(op1.chars, op2.chars)); } @@ -2239,11 +2229,11 @@ exports.composeWithDeletions = function (cs1, cs2, pool) { // This function is 95% like _slicerZipperFunc, we just changed two lines to ensure it merges the attribs of deletions properly. // This is necassary for correct paddiff. But to ensure these changes doesn't affect anything else, we've created a seperate function only used for paddiffs -exports._slicerZipperFuncWithDeletions= function (attOp, csOp, opOut, pool) { +exports._slicerZipperFuncWithDeletions = function (attOp, csOp, opOut, pool) { // attOp is the op from the sequence that is being operated on, either an // attribution string or the earlier of two exportss being composed. // pool can be null if definitely not needed. - //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); + // print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); if (attOp.opcode == '-') { exports.copyOp(attOp, opOut); attOp.opcode = ''; @@ -2252,7 +2242,7 @@ exports._slicerZipperFuncWithDeletions= function (attOp, csOp, opOut, pool) { csOp.opcode = ''; } else { switch (csOp.opcode) { - case '-': + case '-': { if (csOp.chars <= attOp.chars) { // delete or delete part @@ -2260,7 +2250,7 @@ exports._slicerZipperFuncWithDeletions= function (attOp, csOp, opOut, pool) { opOut.opcode = '-'; opOut.chars = csOp.chars; opOut.lines = csOp.lines; - opOut.attribs = csOp.attribs; //changed by yammer + opOut.attribs = csOp.attribs; // changed by yammer } attOp.chars -= csOp.chars; attOp.lines -= csOp.lines; @@ -2274,7 +2264,7 @@ exports._slicerZipperFuncWithDeletions= function (attOp, csOp, opOut, pool) { opOut.opcode = '-'; opOut.chars = attOp.chars; opOut.lines = attOp.lines; - opOut.attribs = csOp.attribs; //changed by yammer + opOut.attribs = csOp.attribs; // changed by yammer } csOp.chars -= attOp.chars; csOp.lines -= attOp.lines; @@ -2282,14 +2272,14 @@ exports._slicerZipperFuncWithDeletions= function (attOp, csOp, opOut, pool) { } break; } - case '+': + case '+': { // insert exports.copyOp(csOp, opOut); csOp.opcode = ''; break; } - case '=': + case '=': { if (csOp.chars <= attOp.chars) { // keep or keep part @@ -2315,7 +2305,7 @@ exports._slicerZipperFuncWithDeletions= function (attOp, csOp, opOut, pool) { } break; } - case '': + case '': { exports.copyOp(attOp, opOut); attOp.opcode = ''; diff --git a/src/static/js/ChangesetUtils.js b/src/static/js/ChangesetUtils.js index cae2d81ac..c7333afcf 100644 --- a/src/static/js/ChangesetUtils.js +++ b/src/static/js/ChangesetUtils.js @@ -18,40 +18,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -exports.buildRemoveRange = function(rep, builder, start, end) { - var startLineOffset = rep.lines.offsetOfIndex(start[0]); - var endLineOffset = rep.lines.offsetOfIndex(end[0]); +exports.buildRemoveRange = function (rep, builder, start, end) { + const startLineOffset = rep.lines.offsetOfIndex(start[0]); + const endLineOffset = rep.lines.offsetOfIndex(end[0]); - if (end[0] > start[0]) - { + if (end[0] > start[0]) { builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]); builder.remove(end[1]); - } - else - { + } else { builder.remove(end[1] - start[1]); } -} +}; -exports.buildKeepRange = function(rep, builder, start, end, attribs, pool) { - var startLineOffset = rep.lines.offsetOfIndex(start[0]); - var endLineOffset = rep.lines.offsetOfIndex(end[0]); +exports.buildKeepRange = function (rep, builder, start, end, attribs, pool) { + const startLineOffset = rep.lines.offsetOfIndex(start[0]); + const endLineOffset = rep.lines.offsetOfIndex(end[0]); - if (end[0] > start[0]) - { + if (end[0] > start[0]) { builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool); builder.keep(end[1], 0, attribs, pool); - } - else - { + } else { builder.keep(end[1] - start[1], 0, attribs, pool); } -} +}; -exports.buildKeepToStartOfRange = function(rep, builder, start) { - var startLineOffset = rep.lines.offsetOfIndex(start[0]); +exports.buildKeepToStartOfRange = function (rep, builder, start) { + const startLineOffset = rep.lines.offsetOfIndex(start[0]); builder.keep(startLineOffset, start[0]); builder.keep(start[1]); -} - +}; diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 36b8558fe..3bc788d0e 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -23,61 +23,57 @@ // requires: top // requires: undefined -var KERNEL_SOURCE = '../static/js/require-kernel.js'; +const KERNEL_SOURCE = '../static/js/require-kernel.js'; Ace2Editor.registry = { - nextId: 1 + nextId: 1, }; -var hooks = require('./pluginfw/hooks'); -var pluginUtils = require('./pluginfw/shared'); -var _ = require('./underscore'); +const hooks = require('./pluginfw/hooks'); +const pluginUtils = require('./pluginfw/shared'); +const _ = require('./underscore'); function scriptTag(source) { return ( - '' - ) + `` + ); } function Ace2Editor() { - var ace2 = Ace2Editor; + const ace2 = Ace2Editor; - var editor = {}; - var info = { - editor: editor, - id: (ace2.registry.nextId++) + const editor = {}; + let info = { + editor, + id: (ace2.registry.nextId++), }; - var loaded = false; + let loaded = false; - var actionsPendingInit = []; + let actionsPendingInit = []; function pendingInit(func, optDoNow) { - return function() { - var that = this; - var args = arguments; - var action = function() { + return function () { + const that = this; + const args = arguments; + const action = function () { func.apply(that, args); - } - if (optDoNow) - { + }; + if (optDoNow) { optDoNow.apply(that, args); } - if (loaded) - { + if (loaded) { action(); - } - else - { + } else { actionsPendingInit.push(action); } }; } function doActionsPendingInit() { - _.each(actionsPendingInit, function(fn,i){ - fn() + _.each(actionsPendingInit, (fn, i) => { + fn(); }); actionsPendingInit = []; } @@ -86,43 +82,56 @@ function Ace2Editor() { // The following functions (prefixed by 'ace_') are exposed by editor, but // execution is delayed until init is complete - var aceFunctionsPendingInit = ['importText', 'importAText', 'focus', - 'setEditable', 'getFormattedCode', 'setOnKeyPress', 'setOnKeyDown', - 'setNotifyDirty', 'setProperty', 'setBaseText', 'setBaseAttributedText', - 'applyChangesToBase', 'applyPreparedChangesetToBase', - 'setUserChangeNotificationCallback', 'setAuthorInfo', - 'setAuthorSelectionRange', 'callWithAce', 'execCommand', 'replaceRange']; + const aceFunctionsPendingInit = ['importText', + 'importAText', + 'focus', + 'setEditable', + 'getFormattedCode', + 'setOnKeyPress', + 'setOnKeyDown', + 'setNotifyDirty', + 'setProperty', + 'setBaseText', + 'setBaseAttributedText', + 'applyChangesToBase', + 'applyPreparedChangesetToBase', + 'setUserChangeNotificationCallback', + 'setAuthorInfo', + 'setAuthorSelectionRange', + 'callWithAce', + 'execCommand', + 'replaceRange']; - _.each(aceFunctionsPendingInit, function(fnName,i){ - var prefix = 'ace_'; - var name = prefix + fnName; - editor[fnName] = pendingInit(function(){ - if(fnName === "setAuthorInfo"){ - if(!arguments[0]){ + _.each(aceFunctionsPendingInit, (fnName, i) => { + const prefix = 'ace_'; + const name = prefix + fnName; + editor[fnName] = pendingInit(function () { + if (fnName === 'setAuthorInfo') { + if (!arguments[0]) { // setAuthorInfo AuthorId not set for some reason - }else{ + } else { info[prefix + fnName].apply(this, arguments); } - }else{ + } else { info[prefix + fnName].apply(this, arguments); } }); }); - editor.exportText = function() { - if (!loaded) return "(awaiting init)\n"; + editor.exportText = function () { + if (!loaded) return '(awaiting init)\n'; return info.ace_exportText(); }; - editor.getFrame = function() { + editor.getFrame = function () { return info.frame || null; }; - editor.getDebugProperty = function(prop) { + editor.getDebugProperty = function (prop) { return info.ace_getDebugProperty(prop); }; - editor.getInInternationalComposition = function() { + editor.getInInternationalComposition = function () { if (!loaded) return false; return info.ace_getInInternationalComposition(); }; @@ -136,26 +145,25 @@ function Ace2Editor() { // to prepareUserChangeset will return an updated changeset that takes into account the // latest user changes, and modify the changeset to be applied by applyPreparedChangesetToBase // accordingly. - editor.prepareUserChangeset = function() { + editor.prepareUserChangeset = function () { if (!loaded) return null; return info.ace_prepareUserChangeset(); }; - editor.getUnhandledErrors = function() { + editor.getUnhandledErrors = function () { if (!loaded) return []; // returns array of {error: , time: +new Date()} return info.ace_getUnhandledErrors(); }; - function sortFilesByEmbeded(files) { - var embededFiles = []; - var remoteFiles = []; + const embededFiles = []; + let remoteFiles = []; if (Ace2Editor.EMBEDED) { - for (var i = 0, ii = files.length; i < ii; i++) { - var file = files[i]; + for (let i = 0, ii = files.length; i < ii; i++) { + const file = files[i]; if (Object.prototype.hasOwnProperty.call(Ace2Editor.EMBEDED, file)) { embededFiles.push(file); } else { @@ -169,9 +177,9 @@ function Ace2Editor() { return {embeded: embededFiles, remote: remoteFiles}; } function pushStyleTagsFor(buffer, files) { - var sorted = sortFilesByEmbeded(files); - var embededFiles = sorted.embeded; - var remoteFiles = sorted.remote; + const sorted = sortFilesByEmbeded(files); + const embededFiles = sorted.embeded; + const remoteFiles = sorted.remote; if (embededFiles.length > 0) { buffer.push(''); - hooks.callAll("aceInitInnerdocbodyHead", { - iframeHTML: iframeHTML + hooks.callAll('aceInitInnerdocbodyHead', { + iframeHTML, }); iframeHTML.push(' '); // Expose myself to global for my child frame. - var thisFunctionsName = "ChildAccessibleAce2Editor"; - (function () {return this}())[thisFunctionsName] = Ace2Editor; + const thisFunctionsName = 'ChildAccessibleAce2Editor'; + (function () { return this; }())[thisFunctionsName] = Ace2Editor; - var outerScript = '\ -editorId = ' + JSON.stringify(info.id) + ';\n\ -editorInfo = parent[' + JSON.stringify(thisFunctionsName) + '].registry[editorId];\n\ + const outerScript = `\ +editorId = ${JSON.stringify(info.id)};\n\ +editorInfo = parent[${JSON.stringify(thisFunctionsName)}].registry[editorId];\n\ window.onload = function () {\n\ window.onload = null;\n\ setTimeout(function () {\n\ @@ -293,34 +300,35 @@ window.onload = function () {\n\ };\n\ var doc = iframe.contentWindow.document;\n\ doc.open();\n\ - var text = (' + JSON.stringify(iframeHTML.join('\n')) + ');\n\ + var text = (${JSON.stringify(iframeHTML.join('\n'))});\n\ doc.write(text);\n\ doc.close();\n\ }, 0);\n\ -}'; +}`; - var outerHTML = [doctype, ''] + const outerHTML = [doctype, ``]; var includedCSS = []; - var $$INCLUDE_CSS = function(filename) {includedCSS.push(filename)}; - $$INCLUDE_CSS("../static/css/iframe_editor.css"); - $$INCLUDE_CSS("../static/css/pad.css?v=" + clientVars.randomVersionString); + var $$INCLUDE_CSS = function (filename) { includedCSS.push(filename); }; + $$INCLUDE_CSS('../static/css/iframe_editor.css'); + $$INCLUDE_CSS(`../static/css/pad.css?v=${clientVars.randomVersionString}`); - var additionalCSS = _(hooks.callAll("aceEditorCSS")).map(function(path){ + var additionalCSS = _(hooks.callAll('aceEditorCSS')).map((path) => { if (path.match(/\/\//)) { // Allow urls to external CSS - http(s):// and //some/path.css return path; } - return '../static/plugins/' + path } + return `../static/plugins/${path}`; + }, ); includedCSS = includedCSS.concat(additionalCSS); - $$INCLUDE_CSS("../static/skins/" + clientVars.skinName + "/pad.css?v=" + clientVars.randomVersionString); + $$INCLUDE_CSS(`../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`); pushStyleTagsFor(outerHTML, includedCSS); // bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly // (throbs busy while typing) - var pluginNames = pluginUtils.clientPluginNames(); + const pluginNames = pluginUtils.clientPluginNames(); outerHTML.push( '', '', @@ -331,14 +339,14 @@ window.onload = function () {\n\ '
                x
                ', ''); - var outerFrame = document.createElement("IFRAME"); - outerFrame.name = "ace_outer"; + const outerFrame = document.createElement('IFRAME'); + outerFrame.name = 'ace_outer'; outerFrame.frameBorder = 0; // for IE - outerFrame.title = "Ether"; + outerFrame.title = 'Ether'; info.frame = outerFrame; document.getElementById(containerId).appendChild(outerFrame); - var editorDocument = outerFrame.contentWindow.document; + const editorDocument = outerFrame.contentWindow.document; editorDocument.open(); editorDocument.write(outerHTML.join('')); diff --git a/src/static/js/ace2_common.js b/src/static/js/ace2_common.js index 2e602097a..9055b34e3 100644 --- a/src/static/js/ace2_common.js +++ b/src/static/js/ace2_common.js @@ -20,27 +20,27 @@ * limitations under the License. */ -var Security = require('./security'); +const Security = require('./security'); function isNodeText(node) { return (node.nodeType == 3); } function object(o) { - var f = function(){}; + const f = function () {}; f.prototype = o; return new f(); } function getAssoc(obj, name) { - return obj["_magicdom_" + name]; + return obj[`_magicdom_${name}`]; } function 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; + obj[`_magicdom_${name}`] = value; } // "func" is a function over 0..(numItems-1) that is monotonically @@ -52,11 +52,10 @@ function binarySearch(numItems, func) { if (numItems < 1) return 0; if (func(0)) return 0; if (!func(numItems - 1)) return numItems; - var low = 0; // func(low) is always false - var high = numItems - 1; // func(high) is always true - while ((high - low) > 1) - { - var x = Math.floor((low + high) / 2); // x != low, x != high + let low = 0; // func(low) is always false + let high = numItems - 1; // func(high) is always true + while ((high - low) > 1) { + const x = Math.floor((low + high) / 2); // x != low, x != high if (func(x)) high = x; else low = x; } @@ -64,7 +63,7 @@ function binarySearch(numItems, func) { } function binarySearchInfinite(expectedLength, func) { - var i = 0; + let i = 0; while (!func(i)) i += expectedLength; return binarySearch(i, func); } @@ -73,7 +72,7 @@ function htmlPrettyEscape(str) { return Security.escapeHTML(str).replace(/\r?\n/g, '\\n'); } -var noop = function(){}; +const noop = function () {}; exports.isNodeText = isNodeText; exports.object = object; diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index efae0e052..277e7d35c 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -19,12 +19,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var _, $, jQuery, plugins, Ace2Common; -var browser = require('./browser'); -if(browser.msie){ +let _, $, jQuery, plugins, Ace2Common; +const browser = require('./browser'); +if (browser.msie) { // Honestly fuck IE royally. // Basically every hack we have since V11 causes a problem - if(parseInt(browser.version) >= 11){ + if (parseInt(browser.version) >= 11) { delete browser.msie; browser.chrome = true; browser.modernIE = true; @@ -35,115 +35,110 @@ Ace2Common = require('./ace2_common'); plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins'); $ = jQuery = require('./rjquery').$; -_ = require("./underscore"); +_ = require('./underscore'); -var isNodeText = Ace2Common.isNodeText, - getAssoc = Ace2Common.getAssoc, - setAssoc = Ace2Common.setAssoc, - isTextNode = Ace2Common.isTextNode, - binarySearchInfinite = Ace2Common.binarySearchInfinite, - htmlPrettyEscape = Ace2Common.htmlPrettyEscape, - noop = Ace2Common.noop; -var hooks = require('./pluginfw/hooks'); +const isNodeText = Ace2Common.isNodeText; +const getAssoc = Ace2Common.getAssoc; +const setAssoc = Ace2Common.setAssoc; +const isTextNode = Ace2Common.isTextNode; +const binarySearchInfinite = Ace2Common.binarySearchInfinite; +const htmlPrettyEscape = Ace2Common.htmlPrettyEscape; +const noop = Ace2Common.noop; +const hooks = require('./pluginfw/hooks'); -function Ace2Inner(){ +function Ace2Inner() { + const makeChangesetTracker = require('./changesettracker').makeChangesetTracker; + const colorutils = require('./colorutils').colorutils; + const makeContentCollector = require('./contentcollector').makeContentCollector; + const makeCSSManager = require('./cssmanager').makeCSSManager; + const domline = require('./domline').domline; + const AttribPool = require('./AttributePool'); + const Changeset = require('./Changeset'); + const ChangesetUtils = require('./ChangesetUtils'); + const linestylefilter = require('./linestylefilter').linestylefilter; + const SkipList = require('./skiplist'); + const undoModule = require('./undomodule').undoModule; + const AttributeManager = require('./AttributeManager'); + const Scroll = require('./scroll'); - var makeChangesetTracker = require('./changesettracker').makeChangesetTracker; - var colorutils = require('./colorutils').colorutils; - var makeContentCollector = require('./contentcollector').makeContentCollector; - var makeCSSManager = require('./cssmanager').makeCSSManager; - var domline = require('./domline').domline; - var AttribPool = require('./AttributePool'); - var Changeset = require('./Changeset'); - var ChangesetUtils = require('./ChangesetUtils'); - var linestylefilter = require('./linestylefilter').linestylefilter; - var SkipList = require('./skiplist'); - var undoModule = require('./undomodule').undoModule; - var AttributeManager = require('./AttributeManager'); - var Scroll = require('./scroll'); - - var DEBUG = false; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;" + const DEBUG = false; // $$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;" // changed to false - var isSetUp = false; + let isSetUp = false; - var THE_TAB = ' '; //4 - var MAX_LIST_LEVEL = 16; + const THE_TAB = ' '; // 4 + const MAX_LIST_LEVEL = 16; - var FORMATTING_STYLES = ['bold', 'italic', 'underline', 'strikethrough']; - var SELECT_BUTTON_CLASS = 'selected'; + const FORMATTING_STYLES = ['bold', 'italic', 'underline', 'strikethrough']; + const SELECT_BUTTON_CLASS = 'selected'; - var caughtErrors = []; + const caughtErrors = []; - var thisAuthor = ''; + let thisAuthor = ''; - var disposed = false; - var editorInfo = parent.editorInfo; + let disposed = false; + const editorInfo = parent.editorInfo; - var iframe = window.frameElement; - var outerWin = iframe.ace_outerWin; + const iframe = window.frameElement; + const outerWin = iframe.ace_outerWin; iframe.ace_outerWin = null; // prevent IE 6 memory leak - var sideDiv = iframe.nextSibling; - var lineMetricsDiv = sideDiv.nextSibling; - var lineNumbersShown; - var sideDivInner; + const sideDiv = iframe.nextSibling; + const lineMetricsDiv = sideDiv.nextSibling; + let lineNumbersShown; + let sideDivInner; initLineNumbers(); - var scroll = Scroll.init(outerWin); + const scroll = Scroll.init(outerWin); - var outsideKeyDown = noop; + let outsideKeyDown = noop; - var outsideKeyPress = function(){return true;}; + let outsideKeyPress = function () { return true; }; - var outsideNotifyDirty = noop; + let outsideNotifyDirty = noop; // selFocusAtStart -- determines whether the selection extends "backwards", so that the focus // point (controlled with the arrow keys) is at the beginning; not supported in IE, though // native IE selections have that behavior (which we try not to interfere with). // Must be false if selection is collapsed! - var rep = { + const rep = { lines: new SkipList(), selStart: null, selEnd: null, selFocusAtStart: false, - alltext: "", + alltext: '', alines: [], - apool: new AttribPool() + apool: new AttribPool(), }; // lines, alltext, alines, and DOM are set up in init() - if (undoModule.enabled) - { + if (undoModule.enabled) { undoModule.apool = rep.apool; } - var root, doc; // set in init() - var isEditable = true; - var doesWrap = true; - var hasLineNumbers = true; - var isStyled = true; + let root, doc; // set in init() + let isEditable = true; + let doesWrap = true; + let hasLineNumbers = true; + let isStyled = true; - var console = (DEBUG && window.console); - var documentAttributeManager; + let console = (DEBUG && window.console); + let documentAttributeManager; - if (!window.console) - { - var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"]; + if (!window.console) { + const names = ['log', 'debug', 'info', 'warn', 'error', 'assert', 'dir', 'dirxml', 'group', 'groupEnd', 'time', 'timeEnd', 'count', 'trace', 'profile', 'profileEnd']; console = {}; - for (var i = 0; i < names.length; ++i) - console[names[i]] = noop; + for (let i = 0; i < names.length; ++i) console[names[i]] = noop; } - var PROFILER = window.PROFILER; - if (!PROFILER) - { - PROFILER = function() { + let PROFILER = window.PROFILER; + if (!PROFILER) { + PROFILER = function () { return { start: noop, mark: noop, literal: noop, end: noop, - cancel: noop + cancel: noop, }; }; } @@ -152,135 +147,118 @@ function Ace2Inner(){ // visible when "?djs=1" is appended to the pad URL. It generally // remains a no-op unless djs is enabled, but we make a habit of // only calling it in error cases or while debugging. - var dmesg = noop; + let dmesg = noop; window.dmesg = noop; - var scheduler = parent; // hack for opera required + const scheduler = parent; // hack for opera required - var dynamicCSS = null; - var outerDynamicCSS = null; - var parentDynamicCSS = null; + let dynamicCSS = null; + let outerDynamicCSS = null; + let parentDynamicCSS = null; function initDynamicCSS() { - dynamicCSS = makeCSSManager("dynamicsyntax"); - outerDynamicCSS = makeCSSManager("dynamicsyntax", "outer"); - parentDynamicCSS = makeCSSManager("dynamicsyntax", "parent"); + dynamicCSS = makeCSSManager('dynamicsyntax'); + outerDynamicCSS = makeCSSManager('dynamicsyntax', 'outer'); + parentDynamicCSS = makeCSSManager('dynamicsyntax', 'parent'); } - var changesetTracker = makeChangesetTracker(scheduler, rep.apool, { - withCallbacks: function(operationName, f) { - inCallStackIfNecessary(operationName, function() { + const changesetTracker = makeChangesetTracker(scheduler, rep.apool, { + withCallbacks(operationName, f) { + inCallStackIfNecessary(operationName, () => { fastIncorp(1); f( - { - setDocumentAttributedText: function(atext) { - setDocAText(atext); - }, - applyChangesetToDocument: function(changeset, preferInsertionAfterCaret) { - var oldEventType = currentCallStack.editEvent.eventType; - currentCallStack.startNewEvent("nonundoable"); + { + setDocumentAttributedText(atext) { + setDocAText(atext); + }, + applyChangesetToDocument(changeset, preferInsertionAfterCaret) { + const oldEventType = currentCallStack.editEvent.eventType; + currentCallStack.startNewEvent('nonundoable'); - performDocumentApplyChangeset(changeset, preferInsertionAfterCaret); + performDocumentApplyChangeset(changeset, preferInsertionAfterCaret); - currentCallStack.startNewEvent(oldEventType); - } - }); + currentCallStack.startNewEvent(oldEventType); + }, + }); }); - } + }, }); - var authorInfos = {}; // presence of key determines if author is present in doc + const authorInfos = {}; // presence of key determines if author is present in doc - function getAuthorInfos(){ + function getAuthorInfos() { return authorInfos; - }; - editorInfo.ace_getAuthorInfos= getAuthorInfos; + } + editorInfo.ace_getAuthorInfos = getAuthorInfos; function setAuthorStyle(author, info) { if (!dynamicCSS) { return; } - var authorSelector = getAuthorColorClassSelector(getAuthorClassName(author)); + const authorSelector = getAuthorColorClassSelector(getAuthorClassName(author)); - var authorStyleSet = hooks.callAll('aceSetAuthorStyle', { - dynamicCSS: dynamicCSS, - parentDynamicCSS: parentDynamicCSS, - outerDynamicCSS: outerDynamicCSS, - info: info, - author: author, - authorSelector: authorSelector, + const authorStyleSet = hooks.callAll('aceSetAuthorStyle', { + dynamicCSS, + parentDynamicCSS, + outerDynamicCSS, + info, + author, + authorSelector, }); // Prevent default behaviour if any hook says so - if (_.any(authorStyleSet, function(it) { return it })) - { - return + if (_.any(authorStyleSet, (it) => it)) { + return; } - if (!info) - { + if (!info) { dynamicCSS.removeSelectorStyle(authorSelector); parentDynamicCSS.removeSelectorStyle(authorSelector); - } - else - { - if (info.bgcolor) - { - var bgcolor = info.bgcolor; - if ((typeof info.fade) == "number") - { - bgcolor = fadeColor(bgcolor, info.fade); - } - - var authorStyle = dynamicCSS.selectorStyle(authorSelector); - var parentAuthorStyle = parentDynamicCSS.selectorStyle(authorSelector); - - // author color - authorStyle.backgroundColor = bgcolor; - parentAuthorStyle.backgroundColor = bgcolor; - - var textColor = colorutils.textColorFromBackgroundColor(bgcolor, parent.parent.clientVars.skinName); - authorStyle.color = textColor; - parentAuthorStyle.color = textColor; + } else if (info.bgcolor) { + let bgcolor = info.bgcolor; + if ((typeof info.fade) === 'number') { + bgcolor = fadeColor(bgcolor, info.fade); } + + const authorStyle = dynamicCSS.selectorStyle(authorSelector); + const parentAuthorStyle = parentDynamicCSS.selectorStyle(authorSelector); + + // author color + authorStyle.backgroundColor = bgcolor; + parentAuthorStyle.backgroundColor = bgcolor; + + const textColor = colorutils.textColorFromBackgroundColor(bgcolor, parent.parent.clientVars.skinName); + authorStyle.color = textColor; + parentAuthorStyle.color = textColor; } } function setAuthorInfo(author, info) { - if ((typeof author) != "string") - { + if ((typeof author) !== 'string') { // Potentially caused by: https://github.com/ether/etherpad-lite/issues/2802"); - throw new Error("setAuthorInfo: author (" + author + ") is not a string"); + throw new Error(`setAuthorInfo: author (${author}) is not a string`); } - if (!info) - { + if (!info) { delete authorInfos[author]; - } - else - { + } else { authorInfos[author] = info; } setAuthorStyle(author, info); } function getAuthorClassName(author) { - return "author-" + author.replace(/[^a-y0-9]/g, function(c) { - if (c == ".") return "-"; - return 'z' + c.charCodeAt(0) + 'z'; - }); + return `author-${author.replace(/[^a-y0-9]/g, (c) => { + if (c == '.') return '-'; + return `z${c.charCodeAt(0)}z`; + })}`; } function className2Author(className) { - if (className.substring(0, 7) == "author-") - { - return className.substring(7).replace(/[a-y0-9]+|-|z.+?z/g, function(cc) { - if (cc == '-') return '.'; - else if (cc.charAt(0) == 'z') - { + if (className.substring(0, 7) == 'author-') { + return className.substring(7).replace(/[a-y0-9]+|-|z.+?z/g, (cc) => { + if (cc == '-') { return '.'; } else if (cc.charAt(0) == 'z') { return String.fromCharCode(Number(cc.slice(1, -1))); - } - else - { + } else { return cc; } }); @@ -289,29 +267,29 @@ function Ace2Inner(){ } function getAuthorColorClassSelector(oneClassName) { - return ".authorColors ." + oneClassName; + return `.authorColors .${oneClassName}`; } function fadeColor(colorCSS, fadeFrac) { - var color = colorutils.css2triple(colorCSS); + let color = colorutils.css2triple(colorCSS); color = colorutils.blend(color, [1, 1, 1], fadeFrac); return colorutils.triple2css(color); } - editorInfo.ace_getRep = function() { + editorInfo.ace_getRep = function () { return rep; }; - editorInfo.ace_getAuthor = function() { + editorInfo.ace_getAuthor = function () { return thisAuthor; - } - - var _nonScrollableEditEvents = { - "applyChangesToBase": 1 }; - _.each(hooks.callAll('aceRegisterNonScrollableEditEvents'), function(eventType) { - _nonScrollableEditEvents[eventType] = 1; + const _nonScrollableEditEvents = { + applyChangesToBase: 1, + }; + + _.each(hooks.callAll('aceRegisterNonScrollableEditEvents'), (eventType) => { + _nonScrollableEditEvents[eventType] = 1; }); function isScrollableEditEvent(eventType) { @@ -323,13 +301,12 @@ function Ace2Inner(){ function inCallStack(type, action) { if (disposed) return; - if (currentCallStack) - { + if (currentCallStack) { // Do not uncomment this in production. It will break Etherpad being provided in iFrames. I'm leaving this in for testing usefulness. // top.console.error("Can't enter callstack " + type + ", already in " + currentCallStack.type); } - var profiling = false; + let profiling = false; function profileRest() { profiling = true; @@ -337,46 +314,34 @@ function Ace2Inner(){ function newEditEvent(eventType) { return { - eventType: eventType, - backset: null + eventType, + backset: null, }; } function submitOldEvent(evt) { - if (rep.selStart && rep.selEnd) - { - var selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; - var selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; + if (rep.selStart && rep.selEnd) { + const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; + const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; evt.selStart = selStartChar; evt.selEnd = selEndChar; evt.selFocusAtStart = rep.selFocusAtStart; } - if (undoModule.enabled) - { - var undoWorked = false; - try - { - if (isPadLoading(evt.eventType)) - { + if (undoModule.enabled) { + let undoWorked = false; + try { + if (isPadLoading(evt.eventType)) { undoModule.clearHistory(); - } - else if (evt.eventType == "nonundoable") - { - if (evt.changeset) - { + } else if (evt.eventType == 'nonundoable') { + if (evt.changeset) { undoModule.reportExternalChange(evt.changeset); } - } - else - { + } else { undoModule.reportEvent(evt); } undoWorked = true; - } - finally - { - if (!undoWorked) - { + } finally { + if (!undoWorked) { undoModule.enabled = false; // for safety } } @@ -384,9 +349,8 @@ function Ace2Inner(){ } function startNewEvent(eventType, dontSubmitOld) { - var oldEvent = currentCallStack.editEvent; - if (!dontSubmitOld) - { + const oldEvent = currentCallStack.editEvent; + if (!dontSubmitOld) { submitOldEvent(oldEvent); } currentCallStack.editEvent = newEditEvent(eventType); @@ -394,75 +358,62 @@ function Ace2Inner(){ } currentCallStack = { - type: type, + type, docTextChanged: false, selectionAffected: false, userChangedSelection: false, domClean: false, - profileRest: profileRest, + profileRest, isUserChange: false, // is this a "user change" type of call-stack repChanged: false, editEvent: newEditEvent(type), - startNewEvent: startNewEvent + startNewEvent, }; - var cleanExit = false; - var result; - try - { + let cleanExit = false; + let result; + try { result = action(); hooks.callAll('aceEditEvent', { callstack: currentCallStack, - editorInfo: editorInfo, - rep: rep, - documentAttributeManager: documentAttributeManager + editorInfo, + rep, + documentAttributeManager, }); cleanExit = true; - } - catch (e) - { + } catch (e) { caughtErrors.push( - { - error: e, - time: +new Date() - }); + { + error: e, + time: +new Date(), + }); dmesg(e.toString()); throw e; - } - finally - { - var cs = currentCallStack; - if (cleanExit) - { + } finally { + const cs = currentCallStack; + if (cleanExit) { submitOldEvent(cs.editEvent); - if (cs.domClean && cs.type != "setup") - { + if (cs.domClean && cs.type != 'setup') { // if (cs.isUserChange) // { // if (cs.repChanged) parenModule.notifyChange(); // else parenModule.notifyTick(); // } - if (cs.selectionAffected) - { + if (cs.selectionAffected) { updateBrowserSelectionFromRep(); } - if ((cs.docTextChanged || cs.userChangedSelection) && isScrollableEditEvent(cs.type)) - { + if ((cs.docTextChanged || cs.userChangedSelection) && isScrollableEditEvent(cs.type)) { scrollSelectionIntoView(); } - if (cs.docTextChanged && cs.type.indexOf("importText") < 0) - { + if (cs.docTextChanged && cs.type.indexOf('importText') < 0) { outsideNotifyDirty(); } } - } - else - { + } else { // non-clean exit - if (currentCallStack.type == "idleWorkTimer") - { + if (currentCallStack.type == 'idleWorkTimer') { idleWorkTimer.atLeast(1000); } } @@ -473,12 +424,9 @@ function Ace2Inner(){ editorInfo.ace_inCallStack = inCallStack; function inCallStackIfNecessary(type, action) { - if (!currentCallStack) - { + if (!currentCallStack) { inCallStack(type, action); - } - else - { + } else { action(); } } @@ -495,34 +443,25 @@ function Ace2Inner(){ function error() { - throw new Error("checkALines"); + throw new Error('checkALines'); } - if (rep.alines.length != rep.lines.length()) - { + if (rep.alines.length != rep.lines.length()) { error(); } - for (var i = 0; i < rep.alines.length; i++) - { - var aline = rep.alines[i]; - var lineText = rep.lines.atIndex(i).text + "\n"; - var lineTextLength = lineText.length; - var opIter = Changeset.opIterator(aline); - var alineLength = 0; - while (opIter.hasNext()) - { - var o = opIter.next(); + for (let i = 0; i < rep.alines.length; i++) { + const aline = rep.alines[i]; + const lineText = `${rep.lines.atIndex(i).text}\n`; + const lineTextLength = lineText.length; + const opIter = Changeset.opIterator(aline); + let alineLength = 0; + while (opIter.hasNext()) { + const o = opIter.next(); alineLength += o.chars; - if (opIter.hasNext()) - { + if (opIter.hasNext()) { if (o.lines !== 0) error(); - } - else - { - if (o.lines != 1) error(); - } + } else if (o.lines != 1) { error(); } } - if (alineLength != lineTextLength) - { + if (alineLength != lineTextLength) { error(); } } @@ -530,32 +469,28 @@ function Ace2Inner(){ function setWraps(newVal) { doesWrap = newVal; - var dwClass = "doesWrap"; + const dwClass = 'doesWrap'; root.classList.toggle('doesWrap', doesWrap); - scheduler.setTimeout(function() { - inCallStackIfNecessary("setWraps", function() { + scheduler.setTimeout(() => { + inCallStackIfNecessary('setWraps', () => { fastIncorp(7); recreateDOM(); fixView(); }); }, 0); - } function setStyled(newVal) { - var oldVal = isStyled; - isStyled = !! newVal; + const oldVal = isStyled; + isStyled = !!newVal; - if (newVal != oldVal) - { - if (!newVal) - { + if (newVal != oldVal) { + if (!newVal) { // clear styles - inCallStackIfNecessary("setStyled", function() { + inCallStackIfNecessary('setStyled', () => { fastIncorp(12); - var clearStyles = []; - for (var k in STYLE_ATTRIBS) - { + const clearStyles = []; + for (const k in STYLE_ATTRIBS) { clearStyles.push([k, '']); } performDocumentApplyAttributesToCharRange(0, rep.alltext.length, clearStyles); @@ -585,53 +520,45 @@ function Ace2Inner(){ } function importText(text, undoable, dontProcess) { - var lines; - if (dontProcess) - { - if (text.charAt(text.length - 1) != "\n") - { - throw new Error("new raw text must end with newline"); + let lines; + if (dontProcess) { + if (text.charAt(text.length - 1) != '\n') { + throw new Error('new raw text must end with newline'); } - if (/[\r\t\xa0]/.exec(text)) - { - throw new Error("new raw text must not contain CR, tab, or nbsp"); + if (/[\r\t\xa0]/.exec(text)) { + throw new Error('new raw text must not contain CR, tab, or nbsp'); } lines = text.substring(0, text.length - 1).split('\n'); - } - else - { + } else { lines = _.map(text.split('\n'), textify); } - var newText = "\n"; - if (lines.length > 0) - { - newText = lines.join('\n') + '\n'; + let newText = '\n'; + if (lines.length > 0) { + newText = `${lines.join('\n')}\n`; } - inCallStackIfNecessary("importText" + (undoable ? "Undoable" : ""), function() { + inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => { setDocText(newText); }); - if (dontProcess && rep.alltext != text) - { - throw new Error("mismatch error setting raw text in importText"); + if (dontProcess && rep.alltext != text) { + throw new Error('mismatch error setting raw text in importText'); } } function importAText(atext, apoolJsonObj, undoable) { atext = Changeset.cloneAText(atext); - if (apoolJsonObj) - { - var wireApool = (new AttribPool()).fromJsonable(apoolJsonObj); + if (apoolJsonObj) { + const wireApool = (new AttribPool()).fromJsonable(apoolJsonObj); atext.attribs = Changeset.moveOpsToNewPool(atext.attribs, wireApool, rep.apool); } - inCallStackIfNecessary("importText" + (undoable ? "Undoable" : ""), function() { + inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => { setDocAText(atext); }); } function setDocAText(atext) { - if (atext.text === "") { + if (atext.text === '') { /* * The server is fine with atext.text being an empty string, but the front * end is not, and crashes. @@ -643,17 +570,17 @@ function Ace2Inner(){ * See for reference: * - https://github.com/ether/etherpad-lite/issues/3861 */ - atext.text = "\n"; + atext.text = '\n'; } fastIncorp(8); - var oldLen = rep.lines.totalWidth(); - var numLines = rep.lines.length(); - var upToLastLine = rep.lines.offsetOfIndex(numLines - 1); - var lastLineLength = rep.lines.atIndex(numLines - 1).text.length; - var assem = Changeset.smartOpAssembler(); - var o = Changeset.newOp('-'); + const oldLen = rep.lines.totalWidth(); + const numLines = rep.lines.length(); + const upToLastLine = rep.lines.offsetOfIndex(numLines - 1); + const lastLineLength = rep.lines.atIndex(numLines - 1).text.length; + const assem = Changeset.smartOpAssembler(); + const o = Changeset.newOp('-'); o.chars = upToLastLine; o.lines = numLines - 1; assem.append(o); @@ -661,20 +588,19 @@ function Ace2Inner(){ o.lines = 0; assem.append(o); Changeset.appendATextToAssembler(atext, assem); - var newLen = oldLen + assem.getLengthChange(); - var changeset = Changeset.checkRep( - Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1))); + const newLen = oldLen + assem.getLengthChange(); + const changeset = Changeset.checkRep( + Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1))); performDocumentApplyChangeset(changeset); performSelectionChange([0, rep.lines.atIndex(0).lineMarker], [0, rep.lines.atIndex(0).lineMarker]); idleWorkTimer.atMost(100); - if (rep.alltext != atext.text) - { + if (rep.alltext != atext.text) { dmesg(htmlPrettyEscape(rep.alltext)); dmesg(htmlPrettyEscape(atext.text)); - throw new Error("mismatch error setting raw text in setDocAText"); + throw new Error('mismatch error setting raw text in setDocAText'); } } @@ -683,16 +609,15 @@ function Ace2Inner(){ } function getDocText() { - var alltext = rep.alltext; - var len = alltext.length; + const alltext = rep.alltext; + let len = alltext.length; if (len > 0) len--; // final extra newline return alltext.substring(0, len); } function exportText() { - if (currentCallStack && !currentCallStack.domClean) - { - inCallStackIfNecessary("exportText", function() { + if (currentCallStack && !currentCallStack.domClean) { + inCallStackIfNecessary('exportText', () => { fastIncorp(2); }); } @@ -716,53 +641,43 @@ function Ace2Inner(){ } function getFormattedCode() { - if (currentCallStack && !currentCallStack.domClean) - { - inCallStackIfNecessary("getFormattedCode", incorporateUserChanges); + if (currentCallStack && !currentCallStack.domClean) { + inCallStackIfNecessary('getFormattedCode', incorporateUserChanges); } - var buf = []; - if (rep.lines.length() > 0) - { + const buf = []; + if (rep.lines.length() > 0) { // should be the case, even for empty file - var entry = rep.lines.atIndex(0); - while (entry) - { - var domInfo = entry.domInfo; - buf.push((domInfo && domInfo.getInnerHTML()) || domline.processSpaces(domline.escapeHTML(entry.text), doesWrap) || ' ' /*empty line*/ ); + let entry = rep.lines.atIndex(0); + while (entry) { + const domInfo = entry.domInfo; + buf.push((domInfo && domInfo.getInnerHTML()) || domline.processSpaces(domline.escapeHTML(entry.text), doesWrap) || ' ' /* empty line*/); entry = rep.lines.next(entry); } } - return '
                ' + buf.join('
                \n
                ') + '
                '; + return `
                ${buf.join('
                \n
                ')}
                `; } - var CMDS = { - clearauthorship: function(prompt) { - if ((!(rep.selStart && rep.selEnd)) || isCaret()) - { - if (prompt) - { + const CMDS = { + clearauthorship(prompt) { + if ((!(rep.selStart && rep.selEnd)) || isCaret()) { + if (prompt) { prompt(); - } - else - { + } else { performDocumentApplyAttributesToCharRange(0, rep.alltext.length, [ - ['author', ''] + ['author', ''], ]); } - } - else - { + } else { setAttributeOnSelection('author', ''); } - } + }, }; function execCommand(cmd) { cmd = cmd.toLowerCase(); - var cmdArgs = Array.prototype.slice.call(arguments, 1); - if (CMDS[cmd]) - { - inCallStackIfNecessary(cmd, function() { + const cmdArgs = Array.prototype.slice.call(arguments, 1); + if (CMDS[cmd]) { + inCallStackIfNecessary(cmd, () => { fastIncorp(9); CMDS[cmd].apply(CMDS, cmdArgs); }); @@ -770,7 +685,7 @@ function Ace2Inner(){ } function replaceRange(start, end, text) { - inCallStackIfNecessary('replaceRange', function() { + inCallStackIfNecessary('replaceRange', () => { fastIncorp(9); performDocumentReplaceRange(start, end, text); }); @@ -789,7 +704,7 @@ function Ace2Inner(){ editorInfo.ace_setEditable = setEditable; editorInfo.ace_execCommand = execCommand; editorInfo.ace_replaceRange = replaceRange; - editorInfo.ace_getAuthorInfos= getAuthorInfos; + editorInfo.ace_getAuthorInfos = getAuthorInfos; editorInfo.ace_performDocumentReplaceRange = performDocumentReplaceRange; editorInfo.ace_performDocumentReplaceCharRange = performDocumentReplaceCharRange; editorInfo.ace_renumberList = renumberList; @@ -797,26 +712,22 @@ function Ace2Inner(){ editorInfo.ace_isBlockElement = isBlockElement; editorInfo.ace_getLineListType = getLineListType; - editorInfo.ace_callWithAce = function(fn, callStack, normalize) { - var wrapper = function() { + editorInfo.ace_callWithAce = function (fn, callStack, normalize) { + let wrapper = function () { return fn(editorInfo); }; - if (normalize !== undefined) - { - var wrapper1 = wrapper; - wrapper = function() { + if (normalize !== undefined) { + const wrapper1 = wrapper; + wrapper = function () { editorInfo.ace_fastIncorp(9); wrapper1(); }; } - if (callStack !== undefined) - { + if (callStack !== undefined) { return editorInfo.ace_inCallStack(callStack, wrapper); - } - else - { + } else { return wrapper(); } }; @@ -824,89 +735,82 @@ function Ace2Inner(){ // This methed exposes a setter for some ace properties // @param key the name of the parameter // @param value the value to set to - editorInfo.ace_setProperty = function(key, value) { + editorInfo.ace_setProperty = function (key, value) { // These properties are exposed - var setters = { + const setters = { wraps: setWraps, showsauthorcolors: (val) => root.classList.toggle('authorColors', !!val), showsuserselections: (val) => root.classList.toggle('userSelections', !!val), - showslinenumbers : function(value){ - hasLineNumbers = !! value; + showslinenumbers(value) { + hasLineNumbers = !!value; sideDiv.parentNode.classList.toggle('line-numbers-hidden', !hasLineNumbers); fixView(); }, grayedout: (val) => outerWin.document.body.classList.toggle('grayedout', !!val), - dmesg: function(){ dmesg = window.dmesg = value; }, - userauthor: function(value){ + dmesg() { dmesg = window.dmesg = value; }, + userauthor(value) { thisAuthor = String(value); documentAttributeManager.author = thisAuthor; }, styled: setStyled, textface: setTextFace, - rtlistrue: function(value) { + rtlistrue(value) { root.classList.toggle('rtl', value); root.classList.toggle('ltr', !value); - document.documentElement.dir = value? 'rtl' : 'ltr' - } + document.documentElement.dir = value ? 'rtl' : 'ltr'; + }, }; - var setter = setters[key.toLowerCase()]; + const setter = setters[key.toLowerCase()]; // check if setter is present - if(setter !== undefined){ - setter(value) + if (setter !== undefined) { + setter(value); } }; - editorInfo.ace_setBaseText = function(txt) { + editorInfo.ace_setBaseText = function (txt) { changesetTracker.setBaseText(txt); }; - editorInfo.ace_setBaseAttributedText = function(atxt, apoolJsonObj) { + editorInfo.ace_setBaseAttributedText = function (atxt, apoolJsonObj) { changesetTracker.setBaseAttributedText(atxt, apoolJsonObj); }; - editorInfo.ace_applyChangesToBase = function(c, optAuthor, apoolJsonObj) { + editorInfo.ace_applyChangesToBase = function (c, optAuthor, apoolJsonObj) { changesetTracker.applyChangesToBase(c, optAuthor, apoolJsonObj); }; - editorInfo.ace_prepareUserChangeset = function() { + editorInfo.ace_prepareUserChangeset = function () { return changesetTracker.prepareUserChangeset(); }; - editorInfo.ace_applyPreparedChangesetToBase = function() { + editorInfo.ace_applyPreparedChangesetToBase = function () { changesetTracker.applyPreparedChangesetToBase(); }; - editorInfo.ace_setUserChangeNotificationCallback = function(f) { + editorInfo.ace_setUserChangeNotificationCallback = function (f) { changesetTracker.setUserChangeNotificationCallback(f); }; - editorInfo.ace_setAuthorInfo = function(author, info) { + editorInfo.ace_setAuthorInfo = function (author, info) { setAuthorInfo(author, info); }; - editorInfo.ace_setAuthorSelectionRange = function(author, start, end) { + editorInfo.ace_setAuthorSelectionRange = function (author, start, end) { changesetTracker.setAuthorSelectionRange(author, start, end); }; - editorInfo.ace_getUnhandledErrors = function() { + editorInfo.ace_getUnhandledErrors = function () { return caughtErrors.slice(); }; - editorInfo.ace_getDocument = function() { + editorInfo.ace_getDocument = function () { return doc; }; - editorInfo.ace_getDebugProperty = function(prop) { - if (prop == "debugger") - { + editorInfo.ace_getDebugProperty = function (prop) { + if (prop == 'debugger') { // obfuscate "eval" so as not to scare yuicompressor - window['ev' + 'al']("debugger"); - } - else if (prop == "rep") - { + window['ev' + 'al']('debugger'); + } else if (prop == 'rep') { return rep; - } - else if (prop == "window") - { + } else if (prop == 'window') { return window; - } - else if (prop == "document") - { + } else if (prop == 'document') { return document; } return undefined; @@ -917,33 +821,28 @@ function Ace2Inner(){ } function newTimeLimit(ms) { - var startTime = now(); - var lastElapsed = 0; - var exceededAlready = false; - var printedTrace = false; - var isTimeUp = function() { - if (exceededAlready) - { - if ((!printedTrace)) - { // && now() - startTime - ms > 300) { - printedTrace = true; - } - return true; + const startTime = now(); + let lastElapsed = 0; + let exceededAlready = false; + let printedTrace = false; + const isTimeUp = function () { + if (exceededAlready) { + if ((!printedTrace)) { // && now() - startTime - ms > 300) { + printedTrace = true; } - var elapsed = now() - startTime; - if (elapsed > ms) - { - exceededAlready = true; - return true; - } - else - { - lastElapsed = elapsed; - return false; - } - }; + return true; + } + const elapsed = now() - startTime; + if (elapsed > ms) { + exceededAlready = true; + return true; + } else { + lastElapsed = elapsed; + return false; + } + }; - isTimeUp.elapsed = function() { + isTimeUp.elapsed = function () { return now() - startTime; }; return isTimeUp; @@ -951,12 +850,11 @@ function Ace2Inner(){ function makeIdleAction(func) { - var scheduledTimeout = null; - var scheduledTime = 0; + let scheduledTimeout = null; + let scheduledTime = 0; function unschedule() { - if (scheduledTimeout) - { + if (scheduledTimeout) { scheduler.clearTimeout(scheduledTimeout); scheduledTimeout = null; } @@ -965,7 +863,7 @@ function Ace2Inner(){ function reschedule(time) { unschedule(); scheduledTime = time; - var delay = time - now(); + let delay = time - now(); if (delay < 0) delay = 0; scheduledTimeout = scheduler.setTimeout(callback, delay); } @@ -976,26 +874,24 @@ function Ace2Inner(){ func(); } return { - atMost: function(ms) { - var latestTime = now() + ms; - if ((!scheduledTimeout) || scheduledTime > latestTime) - { + atMost(ms) { + const latestTime = now() + ms; + if ((!scheduledTimeout) || scheduledTime > latestTime) { reschedule(latestTime); } }, // atLeast(ms) will schedule the action if not scheduled yet. // In other words, "infinity" is replaced by ms, even though // it is technically larger. - atLeast: function(ms) { - var earliestTime = now() + ms; - if ((!scheduledTimeout) || scheduledTime < earliestTime) - { + atLeast(ms) { + const earliestTime = now() + ms; + if ((!scheduledTimeout) || scheduledTime < earliestTime) { reschedule(earliestTime); } }, - never: function() { + never() { unschedule(); - } + }, }; } @@ -1005,24 +901,20 @@ function Ace2Inner(){ } editorInfo.ace_fastIncorp = fastIncorp; - var idleWorkTimer = makeIdleAction(function() { - if (inInternationalComposition) - { + var idleWorkTimer = makeIdleAction(() => { + if (inInternationalComposition) { // don't do idle input incorporation during international input composition idleWorkTimer.atLeast(500); return; } - inCallStackIfNecessary("idleWorkTimer", function() { + inCallStackIfNecessary('idleWorkTimer', () => { + const isTimeUp = newTimeLimit(250); - var isTimeUp = newTimeLimit(250); - - var finishedImportantWork = false; - var finishedWork = false; - - try - { + let finishedImportantWork = false; + let finishedWork = false; + try { // isTimeUp() is a soft constraint for incorporateUserChanges, // which always renormalizes the DOM, no matter how long it takes, // but doesn't necessarily lex and highlight it @@ -1033,27 +925,20 @@ function Ace2Inner(){ updateLineNumbers(); // update line numbers if any time left if (isTimeUp()) return; - var visibleRange = scroll.getVisibleCharRange(rep); - var docRange = [0, rep.lines.totalWidth()]; + const visibleRange = scroll.getVisibleCharRange(rep); + const docRange = [0, rep.lines.totalWidth()]; finishedImportantWork = true; finishedWork = true; - } - finally - { - if (finishedWork) - { + } finally { + if (finishedWork) { idleWorkTimer.atMost(1000); - } - else if (finishedImportantWork) - { + } else if (finishedImportantWork) { // if we've finished highlighting the view area, // more highlighting could be counter-productive, // e.g. if the user just opened a triple-quote and will soon close it. idleWorkTimer.atMost(500); - } - else - { - var timeToWait = Math.round(isTimeUp.elapsed() / 2); + } else { + let timeToWait = Math.round(isTimeUp.elapsed() / 2); if (timeToWait < 100) timeToWait = 100; idleWorkTimer.atMost(timeToWait); } @@ -1061,47 +946,45 @@ function Ace2Inner(){ }); }); - var _nextId = 1; + let _nextId = 1; function uniqueId(n) { // not actually guaranteed to be unique, e.g. if user copy-pastes // nodes with ids - var nid = n.id; + const nid = n.id; if (nid) return nid; - return (n.id = "magicdomid" + (_nextId++)); + return (n.id = `magicdomid${_nextId++}`); } function recolorLinesInRange(startChar, endChar, isTimeUp, optModFunc) { if (endChar <= startChar) return; if (startChar < 0 || startChar >= rep.lines.totalWidth()) return; - var lineEntry = rep.lines.atOffset(startChar); // rounds down to line boundary - var lineStart = rep.lines.offsetOfEntry(lineEntry); - var lineIndex = rep.lines.indexOfEntry(lineEntry); - var selectionNeedsResetting = false; - var firstLine = null; - var lastLine = null; + let lineEntry = rep.lines.atOffset(startChar); // rounds down to line boundary + let lineStart = rep.lines.offsetOfEntry(lineEntry); + let lineIndex = rep.lines.indexOfEntry(lineEntry); + let selectionNeedsResetting = false; + let firstLine = null; + let lastLine = null; isTimeUp = (isTimeUp || noop); // tokenFunc function; accesses current value of lineEntry and curDocChar, // also mutates curDocChar - var curDocChar; - var tokenFunc = function(tokenText, tokenClass) { - lineEntry.domInfo.appendSpan(tokenText, tokenClass); - }; - if (optModFunc) - { - var f = tokenFunc; - tokenFunc = function(tokenText, tokenClass) { + let curDocChar; + let tokenFunc = function (tokenText, tokenClass) { + lineEntry.domInfo.appendSpan(tokenText, tokenClass); + }; + if (optModFunc) { + const f = tokenFunc; + tokenFunc = function (tokenText, tokenClass) { optModFunc(tokenText, tokenClass, f, curDocChar); curDocChar += tokenText.length; }; } - while (lineEntry && lineStart < endChar && !isTimeUp()) - { - //var timer = newTimeLimit(200); - var lineEnd = lineStart + lineEntry.width; + while (lineEntry && lineStart < endChar && !isTimeUp()) { + // var timer = newTimeLimit(200); + const lineEnd = lineStart + lineEntry.width; curDocChar = lineStart; lineEntry.domInfo.clearSpans(); @@ -1110,8 +993,7 @@ function Ace2Inner(){ markNodeClean(lineEntry.lineNode); - if (rep.selStart && rep.selStart[0] == lineIndex || rep.selEnd && rep.selEnd[0] == lineIndex) - { + if (rep.selStart && rep.selStart[0] == lineIndex || rep.selEnd && rep.selEnd[0] == lineIndex) { selectionNeedsResetting = true; } @@ -1121,8 +1003,7 @@ function Ace2Inner(){ lineEntry = rep.lines.next(lineEntry); lineIndex++; } - if (selectionNeedsResetting) - { + if (selectionNeedsResetting) { currentCallStack.selectionAffected = true; } } @@ -1133,53 +1014,48 @@ function Ace2Inner(){ function getSpansForLine(lineEntry, textAndClassFunc, lineEntryOffsetHint) { - var lineEntryOffset = lineEntryOffsetHint; - if ((typeof lineEntryOffset) != "number") - { + let lineEntryOffset = lineEntryOffsetHint; + if ((typeof lineEntryOffset) !== 'number') { lineEntryOffset = rep.lines.offsetOfEntry(lineEntry); } - var text = lineEntry.text; - var width = lineEntry.width; // text.length+1 - if (text.length === 0) - { + const text = lineEntry.text; + const width = lineEntry.width; // text.length+1 + if (text.length === 0) { // allow getLineStyleFilter to set line-div styles - var func = linestylefilter.getLineStyleFilter( - 0, '', textAndClassFunc, rep.apool); + const func = linestylefilter.getLineStyleFilter( + 0, '', textAndClassFunc, rep.apool); func('', ''); - } - else - { - var offsetIntoLine = 0; - var filteredFunc = linestylefilter.getFilterStack(text, textAndClassFunc, browser); - var lineNum = rep.lines.indexOfEntry(lineEntry); - var aline = rep.alines[lineNum]; + } else { + const offsetIntoLine = 0; + let filteredFunc = linestylefilter.getFilterStack(text, textAndClassFunc, browser); + const lineNum = rep.lines.indexOfEntry(lineEntry); + const aline = rep.alines[lineNum]; filteredFunc = linestylefilter.getLineStyleFilter( - text.length, aline, filteredFunc, rep.apool); + text.length, aline, filteredFunc, rep.apool); filteredFunc(text, ''); } } - var observedChanges; + let observedChanges; function clearObservedChanges() { observedChanges = { - cleanNodesNearChanges: {} + cleanNodesNearChanges: {}, }; } clearObservedChanges(); function getCleanNodeByKey(key) { - var p = PROFILER("getCleanNodeByKey", false); + const p = PROFILER('getCleanNodeByKey', false); p.extra = 0; - var n = doc.getElementById(key); + let n = doc.getElementById(key); // copying and pasting can lead to duplicate ids - while (n && isNodeDirty(n)) - { + while (n && isNodeDirty(n)) { p.extra++; - n.id = ""; + n.id = ''; n = doc.getElementById(key); } - p.literal(p.extra, "extra"); + p.literal(p.extra, 'extra'); p.end(); return n; } @@ -1189,41 +1065,31 @@ function Ace2Inner(){ // (from how it looks in our representation) and record them in a way // that can be used to "normalize" the document (apply the changes to our // representation, and put the DOM in a canonical form). - var cleanNode; - var hasAdjacentDirtyness; - if (!isNodeDirty(node)) - { + let cleanNode; + let hasAdjacentDirtyness; + if (!isNodeDirty(node)) { cleanNode = node; var prevSib = cleanNode.previousSibling; var nextSib = cleanNode.nextSibling; hasAdjacentDirtyness = ((prevSib && isNodeDirty(prevSib)) || (nextSib && isNodeDirty(nextSib))); - } - else - { + } else { // node is dirty, look for clean node above - var upNode = node.previousSibling; - while (upNode && isNodeDirty(upNode)) - { + let upNode = node.previousSibling; + while (upNode && isNodeDirty(upNode)) { upNode = upNode.previousSibling; } - if (upNode) - { + if (upNode) { cleanNode = upNode; - } - else - { - var downNode = node.nextSibling; - while (downNode && isNodeDirty(downNode)) - { + } else { + let downNode = node.nextSibling; + while (downNode && isNodeDirty(downNode)) { downNode = downNode.nextSibling; } - if (downNode) - { + if (downNode) { cleanNode = downNode; } } - if (!cleanNode) - { + if (!cleanNode) { // Couldn't find any adjacent clean nodes! // Since top and bottom of doc is dirty, the dirty area will be detected. return; @@ -1231,26 +1097,22 @@ function Ace2Inner(){ hasAdjacentDirtyness = true; } - if (hasAdjacentDirtyness) - { + if (hasAdjacentDirtyness) { // previous or next line is dirty - observedChanges.cleanNodesNearChanges['$' + uniqueId(cleanNode)] = true; - } - else - { + observedChanges.cleanNodesNearChanges[`$${uniqueId(cleanNode)}`] = true; + } else { // next and prev lines are clean (if they exist) - var lineKey = uniqueId(cleanNode); + const lineKey = uniqueId(cleanNode); var prevSib = cleanNode.previousSibling; var nextSib = cleanNode.nextSibling; - var actualPrevKey = ((prevSib && uniqueId(prevSib)) || null); - var actualNextKey = ((nextSib && uniqueId(nextSib)) || null); - var repPrevEntry = rep.lines.prev(rep.lines.atKey(lineKey)); - var repNextEntry = rep.lines.next(rep.lines.atKey(lineKey)); - var repPrevKey = ((repPrevEntry && repPrevEntry.key) || null); - var repNextKey = ((repNextEntry && repNextEntry.key) || null); - if (actualPrevKey != repPrevKey || actualNextKey != repNextKey) - { - observedChanges.cleanNodesNearChanges['$' + uniqueId(cleanNode)] = true; + const actualPrevKey = ((prevSib && uniqueId(prevSib)) || null); + const actualNextKey = ((nextSib && uniqueId(nextSib)) || null); + const repPrevEntry = rep.lines.prev(rep.lines.atKey(lineKey)); + const repNextEntry = rep.lines.next(rep.lines.atKey(lineKey)); + const repPrevKey = ((repPrevEntry && repPrevEntry.key) || null); + const repNextKey = ((repNextEntry && repNextEntry.key) || null); + if (actualPrevKey != repPrevKey || actualNextKey != repNextKey) { + observedChanges.cleanNodesNearChanges[`$${uniqueId(cleanNode)}`] = true; } } } @@ -1259,17 +1121,15 @@ function Ace2Inner(){ if (currentCallStack.observedSelection) return; currentCallStack.observedSelection = true; - var p = PROFILER("getSelection", false); - var selection = getSelection(); + const p = PROFILER('getSelection', false); + const selection = getSelection(); p.end(); - if (selection) - { - var node1 = topLevel(selection.startPoint.node); - var node2 = topLevel(selection.endPoint.node); + if (selection) { + const node1 = topLevel(selection.startPoint.node); + const node2 = topLevel(selection.endPoint.node); if (node1) observeChangesAroundNode(node1); - if (node2 && node1 != node2) - { + if (node2 && node1 != node2) { observeChangesAroundNode(node2); } } @@ -1278,14 +1138,11 @@ function Ace2Inner(){ function observeSuspiciousNodes() { // inspired by Firefox bug #473255, where pasting formatted text // causes the cursor to jump away, making the new HTML never found. - if (root.getElementsByTagName) - { - var nds = root.getElementsByTagName("style"); - for (var i = 0; i < nds.length; i++) - { - var n = topLevel(nds[i]); - if (n && n.parentNode == root) - { + if (root.getElementsByTagName) { + const nds = root.getElementsByTagName('style'); + for (let i = 0; i < nds.length; i++) { + const n = topLevel(nds[i]); + if (n && n.parentNode == root) { observeChangesAroundNode(n); } } @@ -1293,56 +1150,49 @@ function Ace2Inner(){ } function incorporateUserChanges(isTimeUp) { - if (currentCallStack.domClean) return false; currentCallStack.isUserChange = true; isTimeUp = (isTimeUp || - function() { + function () { return false; }); if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false; - var p = PROFILER("incorp", false); + const p = PROFILER('incorp', false); - //if (doc.body.innerHTML.indexOf("AppJet") >= 0) - //dmesg(htmlPrettyEscape(doc.body.innerHTML)); - //if (top.RECORD) top.RECORD.push(doc.body.innerHTML); + // if (doc.body.innerHTML.indexOf("AppJet") >= 0) + // dmesg(htmlPrettyEscape(doc.body.innerHTML)); + // if (top.RECORD) top.RECORD.push(doc.body.innerHTML); // returns true if dom changes were made - if (!root.firstChild) - { - root.innerHTML = "
                "; + if (!root.firstChild) { + root.innerHTML = '
                '; } - p.mark("obs"); + p.mark('obs'); observeChangesAroundSelection(); observeSuspiciousNodes(); - p.mark("dirty"); - var dirtyRanges = getDirtyRanges(); - var dirtyRangesCheckOut = true; - var j = 0; - var a, b; - while (j < dirtyRanges.length) - { + p.mark('dirty'); + let dirtyRanges = getDirtyRanges(); + let dirtyRangesCheckOut = true; + let j = 0; + let a, b; + while (j < dirtyRanges.length) { a = dirtyRanges[j][0]; b = dirtyRanges[j][1]; - if (!((a === 0 || getCleanNodeByKey(rep.lines.atIndex(a - 1).key)) && (b == rep.lines.length() || getCleanNodeByKey(rep.lines.atIndex(b).key)))) - { + if (!((a === 0 || getCleanNodeByKey(rep.lines.atIndex(a - 1).key)) && (b == rep.lines.length() || getCleanNodeByKey(rep.lines.atIndex(b).key)))) { dirtyRangesCheckOut = false; break; } j++; } - if (!dirtyRangesCheckOut) - { - var numBodyNodes = root.childNodes.length; - for (var k = 0; k < numBodyNodes; k++) - { - var bodyNode = root.childNodes.item(k); - if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) - { + if (!dirtyRangesCheckOut) { + const numBodyNodes = root.childNodes.length; + for (var k = 0; k < numBodyNodes; k++) { + const bodyNode = root.childNodes.item(k); + if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) { observeChangesAroundNode(bodyNode); } } @@ -1351,74 +1201,65 @@ function Ace2Inner(){ clearObservedChanges(); - p.mark("getsel"); - var selection = getSelection(); + p.mark('getsel'); + const selection = getSelection(); - var selStart, selEnd; // each one, if truthy, has [line,char] needed to set selection - var i = 0; - var splicesToDo = []; - var netNumLinesChangeSoFar = 0; - var toDeleteAtEnd = []; - p.mark("ranges"); - p.literal(dirtyRanges.length, "numdirt"); - var domInsertsNeeded = []; // each entry is [nodeToInsertAfter, [info1, info2, ...]] - while (i < dirtyRanges.length) - { - var range = dirtyRanges[i]; + let selStart, selEnd; // each one, if truthy, has [line,char] needed to set selection + let i = 0; + const splicesToDo = []; + let netNumLinesChangeSoFar = 0; + const toDeleteAtEnd = []; + p.mark('ranges'); + p.literal(dirtyRanges.length, 'numdirt'); + const domInsertsNeeded = []; // each entry is [nodeToInsertAfter, [info1, info2, ...]] + while (i < dirtyRanges.length) { + const range = dirtyRanges[i]; a = range[0]; b = range[1]; - var firstDirtyNode = (((a === 0) && root.firstChild) || getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling); + let firstDirtyNode = (((a === 0) && root.firstChild) || getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling); firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode); - var lastDirtyNode = (((b == rep.lines.length()) && root.lastChild) || getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling); + let lastDirtyNode = (((b == rep.lines.length()) && root.lastChild) || getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling); lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode); - if (firstDirtyNode && lastDirtyNode) - { - var cc = makeContentCollector(isStyled, browser, rep.apool, null, className2Author); + if (firstDirtyNode && lastDirtyNode) { + const cc = makeContentCollector(isStyled, browser, rep.apool, null, className2Author); cc.notifySelection(selection); - var dirtyNodes = []; - for (var n = firstDirtyNode; n && !(n.previousSibling && n.previousSibling == lastDirtyNode); - n = n.nextSibling) - { - if (browser.msie) - { + const dirtyNodes = []; + for (let n = firstDirtyNode; n && !(n.previousSibling && n.previousSibling == lastDirtyNode); + n = n.nextSibling) { + if (browser.msie) { // try to undo IE's pesky and overzealous linkification - try - { - n.createTextRange().execCommand("unlink", false, null); - } - catch (e) - {} + try { + n.createTextRange().execCommand('unlink', false, null); + } catch (e) {} } cc.collectContent(n); dirtyNodes.push(n); } cc.notifyNextNode(lastDirtyNode.nextSibling); - var lines = cc.getLines(); - if ((lines.length <= 1 || lines[lines.length - 1] !== "") && lastDirtyNode.nextSibling) - { + let lines = cc.getLines(); + if ((lines.length <= 1 || lines[lines.length - 1] !== '') && lastDirtyNode.nextSibling) { // dirty region doesn't currently end a line, even taking the following node // (or lack of node) into account, so include the following clean node. // It could be SPAN or a DIV; basically this is any case where the contentCollector // decides it isn't done. // Note that this clean node might need to be there for the next dirty range. b++; - var cleanLine = lastDirtyNode.nextSibling; + const cleanLine = lastDirtyNode.nextSibling; cc.collectContent(cleanLine); toDeleteAtEnd.push(cleanLine); cc.notifyNextNode(cleanLine.nextSibling); } - var ccData = cc.finish(); - var ss = ccData.selStart; - var se = ccData.selEnd; + const ccData = cc.finish(); + const ss = ccData.selStart; + const se = ccData.selEnd; lines = ccData.lines; - var lineAttribs = ccData.lineAttribs; - var linesWrapped = ccData.linesWrapped; + const lineAttribs = ccData.lineAttribs; + const linesWrapped = ccData.linesWrapped; var scrollToTheLeftNeeded = false; - if (linesWrapped > 0) - { - if(!browser.msie){ + if (linesWrapped > 0) { + if (!browser.msie) { // chrome decides in it's infinite wisdom that its okay to put the browsers visisble window in the middle of the span // an outcome of this is that the first chars of the string are no longer visible to the user.. Yay chrome.. // Move the browsers visible area to the left hand side of the span @@ -1430,120 +1271,113 @@ function Ace2Inner(){ if (ss[0] >= 0) selStart = [ss[0] + a + netNumLinesChangeSoFar, ss[1]]; if (se[0] >= 0) selEnd = [se[0] + a + netNumLinesChangeSoFar, se[1]]; - var entries = []; - var nodeToAddAfter = lastDirtyNode; - var lineNodeInfos = new Array(lines.length); - for (var k = 0; k < lines.length; k++) - { - var lineString = lines[k]; - var newEntry = createDomLineEntry(lineString); + const entries = []; + const nodeToAddAfter = lastDirtyNode; + const lineNodeInfos = new Array(lines.length); + for (var k = 0; k < lines.length; k++) { + const lineString = lines[k]; + const newEntry = createDomLineEntry(lineString); entries.push(newEntry); lineNodeInfos[k] = newEntry.domInfo; } - //var fragment = magicdom.wrapDom(document.createDocumentFragment()); + // var fragment = magicdom.wrapDom(document.createDocumentFragment()); domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]); - _.each(dirtyNodes,function(n){ + _.each(dirtyNodes, (n) => { toDeleteAtEnd.push(n); }); - var spliceHints = {}; + const spliceHints = {}; if (selStart) spliceHints.selStart = selStart; if (selEnd) spliceHints.selEnd = selEnd; splicesToDo.push([a + netNumLinesChangeSoFar, b - a, entries, lineAttribs, spliceHints]); netNumLinesChangeSoFar += (lines.length - (b - a)); - } - else if (b > a) - { - splicesToDo.push([a + netNumLinesChangeSoFar, b - a, [], - [] - ]); + } else if (b > a) { + splicesToDo.push([a + netNumLinesChangeSoFar, + b - a, + [], + []]); } i++; } - var domChanges = (splicesToDo.length > 0); + const domChanges = (splicesToDo.length > 0); // update the representation - p.mark("splice"); - _.each(splicesToDo, function(splice) { + p.mark('splice'); + _.each(splicesToDo, (splice) => { doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]); }); - //p.mark("relex"); - //rep.lexer.lexCharRange(scroll.getVisibleCharRange(rep), function() { return false; }); - //var isTimeUp = newTimeLimit(100); + // p.mark("relex"); + // rep.lexer.lexCharRange(scroll.getVisibleCharRange(rep), function() { return false; }); + // var isTimeUp = newTimeLimit(100); // do DOM inserts - p.mark("insert"); - _.each(domInsertsNeeded,function(ins) { + p.mark('insert'); + _.each(domInsertsNeeded, (ins) => { insertDomLines(ins[0], ins[1], isTimeUp); }); - p.mark("del"); + p.mark('del'); // delete old dom nodes - _.each(toDeleteAtEnd,function(n) { - //var id = n.uniqueId(); + _.each(toDeleteAtEnd, (n) => { + // var id = n.uniqueId(); // parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf) - if(n.parentNode) n.parentNode.removeChild(n); + if (n.parentNode) n.parentNode.removeChild(n); - //dmesg(htmlPrettyEscape(htmlForRemovedChild(n))); + // dmesg(htmlPrettyEscape(htmlForRemovedChild(n))); }); - if(scrollToTheLeftNeeded){ // needed to stop chrome from breaking the ui when long strings without spaces are pasted - $("#innerdocbody").scrollLeft(0); + if (scrollToTheLeftNeeded) { // needed to stop chrome from breaking the ui when long strings without spaces are pasted + $('#innerdocbody').scrollLeft(0); } - p.mark("findsel"); + p.mark('findsel'); // if the nodes that define the selection weren't encountered during // content collection, figure out where those nodes are now. - if (selection && !selStart) - { - //if (domChanges) dmesg("selection not collected"); - var selStartFromHook = hooks.callAll('aceStartLineAndCharForPoint', { + if (selection && !selStart) { + // if (domChanges) dmesg("selection not collected"); + const selStartFromHook = hooks.callAll('aceStartLineAndCharForPoint', { callstack: currentCallStack, - editorInfo: editorInfo, - rep: rep, - root:root, - point:selection.startPoint, - documentAttributeManager: documentAttributeManager + editorInfo, + rep, + root, + point: selection.startPoint, + documentAttributeManager, }); - selStart = (selStartFromHook==null||selStartFromHook.length==0)?getLineAndCharForPoint(selection.startPoint):selStartFromHook; + selStart = (selStartFromHook == null || selStartFromHook.length == 0) ? getLineAndCharForPoint(selection.startPoint) : selStartFromHook; } - if (selection && !selEnd) - { - var selEndFromHook = hooks.callAll('aceEndLineAndCharForPoint', { + if (selection && !selEnd) { + const selEndFromHook = hooks.callAll('aceEndLineAndCharForPoint', { callstack: currentCallStack, - editorInfo: editorInfo, - rep: rep, - root:root, - point:selection.endPoint, - documentAttributeManager: documentAttributeManager + editorInfo, + rep, + root, + point: selection.endPoint, + documentAttributeManager, }); - selEnd = (selEndFromHook==null||selEndFromHook.length==0)?getLineAndCharForPoint(selection.endPoint):selEndFromHook; + selEnd = (selEndFromHook == null || selEndFromHook.length == 0) ? getLineAndCharForPoint(selection.endPoint) : selEndFromHook; } // selection from content collection can, in various ways, extend past final // BR in firefox DOM, so cap the line - var numLines = rep.lines.length(); - if (selStart && selStart[0] >= numLines) - { + const numLines = rep.lines.length(); + if (selStart && selStart[0] >= numLines) { selStart[0] = numLines - 1; selStart[1] = rep.lines.atIndex(selStart[0]).text.length; } - if (selEnd && selEnd[0] >= numLines) - { + if (selEnd && selEnd[0] >= numLines) { selEnd[0] = numLines - 1; selEnd[1] = rep.lines.atIndex(selEnd[0]).text.length; } - p.mark("repsel"); + p.mark('repsel'); // update rep if we have a new selection // NOTE: IE loses the selection when you click stuff in e.g. the // editbar, so removing the selection when it's lost is not a good // idea. if (selection) repSelectionChange(selStart, selEnd, selection && selection.focusAtStart); // update browser selection - p.mark("browsel"); - if (selection && (domChanges || isCaret())) - { + p.mark('browsel'); + if (selection && (domChanges || isCaret())) { // if no DOM changes (not this case), want to treat range selection delicately, // e.g. in IE not lose which end of the selection is the focus/anchor; // on the other hand, we may have just noticed a press of PageUp/PageDown @@ -1552,11 +1386,11 @@ function Ace2Inner(){ currentCallStack.domClean = true; - p.mark("fixview"); + p.mark('fixview'); fixView(); - p.end("END"); + p.end('END'); return domChanges; } @@ -1566,7 +1400,7 @@ function Ace2Inner(){ italic: true, underline: true, strikethrough: true, - list: true + list: true, }; function isStyleAttribute(aname) { @@ -1579,64 +1413,57 @@ function Ace2Inner(){ function insertDomLines(nodeToAddAfter, infoStructs, isTimeUp) { isTimeUp = (isTimeUp || - function() { + function () { return false; }); - var lastEntry; - var lineStartOffset; + let lastEntry; + let lineStartOffset; if (infoStructs.length < 1) return; - var startEntry = rep.lines.atKey(uniqueId(infoStructs[0].node)); - var endEntry = rep.lines.atKey(uniqueId(infoStructs[infoStructs.length - 1].node)); - var charStart = rep.lines.offsetOfEntry(startEntry); - var charEnd = rep.lines.offsetOfEntry(endEntry) + endEntry.width; + const startEntry = rep.lines.atKey(uniqueId(infoStructs[0].node)); + const endEntry = rep.lines.atKey(uniqueId(infoStructs[infoStructs.length - 1].node)); + const charStart = rep.lines.offsetOfEntry(startEntry); + const charEnd = rep.lines.offsetOfEntry(endEntry) + endEntry.width; - //rep.lexer.lexCharRange([charStart, charEnd], isTimeUp); - _.each(infoStructs, function(info) { - var p2 = PROFILER("insertLine", false); - var node = info.node; - var key = uniqueId(node); - var entry; - p2.mark("findEntry"); - if (lastEntry) - { + // rep.lexer.lexCharRange([charStart, charEnd], isTimeUp); + _.each(infoStructs, (info) => { + const p2 = PROFILER('insertLine', false); + const node = info.node; + const key = uniqueId(node); + let entry; + p2.mark('findEntry'); + if (lastEntry) { // optimization to avoid recalculation - var next = rep.lines.next(lastEntry); - if (next && next.key == key) - { + const next = rep.lines.next(lastEntry); + if (next && next.key == key) { entry = next; lineStartOffset += lastEntry.width; } } - if (!entry) - { - p2.literal(1, "nonopt"); + if (!entry) { + p2.literal(1, 'nonopt'); entry = rep.lines.atKey(key); lineStartOffset = rep.lines.offsetOfKey(key); - } - else p2.literal(0, "nonopt"); + } else { p2.literal(0, 'nonopt'); } lastEntry = entry; - p2.mark("spans"); - getSpansForLine(entry, function(tokenText, tokenClass) { + p2.mark('spans'); + getSpansForLine(entry, (tokenText, tokenClass) => { info.appendSpan(tokenText, tokenClass); }, lineStartOffset, isTimeUp()); - //else if (entry.text.length > 0) { - //info.appendSpan(entry.text, 'dirty'); - //} - p2.mark("addLine"); + // else if (entry.text.length > 0) { + // info.appendSpan(entry.text, 'dirty'); + // } + p2.mark('addLine'); info.prepareForAdd(); entry.lineMarker = info.lineMarker; - if (!nodeToAddAfter) - { + if (!nodeToAddAfter) { root.insertBefore(node, root.firstChild); - } - else - { + } else { root.insertBefore(node, nodeToAddAfter.nextSibling); } nodeToAddAfter = node; info.notifyAdded(); - p2.mark("markClean"); + p2.mark('markClean'); markNodeClean(node); p2.end(); }); @@ -1667,175 +1494,141 @@ function Ace2Inner(){ function handleReturnIndentation() { // on return, indent to level of previous line - if (isCaret() && caretColumn() === 0 && caretLine() > 0) - { - var lineNum = caretLine(); - var thisLine = rep.lines.atIndex(lineNum); - var prevLine = rep.lines.prev(thisLine); - var prevLineText = prevLine.text; - var theIndent = /^ *(?:)/.exec(prevLineText)[0]; - var shouldIndent = parent.parent.clientVars.indentationOnNewLine; - if (shouldIndent && /[\[\(\:\{]\s*$/.exec(prevLineText)) - { + if (isCaret() && caretColumn() === 0 && caretLine() > 0) { + const lineNum = caretLine(); + const thisLine = rep.lines.atIndex(lineNum); + const prevLine = rep.lines.prev(thisLine); + const prevLineText = prevLine.text; + let theIndent = /^ *(?:)/.exec(prevLineText)[0]; + const shouldIndent = parent.parent.clientVars.indentationOnNewLine; + if (shouldIndent && /[\[\(\:\{]\s*$/.exec(prevLineText)) { theIndent += THE_TAB; } - var cs = Changeset.builder(rep.lines.totalWidth()).keep( - rep.lines.offsetOfIndex(lineNum), lineNum).insert( - theIndent, [ - ['author', thisAuthor] - ], rep.apool).toString(); + const cs = Changeset.builder(rep.lines.totalWidth()).keep( + rep.lines.offsetOfIndex(lineNum), lineNum).insert( + theIndent, [ + ['author', thisAuthor], + ], rep.apool).toString(); performDocumentApplyChangeset(cs); performSelectionChange([lineNum, theIndent.length], [lineNum, theIndent.length]); } } function getPointForLineAndChar(lineAndChar) { - var line = lineAndChar[0]; - var charsLeft = lineAndChar[1]; + const line = lineAndChar[0]; + let charsLeft = lineAndChar[1]; // Do not uncomment this in production it will break iFrames. - //top.console.log("line: %d, key: %s, node: %o", line, rep.lines.atIndex(line).key, - //getCleanNodeByKey(rep.lines.atIndex(line).key)); - var lineEntry = rep.lines.atIndex(line); + // top.console.log("line: %d, key: %s, node: %o", line, rep.lines.atIndex(line).key, + // getCleanNodeByKey(rep.lines.atIndex(line).key)); + const lineEntry = rep.lines.atIndex(line); charsLeft -= lineEntry.lineMarker; - if (charsLeft < 0) - { + if (charsLeft < 0) { charsLeft = 0; } - var lineNode = lineEntry.lineNode; - var n = lineNode; - var after = false; - if (charsLeft === 0) - { - var index = 0; + const lineNode = lineEntry.lineNode; + let n = lineNode; + let after = false; + if (charsLeft === 0) { + let index = 0; if (browser.msie && parseInt(browser.version) >= 11) { browser.msie = false; // Temp fix to resolve enter and backspace issues.. // Note that this makes MSIE behave like modern browsers.. } - if (browser.msie && line == (rep.lines.length() - 1) && lineNode.childNodes.length === 0) - { + if (browser.msie && line == (rep.lines.length() - 1) && lineNode.childNodes.length === 0) { // best to stay at end of last empty div in IE index = 1; } return { node: lineNode, - index: index, - maxIndex: 1 + index, + maxIndex: 1, }; } - while (!(n == lineNode && after)) - { - if (after) - { - if (n.nextSibling) - { + while (!(n == lineNode && after)) { + if (after) { + if (n.nextSibling) { n = n.nextSibling; after = false; + } else { n = n.parentNode; } + } else if (isNodeText(n)) { + const len = n.nodeValue.length; + if (charsLeft <= len) { + return { + node: n, + index: charsLeft, + maxIndex: len, + }; } - else n = n.parentNode; - } - else - { - if (isNodeText(n)) - { - var len = n.nodeValue.length; - if (charsLeft <= len) - { - return { - node: n, - index: charsLeft, - maxIndex: len - }; - } - charsLeft -= len; - after = true; - } - else - { - if (n.firstChild) n = n.firstChild; - else after = true; - } - } + charsLeft -= len; + after = true; + } else if (n.firstChild) { n = n.firstChild; } else { after = true; } } return { node: lineNode, index: 1, - maxIndex: 1 + maxIndex: 1, }; } function nodeText(n) { - if (browser.msie) { + if (browser.msie) { return n.innerText; - } else { + } else { return n.textContent || n.nodeValue || ''; - } + } } function getLineAndCharForPoint(point) { // Turn DOM node selection into [line,char] selection. // This method has to work when the DOM is not pristine, // assuming the point is not in a dirty node. - if (point.node == root) - { - if (point.index === 0) - { + if (point.node == root) { + if (point.index === 0) { return [0, 0]; - } - else - { - var N = rep.lines.length(); - var ln = rep.lines.atIndex(N - 1); + } else { + const N = rep.lines.length(); + const ln = rep.lines.atIndex(N - 1); return [N - 1, ln.text.length]; } - } - else - { - var n = point.node; - var col = 0; + } else { + let n = point.node; + let col = 0; // if this part fails, it probably means the selection node // was dirty, and we didn't see it when collecting dirty nodes. - if (isNodeText(n)) - { + if (isNodeText(n)) { col = point.index; - } - else if (point.index > 0) - { + } else if (point.index > 0) { col = nodeText(n).length; } - var parNode, prevSib; - while ((parNode = n.parentNode) != root) - { - if ((prevSib = n.previousSibling)) - { + let parNode, prevSib; + while ((parNode = n.parentNode) != root) { + if ((prevSib = n.previousSibling)) { n = prevSib; col += nodeText(n).length; - } - else - { + } else { n = parNode; } } - if (n.firstChild && isBlockElement(n.firstChild)) - { + if (n.firstChild && isBlockElement(n.firstChild)) { col += 1; // lineMarker } - var lineEntry = rep.lines.atKey(n.id); - var lineNum = rep.lines.indexOfEntry(lineEntry); + const lineEntry = rep.lines.atKey(n.id); + const lineNum = rep.lines.indexOfEntry(lineEntry); return [lineNum, col]; } } editorInfo.ace_getLineAndCharForPoint = getLineAndCharForPoint; function createDomLineEntry(lineString) { - var info = doCreateDomLine(lineString.length > 0); - var newNode = info.node; + const info = doCreateDomLine(lineString.length > 0); + const newNode = info.node; return { key: uniqueId(newNode), text: lineString, lineNode: newNode, domInfo: info, - lineMarker: 0 + lineMarker: 0, }; } @@ -1846,39 +1639,35 @@ function Ace2Inner(){ function performDocumentApplyChangeset(changes, insertsAfterSelection) { doRepApplyChangeset(changes, insertsAfterSelection); - var requiredSelectionSetting = null; - if (rep.selStart && rep.selEnd) - { - var selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; - var selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; - var result = Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection); + let requiredSelectionSetting = null; + if (rep.selStart && rep.selEnd) { + const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; + const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; + const result = Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection); requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart]; } - var linesMutatee = { - splice: function(start, numRemoved, newLinesVA) { - var args = Array.prototype.slice.call(arguments, 2); - domAndRepSplice(start, numRemoved, _.map(args, function(s){ return s.slice(0, -1); }), null); + const linesMutatee = { + splice(start, numRemoved, newLinesVA) { + const args = Array.prototype.slice.call(arguments, 2); + domAndRepSplice(start, numRemoved, _.map(args, (s) => s.slice(0, -1)), null); }, - get: function(i) { - return rep.lines.atIndex(i).text + '\n'; + get(i) { + return `${rep.lines.atIndex(i).text}\n`; }, - length: function() { + length() { return rep.lines.length(); }, - slice_notused: function(start, end) { - return _.map(rep.lines.slice(start, end), function(e) { - return e.text + '\n'; - }); - } + slice_notused(start, end) { + return _.map(rep.lines.slice(start, end), (e) => `${e.text}\n`); + }, }; Changeset.mutateTextLines(changes, linesMutatee); checkALines(); - if (requiredSelectionSetting) - { + if (requiredSelectionSetting) { performSelectionChange(lineAndColumnFromChar(requiredSelectionSetting[0]), lineAndColumnFromChar(requiredSelectionSetting[1]), requiredSelectionSetting[2]); } @@ -1887,39 +1676,32 @@ function Ace2Inner(){ // so if no explicit time limit, don't spend a lot of time highlighting isTimeUp = (isTimeUp || newTimeLimit(50)); - var keysToDelete = []; - if (deleteCount > 0) - { - var entryToDelete = rep.lines.atIndex(startLine); - for (var i = 0; i < deleteCount; i++) - { + const keysToDelete = []; + if (deleteCount > 0) { + let entryToDelete = rep.lines.atIndex(startLine); + for (let i = 0; i < deleteCount; i++) { keysToDelete.push(entryToDelete.key); entryToDelete = rep.lines.next(entryToDelete); } } - var lineEntries = _.map(newLineStrings, createDomLineEntry); + const lineEntries = _.map(newLineStrings, createDomLineEntry); doRepLineSplice(startLine, deleteCount, lineEntries); - var nodeToAddAfter; - if (startLine > 0) - { + let nodeToAddAfter; + if (startLine > 0) { nodeToAddAfter = getCleanNodeByKey(rep.lines.atIndex(startLine - 1).key); - } - else nodeToAddAfter = null; + } else { nodeToAddAfter = null; } - insertDomLines(nodeToAddAfter, _.map(lineEntries, function(entry) { - return entry.domInfo; - }), isTimeUp); + insertDomLines(nodeToAddAfter, _.map(lineEntries, (entry) => entry.domInfo), isTimeUp); - _.each(keysToDelete, function(k) { - var n = doc.getElementById(k); + _.each(keysToDelete, (k) => { + const n = doc.getElementById(k); n.parentNode.removeChild(n); }); - if ((rep.selStart && rep.selStart[0] >= startLine && rep.selStart[0] <= startLine + deleteCount) || (rep.selEnd && rep.selEnd[0] >= startLine && rep.selEnd[0] <= startLine + deleteCount)) - { + if ((rep.selStart && rep.selStart[0] >= startLine && rep.selStart[0] <= startLine + deleteCount) || (rep.selEnd && rep.selEnd[0] >= startLine && rep.selEnd[0] <= startLine + deleteCount)) { currentCallStack.selectionAffected = true; } } @@ -1927,31 +1709,25 @@ function Ace2Inner(){ function checkChangesetLineInformationAgainstRep(changes) { return true; // disable for speed - var opIter = Changeset.opIterator(Changeset.unpack(changes).ops); - var curOffset = 0; - var curLine = 0; - var curCol = 0; - while (opIter.hasNext()) - { - var o = opIter.next(); - if (o.opcode == '-' || o.opcode == '=') - { + const opIter = Changeset.opIterator(Changeset.unpack(changes).ops); + let curOffset = 0; + let curLine = 0; + let curCol = 0; + while (opIter.hasNext()) { + const o = opIter.next(); + if (o.opcode == '-' || o.opcode == '=') { curOffset += o.chars; - if (o.lines) - { + if (o.lines) { curLine += o.lines; curCol = 0; - } - else - { + } else { curCol += o.chars; } } - var calcLine = rep.lines.indexOfOffset(curOffset); - var calcLineStart = rep.lines.offsetOfIndex(calcLine); - var calcCol = curOffset - calcLineStart; - if (calcCol != curCol || calcLine != curLine) - { + const calcLine = rep.lines.indexOfOffset(curOffset); + const calcLineStart = rep.lines.offsetOfIndex(calcLine); + const calcCol = curOffset - calcLineStart; + if (calcCol != curCol || calcLine != curLine) { return false; } } @@ -1961,94 +1737,75 @@ function Ace2Inner(){ function doRepApplyChangeset(changes, insertsAfterSelection) { Changeset.checkRep(changes); - if (Changeset.oldLen(changes) != rep.alltext.length) throw new Error("doRepApplyChangeset length mismatch: " + Changeset.oldLen(changes) + "/" + rep.alltext.length); + if (Changeset.oldLen(changes) != rep.alltext.length) throw new Error(`doRepApplyChangeset length mismatch: ${Changeset.oldLen(changes)}/${rep.alltext.length}`); - if (!checkChangesetLineInformationAgainstRep(changes)) - { - throw new Error("doRepApplyChangeset line break mismatch"); + if (!checkChangesetLineInformationAgainstRep(changes)) { + throw new Error('doRepApplyChangeset line break mismatch'); } (function doRecordUndoInformation(changes) { - var editEvent = currentCallStack.editEvent; - if (editEvent.eventType == "nonundoable") - { - if (!editEvent.changeset) - { + const editEvent = currentCallStack.editEvent; + if (editEvent.eventType == 'nonundoable') { + if (!editEvent.changeset) { editEvent.changeset = changes; - } - else - { + } else { editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool); } - } - else - { - var inverseChangeset = Changeset.inverse(changes, { - get: function(i) { - return rep.lines.atIndex(i).text + '\n'; + } else { + const inverseChangeset = Changeset.inverse(changes, { + get(i) { + return `${rep.lines.atIndex(i).text}\n`; }, - length: function() { + length() { return rep.lines.length(); - } + }, }, rep.alines, rep.apool); - if (!editEvent.backset) - { + if (!editEvent.backset) { editEvent.backset = inverseChangeset; - } - else - { + } else { editEvent.backset = Changeset.compose(inverseChangeset, editEvent.backset, rep.apool); } } })(changes); - //rep.alltext = Changeset.applyToText(changes, rep.alltext); + // rep.alltext = Changeset.applyToText(changes, rep.alltext); Changeset.mutateAttributionLines(changes, rep.alines, rep.apool); - if (changesetTracker.isTracking()) - { + if (changesetTracker.isTracking()) { changesetTracker.composeUserChangeset(changes); } - } /* Converts the position of a char (index in String) into a [row, col] tuple */ function lineAndColumnFromChar(x) { - var lineEntry = rep.lines.atOffset(x); - var lineStart = rep.lines.offsetOfEntry(lineEntry); - var lineNum = rep.lines.indexOfEntry(lineEntry); + const lineEntry = rep.lines.atOffset(x); + const lineStart = rep.lines.offsetOfEntry(lineEntry); + const lineNum = rep.lines.indexOfEntry(lineEntry); return [lineNum, x - lineStart]; } function performDocumentReplaceCharRange(startChar, endChar, newText) { - if (startChar == endChar && newText.length === 0) - { + if (startChar == endChar && newText.length === 0) { return; } // Requires that the replacement preserve the property that the // internal document text ends in a newline. Given this, we // rewrite the splice so that it doesn't touch the very last // char of the document. - if (endChar == rep.alltext.length) - { - if (startChar == endChar) - { + if (endChar == rep.alltext.length) { + if (startChar == endChar) { // an insert at end startChar--; endChar--; - newText = '\n' + newText.substring(0, newText.length - 1); - } - else if (newText.length === 0) - { + newText = `\n${newText.substring(0, newText.length - 1)}`; + } else if (newText.length === 0) { // a delete at end startChar--; endChar--; - } - else - { + } else { // a replace at end endChar--; newText = newText.substring(0, newText.length - 1); @@ -2061,18 +1818,18 @@ function Ace2Inner(){ if (start === undefined) start = rep.selStart; if (end === undefined) end = rep.selEnd; - //dmesg(String([start.toSource(),end.toSource(),newText.toSource()])); + // dmesg(String([start.toSource(),end.toSource(),newText.toSource()])); // start[0]: <--- start[1] --->CCCCCCCCCCC\n // CCCCCCCCCCCCCCCCCCCC\n // CCCC\n // end[0]: -------\n - var builder = Changeset.builder(rep.lines.totalWidth()); + const builder = Changeset.builder(rep.lines.totalWidth()); ChangesetUtils.buildKeepToStartOfRange(rep, builder, start); ChangesetUtils.buildRemoveRange(rep, builder, start, end); builder.insert(newText, [ - ['author', thisAuthor] + ['author', thisAuthor], ], rep.apool); - var cs = builder.toString(); + const cs = builder.toString(); performDocumentApplyChangeset(cs); } @@ -2088,71 +1845,71 @@ function Ace2Inner(){ if (!(rep.selStart && rep.selEnd)) return; documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [ - [attributeName, attributeValue] + [attributeName, attributeValue], ]); } editorInfo.ace_setAttributeOnSelection = setAttributeOnSelection; - function getAttributeOnSelection(attributeName, prevChar){ - if (!(rep.selStart && rep.selEnd)) return - var isNotSelection = (rep.selStart[0] == rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]); - if(isNotSelection){ - if(prevChar){ + function getAttributeOnSelection(attributeName, prevChar) { + if (!(rep.selStart && rep.selEnd)) return; + 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 - if(rep.selStart[1] !== 0){ + if (rep.selStart[1] !== 0) { rep.selStart[1]--; } } } - var withIt = Changeset.makeAttribsString('+', [ - [attributeName, 'true'] + const withIt = Changeset.makeAttribsString('+', [ + [attributeName, 'true'], ], rep.apool); - var withItRegex = new RegExp(withIt.replace(/\*/g, '\\*') + "(\\*|$)"); + const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); function hasIt(attribs) { return withItRegex.test(attribs); } - return rangeHasAttrib(rep.selStart, rep.selEnd) + return rangeHasAttrib(rep.selStart, rep.selEnd); function 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 + var 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(var n=selStart[0]+1; n < selEnd[0]; n++) { - hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length]) + for (let n = selStart[0] + 1; n < selEnd[0]; n++) { + hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length]); } // for the last, potentially partial, line - hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]]) + hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]]); - return hasAttrib + return hasAttrib; } // Logic tells us we now have a range on a single line - var lineNum = selStart[0] - , start = selStart[1] - , end = selEnd[1] - , hasAttrib = true + const lineNum = selStart[0]; + const start = selStart[1]; + const end = selEnd[1]; + var hasAttrib = true; // Iterate over attribs on this line - var opIter = Changeset.opIterator(rep.alines[lineNum]) - , indexIntoLine = 0 + const opIter = Changeset.opIterator(rep.alines[lineNum]); + let indexIntoLine = 0; while (opIter.hasNext()) { - var op = opIter.next(); - var opStartInLine = indexIntoLine; - var opEndInLine = opStartInLine + op.chars; + const op = opIter.next(); + const opStartInLine = indexIntoLine; + const opEndInLine = opStartInLine + op.chars; if (!hasIt(op.attribs)) { // does op overlap selection? if (!(opEndInLine <= start || opStartInLine >= end)) { @@ -2163,7 +1920,7 @@ function Ace2Inner(){ indexIntoLine = opEndInLine; } - return hasAttrib + return hasAttrib; } } @@ -2172,59 +1929,52 @@ function Ace2Inner(){ function toggleAttributeOnSelection(attributeName) { if (!(rep.selStart && rep.selEnd)) return; - var selectionAllHasIt = true; - var withIt = Changeset.makeAttribsString('+', [ - [attributeName, 'true'] + let selectionAllHasIt = true; + const withIt = Changeset.makeAttribsString('+', [ + [attributeName, 'true'], ], rep.apool); - var withItRegex = new RegExp(withIt.replace(/\*/g, '\\*') + "(\\*|$)"); + const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); function hasIt(attribs) { return withItRegex.test(attribs); } - var selStartLine = rep.selStart[0]; - var selEndLine = rep.selEnd[0]; - for (var n = selStartLine; n <= selEndLine; n++) - { - var opIter = Changeset.opIterator(rep.alines[n]); - var indexIntoLine = 0; - var selectionStartInLine = 0; + const selStartLine = rep.selStart[0]; + const selEndLine = rep.selEnd[0]; + for (let n = selStartLine; n <= selEndLine; n++) { + const opIter = Changeset.opIterator(rep.alines[n]); + let indexIntoLine = 0; + let selectionStartInLine = 0; if (documentAttributeManager.lineHasMarker(n)) { selectionStartInLine = 1; // ignore "*" used as line marker } - var selectionEndInLine = rep.lines.atIndex(n).text.length; // exclude newline - if (n == selStartLine) - { + let selectionEndInLine = rep.lines.atIndex(n).text.length; // exclude newline + if (n == selStartLine) { selectionStartInLine = rep.selStart[1]; } - if (n == selEndLine) - { + if (n == selEndLine) { selectionEndInLine = rep.selEnd[1]; } - while (opIter.hasNext()) - { - var op = opIter.next(); - var opStartInLine = indexIntoLine; - var opEndInLine = opStartInLine + op.chars; - if (!hasIt(op.attribs)) - { + while (opIter.hasNext()) { + const op = opIter.next(); + const opStartInLine = indexIntoLine; + const opEndInLine = opStartInLine + op.chars; + if (!hasIt(op.attribs)) { // does op overlap selection? - if (!(opEndInLine <= selectionStartInLine || opStartInLine >= selectionEndInLine)) - { + if (!(opEndInLine <= selectionStartInLine || opStartInLine >= selectionEndInLine)) { selectionAllHasIt = false; break; } } indexIntoLine = opEndInLine; } - if (!selectionAllHasIt) - { + if (!selectionAllHasIt) { break; } } - var attributeValue = selectionAllHasIt ? '' : 'true'; + const attributeValue = selectionAllHasIt ? '' : 'true'; documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [[attributeName, attributeValue]]); if (attribIsFormattingStyle(attributeName)) { updateStyleButtonState(attributeName, !selectionAllHasIt); // italic, bold, ... @@ -2242,67 +1992,59 @@ function Ace2Inner(){ function doRepLineSplice(startLine, deleteCount, newLineEntries) { - - _.each(newLineEntries, function(entry) { + _.each(newLineEntries, (entry) => { entry.width = entry.text.length + 1; }); - var startOldChar = rep.lines.offsetOfIndex(startLine); - var endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount); + const startOldChar = rep.lines.offsetOfIndex(startLine); + const endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount); - var oldRegionStart = rep.lines.offsetOfIndex(startLine); - var oldRegionEnd = rep.lines.offsetOfIndex(startLine + deleteCount); + const oldRegionStart = rep.lines.offsetOfIndex(startLine); + const oldRegionEnd = rep.lines.offsetOfIndex(startLine + deleteCount); rep.lines.splice(startLine, deleteCount, newLineEntries); currentCallStack.docTextChanged = true; currentCallStack.repChanged = true; - var newRegionEnd = rep.lines.offsetOfIndex(startLine + newLineEntries.length); + const newRegionEnd = rep.lines.offsetOfIndex(startLine + newLineEntries.length); - var newText = _.map(newLineEntries, function(e) { - return e.text + '\n'; - }).join(''); + const newText = _.map(newLineEntries, (e) => `${e.text}\n`).join(''); rep.alltext = rep.alltext.substring(0, startOldChar) + newText + rep.alltext.substring(endOldChar, rep.alltext.length); - //var newTotalLength = rep.alltext.length; - //rep.lexer.updateBuffer(rep.alltext, oldRegionStart, oldRegionEnd - oldRegionStart, - //newRegionEnd - oldRegionStart); + // var newTotalLength = rep.alltext.length; + // rep.lexer.updateBuffer(rep.alltext, oldRegionStart, oldRegionEnd - oldRegionStart, + // newRegionEnd - oldRegionStart); } function doIncorpLineSplice(startLine, deleteCount, newLineEntries, lineAttribs, hints) { - var startOldChar = rep.lines.offsetOfIndex(startLine); - var endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount); + const startOldChar = rep.lines.offsetOfIndex(startLine); + const endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount); - var oldRegionStart = rep.lines.offsetOfIndex(startLine); + const oldRegionStart = rep.lines.offsetOfIndex(startLine); - var selStartHintChar, selEndHintChar; - if (hints && hints.selStart) - { + let selStartHintChar, selEndHintChar; + if (hints && hints.selStart) { selStartHintChar = rep.lines.offsetOfIndex(hints.selStart[0]) + hints.selStart[1] - oldRegionStart; } - if (hints && hints.selEnd) - { + if (hints && hints.selEnd) { selEndHintChar = rep.lines.offsetOfIndex(hints.selEnd[0]) + hints.selEnd[1] - oldRegionStart; } - var newText = _.map(newLineEntries, function(e) { - return e.text + '\n'; - }).join(''); - var oldText = rep.alltext.substring(startOldChar, endOldChar); - var oldAttribs = rep.alines.slice(startLine, startLine + deleteCount).join(''); - var newAttribs = lineAttribs.join('|1+1') + '|1+1'; // not valid in a changeset - var analysis = analyzeChange(oldText, newText, oldAttribs, newAttribs, selStartHintChar, selEndHintChar); - var commonStart = analysis[0]; - var commonEnd = analysis[1]; - var shortOldText = oldText.substring(commonStart, oldText.length - commonEnd); - var shortNewText = newText.substring(commonStart, newText.length - commonEnd); - var spliceStart = startOldChar + commonStart; - var spliceEnd = endOldChar - commonEnd; - var shiftFinalNewlineToBeforeNewText = false; + const newText = _.map(newLineEntries, (e) => `${e.text}\n`).join(''); + const oldText = rep.alltext.substring(startOldChar, endOldChar); + const oldAttribs = rep.alines.slice(startLine, startLine + deleteCount).join(''); + const newAttribs = `${lineAttribs.join('|1+1')}|1+1`; // not valid in a changeset + const analysis = analyzeChange(oldText, newText, oldAttribs, newAttribs, selStartHintChar, selEndHintChar); + const commonStart = analysis[0]; + let commonEnd = analysis[1]; + let shortOldText = oldText.substring(commonStart, oldText.length - commonEnd); + let shortNewText = newText.substring(commonStart, newText.length - commonEnd); + let spliceStart = startOldChar + commonStart; + let spliceEnd = endOldChar - commonEnd; + let shiftFinalNewlineToBeforeNewText = false; // adjust the splice to not involve the final newline of the document; // be very defensive - if (shortOldText.charAt(shortOldText.length - 1) == '\n' && shortNewText.charAt(shortNewText.length - 1) == '\n') - { + if (shortOldText.charAt(shortOldText.length - 1) == '\n' && shortNewText.charAt(shortNewText.length - 1) == '\n') { // replacing text that ends in newline with text that also ends in newline // (still, after analysis, somehow) shortOldText = shortOldText.slice(0, -1); @@ -2310,19 +2052,16 @@ function Ace2Inner(){ spliceEnd--; commonEnd++; } - if (shortOldText.length === 0 && spliceStart == rep.alltext.length && shortNewText.length > 0) - { + if (shortOldText.length === 0 && spliceStart == rep.alltext.length && shortNewText.length > 0) { // inserting after final newline, bad spliceStart--; spliceEnd--; - shortNewText = '\n' + shortNewText.slice(0, -1); + shortNewText = `\n${shortNewText.slice(0, -1)}`; shiftFinalNewlineToBeforeNewText = true; } - if (spliceEnd == rep.alltext.length && shortOldText.length > 0 && shortNewText.length === 0) - { + if (spliceEnd == rep.alltext.length && shortOldText.length > 0 && shortNewText.length === 0) { // deletion at end of rep.alltext - if (rep.alltext.charAt(spliceStart - 1) == '\n') - { + if (rep.alltext.charAt(spliceStart - 1) == '\n') { // (if not then what the heck? it will definitely lead // to a rep.alltext without a final newline) spliceStart--; @@ -2330,141 +2069,119 @@ function Ace2Inner(){ } } - if (!(shortOldText.length === 0 && shortNewText.length === 0)) - { - var oldDocText = rep.alltext; - var oldLen = oldDocText.length; + if (!(shortOldText.length === 0 && shortNewText.length === 0)) { + const oldDocText = rep.alltext; + const oldLen = oldDocText.length; - var spliceStartLine = rep.lines.indexOfOffset(spliceStart); - var spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine); + const spliceStartLine = rep.lines.indexOfOffset(spliceStart); + const spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine); - var startBuilder = function() { - var builder = Changeset.builder(oldLen); + const startBuilder = function () { + const builder = Changeset.builder(oldLen); builder.keep(spliceStartLineStart, spliceStartLine); builder.keep(spliceStart - spliceStartLineStart); return builder; }; - var eachAttribRun = function(attribs, func /*(startInNewText, endInNewText, attribs)*/ ) { - var attribsIter = Changeset.opIterator(attribs); - var textIndex = 0; - var newTextStart = commonStart; - var newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0); - while (attribsIter.hasNext()) - { - var op = attribsIter.next(); - var nextIndex = textIndex + op.chars; - if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) - { + const eachAttribRun = function (attribs, func /* (startInNewText, endInNewText, attribs)*/) { + const attribsIter = Changeset.opIterator(attribs); + let textIndex = 0; + const newTextStart = commonStart; + const newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0); + while (attribsIter.hasNext()) { + const op = attribsIter.next(); + const nextIndex = textIndex + op.chars; + if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); } textIndex = nextIndex; } }; - var justApplyStyles = (shortNewText == shortOldText); - var theChangeset; + const justApplyStyles = (shortNewText == shortOldText); + let theChangeset; - if (justApplyStyles) - { + if (justApplyStyles) { // create changeset that clears the incorporated styles on // the existing text. we compose this with the // changeset the applies the styles found in the DOM. // This allows us to incorporate, e.g., Safari's native "unbold". - var incorpedAttribClearer = cachedStrFunc(function(oldAtts) { - return Changeset.mapAttribNumbers(oldAtts, function(n) { - var k = rep.apool.getAttribKey(n); - if (isStyleAttribute(k)) - { - return rep.apool.putAttrib([k, '']); - } - return false; - }); - }); + const incorpedAttribClearer = cachedStrFunc((oldAtts) => Changeset.mapAttribNumbers(oldAtts, (n) => { + const k = rep.apool.getAttribKey(n); + if (isStyleAttribute(k)) { + return rep.apool.putAttrib([k, '']); + } + return false; + })); - var builder1 = startBuilder(); - if (shiftFinalNewlineToBeforeNewText) - { + const builder1 = startBuilder(); + if (shiftFinalNewlineToBeforeNewText) { builder1.keep(1, 1); } - eachAttribRun(oldAttribs, function(start, end, attribs) { + eachAttribRun(oldAttribs, (start, end, attribs) => { builder1.keepText(newText.substring(start, end), incorpedAttribClearer(attribs)); }); - var clearer = builder1.toString(); + const clearer = builder1.toString(); - var builder2 = startBuilder(); - if (shiftFinalNewlineToBeforeNewText) - { + const builder2 = startBuilder(); + if (shiftFinalNewlineToBeforeNewText) { builder2.keep(1, 1); } - eachAttribRun(newAttribs, function(start, end, attribs) { + eachAttribRun(newAttribs, (start, end, attribs) => { builder2.keepText(newText.substring(start, end), attribs); }); - var styler = builder2.toString(); + const styler = builder2.toString(); theChangeset = Changeset.compose(clearer, styler, rep.apool); - } - else - { - var builder = startBuilder(); + } else { + const builder = startBuilder(); - var spliceEndLine = rep.lines.indexOfOffset(spliceEnd); - var spliceEndLineStart = rep.lines.offsetOfIndex(spliceEndLine); - if (spliceEndLineStart > spliceStart) - { + const spliceEndLine = rep.lines.indexOfOffset(spliceEnd); + const spliceEndLineStart = rep.lines.offsetOfIndex(spliceEndLine); + if (spliceEndLineStart > spliceStart) { builder.remove(spliceEndLineStart - spliceStart, spliceEndLine - spliceStartLine); builder.remove(spliceEnd - spliceEndLineStart); - } - else - { + } else { builder.remove(spliceEnd - spliceStart); } - var isNewTextMultiauthor = false; - var authorAtt = Changeset.makeAttribsString('+', (thisAuthor ? [ - ['author', thisAuthor] + let isNewTextMultiauthor = false; + const authorAtt = Changeset.makeAttribsString('+', (thisAuthor ? [ + ['author', thisAuthor], ] : []), rep.apool); - var authorizer = cachedStrFunc(function(oldAtts) { - if (isNewTextMultiauthor) - { + const authorizer = cachedStrFunc((oldAtts) => { + if (isNewTextMultiauthor) { // prefer colors from DOM return Changeset.composeAttributes(authorAtt, oldAtts, true, rep.apool); - } - else - { + } else { // use this author's color return Changeset.composeAttributes(oldAtts, authorAtt, true, rep.apool); } }); - var foundDomAuthor = ''; - eachAttribRun(newAttribs, function(start, end, attribs) { - var a = Changeset.attribsAttributeValue(attribs, 'author', rep.apool); - if (a && a != foundDomAuthor) - { - if (!foundDomAuthor) - { + let foundDomAuthor = ''; + eachAttribRun(newAttribs, (start, end, attribs) => { + const a = Changeset.attribsAttributeValue(attribs, 'author', rep.apool); + if (a && a != foundDomAuthor) { + if (!foundDomAuthor) { foundDomAuthor = a; - } - else - { + } else { isNewTextMultiauthor = true; // multiple authors in DOM! } } }); - if (shiftFinalNewlineToBeforeNewText) - { + if (shiftFinalNewlineToBeforeNewText) { builder.insert('\n', authorizer('')); } - eachAttribRun(newAttribs, function(start, end, attribs) { + eachAttribRun(newAttribs, (start, end, attribs) => { builder.insert(newText.substring(start, end), authorizer(attribs)); }); theChangeset = builder.toString(); } - //dmesg(htmlPrettyEscape(theChangeset)); + // dmesg(htmlPrettyEscape(theChangeset)); doRepApplyChangeset(theChangeset); } @@ -2476,10 +2193,9 @@ function Ace2Inner(){ } function cachedStrFunc(func) { - var cache = {}; - return function(s) { - if (!cache[s]) - { + const cache = {}; + return function (s) { + if (!cache[s]) { cache[s] = func(s); } return cache[s]; @@ -2495,12 +2211,11 @@ function Ace2Inner(){ } function attribRuns(attribs) { - var lengs = []; - var atts = []; - var iter = Changeset.opIterator(attribs); - while (iter.hasNext()) - { - var op = iter.next(); + const lengs = []; + const atts = []; + const iter = Changeset.opIterator(attribs); + while (iter.hasNext()) { + const op = iter.next(); lengs.push(op.chars); atts.push(op.attribs); } @@ -2508,94 +2223,76 @@ function Ace2Inner(){ } function attribIterator(runs, backward) { - var lengs = runs[0]; - var atts = runs[1]; - var i = (backward ? lengs.length - 1 : 0); - var j = 0; + const lengs = runs[0]; + const atts = runs[1]; + let i = (backward ? lengs.length - 1 : 0); + let j = 0; return function next() { - while (j >= lengs[i]) - { + while (j >= lengs[i]) { if (backward) i--; else i++; j = 0; } - var a = atts[i]; + const a = atts[i]; j++; return a; }; } - var oldLen = oldText.length; - var newLen = newText.length; - var minLen = Math.min(oldLen, newLen); + const oldLen = oldText.length; + const newLen = newText.length; + const minLen = Math.min(oldLen, newLen); - var oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter)); - var newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter)); + const oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter)); + const newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter)); - var commonStart = 0; - var oldStartIter = attribIterator(oldARuns, false); - var newStartIter = attribIterator(newARuns, false); - while (commonStart < minLen) - { - if (oldText.charAt(commonStart) == newText.charAt(commonStart) && oldStartIter() == newStartIter()) - { + let commonStart = 0; + const oldStartIter = attribIterator(oldARuns, false); + const newStartIter = attribIterator(newARuns, false); + while (commonStart < minLen) { + if (oldText.charAt(commonStart) == newText.charAt(commonStart) && oldStartIter() == newStartIter()) { commonStart++; - } - else break; + } else { break; } } - var commonEnd = 0; - var oldEndIter = attribIterator(oldARuns, true); - var newEndIter = attribIterator(newARuns, true); - while (commonEnd < minLen) - { - if (commonEnd === 0) - { + let commonEnd = 0; + const oldEndIter = attribIterator(oldARuns, true); + const newEndIter = attribIterator(newARuns, true); + while (commonEnd < minLen) { + if (commonEnd === 0) { // assume newline in common oldEndIter(); newEndIter(); commonEnd++; - } - else if (oldText.charAt(oldLen - 1 - commonEnd) == newText.charAt(newLen - 1 - commonEnd) && oldEndIter() == newEndIter()) - { + } else if (oldText.charAt(oldLen - 1 - commonEnd) == newText.charAt(newLen - 1 - commonEnd) && oldEndIter() == newEndIter()) { commonEnd++; - } - else break; + } else { break; } } - var hintedCommonEnd = -1; - if ((typeof optSelEndHint) == "number") - { + let hintedCommonEnd = -1; + if ((typeof optSelEndHint) === 'number') { hintedCommonEnd = newLen - optSelEndHint; } - if (commonStart + commonEnd > oldLen) - { + if (commonStart + commonEnd > oldLen) { // ambiguous insertion var minCommonEnd = oldLen - commonStart; var maxCommonEnd = commonEnd; - if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) - { + if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) { commonEnd = hintedCommonEnd; - } - else - { + } else { commonEnd = minCommonEnd; } commonStart = oldLen - commonEnd; } - if (commonStart + commonEnd > newLen) - { + if (commonStart + commonEnd > newLen) { // ambiguous deletion var minCommonEnd = newLen - commonStart; var maxCommonEnd = commonEnd; - if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) - { + if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) { commonEnd = hintedCommonEnd; - } - else - { + } else { commonEnd = minCommonEnd; } commonStart = newLen - commonEnd; @@ -2611,8 +2308,7 @@ function Ace2Inner(){ } function performSelectionChange(selectStart, selectEnd, focusAtStart) { - if (repSelectionChange(selectStart, selectEnd, focusAtStart)) - { + if (repSelectionChange(selectStart, selectEnd, focusAtStart)) { currentCallStack.selectionAffected = true; } } @@ -2623,12 +2319,11 @@ function Ace2Inner(){ function repSelectionChange(selectStart, selectEnd, focusAtStart) { - focusAtStart = !! focusAtStart; + focusAtStart = !!focusAtStart; - var newSelFocusAtStart = (focusAtStart && ((!selectStart) || (!selectEnd) || (selectStart[0] != selectEnd[0]) || (selectStart[1] != selectEnd[1]))); + const newSelFocusAtStart = (focusAtStart && ((!selectStart) || (!selectEnd) || (selectStart[0] != selectEnd[0]) || (selectStart[1] != selectEnd[1]))); - if ((!equalLineAndChars(rep.selStart, selectStart)) || (!equalLineAndChars(rep.selEnd, selectEnd)) || (rep.selFocusAtStart != newSelFocusAtStart)) - { + if ((!equalLineAndChars(rep.selStart, selectStart)) || (!equalLineAndChars(rep.selEnd, selectEnd)) || (rep.selFocusAtStart != newSelFocusAtStart)) { rep.selStart = selectStart; rep.selEnd = selectEnd; rep.selFocusAtStart = newSelFocusAtStart; @@ -2638,28 +2333,28 @@ function Ace2Inner(){ selectFormattingButtonIfLineHasStyleApplied(rep); hooks.callAll('aceSelectionChanged', { - rep: rep, + rep, callstack: currentCallStack, - documentAttributeManager: documentAttributeManager, + documentAttributeManager, }); // we scroll when user places the caret at the last line of the pad // when this settings is enabled - var docTextChanged = currentCallStack.docTextChanged; - if(!docTextChanged){ - var isScrollableEvent = !isPadLoading(currentCallStack.type) && isScrollableEditEvent(currentCallStack.type); - var innerHeight = getInnerHeight(); - scroll.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep, isScrollableEvent, innerHeight); + const docTextChanged = currentCallStack.docTextChanged; + if (!docTextChanged) { + const isScrollableEvent = !isPadLoading(currentCallStack.type) && isScrollableEditEvent(currentCallStack.type); + const innerHeight = getInnerHeight(); + scroll.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep, isScrollableEvent, innerHeight); } return true; // Do not uncomment this in production it will break iFrames. - //top.console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd, - //String(!!rep.selFocusAtStart)); + // top.console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd, + // String(!!rep.selFocusAtStart)); } return false; // Do not uncomment this in production it will break iFrames. - //top.console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart); + // top.console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart); } function isPadLoading(eventType) { @@ -2667,7 +2362,7 @@ function Ace2Inner(){ } function updateStyleButtonState(attribName, hasStyleOnRepSelection) { - var $formattingButton = parent.parent.$('[data-key="' + attribName + '"]').find('a'); + const $formattingButton = parent.parent.$(`[data-key="${attribName}"]`).find('a'); $formattingButton.toggleClass(SELECT_BUTTON_CLASS, hasStyleOnRepSelection); } @@ -2675,69 +2370,65 @@ function Ace2Inner(){ return _.contains(FORMATTING_STYLES, attributeName); } - function selectFormattingButtonIfLineHasStyleApplied (rep) { - _.each(FORMATTING_STYLES, function (style) { - var hasStyleOnRepSelection = documentAttributeManager.hasAttributeOnSelectionOrCaretPosition(style); + function selectFormattingButtonIfLineHasStyleApplied(rep) { + _.each(FORMATTING_STYLES, (style) => { + const hasStyleOnRepSelection = documentAttributeManager.hasAttributeOnSelectionOrCaretPosition(style); updateStyleButtonState(style, hasStyleOnRepSelection); - }) + }); } function doCreateDomLine(nonEmpty) { - if (browser.msie && (!nonEmpty)) - { - var result = { + if (browser.msie && (!nonEmpty)) { + const result = { node: null, appendSpan: noop, prepareForAdd: noop, notifyAdded: noop, clearSpans: noop, finishUpdate: noop, - lineMarker: 0 + lineMarker: 0, }; - var lineElem = doc.createElement("div"); + const lineElem = doc.createElement('div'); result.node = lineElem; - result.notifyAdded = function() { + result.notifyAdded = function () { // magic -- settng an empty div's innerHTML to the empty string // keeps it from collapsing. Apparently innerHTML must be set *after* // adding the node to the DOM. // Such a div is what IE 6 creates naturally when you make a blank line // in a document of divs. However, when copy-and-pasted the div will // contain a space, so we note its emptiness with a property. - lineElem.innerHTML = " "; // Frist we set a value that isnt blank + lineElem.innerHTML = ' '; // Frist we set a value that isnt blank // a primitive-valued property survives copy-and-paste - setAssoc(lineElem, "shouldBeEmpty", true); + setAssoc(lineElem, 'shouldBeEmpty', true); // an object property doesn't - setAssoc(lineElem, "unpasted", {}); - lineElem.innerHTML = ""; // Then we make it blank.. New line and no space = Awesome :) + setAssoc(lineElem, 'unpasted', {}); + lineElem.innerHTML = ''; // Then we make it blank.. New line and no space = Awesome :) }; - var lineClass = 'ace-line'; - result.appendSpan = function(txt, cls) { - if ((!txt) && cls) - { + let lineClass = 'ace-line'; + result.appendSpan = function (txt, cls) { + if ((!txt) && cls) { // gain a whole-line style (currently to show insertion point in CSS) lineClass = domline.addToLineClass(lineClass, cls); } // otherwise, ignore appendSpan, this is an empty line }; - result.clearSpans = function() { + result.clearSpans = function () { lineClass = ''; // non-null to cause update }; - var writeClass = function() { + const writeClass = function () { if (lineClass !== null) lineElem.className = lineClass; }; result.prepareForAdd = writeClass; result.finishUpdate = writeClass; - result.getInnerHTML = function() { - return ""; + result.getInnerHTML = function () { + return ''; }; return result; - } - else - { + } else { return domline.createDomLine(nonEmpty, doesWrap, browser, doc); } } @@ -2746,21 +2437,21 @@ function Ace2Inner(){ return str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' '); } - var _blockElems = { - "div": 1, - "p": 1, - "pre": 1, - "li": 1, - "ol": 1, - "ul": 1 + const _blockElems = { + div: 1, + p: 1, + pre: 1, + li: 1, + ol: 1, + ul: 1, }; - _.each(hooks.callAll('aceRegisterBlockElements'), function(element){ - _blockElems[element] = 1; + _.each(hooks.callAll('aceRegisterBlockElements'), (element) => { + _blockElems[element] = 1; }); function isBlockElement(n) { - return !!_blockElems[(n.tagName || "").toLowerCase()]; + return !!_blockElems[(n.tagName || '').toLowerCase()]; } function getDirtyRanges() { @@ -2770,47 +2461,42 @@ function Ace2Inner(){ // indicating inserted content. for example, [0,0] means content was inserted // at the top of the document, while [3,4] means line 3 was deleted, modified, // or replaced with one or more new lines of content. ranges do not touch. - var p = PROFILER("getDirtyRanges", false); + const p = PROFILER('getDirtyRanges', false); p.forIndices = 0; p.consecutives = 0; p.corrections = 0; - var cleanNodeForIndexCache = {}; - var N = rep.lines.length(); // old number of lines + const cleanNodeForIndexCache = {}; + const N = rep.lines.length(); // old number of lines function cleanNodeForIndex(i) { // if line (i) in the un-updated line representation maps to a clean node // in the document, return that node. // if (i) is out of bounds, return true. else return false. - if (cleanNodeForIndexCache[i] === undefined) - { + if (cleanNodeForIndexCache[i] === undefined) { p.forIndices++; - var result; - if (i < 0 || i >= N) - { + let result; + if (i < 0 || i >= N) { result = true; // truthy, but no actual node - } - else - { - var key = rep.lines.atIndex(i).key; + } else { + const key = rep.lines.atIndex(i).key; result = (getCleanNodeByKey(key) || false); } cleanNodeForIndexCache[i] = result; } return cleanNodeForIndexCache[i]; } - var isConsecutiveCache = {}; + const isConsecutiveCache = {}; function isConsecutive(i) { - if (isConsecutiveCache[i] === undefined) - { + if (isConsecutiveCache[i] === undefined) { p.consecutives++; - isConsecutiveCache[i] = (function() { + isConsecutiveCache[i] = (function () { // returns whether line (i) and line (i-1), assumed to be map to clean DOM nodes, // or document boundaries, are consecutive in the changed DOM - var a = cleanNodeForIndex(i - 1); - var b = cleanNodeForIndex(i); + const a = cleanNodeForIndex(i - 1); + const b = cleanNodeForIndex(i); if ((!a) || (!b)) return false; // violates precondition if ((a === true) && (b === true)) return !root.firstChild; if ((a === true) && b.previousSibling) return false; @@ -2830,14 +2516,14 @@ function Ace2Inner(){ // list of pairs, each representing a range of lines that is clean and consecutive // in the changed DOM. lines (-1) and (N) are always clean, but may or may not // be consecutive with lines in the document. pairs are in sorted order. - var cleanRanges = [ - [-1, N + 1] + const cleanRanges = [ + [-1, N + 1], ]; function rangeForLine(i) { // returns index of cleanRange containing i, or -1 if none - var answer = -1; - _.each(cleanRanges ,function(r, idx) { + let answer = -1; + _.each(cleanRanges, (r, idx) => { if (i >= r[1]) return false; // keep looking if (i < r[0]) return true; // not found, stop looking answer = idx; @@ -2849,8 +2535,8 @@ function Ace2Inner(){ function removeLineFromRange(rng, line) { // rng is index into cleanRanges, line is line number // precond: line is in rng - var a = cleanRanges[rng][0]; - var b = cleanRanges[rng][1]; + const a = cleanRanges[rng][0]; + const b = cleanRanges[rng][1]; if ((a + 1) == b) cleanRanges.splice(rng, 1); else if (line == a) cleanRanges[rng][0]++; else if (line == (b - 1)) cleanRanges[rng][1]--; @@ -2859,11 +2545,11 @@ function Ace2Inner(){ function splitRange(rng, pt) { // precond: pt splits cleanRanges[rng] into two non-empty ranges - var a = cleanRanges[rng][0]; - var b = cleanRanges[rng][1]; + const a = cleanRanges[rng][0]; + const b = cleanRanges[rng][1]; cleanRanges.splice(rng, 1, [a, pt], [pt, b]); } - var correctedLines = {}; + const correctedLines = {}; function correctlyAssignLine(line) { if (correctedLines[line]) return true; @@ -2873,39 +2559,32 @@ function Ace2Inner(){ // returns whether line was already correctly assigned (i.e. correctly // clean or dirty, according to cleanRanges, and if clean, correctly // attached or not attached (i.e. in the same range as) the prev and next lines). - var rng = rangeForLine(line); - var lineClean = isClean(line); - if (rng < 0) - { - if (lineClean) - { + const rng = rangeForLine(line); + const lineClean = isClean(line); + if (rng < 0) { + if (lineClean) { // somehow lost clean line } return true; } - if (!lineClean) - { + if (!lineClean) { // a clean-range includes this dirty line, fix it removeLineFromRange(rng, line); return false; - } - else - { + } else { // line is clean, but could be wrongly connected to a clean line // above or below - var a = cleanRanges[rng][0]; - var b = cleanRanges[rng][1]; - var didSomething = false; + const a = cleanRanges[rng][0]; + const b = cleanRanges[rng][1]; + let didSomething = false; // we'll leave non-clean adjacent nodes in the clean range for the caller to // detect and deal with. we deal with whether the range should be split // just above or just below this line. - if (a < line && isClean(line - 1) && !isConsecutive(line)) - { + if (a < line && isClean(line - 1) && !isConsecutive(line)) { splitRange(rng, line); didSomething = true; } - if (b > (line + 1) && isClean(line + 1) && !isConsecutive(line + 1)) - { + if (b > (line + 1) && isClean(line + 1) && !isConsecutive(line + 1)) { splitRange(rng, line + 1); didSomething = true; } @@ -2919,63 +2598,50 @@ function Ace2Inner(){ // making for several consecutive lines. note that iteration is over old lines, // so this operation takes time proportional to the number of old lines // that are changed or missing, not the number of new lines inserted. - var correctInARow = 0; - var currentIndex = line; - while (correctInARow < reqInARow && currentIndex >= 0) - { - if (correctlyAssignLine(currentIndex)) - { + let correctInARow = 0; + let currentIndex = line; + while (correctInARow < reqInARow && currentIndex >= 0) { + if (correctlyAssignLine(currentIndex)) { correctInARow++; - } - else correctInARow = 0; + } else { correctInARow = 0; } currentIndex--; } correctInARow = 0; currentIndex = line; - while (correctInARow < reqInARow && currentIndex < N) - { - if (correctlyAssignLine(currentIndex)) - { + while (correctInARow < reqInARow && currentIndex < N) { + if (correctlyAssignLine(currentIndex)) { correctInARow++; - } - else correctInARow = 0; + } else { correctInARow = 0; } currentIndex++; } } - if (N === 0) - { + if (N === 0) { p.cancel(); - if (!isConsecutive(0)) - { + if (!isConsecutive(0)) { splitRange(0, 0); } - } - else - { - p.mark("topbot"); + } else { + p.mark('topbot'); detectChangesAroundLine(0, 1); detectChangesAroundLine(N - 1, 1); - p.mark("obs"); - for (var k in observedChanges.cleanNodesNearChanges) - { - var key = k.substring(1); - if (rep.lines.containsKey(key)) - { - var line = rep.lines.indexOfKey(key); + p.mark('obs'); + for (const k in observedChanges.cleanNodesNearChanges) { + const key = k.substring(1); + if (rep.lines.containsKey(key)) { + const line = rep.lines.indexOfKey(key); detectChangesAroundLine(line, 2); } } - p.mark("stats&calc"); - p.literal(p.forIndices, "byidx"); - p.literal(p.consecutives, "cons"); - p.literal(p.corrections, "corr"); + p.mark('stats&calc'); + p.literal(p.forIndices, 'byidx'); + p.literal(p.consecutives, 'cons'); + p.literal(p.corrections, 'corr'); } - var dirtyRanges = []; - for (var r = 0; r < cleanRanges.length - 1; r++) - { + const dirtyRanges = []; + for (let r = 0; r < cleanRanges.length - 1; r++) { dirtyRanges.push([cleanRanges[r][1], cleanRanges[r + 1][0]]); } @@ -2986,27 +2652,25 @@ function Ace2Inner(){ function markNodeClean(n) { // clean nodes have knownHTML that matches their innerHTML - var dirtiness = {}; + const dirtiness = {}; dirtiness.nodeId = uniqueId(n); dirtiness.knownHTML = n.innerHTML; - if (browser.msie) - { + if (browser.msie) { // adding a space to an "empty" div in IE designMode doesn't // change the innerHTML of the div's parent; also, other // browsers don't support innerText dirtiness.knownText = n.innerText; } - setAssoc(n, "dirtiness", dirtiness); + setAssoc(n, 'dirtiness', dirtiness); } function isNodeDirty(n) { - var p = PROFILER("cleanCheck", false); + const p = PROFILER('cleanCheck', false); if (n.parentNode != root) return true; - var data = getAssoc(n, "dirtiness"); + const data = getAssoc(n, 'dirtiness'); if (!data) return true; if (n.id !== data.nodeId) return true; - if (browser.msie) - { + if (browser.msie) { if (n.innerText !== data.knownText) return true; } if (n.innerHTML !== data.knownHTML) return true; @@ -3015,67 +2679,61 @@ function Ace2Inner(){ } function getViewPortTopBottom() { - var theTop = scroll.getScrollY(); - var doc = outerWin.document; - var height = doc.documentElement.clientHeight; // includes padding + const theTop = scroll.getScrollY(); + const doc = outerWin.document; + const height = doc.documentElement.clientHeight; // includes padding // we have to get the exactly height of the viewport. So it has to subtract all the values which changes // the viewport height (E.g. padding, position top) - var viewportExtraSpacesAndPosition = getEditorPositionTop() + getPaddingTopAddedWhenPageViewIsEnable(); + const viewportExtraSpacesAndPosition = getEditorPositionTop() + getPaddingTopAddedWhenPageViewIsEnable(); return { top: theTop, - bottom: (theTop + height - viewportExtraSpacesAndPosition) + bottom: (theTop + height - viewportExtraSpacesAndPosition), }; } function getEditorPositionTop() { - var editor = parent.document.getElementsByTagName('iframe'); - var editorPositionTop = editor[0].offsetTop; + const editor = parent.document.getElementsByTagName('iframe'); + const editorPositionTop = editor[0].offsetTop; return editorPositionTop; } // ep_page_view adds padding-top, which makes the viewport smaller function getPaddingTopAddedWhenPageViewIsEnable() { - var rootDocument = parent.parent.document; - var aceOuter = rootDocument.getElementsByName("ace_outer"); - var aceOuterPaddingTop = parseInt($(aceOuter).css("padding-top")); + const rootDocument = parent.parent.document; + const aceOuter = rootDocument.getElementsByName('ace_outer'); + const aceOuterPaddingTop = parseInt($(aceOuter).css('padding-top')); return aceOuterPaddingTop; } function handleCut(evt) { - inCallStackIfNecessary("handleCut", function() { + inCallStackIfNecessary('handleCut', () => { doDeleteKey(evt); }); return true; } function handleClick(evt) { - inCallStackIfNecessary("handleClick", function() { + inCallStackIfNecessary('handleClick', () => { idleWorkTimer.atMost(200); }); function isLink(n) { - return (n.tagName || '').toLowerCase() == "a" && n.href; + return (n.tagName || '').toLowerCase() == 'a' && n.href; } // only want to catch left-click - if ((!evt.ctrlKey) && (evt.button != 2) && (evt.button != 3)) - { + if ((!evt.ctrlKey) && (evt.button != 2) && (evt.button != 3)) { // find A tag with HREF - var n = evt.target; - while (n && n.parentNode && !isLink(n)) - { + let n = evt.target; + while (n && n.parentNode && !isLink(n)) { n = n.parentNode; } - if (n && isLink(n)) - { - try - { + if (n && isLink(n)) { + try { window.open(n.href, '_blank', 'noopener,noreferrer'); - } - catch (e) - { + } catch (e) { // absorb "user canceled" error in IE for certain prompts } evt.preventDefault(); @@ -3086,49 +2744,39 @@ function Ace2Inner(){ } function hideEditBarDropdowns() { - if(window.parent.parent.padeditbar){ // required in case its in an iframe should probably use parent.. See Issue 327 https://github.com/ether/etherpad-lite/issues/327 - window.parent.parent.padeditbar.toggleDropDown("none"); + if (window.parent.parent.padeditbar) { // required in case its in an iframe should probably use parent.. See Issue 327 https://github.com/ether/etherpad-lite/issues/327 + window.parent.parent.padeditbar.toggleDropDown('none'); } } function doReturnKey() { - if (!(rep.selStart && rep.selEnd)) - { + if (!(rep.selStart && rep.selEnd)) { return; } - var lineNum = rep.selStart[0]; - var listType = getLineListType(lineNum); + const lineNum = rep.selStart[0]; + let listType = getLineListType(lineNum); - if (listType) - { - var text = rep.lines.atIndex(lineNum).text; + if (listType) { + const text = rep.lines.atIndex(lineNum).text; listType = /([a-z]+)([0-9]+)/.exec(listType); - var type = listType[1]; - var level = Number(listType[2]); + const type = listType[1]; + const level = Number(listType[2]); - //detect empty list item; exclude indentation - if(text === '*' && type !== "indent") - { - //if not already on the highest level - if(level > 1) - { - setLineListType(lineNum, type+(level-1));//automatically decrease the level + // detect empty list item; exclude indentation + if (text === '*' && type !== 'indent') { + // if not already on the highest level + if (level > 1) { + setLineListType(lineNum, type + (level - 1));// automatically decrease the level + } else { + setLineListType(lineNum, '');// remove the list + renumberList(lineNum + 1);// trigger renumbering of list that may be right after } - else - { - setLineListType(lineNum, '');//remove the list - renumberList(lineNum + 1);//trigger renumbering of list that may be right after - } - } - else if (lineNum + 1 <= rep.lines.length()) - { + } else if (lineNum + 1 <= rep.lines.length()) { performDocumentReplaceSelection('\n'); - setLineListType(lineNum + 1, type+level); + setLineListType(lineNum + 1, type + level); } - } - else - { + } else { performDocumentReplaceSelection('\n'); handleReturnIndentation(); } @@ -3136,39 +2784,34 @@ function Ace2Inner(){ function doIndentOutdent(isOut) { if (!((rep.selStart && rep.selEnd) || - ((rep.selStart[0] == rep.selEnd[0]) && (rep.selStart[1] == rep.selEnd[1]) && rep.selEnd[1] > 1)) && + ((rep.selStart[0] == rep.selEnd[0]) && (rep.selStart[1] == rep.selEnd[1]) && rep.selEnd[1] > 1)) && (isOut != true) - ) - { + ) { return false; } - var firstLine, lastLine; + let firstLine, lastLine; firstLine = rep.selStart[0]; lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0)); - var mods = []; - for (var n = firstLine; n <= lastLine; n++) - { - var listType = getLineListType(n); - var t = 'indent'; - var level = 0; - if (listType) - { + const mods = []; + for (let n = firstLine; n <= lastLine; n++) { + let listType = getLineListType(n); + let t = 'indent'; + let level = 0; + if (listType) { listType = /([a-z]+)([0-9]+)/.exec(listType); - if (listType) - { + if (listType) { t = listType[1]; level = Number(listType[2]); } } - var newLevel = Math.max(0, Math.min(MAX_LIST_LEVEL, level + (isOut ? -1 : 1))); - if (level != newLevel) - { + const newLevel = Math.max(0, Math.min(MAX_LIST_LEVEL, level + (isOut ? -1 : 1))); + if (level != newLevel) { mods.push([n, (newLevel > 0) ? t + newLevel : '']); } } - _.each(mods, function(mod){ + _.each(mods, (mod) => { setLineListType(mod[0], mod[1]); }); return true; @@ -3176,119 +2819,96 @@ function Ace2Inner(){ editorInfo.ace_doIndentOutdent = doIndentOutdent; function doTabKey(shiftDown) { - if (!doIndentOutdent(shiftDown)) - { + if (!doIndentOutdent(shiftDown)) { performDocumentReplaceSelection(THE_TAB); } } function doDeleteKey(optEvt) { - var evt = optEvt || {}; - var handled = false; - if (rep.selStart) - { - if (isCaret()) - { - var lineNum = caretLine(); - var col = caretColumn(); + const evt = optEvt || {}; + let handled = false; + if (rep.selStart) { + if (isCaret()) { + const lineNum = caretLine(); + const col = caretColumn(); var lineEntry = rep.lines.atIndex(lineNum); - var lineText = lineEntry.text; - var lineMarker = lineEntry.lineMarker; - if (/^ +$/.exec(lineText.substring(lineMarker, col))) - { - var col2 = col - lineMarker; - var tabSize = THE_TAB.length; - var toDelete = ((col2 - 1) % tabSize) + 1; + const lineText = lineEntry.text; + const lineMarker = lineEntry.lineMarker; + if (/^ +$/.exec(lineText.substring(lineMarker, col))) { + const col2 = col - lineMarker; + const tabSize = THE_TAB.length; + const toDelete = ((col2 - 1) % tabSize) + 1; performDocumentReplaceRange([lineNum, col - toDelete], [lineNum, col], ''); - //scrollSelectionIntoView(); + // scrollSelectionIntoView(); handled = true; } } - if (!handled) - { - if (isCaret()) - { - var theLine = caretLine(); + if (!handled) { + if (isCaret()) { + const theLine = caretLine(); var lineEntry = rep.lines.atIndex(theLine); - if (caretColumn() <= lineEntry.lineMarker) - { + if (caretColumn() <= lineEntry.lineMarker) { // delete at beginning of line - var action = 'delete_newline'; - var prevLineListType = (theLine > 0 ? getLineListType(theLine - 1) : ''); - var thisLineListType = getLineListType(theLine); - var prevLineEntry = (theLine > 0 && rep.lines.atIndex(theLine - 1)); - var prevLineBlank = (prevLineEntry && prevLineEntry.text.length == prevLineEntry.lineMarker); + const action = 'delete_newline'; + const prevLineListType = (theLine > 0 ? getLineListType(theLine - 1) : ''); + const thisLineListType = getLineListType(theLine); + const prevLineEntry = (theLine > 0 && rep.lines.atIndex(theLine - 1)); + const prevLineBlank = (prevLineEntry && prevLineEntry.text.length == prevLineEntry.lineMarker); - var thisLineHasMarker = documentAttributeManager.lineHasMarker(theLine); + const thisLineHasMarker = documentAttributeManager.lineHasMarker(theLine); - if (thisLineListType) - { + if (thisLineListType) { // this line is a list - if (prevLineBlank && !prevLineListType) - { + if (prevLineBlank && !prevLineListType) { // previous line is blank, remove it performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, 0], ''); - } - else - { + } else { // delistify performDocumentReplaceRange([theLine, 0], [theLine, lineEntry.lineMarker], ''); } - }else if (thisLineHasMarker && prevLineEntry){ + } else if (thisLineHasMarker && prevLineEntry) { // If the line has any attributes assigned, remove them by removing the marker '*' - performDocumentReplaceRange([theLine -1 , prevLineEntry.text.length], [theLine, lineEntry.lineMarker], ''); - } - else if (theLine > 0) - { + performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, lineEntry.lineMarker], ''); + } else if (theLine > 0) { // remove newline performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, 0], ''); } - } - else - { - var docChar = caretDocChar(); - if (docChar > 0) - { - if (evt.metaKey || evt.ctrlKey || evt.altKey) - { + } else { + const docChar = caretDocChar(); + if (docChar > 0) { + if (evt.metaKey || evt.ctrlKey || evt.altKey) { // delete as many unicode "letters or digits" in a row as possible; // always delete one char, delete further even if that first char // isn't actually a word char. - var deleteBackTo = docChar - 1; - while (deleteBackTo > lineEntry.lineMarker && isWordChar(rep.alltext.charAt(deleteBackTo - 1))) - { + let deleteBackTo = docChar - 1; + while (deleteBackTo > lineEntry.lineMarker && isWordChar(rep.alltext.charAt(deleteBackTo - 1))) { deleteBackTo--; } performDocumentReplaceCharRange(deleteBackTo, docChar, ''); - } - else - { + } else { // normal delete performDocumentReplaceCharRange(docChar - 1, docChar, ''); } } } - } - else - { + } else { performDocumentReplaceSelection(''); } } } - //if the list has been removed, it is necessary to renumber - //starting from the *next* line because the list may have been - //separated. If it returns null, it means that the list was not cut, try - //from the current one. - var line = caretLine(); - if(line != -1 && renumberList(line+1) === null) - { + // if the list has been removed, it is necessary to renumber + // starting from the *next* line because the list may have been + // separated. If it returns null, it means that the list was not cut, try + // from the current one. + const line = caretLine(); + if (line != -1 && renumberList(line + 1) === null) { renumberList(line); } } // set of "letter or digit" chars is based on section 20.5.16 of the original Java Language Spec - var REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; - var REGEX_SPACE = /\s/; + const REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; + const REGEX_SPACE = /\s/; function isWordChar(c) { return !!REGEX_WORDCHAR.exec(c); @@ -3300,7 +2920,7 @@ function Ace2Inner(){ } function moveByWordInLine(lineText, initialIndex, forwardNotBack) { - var i = initialIndex; + let i = initialIndex; function nextChar() { if (forwardNotBack) return lineText.charAt(i); @@ -3320,25 +2940,18 @@ function Ace2Inner(){ // On Mac and Linux, move right moves to end of word and move left moves to start; // on Windows, always move to start of word. // On Windows, Firefox and IE disagree on whether to stop for punctuation (FF says no). - if (browser.msie && forwardNotBack) - { - while ((!isDone()) && isWordChar(nextChar())) - { + if (browser.msie && forwardNotBack) { + while ((!isDone()) && isWordChar(nextChar())) { advance(); } - while ((!isDone()) && !isWordChar(nextChar())) - { + while ((!isDone()) && !isWordChar(nextChar())) { advance(); } - } - else - { - while ((!isDone()) && !isWordChar(nextChar())) - { + } else { + while ((!isDone()) && !isWordChar(nextChar())) { advance(); } - while ((!isDone()) && isWordChar(nextChar())) - { + while ((!isDone()) && isWordChar(nextChar())) { advance(); } } @@ -3348,68 +2961,62 @@ function Ace2Inner(){ function handleKeyEvent(evt) { if (!isEditable) return; - var type = evt.type; - var charCode = evt.charCode; - var keyCode = evt.keyCode; - var which = evt.which; - var altKey = evt.altKey; - var shiftKey = evt.shiftKey; + const type = evt.type; + const charCode = evt.charCode; + const keyCode = evt.keyCode; + const which = evt.which; + const altKey = evt.altKey; + const shiftKey = evt.shiftKey; // Is caret potentially hidden by the chat button? - var myselection = document.getSelection(); // get the current caret selection - var caretOffsetTop = myselection.focusNode.parentNode.offsetTop | myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214 + const myselection = document.getSelection(); // get the current caret selection + const caretOffsetTop = myselection.focusNode.parentNode.offsetTop | myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214 - if(myselection.focusNode.wholeText){ // Is there any content? If not lineHeight will report wrong.. + if (myselection.focusNode.wholeText) { // Is there any content? If not lineHeight will report wrong.. var lineHeight = myselection.focusNode.parentNode.offsetHeight; // line height of populated links - }else{ + } else { var lineHeight = myselection.focusNode.offsetHeight; // line height of blank lines } - //dmesg("keyevent type: "+type+", which: "+which); + // dmesg("keyevent type: "+type+", which: "+which); // Don't take action based on modifier keys going up and down. // Modifier keys do not generate "keypress" events. // 224 is the command-key under Mac Firefox. // 91 is the Windows key in IE; it is ASCII for open-bracket but isn't the keycode for that key // 20 is capslock in IE. - var isModKey = ((!charCode) && ((type == "keyup") || (type == "keydown")) && (keyCode == 16 || keyCode == 17 || keyCode == 18 || keyCode == 20 || keyCode == 224 || keyCode == 91)); + const isModKey = ((!charCode) && ((type == 'keyup') || (type == 'keydown')) && (keyCode == 16 || keyCode == 17 || keyCode == 18 || keyCode == 20 || keyCode == 224 || keyCode == 91)); if (isModKey) return; // If the key is a keypress and the browser is opera and the key is enter, do nothign at all as this fires twice. - if (keyCode == 13 && browser.opera && (type == "keypress")){ + if (keyCode == 13 && browser.opera && (type == 'keypress')) { return; // This stops double enters in Opera but double Tabs still show on single tab keypress, adding keyCode == 9 to this doesn't help as the event is fired twice } - var specialHandled = false; - var isTypeForSpecialKey = ((browser.msie || browser.safari || browser.chrome || browser.firefox) ? (type == "keydown") : (type == "keypress")); - var isTypeForCmdKey = ((browser.msie || browser.safari || browser.chrome || browser.firefox) ? (type == "keydown") : (type == "keypress")); - var stopped = false; + let specialHandled = false; + const isTypeForSpecialKey = ((browser.msie || browser.safari || browser.chrome || browser.firefox) ? (type == 'keydown') : (type == 'keypress')); + const isTypeForCmdKey = ((browser.msie || browser.safari || browser.chrome || browser.firefox) ? (type == 'keydown') : (type == 'keypress')); + let stopped = false; - inCallStackIfNecessary("handleKeyEvent", function() { - if (type == "keypress" || (isTypeForSpecialKey && keyCode == 13 /*return*/ )) - { + inCallStackIfNecessary('handleKeyEvent', function () { + if (type == 'keypress' || (isTypeForSpecialKey && keyCode == 13 /* return*/)) { // in IE, special keys don't send keypress, the keydown does the action - if (!outsideKeyPress(evt)) - { + if (!outsideKeyPress(evt)) { evt.preventDefault(); stopped = true; } - } - else if (evt.key === "Dead"){ + } else if (evt.key === 'Dead') { // If it's a dead key we don't want to do any Etherpad behavior. stopped = true; return true; - } - else if (type == "keydown") - { + } else if (type == 'keydown') { outsideKeyDown(evt); } - if (!stopped) - { - var specialHandledInHook = hooks.callAll('aceKeyEvent', { + if (!stopped) { + const specialHandledInHook = hooks.callAll('aceKeyEvent', { callstack: currentCallStack, - editorInfo: editorInfo, - rep: rep, - documentAttributeManager: documentAttributeManager, - evt:evt + editorInfo, + rep, + documentAttributeManager, + evt, }); // if any hook returned true, set specialHandled with true @@ -3417,89 +3024,86 @@ function Ace2Inner(){ specialHandled = _.contains(specialHandledInHook, true); } - var padShortcutEnabled = parent.parent.clientVars.padShortcutEnabled; - if ((!specialHandled) && altKey && isTypeForSpecialKey && keyCode == 120 && padShortcutEnabled.altF9){ + const padShortcutEnabled = parent.parent.clientVars.padShortcutEnabled; + if ((!specialHandled) && altKey && isTypeForSpecialKey && keyCode == 120 && padShortcutEnabled.altF9) { // Alt F9 focuses on the File Menu and/or editbar. // Note that while most editors use Alt F10 this is not desirable // As ubuntu cannot use Alt F10.... // Focus on the editbar. -- TODO: Move Focus back to previous state (we know it so we can use it) - var firstEditbarElement = parent.parent.$('#editbar').children("ul").first().children().first().children().first().children().first(); + const firstEditbarElement = parent.parent.$('#editbar').children('ul').first().children().first().children().first().children().first(); $(this).blur(); firstEditbarElement.focus(); evt.preventDefault(); } - if ((!specialHandled) && altKey && keyCode == 67 && type === "keydown" && padShortcutEnabled.altC){ + if ((!specialHandled) && altKey && keyCode == 67 && type === 'keydown' && padShortcutEnabled.altC) { // Alt c focuses on the Chat window $(this).blur(); parent.parent.chat.show(); - parent.parent.$("#chatinput").focus(); + parent.parent.$('#chatinput').focus(); evt.preventDefault(); } - if ((!specialHandled) && evt.ctrlKey && shiftKey && keyCode == 50 && type === "keydown" && padShortcutEnabled.cmdShift2){ + if ((!specialHandled) && evt.ctrlKey && shiftKey && keyCode == 50 && type === 'keydown' && padShortcutEnabled.cmdShift2) { // Control-Shift-2 shows a gritter popup showing a line author - var lineNumber = rep.selEnd[0]; - var alineAttrs = rep.alines[lineNumber]; - var apool = rep.apool; + const lineNumber = rep.selEnd[0]; + const alineAttrs = rep.alines[lineNumber]; + const apool = rep.apool; // TODO: support selection ranges // TODO: Still work when authorship colors have been cleared // TODO: i18n // TODO: There appears to be a race condition or so. - var author = null; + let author = null; if (alineAttrs) { var authors = []; var authorNames = []; - var opIter = Changeset.opIterator(alineAttrs); + const opIter = Changeset.opIterator(alineAttrs); - while (opIter.hasNext()){ - var op = opIter.next(); + while (opIter.hasNext()) { + const op = opIter.next(); authorId = Changeset.opAttributeValue(op, 'author', apool); // Only push unique authors and ones with values - if(authors.indexOf(authorId) === -1 && authorId !== ""){ + if (authors.indexOf(authorId) === -1 && authorId !== '') { authors.push(authorId); } - } - } // No author information is available IE on a new pad. - if(authors.length === 0){ - var authorString = "No author information is available"; - } - else{ + if (authors.length === 0) { + var authorString = 'No author information is available'; + } else { // Known authors info, both current and historical - var padAuthors = parent.parent.pad.userList(); - var authorObj = {}; - authors.forEach(function(authorId){ - padAuthors.forEach(function(padAuthor){ + const padAuthors = parent.parent.pad.userList(); + let authorObj = {}; + authors.forEach((authorId) => { + padAuthors.forEach((padAuthor) => { // If the person doing the lookup is the author.. - if(padAuthor.userId === authorId){ - if(parent.parent.clientVars.userId === authorId){ + if (padAuthor.userId === authorId) { + if (parent.parent.clientVars.userId === authorId) { authorObj = { - name: "Me" - } - }else{ + name: 'Me', + }; + } else { authorObj = padAuthor; } } }); - if(!authorObj){ - author = "Unknown"; + if (!authorObj) { + author = 'Unknown'; return; } author = authorObj.name; - if(!author) author = "Unknown"; + if (!author) author = 'Unknown'; authorNames.push(author); - }) + }); } - if(authors.length === 1){ - var authorString = "The author of this line is " + authorNames; + if (authors.length === 1) { + var authorString = `The author of this line is ${authorNames}`; } - if(authors.length > 1){ - var authorString = "The authors of this line are " + authorNames.join(" & "); + if (authors.length > 1) { + var authorString = `The authors of this line are ${authorNames.join(' & ')}`; } parent.parent.$.gritter.add({ @@ -3510,11 +3114,10 @@ function Ace2Inner(){ // (bool | optional) if you want it to fade out on its own or just sit there sticky: false, // (int | optional) the time you want it to be alive for before fading out - time: '4000' + time: '4000', }); } - if ((!specialHandled) && isTypeForSpecialKey && keyCode == 8 && padShortcutEnabled.delete) - { + if ((!specialHandled) && isTypeForSpecialKey && keyCode == 8 && padShortcutEnabled.delete) { // "delete" key; in mozilla, if we're at the beginning of a line, normalize now, // or else deleting a blank line can take two delete presses. // -- @@ -3527,21 +3130,19 @@ function Ace2Inner(){ doDeleteKey(evt); specialHandled = true; } - if ((!specialHandled) && isTypeForSpecialKey && keyCode == 13 && padShortcutEnabled.return) - { + if ((!specialHandled) && isTypeForSpecialKey && keyCode == 13 && padShortcutEnabled.return) { // return key, handle specially; // note that in mozilla we need to do an incorporation for proper return behavior anyway. fastIncorp(4); evt.preventDefault(); doReturnKey(); - //scrollSelectionIntoView(); - scheduler.setTimeout(function() { + // scrollSelectionIntoView(); + scheduler.setTimeout(() => { outerWin.scrollBy(-100, 0); }, 0); specialHandled = true; } - if ((!specialHandled) && isTypeForSpecialKey && keyCode == 27 && padShortcutEnabled.esc) - { + if ((!specialHandled) && isTypeForSpecialKey && keyCode == 27 && padShortcutEnabled.esc) { // prevent esc key; // in mozilla versions 14-19 avoid reconnecting pad. @@ -3552,229 +3153,202 @@ function Ace2Inner(){ // close all gritters when the user hits escape key parent.parent.$.gritter.removeAll(); } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "s" && (evt.metaKey || evt.ctrlKey) && !evt.altKey && padShortcutEnabled.cmdS) /* Do a saved revision on ctrl S */ + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 's' && (evt.metaKey || evt.ctrlKey) && !evt.altKey && padShortcutEnabled.cmdS) /* Do a saved revision on ctrl S */ { evt.preventDefault(); - var originalBackground = parent.parent.$('#revisionlink').css("background") - parent.parent.$('#revisionlink').css({"background":"lightyellow"}); - scheduler.setTimeout(function(){ - parent.parent.$('#revisionlink').css({"background":originalBackground}); + const originalBackground = parent.parent.$('#revisionlink').css('background'); + parent.parent.$('#revisionlink').css({background: 'lightyellow'}); + scheduler.setTimeout(() => { + parent.parent.$('#revisionlink').css({background: originalBackground}); }, 1000); - parent.parent.pad.collabClient.sendMessage({"type":"SAVE_REVISION"}); /* The parent.parent part of this is BAD and I feel bad.. It may break something */ + parent.parent.pad.collabClient.sendMessage({type: 'SAVE_REVISION'}); /* The parent.parent part of this is BAD and I feel bad.. It may break something */ specialHandled = true; } - if ((!specialHandled) && isTypeForSpecialKey && keyCode == 9 && !(evt.metaKey || evt.ctrlKey) && padShortcutEnabled.tab) - { + if ((!specialHandled) && isTypeForSpecialKey && keyCode == 9 && !(evt.metaKey || evt.ctrlKey) && padShortcutEnabled.tab) { // tab fastIncorp(5); evt.preventDefault(); doTabKey(evt.shiftKey); - //scrollSelectionIntoView(); + // scrollSelectionIntoView(); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "z" && (evt.metaKey || evt.ctrlKey) && !evt.altKey && padShortcutEnabled.cmdZ) - { + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'z' && (evt.metaKey || evt.ctrlKey) && !evt.altKey && padShortcutEnabled.cmdZ) { // cmd-Z (undo) fastIncorp(6); evt.preventDefault(); - if (evt.shiftKey) - { - doUndoRedo("redo"); - } - else - { - doUndoRedo("undo"); + if (evt.shiftKey) { + doUndoRedo('redo'); + } else { + doUndoRedo('undo'); } specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "y" && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdY) - { + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'y' && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdY) { // cmd-Y (redo) fastIncorp(10); evt.preventDefault(); - doUndoRedo("redo"); + doUndoRedo('redo'); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "b" && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdB) - { + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'b' && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdB) { // cmd-B (bold) fastIncorp(13); evt.preventDefault(); toggleAttributeOnSelection('bold'); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "i" && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdI) - { + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'i' && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdI) { // cmd-I (italic) fastIncorp(14); evt.preventDefault(); toggleAttributeOnSelection('italic'); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "u" && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdU) - { + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'u' && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdU) { // cmd-U (underline) fastIncorp(15); evt.preventDefault(); toggleAttributeOnSelection('underline'); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "5" && (evt.metaKey || evt.ctrlKey) && evt.altKey !== true && padShortcutEnabled.cmd5) - { + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == '5' && (evt.metaKey || evt.ctrlKey) && evt.altKey !== true && padShortcutEnabled.cmd5) { // cmd-5 (strikethrough) fastIncorp(13); evt.preventDefault(); toggleAttributeOnSelection('strikethrough'); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "l" && (evt.metaKey || evt.ctrlKey) && evt.shiftKey && padShortcutEnabled.cmdShiftL) - { + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'l' && (evt.metaKey || evt.ctrlKey) && evt.shiftKey && padShortcutEnabled.cmdShiftL) { // cmd-shift-L (unorderedlist) fastIncorp(9); evt.preventDefault(); - doInsertUnorderedList() + doInsertUnorderedList(); specialHandled = true; - } - if ((!specialHandled) && isTypeForCmdKey && ((String.fromCharCode(which).toLowerCase() == "n" && padShortcutEnabled.cmdShiftN) || (String.fromCharCode(which) == 1 && padShortcutEnabled.cmdShift1)) && (evt.metaKey || evt.ctrlKey) && evt.shiftKey) - { + } + if ((!specialHandled) && isTypeForCmdKey && ((String.fromCharCode(which).toLowerCase() == 'n' && padShortcutEnabled.cmdShiftN) || (String.fromCharCode(which) == 1 && padShortcutEnabled.cmdShift1)) && (evt.metaKey || evt.ctrlKey) && evt.shiftKey) { // cmd-shift-N and cmd-shift-1 (orderedlist) fastIncorp(9); evt.preventDefault(); - doInsertOrderedList() + doInsertOrderedList(); specialHandled = true; - } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "c" && (evt.metaKey || evt.ctrlKey) && evt.shiftKey && padShortcutEnabled.cmdShiftC) { + } + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'c' && (evt.metaKey || evt.ctrlKey) && evt.shiftKey && padShortcutEnabled.cmdShiftC) { // cmd-shift-C (clearauthorship) fastIncorp(9); evt.preventDefault(); CMDS.clearauthorship(); } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "h" && (evt.ctrlKey) && padShortcutEnabled.cmdH) - { + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'h' && (evt.ctrlKey) && padShortcutEnabled.cmdH) { // cmd-H (backspace) fastIncorp(20); evt.preventDefault(); doDeleteKey(); specialHandled = true; } - if((evt.which == 36 && evt.ctrlKey == true) && padShortcutEnabled.ctrlHome){ scroll.setScrollY(0); } // Control Home send to Y = 0 - if((evt.which == 33 || evt.which == 34) && type == 'keydown' && !evt.ctrlKey){ - + if ((evt.which == 36 && evt.ctrlKey == true) && padShortcutEnabled.ctrlHome) { scroll.setScrollY(0); } // Control Home send to Y = 0 + if ((evt.which == 33 || evt.which == 34) && type == 'keydown' && !evt.ctrlKey) { evt.preventDefault(); // This is required, browsers will try to do normal default behavior on page up / down and the default behavior SUCKS - var oldVisibleLineRange = scroll.getVisibleLineRange(rep); - var topOffset = rep.selStart[0] - oldVisibleLineRange[0]; - if(topOffset < 0 ){ + const oldVisibleLineRange = scroll.getVisibleLineRange(rep); + let topOffset = rep.selStart[0] - oldVisibleLineRange[0]; + if (topOffset < 0) { topOffset = 0; } - var isPageDown = evt.which === 34; - var isPageUp = evt.which === 33; + const isPageDown = evt.which === 34; + const isPageUp = evt.which === 33; - scheduler.setTimeout(function(){ - var newVisibleLineRange = scroll.getVisibleLineRange(rep); // the visible lines IE 1,10 - var linesCount = rep.lines.length(); // total count of lines in pad IE 10 - var numberOfLinesInViewport = newVisibleLineRange[1] - newVisibleLineRange[0]; // How many lines are in the viewport right now? + scheduler.setTimeout(() => { + const newVisibleLineRange = scroll.getVisibleLineRange(rep); // the visible lines IE 1,10 + const linesCount = rep.lines.length(); // total count of lines in pad IE 10 + const numberOfLinesInViewport = newVisibleLineRange[1] - newVisibleLineRange[0]; // How many lines are in the viewport right now? - if(isPageUp && padShortcutEnabled.pageUp){ + if (isPageUp && padShortcutEnabled.pageUp) { rep.selEnd[0] = rep.selEnd[0] - numberOfLinesInViewport; // move to the bottom line +1 in the viewport (essentially skipping over a page) rep.selStart[0] = rep.selStart[0] - numberOfLinesInViewport; // move to the bottom line +1 in the viewport (essentially skipping over a page) } - if(isPageDown && padShortcutEnabled.pageDown){ // if we hit page down - if(rep.selEnd[0] >= oldVisibleLineRange[0]){ // If the new viewpoint position is actually further than where we are right now - rep.selStart[0] = oldVisibleLineRange[1] -1; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content - rep.selEnd[0] = oldVisibleLineRange[1] -1; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content + if (isPageDown && padShortcutEnabled.pageDown) { // if we hit page down + if (rep.selEnd[0] >= oldVisibleLineRange[0]) { // If the new viewpoint position is actually further than where we are right now + rep.selStart[0] = oldVisibleLineRange[1] - 1; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content + rep.selEnd[0] = oldVisibleLineRange[1] - 1; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content } } - //ensure min and max - if(rep.selEnd[0] < 0){ + // ensure min and max + if (rep.selEnd[0] < 0) { rep.selEnd[0] = 0; } - if(rep.selStart[0] < 0){ + if (rep.selStart[0] < 0) { rep.selStart[0] = 0; } - if(rep.selEnd[0] >= linesCount){ - rep.selEnd[0] = linesCount-1; + if (rep.selEnd[0] >= linesCount) { + rep.selEnd[0] = linesCount - 1; } updateBrowserSelectionFromRep(); - var myselection = document.getSelection(); // get the current caret selection, can't use rep. here because that only gives us the start position not the current - var caretOffsetTop = myselection.focusNode.parentNode.offsetTop || myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214 + const myselection = document.getSelection(); // get the current caret selection, can't use rep. here because that only gives us the start position not the current + let caretOffsetTop = myselection.focusNode.parentNode.offsetTop || myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214 // sometimes the first selection is -1 which causes problems (Especially with ep_page_view) // so use focusNode.offsetTop value. - if(caretOffsetTop === -1) caretOffsetTop = myselection.focusNode.offsetTop; + if (caretOffsetTop === -1) caretOffsetTop = myselection.focusNode.offsetTop; scroll.setScrollY(caretOffsetTop); // set the scrollY offset of the viewport on the document - }, 200); } // scroll to viewport when user presses arrow keys and caret is out of the viewport - if((evt.which == 37 || evt.which == 38 || evt.which == 39 || evt.which == 40)){ + if ((evt.which == 37 || evt.which == 38 || evt.which == 39 || evt.which == 40)) { // we use arrowKeyWasReleased to avoid triggering the animation when a key is continuously pressed // this makes the scroll smooth - if(!continuouslyPressingArrowKey(type)){ + if (!continuouslyPressingArrowKey(type)) { // We use getSelection() instead of rep to get the caret position. This avoids errors like when // the caret position is not synchronized with the rep. For example, when an user presses arrow // down to scroll the pad without releasing the key. When the key is released the rep is not // synchronized, so we don't get the right node where caret is. - var selection = getSelection(); + const selection = getSelection(); - if(selection){ - var arrowUp = evt.which === 38; - var innerHeight = getInnerHeight(); + if (selection) { + const arrowUp = evt.which === 38; + const innerHeight = getInnerHeight(); scroll.scrollWhenPressArrowKeys(arrowUp, rep, innerHeight); } } } } - if (type == "keydown") - { + if (type == 'keydown') { idleWorkTimer.atLeast(500); - } - else if (type == "keypress") - { - if ((!specialHandled) && false /*parenModule.shouldNormalizeOnChar(charCode)*/) - { + } else if (type == 'keypress') { + if ((!specialHandled) && false /* parenModule.shouldNormalizeOnChar(charCode)*/) { idleWorkTimer.atMost(0); - } - else - { + } else { idleWorkTimer.atLeast(500); } - } - else if (type == "keyup") - { - var wait = 0; + } else if (type == 'keyup') { + const wait = 0; idleWorkTimer.atLeast(wait); idleWorkTimer.atMost(wait); } // Is part of multi-keystroke international character on Firefox Mac - var isFirefoxHalfCharacter = (browser.firefox && evt.altKey && charCode === 0 && keyCode === 0); + const isFirefoxHalfCharacter = (browser.firefox && evt.altKey && charCode === 0 && keyCode === 0); // Is part of multi-keystroke international character on Safari Mac - var isSafariHalfCharacter = (browser.safari && evt.altKey && keyCode == 229); + const isSafariHalfCharacter = (browser.safari && evt.altKey && keyCode == 229); - if (thisKeyDoesntTriggerNormalize || isFirefoxHalfCharacter || isSafariHalfCharacter) - { + if (thisKeyDoesntTriggerNormalize || isFirefoxHalfCharacter || isSafariHalfCharacter) { idleWorkTimer.atLeast(3000); // give user time to type // if this is a keydown, e.g., the keyup shouldn't trigger a normalize thisKeyDoesntTriggerNormalize = true; } - if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!inInternationalComposition)) - { - if (type != "keyup") - { + if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!inInternationalComposition)) { + if (type != 'keyup') { observeChangesAroundSelection(); } } - if (type == "keyup") - { + if (type == 'keyup') { thisKeyDoesntTriggerNormalize = false; } }); @@ -3782,12 +3356,11 @@ function Ace2Inner(){ var thisKeyDoesntTriggerNormalize = false; - var arrowKeyWasReleased = true; + let arrowKeyWasReleased = true; function continuouslyPressingArrowKey(type) { - var firstTimeKeyIsContinuouslyPressed = false; + let firstTimeKeyIsContinuouslyPressed = false; - if (type == 'keyup') arrowKeyWasReleased = true; - else if (type == 'keydown' && arrowKeyWasReleased) { + if (type == 'keyup') { arrowKeyWasReleased = true; } else if (type == 'keydown' && arrowKeyWasReleased) { firstTimeKeyIsContinuouslyPressed = true; arrowKeyWasReleased = false; } @@ -3797,25 +3370,21 @@ function Ace2Inner(){ function doUndoRedo(which) { // precond: normalized DOM - if (undoModule.enabled) - { - var whichMethod; - if (which == "undo") whichMethod = 'performUndo'; - if (which == "redo") whichMethod = 'performRedo'; - if (whichMethod) - { - var oldEventType = currentCallStack.editEvent.eventType; + if (undoModule.enabled) { + let whichMethod; + if (which == 'undo') whichMethod = 'performUndo'; + if (which == 'redo') whichMethod = 'performRedo'; + if (whichMethod) { + const oldEventType = currentCallStack.editEvent.eventType; currentCallStack.startNewEvent(which); - undoModule[whichMethod](function(backset, selectionInfo) { - if (backset) - { + undoModule[whichMethod]((backset, selectionInfo) => { + if (backset) { performDocumentApplyChangeset(backset); } - if (selectionInfo) - { + if (selectionInfo) { performSelectionChange(lineAndColumnFromChar(selectionInfo.selStart), lineAndColumnFromChar(selectionInfo.selEnd), selectionInfo.selFocusAtStart); } - var oldEvent = currentCallStack.startNewEvent(oldEventType, true); + const oldEvent = currentCallStack.startNewEvent(oldEventType, true); return oldEvent; }); } @@ -3825,24 +3394,23 @@ function Ace2Inner(){ function updateBrowserSelectionFromRep() { // requires normalized DOM! - var selStart = rep.selStart, - selEnd = rep.selEnd; + const selStart = rep.selStart; + const selEnd = rep.selEnd; - if (!(selStart && selEnd)) - { + if (!(selStart && selEnd)) { setSelection(null); return; } - var selection = {}; + const selection = {}; - var ss = [selStart[0], selStart[1]]; + const ss = [selStart[0], selStart[1]]; selection.startPoint = getPointForLineAndChar(ss); - var se = [selEnd[0], selEnd[1]]; + const se = [selEnd[0], selEnd[1]]; selection.endPoint = getPointForLineAndChar(se); - selection.focusAtStart = !! rep.selFocusAtStart; + selection.focusAtStart = !!rep.selFocusAtStart; setSelection(selection); } editorInfo.ace_updateBrowserSelectionFromRep = updateBrowserSelectionFromRep; @@ -3853,21 +3421,15 @@ function Ace2Inner(){ } function hasIESelection() { - var browserSelection; - try - { + let browserSelection; + try { browserSelection = doc.selection; - } - catch (e) - {} + } catch (e) {} if (!browserSelection) return false; - var origSelectionRange; - try - { + let origSelectionRange; + try { origSelectionRange = browserSelection.createRange(); - } - catch (e) - {} + } catch (e) {} if (!origSelectionRange) return false; return true; } @@ -3877,167 +3439,138 @@ function Ace2Inner(){ // each of which has node (a magicdom node), index, and maxIndex. If the node // is a text node, maxIndex is the length of the text; else maxIndex is 1. // index is between 0 and maxIndex, inclusive. - if (browser.msie) - { + if (browser.msie) { var browserSelection; - try - { + try { browserSelection = doc.selection; - } - catch (e) - {} + } catch (e) {} if (!browserSelection) return null; - var origSelectionRange; - try - { + let origSelectionRange; + try { origSelectionRange = browserSelection.createRange(); - } - catch (e) - {} + } catch (e) {} if (!origSelectionRange) return null; - var selectionParent = origSelectionRange.parentElement(); + const selectionParent = origSelectionRange.parentElement(); if (selectionParent.ownerDocument != doc) return null; - var newRange = function() { + const newRange = function () { return doc.body.createTextRange(); }; - var rangeForElementNode = function(nd) { - var rng = newRange(); + const rangeForElementNode = function (nd) { + const rng = newRange(); // doesn't work on text nodes rng.moveToElementText(nd); return rng; }; - var pointFromCollapsedRange = function(rng) { - var parNode = rng.parentElement(); - var elemBelow = -1; - var elemAbove = parNode.childNodes.length; - var rangeWithin = rangeForElementNode(parNode); + const pointFromCollapsedRange = function (rng) { + const parNode = rng.parentElement(); + let elemBelow = -1; + let elemAbove = parNode.childNodes.length; + const rangeWithin = rangeForElementNode(parNode); - if (rng.compareEndPoints("StartToStart", rangeWithin) === 0) - { + if (rng.compareEndPoints('StartToStart', rangeWithin) === 0) { return { node: parNode, index: 0, - maxIndex: 1 + maxIndex: 1, }; - } - else if (rng.compareEndPoints("EndToEnd", rangeWithin) === 0) - { - if (isBlockElement(parNode) && parNode.nextSibling) - { + } else if (rng.compareEndPoints('EndToEnd', rangeWithin) === 0) { + if (isBlockElement(parNode) && parNode.nextSibling) { // caret after block is not consistent across browsers // (same line vs next) so put caret before next node return { node: parNode.nextSibling, index: 0, - maxIndex: 1 + maxIndex: 1, }; } return { node: parNode, index: 1, - maxIndex: 1 + maxIndex: 1, }; - } - else if (parNode.childNodes.length === 0) - { + } else if (parNode.childNodes.length === 0) { return { node: parNode, index: 0, - maxIndex: 1 + maxIndex: 1, }; } - for (var i = 0; i < parNode.childNodes.length; i++) - { - var n = parNode.childNodes.item(i); - if (!isNodeText(n)) - { - var nodeRange = rangeForElementNode(n); - var startComp = rng.compareEndPoints("StartToStart", nodeRange); - var endComp = rng.compareEndPoints("EndToEnd", nodeRange); - if (startComp >= 0 && endComp <= 0) - { - var index = 0; - if (startComp > 0) - { + for (let i = 0; i < parNode.childNodes.length; i++) { + const n = parNode.childNodes.item(i); + if (!isNodeText(n)) { + const nodeRange = rangeForElementNode(n); + const startComp = rng.compareEndPoints('StartToStart', nodeRange); + const endComp = rng.compareEndPoints('EndToEnd', nodeRange); + if (startComp >= 0 && endComp <= 0) { + let index = 0; + if (startComp > 0) { index = 1; } return { node: n, - index: index, - maxIndex: 1 + index, + maxIndex: 1, }; - } - else if (endComp > 0) - { - if (i > elemBelow) - { + } else if (endComp > 0) { + if (i > elemBelow) { elemBelow = i; - rangeWithin.setEndPoint("StartToEnd", nodeRange); + rangeWithin.setEndPoint('StartToEnd', nodeRange); } - } - else if (startComp < 0) - { - if (i < elemAbove) - { + } else if (startComp < 0) { + if (i < elemAbove) { elemAbove = i; - rangeWithin.setEndPoint("EndToStart", nodeRange); + rangeWithin.setEndPoint('EndToStart', nodeRange); } } } } - if ((elemAbove - elemBelow) == 1) - { - if (elemBelow >= 0) - { + if ((elemAbove - elemBelow) == 1) { + if (elemBelow >= 0) { return { node: parNode.childNodes.item(elemBelow), index: 1, - maxIndex: 1 + maxIndex: 1, }; - } - else - { + } else { return { node: parNode.childNodes.item(elemAbove), index: 0, - maxIndex: 1 + maxIndex: 1, }; } } - var idx = 0; - var r = rng.duplicate(); + let idx = 0; + const r = rng.duplicate(); // infinite stateful binary search! call function for values 0 to inf, // expecting the answer to be about 40. return index of smallest // true value. - var indexIntoRange = binarySearchInfinite(40, function(i) { + const indexIntoRange = binarySearchInfinite(40, (i) => { // the search algorithm whips the caret back and forth, // though it has to be moved relatively and may hit // the end of the buffer - var delta = i - idx; - var moved = Math.abs(r.move("character", -delta)); + const delta = i - idx; + const moved = Math.abs(r.move('character', -delta)); // next line is work-around for fact that when moving left, the beginning // of a text node is considered to be after the start of the parent element: - if (r.move("character", -1)) r.move("character", 1); + if (r.move('character', -1)) r.move('character', 1); if (delta < 0) idx -= moved; else idx += moved; - return (r.compareEndPoints("StartToStart", rangeWithin) <= 0); + return (r.compareEndPoints('StartToStart', rangeWithin) <= 0); }); // iterate over consecutive text nodes, point is in one of them - var textNode = elemBelow + 1; - var indexLeft = indexIntoRange; - while (textNode < elemAbove) - { + let textNode = elemBelow + 1; + let indexLeft = indexIntoRange; + while (textNode < elemAbove) { var tn = parNode.childNodes.item(textNode); - if (indexLeft <= tn.nodeValue.length) - { + if (indexLeft <= tn.nodeValue.length) { return { node: tn, index: indexLeft, - maxIndex: tn.nodeValue.length + maxIndex: tn.nodeValue.length, }; } indexLeft -= tn.nodeValue.length; @@ -4047,98 +3580,84 @@ function Ace2Inner(){ return { node: tn, index: tn.nodeValue.length, - maxIndex: tn.nodeValue.length + maxIndex: tn.nodeValue.length, }; }; var selection = {}; - if (origSelectionRange.compareEndPoints("StartToEnd", origSelectionRange) === 0) - { + if (origSelectionRange.compareEndPoints('StartToEnd', origSelectionRange) === 0) { // collapsed - var pnt = pointFromCollapsedRange(origSelectionRange); + const pnt = pointFromCollapsedRange(origSelectionRange); selection.startPoint = pnt; selection.endPoint = { node: pnt.node, index: pnt.index, - maxIndex: pnt.maxIndex + maxIndex: pnt.maxIndex, }; - } - else - { - var start = origSelectionRange.duplicate(); + } else { + const start = origSelectionRange.duplicate(); start.collapse(true); - var end = origSelectionRange.duplicate(); + const end = origSelectionRange.duplicate(); end.collapse(false); selection.startPoint = pointFromCollapsedRange(start); selection.endPoint = pointFromCollapsedRange(end); } return selection; - } - else - { + } else { // non-IE browser var browserSelection = window.getSelection(); - if (browserSelection && browserSelection.type != "None" && browserSelection.rangeCount !== 0) - { - var range = browserSelection.getRangeAt(0); + if (browserSelection && browserSelection.type != 'None' && browserSelection.rangeCount !== 0) { + const range = browserSelection.getRangeAt(0); function isInBody(n) { - while (n && !(n.tagName && n.tagName.toLowerCase() == "body")) - { + while (n && !(n.tagName && n.tagName.toLowerCase() == 'body')) { n = n.parentNode; } return !!n; } function pointFromRangeBound(container, offset) { - if (!isInBody(container)) - { + if (!isInBody(container)) { // command-click in Firefox selects whole document, HEAD and BODY! return { node: root, index: 0, - maxIndex: 1 + maxIndex: 1, }; } - var n = container; - var childCount = n.childNodes.length; - if (isNodeText(n)) - { + const n = container; + const childCount = n.childNodes.length; + if (isNodeText(n)) { return { node: n, index: offset, - maxIndex: n.nodeValue.length + maxIndex: n.nodeValue.length, }; - } - else if (childCount === 0) - { + } else if (childCount === 0) { return { node: n, index: 0, - maxIndex: 1 + maxIndex: 1, }; } // treat point between two nodes as BEFORE the second (rather than after the first) // if possible; this way point at end of a line block-element is treated as // at beginning of next line - else if (offset == childCount) - { + else if (offset == childCount) { var nd = n.childNodes.item(childCount - 1); var max = nodeMaxIndex(nd); return { node: nd, index: max, - maxIndex: max + maxIndex: max, }; - } - else - { + } else { var nd = n.childNodes.item(offset); var max = nodeMaxIndex(nd); return { node: nd, index: 0, - maxIndex: max + maxIndex: max, }; } } @@ -4147,13 +3666,12 @@ function Ace2Inner(){ selection.endPoint = pointFromRangeBound(range.endContainer, range.endOffset); selection.focusAtStart = (((range.startContainer != range.endContainer) || (range.startOffset != range.endOffset)) && browserSelection.anchorNode && (browserSelection.anchorNode == range.endContainer) && (browserSelection.anchorOffset == range.endOffset)); - if(selection.startPoint.node.ownerDocument !== window.document){ + if (selection.startPoint.node.ownerDocument !== window.document) { return null; } return selection; - } - else return null; + } else { return null; } } } @@ -4162,18 +3680,16 @@ function Ace2Inner(){ return { node: pt.node, index: pt.index, - maxIndex: pt.maxIndex + maxIndex: pt.maxIndex, }; } - if (browser.msie) - { + if (browser.msie) { // Oddly enough, accessing scrollHeight fixes return key handling on IE 8, // presumably by forcing some kind of internal DOM update. doc.body.scrollHeight; function moveToElementText(s, n) { - while (n.firstChild && !isNodeText(n.firstChild)) - { + while (n.firstChild && !isNodeText(n.firstChild)) { n = n.firstChild; } s.moveToElementText(n); @@ -4185,25 +3701,18 @@ function Ace2Inner(){ function setCollapsedBefore(s, n) { // s is an IE TextRange, n is a dom node - if (isNodeText(n)) - { + if (isNodeText(n)) { // previous node should not also be text, but prevent inf recurs - if (n.previousSibling && !isNodeText(n.previousSibling)) - { + if (n.previousSibling && !isNodeText(n.previousSibling)) { setCollapsedAfter(s, n.previousSibling); - } - else - { + } else { setCollapsedBefore(s, n.parentNode); } - } - else - { + } else { moveToElementText(s, n); // work around for issue that caret at beginning of line // somehow ends up at end of previous line - if (s.move('character', 1)) - { + if (s.move('character', 1)) { s.move('character', -1); } s.collapse(true); // to start @@ -4212,59 +3721,48 @@ function Ace2Inner(){ function setCollapsedAfter(s, n) { // s is an IE TextRange, n is a magicdom node - if (isNodeText(n)) - { + if (isNodeText(n)) { // can't use end of container when no nextSibling (could be on next line), // so use previousSibling or start of container and move forward. setCollapsedBefore(s, n); - s.move("character", n.nodeValue.length); - } - else - { + s.move('character', n.nodeValue.length); + } else { moveToElementText(s, n); s.collapse(false); // to end } } function getPointRange(point) { - var s = newRange(); - var n = point.node; - if (isNodeText(n)) - { + const s = newRange(); + const n = point.node; + if (isNodeText(n)) { setCollapsedBefore(s, n); - s.move("character", point.index); - } - else if (point.index === 0) - { + s.move('character', point.index); + } else if (point.index === 0) { setCollapsedBefore(s, n); - } - else - { + } else { setCollapsedAfter(s, n); } return s; } - if (selection) - { - if (!hasIESelection()) - { + if (selection) { + if (!hasIESelection()) { return; // don't steal focus } - var startPoint = copyPoint(selection.startPoint); - var endPoint = copyPoint(selection.endPoint); + const startPoint = copyPoint(selection.startPoint); + const endPoint = copyPoint(selection.endPoint); // fix issue where selection can't be extended past end of line // with shift-rightarrow or shift-downarrow - if (endPoint.index == endPoint.maxIndex && endPoint.node.nextSibling) - { + if (endPoint.index == endPoint.maxIndex && endPoint.node.nextSibling) { endPoint.node = endPoint.node.nextSibling; endPoint.index = 0; endPoint.maxIndex = nodeMaxIndex(endPoint.node); } var range = getPointRange(startPoint); - range.setEndPoint("EndToEnd", getPointRange(endPoint)); + range.setEndPoint('EndToEnd', getPointRange(endPoint)); // setting the selection in IE causes everything to scroll // so that the selection is visible. if setting the selection @@ -4272,78 +3770,58 @@ function Ace2Inner(){ function isEqualToDocumentSelection(rng) { - var browserSelection; - try - { + let browserSelection; + try { browserSelection = doc.selection; - } - catch (e) - {} + } catch (e) {} if (!browserSelection) return false; - var rng2 = browserSelection.createRange(); + const rng2 = browserSelection.createRange(); if (rng2.parentElement().ownerDocument != doc) return false; - if (rng.compareEndPoints("StartToStart", rng2) !== 0) return false; - if (rng.compareEndPoints("EndToEnd", rng2) !== 0) return false; + if (rng.compareEndPoints('StartToStart', rng2) !== 0) return false; + if (rng.compareEndPoints('EndToEnd', rng2) !== 0) return false; return true; } - if (!isEqualToDocumentSelection(range)) - { - //dmesg(toSource(selection)); - //dmesg(escapeHTML(doc.body.innerHTML)); + if (!isEqualToDocumentSelection(range)) { + // dmesg(toSource(selection)); + // dmesg(escapeHTML(doc.body.innerHTML)); range.select(); } - } - else - { - try - { + } else { + try { doc.selection.empty(); - } - catch (e) - {} + } catch (e) {} } - } - else - { + } else { // non-IE browser - var isCollapsed; + let isCollapsed; function pointToRangeBound(pt) { - var p = copyPoint(pt); + const p = copyPoint(pt); // Make sure Firefox cursor is deep enough; fixes cursor jumping when at top level, // and also problem where cut/copy of a whole line selected with fake arrow-keys // copies the next line too. - if (isCollapsed) - { + if (isCollapsed) { function diveDeep() { - while (p.node.childNodes.length > 0) - { - //&& (p.node == root || p.node.parentNode == root)) { - if (p.index === 0) - { + while (p.node.childNodes.length > 0) { + // && (p.node == root || p.node.parentNode == root)) { + if (p.index === 0) { p.node = p.node.firstChild; p.maxIndex = nodeMaxIndex(p.node); - } - else if (p.index == p.maxIndex) - { + } else if (p.index == p.maxIndex) { p.node = p.node.lastChild; p.maxIndex = nodeMaxIndex(p.node); p.index = p.maxIndex; - } - else break; + } else { break; } } } // now fix problem where cursor at end of text node at end of span-like element // with background doesn't seem to show up... - if (isNodeText(p.node) && p.index == p.maxIndex) - { - var n = p.node; - while ((!n.nextSibling) && (n != root) && (n.parentNode != root)) - { + if (isNodeText(p.node) && p.index == p.maxIndex) { + let n = p.node; + while ((!n.nextSibling) && (n != root) && (n.parentNode != root)) { n = n.parentNode; } - if (n.nextSibling && (!((typeof n.nextSibling.tagName) == "string" && n.nextSibling.tagName.toLowerCase() == "br")) && (n != p.node) && (n != root) && (n.parentNode != root)) - { + if (n.nextSibling && (!((typeof n.nextSibling.tagName) === 'string' && n.nextSibling.tagName.toLowerCase() == 'br')) && (n != p.node) && (n != root) && (n.parentNode != root)) { // found a parent, go to next node and dive in p.node = n.nextSibling; p.maxIndex = nodeMaxIndex(p.node); @@ -4353,46 +3831,37 @@ function Ace2Inner(){ } // try to make sure insertion point is styled; // also fixes other FF problems - if (!isNodeText(p.node)) - { + if (!isNodeText(p.node)) { diveDeep(); } } - if (isNodeText(p.node)) - { + if (isNodeText(p.node)) { return { container: p.node, - offset: p.index + offset: p.index, }; - } - else - { + } else { // p.index in {0,1} return { container: p.node.parentNode, - offset: childIndex(p.node) + p.index + offset: childIndex(p.node) + p.index, }; } } - var browserSelection = window.getSelection(); - if (browserSelection) - { + const browserSelection = window.getSelection(); + if (browserSelection) { browserSelection.removeAllRanges(); - if (selection) - { + if (selection) { isCollapsed = (selection.startPoint.node === selection.endPoint.node && selection.startPoint.index === selection.endPoint.index); - var start = pointToRangeBound(selection.startPoint); - var end = pointToRangeBound(selection.endPoint); + const start = pointToRangeBound(selection.startPoint); + const end = pointToRangeBound(selection.endPoint); - if ((!isCollapsed) && selection.focusAtStart && browserSelection.collapse && browserSelection.extend) - { + if ((!isCollapsed) && selection.focusAtStart && browserSelection.collapse && browserSelection.extend) { // can handle "backwards"-oriented selection, shift-arrow-keys move start // of selection browserSelection.collapse(end.container, end.offset); browserSelection.extend(start.container, start.offset); - } - else - { + } else { var range = doc.createRange(); range.setStart(start.container, start.offset); range.setEnd(end.container, end.offset); @@ -4405,9 +3874,8 @@ function Ace2Inner(){ } function childIndex(n) { - var idx = 0; - while (n.previousSibling) - { + let idx = 0; + while (n.previousSibling) { idx++; n = n.previousSibling; } @@ -4416,27 +3884,26 @@ function Ace2Inner(){ function fixView() { // calling this method repeatedly should be fast - if (getInnerWidth() === 0 || getInnerHeight() === 0) - { + if (getInnerWidth() === 0 || getInnerHeight() === 0) { return; } - var win = outerWin; + const win = outerWin; enforceEditability(); $(sideDiv).addClass('sidedivdelayed'); } - var _teardownActions = []; + const _teardownActions = []; function teardown() { - _.each(_teardownActions, function(a) { + _.each(_teardownActions, (a) => { a(); }); } - var iePastedLines = null; + const iePastedLines = null; function handleIEPaste(evt) { // Pasting in IE loses blank lines in a way that loses information; @@ -4444,19 +3911,18 @@ function Ace2Inner(){ // which becomes "one\ntwo\nthree". We can get the correct text // from the clipboard directly, but we still have to let the paste // happen to get the style information. - var clipText = window.clipboardData && window.clipboardData.getData("Text"); - if (clipText && doc.selection) - { + const clipText = window.clipboardData && window.clipboardData.getData('Text'); + if (clipText && doc.selection) { // this "paste" event seems to mess with the selection whether we try to // stop it or not, so can't really do document-level manipulation now // or in an idle call-stack. instead, use IE native manipulation - //function escapeLine(txt) { - //return processSpaces(escapeHTML(textify(txt))); - //} - //var newHTML = map(clipText.replace(/\r/g,'').split('\n'), escapeLine).join('
                '); - //doc.selection.createRange().pasteHTML(newHTML); - //evt.preventDefault(); - //iePastedLines = map(clipText.replace(/\r/g,'').split('\n'), textify); + // function escapeLine(txt) { + // return processSpaces(escapeHTML(textify(txt))); + // } + // var newHTML = map(clipText.replace(/\r/g,'').split('\n'), escapeLine).join('
                '); + // doc.selection.createRange().pasteHTML(newHTML); + // evt.preventDefault(); + // iePastedLines = map(clipText.replace(/\r/g,'').split('\n'), textify); } } @@ -4464,42 +3930,38 @@ function Ace2Inner(){ var inInternationalComposition = false; function handleCompositionEvent(evt) { // international input events, fired in FF3, at least; allow e.g. Japanese input - if (evt.type == "compositionstart") - { + if (evt.type == 'compositionstart') { inInternationalComposition = true; - } - else if (evt.type == "compositionend") - { + } else if (evt.type == 'compositionend') { inInternationalComposition = false; } } editorInfo.ace_getInInternationalComposition = function () { return inInternationalComposition; - } + }; function bindTheEventHandlers() { - $(document).on("keydown", handleKeyEvent); - $(document).on("keypress", handleKeyEvent); - $(document).on("keyup", handleKeyEvent); - $(document).on("click", handleClick); + $(document).on('keydown', handleKeyEvent); + $(document).on('keypress', handleKeyEvent); + $(document).on('keyup', handleKeyEvent); + $(document).on('click', handleClick); // dropdowns on edit bar need to be closed on clicks on both pad inner and pad outer - $(outerWin.document).on("click", hideEditBarDropdowns); + $(outerWin.document).on('click', hideEditBarDropdowns); // Disabled: https://github.com/ether/etherpad-lite/issues/2546 // Will break OL re-numbering: https://github.com/ether/etherpad-lite/pull/2533 // $(document).on("cut", handleCut); - $(root).on("blur", handleBlur); - if (browser.msie) - { - $(document).on("click", handleIEOuterClick); + $(root).on('blur', handleBlur); + if (browser.msie) { + $(document).on('click', handleIEOuterClick); } - if (browser.msie) $(root).on("paste", handleIEPaste); + if (browser.msie) $(root).on('paste', handleIEPaste); // If non-nullish, pasting on a link should be suppressed. let suppressPasteOnLink = null; - $(root).on('auxclick', function(e) { + $(root).on('auxclick', (e) => { if (e.originalEvent.button === 1 && (e.target.a || e.target.localName === 'a')) { // The user middle-clicked on a link. Usually users do this to open a link in a new tab, but // in X11 (Linux) this will instead paste the contents of the primary selection at the mouse @@ -4521,8 +3983,8 @@ function Ace2Inner(){ } }); - $(root).on("paste", function(e){ - if (suppressPasteOnLink != null && (e.target.a || e.target.localName === "a")) { + $(root).on('paste', (e) => { + if (suppressPasteOnLink != null && (e.target.a || e.target.localName === 'a')) { scheduler.clearTimeout(suppressPasteOnLink); suppressPasteOnLink = null; e.preventDefault(); @@ -4531,18 +3993,18 @@ function Ace2Inner(){ // Call paste hook hooks.callAll('acePaste', { - editorInfo: editorInfo, - rep: rep, - documentAttributeManager: documentAttributeManager, - e: e + editorInfo, + rep, + documentAttributeManager, + e, }); - }) + }); // We reference document here, this is because if we don't this will expose a bug // in Google Chrome. This bug will cause the last character on the last line to // not fire an event when dropped into.. - $(document).on("drop", function(e){ - if(e.target.a || e.target.localName === "a"){ + $(document).on('drop', (e) => { + if (e.target.a || e.target.localName === 'a') { e.preventDefault(); } @@ -4550,72 +4012,66 @@ function Ace2Inner(){ // need to merge the changes into a single changeset. So mark origin with empty
                ', + expectedHTML: 'empty

                ', + expectedText: 'empty\n\n', + }, }; describe(__filename, function () { diff --git a/tests/backend/specs/contentcollector.js b/tests/backend/specs/contentcollector.js index 156568ab2..3608faed6 100644 --- a/tests/backend/specs/contentcollector.js +++ b/tests/backend/specs/contentcollector.js @@ -107,7 +107,12 @@ const tests = { noteToSelf: "

                should create a line break but not break numbering -- This is what I can't get working!", disabled: true, }, - + ignoreAnyTagsOutsideBody: { + description: 'Content outside body should be ignored', + html: 'titleempty
                ', + expectedLineAttribs: ['+5'], + expectedText: ['empty'], + }, }; describe(__filename, function () { @@ -122,7 +127,7 @@ describe(__filename, function () { it(testObj.description, function (done) { const $ = cheerio.load(testObj.html); // Load HTML into Cheerio - const doc = $('html')[0]; // Creates a dom-like representation of HTML + const doc = $('body')[0]; // Creates a dom-like representation of HTML // Create an empty attribute pool const apool = new AttributePool(); // Convert a dom tree into a list of lines and attribute liens From b82bf5c72658c9f10692e7f985c66d93a214e423 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 18 Dec 2020 18:13:02 -0500 Subject: [PATCH 288/315] Drop support for Internet Explorer --- CHANGELOG.md | 2 + src/static/js/ace2_inner.js | 736 ++++++------------------------ src/static/js/contentcollector.js | 8 +- src/static/js/domline.js | 2 +- src/static/js/linestylefilter.js | 7 - src/static/js/pad.js | 7 - src/static/js/pad_userlist.js | 6 +- 7 files changed, 149 insertions(+), 619 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3497e754a..71681f8eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ Existing group pads that were previously password protected will no longer be password protected. If you need fine-grained access control, you can restrict API session creation in your frontend service, or you can use plugins. +* All workarounds for Microsoft Internet Explorer have been removed. IE might + still work, but it is untested. * Plugin hook functions are now subject to new sanity checks. Buggy hook functions will cause an error message to be logged * Authorization failures now return 403 by default instead of 401 diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 30eed957f..fc339ab78 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -24,15 +24,6 @@ const padutils = require('./pad_utils').padutils; let _, $, jQuery, plugins, Ace2Common; const browser = require('./browser'); -if (browser.msie) { - // Honestly fuck IE royally. - // Basically every hack we have since V11 causes a problem - if (parseInt(browser.version) >= 11) { - delete browser.msie; - browser.chrome = true; - browser.modernIE = true; - } -} Ace2Common = require('./ace2_common'); @@ -1180,12 +1171,6 @@ function Ace2Inner() { const dirtyNodes = []; for (let n = firstDirtyNode; n && !(n.previousSibling && n.previousSibling == lastDirtyNode); n = n.nextSibling) { - if (browser.msie) { - // try to undo IE's pesky and overzealous linkification - try { - n.createTextRange().execCommand('unlink', false, null); - } catch (e) {} - } cc.collectContent(n); dirtyNodes.push(n); } @@ -1213,13 +1198,12 @@ function Ace2Inner() { var scrollToTheLeftNeeded = false; if (linesWrapped > 0) { - if (!browser.msie) { - // chrome decides in it's infinite wisdom that its okay to put the browsers visisble window in the middle of the span - // an outcome of this is that the first chars of the string are no longer visible to the user.. Yay chrome.. - // Move the browsers visible area to the left hand side of the span - // Firefox isn't quite so bad, but it's still pretty quirky. - var scrollToTheLeftNeeded = true; - } + // Chrome decides in its infinite wisdom that it's okay to put the browser's visisble + // window in the middle of the span. An outcome of this is that the first chars of the + // string are no longer visible to the user.. Yay chrome.. Move the browser's visible area + // to the left hand side of the span. Firefox isn't quite so bad, but it's still pretty + // quirky. + var scrollToTheLeftNeeded = true; } if (ss[0] >= 0) selStart = [ss[0] + a + netNumLinesChangeSoFar, ss[1]]; @@ -1472,15 +1456,6 @@ function Ace2Inner() { let after = false; if (charsLeft === 0) { let index = 0; - - if (browser.msie && parseInt(browser.version) >= 11) { - browser.msie = false; // Temp fix to resolve enter and backspace issues.. - // Note that this makes MSIE behave like modern browsers.. - } - if (browser.msie && line == (rep.lines.length() - 1) && lineNode.childNodes.length === 0) { - // best to stay at end of last empty div in IE - index = 1; - } return { node: lineNode, index, @@ -1514,11 +1489,7 @@ function Ace2Inner() { } function nodeText(n) { - if (browser.msie) { - return n.innerText; - } else { - return n.textContent || n.nodeValue || ''; - } + return n.textContent || n.nodeValue || ''; } function getLineAndCharForPoint(point) { @@ -2281,59 +2252,7 @@ function Ace2Inner() { } function doCreateDomLine(nonEmpty) { - if (browser.msie && (!nonEmpty)) { - const result = { - node: null, - appendSpan: noop, - prepareForAdd: noop, - notifyAdded: noop, - clearSpans: noop, - finishUpdate: noop, - lineMarker: 0, - }; - - const lineElem = doc.createElement('div'); - result.node = lineElem; - - result.notifyAdded = function () { - // magic -- settng an empty div's innerHTML to the empty string - // keeps it from collapsing. Apparently innerHTML must be set *after* - // adding the node to the DOM. - // Such a div is what IE 6 creates naturally when you make a blank line - // in a document of divs. However, when copy-and-pasted the div will - // contain a space, so we note its emptiness with a property. - lineElem.innerHTML = ' '; // Frist we set a value that isnt blank - // a primitive-valued property survives copy-and-paste - setAssoc(lineElem, 'shouldBeEmpty', true); - // an object property doesn't - setAssoc(lineElem, 'unpasted', {}); - lineElem.innerHTML = ''; // Then we make it blank.. New line and no space = Awesome :) - }; - let lineClass = 'ace-line'; - result.appendSpan = function (txt, cls) { - if ((!txt) && cls) { - // gain a whole-line style (currently to show insertion point in CSS) - lineClass = domline.addToLineClass(lineClass, cls); - } - // otherwise, ignore appendSpan, this is an empty line - }; - result.clearSpans = function () { - lineClass = ''; // non-null to cause update - }; - - const writeClass = function () { - if (lineClass !== null) lineElem.className = lineClass; - }; - - result.prepareForAdd = writeClass; - result.finishUpdate = writeClass; - result.getInnerHTML = function () { - return ''; - }; - return result; - } else { - return domline.createDomLine(nonEmpty, doesWrap, browser, doc); - } + return domline.createDomLine(nonEmpty, doesWrap, browser, doc); } function textify(str) { @@ -2558,12 +2477,6 @@ function Ace2Inner() { const dirtiness = {}; dirtiness.nodeId = uniqueId(n); dirtiness.knownHTML = n.innerHTML; - if (browser.msie) { - // adding a space to an "empty" div in IE designMode doesn't - // change the innerHTML of the div's parent; also, other - // browsers don't support innerText - dirtiness.knownText = n.innerText; - } setAssoc(n, 'dirtiness', dirtiness); } @@ -2573,9 +2486,6 @@ function Ace2Inner() { const data = getAssoc(n, 'dirtiness'); if (!data) return true; if (n.id !== data.nodeId) return true; - if (browser.msie) { - if (n.innerText !== data.knownText) return true; - } if (n.innerHTML !== data.knownHTML) return true; p.end(); return false; @@ -2839,20 +2749,11 @@ function Ace2Inner() { // On Mac and Linux, move right moves to end of word and move left moves to start; // on Windows, always move to start of word. // On Windows, Firefox and IE disagree on whether to stop for punctuation (FF says no). - if (browser.msie && forwardNotBack) { - while ((!isDone()) && isWordChar(nextChar())) { - advance(); - } - while ((!isDone()) && !isWordChar(nextChar())) { - advance(); - } - } else { - while ((!isDone()) && !isWordChar(nextChar())) { - advance(); - } - while ((!isDone()) && isWordChar(nextChar())) { - advance(); - } + while ((!isDone()) && !isWordChar(nextChar())) { + advance(); + } + while ((!isDone()) && isWordChar(nextChar())) { + advance(); } return i; @@ -2891,8 +2792,8 @@ function Ace2Inner() { return; // This stops double enters in Opera but double Tabs still show on single tab keypress, adding keyCode == 9 to this doesn't help as the event is fired twice } let specialHandled = false; - const isTypeForSpecialKey = ((browser.msie || browser.safari || browser.chrome || browser.firefox) ? (type == 'keydown') : (type == 'keypress')); - const isTypeForCmdKey = ((browser.msie || browser.safari || browser.chrome || browser.firefox) ? (type == 'keydown') : (type == 'keypress')); + const isTypeForSpecialKey = ((browser.safari || browser.chrome || browser.firefox) ? (type == 'keydown') : (type == 'keypress')); + const isTypeForCmdKey = ((browser.safari || browser.chrome || browser.firefox) ? (type == 'keydown') : (type == 'keypress')); let stopped = false; inCallStackIfNecessary('handleKeyEvent', function () { @@ -3319,259 +3220,80 @@ function Ace2Inner() { else return 1; } - function hasIESelection() { - let browserSelection; - try { - browserSelection = doc.selection; - } catch (e) {} - if (!browserSelection) return false; - let origSelectionRange; - try { - origSelectionRange = browserSelection.createRange(); - } catch (e) {} - if (!origSelectionRange) return false; - return true; - } - function getSelection() { // returns null, or a structure containing startPoint and endPoint, // each of which has node (a magicdom node), index, and maxIndex. If the node // is a text node, maxIndex is the length of the text; else maxIndex is 1. // index is between 0 and maxIndex, inclusive. - if (browser.msie) { - var browserSelection; - try { - browserSelection = doc.selection; - } catch (e) {} - if (!browserSelection) return null; - let origSelectionRange; - try { - origSelectionRange = browserSelection.createRange(); - } catch (e) {} - if (!origSelectionRange) return null; - const selectionParent = origSelectionRange.parentElement(); - if (selectionParent.ownerDocument != doc) return null; + var browserSelection = window.getSelection(); + if (!browserSelection || browserSelection.type === 'None' || + browserSelection.rangeCount === 0) { + return null; + } + const range = browserSelection.getRangeAt(0); - const newRange = function () { - return doc.body.createTextRange(); - }; + function isInBody(n) { + while (n && !(n.tagName && n.tagName.toLowerCase() == 'body')) { + n = n.parentNode; + } + return !!n; + } - const rangeForElementNode = function (nd) { - const rng = newRange(); - // doesn't work on text nodes - rng.moveToElementText(nd); - return rng; - }; - - const pointFromCollapsedRange = function (rng) { - const parNode = rng.parentElement(); - let elemBelow = -1; - let elemAbove = parNode.childNodes.length; - const rangeWithin = rangeForElementNode(parNode); - - if (rng.compareEndPoints('StartToStart', rangeWithin) === 0) { - return { - node: parNode, - index: 0, - maxIndex: 1, - }; - } else if (rng.compareEndPoints('EndToEnd', rangeWithin) === 0) { - if (isBlockElement(parNode) && parNode.nextSibling) { - // caret after block is not consistent across browsers - // (same line vs next) so put caret before next node - return { - node: parNode.nextSibling, - index: 0, - maxIndex: 1, - }; - } - return { - node: parNode, - index: 1, - maxIndex: 1, - }; - } else if (parNode.childNodes.length === 0) { - return { - node: parNode, - index: 0, - maxIndex: 1, - }; - } - - for (let i = 0; i < parNode.childNodes.length; i++) { - const n = parNode.childNodes.item(i); - if (!isNodeText(n)) { - const nodeRange = rangeForElementNode(n); - const startComp = rng.compareEndPoints('StartToStart', nodeRange); - const endComp = rng.compareEndPoints('EndToEnd', nodeRange); - if (startComp >= 0 && endComp <= 0) { - let index = 0; - if (startComp > 0) { - index = 1; - } - return { - node: n, - index, - maxIndex: 1, - }; - } else if (endComp > 0) { - if (i > elemBelow) { - elemBelow = i; - rangeWithin.setEndPoint('StartToEnd', nodeRange); - } - } else if (startComp < 0) { - if (i < elemAbove) { - elemAbove = i; - rangeWithin.setEndPoint('EndToStart', nodeRange); - } - } - } - } - if ((elemAbove - elemBelow) == 1) { - if (elemBelow >= 0) { - return { - node: parNode.childNodes.item(elemBelow), - index: 1, - maxIndex: 1, - }; - } else { - return { - node: parNode.childNodes.item(elemAbove), - index: 0, - maxIndex: 1, - }; - } - } - let idx = 0; - const r = rng.duplicate(); - // infinite stateful binary search! call function for values 0 to inf, - // expecting the answer to be about 40. return index of smallest - // true value. - const indexIntoRange = binarySearchInfinite(40, (i) => { - // the search algorithm whips the caret back and forth, - // though it has to be moved relatively and may hit - // the end of the buffer - const delta = i - idx; - const moved = Math.abs(r.move('character', -delta)); - // next line is work-around for fact that when moving left, the beginning - // of a text node is considered to be after the start of the parent element: - if (r.move('character', -1)) r.move('character', 1); - if (delta < 0) idx -= moved; - else idx += moved; - return (r.compareEndPoints('StartToStart', rangeWithin) <= 0); - }); - // iterate over consecutive text nodes, point is in one of them - let textNode = elemBelow + 1; - let indexLeft = indexIntoRange; - while (textNode < elemAbove) { - var tn = parNode.childNodes.item(textNode); - if (indexLeft <= tn.nodeValue.length) { - return { - node: tn, - index: indexLeft, - maxIndex: tn.nodeValue.length, - }; - } - indexLeft -= tn.nodeValue.length; - textNode++; - } - var tn = parNode.childNodes.item(textNode - 1); + function pointFromRangeBound(container, offset) { + if (!isInBody(container)) { + // command-click in Firefox selects whole document, HEAD and BODY! return { - node: tn, - index: tn.nodeValue.length, - maxIndex: tn.nodeValue.length, + node: root, + index: 0, + maxIndex: 1, }; - }; - - var selection = {}; - if (origSelectionRange.compareEndPoints('StartToEnd', origSelectionRange) === 0) { - // collapsed - const pnt = pointFromCollapsedRange(origSelectionRange); - selection.startPoint = pnt; - selection.endPoint = { - node: pnt.node, - index: pnt.index, - maxIndex: pnt.maxIndex, + } + const n = container; + const childCount = n.childNodes.length; + if (isNodeText(n)) { + return { + node: n, + index: offset, + maxIndex: n.nodeValue.length, + }; + } else if (childCount === 0) { + return { + node: n, + index: 0, + maxIndex: 1, + }; + } + // treat point between two nodes as BEFORE the second (rather than after the first) + // if possible; this way point at end of a line block-element is treated as + // at beginning of next line + else if (offset == childCount) { + var nd = n.childNodes.item(childCount - 1); + var max = nodeMaxIndex(nd); + return { + node: nd, + index: max, + maxIndex: max, }; } else { - const start = origSelectionRange.duplicate(); - start.collapse(true); - const end = origSelectionRange.duplicate(); - end.collapse(false); - selection.startPoint = pointFromCollapsedRange(start); - selection.endPoint = pointFromCollapsedRange(end); + var nd = n.childNodes.item(offset); + var max = nodeMaxIndex(nd); + return { + node: nd, + index: 0, + maxIndex: max, + }; } - return selection; - } else { - // non-IE browser - var browserSelection = window.getSelection(); - if (browserSelection && browserSelection.type != 'None' && browserSelection.rangeCount !== 0) { - const range = browserSelection.getRangeAt(0); - - function isInBody(n) { - while (n && !(n.tagName && n.tagName.toLowerCase() == 'body')) { - n = n.parentNode; - } - return !!n; - } - - function pointFromRangeBound(container, offset) { - if (!isInBody(container)) { - // command-click in Firefox selects whole document, HEAD and BODY! - return { - node: root, - index: 0, - maxIndex: 1, - }; - } - const n = container; - const childCount = n.childNodes.length; - if (isNodeText(n)) { - return { - node: n, - index: offset, - maxIndex: n.nodeValue.length, - }; - } else if (childCount === 0) { - return { - node: n, - index: 0, - maxIndex: 1, - }; - } - // treat point between two nodes as BEFORE the second (rather than after the first) - // if possible; this way point at end of a line block-element is treated as - // at beginning of next line - else if (offset == childCount) { - var nd = n.childNodes.item(childCount - 1); - var max = nodeMaxIndex(nd); - return { - node: nd, - index: max, - maxIndex: max, - }; - } else { - var nd = n.childNodes.item(offset); - var max = nodeMaxIndex(nd); - return { - node: nd, - index: 0, - maxIndex: max, - }; - } - } - var selection = {}; - selection.startPoint = pointFromRangeBound(range.startContainer, range.startOffset); - selection.endPoint = pointFromRangeBound(range.endContainer, range.endOffset); - selection.focusAtStart = (((range.startContainer != range.endContainer) || (range.startOffset != range.endOffset)) && browserSelection.anchorNode && (browserSelection.anchorNode == range.endContainer) && (browserSelection.anchorOffset == range.endOffset)); - - if (selection.startPoint.node.ownerDocument !== window.document) { - return null; - } - - return selection; - } else { return null; } } + var selection = {}; + selection.startPoint = pointFromRangeBound(range.startContainer, range.startOffset); + selection.endPoint = pointFromRangeBound(range.endContainer, range.endOffset); + selection.focusAtStart = (((range.startContainer != range.endContainer) || (range.startOffset != range.endOffset)) && browserSelection.anchorNode && (browserSelection.anchorNode == range.endContainer) && (browserSelection.anchorOffset == range.endOffset)); + + if (selection.startPoint.node.ownerDocument !== window.document) { + return null; + } + + return selection; } function setSelection(selection) { @@ -3582,191 +3304,80 @@ function Ace2Inner() { maxIndex: pt.maxIndex, }; } - if (browser.msie) { - // Oddly enough, accessing scrollHeight fixes return key handling on IE 8, - // presumably by forcing some kind of internal DOM update. - doc.body.scrollHeight; + let isCollapsed; - function moveToElementText(s, n) { - while (n.firstChild && !isNodeText(n.firstChild)) { - n = n.firstChild; - } - s.moveToElementText(n); - } - - function newRange() { - return doc.body.createTextRange(); - } - - function setCollapsedBefore(s, n) { - // s is an IE TextRange, n is a dom node - if (isNodeText(n)) { - // previous node should not also be text, but prevent inf recurs - if (n.previousSibling && !isNodeText(n.previousSibling)) { - setCollapsedAfter(s, n.previousSibling); - } else { - setCollapsedBefore(s, n.parentNode); - } - } else { - moveToElementText(s, n); - // work around for issue that caret at beginning of line - // somehow ends up at end of previous line - if (s.move('character', 1)) { - s.move('character', -1); - } - s.collapse(true); // to start - } - } - - function setCollapsedAfter(s, n) { - // s is an IE TextRange, n is a magicdom node - if (isNodeText(n)) { - // can't use end of container when no nextSibling (could be on next line), - // so use previousSibling or start of container and move forward. - setCollapsedBefore(s, n); - s.move('character', n.nodeValue.length); - } else { - moveToElementText(s, n); - s.collapse(false); // to end - } - } - - function getPointRange(point) { - const s = newRange(); - const n = point.node; - if (isNodeText(n)) { - setCollapsedBefore(s, n); - s.move('character', point.index); - } else if (point.index === 0) { - setCollapsedBefore(s, n); - } else { - setCollapsedAfter(s, n); - } - return s; - } - - if (selection) { - if (!hasIESelection()) { - return; // don't steal focus - } - - const startPoint = copyPoint(selection.startPoint); - const endPoint = copyPoint(selection.endPoint); - - // fix issue where selection can't be extended past end of line - // with shift-rightarrow or shift-downarrow - if (endPoint.index == endPoint.maxIndex && endPoint.node.nextSibling) { - endPoint.node = endPoint.node.nextSibling; - endPoint.index = 0; - endPoint.maxIndex = nodeMaxIndex(endPoint.node); - } - var range = getPointRange(startPoint); - range.setEndPoint('EndToEnd', getPointRange(endPoint)); - - // setting the selection in IE causes everything to scroll - // so that the selection is visible. if setting the selection - // definitely accomplishes nothing, don't do it. - - - function isEqualToDocumentSelection(rng) { - let browserSelection; - try { - browserSelection = doc.selection; - } catch (e) {} - if (!browserSelection) return false; - const rng2 = browserSelection.createRange(); - if (rng2.parentElement().ownerDocument != doc) return false; - if (rng.compareEndPoints('StartToStart', rng2) !== 0) return false; - if (rng.compareEndPoints('EndToEnd', rng2) !== 0) return false; - return true; - } - if (!isEqualToDocumentSelection(range)) { - // dmesg(toSource(selection)); - // dmesg(escapeHTML(doc.body.innerHTML)); - range.select(); - } - } else { - try { - doc.selection.empty(); - } catch (e) {} - } - } else { - // non-IE browser - let isCollapsed; - - function pointToRangeBound(pt) { - const p = copyPoint(pt); - // Make sure Firefox cursor is deep enough; fixes cursor jumping when at top level, - // and also problem where cut/copy of a whole line selected with fake arrow-keys - // copies the next line too. - if (isCollapsed) { - function diveDeep() { - while (p.node.childNodes.length > 0) { - // && (p.node == root || p.node.parentNode == root)) { - if (p.index === 0) { - p.node = p.node.firstChild; - p.maxIndex = nodeMaxIndex(p.node); - } else if (p.index == p.maxIndex) { - p.node = p.node.lastChild; - p.maxIndex = nodeMaxIndex(p.node); - p.index = p.maxIndex; - } else { break; } - } - } - // now fix problem where cursor at end of text node at end of span-like element - // with background doesn't seem to show up... - if (isNodeText(p.node) && p.index == p.maxIndex) { - let n = p.node; - while ((!n.nextSibling) && (n != root) && (n.parentNode != root)) { - n = n.parentNode; - } - if (n.nextSibling && (!((typeof n.nextSibling.tagName) === 'string' && n.nextSibling.tagName.toLowerCase() == 'br')) && (n != p.node) && (n != root) && (n.parentNode != root)) { - // found a parent, go to next node and dive in - p.node = n.nextSibling; + function pointToRangeBound(pt) { + const p = copyPoint(pt); + // Make sure Firefox cursor is deep enough; fixes cursor jumping when at top level, + // and also problem where cut/copy of a whole line selected with fake arrow-keys + // copies the next line too. + if (isCollapsed) { + function diveDeep() { + while (p.node.childNodes.length > 0) { + // && (p.node == root || p.node.parentNode == root)) { + if (p.index === 0) { + p.node = p.node.firstChild; p.maxIndex = nodeMaxIndex(p.node); - p.index = 0; - diveDeep(); - } + } else if (p.index == p.maxIndex) { + p.node = p.node.lastChild; + p.maxIndex = nodeMaxIndex(p.node); + p.index = p.maxIndex; + } else { break; } } - // try to make sure insertion point is styled; - // also fixes other FF problems - if (!isNodeText(p.node)) { + } + // now fix problem where cursor at end of text node at end of span-like element + // with background doesn't seem to show up... + if (isNodeText(p.node) && p.index == p.maxIndex) { + let n = p.node; + while ((!n.nextSibling) && (n != root) && (n.parentNode != root)) { + n = n.parentNode; + } + if (n.nextSibling && (!((typeof n.nextSibling.tagName) === 'string' && n.nextSibling.tagName.toLowerCase() == 'br')) && (n != p.node) && (n != root) && (n.parentNode != root)) { + // found a parent, go to next node and dive in + p.node = n.nextSibling; + p.maxIndex = nodeMaxIndex(p.node); + p.index = 0; diveDeep(); } } - if (isNodeText(p.node)) { - return { - container: p.node, - offset: p.index, - }; - } else { - // p.index in {0,1} - return { - container: p.node.parentNode, - offset: childIndex(p.node) + p.index, - }; + // try to make sure insertion point is styled; + // also fixes other FF problems + if (!isNodeText(p.node)) { + diveDeep(); } } - const browserSelection = window.getSelection(); - if (browserSelection) { - browserSelection.removeAllRanges(); - if (selection) { - isCollapsed = (selection.startPoint.node === selection.endPoint.node && selection.startPoint.index === selection.endPoint.index); - const start = pointToRangeBound(selection.startPoint); - const end = pointToRangeBound(selection.endPoint); + if (isNodeText(p.node)) { + return { + container: p.node, + offset: p.index, + }; + } else { + // p.index in {0,1} + return { + container: p.node.parentNode, + offset: childIndex(p.node) + p.index, + }; + } + } + const browserSelection = window.getSelection(); + if (browserSelection) { + browserSelection.removeAllRanges(); + if (selection) { + isCollapsed = (selection.startPoint.node === selection.endPoint.node && selection.startPoint.index === selection.endPoint.index); + const start = pointToRangeBound(selection.startPoint); + const end = pointToRangeBound(selection.endPoint); - if ((!isCollapsed) && selection.focusAtStart && browserSelection.collapse && browserSelection.extend) { - // can handle "backwards"-oriented selection, shift-arrow-keys move start - // of selection - browserSelection.collapse(end.container, end.offset); - browserSelection.extend(start.container, start.offset); - } else { - var range = doc.createRange(); - range.setStart(start.container, start.offset); - range.setEnd(end.container, end.offset); - browserSelection.removeAllRanges(); - browserSelection.addRange(range); - } + if ((!isCollapsed) && selection.focusAtStart && browserSelection.collapse && browserSelection.extend) { + // can handle "backwards"-oriented selection, shift-arrow-keys move start + // of selection + browserSelection.collapse(end.container, end.offset); + browserSelection.extend(start.container, start.offset); + } else { + var range = doc.createRange(); + range.setStart(start.container, start.offset); + range.setEnd(end.container, end.offset); + browserSelection.removeAllRanges(); + browserSelection.addRange(range); } } } @@ -3802,30 +3413,6 @@ function Ace2Inner() { }); } - const iePastedLines = null; - - function handleIEPaste(evt) { - // Pasting in IE loses blank lines in a way that loses information; - // "one\n\ntwo\nthree" becomes "

                one

                two

                three

                ", - // which becomes "one\ntwo\nthree". We can get the correct text - // from the clipboard directly, but we still have to let the paste - // happen to get the style information. - const clipText = window.clipboardData && window.clipboardData.getData('Text'); - if (clipText && doc.selection) { - // this "paste" event seems to mess with the selection whether we try to - // stop it or not, so can't really do document-level manipulation now - // or in an idle call-stack. instead, use IE native manipulation - // function escapeLine(txt) { - // return processSpaces(escapeHTML(textify(txt))); - // } - // var newHTML = map(clipText.replace(/\r/g,'').split('\n'), escapeLine).join('
                '); - // doc.selection.createRange().pasteHTML(newHTML); - // evt.preventDefault(); - // iePastedLines = map(clipText.replace(/\r/g,'').split('\n'), textify); - } - } - - var inInternationalComposition = false; function handleCompositionEvent(evt) { // international input events, fired in FF3, at least; allow e.g. Japanese input @@ -3852,10 +3439,6 @@ function Ace2Inner() { // $(document).on("cut", handleCut); $(root).on('blur', handleBlur); - if (browser.msie) { - $(document).on('click', handleIEOuterClick); - } - if (browser.msie) $(root).on('paste', handleIEPaste); // If non-nullish, pasting on a link should be suppressed. let suppressPasteOnLink = null; @@ -3932,11 +3515,8 @@ function Ace2Inner() { }); }); - // CompositionEvent is not implemented below IE version 8 - if (!(browser.msie && parseInt(browser.version <= 9)) && document.documentElement) { - $(document.documentElement).on('compositionstart', handleCompositionEvent); - $(document.documentElement).on('compositionend', handleCompositionEvent); - } + $(document.documentElement).on('compositionstart', handleCompositionEvent); + $(document.documentElement).on('compositionend', handleCompositionEvent); } function topLevel(n) { @@ -3947,26 +3527,6 @@ function Ace2Inner() { return n; } - function handleIEOuterClick(evt) { - if ((evt.target.tagName || '').toLowerCase() != 'html') { - return; - } - if (!(evt.pageY > root.clientHeight)) { - return; - } - - // click below the body - inCallStackIfNecessary('handleOuterClick', () => { - // put caret at bottom of doc - fastIncorp(11); - if (isCaret()) { // don't interfere with drag - const lastLine = rep.lines.length() - 1; - const lastCol = rep.lines.atIndex(lastLine).text.length; - performSelectionChange([lastLine, lastCol], [lastLine, lastCol]); - } - }); - } - function getClassArray(elem, optFilter) { const bodyClasses = []; (elem.className || '').replace(/\S+/g, (c) => { @@ -3985,14 +3545,7 @@ function Ace2Inner() { window.focus(); } - function handleBlur(evt) { - if (browser.msie) { - // a fix: in IE, clicking on a control like a button outside the - // iframe can "blur" the editor, causing it to stop getting - // events, though typing still affects it(!). - setSelection(null); - } - } + function handleBlur(evt) {} function getSelectionPointX(point) { // doesn't work in wrap-mode @@ -4335,7 +3888,6 @@ function Ace2Inner() { root = body; // defined as a var in scope outside if (browser.firefox) $(root).addClass('mozilla'); if (browser.safari) $(root).addClass('safari'); - if (browser.msie) $(root).addClass('msie'); root.classList.toggle('authorColors', true); root.classList.toggle('doesWrap', doesWrap); diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index 7d6db689d..7177732db 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -606,9 +606,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas } } } - if (!abrowser.msie) { - _reachBlockPoint(node, 1, state); - } + _reachBlockPoint(node, 1, state); if (isBlock) { if (lines.length() - 1 == startLine) { // added additional check to resolve https://github.com/JohnMcLear/ep_copy_paste_images/issues/20 @@ -624,10 +622,6 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas _ensureColumnZero(state); } } - if (abrowser.msie) { - // in IE, a point immediately after a DIV appears on the next line - _reachBlockPoint(node, 1, state); - } state.localAttribs = localAttribs; }; // can pass a falsy value for end of doc diff --git a/src/static/js/domline.js b/src/static/js/domline.js index 7a82565db..9ec708ce2 100644 --- a/src/static/js/domline.js +++ b/src/static/js/domline.js @@ -201,7 +201,7 @@ domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) { if (!newHTML) { if ((!document) || (!optBrowser)) { newHTML += ' '; - } else if (!optBrowser.msie) { + } else { newHTML += '
                '; } } diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index f0e48268c..ddab47224 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -275,13 +275,6 @@ linestylefilter.getFilterStack = function (lineText, textAndClassFunc, abrowser) func = hookFilter(lineText, func); }); - if (abrowser !== undefined && abrowser.msie) { - // IE7+ will take an e-mail address like and linkify it to foo@bar.com. - // We then normalize it back to text with no angle brackets. It's weird. So always - // break spans at an "at" sign. - func = linestylefilter.getAtSignSplitterFilter( - lineText, func); - } return func; }; diff --git a/src/static/js/pad.js b/src/static/js/pad.js index d75e1c4f0..424c45241 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -406,13 +406,6 @@ var pad = { pad.initTime = +(new Date()); pad.padOptions = clientVars.initialOptions; - // for IE - if (browser.msie) { - try { - document.execCommand('BackgroundImageCache', false, true); - } catch (e) {} - } - // order of inits is important here: pad.myUserInfo = { userId: clientVars.userId, diff --git a/src/static/js/pad_userlist.js b/src/static/js/pad_userlist.js index 54e7fd4a8..6285ffad2 100644 --- a/src/static/js/pad_userlist.js +++ b/src/static/js/pad_userlist.js @@ -543,11 +543,7 @@ const paduserlist = (function () { $('#myswatch').css({'background-color': myUserInfo.colorId}); - if (browser.msie && parseInt(browser.version) <= 8) { - $('li[data-key=showusers] > a').css({'box-shadow': `inset 0 0 30px ${myUserInfo.colorId}`, 'background-color': myUserInfo.colorId}); - } else { - $('li[data-key=showusers] > a').css({'box-shadow': `inset 0 0 30px ${myUserInfo.colorId}`}); - } + $('li[data-key=showusers] > a').css({'box-shadow': `inset 0 0 30px ${myUserInfo.colorId}`}); }, }; return self; From a637920e55790ca4ec5468a85f4cb682346e166c Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Sun, 20 Dec 2020 07:00:18 +0100 Subject: [PATCH 289/315] add list-style:none for ul.indents in exported HTML (#4586) * add list-style:none for ul.indents in exported HTML * use list-style-type not list-style --- src/templates/export_html.html | 5 +++++ tests/backend/specs/api/importexport.js | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/src/templates/export_html.html b/src/templates/export_html.html index 4f386cf0d..fb14edd0a 100644 --- a/src/templates/export_html.html +++ b/src/templates/export_html.html @@ -31,6 +31,11 @@ ol ol > li:before { content: counters(item, ".") ". "; margin-left: -20px; } + +ul.indent { + list-style-type: none; +} + <%- extraCSS %> diff --git a/tests/backend/specs/api/importexport.js b/tests/backend/specs/api/importexport.js index 02395eae7..7490f2160 100644 --- a/tests/backend/specs/api/importexport.js +++ b/tests/backend/specs/api/importexport.js @@ -62,6 +62,12 @@ const testImports = { expectedHTML: 'empty

                ', expectedText: 'empty\n\n', }, + 'indentedListsAreNotBullets': { + description: 'Indented lists are represented with tabs and without bullets', + input: '
                • indent
                • indent
                ', + expectedHTML: '
                • indent
                • indent

                ', + expectedText: '\tindent\n\tindent\n\n' + } }; describe(__filename, function () { From 040057239e557aaed39fd47062248597ec4054c9 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Sun, 20 Dec 2020 07:18:19 +0100 Subject: [PATCH 290/315] tests for spaces (#4594) --- tests/backend/specs/api/importexport.js | 158 +++++++++++++++++++++++- tests/backend/specs/contentcollector.js | 158 ++++++++++++++++++++++++ 2 files changed, 315 insertions(+), 1 deletion(-) diff --git a/tests/backend/specs/api/importexport.js b/tests/backend/specs/api/importexport.js index 7490f2160..bfafc6b31 100644 --- a/tests/backend/specs/api/importexport.js +++ b/tests/backend/specs/api/importexport.js @@ -67,7 +67,163 @@ const testImports = { input: '
                • indent
                • indent
                ', expectedHTML: '
                • indent
                • indent

                ', expectedText: '\tindent\n\tindent\n\n' - } + }, + lineWithMultipleSpaces: { + description: 'Multiple spaces should be collapsed', + input: 'Text with more than one space.
                ', + expectedHTML: 'Text with more than one space.

                ', + expectedText: 'Text with more than one space.\n\n' + }, + lineWithMultipleNonBreakingAndNormalSpaces: { + // XXX the HTML between "than" and "one" looks strange + description: 'non-breaking space should be preserved, but can be replaced when it', + input: 'Text with  more   than  one space.
                ', + expectedHTML: 'Text with  more   than  one space.

                ', + expectedText: 'Text with more than one space.\n\n' + }, + multiplenbsp: { + description: 'Multiple non-breaking space should be preserved', + input: '  
                ', + expectedHTML: '  

                ', + expectedText: ' \n\n' + }, + multipleNonBreakingSpaceBetweenWords: { + description: 'A normal space is always inserted before a word', + input: '  word1  word2   word3
                ', + expectedHTML: '  word1  word2   word3

                ', + expectedText: ' word1 word2 word3\n\n' + }, + nonBreakingSpacePreceededBySpaceBetweenWords: { + description: 'A non-breaking space preceeded by a normal space', + input: '  word1  word2  word3
                ', + expectedHTML: ' word1  word2  word3

                ', + expectedText: ' word1 word2 word3\n\n' + }, + nonBreakingSpaceFollowededBySpaceBetweenWords: { + description: 'A non-breaking space followed by a normal space', + input: '  word1  word2  word3
                ', + expectedHTML: '  word1  word2  word3

                ', + expectedText: ' word1 word2 word3\n\n' + }, + spacesAfterNewline: { + description: 'Collapse spaces that follow a newline', + input:'something
                something
                ', + expectedHTML: 'something
                something

                ', + expectedText: 'something\nsomething\n\n' + }, + spacesAfterNewlineP: { + description: 'Collapse spaces that follow a paragraph', + input:'something

                something
                ', + expectedHTML: 'something

                something

                ', + expectedText: 'something\n\nsomething\n\n' + }, + spacesAtEndOfLine: { + description: 'Collapse spaces that preceed/follow a newline', + input:'something
                something
                ', + expectedHTML: 'something
                something

                ', + expectedText: 'something\nsomething\n\n' + }, + spacesAtEndOfLineP: { + description: 'Collapse spaces that preceed/follow a paragraph', + input:'something

                something
                ', + expectedHTML: 'something

                something

                ', + expectedText: 'something\n\nsomething\n\n' + }, + nonBreakingSpacesAfterNewlines: { + description: 'Don\'t collapse non-breaking spaces that follow a newline', + input:'something
                   something
                ', + expectedHTML: 'something
                   something

                ', + expectedText: 'something\n something\n\n' + }, + nonBreakingSpacesAfterNewlinesP: { + description: 'Don\'t collapse non-breaking spaces that follow a paragraph', + input:'something

                   something
                ', + expectedHTML: 'something

                   something

                ', + expectedText: 'something\n\n something\n\n' + }, + collapseSpacesInsideElements: { + description: 'Preserve only one space when multiple are present', + input: 'Need more space s !
                ', + expectedHTML: 'Need more space s !

                ', + expectedText: 'Need more space s !\n\n' + }, + collapseSpacesAcrossNewlines: { + description: 'Newlines and multiple spaces across newlines should be collapsed', + input: ` + Need + more + space + s + !
                `, + expectedHTML: 'Need more space s !

                ', + expectedText: 'Need more space s !\n\n' + }, + multipleNewLinesAtBeginning: { + description: 'Multiple new lines and paragraphs at the beginning should be preserved', + input: '

                first line

                second line
                ', + expectedHTML: '



                first line

                second line

                ', + expectedText: '\n\n\n\nfirst line\n\nsecond line\n\n' + }, + multiLineParagraph:{ + description: "A paragraph with multiple lines should not loose spaces when lines are combined", + input:` +

                + а б в г ґ д е є ж з и і ї й к л м н о + п р с т у ф х ц ч ш щ ю я ь +

                +`, + expectedHTML: 'а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь

                ', + expectedText: 'а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь\n\n' + }, + multiLineParagraphWithPre:{ + //XXX why is there   before "in"? + description: "lines in preformatted text should be kept intact", + input:` +

                + а б в г ґ д е є ж з и і ї й к л м н о

                multiple
                +   lines
                + in
                +      pre
                +

                п р с т у ф х ц ч ш щ ю я +ь

                +`, + expectedHTML: 'а б в г ґ д е є ж з и і ї й к л м н о
                multiple
                   lines
                 in
                      pre

                п р с т у ф х ц ч ш щ ю я ь

                ', + expectedText: 'а б в г ґ д е є ж з и і ї й к л м н о\nmultiple\n lines\n in\n pre\n\nп р с т у ф х ц ч ш щ ю я ь\n\n' + }, + preIntroducesASpace: { + description: "pre should be on a new line not preceeded by a space", + input:`

                + 1 +

                preline
                +

                `, + expectedHTML: '1
                preline


                ', + expectedText: '1\npreline\n\n\n' + }, + dontDeleteSpaceInsideElements: { + description: 'Preserve spaces inside elements', + input: 'Need more space s !
                ', + expectedHTML: 'Need more space s !

                ', + expectedText: 'Need more space s !\n\n' + }, + dontDeleteSpaceOutsideElements: { + description: 'Preserve spaces outside elements', + input: 'Need more space s !
                ', + expectedHTML: 'Need more space s !

                ', + expectedText: 'Need more space s !\n\n' + }, + dontDeleteSpaceAtEndOfElement: { + description: 'Preserve spaces at the end of an element', + input: 'Need more space s !
                ', + expectedHTML: 'Need more space s !

                ', + expectedText: 'Need more space s !\n\n' + }, + dontDeleteSpaceAtBeginOfElements: { + description: 'Preserve spaces at the start of an element', + input: 'Need more space s !
                ', + expectedHTML: 'Need more space s !

                ', + expectedText: 'Need more space s !\n\n' + }, +>>>>>>> 5a47aff2... tests for spaces }; describe(__filename, function () { diff --git a/tests/backend/specs/contentcollector.js b/tests/backend/specs/contentcollector.js index 3608faed6..c85904917 100644 --- a/tests/backend/specs/contentcollector.js +++ b/tests/backend/specs/contentcollector.js @@ -1,6 +1,14 @@ 'use strict'; /* eslint-disable max-len */ +/* + * While importexport tests target the `setHTML` API endpoint, which is nearly identical to what happens + * when a user manually imports a document via the UI, the contentcollector tests here don't use rehype to process + * the document. Rehype removes spaces and newĺines were applicable, so the expected results here can + * differ from importexport.js. + * + * If you add tests here, please also add them to importexport.js + */ const contentcollector = require('../../../src/static/js/contentcollector'); const AttributePool = require('../../../src/static/js/AttributePool'); @@ -113,6 +121,156 @@ const tests = { expectedLineAttribs: ['+5'], expectedText: ['empty'], }, + lineWithMultipleSpaces: { + description: 'Multiple spaces should be preserved', + html: 'Text with more than one space.
                ', + expectedLineAttribs: [ '+10' ], + expectedText: ['Text with more than one space.'] + }, + lineWithMultipleNonBreakingAndNormalSpaces: { + description: 'non-breaking and normal space should be preserved', + html: 'Text with  more   than  one space.
                ', + expectedLineAttribs: [ '+10' ], + expectedText: ['Text with more than one space.'] + }, + multiplenbsp: { + description: 'Multiple nbsp should be preserved', + html: '  
                ', + expectedLineAttribs: [ '+2' ], + expectedText: [' '] + }, + multipleNonBreakingSpaceBetweenWords: { + description: 'Multiple nbsp between words ', + html: '  word1  word2   word3
                ', + expectedLineAttribs: [ '+m' ], + expectedText: [' word1 word2 word3'] + }, + nonBreakingSpacePreceededBySpaceBetweenWords: { + description: 'A non-breaking space preceeded by a normal space', + html: '  word1  word2  word3
                ', + expectedLineAttribs: [ '+l' ], + expectedText: [' word1 word2 word3'] + }, + nonBreakingSpaceFollowededBySpaceBetweenWords: { + description: 'A non-breaking space followed by a normal space', + html: '  word1  word2  word3
                ', + expectedLineAttribs: [ '+l' ], + expectedText: [' word1 word2 word3'] + }, + spacesAfterNewline: { + description: 'Don\'t collapse spaces that follow a newline', + html:'something
                something
                ', + expectedLineAttribs: ['+9', '+m'], + expectedText: ['something', ' something'] + }, + spacesAfterNewlineP: { + description: 'Don\'t collapse spaces that follow a empty paragraph', + html:'something

                something
                ', + expectedLineAttribs: ['+9', '', '+m'], + expectedText: ['something', '', ' something'] + }, + spacesAtEndOfLine: { + description: 'Don\'t collapse spaces that preceed/follow a newline', + html:'something
                something
                ', + expectedLineAttribs: ['+l', '+m'], + expectedText: ['something ', ' something'] + }, + spacesAtEndOfLineP: { + description: 'Don\'t collapse spaces that preceed/follow a empty paragraph', + html:'something

                something
                ', + expectedLineAttribs: ['+l', '', '+m'], + expectedText: ['something ', '', ' something'] + }, + nonBreakingSpacesAfterNewlines: { + description: 'Don\'t collapse non-breaking spaces that follow a newline', + html:'something
                   something
                ', + expectedLineAttribs: ['+9', '+c'], + expectedText: ['something', ' something'] + }, + nonBreakingSpacesAfterNewlinesP: { + description: 'Don\'t collapse non-breaking spaces that follow a paragraph', + html:'something

                   something
                ', + expectedLineAttribs: ['+9', '', '+c'], + expectedText: ['something', '', ' something'] + }, + preserveSpacesInsideElements: { + description: 'Preserve all spaces when multiple are present', + html: 'Need more space s !
                ', + expectedLineAttribs: ['+h*0+4+2'], + expectedText: ['Need more space s !'], + }, + preserveSpacesAcrossNewlines: { + description: 'Newlines and multiple spaces across newlines should be preserved', + html: ` + Need + more + space + s + !
                `, + expectedLineAttribs: [ '+19*0+4+b' ], + expectedText: [ 'Need more space s !' ] + }, + multipleNewLinesAtBeginning: { + description: 'Multiple new lines at the beginning should be preserved', + html: '

                first line

                second line
                ', + expectedLineAttribs: ['', '', '', '', '+a', '', '+b'], + expectedText: [ '', '', '', '', 'first line', '', 'second line'] + }, + multiLineParagraph:{ + description: "A paragraph with multiple lines should not loose spaces when lines are combined", + html:`

                +а б в г ґ д е є ж з и і ї й к л м н о +п р с т у ф х ц ч ш щ ю я ь

                +`, + expectedLineAttribs: [ '+1t' ], + expectedText: ["а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь"] + }, + multiLineParagraphWithPre:{ + description: "lines in preformatted text should be kept intact", + html:`

                +а б в г ґ д е є ж з и і ї й к л м н о

                multiple
                +lines
                +in
                +pre
                +

                п р с т у ф х ц ч ш щ ю я +ь

                +`, + expectedLineAttribs: [ '+11', '+8', '+5', '+2', '+3', '+r' ], + expectedText: ['а б в г ґ д е є ж з и і ї й к л м н о', 'multiple', 'lines', 'in', 'pre', 'п р с т у ф х ц ч ш щ ю я ь'] + }, + preIntroducesASpace: { + description: "pre should be on a new line not preceeded by a space", + html:`

                + 1 +

                preline
                +

                `, + expectedLineAttribs: [ '+6', '+7' ], + expectedText: [' 1 ', 'preline'] + }, + dontDeleteSpaceInsideElements: { + description: 'Preserve spaces on the beginning and end of a element', + html: 'Need more space s !
                ', + expectedLineAttribs: ['+f*0+3+1'], + expectedText: ['Need more space s !'] + }, + dontDeleteSpaceOutsideElements: { + description: 'Preserve spaces outside elements', + html: 'Need more space s !
                ', + expectedLineAttribs: ['+g*0+1+2'], + expectedText: ['Need more space s !'] + }, + dontDeleteSpaceAtEndOfElement: { + description: 'Preserve spaces at the end of an element', + html: 'Need more space s !
                ', + expectedLineAttribs: ['+g*0+2+1'], + expectedText: ['Need more space s !'] + }, + dontDeleteSpaceAtBeginOfElements: { + description: 'Preserve spaces at the start of an element', + html: 'Need more space s !
                ', + expectedLineAttribs: ['+f*0+2+2'], + expectedText: ['Need more space s !'] + }, }; describe(__filename, function () { From 34ee77993f58ef4bc2d4ae683c387d06cab47f4c Mon Sep 17 00:00:00 2001 From: Hossein Marzban Date: Sun, 20 Dec 2020 09:48:49 +0330 Subject: [PATCH 291/315] Lint: pluginfw tsort.js (#4576) * lint: pluginfw tsort.js * Don't comment out the `console.log()` call Disabling the log message is out of scope for the pull request. * Put const and let on separate lines * Convert `tsort` from function to arrow function ESLint doesn't complain about this due to a bug in prefer-arrow/prefer-arrow-functions rule: https://github.com/TristonJ/eslint-plugin-prefer-arrow/issues/24 Co-authored-by: Richard Hansen --- src/static/js/pluginfw/tsort.js | 35 +++++++++++++++++---------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/static/js/pluginfw/tsort.js b/src/static/js/pluginfw/tsort.js index 18e1f158a..f988de3ad 100644 --- a/src/static/js/pluginfw/tsort.js +++ b/src/static/js/pluginfw/tsort.js @@ -1,3 +1,5 @@ +'use strict'; + /** * general topological sort * from https://gist.github.com/1232505 @@ -7,7 +9,7 @@ * @returns Array : topological sorted list of IDs **/ -function tsort(edges) { +const tsort = (edges) => { const nodes = {}; // hash: stringified id of the node => { id: id, afters: lisf of ids } const sorted = []; // sorted list of IDs ( returned value ) const visited = {}; // hash: id of already visited node => true @@ -26,8 +28,7 @@ function tsort(edges) { nodes[from].afters.push(to); }); - // 2. topological sort - Object.keys(nodes).forEach(function visit(idstr, ancestors) { + const visit = (idstr, ancestors) => { const node = nodes[idstr]; const id = node.id; @@ -41,14 +42,17 @@ function tsort(edges) { visited[idstr] = true; node.afters.forEach((afterID) => { - if (ancestors.indexOf(afterID) >= 0) // if already in ancestors, a closed chain exists. - { throw new Error(`closed chain : ${afterID} is in ${id}`); } + // if already in ancestors, a closed chain exists. + if (ancestors.indexOf(afterID) >= 0) throw new Error(`closed chain : ${afterID} is in ${id}`); visit(afterID.toString(), ancestors.map((v) => v)); // recursive call }); sorted.unshift(id); - }); + }; + + // 2. topological sort + Object.keys(nodes).forEach(visit); return sorted; } @@ -56,7 +60,7 @@ function tsort(edges) { /** * TEST **/ -function tsortTest() { +const tsortTest = () => { // example 1: success let edges = [ [1, 2], @@ -82,15 +86,13 @@ function tsortTest() { } // example 3: generate random edges - const max = 100; const - iteration = 30; - function randomInt(max) { - return Math.floor(Math.random() * max) + 1; - } + const max = 100; + const iteration = 30; + const randomInt = (max) => Math.floor(Math.random() * max) + 1; - edges = (function () { - const ret = []; let - i = 0; + edges = (() => { + const ret = []; + let i = 0; while (i++ < iteration) ret.push([randomInt(max), randomInt(max)]); return ret; })(); @@ -101,8 +103,7 @@ function tsortTest() { } catch (e) { console.log('failed', e.message); } -} - +}; // for node.js if (typeof exports === 'object' && exports === this) { From 0362d3b05db2e22e1e1759fe1c00693f013b2131 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 20 Dec 2020 07:15:58 +0000 Subject: [PATCH 292/315] lint: pad prefix files (#4577) * lint: pad_connectionstatus * lint: pad_utils * lint: pad_userlist.js -- still WIP * shift underscore not to be in require but to be used from window * lint: pad_modals * pad_impexp.js * lint: more errors done * lint: auto reconn * lint: pad_editor * lint: finish auto reconn * lint: imp exp rework * lint: import * lint: pad.js nearly done but pizza here... * lint: clientVars global query * put clientVars in window * Revert incorrect lint fixes * Properly fix guard-for-in lint errors * Properly fix no-unused-vars error regarding `gritter` * Refine lint fixes Co-authored-by: Richard Hansen --- src/static/js/pad.js | 480 ++++++++++++----------- src/static/js/pad_automatic_reconnect.js | 50 +-- src/static/js/pad_connectionstatus.js | 24 +- src/static/js/pad_cookie.js | 2 + src/static/js/pad_editbar.js | 1 + src/static/js/pad_editor.js | 47 +-- src/static/js/pad_impexp.js | 84 ++-- src/static/js/pad_modals.js | 14 +- src/static/js/pad_savedrevs.js | 11 +- src/static/js/pad_userlist.js | 254 ++++++------ src/static/js/pad_utils.js | 4 +- 11 files changed, 512 insertions(+), 459 deletions(-) diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 424c45241..c19bd58ea 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -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. @@ -20,8 +22,6 @@ * limitations under the License. */ -/* global $, window */ - let socket; // These jQuery things should create local references, but for now `require()` @@ -43,24 +43,13 @@ const padsavedrevs = require('./pad_savedrevs'); const paduserlist = require('./pad_userlist').paduserlist; const padutils = require('./pad_utils').padutils; const colorutils = require('./colorutils').colorutils; -var randomString = require('./pad_utils').randomString; -const gritter = require('./gritter').gritter; +const randomString = require('./pad_utils').randomString; +require('./gritter'); // Mutates the jQuery object to make $.gritter available. const hooks = require('./pluginfw/hooks'); let receivedClientVars = false; -function randomString() { - const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - const string_length = 20; - let randomstring = ''; - for (let i = 0; i < string_length; i++) { - const rnum = Math.floor(Math.random() * chars.length); - randomstring += chars.substring(rnum, rnum + 1); - } - return `t.${randomstring}`; -} - // This array represents all GET-parameters which can be used to change a setting. // name: the parameter-name, eg `?noColors=true` => `noColors` // checkVal: the callback is only executed when @@ -68,26 +57,101 @@ function randomString() { // * the parameter was supplied and checkVal is null // callback: the function to call when all above succeeds, `val` is the value supplied by the user const getParameters = [ - {name: 'noColors', checkVal: 'true', callback(val) { settings.noColors = true; $('#clearAuthorship').hide(); }}, - {name: 'showControls', checkVal: 'true', callback(val) { $('#editbar').css('display', 'flex'); }}, - {name: 'showChat', checkVal: null, callback(val) { if (val === 'false') { settings.hideChat = true; chat.hide(); $('#chaticon').hide(); } }}, - {name: 'showLineNumbers', checkVal: 'false', callback(val) { settings.LineNumbersDisabled = true; }}, - {name: 'useMonospaceFont', checkVal: 'true', callback(val) { settings.useMonospaceFontGlobal = true; }}, - // If the username is set as a parameter we should set a global value that we can call once we have initiated the pad. - {name: 'userName', checkVal: null, callback(val) { settings.globalUserName = decodeURIComponent(val); clientVars.userName = decodeURIComponent(val); }}, - // If the userColor is set as a parameter, set a global value to use once we have initiated the pad. - {name: 'userColor', checkVal: null, callback(val) { settings.globalUserColor = decodeURIComponent(val); clientVars.userColor = decodeURIComponent(val); }}, - {name: 'rtl', checkVal: 'true', callback(val) { settings.rtlIsTrue = true; }}, - {name: 'alwaysShowChat', checkVal: 'true', callback(val) { if (!settings.hideChat) chat.stickToScreen(); }}, - {name: 'chatAndUsers', checkVal: 'true', callback(val) { chat.chatAndUsers(); }}, - {name: 'lang', checkVal: null, callback(val) { window.html10n.localize([val, 'en']); Cookies.set('language', val); }}, + { + name: 'noColors', + checkVal: 'true', + callback: (val) => { + settings.noColors = true; + $('#clearAuthorship').hide(); + }, + }, + { + name: 'showControls', + checkVal: 'true', + callback: (val) => { + $('#editbar').css('display', 'flex'); + }, + }, + { + name: 'showChat', + checkVal: null, + callback: (val) => { + if (val === 'false') { + settings.hideChat = true; + chat.hide(); + $('#chaticon').hide(); + } + }, + }, + { + name: 'showLineNumbers', + checkVal: 'false', + callback: (val) => { + settings.LineNumbersDisabled = true; + }, + }, + { + name: 'useMonospaceFont', + checkVal: 'true', + callback: (val) => { + settings.useMonospaceFontGlobal = true; + }, + }, + // If the username is set as a parameter we should set a global value that we can call once we + // have initiated the pad. + { + name: 'userName', + checkVal: null, + callback: (val) => { + settings.globalUserName = decodeURIComponent(val); + clientVars.userName = decodeURIComponent(val); + }, + }, + // If the userColor is set as a parameter, set a global value to use once we have initiated the + // pad. + { + name: 'userColor', + checkVal: null, + callback: (val) => { + settings.globalUserColor = decodeURIComponent(val); + clientVars.userColor = decodeURIComponent(val); + }, + }, + { + name: 'rtl', + checkVal: 'true', + callback: (val) => { + settings.rtlIsTrue = true; + }, + }, + { + name: 'alwaysShowChat', + checkVal: 'true', + callback: (val) => { + if (!settings.hideChat) chat.stickToScreen(); + }, + }, + { + name: 'chatAndUsers', + checkVal: 'true', + callback: (val) => { + chat.chatAndUsers(); + }, + }, + { + name: 'lang', + checkVal: null, + callback: (val) => { + window.html10n.localize([val, 'en']); + Cookies.set('language', val); + }, + }, ]; -function getParams() { +const getParams = () => { // Tries server enforced options first.. - for (var i = 0; i < getParameters.length; i++) { - var setting = getParameters[i]; - var value = clientVars.padOptions[setting.name]; + for (const setting of getParameters) { + const value = clientVars.padOptions[setting.name]; if (value.toString() === setting.checkVal) { setting.callback(value); } @@ -96,19 +160,18 @@ function getParams() { // Then URL applied stuff const params = getUrlVars(); - for (var i = 0; i < getParameters.length; i++) { - var setting = getParameters[i]; - var value = params[setting.name]; + for (const setting of getParameters) { + const value = params[setting.name]; - if (value && (value == setting.checkVal || setting.checkVal == null)) { + if (value && (value === setting.checkVal || setting.checkVal == null)) { setting.callback(value); } } -} +}; -function getUrlVars() { - const vars = []; let - hash; +const getUrlVars = () => { + const vars = []; + let hash; const hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); for (let i = 0; i < hashes.length; i++) { hash = hashes[i].split('='); @@ -116,12 +179,13 @@ function getUrlVars() { vars[hash[0]] = hash[1]; } return vars; -} +}; -function sendClientReady(isReconnect, messageType) { +const sendClientReady = (isReconnect, messageType) => { messageType = typeof messageType !== 'undefined' ? messageType : 'CLIENT_READY'; let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1); - padId = decodeURIComponent(padId); // unescape neccesary due to Safari and Opera interpretation of spaces + // unescape neccesary due to Safari and Opera interpretation of spaces + padId = decodeURIComponent(padId); if (!isReconnect) { const titleArray = document.title.split('|'); @@ -151,12 +215,12 @@ function sendClientReady(isReconnect, messageType) { } socket.json.send(msg); -} +}; -function handshake() { +const handshake = () => { const loc = document.location; // get the correct port - const port = loc.port == '' ? (loc.protocol == 'https:' ? 443 : 80) : loc.port; + const port = loc.port === '' ? (loc.protocol === 'https:' ? 443 : 80) : loc.port; // create the url const url = `${loc.protocol}//${loc.hostname}:${port}/`; // find out in which subfolder we are @@ -211,12 +275,10 @@ function handshake() { throw new Error(`socket.io connection error: ${JSON.stringify(error)}`); }); - let initalized = false; - socket.on('message', (obj) => { // the access was not granted, give the user a message if (obj.accessStatus) { - if (obj.accessStatus == 'deny') { + if (obj.accessStatus === 'deny') { $('#loading').hide(); $('#permissionDenied').show(); @@ -226,18 +288,15 @@ function handshake() { $('#editorloadingbox').show(); } } - } - - // if we haven't recieved the clientVars yet, then this message should it be - else if (!receivedClientVars && obj.type == 'CLIENT_VARS') { + } else if (!receivedClientVars && obj.type === 'CLIENT_VARS') { + // if we haven't recieved the clientVars yet, then this message should it be receivedClientVars = true; // set some client vars - clientVars = obj.data; + window.clientVars = obj.data; // initalize the pad pad._afterHandshake(); - initalized = true; if (clientVars.readonly) { chat.hide(); @@ -255,63 +314,61 @@ function handshake() { }); // If the LineNumbersDisabled value is set to true then we need to hide the Line Numbers - if (settings.LineNumbersDisabled == true) { + if (settings.LineNumbersDisabled === true) { pad.changeViewOption('showLineNumbers', false); } - // If the noColors value is set to true then we need to hide the background colors on the ace spans - if (settings.noColors == true) { + // If the noColors value is set to true then we need to + // hide the background colors on the ace spans + if (settings.noColors === true) { pad.changeViewOption('noColors', true); } - if (settings.rtlIsTrue == true) { + if (settings.rtlIsTrue === true) { pad.changeViewOption('rtlIsTrue', true); } // If the Monospacefont value is set to true then change it to monospace. - if (settings.useMonospaceFontGlobal == true) { + if (settings.useMonospaceFontGlobal === true) { pad.changeViewOption('padFontFamily', 'monospace'); } - // if the globalUserName value is set we need to tell the server and the client about the new authorname + // if the globalUserName value is set we need to tell the server and + // the client about the new authorname if (settings.globalUserName !== false) { pad.notifyChangeName(settings.globalUserName); // Notifies the server pad.myUserInfo.name = settings.globalUserName; $('#myusernameedit').val(settings.globalUserName); // Updates the current users UI } if (settings.globalUserColor !== false && colorutils.isCssHex(settings.globalUserColor)) { - // Add a 'globalUserColor' property to myUserInfo, so collabClient knows we have a query parameter. + // Add a 'globalUserColor' property to myUserInfo, + // so collabClient knows we have a query parameter. pad.myUserInfo.globalUserColor = settings.globalUserColor; pad.notifyChangeColor(settings.globalUserColor); // Updates pad.myUserInfo.colorId paduserlist.setMyUserInfo(pad.myUserInfo); } - } - // This handles every Message after the clientVars - else { - // this message advices the client to disconnect - if (obj.disconnect) { - padconnectionstatus.disconnected(obj.disconnect); - socket.disconnect(); + } else if (obj.disconnect) { + padconnectionstatus.disconnected(obj.disconnect); + socket.disconnect(); - // block user from making any change to the pad - padeditor.disable(); - padeditbar.disable(); - padimpexp.disable(); + // block user from making any change to the pad + padeditor.disable(); + padeditbar.disable(); + padimpexp.disable(); - return; - } else { - pad.collabClient.handleMessageFromServer(obj); - } + return; + } else { + pad.collabClient.handleMessageFromServer(obj); } }); // Bind the colorpicker - const fb = $('#colorpicker').farbtastic({callback: '#mycolorpickerpreview', width: 220}); + $('#colorpicker').farbtastic({callback: '#mycolorpickerpreview', width: 220}); // Bind the read only button $('#readonlyinput').on('click', () => { padeditbar.setEmbedLinks(); }); -} +}; -var pad = { +const pad = { // don't access these directly from outside this file, except // for debugging collabClient: null, @@ -322,44 +379,28 @@ var pad = { padOptions: {}, // these don't require init; clientVars should all go through here - getPadId() { - return clientVars.padId; - }, - getClientIp() { - return clientVars.clientIp; - }, - getColorPalette() { - return clientVars.colorPalette; - }, - getIsDebugEnabled() { - return clientVars.debugEnabled; - }, - getPrivilege(name) { - return clientVars.accountPrivs[name]; - }, - getUserId() { - return pad.myUserInfo.userId; - }, - getUserName() { - return pad.myUserInfo.name; - }, - userList() { - return paduserlist.users(); - }, - switchToPad(padId) { - let newHref = new RegExp(/.*\/p\/[^\/]+/).exec(document.location.pathname) || clientVars.padId; + getPadId: () => clientVars.padId, + getClientIp: () => clientVars.clientIp, + getColorPalette: () => clientVars.colorPalette, + getIsDebugEnabled: () => clientVars.debugEnabled, + getPrivilege: (name) => clientVars.accountPrivs[name], + getUserId: () => pad.myUserInfo.userId, + getUserName: () => pad.myUserInfo.name, + userList: () => paduserlist.users(), + switchToPad: (padId) => { + let newHref = new RegExp(/.*\/p\/[^/]+/).exec(document.location.pathname) || clientVars.padId; newHref = newHref[0]; const options = clientVars.padOptions; if (typeof options !== 'undefined' && options != null) { - var option_str = []; + const optionArr = []; $.each(options, (k, v) => { const str = `${k}=${v}`; - option_str.push(str); + optionArr.push(str); }); - var option_str = option_str.join('&'); + const optionStr = optionArr.join('&'); - newHref = `${newHref}?${option_str}`; + newHref = `${newHref}?${optionStr}`; } // destroy old pad from DOM @@ -373,21 +414,21 @@ var pad = { window.history.pushState('', '', newHref); receivedClientVars = false; sendClientReady(false, 'SWITCH_TO_PAD'); - } else // fallback - { + } else { + // fallback window.location.href = newHref; } }, - sendClientMessage(msg) { + sendClientMessage: (msg) => { pad.collabClient.sendClientMessage(msg); }, - init() { + init: () => { padutils.setupGlobalExceptionHandler(); $(document).ready(() => { // start the custom js - if (typeof customStart === 'function') customStart(); + if (typeof customStart === 'function') customStart(); // eslint-disable-line no-undef handshake(); // To use etherpad you have to allow cookies. @@ -406,7 +447,6 @@ var pad = { pad.initTime = +(new Date()); pad.padOptions = clientVars.initialOptions; - // order of inits is important here: pad.myUserInfo = { userId: clientVars.userId, name: clientVars.userName, @@ -414,18 +454,60 @@ var pad = { colorId: clientVars.userColor, }; + const postAceInit = () => { + padeditbar.init(); + setTimeout(() => { + padeditor.ace.focus(); + }, 0); + // if we have a cookie for always showing chat then show it + if (padcookie.getPref('chatAlwaysVisible')) { + chat.stickToScreen(true); // stick it to the screen + $('#options-stickychat').prop('checked', true); // set the checkbox to on + } + // if we have a cookie for always showing chat then show it + if (padcookie.getPref('chatAndUsers')) { + chat.chatAndUsers(true); // stick it to the screen + $('#options-chatandusers').prop('checked', true); // set the checkbox to on + } + if (padcookie.getPref('showAuthorshipColors') === false) { + pad.changeViewOption('showAuthorColors', false); + } + if (padcookie.getPref('showLineNumbers') === false) { + pad.changeViewOption('showLineNumbers', false); + } + if (padcookie.getPref('rtlIsTrue') === true) { + pad.changeViewOption('rtlIsTrue', true); + } + pad.changeViewOption('padFontFamily', padcookie.getPref('padFontFamily')); + $('#viewfontmenu').val(padcookie.getPref('padFontFamily')).niceSelect('update'); + + // Prevent sticky chat or chat and users to be checked for mobiles + const checkChatAndUsersVisibility = (x) => { + if (x.matches) { // If media query matches + $('#options-chatandusers:checked').click(); + $('#options-stickychat:checked').click(); + } + }; + const mobileMatch = window.matchMedia('(max-width: 800px)'); + mobileMatch.addListener(checkChatAndUsersVisibility); // check if window resized + setTimeout(() => { checkChatAndUsersVisibility(mobileMatch); }, 0); // check now after load + + $('#editorcontainer').addClass('initialized'); + + hooks.aCallAll('postAceInit', {ace: padeditor.ace, pad}); + }; + + // order of inits is important here: padimpexp.init(this); padsavedrevs.init(this); - padeditor.init(postAceInit, pad.padOptions.view || {}, this); - paduserlist.init(pad.myUserInfo, this); padconnectionstatus.init(); padmodals.init(this); - pad.collabClient = getCollabClient(padeditor.ace, clientVars.collab_client_vars, pad.myUserInfo, { - colorPalette: pad.getColorPalette(), - }, pad); + pad.collabClient = getCollabClient( + padeditor.ace, clientVars.collab_client_vars, pad.myUserInfo, + {colorPalette: pad.getColorPalette()}, pad); pad.collabClient.setOnUserJoin(pad.handleUserJoin); pad.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate); pad.collabClient.setOnUserLeave(pad.handleUserLeave); @@ -434,68 +516,27 @@ var pad = { pad.collabClient.setOnInternalAction(pad.handleCollabAction); // load initial chat-messages - if (clientVars.chatHead != -1) { + if (clientVars.chatHead !== -1) { const chatHead = clientVars.chatHead; const start = Math.max(chatHead - 100, 0); pad.collabClient.sendMessage({type: 'GET_CHAT_MESSAGES', start, end: chatHead}); - } else // there are no messages - { + } else { + // there are no messages $('#chatloadmessagesbutton').css('display', 'none'); } - - function postAceInit() { - padeditbar.init(); - setTimeout(() => { - padeditor.ace.focus(); - }, 0); - if (padcookie.getPref('chatAlwaysVisible')) { // if we have a cookie for always showing chat then show it - chat.stickToScreen(true); // stick it to the screen - $('#options-stickychat').prop('checked', true); // set the checkbox to on - } - if (padcookie.getPref('chatAndUsers')) { // if we have a cookie for always showing chat then show it - chat.chatAndUsers(true); // stick it to the screen - $('#options-chatandusers').prop('checked', true); // set the checkbox to on - } - if (padcookie.getPref('showAuthorshipColors') == false) { - pad.changeViewOption('showAuthorColors', false); - } - if (padcookie.getPref('showLineNumbers') == false) { - pad.changeViewOption('showLineNumbers', false); - } - if (padcookie.getPref('rtlIsTrue') == true) { - pad.changeViewOption('rtlIsTrue', true); - } - pad.changeViewOption('padFontFamily', padcookie.getPref('padFontFamily')); - $('#viewfontmenu').val(padcookie.getPref('padFontFamily')).niceSelect('update'); - - // Prevent sticky chat or chat and users to be checked for mobiles - function checkChatAndUsersVisibility(x) { - if (x.matches) { // If media query matches - $('#options-chatandusers:checked').click(); - $('#options-stickychat:checked').click(); - } - } - const mobileMatch = window.matchMedia('(max-width: 800px)'); - mobileMatch.addListener(checkChatAndUsersVisibility); // check if window resized - setTimeout(() => { checkChatAndUsersVisibility(mobileMatch); }, 0); // check now after load - - $('#editorcontainer').addClass('initialized'); - - hooks.aCallAll('postAceInit', {ace: padeditor.ace, pad}); - } }, - dispose() { + dispose: () => { padeditor.dispose(); }, - notifyChangeName(newName) { + notifyChangeName: (newName) => { pad.myUserInfo.name = newName; pad.collabClient.updateUserInfo(pad.myUserInfo); }, - notifyChangeColor(newColorId) { + notifyChangeColor: (newColorId) => { pad.myUserInfo.colorId = newColorId; pad.collabClient.updateUserInfo(pad.myUserInfo); }, - changePadOption(key, value) { + changePadOption: (key, value) => { const options = {}; options[key] = value; pad.handleOptionsChange(options); @@ -506,32 +547,30 @@ var pad = { changedBy: pad.myUserInfo.name || 'unnamed', }); }, - changeViewOption(key, value) { + changeViewOption: (key, value) => { const options = { view: {}, }; options.view[key] = value; pad.handleOptionsChange(options); }, - handleOptionsChange(opts) { + handleOptionsChange: (opts) => { // opts object is a full set of options or just // some options to change if (opts.view) { if (!pad.padOptions.view) { pad.padOptions.view = {}; } - for (const k in opts.view) { - pad.padOptions.view[k] = opts.view[k]; - padcookie.setPref(k, opts.view[k]); + for (const [k, v] of Object.entries(opts.view)) { + pad.padOptions.view[k] = v; + padcookie.setPref(k, v); } padeditor.setViewOptions(pad.padOptions.view); } }, - getPadOptions() { - // caller shouldn't mutate the object - return pad.padOptions; - }, - suggestUserName(userId, name) { + // caller shouldn't mutate the object + getPadOptions: () => pad.padOptions, + suggestUserName: (userId, name) => { pad.collabClient.sendClientMessage( { type: 'suggestUserName', @@ -539,31 +578,31 @@ var pad = { newName: name, }); }, - handleUserJoin(userInfo) { + handleUserJoin: (userInfo) => { paduserlist.userJoinOrUpdate(userInfo); }, - handleUserUpdate(userInfo) { + handleUserUpdate: (userInfo) => { paduserlist.userJoinOrUpdate(userInfo); }, - handleUserLeave(userInfo) { + handleUserLeave: (userInfo) => { paduserlist.userLeave(userInfo); }, - handleClientMessage(msg) { - if (msg.type == 'suggestUserName') { - if (msg.unnamedId == pad.myUserInfo.userId && msg.newName && !pad.myUserInfo.name) { + handleClientMessage: (msg) => { + if (msg.type === 'suggestUserName') { + if (msg.unnamedId === pad.myUserInfo.userId && msg.newName && !pad.myUserInfo.name) { pad.notifyChangeName(msg.newName); paduserlist.setMyUserInfo(pad.myUserInfo); } - } else if (msg.type == 'newRevisionList') { + } else if (msg.type === 'newRevisionList') { padsavedrevs.newRevisionList(msg.revisionList); - } else if (msg.type == 'revisionLabel') { + } else if (msg.type === 'revisionLabel') { padsavedrevs.newRevisionList(msg.revisionList); - } else if (msg.type == 'padoptions') { + } else if (msg.type === 'padoptions') { const opts = msg.options; pad.handleOptionsChange(opts); } }, - dmesg(m) { + dmesg: (m) => { if (pad.getIsDebugEnabled()) { const djs = $('#djs').get(0); const wasAtBottom = (djs.scrollTop - (djs.scrollHeight - $(djs).height()) >= -20); @@ -573,31 +612,30 @@ var pad = { } } }, - handleChannelStateChange(newState, message) { + handleChannelStateChange: (newState, message) => { const oldFullyConnected = !!padconnectionstatus.isFullyConnected(); - const wasConnecting = (padconnectionstatus.getStatus().what == 'connecting'); - if (newState == 'CONNECTED') { + const wasConnecting = (padconnectionstatus.getStatus().what === 'connecting'); + if (newState === 'CONNECTED') { padeditor.enable(); padeditbar.enable(); padimpexp.enable(); padconnectionstatus.connected(); - } else if (newState == 'RECONNECTING') { + } else if (newState === 'RECONNECTING') { padeditor.disable(); padeditbar.disable(); padimpexp.disable(); padconnectionstatus.reconnecting(); - } else if (newState == 'DISCONNECTED') { + } else if (newState === 'DISCONNECTED') { pad.diagnosticInfo.disconnectedMessage = message; pad.diagnosticInfo.padId = pad.getPadId(); pad.diagnosticInfo.socket = {}; // we filter non objects from the socket object and put them in the diagnosticInfo // this ensures we have no cyclic data - this allows us to stringify the data - for (const i in socket.socket) { - const value = socket.socket[i]; + for (const [i, value] of Object.entries(socket.socket || {})) { const type = typeof value; - if (type == 'string' || type == 'number') { + if (type === 'string' || type === 'number') { pad.diagnosticInfo.socket[i] = value; } } @@ -613,11 +651,11 @@ var pad = { padconnectionstatus.disconnected(message); } const newFullyConnected = !!padconnectionstatus.isFullyConnected(); - if (newFullyConnected != oldFullyConnected) { + if (newFullyConnected !== oldFullyConnected) { pad.handleIsFullyConnected(newFullyConnected, wasConnecting); } }, - handleIsFullyConnected(isConnected, isInitialConnect) { + handleIsFullyConnected: (isConnected, isInitialConnect) => { pad.determineChatVisibility(isConnected && !isInitialConnect); pad.determineChatAndUsersVisibility(isConnected && !isInitialConnect); pad.determineAuthorshipColorsVisibility(); @@ -625,7 +663,7 @@ var pad = { padeditbar.toggleDropDown('none'); }, 1000); }, - determineChatVisibility(asNowConnectedFeedback) { + determineChatVisibility: (asNowConnectedFeedback) => { const chatVisCookie = padcookie.getPref('chatAlwaysVisible'); if (chatVisCookie) { // if the cookie is set for chat always visible chat.stickToScreen(true); // stick it to the screen @@ -634,7 +672,7 @@ var pad = { $('#options-stickychat').prop('checked', false); // set the checkbox for off } }, - determineChatAndUsersVisibility(asNowConnectedFeedback) { + determineChatAndUsersVisibility: (asNowConnectedFeedback) => { const chatAUVisCookie = padcookie.getPref('chatAndUsersVisible'); if (chatAUVisCookie) { // if the cookie is set for chat always visible chat.chatAndUsers(true); // stick it to the screen @@ -643,7 +681,7 @@ var pad = { $('#options-chatandusers').prop('checked', false); // set the checkbox for off } }, - determineAuthorshipColorsVisibility() { + determineAuthorshipColorsVisibility: () => { const authColCookie = padcookie.getPref('showAuthorshipColors'); if (authColCookie) { pad.changeViewOption('showAuthorColors', true); @@ -652,14 +690,14 @@ var pad = { $('#options-colorscheck').prop('checked', false); } }, - handleCollabAction(action) { - if (action == 'commitPerformed') { + handleCollabAction: (action) => { + if (action === 'commitPerformed') { padeditbar.setSyncStatus('syncing'); - } else if (action == 'newlyIdle') { + } else if (action === 'newlyIdle') { padeditbar.setSyncStatus('done'); } }, - asyncSendDiagnosticInfo() { + asyncSendDiagnosticInfo: () => { window.setTimeout(() => { $.ajax( { @@ -668,32 +706,29 @@ var pad = { data: { diagnosticInfo: JSON.stringify(pad.diagnosticInfo), }, - success() {}, - error() {}, + success: () => {}, + error: () => {}, }); }, 0); }, - forceReconnect() { + forceReconnect: () => { $('form#reconnectform input.padId').val(pad.getPadId()); pad.diagnosticInfo.collabDiagnosticInfo = pad.collabClient.getDiagnosticInfo(); $('form#reconnectform input.diagnosticInfo').val(JSON.stringify(pad.diagnosticInfo)); - $('form#reconnectform input.missedChanges').val(JSON.stringify(pad.collabClient.getMissedChanges())); + $('form#reconnectform input.missedChanges') + .val(JSON.stringify(pad.collabClient.getMissedChanges())); $('form#reconnectform').submit(); }, // this is called from code put into a frame from the server: - handleImportExportFrameCall(callName, varargs) { + handleImportExportFrameCall: (callName, varargs) => { padimpexp.handleFrameCall.call(padimpexp, callName, Array.prototype.slice.call(arguments, 1)); }, - callWhenNotCommitting(f) { + callWhenNotCommitting: (f) => { pad.collabClient.callWhenNotCommitting(f); }, - getCollabRevisionNumber() { - return pad.collabClient.getCurrentRevisionNumber(); - }, - isFullyConnected() { - return padconnectionstatus.isFullyConnected(); - }, - addHistoricalAuthors(data) { + getCollabRevisionNumber: () => pad.collabClient.getCurrentRevisionNumber(), + isFullyConnected: () => padconnectionstatus.isFullyConnected(), + addHistoricalAuthors: (data) => { if (!pad.collabClient) { window.setTimeout(() => { pad.addHistoricalAuthors(data); @@ -704,11 +739,9 @@ var pad = { }, }; -function init() { - return pad.init(); -} +const init = () => pad.init(); -var settings = { +const settings = { LineNumbersDisabled: false, noColors: false, useMonospaceFontGlobal: false, @@ -718,6 +751,7 @@ var settings = { }; pad.settings = settings; + exports.baseURL = ''; exports.settings = settings; exports.randomString = randomString; diff --git a/src/static/js/pad_automatic_reconnect.js b/src/static/js/pad_automatic_reconnect.js index 8edd09b38..db803e896 100644 --- a/src/static/js/pad_automatic_reconnect.js +++ b/src/static/js/pad_automatic_reconnect.js @@ -1,4 +1,6 @@ -exports.showCountDownTimerToReconnectOnModal = function ($modal, pad) { +'use strict'; + +exports.showCountDownTimerToReconnectOnModal = ($modal, pad) => { if (clientVars.automaticReconnectionTimeout && $modal.is('.with_reconnect_timer')) { createCountDownElementsIfNecessary($modal); @@ -13,7 +15,7 @@ exports.showCountDownTimerToReconnectOnModal = function ($modal, pad) { } }; -var createCountDownElementsIfNecessary = function ($modal) { +const createCountDownElementsIfNecessary = ($modal) => { const elementsDoNotExist = $modal.find('#cancelreconnect').length === 0; if (elementsDoNotExist) { const $defaultMessage = $modal.find('#defaulttext'); @@ -45,12 +47,13 @@ var createCountDownElementsIfNecessary = function ($modal) { } }; -var localize = function ($element) { +const localize = ($element) => { html10n.translateElement(html10n.translations, $element.get(0)); }; -var createTimerForModal = function ($modal, pad) { - const timeUntilReconnection = clientVars.automaticReconnectionTimeout * reconnectionTries.nextTry(); +const createTimerForModal = ($modal, pad) => { + const timeUntilReconnection = + clientVars.automaticReconnectionTimeout * reconnectionTries.nextTry(); const timer = new CountDownTimer(timeUntilReconnection); timer.onTick((minutes, seconds) => { @@ -68,23 +71,23 @@ var createTimerForModal = function ($modal, pad) { return timer; }; -var disableAutomaticReconnection = function ($modal) { +const disableAutomaticReconnection = ($modal) => { toggleAutomaticReconnectionOption($modal, true); }; -var enableAutomaticReconnection = function ($modal) { +const enableAutomaticReconnection = ($modal) => { toggleAutomaticReconnectionOption($modal, false); }; -var toggleAutomaticReconnectionOption = function ($modal, disableAutomaticReconnect) { +const toggleAutomaticReconnectionOption = ($modal, disableAutomaticReconnect) => { $modal.find('#cancelreconnect, .reconnecttimer').toggleClass('hidden', disableAutomaticReconnect); $modal.find('#defaulttext').toggleClass('hidden', !disableAutomaticReconnect); }; -var waitUntilClientCanConnectToServerAndThen = function (callback, pad) { +const waitUntilClientCanConnectToServerAndThen = (callback, pad) => { whenConnectionIsRestablishedWithServer(callback, pad); pad.socket.connect(); }; -var whenConnectionIsRestablishedWithServer = function (callback, pad) { +const whenConnectionIsRestablishedWithServer = (callback, pad) => { // only add listener for the first try, don't need to add another listener // on every unsuccessful try if (reconnectionTries.counter === 1) { @@ -92,11 +95,11 @@ var whenConnectionIsRestablishedWithServer = function (callback, pad) { } }; -var forceReconnection = function ($modal) { +const forceReconnection = ($modal) => { $modal.find('#forcereconnect').click(); }; -var updateCountDownTimerMessage = function ($modal, minutes, seconds) { +const updateCountDownTimerMessage = ($modal, minutes, seconds) => { minutes = minutes < 10 ? `0${minutes}` : minutes; seconds = seconds < 10 ? `0${seconds}` : seconds; @@ -105,7 +108,7 @@ var updateCountDownTimerMessage = function ($modal, minutes, seconds) { // store number of tries to reconnect to server, in order to increase time to wait // until next try -var reconnectionTries = { +const reconnectionTries = { counter: 0, nextTry() { @@ -119,8 +122,9 @@ var reconnectionTries = { // Timer based on http://stackoverflow.com/a/20618517. // duration: how many **seconds** until the timer ends -// granularity (optional): how many **milliseconds** between each 'tick' of timer. Default: 1000ms (1s) -var CountDownTimer = function (duration, granularity) { +// granularity (optional): how many **milliseconds** +// between each 'tick' of timer. Default: 1000ms (1s) +const CountDownTimer = function (duration, granularity) { this.duration = duration; this.granularity = granularity || 1000; this.running = false; @@ -137,8 +141,7 @@ CountDownTimer.prototype.start = function () { const start = Date.now(); const that = this; let diff; - - (function timer() { + const timer = () => { diff = that.duration - Math.floor((Date.now() - start) / 1000); if (diff > 0) { @@ -149,7 +152,8 @@ CountDownTimer.prototype.start = function () { that.tick(0); that.expire(); } - }()); + }; + timer(); }; CountDownTimer.prototype.tick = function (diff) { @@ -184,9 +188,7 @@ CountDownTimer.prototype.cancel = function () { return this; }; -CountDownTimer.parse = function (seconds) { - return { - minutes: (seconds / 60) | 0, - seconds: (seconds % 60) | 0, - }; -}; +CountDownTimer.parse = (seconds) => ({ + minutes: (seconds / 60) | 0, + seconds: (seconds % 60) | 0, +}); diff --git a/src/static/js/pad_connectionstatus.js b/src/static/js/pad_connectionstatus.js index 54d7dc760..1282d488c 100644 --- a/src/static/js/pad_connectionstatus.js +++ b/src/static/js/pad_connectionstatus.js @@ -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,25 +24,25 @@ const padmodals = require('./pad_modals').padmodals; -const padconnectionstatus = (function () { +const padconnectionstatus = (() => { let status = { what: 'connecting', }; const self = { - init() { + init: () => { $('button#forcereconnect').click(() => { window.location.reload(); }); }, - connected() { + connected: () => { status = { what: 'connected', }; padmodals.showModal('connected'); padmodals.hideOverlay(); }, - reconnecting() { + reconnecting: () => { status = { what: 'reconnecting', }; @@ -48,8 +50,8 @@ const padconnectionstatus = (function () { padmodals.showModal('reconnecting'); padmodals.showOverlay(); }, - disconnected(msg) { - if (status.what == 'disconnected') return; + disconnected: (msg) => { + if (status.what === 'disconnected') return; status = { what: 'disconnected', @@ -81,14 +83,10 @@ const padconnectionstatus = (function () { padmodals.showModal(k); padmodals.showOverlay(); }, - isFullyConnected() { - return status.what == 'connected'; - }, - getStatus() { - return status; - }, + isFullyConnected: () => status.what === 'connected', + getStatus: () => status, }; return self; -}()); +})(); exports.padconnectionstatus = padconnectionstatus; diff --git a/src/static/js/pad_cookie.js b/src/static/js/pad_cookie.js index f3dedf03c..620d41735 100644 --- a/src/static/js/pad_cookie.js +++ b/src/static/js/pad_cookie.js @@ -1,3 +1,5 @@ +'use strict'; + /** * Copyright 2009 Google Inc. * diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index dae96b192..788d5ecd0 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -1,4 +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. diff --git a/src/static/js/pad_editor.js b/src/static/js/pad_editor.js index 912c50f64..b7b94f720 100644 --- a/src/static/js/pad_editor.js +++ b/src/static/js/pad_editor.js @@ -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. @@ -24,26 +26,26 @@ const Cookies = require('./pad_utils').Cookies; const padcookie = require('./pad_cookie').padcookie; const padutils = require('./pad_utils').padutils; -const padeditor = (function () { +const padeditor = (() => { let Ace2Editor = undefined; let pad = undefined; let settings = undefined; - var self = { + const self = { ace: null, // this is accessed directly from other files viewZoom: 100, - init(readyFunc, initialViewOptions, _pad) { + init: (readyFunc, initialViewOptions, _pad) => { Ace2Editor = require('./ace').Ace2Editor; pad = _pad; settings = pad.settings; - function aceReady() { + const aceReady = () => { $('#editorloadingbox').hide(); if (readyFunc) { readyFunc(); } - } + }; self.ace = new Ace2Editor(); self.ace.init('editorcontainer', '', aceReady); @@ -57,7 +59,7 @@ const padeditor = (function () { // view bar $('#viewbarcontents').show(); }, - initViewOptions() { + initViewOptions: () => { // Line numbers padutils.bindCheckboxChange($('#options-linenoscheck'), () => { pad.changeViewOption('showLineNumbers', padutils.getCheckbox($('#options-linenoscheck'))); @@ -74,8 +76,8 @@ const padeditor = (function () { pad.changeViewOption('rtlIsTrue', padutils.getCheckbox($('#options-rtlcheck'))); }); html10n.bind('localized', () => { - pad.changeViewOption('rtlIsTrue', ('rtl' == html10n.getDirection())); - padutils.setCheckbox($('#options-rtlcheck'), ('rtl' == html10n.getDirection())); + pad.changeViewOption('rtlIsTrue', ('rtl' === html10n.getDirection())); + padutils.setCheckbox($('#options-rtlcheck'), ('rtl' === html10n.getDirection())); }); // font family change @@ -87,9 +89,10 @@ const padeditor = (function () { html10n.bind('localized', () => { $('#languagemenu').val(html10n.getLanguage()); // translate the value of 'unnamed' and 'Enter your name' textboxes in the userlist - // this does not interfere with html10n's normal value-setting because html10n just ingores s - // also, a value which has been set by the user will be not overwritten since a user-edited - // does *not* have the editempty-class + // this does not interfere with html10n's normal value-setting because + // html10n just ingores s + // also, a value which has been set by the user will be not overwritten + // since a user-edited does *not* have the editempty-class $('input[data-l10n-id]').each((key, input) => { input = $(input); if (input.hasClass('editempty')) { @@ -106,17 +109,17 @@ const padeditor = (function () { } }); }, - setViewOptions(newOptions) { - function getOption(key, defaultValue) { + setViewOptions: (newOptions) => { + const getOption = (key, defaultValue) => { const value = String(newOptions[key]); - if (value == 'true') return true; - if (value == 'false') return false; + if (value === 'true') return true; + if (value === 'false') return false; return defaultValue; - } + }; let v; - v = getOption('rtlIsTrue', ('rtl' == html10n.getDirection())); + v = getOption('rtlIsTrue', ('rtl' === html10n.getDirection())); self.ace.setProperty('rtlIsTrue', v); padutils.setCheckbox($('#options-rtlcheck'), v); @@ -137,29 +140,29 @@ const padeditor = (function () { self.ace.setProperty('textface', newOptions.padFontFamily || ''); }, - dispose() { + dispose: () => { if (self.ace) { self.ace.destroy(); self.ace = null; } }, - enable() { + enable: () => { if (self.ace) { self.ace.setEditable(true); } }, - disable() { + disable: () => { if (self.ace) { self.ace.setProperty('grayedOut', true); self.ace.setEditable(false); } }, - restoreRevisionText(dataFromServer) { + restoreRevisionText: (dataFromServer) => { pad.addHistoricalAuthors(dataFromServer.historicalAuthorData); self.ace.importAText(dataFromServer.atext, dataFromServer.apool, true); }, }; return self; -}()); +})(); exports.padeditor = padeditor; diff --git a/src/static/js/pad_impexp.js b/src/static/js/pad_impexp.js index 9c5dd076e..a44c69d8a 100644 --- a/src/static/js/pad_impexp.js +++ b/src/static/js/pad_impexp.js @@ -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. @@ -20,24 +22,27 @@ * limitations under the License. */ -const padimpexp = (function () { +const padimpexp = (() => { // /// import let currentImportTimer = null; - function addImportFrames() { + const addImportFrames = () => { $('#import .importframe').remove(); - const iframe = $(''); + const iframe = $('
          • NameDescriptionVersionLast updateNameDescriptionVersionLast update
            - +

             

            -

            No plugins found.

            -


            Fetching...

            +

            No plugins found.

            +


            Fetching…