From dfe03689107c27d49d9ce9aaf44a07ba0de5627b Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 17 Sep 2020 16:40:29 +0200 Subject: [PATCH 01/18] Localisation updates from https://translatewiki.net. --- src/locales/pt.json | 3 ++- src/locales/sv.json | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/locales/pt.json b/src/locales/pt.json index 3e0345ce2..b770bd15d 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -9,6 +9,7 @@ "Luckas", "Macofe", "Mansil alfalb", + "MuratTheTurkish", "Ti4goc", "Tuliouel", "Waldir", @@ -110,7 +111,7 @@ "timeslider.toolbar.exportlink.title": "Exportar", "timeslider.exportCurrent": "Exportar versão atual como:", "timeslider.version": "Versão {{version}}", - "timeslider.saved": "Gravado a {{day}} de {{month}} de {{ano}}", + "timeslider.saved": "Gravado a {{day}} de {{month}} de {{year}}", "timeslider.playPause": "Reproduzir / pausar conteúdo da nota", "timeslider.backRevision": "Voltar a uma revisão anterior desta nota", "timeslider.forwardRevision": "Avançar para uma revisão posterior desta nota", diff --git a/src/locales/sv.json b/src/locales/sv.json index 07f4460b4..fcef0a10c 100644 --- a/src/locales/sv.json +++ b/src/locales/sv.json @@ -1,6 +1,7 @@ { "@metadata": { "authors": [ + "Bengtsson96", "Jopparn", "Lokal Profil", "Sabelöga", @@ -15,7 +16,7 @@ "pad.toolbar.underline.title": "Understruken (Ctrl+U)", "pad.toolbar.strikethrough.title": "Genomstruken (Ctrl+5)", "pad.toolbar.ol.title": "Numrerad lista (Ctrl+Shift+N)", - "pad.toolbar.ul.title": "Onumrerad lista (Ctrl+Shift+L)", + "pad.toolbar.ul.title": "Punktlista (Ctrl+Shift+L)", "pad.toolbar.indent.title": "Öka indrag (TABB)", "pad.toolbar.unindent.title": "Minska indrag (Shift+TABB)", "pad.toolbar.undo.title": "Ångra (Ctrl+Z)", @@ -29,7 +30,7 @@ "pad.toolbar.showusers.title": "Visa användarna på detta block", "pad.colorpicker.save": "Spara", "pad.colorpicker.cancel": "Avbryt", - "pad.loading": "Läser in...", + "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", @@ -58,7 +59,7 @@ "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.abiword.innerHTML": "Du kan endast importera från oformaterad text eller HTML-format. För mer avancerade importfunktioner, var god installera AbiWord eller LibreOffice.", "pad.modals.connected": "Ansluten.", - "pad.modals.reconnecting": "Återansluter till ditt block...", + "pad.modals.reconnecting": "Återansluter till ditt block …", "pad.modals.forcereconnect": "Tvinga återanslutning", "pad.modals.reconnecttimer": "Försöker ansluta igen", "pad.modals.cancel": "Avbryt", @@ -129,7 +130,7 @@ "pad.userlist.approve": "Godkänn", "pad.editbar.clearcolors": "Rensa författarfärger för hela dokumentet? Detta kan inte ångras", "pad.impexp.importbutton": "Importera nu", - "pad.impexp.importing": "Importerar...", + "pad.impexp.importing": "Importerar …", "pad.impexp.confirmimport": "Att importera en fil kommer att skriva över den aktuella texten i blocket. Är du säker på att du vill fortsätta?", "pad.impexp.convertFailed": "Vi kunde inte importera denna fil. Var god använd ett annat dokumentformat eller kopiera och klistra in den manuellt", "pad.impexp.padHasData": "Vi kunde inte importera denna fil eftersom detta block redan har redigerats. Importera den till ett nytt block.", From 85f52a2f23704c4fcee54d02189faa0a8cda1a6f Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Fri, 18 Sep 2020 17:28:42 +0200 Subject: [PATCH 02/18] tests: Plugin backend tests in ci (#4314) --- src/package.json | 3 +-- tests/frontend/travis/runnerBackend.sh | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/package.json b/src/package.json index dfb350583..6ca046868 100644 --- a/src/package.json +++ b/src/package.json @@ -92,8 +92,7 @@ "url": "https://github.com/ether/etherpad-lite.git" }, "scripts": { - "test": "nyc wtfnode node_modules/.bin/_mocha --timeout 5000 --recursive ../tests/backend/specs", - "test-contentcollector": "nyc mocha --timeout 5000 ../tests/backend/specs", + "test": "nyc wtfnode node_modules/.bin/_mocha --timeout 5000 --recursive ../tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", "test-container": "nyc mocha --timeout 5000 ../tests/container/specs/api" }, "version": "1.8.5", diff --git a/tests/frontend/travis/runnerBackend.sh b/tests/frontend/travis/runnerBackend.sh index 680e96f9e..c595dce02 100755 --- a/tests/frontend/travis/runnerBackend.sh +++ b/tests/frontend/travis/runnerBackend.sh @@ -46,6 +46,5 @@ cd src failed=0 npm run test || failed=1 -npm run test-contentcollector || failed=1 exit $failed From 299bd962b615cf971234ca86edf8b887ddac132d Mon Sep 17 00:00:00 2001 From: Stefan Mueller Date: Fri, 18 Sep 2020 21:14:19 +0200 Subject: [PATCH 03/18] Update version to 1.8.6 and add changelog informations --- CHANGELOG.md | 9 +++++++++ src/package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28bc0d2d8..69d5568bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Develop -- TODO Change to 1.8.x. * ... +# 1.8.6 +* IMPORTANT: This fixes a severe problem with postgresql in 1.8.5 +* SECURITY: Fix authentication bypass vulnerability +* API: Update version to 1.2.15 +* FEATURE: Add copyPadWithoutHistory API (#4295) +* FEATURE: Package more asset files to save http requests (#4286) +* MINOR: Improve UI when reconnecting +* TESTS: Improve tests + # 1.8.5 * IMPORTANT DROP OF SUPPORT: Drop support for IE. Browsers now need async/await. * IMPORTANT SECURITY: Rate limit Commits when env=production diff --git a/src/package.json b/src/package.json index 6ca046868..b01e07a02 100644 --- a/src/package.json +++ b/src/package.json @@ -95,6 +95,6 @@ "test": "nyc wtfnode node_modules/.bin/_mocha --timeout 5000 --recursive ../tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", "test-container": "nyc mocha --timeout 5000 ../tests/container/specs/api" }, - "version": "1.8.5", + "version": "1.8.6", "license": "Apache-2.0" } From 12bd617f511de1da4f4511617ffed6e2804ac9d7 Mon Sep 17 00:00:00 2001 From: Sebastian Castro Date: Sat, 19 Sep 2020 20:09:30 +0200 Subject: [PATCH 04/18] 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 05/18] 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 df7fa1fd41bfb08b73252f0997cd1b9af0c73174 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 20 Sep 2020 15:21:46 -0400 Subject: [PATCH 06/18] changelog: Mention fix for authz bypass vulnerability in 1.8.6 (#4318) --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69d5568bf..093150284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,9 @@ # 1.8.6 * IMPORTANT: This fixes a severe problem with postgresql in 1.8.5 -* SECURITY: Fix authentication bypass vulnerability +* SECURITY: Fix authentication and authorization bypass vulnerabilities * API: Update version to 1.2.15 -* FEATURE: Add copyPadWithoutHistory API (#4295) +* FEATURE: Add copyPadWithoutHistory API (#4295) * FEATURE: Package more asset files to save http requests (#4286) * MINOR: Improve UI when reconnecting * TESTS: Improve tests From 65942691b6b8bdc22891624d69eff8d4933fa97f Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 21 Sep 2020 16:02:42 +0200 Subject: [PATCH 07/18] 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 08/18] 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 09/18] 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 10/18] 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 11/18] 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 12/18] 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 13/18] 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 14/18] 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 15/18] 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 16/18] 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 17/18] 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 18/18] 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); } };