From 0e854a58929d42167841c9a6495be97f83e31b2a Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Mon, 22 Mar 2021 17:23:50 +0100 Subject: [PATCH 001/218] fix wrong changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f60b235f36..d5441ac4804 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ### Notable fixes * Fixed a bug in the safeRun.sh script (#4935) -* Don't create sessions on some static resources (#4921) +* Add more endpoints that do not need authentication/authorization (#4921) * Fixed issue with non-opening device keyboard on smartphones (#4929) * Add version string to iframe_editor.css to prevent stale cache entry (#4964) From 2b98b930d73698edd95f3ed41541df9691f66000 Mon Sep 17 00:00:00 2001 From: John McLear Date: Wed, 24 Mar 2021 16:07:11 +0000 Subject: [PATCH 002/218] scaling: include padId in socketio query string --- src/static/js/pad.js | 7 +++++++ src/static/js/timeslider.js | 2 +- src/tests/backend/specs/socketio.js | 3 ++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 485a4b2fe17..ec156eb46d5 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -218,7 +218,14 @@ const sendClientReady = (isReconnect, messageType) => { }; const handshake = () => { + let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1); + // unescape neccesary due to Safari and Opera interpretation of spaces + padId = decodeURIComponent(padId); + + // padId is used here for sharding / scaling. We prefix the padId with padId: so it's clear + // to the proxy/gateway/whatever that this is a pad connection and should be treated as such socket = pad.socket = socketio.connect(exports.baseURL, '/', { + query: {padId}, reconnectionAttempts: 5, reconnection: true, reconnectionDelay: 1000, diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js index 74aaf4f5d75..47246b4a1b4 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -52,7 +52,7 @@ const init = () => { Cookies.set('token', token, {expires: 60}); } - socket = socketio.connect(exports.baseURL); + socket = socketio.connect(exports.baseURL, '/', {query: {padId}}); // send the ready message once we're connected socket.on('connect', () => { diff --git a/src/tests/backend/specs/socketio.js b/src/tests/backend/specs/socketio.js index fdb578b5532..dbd633add11 100644 --- a/src/tests/backend/specs/socketio.js +++ b/src/tests/backend/specs/socketio.js @@ -52,12 +52,13 @@ const connect = async (res) => { ([name, cookie]) => `${name}=${encodeURIComponent(cookie.value)}`).join('; '); logger.debug('socket.io connecting...'); + const padId = res.req.path.split('/p/')[1]; 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 // express_sid cookie must be passed as a query parameter. - query: {cookie: reqCookieHdr}, + query: {cookie: reqCookieHdr, padId}, }); try { await getSocketEvent(socket, 'connect'); From b80f5bdae8c2de4101557c6fd844b091b2e4c337 Mon Sep 17 00:00:00 2001 From: John McLear Date: Wed, 24 Mar 2021 21:03:48 +0000 Subject: [PATCH 003/218] bugfix/tests/scaling: Socket query test fix (#4974) fix socketio test where res.req is not available. --- src/tests/backend/specs/socketio.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tests/backend/specs/socketio.js b/src/tests/backend/specs/socketio.js index dbd633add11..9899856e0c4 100644 --- a/src/tests/backend/specs/socketio.js +++ b/src/tests/backend/specs/socketio.js @@ -52,7 +52,10 @@ const connect = async (res) => { ([name, cookie]) => `${name}=${encodeURIComponent(cookie.value)}`).join('; '); logger.debug('socket.io connecting...'); - const padId = res.req.path.split('/p/')[1]; + let padId = null; + if (res) { + padId = res.req.path.split('/p/')[1]; + } const socket = io(`${common.baseUrl}/`, { forceNew: true, // Different tests will have different query parameters. path: '/socket.io', From eac5a52690aa440624e47013342eb4a095c00736 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 29 Mar 2021 17:55:09 +0200 Subject: [PATCH 004/218] Localisation updates from https://translatewiki.net. --- src/locales/sk.json | 121 ++++++++++++++++++++++++++++++-------------- 1 file changed, 83 insertions(+), 38 deletions(-) diff --git a/src/locales/sk.json b/src/locales/sk.json index 357af9ff5fa..bc3f45462b9 100644 --- a/src/locales/sk.json +++ b/src/locales/sk.json @@ -5,47 +5,84 @@ "Lexected", "Mark", "Rudko", - "Teslaton" + "Teslaton", + "Yardom78" ] }, - "index.newPad": "Nový Pad", - "index.createOpenPad": "alebo vytvoriť/otvoriť Pad s názvom:", - "pad.toolbar.bold.title": "Tučné (Ctrl-B)", - "pad.toolbar.italic.title": "Kurzíva (Ctrl-I)", - "pad.toolbar.underline.title": "Podčiarknuté (Ctrl-U)", + "admin.page-title": "Ovládací panel správu - Etherpad", + "admin_plugins": "Správca doplnkov", + "admin_plugins.available": "Dostupné doplnky", + "admin_plugins.available_not-found": "Doplnky neboli nájdené.", + "admin_plugins.available_fetching": "Načítavanie...", + "admin_plugins.available_install.value": "Inštalovať", + "admin_plugins.available_search.placeholder": "Vyhľadať doplnky na inštaláciu", + "admin_plugins.description": "Popis", + "admin_plugins.installed": "Nainštalované doplnky", + "admin_plugins.installed_fetching": "Načítavanie nainštalovaných doplnkov...", + "admin_plugins.installed_nothing": "Ešte ste nenainštalovali žiadne doplnky.", + "admin_plugins.installed_uninstall.value": "Odinštalovať", + "admin_plugins.last-update": "Posledná aktualizácia", + "admin_plugins.name": "Názov", + "admin_plugins.page-title": "Správca doplnkov - Etherpad", + "admin_plugins.version": "Verzia", + "admin_plugins_info": "Informácie k riešeniu problémov", + "admin_plugins_info.hooks": "Nainštalované súčasti", + "admin_plugins_info.hooks_client": "Súčasti na strane klienta", + "admin_plugins_info.hooks_server": "Súčasti na strane servera", + "admin_plugins_info.parts": "Nainštalované súčasti", + "admin_plugins_info.plugins": "Nainštalované doplnky", + "admin_plugins_info.page-title": "Informácie o doplnkoch - Etherpad", + "admin_plugins_info.version": "Verzia Etherpadu", + "admin_plugins_info.version_latest": "Posledná dostupná verzia", + "admin_plugins_info.version_number": "Číslo verzie", + "admin_settings": "Nastavenia", + "admin_settings.current": "Aktuálne nastavenia", + "admin_settings.current_example-devel": "Príklad šablóny vývojárskeho nastavenia", + "admin_settings.current_example-prod": "Príklad šablóny výrobného nastavenia", + "admin_settings.current_restart.value": "Reštartovať Ehterpad", + "admin_settings.current_save.value": "Uložiť nastavenia", + "admin_settings.page-title": "Nastavenia - Etherpad", + "index.newPad": "Nový poznámkový blok", + "index.createOpenPad": "alebo vytvoriť/otvoriť poznámkový blok s názvom:", + "index.openPad": "otvoriť poznámkový blok s názvom:", + "pad.toolbar.bold.title": "Tučné (Ctrl+B)", + "pad.toolbar.italic.title": "Kurzíva (Ctrl+I)", + "pad.toolbar.underline.title": "Podčiarknuté (Ctrl+U)", "pad.toolbar.strikethrough.title": "Prečiarknuté (Ctrl+5)", - "pad.toolbar.ol.title": "Usporiadaný zoznam (Ctrl+Shift+N)", + "pad.toolbar.ol.title": "Zoradený zoznam (Ctrl+Shift+N)", "pad.toolbar.ul.title": "Nezoradený zoznam (Ctrl+Shift+L)", - "pad.toolbar.indent.title": "Zväčšiť odsadenie (TAB)", - "pad.toolbar.unindent.title": "Zmenšiť odsadenie (Shift+TAB)", + "pad.toolbar.indent.title": "Zväčšiť okraj (TAB)", + "pad.toolbar.unindent.title": "Zmenšiť okraj (Shift+TAB)", "pad.toolbar.undo.title": "Späť (Ctrl-Z)", - "pad.toolbar.redo.title": "Znova (Ctrl-Y)", - "pad.toolbar.clearAuthorship.title": "Odstrániť farby autorstva (Ctrl+Shift+C)", + "pad.toolbar.redo.title": "Opakovať (Ctrl-Y)", + "pad.toolbar.clearAuthorship.title": "Odstrániť farebné označovanie autorov (Ctrl+Shift+C)", "pad.toolbar.import_export.title": "Import/export z/do rôznych formátov súborov", "pad.toolbar.timeslider.title": "Časová os", "pad.toolbar.savedRevision.title": "Uložiť revíziu", "pad.toolbar.settings.title": "Nastavenia", - "pad.toolbar.embed.title": "Zdieľať alebo vložiť tento Pad", - "pad.toolbar.showusers.title": "Zobraziť používateľov tohoto Padu", + "pad.toolbar.embed.title": "Zdieľať alebo vložiť tento poznámkový blok", + "pad.toolbar.showusers.title": "Zobraziť používateľov tohoto poznámkového bloku", "pad.colorpicker.save": "Uložiť", "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.permissionDenied": "Ľutujeme, nemáte oprávnenie pristupovať k tomuto Padu", - "pad.settings.padSettings": "Nastavenia Padu", + "pad.noCookie": "Cookie nebolo možné nájsť. Povoľte prosím cookies vo vašom prehliadači. Vaše sedenie a nastavenia sa medzi návštevami stránky neuložia. To môže byť spôsobené tým že Etherpad je zahrnutý do iFrame v niektorých prehliadačoch. Prosím uistite sa, že Etherpad sa nachádza na tej istej doméne ako hlavný iFrame", + "pad.permissionDenied": "Ľutujeme, nemáte oprávnenie pristupovať k tomuto poznámkovému bloku", + "pad.settings.padSettings": "Nastavenia poznámkového bloku", "pad.settings.myView": "Vlastný pohľad", - "pad.settings.stickychat": "Chat stále na obrazovke", - "pad.settings.chatandusers": "Zobraziť chat a užívateľov", - "pad.settings.colorcheck": "Farby autorstva", + "pad.settings.stickychat": "Rozhovor stále na obrazovke", + "pad.settings.chatandusers": "Zobraziť rozhovor a používateľov", + "pad.settings.colorcheck": "Farby autorov", "pad.settings.linenocheck": "Čísla riadkov", "pad.settings.rtlcheck": "Čítať obsah sprava doľava?", "pad.settings.fontType": "Typ písma:", "pad.settings.fontType.normal": "Normálne", "pad.settings.language": "Jazyk:", + "pad.settings.about": "O Etherpade", + "pad.settings.poweredBy": "Poháňané cez", "pad.importExport.import_export": "Import/Export", "pad.importExport.import": "Nahrať ľubovoľný textový súbor alebo dokument", "pad.importExport.importSuccessful": "Import úspešný!", - "pad.importExport.export": "Exportovať aktuálny Pad ako:", + "pad.importExport.export": "Exportovať aktuálny poznámkový blok ako:", "pad.importExport.exportetherpad": "Etherpad", "pad.importExport.exporthtml": "HTML", "pad.importExport.exportplain": "Čistý text", @@ -54,12 +91,12 @@ "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.abiword.innerHTML": "Importovať môžete len čistý text alebo HTML. Pre pokročilejšie funkcie importu prosím nainštalujte „AbiWord“.", "pad.modals.connected": "Pripojené.", - "pad.modals.reconnecting": "Opätovné pripájanie k vášmu Padu...", + "pad.modals.reconnecting": "Opätovné pripájanie k vášmu poznámkovému bloku...", "pad.modals.forcereconnect": "Vynútiť znovupripojenie", "pad.modals.reconnecttimer": "Skúšam sa pripojiť", "pad.modals.cancel": "Zrušiť", "pad.modals.userdup": "Otvorené v inom okne", - "pad.modals.userdup.explanation": "Zdá sa, že tento Pad je na tomto počítači otvorený vo viacerých oknách prehliadača.", + "pad.modals.userdup.explanation": "Zdá sa, že tento poznámkový blok je na tomto počítači otvorený vo viacerých oknách prehliadača.", "pad.modals.userdup.advice": "Pre použitie tohoto okna se musíte znovu pripojiť.", "pad.modals.unauth": "Nie ste autorizovaný", "pad.modals.unauth.explanation": "Vaše oprávnenia sa počas prehliadania tejto stránky zmenili. Skúste sa pripojiť znovu.", @@ -70,33 +107,40 @@ "pad.modals.initsocketfail.cause": "Príčinou je pravdepodobne problém s prehliadačom alebo internetovým pripojením.", "pad.modals.slowcommit.explanation": "Server neodpovedá.", "pad.modals.slowcommit.cause": "Príčinou môže byť problém so sieťovým pripojením.", - "pad.modals.badChangeset.explanation": "Editácia, kterú ste vykonali byla synchronizáciou serveru vyhodnotená ako nepovolená.", + "pad.modals.badChangeset.explanation": "Úprava, ktorú ste vykonali bola synchronizáciou serveru vyhodnotená ako nepovolená.", "pad.modals.badChangeset.cause": "To môže byť z dôvodu nesprávnej konfigurácie servera alebo iného neočakávaného správania. Ak máte pocit že došlo k chybe, kontaktuje prosím správcu služby. Pokúste sa pripojiť znova a pokračovať v úpravách.", - "pad.modals.corruptPad.explanation": "Pad ku ktorému sa snažíte získať prístup je poškodený.", + "pad.modals.corruptPad.explanation": "Poznámkový blok ku ktorému sa snažíte získať prístup je poškodený.", "pad.modals.corruptPad.cause": "To môže byť z dôvodu nesprávnej konfigurácie servera alebo iného neočakávaného správania. Prosím, obráťte sa na správcu služby.", "pad.modals.deleted": "Odstránené.", - "pad.modals.deleted.explanation": "Tento Pad bol odstránený.", + "pad.modals.deleted.explanation": "Tento poznámkový blok bol odstránený.", + "pad.modals.rateLimited": "Rýchlosť obmedzená.", + "pad.modals.rateLimited.explanation": "Do tohto poznámkového bloku ste poslali príliš veľa správ a preto ste boli odpojení.", + "pad.modals.rejected.explanation": "Server odmietol správu poslanú Vašim prehliadačom.", + "pad.modals.rejected.cause": "Počas prehliadania poznámkové bloku mohlo dôjsť k aktualizácii servera alebo je niekde v Etherpade chyba. Skúste stránku načítať znovu.", "pad.modals.disconnected": "Boli ste odpojení.", "pad.modals.disconnected.explanation": "Spojenie so serverom sa prerušilo", "pad.modals.disconnected.cause": "Server môže byť nedostupný. Ak by problém pretrvával, informujte správcu služby.", - "pad.share": "Zdieľať tento Pad", + "pad.share": "Zdieľať tento poznámkový blok", "pad.share.readonly": "Len na čítanie", "pad.share.link": "Odkaz", "pad.share.emebdcode": "Vložiť URL", - "pad.chat": "Chat", - "pad.chat.title": "Otvoriť chat tohoto Padu.", + "pad.chat": "Rozhovor", + "pad.chat.title": "Otvoriť rozhovor tohoto poznámkového bloku.", "pad.chat.loadmessages": "Načítať ďalšie správy", + "pad.chat.stick.title": "Prilepiť rozhovor na obrazovku", + "pad.chat.writeMessage.placeholder": "Sem napíšte svoju správu", + "timeslider.followContents": "Sledovať aktualizácie obsahu poznámkového bloku", "timeslider.pageTitle": "Časová os {{appTitle}}", - "timeslider.toolbar.returnbutton": "Návrat do Padu", + "timeslider.toolbar.returnbutton": "Späť do poznámkového bloku", "timeslider.toolbar.authors": "Autori:", "timeslider.toolbar.authorsList": "Bez autorov", "timeslider.toolbar.exportlink.title": "Export", "timeslider.exportCurrent": "Exportovať aktuálnu verziu ako:", "timeslider.version": "Verzia {{version}}", "timeslider.saved": "Uložené {{day}}. {{month}} {{year}}", - "timeslider.playPause": "Pustiť / Pozastaviť obsah padu", - "timeslider.backRevision": "Ísť v tomto pade a revíziu späť", - "timeslider.forwardRevision": "Ísť v tomto pade o revíziu vpred", + "timeslider.playPause": "Pustiť / Pozastaviť obsah poznámkového bloku", + "timeslider.backRevision": "Ísť v tomto poznámkovom bloku o jednu revíziu späť", + "timeslider.forwardRevision": "Ísť v tomto poznámkovom bloku o jednu revíziu vpred", "timeslider.dateformat": "{{day}}. {{month}} {{year}} {{hours}}:{{minutes}}:{{seconds}}", "timeslider.month.january": "januára", "timeslider.month.february": "februára", @@ -110,19 +154,20 @@ "timeslider.month.october": "októbra", "timeslider.month.november": "novembra", "timeslider.month.december": "decembra", - "timeslider.unnamedauthors": "{{num}} {[ plural(num) one: nemenovaný autor, few: nemenovaní autori, other: nemenovaných autorov ]}", + "timeslider.unnamedauthors": "{[plural(num) one:Počet nemenovaných autorov:, other: Počet nemenovaných autorov:]} {{num}}", "pad.savedrevs.marked": "Táto revízia bola označená ako uložená", "pad.savedrevs.timeslider": "Návštevou časovej osi môžete zobraziť uložené revízie", "pad.userlist.entername": "Zadajte svoje meno", "pad.userlist.unnamed": "nemenovaný", - "pad.editbar.clearcolors": "Skutočne odstrániť autorské farby z celého dokumentu?", - "pad.impexp.importbutton": "Importovať", + "pad.editbar.clearcolors": "Odstrániť farby autorov z celého dokumentu? Táto akcia sa nedá vrátiť", + "pad.impexp.importbutton": "Importovať teraz", "pad.impexp.importing": "Prebieha import...", - "pad.impexp.confirmimport": "Import súboru prepíše celý súčasný obsah Padu. Skutočne si želáte vykonať túto akciu?", + "pad.impexp.confirmimport": "Import súboru prepíše celý súčasný obsah poznámkového bloku. Skutočne si želáte vykonať túto akciu?", "pad.impexp.convertFailed": "Tento súbor nie je možné importovať. Použite prosím iný formát súboru alebo nakopírujte text manuálne", - "pad.impexp.padHasData": "Nebolo možné importovať tento súbor, pretože tento pad už bol pozmenený. Importujte prosím súbor do nového padu", + "pad.impexp.padHasData": "Nebolo možné importovať tento súbor, pretože tento poznámkový blok už bol pozmenený. Importujte prosím súbor do nového poznámkového bloku", "pad.impexp.uploadFailed": "Nahrávanie zlyhalo, skúste to prosím znovu", "pad.impexp.importfailed": "Import zlyhal", "pad.impexp.copypaste": "Vložte prosím kópiu cez schránku", - "pad.impexp.exportdisabled": "Export do formátu {{type}} nie je povolený. Kontaktujte prosím administrátora pre zistenie detailov." + "pad.impexp.exportdisabled": "Export do formátu {{type}} nie je povolený. Kontaktujte prosím administrátora pre zistenie detailov.", + "pad.impexp.maxFileSize": "Súbor je príliš veľký. Kontaktujte správcu pre zväčšenie povolenej veľkosti súborov pre import" } From 27b35699ead1c15a90c5f65bec22f9c0e2182ef1 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 25 Mar 2021 15:47:26 -0400 Subject: [PATCH 005/218] tests: Fix `helper.newPad()` retries * Pass retry count in options object so that each pad has its own retry count. * Delete useless `origPadName` variable. --- src/tests/frontend/helper.js | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/tests/frontend/helper.js b/src/tests/frontend/helper.js index 4b18760868e..28ec9aaf62b 100644 --- a/src/tests/frontend/helper.js +++ b/src/tests/frontend/helper.js @@ -89,19 +89,11 @@ const helper = {}; } helper.evtType = evtType; - // @todo needs fixing asap - // newPad occasionally timeouts, might be a problem with ready/onload code during page setup - // This ensures that tests run regardless of this problem - helper.retry = 0; - - helper.newPad = (cb, padName) => { - // build opts object - let opts = {clearCookies: true}; - if (typeof cb === 'function') { - opts.cb = cb; - } else { - opts = _.defaults(cb, opts); - } + helper.newPad = (opts, padName) => { + opts = Object.assign({ + _retry: 0, + clearCookies: true, + }, typeof opts === 'function' ? {cb: opts} : opts); // if opts.params is set we manipulate the URL to include URL parameters IE ?foo=Bah. let encodedParams; @@ -120,8 +112,6 @@ const helper = {}; if (!padName) padName = `FRONTEND_TEST_${helper.randomString(20)}`; $iframe = $(``); - // needed for retry - const origPadName = padName; // clean up inner iframe references helper.padChrome$ = helper.padOuter$ = helper.padInner$ = null; @@ -170,11 +160,8 @@ const helper = {}; helper.spyOnSocketIO(); opts.cb(); }).fail(() => { - if (helper.retry > 3) { - throw new Error('Pad never loaded'); - } - helper.retry++; - helper.newPad(cb, origPadName); + if (opts._retry++ >= 4) throw new Error('Pad never loaded'); + helper.newPad(opts, padName); }); }); From ec76a6548fbe5eeae55428f6f60b502704f20fe7 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 25 Mar 2021 16:08:50 -0400 Subject: [PATCH 006/218] tests: Make the `helper.newPad()` callback optional --- src/tests/frontend/helper.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/frontend/helper.js b/src/tests/frontend/helper.js index 28ec9aaf62b..12693200d14 100644 --- a/src/tests/frontend/helper.js +++ b/src/tests/frontend/helper.js @@ -92,6 +92,7 @@ const helper = {}; helper.newPad = (opts, padName) => { opts = Object.assign({ _retry: 0, + cb: (err) => { if (err != null) throw err; }, clearCookies: true, }, typeof opts === 'function' ? {cb: opts} : opts); From 718da6fc1b0d0e163ec8fc0e1317f345da89145a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 25 Mar 2021 15:20:15 -0400 Subject: [PATCH 007/218] tests: New `helper.aNewPad()` (promisified `newPad()`) --- src/tests/frontend/helper.js | 108 +++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 50 deletions(-) diff --git a/src/tests/frontend/helper.js b/src/tests/frontend/helper.js index 12693200d14..2ce623f823c 100644 --- a/src/tests/frontend/helper.js +++ b/src/tests/frontend/helper.js @@ -89,12 +89,22 @@ const helper = {}; } helper.evtType = evtType; - helper.newPad = (opts, padName) => { + // Deprecated; use helper.aNewPad() instead. + helper.newPad = (opts, id) => { + if (!id) id = `FRONTEND_TEST_${helper.randomString(20)}`; + opts = Object.assign({id}, typeof opts === 'function' ? {cb: opts} : opts); + const {cb = (err) => { if (err != null) throw err; }} = opts; + delete opts.cb; + helper.aNewPad(opts).then((id) => cb(null, id), (err) => cb(err || new Error(err))); + return id; + }; + + helper.aNewPad = async (opts = {}) => { opts = Object.assign({ _retry: 0, - cb: (err) => { if (err != null) throw err; }, clearCookies: true, - }, typeof opts === 'function' ? {cb: opts} : opts); + id: `FRONTEND_TEST_${helper.randomString(20)}`, + }, opts); // if opts.params is set we manipulate the URL to include URL parameters IE ?foo=Bah. let encodedParams; @@ -111,8 +121,7 @@ const helper = {}; helper.clearSessionCookies(); } - if (!padName) padName = `FRONTEND_TEST_${helper.randomString(20)}`; - $iframe = $(``); + $iframe = $(``); // clean up inner iframe references helper.padChrome$ = helper.padOuter$ = helper.padInner$ = null; @@ -121,52 +130,51 @@ const helper = {}; $('#iframe-container iframe').remove(); // set new iframe $('#iframe-container').append($iframe); - $iframe.one('load', () => { - helper.padChrome$ = getFrameJQuery($('#iframe-container iframe')); - if (opts.clearCookies) { - helper.clearPadPrefCookie(); - } - if (opts.padPrefs) { - helper.setPadPrefCookie(opts.padPrefs); - } - helper.waitFor(() => !$iframe.contents().find('#editorloadingbox') - .is(':visible'), 10000).done(() => { - helper.padOuter$ = getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]')); - helper.padInner$ = getFrameJQuery(helper.padOuter$('iframe[name="ace_inner"]')); - - // disable all animations, this makes tests faster and easier - helper.padChrome$.fx.off = true; - helper.padOuter$.fx.off = true; - helper.padInner$.fx.off = true; - - /* - * chat messages received - * @type {Array} - */ - helper.chatMessages = []; - - /* - * changeset commits from the server - * @type {Array} - */ - helper.commits = []; - - /* - * userInfo messages from the server - * @type {Array} - */ - helper.userInfos = []; - - // listen for server messages - helper.spyOnSocketIO(); - opts.cb(); - }).fail(() => { - if (opts._retry++ >= 4) throw new Error('Pad never loaded'); - helper.newPad(opts, padName); - }); - }); + await new Promise((resolve) => $iframe.one('load', resolve)); + helper.padChrome$ = getFrameJQuery($('#iframe-container iframe')); + if (opts.clearCookies) { + helper.clearPadPrefCookie(); + } + if (opts.padPrefs) { + helper.setPadPrefCookie(opts.padPrefs); + } + try { + await helper.waitForPromise( + () => !$iframe.contents().find('#editorloadingbox').is(':visible'), 10000); + } catch (err) { + if (opts._retry++ >= 4) throw new Error('Pad never loaded'); + return await helper.aNewPad(opts); + } + helper.padOuter$ = getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]')); + helper.padInner$ = getFrameJQuery(helper.padOuter$('iframe[name="ace_inner"]')); + + // disable all animations, this makes tests faster and easier + helper.padChrome$.fx.off = true; + helper.padOuter$.fx.off = true; + helper.padInner$.fx.off = true; + + /* + * chat messages received + * @type {Array} + */ + helper.chatMessages = []; + + /* + * changeset commits from the server + * @type {Array} + */ + helper.commits = []; + + /* + * userInfo messages from the server + * @type {Array} + */ + helper.userInfos = []; + + // listen for server messages + helper.spyOnSocketIO(); - return padName; + return opts.id; }; helper.newAdmin = async (page) => { From b9753dcc7156d8471a5aa5b6c9b85af47f630aa8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 22 Mar 2021 03:23:29 -0400 Subject: [PATCH 008/218] Changeset: Return a new op object by default when iterating Reusing the same op object for each iteration can result in very weird behaviors because previously yielded op objects will get a surprise mutation. It is unclear why the code was written to reuse the same object. There was no comment, nor is there a commit message providing rationale (it has behaved this way since the very first commit). Perhaps the objects were reused to improve performance (fewer object allocations that need to be garbage collected). I do expect this change to reduce performance somewhat, but not enough to warrant reverting this commit. --- src/static/js/Changeset.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 8c3627b0486..6c236129bd7 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -126,10 +126,9 @@ exports.opIterator = (opsStr, optStartIndex) => { return result; }; let regexResult = nextRegexMatch(); - const obj = exports.newOp(); - const next = (optObj) => { - const op = (optObj || obj); + const next = (optOp) => { + const op = optOp || exports.newOp(); if (regexResult[0]) { op.attribs = regexResult[1]; op.lines = exports.parseNum(regexResult[2] || 0); From dd09a3f12b7db19ef07c04d89ad8b09ce2bb4040 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Tue, 30 Mar 2021 05:03:11 +0000 Subject: [PATCH 009/218] fix: src/package.json & src/package-lock.json to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-UNDERSCORE-1080984 --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 409d3a332d1..9fd24fb98b4 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -8464,9 +8464,9 @@ } }, "underscore": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.0.tgz", - "integrity": "sha512-21rQzss/XPMjolTiIezSu3JAjgagXKROtNrYFEOWK109qY1Uv2tVjPTZ1ci2HgvQDA16gHYSthQIJfB+XId/rQ==" + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" }, "unified": { "version": "9.2.0", diff --git a/src/package.json b/src/package.json index 853065a2297..c207ec5add5 100644 --- a/src/package.json +++ b/src/package.json @@ -70,7 +70,7 @@ "tiny-worker": "^2.3.0", "tinycon": "0.6.8", "ueberdb2": "^1.4.4", - "underscore": "1.12.0", + "underscore": "1.12.1", "unorm": "1.6.0", "wtfnode": "^0.8.4" }, From 81b9a2544d6e743f47f2e248191bbc7caa290014 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 26 Mar 2021 19:33:14 -0400 Subject: [PATCH 010/218] collab_client: Factor out duplicate `ACCEPT_COMMIT` code --- src/static/js/collab_client.js | 42 ++++++++++++---------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.js index a4cbafd9220..23b9cb49eb1 100644 --- a/src/static/js/collab_client.js +++ b/src/static/js/collab_client.js @@ -119,15 +119,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) const newRev = msg.newRev; rev = newRev; if (msg.type === 'ACCEPT_COMMIT') { - editor.applyPreparedChangesetToBase(); - setStateIdle(); - callCatchingErrors('onInternalAction', () => { - callbacks.onInternalAction('commitAcceptedByServer'); - }); - callCatchingErrors('onConnectionTrouble', () => { - callbacks.onConnectionTrouble('OK'); - }); - handleUserChanges(); + acceptCommit(); } else if (msg.type === 'NEW_CHANGES') { const changeset = msg.changeset; const author = (msg.author || ''); @@ -170,6 +162,18 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) } }; + const acceptCommit = () => { + editor.applyPreparedChangesetToBase(); + setStateIdle(); + callCatchingErrors('onInternalAction', () => { + callbacks.onInternalAction('commitAcceptedByServer'); + }); + callCatchingErrors('onConnectionTrouble', () => { + callbacks.onConnectionTrouble('OK'); + }); + handleUserChanges(); + }; + const setUpSocket = () => { setChannelState('CONNECTED'); doDeferredActions(); @@ -257,15 +261,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) return; } rev = newRev; - editor.applyPreparedChangesetToBase(); - setStateIdle(); - callCatchingErrors('onInternalAction', () => { - callbacks.onInternalAction('commitAcceptedByServer'); - }); - callCatchingErrors('onConnectionTrouble', () => { - callbacks.onConnectionTrouble('OK'); - }); - handleUserChanges(); + acceptCommit(); } else if (msg.type === 'CLIENT_RECONNECT') { // Server sends a CLIENT_RECONNECT message when there is a client reconnect. // Server also returns all pending revisions along with this CLIENT_RECONNECT message @@ -301,15 +297,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) rev = newRev; if (author === pad.getUserId()) { - editor.applyPreparedChangesetToBase(); - setStateIdle(); - callCatchingErrors('onInternalAction', () => { - callbacks.onInternalAction('commitAcceptedByServer'); - }); - callCatchingErrors('onConnectionTrouble', () => { - callbacks.onConnectionTrouble('OK'); - }); - handleUserChanges(); + acceptCommit(); } else { editor.applyChangesToBase(changeset, author, apool); } From 3ee6b5eb2b072d3c1b06723db1344570a20c5b74 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 26 Mar 2021 19:46:46 -0400 Subject: [PATCH 011/218] collab_client: Delete unused `caughtErrors` --- src/static/js/collab_client.js | 43 +++++++--------------------------- 1 file changed, 8 insertions(+), 35 deletions(-) diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.js index 23b9cb49eb1..71e20397695 100644 --- a/src/static/js/collab_client.js +++ b/src/static/js/collab_client.js @@ -50,9 +50,6 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) const userSet = {}; // userId -> userInfo userSet[userId] = initialUserInfo; - const caughtErrors = []; - const caughtErrorCatchers = []; - const caughtErrorTimes = []; const msgQueue = []; let isPendingRevision = false; @@ -84,7 +81,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) setChannelState('DISCONNECTED', 'initsocketfail'); } else { // check again in a bit - setTimeout(wrapRecordingErrors('setTimeout(handleUserChanges)', handleUserChanges), 1000); + setTimeout(handleUserChanges, 1000); } return; } @@ -99,16 +96,14 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) callbacks.onConnectionTrouble('SLOW'); } else { // run again in a few seconds, to detect a disconnect - setTimeout(wrapRecordingErrors('setTimeout(handleUserChanges)', handleUserChanges), 3000); + setTimeout(handleUserChanges, 3000); } return; } const earliestCommit = lastCommitTime + 500; if (t < earliestCommit) { - setTimeout( - wrapRecordingErrors('setTimeout(handleUserChanges)', handleUserChanges), - earliestCommit - t); + setTimeout(handleUserChanges, earliestCommit - t); return; } @@ -153,24 +148,22 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) } } else { // run again in a few seconds, to check if there was a reconnection attempt - setTimeout(wrapRecordingErrors('setTimeout(handleUserChanges)', handleUserChanges), 3000); + setTimeout(handleUserChanges, 3000); } if (sentMessage) { // run again in a few seconds, to detect a disconnect - setTimeout(wrapRecordingErrors('setTimeout(handleUserChanges)', handleUserChanges), 3000); + setTimeout(handleUserChanges, 3000); } }; const acceptCommit = () => { editor.applyPreparedChangesetToBase(); setStateIdle(); - callCatchingErrors('onInternalAction', () => { + try { callbacks.onInternalAction('commitAcceptedByServer'); - }); - callCatchingErrors('onConnectionTrouble', () => { callbacks.onConnectionTrouble('OK'); - }); + } catch (err) { /* intentionally ignored */ } handleUserChanges(); }; @@ -190,25 +183,6 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) }); }; - const wrapRecordingErrors = (catcher, func) => function (...args) { - try { - return func.call(this, ...args); - } catch (e) { - caughtErrors.push(e); - caughtErrorCatchers.push(catcher); - caughtErrorTimes.push(+new Date()); - // console.dir({catcher: catcher, e: e}); - throw e; - } - }; - - const callCatchingErrors = (catcher, func) => { - try { - wrapRecordingErrors(catcher, func)(); - } catch (e) { /* absorb*/ - } - }; - const handleMessageFromServer = (evt) => { if (!getSocket()) return; if (!evt.data) return; @@ -566,8 +540,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) editor.setProperty('userAuthor', userId); editor.setBaseAttributedText(serverVars.initialAttributedText, serverVars.apool); - editor.setUserChangeNotificationCallback( - wrapRecordingErrors('handleUserChanges', handleUserChanges)); + editor.setUserChangeNotificationCallback(handleUserChanges); setUpSocket(); return self; From 5c445eac21e2732bc507489ce51c5038536d5166 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 26 Mar 2021 20:02:22 -0400 Subject: [PATCH 012/218] collab_client: Convert `state` var to `committing` bool --- src/static/js/collab_client.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.js index 71e20397695..95c51f6dde0 100644 --- a/src/static/js/collab_client.js +++ b/src/static/js/collab_client.js @@ -39,7 +39,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) pad = _pad; // Inject pad to avoid a circular dependency. let rev = serverVars.rev; - let state = 'IDLE'; + let committing = false; let stateMessage; let channelState = 'CONNECTING'; let lastCommitTime = 0; @@ -88,11 +88,11 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) const t = (+new Date()); - if (state !== 'IDLE') { - if (state === 'COMMITTING' && msgQueue.length === 0 && (t - lastCommitTime) > 20000) { + if (committing) { + if (msgQueue.length === 0 && (t - lastCommitTime) > 20000) { // a commit is taking too long setChannelState('DISCONNECTED', 'slowcommit'); - } else if (state === 'COMMITTING' && msgQueue.length === 0 && (t - lastCommitTime) > 5000) { + } else if (msgQueue.length === 0 && (t - lastCommitTime) > 5000) { callbacks.onConnectionTrouble('SLOW'); } else { // run again in a few seconds, to detect a disconnect @@ -135,7 +135,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) const userChangesData = editor.prepareUserChangeset(); if (userChangesData.changeset) { lastCommitTime = t; - state = 'COMMITTING'; + committing = true; stateMessage = { type: 'USER_CHANGES', baseRev: rev, @@ -281,7 +281,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) setIsPendingRevision(false); } } else if (msg.type === 'NO_COMMIT_PENDING') { - if (state === 'COMMITTING') { + if (committing) { // server missed our commit message; abort that commit setStateIdle(); handleUserChanges(); @@ -458,7 +458,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) const obj = {}; obj.userInfo = userSet[userId]; obj.baseRev = rev; - if (state === 'COMMITTING' && stateMessage) { + if (committing && stateMessage) { obj.committedChangeset = stateMessage.changeset; obj.committedChangesetAPool = stateMessage.apool; editor.applyPreparedChangesetToBase(); @@ -472,7 +472,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) }; const setStateIdle = () => { - state = 'IDLE'; + committing = false; callbacks.onInternalAction('newlyIdle'); schedulePerhapsCallIdleFuncs(); }; @@ -490,7 +490,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) const schedulePerhapsCallIdleFuncs = () => { setTimeout(() => { - if (state === 'IDLE') { + if (!committing) { while (idleFuncs.length > 0) { const f = idleFuncs.shift(); f(); From e99fe885372ee65d0ced3bd80a74f467411b817e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 26 Mar 2021 20:20:54 -0400 Subject: [PATCH 013/218] collab_client: Use `Date.now()` instead of casting a Date object Also rename the `t` variable to `now` to improve readability. --- src/static/js/collab_client.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.js index 95c51f6dde0..680c293b908 100644 --- a/src/static/js/collab_client.js +++ b/src/static/js/collab_client.js @@ -76,8 +76,9 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) const handleUserChanges = () => { if (editor.getInInternationalComposition()) return; + const now = Date.now(); if ((!getSocket()) || channelState === 'CONNECTING') { - if (channelState === 'CONNECTING' && (((+new Date()) - initialStartConnectTime) > 20000)) { + if (channelState === 'CONNECTING' && (now - initialStartConnectTime) > 20000) { setChannelState('DISCONNECTED', 'initsocketfail'); } else { // check again in a bit @@ -86,13 +87,11 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) return; } - const t = (+new Date()); - if (committing) { - if (msgQueue.length === 0 && (t - lastCommitTime) > 20000) { + if (msgQueue.length === 0 && (now - lastCommitTime) > 20000) { // a commit is taking too long setChannelState('DISCONNECTED', 'slowcommit'); - } else if (msgQueue.length === 0 && (t - lastCommitTime) > 5000) { + } else if (msgQueue.length === 0 && (now - lastCommitTime) > 5000) { callbacks.onConnectionTrouble('SLOW'); } else { // run again in a few seconds, to detect a disconnect @@ -102,8 +101,8 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) } const earliestCommit = lastCommitTime + 500; - if (t < earliestCommit) { - setTimeout(handleUserChanges, earliestCommit - t); + if (now < earliestCommit) { + setTimeout(handleUserChanges, earliestCommit - now); return; } @@ -134,7 +133,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) if (!isPendingRevision) { const userChangesData = editor.prepareUserChangeset(); if (userChangesData.changeset) { - lastCommitTime = t; + lastCommitTime = now; committing = true; stateMessage = { type: 'USER_CHANGES', @@ -171,7 +170,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) setChannelState('CONNECTED'); doDeferredActions(); - initialStartConnectTime = +new Date(); + initialStartConnectTime = Date.now(); }; const sendMessage = (msg) => { From 63a1f078f4cbc36ed0c1b808e04ea714b7420d27 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 29 Mar 2021 02:50:32 -0400 Subject: [PATCH 014/218] collab_client: Redo server message queueing Move server message queue processing out of `handleUserChanges()` for the following reasons: * Fix a race condition: Before this change the client would stop processing incoming messages and stop sending changes to the server if a `NEW_CHANGES` message arrived while the user was composing a character and waiting for an `ACCEPT_COMMIT` message. * Improve readability: The `handleUserChanges()` function is for handling changes from the local user, not for handling changes from other users. * Simplify the code. --- src/static/js/ace.js | 2 +- src/static/js/ace2_inner.js | 22 ++--- src/static/js/collab_client.js | 166 +++++++++++++-------------------- 3 files changed, 75 insertions(+), 115 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 059bac76e92..0772648af4a 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -141,7 +141,7 @@ const Ace2Editor = function () { this.getDebugProperty = (prop) => info.ace_getDebugProperty(prop); this.getInInternationalComposition = - () => loaded ? info.ace_getInInternationalComposition() : false; + () => loaded ? info.ace_getInInternationalComposition() : null; // prepareUserChangeset: // Returns null if no new changes or ACE not ready. Otherwise, bundles up all user changes diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index d577731961e..a8cc3cb984c 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -3504,16 +3504,7 @@ function Ace2Inner(editorInfo, cssManagers) { const teardown = () => _teardownActions.forEach((a) => a()); - let inInternationalComposition = false; - const handleCompositionEvent = (evt) => { - // international input events, fired in FF3, at least; allow e.g. Japanese input - if (evt.type === 'compositionstart') { - inInternationalComposition = true; - } else if (evt.type === 'compositionend') { - inInternationalComposition = false; - } - }; - + let inInternationalComposition = null; editorInfo.ace_getInInternationalComposition = () => inInternationalComposition; const bindTheEventHandlers = () => { @@ -3602,8 +3593,15 @@ function Ace2Inner(editorInfo, cssManagers) { }); }); - $(document.documentElement).on('compositionstart', handleCompositionEvent); - $(document.documentElement).on('compositionend', handleCompositionEvent); + $(document.documentElement).on('compositionstart', () => { + if (inInternationalComposition) return; + inInternationalComposition = new Promise((resolve) => { + $(document.documentElement).one('compositionend', () => { + inInternationalComposition = null; + resolve(); + }); + }); + }); }; const topLevel = (n) => { diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.js index 680c293b908..a9c289ee3e5 100644 --- a/src/static/js/collab_client.js +++ b/src/static/js/collab_client.js @@ -50,8 +50,6 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) const userSet = {}; // userId -> userInfo userSet[userId] = initialUserInfo; - const msgQueue = []; - let isPendingRevision = false; const callbacks = { @@ -75,7 +73,11 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) } const handleUserChanges = () => { - if (editor.getInInternationalComposition()) return; + if (editor.getInInternationalComposition()) { + // handleUserChanges() will be called again once composition ends so there's no need to set up + // a future call before returning. + return; + } const now = Date.now(); if ((!getSocket()) || channelState === 'CONNECTING') { if (channelState === 'CONNECTING' && (now - initialStartConnectTime) > 20000) { @@ -88,10 +90,10 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) } if (committing) { - if (msgQueue.length === 0 && (now - lastCommitTime) > 20000) { + if (now - lastCommitTime > 20000) { // a commit is taking too long setChannelState('DISCONNECTED', 'slowcommit'); - } else if (msgQueue.length === 0 && (now - lastCommitTime) > 5000) { + } else if (now - lastCommitTime > 5000) { callbacks.onConnectionTrouble('SLOW'); } else { // run again in a few seconds, to detect a disconnect @@ -106,27 +108,6 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) return; } - // apply msgQueue changeset. - if (msgQueue.length !== 0) { - let msg; - while ((msg = msgQueue.shift())) { - const newRev = msg.newRev; - rev = newRev; - if (msg.type === 'ACCEPT_COMMIT') { - acceptCommit(); - } else if (msg.type === 'NEW_CHANGES') { - const changeset = msg.changeset; - const author = (msg.author || ''); - const apool = msg.apool; - - editor.applyChangesToBase(changeset, author, apool); - } - } - if (isPendingRevision) { - setIsPendingRevision(false); - } - } - let sentMessage = false; // Check if there are any pending revisions to be received from server. // Allow only if there are no pending revisions to be received from server @@ -182,6 +163,21 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) }); }; + const serverMessageTaskQueue = new class { + constructor() { + this._promiseChain = Promise.resolve(); + } + + async enqueue(fn) { + const taskPromise = this._promiseChain.then(fn); + // Use .catch() to prevent rejections from halting the queue. + this._promiseChain = taskPromise.catch(() => {}); + // Do NOT do `return await this._promiseChain;` because the caller would not see an error if + // fn() throws/rejects (due to the .catch() added above). + return await taskPromise; + } + }(); + const handleMessageFromServer = (evt) => { if (!getSocket()) return; if (!evt.data) return; @@ -190,95 +186,61 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) const msg = wrapper.data; if (msg.type === 'NEW_CHANGES') { - const newRev = msg.newRev; - const changeset = msg.changeset; - const author = (msg.author || ''); - const apool = msg.apool; - - // When inInternationalComposition, msg pushed msgQueue. - if (msgQueue.length > 0 || editor.getInInternationalComposition()) { - const oldRev = msgQueue.length > 0 ? msgQueue[msgQueue.length - 1].newRev : rev; - if (newRev !== (oldRev + 1)) { - window.console.warn(`bad message revision on NEW_CHANGES: ${newRev} not ${oldRev + 1}`); + serverMessageTaskQueue.enqueue(async () => { + // Avoid updating the DOM while the user is composing a character. Notes about this `await`: + // * `await null;` is equivalent to `await Promise.resolve(null);`, so if the user is not + // currently composing a character then execution will continue without error. + // * We assume that it is not possible for a new 'compositionstart' event to fire after + // the `await` but before the next line of code after the `await` (or, if it is + // possible, that the chances are so small or the consequences so minor that it's not + // worth addressing). + await editor.getInInternationalComposition(); + const {newRev, changeset, author = '', apool} = msg; + if (newRev !== (rev + 1)) { + window.console.warn(`bad message revision on NEW_CHANGES: ${newRev} not ${rev + 1}`); // setChannelState("DISCONNECTED", "badmessage_newchanges"); return; } - msgQueue.push(msg); - return; - } - - if (newRev !== (rev + 1)) { - window.console.warn(`bad message revision on NEW_CHANGES: ${newRev} not ${rev + 1}`); - // setChannelState("DISCONNECTED", "badmessage_newchanges"); - return; - } - rev = newRev; - - editor.applyChangesToBase(changeset, author, apool); + rev = newRev; + editor.applyChangesToBase(changeset, author, apool); + }); } else if (msg.type === 'ACCEPT_COMMIT') { - const newRev = msg.newRev; - if (msgQueue.length > 0) { - if (newRev !== (msgQueue[msgQueue.length - 1].newRev + 1)) { - window.console.warn('bad message revision on ACCEPT_COMMIT: ' + - `${newRev} not ${msgQueue[msgQueue.length - 1][0] + 1}`); + serverMessageTaskQueue.enqueue(() => { + const newRev = msg.newRev; + if (newRev !== (rev + 1)) { + window.console.warn(`bad message revision on ACCEPT_COMMIT: ${newRev} not ${rev + 1}`); // setChannelState("DISCONNECTED", "badmessage_acceptcommit"); return; } - msgQueue.push(msg); - return; - } - - if (newRev !== (rev + 1)) { - window.console.warn(`bad message revision on ACCEPT_COMMIT: ${newRev} not ${rev + 1}`); - // setChannelState("DISCONNECTED", "badmessage_acceptcommit"); - return; - } - rev = newRev; - acceptCommit(); + rev = newRev; + acceptCommit(); + }); } else if (msg.type === 'CLIENT_RECONNECT') { // Server sends a CLIENT_RECONNECT message when there is a client reconnect. // Server also returns all pending revisions along with this CLIENT_RECONNECT message - if (msg.noChanges) { - // If no revisions are pending, just make everything normal - setIsPendingRevision(false); - return; - } - - const headRev = msg.headRev; - const newRev = msg.newRev; - const changeset = msg.changeset; - const author = (msg.author || ''); - const apool = msg.apool; - - if (msgQueue.length > 0) { - if (newRev !== (msgQueue[msgQueue.length - 1].newRev + 1)) { - window.console.warn('bad message revision on CLIENT_RECONNECT: ' + - `${newRev} not ${msgQueue[msgQueue.length - 1][0] + 1}`); + serverMessageTaskQueue.enqueue(() => { + if (msg.noChanges) { + // If no revisions are pending, just make everything normal + setIsPendingRevision(false); + return; + } + const {headRev, newRev, changeset, author = '', apool} = msg; + if (newRev !== (rev + 1)) { + window.console.warn(`bad message revision on CLIENT_RECONNECT: ${newRev} not ${rev + 1}`); // setChannelState("DISCONNECTED", "badmessage_acceptcommit"); return; } - msg.type = 'NEW_CHANGES'; - msgQueue.push(msg); - return; - } - - if (newRev !== (rev + 1)) { - window.console.warn(`bad message revision on CLIENT_RECONNECT: ${newRev} not ${rev + 1}`); - // setChannelState("DISCONNECTED", "badmessage_acceptcommit"); - return; - } - - rev = newRev; - if (author === pad.getUserId()) { - acceptCommit(); - } else { - editor.applyChangesToBase(changeset, author, apool); - } - - if (newRev === headRev) { - // Once we have applied all pending revisions, make everything normal - setIsPendingRevision(false); - } + rev = newRev; + if (author === pad.getUserId()) { + acceptCommit(); + } else { + editor.applyChangesToBase(changeset, author, apool); + } + if (newRev === headRev) { + // Once we have applied all pending revisions, make everything normal + setIsPendingRevision(false); + } + }); } else if (msg.type === 'NO_COMMIT_PENDING') { if (committing) { // server missed our commit message; abort that commit From 1fdaf95c3b1fbbd711511b686f25db6e4fbfb7a8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 29 Mar 2021 03:09:47 -0400 Subject: [PATCH 015/218] collab_client: Delete unused `NO_COMMIT_PENDING` handling --- src/static/js/collab_client.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.js index a9c289ee3e5..4b6c3ffcf0d 100644 --- a/src/static/js/collab_client.js +++ b/src/static/js/collab_client.js @@ -241,12 +241,6 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) setIsPendingRevision(false); } }); - } else if (msg.type === 'NO_COMMIT_PENDING') { - if (committing) { - // server missed our commit message; abort that commit - setStateIdle(); - handleUserChanges(); - } } else if (msg.type === 'USER_NEWINFO') { const userInfo = msg.userInfo; const id = userInfo.userId; From f2034ad36804db63d708620b8da35eb7f2d330e2 Mon Sep 17 00:00:00 2001 From: Guilherme Goncalves Date: Wed, 24 Mar 2021 17:17:43 -0300 Subject: [PATCH 016/218] tests: Add regression tests for character composition race See: https://github.com/ether/etherpad-lite/issues/4978 --- src/tests/frontend/helper/multipleUsers.js | 147 +++++++++++++++++++++ src/tests/frontend/index.html | 1 + src/tests/frontend/specs/collab_client.js | 102 ++++++++++++++ 3 files changed, 250 insertions(+) create mode 100644 src/tests/frontend/helper/multipleUsers.js create mode 100644 src/tests/frontend/specs/collab_client.js diff --git a/src/tests/frontend/helper/multipleUsers.js b/src/tests/frontend/helper/multipleUsers.js new file mode 100644 index 00000000000..08ed502172e --- /dev/null +++ b/src/tests/frontend/helper/multipleUsers.js @@ -0,0 +1,147 @@ +'use strict'; + +helper.multipleUsers = { + thisUser: null, + otherUser: null, + + // open the same pad on different frames (allows concurrent editions to pad) + async init() { + // do some cleanup, in case any of the tests failed on the previous run + const currentToken = _createTokenForCurrentUser(); + const otherToken = _createTokenForAnotherUser(); + _removeExistingTokensFromCookie(); + + this.thisUser = { + $frame: $('#iframe-container iframe'), + token: currentToken, + // we'll switch between pads, need to store current values of helper.pad* + // to be able to restore those values later + padChrome$: helper.padChrome$, + padOuter$: helper.padOuter$, + padInner$: helper.padInner$, + }; + + this.otherUser = { + token: otherToken, + }; + + // need to perform as the other user, otherwise we'll get the userdup error message + await this.performAsOtherUser(this._createFrameForOtherUser.bind(this)); + }, + + async performAsOtherUser(action) { + _startActingLike(this.otherUser); + await action(); + // go back to initial state when we're done + _startActingLike(this.thisUser); + }, + + close() { + this.thisUser.$frame.attr('style', ''); // make the default ocopy the full height + this.otherUser.$frame.remove(); + }, + + async _loadJQueryCodeForOtherFrame() { + const code = await $.get('/static/js/jquery.js'); + + // make sure we don't override existing jquery + const jQueryCode = `if(typeof $ === "undefined") {\n${code}\n}`; + const sendkeysCode = await $.get('/tests/frontend/lib/sendkeys.js'); + const codesToLoad = [jQueryCode, sendkeysCode]; + + this.otherUser.padChrome$ = _getFrameJQuery(codesToLoad, this.otherUser.$frame); + this.otherUser.padOuter$ = + _getFrameJQuery(codesToLoad, this.otherUser.padChrome$('iframe[name="ace_outer"]')); + this.otherUser.padInner$ = + _getFrameJQuery(codesToLoad, this.otherUser.padOuter$('iframe[name="ace_inner"]')); + + // update helper vars now that they are available + helper.padChrome$ = this.otherUser.padChrome$; + helper.padOuter$ = this.otherUser.padOuter$; + helper.padInner$ = this.otherUser.padInner$; + }, + + async _createFrameForOtherUser() { + // create the iframe + const padUrl = this.thisUser.$frame.attr('src'); + this.otherUser.$frame = $(``); + + // place one iframe (visually) below the other + this.thisUser.$frame.attr('style', 'height: 50%'); + this.otherUser.$frame.attr('style', 'height: 50%; top: 50%'); + this.otherUser.$frame.insertAfter(this.thisUser.$frame); + + // wait for other pad to load + await new Promise((resolve) => this.otherUser.$frame.one('load', resolve)); + + const $editorLoadingMessage = this.otherUser.$frame.contents().find('#editorloadingbox'); + const $errorMessageModal = this.thisUser.$frame.contents().find('#connectivity .userdup'); + + await helper.waitForPromise(() => { + const finishedLoadingOtherFrame = !$editorLoadingMessage.is(':visible'); + // make sure we don't get the userdup by mistake + const didNotDetectUserDup = !$errorMessageModal.is(':visible'); + + return finishedLoadingOtherFrame && didNotDetectUserDup; + }, 50000); + + // need to get values for this.otherUser.pad* vars + await this._loadJQueryCodeForOtherFrame(); + }, +}; + +// adapted form helper.js on Etherpad code +const _getFrameJQuery = (codesToLoad, $iframe) => { + const win = $iframe[0].contentWindow; + const doc = win.document; + + for (let i = 0; i < codesToLoad.length; i++) { + win.eval(codesToLoad[i]); + } + + win.$.window = win; + win.$.document = doc; + + return win.$; +}; + +const _getDocumentWithCookie = () => ( + helper.padChrome$ + ? helper.padChrome$.document + : helper.multipleUsers.thisUser.$frame.get(0).contentDocument +); + +const _setTokenOnCookie = (token) => { + _getDocumentWithCookie().cookie = `token=${token};secure`; +}; + +const _getTokenFromCookie = () => { + const fullCookie = _getDocumentWithCookie().cookie; + return fullCookie.replace(/.*token=([^;]*).*/, '$1').trim(); +}; + +const _createTokenForCurrentUser = () => ( + _getTokenFromCookie().replace(/-other_user.*/g, '') +); + +const _createTokenForAnotherUser = () => { + const currentToken = _createTokenForCurrentUser(); + return `${currentToken}-other_user${helper.randomString(4)}`; +}; + +const _startActingLike = (user) => { + // update helper references, so other methods will act as if the main frame + // was the one we're using from now on + helper.padChrome$ = user.padChrome$; + helper.padOuter$ = user.padOuter$; + helper.padInner$ = user.padInner$; + + _setTokenOnCookie(user.token); +}; + +const _removeExistingTokensFromCookie = () => { + // Expire cookie, to make sure it is removed by the browser. + // See https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#Example_4_Reset_the_previous_cookie + _getDocumentWithCookie().cookie = 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/p'; + _getDocumentWithCookie().cookie = 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; +}; diff --git a/src/tests/frontend/index.html b/src/tests/frontend/index.html index 3d806f626ae..e8a8012fb39 100644 --- a/src/tests/frontend/index.html +++ b/src/tests/frontend/index.html @@ -20,6 +20,7 @@ + diff --git a/src/tests/frontend/specs/collab_client.js b/src/tests/frontend/specs/collab_client.js new file mode 100644 index 00000000000..307e379082d --- /dev/null +++ b/src/tests/frontend/specs/collab_client.js @@ -0,0 +1,102 @@ +'use strict'; + +describe('Messages in the COLLABROOM', function () { + const user1Text = 'text created by user 1'; + const user2Text = 'text created by user 2'; + + const triggerEvent = (eventName) => { + const event = new helper.padInner$.Event(eventName); + helper.padInner$('#innerdocbody').trigger(event); + }; + + const replaceLineText = async (lineNumber, newText) => { + const inner$ = helper.padInner$; + + // get the line element + const $line = inner$('div').eq(lineNumber); + + // simulate key presses to delete content + $line.sendkeys('{selectall}'); // select all + $line.sendkeys('{del}'); // clear the first line + $line.sendkeys(newText); // insert the string + + await helper.waitForPromise(() => inner$('div').eq(lineNumber).text() === newText); + }; + + before(async function () { + this.timeout(10000); + await helper.aNewPad(); + await helper.multipleUsers.init(); + }); + + it('bug #4978 regression test', async function () { + // The bug was triggered by receiving a change from another user while simultaneously composing + // a character and waiting for an acknowledgement of a previously sent change. + + // User 1 starts sending a change to the server. + let sendStarted; + const finishSend = (() => { + const socketJsonObj = helper.padChrome$.window.pad.socket.json; + const sendBackup = socketJsonObj.send; + let startSend; + sendStarted = new Promise((resolve) => { startSend = resolve; }); + let finishSend; + const sendP = new Promise((resolve) => { finishSend = resolve; }); + socketJsonObj.send = (...args) => { + startSend(); + sendP.then(() => { + socketJsonObj.send = sendBackup; + socketJsonObj.send(...args); + }); + }; + return finishSend; + })(); + await replaceLineText(0, user1Text); + await sendStarted; + + // User 1 starts a character composition. + triggerEvent('compositionstart'); + + // User 1 receives a change from user 2. (User 1 will not incorporate the change until the + // composition is completed.) + const user2ChangeArrivedAtUser1 = new Promise((resolve) => { + const cc = helper.padChrome$.window.pad.collabClient; + const origHM = cc.handleMessageFromServer; + cc.handleMessageFromServer = (evt) => { + if (evt.type === 'COLLABROOM' && evt.data.type === 'NEW_CHANGES') { + cc.handleMessageFromServer = origHM; + resolve(); + } + return origHM.call(cc, evt); + }; + }); + await helper.multipleUsers.performAsOtherUser(async () => await replaceLineText(1, user2Text)); + await user2ChangeArrivedAtUser1; + + // User 1 finishes sending the change to the server. User 2 should see the changes right away. + finishSend(); + await helper.multipleUsers.performAsOtherUser(async () => await helper.waitForPromise( + () => helper.padInner$('div').eq(0).text() === user1Text)); + + // User 1 finishes the character composition. User 2's change should then become visible. + triggerEvent('compositionend'); + await helper.waitForPromise(() => helper.padInner$('div').eq(1).text() === user2Text); + + // Users 1 and 2 make some more changes. + await helper.multipleUsers.performAsOtherUser(async () => await replaceLineText(3, user2Text)); + await replaceLineText(2, user1Text); + + // All changes should appear in both views. + const assertContent = async () => await helper.waitForPromise(() => { + const expectedLines = [ + user1Text, + user2Text, + user1Text, + user2Text, + ]; + return expectedLines.every((txt, i) => helper.padInner$('div').eq(i).text() === txt); + }); + await assertContent(); + await helper.multipleUsers.performAsOtherUser(assertContent); + }); +}); From 056939cd22a17bc6a65612769828b644ad816e4c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 31 Mar 2021 02:38:05 -0400 Subject: [PATCH 017/218] tests: Refine `helper/multipleUsers.js` * Rename "current"/"other" to "user0"/"user1". * Delete unnecessary `_createTokenFor*` functions. * Rename helper functions to remove unnecessary leading underscore and for brevity. * Use jQuery's `.attr()` to build the second iframe. * Use js-cookie to manipulate the token cookie. * Don't attempt to set the token cookie if the pad isn't loaded. * Use the token generated by the pad. * Only clear the token cookie at path=/. --- src/tests/frontend/helper/multipleUsers.js | 122 ++++++++------------- 1 file changed, 48 insertions(+), 74 deletions(-) diff --git a/src/tests/frontend/helper/multipleUsers.js b/src/tests/frontend/helper/multipleUsers.js index 08ed502172e..d34676a66b0 100644 --- a/src/tests/frontend/helper/multipleUsers.js +++ b/src/tests/frontend/helper/multipleUsers.js @@ -1,47 +1,39 @@ 'use strict'; helper.multipleUsers = { - thisUser: null, - otherUser: null, + _user0: null, + _user1: null, // open the same pad on different frames (allows concurrent editions to pad) async init() { - // do some cleanup, in case any of the tests failed on the previous run - const currentToken = _createTokenForCurrentUser(); - const otherToken = _createTokenForAnotherUser(); - _removeExistingTokensFromCookie(); - - this.thisUser = { + this._user0 = { $frame: $('#iframe-container iframe'), - token: currentToken, + token: getToken(), // we'll switch between pads, need to store current values of helper.pad* // to be able to restore those values later padChrome$: helper.padChrome$, padOuter$: helper.padOuter$, padInner$: helper.padInner$, }; - - this.otherUser = { - token: otherToken, - }; - + this._user1 = {}; + // Force generation of a new token. + clearToken(); // need to perform as the other user, otherwise we'll get the userdup error message - await this.performAsOtherUser(this._createFrameForOtherUser.bind(this)); + await this.performAsOtherUser(this._createUser1Frame.bind(this)); }, async performAsOtherUser(action) { - _startActingLike(this.otherUser); + startActingLike(this._user1); await action(); - // go back to initial state when we're done - _startActingLike(this.thisUser); + startActingLike(this._user0); }, close() { - this.thisUser.$frame.attr('style', ''); // make the default ocopy the full height - this.otherUser.$frame.remove(); + this._user0.$frame.attr('style', ''); // make the default ocopy the full height + this._user1.$frame.remove(); }, - async _loadJQueryCodeForOtherFrame() { + async _loadJQueryForUser1Frame() { const code = await $.get('/static/js/jquery.js'); // make sure we don't override existing jquery @@ -49,49 +41,53 @@ helper.multipleUsers = { const sendkeysCode = await $.get('/tests/frontend/lib/sendkeys.js'); const codesToLoad = [jQueryCode, sendkeysCode]; - this.otherUser.padChrome$ = _getFrameJQuery(codesToLoad, this.otherUser.$frame); - this.otherUser.padOuter$ = - _getFrameJQuery(codesToLoad, this.otherUser.padChrome$('iframe[name="ace_outer"]')); - this.otherUser.padInner$ = - _getFrameJQuery(codesToLoad, this.otherUser.padOuter$('iframe[name="ace_inner"]')); + this._user1.padChrome$ = getFrameJQuery(codesToLoad, this._user1.$frame); + this._user1.padOuter$ = + getFrameJQuery(codesToLoad, this._user1.padChrome$('iframe[name="ace_outer"]')); + this._user1.padInner$ = + getFrameJQuery(codesToLoad, this._user1.padOuter$('iframe[name="ace_inner"]')); // update helper vars now that they are available - helper.padChrome$ = this.otherUser.padChrome$; - helper.padOuter$ = this.otherUser.padOuter$; - helper.padInner$ = this.otherUser.padInner$; + helper.padChrome$ = this._user1.padChrome$; + helper.padOuter$ = this._user1.padOuter$; + helper.padInner$ = this._user1.padInner$; }, - async _createFrameForOtherUser() { + async _createUser1Frame() { // create the iframe - const padUrl = this.thisUser.$frame.attr('src'); - this.otherUser.$frame = $(``); + const padUrl = this._user0.$frame.attr('src'); + this._user1.$frame = $('`); - $originalPadFrame = $('#iframe-container iframe'); - $otherIframeWithSamePad.insertAfter($originalPadFrame); + // make sure there's a timeout set, otherwise automatic reconnect won't be enabled + helper.padChrome$.window.clientVars.automaticReconnectionTimeout = 2; - // wait for modal to be displayed - helper.waitFor(() => $errorMessageModal.is(':visible'), 50000).done(done); - }); + // open same pad on another iframe, to force userdup error + const $otherIframeWithSamePad = $(``); + $originalPadFrame = $('#iframe-container iframe'); + $otherIframeWithSamePad.insertAfter($originalPadFrame); - this.timeout(60000); + // wait for modal to be displayed + await helper.waitForPromise(() => $errorMessageModal.is(':visible'), 50000); }); - it('displays a count down timer to automatically reconnect', function (done) { + it('displays a count down timer to automatically reconnect', async function () { const $errorMessageModal = helper.padChrome$('#connectivity .userdup'); const $countDownTimer = $errorMessageModal.find('.reconnecttimer'); expect($countDownTimer.is(':visible')).to.be(true); - - done(); }); context('and user clicks on Cancel', function () { @@ -41,31 +38,20 @@ describe('Automatic pad reload on Force Reconnect message', function () { () => helper.padChrome$('#connectivity .userdup').is(':visible') === true); }); - it('does not show Cancel button nor timer anymore', function (done) { + it('does not show Cancel button nor timer anymore', async function () { const $errorMessageModal = helper.padChrome$('#connectivity .userdup'); const $countDownTimer = $errorMessageModal.find('.reconnecttimer'); const $cancelButton = $errorMessageModal.find('#cancelreconnect'); expect($countDownTimer.is(':visible')).to.be(false); expect($cancelButton.is(':visible')).to.be(false); - - done(); }); }); context('and user does not click on Cancel until timer expires', function () { - let padWasReloaded = false; - - beforeEach(async function () { - $originalPadFrame.one('load', () => { - padWasReloaded = true; - }); - }); - - it('reloads the pad', function (done) { - helper.waitFor(() => padWasReloaded, 10000).done(done); - + it('reloads the pad', async function () { this.timeout(10000); + await new Promise((resolve) => $originalPadFrame.one('load', resolve)); }); }); }); From d15ff9ce8d77e461175f4d1b3ec287f9bd380895 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 31 Mar 2021 22:06:00 -0400 Subject: [PATCH 032/218] tests: Add missing `await`s to `change_user_name.js` Also increase the timeouts. --- src/tests/frontend/specs/change_user_name.js | 29 ++++++++------------ 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/tests/frontend/specs/change_user_name.js b/src/tests/frontend/specs/change_user_name.js index 166695db9b0..bfbf42d4633 100644 --- a/src/tests/frontend/specs/change_user_name.js +++ b/src/tests/frontend/specs/change_user_name.js @@ -7,27 +7,22 @@ describe('change username value', function () { }); it('Remembers the user name after a refresh', async function () { - this.timeout(1500); - helper.toggleUserList(); - helper.setUserName('😃'); - - helper.newPad({ // get a new pad, but don't clear the cookies - clearCookies: false, - cb() { - helper.toggleUserList(); - - expect(helper.usernameField().val()).to.be('😃'); - }, - }); + this.timeout(10000); + await helper.toggleUserList(); + await helper.setUserName('😃'); + // get a new pad, but don't clear the cookies + await helper.aNewPad({clearCookies: false}); + await helper.toggleUserList(); + expect(helper.usernameField().val()).to.be('😃'); }); it('Own user name is shown when you enter a chat', async function () { - this.timeout(1500); - helper.toggleUserList(); - helper.setUserName('😃'); + this.timeout(10000); + await helper.toggleUserList(); + await helper.setUserName('😃'); - helper.showChat(); - helper.sendChatMessage('O hi{enter}'); + await helper.showChat(); + await helper.sendChatMessage('O hi{enter}'); await helper.waitForPromise(() => { // username:hours:minutes text From 7a154b1e1dcc959cb4a0a176ab2cad852de717e0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 1 Apr 2021 04:32:05 -0400 Subject: [PATCH 033/218] tests: Wait for commit instead of sleep in `timeslider_revisions.js` --- .../frontend/specs/timeslider_revisions.js | 25 +++---------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/src/tests/frontend/specs/timeslider_revisions.js b/src/tests/frontend/specs/timeslider_revisions.js index 90d938ff94d..24b3ce4b120 100644 --- a/src/tests/frontend/specs/timeslider_revisions.js +++ b/src/tests/frontend/specs/timeslider_revisions.js @@ -9,18 +9,10 @@ describe('timeslider', function () { it('loads adds a hundred revisions', async function () { this.timeout(100000); - const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; - // make some changes to produce 100 revisions - const timePerRev = 900; - const revs = 99; - this.timeout(revs * timePerRev + 10000); - for (let i = 0; i < revs; i++) { - await new Promise((resolve) => setTimeout(resolve, timePerRev)); - // enter 'a' in the first text element - inner$('div').first().sendkeys('a'); - } + // Create a bunch of revisions. + for (let i = 0; i < 99; i++) await helper.edit('a'); chrome$('.buttonicon-savedRevision').click(); // go to timeslider @@ -65,17 +57,8 @@ describe('timeslider', function () { // Disabled as jquery trigger no longer works properly xit('changes the url when clicking on the timeslider', async function () { - const inner$ = helper.padInner$; - - // make some changes to produce 7 revisions - const timePerRev = 1000; - const revs = 20; - this.timeout(revs * timePerRev + 10000); - for (let i = 0; i < revs; i++) { - await new Promise((resolve) => setTimeout(resolve, timePerRev)); - // enter 'a' in the first text element - inner$('div').first().sendkeys('a'); - } + // Create some revisions. + for (let i = 0; i < 20; i++) await helper.edit('a'); // go to timeslider $('#iframe-container iframe') From 7cbb3f565dc5414d226fe399767cb4375449b32a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 1 Apr 2021 15:52:39 -0400 Subject: [PATCH 034/218] tests: Speed up `helper.edit()` and `helper.clearPad()` --- src/static/js/collab_client.js | 5 ++++- src/tests/frontend/helper.js | 18 ++++++++++++++++++ src/tests/frontend/helper/methods.js | 16 +++++++++++----- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.js index 4b6c3ffcf0d..e507d6b06ec 100644 --- a/src/static/js/collab_client.js +++ b/src/static/js/collab_client.js @@ -44,6 +44,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) let channelState = 'CONNECTING'; let lastCommitTime = 0; let initialStartConnectTime = 0; + let commitDelay = 500; const userId = initialUserInfo.userId; // var socket; @@ -102,7 +103,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) return; } - const earliestCommit = lastCommitTime + 500; + const earliestCommit = lastCommitTime + commitDelay; if (now < earliestCommit) { setTimeout(handleUserChanges, earliestCommit - now); return; @@ -488,6 +489,8 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) setChannelState, setStateIdle, setIsPendingRevision, + set commitDelay(ms) { commitDelay = ms; }, + get commitDelay() { return commitDelay; }, }; tellAceAboutHistoricalAuthors(serverVars.historicalAuthorData); diff --git a/src/tests/frontend/helper.js b/src/tests/frontend/helper.js index 05c5f33470d..86af1ae5822 100644 --- a/src/tests/frontend/helper.js +++ b/src/tests/frontend/helper.js @@ -127,6 +127,8 @@ const helper = {}; $('#iframe-container').append($iframe); await new Promise((resolve) => $iframe.one('load', resolve)); helper.padChrome$ = getFrameJQuery($('#iframe-container iframe')); + helper.padChrome$.padeditor = + helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_editor').padeditor; if (opts.clearCookies) { helper.clearPadPrefCookie(); } @@ -260,6 +262,22 @@ const helper = {}; selection.addRange(range); }; + // Temporarily reduces minimum time between commits and calls the provided function with a single + // argument: a function that immediately incorporates all pad edits (as opposed to waiting for the + // idle timer to fire). + helper.withFastCommit = async (fn) => { + const incorp = () => helper.padChrome$.padeditor.ace.callWithAce( + (ace) => ace.ace_inCallStackIfNecessary('helper.edit', () => ace.ace_fastIncorp())); + const cc = helper.padChrome$.window.pad.collabClient; + const {commitDelay} = cc; + cc.commitDelay = 0; + try { + return await fn(incorp); + } finally { + cc.commitDelay = commitDelay; + } + }; + const getTextNodeAndOffsetOf = ($targetLine, targetOffsetAtLine) => { const $textNodes = $targetLine.find('*').contents().filter(function () { return this.nodeType === Node.TEXT_NODE; diff --git a/src/tests/frontend/helper/methods.js b/src/tests/frontend/helper/methods.js index 0dd251a0c6d..253bfbc0df6 100644 --- a/src/tests/frontend/helper/methods.js +++ b/src/tests/frontend/helper/methods.js @@ -32,8 +32,11 @@ helper.spyOnSocketIO = () => { helper.edit = async (message, line) => { const editsNum = helper.commits.length; line = line ? line - 1 : 0; - helper.linesDiv()[line].sendkeys(message); - return helper.waitForPromise(() => editsNum + 1 === helper.commits.length); + await helper.withFastCommit(async (incorp) => { + helper.linesDiv()[line].sendkeys(message); + incorp(); + await helper.waitForPromise(() => editsNum + 1 === helper.commits.length); + }); }; /** @@ -216,7 +219,10 @@ helper.clearPad = async () => { await helper.waitForPromise(() => !helper.padInner$.document.getSelection().isCollapsed); const e = new helper.padInner$.Event(helper.evtType); e.keyCode = 8; // delete key - helper.padInner$('#innerdocbody').trigger(e); - await helper.waitForPromise(helper.padIsEmpty); - await helper.waitForPromise(() => helper.commits.length > commitsBefore); + await helper.withFastCommit(async (incorp) => { + helper.padInner$('#innerdocbody').trigger(e); + incorp(); + await helper.waitForPromise(helper.padIsEmpty); + await helper.waitForPromise(() => helper.commits.length > commitsBefore); + }); }; From 4ad80d40726caca5821f762bbc4a1527c30f4704 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 1 Apr 2021 15:51:03 -0400 Subject: [PATCH 035/218] tests: Delete overly aggressive frontend test timeouts This should reduce test flakiness. --- src/tests/frontend/specs/alphabet.js | 1 - src/tests/frontend/specs/bold.js | 2 -- src/tests/frontend/specs/delete.js | 1 - src/tests/frontend/specs/drag_and_drop.js | 2 -- src/tests/frontend/specs/embed_value.js | 4 ---- src/tests/frontend/specs/font_type.js | 1 - src/tests/frontend/specs/indentation.js | 2 -- src/tests/frontend/specs/italic.js | 2 -- .../specs/multiple_authors_clear_authorship_colors.js | 2 -- src/tests/frontend/specs/ordered_list.js | 4 ---- src/tests/frontend/specs/pad_modal.js | 5 ----- src/tests/frontend/specs/redo.js | 2 -- src/tests/frontend/specs/scrollTo.js | 2 -- src/tests/frontend/specs/select_formatting_buttons.js | 3 --- src/tests/frontend/specs/strikethrough.js | 1 - src/tests/frontend/specs/undo.js | 3 --- src/tests/frontend/specs/unordered_list.js | 5 ----- 17 files changed, 42 deletions(-) diff --git a/src/tests/frontend/specs/alphabet.js b/src/tests/frontend/specs/alphabet.js index 473d2029989..d57fb86c605 100644 --- a/src/tests/frontend/specs/alphabet.js +++ b/src/tests/frontend/specs/alphabet.js @@ -10,7 +10,6 @@ describe('All the alphabet works n stuff', function () { }); it('when you enter any char it appears right', function (done) { - this.timeout(250); const inner$ = helper.padInner$; // get the first text element out of the inner iframe diff --git a/src/tests/frontend/specs/bold.js b/src/tests/frontend/specs/bold.js index ce62b75a8de..0fc8cd7eaee 100644 --- a/src/tests/frontend/specs/bold.js +++ b/src/tests/frontend/specs/bold.js @@ -8,7 +8,6 @@ describe('bold button', function () { }); it('makes text bold on click', function (done) { - this.timeout(200); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -37,7 +36,6 @@ describe('bold button', function () { }); it('makes text bold on keypress', function (done) { - this.timeout(200); const inner$ = helper.padInner$; // get the first text element out of the inner iframe diff --git a/src/tests/frontend/specs/delete.js b/src/tests/frontend/specs/delete.js index 869fc077514..39deac770ee 100644 --- a/src/tests/frontend/specs/delete.js +++ b/src/tests/frontend/specs/delete.js @@ -8,7 +8,6 @@ describe('delete keystroke', function () { }); it('makes text delete', async function () { - this.timeout(50); const inner$ = helper.padInner$; // get the first text element out of the inner iframe diff --git a/src/tests/frontend/specs/drag_and_drop.js b/src/tests/frontend/specs/drag_and_drop.js index 8f608d714ad..c080ed0fdf2 100644 --- a/src/tests/frontend/specs/drag_and_drop.js +++ b/src/tests/frontend/specs/drag_and_drop.js @@ -30,7 +30,6 @@ describe('drag and drop', function () { }); it('moves text back to its original place', async function () { - this.timeout(50); // test text was removed from drop target const $targetLine = getLine(TARGET_LINE); expect($targetLine.text()).to.be('Target line []'); @@ -66,7 +65,6 @@ describe('drag and drop', function () { }); it('moves text back to its original place', async function () { - this.timeout(50); // test text was removed from drop target const $targetLine = getLine(TARGET_LINE); expect($targetLine.text()).to.be('Target line []'); diff --git a/src/tests/frontend/specs/embed_value.js b/src/tests/frontend/specs/embed_value.js index 388738358ed..acd0c5f8cc0 100644 --- a/src/tests/frontend/specs/embed_value.js +++ b/src/tests/frontend/specs/embed_value.js @@ -57,7 +57,6 @@ describe('embed links', function () { describe('the share link', function () { it('is the actual pad url', async function () { - this.timeout(100); const chrome$ = helper.padChrome$; // open share dropdown @@ -72,7 +71,6 @@ describe('embed links', function () { describe('the embed as iframe code', function () { it('is an iframe with the the correct url parameters and correct size', async function () { - this.timeout(50); const chrome$ = helper.padChrome$; // open share dropdown @@ -94,7 +92,6 @@ describe('embed links', function () { describe('the share link', function () { it('shows a read only url', async function () { - this.timeout(50); const chrome$ = helper.padChrome$; // open share dropdown @@ -111,7 +108,6 @@ describe('embed links', function () { describe('the embed as iframe code', function () { it('is an iframe with the the correct url parameters and correct size', async function () { - this.timeout(50); const chrome$ = helper.padChrome$; // open share dropdown diff --git a/src/tests/frontend/specs/font_type.js b/src/tests/frontend/specs/font_type.js index 02d33db2e30..b7ab17f26a4 100644 --- a/src/tests/frontend/specs/font_type.js +++ b/src/tests/frontend/specs/font_type.js @@ -8,7 +8,6 @@ describe('font select', function () { }); it('makes text RobotoMono', async function () { - this.timeout(100); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; diff --git a/src/tests/frontend/specs/indentation.js b/src/tests/frontend/specs/indentation.js index bcf6f7bf0ac..892849ea221 100644 --- a/src/tests/frontend/specs/indentation.js +++ b/src/tests/frontend/specs/indentation.js @@ -8,7 +8,6 @@ describe('indentation button', function () { }); it('indent text with keypress', async function () { - this.timeout(100); const inner$ = helper.padInner$; // get the first text element out of the inner iframe @@ -25,7 +24,6 @@ describe('indentation button', function () { }); it('indent text with button', async function () { - this.timeout(100); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; diff --git a/src/tests/frontend/specs/italic.js b/src/tests/frontend/specs/italic.js index 84663b902a5..706cfffc9fc 100644 --- a/src/tests/frontend/specs/italic.js +++ b/src/tests/frontend/specs/italic.js @@ -8,7 +8,6 @@ describe('italic some text', function () { }); it('makes text italic using button', async function () { - this.timeout(100); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -36,7 +35,6 @@ describe('italic some text', function () { }); it('makes text italic using keypress', async function () { - this.timeout(100); const inner$ = helper.padInner$; // get the first text element out of the inner iframe diff --git a/src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js b/src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js index 0b18400622b..d2ee3853c11 100755 --- a/src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js +++ b/src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js @@ -24,8 +24,6 @@ describe('author of pad edition', function () { // author 2 makes some changes on the pad it('Clears Authorship by second user', async function () { - this.timeout(100); - const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; diff --git a/src/tests/frontend/specs/ordered_list.js b/src/tests/frontend/specs/ordered_list.js index 8754656d355..cc0e1bbcbf2 100644 --- a/src/tests/frontend/specs/ordered_list.js +++ b/src/tests/frontend/specs/ordered_list.js @@ -9,7 +9,6 @@ describe('ordered_list.js', function () { }); it('inserts ordered list text', async function () { - this.timeout(200); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -29,7 +28,6 @@ describe('ordered_list.js', function () { }); it('inserts unordered list', async function () { - this.timeout(50); await helper.waitForPromise( () => helper.padInner$('div').first().find('ol li').length === 1); }); @@ -76,7 +74,6 @@ describe('ordered_list.js', function () { }); it('inserts unordered list', async function () { - this.timeout(200); helper.waitForPromise(() => helper.padInner$('div').first().find('ol li').length === 1); }); }); @@ -177,7 +174,6 @@ describe('ordered_list.js', function () { }); it('indent and de-indent list item with keypress', async function () { - this.timeout(200); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; diff --git a/src/tests/frontend/specs/pad_modal.js b/src/tests/frontend/specs/pad_modal.js index 2efa65eff64..4b4a89c6962 100644 --- a/src/tests/frontend/specs/pad_modal.js +++ b/src/tests/frontend/specs/pad_modal.js @@ -17,13 +17,11 @@ describe('Pad modal', function () { }); it('disables editor', async function () { - this.timeout(200); expect(isEditorDisabled()).to.be(true); }); context('and user clicks on editor', function () { it('does not close the modal', async function () { - this.timeout(200); clickOnPadInner(); const $modal = helper.padChrome$(MODAL_SELECTOR); const modalIsVisible = $modal.hasClass('popup-show'); @@ -34,7 +32,6 @@ describe('Pad modal', function () { context('and user clicks on pad outer', function () { it('does not close the modal', async function () { - this.timeout(200); clickOnPadOuter(); const $modal = helper.padChrome$(MODAL_SELECTOR); const modalIsVisible = $modal.hasClass('popup-show'); @@ -61,7 +58,6 @@ describe('Pad modal', function () { context('and user clicks on editor', function () { it('closes the modal', async function () { - this.timeout(200); clickOnPadInner(); await helper.waitForPromise(() => isModalOpened(MODAL_SELECTOR) === false); }); @@ -69,7 +65,6 @@ describe('Pad modal', function () { context('and user clicks on pad outer', function () { it('closes the modal', async function () { - this.timeout(200); clickOnPadOuter(); await helper.waitForPromise(() => isModalOpened(MODAL_SELECTOR) === false); }); diff --git a/src/tests/frontend/specs/redo.js b/src/tests/frontend/specs/redo.js index 37187e46779..bf871a08c17 100644 --- a/src/tests/frontend/specs/redo.js +++ b/src/tests/frontend/specs/redo.js @@ -7,7 +7,6 @@ describe('undo button then redo button', function () { }); it('redo some typing with button', async function () { - this.timeout(200); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -33,7 +32,6 @@ describe('undo button then redo button', function () { }); it('redo some typing with keypress', async function () { - this.timeout(200); const inner$ = helper.padInner$; // get the first text element inside the editable space diff --git a/src/tests/frontend/specs/scrollTo.js b/src/tests/frontend/specs/scrollTo.js index dd9b546fa68..199a4c0bf6b 100755 --- a/src/tests/frontend/specs/scrollTo.js +++ b/src/tests/frontend/specs/scrollTo.js @@ -9,7 +9,6 @@ describe('scrollTo.js', function () { }); it('Scrolls down to Line 4', async function () { - this.timeout(100); const chrome$ = helper.padChrome$; await helper.waitForPromise(() => { const topOffset = parseInt(chrome$('iframe').first('iframe') @@ -27,7 +26,6 @@ describe('scrollTo.js', function () { }); it('Does NOT change scroll', async function () { - this.timeout(100); const chrome$ = helper.padChrome$; await helper.waitForPromise(() => { const topOffset = parseInt(chrome$('iframe').first('iframe') diff --git a/src/tests/frontend/specs/select_formatting_buttons.js b/src/tests/frontend/specs/select_formatting_buttons.js index c0ed852a7d7..aac8797811c 100644 --- a/src/tests/frontend/specs/select_formatting_buttons.js +++ b/src/tests/frontend/specs/select_formatting_buttons.js @@ -44,14 +44,12 @@ describe('select formatting buttons when selection has style applied', function const testIfFormattingButtonIsDeselected = function (style) { it(`deselects the ${style} button`, async function () { - this.timeout(100); await helper.waitForPromise(() => !isButtonSelected(style)); }); }; const testIfFormattingButtonIsSelected = function (style) { it(`selects the ${style} button`, async function () { - this.timeout(100); await helper.waitForPromise(() => isButtonSelected(style)); }); }; @@ -128,7 +126,6 @@ describe('select formatting buttons when selection has style applied', function context('when user applies a style and the selection does not change', function () { it('selects the style button', async function () { - this.timeout(100); const style = STYLES[0]; // italic applyStyleOnLine(style, FIRST_LINE); await helper.waitForPromise(() => isButtonSelected(style) === true); diff --git a/src/tests/frontend/specs/strikethrough.js b/src/tests/frontend/specs/strikethrough.js index 7ca08d857cc..7e96b302d38 100644 --- a/src/tests/frontend/specs/strikethrough.js +++ b/src/tests/frontend/specs/strikethrough.js @@ -8,7 +8,6 @@ describe('strikethrough button', function () { }); it('makes text strikethrough', async function () { - this.timeout(100); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; diff --git a/src/tests/frontend/specs/undo.js b/src/tests/frontend/specs/undo.js index 97adf7aaa65..2f2a45ee1a3 100644 --- a/src/tests/frontend/specs/undo.js +++ b/src/tests/frontend/specs/undo.js @@ -7,8 +7,6 @@ describe('undo button', function () { }); it('undo some typing by clicking undo button', async function () { - this.timeout(100); - this.timeout(150); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -29,7 +27,6 @@ describe('undo button', function () { }); it('undo some typing using a keypress', async function () { - this.timeout(150); const inner$ = helper.padInner$; // get the first text element inside the editable space diff --git a/src/tests/frontend/specs/unordered_list.js b/src/tests/frontend/specs/unordered_list.js index f4c34b376e7..0a89c089c9e 100644 --- a/src/tests/frontend/specs/unordered_list.js +++ b/src/tests/frontend/specs/unordered_list.js @@ -9,7 +9,6 @@ describe('unordered_list.js', function () { }); it('insert unordered list text then removes by outdent', async function () { - this.timeout(150); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; const originalText = inner$('div').first().text(); @@ -36,7 +35,6 @@ describe('unordered_list.js', function () { }); it('insert unordered list text then remove by clicking list again', async function () { - this.timeout(150); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; const originalText = inner$('div').first().text(); @@ -64,7 +62,6 @@ describe('unordered_list.js', function () { }); it('Keeps the unordered list on enter for the new line', async function () { - this.timeout(250); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -95,7 +92,6 @@ describe('unordered_list.js', function () { }); it('indent and de-indent list item with keypress', async function () { - this.timeout(150); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -130,7 +126,6 @@ describe('unordered_list.js', function () { }); it('indent and de-indent list item with indent button', async function () { - this.timeout(150); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; From 58dac4c0fc8aa84ddc5f453d506798fbc901427d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 1 Apr 2021 21:22:15 -0400 Subject: [PATCH 036/218] tests: Fix races in `inner_height.js` --- src/tests/frontend/specs/inner_height.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/tests/frontend/specs/inner_height.js b/src/tests/frontend/specs/inner_height.js index 8bd0a231760..d1a6b118bd9 100644 --- a/src/tests/frontend/specs/inner_height.js +++ b/src/tests/frontend/specs/inner_height.js @@ -7,10 +7,11 @@ describe('height regression after ace.js refactoring', function () { // everything fits inside the viewport it('clientHeight should equal scrollHeight with few lines', async function () { - const aceOuter = helper.padChrome$('iframe')[0].contentDocument; - const clientHeight = aceOuter.documentElement.clientHeight; - const scrollHeight = aceOuter.documentElement.scrollHeight; - expect(clientHeight).to.be(scrollHeight); + await helper.clearPad(); + const outerHtml = helper.padChrome$('iframe')[0].contentDocument.documentElement; + // Give some time for the heights to settle. + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(outerHtml.clientHeight).to.be(outerHtml.scrollHeight); }); it('client height should be less than scrollHeight with many lines', async function () { @@ -23,9 +24,8 @@ describe('height regression after ace.js refactoring', function () { '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'); - const aceOuter = helper.padChrome$('iframe')[0].contentDocument; - const clientHeight = aceOuter.documentElement.clientHeight; - const scrollHeight = aceOuter.documentElement.scrollHeight; - expect(clientHeight).to.be.lessThan(scrollHeight); + const outerHtml = helper.padChrome$('iframe')[0].contentDocument.documentElement; + // Need to poll because the heights take some time to settle. + await helper.waitForPromise(() => outerHtml.clientHeight < outerHtml.scrollHeight); }); }); From 27e537305090545953f579c14b568dfe5fda1243 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 1 Apr 2021 21:49:15 -0400 Subject: [PATCH 037/218] tests: Fix race in `change_user_name.js` --- src/tests/frontend/specs/change_user_name.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tests/frontend/specs/change_user_name.js b/src/tests/frontend/specs/change_user_name.js index bfbf42d4633..b146a128146 100644 --- a/src/tests/frontend/specs/change_user_name.js +++ b/src/tests/frontend/specs/change_user_name.js @@ -10,10 +10,12 @@ describe('change username value', function () { this.timeout(10000); await helper.toggleUserList(); await helper.setUserName('😃'); + // Give the server an opportunity to write the new name. + await new Promise((resolve) => setTimeout(resolve, 1000)); // get a new pad, but don't clear the cookies await helper.aNewPad({clearCookies: false}); await helper.toggleUserList(); - expect(helper.usernameField().val()).to.be('😃'); + await helper.waitForPromise(() => helper.usernameField().val() === '😃'); }); it('Own user name is shown when you enter a chat', async function () { From e4754eb9dfb1ca032eacd1fbea06a62d07ed7190 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 1 Apr 2021 22:29:31 -0400 Subject: [PATCH 038/218] tests: Fix race in `timeslider_revisions.js` --- src/tests/frontend/specs/timeslider_revisions.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tests/frontend/specs/timeslider_revisions.js b/src/tests/frontend/specs/timeslider_revisions.js index 24b3ce4b120..03a05b236f7 100644 --- a/src/tests/frontend/specs/timeslider_revisions.js +++ b/src/tests/frontend/specs/timeslider_revisions.js @@ -14,6 +14,9 @@ describe('timeslider', function () { // Create a bunch of revisions. for (let i = 0; i < 99; i++) await helper.edit('a'); chrome$('.buttonicon-savedRevision').click(); + await helper.waitForPromise(() => helper.padChrome$('.saved-revision').length > 0); + // Give some time to send the SAVE_REVISION message to the server before navigating away. + await new Promise((resolve) => setTimeout(resolve, 100)); // go to timeslider $('#iframe-container iframe') From e86547c4f5205726a3959162111727cc98ff26dc Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Fri, 2 Apr 2021 21:53:36 +0000 Subject: [PATCH 039/218] fix: upgrade openapi-backend from 3.9.0 to 3.9.1 Snyk has created this PR to upgrade openapi-backend from 3.9.0 to 3.9.1. See this package in npm: https://www.npmjs.com/package/openapi-backend See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=upgrade-pr --- src/package-lock.json | 33 +++++++++++++++++++++------------ src/package.json | 2 +- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 9fd24fb98b4..5eade554e3b 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -910,7 +910,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -2363,7 +2362,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz", "integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==", - "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -2478,8 +2476,7 @@ "has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" }, "has-unicode": { "version": "2.0.1", @@ -6929,8 +6926,7 @@ "object-inspect": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", - "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", - "dev": true + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" }, "object-keys": { "version": "1.1.1", @@ -6988,9 +6984,9 @@ } }, "openapi-backend": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-3.9.0.tgz", - "integrity": "sha512-RaEBFBFBGFcnOSqoKiX+Dg+CxS0zWBop1qw+JjPLzLs7ob637QEZcEGvKYKcwGv99mWwvcCHIuGH9l+LV5pHew==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-3.9.1.tgz", + "integrity": "sha512-PXd7C1ln913Hpw1/JCpoZm3kv/0K138gvI9Abidib34yzW3vHBGYj+uxVEYSFEcy+5GVCvMJoMCMwO61Tb2t2Q==", "requires": { "@apidevtools/json-schema-ref-parser": "^9.0.7", "ajv": "^6.10.0", @@ -7004,9 +7000,12 @@ }, "dependencies": { "qs": { - "version": "6.9.6", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz", - "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==" + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "requires": { + "side-channel": "^1.0.4" + } } } }, @@ -7682,6 +7681,16 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", diff --git a/src/package.json b/src/package.json index c207ec5add5..1bd9dce0a60 100644 --- a/src/package.json +++ b/src/package.json @@ -55,7 +55,7 @@ "mime-types": "^2.1.27", "nodeify": "1.0.1", "npm": "6.14.11", - "openapi-backend": "^3.9.0", + "openapi-backend": "^3.9.1", "proxy-addr": "^2.0.6", "rate-limiter-flexible": "^2.1.4", "rehype": "^10.0.0", From e483b919163cd0c1a7bea96bd7d192c2f7900163 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Sun, 4 Apr 2021 16:17:11 +0200 Subject: [PATCH 040/218] Don't make browsers fail on sync-xhr until require-kernel is dropped --- src/node/hooks/express/specialpages.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js index 61471348d47..c1e3fec9964 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.js @@ -54,6 +54,8 @@ exports.expressCreateServer = (hookName, args, cb) => { isReadOnly, }); + // can be removed when require-kernel is dropped + res.header('Feature-Policy', 'sync-xhr \'self\''); res.send(eejs.require('ep_etherpad-lite/templates/pad.html', { req, toolbar, From 78ea888cb723f0a643800ad635dbe25d46d0cd1f Mon Sep 17 00:00:00 2001 From: Mikk Andresen Date: Tue, 6 Apr 2021 22:07:36 +0300 Subject: [PATCH 041/218] DOCS: Add basic styles for tables and resources section to Changeset docs - https://github.com/citizenos/citizenos-fe/issues/535 --- doc/api/changeset_library.md | 7 ++++++- doc/assets/style.css | 27 +++++++++++++++++++++------ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/doc/api/changeset_library.md b/doc/api/changeset_library.md index 863ae1cf2b9..c776c17c56c 100644 --- a/doc/api/changeset_library.md +++ b/doc/api/changeset_library.md @@ -148,4 +148,9 @@ This is an atext. An atext has two parts: text and attribs. The text is just the The attribs are again a bunch of operators like .ops in the changeset was. But these operators are only + operators. They describe which part of the text has which attributes -For more information see /doc/easysync/easysync-notes.txt in the source. +## Resources / further reading + +Detailed information about the changesets & Easysync protocol: + +* Easysync Protocol - [/doc/easysync/easysync-notes.pdf](https://github.com/ether/etherpad-lite/blob/develop/doc/easysync/easysync-notes.pdf) +* Etherpad and EasySync Technical Manual - [/doc/easysync/easysync-full-description.pdf](https://github.com/ether/etherpad-lite/blob/develop/doc/easysync/easysync-full-description.pdf) diff --git a/doc/assets/style.css b/doc/assets/style.css index 8c6720fa8eb..d8812043b1a 100644 --- a/doc/assets/style.css +++ b/doc/assets/style.css @@ -1,4 +1,4 @@ -body{ +body { border-top: solid #44b492 5pt; line-height:150%; font-family: 'Quicksand',sans-serif; @@ -8,28 +8,43 @@ body{ padding: 20px; } -a{ +a { color: #555; } -h1{ +h1 { color: #44b492; line-height:100%; } -a:hover{ +a:hover { color: #44b492; } -pre{ +pre { background-color: #e0e0e0; padding:20px; } -code{ +code { background-color: #e0e0e0; } img { max-width: 100%; } + +table, th, td { + text-align: left; + border: 1px solid gray; + border-collapse: collapse; +} + +th { + padding: 0.5em; + background: #EEE; +} + +td { + padding: 0.5em; +} From af19a010c573b8879f24e9afbdb48b20e7c836c3 Mon Sep 17 00:00:00 2001 From: Mikk Andresen Date: Tue, 6 Apr 2021 22:10:55 +0300 Subject: [PATCH 042/218] DOCS: Fix broken links in TOC - use Marked to generate ID slugs instead of local implementation that was giving out different IDs in some cases - https://github.com/citizenos/citizenos-fe/issues/535 --- src/bin/doc/html.js | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/bin/doc/html.js b/src/bin/doc/html.js index f4be7ce134b..ff07ae7c831 100644 --- a/src/bin/doc/html.js +++ b/src/bin/doc/html.js @@ -146,15 +146,16 @@ const buildToc = (lexed, filename, cb) => { lexed.forEach((tok) => { if (tok.type !== 'heading') return; if (tok.depth - depth > 1) { - return cb(new Error(`Inappropriate heading level\n${ - JSON.stringify(tok)}`)); + return cb(new Error(`Inappropriate heading level\n${JSON.stringify(tok)}`)); } depth = tok.depth; - const id = getId(`${filename}_${tok.text.trim()}`); - toc.push(`${new Array((depth - 1) * 2 + 1).join(' ') - }* ${ - tok.text}`); + + const slugger = new marked.Slugger(); + const id = slugger.slug(`${filename}_${tok.text.trim()}`); + + toc.push(`${new Array((depth - 1) * 2 + 1).join(' ')}* ${tok.text}`); + tok.text += `#`; }); @@ -162,17 +163,3 @@ const buildToc = (lexed, filename, cb) => { toc = marked.parse(toc.join('\n')); cb(null, toc); }; - -const idCounters = {}; -const getId = (text) => { - text = text.toLowerCase(); - text = text.replace(/[^a-z0-9]+/g, '_'); - text = text.replace(/^_+|_+$/, ''); - text = text.replace(/^([^a-z])/, '_$1'); - if (Object.prototype.hasOwnProperty.call(idCounters, text)) { - text += `_${++idCounters[text]}`; - } else { - idCounters[text] = 0; - } - return text; -}; From 9408d4395f67cc0fc9304bfc5177afe981a7374a Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Mon, 5 Apr 2021 10:02:25 +0200 Subject: [PATCH 043/218] remove custom timeouts --- src/tests/frontend/specs/alphabet.js | 1 - src/tests/frontend/specs/authorship_of_editions.js | 1 - src/tests/frontend/specs/bold.js | 1 - src/tests/frontend/specs/change_user_color.js | 1 - src/tests/frontend/specs/chat.js | 1 - src/tests/frontend/specs/chat_load_messages.js | 3 --- src/tests/frontend/specs/clear_authorship_colors.js | 1 - src/tests/frontend/specs/delete.js | 1 - src/tests/frontend/specs/drag_and_drop.js | 1 - src/tests/frontend/specs/embed_value.js | 2 -- src/tests/frontend/specs/enter.js | 1 - src/tests/frontend/specs/font_type.js | 1 - src/tests/frontend/specs/helper.js | 4 ---- src/tests/frontend/specs/importexport.js | 1 - src/tests/frontend/specs/importindents.js | 1 - src/tests/frontend/specs/indentation.js | 1 - src/tests/frontend/specs/italic.js | 1 - src/tests/frontend/specs/language.js | 1 - .../specs/multiple_authors_clear_authorship_colors.js | 1 - src/tests/frontend/specs/ordered_list.js | 3 --- src/tests/frontend/specs/pad_modal.js | 2 -- src/tests/frontend/specs/redo.js | 1 - src/tests/frontend/specs/scrollTo.js | 2 -- src/tests/frontend/specs/select_formatting_buttons.js | 1 - src/tests/frontend/specs/strikethrough.js | 1 - src/tests/frontend/specs/timeslider.js | 1 - src/tests/frontend/specs/timeslider_numeric_padID.js | 1 - src/tests/frontend/specs/timeslider_revisions.js | 1 - src/tests/frontend/specs/undo.js | 1 - src/tests/frontend/specs/unordered_list.js | 5 ----- src/tests/frontend/specs/urls_become_clickable.js | 1 - src/tests/frontend/specs/xxauto_reconnect.js | 1 - 32 files changed, 46 deletions(-) diff --git a/src/tests/frontend/specs/alphabet.js b/src/tests/frontend/specs/alphabet.js index d57fb86c605..999cfdf3a24 100644 --- a/src/tests/frontend/specs/alphabet.js +++ b/src/tests/frontend/specs/alphabet.js @@ -5,7 +5,6 @@ describe('All the alphabet works n stuff', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/authorship_of_editions.js b/src/tests/frontend/specs/authorship_of_editions.js index 4b71cd15053..9fd90ffb7a5 100644 --- a/src/tests/frontend/specs/authorship_of_editions.js +++ b/src/tests/frontend/specs/authorship_of_editions.js @@ -7,7 +7,6 @@ describe('author of pad edition', function () { // author 1 creates a new pad with some content (regular lines and lists) before(async function () { - this.timeout(60000); const padId = await helper.aNewPad(); // make sure pad has at least 3 lines diff --git a/src/tests/frontend/specs/bold.js b/src/tests/frontend/specs/bold.js index 0fc8cd7eaee..51655588df7 100644 --- a/src/tests/frontend/specs/bold.js +++ b/src/tests/frontend/specs/bold.js @@ -3,7 +3,6 @@ describe('bold button', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/change_user_color.js b/src/tests/frontend/specs/change_user_color.js index aedf79b22d2..d8fd976434b 100644 --- a/src/tests/frontend/specs/change_user_color.js +++ b/src/tests/frontend/specs/change_user_color.js @@ -3,7 +3,6 @@ describe('change user color', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/chat.js b/src/tests/frontend/specs/chat.js index 60bfe3d5a58..f471c447934 100644 --- a/src/tests/frontend/specs/chat.js +++ b/src/tests/frontend/specs/chat.js @@ -87,7 +87,6 @@ describe('Chat messages and UI', function () { xit('Checks showChat=false URL Parameter hides chat then' + ' when removed it shows chat', async function () { - this.timeout(60000); // give it a second to save the username on the server side await new Promise((resolve) => setTimeout(resolve, 3000)); diff --git a/src/tests/frontend/specs/chat_load_messages.js b/src/tests/frontend/specs/chat_load_messages.js index f46c93170c7..d720fbba187 100644 --- a/src/tests/frontend/specs/chat_load_messages.js +++ b/src/tests/frontend/specs/chat_load_messages.js @@ -4,7 +4,6 @@ describe('chat-load-messages', function () { let padName; it('creates a pad', async function () { - this.timeout(60000); padName = await helper.aNewPad(); }); @@ -15,8 +14,6 @@ describe('chat-load-messages', function () { const chatInput = chrome$('#chatinput'); const chatText = chrome$('#chattext'); - this.timeout(60000); - const messages = 140; for (let i = 1; i <= messages; i++) { let num = `${i}`; diff --git a/src/tests/frontend/specs/clear_authorship_colors.js b/src/tests/frontend/specs/clear_authorship_colors.js index 8a8b5a32b50..58ce937044a 100644 --- a/src/tests/frontend/specs/clear_authorship_colors.js +++ b/src/tests/frontend/specs/clear_authorship_colors.js @@ -3,7 +3,6 @@ describe('clear authorship colors button', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/delete.js b/src/tests/frontend/specs/delete.js index 39deac770ee..05164280b4c 100644 --- a/src/tests/frontend/specs/delete.js +++ b/src/tests/frontend/specs/delete.js @@ -3,7 +3,6 @@ describe('delete keystroke', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/drag_and_drop.js b/src/tests/frontend/specs/drag_and_drop.js index c080ed0fdf2..6d3f5a363b0 100644 --- a/src/tests/frontend/specs/drag_and_drop.js +++ b/src/tests/frontend/specs/drag_and_drop.js @@ -3,7 +3,6 @@ // WARNING: drag and drop is only simulated on these tests, manual testing might also be necessary describe('drag and drop', function () { before(async function () { - this.timeout(60000); await helper.aNewPad(); await createScriptWithSeveralLines(); }); diff --git a/src/tests/frontend/specs/embed_value.js b/src/tests/frontend/specs/embed_value.js index acd0c5f8cc0..e92d070a740 100644 --- a/src/tests/frontend/specs/embed_value.js +++ b/src/tests/frontend/specs/embed_value.js @@ -51,7 +51,6 @@ describe('embed links', function () { describe('read and write', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); @@ -86,7 +85,6 @@ describe('embed links', function () { describe('when read only option is set', function () { beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/enter.js b/src/tests/frontend/specs/enter.js index 070b455443a..a32a90c6ea2 100644 --- a/src/tests/frontend/specs/enter.js +++ b/src/tests/frontend/specs/enter.js @@ -3,7 +3,6 @@ describe('enter keystroke', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/font_type.js b/src/tests/frontend/specs/font_type.js index b7ab17f26a4..fbebcdfd742 100644 --- a/src/tests/frontend/specs/font_type.js +++ b/src/tests/frontend/specs/font_type.js @@ -3,7 +3,6 @@ describe('font select', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/helper.js b/src/tests/frontend/specs/helper.js index 9dd2b05ce0c..0d876b33659 100644 --- a/src/tests/frontend/specs/helper.js +++ b/src/tests/frontend/specs/helper.js @@ -28,8 +28,6 @@ describe('the test helper', function () { // timeout may or may end up in the code. None the less, we test here // to catch it if the bug comes up again. it('clears cookies', async function () { - this.timeout(60000); - // set cookies far into the future to make sure they're not expired yet window.Cookies.set('token', 'foo', {expires: 7 /* days */}); window.Cookies.set('language', 'bar', {expires: 7 /* days */}); @@ -95,7 +93,6 @@ describe('the test helper', function () { }); it('sets pad prefs cookie', async function () { - this.timeout(60000); await helper.aNewPad({padPrefs: {foo: 'padPrefs test'}}); const {padcookie} = helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_cookie'); expect(padcookie.getPref('foo')).to.be('padPrefs test'); @@ -239,7 +236,6 @@ describe('the test helper', function () { }; before(async function () { - this.timeout(60000); await helper.aNewPad(); // create some lines to be used on the tests diff --git a/src/tests/frontend/specs/importexport.js b/src/tests/frontend/specs/importexport.js index 954cd518ba5..46254bedd34 100644 --- a/src/tests/frontend/specs/importexport.js +++ b/src/tests/frontend/specs/importexport.js @@ -499,7 +499,6 @@ describe('importexport.js', function () { let confirm; before(async function () { - this.timeout(60000); await helper.aNewPad(); confirm = helper.padChrome$.window.confirm; helper.padChrome$.window.confirm = () => true; diff --git a/src/tests/frontend/specs/importindents.js b/src/tests/frontend/specs/importindents.js index 434c86b5f1d..0d04d16d449 100644 --- a/src/tests/frontend/specs/importindents.js +++ b/src/tests/frontend/specs/importindents.js @@ -2,7 +2,6 @@ describe('import indents functionality', function () { beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/indentation.js b/src/tests/frontend/specs/indentation.js index 892849ea221..80a0f914077 100644 --- a/src/tests/frontend/specs/indentation.js +++ b/src/tests/frontend/specs/indentation.js @@ -3,7 +3,6 @@ describe('indentation button', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/italic.js b/src/tests/frontend/specs/italic.js index 706cfffc9fc..9b7b00b9fec 100644 --- a/src/tests/frontend/specs/italic.js +++ b/src/tests/frontend/specs/italic.js @@ -3,7 +3,6 @@ describe('italic some text', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/language.js b/src/tests/frontend/specs/language.js index d595206ddf0..0f0e3f34540 100644 --- a/src/tests/frontend/specs/language.js +++ b/src/tests/frontend/specs/language.js @@ -6,7 +6,6 @@ describe('Language select and change', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js b/src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js index d2ee3853c11..e0c86fa439e 100755 --- a/src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js +++ b/src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js @@ -3,7 +3,6 @@ describe('author of pad edition', function () { // author 1 creates a new pad with some content (regular lines and lists) before(async function () { - this.timeout(60000); const padId = await helper.aNewPad(); // make sure pad has at least 3 lines diff --git a/src/tests/frontend/specs/ordered_list.js b/src/tests/frontend/specs/ordered_list.js index cc0e1bbcbf2..21331596f70 100644 --- a/src/tests/frontend/specs/ordered_list.js +++ b/src/tests/frontend/specs/ordered_list.js @@ -4,7 +4,6 @@ describe('ordered_list.js', function () { describe('assign ordered list', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); @@ -169,7 +168,6 @@ describe('ordered_list.js', function () { describe('Pressing Tab in an OL increases and decreases indentation', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); @@ -204,7 +202,6 @@ describe('ordered_list.js', function () { 'decreases indentation and bullet / ol formatting', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/pad_modal.js b/src/tests/frontend/specs/pad_modal.js index 4b4a89c6962..735abe8c04b 100644 --- a/src/tests/frontend/specs/pad_modal.js +++ b/src/tests/frontend/specs/pad_modal.js @@ -5,7 +5,6 @@ describe('Pad modal', function () { const MODAL_SELECTOR = '#connectivity'; beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); // force a "slowcommit" error @@ -46,7 +45,6 @@ describe('Pad modal', function () { const MODAL_SELECTOR = '#settings'; beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); await openSettingsAndWaitForModalToBeVisible(); }); diff --git a/src/tests/frontend/specs/redo.js b/src/tests/frontend/specs/redo.js index bf871a08c17..bbe85a7ea4a 100644 --- a/src/tests/frontend/specs/redo.js +++ b/src/tests/frontend/specs/redo.js @@ -2,7 +2,6 @@ describe('undo button then redo button', function () { beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/scrollTo.js b/src/tests/frontend/specs/scrollTo.js index 199a4c0bf6b..e62582c0b4f 100755 --- a/src/tests/frontend/specs/scrollTo.js +++ b/src/tests/frontend/specs/scrollTo.js @@ -4,7 +4,6 @@ describe('scrollTo.js', function () { describe('scrolls to line', function () { // create a new pad with URL hash set before each test run before(async function () { - this.timeout(60000); await helper.aNewPad({hash: 'L4'}); }); @@ -21,7 +20,6 @@ describe('scrollTo.js', function () { describe('doesnt break on weird hash input', function () { // create a new pad with URL hash set before each test run before(async function () { - this.timeout(60000); await helper.aNewPad({hash: '#DEEZ123123NUTS'}); }); diff --git a/src/tests/frontend/specs/select_formatting_buttons.js b/src/tests/frontend/specs/select_formatting_buttons.js index aac8797811c..28921357d3b 100644 --- a/src/tests/frontend/specs/select_formatting_buttons.js +++ b/src/tests/frontend/specs/select_formatting_buttons.js @@ -6,7 +6,6 @@ describe('select formatting buttons when selection has style applied', function const FIRST_LINE = 0; before(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/strikethrough.js b/src/tests/frontend/specs/strikethrough.js index 7e96b302d38..4c33244cd49 100644 --- a/src/tests/frontend/specs/strikethrough.js +++ b/src/tests/frontend/specs/strikethrough.js @@ -3,7 +3,6 @@ describe('strikethrough button', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/timeslider.js b/src/tests/frontend/specs/timeslider.js index 9a2e8df35a9..350965720af 100644 --- a/src/tests/frontend/specs/timeslider.js +++ b/src/tests/frontend/specs/timeslider.js @@ -3,7 +3,6 @@ // deactivated, we need a nice way to get the timeslider, this is ugly xdescribe('timeslider button takes you to the timeslider of a pad', function () { beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/timeslider_numeric_padID.js b/src/tests/frontend/specs/timeslider_numeric_padID.js index 8138c952c49..a1ba6728f30 100644 --- a/src/tests/frontend/specs/timeslider_numeric_padID.js +++ b/src/tests/frontend/specs/timeslider_numeric_padID.js @@ -9,7 +9,6 @@ describe('timeslider', function () { }); it('Makes sure the export URIs are as expected when the padID is numeric', async function () { - this.timeout(60000); await helper.edit('a\n'); await helper.gotoTimeslider(1); diff --git a/src/tests/frontend/specs/timeslider_revisions.js b/src/tests/frontend/specs/timeslider_revisions.js index 03a05b236f7..8c4f07458d5 100644 --- a/src/tests/frontend/specs/timeslider_revisions.js +++ b/src/tests/frontend/specs/timeslider_revisions.js @@ -3,7 +3,6 @@ describe('timeslider', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/undo.js b/src/tests/frontend/specs/undo.js index 2f2a45ee1a3..d853eaf7ba0 100644 --- a/src/tests/frontend/specs/undo.js +++ b/src/tests/frontend/specs/undo.js @@ -2,7 +2,6 @@ describe('undo button', function () { beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/unordered_list.js b/src/tests/frontend/specs/unordered_list.js index 0a89c089c9e..9e5439a7782 100644 --- a/src/tests/frontend/specs/unordered_list.js +++ b/src/tests/frontend/specs/unordered_list.js @@ -4,7 +4,6 @@ describe('unordered_list.js', function () { describe('assign unordered list', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); @@ -30,7 +29,6 @@ describe('unordered_list.js', function () { describe('unassign unordered list', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); @@ -57,7 +55,6 @@ describe('unordered_list.js', function () { describe('keep unordered list on enter key', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); @@ -87,7 +84,6 @@ describe('unordered_list.js', function () { describe('Pressing Tab in an UL increases and decreases indentation', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); @@ -121,7 +117,6 @@ describe('unordered_list.js', function () { 'and bullet / ol formatting', function () { // create a new pad before each test run beforeEach(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/urls_become_clickable.js b/src/tests/frontend/specs/urls_become_clickable.js index e2a4e14c358..f170a2a41d8 100644 --- a/src/tests/frontend/specs/urls_become_clickable.js +++ b/src/tests/frontend/specs/urls_become_clickable.js @@ -6,7 +6,6 @@ describe('urls', function () { const txt = () => helper.padInner$('div').first(); before(async function () { - this.timeout(60000); await helper.aNewPad(); }); diff --git a/src/tests/frontend/specs/xxauto_reconnect.js b/src/tests/frontend/specs/xxauto_reconnect.js index 97b8bdcaa80..c04ca9d2974 100644 --- a/src/tests/frontend/specs/xxauto_reconnect.js +++ b/src/tests/frontend/specs/xxauto_reconnect.js @@ -4,7 +4,6 @@ describe('Automatic pad reload on Force Reconnect message', function () { let padId, $originalPadFrame; beforeEach(async function () { - this.timeout(60000); padId = await helper.aNewPad(); // enable userdup error to have timer to force reconnect From a7968115581e20ef47a533e030f59f830486bdfa Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Tue, 6 Apr 2021 12:52:04 +0200 Subject: [PATCH 044/218] escape userId before setting it as HTML attribute --- src/static/js/chat.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/static/js/chat.js b/src/static/js/chat.js index a3c47561682..00811ff44b7 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -129,6 +129,7 @@ exports.chat = (() => { 'Replacing with "unknown". This may be a bug or a database corruption.'); } + msg.userId = padutils.escapeHtml(msg.userId); const authorClass = `author-${msg.userId.replace(/[^a-y0-9]/g, (c) => { if (c === '.') return '-'; return `z${c.charCodeAt(0)}z`; From e31da37d0003accd303ef9eb557b5bd92f62db14 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 8 Apr 2021 14:54:44 +0200 Subject: [PATCH 045/218] Localisation updates from https://translatewiki.net. --- src/locales/bn.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/locales/bn.json b/src/locales/bn.json index 88f87207e36..e30734c2d26 100644 --- a/src/locales/bn.json +++ b/src/locales/bn.json @@ -12,11 +12,11 @@ ] }, "admin_plugins.available_fetching": "আনা হচ্ছে...", - "admin_plugins.available_install.value": "ইন্সটল করুন", + "admin_plugins.available_install.value": "ইনস্টল করুন", "admin_plugins.description": "বিবরণ", "admin_plugins.installed": "ইন্সটল হওয়া প্লাগিনসমূহ", "admin_plugins.installed_fetching": "ইন্সটলকৃত প্লাগিন আনা হচ্ছে", - "admin_plugins.installed_uninstall.value": "আনইন্সটল করুন", + "admin_plugins.installed_uninstall.value": "আনইনস্টল করুন", "admin_plugins.last-update": "সর্বশেষ হালনাগাদ", "admin_plugins.name": "নাম", "admin_plugins.version": "সংস্করণ", @@ -52,7 +52,7 @@ "pad.permissionDenied": "দুঃখিত, এ প্যাড-টি দেখার অধিকার আপনার নেই", "pad.settings.padSettings": "প্যাডের স্থাপন", "pad.settings.myView": "আমার দৃশ্য", - "pad.settings.stickychat": "চ্যাট সক্রীনে প্রদর্শন করা হবে", + "pad.settings.stickychat": "সর্বদা পর্দায় চ্যাট দেখান", "pad.settings.chatandusers": "চ্যাট এবং ব্যবহারকারী দেখান", "pad.settings.colorcheck": "লেখকদের নিজস্ব নির্বাচিত রং", "pad.settings.linenocheck": "লাইন নম্বর", @@ -61,19 +61,19 @@ "pad.settings.fontType.normal": "সাধারণ", "pad.settings.language": "ভাষা:", "pad.settings.about": "পরিচিতি", - "pad.settings.poweredBy": "$1 দ্বারা চালিত", + "pad.settings.poweredBy": "এটি দ্বারা চালিত:", "pad.importExport.import_export": "আমদানি/রপ্তানি", "pad.importExport.import": "কোন টেক্সট ফাইল বা নথি আপলোড করুন", "pad.importExport.importSuccessful": "সফল!", - "pad.importExport.export": "এই প্যাডটি রপ্তানি করুন:", + "pad.importExport.export": "এইরূপে এই প্যাডটি রপ্তানি করুন:", "pad.importExport.exportetherpad": "ইথারপ্যাড", "pad.importExport.exporthtml": "এইচটিএমএল", "pad.importExport.exportplain": "সাধারণ লেখা", "pad.importExport.exportword": "মাইক্রোসফট ওয়ার্ড", "pad.importExport.exportpdf": "পিডিএফ", "pad.importExport.exportopen": "ওডিএফ (ওপেন ডকুমেন্ট ফরম্যাট)", - "pad.modals.connected": "যোগাযোগ সফল", - "pad.modals.reconnecting": "আপনার প্যাডের সাথে সংযোগস্থাপন করা হচ্ছে..", + "pad.modals.connected": "সংযোগস্থাপন করা হয়েছে।", + "pad.modals.reconnecting": "আপনার প্যাডের সাথে সংযোগস্থাপন করা হচ্ছে…", "pad.modals.forcereconnect": "পুনরায় সংযোগস্থাপনের চেষ্টা", "pad.modals.userdup": "অন্য উইন্ডো-তে খোলা হয়েছে", "pad.modals.unauth": "আপনার অধিকার নেই", From 09c349e2a1342ddce2a4f6063bab4f27fc0275ac Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 29 Mar 2021 19:40:03 -0400 Subject: [PATCH 046/218] import: Use a Set for supported elements --- src/node/utils/ImportEtherpad.js | 4 ++-- src/static/js/contentcollector.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/node/utils/ImportEtherpad.js b/src/node/utils/ImportEtherpad.js index d47443733da..459d5a38b08 100644 --- a/src/node/utils/ImportEtherpad.js +++ b/src/node/utils/ImportEtherpad.js @@ -25,7 +25,7 @@ exports.setPadRaw = (padId, r) => { // get supported block Elements from plugins, we will use this later. hooks.callAll('ccRegisterBlockElements').forEach((element) => { - supportedElems.push(element); + supportedElems.add(element); }); Object.keys(records).forEach(async (key) => { @@ -64,7 +64,7 @@ exports.setPadRaw = (padId, r) => { if (value.pool) { for (const attrib of Object.keys(value.pool.numToAttrib)) { const attribName = value.pool.numToAttrib[attrib][0]; - if (supportedElems.indexOf(attribName) === -1) { + if (!supportedElems.has(attribName)) { console.warn('Plugin missing: ' + `You might want to install a plugin to support this node name: ${attribName}`); } diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index cf921cd4777..1b7ef51d36c 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -56,7 +56,7 @@ const getAttribute = (n, a) => { return null; }; // supportedElems are Supported natively within Etherpad and don't require a plugin -const supportedElems = [ +const supportedElems = new Set([ 'author', 'b', 'bold', @@ -76,7 +76,7 @@ const supportedElems = [ 'span', 'u', 'ul', -]; +]); const makeContentCollector = (collectStyles, abrowser, apool, className2Author) => { const _blockElems = { @@ -88,7 +88,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) hooks.callAll('ccRegisterBlockElements').forEach((element) => { _blockElems[element] = 1; - supportedElems.push(element); + supportedElems.add(element); }); const isBlockElement = (n) => !!_blockElems[tagName(n) || '']; @@ -339,7 +339,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) state.localAttribs = null; const isBlock = isBlockElement(node); if (!isBlock && node.name && (node.name !== 'body')) { - if (supportedElems.indexOf(node.name) === -1) { + if (!supportedElems.has(node.name)) { console.warn('Plugin missing: ' + `You might want to install a plugin to support this node name: ${node.name}`); } From 91e99c84caf3b4887d014a03ec1ca57e06716723 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 29 Mar 2021 20:06:33 -0400 Subject: [PATCH 047/218] import: Reduce log spam from unsupported elements --- src/node/utils/ImportEtherpad.js | 15 +++++++++++---- src/static/js/contentcollector.js | 12 ++++++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/node/utils/ImportEtherpad.js b/src/node/utils/ImportEtherpad.js index 459d5a38b08..b90718a5780 100644 --- a/src/node/utils/ImportEtherpad.js +++ b/src/node/utils/ImportEtherpad.js @@ -18,8 +18,11 @@ const db = require('../db/DB'); const hooks = require('../../static/js/pluginfw/hooks'); +const log4js = require('log4js'); const supportedElems = require('../../static/js/contentcollector').supportedElems; +const logger = log4js.getLogger('ImportEtherpad'); + exports.setPadRaw = (padId, r) => { const records = JSON.parse(r); @@ -28,6 +31,8 @@ exports.setPadRaw = (padId, r) => { supportedElems.add(element); }); + const unsupportedElements = new Set(); + Object.keys(records).forEach(async (key) => { let value = records[key]; @@ -64,10 +69,7 @@ exports.setPadRaw = (padId, r) => { if (value.pool) { for (const attrib of Object.keys(value.pool.numToAttrib)) { const attribName = value.pool.numToAttrib[attrib][0]; - if (!supportedElems.has(attribName)) { - console.warn('Plugin missing: ' + - `You might want to install a plugin to support this node name: ${attribName}`); - } + if (!supportedElems.has(attribName)) unsupportedElements.add(attribName); } } const oldPadId = key.split(':'); @@ -92,4 +94,9 @@ exports.setPadRaw = (padId, r) => { // Write the value to the server await db.set(newKey, value); }); + + if (unsupportedElements.size) { + logger.warn('Ignoring unsupported elements (you might want to install a plugin): ' + + `${[...unsupportedElements].join(', ')}`); + } }; diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index 1b7ef51d36c..c02e7388748 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -318,6 +318,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) cc.incrementAttrib(state, na); }; cc.collectContent = function (node, state) { + let unsupportedElements = null; if (!state) { state = { flags: { /* name -> nesting counter*/ @@ -333,16 +334,15 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) 'list': 'bullet1', */ }, + unsupportedElements: new Set(), }; + unsupportedElements = state.unsupportedElements; } const localAttribs = state.localAttribs; state.localAttribs = null; const isBlock = isBlockElement(node); if (!isBlock && node.name && (node.name !== 'body')) { - if (!supportedElems.has(node.name)) { - console.warn('Plugin missing: ' + - `You might want to install a plugin to support this node name: ${node.name}`); - } + if (!supportedElems.has(node.name)) state.unsupportedElements.add(node.name); } const isEmpty = _isEmpty(node, state); if (isBlock) _ensureColumnZero(state); @@ -621,6 +621,10 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) } } state.localAttribs = localAttribs; + if (unsupportedElements && unsupportedElements.size) { + console.warn('Ignoring unsupported elements (you might want to install a plugin): ' + + `${[...unsupportedElements].join(', ')}`); + } }; // can pass a falsy value for end of doc cc.notifyNextNode = (node) => { From 1ad134a53867345c91afbb326965bd25e327d298 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 2 Sep 2020 20:50:19 -0400 Subject: [PATCH 048/218] PadMessageHandler: Improve logging of pre-CLIENT_READY drops This should make it easier to see what is emitting the the messages so it can be fixed. --- src/node/handler/PadMessageHandler.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 174a6bc5f07..d2f6fc7baea 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -210,8 +210,11 @@ exports.handleMessage = async (socket, message) => { const auth = thisSession.auth; if (!auth) { - console.error('Auth was never applied to a session. If you are using the ' + - 'stress-test tool then restart Etherpad and the Stress test tool.'); + const ip = settings.disableIPlogging ? 'ANONYMOUS' : (socket.request.ip || ''); + const msg = JSON.stringify(message, null, 2); + messageLogger.error(`Dropping pre-CLIENT_READY message from IP ${ip}: ${msg}`); + messageLogger.debug( + 'If you are using the stress-test tool then restart Etherpad and the Stress test tool.'); return; } From 74554d36a528c4e44da47d0f5b4bd722f8bbb401 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 8 Apr 2021 21:39:11 -0400 Subject: [PATCH 049/218] chat: Allow `chatNewMessage` hook to modify more values --- doc/api/hooks_client-side.md | 5 ++++ src/static/js/chat.js | 44 +++++++++++++++++------------------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/doc/api/hooks_client-side.md b/doc/api/hooks_client-side.md index e87536cfe34..2559a4e0862 100755 --- a/doc/api/hooks_client-side.md +++ b/doc/api/hooks_client-side.md @@ -294,6 +294,11 @@ Things in context: This hook is called on the client side whenever a chat message is received from the server. It can be used to create different notifications for chat messages. +Hoook functions can modify the `author`, `authorName`, `duration`, `sticky`, +`text`, and `timeStr` context properties to change how the message is processed. +The `text` and `timeStr` properties may contain HTML, but plugins should be +careful to sanitize any added user input to avoid introducing an XSS +vulnerability. ## collectContentPre diff --git a/src/static/js/chat.js b/src/static/js/chat.js index 00811ff44b7..c6b00af13a4 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -109,14 +109,6 @@ exports.chat = (() => { // correct the time msg.time += this._pad.clientTimeOffset; - // create the time string - let minutes = `${new Date(msg.time).getMinutes()}`; - let hours = `${new Date(msg.time).getHours()}`; - if (minutes.length === 1) minutes = `0${minutes}`; - if (hours.length === 1) hours = `0${hours}`; - const timeStr = `${hours}:${minutes}`; - - // create the authorclass if (!msg.userId) { /* * If, for a bug or a database corruption, the message coming from the @@ -129,25 +121,25 @@ exports.chat = (() => { 'Replacing with "unknown". This may be a bug or a database corruption.'); } - msg.userId = padutils.escapeHtml(msg.userId); - const authorClass = `author-${msg.userId.replace(/[^a-y0-9]/g, (c) => { + const authorClass = (authorId) => `author-${authorId.replace(/[^a-y0-9]/g, (c) => { if (c === '.') return '-'; return `z${c.charCodeAt(0)}z`; })}`; - const text = padutils.escapeHtmlWithClickableLinks(msg.text, '_blank'); - - const authorName = msg.userName == null ? html10n.get('pad.userlist.unnamed') - : padutils.escapeHtml(msg.userName); - // the hook args const ctx = { - authorName, + authorName: msg.userName != null ? msg.userName : html10n.get('pad.userlist.unnamed'), author: msg.userId, - text, + text: padutils.escapeHtmlWithClickableLinks(msg.text, '_blank'), sticky: false, timestamp: msg.time, - timeStr, + timeStr: (() => { + let minutes = `${new Date(msg.time).getMinutes()}`; + let hours = `${new Date(msg.time).getHours()}`; + if (minutes.length === 1) minutes = `0${minutes}`; + if (hours.length === 1) hours = `0${hours}`; + return `${hours}:${minutes}`; + })(), duration: 4000, }; @@ -160,7 +152,7 @@ exports.chat = (() => { // does this message contain this user's name? (is the curretn user mentioned?) const myName = $('#myusernameedit').val(); const wasMentioned = - text.toLowerCase().indexOf(myName.toLowerCase()) !== -1 && myName !== 'undefined'; + ctx.text.toLowerCase().indexOf(myName.toLowerCase()) !== -1 && myName !== 'undefined'; // If the user was mentioned, make the message sticky if (wasMentioned && !alreadyFocused && !isHistoryAdd && !chatOpen) { @@ -171,9 +163,14 @@ exports.chat = (() => { // Call chat message hook hooks.aCallAll('chatNewMessage', ctx, () => { + const cls = authorClass(ctx.author); const html = - `

${authorName}:` + - `${ctx.timeStr} ${ctx.text}

`; + `

` + + `${padutils.escapeHtml(ctx.authorName)}:` + + // ctx.text was HTML-escaped before calling the hook, and ctx.timeStr couldn't have had + // any HTML. Hook functions are trusted to not introduce an XSS vulnerability by adding + // unescaped user input to either ctx.text or ctx.timeStr. + `${ctx.timeStr} ${ctx.text}

`; if (isHistoryAdd) $(html).insertAfter('#chatloadmessagesbutton'); else $('#chattext').append(html); @@ -186,9 +183,10 @@ exports.chat = (() => { if (!chatOpen && ctx.duration > 0) { $.gritter.add({ - // Note: ctx.authorName and ctx.text are already HTML-escaped. text: $('

') - .append($('').addClass('author-name').html(ctx.authorName)) + .append($('').addClass('author-name').text(ctx.authorName)) + // ctx.text was HTML-escaped before calling the hook. Hook functions are trusted + // to not introduce an XSS vulnerability by adding unescaped user input. .append(ctx.text), sticky: ctx.sticky, time: 5000, From a3a0ff7bc193017dbcda0d151229bd46bf03ec37 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 8 Apr 2021 23:39:08 -0400 Subject: [PATCH 050/218] chat: Use jQuery to build the chat message DOM object This reduces the likelihood of accidentally introducing an XSS vulnerability. --- src/static/js/chat.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/static/js/chat.js b/src/static/js/chat.js index c6b00af13a4..42800a83c63 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -164,15 +164,22 @@ exports.chat = (() => { // Call chat message hook hooks.aCallAll('chatNewMessage', ctx, () => { const cls = authorClass(ctx.author); - const html = - `

` + - `${padutils.escapeHtml(ctx.authorName)}:` + - // ctx.text was HTML-escaped before calling the hook, and ctx.timeStr couldn't have had - // any HTML. Hook functions are trusted to not introduce an XSS vulnerability by adding - // unescaped user input to either ctx.text or ctx.timeStr. - `${ctx.timeStr} ${ctx.text}

`; - if (isHistoryAdd) $(html).insertAfter('#chatloadmessagesbutton'); - else $('#chattext').append(html); + const chatMsg = $('

') + .attr('data-authorId', ctx.author) + .addClass(cls) + .append($('').text(`${ctx.authorName}:`)) + .append($('') + .addClass('time') + .addClass(cls) + // Hook functions are trusted to not introduce an XSS vulnerability by adding + // unescaped user input to ctx.timeStr. + .html(ctx.timeStr)) + .append(' ') + // ctx.text was HTML-escaped before calling the hook. Hook functions are trusted to not + // introduce an XSS vulnerability by adding unescaped user input. + .append($('

').html(ctx.text).contents()); + if (isHistoryAdd) chatMsg.insertAfter('#chatloadmessagesbutton'); + else $('#chattext').append(chatMsg); // should we increment the counter?? if (increment && !isHistoryAdd) { From d01b593d3c10c9adb98662c5c7130401b587d0a1 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 8 Apr 2021 23:39:44 -0400 Subject: [PATCH 051/218] chat: Ensure that `ctx.text` is interpreted as HTML --- src/static/js/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/chat.js b/src/static/js/chat.js index 42800a83c63..1d16e75bf0d 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -194,7 +194,7 @@ exports.chat = (() => { .append($('').addClass('author-name').text(ctx.authorName)) // ctx.text was HTML-escaped before calling the hook. Hook functions are trusted // to not introduce an XSS vulnerability by adding unescaped user input. - .append(ctx.text), + .append($('
').html(ctx.text).contents()), sticky: ctx.sticky, time: 5000, position: 'bottom', From 7d5cad693291db49c7e0eaf62117c660dccc0c6a Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 12 Apr 2021 15:43:27 +0200 Subject: [PATCH 052/218] Localisation updates from https://translatewiki.net. --- src/locales/it.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/locales/it.json b/src/locales/it.json index 0b3914b472f..d6fb5c535ee 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -1,6 +1,7 @@ { "@metadata": { "authors": [ + "Ajeje Brazorf", "Beta16", "Gianfranco", "Jack", @@ -10,6 +11,7 @@ "Vituzzu" ] }, + "admin_plugins.name": "Nome", "index.newPad": "Nuovo pad", "index.createOpenPad": "o crea/apre un pad con il nome:", "index.openPad": "apri un Pad esistente col nome:", From 0d33793908e022347419d4f2e10e6e1edf31ce76 Mon Sep 17 00:00:00 2001 From: pcworld <0188801@gmail.com> Date: Sun, 11 Apr 2021 04:00:14 +0200 Subject: [PATCH 053/218] tests: readonly pastes must be readable+exportable with authentication readonly paste links should be readable even if authentication is turned on, as long as the user provides valid login data. This test currently fails. Also test that readonly paste IDs can be exported under the same condition, which currently succeeds. --- .../backend/specs/api/importexportGetPost.js | 34 ++++++++++--------- src/tests/backend/specs/socketio.js | 28 +++++++++++++++ 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/tests/backend/specs/api/importexportGetPost.js b/src/tests/backend/specs/api/importexportGetPost.js index 9261aafa6e0..a68ba40110e 100644 --- a/src/tests/backend/specs/api/importexportGetPost.js +++ b/src/tests/backend/specs/api/importexportGetPost.js @@ -109,22 +109,24 @@ describe(__filename, 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 () { - this.timeout(250); - const ro = await agent.get(`${endPoint('getReadOnlyID')}&padID=${testPadId}`) - .expect(200) - .expect((res) => assert.ok(JSON.parse(res.text).data.readOnlyID)); - const 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)); - }); - + for (const authn of [false, true]) { + it(`can export from read-only pad ID, authn ${authn}`, async function () { + this.timeout(250); + settings.requireAuthentication = authn; + const get = (ep) => { + let req = agent.get(ep); + if (authn) req = req.auth('user', 'user-password'); + return req.expect(200); + }; + const ro = await get(`${endPoint('getReadOnlyID')}&padID=${testPadId}`) + .expect((res) => assert.ok(JSON.parse(res.text).data.readOnlyID)); + const readOnlyId = JSON.parse(ro.text).data.readOnlyID; + await get(`/p/${readOnlyId}/export/html`) + .expect((res) => assert(res.text.indexOf('This is the') !== -1)); + await get(`/p/${readOnlyId}/export/txt`) + .expect((res) => assert(res.text.indexOf('This is the') !== -1)); + }); + } describe('Import/Export tests requiring AbiWord/LibreOffice', function () { this.timeout(10000); diff --git a/src/tests/backend/specs/socketio.js b/src/tests/backend/specs/socketio.js index 9899856e0c4..e19250e9260 100644 --- a/src/tests/backend/specs/socketio.js +++ b/src/tests/backend/specs/socketio.js @@ -5,6 +5,7 @@ const common = require('../common'); const io = require('socket.io-client'); const padManager = require('../../../node/db/PadManager'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); +const readOnlyManager = require('../../../node/db/ReadOnlyManager'); const setCookieParser = require('set-cookie-parser'); const settings = require('../../../node/utils/Settings'); @@ -168,6 +169,33 @@ describe(__filename, function () { const clientVars = await handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); + + for (const authn of [false, true]) { + const desc = authn ? 'authn user' : '!authn anonymous'; + it(`${desc} read-only /p/pad -> 200, ok`, async function () { + this.timeout(400); + const get = (ep) => { + let res = agent.get(ep); + if (authn) res = res.auth('user', 'user-password'); + return res.expect(200); + }; + settings.requireAuthentication = authn; + let res = await get('/p/pad'); + socket = await connect(res); + let clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + const readOnlyId = clientVars.data.readOnlyId; + assert(readOnlyManager.isReadOnlyId(readOnlyId)); + socket.close(); + res = await get(`/p/${readOnlyId}`); + socket = await connect(res); + clientVars = await handshake(socket, readOnlyId); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, true); + }); + } + it('authz user /p/pad -> 200, ok', async function () { this.timeout(400); settings.requireAuthentication = true; From 3c71e8983bcd967728e360f49b75c2321f7afe41 Mon Sep 17 00:00:00 2001 From: pcworld <0188801@gmail.com> Date: Sun, 11 Apr 2021 03:59:52 +0200 Subject: [PATCH 054/218] Fix read only pad access with authentication Before this commit, webaccess.checkAccess saved the authorization in user.padAuthorizations[padId] with padId being the read-only pad ID, however later stages, e.g. in PadMessageHandler, use the real pad ID for access checks. This led to authorization being denied. This commit fixes it by only storing and comparing the real pad IDs and not read-only pad IDs. This fixes test case "authn user readonly pad -> 200, ok" in src/tests/backend/specs/socketio.js. --- doc/api/hooks_server-side.md | 6 ++++-- src/node/db/SecurityManager.js | 10 ++++++++++ src/node/handler/PadMessageHandler.js | 10 +--------- src/node/hooks/express/webaccess.js | 21 +++++++++++++-------- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index e13adfa97f5..32a09ecf467 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -156,11 +156,13 @@ Called from: src/node/db/SecurityManager.js Things in context: -1. padID - the pad the user wants to access +1. padID - the real ID (never the read-only ID) of the pad the user wants to + access 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. +This hook gets called when the access to the concrete pad is being checked. +Return `false` to deny access. ## padCreate Called from: src/node/db/Pad.js diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 24d966d18ce..4851866d52b 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -22,6 +22,7 @@ const authorManager = require('./AuthorManager'); const hooks = require('../../static/js/pluginfw/hooks.js'); const padManager = require('./PadManager'); +const readOnlyManager = require('./ReadOnlyManager'); const sessionManager = require('./SessionManager'); const settings = require('../utils/Settings'); const webaccess = require('../hooks/express/webaccess'); @@ -56,6 +57,15 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => { let canCreate = !settings.editOnly; + if (readOnlyManager.isReadOnlyId(padID)) { + canCreate = false; + padID = await readOnlyManager.getPadId(padID); + if (padID == null) { + authLogger.debug('access denied: read-only pad ID for a pad that does not exist'); + return DENY; + } + } + // Authentication and authorization checks. if (settings.loadTest) { console.warn( diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index d2f6fc7baea..0e6869e8894 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -218,17 +218,9 @@ exports.handleMessage = async (socket, message) => { return; } - // check if pad is requested via readOnly - let padId = auth.padID; - - if (padId.indexOf('r.') === 0) { - // Pad is readOnly, first get the real Pad ID - padId = await readOnlyManager.getPadId(padId); - } - const {session: {user} = {}} = socket.client.request; const {accessStatus, authorID} = - await securityManager.checkAccess(padId, auth.sessionID, auth.token, user); + await securityManager.checkAccess(auth.padID, auth.sessionID, auth.token, user); if (accessStatus !== 'grant') { // Access denied. Send the reason to the user. socket.json.send({accessStatus}); diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 5ff957a5289..99210d9c57a 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -70,14 +70,19 @@ const checkAccess = async (req, res, next) => { // This helper is used in steps 2 and 4 below, so it may be called twice per access: once before // authentication is checked and once after (if settings.requireAuthorization is true). const authorize = async () => { - const grant = (level) => { + const grant = async (level) => { level = exports.normalizeAuthzLevel(level); if (!level) return false; const user = req.session.user; if (user == null) return true; // This will happen if authentication is not required. const encodedPadId = (req.path.match(/^\/p\/([^/]*)/) || [])[1]; if (encodedPadId == null) return true; - const padId = decodeURIComponent(encodedPadId); + let padId = decodeURIComponent(encodedPadId); + if (readOnlyManager.isReadOnlyId(padId)) { + // pad is read-only, first get the real pad ID + padId = await readOnlyManager.getPadId(padId); + if (padId == null) return false; + } // 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 = {}; @@ -85,13 +90,13 @@ const checkAccess = async (req, res, next) => { return true; }; const isAuthenticated = req.session && req.session.user; - if (isAuthenticated && req.session.user.is_admin) return grant('create'); + if (isAuthenticated && req.session.user.is_admin) return await 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'); - return grant(await aCallFirst0('authorize', {req, res, next, resource: req.path})); + if (!requireAuthn) return await grant('create'); + if (!isAuthenticated) return await grant(false); + if (requireAdmin && !req.session.user.is_admin) return await grant(false); + if (!settings.requireAuthorization) return await grant('create'); + return await grant(await aCallFirst0('authorize', {req, res, next, resource: req.path})); }; // /////////////////////////////////////////////////////////////////////////////////////////////// From f63610bb12f882a026cac4dacaff3b9f00faac30 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 12 Apr 2021 15:31:36 -0400 Subject: [PATCH 055/218] tests: Test access bypass via read-only pad ID --- src/tests/backend/specs/socketio.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/tests/backend/specs/socketio.js b/src/tests/backend/specs/socketio.js index e19250e9260..9b9e2101b16 100644 --- a/src/tests/backend/specs/socketio.js +++ b/src/tests/backend/specs/socketio.js @@ -231,6 +231,24 @@ describe(__filename, function () { const message = await handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); + + it('authn anonymous read-only /p/pad -> 401, error', async function () { + this.timeout(400); + settings.requireAuthentication = true; + let 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'); + const readOnlyId = clientVars.data.readOnlyId; + assert(readOnlyManager.isReadOnlyId(readOnlyId)); + socket.close(); + res = await agent.get(`/p/${readOnlyId}`).expect(401); + // Despite the 401, try to read the pad via a socket.io connection anyway. + socket = await connect(res); + const message = await handshake(socket, readOnlyId); + assert.equal(message.accessStatus, 'deny'); + }); + it('authn !cookie -> error', async function () { this.timeout(400); settings.requireAuthentication = true; From 329d03743151602e3014932ff16585c8a9f80132 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 12 Apr 2021 17:57:44 -0400 Subject: [PATCH 056/218] Simplify read-only pad ID checks --- src/node/db/ReadOnlyManager.js | 4 ++-- src/node/hooks/express/specialpages.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/node/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.js index 5dabe2e0662..0b8d2917164 100644 --- a/src/node/db/ReadOnlyManager.js +++ b/src/node/db/ReadOnlyManager.js @@ -28,7 +28,7 @@ const randomString = require('../utils/randomstring'); * checks if the id pattern matches a read-only pad id * @param {String} the pad's id */ -exports.isReadOnlyId = (id) => id.indexOf('r.') === 0; +exports.isReadOnlyId = (id) => id.startsWith('r.'); /** * returns a read only id for a pad @@ -59,7 +59,7 @@ exports.getPadId = (readOnlyId) => db.get(`readonly2pad:${readOnlyId}`); * @param {String} padIdOrReadonlyPadId read only id or real pad id */ exports.getIds = async (id) => { - const readonly = (id.indexOf('r.') === 0); + const readonly = exports.isReadOnlyId(id); // Might be null, if this is an unknown read-only id const readOnlyPadId = readonly ? id : await exports.getReadOnlyId(id); diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js index c1e3fec9964..f5518088857 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.js @@ -46,8 +46,7 @@ exports.expressCreateServer = (hookName, args, cb) => { // 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); + const isReadOnly = !webaccess.userCanModify(req.params.pad, req); hooks.callAll('padInitToolbar', { toolbar, From 951d369e3fe0d3dffb866bfbdc62c2b536818036 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 12 Apr 2021 17:30:05 -0400 Subject: [PATCH 057/218] padaccess: Delete useless try/catch --- src/node/padaccess.js | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/node/padaccess.js b/src/node/padaccess.js index 5ca5641b508..e9cc7cde5ec 100644 --- a/src/node/padaccess.js +++ b/src/node/padaccess.js @@ -3,21 +3,16 @@ const securityManager = require('./db/SecurityManager'); // checks for padAccess module.exports = async (req, res) => { - try { - const {session: {user} = {}} = req; - const accessObj = await securityManager.checkAccess( - req.params.pad, req.cookies.sessionID, req.cookies.token, user); + const {session: {user} = {}} = req; + const accessObj = await securityManager.checkAccess( + req.params.pad, req.cookies.sessionID, req.cookies.token, user); - if (accessObj.accessStatus === 'grant') { - // there is access, continue - return true; - } else { - // no access - res.status(403).send("403 - Can't touch this"); - return false; - } - } catch (err) { - // @TODO - send internal server error here? - throw err; + if (accessObj.accessStatus === 'grant') { + // there is access, continue + return true; + } else { + // no access + res.status(403).send("403 - Can't touch this"); + return false; } }; From a001a13411e377773b1e17ebda04585cb4b05070 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 13 Apr 2021 10:10:56 +0200 Subject: [PATCH 058/218] fix(perf): Disable wtfnode dump by default Consumes a lot of CPU so it's better to enable it on purpose --- settings.json.docker | 5 +++++ settings.json.template | 5 +++++ src/node/server.js | 29 +++++++++++++++++++++-------- src/node/utils/Settings.js | 5 +++++ 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/settings.json.docker b/settings.json.docker index 7bb2917c293..228c70d2a08 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -463,6 +463,11 @@ */ "loadTest": "${LOAD_TEST:false}", + /** + * Disable dump of objects preventing a clean exit + */ + "dumpOnUncleanExit": false, + /* * Disable indentation on new line when previous line ends with some special * chars (':', '[', '(', '{') diff --git a/settings.json.template b/settings.json.template index b8722fbdcf3..8a676b56dd6 100644 --- a/settings.json.template +++ b/settings.json.template @@ -468,6 +468,11 @@ */ "loadTest": false, + /** + * Disable dump of objects preventing a clean exit + */ + "dumpOnUncleanExit": false, + /* * Disable indentation on new line when previous line ends with some special * chars (':', '[', '(', '{') diff --git a/src/node/server.js b/src/node/server.js index a574116ebc7..f895df2af90 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -27,9 +27,14 @@ const log4js = require('log4js'); log4js.replaceConsole(); -// wtfnode should be loaded after log4js.replaceConsole() so that it uses log4js for logging, and it -// should be above everything else so that it can hook in before resources are used. -const wtfnode = require('wtfnode'); +const settings = require('./utils/Settings'); + +let wtfnode; +if (settings.dumpOnUncleanExit) { + // wtfnode should be loaded after log4js.replaceConsole() so that it uses log4js for logging, and + // it should be above everything else so that it can hook in before resources are used. + wtfnode = require('wtfnode'); +} /* * early check for version compatibility before calling @@ -45,7 +50,6 @@ const express = require('./hooks/express'); const hooks = require('../static/js/pluginfw/hooks'); const pluginDefs = require('../static/js/pluginfw/plugin_defs'); const plugins = require('../static/js/pluginfw/plugins'); -const settings = require('./utils/Settings'); const stats = require('./stats'); const logger = log4js.getLogger('server'); @@ -248,16 +252,25 @@ exports.exit = async (err = null) => { exitGate = new Gate(); state = State.EXITING; exitGate.resolve(); + // Node.js should exit on its own without further action. Add a timeout to force Node.js to exit - // just in case something failed to get cleaned up during the shutdown hook. unref() is called on - // the timeout so that the timeout itself does not prevent Node.js from exiting. + // just in case something failed to get cleaned up during the shutdown hook. unref() is called + // on the timeout so that the timeout itself does not prevent Node.js from exiting. setTimeout(() => { logger.error('Something that should have been cleaned up during the shutdown hook (such as ' + - 'a timer, worker thread, or open connection) is preventing Node.js from exiting'); - wtfnode.dump(); + 'a timer, worker thread, or open connection) is preventing Node.js from exiting'); + + if (settings.dumpOnUncleanExit) { + wtfnode.dump(); + } else { + logger.error('Enable `dumpOnUncleanExit` setting to get a dump of objects preventing a ' + + 'clean exit'); + } + logger.error('Forcing an unclean exit...'); process.exit(1); }, 5000).unref(); + logger.info('Waiting for Node.js to exit...'); state = State.WAITING_FOR_EXIT; /* eslint-enable no-process-exit */ diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 0333f7972dd..60f4e64421f 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -250,6 +250,11 @@ exports.automaticReconnectionTimeout = 0; */ exports.loadTest = false; +/** + * Disable dump of objects preventing a clean exit + */ +exports.dumpOnUncleanExit = false; + /** * Enable indentation on new lines */ From 20c512c8a90f2653458e1c4cf4b02df2a266977c Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Sun, 18 Apr 2021 00:50:35 +0200 Subject: [PATCH 059/218] test for await db.set in createAuthor --- src/tests/backend/specs/regression-db.js | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/tests/backend/specs/regression-db.js diff --git a/src/tests/backend/specs/regression-db.js b/src/tests/backend/specs/regression-db.js new file mode 100644 index 00000000000..221193c3b87 --- /dev/null +++ b/src/tests/backend/specs/regression-db.js @@ -0,0 +1,31 @@ +'use strict'; + +const AuthorManager = require('../../../node/db/AuthorManager'); +const assert = require('assert').strict; +const common = require('../common'); +const db = require('../../../node/db/DB'); + +describe(__filename, function () { + let setBackup; + + before(async function () { + await common.init(); + setBackup = db.set; + + db.set = async (...args) => { + // delay db.set + await new Promise((resolve) => { setTimeout(() => resolve(), 500); }); + return await setBackup.call(db, ...args); + }; + }); + + after(async function () { + db.set = setBackup; + }); + + it('regression test for missing await in createAuthor (#5000)', async function () { + this.timeout(700); + const {authorID} = await AuthorManager.createAuthor(); // Should block until db.set() finishes. + assert(await AuthorManager.doesAuthorExist(authorID)); + }); +}); From 35797e57fc16b53ac1567ac4591dce1eef5b76f8 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Sun, 18 Apr 2021 00:52:56 +0200 Subject: [PATCH 060/218] AuthorManager: await db.set in createAuthor --- src/node/db/AuthorManager.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index 3fde1e4b1fd..d56ba2f101a 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -156,13 +156,13 @@ async function mapAuthorWithDBKey(mapperkey, mapper) { // return the author return {authorID: author}; -} +}; /** * Internal function that creates the database entry for an author * @param {String} name The name of the author */ -exports.createAuthor = (name) => { +exports.createAuthor = async (name) => { // create the new author name const author = `a.${randomString(16)}`; @@ -174,8 +174,7 @@ exports.createAuthor = (name) => { }; // 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); + await db.set(`globalAuthor:${author}`, authorObj); return {authorID: author}; }; From 3a5af19492ad3f85166f7dbecb47dab404794224 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Sun, 18 Apr 2021 00:56:22 +0200 Subject: [PATCH 061/218] AuthorManager: await for more db methods --- src/node/db/AuthorManager.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index d56ba2f101a..de868b97d52 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -183,34 +183,35 @@ exports.createAuthor = async (name) => { * Returns the Author Obj of the author * @param {String} author The id of the author */ -exports.getAuthor = (author) => db.get(`globalAuthor:${author}`); +exports.getAuthor = async (author) => await db.get(`globalAuthor:${author}`); /** * Returns the color Id of the author * @param {String} author The id of the author */ -exports.getAuthorColorId = (author) => db.getSub(`globalAuthor:${author}`, ['colorId']); +exports.getAuthorColorId = async (author) => await db.getSub(`globalAuthor:${author}`, ['colorId']); /** * Sets the color Id of the author * @param {String} author The id of the author * @param {String} colorId The color id of the author */ -exports.setAuthorColorId = (author, colorId) => db.setSub( +exports.setAuthorColorId = async (author, colorId) => await db.setSub( `globalAuthor:${author}`, ['colorId'], colorId); /** * Returns the name of the author * @param {String} author The id of the author */ -exports.getAuthorName = (author) => db.getSub(`globalAuthor:${author}`, ['name']); +exports.getAuthorName = async (author) => await db.getSub(`globalAuthor:${author}`, ['name']); /** * Sets the name of the author * @param {String} author The id of the author * @param {String} name The name of the author */ -exports.setAuthorName = (author, name) => db.setSub(`globalAuthor:${author}`, ['name'], name); +exports.setAuthorName = async (author, name) => await db.setSub( + `globalAuthor:${author}`, ['name'], name); /** * Returns an array of all pads this author contributed to @@ -260,7 +261,7 @@ exports.addPad = async (authorID, padID) => { author.padIDs[padID] = 1; // anything, because value is not used // save the new element back - db.set(`globalAuthor:${authorID}`, author); + await db.set(`globalAuthor:${authorID}`, author); }; /** From 96208e8239c5c064e62856c943ceb409448d535b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 20 Apr 2021 12:19:32 -0400 Subject: [PATCH 062/218] tests: Rename workflow to "Upgrade from latest release" --- ...it-pull-update.yml => upgrade-from-latest-release.yml} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename .github/workflows/{major-version-git-pull-update.yml => upgrade-from-latest-release.yml} (94%) diff --git a/.github/workflows/major-version-git-pull-update.yml b/.github/workflows/upgrade-from-latest-release.yml similarity index 94% rename from .github/workflows/major-version-git-pull-update.yml rename to .github/workflows/upgrade-from-latest-release.yml index 5d3b0d748c6..05e0e8f7d04 100644 --- a/.github/workflows/major-version-git-pull-update.yml +++ b/.github/workflows/upgrade-from-latest-release.yml @@ -1,4 +1,4 @@ -name: "In-place git pull from master" +name: "Upgrade from latest release" # any branch is useful for testing before a PR is submitted on: [push, pull_request] @@ -19,7 +19,7 @@ jobs: node: [10, 12, 14, 15] steps: - - name: Checkout master repository + - name: Check out latest release uses: actions/checkout@v2 with: ref: master @@ -60,10 +60,10 @@ jobs: - name: Run the backend tests run: cd src && npm test - - name: Git fetch + - name: Fetch the new Git commits run: git fetch - - name: Checkout this branch over master + - name: Upgrade to the new Git revision run: git checkout "${GITHUB_SHA}" - name: Install all dependencies and symlink for ep_etherpad-lite From c2ac5e6145a6a8c0b4484c2653666b16db97a6ea Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 20 Apr 2021 12:20:18 -0400 Subject: [PATCH 063/218] tests: Fix missing commit in "Upgrade from latest release" workflow --- .github/workflows/upgrade-from-latest-release.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/upgrade-from-latest-release.yml b/.github/workflows/upgrade-from-latest-release.yml index 05e0e8f7d04..d32c51f9731 100644 --- a/.github/workflows/upgrade-from-latest-release.yml +++ b/.github/workflows/upgrade-from-latest-release.yml @@ -60,10 +60,18 @@ jobs: - name: Run the backend tests run: cd src && npm test + # Because actions/checkout@v2 is called with "ref: master" and without + # "fetch-depth: 0", the local clone does not have the ${GITHUB_SHA} commit. + # Fetch ${GITHUB_REF} to get the ${GITHUB_SHA} commit. Note that a plain + # "git fetch" only fetches "normal" references (refs/heads/* and + # refs/tags/*), and for pull requests none of the normal references include + # ${GITHUB_SHA}, so we have to explicitly tell Git to fetch ${GITHUB_REF}. - name: Fetch the new Git commits - run: git fetch + run: git fetch --depth=1 origin "${GITHUB_REF}" - name: Upgrade to the new Git revision + # For pull requests, ${GITHUB_SHA} is the automatically generated merge + # commit that merges the PR's source branch to its destination branch. run: git checkout "${GITHUB_SHA}" - name: Install all dependencies and symlink for ep_etherpad-lite From d0d4b959809051b37f524eb42eaf3f9f73be2040 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 19 Apr 2021 22:33:23 -0400 Subject: [PATCH 064/218] favicon: Only serve from `/favicon.ico` --- src/node/hooks/express/specialpages.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js index f5518088857..b49132e9137 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.js @@ -74,8 +74,7 @@ exports.expressCreateServer = (hookName, args, cb) => { })); }); - // serve favicon.ico from all path levels except as a pad name - args.app.get(/\/favicon.ico$/, (req, res) => { + args.app.get('/favicon.ico', (req, res) => { let filePath = path.join( settings.root, 'src', From 92e0bff80c3232a50056dddc7aab35f4ea750926 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 19 Apr 2021 22:47:02 -0400 Subject: [PATCH 065/218] favicon: Refactor handler and add tests --- src/node/hooks/express/specialpages.js | 36 +++++++----- src/tests/backend/specs/favicon-test-skin.png | Bin 0 -> 419 bytes src/tests/backend/specs/favicon.js | 55 ++++++++++++++++++ 3 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 src/tests/backend/specs/favicon-test-skin.png create mode 100644 src/tests/backend/specs/favicon.js diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js index b49132e9137..725139c0986 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.js @@ -2,9 +2,12 @@ const path = require('path'); const eejs = require('../../eejs'); +const fs = require('fs'); +const fsp = fs.promises; const toolbar = require('../../utils/toolbar'); const hooks = require('../../../static/js/pluginfw/hooks'); const settings = require('../../utils/Settings'); +const util = require('util'); const webaccess = require('./webaccess'); exports.expressCreateServer = (hookName, args, cb) => { @@ -74,23 +77,24 @@ exports.expressCreateServer = (hookName, args, cb) => { })); }); - args.app.get('/favicon.ico', (req, res) => { - let filePath = path.join( - settings.root, - 'src', - 'static', - 'skins', - settings.skinName, - 'favicon.ico' - ); - res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); - 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); + args.app.get('/favicon.ico', (req, res, next) => { + (async () => { + const fns = [ + path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'), + path.join(settings.root, 'src', 'static', 'favicon.ico'), + ]; + for (const fn of fns) { + try { + await fsp.access(fn, fs.constants.R_OK); + } catch (err) { + continue; + } + res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); + await util.promisify(res.sendFile.bind(res))(fn); + return; } - }); + next(); + })().catch((err) => next(err || new Error(err))); }); return cb(); diff --git a/src/tests/backend/specs/favicon-test-skin.png b/src/tests/backend/specs/favicon-test-skin.png new file mode 100644 index 0000000000000000000000000000000000000000..87bdadbbb323675893ecc522b73bfffdf21aea60 GIT binary patch literal 419 zcmV;U0bKrxP)4J8~X zffI5dxrX=5g`fHP7o$fsz`zB8BK_~?y(HImW_g{!^jNlKWYMdJv6=8g@@B%(tA=42 z+3)SlD8?YU<|a38Ln!4K#+~>fHtr-)$}u!&R Date: Tue, 20 Apr 2021 00:53:22 -0400 Subject: [PATCH 066/218] favicon: Redo favicon customization --- CHANGELOG.md | 9 +++++ settings.json.docker | 8 ++-- settings.json.template | 8 ++-- src/node/hooks/express/specialpages.js | 1 + src/node/utils/Settings.js | 9 +++-- src/templates/index.html | 2 +- src/templates/pad.html | 2 +- src/templates/timeslider.html | 2 +- .../backend/specs/favicon-test-custom.png | Bin 0 -> 635 bytes src/tests/backend/specs/favicon.js | 36 ++++++++++++++++++ 10 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 src/tests/backend/specs/favicon-test-custom.png diff --git a/CHANGELOG.md b/CHANGELOG.md index d5441ac4804..d28dd816dff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# Next release + +### Compatibility changes + +* The `favicon` setting is now interpreted as a pathname to a favicon file, not + a URL. Please see the documentation comment in `settings.json.template`. +* The undocumented `faviconPad` and `faviconTimeslider` settings have been + removed. + # 1.8.13 ### Notable fixes diff --git a/settings.json.docker b/settings.json.docker index 228c70d2a08..030ab88f11d 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -80,10 +80,12 @@ "title": "${TITLE:Etherpad}", /* - * favicon default name - * alternatively, set up a fully specified Url to your own favicon + * Pathname of the favicon you want to use. If null, the skin's favicon is + * used if one is provided by the skin, otherwise the default Etherpad favicon + * is used. If this is a relative path it is interpreted as relative to the + * Etherpad root directory. */ - "favicon": "${FAVICON:favicon.ico}", + "favicon": "${FAVICON}", /* * Skin name. diff --git a/settings.json.template b/settings.json.template index 8a676b56dd6..90a89970c2c 100644 --- a/settings.json.template +++ b/settings.json.template @@ -71,10 +71,12 @@ "title": "Etherpad", /* - * favicon default name - * alternatively, set up a fully specified Url to your own favicon + * Pathname of the favicon you want to use. If null, the skin's favicon is + * used if one is provided by the skin, otherwise the default Etherpad favicon + * is used. If this is a relative path it is interpreted as relative to the + * Etherpad root directory. */ - "favicon": "favicon.ico", + "favicon": null, /* * Skin name. diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js index 725139c0986..66ee0221e2c 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.js @@ -80,6 +80,7 @@ exports.expressCreateServer = (hookName, args, cb) => { args.app.get('/favicon.ico', (req, res, next) => { (async () => { const fns = [ + ...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []), path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'), path.join(settings.root, 'src', 'static', 'favicon.ico'), ]; diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 60f4e64421f..b31cf16b466 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -50,11 +50,12 @@ console.log('All relative paths will be interpreted relative to the identified ' exports.title = 'Etherpad'; /** - * The app favicon fully specified url, visible e.g. in the browser window + * Pathname of the favicon you want to use. If null, the skin's favicon is + * used if one is provided by the skin, otherwise the default Etherpad favicon + * is used. If this is a relative path it is interpreted as relative to the + * Etherpad root directory. */ -exports.favicon = 'favicon.ico'; -exports.faviconPad = `../${exports.favicon}`; -exports.faviconTimeslider = `../../${exports.favicon}`; +exports.favicon = null; /* * Skin name. diff --git a/src/templates/index.html b/src/templates/index.html index 8f20c08da9a..4d08312f9e4 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -8,7 +8,7 @@ - + diff --git a/src/templates/pad.html b/src/templates/pad.html index 7bf2346f9c8..d7c3c082309 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -39,7 +39,7 @@ - + <% e.begin_block("styles"); %> diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html index b3fd3d0064a..fe43668c8c8 100644 --- a/src/templates/timeslider.html +++ b/src/templates/timeslider.html @@ -33,7 +33,7 @@ - + <% e.begin_block("timesliderStyles"); %> diff --git a/src/tests/backend/specs/favicon-test-custom.png b/src/tests/backend/specs/favicon-test-custom.png new file mode 100644 index 0000000000000000000000000000000000000000..9c6532c9610ab912675719e1a896618b7ae6fc65 GIT binary patch literal 635 zcmV->0)+jEP)0M5-E^;aMpPHM>C%wOREX)$uV_IK5nYHG-E@%}Ni>};3$kL7I)|7QY7-+A z6wTS6mCf^uVSzYMD-bQ|V%16)Y)=={%}t{^@ZKE0_u>2Ab3UCe?l5N3BSiz6GL0qz zVJXu{(ZGaBA739ej%FsHi6%59h^Fud1(CwRXl6pJ`F-{lq*KV*d)AMwxs#$3CCm>N1$0OeSPuwE!P3CtNc#5rbF_3kX6W5Ky^{;vYVYHUt(>ew z`}p=eKd@nhc>VGe(9>EG4hYbCDcGJd`)BsV7=pPKL85aPcoHz%FXgQms}&$yrBd94x z<6;C5g$T!Amq`mR({6p*hZsNvG@*poSqd+j5 Date: Tue, 20 Apr 2021 14:29:01 -0400 Subject: [PATCH 067/218] deps: Bump ueberdb2 --- CHANGELOG.md | 10 ++ src/package-lock.json | 285 ++++++++++++++++++++++-------------------- src/package.json | 2 +- 3 files changed, 161 insertions(+), 136 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d28dd816dff..7931773fec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ a URL. Please see the documentation comment in `settings.json.template`. * The undocumented `faviconPad` and `faviconTimeslider` settings have been removed. +* MySQL/MariaDB now uses connection pooling, which means you will see up to 10 + connections to the MySQL/MariaDB server (by default) instead of 1. This might + cause Etherpad to crash with a "ER_CON_COUNT_ERROR: Too many connections" + error if your server is configured with a low connection limit. + +### Notable enhancements + +* MySQL/MariaDB now uses connection pooling, which should improve stability and + reduce latency. +* Bulk database writes are now retried individually on write failure. # 1.8.13 diff --git a/src/package-lock.json b/src/package-lock.json index 5eade554e3b..8e03c1cdd30 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -23,25 +23,25 @@ }, "dependencies": { "tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" } } }, "@azure/core-auth": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.2.0.tgz", - "integrity": "sha512-KUl+Nwn/Sm6Lw5d3U90m1jZfNSL087SPcqHLxwn2T6PupNKmcgsEbDjHB25gDvHO4h7pBsTlrdJAY7dz+Qk8GA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.3.0.tgz", + "integrity": "sha512-kSDSZBL6c0CYdhb+7KuutnKGf2geeT+bCJAgccB0DD7wmNJSsQPcF7TcuoZX83B7VK4tLz/u+8sOO/CnCsYp8A==", "requires": { "@azure/abort-controller": "^1.0.0", "tslib": "^2.0.0" }, "dependencies": { "tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" } } }, @@ -51,13 +51,11 @@ "integrity": "sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw==" }, "@azure/ms-rest-js": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.2.3.tgz", - "integrity": "sha512-sXOhOu/37Tr8428f32Jwuwga975Xw64pYg1UeUwOBMhkNgtn5vUuNRa3fhmem+I6f8EKoi6hOsYDFlaHeZ52jA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.4.0.tgz", + "integrity": "sha512-kvksFowDDZ/Tqu0hkCp+oDRhh87QOlRPgx8tOIMbdbfm/Muvfdp5f0cwrgzguqT3nhmkcLzt1cvQbLTM0HmS/A==", "requires": { "@azure/core-auth": "^1.1.4", - "@types/node-fetch": "^2.3.7", - "@types/tunnel": "0.0.1", "abort-controller": "^3.0.0", "form-data": "^2.5.0", "node-fetch": "^2.6.0", @@ -91,13 +89,36 @@ } }, "@azure/ms-rest-nodeauth": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.0.7.tgz", - "integrity": "sha512-7Q1MyMB+eqUQy8JO+virSIzAjqR2UbKXE/YQZe+53gC8yakm8WOQ5OzGfPP+eyHqeRs6bQESyw2IC5feLWlT2A==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.0.9.tgz", + "integrity": "sha512-+GdDHUJlWtIDanRZemFooLy68NsBDhN/Oni9DSFeoXIFNGlSe1IOes8/IRkQdrNXyUvBanuzzR7I5WYYzYQsmA==", "requires": { "@azure/ms-rest-azure-env": "^2.0.0", "@azure/ms-rest-js": "^2.0.4", "adal-node": "^0.1.28" + }, + "dependencies": { + "@types/node": { + "version": "8.10.66", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", + "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==" + }, + "adal-node": { + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz", + "integrity": "sha1-RoxLs+u9lrEnBmn0ucuk4AZepIU=", + "requires": { + "@types/node": "^8.0.47", + "async": ">=0.6.0", + "date-utils": "*", + "jws": "3.x.x", + "request": ">= 2.52.0", + "underscore": ">= 1.3.1", + "uuid": "^3.1.0", + "xmldom": ">= 0.1.x", + "xpath.js": "~1.1.0" + } + } } }, "@babel/code-frame": { @@ -287,30 +308,9 @@ "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/node": { - "version": "14.14.34", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.34.tgz", - "integrity": "sha512-dBPaxocOK6UVyvhbnpFIj2W+S+1cBTkHQbFQfeeJhoKFbzYcVUGHvddeWPSucKATb3F0+pgDq0i6ghEaZjsugA==" - }, - "@types/node-fetch": { - "version": "2.5.8", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.8.tgz", - "integrity": "sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw==", - "requires": { - "@types/node": "*", - "form-data": "^3.0.0" - }, - "dependencies": { - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } + "version": "14.14.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.41.tgz", + "integrity": "sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g==" }, "@types/request": { "version": "2.48.5", @@ -340,14 +340,6 @@ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==" }, - "@types/tunnel": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.1.tgz", - "integrity": "sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A==", - "requires": { - "@types/node": "*" - } - }, "@types/unist": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", @@ -389,15 +381,15 @@ "dev": true }, "adal-node": { - "version": "0.1.28", - "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz", - "integrity": "sha1-RoxLs+u9lrEnBmn0ucuk4AZepIU=", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.2.2.tgz", + "integrity": "sha512-luzQ9cXOjUlZoCiWeYbyR+nHwScSrPTDTbOInFphQs/PnwNz6wAIVkbsHEXtvYBnjLctByTTI8ccfpGX100oRQ==", "requires": { "@types/node": "^8.0.47", - "async": ">=0.6.0", + "async": "^2.6.3", + "axios": "^0.21.1", "date-utils": "*", "jws": "3.x.x", - "request": ">= 2.52.0", "underscore": ">= 1.3.1", "uuid": "^3.1.0", "xmldom": ">= 0.1.x", @@ -408,13 +400,21 @@ "version": "8.10.66", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==" + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "requires": { + "lodash": "^4.17.14" + } } } }, "adm-zip": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", - "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==" + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.5.tgz", + "integrity": "sha512-IWwXKnCbirdbyXSfUDvCCrmYrOHANRZcc8NcRrvTlIApdl7PwE9oGcsYvNeJPAVY1M+70b4PxXGKIf8AEuiQ6w==" }, "after": { "version": "0.8.2", @@ -664,6 +664,14 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", @@ -687,8 +695,7 @@ "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "base64id": { "version": "2.0.0", @@ -866,15 +873,14 @@ "dev": true }, "bson": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", - "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz", + "integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==" }, "buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -937,13 +943,13 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "cassandra-driver": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.6.1.tgz", - "integrity": "sha512-Vk0kUHlMV4vFXRPwRpKnCZEEMZkp9/RucBDB7gpaUmn9sCusKzzUzVkXeusTxKSoGuIgLJJ7YBiFJdXOctUS7A==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.6.2.tgz", + "integrity": "sha512-XfDMlpyEl+mI1DSaw8YdLBUyc5wXwuVYWN0WEZOC50BrHSPUmCXA7fyyexwk3j5XiYd/7BXWvO9nnZ50asJPoQ==", "requires": { "@types/long": "^4.0.0", "@types/node": ">=8", - "adm-zip": "^0.4.13", + "adm-zip": "^0.5.3", "long": "^2.2.0" } }, @@ -2256,6 +2262,11 @@ "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==", "dev": true }, + "follow-redirects": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", + "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==" + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -2619,8 +2630,7 @@ "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "ignore": { "version": "4.0.6", @@ -3458,14 +3468,14 @@ } }, "mongodb": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.4.tgz", - "integrity": "sha512-Y+Ki9iXE9jI+n9bVtbTOOdK0B95d6wVGSucwtBkvQ+HIvVdTCfpVRp01FDC24uhC/Q2WXQ8Lpq3/zwtB5Op9Qw==", + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.6.tgz", + "integrity": "sha512-WlirMiuV1UPbej5JeCMqE93JRfZ/ZzqE7nJTwP85XzjAF4rRSeq2bGCb1cjfoHLOF06+HxADaPGqT0g3SbVT1w==", "requires": { "bl": "^2.2.1", "bson": "^1.1.4", "denque": "^1.4.1", - "require_optional": "^1.0.1", + "optional-require": "^1.0.2", "safe-buffer": "^5.1.2", "saslprep": "^1.0.0" } @@ -3476,14 +3486,15 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "mssql": { - "version": "7.0.0-beta.3", - "resolved": "https://registry.npmjs.org/mssql/-/mssql-7.0.0-beta.3.tgz", - "integrity": "sha512-Jn/q64Dg2UjbNTqsBwCHFdMjxs4xIVqgWQ1hmDKvBR0T8ebHfPnGTzfNl4oE/VwqP1m0As+v2CMjyqOi9WneuQ==", + "version": "7.0.0-beta.5", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-7.0.0-beta.5.tgz", + "integrity": "sha512-LfvKm/vcaQNA4w/ARbFCFraFmri0arwOgcSRkyajIVIrfW7Yd2zUZ2w+c8Ee7k3k5BeD6c0D3G6gdfW9aW0fUg==", "requires": { "@tediousjs/connection-string": "^0.3.0", "debug": "^4", + "rfdc": "^1.3.0", "tarn": "^3.0.1", - "tedious": "^9.2.3" + "tedious": "^11.0.5" }, "dependencies": { "debug": { @@ -3641,6 +3652,11 @@ } } }, + "node-abort-controller": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-1.2.1.tgz", + "integrity": "sha512-79PYeJuj6S9+yOHirR0JBLFOgjB6sQCir10uN6xRx25iD+ZD4ULqgRn3MwWBRaQGB0vEgReJzWwJo42T1R6YbQ==" + }, "node-addon-api": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz", @@ -7041,6 +7057,11 @@ "resolved": "https://registry.npmjs.org/optional-js/-/optional-js-2.3.0.tgz", "integrity": "sha512-B0LLi+Vg+eko++0z/b8zIv57kp7HKEzaPJo7LowJXMUKYdf+3XJGu/cw03h/JhIOsLnP+cG5QnTHAuicjA5fMw==" }, + "optional-require": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz", + "integrity": "sha512-RV2Zp2MY2aeYK5G+B/Sps8lW5NHAzE5QClbFP15j+PWmP+T9PxlJXBOOLoSAdgwFvS4t0aMR4vpedMkbHfh0nA==" + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -7167,23 +7188,23 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pg": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.5.1.tgz", - "integrity": "sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.6.0.tgz", + "integrity": "sha512-qNS9u61lqljTDFvmk/N66EeGq3n6Ujzj0FFyNMGQr6XuEv4tgNTXvJQTfJdcvGit5p5/DWPu+wj920hAJFI+QQ==", "requires": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", - "pg-connection-string": "^2.4.0", - "pg-pool": "^3.2.2", - "pg-protocol": "^1.4.0", + "pg-connection-string": "^2.5.0", + "pg-pool": "^3.3.0", + "pg-protocol": "^1.5.0", "pg-types": "^2.1.0", "pgpass": "1.x" } }, "pg-connection-string": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz", - "integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", + "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" }, "pg-int8": { "version": "1.0.1", @@ -7191,14 +7212,14 @@ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" }, "pg-pool": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.2.2.tgz", - "integrity": "sha512-ORJoFxAlmmros8igi608iVEbQNNZlp89diFVx6yV5v+ehmpMY9sK6QgpmgoXbmkNaBAx8cOOZh9g80kJv1ooyA==" + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.3.0.tgz", + "integrity": "sha512-0O5huCql8/D6PIRFAlmccjphLYWC+JIzvUhSzXSpGaf+tjTZc4nn+Lr7mLXBbFJfvwbP0ywDv73EiaBsxn7zdg==" }, "pg-protocol": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.4.0.tgz", - "integrity": "sha512-El+aXWcwG/8wuFICMQjM5ZSAm6OWiJicFdNYo+VY3QP+8vI4SvLIWVe51PppTzMhikUJR+PsyIFKqfdXPz/yxA==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", + "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" }, "pg-types": { "version": "2.2.0", @@ -7395,12 +7416,12 @@ } }, "redis": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/redis/-/redis-3.0.2.tgz", - "integrity": "sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.1.tgz", + "integrity": "sha512-QhkKhOuzhogR1NDJfBD34TQJz2ZJwDhhIC6ZmvpftlmfYShHHQXjjNspAJ+Z2HH5NwSBVYBVganbiZ8bgFMHjg==", "requires": { - "denque": "^1.4.1", - "redis-commands": "^1.5.0", + "denque": "^1.5.0", + "redis-commands": "^1.7.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0" } @@ -7528,15 +7549,6 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, - "require_optional": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", - "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", - "requires": { - "resolve-from": "^2.0.0", - "semver": "^5.1.0" - } - }, "resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", @@ -7546,11 +7558,6 @@ "path-parse": "^1.0.6" } }, - "resolve-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", - "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" - }, "rethinkdb": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/rethinkdb/-/rethinkdb-2.4.2.tgz", @@ -7559,6 +7566,11 @@ "bluebird": ">= 2.3.2 < 3" } }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -7698,19 +7710,19 @@ "optional": true }, "simple-git": { - "version": "2.36.2", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.36.2.tgz", - "integrity": "sha512-orBEf65GfSiQMsYedbJXSiRNnIRvhbeE5rrxZuEimCpWxDZOav0KLy2IEiPi1YJCF+zaC2quiJF8A4TsxI9/tw==", + "version": "2.38.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.38.0.tgz", + "integrity": "sha512-CORjrfirWMEGbJAxaXDH/PjZVOeATeG2bkafM9DsLVcFkbF9sXQGIIpEI6FeyXpvUsFK69T/pa4+4FKY9TUJMQ==", "requires": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", - "debug": "^4.3.2" + "debug": "^4.3.1" }, "dependencies": { "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { "ms": "2.1.2" } @@ -8257,29 +8269,32 @@ "integrity": "sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw==" }, "tedious": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/tedious/-/tedious-9.2.3.tgz", - "integrity": "sha512-+mI2r/5mqxpTHKBZ/SW+NNH2MK5i3Pwwkw0gF5ZrS2wf2uT/03bLSss8nm7xh604abJXyjx0sirhwH63H328qA==", + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-11.0.7.tgz", + "integrity": "sha512-/nFPRzDoz0VS/+JXx40ovHbs8SvRQ5cgRCJN4uDxkeWfKc9iuNuh84mO3W47i2cHMJ4biiGYw5UfY9a5hHn/Sg==", "requires": { "@azure/ms-rest-nodeauth": "^3.0.6", - "@js-joda/core": "^3.1.0", - "adal-node": "^0.1.28", - "bl": "^3.0.0", + "@js-joda/core": "^3.2.0", + "adal-node": "^0.2.1", + "bl": "^4.0.3", "depd": "^2.0.0", "iconv-lite": "^0.6.2", - "jsbi": "^3.1.3", + "jsbi": "^3.1.4", "native-duplexpair": "^1.0.0", + "node-abort-controller": "^1.1.0", "punycode": "^2.1.0", "readable-stream": "^3.6.0", "sprintf-js": "^1.1.2" }, "dependencies": { "bl": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/bl/-/bl-3.0.1.tgz", - "integrity": "sha512-jrCW5ZhfQ/Vt07WX1Ngs+yn9BDqPL/gw28S7s9H6QK/gupnizNzJAss5akW20ISgOrbLTlXOOCTJeNUQqruAWQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "requires": { - "readable-stream": "^3.0.1" + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" } }, "depd": { @@ -8445,9 +8460,9 @@ } }, "ueberdb2": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.4.tgz", - "integrity": "sha512-hcexgTdMa6gMquv5r6rOBsr76awMlqAjQiMMJ72qrzuatLYJ6D1EQTK/Jqo4nOD/jklXHM2yFw1mNcHsrlEzrw==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.7.tgz", + "integrity": "sha512-lpD7QgxzgQ4UQ8Sj8/oYG487I/+Kj0s9O1GY+eyI/2xYQg/IXq5iw21AMzCKCnPI6WM1jF52dK0QaLxddkAVQg==", "requires": { "async": "^3.2.0", "cassandra-driver": "^4.5.1", @@ -8782,9 +8797,9 @@ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" }, "xmldom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.5.0.tgz", - "integrity": "sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA==" + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz", + "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==" }, "xmlhttprequest-ssl": { "version": "1.5.5", diff --git a/src/package.json b/src/package.json index 1bd9dce0a60..1d8778f3464 100644 --- a/src/package.json +++ b/src/package.json @@ -69,7 +69,7 @@ "threads": "^1.4.0", "tiny-worker": "^2.3.0", "tinycon": "0.6.8", - "ueberdb2": "^1.4.4", + "ueberdb2": "^1.4.7", "underscore": "1.12.1", "unorm": "1.6.0", "wtfnode": "^0.8.4" From 3afc77dae7aa07c1997b8f2f2e2b0f71f7b0e0c4 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 26 Apr 2021 15:20:55 +0200 Subject: [PATCH 068/218] Localisation updates from https://translatewiki.net. --- src/locales/pt.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/locales/pt.json b/src/locales/pt.json index 334c8bf375e..efb11c80b23 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -13,6 +13,7 @@ "MuratTheTurkish", "Ti4goc", "Tuliouel", + "Unamane", "Waldir", "Waldyrious" ] @@ -29,7 +30,7 @@ "admin_plugins.installed_fetching": "A obter plugins instalados...", "admin_plugins.installed_nothing": "Não instalas-te nenhum plugin ainda.", "admin_plugins.installed_uninstall.value": "Desinstalar", - "admin_plugins.last-update": "Ultima atualização", + "admin_plugins.last-update": "Última atualização", "admin_plugins.name": "Nome", "admin_plugins.page-title": "Gestor de plugins - Etherpad", "admin_plugins.version": "Versão", From 3a8d66ba6aa655a4a76e5eae988ec8f59d8db44e Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Fri, 30 Apr 2021 08:49:17 +0200 Subject: [PATCH 069/218] Localisation updates from https://translatewiki.net. --- src/locales/gu.json | 3 ++- src/locales/ia.json | 4 ++-- src/locales/oc.json | 29 +++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/locales/gu.json b/src/locales/gu.json index b3a91d42d5e..d2f7c4fa1cc 100644 --- a/src/locales/gu.json +++ b/src/locales/gu.json @@ -2,6 +2,7 @@ "@metadata": { "authors": [ "Bhatakati aatma", + "Dsvyas", "Harsh4101991", "KartikMistry" ] @@ -10,7 +11,7 @@ "pad.toolbar.bold.title": "બોલ્ડ", "pad.toolbar.settings.title": "ગોઠવણીઓ", "pad.colorpicker.save": "સાચવો", - "pad.colorpicker.cancel": "રદ્દ કરો", + "pad.colorpicker.cancel": "રદ કરો", "pad.loading": "લાવે છે...", "pad.noCookie": "કુકી મળી નહી. આપના બ્રાઉઝર સેટિંગમાં જઇ કુકી સક્રિય કરો!", "pad.permissionDenied": "આ પેડના ઉપયોગની આપને પરવાનગી નથી", diff --git a/src/locales/ia.json b/src/locales/ia.json index 4e1904ff6dc..3229a910e5e 100644 --- a/src/locales/ia.json +++ b/src/locales/ia.json @@ -26,7 +26,7 @@ "pad.colorpicker.save": "Salveguardar", "pad.colorpicker.cancel": "Cancellar", "pad.loading": "Cargamento…", - "pad.noCookie": "Le cookie non pote esser trovate. Per favor permitte le cookies in tu navigator!", + "pad.noCookie": "Le cookie non pote esser trovate. Per favor permitte le cookies in tu navigator! Tu session e parametros non essera salveguardate inter visitas. Isto pote esser debite al facto que Etherpad ha essite includite in un iFrame in alcun navigatores. Assecura te que Etherpad es sure le mesme subdominio/dominio que su iFrame parente.", "pad.permissionDenied": "Tu non ha le permission de acceder a iste pad", "pad.settings.padSettings": "Configuration del pad", "pad.settings.myView": "Mi vista", @@ -50,7 +50,7 @@ "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.abiword.innerHTML": "Tu pote solmente importar files in formato de texto simple o HTML. Pro functionalitate de importation plus extense, installa AbiWord o LibreOffice.", "pad.modals.connected": "Connectite.", - "pad.modals.reconnecting": "Reconnecte a tu pad…", + "pad.modals.reconnecting": "Reconnexion a tu pad…", "pad.modals.forcereconnect": "Fortiar reconnexion", "pad.modals.reconnecttimer": "Tentativa de reconnexion in", "pad.modals.cancel": "Cancellar", diff --git a/src/locales/oc.json b/src/locales/oc.json index 8ee8facb356..84a015ae461 100644 --- a/src/locales/oc.json +++ b/src/locales/oc.json @@ -5,6 +5,33 @@ "Quentí" ] }, + "admin.page-title": "Panèl d’administracion - Etherpad", + "admin_plugins": "Gestion de las extensions", + "admin_plugins.available": "Extensions disponiblas", + "admin_plugins.available_not-found": "Cap d’extension pas trobada.", + "admin_plugins.available_fetching": "Recuperacion…", + "admin_plugins.available_install.value": "Installar", + "admin_plugins.available_search.placeholder": "Cercar las extensions d’installar", + "admin_plugins.description": "Descripcion", + "admin_plugins.installed": "Extensions installadas", + "admin_plugins.installed_fetching": "Recuperacion de las extensions installadas...", + "admin_plugins.installed_nothing": "Avètz pas installat cap d’extensions pel moment.", + "admin_plugins.installed_uninstall.value": "Desinstallar", + "admin_plugins.last-update": "Darrièra mesa a jorn", + "admin_plugins.name": "Nom", + "admin_plugins.page-title": "Gestion de las extensions - Etherpad", + "admin_plugins.version": "Version", + "admin_plugins_info": "Informacion de resolucion de problèmas", + "admin_plugins_info.plugins": "Extensions installadas", + "admin_plugins_info.page-title": "Informacion d’extension - Etherpad", + "admin_plugins_info.version": "Version d’Etherpad", + "admin_plugins_info.version_latest": "Darrièra version disponibla", + "admin_plugins_info.version_number": "Numèro de version", + "admin_settings": "Paramètres", + "admin_settings.current": "Configuracion actuala", + "admin_settings.current_restart.value": "Reaviar Etherpad", + "admin_settings.current_save.value": "Enregistrar los paramètres", + "admin_settings.page-title": "Paramètres - Etherpad", "index.newPad": "Pad novèl", "index.createOpenPad": "o crear/dobrir un Pad intitulat :", "pad.toolbar.bold.title": "Gras (Ctrl-B)", @@ -39,6 +66,8 @@ "pad.settings.fontType": "Tipe de poliça :", "pad.settings.fontType.normal": "Normal", "pad.settings.language": "Lenga :", + "pad.settings.about": "A prepaus", + "pad.settings.poweredBy": "Propulsat per", "pad.importExport.import_export": "Importar/Exportar", "pad.importExport.import": "Cargar un tèxte o un document", "pad.importExport.importSuccessful": "Capitat !", From aaacbd3a7a234009431bb7dfe9394008251d47d0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 21 Apr 2021 16:24:27 -0400 Subject: [PATCH 070/218] Minify: Refactor `requestURI()` for readability --- src/node/utils/Minify.js | 54 +++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index 7969f9c1454..197c2cc361a 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -48,7 +48,7 @@ const LIBRARY_WHITELIST = [ // 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. -const requestURI = async (url, method, headers) => await new Promise((resolve, reject) => { +const requestURI = async (url, method, headers) => { const parsedUrl = new URL(url); let status = 500; const content = []; @@ -58,31 +58,35 @@ const requestURI = async (url, method, headers) => await new Promise((resolve, r params: {filename: (parsedUrl.pathname + parsedUrl.search).replace(/^\/static\//, '')}, headers, }; - const mockResponse = { - writeHead: (_status, _headers) => { - status = _status; - for (const header in _headers) { - if (Object.prototype.hasOwnProperty.call(_headers, header)) { - headers[header] = _headers[header]; + let mockResponse; + const p = new Promise((resolve) => { + mockResponse = { + writeHead: (_status, _headers) => { + status = _status; + for (const header in _headers) { + if (Object.prototype.hasOwnProperty.call(_headers, header)) { + headers[header] = _headers[header]; + } } - } - }, - setHeader: (header, value) => { - headers[header.toLowerCase()] = value.toString(); - }, - header: (header, value) => { - headers[header.toLowerCase()] = value.toString(); - }, - write: (_content) => { - _content && content.push(_content); - }, - end: (_content) => { - _content && content.push(_content); - resolve([status, headers, content.join('')]); - }, - }; - minify(mockRequest, mockResponse).catch(reject); -}); + }, + setHeader: (header, value) => { + headers[header.toLowerCase()] = value.toString(); + }, + header: (header, value) => { + headers[header.toLowerCase()] = value.toString(); + }, + write: (_content) => { + _content && content.push(_content); + }, + end: (_content) => { + _content && content.push(_content); + resolve([status, headers, content.join('')]); + }, + }; + }); + await minify(mockRequest, mockResponse); + return await p; +}; const requestURIs = (locations, method, headers, callback) => { Promise.all(locations.map((loc) => requestURI(loc, method, headers))).then((responses) => { From e8df643d752a2c98c050b30c7aad08b51ad50637 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 21 Apr 2021 16:26:24 -0400 Subject: [PATCH 071/218] Minify: Treat `ENOTDIR` like `ENOENT` when statting a file This avoids an exception when require-kernel requests a path like `existing-file.js/index.js`. --- src/node/utils/Minify.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index 197c2cc361a..39cb24c9f15 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -247,7 +247,7 @@ const statFile = async (filename, dirStatLimit) => { try { stats = await fs.stat(path.resolve(ROOT_DIR, filename)); } catch (err) { - if (err.code === 'ENOENT') { + if (['ENOENT', 'ENOTDIR'].includes(err.code)) { // Stat the directory instead. const [date] = await statFile(path.dirname(filename), dirStatLimit - 1); return [date, false]; From 8f236b86877efd5db3ca289619baaa194c95048b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 21 Apr 2021 16:29:55 -0400 Subject: [PATCH 072/218] Minify: Avoid crash due to unhandled Promise rejection if stat fails --- src/node/utils/Minify.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index 39cb24c9f15..83c4be55033 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -89,7 +89,15 @@ const requestURI = async (url, method, headers) => { }; const requestURIs = (locations, method, headers, callback) => { - Promise.all(locations.map((loc) => requestURI(loc, method, headers))).then((responses) => { + Promise.all(locations.map(async (loc) => { + try { + return await requestURI(loc, method, headers); + } catch (err) { + logger.debug(`requestURI(${JSON.stringify(loc)}, ${JSON.stringify(method)}, ` + + `${JSON.stringify(headers)}) failed: ${err.stack || err}`); + return [500, headers, '']; + } + })).then((responses) => { const statuss = responses.map((x) => x[0]); const headerss = responses.map((x) => x[1]); const contentss = responses.map((x) => x[2]); From 6011d31b2259be5c1f20804c111060206815f18e Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Fri, 30 Apr 2021 21:53:32 +0000 Subject: [PATCH 073/218] fix: upgrade underscore from 1.12.1 to 1.13.0 Snyk has created this PR to upgrade underscore from 1.12.1 to 1.13.0. See this package in npm: https://www.npmjs.com/package/underscore See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=upgrade-pr --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 8e03c1cdd30..c08cfc8338d 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -8488,9 +8488,9 @@ } }, "underscore": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", - "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.0.tgz", + "integrity": "sha512-sCs4H3pCytsb5K7i072FAEC9YlSYFIbosvM0tAKAlpSSUgD7yC1iXSEGdl5XrDKQ1YUB+p/HDzYrSG2H2Vl36g==" }, "unified": { "version": "9.2.0", diff --git a/src/package.json b/src/package.json index 1d8778f3464..33377460271 100644 --- a/src/package.json +++ b/src/package.json @@ -70,7 +70,7 @@ "tiny-worker": "^2.3.0", "tinycon": "0.6.8", "ueberdb2": "^1.4.7", - "underscore": "1.12.1", + "underscore": "1.13.0", "unorm": "1.6.0", "wtfnode": "^0.8.4" }, From 4c4415e14aea516eeefc866557db49ae0ba58961 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 12 Apr 2021 17:31:30 -0400 Subject: [PATCH 074/218] PadMessageHandler: Register `activePads` metric only once --- src/node/handler/PadMessageHandler.js | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 0e6869e8894..a10b1167c54 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -62,8 +62,15 @@ exports.socketio = () => { const sessioninfos = {}; exports.sessioninfos = sessioninfos; -// Measure total amount of users stats.gauge('totalUsers', () => Object.keys(socketio.sockets.sockets).length); +stats.gauge('activePads', () => { + const padIds = new Set(); + for (const {padId} of Object.values(sessioninfos)) { + if (!padId) continue; + padIds.add(padId); + } + return padIds.size; +}); /** * A changeset queue per pad that is processed by handleUserChanges() @@ -94,18 +101,6 @@ exports.handleConnect = (socket) => { // Initialize sessioninfos for this new session sessioninfos[socket.id] = {}; - - stats.gauge('activePads', () => { - const padIds = []; - for (const session of Object.keys(sessioninfos)) { - if (sessioninfos[session].padId) { - if (padIds.indexOf(sessioninfos[session].padId) === -1) { - padIds.push(sessioninfos[session].padId); - } - } - } - return padIds.length; - }); }; /** From d5c6a44d9cd1a343742b811f95a6ade2397b287d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 12 Apr 2021 17:32:05 -0400 Subject: [PATCH 075/218] PadMessageHandler: Improve documentation of `sessioninfos` --- src/node/handler/PadMessageHandler.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index a10b1167c54..89896fa7194 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -50,14 +50,22 @@ exports.socketio = () => { }; /** - * A associative array that saves information about a session - * key = sessionId - * values = padId, readonlyPadId, readonly, author, rev - * padId = the real padId of the pad - * 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 ID used for this session + * Contains information about socket.io connections: + * - key: Socket.io socket ID. + * - value: Object that is initially empty immediately after connect. Once the client's + * CLIENT_READY message is processed, it has the following properties: + * - auth: Object with the following properties copied from the client's CLIENT_READY message: + * - padID: Pad ID requested by the user. Unlike the padId property described below, this + * may be a read-only pad ID. + * - sessionID: Copied from the client's sessionID cookie, which should be the value + * returned from the createSession() HTTP API. This will be null/undefined if + * createSession() isn't used or the portal doesn't set the sessionID cookie. + * - token: User-supplied token. + * - author: The user's author ID. + * - padId: The real (not read-only) ID of the pad. + * - readonlyPadId: The read-only ID of the pad. + * - readonly: Whether the client has read-only access (true) or read/write access (false). + * - rev: The last revision that was sent to the client. */ const sessioninfos = {}; exports.sessioninfos = sessioninfos; From c85391862b160d95884ba69679c74963a8ccf3f2 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 12 Apr 2021 23:22:36 -0400 Subject: [PATCH 076/218] PadMessageHandler: Avoid unnecessary property lookups --- src/node/handler/PadMessageHandler.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 89896fa7194..d8042b50837 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -732,12 +732,11 @@ exports.updatePadClients = async (pad) => { const revChangeset = revision.changeset; const currentTime = revision.meta.timestamp; - // next if session has not been deleted - if (sessioninfos[sid] == null) { - continue; - } + // Re-check sessioninfos in case the client disconnected during the above await. + const sessioninfo = sessioninfos[sid]; + if (sessioninfo == null) continue; - if (author === sessioninfos[sid].author) { + if (author === sessioninfo.author) { socket.json.send({type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev: r}}); } else { const forWire = Changeset.prepareForWire(revChangeset, pad.pool); @@ -748,15 +747,12 @@ exports.updatePadClients = async (pad) => { apool: forWire.pool, author, currentTime, - timeDelta: currentTime - sessioninfos[sid].time}}; + timeDelta: currentTime - sessioninfo.time}}; socket.json.send(wireMsg); } - - if (sessioninfos[sid]) { - sessioninfos[sid].time = currentTime; - sessioninfos[sid].rev = r; - } + sessioninfo.time = currentTime; + sessioninfo.rev = r; } } }; From 770755debfcd650bcc92d05bb4f14acd4d93a37a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 12 Apr 2021 23:49:24 -0400 Subject: [PATCH 077/218] PadMessageHandler: Assume sessioninfo stays valid during client update ...but add a try/catch around the message transmission just in case. --- src/node/handler/PadMessageHandler.js | 37 +++++++++++++++------------ 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index d8042b50837..89adbd8efb0 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -715,13 +715,13 @@ exports.updatePadClients = async (pad) => { // but benefit of reusing cached revision object is HUGE const revCache = {}; - // go through all sessions on this pad - for (const socket of roomSockets) { - const sid = socket.id; + socket: for (const socket of roomSockets) { + const sessioninfo = sessioninfos[socket.id]; + // The user might have disconnected since _getRoomSockets() was called. + if (sessioninfo == null) continue; - // send them all new changesets - while (sessioninfos[sid] && sessioninfos[sid].rev < pad.getHeadRevisionNumber()) { - const r = sessioninfos[sid].rev + 1; + while (sessioninfo.rev < pad.getHeadRevisionNumber()) { + const r = sessioninfo.rev + 1; let revision = revCache[r]; if (!revision) { revision = await pad.getRevision(r); @@ -732,24 +732,29 @@ exports.updatePadClients = async (pad) => { const revChangeset = revision.changeset; const currentTime = revision.meta.timestamp; - // Re-check sessioninfos in case the client disconnected during the above await. - const sessioninfo = sessioninfos[sid]; - if (sessioninfo == null) continue; - + let msg; if (author === sessioninfo.author) { - socket.json.send({type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev: r}}); + msg = {type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev: r}}; } else { const forWire = Changeset.prepareForWire(revChangeset, pad.pool); - const wireMsg = {type: 'COLLABROOM', - data: {type: 'NEW_CHANGES', + msg = { + type: 'COLLABROOM', + data: { + type: 'NEW_CHANGES', newRev: r, changeset: forWire.translated, apool: forWire.pool, author, currentTime, - timeDelta: currentTime - sessioninfo.time}}; - - socket.json.send(wireMsg); + timeDelta: currentTime - sessioninfo.time, + }, + }; + } + try { + socket.json.send(msg); + } catch (err) { + messageLogger.error(`Failed to notify user of new revision: ${err.stack || err}`); + continue socket; } sessioninfo.time = currentTime; sessioninfo.rev = r; From 14d4aadfe41f66bba29fa5e590786026e5253659 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 13 Apr 2021 00:00:21 -0400 Subject: [PATCH 078/218] PadMessageHandler: Parallelize client updates Multiple clients are updated in parallel, but multiple revisions sent to a particular client are still sent sequentially. --- src/node/handler/PadMessageHandler.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 89adbd8efb0..899991a1122 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -715,10 +715,10 @@ exports.updatePadClients = async (pad) => { // but benefit of reusing cached revision object is HUGE const revCache = {}; - socket: for (const socket of roomSockets) { + await Promise.all(roomSockets.map(async (socket) => { const sessioninfo = sessioninfos[socket.id]; // The user might have disconnected since _getRoomSockets() was called. - if (sessioninfo == null) continue; + if (sessioninfo == null) return; while (sessioninfo.rev < pad.getHeadRevisionNumber()) { const r = sessioninfo.rev + 1; @@ -754,12 +754,12 @@ exports.updatePadClients = async (pad) => { socket.json.send(msg); } catch (err) { messageLogger.error(`Failed to notify user of new revision: ${err.stack || err}`); - continue socket; + return; } sessioninfo.time = currentTime; sessioninfo.rev = r; } - } + })); }; /** From eeead4643792d2e7a10aef935112bab8e620375c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 13 Apr 2021 00:49:06 -0400 Subject: [PATCH 079/218] PadMessageHandler: Use a `Map` for `sessioninfos` Maps are a bit more flexible, have clearer semantics, and have a convenient `size` property. --- src/node/db/API.js | 9 ++--- src/node/handler/PadMessageHandler.js | 56 ++++++++++++--------------- 2 files changed, 28 insertions(+), 37 deletions(-) diff --git a/src/node/db/API.js b/src/node/db/API.js index c262e078b9f..01edf5e704f 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -810,15 +810,12 @@ exports.createDiffHTML = async (padID, startRev, endRev) => { exports.getStats = async () => { const sessionInfos = padMessageHandler.sessioninfos; - - const sessionKeys = Object.keys(sessionInfos); - const activePads = new Set(Object.entries(sessionInfos).map((k) => k[1].padId)); - + const map = function* (it, fn) { for (const i of it) yield fn(i); }; + const activePads = new Set(map(sessionInfos.values(), ({padId}) => padId)); const {padIDs} = await padManager.listAllPads(); - return { totalPads: padIDs.length, - totalSessions: sessionKeys.length, + totalSessions: sessionInfos.size, totalActivePads: activePads.size, }; }; diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 899991a1122..a39cebe2ad7 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -67,13 +67,13 @@ exports.socketio = () => { * - readonly: Whether the client has read-only access (true) or read/write access (false). * - rev: The last revision that was sent to the client. */ -const sessioninfos = {}; +const sessioninfos = new Map(); exports.sessioninfos = sessioninfos; stats.gauge('totalUsers', () => Object.keys(socketio.sockets.sockets).length); stats.gauge('activePads', () => { const padIds = new Set(); - for (const {padId} of Object.values(sessioninfos)) { + for (const {padId} of sessioninfos.values()) { if (!padId) continue; padIds.add(padId); } @@ -106,9 +106,7 @@ exports.setSocketIO = (socket_io) => { */ exports.handleConnect = (socket) => { stats.meter('connects').mark(); - - // Initialize sessioninfos for this new session - sessioninfos[socket.id] = {}; + sessioninfos.set(socket.id, {}); }; /** @@ -130,9 +128,7 @@ exports.kickSessionsFromPad = (padID) => { */ exports.handleDisconnect = async (socket) => { stats.meter('disconnects').mark(); - - // save the padname of this session - const session = sessioninfos[socket.id]; + const session = sessioninfos.get(socket.id); // if this connection was already etablished with a handshake, // send a disconnect message to the others @@ -166,9 +162,7 @@ exports.handleDisconnect = async (socket) => { // Allow plugins to hook into users leaving the pad hooks.callAll('userLeave', session); } - - // Delete the sessioninfos entrys of this session - delete sessioninfos[socket.id]; + sessioninfos.delete(socket.id); }; /** @@ -199,7 +193,7 @@ exports.handleMessage = async (socket, message) => { return; } - const thisSession = sessioninfos[socket.id]; + const thisSession = sessioninfos.get(socket.id); if (!thisSession) { messageLogger.warn('Dropped message from an unknown connection.'); @@ -256,7 +250,7 @@ exports.handleMessage = async (socket, message) => { } // Drop the message if the client disconnected during the above processing. - if (sessioninfos[socket.id] !== thisSession) { + if (sessioninfos.get(socket.id) !== thisSession) { messageLogger.warn('Dropping message from a connection that has gone away.'); return; } @@ -301,7 +295,7 @@ exports.handleMessage = async (socket, message) => { * @param message the message from the client */ const handleSaveRevisionMessage = async (socket, message) => { - const {padId, author: authorId} = sessioninfos[socket.id]; + const {padId, author: authorId} = sessioninfos.get(socket.id); const pad = await padManager.getPad(padId); await pad.addSavedRevision(pad.head, authorId); }; @@ -351,7 +345,7 @@ exports.handleCustomMessage = (padID, msgString) => { const handleChatMessage = async (socket, message) => { const time = Date.now(); const text = message.data.text; - const {padId, author: authorId} = sessioninfos[socket.id]; + const {padId, author: authorId} = sessioninfos.get(socket.id); await exports.sendChatMessageToPadClients(time, authorId, text, padId); }; @@ -409,7 +403,7 @@ const handleGetChatMessages = async (socket, message) => { return; } - const padId = sessioninfos[socket.id].padId; + const {padId} = sessioninfos.get(socket.id); const pad = await padManager.getPad(padId); const chatMessages = await pad.getChatMessages(start, end); @@ -442,11 +436,11 @@ const handleSuggestUserName = (socket, message) => { return; } - const padId = sessioninfos[socket.id].padId; + const {padId} = sessioninfos.get(socket.id); // search the author and send him this message _getRoomSockets(padId).forEach((socket) => { - const session = sessioninfos[socket.id]; + const session = sessioninfos.get(socket.id); if (session && session.author === message.data.payload.unnamedId) { socket.json.send(message); } @@ -472,7 +466,7 @@ const handleUserInfoUpdate = async (socket, message) => { } // Check that we have a valid session and author to update. - const session = sessioninfos[socket.id]; + const session = sessioninfos.get(socket.id); if (!session || !session.author || !session.padId) { messageLogger.warn(`Dropped message, USERINFO_UPDATE Session not ready.${message.data}`); return; @@ -554,7 +548,7 @@ const handleUserChanges = async (socket, message) => { // The client might disconnect between our callbacks. We should still // finish processing the changeset, so keep a reference to the session. - const thisSession = sessioninfos[socket.id]; + const thisSession = sessioninfos.get(socket.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 @@ -716,7 +710,7 @@ exports.updatePadClients = async (pad) => { const revCache = {}; await Promise.all(roomSockets.map(async (socket) => { - const sessioninfo = sessioninfos[socket.id]; + const sessioninfo = sessioninfos.get(socket.id); // The user might have disconnected since _getRoomSockets() was called. if (sessioninfo == null) return; @@ -811,7 +805,7 @@ const _correctMarkersInPad = (atext, apool) => { }; const handleSwitchToPad = async (socket, message, _authorID) => { - const currentSessionInfo = sessioninfos[socket.id]; + const currentSessionInfo = sessioninfos.get(socket.id); const padId = currentSessionInfo.padId; // Check permissions for the new pad. @@ -830,20 +824,20 @@ const handleSwitchToPad = async (socket, message, _authorID) => { assert(authorID === currentSessionInfo.author); // Check if the connection dropped during the access check. - if (sessioninfos[socket.id] !== currentSessionInfo) return; + if (sessioninfos.get(socket.id) !== currentSessionInfo) return; // clear the session and leave the room _getRoomSockets(padId).forEach((socket) => { - const sinfo = sessioninfos[socket.id]; + const sinfo = sessioninfos.get(socket.id); if (sinfo && sinfo.author === currentSessionInfo.author) { // fix user's counter, works on page refresh or if user closes browser window and then rejoins - sessioninfos[socket.id] = {}; + sessioninfos.set(socket.id, {}); socket.leave(padId); } }); // start up the new pad - const newSessionInfo = sessioninfos[socket.id]; + const newSessionInfo = sessioninfos.get(socket.id); createSessionInfoAuth(newSessionInfo, message); await handleClientReady(socket, message, authorID); }; @@ -927,17 +921,17 @@ const handleClientReady = async (socket, message, authorID) => { // 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. - const sessionInfo = sessioninfos[socket.id]; + const sessionInfo = sessioninfos.get(socket.id); if (sessionInfo == null) return; // Check if this author is already on the pad, if yes, kick the other sessions! const roomSockets = _getRoomSockets(pad.id); for (const socket of roomSockets) { - const sinfo = sessioninfos[socket.id]; + const sinfo = sessioninfos.get(socket.id); if (sinfo && sinfo.author === authorID) { // fix user's counter, works on page refresh or if user closes browser window and then rejoins - sessioninfos[socket.id] = {}; + sessioninfos.set(socket.id, {}); socket.leave(padIds.padId); socket.json.send({disconnect: 'userdup'}); } @@ -1151,7 +1145,7 @@ const handleClientReady = async (socket, message, authorID) => { // Since sessioninfos might change while being enumerated, check if the // sessionID is still assigned to a valid session - const sessionInfo = sessioninfos[roomSocket.id]; + const sessionInfo = sessioninfos.get(roomSocket.id); if (sessionInfo == null) return; // get the authorname & colorId @@ -1433,7 +1427,7 @@ exports.padUsers = async (padID) => { // iterate over all clients (in parallel) await Promise.all(_getRoomSockets(padID).map(async (roomSocket) => { - const s = sessioninfos[roomSocket.id]; + const s = sessioninfos.get(roomSocket.id); if (s) { const author = await authorManager.getAuthor(s.author); // Fixes: https://github.com/ether/etherpad-lite/issues/4120 From ab4e99f67aba256bf3aecfcba164388cd68f54ab Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 10 Apr 2021 01:35:43 -0400 Subject: [PATCH 080/218] editor: Delete commented-out code --- src/static/js/Changeset.js | 57 ------------------------------------- src/static/js/ace2_inner.js | 39 +------------------------ src/static/js/skiplist.js | 1 - 3 files changed, 1 insertion(+), 96 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 6c236129bd7..d985e504f8b 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -108,7 +108,6 @@ exports.newLen = (cs) => exports.unpack(cs).newLen; * @return {Op} type object iterator */ exports.opIterator = (opsStr, optStartIndex) => { - // print(opsStr); const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g; const startIndex = (optStartIndex || 0); let curIndex = startIndex; @@ -644,15 +643,8 @@ exports.textLinesMutator = (lines) => { curLine += L; curCol = 0; } - // 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"); }; const skip = (N, L, includeInSplice) => { @@ -667,7 +659,6 @@ exports.textLinesMutator = (lines) => { putCurLineInSplice(); } curCol += N; - // debugPrint("skip"); } } }; @@ -684,10 +675,8 @@ exports.textLinesMutator = (lines) => { return lines_slice(m, m + k).join(''); }; if (isCurLineInSplice()) { - // print(curCol); if (curCol === 0) { removed = curSplice[curSplice.length - 1]; - // print("FOO"); // case foo curSplice.length--; removed += nextKLinesText(L - 1); curSplice[1] += L - 1; @@ -704,7 +693,6 @@ exports.textLinesMutator = (lines) => { removed = nextKLinesText(L); curSplice[1] += L; } - // debugPrint("remove"); } return removed; }; @@ -722,7 +710,6 @@ exports.textLinesMutator = (lines) => { removed = curSplice[sline].substring(curCol, curCol + N); curSplice[sline] = curSplice[sline].substring(0, curCol) + curSplice[sline].substring(curCol + N); - // debugPrint("remove"); } } return removed; @@ -736,13 +723,6 @@ exports.textLinesMutator = (lines) => { if (L) { const newLines = exports.splitTextLines(text); if (isCurLineInSplice()) { - // if (curCol == 0) { - // curSplice.length--; - // curSplice[1]--; - // Array.prototype.push.apply(curSplice, newLines); - // curLine += newLines.length; - // } - // else { const sline = curSplice.length - 1; const theLine = curSplice[sline]; const lineCol = curCol; @@ -753,7 +733,6 @@ exports.textLinesMutator = (lines) => { curLine += newLines.length; curSplice.push(theLine.substring(lineCol)); curCol = 0; - // } } else { Array.prototype.push.apply(curSplice, newLines); curLine += newLines.length; @@ -767,12 +746,10 @@ exports.textLinesMutator = (lines) => { curSplice[sline].substring(curCol); curCol += text.length; } - // debugPrint("insert"); } }; const hasMore = () => { - // print(lines.length+" / "+inSplice+" / "+(curSplice.length - 2)+" / "+curSplice[1]); let docLines = lines_length(); if (inSplice) { docLines += curSplice.length - 2 - curSplice[1]; @@ -784,7 +761,6 @@ exports.textLinesMutator = (lines) => { if (inSplice) { leaveSplice(); } - // debugPrint("close"); }; const self = { @@ -826,7 +802,6 @@ exports.applyZip = (in1, idx1, in2, idx2, func) => { if ((!op2.opcode) && iter2.hasNext()) iter2.next(op2); func(op1, op2, opOut); if (opOut.opcode) { - // print(opOut.toSource()); assem.append(opOut); opOut.opcode = ''; } @@ -1011,7 +986,6 @@ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => { buf.append('*'); buf.append(exports.numToString(pool.putAttrib(atts[i]))); } - // print(att1+" / "+att2+" / "+buf.toString()); return buf.toString(); }; @@ -1023,7 +997,6 @@ exports._slicerZipperFunc = (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()); if (attOp.opcode === '-') { exports.copyOp(attOp, opOut); attOp.opcode = ''; @@ -1120,15 +1093,7 @@ exports.applyToAttribution = (cs, astr, pool) => { (op1, op2, opOut) => exports._slicerZipperFunc(op1, op2, opOut, pool)); }; -/* exports.oneInsertedLineAtATimeOpIterator = function(opsStr, optStartIndex, charBank) { - var iter = exports.opIterator(opsStr, optStartIndex); - var bankIndex = 0; - -};*/ - exports.mutateAttributionLines = (cs, lines, pool) => { - // dmesg(cs); - // dmesg(lines.toSource()+" ->"); const unpacked = exports.unpack(cs); const csIter = exports.opIterator(unpacked.ops); const csBank = unpacked.charBank; @@ -1154,7 +1119,6 @@ exports.mutateAttributionLines = (cs, lines, pool) => { let lineAssem = null; const outputMutOp = (op) => { - // print("outputMutOp: "+op.toSource()); if (!lineAssem) { lineAssem = exports.mergingOpAssembler(); } @@ -1174,17 +1138,12 @@ exports.mutateAttributionLines = (cs, lines, pool) => { 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()); 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); csOp.opcode = ''; } else if (csOp.opcode === '+') { if (csOp.lines > 1) { @@ -1205,7 +1164,6 @@ exports.mutateAttributionLines = (cs, lines, pool) => { if ((!attOp.opcode) && isNextMutOp()) { nextMutOp(attOp); } - // print("attOp: "+attOp.toSource()); exports._slicerZipperFunc(attOp, csOp, opOut, pool); if (opOut.opcode) { outputMutOp(opOut); @@ -1216,8 +1174,6 @@ exports.mutateAttributionLines = (cs, lines, pool) => { exports.assert(!lineAssem, `line assembler not finished:${cs}`); mut.close(); - - // dmesg("-> "+lines.toSource()); }; /** @@ -1299,11 +1255,6 @@ exports.compose = (cs1, cs2, pool) => { const bankAssem = exports.stringAssembler(); 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 === '-') { @@ -1317,13 +1268,6 @@ exports.compose = (cs1, cs2, pool) => { bankAssem.append(bankIter1.take(opOut.chars)); } } - - // 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()); @@ -2196,7 +2140,6 @@ exports._slicerZipperFuncWithDeletions = (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()); if (attOp.opcode === '-') { exports.copyOp(attOp, opOut); attOp.opcode = ''; diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index a8cc3cb984c..410f87bf368 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -301,12 +301,6 @@ function Ace2Inner(editorInfo, cssManagers) { const inCallStack = (type, action) => { if (disposed) return; - if (currentCallStack) { - // Do not uncomment this in production. It will break Etherpad being provided in iFrames. - // I am leaving this in for testing usefulness. - // top.console.error(`Can't enter callstack ${type}, already in ${currentCallStack.type}`); - } - const newEditEvent = (eventType) => ({ eventType, backset: null, @@ -388,11 +382,6 @@ function Ace2Inner(editorInfo, cssManagers) { if (cleanExit) { submitOldEvent(cs.editEvent); if (cs.domClean && cs.type !== 'setup') { - // if (cs.isUserChange) - // { - // if (cs.repChanged) parenModule.notifyChange(); - // else parenModule.notifyTick(); - // } if (cs.selectionAffected) { updateBrowserSelectionFromRep(); } @@ -753,7 +742,7 @@ function Ace2Inner(editorInfo, cssManagers) { let printedTrace = false; const isTimeUp = () => { if (exceededAlready) { - if ((!printedTrace)) { // && now() - startTime - ms > 300) { + if ((!printedTrace)) { printedTrace = true; } return true; @@ -1176,7 +1165,6 @@ function Ace2Inner(editorInfo, cssManagers) { entries.push(newEntry); lineNodeInfos[k] = newEntry.domInfo; } - // var fragment = magicdom.wrapDom(document.createDocumentFragment()); domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]); dirtyNodes.forEach((n) => { toDeleteAtEnd.push(n); @@ -1212,11 +1200,8 @@ function Ace2Inner(editorInfo, cssManagers) { p.mark('del'); // delete old dom nodes toDeleteAtEnd.forEach((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); - - // dmesg(htmlPrettyEscape(htmlForRemovedChild(n))); }); // needed to stop chrome from breaking the ui when long strings without spaces are pasted @@ -1228,7 +1213,6 @@ function Ace2Inner(editorInfo, cssManagers) { // 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"); const selStartFromHook = hooks.callAll('aceStartLineAndCharForPoint', { callstack: currentCallStack, editorInfo, @@ -1398,9 +1382,6 @@ function Ace2Inner(editorInfo, cssManagers) { const getPointForLineAndChar = (lineAndChar) => { 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)); const lineEntry = rep.lines.atIndex(line); charsLeft -= lineEntry.lineMarker; if (charsLeft < 0) { @@ -1574,7 +1555,6 @@ function Ace2Inner(editorInfo, cssManagers) { throw new Error(`doRepApplyChangeset length mismatch: ${errMsg}`); } - // (function doRecordUndoInformation(changes) { ((changes) => { const editEvent = currentCallStack.editEvent; if (editEvent.eventType === 'nonundoable') { @@ -1597,7 +1577,6 @@ function Ace2Inner(editorInfo, cssManagers) { } })(changes); - // rep.alltext = Changeset.applyToText(changes, rep.alltext); Changeset.mutateAttributionLines(changes, rep.alines, rep.apool); if (changesetTracker.isTracking()) { @@ -1994,7 +1973,6 @@ function Ace2Inner(editorInfo, cssManagers) { theChangeset = builder.toString(); } - // dmesg(htmlPrettyEscape(theChangeset)); doRepApplyChangeset(theChangeset); } @@ -2170,13 +2148,8 @@ function Ace2Inner(editorInfo, cssManagers) { } 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)); } return false; - // Do not uncomment this in production it will break iFrames. - // top.console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart); }; const isPadLoading = (eventType) => ( @@ -2641,7 +2614,6 @@ function Ace2Inner(editorInfo, cssManagers) { const tabSize = THE_TAB.length; const toDelete = ((col2 - 1) % tabSize) + 1; performDocumentReplaceRange([lineNum, col - toDelete], [lineNum, col], ''); - // scrollSelectionIntoView(); handled = true; } } @@ -2730,7 +2702,6 @@ function Ace2Inner(editorInfo, cssManagers) { const altKey = evt.altKey; const shiftKey = evt.shiftKey; - // 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. @@ -2930,7 +2901,6 @@ function Ace2Inner(editorInfo, cssManagers) { fastIncorp(4); evt.preventDefault(); doReturnKey(); - // scrollSelectionIntoView(); scheduler.setTimeout(() => { outerWin.scrollBy(-100, 0); }, 0); @@ -2979,7 +2949,6 @@ function Ace2Inner(editorInfo, cssManagers) { fastIncorp(5); evt.preventDefault(); doTabKey(evt.shiftKey); - // scrollSelectionIntoView(); specialHandled = true; } if ((!specialHandled) && @@ -3273,7 +3242,6 @@ function Ace2Inner(editorInfo, cssManagers) { if (isCollapsed) { const 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); @@ -3514,9 +3482,6 @@ function Ace2Inner(editorInfo, cssManagers) { $(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); - // 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); // If non-nullish, pasting on a link should be suppressed. let suppressPasteOnLink = null; @@ -3715,7 +3680,6 @@ function Ace2Inner(editorInfo, cssManagers) { const mods = []; for (let n = firstLine; n <= lastLine; n++) { - // var t = ''; let level = 0; let togglingOn = true; const listType = /([a-z]+)([0-9]+)/.exec(getLineListType(n)); @@ -3726,7 +3690,6 @@ function Ace2Inner(editorInfo, cssManagers) { } if (listType) { - // t = listType[1]; level = Number(listType[2]); } const t = getLineListType(n); diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index 4ea74010415..d2c96b4d6a5 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -356,7 +356,6 @@ that is a string. return self.indexOfEntry(self.atOffset(offset)); }, search: (entryFunc) => _search(entryFunc), - // debugToString: _debugToString, debugGetPoint: _getPoint, debugDepth: () => start.levels, }); From f650c3d73eecf4ea2301190bf6bc3561bc84597b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 10 Apr 2021 04:09:34 -0400 Subject: [PATCH 081/218] editor: Delete unused `PROFILER` code --- src/static/js/ace2_inner.js | 62 +------------------------------------ src/static/js/skiplist.js | 21 ------------- 2 files changed, 1 insertion(+), 82 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 410f87bf368..9e51b412f36 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -136,17 +136,6 @@ function Ace2Inner(editorInfo, cssManagers) { for (let i = 0; i < names.length; ++i) console[names[i]] = noop; } - let PROFILER = window.PROFILER; - if (!PROFILER) { - PROFILER = () => ({ - start: noop, - mark: noop, - literal: noop, - end: noop, - cancel: noop, - }); - } - // "dmesg" is for displaying messages in the in-page output pane // 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 @@ -938,17 +927,12 @@ function Ace2Inner(editorInfo, cssManagers) { clearObservedChanges(); const getCleanNodeByKey = (key) => { - const p = PROFILER('getCleanNodeByKey', false); // eslint-disable-line new-cap - p.extra = 0; let n = doc.getElementById(key); // copying and pasting can lead to duplicate ids while (n && isNodeDirty(n)) { - p.extra++; n.id = ''; n = doc.getElementById(key); } - p.literal(p.extra, 'extra'); - p.end(); return n; }; @@ -1014,9 +998,7 @@ function Ace2Inner(editorInfo, cssManagers) { if (currentCallStack.observedSelection) return; currentCallStack.observedSelection = true; - const p = PROFILER('getSelection', false); // eslint-disable-line new-cap const selection = getSelection(); - p.end(); if (selection) { const node1 = topLevel(selection.startPoint.node); @@ -1049,17 +1031,13 @@ function Ace2Inner(editorInfo, cssManagers) { if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false; - const p = PROFILER('incorp', false); // eslint-disable-line new-cap - // returns true if dom changes were made if (!root.firstChild) { root.innerHTML = '
'; } - p.mark('obs'); observeChangesAroundSelection(); observeSuspiciousNodes(); - p.mark('dirty'); let dirtyRanges = getDirtyRanges(); let dirtyRangesCheckOut = true; let j = 0; @@ -1089,7 +1067,6 @@ function Ace2Inner(editorInfo, cssManagers) { clearObservedChanges(); - p.mark('getsel'); const selection = getSelection(); let selStart, selEnd; // each one, if truthy, has [line,char] needed to set selection @@ -1097,8 +1074,6 @@ function Ace2Inner(editorInfo, cssManagers) { 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]; @@ -1186,18 +1161,15 @@ function Ace2Inner(editorInfo, cssManagers) { const domChanges = (splicesToDo.length > 0); // update the representation - p.mark('splice'); splicesToDo.forEach((splice) => { doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]); }); // do DOM inserts - p.mark('insert'); domInsertsNeeded.forEach((ins) => { insertDomLines(ins[0], ins[1]); }); - p.mark('del'); // delete old dom nodes toDeleteAtEnd.forEach((n) => { // parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf) @@ -1209,7 +1181,6 @@ function Ace2Inner(editorInfo, cssManagers) { $('#innerdocbody').scrollLeft(0); } - 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) { @@ -1250,14 +1221,12 @@ function Ace2Inner(editorInfo, cssManagers) { selEnd[1] = rep.lines.atIndex(selEnd[0]).text.length; } - 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())) { // 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; @@ -1267,12 +1236,8 @@ function Ace2Inner(editorInfo, cssManagers) { currentCallStack.domClean = true; - p.mark('fixview'); - fixView(); - p.end('END'); - return domChanges; }; @@ -1295,11 +1260,9 @@ function Ace2Inner(editorInfo, cssManagers) { if (infoStructs.length < 1) return; infoStructs.forEach((info) => { - const p2 = PROFILER('insertLine', false); // eslint-disable-line new-cap const node = info.node; const key = uniqueId(node); let entry; - p2.mark('findEntry'); if (lastEntry) { // optimization to avoid recalculation const next = rep.lines.next(lastEntry); @@ -1309,16 +1272,13 @@ function Ace2Inner(editorInfo, cssManagers) { } } if (!entry) { - p2.literal(1, 'nonopt'); entry = rep.lines.atKey(key); lineStartOffset = rep.lines.offsetOfKey(key); - } else { p2.literal(0, 'nonopt'); } + } lastEntry = entry; - p2.mark('spans'); getSpansForLine(entry, (tokenText, tokenClass) => { info.appendSpan(tokenText, tokenClass); }, lineStartOffset); - p2.mark('addLine'); info.prepareForAdd(); entry.lineMarker = info.lineMarker; if (!nodeToAddAfter) { @@ -1328,9 +1288,7 @@ function Ace2Inner(editorInfo, cssManagers) { } nodeToAddAfter = node; info.notifyAdded(); - p2.mark('markClean'); markNodeClean(node); - p2.end(); }); }; @@ -2201,10 +2159,6 @@ function Ace2Inner(editorInfo, cssManagers) { // 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. - const p = PROFILER('getDirtyRanges', false); // eslint-disable-line new-cap - p.forIndices = 0; - p.consecutives = 0; - p.corrections = 0; const cleanNodeForIndexCache = {}; const N = rep.lines.length(); // old number of lines @@ -2215,7 +2169,6 @@ function Ace2Inner(editorInfo, cssManagers) { // in the document, return that node. // if (i) is out of bounds, return true. else return false. if (cleanNodeForIndexCache[i] === undefined) { - p.forIndices++; let result; if (i < 0 || i >= N) { result = true; // truthy, but no actual node @@ -2231,7 +2184,6 @@ function Ace2Inner(editorInfo, cssManagers) { const isConsecutive = (i) => { if (isConsecutiveCache[i] === undefined) { - p.consecutives++; isConsecutiveCache[i] = (() => { // 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 @@ -2293,7 +2245,6 @@ function Ace2Inner(editorInfo, cssManagers) { const correctlyAssignLine = (line) => { if (correctedLines[line]) return true; - p.corrections++; correctedLines[line] = true; // "line" is an index of a line in the un-updated rep. // returns whether line was already correctly assigned (i.e. correctly @@ -2357,16 +2308,13 @@ function Ace2Inner(editorInfo, cssManagers) { }; if (N === 0) { - p.cancel(); if (!isConsecutive(0)) { splitRange(0, 0); } } else { - p.mark('topbot'); detectChangesAroundLine(0, 1); detectChangesAroundLine(N - 1, 1); - p.mark('obs'); for (const k in observedChanges.cleanNodesNearChanges) { if (observedChanges.cleanNodesNearChanges[k]) { const key = k.substring(1); @@ -2376,10 +2324,6 @@ function Ace2Inner(editorInfo, cssManagers) { } } } - p.mark('stats&calc'); - p.literal(p.forIndices, 'byidx'); - p.literal(p.consecutives, 'cons'); - p.literal(p.corrections, 'corr'); } const dirtyRanges = []; @@ -2387,8 +2331,6 @@ function Ace2Inner(editorInfo, cssManagers) { dirtyRanges.push([cleanRanges[r][1], cleanRanges[r + 1][0]]); } - p.end(); - return dirtyRanges; }; @@ -2401,13 +2343,11 @@ function Ace2Inner(editorInfo, cssManagers) { }; const isNodeDirty = (n) => { - const p = PROFILER('cleanCheck', false); // eslint-disable-line new-cap if (n.parentNode !== root) return true; const data = getAssoc(n, 'dirtiness'); if (!data) return true; if (n.id !== data.nodeId) return true; if (n.innerHTML !== data.knownHTML) return true; - p.end(); return false; }; diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index d2c96b4d6a5..f603bc534c6 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -22,23 +22,9 @@ * limitations under the License. */ -const Ace2Common = require('./ace2_common'); const _ = require('./underscore'); -const noop = Ace2Common.noop; - function SkipList() { - let PROFILER = window.PROFILER; - if (!PROFILER) { - PROFILER = () => ({ - start: noop, - mark: noop, - literal: noop, - end: noop, - cancel: noop, - }); - } - // if there are N elements in the skiplist, "start" is element -1 and "end" is element N const start = { key: null, @@ -122,7 +108,6 @@ function SkipList() { const _entryWidth = (e) => (e && e.width) || 0; const _insertKeyAtPoint = (point, newKey, entry) => { - const p = PROFILER('insertKey', false); // eslint-disable-line new-cap const newNode = { key: newKey, levels: 0, @@ -131,13 +116,11 @@ function SkipList() { downSkips: [], downSkipWidths: [], }; - p.mark('donealloc'); const pNodes = point.nodes; const pIdxs = point.idxs; const pLoc = point.loc; const widthLoc = point.widthSkips[0] + point.nodes[0].downSkipWidths[0]; const newWidth = _entryWidth(entry); - p.mark('loop1'); // The new node will have at least level 1 // With a proability of 0.01^(n-1) the nodes level will be >= n @@ -173,18 +156,14 @@ function SkipList() { up.downSkipWidths[lvl] = widthSkip1; me.downSkipWidths[lvl] = widthSkip2; } - p.mark('loop2'); - p.literal(pNodes.length, 'PNL'); for (let lvl = newNode.levels; lvl < pNodes.length; lvl++) { const up = pNodes[lvl]; up.downSkips[lvl]++; up.downSkipWidths[lvl] += newWidth; } - p.mark('map'); keyToNodeMap[`$KEY$${newKey}`] = newNode; numNodes++; totalWidth += newWidth; - p.end(); }; const _getNodeAtPoint = (point) => point.nodes[0].downPtrs[0]; From e3d32a26b6d447932dfe94397f5b89600c273f85 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 10 Apr 2021 04:43:41 -0400 Subject: [PATCH 082/218] skiplist: Delete unused methods --- src/static/js/skiplist.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index f603bc534c6..a3780c645e3 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -335,8 +335,6 @@ that is a string. return self.indexOfEntry(self.atOffset(offset)); }, search: (entryFunc) => _search(entryFunc), - debugGetPoint: _getPoint, - debugDepth: () => start.levels, }); } From d40d59d9eb6754a1be745a3c76e3e4718865ba1a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 10 Apr 2021 03:46:29 -0400 Subject: [PATCH 083/218] AttributeManager: Simplify logic --- src/static/js/AttributeManager.js | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js index c89fc410f20..0c9eb84e1ba 100644 --- a/src/static/js/AttributeManager.js +++ b/src/static/js/AttributeManager.js @@ -68,10 +68,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ // see https://github.com/ether/etherpad-lite/issues/2772 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]; - + const [startCol, endCol] = this._findRowRange(row, start, end); const rowChangeset = this._setAttributesOnRangeByLine(row, startCol, endCol, attribs); // compose changesets of all rows into a single changeset @@ -89,26 +86,12 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ }, _findRowRange(row, start, end) { - let startCol, endCol; - - 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? - startCol = start[1]; - } else { - startCol = this.lineHasMarker(row) ? 1 : 0; // remove "*" used as line marker - } - - // find column where range on this row ends - if (row === end[0]) { // are we on the last row of range? - endCol = end[1]; // if so, get the end of range, not end of row - } else { - endCol = lineLength - 1; // remove "\n" - } - + // Subtract 1 for the end-of-line '\n' (it is never selected). + const lineLength = + this.rep.lines.offsetOfIndex(row + 1) - this.rep.lines.offsetOfIndex(row) - 1; + const markerWidth = this.lineHasMarker(row) ? 1 : 0; + const startCol = row === start[0] ? start[1] : markerWidth; + const endCol = row === end[0] ? end[1] : lineLength; return [startCol, endCol]; }, From cbbcef8e90728ed329d564554ce5f32a15ea4608 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 10 Apr 2021 03:54:06 -0400 Subject: [PATCH 084/218] AttributeManager: Add sanity checks --- src/static/js/AttributeManager.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js index 0c9eb84e1ba..5d2aae48ecc 100644 --- a/src/static/js/AttributeManager.js +++ b/src/static/js/AttributeManager.js @@ -63,6 +63,14 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ @param attribs: an array of attributes */ setAttributesOnRange(start, end, attribs) { + if (start[0] < 0) throw new RangeError('selection start line number is negative'); + if (start[1] < 0) throw new RangeError('selection start column number is negative'); + if (end[0] < 0) throw new RangeError('selection end line number is negative'); + if (end[1] < 0) throw new RangeError('selection end column number is negative'); + if (start[0] > end[0] || (start[0] === end[0] && start[1] > end[1])) { + throw new RangeError('selection ends before it starts'); + } + // 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 @@ -86,12 +94,24 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ }, _findRowRange(row, start, end) { + if (row < start[0] || row > end[0]) throw new RangeError(`line ${row} not in selection`); + if (row >= this.rep.lines.length()) throw new RangeError(`selected line ${row} does not exist`); + // Subtract 1 for the end-of-line '\n' (it is never selected). const lineLength = this.rep.lines.offsetOfIndex(row + 1) - this.rep.lines.offsetOfIndex(row) - 1; const markerWidth = this.lineHasMarker(row) ? 1 : 0; + if (lineLength - markerWidth < 0) throw new Error(`line ${row} has negative length`); + const startCol = row === start[0] ? start[1] : markerWidth; + if (startCol - markerWidth < 0) throw new RangeError('selection starts before line start'); + if (startCol > lineLength) throw new RangeError('selection starts after line end'); + const endCol = row === end[0] ? end[1] : lineLength; + if (endCol - markerWidth < 0) throw new RangeError('selection ends before line start'); + if (endCol > lineLength) throw new RangeError('selection ends after line end'); + if (startCol > endCol) throw new RangeError('selection ends before it starts'); + return [startCol, endCol]; }, From 303fd297bd8427c5a0ba3454db5370e79bf917a0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 10 Apr 2021 04:12:10 -0400 Subject: [PATCH 085/218] editor: Improve documentation comments --- src/static/js/AttributeManager.js | 14 ++++++------- src/static/js/ace2_inner.js | 34 ++++++++++++++++++++----------- src/static/js/skiplist.js | 13 +++++------- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js index 5d2aae48ecc..a2ea15b6c31 100644 --- a/src/static/js/AttributeManager.js +++ b/src/static/js/AttributeManager.js @@ -115,13 +115,13 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ return [startCol, endCol]; }, - /* - Sets attributes on a range, by line - @param row the row where range is - @param startCol column where range starts - @param endCol column where range ends - @param attribs: an array of attributes - */ + /** + * Sets attributes on a range, by line + * @param row the row where range is + * @param startCol column where range starts + * @param endCol column where range ends (one past the last selected column) + * @param attribs an array of attributes + */ _setAttributesOnRangeByLine(row, startCol, endCol, attribs) { const builder = Changeset.builder(this.rep.lines.totalWidth()); ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]); diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 9e51b412f36..bbe26a06bd7 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -86,14 +86,24 @@ function Ace2Inner(editorInfo, cssManagers) { let outsideKeyPress = (e) => true; 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! + // Document representation. const rep = { + // Each entry in this skip list is an object created by createDomLineEntry(). The object + // represents a line (paragraph) of content. lines: new SkipList(), + // Points at the start of the selection. Represented as [zeroBasedLineNumber, + // zeroBasedColumnNumber]. + // TODO: If the selection starts at the beginning of a line, I think this could be either + // [lineNumber, 0] or [previousLineNumber, previousLineLength]. Need to confirm. selStart: null, + // Points at the character just past the last selected character. Same representation as + // selStart. + // TODO: If the last selected character is the last character of a line, I think this could be + // either [lineNumber, lineLength] or [lineNumber+1, 0]. Need to confirm. selEnd: null, + // Whether the selection extends "backwards", so that the focus point (controlled with the arrow + // keys) is at the beginning. This is 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! selFocusAtStart: false, alltext: '', alines: [], @@ -646,9 +656,11 @@ function Ace2Inner(editorInfo, cssManagers) { } }; - // This methed exposes a setter for some ace properties - // @param key the name of the parameter - // @param value the value to set to + /** + * 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 = (key, value) => { // These properties are exposed const setters = { @@ -1301,8 +1313,6 @@ function Ace2Inner(editorInfo, cssManagers) { editorInfo.ace_isCaret = isCaret; // prereq: isCaret() - - const caretLine = () => rep.selStart[0]; editorInfo.ace_caretLine = caretLine; @@ -1542,9 +1552,9 @@ function Ace2Inner(editorInfo, cssManagers) { } }; - /* - Converts the position of a char (index in String) into a [row, col] tuple - */ + /** + * Converts the position of a char (index in String) into a [row, col] tuple + */ const lineAndColumnFromChar = (x) => { const lineEntry = rep.lines.atOffset(x); const lineStart = rep.lines.offsetOfEntry(lineEntry); diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index a3780c645e3..4599bb0508f 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -24,6 +24,10 @@ const _ = require('./underscore'); +/** + * The skip-list contains "entries", JavaScript objects that each must have a unique "key" + * property that is a string. + */ function SkipList() { // if there are N elements in the skiplist, "start" is element -1 and "end" is element N const start = { @@ -47,13 +51,12 @@ function SkipList() { const keyToNodeMap = {}; start.downPtrs[0] = end; end.upPtrs[0] = start; + // a "point" object at location x allows modifications immediately after the first // x elements of the skiplist, such as multiple inserts or deletes. // After an insert or delete using point P, the point is still valid and points // to the same index in the skiplist. Other operations with other points invalidate // this point. - - const _getPoint = (targetLoc) => { const numLevels = start.levels; let lvl = numLevels - 1; @@ -225,8 +228,6 @@ function SkipList() { // Returns index of first entry such that entryFunc(entry) is truthy, // or length() if no such entry. Assumes all falsy entries come before // all truthy entries. - - const _search = (entryFunc) => { let low = start; let lvl = start.levels - 1; @@ -250,10 +251,6 @@ function SkipList() { return lowIndex + 1; }; - /* -The skip-list contains "entries", JavaScript objects that each must have a unique "key" property -that is a string. - */ const self = this; _.extend(this, { length: () => numNodes, From ab8c354f187848f7042a6c6a4dc77df015187239 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 10 Apr 2021 15:11:12 -0400 Subject: [PATCH 086/218] skiplist: Use ES6 class syntax This makess it easier to examine state in dev console. --- src/static/js/skiplist.js | 294 +++++++++++++++++++------------------- 1 file changed, 147 insertions(+), 147 deletions(-) diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index 4599bb0508f..fb97bee4111 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -22,50 +22,53 @@ * limitations under the License. */ -const _ = require('./underscore'); +const _entryWidth = (e) => (e && e.width) || 0; +const _getNodeAtPoint = (point) => point.nodes[0].downPtrs[0]; /** * The skip-list contains "entries", JavaScript objects that each must have a unique "key" * property that is a string. */ -function SkipList() { - // if there are N elements in the skiplist, "start" is element -1 and "end" is element N - const start = { - key: null, - levels: 1, - upPtrs: [null], - downPtrs: [null], - downSkips: [1], - downSkipWidths: [0], - }; - const end = { - key: null, - levels: 1, - upPtrs: [null], - downPtrs: [null], - downSkips: [null], - downSkipWidths: [null], - }; - let numNodes = 0; - let totalWidth = 0; - const keyToNodeMap = {}; - start.downPtrs[0] = end; - end.upPtrs[0] = start; +class SkipList { + constructor() { + // if there are N elements in the skiplist, "start" is element -1 and "end" is element N + this._start = { + key: null, + levels: 1, + upPtrs: [null], + downPtrs: [null], + downSkips: [1], + downSkipWidths: [0], + }; + this._end = { + key: null, + levels: 1, + upPtrs: [null], + downPtrs: [null], + downSkips: [null], + downSkipWidths: [null], + }; + this._numNodes = 0; + this._totalWidth = 0; + this._keyToNodeMap = {}; + this._start.downPtrs[0] = this._end; + this._end.upPtrs[0] = this._start; + } // a "point" object at location x allows modifications immediately after the first // x elements of the skiplist, such as multiple inserts or deletes. // After an insert or delete using point P, the point is still valid and points // to the same index in the skiplist. Other operations with other points invalidate // this point. - const _getPoint = (targetLoc) => { - const numLevels = start.levels; + _getPoint(targetLoc) { + const numLevels = this._start.levels; let lvl = numLevels - 1; let i = -1; let ws = 0; const nodes = new Array(numLevels); const idxs = new Array(numLevels); const widthSkips = new Array(numLevels); - nodes[lvl] = start; + nodes[lvl] = this._start; idxs[lvl] = -1; widthSkips[lvl] = 0; while (lvl >= 0) { @@ -90,12 +93,12 @@ function SkipList() { widthSkips, toString: () => `getPoint(${targetLoc})`, }; - }; + } - const _getNodeAtOffset = (targetOffset) => { + _getNodeAtOffset(targetOffset) { let i = 0; - let n = start; - let lvl = start.levels - 1; + let n = this._start; + let lvl = this._start.levels - 1; while (lvl >= 0 && n.downPtrs[lvl]) { while (n.downPtrs[lvl] && (i + n.downSkipWidths[lvl] <= targetOffset)) { i += n.downSkipWidths[lvl]; @@ -103,14 +106,14 @@ function SkipList() { } lvl--; } - if (n === start) return (start.downPtrs[0] || null); - else if (n === end) return (targetOffset === totalWidth ? (end.upPtrs[0] || null) : null); + if (n === this._start) return (this._start.downPtrs[0] || null); + if (n === this._end) { + return targetOffset === this._totalWidth ? (this._end.upPtrs[0] || null) : null; + } return n; - }; + } - const _entryWidth = (e) => (e && e.width) || 0; - - const _insertKeyAtPoint = (point, newKey, entry) => { + _insertKeyAtPoint(point, newKey, entry) { const newNode = { key: newKey, levels: 0, @@ -133,14 +136,14 @@ function SkipList() { if (lvl === pNodes.length) { // assume we have just passed the end of point.nodes, and reached one level greater // than the skiplist currently supports - pNodes[lvl] = start; + pNodes[lvl] = this._start; pIdxs[lvl] = -1; - start.levels++; - end.levels++; - start.downPtrs[lvl] = end; - end.upPtrs[lvl] = start; - start.downSkips[lvl] = numNodes + 1; - start.downSkipWidths[lvl] = totalWidth; + this._start.levels++; + this._end.levels++; + this._start.downPtrs[lvl] = this._end; + this._end.upPtrs[lvl] = this._start; + this._start.downSkips[lvl] = this._numNodes + 1; + this._start.downSkipWidths[lvl] = this._totalWidth; point.widthSkips[lvl] = 0; } const me = newNode; @@ -164,14 +167,12 @@ function SkipList() { up.downSkips[lvl]++; up.downSkipWidths[lvl] += newWidth; } - keyToNodeMap[`$KEY$${newKey}`] = newNode; - numNodes++; - totalWidth += newWidth; - }; - - const _getNodeAtPoint = (point) => point.nodes[0].downPtrs[0]; + this._keyToNodeMap[`$KEY$${newKey}`] = newNode; + this._numNodes++; + this._totalWidth += newWidth; + } - const _deleteKeyAtPoint = (point) => { + _deleteKeyAtPoint(point) { const elem = point.nodes[0].downPtrs[0]; const elemWidth = _entryWidth(elem.entry); for (let i = 0; i < point.nodes.length; i++) { @@ -190,12 +191,12 @@ function SkipList() { up.downSkipWidths[i] -= elemWidth; } } - delete keyToNodeMap[`$KEY$${elem.key}`]; - numNodes--; - totalWidth -= elemWidth; - }; + delete this._keyToNodeMap[`$KEY$${elem.key}`]; + this._numNodes--; + this._totalWidth -= elemWidth; + } - const _propagateWidthChange = (node) => { + _propagateWidthChange(node) { const oldWidth = node.downSkipWidths[0]; const newWidth = _entryWidth(node.entry); const widthChange = newWidth - oldWidth; @@ -208,34 +209,34 @@ function SkipList() { n = n.upPtrs[lvl - 1]; } } - totalWidth += widthChange; - }; + this._totalWidth += widthChange; + } - const _getNodeIndex = (node, byWidth) => { + _getNodeIndex(node, byWidth) { let dist = (byWidth ? 0 : -1); let n = node; - while (n !== start) { + while (n !== this._start) { const lvl = n.levels - 1; n = n.upPtrs[lvl]; if (byWidth) dist += n.downSkipWidths[lvl]; else dist += n.downSkips[lvl]; } return dist; - }; + } - const _getNodeByKey = (key) => keyToNodeMap[`$KEY$${key}`]; + _getNodeByKey(key) { return this._keyToNodeMap[`$KEY$${key}`]; } // Returns index of first entry such that entryFunc(entry) is truthy, // or length() if no such entry. Assumes all falsy entries come before // all truthy entries. - const _search = (entryFunc) => { - let low = start; - let lvl = start.levels - 1; + search(entryFunc) { + let low = this._start; + let lvl = this._start.levels - 1; let lowIndex = -1; const f = (node) => { - if (node === start) return false; - else if (node === end) return true; + if (node === this._start) return false; + else if (node === this._end) return true; else return entryFunc(node.entry); }; @@ -249,90 +250,89 @@ function SkipList() { lvl--; } return lowIndex + 1; - }; + } - const self = this; - _.extend(this, { - length: () => numNodes, - atIndex: (i) => { - if (i < 0) console.warn(`atIndex(${i})`); - if (i >= numNodes) console.warn(`atIndex(${i}>=${numNodes})`); - return _getNodeAtPoint(_getPoint(i)).entry; - }, - // differs from Array.splice() in that new elements are in an array, not varargs - splice: (start, deleteCount, newEntryArray) => { - if (start < 0) console.warn(`splice(${start}, ...)`); - if (start + deleteCount > numNodes) { - console.warn(`splice(${start}, ${deleteCount}, ...), N=${numNodes}`); - console.warn('%s %s %s', typeof start, typeof deleteCount, typeof numNodes); - console.trace(); - } + length() { return this._numNodes; } - if (!newEntryArray) newEntryArray = []; - const pt = _getPoint(start); - for (let i = 0; i < deleteCount; i++) { - _deleteKeyAtPoint(pt); - } - for (let i = (newEntryArray.length - 1); i >= 0; i--) { - const entry = newEntryArray[i]; - _insertKeyAtPoint(pt, entry.key, entry); - const node = _getNodeByKey(entry.key); - node.entry = entry; - } - }, - next: (entry) => _getNodeByKey(entry.key).downPtrs[0].entry || null, - prev: (entry) => _getNodeByKey(entry.key).upPtrs[0].entry || null, - push: (entry) => { - self.splice(numNodes, 0, [entry]); - }, - slice: (start, end) => { - // act like Array.slice() - if (start === undefined) start = 0; - else if (start < 0) start += numNodes; - if (end === undefined) end = numNodes; - else if (end < 0) end += numNodes; + atIndex(i) { + if (i < 0) console.warn(`atIndex(${i})`); + if (i >= this._numNodes) console.warn(`atIndex(${i}>=${this._numNodes})`); + return _getNodeAtPoint(this._getPoint(i)).entry; + } + + // differs from Array.splice() in that new elements are in an array, not varargs + splice(start, deleteCount, newEntryArray) { + if (start < 0) console.warn(`splice(${start}, ...)`); + if (start + deleteCount > this._numNodes) { + console.warn(`splice(${start}, ${deleteCount}, ...), N=${this._numNodes}`); + console.warn('%s %s %s', typeof start, typeof deleteCount, typeof this._numNodes); + console.trace(); + } - if (start < 0) start = 0; - if (start > numNodes) start = numNodes; - if (end < 0) end = 0; - if (end > numNodes) end = numNodes; + if (!newEntryArray) newEntryArray = []; + const pt = this._getPoint(start); + for (let i = 0; i < deleteCount; i++) { + this._deleteKeyAtPoint(pt); + } + for (let i = (newEntryArray.length - 1); i >= 0; i--) { + const entry = newEntryArray[i]; + this._insertKeyAtPoint(pt, entry.key, entry); + const node = this._getNodeByKey(entry.key); + node.entry = entry; + } + } - window.dmesg(String([start, end, numNodes])); - if (end <= start) return []; - let n = self.atIndex(start); - const array = [n]; - for (let i = 1; i < (end - start); i++) { - n = self.next(n); - array.push(n); - } - return array; - }, - atKey: (key) => _getNodeByKey(key).entry, - indexOfKey: (key) => _getNodeIndex(_getNodeByKey(key)), - indexOfEntry: (entry) => self.indexOfKey(entry.key), - containsKey: (key) => !!(_getNodeByKey(key)), - // gets the last entry starting at or before the offset - atOffset: (offset) => _getNodeAtOffset(offset).entry, - keyAtOffset: (offset) => self.atOffset(offset).key, - offsetOfKey: (key) => _getNodeIndex(_getNodeByKey(key), true), - offsetOfEntry: (entry) => self.offsetOfKey(entry.key), - setEntryWidth: (entry, width) => { - entry.width = width; - _propagateWidthChange(_getNodeByKey(entry.key)); - }, - totalWidth: () => totalWidth, - offsetOfIndex: (i) => { - if (i < 0) return 0; - if (i >= numNodes) return totalWidth; - return self.offsetOfEntry(self.atIndex(i)); - }, - indexOfOffset: (offset) => { - if (offset <= 0) return 0; - if (offset >= totalWidth) return numNodes; - return self.indexOfEntry(self.atOffset(offset)); - }, - search: (entryFunc) => _search(entryFunc), - }); + next(entry) { return this._getNodeByKey(entry.key).downPtrs[0].entry || null; } + prev(entry) { return this._getNodeByKey(entry.key).upPtrs[0].entry || null; } + push(entry) { this.splice(this._numNodes, 0, [entry]); } + + slice(start, end) { + // act like Array.slice() + if (start === undefined) start = 0; + else if (start < 0) start += this._numNodes; + if (end === undefined) end = this._numNodes; + else if (end < 0) end += this._numNodes; + + if (start < 0) start = 0; + if (start > this._numNodes) start = this._numNodes; + if (end < 0) end = 0; + if (end > this._numNodes) end = this._numNodes; + + window.dmesg(String([start, end, this._numNodes])); + if (end <= start) return []; + let n = this.atIndex(start); + const array = [n]; + for (let i = 1; i < (end - start); i++) { + n = this.next(n); + array.push(n); + } + return array; + } + + atKey(key) { return this._getNodeByKey(key).entry; } + indexOfKey(key) { return this._getNodeIndex(this._getNodeByKey(key)); } + indexOfEntry(entry) { return this.indexOfKey(entry.key); } + containsKey(key) { return !!this._getNodeByKey(key); } + // gets the last entry starting at or before the offset + atOffset(offset) { return this._getNodeAtOffset(offset).entry; } + keyAtOffset(offset) { return this.atOffset(offset).key; } + offsetOfKey(key) { return this._getNodeIndex(this._getNodeByKey(key), true); } + offsetOfEntry(entry) { return this.offsetOfKey(entry.key); } + setEntryWidth(entry, width) { + entry.width = width; + this._propagateWidthChange(this._getNodeByKey(entry.key)); + } + totalWidth() { return this._totalWidth; } + offsetOfIndex(i) { + if (i < 0) return 0; + if (i >= this._numNodes) return this._totalWidth; + return this.offsetOfEntry(this.atIndex(i)); + } + indexOfOffset(offset) { + if (offset <= 0) return 0; + if (offset >= this._totalWidth) return this._numNodes; + return this.indexOfEntry(this.atOffset(offset)); + } } module.exports = SkipList; From 0e424fa8c3ff517e6c9061c5e573c2142fc0b8e9 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 11 Apr 2021 23:35:00 -0400 Subject: [PATCH 087/218] skiplist: Remove unnecessary `newKey` arg from `_insertKeyAtPoint()` --- src/static/js/skiplist.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index fb97bee4111..c32e7cce346 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -113,9 +113,9 @@ class SkipList { return n; } - _insertKeyAtPoint(point, newKey, entry) { + _insertKeyAtPoint(point, entry) { const newNode = { - key: newKey, + key: entry.key, levels: 0, upPtrs: [], downPtrs: [], @@ -167,7 +167,7 @@ class SkipList { up.downSkips[lvl]++; up.downSkipWidths[lvl] += newWidth; } - this._keyToNodeMap[`$KEY$${newKey}`] = newNode; + this._keyToNodeMap[`$KEY$${newNode.key}`] = newNode; this._numNodes++; this._totalWidth += newWidth; } @@ -276,7 +276,7 @@ class SkipList { } for (let i = (newEntryArray.length - 1); i >= 0; i--) { const entry = newEntryArray[i]; - this._insertKeyAtPoint(pt, entry.key, entry); + this._insertKeyAtPoint(pt, entry); const node = this._getNodeByKey(entry.key); node.entry = entry; } From 8ae40e80f912f0d780743e6c571f1eeb258a77cf Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 11 Apr 2021 23:36:44 -0400 Subject: [PATCH 088/218] skiplist: Save entry in `_insertKeyAtPoint()` --- src/static/js/skiplist.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index c32e7cce346..1bacce43c13 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -116,6 +116,7 @@ class SkipList { _insertKeyAtPoint(point, entry) { const newNode = { key: entry.key, + entry, levels: 0, upPtrs: [], downPtrs: [], @@ -277,8 +278,6 @@ class SkipList { for (let i = (newEntryArray.length - 1); i >= 0; i--) { const entry = newEntryArray[i]; this._insertKeyAtPoint(pt, entry); - const node = this._getNodeByKey(entry.key); - node.entry = entry; } } From 3c1be95e077edea44ffa2cae80923c21eb71430e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 10 Apr 2021 15:43:22 -0400 Subject: [PATCH 089/218] skiplist: Move point creation to a new `Point` class --- src/static/js/skiplist.js | 87 ++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index 1bacce43c13..1799054e127 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -25,6 +25,49 @@ const _entryWidth = (e) => (e && e.width) || 0; const _getNodeAtPoint = (point) => point.nodes[0].downPtrs[0]; +// A "point" object at index x allows modifications immediately after the first x elements of the +// skiplist, such as multiple inserts or deletes. After an insert or delete using point P, the point +// is still valid and points to the same index in the skiplist. Other operations with other points +// invalidate this point. +class Point { + constructor(skipList, loc) { + this._skipList = skipList; + this.loc = loc; + const numLevels = this._skipList._start.levels; + let lvl = numLevels - 1; + let i = -1; + let ws = 0; + const nodes = new Array(numLevels); + const idxs = new Array(numLevels); + const widthSkips = new Array(numLevels); + nodes[lvl] = this._skipList._start; + idxs[lvl] = -1; + widthSkips[lvl] = 0; + while (lvl >= 0) { + let n = nodes[lvl]; + while (n.downPtrs[lvl] && (i + n.downSkips[lvl] < this.loc)) { + i += n.downSkips[lvl]; + ws += n.downSkipWidths[lvl]; + n = n.downPtrs[lvl]; + } + nodes[lvl] = n; + idxs[lvl] = i; + widthSkips[lvl] = ws; + lvl--; + if (lvl >= 0) { + nodes[lvl] = n; + } + } + this.idxs = idxs; + this.nodes = nodes; + this.widthSkips = widthSkips; + } + + toString() { + return `Point(${this.loc})`; + } +} + /** * The skip-list contains "entries", JavaScript objects that each must have a unique "key" * property that is a string. @@ -55,46 +98,6 @@ class SkipList { this._end.upPtrs[0] = this._start; } - // a "point" object at location x allows modifications immediately after the first - // x elements of the skiplist, such as multiple inserts or deletes. - // After an insert or delete using point P, the point is still valid and points - // to the same index in the skiplist. Other operations with other points invalidate - // this point. - _getPoint(targetLoc) { - const numLevels = this._start.levels; - let lvl = numLevels - 1; - let i = -1; - let ws = 0; - const nodes = new Array(numLevels); - const idxs = new Array(numLevels); - const widthSkips = new Array(numLevels); - nodes[lvl] = this._start; - idxs[lvl] = -1; - widthSkips[lvl] = 0; - while (lvl >= 0) { - let n = nodes[lvl]; - while (n.downPtrs[lvl] && (i + n.downSkips[lvl] < targetLoc)) { - i += n.downSkips[lvl]; - ws += n.downSkipWidths[lvl]; - n = n.downPtrs[lvl]; - } - nodes[lvl] = n; - idxs[lvl] = i; - widthSkips[lvl] = ws; - lvl--; - if (lvl >= 0) { - nodes[lvl] = n; - } - } - return { - nodes, - idxs, - loc: targetLoc, - widthSkips, - toString: () => `getPoint(${targetLoc})`, - }; - } - _getNodeAtOffset(targetOffset) { let i = 0; let n = this._start; @@ -258,7 +261,7 @@ class SkipList { atIndex(i) { if (i < 0) console.warn(`atIndex(${i})`); if (i >= this._numNodes) console.warn(`atIndex(${i}>=${this._numNodes})`); - return _getNodeAtPoint(this._getPoint(i)).entry; + return _getNodeAtPoint(new Point(this, i)).entry; } // differs from Array.splice() in that new elements are in an array, not varargs @@ -271,7 +274,7 @@ class SkipList { } if (!newEntryArray) newEntryArray = []; - const pt = this._getPoint(start); + const pt = new Point(this, start); for (let i = 0; i < deleteCount; i++) { this._deleteKeyAtPoint(pt); } From 9fc88f3601111aeacc8b7783203e1e91efeb50d2 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 11 Apr 2021 21:11:30 -0400 Subject: [PATCH 090/218] skiplist: Convert point operations into `Point` methods --- src/static/js/skiplist.js | 161 +++++++++++++++++++------------------- 1 file changed, 81 insertions(+), 80 deletions(-) diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index 1799054e127..4ef63d73796 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -23,7 +23,6 @@ */ const _entryWidth = (e) => (e && e.width) || 0; -const _getNodeAtPoint = (point) => point.nodes[0].downPtrs[0]; // A "point" object at index x allows modifications immediately after the first x elements of the // skiplist, such as multiple inserts or deletes. After an insert or delete using point P, the point @@ -66,57 +65,8 @@ class Point { toString() { return `Point(${this.loc})`; } -} -/** - * The skip-list contains "entries", JavaScript objects that each must have a unique "key" - * property that is a string. - */ -class SkipList { - constructor() { - // if there are N elements in the skiplist, "start" is element -1 and "end" is element N - this._start = { - key: null, - levels: 1, - upPtrs: [null], - downPtrs: [null], - downSkips: [1], - downSkipWidths: [0], - }; - this._end = { - key: null, - levels: 1, - upPtrs: [null], - downPtrs: [null], - downSkips: [null], - downSkipWidths: [null], - }; - this._numNodes = 0; - this._totalWidth = 0; - this._keyToNodeMap = {}; - this._start.downPtrs[0] = this._end; - this._end.upPtrs[0] = this._start; - } - - _getNodeAtOffset(targetOffset) { - let i = 0; - let n = this._start; - let lvl = this._start.levels - 1; - while (lvl >= 0 && n.downPtrs[lvl]) { - while (n.downPtrs[lvl] && (i + n.downSkipWidths[lvl] <= targetOffset)) { - i += n.downSkipWidths[lvl]; - n = n.downPtrs[lvl]; - } - lvl--; - } - if (n === this._start) return (this._start.downPtrs[0] || null); - if (n === this._end) { - return targetOffset === this._totalWidth ? (this._end.upPtrs[0] || null) : null; - } - return n; - } - - _insertKeyAtPoint(point, entry) { + insert(entry) { const newNode = { key: entry.key, entry, @@ -126,10 +76,10 @@ class SkipList { downSkips: [], downSkipWidths: [], }; - const pNodes = point.nodes; - const pIdxs = point.idxs; - const pLoc = point.loc; - const widthLoc = point.widthSkips[0] + point.nodes[0].downSkipWidths[0]; + const pNodes = this.nodes; + const pIdxs = this.idxs; + const pLoc = this.loc; + const widthLoc = this.widthSkips[0] + this.nodes[0].downSkipWidths[0]; const newWidth = _entryWidth(entry); // The new node will have at least level 1 @@ -138,17 +88,17 @@ class SkipList { const lvl = newNode.levels; newNode.levels++; if (lvl === pNodes.length) { - // assume we have just passed the end of point.nodes, and reached one level greater + // assume we have just passed the end of this.nodes, and reached one level greater // than the skiplist currently supports - pNodes[lvl] = this._start; + pNodes[lvl] = this._skipList._start; pIdxs[lvl] = -1; - this._start.levels++; - this._end.levels++; - this._start.downPtrs[lvl] = this._end; - this._end.upPtrs[lvl] = this._start; - this._start.downSkips[lvl] = this._numNodes + 1; - this._start.downSkipWidths[lvl] = this._totalWidth; - point.widthSkips[lvl] = 0; + this._skipList._start.levels++; + this._skipList._end.levels++; + this._skipList._start.downPtrs[lvl] = this._skipList._end; + this._skipList._end.upPtrs[lvl] = this._skipList._start; + this._skipList._start.downSkips[lvl] = this._skipList._numNodes + 1; + this._skipList._start.downSkipWidths[lvl] = this._skipList._totalWidth; + this.widthSkips[lvl] = 0; } const me = newNode; const up = pNodes[lvl]; @@ -161,7 +111,7 @@ class SkipList { me.upPtrs[lvl] = up; me.downPtrs[lvl] = down; down.upPtrs[lvl] = me; - const widthSkip1 = widthLoc - point.widthSkips[lvl]; + const widthSkip1 = widthLoc - this.widthSkips[lvl]; const widthSkip2 = up.downSkipWidths[lvl] + newWidth - widthSkip1; up.downSkipWidths[lvl] = widthSkip1; me.downSkipWidths[lvl] = widthSkip2; @@ -171,15 +121,15 @@ class SkipList { up.downSkips[lvl]++; up.downSkipWidths[lvl] += newWidth; } - this._keyToNodeMap[`$KEY$${newNode.key}`] = newNode; - this._numNodes++; - this._totalWidth += newWidth; + this._skipList._keyToNodeMap[`$KEY$${newNode.key}`] = newNode; + this._skipList._numNodes++; + this._skipList._totalWidth += newWidth; } - _deleteKeyAtPoint(point) { - const elem = point.nodes[0].downPtrs[0]; + delete() { + const elem = this.nodes[0].downPtrs[0]; const elemWidth = _entryWidth(elem.entry); - for (let i = 0; i < point.nodes.length; i++) { + for (let i = 0; i < this.nodes.length; i++) { if (i < elem.levels) { const up = elem.upPtrs[i]; const down = elem.downPtrs[i]; @@ -190,14 +140,67 @@ class SkipList { const totalWidthSkip = up.downSkipWidths[i] + elem.downSkipWidths[i] - elemWidth; up.downSkipWidths[i] = totalWidthSkip; } else { - const up = point.nodes[i]; + const up = this.nodes[i]; up.downSkips[i]--; up.downSkipWidths[i] -= elemWidth; } } - delete this._keyToNodeMap[`$KEY$${elem.key}`]; - this._numNodes--; - this._totalWidth -= elemWidth; + delete this._skipList._keyToNodeMap[`$KEY$${elem.key}`]; + this._skipList._numNodes--; + this._skipList._totalWidth -= elemWidth; + } + + getNode() { + return this.nodes[0].downPtrs[0]; + } +} + +/** + * The skip-list contains "entries", JavaScript objects that each must have a unique "key" + * property that is a string. + */ +class SkipList { + constructor() { + // if there are N elements in the skiplist, "start" is element -1 and "end" is element N + this._start = { + key: null, + levels: 1, + upPtrs: [null], + downPtrs: [null], + downSkips: [1], + downSkipWidths: [0], + }; + this._end = { + key: null, + levels: 1, + upPtrs: [null], + downPtrs: [null], + downSkips: [null], + downSkipWidths: [null], + }; + this._numNodes = 0; + this._totalWidth = 0; + this._keyToNodeMap = {}; + this._start.downPtrs[0] = this._end; + this._end.upPtrs[0] = this._start; + } + + _getNodeAtOffset(targetOffset) { + let i = 0; + let n = this._start; + let lvl = this._start.levels - 1; + while (lvl >= 0 && n.downPtrs[lvl]) { + while (n.downPtrs[lvl] && (i + n.downSkipWidths[lvl] <= targetOffset)) { + i += n.downSkipWidths[lvl]; + n = n.downPtrs[lvl]; + } + lvl--; + } + if (n === this._start) return (this._start.downPtrs[0] || null); + if (n === this._end) { + return targetOffset === this._totalWidth ? (this._end.upPtrs[0] || null) : null; + } + return n; } _propagateWidthChange(node) { @@ -261,7 +264,7 @@ class SkipList { atIndex(i) { if (i < 0) console.warn(`atIndex(${i})`); if (i >= this._numNodes) console.warn(`atIndex(${i}>=${this._numNodes})`); - return _getNodeAtPoint(new Point(this, i)).entry; + return (new Point(this, i)).getNode().entry; } // differs from Array.splice() in that new elements are in an array, not varargs @@ -275,12 +278,10 @@ class SkipList { if (!newEntryArray) newEntryArray = []; const pt = new Point(this, start); - for (let i = 0; i < deleteCount; i++) { - this._deleteKeyAtPoint(pt); - } + for (let i = 0; i < deleteCount; i++) pt.delete(); for (let i = (newEntryArray.length - 1); i >= 0; i--) { const entry = newEntryArray[i]; - this._insertKeyAtPoint(pt, entry); + pt.insert(entry); } } From fc103e7f2a07c8c078bcf1a1c1ccac3dbc1a1b6c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 11 Apr 2021 23:50:42 -0400 Subject: [PATCH 091/218] skiplist: Define a new `Node` class --- src/static/js/skiplist.js | 40 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index 4ef63d73796..80ba434d8f9 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -24,6 +24,18 @@ const _entryWidth = (e) => (e && e.width) || 0; +class Node { + constructor(entry, levels = 0, downSkips = 1, downSkipWidths = 0) { + this.key = entry != null ? entry.key : null; + this.entry = entry; + this.levels = levels; + this.upPtrs = Array(levels).fill(null); + this.downPtrs = Array(levels).fill(null); + this.downSkips = Array(levels).fill(downSkips); + this.downSkipWidths = Array(levels).fill(downSkipWidths); + } +} + // A "point" object at index x allows modifications immediately after the first x elements of the // skiplist, such as multiple inserts or deletes. After an insert or delete using point P, the point // is still valid and points to the same index in the skiplist. Other operations with other points @@ -67,15 +79,7 @@ class Point { } insert(entry) { - const newNode = { - key: entry.key, - entry, - levels: 0, - upPtrs: [], - downPtrs: [], - downSkips: [], - downSkipWidths: [], - }; + const newNode = new Node(entry); const pNodes = this.nodes; const pIdxs = this.idxs; const pLoc = this.loc; @@ -162,22 +166,8 @@ class Point { class SkipList { constructor() { // if there are N elements in the skiplist, "start" is element -1 and "end" is element N - this._start = { - key: null, - levels: 1, - upPtrs: [null], - downPtrs: [null], - downSkips: [1], - downSkipWidths: [0], - }; - this._end = { - key: null, - levels: 1, - upPtrs: [null], - downPtrs: [null], - downSkips: [null], - downSkipWidths: [null], - }; + this._start = new Node(null, 1); + this._end = new Node(null, 1, null, null); this._numNodes = 0; this._totalWidth = 0; this._keyToNodeMap = {}; From 9e2ef6ad5bb69848b01dc1bce24dabd2647b13ea Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 12 Apr 2021 00:24:39 -0400 Subject: [PATCH 092/218] skiplist: Move `propagateWidthChange()` to `Node` class --- src/static/js/skiplist.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index 80ba434d8f9..798991ad90a 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -34,6 +34,22 @@ class Node { this.downSkips = Array(levels).fill(downSkips); this.downSkipWidths = Array(levels).fill(downSkipWidths); } + + propagateWidthChange() { + const oldWidth = this.downSkipWidths[0]; + const newWidth = _entryWidth(this.entry); + const widthChange = newWidth - oldWidth; + let n = this; + let lvl = 0; + while (lvl < n.levels) { + n.downSkipWidths[lvl] += widthChange; + lvl++; + while (lvl >= n.levels && n.upPtrs[lvl - 1]) { + n = n.upPtrs[lvl - 1]; + } + } + return widthChange; + } } // A "point" object at index x allows modifications immediately after the first x elements of the @@ -193,22 +209,6 @@ class SkipList { return n; } - _propagateWidthChange(node) { - const oldWidth = node.downSkipWidths[0]; - const newWidth = _entryWidth(node.entry); - const widthChange = newWidth - oldWidth; - let n = node; - let lvl = 0; - while (lvl < n.levels) { - n.downSkipWidths[lvl] += widthChange; - lvl++; - while (lvl >= n.levels && n.upPtrs[lvl - 1]) { - n = n.upPtrs[lvl - 1]; - } - } - this._totalWidth += widthChange; - } - _getNodeIndex(node, byWidth) { let dist = (byWidth ? 0 : -1); let n = node; @@ -313,7 +313,7 @@ class SkipList { offsetOfEntry(entry) { return this.offsetOfKey(entry.key); } setEntryWidth(entry, width) { entry.width = width; - this._propagateWidthChange(this._getNodeByKey(entry.key)); + this._totalWidth += this._keyToNodeMap.get(entry.key).propagateWidthChange(); } totalWidth() { return this._totalWidth; } offsetOfIndex(i) { From e2eb7327c2bbc7f6b29859c9a9b17fe97d60c5b7 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 12 Apr 2021 01:35:27 -0400 Subject: [PATCH 093/218] skiplist: Sanity check inserted entries --- src/static/js/skiplist.js | 5 +++++ src/tests/frontend/index.html | 1 + src/tests/frontend/runner.js | 5 +++++ src/tests/frontend/specs/skiplist.js | 18 ++++++++++++++++++ 4 files changed, 29 insertions(+) create mode 100644 src/tests/frontend/specs/skiplist.js diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index 798991ad90a..105f5902c3b 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -95,6 +95,11 @@ class Point { } insert(entry) { + if (entry.key == null) throw new Error('entry.key must not be null'); + if (this._skipList.containsKey(entry.key)) { + throw new Error(`an entry with key ${entry.key} already exists`); + } + const newNode = new Node(entry); const pNodes = this.nodes; const pIdxs = this.idxs; diff --git a/src/tests/frontend/index.html b/src/tests/frontend/index.html index 3b0f1c6e08c..6e2f8ca7786 100644 --- a/src/tests/frontend/index.html +++ b/src/tests/frontend/index.html @@ -9,6 +9,7 @@
+ diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index d9f37bcae9c..b119f296318 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -159,6 +159,11 @@ $(() => { // get the list of specs and filter it if requested const specs = specs_list.slice(); + const absUrl = (url) => new URL(url, window.location.href).href; + require.setRootURI(absUrl('../../javascripts/src')); + require.setLibraryURI(absUrl('../../javascripts/lib')); + require.setGlobalKeyPath('require'); + // inject spec scripts into the dom const $body = $('body'); $.each(specs, (i, spec) => { diff --git a/src/tests/frontend/specs/skiplist.js b/src/tests/frontend/specs/skiplist.js new file mode 100644 index 00000000000..4b2ebd5aafa --- /dev/null +++ b/src/tests/frontend/specs/skiplist.js @@ -0,0 +1,18 @@ +'use strict'; + +const SkipList = require('ep_etherpad-lite/static/js/skiplist'); + +describe('skiplist.js', function () { + it('rejects null keys', async function () { + const skiplist = new SkipList(); + for (const key of [undefined, null]) { + expect(() => skiplist.push({key})).to.throwError(); + } + }); + + it('rejects duplicate keys', async function () { + const skiplist = new SkipList(); + skiplist.push({key: 'foo'}); + expect(() => skiplist.push({key: 'foo'})).to.throwError(); + }); +}); From 1cdfe9193b36b952aba391cec03c5e4aefd5c8b4 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 12 Apr 2021 00:37:17 -0400 Subject: [PATCH 094/218] skiplist: Convert `_keyToNodeMap` to a `Map` object --- src/static/js/skiplist.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index 105f5902c3b..4b0bc82d70f 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -146,7 +146,7 @@ class Point { up.downSkips[lvl]++; up.downSkipWidths[lvl] += newWidth; } - this._skipList._keyToNodeMap[`$KEY$${newNode.key}`] = newNode; + this._skipList._keyToNodeMap.set(newNode.key, newNode); this._skipList._numNodes++; this._skipList._totalWidth += newWidth; } @@ -170,7 +170,7 @@ class Point { up.downSkipWidths[i] -= elemWidth; } } - delete this._skipList._keyToNodeMap[`$KEY$${elem.key}`]; + this._skipList._keyToNodeMap.delete(elem.key); this._skipList._numNodes--; this._skipList._totalWidth -= elemWidth; } @@ -191,7 +191,7 @@ class SkipList { this._end = new Node(null, 1, null, null); this._numNodes = 0; this._totalWidth = 0; - this._keyToNodeMap = {}; + this._keyToNodeMap = new Map(); this._start.downPtrs[0] = this._end; this._end.upPtrs[0] = this._start; } @@ -226,8 +226,6 @@ class SkipList { return dist; } - _getNodeByKey(key) { return this._keyToNodeMap[`$KEY$${key}`]; } - // Returns index of first entry such that entryFunc(entry) is truthy, // or length() if no such entry. Assumes all falsy entries come before // all truthy entries. @@ -280,8 +278,8 @@ class SkipList { } } - next(entry) { return this._getNodeByKey(entry.key).downPtrs[0].entry || null; } - prev(entry) { return this._getNodeByKey(entry.key).upPtrs[0].entry || null; } + next(entry) { return this._keyToNodeMap.get(entry.key).downPtrs[0].entry || null; } + prev(entry) { return this._keyToNodeMap.get(entry.key).upPtrs[0].entry || null; } push(entry) { this.splice(this._numNodes, 0, [entry]); } slice(start, end) { @@ -307,14 +305,14 @@ class SkipList { return array; } - atKey(key) { return this._getNodeByKey(key).entry; } - indexOfKey(key) { return this._getNodeIndex(this._getNodeByKey(key)); } + atKey(key) { return this._keyToNodeMap.get(key).entry; } + indexOfKey(key) { return this._getNodeIndex(this._keyToNodeMap.get(key)); } indexOfEntry(entry) { return this.indexOfKey(entry.key); } - containsKey(key) { return !!this._getNodeByKey(key); } + containsKey(key) { return this._keyToNodeMap.has(key); } // gets the last entry starting at or before the offset atOffset(offset) { return this._getNodeAtOffset(offset).entry; } keyAtOffset(offset) { return this.atOffset(offset).key; } - offsetOfKey(key) { return this._getNodeIndex(this._getNodeByKey(key), true); } + offsetOfKey(key) { return this._getNodeIndex(this._keyToNodeMap.get(key), true); } offsetOfEntry(entry) { return this.offsetOfKey(entry.key); } setEntryWidth(entry, width) { entry.width = width; From c00031a8d8a6c6fde7d334cbaf10270d1103f2a0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 12 Apr 2021 03:42:16 -0400 Subject: [PATCH 095/218] skiplist: Use `Map.size` to get number of nodes --- src/static/js/skiplist.js | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index 4b0bc82d70f..aeb67733607 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -121,7 +121,7 @@ class Point { this._skipList._end.levels++; this._skipList._start.downPtrs[lvl] = this._skipList._end; this._skipList._end.upPtrs[lvl] = this._skipList._start; - this._skipList._start.downSkips[lvl] = this._skipList._numNodes + 1; + this._skipList._start.downSkips[lvl] = this._skipList._keyToNodeMap.size + 1; this._skipList._start.downSkipWidths[lvl] = this._skipList._totalWidth; this.widthSkips[lvl] = 0; } @@ -147,7 +147,6 @@ class Point { up.downSkipWidths[lvl] += newWidth; } this._skipList._keyToNodeMap.set(newNode.key, newNode); - this._skipList._numNodes++; this._skipList._totalWidth += newWidth; } @@ -171,7 +170,6 @@ class Point { } } this._skipList._keyToNodeMap.delete(elem.key); - this._skipList._numNodes--; this._skipList._totalWidth -= elemWidth; } @@ -189,7 +187,6 @@ class SkipList { // if there are N elements in the skiplist, "start" is element -1 and "end" is element N this._start = new Node(null, 1); this._end = new Node(null, 1, null, null); - this._numNodes = 0; this._totalWidth = 0; this._keyToNodeMap = new Map(); this._start.downPtrs[0] = this._end; @@ -252,20 +249,20 @@ class SkipList { return lowIndex + 1; } - length() { return this._numNodes; } + length() { return this._keyToNodeMap.size; } atIndex(i) { if (i < 0) console.warn(`atIndex(${i})`); - if (i >= this._numNodes) console.warn(`atIndex(${i}>=${this._numNodes})`); + if (i >= this._keyToNodeMap.size) console.warn(`atIndex(${i}>=${this._keyToNodeMap.size})`); return (new Point(this, i)).getNode().entry; } // differs from Array.splice() in that new elements are in an array, not varargs splice(start, deleteCount, newEntryArray) { if (start < 0) console.warn(`splice(${start}, ...)`); - if (start + deleteCount > this._numNodes) { - console.warn(`splice(${start}, ${deleteCount}, ...), N=${this._numNodes}`); - console.warn('%s %s %s', typeof start, typeof deleteCount, typeof this._numNodes); + if (start + deleteCount > this._keyToNodeMap.size) { + console.warn(`splice(${start}, ${deleteCount}, ...), N=${this._keyToNodeMap.size}`); + console.warn('%s %s %s', typeof start, typeof deleteCount, typeof this._keyToNodeMap.size); console.trace(); } @@ -280,21 +277,21 @@ class SkipList { next(entry) { return this._keyToNodeMap.get(entry.key).downPtrs[0].entry || null; } prev(entry) { return this._keyToNodeMap.get(entry.key).upPtrs[0].entry || null; } - push(entry) { this.splice(this._numNodes, 0, [entry]); } + push(entry) { this.splice(this._keyToNodeMap.size, 0, [entry]); } slice(start, end) { // act like Array.slice() if (start === undefined) start = 0; - else if (start < 0) start += this._numNodes; - if (end === undefined) end = this._numNodes; - else if (end < 0) end += this._numNodes; + else if (start < 0) start += this._keyToNodeMap.size; + if (end === undefined) end = this._keyToNodeMap.size; + else if (end < 0) end += this._keyToNodeMap.size; if (start < 0) start = 0; - if (start > this._numNodes) start = this._numNodes; + if (start > this._keyToNodeMap.size) start = this._keyToNodeMap.size; if (end < 0) end = 0; - if (end > this._numNodes) end = this._numNodes; + if (end > this._keyToNodeMap.size) end = this._keyToNodeMap.size; - window.dmesg(String([start, end, this._numNodes])); + window.dmesg(String([start, end, this._keyToNodeMap.size])); if (end <= start) return []; let n = this.atIndex(start); const array = [n]; @@ -321,12 +318,12 @@ class SkipList { totalWidth() { return this._totalWidth; } offsetOfIndex(i) { if (i < 0) return 0; - if (i >= this._numNodes) return this._totalWidth; + if (i >= this._keyToNodeMap.size) return this._totalWidth; return this.offsetOfEntry(this.atIndex(i)); } indexOfOffset(offset) { if (offset <= 0) return 0; - if (offset >= this._totalWidth) return this._numNodes; + if (offset >= this._totalWidth) return this._keyToNodeMap.size; return this.indexOfEntry(this.atOffset(offset)); } } From a58fa4a2c59968df4fb2ce85da20fd062170a4f8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 12 Apr 2021 03:43:17 -0400 Subject: [PATCH 096/218] tests: Add tests for `SkipList.atOffset()` --- src/tests/frontend/specs/skiplist.js | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/tests/frontend/specs/skiplist.js b/src/tests/frontend/specs/skiplist.js index 4b2ebd5aafa..16b98561515 100644 --- a/src/tests/frontend/specs/skiplist.js +++ b/src/tests/frontend/specs/skiplist.js @@ -15,4 +15,40 @@ describe('skiplist.js', function () { skiplist.push({key: 'foo'}); expect(() => skiplist.push({key: 'foo'})).to.throwError(); }); + + it('atOffset() returns last entry that touches offset', async function () { + const skiplist = new SkipList(); + const entries = []; + let nextId = 0; + const makeEntry = (width) => { + const entry = {key: `id${nextId++}`, width}; + entries.push(entry); + return entry; + }; + + skiplist.push(makeEntry(5)); + expect(skiplist.atOffset(4)).to.be(entries[0]); + expect(skiplist.atOffset(5)).to.be(entries[0]); + expect(() => skiplist.atOffset(6)).to.throwError(); + + skiplist.push(makeEntry(0)); + expect(skiplist.atOffset(4)).to.be(entries[0]); + expect(skiplist.atOffset(5)).to.be(entries[1]); + expect(() => skiplist.atOffset(6)).to.throwError(); + + skiplist.push(makeEntry(0)); + expect(skiplist.atOffset(4)).to.be(entries[0]); + expect(skiplist.atOffset(5)).to.be(entries[2]); + expect(() => skiplist.atOffset(6)).to.throwError(); + + skiplist.splice(2, 0, [makeEntry(0)]); + expect(skiplist.atOffset(4)).to.be(entries[0]); + expect(skiplist.atOffset(5)).to.be(entries[2]); + expect(() => skiplist.atOffset(6)).to.throwError(); + + skiplist.push(makeEntry(3)); + expect(skiplist.atOffset(4)).to.be(entries[0]); + expect(skiplist.atOffset(5)).to.be(entries[4]); + expect(skiplist.atOffset(6)).to.be(entries[4]); + }); }); From 59be8d5c0577d80f2f506fa1159474af4cf12a27 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 14:58:07 -0400 Subject: [PATCH 097/218] remote_runner: Delete commented-out browsers Also delete useless and incorrect browser comments. --- src/tests/frontend/travis/remote_runner.js | 26 ---------------------- 1 file changed, 26 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 13498e4e75d..b621ceeb2e7 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -129,59 +129,33 @@ const sauceTestWorker = async.queue((testSettings, callback) => { }, 6); // run 6 tests in parrallel if (!isAdminRunner) { - // 1) Firefox on Linux sauceTestWorker.push({ platform: 'Windows 10', browserName: 'firefox', version: '84.0', }); - - // 2) Chrome on Linux sauceTestWorker.push({ platform: 'Windows 7', browserName: 'chrome', version: '55.0', 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({ platform: 'OS X 10.15', browserName: 'safari', version: '13.1', }); - // IE 10 doesn't appear to be working anyway - /* - // 4) IE 10 on Win 8 - sauceTestWorker.push({ - 'platform' : 'Windows 8' - , 'browserName' : 'iexplore' - , 'version' : '10.0' - }); - */ - // 5) Edge on Win 10 sauceTestWorker.push({ platform: 'Windows 10', browserName: 'microsoftedge', version: '83.0', }); - // 6) Firefox on Win 7 sauceTestWorker.push({ platform: 'Windows 7', browserName: 'firefox', version: '78.0', }); } else { - // 4) Safari on OSX 10.14 sauceTestWorker.push({ platform: 'OS X 10.15', browserName: 'safari', From 1f3a831cc3bf969a6005197d22e841f995665c0a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 15:04:12 -0400 Subject: [PATCH 098/218] remote_runner: Avoid duplication in task list --- src/tests/frontend/travis/remote_runner.js | 58 ++++++++++------------ 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index b621ceeb2e7..98b4c18467d 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -128,40 +128,36 @@ const sauceTestWorker = async.queue((testSettings, callback) => { }); }, 6); // run 6 tests in parrallel -if (!isAdminRunner) { - sauceTestWorker.push({ - platform: 'Windows 10', - browserName: 'firefox', - version: '84.0', - }); - sauceTestWorker.push({ - platform: 'Windows 7', - browserName: 'chrome', - version: '55.0', - args: ['--use-fake-device-for-media-stream'], - }); - sauceTestWorker.push({ +[ + { platform: 'OS X 10.15', browserName: 'safari', version: '13.1', - }); - sauceTestWorker.push({ - platform: 'Windows 10', - browserName: 'microsoftedge', - version: '83.0', - }); - sauceTestWorker.push({ - platform: 'Windows 7', - browserName: 'firefox', - version: '78.0', - }); -} else { - sauceTestWorker.push({ - platform: 'OS X 10.15', - browserName: 'safari', - version: '13.1', - }); -} + }, + ...(isAdminRunner ? [] : [ + { + platform: 'Windows 10', + browserName: 'firefox', + version: '84.0', + }, + { + platform: 'Windows 7', + browserName: 'chrome', + version: '55.0', + args: ['--use-fake-device-for-media-stream'], + }, + { + platform: 'Windows 10', + browserName: 'microsoftedge', + version: '83.0', + }, + { + platform: 'Windows 7', + browserName: 'firefox', + version: '78.0', + }, + ]), +].forEach((task) => sauceTestWorker.push(task)); sauceTestWorker.drain(() => { process.exit(allTestsPassed ? 0 : 1); From 014e19cf7d1713d66d6e9369d99c9640328f3461 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 15:28:55 -0400 Subject: [PATCH 099/218] remote_runner: `await` each browser test --- src/tests/frontend/travis/remote_runner.js | 28 ++++++---------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 98b4c18467d..7f5cfa13173 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -1,5 +1,9 @@ 'use strict'; +// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an +// unhandled rejection into an uncaught exception, which does cause Node.js to exit. +process.on('unhandledRejection', (err) => { throw err; }); + const async = require('async'); const wd = require('wd'); @@ -12,18 +16,6 @@ const config = { const isAdminRunner = process.argv[2] === 'admin'; -let allTestsPassed = true; -// overwrite the default exit code -// in case not all worker can be run (due to saucelabs limits), -// `queue.drain` below will not be called -// and the script would silently exit with error code 0 -process.exitCode = 2; -process.on('exit', (code) => { - if (code === 2) { - console.log('\x1B[31mFAILED\x1B[39m Not all saucelabs runner have been started.'); - } -}); - const sauceTestWorker = async.queue((testSettings, callback) => { const browser = wd.promiseChainRemote( config.host, config.port, config.username, config.accessKey); @@ -48,9 +40,7 @@ const sauceTestWorker = async.queue((testSettings, callback) => { clearTimeout(timeout); browser.quit(() => { - if (!success) { - allTestsPassed = false; - } + if (!success) process.exitCode = 1; // if stopSauce is called via timeout // (in contrast to via getStatusInterval) than the log of up to the last @@ -128,7 +118,7 @@ const sauceTestWorker = async.queue((testSettings, callback) => { }); }, 6); // run 6 tests in parrallel -[ +Promise.all([ { platform: 'OS X 10.15', browserName: 'safari', @@ -157,8 +147,4 @@ const sauceTestWorker = async.queue((testSettings, callback) => { version: '78.0', }, ]), -].forEach((task) => sauceTestWorker.push(task)); - -sauceTestWorker.drain(() => { - process.exit(allTestsPassed ? 0 : 1); -}); +].map(async (task) => await sauceTestWorker.push(task))); From 925f789d4c9d1f2987897a1d0f20743d58906c74 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 15:44:52 -0400 Subject: [PATCH 100/218] remote_runner: Simplify logging --- src/tests/frontend/travis/remote_runner.js | 38 ++++++++++++---------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 7f5cfa13173..39d25ef1d77 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -16,13 +16,25 @@ const config = { const isAdminRunner = process.argv[2] === 'admin'; +const colorSubst = { + red: '\x1B[31m', + yellow: '\x1B[33m', + green: '\x1B[32m', + clear: '\x1B[39m', +}; +const colorRegex = new RegExp(`\\[(${Object.keys(colorSubst).join('|')})\\]`, 'g'); + +const log = (msg, pfx = '') => { + console.log(`${pfx}${msg.replace(colorRegex, (m, p1) => colorSubst[p1])}`); +}; + const sauceTestWorker = async.queue((testSettings, callback) => { + const name = `${testSettings.browserName} ${testSettings.version}, ${testSettings.platform}`; + const pfx = `[${name}] `; + const fullName = [process.env.GIT_HASH].concat(process.env.SAUCE_NAME || [], name).join(' - '); const browser = wd.promiseChainRemote( config.host, config.port, config.username, config.accessKey); - const name = [process.env.GIT_HASH].concat(process.env.SAUCE_NAME || []).concat([ - `${testSettings.browserName} ${testSettings.version}, ${testSettings.platform}`, - ]).join(' - '); - testSettings.name = name; + testSettings.name = fullName; testSettings.public = true; testSettings.build = process.env.GIT_HASH; // console.json can be downloaded via saucelabs, @@ -32,7 +44,7 @@ const sauceTestWorker = async.queue((testSettings, callback) => { browser.init(testSettings).get('http://localhost:9001/tests/frontend/', () => { const url = `https://saucelabs.com/jobs/${browser.sessionID}`; - console.log(`Remote sauce test '${name}' started! ${url}`); + log(`Remote sauce test started! ${url}`, pfx); // tear down the test excecution const stopSauce = (success, timesup) => { @@ -48,11 +60,9 @@ const sauceTestWorker = async.queue((testSettings, callback) => { printLog(logIndex); if (timesup) { - console.log(`[${testSettings.browserName} ${testSettings.platform}` + - `${testSettings.version === '' ? '' : (` ${testSettings.version}`)}]` + - ' \x1B[31mFAILED\x1B[39m allowed test duration exceeded'); + log('[red]FAILED[clear] allowed test duration exceeded', pfx); } - console.log(`Remote sauce test '${name}' finished! ${url}`); + log(`Remote sauce test finished! ${url}`, pfx); callback(); }); @@ -105,15 +115,7 @@ const sauceTestWorker = async.queue((testSettings, callback) => { * @param {number} index offset from where to start */ const printLog = (index) => { - let testResult = knownConsoleText.substring(index) - .replace(/\[red\]/g, '\x1B[31m').replace(/\[yellow\]/g, '\x1B[33m') - .replace(/\[green\]/g, '\x1B[32m').replace(/\[clear\]/g, '\x1B[39m'); - testResult = testResult.split('\\n').map((line) => `[${testSettings.browserName} ` + - `${testSettings.platform}` + - `${testSettings.version === '' ? '' : (` ${testSettings.version}`)}]` + - `${line}`).join('\n'); - - console.log(testResult); + knownConsoleText.substring(index).split('\\n').forEach((line) => log(line, pfx)); }; }); }, 6); // run 6 tests in parrallel From a12c4757768cb7c63d944d2f15d66ca37c1d370d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 16:01:06 -0400 Subject: [PATCH 101/218] remote_runner: Use an options object to create webdriver object --- src/tests/frontend/travis/remote_runner.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 39d25ef1d77..05c15f04628 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -8,10 +8,10 @@ const async = require('async'); const wd = require('wd'); const config = { - host: 'ondemand.saucelabs.com', + hostname: 'ondemand.saucelabs.com', port: 80, - username: process.env.SAUCE_USER, - accessKey: process.env.SAUCE_ACCESS_KEY, + user: process.env.SAUCE_USER, + pwd: process.env.SAUCE_ACCESS_KEY, }; const isAdminRunner = process.argv[2] === 'admin'; @@ -32,8 +32,6 @@ const sauceTestWorker = async.queue((testSettings, callback) => { const name = `${testSettings.browserName} ${testSettings.version}, ${testSettings.platform}`; const pfx = `[${name}] `; const fullName = [process.env.GIT_HASH].concat(process.env.SAUCE_NAME || [], name).join(' - '); - const browser = wd.promiseChainRemote( - config.host, config.port, config.username, config.accessKey); testSettings.name = fullName; testSettings.public = true; testSettings.build = process.env.GIT_HASH; @@ -41,7 +39,7 @@ const sauceTestWorker = async.queue((testSettings, callback) => { // don't know how to print them into output of the tests testSettings.extendedDebugging = true; testSettings.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; - + const browser = wd.remote(config, 'promiseChain'); browser.init(testSettings).get('http://localhost:9001/tests/frontend/', () => { const url = `https://saucelabs.com/jobs/${browser.sessionID}`; log(`Remote sauce test started! ${url}`, pfx); From 08856fe42ea3ee9d25a6a24a6247217622406e7d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 16:08:00 -0400 Subject: [PATCH 102/218] remote_runner: Move `logIndex` updates into `printLog()` --- src/tests/frontend/travis/remote_runner.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 05c15f04628..39634b05506 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -55,7 +55,7 @@ const sauceTestWorker = async.queue((testSettings, callback) => { // if stopSauce is called via timeout // (in contrast to via getStatusInterval) than the log of up to the last // five seconds may not be available here. It's an error anyway, so don't care about it. - printLog(logIndex); + printLog(); if (timesup) { log('[red]FAILED[clear] allowed test duration exceeded', pfx); @@ -100,8 +100,7 @@ const sauceTestWorker = async.queue((testSettings, callback) => { } } else { // not finished yet - printLog(logIndex); - logIndex = knownConsoleText.length; + printLog(); } }); }, 5000); @@ -109,11 +108,10 @@ const sauceTestWorker = async.queue((testSettings, callback) => { /** * Replaces color codes in the test runners log, appends * browser name, platform etc. to every line and prints them. - * - * @param {number} index offset from where to start */ - const printLog = (index) => { - knownConsoleText.substring(index).split('\\n').forEach((line) => log(line, pfx)); + const printLog = () => { + knownConsoleText.substring(logIndex).split('\\n').forEach((line) => log(line, pfx)); + logIndex = knownConsoleText.length; }; }); }, 6); // run 6 tests in parrallel From b0e367a982d98131ab9d8c9215704713fe96e188 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 16:19:24 -0400 Subject: [PATCH 103/218] remote_runner: Simplify logging of console text --- src/tests/frontend/travis/remote_runner.js | 27 ++++------------------ 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 39634b05506..1430349773d 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -51,12 +51,6 @@ const sauceTestWorker = async.queue((testSettings, callback) => { browser.quit(() => { if (!success) process.exitCode = 1; - - // if stopSauce is called via timeout - // (in contrast to via getStatusInterval) than the log of up to the last - // five seconds may not be available here. It's an error anyway, so don't care about it. - printLog(); - if (timesup) { log('[red]FAILED[clear] allowed test duration exceeded', pfx); } @@ -77,7 +71,6 @@ const sauceTestWorker = async.queue((testSettings, callback) => { stopSauce(false, true); }, 870000); // travis timeout is 15 minutes, set this to a slightly lower value - let knownConsoleText = ''; // how many characters of the log have been sent to travis let logIndex = 0; const getStatusInterval = setInterval(() => { @@ -85,10 +78,10 @@ const sauceTestWorker = async.queue((testSettings, callback) => { if (!consoleText || err) { return; } - knownConsoleText = consoleText; - - if (knownConsoleText.indexOf('FINISHED') > 0) { - const match = knownConsoleText.match( + consoleText.substring(logIndex).split('\\n').forEach((line) => log(line, pfx)); + logIndex = consoleText.length; + if (consoleText.indexOf('FINISHED') > 0) { + const match = consoleText.match( /FINISHED.*([0-9]+) tests passed, ([0-9]+) tests failed/); // finished without failures if (match[2] && match[2] === '0') { @@ -98,21 +91,9 @@ const sauceTestWorker = async.queue((testSettings, callback) => { } else { stopSauce(false); } - } else { - // not finished yet - printLog(); } }); }, 5000); - - /** - * Replaces color codes in the test runners log, appends - * browser name, platform etc. to every line and prints them. - */ - const printLog = () => { - knownConsoleText.substring(logIndex).split('\\n').forEach((line) => log(line, pfx)); - logIndex = knownConsoleText.length; - }; }); }, 6); // run 6 tests in parrallel From 4ec02a9af9f85c2dba526687ba6a33d715b8fd29 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 16:32:06 -0400 Subject: [PATCH 104/218] remote_runner: Simplify finished test check --- src/tests/frontend/travis/remote_runner.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 1430349773d..236a0aaa5cb 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -28,6 +28,8 @@ const log = (msg, pfx = '') => { console.log(`${pfx}${msg.replace(colorRegex, (m, p1) => colorSubst[p1])}`); }; +const finishedRegex = /FINISHED.*[0-9]+ tests passed, ([0-9]+) tests failed/; + const sauceTestWorker = async.queue((testSettings, callback) => { const name = `${testSettings.browserName} ${testSettings.version}, ${testSettings.platform}`; const pfx = `[${name}] `; @@ -80,18 +82,8 @@ const sauceTestWorker = async.queue((testSettings, callback) => { } consoleText.substring(logIndex).split('\\n').forEach((line) => log(line, pfx)); logIndex = consoleText.length; - if (consoleText.indexOf('FINISHED') > 0) { - const match = consoleText.match( - /FINISHED.*([0-9]+) tests passed, ([0-9]+) tests failed/); - // finished without failures - if (match[2] && match[2] === '0') { - stopSauce(true); - - // finished but some tests did not return or some tests failed - } else { - stopSauce(false); - } - } + const [finished, nFailedStr] = consoleText.match(finishedRegex) || []; + if (finished) stopSauce(nFailedStr === '0'); }); }, 5000); }); From 7f57b17b2e4d0cd7882a9fb2fcc80e4e7e8518bc Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 16:40:01 -0400 Subject: [PATCH 105/218] remote_runner: Use Error objects to convey pass/fail --- src/tests/frontend/travis/remote_runner.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 236a0aaa5cb..3144b95ed40 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -47,14 +47,14 @@ const sauceTestWorker = async.queue((testSettings, callback) => { log(`Remote sauce test started! ${url}`, pfx); // tear down the test excecution - const stopSauce = (success, timesup) => { + const stopSauce = (err) => { clearInterval(getStatusInterval); clearTimeout(timeout); browser.quit(() => { - if (!success) process.exitCode = 1; - if (timesup) { - log('[red]FAILED[clear] allowed test duration exceeded', pfx); + if (err) { + log(`[red]FAILED[clear] ${err}`, pfx); + process.exitCode = 1; } log(`Remote sauce test finished! ${url}`, pfx); @@ -70,7 +70,7 @@ const sauceTestWorker = async.queue((testSettings, callback) => { * https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-Timeouts */ const timeout = setTimeout(() => { - stopSauce(false, true); + stopSauce(new Error('allowed test duration exceeded')); }, 870000); // travis timeout is 15 minutes, set this to a slightly lower value // how many characters of the log have been sent to travis @@ -83,7 +83,9 @@ const sauceTestWorker = async.queue((testSettings, callback) => { consoleText.substring(logIndex).split('\\n').forEach((line) => log(line, pfx)); logIndex = consoleText.length; const [finished, nFailedStr] = consoleText.match(finishedRegex) || []; - if (finished) stopSauce(nFailedStr === '0'); + if (finished) { + stopSauce(nFailedStr === '0' ? null : new Error(`${nFailedStr} tests failed`)); + } }); }, 5000); }); From c803ec81f1a247e87a05b43de5ae5c8e3f7cbc29 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 17:10:32 -0400 Subject: [PATCH 106/218] remote_runner: Handle webdriver errors --- src/tests/frontend/travis/remote_runner.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 3144b95ed40..d190c66e757 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -42,22 +42,22 @@ const sauceTestWorker = async.queue((testSettings, callback) => { testSettings.extendedDebugging = true; testSettings.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; const browser = wd.remote(config, 'promiseChain'); - browser.init(testSettings).get('http://localhost:9001/tests/frontend/', () => { + browser.init(testSettings).get('http://localhost:9001/tests/frontend/', (err) => { + if (err != null) return callback(err); const url = `https://saucelabs.com/jobs/${browser.sessionID}`; log(`Remote sauce test started! ${url}`, pfx); // tear down the test excecution - const stopSauce = (err) => { + const stopSauce = (testErr) => { clearInterval(getStatusInterval); clearTimeout(timeout); - - browser.quit(() => { - if (err) { - log(`[red]FAILED[clear] ${err}`, pfx); + browser.quit((err) => { + if (err) return callback(err); + if (testErr) { + log(`[red]FAILED[clear] ${testErr}`, pfx); process.exitCode = 1; } log(`Remote sauce test finished! ${url}`, pfx); - callback(); }); }; @@ -77,9 +77,8 @@ const sauceTestWorker = async.queue((testSettings, callback) => { let logIndex = 0; const getStatusInterval = setInterval(() => { browser.eval("$('#console').text()", (err, consoleText) => { - if (!consoleText || err) { - return; - } + if (err != null) return stopSauce(err); + if (!consoleText) return; consoleText.substring(logIndex).split('\\n').forEach((line) => log(line, pfx)); logIndex = consoleText.length; const [finished, nFailedStr] = consoleText.match(finishedRegex) || []; From 9059a5587388d2937b95244020c6eb7ae81b6772 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 17:16:03 -0400 Subject: [PATCH 107/218] remote_runner: Improve readability of timeout duration --- src/tests/frontend/travis/remote_runner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index d190c66e757..be19baff1ca 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -71,7 +71,7 @@ const sauceTestWorker = async.queue((testSettings, callback) => { */ const timeout = setTimeout(() => { stopSauce(new Error('allowed test duration exceeded')); - }, 870000); // travis timeout is 15 minutes, set this to a slightly lower value + }, 14.5 * 60 * 1000); // Slightly less than overall test timeout. // how many characters of the log have been sent to travis let logIndex = 0; From bbb3046a8753c629fd77c6e1f83151dfccff0ac0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 17:35:06 -0400 Subject: [PATCH 108/218] remote_runner: Promisify --- src/tests/frontend/travis/remote_runner.js | 75 ++++++++-------------- 1 file changed, 28 insertions(+), 47 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index be19baff1ca..67330b19b82 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -30,7 +30,7 @@ const log = (msg, pfx = '') => { const finishedRegex = /FINISHED.*[0-9]+ tests passed, ([0-9]+) tests failed/; -const sauceTestWorker = async.queue((testSettings, callback) => { +const sauceTestWorker = async.queue(async (testSettings) => { const name = `${testSettings.browserName} ${testSettings.version}, ${testSettings.platform}`; const pfx = `[${name}] `; const fullName = [process.env.GIT_HASH].concat(process.env.SAUCE_NAME || [], name).join(' - '); @@ -42,52 +42,33 @@ const sauceTestWorker = async.queue((testSettings, callback) => { testSettings.extendedDebugging = true; testSettings.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; const browser = wd.remote(config, 'promiseChain'); - browser.init(testSettings).get('http://localhost:9001/tests/frontend/', (err) => { - if (err != null) return callback(err); - const url = `https://saucelabs.com/jobs/${browser.sessionID}`; - log(`Remote sauce test started! ${url}`, pfx); - - // tear down the test excecution - const stopSauce = (testErr) => { - clearInterval(getStatusInterval); - clearTimeout(timeout); - browser.quit((err) => { - if (err) return callback(err); - if (testErr) { - log(`[red]FAILED[clear] ${testErr}`, pfx); - process.exitCode = 1; - } - log(`Remote sauce test finished! ${url}`, pfx); - callback(); - }); - }; - - /** - * timeout if a test hangs or the job exceeds 14.5 minutes - * It's necessary because if travis kills the saucelabs session due to inactivity, - * we don't get any output - * @todo this should be configured in testSettings, see - * https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-Timeouts - */ - const timeout = setTimeout(() => { - stopSauce(new Error('allowed test duration exceeded')); - }, 14.5 * 60 * 1000); // Slightly less than overall test timeout. - - // how many characters of the log have been sent to travis - let logIndex = 0; - const getStatusInterval = setInterval(() => { - browser.eval("$('#console').text()", (err, consoleText) => { - if (err != null) return stopSauce(err); - if (!consoleText) return; - consoleText.substring(logIndex).split('\\n').forEach((line) => log(line, pfx)); - logIndex = consoleText.length; - const [finished, nFailedStr] = consoleText.match(finishedRegex) || []; - if (finished) { - stopSauce(nFailedStr === '0' ? null : new Error(`${nFailedStr} tests failed`)); - } - }); - }, 5000); - }); + await browser.init(testSettings); + const url = `https://saucelabs.com/jobs/${browser.sessionID}`; + await browser.get('http://localhost:9001/tests/frontend/'); + log(`Remote sauce test started! ${url}`, pfx); + // @TODO this should be configured in testSettings, see + // https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-Timeouts + const deadline = Date.now() + 14.5 * 60 * 1000; // Slightly less than overall test timeout. + // how many characters of the log have been sent to travis + let logIndex = 0; + while (true) { + const consoleText = await browser.eval("$('#console').text()") || ''; + consoleText.substring(logIndex).split('\\n').forEach((line) => log(line, pfx)); + logIndex = consoleText.length; + const [finished, nFailedStr] = consoleText.match(finishedRegex) || []; + if (finished) { + if (nFailedStr !== '0') process.exitCode = 1; + break; + } + if (Date.now() >= deadline) { + log('[red]FAILED[clear] allowed test duration exceeded'); + process.exitCode = 1; + break; + } + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + log(`Remote sauce test finished! ${url}`, pfx); + await browser.quit(); }, 6); // run 6 tests in parrallel Promise.all([ From 7d75e0ef8f63f291f8f8f6ffa40cc6302c199895 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 20:28:16 -0400 Subject: [PATCH 109/218] remote_runner: Simplify `append()` --- src/tests/frontend/runner.js | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index b119f296318..8f6665cba88 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -102,27 +102,13 @@ $(() => { const $console = $('#console'); const append = (text) => { - const oldText = $console.text(); - - let space = ''; - for (let i = 0; i < level * 2; i++) { - space += ' '; - } - - let splitedText = ''; - _(text.split('\n')).each((line) => { - while (line.length > 0) { - const split = line.substr(0, 100); - line = line.substr(100); - if (splitedText.length > 0) splitedText += '\n'; - splitedText += split; - } - }); - - // indent all lines with the given amount of space - const newText = _(splitedText.split('\n')).map((line) => space + line).join('\\n'); - - $console.text(`${oldText + newText}\\n`); + const lines = text.split('\n') + // Break long lines into multiple lines: + .map((line) => line.match(/.{1,100}/g)) + .reduce((soFar, next) => soFar.concat(next), []) + // Indent each line: + .map((line) => ' '.repeat(level * 2) + line); + $console.append(document.createTextNode(`${lines.join('\\n')}\\n`)); }; const total = runner.total; From 713e57b451080586c70c85de81d8a4c0e8ecb7e8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 20:31:25 -0400 Subject: [PATCH 110/218] remote_runner: Don't break long lines Breaking lines makes it harder to read and search the test output. --- src/tests/frontend/runner.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index 8f6665cba88..c405b79eec7 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -102,12 +102,8 @@ $(() => { const $console = $('#console'); const append = (text) => { - const lines = text.split('\n') - // Break long lines into multiple lines: - .map((line) => line.match(/.{1,100}/g)) - .reduce((soFar, next) => soFar.concat(next), []) - // Indent each line: - .map((line) => ' '.repeat(level * 2) + line); + // Indent each line. + const lines = text.split('\n').map((line) => ' '.repeat(level * 2) + line); $console.append(document.createTextNode(`${lines.join('\\n')}\\n`)); }; From 68b041c4fb96e036b28ca6d891b77926ba61128b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 20:46:00 -0400 Subject: [PATCH 111/218] remote_runner: Use newline instead of backslash n --- src/tests/frontend/runner.js | 2 +- src/tests/frontend/travis/remote_runner.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index c405b79eec7..7389c13ee00 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -104,7 +104,7 @@ $(() => { const append = (text) => { // Indent each line. const lines = text.split('\n').map((line) => ' '.repeat(level * 2) + line); - $console.append(document.createTextNode(`${lines.join('\\n')}\\n`)); + $console.append(document.createTextNode(`${lines.join('\n')}\n`)); }; const total = runner.total; diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 67330b19b82..bbb41addb16 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -53,7 +53,7 @@ const sauceTestWorker = async.queue(async (testSettings) => { let logIndex = 0; while (true) { const consoleText = await browser.eval("$('#console').text()") || ''; - consoleText.substring(logIndex).split('\\n').forEach((line) => log(line, pfx)); + consoleText.substring(logIndex).split('\n').forEach((line) => log(line, pfx)); logIndex = consoleText.length; const [finished, nFailedStr] = consoleText.match(finishedRegex) || []; if (finished) { From 3409e3f5e6b80969190b002610e9d3e6bdbf972c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 21:36:32 -0400 Subject: [PATCH 112/218] remote_runner: Prevent Sauce errors from interrupting other tests --- src/tests/frontend/travis/remote_runner.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index bbb41addb16..047095a1989 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -30,9 +30,7 @@ const log = (msg, pfx = '') => { const finishedRegex = /FINISHED.*[0-9]+ tests passed, ([0-9]+) tests failed/; -const sauceTestWorker = async.queue(async (testSettings) => { - const name = `${testSettings.browserName} ${testSettings.version}, ${testSettings.platform}`; - const pfx = `[${name}] `; +const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { const fullName = [process.env.GIT_HASH].concat(process.env.SAUCE_NAME || [], name).join(' - '); testSettings.name = fullName; testSettings.public = true; @@ -100,4 +98,13 @@ Promise.all([ version: '78.0', }, ]), -].map(async (task) => await sauceTestWorker.push(task))); +].map(async (testSettings) => { + const name = `${testSettings.browserName} ${testSettings.version}, ${testSettings.platform}`; + const pfx = `[${name}] `; + try { + await sauceTestWorker.push({name, pfx, testSettings}); + } catch (err) { + log(`[red]FAILED[clear] ${err.stack || err}`, pfx); + process.exitCode = 1; + } +})); From a17556b8765fe4d48b22baa86389bec9e729f68f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 22:46:48 -0400 Subject: [PATCH 113/218] remote_runner: Avoid searching the full text for "FINISHED" --- src/tests/frontend/travis/remote_runner.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 047095a1989..aa1be106631 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -50,9 +50,9 @@ const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { // how many characters of the log have been sent to travis let logIndex = 0; while (true) { - const consoleText = await browser.eval("$('#console').text()") || ''; - consoleText.substring(logIndex).split('\n').forEach((line) => log(line, pfx)); - logIndex = consoleText.length; + const consoleText = (await browser.eval("$('#console').text()") || '').substring(logIndex); + consoleText.split('\n').forEach((line) => log(line, pfx)); + logIndex += consoleText.length; const [finished, nFailedStr] = consoleText.match(finishedRegex) || []; if (finished) { if (nFailedStr !== '0') process.exitCode = 1; From 25275f2744c287ecf2a8bd79ba220301e1576ce7 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 21:37:59 -0400 Subject: [PATCH 114/218] remote_runner: Treat no text as 0 lines, not 1 empty line --- src/tests/frontend/travis/remote_runner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index aa1be106631..d7d05d2ec32 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -51,7 +51,7 @@ const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { let logIndex = 0; while (true) { const consoleText = (await browser.eval("$('#console').text()") || '').substring(logIndex); - consoleText.split('\n').forEach((line) => log(line, pfx)); + (consoleText ? consoleText.split('\n') : []).forEach((line) => log(line, pfx)); logIndex += consoleText.length; const [finished, nFailedStr] = consoleText.match(finishedRegex) || []; if (finished) { From a7cd0a4b2597b7a24ad85b605fb1c7e6598e4f5f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 2 May 2021 21:44:02 -0400 Subject: [PATCH 115/218] remote_runner: Avoid re-sending the same console text over and over --- src/tests/frontend/travis/remote_runner.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index d7d05d2ec32..49f735903c9 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -50,7 +50,8 @@ const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { // how many characters of the log have been sent to travis let logIndex = 0; while (true) { - const consoleText = (await browser.eval("$('#console').text()") || '').substring(logIndex); + const remoteFn = ($, skipChars) => $('#console').text().substring(skipChars); + const consoleText = await browser.eval(`(${remoteFn})($, ${JSON.stringify(logIndex)})`); (consoleText ? consoleText.split('\n') : []).forEach((line) => log(line, pfx)); logIndex += consoleText.length; const [finished, nFailedStr] = consoleText.match(finishedRegex) || []; From 081f739a8ddf7d23c06765deaee9e7137f6d9c28 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 3 May 2021 00:18:47 -0400 Subject: [PATCH 116/218] remote_runner: Update browser list Use latest versions of Chrome, Firefox, Safari, and Edge. Keep the old Chrome version. --- src/tests/frontend/travis/remote_runner.js | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 49f735903c9..ee77e56654c 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -72,31 +72,32 @@ const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { Promise.all([ { - platform: 'OS X 10.15', + platform: 'macOS 11.00', browserName: 'safari', - version: '13.1', + version: 'latest', }, ...(isAdminRunner ? [] : [ { platform: 'Windows 10', browserName: 'firefox', - version: '84.0', + version: 'latest', }, { - platform: 'Windows 7', - browserName: 'chrome', - version: '55.0', - args: ['--use-fake-device-for-media-stream'], + platform: 'Windows 10', + browserName: 'MicrosoftEdge', + version: 'latest', }, { platform: 'Windows 10', - browserName: 'microsoftedge', - version: '83.0', + browserName: 'chrome', + version: 'latest', + args: ['--use-fake-device-for-media-stream'], }, { platform: 'Windows 7', - browserName: 'firefox', - version: '78.0', + browserName: 'chrome', + version: '55.0', + args: ['--use-fake-device-for-media-stream'], }, ]), ].map(async (testSettings) => { From 8baacd514e4c2f11e1858e4293ee4b0abf9b1b91 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 3 May 2021 00:43:23 -0400 Subject: [PATCH 117/218] remote_runner: Always call `browser.quit()` --- src/tests/frontend/travis/remote_runner.js | 51 ++++++++++++---------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index ee77e56654c..1e545372085 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -42,32 +42,35 @@ const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { const browser = wd.remote(config, 'promiseChain'); await browser.init(testSettings); const url = `https://saucelabs.com/jobs/${browser.sessionID}`; - await browser.get('http://localhost:9001/tests/frontend/'); - log(`Remote sauce test started! ${url}`, pfx); - // @TODO this should be configured in testSettings, see - // https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-Timeouts - const deadline = Date.now() + 14.5 * 60 * 1000; // Slightly less than overall test timeout. - // how many characters of the log have been sent to travis - let logIndex = 0; - while (true) { - const remoteFn = ($, skipChars) => $('#console').text().substring(skipChars); - const consoleText = await browser.eval(`(${remoteFn})($, ${JSON.stringify(logIndex)})`); - (consoleText ? consoleText.split('\n') : []).forEach((line) => log(line, pfx)); - logIndex += consoleText.length; - const [finished, nFailedStr] = consoleText.match(finishedRegex) || []; - if (finished) { - if (nFailedStr !== '0') process.exitCode = 1; - break; - } - if (Date.now() >= deadline) { - log('[red]FAILED[clear] allowed test duration exceeded'); - process.exitCode = 1; - break; + try { + await browser.get('http://localhost:9001/tests/frontend/'); + log(`Remote sauce test started! ${url}`, pfx); + // @TODO this should be configured in testSettings, see + // https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-Timeouts + const deadline = Date.now() + 14.5 * 60 * 1000; // Slightly less than overall test timeout. + // how many characters of the log have been sent to travis + let logIndex = 0; + while (true) { + const remoteFn = ($, skipChars) => $('#console').text().substring(skipChars); + const consoleText = await browser.eval(`(${remoteFn})($, ${JSON.stringify(logIndex)})`); + (consoleText ? consoleText.split('\n') : []).forEach((line) => log(line, pfx)); + logIndex += consoleText.length; + const [finished, nFailedStr] = consoleText.match(finishedRegex) || []; + if (finished) { + if (nFailedStr !== '0') process.exitCode = 1; + break; + } + if (Date.now() >= deadline) { + log('[red]FAILED[clear] allowed test duration exceeded'); + process.exitCode = 1; + break; + } + await new Promise((resolve) => setTimeout(resolve, 5000)); } - await new Promise((resolve) => setTimeout(resolve, 5000)); + } finally { + log(`Remote sauce test finished! ${url}`, pfx); + await browser.quit(); } - log(`Remote sauce test finished! ${url}`, pfx); - await browser.quit(); }, 6); // run 6 tests in parrallel Promise.all([ From 3c087af038b8d2d7dc7fbe4c3deff33fab89f1a5 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Tue, 4 May 2021 23:56:10 +0200 Subject: [PATCH 118/218] caretPosition: fix loading when iframe is hidden --- src/static/js/caretPosition.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/caretPosition.js b/src/static/js/caretPosition.js index 077fe58fc34..03af77f33fc 100644 --- a/src/static/js/caretPosition.js +++ b/src/static/js/caretPosition.js @@ -195,7 +195,7 @@ const getSelectionRange = () => { return; } const selection = window.getSelection(); - if (selection.rangeCount > 0) { + if (selection && selection.type !== 'None' && selection.rangeCount > 0) { return selection.getRangeAt(0); } else { return null; From b040ebf41932d8d74e089abdcad6ad1fdb1bee37 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 5 May 2021 18:09:10 -0400 Subject: [PATCH 119/218] Revert "PadMessageHandler: Use a `Map` for `sessioninfos`" Switching to a Map broke ep_webrtc and maybe other plugins. This reverts commit eeead4643792d2e7a10aef935112bab8e620375c. --- src/node/db/API.js | 9 +++-- src/node/handler/PadMessageHandler.js | 56 +++++++++++++++------------ 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/src/node/db/API.js b/src/node/db/API.js index 01edf5e704f..c262e078b9f 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -810,12 +810,15 @@ exports.createDiffHTML = async (padID, startRev, endRev) => { exports.getStats = async () => { const sessionInfos = padMessageHandler.sessioninfos; - const map = function* (it, fn) { for (const i of it) yield fn(i); }; - const activePads = new Set(map(sessionInfos.values(), ({padId}) => padId)); + + const sessionKeys = Object.keys(sessionInfos); + const activePads = new Set(Object.entries(sessionInfos).map((k) => k[1].padId)); + const {padIDs} = await padManager.listAllPads(); + return { totalPads: padIDs.length, - totalSessions: sessionInfos.size, + totalSessions: sessionKeys.length, totalActivePads: activePads.size, }; }; diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index a39cebe2ad7..899991a1122 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -67,13 +67,13 @@ exports.socketio = () => { * - readonly: Whether the client has read-only access (true) or read/write access (false). * - rev: The last revision that was sent to the client. */ -const sessioninfos = new Map(); +const sessioninfos = {}; exports.sessioninfos = sessioninfos; stats.gauge('totalUsers', () => Object.keys(socketio.sockets.sockets).length); stats.gauge('activePads', () => { const padIds = new Set(); - for (const {padId} of sessioninfos.values()) { + for (const {padId} of Object.values(sessioninfos)) { if (!padId) continue; padIds.add(padId); } @@ -106,7 +106,9 @@ exports.setSocketIO = (socket_io) => { */ exports.handleConnect = (socket) => { stats.meter('connects').mark(); - sessioninfos.set(socket.id, {}); + + // Initialize sessioninfos for this new session + sessioninfos[socket.id] = {}; }; /** @@ -128,7 +130,9 @@ exports.kickSessionsFromPad = (padID) => { */ exports.handleDisconnect = async (socket) => { stats.meter('disconnects').mark(); - const session = sessioninfos.get(socket.id); + + // save the padname of this session + const session = sessioninfos[socket.id]; // if this connection was already etablished with a handshake, // send a disconnect message to the others @@ -162,7 +166,9 @@ exports.handleDisconnect = async (socket) => { // Allow plugins to hook into users leaving the pad hooks.callAll('userLeave', session); } - sessioninfos.delete(socket.id); + + // Delete the sessioninfos entrys of this session + delete sessioninfos[socket.id]; }; /** @@ -193,7 +199,7 @@ exports.handleMessage = async (socket, message) => { return; } - const thisSession = sessioninfos.get(socket.id); + const thisSession = sessioninfos[socket.id]; if (!thisSession) { messageLogger.warn('Dropped message from an unknown connection.'); @@ -250,7 +256,7 @@ exports.handleMessage = async (socket, message) => { } // Drop the message if the client disconnected during the above processing. - if (sessioninfos.get(socket.id) !== thisSession) { + if (sessioninfos[socket.id] !== thisSession) { messageLogger.warn('Dropping message from a connection that has gone away.'); return; } @@ -295,7 +301,7 @@ exports.handleMessage = async (socket, message) => { * @param message the message from the client */ const handleSaveRevisionMessage = async (socket, message) => { - const {padId, author: authorId} = sessioninfos.get(socket.id); + const {padId, author: authorId} = sessioninfos[socket.id]; const pad = await padManager.getPad(padId); await pad.addSavedRevision(pad.head, authorId); }; @@ -345,7 +351,7 @@ exports.handleCustomMessage = (padID, msgString) => { const handleChatMessage = async (socket, message) => { const time = Date.now(); const text = message.data.text; - const {padId, author: authorId} = sessioninfos.get(socket.id); + const {padId, author: authorId} = sessioninfos[socket.id]; await exports.sendChatMessageToPadClients(time, authorId, text, padId); }; @@ -403,7 +409,7 @@ const handleGetChatMessages = async (socket, message) => { return; } - const {padId} = sessioninfos.get(socket.id); + const padId = sessioninfos[socket.id].padId; const pad = await padManager.getPad(padId); const chatMessages = await pad.getChatMessages(start, end); @@ -436,11 +442,11 @@ const handleSuggestUserName = (socket, message) => { return; } - const {padId} = sessioninfos.get(socket.id); + const padId = sessioninfos[socket.id].padId; // search the author and send him this message _getRoomSockets(padId).forEach((socket) => { - const session = sessioninfos.get(socket.id); + const session = sessioninfos[socket.id]; if (session && session.author === message.data.payload.unnamedId) { socket.json.send(message); } @@ -466,7 +472,7 @@ const handleUserInfoUpdate = async (socket, message) => { } // Check that we have a valid session and author to update. - const session = sessioninfos.get(socket.id); + const session = sessioninfos[socket.id]; if (!session || !session.author || !session.padId) { messageLogger.warn(`Dropped message, USERINFO_UPDATE Session not ready.${message.data}`); return; @@ -548,7 +554,7 @@ const handleUserChanges = async (socket, message) => { // The client might disconnect between our callbacks. We should still // finish processing the changeset, so keep a reference to the session. - const thisSession = sessioninfos.get(socket.id); + const thisSession = sessioninfos[socket.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 @@ -710,7 +716,7 @@ exports.updatePadClients = async (pad) => { const revCache = {}; await Promise.all(roomSockets.map(async (socket) => { - const sessioninfo = sessioninfos.get(socket.id); + const sessioninfo = sessioninfos[socket.id]; // The user might have disconnected since _getRoomSockets() was called. if (sessioninfo == null) return; @@ -805,7 +811,7 @@ const _correctMarkersInPad = (atext, apool) => { }; const handleSwitchToPad = async (socket, message, _authorID) => { - const currentSessionInfo = sessioninfos.get(socket.id); + const currentSessionInfo = sessioninfos[socket.id]; const padId = currentSessionInfo.padId; // Check permissions for the new pad. @@ -824,20 +830,20 @@ const handleSwitchToPad = async (socket, message, _authorID) => { assert(authorID === currentSessionInfo.author); // Check if the connection dropped during the access check. - if (sessioninfos.get(socket.id) !== currentSessionInfo) return; + if (sessioninfos[socket.id] !== currentSessionInfo) return; // clear the session and leave the room _getRoomSockets(padId).forEach((socket) => { - const sinfo = sessioninfos.get(socket.id); + const sinfo = sessioninfos[socket.id]; if (sinfo && sinfo.author === currentSessionInfo.author) { // fix user's counter, works on page refresh or if user closes browser window and then rejoins - sessioninfos.set(socket.id, {}); + sessioninfos[socket.id] = {}; socket.leave(padId); } }); // start up the new pad - const newSessionInfo = sessioninfos.get(socket.id); + const newSessionInfo = sessioninfos[socket.id]; createSessionInfoAuth(newSessionInfo, message); await handleClientReady(socket, message, authorID); }; @@ -921,17 +927,17 @@ const handleClientReady = async (socket, message, authorID) => { // 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. - const sessionInfo = sessioninfos.get(socket.id); + const sessionInfo = sessioninfos[socket.id]; if (sessionInfo == null) return; // Check if this author is already on the pad, if yes, kick the other sessions! const roomSockets = _getRoomSockets(pad.id); for (const socket of roomSockets) { - const sinfo = sessioninfos.get(socket.id); + const sinfo = sessioninfos[socket.id]; if (sinfo && sinfo.author === authorID) { // fix user's counter, works on page refresh or if user closes browser window and then rejoins - sessioninfos.set(socket.id, {}); + sessioninfos[socket.id] = {}; socket.leave(padIds.padId); socket.json.send({disconnect: 'userdup'}); } @@ -1145,7 +1151,7 @@ const handleClientReady = async (socket, message, authorID) => { // Since sessioninfos might change while being enumerated, check if the // sessionID is still assigned to a valid session - const sessionInfo = sessioninfos.get(roomSocket.id); + const sessionInfo = sessioninfos[roomSocket.id]; if (sessionInfo == null) return; // get the authorname & colorId @@ -1427,7 +1433,7 @@ exports.padUsers = async (padID) => { // iterate over all clients (in parallel) await Promise.all(_getRoomSockets(padID).map(async (roomSocket) => { - const s = sessioninfos.get(roomSocket.id); + const s = sessioninfos[roomSocket.id]; if (s) { const author = await authorManager.getAuthor(s.author); // Fixes: https://github.com/ether/etherpad-lite/issues/4120 From 4701cc43fa93a88c1ee269cd41c96ff64797cc6e Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Fri, 7 May 2021 10:50:38 +0200 Subject: [PATCH 120/218] Localisation updates from https://translatewiki.net. --- src/locales/ia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/ia.json b/src/locales/ia.json index 3229a910e5e..d78ce31627f 100644 --- a/src/locales/ia.json +++ b/src/locales/ia.json @@ -9,7 +9,7 @@ "pad.toolbar.bold.title": "Grasse (Ctrl-B)", "pad.toolbar.italic.title": "Italic (Ctrl-I)", "pad.toolbar.underline.title": "Sublinear (Ctrl-U)", - "pad.toolbar.strikethrough.title": "Cancellar (Ctrl+5)", + "pad.toolbar.strikethrough.title": "Barrate (Ctrl+5)", "pad.toolbar.ol.title": "Lista ordinate (Ctrl+Shift+N)", "pad.toolbar.ul.title": "Lista non ordinate (Ctrl+Shift+L)", "pad.toolbar.indent.title": "Indentar (TAB)", From ff245dbbebc5c3c794d40c0f2c8f58db48362046 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Thu, 6 May 2021 21:53:23 +0000 Subject: [PATCH 121/218] fix: upgrade underscore from 1.13.0 to 1.13.1 Snyk has created this PR to upgrade underscore from 1.13.0 to 1.13.1. See this package in npm: https://www.npmjs.com/package/underscore See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=upgrade-pr --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index c08cfc8338d..b9944c8cbf8 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -8488,9 +8488,9 @@ } }, "underscore": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.0.tgz", - "integrity": "sha512-sCs4H3pCytsb5K7i072FAEC9YlSYFIbosvM0tAKAlpSSUgD7yC1iXSEGdl5XrDKQ1YUB+p/HDzYrSG2H2Vl36g==" + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", + "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==" }, "unified": { "version": "9.2.0", diff --git a/src/package.json b/src/package.json index 33377460271..335d5540ee5 100644 --- a/src/package.json +++ b/src/package.json @@ -70,7 +70,7 @@ "tiny-worker": "^2.3.0", "tinycon": "0.6.8", "ueberdb2": "^1.4.7", - "underscore": "1.13.0", + "underscore": "1.13.1", "unorm": "1.6.0", "wtfnode": "^0.8.4" }, From 10f00906f846da91aff5c33364cab76d2266d577 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Fri, 7 May 2021 14:34:36 +0200 Subject: [PATCH 122/218] update package-lock.json --- src/package-lock.json | 718 +++++++++++++++++++++--------------------- 1 file changed, 357 insertions(+), 361 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index b9944c8cbf8..e6aeedf2f9e 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -51,9 +51,9 @@ "integrity": "sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw==" }, "@azure/ms-rest-js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.4.0.tgz", - "integrity": "sha512-kvksFowDDZ/Tqu0hkCp+oDRhh87QOlRPgx8tOIMbdbfm/Muvfdp5f0cwrgzguqT3nhmkcLzt1cvQbLTM0HmS/A==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.4.1.tgz", + "integrity": "sha512-76Tc+RRqXj1bqwg/1TX2KHaAGyFLzypqBQ65+FaVxw+oSeZhZcePRFM4r0lqRVwweC9RTct95aNPnMZSzGxDwA==", "requires": { "@azure/core-auth": "^1.1.4", "abort-controller": "^3.0.0", @@ -131,57 +131,26 @@ } }, "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", "dev": true }, "@babel/highlight": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz", - "integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.12.11", + "@babel/helper-validator-identifier": "^7.14.0", "chalk": "^2.0.0", "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } } }, "@eslint/eslintrc": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz", - "integrity": "sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.0.tgz", + "integrity": "sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -191,7 +160,6 @@ "ignore": "^4.0.6", "import-fresh": "^3.2.1", "js-yaml": "^3.13.1", - "lodash": "^4.17.20", "minimatch": "^3.0.4", "strip-json-comments": "^3.1.1" }, @@ -205,6 +173,15 @@ "ms": "2.1.2" } }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -258,9 +235,9 @@ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" }, "@sinonjs/commons": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.2.tgz", - "integrity": "sha512-sruwd86RJHdsVf/AtBoijDmUqJp3B6hF/DGC23C+JaegnDHaZyewCjoVGTdg3J0uz3Zs7NnIT05OBOmML72lQw==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", "dev": true, "requires": { "type-detect": "4.0.8" @@ -308,9 +285,9 @@ "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/node": { - "version": "14.14.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.41.tgz", - "integrity": "sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g==" + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz", + "integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==" }, "@types/request": { "version": "2.48.5", @@ -452,14 +429,17 @@ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } }, "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "dev": true, "requires": { "normalize-path": "^3.0.0", @@ -683,9 +663,9 @@ "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==" }, "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "base64-arraybuffer": { "version": "0.1.4", @@ -959,15 +939,13 @@ "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==" }, "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, "channels": { @@ -1303,16 +1281,6 @@ "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" - }, - "dependencies": { - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - } } }, "css-select": { @@ -1500,6 +1468,30 @@ "agentkeepalive": "^3.4.1", "chalk": "^1.0.0", "lodash": "^4.17.10" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } } }, "emoji-regex": { @@ -1556,9 +1548,9 @@ } }, "engine.io-client": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.0.tgz", - "integrity": "sha512-12wPRfMrugVw/DNyJk34GQ5vIVArEcVMXWugQGGuw2XxUSztFNmJggZmv8IZlLyEdnpO1QB9LkcjeWewO2vxtA==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.2.tgz", + "integrity": "sha512-QEqIp+gJ/kMHeUun7f5Vv3bteRHppHH/FMBQX/esFj/fuYfjyUKWGMo3VCvIP/V8bE9KcjHmRZrhIz2Z9oNsDA==", "requires": { "component-emitter": "~1.3.0", "component-inherit": "0.0.3", @@ -1569,7 +1561,7 @@ "parseqs": "0.0.6", "parseuri": "0.0.6", "ws": "~7.4.2", - "xmlhttprequest-ssl": "~1.5.4", + "xmlhttprequest-ssl": "~1.6.2", "yeast": "0.1.2" }, "dependencies": { @@ -1615,25 +1607,27 @@ "integrity": "sha1-eYCZstvTfKK8dJ5TinwTB9C1BJk=" }, "es-abstract": { - "version": "1.18.0-next.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz", - "integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz", + "integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==", "dev": true, "requires": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2", + "get-intrinsic": "^1.1.1", "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.1", + "is-regex": "^1.1.2", + "is-string": "^1.0.5", "object-inspect": "^1.9.0", "object-keys": "^1.1.1", "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.3", - "string.prototype.trimstart": "^1.0.3" + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.0" }, "dependencies": { "object.assign": { @@ -1672,13 +1666,13 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.20.0.tgz", - "integrity": "sha512-qGi0CTcOGP2OtCQBgWZlQjcTuP0XkIpYFj25XtRTQSHC+umNnp7UMshr2G8SLsRFYDdAPFeHOsiteadmMH02Yw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.25.0.tgz", + "integrity": "sha512-TVpSovpvCNpLURIScDRB6g5CYu/ZFq9GfX2hLNIV4dSBKxIWojeDODvYl3t0k0VtMxYeR8OXPCFE5+oHMlGfhw==", "dev": true, "requires": { "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.3.0", + "@eslint/eslintrc": "^0.4.0", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -1691,10 +1685,10 @@ "espree": "^7.3.1", "esquery": "^1.4.0", "esutils": "^2.0.2", - "file-entry-cache": "^6.0.0", + "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^5.0.0", - "globals": "^12.1.0", + "globals": "^13.6.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", @@ -1702,7 +1696,7 @@ "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", - "lodash": "^4.17.20", + "lodash": "^4.17.21", "minimatch": "^3.0.4", "natural-compare": "^1.4.0", "optionator": "^0.9.1", @@ -1732,9 +1726,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -1778,9 +1772,9 @@ "dev": true }, "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -1819,9 +1813,9 @@ "dev": true }, "eslint-plugin-cypress": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.11.2.tgz", - "integrity": "sha512-1SergF1sGbVhsf7MYfOLiBhdOg6wqyeV9pXUAIDIffYTGMN3dTBQS9nFAzhLsHhO+Bn0GaVM1Ecm71XUidQ7VA==", + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.11.3.tgz", + "integrity": "sha512-hOoAid+XNFtpvOzZSNWP5LDrQBEJwbZwjib4XJ1KcRYKjeVj0mAmPmucG4Egli4j/aruv+Ow/acacoloWWCl9Q==", "dev": true, "requires": { "globals": "^11.12.0" @@ -1864,9 +1858,9 @@ } }, "eslint-plugin-mocha": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-8.0.0.tgz", - "integrity": "sha512-n67etbWDz6NQM+HnTwZHyBwz/bLlYPOxUbw7bPuCyFujv7ZpaT/Vn6KTAbT02gf7nRljtYIjWcTxK/n8a57rQQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-8.1.0.tgz", + "integrity": "sha512-1EgHvXKRl7W3mq3sntZAi5T24agRMyiTPL4bSXe+B4GksYOjAPEWYx+J3eJg4It1l2NMNZJtk0gQyQ6mfiPhQg==", "dev": true, "requires": { "eslint-utils": "^2.1.0", @@ -1893,16 +1887,6 @@ "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true }, - "resolve": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", - "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", - "dev": true, - "requires": { - "is-core-module": "^2.1.0", - "path-parse": "^1.0.6" - } - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -1960,9 +1944,9 @@ } }, "eslint-visitor-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", - "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true }, "esm": { @@ -2263,9 +2247,9 @@ "dev": true }, "follow-redirects": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", - "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==" + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.0.tgz", + "integrity": "sha512-0vRwd7RKQBTt+mgu87mtYeofLFZpTas2S9zY+jIeuLJMNvudIgF52nr19q40HOwH5RrhWIPuj9puybzSJiRrVg==" }, "forever-agent": { "version": "0.6.1", @@ -2370,9 +2354,9 @@ "dev": true }, "get-intrinsic": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz", - "integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -2388,9 +2372,9 @@ } }, "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2401,27 +2385,35 @@ } }, "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "requires": { "is-glob": "^4.0.1" } }, "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.8.0.tgz", + "integrity": "sha512-rHtdA6+PDBIjeEvA91rpqzEvk/k3/i7EeNQiryiWuJH0Hw9cpyJMAt2jtbAwUaRdhD+573X4vWw6IcjKPasi9Q==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "type-fest": "^0.20.2" + }, + "dependencies": { + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": 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==" + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" }, "growl": { "version": "1.10.5", @@ -2459,6 +2451,12 @@ "ansi-regex": "^2.0.0" } }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true + }, "has-binary2": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", @@ -2485,9 +2483,9 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, "has-unicode": { "version": "2.0.1", @@ -2639,9 +2637,9 @@ "dev": true }, "ignore-walk": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz", + "integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==", "optional": true, "requires": { "minimatch": "^3.0.4" @@ -2655,14 +2653,6 @@ "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - } } }, "imurmurhash": { @@ -2720,6 +2710,12 @@ "is-decimal": "^1.0.0" } }, + "is-bigint": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", + "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", + "dev": true + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2729,29 +2725,38 @@ "binary-extensions": "^2.0.0" } }, + "is-boolean-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz", + "integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==", + "dev": true, + "requires": { + "call-bind": "^1.0.0" + } + }, "is-buffer": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" }, "is-callable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", - "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", "dev": true }, "is-core-module": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", - "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.3.0.tgz", + "integrity": "sha512-xSphU2KG9867tsYdLD4RWQ1VqdFl4HTO9Thf3I/3dLEfr0dbPTWKsuCKrgqMljg4nPE+Gq0VCnzT3gr0CyBmsw==", "requires": { "has": "^1.0.3" } }, "is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.3.tgz", + "integrity": "sha512-tDpEUInNcy2Yw3lNSepK3Wdw1RnXLcIVienz6Ou631Acl15cJyRWK4dgA1vCmOEgIbtOV0W7MHg+AR2Gdg1NXQ==", "dev": true }, "is-decimal": { @@ -2799,13 +2804,16 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-number-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", + "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==", + "dev": true + }, "is-observable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", - "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", - "requires": { - "symbol-observable": "^1.1.0" - } + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-2.1.0.tgz", + "integrity": "sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw==" }, "is-plain-obj": { "version": "2.1.0", @@ -2818,14 +2826,21 @@ "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=" }, "is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", + "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", "dev": true, "requires": { + "call-bind": "^1.0.2", "has-symbols": "^1.0.1" } }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, "is-symbol": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", @@ -2866,36 +2881,10 @@ "minimatch": "^3.0.4" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, "async": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -2979,9 +2968,9 @@ } }, "just-extend": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz", - "integrity": "sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, "jwa": { @@ -3004,9 +2993,9 @@ } }, "kebab-case": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.0.tgz", - "integrity": "sha1-P55JkK3K0MaGwOcB92RYaPdfkes=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.1.tgz", + "integrity": "sha512-txPHx6nVLhv8PHGXIlAk0nYoh894SpAqGPXNvbg2hh8spvHXIah3+vT87DLoa59nKgC6scD3u3xAuRIgiMqbfQ==", "dev": true }, "languages4translatewiki": { @@ -3082,9 +3071,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.assignin": { "version": "4.2.0", @@ -3169,6 +3158,12 @@ "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true + }, "lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", @@ -3182,37 +3177,6 @@ "dev": true, "requires": { "chalk": "^2.4.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } } }, "log4js": { @@ -3305,16 +3269,16 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, "mime-db": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", - "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==" + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", + "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==" }, "mime-types": { - "version": "2.1.28", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", - "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", + "version": "2.1.30", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", + "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", "requires": { - "mime-db": "1.45.0" + "mime-db": "1.47.0" } }, "minimatch": { @@ -3442,6 +3406,15 @@ "requires": { "has-flag": "^3.0.0" } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } } } }, @@ -3486,15 +3459,15 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "mssql": { - "version": "7.0.0-beta.5", - "resolved": "https://registry.npmjs.org/mssql/-/mssql-7.0.0-beta.5.tgz", - "integrity": "sha512-LfvKm/vcaQNA4w/ARbFCFraFmri0arwOgcSRkyajIVIrfW7Yd2zUZ2w+c8Ee7k3k5BeD6c0D3G6gdfW9aW0fUg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-7.0.0.tgz", + "integrity": "sha512-/6IXQUucUw4wpWXq5eO56P17NQkKbY6sak8NAUrofY5whWkGg0VCxpPQjq8qiuV2hGPUt6+Yo/5La0o4SpatZA==", "requires": { "@tediousjs/connection-string": "^0.3.0", "debug": "^4", "rfdc": "^1.3.0", "tarn": "^3.0.1", - "tedious": "^11.0.5" + "tedious": "^11.0.7" }, "dependencies": { "debug": { @@ -3629,9 +3602,9 @@ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, "nise": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz", - "integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", + "integrity": "sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0", @@ -3671,14 +3644,6 @@ "requires": { "object.getownpropertydescriptors": "^2.0.3", "semver": "^5.7.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } } }, "node-fetch": { @@ -3711,6 +3676,15 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", "optional": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "optional": true, + "requires": { + "isexe": "^2.0.0" + } } } }, @@ -6878,9 +6852,9 @@ } }, "npm-bundled": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", + "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", "optional": true, "requires": { "npm-normalize-package-bin": "^1.0.1" @@ -6940,9 +6914,9 @@ "optional": true }, "object-inspect": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", - "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.2.tgz", + "integrity": "sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA==" }, "object-keys": { "version": "1.1.1", @@ -6963,14 +6937,14 @@ } }, "object.getownpropertydescriptors": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz", - "integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz", + "integrity": "sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ==", "dev": true, "requires": { - "call-bind": "^1.0.0", + "call-bind": "^1.0.2", "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" + "es-abstract": "^1.18.0-next.2" } }, "observable-fns": { @@ -7000,9 +6974,9 @@ } }, "openapi-backend": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-3.9.1.tgz", - "integrity": "sha512-PXd7C1ln913Hpw1/JCpoZm3kv/0K138gvI9Abidib34yzW3vHBGYj+uxVEYSFEcy+5GVCvMJoMCMwO61Tb2t2Q==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-3.9.2.tgz", + "integrity": "sha512-+IqhtObMGeRf4aDB6L5Lc3nZYPHB9JRkTiOaNHKx26SDWcaMAof6RnABbgLDNVRRiz+fbJPmizWcFSkCPX8qeQ==", "requires": { "@apidevtools/json-schema-ref-parser": "^9.0.7", "ajv": "^6.10.0", @@ -7242,9 +7216,9 @@ } }, "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", + "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", "dev": true }, "postgres-array": { @@ -7350,9 +7324,9 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "rate-limiter-flexible": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.2.1.tgz", - "integrity": "sha512-rxCP6kDDdn0cZmVqVlF06yLU+mG3TuwaHV/fUIw3OQyYhza7pzVBtdMhUmfXbBzMS+O464XP+x33pfTDGRGYVA==" + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.2.2.tgz", + "integrity": "sha512-8qpJC/Zc/0dM9BW21/JyROt6eUeLZ8l06vrSWZFwgNV9IpthIJe6Pcuowpzxe0PJ3vYDaECiqvF/1J/+Nh5wgA==" }, "raw-body": { "version": "2.4.0", @@ -7416,9 +7390,9 @@ } }, "redis": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.1.tgz", - "integrity": "sha512-QhkKhOuzhogR1NDJfBD34TQJz2ZJwDhhIC6ZmvpftlmfYShHHQXjjNspAJ+Z2HH5NwSBVYBVganbiZ8bgFMHjg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz", + "integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==", "requires": { "denque": "^1.5.0", "redis-commands": "^1.7.0", @@ -7472,9 +7446,9 @@ }, "dependencies": { "unist-util-is": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.0.4.tgz", - "integrity": "sha512-3dF39j/u423v4BBQrk1AQ2Ve1FxY5W3JKwXxVFzBODQ6WEvccguhgp802qQLKSnxPODE6WuRZtV+ohlUg4meBA==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==" } } }, @@ -7558,6 +7532,12 @@ "path-parse": "^1.0.6" } }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, "rethinkdb": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/rethinkdb/-/rethinkdb-2.4.2.tgz", @@ -7670,9 +7650,9 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "set-cookie-parser": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.7.tgz", - "integrity": "sha512-VaSdYN1DlYuKOzBKqhYJnwaPeirZdNNUNmYdnp9/6Umr9s8amidctYitrX2Gk8wCqiBuiG5mpOYCiVhG5o4iMQ==", + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz", + "integrity": "sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg==", "dev": true }, "setprototypeof": { @@ -8014,22 +7994,22 @@ } }, "string.prototype.trimend": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", - "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", "dev": true, "requires": { - "call-bind": "^1.0.0", + "call-bind": "^1.0.2", "define-properties": "^1.1.3" } }, "string.prototype.trimstart": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", - "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", "dev": true, "requires": { - "call-bind": "^1.0.0", + "call-bind": "^1.0.2", "define-properties": "^1.1.3" } }, @@ -8148,36 +8128,36 @@ } }, "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } }, "swagger-schema-official": { "version": "2.0.0-bab6bed", "resolved": "https://registry.npmjs.org/swagger-schema-official/-/swagger-schema-official-2.0.0-bab6bed.tgz", "integrity": "sha1-cAcEaNbSl3ylI3suUZyn0Gouo/0=" }, - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" - }, "table": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/table/-/table-6.0.7.tgz", - "integrity": "sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.0.tgz", + "integrity": "sha512-SAM+5p6V99gYiiy2gT5ArdzgM1dLDed0nkrWmG6Fry/bUS/m9x83BwpJUOf1Qj/x2qJd+thL6IkIx7qPGRxqBw==", "dev": true, "requires": { - "ajv": "^7.0.2", - "lodash": "^4.17.20", + "ajv": "^8.0.1", + "lodash.clonedeep": "^4.5.0", + "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", - "string-width": "^4.2.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0" }, "dependencies": { "ajv": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.1.1.tgz", - "integrity": "sha512-ga/aqDYnUy/o7vbsRTFhhTsNeXiYb5JWDIcRIeZfwRNCefwjNTVYCGdGSUrEmiu3yDK3vFvNbgJxvrQW4JXrYQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.2.0.tgz", + "integrity": "sha512-WSNGFuyWd//XO8n/m/EaOlNLtO0yL8EXT/74LqT4khdhpZjP7lkj/kT5uwRmGitKEVp/Oj7ZUHeGfPtgHhQ5CA==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -8205,9 +8185,9 @@ "dev": true }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { "emoji-regex": "^8.0.0", @@ -8251,9 +8231,9 @@ }, "dependencies": { "bl": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", - "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, "requires": { "buffer": "^5.5.0", @@ -8269,9 +8249,9 @@ "integrity": "sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw==" }, "tedious": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/tedious/-/tedious-11.0.7.tgz", - "integrity": "sha512-/nFPRzDoz0VS/+JXx40ovHbs8SvRQ5cgRCJN4uDxkeWfKc9iuNuh84mO3W47i2cHMJ4biiGYw5UfY9a5hHn/Sg==", + "version": "11.0.8", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-11.0.8.tgz", + "integrity": "sha512-Qrl0Vo6nazO7KhFgjG0jqUkX6lVauj+0QS5dBEeVx5VkHFa3g4fDTd0vwSTuy7o1aeleiezMPERU8e4svWxPSQ==", "requires": { "@azure/ms-rest-nodeauth": "^3.0.6", "@js-joda/core": "^3.2.0", @@ -8334,13 +8314,13 @@ "dev": true }, "threads": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/threads/-/threads-1.6.3.tgz", - "integrity": "sha512-tKwFIWRgfAT85KGkrpDt2jWPO8IVH0sLNfB/pXad/VW9eUIY2Zlz+QyeizypXhPHv9IHfqRzvk2t3mPw+imhWw==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/threads/-/threads-1.6.4.tgz", + "integrity": "sha512-A+9MQFAUha9W8MjIPmrvETy98qVmZFr5Unox9D95y7kvz3fBpGiFS7JOZs07B2KvTHoRNI5MrGudRVeCmv4Alw==", "requires": { "callsites": "^3.1.0", - "debug": "^4.1.1", - "is-observable": "^1.1.0", + "debug": "^4.2.0", + "is-observable": "^2.1.0", "observable-fns": "^0.5.1", "tiny-worker": ">= 2" }, @@ -8460,9 +8440,9 @@ } }, "ueberdb2": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.7.tgz", - "integrity": "sha512-lpD7QgxzgQ4UQ8Sj8/oYG487I/+Kj0s9O1GY+eyI/2xYQg/IXq5iw21AMzCKCnPI6WM1jF52dK0QaLxddkAVQg==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.8.tgz", + "integrity": "sha512-11l+4kwC6XhMaj8sbLroVBM3utCACTEQWlhCEzmyy5Bm3l5uKqtlyUNOMnd/awZUEiazcKQH9PDYdFuhkqxDhw==", "requires": { "async": "^3.2.0", "cassandra-driver": "^4.5.1", @@ -8473,7 +8453,7 @@ "mysql": "2.18.1", "nano": "^8.2.2", "pg": "^8.0.3", - "redis": "^3.0.2", + "redis": "^3.1.2", "rethinkdb": "^2.4.2", "simple-git": "^2.4.0", "sqlite3": "^5.0.1" @@ -8487,15 +8467,27 @@ "random-bytes": "~1.0.0" } }, + "unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + } + }, "underscore": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==" }, "unified": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.0.tgz", - "integrity": "sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==", + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.1.tgz", + "integrity": "sha512-juWjuI8Z4xFg8pJbnEZ41b5xjGUWGHqXALmBZ3FC3WX0PIx1CZBIIJ6mXbYMcf6Yw4Fi0rFUTA1cdz/BglbOhA==", "requires": { "bail": "^1.0.0", "extend": "^3.0.0", @@ -8552,9 +8544,9 @@ "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, "v8-compile-cache": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", - "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, "vargs": { @@ -8680,13 +8672,26 @@ "integrity": "sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==" }, "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "requires": { "isexe": "^2.0.0" } }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", @@ -8724,15 +8729,6 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -8773,9 +8769,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz", - "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==" + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==" }, "wtfnode": { "version": "0.8.4", @@ -8802,9 +8798,9 @@ "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==" }, "xmlhttprequest-ssl": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", - "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.2.tgz", + "integrity": "sha512-tYOaldF/0BLfKuoA39QMwD4j2m8lq4DIncqj1yuNELX4vz9+z/ieG/vwmctjJce+boFHXstqhWnHSxc4W8f4qg==" }, "xpath.js": { "version": "1.1.0", @@ -8817,9 +8813,9 @@ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, "yallist": { From 24929d3417b4489a3abb1fd729e9276cbe930028 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Fri, 7 May 2021 14:44:17 +0200 Subject: [PATCH 123/218] package.json: bump npm from 6.14.11 to 6.14.13 --- src/package-lock.json | 16 ++++++++-------- src/package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index e6aeedf2f9e..e9220ecf76e 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -3758,9 +3758,9 @@ "dev": true }, "npm": { - "version": "6.14.11", - "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.11.tgz", - "integrity": "sha512-1Zh7LjuIoEhIyjkBflSSGzfjuPQwDlghNloppjruOH5bmj9midT9qcNT0tRUZRR04shU9ekrxNy9+UTBrqeBpQ==", + "version": "6.14.13", + "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.13.tgz", + "integrity": "sha512-SRl4jJi0EBHY2xKuu98FLRMo3VhYQSA6otyLnjSEiHoSG/9shXCFNJy9tivpUJvtkN9s6VDdItHa5Rn+fNBzag==", "requires": { "JSONStream": "^1.3.5", "abbrev": "~1.1.1", @@ -3793,7 +3793,7 @@ "glob": "^7.1.6", "graceful-fs": "^4.2.4", "has-unicode": "~2.0.1", - "hosted-git-info": "^2.8.8", + "hosted-git-info": "^2.8.9", "iferr": "^1.0.2", "imurmurhash": "*", "infer-owner": "^1.0.4", @@ -3869,7 +3869,7 @@ "slide": "~1.1.6", "sorted-object": "~2.0.1", "sorted-union-stream": "~2.1.3", - "ssri": "^6.0.1", + "ssri": "^6.0.2", "stringify-package": "^1.0.1", "tar": "^4.4.13", "text-table": "~0.2.0", @@ -4917,7 +4917,7 @@ "bundled": true }, "hosted-git-info": { - "version": "2.8.8", + "version": "2.8.9", "bundled": true }, "http-cache-semantics": { @@ -6313,7 +6313,7 @@ } }, "ssri": { - "version": "6.0.1", + "version": "6.0.2", "bundled": true, "requires": { "figgy-pudding": "^3.5.1" @@ -6752,7 +6752,7 @@ "bundled": true }, "y18n": { - "version": "4.0.0", + "version": "4.0.1", "bundled": true }, "yallist": { diff --git a/src/package.json b/src/package.json index 335d5540ee5..228368c1a89 100644 --- a/src/package.json +++ b/src/package.json @@ -54,7 +54,7 @@ "measured-core": "1.51.1", "mime-types": "^2.1.27", "nodeify": "1.0.1", - "npm": "6.14.11", + "npm": "6.14.13", "openapi-backend": "^3.9.1", "proxy-addr": "^2.0.6", "rate-limiter-flexible": "^2.1.4", From 30eadad79d7cbbee57a0e372acbe64642c74779b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 11 May 2021 15:42:21 -0400 Subject: [PATCH 124/218] lint: Bump ESLint dependencies --- src/package-lock.json | 32 ++++++++++++++++---------------- src/package.json | 10 +++++----- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index e9220ecf76e..2e5bce2ce9d 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -148,9 +148,9 @@ } }, "@eslint/eslintrc": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.0.tgz", - "integrity": "sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.1.tgz", + "integrity": "sha512-5v7TDE9plVhvxQeWLXDTvFvJBdH6pEsdnl2g/dAptmuFEPedQ4Erq5rsDsX+mvAM610IhNaO2W5V1dOOnDKxkQ==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -1666,13 +1666,13 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.25.0.tgz", - "integrity": "sha512-TVpSovpvCNpLURIScDRB6g5CYu/ZFq9GfX2hLNIV4dSBKxIWojeDODvYl3t0k0VtMxYeR8OXPCFE5+oHMlGfhw==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.26.0.tgz", + "integrity": "sha512-4R1ieRf52/izcZE7AlLy56uIHHDLT74Yzz2Iv2l6kDaYvEu9x+wMB5dZArVL8SYGXSYV2YAg70FcW5Y5nGGNIg==", "dev": true, "requires": { "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.0", + "@eslint/eslintrc": "^0.4.1", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -1807,9 +1807,9 @@ } }, "eslint-config-etherpad": { - "version": "1.0.26", - "resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-1.0.26.tgz", - "integrity": "sha512-xPnDnJIpQuYJNRYGIHIucct0U6CtciyZKItpet+NqoGJgxUMkwAXgD5bzuXQvd9u4I2aj/kRU1BIL2DbAGe+pA==", + "version": "1.0.27", + "resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-1.0.27.tgz", + "integrity": "sha512-WLo7WOazNiPirTpJkJcUK9LTv0/EdwrzJuP7NljaAsA1h9f5/VhMHKnrcmXyOv2255mjpG52ouidvZLcD356Hw==", "dev": true }, "eslint-plugin-cypress": { @@ -1902,9 +1902,9 @@ "dev": true }, "eslint-plugin-promise": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.3.1.tgz", - "integrity": "sha512-bY2sGqyptzFBDLh/GMbAxfdJC+b0f23ME63FOE4+Jao0oZ3E1LEwFtWJX/1pGMJLiTtrSSern2CRM/g+dfc0eQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.1.0.tgz", + "integrity": "sha512-NGmI6BH5L12pl7ScQHbg7tvtk4wPxxj8yPHH47NvSmMtFneC077PSeY3huFj06ZWZvtbfxSPt3RuOQD5XcR4ng==", "dev": true }, "eslint-plugin-you-dont-need-lodash-underscore": { @@ -8155,9 +8155,9 @@ }, "dependencies": { "ajv": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.2.0.tgz", - "integrity": "sha512-WSNGFuyWd//XO8n/m/EaOlNLtO0yL8EXT/74LqT4khdhpZjP7lkj/kT5uwRmGitKEVp/Oj7ZUHeGfPtgHhQ5CA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.3.0.tgz", + "integrity": "sha512-RYE7B5An83d7eWnDR8kbdaIFqmKCNsP16ay1hDbJEU+sa0e3H9SebskCt0Uufem6cfAVu7Col6ubcn/W+Sm8/Q==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", diff --git a/src/package.json b/src/package.json index 228368c1a89..7d70e776b38 100644 --- a/src/package.json +++ b/src/package.json @@ -78,14 +78,14 @@ "etherpad-lite": "node/server.js" }, "devDependencies": { - "eslint": "^7.20.0", - "eslint-config-etherpad": "^1.0.26", - "eslint-plugin-cypress": "^2.11.2", + "eslint": "^7.26.0", + "eslint-config-etherpad": "^1.0.27", + "eslint-plugin-cypress": "^2.11.3", "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-mocha": "^8.0.0", + "eslint-plugin-mocha": "^8.1.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prefer-arrow": "^1.2.3", - "eslint-plugin-promise": "^4.3.1", + "eslint-plugin-promise": "^5.1.0", "eslint-plugin-you-dont-need-lodash-underscore": "^6.11.0", "etherpad-cli-client": "0.0.9", "mocha": "7.1.2", From 59c03bde20a7624d996a943f88b4025d6c92eb94 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 11 May 2021 15:44:50 -0400 Subject: [PATCH 125/218] lint: Re-run `eslint --fix` --- src/bin/plugins/stalePlugins.js | 6 +++--- src/node/db/AuthorManager.js | 2 +- src/node/hooks/express/webaccess.js | 2 +- src/node/utils/Settings.js | 2 +- src/tests/backend/specs/api/api.js | 1 - src/tests/frontend/specs/chat.js | 1 - 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/bin/plugins/stalePlugins.js b/src/bin/plugins/stalePlugins.js index 8b2427f2729..7e1817c66cb 100644 --- a/src/bin/plugins/stalePlugins.js +++ b/src/bin/plugins/stalePlugins.js @@ -5,7 +5,7 @@ const superagent = require('superagent'); const currentTime = new Date(); -(async() => { +(async () => { const res = await superagent.get('https://static.etherpad.org/plugins.full.json'); const plugins = JSON.parse(res.text); for (const plugin of Object.keys(plugins)) { @@ -13,8 +13,8 @@ const currentTime = new Date(); const date = new Date(plugins[plugin].time); const diffTime = Math.abs(currentTime - date); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - if (diffDays > (365*2)) { - console.log(`${name}, ${plugins[plugin].data.maintainers[0].email}`) + if (diffDays > (365 * 2)) { + console.log(`${name}, ${plugins[plugin].data.maintainers[0].email}`); } } })(); diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index de868b97d52..e457b7eb288 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -156,7 +156,7 @@ async function mapAuthorWithDBKey(mapperkey, mapper) { // return the author return {authorID: author}; -}; +} /** * Internal function that creates the database entry for an author diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 99210d9c57a..8a183681c1b 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -23,7 +23,7 @@ const staticPathsRE = new RegExp(`^/(?:${[ 'robots.txt', 'static/.*', 'stats/?', - 'tests/frontend(?:/.*)?' + 'tests/frontend(?:/.*)?', ].join('|')})$`); exports.normalizeAuthzLevel = (level) => { diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index b31cf16b466..38ac5bceac9 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -602,7 +602,7 @@ const lookupEnvironmentVariables = (obj) => { const defaultValue = match[3]; if ((envVarValue === undefined) && (defaultValue === undefined)) { - console.warn(`Environment variable "${envVarName}" does not contain any value for `+ + console.warn(`Environment variable "${envVarName}" does not contain any value for ` + `configuration key "${key}", and no default was given. Returning null.`); /* diff --git a/src/tests/backend/specs/api/api.js b/src/tests/backend/specs/api/api.js index d05a9989d5a..1415795b277 100644 --- a/src/tests/backend/specs/api/api.js +++ b/src/tests/backend/specs/api/api.js @@ -55,5 +55,4 @@ describe(__filename, function () { } }); }); - }); diff --git a/src/tests/frontend/specs/chat.js b/src/tests/frontend/specs/chat.js index f471c447934..3b1eaebf360 100644 --- a/src/tests/frontend/specs/chat.js +++ b/src/tests/frontend/specs/chat.js @@ -87,7 +87,6 @@ describe('Chat messages and UI', function () { xit('Checks showChat=false URL Parameter hides chat then' + ' when removed it shows chat', async function () { - // give it a second to save the username on the server side await new Promise((resolve) => setTimeout(resolve, 3000)); From 6f2f20233f389dc63e7f9435232a9778da453fec Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 11 May 2021 16:56:06 -0400 Subject: [PATCH 126/218] lint: Fix straightforward ESLint errors --- src/bin/extractPadData.js | 2 + src/bin/importSqlFile.js | 2 + src/node/db/API.js | 4 +- src/node/db/AuthorManager.js | 4 +- src/node/db/DB.js | 2 +- src/node/db/Pad.js | 100 +++++++++++++++------------------- src/node/db/SessionManager.js | 2 +- src/node/easysync_tests.js | 2 +- src/node/utils/Settings.js | 6 +- 9 files changed, 57 insertions(+), 67 deletions(-) diff --git a/src/bin/extractPadData.js b/src/bin/extractPadData.js index 0688245d491..353c5d21e99 100644 --- a/src/bin/extractPadData.js +++ b/src/bin/extractPadData.js @@ -10,6 +10,8 @@ // unhandled rejection into an uncaught exception, which does cause Node.js to exit. process.on('unhandledRejection', (err) => { throw err; }); +const util = require('util'); + if (process.argv.length !== 3) throw new Error('Use: node extractPadData.js $PADID'); // get the padID diff --git a/src/bin/importSqlFile.js b/src/bin/importSqlFile.js index 5a052088506..b7e3a16c0f2 100644 --- a/src/bin/importSqlFile.js +++ b/src/bin/importSqlFile.js @@ -4,6 +4,8 @@ // unhandled rejection into an uncaught exception, which does cause Node.js to exit. process.on('unhandledRejection', (err) => { throw err; }); +const util = require('util'); + const startTime = Date.now(); const log = (str) => { diff --git a/src/node/db/API.js b/src/node/db/API.js index c262e078b9f..f119f58479b 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -831,7 +831,7 @@ exports.getStats = async () => { const isInt = (value) => (parseFloat(value) === parseInt(value, 10)) && !isNaN(value); // gets a pad safe -async function getPadSafe(padID, shouldExist, text) { +const getPadSafe = async (padID, shouldExist, text) => { // check if padID is a string if (typeof padID !== 'string') { throw new CustomError('padID is not a string', 'apierror'); @@ -857,7 +857,7 @@ async function getPadSafe(padID, shouldExist, text) { // pad exists, let's get it return padManager.getPad(padID, text); -} +}; // checks if a rev is a legal number // pre-condition is that `rev` is not undefined diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index e457b7eb288..2a354d425c5 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -135,7 +135,7 @@ exports.createAuthorIfNotExistsFor = async (authorMapper, name) => { * @param {String} mapperkey The database key name for this mapper * @param {String} mapper The mapper */ -async function mapAuthorWithDBKey(mapperkey, mapper) { +const mapAuthorWithDBKey = async (mapperkey, mapper) => { // try to map to an author const author = await db.get(`${mapperkey}:${mapper}`); @@ -156,7 +156,7 @@ async function mapAuthorWithDBKey(mapperkey, mapper) { // return the author return {authorID: author}; -} +}; /** * Internal function that creates the database entry for an author diff --git a/src/node/db/DB.js b/src/node/db/DB.js index e1097e90566..fbf71f88de4 100644 --- a/src/node/db/DB.js +++ b/src/node/db/DB.js @@ -29,7 +29,7 @@ const util = require('util'); // set database settings const db = - new ueberDB.database(settings.dbType, settings.dbSettings, null, log4js.getLogger('ueberDB')); + new ueberDB.Database(settings.dbType, settings.dbSettings, null, log4js.getLogger('ueberDB')); /** * The UeberDB Object that provides the database functions diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index a3466898786..db35daf19e3 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -32,7 +32,7 @@ exports.cleanText = (txt) => txt.replace(/\r\n/g, '\n') .replace(/\t/g, ' ') .replace(/\xa0/g, ' '); -const Pad = function Pad(id) { +const Pad = function (id) { this.atext = Changeset.makeAText('\n'); this.pool = new AttributePool(); this.head = -1; @@ -44,32 +44,29 @@ const Pad = function Pad(id) { exports.Pad = Pad; -Pad.prototype.apool = function apool() { +Pad.prototype.apool = function () { return this.pool; }; -Pad.prototype.getHeadRevisionNumber = function getHeadRevisionNumber() { +Pad.prototype.getHeadRevisionNumber = function () { return this.head; }; -Pad.prototype.getSavedRevisionsNumber = function getSavedRevisionsNumber() { +Pad.prototype.getSavedRevisionsNumber = function () { return this.savedRevisions.length; }; -Pad.prototype.getSavedRevisionsList = function getSavedRevisionsList() { - const savedRev = []; - for (const rev in this.savedRevisions) { - savedRev.push(this.savedRevisions[rev].revNum); - } +Pad.prototype.getSavedRevisionsList = function () { + const savedRev = this.savedRevisions.map((rev) => rev.revNum); savedRev.sort((a, b) => a - b); return savedRev; }; -Pad.prototype.getPublicStatus = function getPublicStatus() { +Pad.prototype.getPublicStatus = function () { return this.publicStatus; }; -Pad.prototype.appendRevision = async function appendRevision(aChangeset, author) { +Pad.prototype.appendRevision = async function (aChangeset, author) { if (!author) { author = ''; } @@ -115,7 +112,7 @@ Pad.prototype.appendRevision = async function appendRevision(aChangeset, author) }; // save all attributes to the database -Pad.prototype.saveToDatabase = async function saveToDatabase() { +Pad.prototype.saveToDatabase = async function () { const dbObject = {}; for (const attr in this) { @@ -133,24 +130,24 @@ Pad.prototype.saveToDatabase = async function saveToDatabase() { }; // get time of last edit (changeset application) -Pad.prototype.getLastEdit = function getLastEdit() { +Pad.prototype.getLastEdit = function () { const revNum = this.getHeadRevisionNumber(); return db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']); }; -Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum) { +Pad.prototype.getRevisionChangeset = function (revNum) { return db.getSub(`pad:${this.id}:revs:${revNum}`, ['changeset']); }; -Pad.prototype.getRevisionAuthor = function getRevisionAuthor(revNum) { +Pad.prototype.getRevisionAuthor = function (revNum) { return db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'author']); }; -Pad.prototype.getRevisionDate = function getRevisionDate(revNum) { +Pad.prototype.getRevisionDate = function (revNum) { return db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']); }; -Pad.prototype.getAllAuthors = function getAllAuthors() { +Pad.prototype.getAllAuthors = function () { const authors = []; for (const key in this.pool.numToAttrib) { @@ -162,7 +159,7 @@ Pad.prototype.getAllAuthors = function getAllAuthors() { return authors; }; -Pad.prototype.getInternalRevisionAText = async function getInternalRevisionAText(targetRev) { +Pad.prototype.getInternalRevisionAText = async function (targetRev) { const keyRev = this.getKeyRevisionNumber(targetRev); // find out which changesets are needed @@ -197,11 +194,11 @@ Pad.prototype.getInternalRevisionAText = async function getInternalRevisionAText return atext; }; -Pad.prototype.getRevision = function getRevisionChangeset(revNum) { +Pad.prototype.getRevision = function (revNum) { return db.get(`pad:${this.id}:revs:${revNum}`); }; -Pad.prototype.getAllAuthorColors = async function getAllAuthorColors() { +Pad.prototype.getAllAuthorColors = async function () { const authors = this.getAllAuthors(); const returnTable = {}; const colorPalette = authorManager.getColorPalette(); @@ -215,7 +212,7 @@ Pad.prototype.getAllAuthorColors = async function getAllAuthorColors() { return returnTable; }; -Pad.prototype.getValidRevisionRange = function getValidRevisionRange(startRev, endRev) { +Pad.prototype.getValidRevisionRange = function (startRev, endRev) { startRev = parseInt(startRev, 10); const head = this.getHeadRevisionNumber(); endRev = endRev ? parseInt(endRev, 10) : head; @@ -236,15 +233,15 @@ Pad.prototype.getValidRevisionRange = function getValidRevisionRange(startRev, e return null; }; -Pad.prototype.getKeyRevisionNumber = function getKeyRevisionNumber(revNum) { +Pad.prototype.getKeyRevisionNumber = function (revNum) { return Math.floor(revNum / 100) * 100; }; -Pad.prototype.text = function text() { +Pad.prototype.text = function () { return this.atext.text; }; -Pad.prototype.setText = async function setText(newText) { +Pad.prototype.setText = async function (newText) { // clean the new text newText = exports.cleanText(newText); @@ -264,7 +261,7 @@ Pad.prototype.setText = async function setText(newText) { await this.appendRevision(changeset); }; -Pad.prototype.appendText = async function appendText(newText) { +Pad.prototype.appendText = async function (newText) { // clean the new text newText = exports.cleanText(newText); @@ -277,7 +274,7 @@ Pad.prototype.appendText = async function appendText(newText) { await this.appendRevision(changeset); }; -Pad.prototype.appendChatMessage = async function appendChatMessage(text, userId, time) { +Pad.prototype.appendChatMessage = async function (text, userId, time) { this.chatHead++; // save the chat entry in the database await Promise.all([ @@ -286,7 +283,7 @@ Pad.prototype.appendChatMessage = async function appendChatMessage(text, userId, ]); }; -Pad.prototype.getChatMessage = async function getChatMessage(entryNum) { +Pad.prototype.getChatMessage = async function (entryNum) { // get the chat entry const entry = await db.get(`pad:${this.id}:chat:${entryNum}`); @@ -298,7 +295,7 @@ Pad.prototype.getChatMessage = async function getChatMessage(entryNum) { return entry; }; -Pad.prototype.getChatMessages = async function getChatMessages(start, end) { +Pad.prototype.getChatMessages = async function (start, end) { // collect the numbers of chat entries and in which order we need them const neededEntries = []; for (let order = 0, entryNum = start; entryNum <= end; ++order, ++entryNum) { @@ -326,7 +323,7 @@ Pad.prototype.getChatMessages = async function getChatMessages(start, end) { return cleanedEntries; }; -Pad.prototype.init = async function init(text) { +Pad.prototype.init = async function (text) { // replace text with default text if text isn't set if (text == null) { text = settings.defaultPadText; @@ -355,8 +352,7 @@ Pad.prototype.init = async function init(text) { hooks.callAll('padLoad', {pad: this}); }; -Pad.prototype.copy = async function copy(destinationID, force) { - let destGroupID; +Pad.prototype.copy = async function (destinationID, force) { const sourceID = this.id; // Kick everyone from this pad. @@ -367,16 +363,11 @@ Pad.prototype.copy = async function copy(destinationID, force) { // flush the source pad: await this.saveToDatabase(); + // if it's a group pad, let's make sure the group exists. + const destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID); - try { - // if it's a group pad, let's make sure the group exists. - destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID); - - // if force is true and already exists a Pad with the same id, remove that Pad - await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force); - } catch (err) { - throw err; - } + // if force is true and already exists a Pad with the same id, remove that Pad + await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force); // copy the 'pad' entry const pad = await db.get(`pad:${sourceID}`); @@ -423,7 +414,7 @@ Pad.prototype.copy = async function copy(destinationID, force) { return {padID: destinationID}; }; -Pad.prototype.checkIfGroupExistAndReturnIt = async function checkIfGroupExistAndReturnIt(destinationID) { +Pad.prototype.checkIfGroupExistAndReturnIt = async function (destinationID) { let destGroupID = false; if (destinationID.indexOf('$') >= 0) { @@ -438,7 +429,7 @@ Pad.prototype.checkIfGroupExistAndReturnIt = async function checkIfGroupExistAnd return destGroupID; }; -Pad.prototype.removePadIfForceIsTrueAndAlreadyExist = async function removePadIfForceIsTrueAndAlreadyExist(destinationID, force) { +Pad.prototype.removePadIfForceIsTrueAndAlreadyExist = async function (destinationID, force) { // if the pad exists, we should abort, unless forced. const exists = await padManager.doesPadExist(destinationID); @@ -461,29 +452,24 @@ Pad.prototype.removePadIfForceIsTrueAndAlreadyExist = async function removePadIf } }; -Pad.prototype.copyAuthorInfoToDestinationPad = function copyAuthorInfoToDestinationPad(destinationID) { +Pad.prototype.copyAuthorInfoToDestinationPad = function (destinationID) { // add the new sourcePad to all authors who contributed to the old one this.getAllAuthors().forEach((authorID) => { authorManager.addPad(authorID, destinationID); }); }; -Pad.prototype.copyPadWithoutHistory = async function copyPadWithoutHistory(destinationID, force) { - let destGroupID; +Pad.prototype.copyPadWithoutHistory = async function (destinationID, force) { const sourceID = this.id; // flush the source pad this.saveToDatabase(); - try { - // if it's a group pad, let's make sure the group exists. - destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID); + // if it's a group pad, let's make sure the group exists. + const destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID); - // if force is true and already exists a Pad with the same id, remove that Pad - await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force); - } catch (err) { - throw err; - } + // if force is true and already exists a Pad with the same id, remove that Pad + await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force); const sourcePad = await padManager.getPad(sourceID); @@ -526,7 +512,7 @@ Pad.prototype.copyPadWithoutHistory = async function copyPadWithoutHistory(desti }; -Pad.prototype.remove = async function remove() { +Pad.prototype.remove = async function () { const padID = this.id; const p = []; @@ -579,12 +565,12 @@ Pad.prototype.remove = async function remove() { }; // set in db -Pad.prototype.setPublicStatus = async function setPublicStatus(publicStatus) { +Pad.prototype.setPublicStatus = async function (publicStatus) { this.publicStatus = publicStatus; await this.saveToDatabase(); }; -Pad.prototype.addSavedRevision = async function addSavedRevision(revNum, savedById, label) { +Pad.prototype.addSavedRevision = async function (revNum, savedById, label) { // if this revision is already saved, return silently for (const i in this.savedRevisions) { if (this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum) { @@ -605,6 +591,6 @@ Pad.prototype.addSavedRevision = async function addSavedRevision(revNum, savedBy await this.saveToDatabase(); }; -Pad.prototype.getSavedRevisions = function getSavedRevisions() { +Pad.prototype.getSavedRevisions = function () { return this.savedRevisions; }; diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index d46ed9faea9..9a222e2fb01 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -250,7 +250,7 @@ const listSessionsWithDBKey = async (dbkey) => { const sessions = sessionObject ? sessionObject.sessionIDs : null; // iterate through the sessions and get the sessioninfos - for (const sessionID in sessions) { + for (const sessionID of Object.keys(sessions || {})) { try { const sessionInfo = await exports.getSessionInfo(sessionID); sessions[sessionID] = sessionInfo; diff --git a/src/node/easysync_tests.js b/src/node/easysync_tests.js index 78d9f61e8cf..25a70e028d9 100644 --- a/src/node/easysync_tests.js +++ b/src/node/easysync_tests.js @@ -42,7 +42,7 @@ const runTests = () => { const literal = (v) => { if ((typeof v) === 'string') { - return `"${v.replace(/[\\\"]/g, '\\$1').replace(/\n/g, '\\n')}"`; + return `"${v.replace(/[\\"]/g, '\\$1').replace(/\n/g, '\\n')}"`; } else { return JSON.stringify(v); } }; diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 38ac5bceac9..e5b1636db00 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -141,7 +141,7 @@ exports.padOptions = { alwaysShowChat: false, chatAndUsers: false, lang: 'en-gb', -}, +}; /** * Whether certain shortcut keys are enabled for a user in the pad @@ -169,7 +169,7 @@ exports.padShortcutEnabled = { ctrlHome: true, pageUp: true, pageDown: true, -}, +}; /** * The toolbar buttons and order. @@ -466,7 +466,7 @@ exports.getEpVersion = () => require('../../package.json').version; * both "settings.json" and "credentials.json". */ const storeSettings = (settingsObj) => { - for (const i in settingsObj) { + for (const i of Object.keys(settingsObj || {})) { // test if the setting starts with a lowercase character if (i.charAt(0).search('[a-z]') !== 0) { console.warn(`Settings should start with a lowercase character: '${i}'`); From c426e939d177a985baf149b73df61f85bce8bc95 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 24 May 2021 14:00:35 +0200 Subject: [PATCH 127/218] Localisation updates from https://translatewiki.net. --- src/locales/sq.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/locales/sq.json b/src/locales/sq.json index f71a7b4435c..0c59f590d50 100644 --- a/src/locales/sq.json +++ b/src/locales/sq.json @@ -40,7 +40,7 @@ "admin_settings.current_restart.value": "Rinise Etherpad-in", "admin_settings.current_save.value": "Ruaji Rregullimet", "admin_settings.page-title": "Rregullime - Etherpad", - "index.newPad": "Bllok i ri", + "index.newPad": "Bllok i Ri", "index.createOpenPad": "ose krijoni/hapni një Bllok me emrin:", "index.openPad": "hapni një Bllok ekzistues me emrin:", "pad.toolbar.bold.title": "Të trasha (Ctrl-B)", @@ -56,7 +56,7 @@ "pad.toolbar.clearAuthorship.title": "Hiqu Ngjyra Autorësish (Ctrl+Shift+C)", "pad.toolbar.import_export.title": "Importoni/Eksportoni nga/në formate të tjera kartelash", "pad.toolbar.timeslider.title": "Rrjedha kohore", - "pad.toolbar.savedRevision.title": "Ruaje rishikimin", + "pad.toolbar.savedRevision.title": "Ruaje Rishikimin", "pad.toolbar.settings.title": "Rregullime", "pad.toolbar.embed.title": "Ndajeni me të tjerët dhe Trupëzojeni këtë bllok", "pad.toolbar.showusers.title": "Shfaq përdoruesit në këtë bllok", @@ -66,7 +66,7 @@ "pad.noCookie": "S’u gjet dot cookie. Ju lutemi, lejoni cookie-t te shfletuesi juaj! Sesioni dhe rregullimet tuaja s’do të ruhen nga një sesion në tjetër. Kjo mund të vijë ngaqë Etherpad përfshihet brenda një iFrame në disa shfletues. Ju lutemi, sigurohuni që Etherpad-i të jetë në të njëjtën nënpërkatësi/përkatësi si iFrame-i mëmë.", "pad.permissionDenied": "S’keni leje të hyni në këtë bllok", "pad.settings.padSettings": "Rregullime Blloku", - "pad.settings.myView": "Pamja ime", + "pad.settings.myView": "Pamja Ime", "pad.settings.stickychat": "Fjalosje përherë në ekran", "pad.settings.chatandusers": "Shfaq Fjalosje dhe Përdorues", "pad.settings.colorcheck": "Ngjyra autorësish", @@ -90,12 +90,12 @@ "pad.importExport.abiword.innerHTML": "Mund të importoni vetëm prej formati tekst i thjeshtë ose HTML. Për veçori më të thelluara importimi, ju lutemi, instaloni AbiWord-in ose LibreOffice.", "pad.modals.connected": "I lidhur.", "pad.modals.reconnecting": "Po rilidheni te blloku juaj…", - "pad.modals.forcereconnect": "Rilidhje e detyruar", + "pad.modals.forcereconnect": "Detyro rilidhje", "pad.modals.reconnecttimer": "Provë për rilidhje pas", "pad.modals.cancel": "Anuloje", "pad.modals.userdup": "Hapur në një tjetër dritare", "pad.modals.userdup.explanation": "Ky bllok duket se gjendet i hapur në më shumë se një dritare shfletuesi në këtë kompjuter.", - "pad.modals.userdup.advice": "Rilidhuni që të përdoret kjo dritare.", + "pad.modals.userdup.advice": "Që të përdoret kjo dritare, rilidhuni.", "pad.modals.unauth": "I paautorizuar", "pad.modals.unauth.explanation": "Lejet tuaja ndryshuan teksa shihnit këtë dritare. Provoni të rilidheni.", "pad.modals.looping.explanation": "Ka probleme komunikimi me shërbyesin e njëkohësimit.", @@ -103,10 +103,10 @@ "pad.modals.initsocketfail": "Shërbyesi është i pakapshëm.", "pad.modals.initsocketfail.explanation": "S’u lidh dot te shërbyesi i njëkohësimit.", "pad.modals.initsocketfail.cause": "Ka gjasa që kjo vjen për shkak të një problemi me shfletuesin tuaj ose lidhjen tuaj në internet.", - "pad.modals.slowcommit.explanation": "Shërbyesi nuk po përgjigjet.", + "pad.modals.slowcommit.explanation": "Shërbyesi s’po përgjigjet.", "pad.modals.slowcommit.cause": "Kjo mund të vijë për shkak problemesh lidhjeje me rrjetin.", - "pad.modals.badChangeset.explanation": "Një përpunim që keni bërë u vlerësua si i paligjshëm nga shërbyesi i njëkohësimit.", - "pad.modals.badChangeset.cause": "Kjo mund të jetë për shkak të një formësimi të gabuar të shërbyesit ose ndonjë tjetër sjelljeje të papritur. Ju lutemi, lidhuni me përgjegjësin e shërbimit, nëse mendoni që ky është një gabim. Provoni të rilidheni që të vazhdoni përpunimin.", + "pad.modals.badChangeset.explanation": "Një përpunim që keni bërë, u vlerësua si i paligjshëm nga shërbyesi i njëkohësimit.", + "pad.modals.badChangeset.cause": "Kjo mund të jetë për shkak të një formësimi të gabuar të shërbyesit ose ndonjë tjetër sjelljeje të papritur. Ju lutemi, lidhuni me përgjegjësin e shërbimit, nëse mendoni se ky është një gabim. Që të vazhdoni përpunimin, provoni të rilidheni.", "pad.modals.corruptPad.explanation": "Blloku te i cili po përpiqeni të hyni është i dëmtuar.", "pad.modals.corruptPad.cause": "Kjo mund të vijë nga një formësim i gabuar shërbyesi ose ndonjë tjetër sjellje e papritur. Ju lutemi, lidhuni me përgjegjësin e shërbimit.", "pad.modals.deleted": "I fshirë.", @@ -131,7 +131,7 @@ "timeslider.pageTitle": "Rrjedhë kohore e {{appTitle}}", "timeslider.toolbar.returnbutton": "Rikthehuni te blloku", "timeslider.toolbar.authors": "Autorë:", - "timeslider.toolbar.authorsList": "S’ka autorë", + "timeslider.toolbar.authorsList": "S’ka Autorë", "timeslider.toolbar.exportlink.title": "Eksportoje", "timeslider.exportCurrent": "Eksportojeni versionin e tanishëm si:", "timeslider.version": "Versioni {{version}}", From a04089636c86453deda47c0ba564c912fe90926c Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 27 May 2021 15:37:31 +0200 Subject: [PATCH 128/218] Localisation updates from https://translatewiki.net. --- src/locales/ar.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/locales/ar.json b/src/locales/ar.json index fa872a956fd..6bfda085453 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -3,6 +3,7 @@ "authors": [ "Alami", "Ali1", + "ArticleEditor404", "Haytham morsy", "Meno25", "Mido", @@ -14,11 +15,21 @@ "محمد أحمد عبد الفتاح" ] }, + "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_uninstall.value": "فك التنصيب", + "admin_plugins.last-update": "آخر تحديث", "admin_plugins.name": "الاسم", "admin_plugins.version": "الإصدار", + "admin_plugins_info.version_latest": "أحدث نسخة متاحة", "admin_plugins_info.version_number": "رقم الإصدار", "admin_settings": "إعدادات", + "admin_settings.current": "التكوين الحالي", "index.newPad": "باد جديد", "index.createOpenPad": "أو صنع/فتح باد بوضع اسمه:", "index.openPad": "افتح باد موجودة بالاسم:", @@ -90,6 +101,8 @@ "pad.modals.corruptPad.cause": "قد يكون هذا بسبب تكوين ملقم خاطئ أو بسبب سلوك آخر غير متوقع. يرجى الاتصال بمسؤول الخدمة.", "pad.modals.deleted": "محذوف.", "pad.modals.deleted.explanation": "تمت إزالة هذا الباد.", + "pad.modals.rateLimited.explanation": "لقد أرسلت عددًا كبيرًا جدًا من الرسائل إلى هذه اللوحة ، لذا فقد قطع اتصالك.", + "pad.modals.rejected.explanation": "رفض الخادم رسالة أرسلها متصفحك.", "pad.modals.disconnected": "لم تعد متصلا.", "pad.modals.disconnected.explanation": "تم فقدان الاتصال بالخادم", "pad.modals.disconnected.cause": "قد يكون الخادم غير متوفر. يرجى إعلام مسؤول الخدمة إذا كان هذا لا يزال يحدث.", From 8f63671ea973f991ca90b10d0d9ce4ad75ef5994 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 31 May 2021 10:02:43 +0200 Subject: [PATCH 129/218] Localisation updates from https://translatewiki.net. --- src/locales/sl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/sl.json b/src/locales/sl.json index 7a768c05801..e8e952d4815 100644 --- a/src/locales/sl.json +++ b/src/locales/sl.json @@ -72,7 +72,7 @@ "pad.modals.unauth": "Nepooblaščen dostop", "pad.modals.unauth.explanation": "Med ogledovanjem strani so se dovoljenja za ogled spremenila. Poskusite se znova povezati.", "pad.modals.looping.explanation": "Zaznane so težave pri komunikaciji s strežnikom za usklajevanje.", - "pad.modals.looping.cause": "Morda ste se povezali preko neustrezno nastavljenega požarnega zidu ali posredniškega strežnika.", + "pad.modals.looping.cause": "Morda ste se povezali skozi neustrezno nastavljen požarni zid ali s posredniškim strežnikom.", "pad.modals.initsocketfail": "Strežnik je nedosegljiv.", "pad.modals.initsocketfail.explanation": "Povezava s strežnikom za usklajevanje ni mogoča.", "pad.modals.initsocketfail.cause": "Najverjetneje gre za težavo z vašim brskalnikom, ali internetno povezavo.", From 3bca85286b096d67ac9ec5c04f37ff8087f3cc84 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 3 Jun 2021 15:12:59 +0200 Subject: [PATCH 130/218] Localisation updates from https://translatewiki.net. --- src/locales/vec.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/vec.json b/src/locales/vec.json index 2dd38663d31..b925baf734d 100644 --- a/src/locales/vec.json +++ b/src/locales/vec.json @@ -31,7 +31,7 @@ "timeslider.month.march": "Marso", "timeslider.month.april": "Apriłe", "timeslider.month.may": "Majo", - "timeslider.month.june": "Xugno", + "timeslider.month.june": "Zugno", "timeslider.month.july": "Lujo", "timeslider.month.august": "Agosto", "timeslider.month.september": "Setenbre", From 926da57e3484caa7242e8ad89fcb628709954f42 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 7 May 2021 23:25:59 -0400 Subject: [PATCH 131/218] Minify: Refine `sanitizePathname` to avoid pathname traversal --- src/node/utils/Minify.js | 49 +++++++--------- src/tests/backend/specs/Minify.js | 98 +++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 28 deletions(-) create mode 100644 src/tests/backend/specs/Minify.js diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index 83c4be55033..b4d5c851407 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -21,7 +21,6 @@ * limitations under the License. */ -const assert = require('assert').strict; const settings = require('./Settings'); const fs = require('fs').promises; const path = require('path'); @@ -105,33 +104,23 @@ const requestURIs = (locations, method, headers, callback) => { }); }; -const sanitizePathname = (p) => { - // Replace all backslashes with forward slashes to support Windows. This MUST be done BEFORE path - // normalization, otherwise an attacker will be able to read arbitrary files anywhere on the - // filesystem. See https://nvd.nist.gov/vuln/detail/CVE-2015-3297. Node.js treats both the - // backlash and the forward slash characters as pathname component separators on Windows so this - // does not change the meaning of the pathname. - p = p.replace(/\\/g, '/'); - // The Node.js documentation says that path.join() normalizes, and the documentation for - // path.normalize() says that it resolves '..' and '.' components. The word "resolve" implies that - // it examines the filesystem to resolve symbolic links, so 'a/../b' might not be the same thing - // as 'b'. Most path normalization functions from other libraries (e.g. Python's - // os.path.normpath()) clearly state that they do not examine the filesystem -- they are simple - // string manipulations. Node.js's path.normalize() probably also does a simple string - // manipulation, but if not it must be given a real pathname. Join with ROOT_DIR here just in - // case. ROOT_DIR will be removed later. - p = path.join(ROOT_DIR, p); - // Prevent attempts to read outside of ROOT_DIR via extra '..' components. ROOT_DIR is assumed to - // be normalized. - assert(ROOT_DIR.endsWith(path.sep)); - if (!p.startsWith(ROOT_DIR)) throw new Error(`attempt to read outside ROOT_DIR (${ROOT_DIR})`); - // Convert back to a relative pathname. - p = p.slice(ROOT_DIR.length); - // On Windows, path.normalize replaces forward slashes with backslashes. Convert back to forward - // slashes. THIS IS DANGEROUS UNLESS BACKSLASHES ARE REPLACED WITH FORWARD SLASHES BEFORE PATH - // NORMALIZATION, otherwise on POSIXish systems '..\\' in the input pathname would not be - // normalized away before being converted to '../'. - p = p.replace(/\\/g, '/'); +// Normalizes p and ensures that it is a relative path that does not reach outside. See +// https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context. +const sanitizePathname = (p, pathApi = path) => { + // The documentation for path.normalize() says that it resolves '..' and '.' segments. The word + // "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might + // not be the same thing as 'b'. Most path normalization functions from other libraries (e.g., + // Python's os.path.normpath()) clearly state that they do not examine the filesystem. Here we + // assume Node.js's path.normalize() does the same; that it is only a simple string manipulation. + p = pathApi.normalize(p); + if (pathApi.isAbsolute(p)) throw new Error(`absolute paths are forbidden: ${p}`); + if (p.split(pathApi.sep)[0] === '..') throw new Error(`directory traversal: ${p}`); + // On Windows, path normalization replaces forwardslashes with backslashes. Convert them back to + // forwardslashes. Node.js treats both the backlash and the forwardslash characters as pathname + // component separators on Windows so this does not change the meaning of the pathname on Windows. + // THIS CONVERSION MUST ONLY BE DONE ON WINDOWS, otherwise on POSIXish systems '..\\' in the input + // pathname would not be normalized away before being converted to '../'. + if (pathApi.sep === '\\') p = p.replace(/\\/g, '/'); return p; }; @@ -351,3 +340,7 @@ exports.requestURIs = requestURIs; exports.shutdown = async (hookName, context) => { await threadsPool.terminate(); }; + +exports.exportedForTestingOnly = { + sanitizePathname, +}; diff --git a/src/tests/backend/specs/Minify.js b/src/tests/backend/specs/Minify.js new file mode 100644 index 00000000000..5834bb94e68 --- /dev/null +++ b/src/tests/backend/specs/Minify.js @@ -0,0 +1,98 @@ +'use strict'; + +const Minify = require('../../../node/utils/Minify'); +const assert = require('assert').strict; +const path = require('path'); + +const {sanitizePathname} = Minify.exportedForTestingOnly; + +describe(__filename, function () { + describe('absolute paths rejected', function () { + const testCases = [ + ['posix', '/'], + ['posix', '/foo'], + ['win32', '/'], + ['win32', '\\'], + ['win32', 'C:/foo'], + ['win32', 'C:\\foo'], + ['win32', 'c:/foo'], + ['win32', 'c:\\foo'], + ['win32', '/foo'], + ['win32', '\\foo'], + ]; + for (const [platform, p] of testCases) { + it(`${platform} ${p}`, async function () { + assert.throws(() => sanitizePathname(p, path[platform]), {message: /absolute path/}); + }); + } + }); + describe('directory traversal rejected', function () { + const testCases = [ + ['posix', '..'], + ['posix', '../'], + ['posix', '../foo'], + ['posix', 'foo/../..'], + ['win32', '..'], + ['win32', '../'], + ['win32', '..\\'], + ['win32', '../foo'], + ['win32', '..\\foo'], + ['win32', 'foo/../..'], + ['win32', 'foo\\..\\..'], + ]; + for (const [platform, p] of testCases) { + it(`${platform} ${p}`, async function () { + assert.throws(() => sanitizePathname(p, path[platform]), {message: /travers/}); + }); + } + }); + + describe('accepted paths', function () { + const testCases = [ + ['posix', '', '.'], + ['posix', '.'], + ['posix', './'], + ['posix', 'foo'], + ['posix', 'foo/'], + ['posix', 'foo/bar/..', 'foo'], + ['posix', 'foo/bar/../', 'foo/'], + ['posix', './foo', 'foo'], + ['posix', 'foo/bar'], + ['posix', 'foo\\bar'], + ['posix', '\\foo'], + ['posix', '..\\foo'], + ['posix', 'foo/../bar', 'bar'], + ['posix', 'C:/foo'], + ['posix', 'C:\\foo'], + ['win32', '', '.'], + ['win32', '.'], + ['win32', './'], + ['win32', '.\\', './'], + ['win32', 'foo'], + ['win32', 'foo/'], + ['win32', 'foo\\', 'foo/'], + ['win32', 'foo/bar/..', 'foo'], + ['win32', 'foo\\bar\\..', 'foo'], + ['win32', 'foo/bar/../', 'foo/'], + ['win32', 'foo\\bar\\..\\', 'foo/'], + ['win32', './foo', 'foo'], + ['win32', '.\\foo', 'foo'], + ['win32', 'foo/bar'], + ['win32', 'foo\\bar', 'foo/bar'], + ['win32', 'foo/../bar', 'bar'], + ['win32', 'foo\\..\\bar', 'bar'], + ['win32', 'foo/..\\bar', 'bar'], + ['win32', 'foo\\../bar', 'bar'], + ]; + for (const [platform, p, tcWant] of testCases) { + const want = tcWant == null ? p : tcWant; + it(`${platform} ${p || ''} -> ${want}`, async function () { + assert.equal(sanitizePathname(p, path[platform]), want); + }); + } + }); + + it('default path API', async function () { + assert.equal(sanitizePathname('foo'), 'foo'); + }); +}); From 0d9476529e08359e02630d58e8519529b37db9c0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 8 May 2021 17:40:36 -0400 Subject: [PATCH 132/218] sanitizePathname: Move to separate module to facilitate reuse --- src/node/utils/Minify.js | 25 +------------------ src/node/utils/sanitizePathname.js | 23 +++++++++++++++++ .../specs/{Minify.js => sanitizePathname.js} | 4 +-- 3 files changed, 25 insertions(+), 27 deletions(-) create mode 100644 src/node/utils/sanitizePathname.js rename src/tests/backend/specs/{Minify.js => sanitizePathname.js} (96%) diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index b4d5c851407..02728824dc5 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -29,6 +29,7 @@ const RequireKernel = require('etherpad-require-kernel'); const mime = require('mime-types'); const Threads = require('threads'); const log4js = require('log4js'); +const sanitizePathname = require('./sanitizePathname'); const logger = log4js.getLogger('Minify'); @@ -104,26 +105,6 @@ const requestURIs = (locations, method, headers, callback) => { }); }; -// Normalizes p and ensures that it is a relative path that does not reach outside. See -// https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context. -const sanitizePathname = (p, pathApi = path) => { - // The documentation for path.normalize() says that it resolves '..' and '.' segments. The word - // "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might - // not be the same thing as 'b'. Most path normalization functions from other libraries (e.g., - // Python's os.path.normpath()) clearly state that they do not examine the filesystem. Here we - // assume Node.js's path.normalize() does the same; that it is only a simple string manipulation. - p = pathApi.normalize(p); - if (pathApi.isAbsolute(p)) throw new Error(`absolute paths are forbidden: ${p}`); - if (p.split(pathApi.sep)[0] === '..') throw new Error(`directory traversal: ${p}`); - // On Windows, path normalization replaces forwardslashes with backslashes. Convert them back to - // forwardslashes. Node.js treats both the backlash and the forwardslash characters as pathname - // component separators on Windows so this does not change the meaning of the pathname on Windows. - // THIS CONVERSION MUST ONLY BE DONE ON WINDOWS, otherwise on POSIXish systems '..\\' in the input - // pathname would not be normalized away before being converted to '../'. - if (pathApi.sep === '\\') p = p.replace(/\\/g, '/'); - return p; -}; - const compatPaths = { 'js/browser.js': 'js/vendors/browser.js', 'js/farbtastic.js': 'js/vendors/farbtastic.js', @@ -340,7 +321,3 @@ exports.requestURIs = requestURIs; exports.shutdown = async (hookName, context) => { await threadsPool.terminate(); }; - -exports.exportedForTestingOnly = { - sanitizePathname, -}; diff --git a/src/node/utils/sanitizePathname.js b/src/node/utils/sanitizePathname.js new file mode 100644 index 00000000000..61b61116661 --- /dev/null +++ b/src/node/utils/sanitizePathname.js @@ -0,0 +1,23 @@ +'use strict'; + +const path = require('path'); + +// Normalizes p and ensures that it is a relative path that does not reach outside. See +// https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context. +module.exports = (p, pathApi = path) => { + // The documentation for path.normalize() says that it resolves '..' and '.' segments. The word + // "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might + // not be the same thing as 'b'. Most path normalization functions from other libraries (e.g., + // Python's os.path.normpath()) clearly state that they do not examine the filesystem. Here we + // assume Node.js's path.normalize() does the same; that it is only a simple string manipulation. + p = pathApi.normalize(p); + if (pathApi.isAbsolute(p)) throw new Error(`absolute paths are forbidden: ${p}`); + if (p.split(pathApi.sep)[0] === '..') throw new Error(`directory traversal: ${p}`); + // On Windows, path normalization replaces forwardslashes with backslashes. Convert them back to + // forwardslashes. Node.js treats both the backlash and the forwardslash characters as pathname + // component separators on Windows so this does not change the meaning of the pathname on Windows. + // THIS CONVERSION MUST ONLY BE DONE ON WINDOWS, otherwise on POSIXish systems '..\\' in the input + // pathname would not be normalized away before being converted to '../'. + if (pathApi.sep === '\\') p = p.replace(/\\/g, '/'); + return p; +}; diff --git a/src/tests/backend/specs/Minify.js b/src/tests/backend/specs/sanitizePathname.js similarity index 96% rename from src/tests/backend/specs/Minify.js rename to src/tests/backend/specs/sanitizePathname.js index 5834bb94e68..767221920dd 100644 --- a/src/tests/backend/specs/Minify.js +++ b/src/tests/backend/specs/sanitizePathname.js @@ -1,10 +1,8 @@ 'use strict'; -const Minify = require('../../../node/utils/Minify'); const assert = require('assert').strict; const path = require('path'); - -const {sanitizePathname} = Minify.exportedForTestingOnly; +const sanitizePathname = require('../../../node/utils/sanitizePathname'); describe(__filename, function () { describe('absolute paths rejected', function () { From d87b4e0c20ccd03771833c878345edc4b39139a1 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 9 May 2021 16:23:49 -0400 Subject: [PATCH 133/218] tests: Use `async`/`await` instead of returning Promises This makes stack traces more useful. --- src/node/hooks/express/tests.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index c533ae8ca5c..247c8fb97ad 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -87,12 +87,12 @@ exports.getPluginTests = async (callback) => { const promises = plugins .map((plugin) => [plugin, moduleDir + plugin + specPath]) .filter(([plugin, specDir]) => fs.existsSync(specDir)) // check plugin exists - .map(([plugin, specDir]) => readdir(specDir) + .map(async ([plugin, specDir]) => await readdir(specDir) .then((specFiles) => specFiles.map((spec) => { pluginSpecs.push(staticDir + plugin + specPath + spec); }))); - return Promise.all(promises).then(() => pluginSpecs); + return await Promise.all(promises).then(() => pluginSpecs); }; -exports.getCoreTests = () => readdir('src/tests/frontend/specs'); +exports.getCoreTests = async () => await readdir('src/tests/frontend/specs'); From f00f9aa14cb87758e57faa9b45f8d3cbe03033ca Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 9 May 2021 16:30:53 -0400 Subject: [PATCH 134/218] tests: Avoid `.then()` inside `async` functions --- src/node/hooks/express/tests.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index 247c8fb97ad..c2557f79943 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -84,15 +84,16 @@ exports.getPluginTests = async (callback) => { const pluginSpecs = []; const plugins = await readdir(moduleDir); - const promises = plugins + await Promise.all(plugins .map((plugin) => [plugin, moduleDir + plugin + specPath]) .filter(([plugin, specDir]) => fs.existsSync(specDir)) // check plugin exists - .map(async ([plugin, specDir]) => await readdir(specDir) - .then((specFiles) => specFiles.map((spec) => { - pluginSpecs.push(staticDir + plugin + specPath + spec); - }))); - - return await Promise.all(promises).then(() => pluginSpecs); + .map(async ([plugin, specDir]) => { + const specFiles = await readdir(specDir); + return specFiles.map((spec) => { + pluginSpecs.push(staticDir + plugin + specPath + spec); + }); + })); + return pluginSpecs; }; exports.getCoreTests = async () => await readdir('src/tests/frontend/specs'); From 6cf27a713355ddd7ccd6f907b699e2fd14be3695 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 9 May 2021 16:33:09 -0400 Subject: [PATCH 135/218] tests: Use `fs.promises` instead of wrapping with `util.promisify` --- src/node/hooks/express/tests.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index c2557f79943..e5fc24c7cb8 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -2,7 +2,7 @@ const path = require('path'); const fs = require('fs'); -const util = require('util'); +const fsp = fs.promises; const settings = require('../../utils/Settings'); exports.expressCreateServer = (hookName, args, cb) => { @@ -74,8 +74,6 @@ exports.expressCreateServer = (hookName, args, cb) => { return cb(); }; -const readdir = util.promisify(fs.readdir); - exports.getPluginTests = async (callback) => { const moduleDir = 'node_modules/'; const specPath = '/static/tests/frontend/specs/'; @@ -83,12 +81,12 @@ exports.getPluginTests = async (callback) => { const pluginSpecs = []; - const plugins = await readdir(moduleDir); + const plugins = await fsp.readdir(moduleDir); await Promise.all(plugins .map((plugin) => [plugin, moduleDir + plugin + specPath]) .filter(([plugin, specDir]) => fs.existsSync(specDir)) // check plugin exists .map(async ([plugin, specDir]) => { - const specFiles = await readdir(specDir); + const specFiles = await fsp.readdir(specDir); return specFiles.map((spec) => { pluginSpecs.push(staticDir + plugin + specPath + spec); }); @@ -96,4 +94,4 @@ exports.getPluginTests = async (callback) => { return pluginSpecs; }; -exports.getCoreTests = async () => await readdir('src/tests/frontend/specs'); +exports.getCoreTests = async () => await fsp.readdir('src/tests/frontend/specs'); From 998e77ec25d8037638253c637be5635ff2248c57 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 9 May 2021 16:36:47 -0400 Subject: [PATCH 136/218] tests: Switch to promisified `readFile` --- src/node/hooks/express/tests.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index e5fc24c7cb8..e8e3108b9f1 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -46,20 +46,17 @@ exports.expressCreateServer = (hookName, args, cb) => { return filePath; }; - args.app.get('/tests/frontend/specs/*', (req, res) => { - const specFilePath = url2FilePath(req.url); - const specFileName = path.basename(specFilePath); - - fs.readFile(specFilePath, (err, content) => { - if (err) { return res.send(500); } - + args.app.get('/tests/frontend/specs/*', (req, res, next) => { + (async () => { + const specFilePath = url2FilePath(req.url); + const specFileName = path.basename(specFilePath); + let content = await fsp.readFile(specFilePath); content = `describe(${JSON.stringify(specFileName)}, function(){${content}});`; - if (!specFilePath.endsWith('index.html')) { res.setHeader('content-type', 'application/javascript'); } res.send(content); - }); + })().catch((err) => next(err || new Error(err))); }); args.app.get('/tests/frontend/*', (req, res) => { From e1c2c963f090d7338b6932e687d7fc46a8f0af75 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 8 May 2021 18:33:36 -0400 Subject: [PATCH 137/218] tests: URL decode test spec pathnames Express automatically URL decodes route parameters. --- src/node/hooks/express/tests.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index e8e3108b9f1..7c2795af215 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -30,15 +30,11 @@ exports.expressCreateServer = (hookName, args, cb) => { const rootTestFolder = path.join(settings.root, 'src/tests/frontend/'); - const url2FilePath = (url) => { - let subPath = url.substr('/tests/frontend'.length); + const sanitizePath = (subPath) => { if (subPath === '') { subPath = 'index.html'; } - subPath = subPath.split('?')[0]; - let filePath = 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'); @@ -46,9 +42,12 @@ exports.expressCreateServer = (hookName, args, cb) => { return filePath; }; - args.app.get('/tests/frontend/specs/*', (req, res, next) => { + // The regexp /[\d\D]{0,}/ is equivalent to the regexp /.*/. The Express route path used here + // uses the more verbose /[\d\D]{0,}/ pattern instead of /.*/ because path-to-regexp v0.1.7 (the + // version used with Express v4.x) interprets '.' and '*' differently than regexp. + args.app.get('/tests/frontend/specs/:file([\\d\\D]{0,})', (req, res, next) => { (async () => { - const specFilePath = url2FilePath(req.url); + const specFilePath = sanitizePath(`specs/${req.params.file}`); const specFileName = path.basename(specFilePath); let content = await fsp.readFile(specFilePath); content = `describe(${JSON.stringify(specFileName)}, function(){${content}});`; @@ -59,8 +58,8 @@ exports.expressCreateServer = (hookName, args, cb) => { })().catch((err) => next(err || new Error(err))); }); - args.app.get('/tests/frontend/*', (req, res) => { - const filePath = url2FilePath(req.url); + args.app.get('/tests/frontend/:file([\\d\\D]{0,})', (req, res) => { + const filePath = sanitizePath(req.params.file); res.sendFile(filePath); }); From 995e381243304a217f2f013ea90e091870943304 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 9 May 2021 17:23:02 -0400 Subject: [PATCH 138/218] tests: Only wrap `*.js` files in `describe()` --- src/node/hooks/express/tests.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index 7c2795af215..9e1e0287376 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -47,14 +47,14 @@ exports.expressCreateServer = (hookName, args, cb) => { // version used with Express v4.x) interprets '.' and '*' differently than regexp. args.app.get('/tests/frontend/specs/:file([\\d\\D]{0,})', (req, res, next) => { (async () => { - const specFilePath = sanitizePath(`specs/${req.params.file}`); - const specFileName = path.basename(specFilePath); - let content = await fsp.readFile(specFilePath); - content = `describe(${JSON.stringify(specFileName)}, function(){${content}});`; - if (!specFilePath.endsWith('index.html')) { + const file = sanitizePath(`specs/${req.params.file}`); + if (file.endsWith('.js')) { + const content = await fsp.readFile(file); res.setHeader('content-type', 'application/javascript'); + res.send(`describe(${JSON.stringify(path.basename(file))}, function () {\n${content}\n});`); + } else { + res.sendFile(file); } - res.send(content); })().catch((err) => next(err || new Error(err))); }); From ade17490e0361b9949dde0cf81578e4993063a9a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 9 May 2021 17:34:49 -0400 Subject: [PATCH 139/218] tests: Combine frontend test file handlers --- src/node/hooks/express/tests.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index 9e1e0287376..b3974722725 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -45,10 +45,10 @@ exports.expressCreateServer = (hookName, args, cb) => { // The regexp /[\d\D]{0,}/ is equivalent to the regexp /.*/. The Express route path used here // uses the more verbose /[\d\D]{0,}/ pattern instead of /.*/ because path-to-regexp v0.1.7 (the // version used with Express v4.x) interprets '.' and '*' differently than regexp. - args.app.get('/tests/frontend/specs/:file([\\d\\D]{0,})', (req, res, next) => { + args.app.get('/tests/frontend/:file([\\d\\D]{0,})', (req, res, next) => { (async () => { - const file = sanitizePath(`specs/${req.params.file}`); - if (file.endsWith('.js')) { + const file = sanitizePath(req.params.file); + if (req.params.file.startsWith('specs/') && file.endsWith('.js')) { const content = await fsp.readFile(file); res.setHeader('content-type', 'application/javascript'); res.send(`describe(${JSON.stringify(path.basename(file))}, function () {\n${content}\n});`); @@ -58,11 +58,6 @@ exports.expressCreateServer = (hookName, args, cb) => { })().catch((err) => next(err || new Error(err))); }); - args.app.get('/tests/frontend/:file([\\d\\D]{0,})', (req, res) => { - const filePath = sanitizePath(req.params.file); - res.sendFile(filePath); - }); - args.app.get('/tests/frontend', (req, res) => { res.redirect('/tests/frontend/index.html'); }); From b85a040f132e57cea0bda9eb86da82bab3ba4fbc Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 9 May 2021 17:36:18 -0400 Subject: [PATCH 140/218] tests: Reuse `sanitizePathname` when serving frontend specs --- src/node/hooks/express/tests.js | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index b3974722725..5b2436989b2 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -3,6 +3,7 @@ const path = require('path'); const fs = require('fs'); const fsp = fs.promises; +const sanitizePathname = require('../../utils/sanitizePathname'); const settings = require('../../utils/Settings'); exports.expressCreateServer = (hookName, args, cb) => { @@ -30,25 +31,15 @@ exports.expressCreateServer = (hookName, args, cb) => { const rootTestFolder = path.join(settings.root, 'src/tests/frontend/'); - const sanitizePath = (subPath) => { - if (subPath === '') { - subPath = 'index.html'; - } - let filePath = 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'); - } - return filePath; - }; - // The regexp /[\d\D]{0,}/ is equivalent to the regexp /.*/. The Express route path used here // uses the more verbose /[\d\D]{0,}/ pattern instead of /.*/ because path-to-regexp v0.1.7 (the // version used with Express v4.x) interprets '.' and '*' differently than regexp. args.app.get('/tests/frontend/:file([\\d\\D]{0,})', (req, res, next) => { (async () => { - const file = sanitizePath(req.params.file); - if (req.params.file.startsWith('specs/') && file.endsWith('.js')) { + let relFile = sanitizePathname(req.params.file); + if (['', '.', './'].includes(relFile)) relFile = 'index.html'; + const file = path.join(rootTestFolder, relFile); + if (relFile.startsWith('specs/') && file.endsWith('.js')) { const content = await fsp.readFile(file); res.setHeader('content-type', 'application/javascript'); res.send(`describe(${JSON.stringify(path.basename(file))}, function () {\n${content}\n});`); From 1be1b704f161b5627e681a62590c9ebd016a2b7c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 22 Apr 2021 01:10:19 -0400 Subject: [PATCH 141/218] tests: Simplify iteration over frontend test specs --- src/tests/frontend/runner.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index 7389c13ee00..31fb6d241b8 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -138,24 +138,15 @@ $(() => { const getURLParameter = (name) => (new URLSearchParams(location.search)).get(name); - // get the list of specs and filter it if requested - const specs = specs_list.slice(); - const absUrl = (url) => new URL(url, window.location.href).href; require.setRootURI(absUrl('../../javascripts/src')); require.setLibraryURI(absUrl('../../javascripts/lib')); require.setGlobalKeyPath('require'); - // inject spec scripts into the dom const $body = $('body'); - $.each(specs, (i, spec) => { - // if the spec isn't a plugin spec which means the spec file might be in a different subfolder - if (!spec.startsWith('/')) { - $body.append(``); - } else { - $body.append(``); - } - }); + for (const spec of specs_list.map((spec) => encodeURI(spec))) { + $body.append($(' - + diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index 31fb6d241b8..cdc218cad9f 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -1,6 +1,6 @@ 'use strict'; -/* global specs_list */ +/* global frontendTestSpecs */ $(() => { const stringifyException = (exception) => { @@ -144,7 +144,7 @@ $(() => { require.setGlobalKeyPath('require'); const $body = $('body'); - for (const spec of specs_list.map((spec) => encodeURI(spec))) { + for (const spec of frontendTestSpecs.map((spec) => encodeURI(spec))) { $body.append($(' - - - + + + + From 573da027e57d6d91ada15d065d146d4861d74d89 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 9 May 2021 17:46:16 -0400 Subject: [PATCH 156/218] tests: Preserve query string when redirecting --- src/node/hooks/express/tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index 4a732bc1c73..96503fd5cca 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -47,7 +47,7 @@ exports.expressCreateServer = (hookName, args, cb) => { }); args.app.get('/tests/frontend', (req, res) => { - res.redirect('./frontend/index.html'); + res.redirect(['./frontend/index.html', ...req.url.split('?').slice(1)].join('?')); }); return cb(); From 712b8c5769680c42b103ee5b9dd1876f1d9578aa Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 9 May 2021 17:47:08 -0400 Subject: [PATCH 157/218] tests: Redirect `/tests/frontend` to `/tests/frontend/` --- src/node/hooks/express/tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index 96503fd5cca..551ff8ed727 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -47,7 +47,7 @@ exports.expressCreateServer = (hookName, args, cb) => { }); args.app.get('/tests/frontend', (req, res) => { - res.redirect(['./frontend/index.html', ...req.url.split('?').slice(1)].join('?')); + res.redirect(['./frontend/', ...req.url.split('?').slice(1)].join('?')); }); return cb(); From 5d54c1657abd51ab58c2529c82650da405c8a30e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 8 May 2021 18:56:26 -0400 Subject: [PATCH 158/218] tests: Redirect `/tests/frontend/index.html` to `/tests/frontend/` --- src/node/hooks/express/tests.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index 551ff8ed727..617d385c015 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -28,6 +28,10 @@ exports.expressCreateServer = (hookName, args, cb) => { const rootTestFolder = path.join(settings.root, 'src/tests/frontend/'); + args.app.get('/tests/frontend/index.html', (req, res) => { + res.redirect(['./', ...req.url.split('?').slice(1)].join('?')); + }); + // The regexp /[\d\D]{0,}/ is equivalent to the regexp /.*/. The Express route path used here // uses the more verbose /[\d\D]{0,}/ pattern instead of /.*/ because path-to-regexp v0.1.7 (the // version used with Express v4.x) interprets '.' and '*' differently than regexp. From d8eb79428fa504a9a665cee8b8ad34614dec5ebb Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 8 May 2021 14:42:10 -0400 Subject: [PATCH 159/218] tests: Recurse under frontend spec dir --- src/node/hooks/express/tests.js | 37 +++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index 617d385c015..26fcaaaac7f 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -6,6 +6,29 @@ const plugins = require('../../../static/js/pluginfw/plugin_defs'); const sanitizePathname = require('../../utils/sanitizePathname'); const settings = require('../../utils/Settings'); +// Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/' +// instead of path.sep to separate pathname components. +const findSpecs = async (specDir) => { + let dirents; + try { + dirents = await fsp.readdir(specDir, {withFileTypes: true}); + } catch (err) { + if (['ENOENT', 'ENOTDIR'].includes(err.code)) return []; + throw err; + } + const specs = []; + await Promise.all(dirents.map(async (dirent) => { + if (dirent.isDirectory()) { + const subdirSpecs = await findSpecs(path.join(specDir, dirent.name)); + specs.push(...subdirSpecs.map((spec) => `${dirent.name}/${spec}`)); + return; + } + if (!dirent.name.endsWith('.js')) return; + specs.push(dirent.name); + })); + return specs; +}; + exports.expressCreateServer = (hookName, args, cb) => { args.app.get('/tests/frontend/frontendTestSpecs.js', async (req, res) => { const [coreTests, pluginTests] = await Promise.all([getCoreTests(), getPluginTests()]); @@ -13,9 +36,6 @@ exports.expressCreateServer = (hookName, args, cb) => { // merge the two sets of results let files = [].concat(coreTests, pluginTests).sort(); - // Keep only *.js files - files = files.filter((f) => f.endsWith('.js')); - // remove admin tests if the setting to enable them isn't in settings.json if (!settings.enableAdminUITests) { files = files.filter((file) => file.indexOf('admin') !== 0); @@ -62,15 +82,10 @@ const getPluginTests = async (callback) => { const specLists = await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => { if (plugin === 'ep_etherpad-lite') return []; const {package: {path: pluginPath}} = def; - try { - const specs = await fsp.readdir(path.join(pluginPath, specPath)); - return specs.map((spec) => `/static/plugins/${plugin}/${specPath}/${spec}`); - } catch (err) { - if (['ENOENT', 'ENOTDIR'].includes(err.code)) return []; - throw err; - } + const specs = await findSpecs(path.join(pluginPath, specPath)); + return specs.map((spec) => `/static/plugins/${plugin}/${specPath}/${spec}`); })); return [].concat(...specLists); }; -const getCoreTests = async () => await fsp.readdir('src/tests/frontend/specs'); +const getCoreTests = async () => await findSpecs('src/tests/frontend/specs'); From e4f011df760b2eb6cd1142a14a4dbd17c4eed2b6 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 8 May 2021 19:28:08 -0400 Subject: [PATCH 160/218] tests: Use `require()` to load frontend test specs This makes core and plugin tests consistent with each other, makes it possible to `require()` relative paths in spec files, simplifies the code somewhat, and should make it easier to move away from require-kernel. Also: * Wrap plugin tests inside a `describe()` that contains the plugin name to make it easier to grep for a plugin's tests and for consistency with core tests. * Add "" to the core test descriptions to make it easier to distinguish them from plugin tests. --- src/node/hooks/express/tests.js | 60 ++++++++++++++------------------- src/node/utils/Minify.js | 2 ++ src/tests/frontend/runner.js | 14 ++++++-- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index 26fcaaaac7f..c0f2fcdb62c 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -31,19 +31,29 @@ const findSpecs = async (specDir) => { exports.expressCreateServer = (hookName, args, cb) => { args.app.get('/tests/frontend/frontendTestSpecs.js', async (req, res) => { - const [coreTests, pluginTests] = await Promise.all([getCoreTests(), getPluginTests()]); - - // merge the two sets of results - let files = [].concat(coreTests, pluginTests).sort(); - - // remove admin tests if the setting to enable them isn't in settings.json - if (!settings.enableAdminUITests) { - files = files.filter((file) => file.indexOf('admin') !== 0); - } - - console.debug('Sent browser the following test specs:', files); + const modules = []; + await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => { + let {package: {path: pluginPath}} = def; + if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep; + const specDir = `${plugin === 'ep_etherpad-lite' ? '' : 'static/'}tests/frontend/specs`; + for (const spec of await findSpecs(path.join(pluginPath, specDir))) { + if (plugin === 'ep_etherpad-lite' && !settings.enableAdminUITests && + spec.startsWith('admin')) continue; + modules.push(`${plugin}/${specDir}/${spec.replace(/\.js$/, '')}`); + } + })); + // Sort plugin tests before core tests. + modules.sort((a, b) => { + a = String(a); + b = String(b); + const aCore = a.startsWith('ep_etherpad-lite/'); + const bCore = b.startsWith('ep_etherpad-lite/'); + if (aCore === bCore) return a.localeCompare(b); + return aCore ? 1 : -1; + }); + console.debug('Sent browser the following test spec modules:', modules); res.setHeader('content-type', 'application/javascript'); - res.end(`window.frontendTestSpecs = ${JSON.stringify(files, null, 2)};\n`); + res.end(`window.frontendTestSpecs = ${JSON.stringify(modules, null, 2)};\n`); }); const rootTestFolder = path.join(settings.root, 'src/tests/frontend/'); @@ -57,16 +67,9 @@ exports.expressCreateServer = (hookName, args, cb) => { // version used with Express v4.x) interprets '.' and '*' differently than regexp. args.app.get('/tests/frontend/:file([\\d\\D]{0,})', (req, res, next) => { (async () => { - let relFile = sanitizePathname(req.params.file); - if (['', '.', './'].includes(relFile)) relFile = 'index.html'; - const file = path.join(rootTestFolder, relFile); - if (relFile.startsWith('specs/') && file.endsWith('.js')) { - const content = await fsp.readFile(file); - res.setHeader('content-type', 'application/javascript'); - res.send(`describe(${JSON.stringify(path.basename(file))}, function () {\n${content}\n});`); - } else { - res.sendFile(file); - } + let file = sanitizePathname(req.params.file); + if (['', '.', './'].includes(file)) file = 'index.html'; + res.sendFile(path.join(rootTestFolder, file)); })().catch((err) => next(err || new Error(err))); }); @@ -76,16 +79,3 @@ exports.expressCreateServer = (hookName, args, cb) => { return cb(); }; - -const getPluginTests = async (callback) => { - const specPath = 'static/tests/frontend/specs'; - const specLists = await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => { - if (plugin === 'ep_etherpad-lite') return []; - const {package: {path: pluginPath}} = def; - const specs = await findSpecs(path.join(pluginPath, specPath)); - return specs.map((spec) => `/static/plugins/${plugin}/${specPath}/${spec}`); - })); - return [].concat(...specLists); -}; - -const getCoreTests = async () => await findSpecs('src/tests/frontend/specs'); diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index 02728824dc5..ed107af7e7e 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -165,6 +165,8 @@ const minify = async (req, res) => { filename = path.join('../node_modules/', library, libraryPath); } } + const [, spec] = /^plugins\/ep_etherpad-lite\/(tests\/frontend\/specs\/.*)/.exec(filename) || []; + if (spec != null) filename = `../${spec}`; const contentType = mime.lookup(filename); diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index d8c54210790..ebb956a4421 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -141,9 +141,17 @@ $(() => { require.setLibraryURI(absUrl('../../javascripts/lib')); require.setGlobalKeyPath('require'); - const $body = $('body'); - for (const spec of window.frontendTestSpecs.map((spec) => encodeURI(spec))) { - $body.append($(' - - diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index c6d2d907200..3100b13dfe6 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -149,7 +149,7 @@ $(() => (async () => { // asynchronous form of require()). In addition, the performance gains would be minimal because // require-kernel only loads 2 at a time by default. (Increasing the default could cause problems // because browsers like to limit the number of concurrent fetches.) - for (const spec of window.frontendTestSpecs) { + for (const spec of await $.getJSON('frontendTestSpecs.json')) { const desc = spec .replace(/^ep_etherpad-lite\/tests\/frontend\/specs\//, ' ') .replace(/^([^/ ]*)\/static\/tests\/frontend\/specs\//, '<$1> '); From 76634eb6ff2f488f96a9a814c11dd406ef0001dc Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 5 Jun 2021 03:07:59 -0400 Subject: [PATCH 164/218] tests: Add missing `` and `` tags --- src/tests/frontend/index.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tests/frontend/index.html b/src/tests/frontend/index.html index 178ba60306f..bd8e76606c0 100644 --- a/src/tests/frontend/index.html +++ b/src/tests/frontend/index.html @@ -1,10 +1,12 @@ + Frontend tests - + +
@@ -24,4 +26,5 @@ + From e9f08bdd119b094fd1fd6dfd7eb147abcdbcc826 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 5 Jun 2021 03:11:10 -0400 Subject: [PATCH 165/218] tests: Fix frontend test CSS selectors --- src/tests/frontend/runner.css | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/tests/frontend/runner.css b/src/tests/frontend/runner.css index b35918028fd..2ae0d58dea7 100644 --- a/src/tests/frontend/runner.css +++ b/src/tests/frontend/runner.css @@ -34,10 +34,9 @@ body { overflow: auto; width:20%; font-size:80%; - } -#mocha #report { +#mocha-report { margin: 0; padding: 0; margin-top: 10px; @@ -170,26 +169,26 @@ body { -webkit-box-shadow: 0 1px 3px #eee; } -#report ul { +#mocha-report ul { padding: 0; } -#report.pass .test.fail { +#mocha-report.pass .test.fail { display: none; } -#report.fail .test.pass { +#mocha-report.fail .test.pass { display: none; } -#error { +#mocha-error { color: #c00; font-size: 1.5 em; font-weight: 100; letter-spacing: 1px; } -#stats { +#mocha-stats { padding: 10px; font-size: 12px; margin: 0; @@ -207,20 +206,20 @@ body { margin-right:5px; } -#stats em { +#mocha-stats em { color: black; } -#stats a { +#mocha-stats a { text-decoration: none; color: inherit; } -#stats a:hover { +#mocha-stats a:hover { border-bottom: 1px solid #eee; } -#stats li { +#mocha-stats li { display: inline-block; margin: 0 5px; list-style: none; From b09b895ac78826c277f4cebab49b8393ca77c7ce Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 5 Jun 2021 03:14:40 -0400 Subject: [PATCH 166/218] tests: Remove border around iframe --- src/tests/frontend/runner.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/frontend/runner.css b/src/tests/frontend/runner.css index 2ae0d58dea7..72f1e0475d9 100644 --- a/src/tests/frontend/runner.css +++ b/src/tests/frontend/runner.css @@ -22,6 +22,7 @@ body { } #iframe-container iframe { + border: 0; height: 100%; width:100%; } From 960c2c0c0dacf60cdb507a864979acbc63997ac2 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 5 Jun 2021 03:16:33 -0400 Subject: [PATCH 167/218] tests: Tweak mocha report spacing --- src/tests/frontend/runner.css | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/tests/frontend/runner.css b/src/tests/frontend/runner.css index 72f1e0475d9..4e7202d7c28 100644 --- a/src/tests/frontend/runner.css +++ b/src/tests/frontend/runner.css @@ -37,17 +37,6 @@ body { font-size:80%; } -#mocha-report { - margin: 0; - padding: 0; - margin-top: 10px; -} - -#mocha li { - margin: 0; - padding: 0; -} - #mocha ul { list-style: none; } @@ -197,10 +186,6 @@ body { text-align: right; } -#mocha-stats { - height: 80px; -} - #mocha-stats .progress { float: right; padding-top: 0; From fc3b81172628d080a2988b5f4f8bd8bc4d914ed1 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 5 Jun 2021 03:18:06 -0400 Subject: [PATCH 168/218] tests: Move iframe min width to iframe selector --- src/tests/frontend/runner.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/frontend/runner.css b/src/tests/frontend/runner.css index 4e7202d7c28..946bbe53779 100644 --- a/src/tests/frontend/runner.css +++ b/src/tests/frontend/runner.css @@ -17,14 +17,14 @@ body { #iframe-container { width: 80%; - min-width: 820px; height: 100%; } #iframe-container iframe { border: 0; height: 100%; - width:100%; + width: 100%; + min-width: 820px; } #mocha { From c4239b605997aff1d71ae12643790b97973bd3ac Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 5 Jun 2021 03:19:58 -0400 Subject: [PATCH 169/218] tests: Show a scrollbar if the pad is too wide --- src/tests/frontend/runner.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/frontend/runner.css b/src/tests/frontend/runner.css index 946bbe53779..8af5440ff93 100644 --- a/src/tests/frontend/runner.css +++ b/src/tests/frontend/runner.css @@ -18,6 +18,7 @@ body { #iframe-container { width: 80%; height: 100%; + overflow: auto hidden; } #iframe-container iframe { From 45ca82fd9fcf74d7b9d749e5bf638fecbd58db20 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 5 Jun 2021 03:34:55 -0400 Subject: [PATCH 170/218] tests: Make the Mocha results area resizable --- src/node/utils/Minify.js | 1 + src/package-lock.json | 5 +++++ src/package.json | 1 + src/tests/frontend/index.html | 7 +++++-- src/tests/frontend/runner.css | 20 ++++++++++++++------ src/tests/frontend/runner.js | 8 ++++++++ 6 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index ed107af7e7e..58dba2097a1 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -41,6 +41,7 @@ const LIBRARY_WHITELIST = [ 'async', 'js-cookie', 'security', + 'split-grid', 'tinycon', 'underscore', 'unorm', diff --git a/src/package-lock.json b/src/package-lock.json index 2e5bce2ce9d..e5c722fe646 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -7933,6 +7933,11 @@ "memory-pager": "^1.0.2" } }, + "split-grid": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/split-grid/-/split-grid-1.0.11.tgz", + "integrity": "sha512-ELtFtxc3r5we5GZfe6Fi0BFFxIi2M6BY1YEntBscKRDD3zx4JVHqx2VnTRSQu1BixCYSTH3MTjKd4esI2R7EgQ==" + }, "split2": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", diff --git a/src/package.json b/src/package.json index 7d70e776b38..de84b7ee068 100644 --- a/src/package.json +++ b/src/package.json @@ -65,6 +65,7 @@ "security": "1.0.0", "semver": "5.7.1", "socket.io": "^2.4.1", + "split-grid": "^1.0.11", "terser": "^4.7.0", "threads": "^1.4.0", "tiny-worker": "^2.3.0", diff --git a/src/tests/frontend/index.html b/src/tests/frontend/index.html index bd8e76606c0..2b70ca667c5 100644 --- a/src/tests/frontend/index.html +++ b/src/tests/frontend/index.html @@ -8,8 +8,11 @@
-
-
+
+
+
+
+
diff --git a/src/tests/frontend/runner.css b/src/tests/frontend/runner.css index 8af5440ff93..b751d414ad6 100644 --- a/src/tests/frontend/runner.css +++ b/src/tests/frontend/runner.css @@ -6,8 +6,6 @@ body { padding: 0px; margin: 0px; height: 100%; - display: flex; - flex-direction: row; overflow: hidden; } @@ -15,8 +13,21 @@ body { display: none; } +#split-view { + width: 100%; + height: 100%; + display: grid; + grid-template-columns: 20% 10px 1fr; +} + +#separator { + grid-column: 2; + grid-row: 1/-1; + cursor: col-resize; + background-color: #999; +} + #iframe-container { - width: 80%; height: 100%; overflow: auto hidden; } @@ -30,11 +41,8 @@ body { #mocha { font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; - border-right: 2px solid #999; - flex: 1 auto; height: 100%; overflow: auto; - width:20%; font-size:80%; } diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index 3100b13dfe6..621f1d2e2e5 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -143,6 +143,14 @@ $(() => (async () => { require.setLibraryURI(absUrl('../../javascripts/lib')); require.setGlobalKeyPath('require'); + const Split = require('split-grid/dist/split-grid.min'); + new Split({ + columnGutters: [{ + track: 1, + element: document.getElementById('separator'), + }], + }); + // This loads the test specs serially. While it is technically possible to load them in parallel, // the code would be very complex (it involves wrapping require.define(), configuring // require-kernel to use the wrapped .define() via require.setGlobalKeyPath(), and using the From 299dbbe7e67dd642d748cbd448b4cfb7ef561cd2 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 6 Jun 2021 06:42:29 -0400 Subject: [PATCH 171/218] tests: Move split-grid to dev dependencies This is only used for testing. --- src/package-lock.json | 3 ++- src/package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index e5c722fe646..4fb2f314c36 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -7936,7 +7936,8 @@ "split-grid": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/split-grid/-/split-grid-1.0.11.tgz", - "integrity": "sha512-ELtFtxc3r5we5GZfe6Fi0BFFxIi2M6BY1YEntBscKRDD3zx4JVHqx2VnTRSQu1BixCYSTH3MTjKd4esI2R7EgQ==" + "integrity": "sha512-ELtFtxc3r5we5GZfe6Fi0BFFxIi2M6BY1YEntBscKRDD3zx4JVHqx2VnTRSQu1BixCYSTH3MTjKd4esI2R7EgQ==", + "dev": true }, "split2": { "version": "3.2.2", diff --git a/src/package.json b/src/package.json index de84b7ee068..92fbe868a8b 100644 --- a/src/package.json +++ b/src/package.json @@ -65,7 +65,6 @@ "security": "1.0.0", "semver": "5.7.1", "socket.io": "^2.4.1", - "split-grid": "^1.0.11", "terser": "^4.7.0", "threads": "^1.4.0", "tiny-worker": "^2.3.0", @@ -94,6 +93,7 @@ "openapi-schema-validation": "^0.4.2", "set-cookie-parser": "^2.4.6", "sinon": "^9.2.0", + "split-grid": "^1.0.11", "superagent": "^3.8.3", "supertest": "4.0.2", "wd": "1.12.1" From c7bb18c6dafe0eba0c14ed77ab2b9371bc4c1c3e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 6 Jun 2021 02:14:50 -0400 Subject: [PATCH 172/218] Settings: Support null and undefined env var substitutions --- CHANGELOG.md | 13 +++++++++++++ settings.json.docker | 23 +++++++++++++++++++++++ settings.json.template | 23 +++++++++++++++++++++++ src/node/utils/Settings.js | 16 ++++++---------- 4 files changed, 65 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7931773fec6..5c50a3f6385 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,19 @@ connections to the MySQL/MariaDB server (by default) instead of 1. This might cause Etherpad to crash with a "ER_CON_COUNT_ERROR: Too many connections" error if your server is configured with a low connection limit. +* Changes to environment variable substitution in `settings.json` (see the + documentation comments in `settings.json.template` for details): + * An environment variable set to the string "null" now becomes `null` instead + of the string "null". Similarly, if the environment variable is unset and + the default value is "null" (e.g., `"${UNSET_VAR:null}"`), the value now + becomes `null` instead of the string "null". It is no longer possible to + produce the string "null" via environment variable substitution. + * An environment variable set to the string "undefined" now causes the setting + to be removed instead of set to the string "undefined". Similarly, if the + environment variable is unset and the default value is "undefined" (e.g., + `"${UNSET_VAR:undefined}"`), the setting is now removed instead of set to + the string "undefined". It is no longer possible to produce the string + "undefined" via environment variable substitution. ### Notable enhancements diff --git a/settings.json.docker b/settings.json.docker index 030ab88f11d..9c99975cd41 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -24,6 +24,29 @@ * * This is useful, for example, when running in a Docker container. * + * DETAILED RULES: + * - If the environment variable is set to the string "true" or "false", the + * value becomes Boolean true or false. + * - If the environment variable is set to the string "null", the value + * becomes null. + * - If the environment variable is set to the string "undefined", the setting + * is removed entirely, except when used as the member of an array in which + * case it becomes null. + * - If the environment variable is set to a string representation of a finite + * number, the string is converted to that number. + * - If the environment variable is set to any other string, including the + * empty string, the value is that string. + * - If the environment variable is unset and a default value is provided, the + * value is as if the environment variable was set to the provided default: + * - "${UNSET_VAR:}" becomes the empty string. + * - "${UNSET_VAR:foo}" becomes the string "foo". + * - "${UNSET_VAR:true}" and "${UNSET_VAR:false}" become true and false. + * - "${UNSET_VAR:null}" becomes null. + * - "${UNSET_VAR:undefined}" causes the setting to be removed (or be set + * to null, if used as a member of an array). + * - If the environment variable is unset and no default value is provided, + * the value becomes null. + * * EXAMPLE: * "port": "${PORT:9001}" * "minify": "${MINIFY}" diff --git a/settings.json.template b/settings.json.template index 90a89970c2c..3469cc7a46e 100644 --- a/settings.json.template +++ b/settings.json.template @@ -15,6 +15,29 @@ * * This is useful, for example, when running in a Docker container. * + * DETAILED RULES: + * - If the environment variable is set to the string "true" or "false", the + * value becomes Boolean true or false. + * - If the environment variable is set to the string "null", the value + * becomes null. + * - If the environment variable is set to the string "undefined", the setting + * is removed entirely, except when used as the member of an array in which + * case it becomes null. + * - If the environment variable is set to a string representation of a finite + * number, the string is converted to that number. + * - If the environment variable is set to any other string, including the + * empty string, the value is that string. + * - If the environment variable is unset and a default value is provided, the + * value is as if the environment variable was set to the provided default: + * - "${UNSET_VAR:}" becomes the empty string. + * - "${UNSET_VAR:foo}" becomes the string "foo". + * - "${UNSET_VAR:true}" and "${UNSET_VAR:false}" become true and false. + * - "${UNSET_VAR:null}" becomes null. + * - "${UNSET_VAR:undefined}" causes the setting to be removed (or be set + * to null, if used as a member of an array). + * - If the environment variable is unset and no default value is provided, + * the value becomes null. + * * EXAMPLE: * "port": "${PORT:9001}" * "minify": "${MINIFY}" diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index e5b1636db00..27630a04690 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -509,17 +509,13 @@ const coerceValue = (stringValue) => { return +stringValue; } - // the boolean literal case is easy. - if (stringValue === 'true') { - return true; + switch (stringValue) { + case 'true': return true; + case 'false': return false; + case 'undefined': return undefined; + case 'null': return null; + default: return stringValue; } - - if (stringValue === 'false') { - return false; - } - - // otherwise, return this value as-is - return stringValue; }; /** From aa221698c8d641d41d5ff27e5889f028702e505c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 6 Jun 2021 02:18:43 -0400 Subject: [PATCH 173/218] Docker: Explicitly default env var substitutions to null --- settings.json.docker | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/settings.json.docker b/settings.json.docker index 9c99975cd41..80becd3be09 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -108,7 +108,7 @@ * is used. If this is a relative path it is interpreted as relative to the * Etherpad root directory. */ - "favicon": "${FAVICON}", + "favicon": "${FAVICON:null}", /* * Skin name. @@ -205,12 +205,12 @@ "dbType": "${DB_TYPE:dirty}", "dbSettings": { - "host": "${DB_HOST}", - "port": "${DB_PORT}", - "database": "${DB_NAME}", - "user": "${DB_USER}", - "password": "${DB_PASS}", - "charset": "${DB_CHARSET}", + "host": "${DB_HOST:null}", + "port": "${DB_PORT:null}", + "database": "${DB_NAME:null}", + "user": "${DB_USER:null}", + "password": "${DB_PASS:null}", + "charset": "${DB_CHARSET:null}", "filename": "${DB_FILENAME:var/dirty.db}" }, @@ -308,7 +308,7 @@ * it to null disables Abiword and will only allow plain text and HTML * import/exports. */ - "abiword": "${ABIWORD}", + "abiword": "${ABIWORD:null}", /* * This is the absolute path to the soffice executable. @@ -316,7 +316,7 @@ * LibreOffice can be used in lieu of Abiword to export pads. * Setting it to null disables LibreOffice exporting. */ - "soffice": "${SOFFICE}", + "soffice": "${SOFFICE:null}", /* * Path to the Tidy executable. @@ -324,7 +324,7 @@ * Tidy is used to improve the quality of exported pads. * Setting it to null disables Tidy. */ - "tidyHtml": "${TIDY_HTML}", + "tidyHtml": "${TIDY_HTML:null}", /* * Allow import of file types other than the supported ones: @@ -454,13 +454,13 @@ "admin": { // 1) "password" can be replaced with "hash" if you install ep_hash_auth // 2) please note that if password is null, the user will not be created - "password": "${ADMIN_PASSWORD}", + "password": "${ADMIN_PASSWORD:null}", "is_admin": true }, "user": { // 1) "password" can be replaced with "hash" if you install ep_hash_auth // 2) please note that if password is null, the user will not be created - "password": "${USER_PASSWORD}", + "password": "${USER_PASSWORD:null}", "is_admin": false } }, From 428f8d1684c7e14262b4971887d5aa774b157980 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 6 Jun 2021 02:36:09 -0400 Subject: [PATCH 174/218] Settings: Deprecate null as the default default value --- CHANGELOG.md | 3 +++ settings.json.docker | 4 +++- settings.json.template | 4 +++- src/node/utils/Settings.js | 4 +++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c50a3f6385..5959858fba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ `"${UNSET_VAR:undefined}"`), the setting is now removed instead of set to the string "undefined". It is no longer possible to produce the string "undefined" via environment variable substitution. + * Support for unset variables without a default value is now deprecated. + Please change all instances of `"${FOO}"` in your `settings.json` to + `${FOO:null}` to keep the current behavior. ### Notable enhancements diff --git a/settings.json.docker b/settings.json.docker index 80becd3be09..426a5895334 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -45,7 +45,9 @@ * - "${UNSET_VAR:undefined}" causes the setting to be removed (or be set * to null, if used as a member of an array). * - If the environment variable is unset and no default value is provided, - * the value becomes null. + * the value becomes null. THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF + * ETHERPAD; if you want the default value to be null, you should explicitly + * specify "null" as the default value. * * EXAMPLE: * "port": "${PORT:9001}" diff --git a/settings.json.template b/settings.json.template index 3469cc7a46e..2d7f119a227 100644 --- a/settings.json.template +++ b/settings.json.template @@ -36,7 +36,9 @@ * - "${UNSET_VAR:undefined}" causes the setting to be removed (or be set * to null, if used as a member of an array). * - If the environment variable is unset and no default value is provided, - * the value becomes null. + * the value becomes null. THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF + * ETHERPAD; if you want the default value to be null, you should explicitly + * specify "null" as the default value. * * EXAMPLE: * "port": "${PORT:9001}" diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 27630a04690..482abd730fe 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -599,7 +599,9 @@ const lookupEnvironmentVariables = (obj) => { if ((envVarValue === undefined) && (defaultValue === undefined)) { console.warn(`Environment variable "${envVarName}" does not contain any value for ` + - `configuration key "${key}", and no default was given. Returning null.`); + `configuration key "${key}", and no default was given. Using null. ` + + 'THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF ETHERPAD; you should ' + + 'explicitly use "null" as the default if you want to continue to use null.'); /* * We have to return null, because if we just returned undefined, the From de0a450aec9af53d047778ac847384c432d6745d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 6 Jun 2021 03:28:47 -0400 Subject: [PATCH 175/218] Docker: If `DB_*` env var is unset, remove the corresponding setting --- CHANGELOG.md | 2 ++ settings.json.docker | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5959858fba7..a2e3184df73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ * Support for unset variables without a default value is now deprecated. Please change all instances of `"${FOO}"` in your `settings.json` to `${FOO:null}` to keep the current behavior. + * The `DB_*` variable substitutions in `settings.json.docker` that previously + defaulted to `null` now default to "undefined". ### Notable enhancements diff --git a/settings.json.docker b/settings.json.docker index 426a5895334..d9373f5eb70 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -207,12 +207,12 @@ "dbType": "${DB_TYPE:dirty}", "dbSettings": { - "host": "${DB_HOST:null}", - "port": "${DB_PORT:null}", - "database": "${DB_NAME:null}", - "user": "${DB_USER:null}", - "password": "${DB_PASS:null}", - "charset": "${DB_CHARSET:null}", + "host": "${DB_HOST:undefined}", + "port": "${DB_PORT:undefined}", + "database": "${DB_NAME:undefined}", + "user": "${DB_USER:undefined}", + "password": "${DB_PASS:undefined}", + "charset": "${DB_CHARSET:undefined}", "filename": "${DB_FILENAME:var/dirty.db}" }, From 6c2f31a5cb4d3add6c9c286073befc2ba2a93788 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 6 Jun 2021 05:26:52 -0400 Subject: [PATCH 176/218] tests: Add tests for `settings.json` parsing --- src/node/utils/Settings.js | 4 ++ src/tests/backend/specs/settings.js | 61 +++++++++++++++++++++++++++ src/tests/backend/specs/settings.json | 39 +++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 src/tests/backend/specs/settings.js create mode 100644 src/tests/backend/specs/settings.json diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 482abd730fe..40576c3459c 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -825,5 +825,9 @@ exports.reloadSettings = () => { console.log(`Random string used for versioning assets: ${exports.randomVersionString}`); }; +exports.exportedForTestingOnly = { + parseSettings, +}; + // initially load settings exports.reloadSettings(); diff --git a/src/tests/backend/specs/settings.js b/src/tests/backend/specs/settings.js new file mode 100644 index 00000000000..e737f4f3445 --- /dev/null +++ b/src/tests/backend/specs/settings.js @@ -0,0 +1,61 @@ +'use strict'; + +const assert = require('assert').strict; +const {parseSettings} = require('../../../node/utils/Settings').exportedForTestingOnly; +const path = require('path'); +const process = require('process'); + +describe(__filename, function () { + describe('parseSettings', function () { + let settings; + const envVarSubstTestCases = [ + {name: 'true', val: 'true', var: 'SET_VAR_TRUE', want: true}, + {name: 'false', val: 'false', var: 'SET_VAR_FALSE', want: false}, + {name: 'null', val: 'null', var: 'SET_VAR_NULL', want: null}, + {name: 'undefined', val: 'undefined', var: 'SET_VAR_UNDEFINED', want: undefined}, + {name: 'number', val: '123', var: 'SET_VAR_NUMBER', want: 123}, + {name: 'string', val: 'foo', var: 'SET_VAR_STRING', want: 'foo'}, + {name: 'empty string', val: '', var: 'SET_VAR_EMPTY_STRING', want: ''}, + ]; + + before(async function () { + for (const tc of envVarSubstTestCases) process.env[tc.var] = tc.val; + delete process.env.UNSET_VAR; + settings = parseSettings(path.join(__dirname, 'settings.json'), true); + assert(settings != null); + }); + + describe('environment variable substitution', function () { + describe('set', function () { + for (const tc of envVarSubstTestCases) { + it(tc.name, async function () { + const obj = settings['environment variable substitution'].set; + if (tc.name === 'undefined') { + assert(!(tc.name in obj)); + } else { + assert.equal(obj[tc.name], tc.want); + } + }); + } + }); + + describe('unset', function () { + it('no default', async function () { + const obj = settings['environment variable substitution'].unset; + assert.equal(obj['no default'], null); + }); + + for (const tc of envVarSubstTestCases) { + it(tc.name, async function () { + const obj = settings['environment variable substitution'].unset; + if (tc.name === 'undefined') { + assert(!(tc.name in obj)); + } else { + assert.equal(obj[tc.name], tc.want); + } + }); + } + }); + }); + }); +}); diff --git a/src/tests/backend/specs/settings.json b/src/tests/backend/specs/settings.json new file mode 100644 index 00000000000..12b4748c097 --- /dev/null +++ b/src/tests/backend/specs/settings.json @@ -0,0 +1,39 @@ +// line comment +/* + * block comment + */ +{ + "trailing commas": { + "lists": { + "multiple lines": [ + "", + ] + }, + "objects": { + "multiple lines": { + "key": "", + } + } + }, + "environment variable substitution": { + "set": { + "true": "${SET_VAR_TRUE}", + "false": "${SET_VAR_FALSE}", + "null": "${SET_VAR_NULL}", + "undefined": "${SET_VAR_UNDEFINED}", + "number": "${SET_VAR_NUMBER}", + "string": "${SET_VAR_STRING}", + "empty string": "${SET_VAR_EMPTY_STRING}" + }, + "unset": { + "no default": "${UNSET_VAR}", + "true": "${UNSET_VAR:true}", + "false": "${UNSET_VAR:false}", + "null": "${UNSET_VAR:null}", + "undefined": "${UNSET_VAR:undefined}", + "number": "${UNSET_VAR:123}", + "string": "${UNSET_VAR:foo}", + "empty string": "${UNSET_VAR:}" + } + } +} From 1756415495aa514f11f66560bf3ebf5203844467 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 6 Jun 2021 06:15:35 -0400 Subject: [PATCH 177/218] tests: Avoid jQuery when reading Mocha output --- src/tests/frontend/travis/remote_runner.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 1e545372085..80b44f679a5 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -50,9 +50,17 @@ const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { const deadline = Date.now() + 14.5 * 60 * 1000; // Slightly less than overall test timeout. // how many characters of the log have been sent to travis let logIndex = 0; + const remoteFn = (skipChars) => { + const console = document.getElementById('console'); // eslint-disable-line no-undef + if (console == null) return ''; + let text = ''; + for (const n of console.childNodes) { + if (n.nodeType === n.TEXT_NODE) text += n.data; + } + return text.substring(skipChars); + }; while (true) { - const remoteFn = ($, skipChars) => $('#console').text().substring(skipChars); - const consoleText = await browser.eval(`(${remoteFn})($, ${JSON.stringify(logIndex)})`); + const consoleText = await browser.eval(`(${remoteFn})(${JSON.stringify(logIndex)})`); (consoleText ? consoleText.split('\n') : []).forEach((line) => log(line, pfx)); logIndex += consoleText.length; const [finished, nFailedStr] = consoleText.match(finishedRegex) || []; From 752e2488af938dab053a85d064d53d310f576e25 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 6 Jun 2021 16:23:56 -0400 Subject: [PATCH 178/218] tests: Migrate from `wd` to `selenium-webdriver` --- src/package-lock.json | 463 +++++---------------- src/package.json | 4 +- src/tests/frontend/travis/remote_runner.js | 72 ++-- 3 files changed, 144 insertions(+), 395 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 4fb2f314c36..4ae45a3be4d 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -452,88 +452,6 @@ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "optional": true }, - "archiver": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz", - "integrity": "sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg==", - "dev": true, - "requires": { - "archiver-utils": "^2.1.0", - "async": "^2.6.3", - "buffer-crc32": "^0.2.1", - "glob": "^7.1.4", - "readable-stream": "^3.4.0", - "tar-stream": "^2.1.0", - "zip-stream": "^2.1.2" - }, - "dependencies": { - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - } - } - }, - "archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dev": true, - "requires": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "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" - } - } - } - }, "are-we-there-yet": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", @@ -866,12 +784,6 @@ "ieee754": "^1.1.13" } }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true - }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -1143,56 +1055,6 @@ "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" }, - "compress-commons": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz", - "integrity": "sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q==", - "dev": true, - "requires": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^3.0.1", - "normalize-path": "^3.0.0", - "readable-stream": "^2.3.6" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "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" - } - } - } - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1254,25 +1116,6 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, - "crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, - "requires": { - "buffer": "^5.1.0" - } - }, - "crc32-stream": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-3.0.1.tgz", - "integrity": "sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==", - "dev": true, - "requires": { - "crc": "^3.4.4", - "readable-stream": "^3.4.0" - } - }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1505,15 +1348,6 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, "engine.io": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz", @@ -2281,12 +2115,6 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, "fs-minipass": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", @@ -2413,7 +2241,8 @@ "graceful-fs": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "optional": true }, "growl": { "version": "1.10.5", @@ -2645,6 +2474,12 @@ "minimatch": "^3.0.4" } }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "dev": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2967,49 +2802,16 @@ "verror": "1.10.0" } }, - "just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", - "dev": true - }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "kebab-case": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.1.tgz", - "integrity": "sha512-txPHx6nVLhv8PHGXIlAk0nYoh894SpAqGPXNvbg2hh8spvHXIah3+vT87DLoa59nKgC6scD3u3xAuRIgiMqbfQ==", - "dev": true - }, - "languages4translatewiki": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/languages4translatewiki/-/languages4translatewiki-0.1.3.tgz", - "integrity": "sha1-xDYgbgUtIUkLEQF6RNURj5Ih5ds=" - }, - "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "jszip": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz", + "integrity": "sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==", "dev": true, "requires": { - "readable-stream": "^2.0.5" + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" }, "dependencies": { "isarray": { @@ -3050,6 +2852,42 @@ } } }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "kebab-case": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.1.tgz", + "integrity": "sha512-txPHx6nVLhv8PHGXIlAk0nYoh894SpAqGPXNvbg2hh8spvHXIah3+vT87DLoa59nKgC6scD3u3xAuRIgiMqbfQ==", + "dev": true + }, + "languages4translatewiki": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/languages4translatewiki/-/languages4translatewiki-0.1.3.tgz", + "integrity": "sha1-xDYgbgUtIUkLEQF6RNURj5Ih5ds=" + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3060,6 +2898,15 @@ "type-check": "~0.4.0" } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "requires": { + "immediate": "~3.0.5" + } + }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -3095,12 +2942,6 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" }, - "lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", - "dev": true - }, "lodash.filter": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", @@ -3122,12 +2963,6 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", - "dev": true - }, "lodash.map": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", @@ -3164,12 +2999,6 @@ "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", "dev": true }, - "lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=", - "dev": true - }, "log-symbols": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", @@ -7101,6 +6930,12 @@ "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7296,12 +7131,6 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true - }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -7555,7 +7384,6 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "optional": true, "requires": { "glob": "^7.1.3" } @@ -7589,6 +7417,18 @@ "resolved": "https://registry.npmjs.org/security/-/security-1.0.0.tgz", "integrity": "sha1-gRwwAxNoYTPvAAcSXjsO1wCXiBU=" }, + "selenium-webdriver": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-beta.3.tgz", + "integrity": "sha512-R0mGHpQkSKgIWiPgcKDcckh4A6aaK0KTyWxs5ieuiI7zsXQ+Kb6neph+dNoeqq3jSBGyv3ONo2w3oohoL4D/Rg==", + "dev": true, + "requires": { + "jszip": "^3.5.0", + "rimraf": "^2.7.1", + "tmp": "^0.2.1", + "ws": "^7.3.1" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -7655,6 +7495,12 @@ "integrity": "sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg==", "dev": true }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, "setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", @@ -8223,32 +8069,6 @@ "inherits": "2" } }, - "tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "dependencies": { - "bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - } - } - }, "tarn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz", @@ -8359,6 +8179,26 @@ "resolved": "https://registry.npmjs.org/tinycon/-/tinycon-0.6.8.tgz", "integrity": "sha1-59oiPj7gy/nbeWP6M1aZuyF3enM=" }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, "to-array": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", @@ -8555,12 +8395,6 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, - "vargs": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/vargs/-/vargs-0.1.0.tgz", - "integrity": "sha1-a2GE2mUgzDIEzhtAfKwm2SYJ6/8=", - "dev": true - }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -8596,82 +8430,6 @@ "unist-util-stringify-position": "^2.0.0" } }, - "wd": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/wd/-/wd-1.12.1.tgz", - "integrity": "sha512-O99X8OnOgkqfmsPyLIRzG9LmZ+rjmdGFBCyhGpnsSL4MB4xzHoeWmSVcumDiQ5QqPZcwGkszTgeJvjk2VjtiNw==", - "dev": true, - "requires": { - "archiver": "^3.0.0", - "async": "^2.0.0", - "lodash": "^4.0.0", - "mkdirp": "^0.5.1", - "q": "^1.5.1", - "request": "2.88.0", - "vargs": "^0.1.0" - }, - "dependencies": { - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - } - } - } - }, "web-namespaces": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz", @@ -8913,17 +8671,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" - }, - "zip-stream": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz", - "integrity": "sha512-EkXc2JGcKhO5N5aZ7TmuNo45budRaFGHOmz24wtJR7znbNqDPmdZtUauKX6et8KAVseAMBOyWJqEpXcHTBsh7Q==", - "dev": true, - "requires": { - "archiver-utils": "^2.1.0", - "compress-commons": "^2.1.1", - "readable-stream": "^3.4.0" - } } } } diff --git a/src/package.json b/src/package.json index 92fbe868a8b..6b8382715da 100644 --- a/src/package.json +++ b/src/package.json @@ -91,12 +91,12 @@ "mocha": "7.1.2", "mocha-froth": "^0.2.10", "openapi-schema-validation": "^0.4.2", + "selenium-webdriver": "^4.0.0-beta.3", "set-cookie-parser": "^2.4.6", "sinon": "^9.2.0", "split-grid": "^1.0.11", "superagent": "^3.8.3", - "supertest": "4.0.2", - "wd": "1.12.1" + "supertest": "4.0.2" }, "eslintConfig": { "ignorePatterns": [ diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 80b44f679a5..1d877fb6c68 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -5,14 +5,8 @@ process.on('unhandledRejection', (err) => { throw err; }); const async = require('async'); -const wd = require('wd'); - -const config = { - hostname: 'ondemand.saucelabs.com', - port: 80, - user: process.env.SAUCE_USER, - pwd: process.env.SAUCE_ACCESS_KEY, -}; +const swd = require('selenium-webdriver'); +const swdChrome = require('selenium-webdriver/chrome'); const isAdminRunner = process.argv[2] === 'admin'; @@ -31,19 +25,28 @@ const log = (msg, pfx = '') => { const finishedRegex = /FINISHED.*[0-9]+ tests passed, ([0-9]+) tests failed/; const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { - const fullName = [process.env.GIT_HASH].concat(process.env.SAUCE_NAME || [], name).join(' - '); - testSettings.name = fullName; - testSettings.public = true; - testSettings.build = process.env.GIT_HASH; - // console.json can be downloaded via saucelabs, - // don't know how to print them into output of the tests - testSettings.extendedDebugging = true; - testSettings.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; - const browser = wd.remote(config, 'promiseChain'); - await browser.init(testSettings); - const url = `https://saucelabs.com/jobs/${browser.sessionID}`; + const chromeOptions = new swdChrome.Options() + .addArguments('use-fake-device-for-media-stream'); + const driver = await new swd.Builder() + .usingServer('https://ondemand.saucelabs.com/wd/hub') + .withCapabilities(Object.assign({ + 'sauce:options': { + username: process.env.SAUCE_USERNAME, + accessKey: process.env.SAUCE_ACCESS_KEY, + name: [process.env.GIT_HASH].concat(process.env.SAUCE_NAME || [], name).join(' - '), + public: true, + build: process.env.GIT_HASH, + // console.json can be downloaded via saucelabs, + // don't know how to print them into output of the tests + extendedDebugging: true, + tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER, + }, + }, testSettings)) + .setChromeOptions(chromeOptions) + .build(); + const url = `https://saucelabs.com/jobs/${(await driver.getSession()).getId()}`; try { - await browser.get('http://localhost:9001/tests/frontend/'); + await driver.get('http://localhost:9001/tests/frontend/'); log(`Remote sauce test started! ${url}`, pfx); // @TODO this should be configured in testSettings, see // https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-Timeouts @@ -60,7 +63,7 @@ const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { return text.substring(skipChars); }; while (true) { - const consoleText = await browser.eval(`(${remoteFn})(${JSON.stringify(logIndex)})`); + const consoleText = await driver.executeScript(remoteFn, logIndex); (consoleText ? consoleText.split('\n') : []).forEach((line) => log(line, pfx)); logIndex += consoleText.length; const [finished, nFailedStr] = consoleText.match(finishedRegex) || []; @@ -77,42 +80,41 @@ const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { } } finally { log(`Remote sauce test finished! ${url}`, pfx); - await browser.quit(); + await driver.quit(); } }, 6); // run 6 tests in parrallel Promise.all([ { - platform: 'macOS 11.00', + platformName: 'macOS 11.00', browserName: 'safari', - version: 'latest', + browserVersion: 'latest', }, ...(isAdminRunner ? [] : [ { - platform: 'Windows 10', + platformName: 'Windows 10', browserName: 'firefox', - version: 'latest', + browserVersion: 'latest', }, { - platform: 'Windows 10', + platformName: 'Windows 10', browserName: 'MicrosoftEdge', - version: 'latest', + browserVersion: 'latest', }, { - platform: 'Windows 10', + platformName: 'Windows 10', browserName: 'chrome', - version: 'latest', - args: ['--use-fake-device-for-media-stream'], + browserVersion: 'latest', }, { - platform: 'Windows 7', + platformName: 'Windows 7', browserName: 'chrome', - version: '55.0', - args: ['--use-fake-device-for-media-stream'], + browserVersion: '55.0', }, ]), ].map(async (testSettings) => { - const name = `${testSettings.browserName} ${testSettings.version}, ${testSettings.platform}`; + const name = + `${testSettings.browserName} ${testSettings.browserVersion}, ${testSettings.platformName}`; const pfx = `[${name}] `; try { await sauceTestWorker.push({name, pfx, testSettings}); From 8f0d70312d3a4ddc1003aec31adccb4f3eaf62f0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 6 Jun 2021 16:25:15 -0400 Subject: [PATCH 179/218] tests: Also pass `--use-fake-ui-for-media-stream` to Chrome For testing ep_webrtc. --- src/tests/frontend/travis/remote_runner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 1d877fb6c68..f70f617ff20 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -26,7 +26,7 @@ const finishedRegex = /FINISHED.*[0-9]+ tests passed, ([0-9]+) tests failed/; const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { const chromeOptions = new swdChrome.Options() - .addArguments('use-fake-device-for-media-stream'); + .addArguments('use-fake-device-for-media-stream', 'use-fake-ui-for-media-stream'); const driver = await new swd.Builder() .usingServer('https://ondemand.saucelabs.com/wd/hub') .withCapabilities(Object.assign({ From 63a5dc6599057119722edf97d0e3b5959eaf1664 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 6 Jun 2021 16:26:16 -0400 Subject: [PATCH 180/218] tests: Configure Firefox to use fake webcam This makes it possible to test ep_webrtc in Firefox. --- src/tests/frontend/travis/remote_runner.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index f70f617ff20..958f280b514 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -7,6 +7,7 @@ process.on('unhandledRejection', (err) => { throw err; }); const async = require('async'); const swd = require('selenium-webdriver'); const swdChrome = require('selenium-webdriver/chrome'); +const swdFirefox = require('selenium-webdriver/firefox'); const isAdminRunner = process.argv[2] === 'admin'; @@ -27,6 +28,9 @@ const finishedRegex = /FINISHED.*[0-9]+ tests passed, ([0-9]+) tests failed/; const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { const chromeOptions = new swdChrome.Options() .addArguments('use-fake-device-for-media-stream', 'use-fake-ui-for-media-stream'); + const firefoxOptions = new swdFirefox.Options() + .setPreference('media.navigator.permission.disabled', true) + .setPreference('media.navigator.streams.fake', true); const driver = await new swd.Builder() .usingServer('https://ondemand.saucelabs.com/wd/hub') .withCapabilities(Object.assign({ @@ -43,6 +47,7 @@ const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { }, }, testSettings)) .setChromeOptions(chromeOptions) + .setFirefoxOptions(firefoxOptions) .build(); const url = `https://saucelabs.com/jobs/${(await driver.getSession()).getId()}`; try { From b2e94685fbba483d399421d474c5ee2df3c5df38 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 3 Jun 2021 15:01:23 -0400 Subject: [PATCH 181/218] pad: Display error name in the gritter box --- src/static/js/pad_utils.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index 8cc6efb1a5a..8a450d6fbf2 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -311,6 +311,9 @@ padutils.setupGlobalExceptionHandler = () => { } else { throw new Error(`unknown event: ${e.toString()}`); } + if (err.name != null && msg !== err.name && !msg.startsWith(`${err.name}: `)) { + msg = `${err.name}: ${msg}`; + } const errorId = randomString(20); let msgAlreadyVisible = false; From cccabf45b8fd7dd1c971aa0388a7552c6f2f0b51 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 3 Jun 2021 15:03:16 -0400 Subject: [PATCH 182/218] pad: Move error message to the top of the gritter box --- src/static/js/pad_utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index 8a450d6fbf2..48f0624e9d8 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -331,12 +331,12 @@ padutils.setupGlobalExceptionHandler = () => { $('

') .text('If the problem persists, please send this error message to your webmaster:'), $('

').css('text-align', 'left').css('font-size', '.8em').css('margin-top', '1em') + .append($('').addClass('error-msg').text(msg)).append($('
')) + .append(txt(`at ${url} at line ${linenumber}`)).append($('
')) .append(txt(`ErrorId: ${errorId}`)).append($('
')) .append(txt(type)).append($('
')) .append(txt(`URL: ${window.location.href}`)).append($('
')) - .append(txt(`UserAgent: ${navigator.userAgent}`)).append($('
')) - .append($('').addClass('error-msg').text(msg)).append($('
')) - .append(txt(`at ${url} at line ${linenumber}`)).append($('
')), + .append(txt(`UserAgent: ${navigator.userAgent}`)).append($('
')), ]; $.gritter.add({ From 28f2acf98aa032d6f752f9887ee5dbc5117f057c Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 7 Jun 2021 14:48:08 +0200 Subject: [PATCH 183/218] Localisation updates from https://translatewiki.net. --- src/locales/qqq.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/locales/qqq.json b/src/locales/qqq.json index 4e935b3c727..80b88ddbb06 100644 --- a/src/locales/qqq.json +++ b/src/locales/qqq.json @@ -1,6 +1,7 @@ { "@metadata": { "authors": [ + "BryanDavis", "Liuxinyu970226", "Mklehr", "Nemo bis", @@ -10,6 +11,12 @@ "Tim.krieger" ] }, + "admin_plugins.available_install.value": "{{Identical|Install}}", + "admin_plugins.description": "{{Identical|Description}}", + "admin_plugins.installed_uninstall.value": "{{Identical|Uninstall}}", + "admin_plugins.last-update": "{{Identical|Last update}}", + "admin_plugins.name": "{{Identical|Name}}", + "admin_plugins.version": "{{Identical|Version}}", "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.", @@ -43,6 +50,7 @@ "pad.settings.fontType": "Used as label for the \"Font type\" select box which has the following options:\n* {{msg-etherpadlite|Pad.settings.fontType.normal}}\n* {{msg-etherpadlite|Pad.settings.fontType.monospaced}}", "pad.settings.fontType.normal": "Used as an option in the \"Font type\" select box which is labeled {{msg-etherpadlite|Pad.settings.fontType}}.\n{{Identical|Normal}}", "pad.settings.language": "This is a label for a select list of languages.\n{{Identical|Language}}", + "pad.settings.about": "{{Identical|About}}", "pad.importExport.import_export": "Used as HTML

heading of window.\n\nFollowed by the child heading {{msg-etherpadlite|Pad.importExport.import}}.", "pad.importExport.import": "Used as HTML

heading.\n\nPreceded by the parent heading {{msg-etherpadlite|Pad.importExport.import_export}}.", "pad.importExport.importSuccessful": "Used as success message to indicate that the pad has been imported successfully.\n{{Identical|Successful}}", From ea4500ef64176b13605505db0e3279f9482d4878 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Wed, 9 Jun 2021 22:13:02 +0000 Subject: [PATCH 184/218] fix: upgrade express-session from 1.17.1 to 1.17.2 Snyk has created this PR to upgrade express-session from 1.17.1 to 1.17.2. See this package in npm: https://www.npmjs.com/package/express-session See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=upgrade-pr --- src/package-lock.json | 20 ++++++++++---------- src/package.json | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 4ae45a3be4d..eabd53b76e2 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1938,29 +1938,29 @@ "integrity": "sha512-nE96xaxGfxiS5jP3tD3kIW1Jg9yQgX0rXCs3rCkZtmbWHEGyotwaezkLj7bnB41Z0uaOLM8W4AX6qHao4IZ2YA==" }, "express-session": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.1.tgz", - "integrity": "sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q==", + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.2.tgz", + "integrity": "sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ==", "requires": { - "cookie": "0.4.0", + "cookie": "0.4.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "~2.0.0", "on-headers": "~1.0.2", "parseurl": "~1.3.3", - "safe-buffer": "5.2.0", + "safe-buffer": "5.2.1", "uid-safe": "~2.1.5" }, "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" } } }, diff --git a/src/package.json b/src/package.json index 6b8382715da..7752ced484d 100644 --- a/src/package.json +++ b/src/package.json @@ -42,7 +42,7 @@ "etherpad-yajsml": "0.0.4", "express": "4.17.1", "express-rate-limit": "5.2.6", - "express-session": "1.17.1", + "express-session": "1.17.2", "find-root": "1.1.0", "formidable": "1.2.2", "http-errors": "1.8.0", From f5046f4b18ae934a0e9ce97c14574f103d587f1b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 9 Jun 2021 00:29:41 -0400 Subject: [PATCH 185/218] tests: Keep the `#mocha-stats` div visible --- src/tests/frontend/runner.css | 11 ++++++++++- src/tests/frontend/runner.js | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/tests/frontend/runner.css b/src/tests/frontend/runner.css index b751d414ad6..04a15016fd2 100644 --- a/src/tests/frontend/runner.css +++ b/src/tests/frontend/runner.css @@ -42,8 +42,10 @@ body { #mocha { font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; height: 100%; - overflow: auto; + min-height: 100%; /* https://css-tricks.com/preventing-a-grid-blowout/ */ font-size:80%; + display: flex; + flex-direction: column; } #mocha ul { @@ -168,6 +170,12 @@ body { -webkit-box-shadow: 0 1px 3px #eee; } +#mocha-report { + flex: 0 1 auto; + overflow: auto; + margin: 0; +} + #mocha-report ul { padding: 0; } @@ -188,6 +196,7 @@ body { } #mocha-stats { + flex: 0 0 auto; padding: 10px; font-size: 12px; margin: 0; diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index 621f1d2e2e5..620f506929d 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -50,7 +50,7 @@ $(() => (async () => { }); // Scroll down test display after each test - const mochaEl = $('#mocha')[0]; + const mochaEl = $('#mocha-report')[0]; runner.on('test', () => { mochaEl.scrollTop = mochaEl.scrollHeight; }); From e0ae997501c56bea76bfe7c62439023ffdbfa3be Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 9 Jun 2021 00:33:04 -0400 Subject: [PATCH 186/218] tests: Don't auto-scroll Mocha results if user scrolls up --- src/tests/frontend/runner.css | 4 ++-- src/tests/frontend/runner.js | 45 +++++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/tests/frontend/runner.css b/src/tests/frontend/runner.css index 04a15016fd2..aac044c1354 100644 --- a/src/tests/frontend/runner.css +++ b/src/tests/frontend/runner.css @@ -57,7 +57,7 @@ body { } #mocha h1 { - margin-top: 15px; + padding-top: 15px; /* margin-top breaks autoscrolling */ font-size: 1em; font-weight: 200; } @@ -68,7 +68,7 @@ body { } #mocha .suite .suite h1 { - margin-top: 0; + padding-top: 0; font-size: .8em; } diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index 620f506929d..e3394269363 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -29,13 +29,49 @@ $(() => (async () => { if (!runner) return; + // AUTO-SCROLLING: + + // Mocha can start multiple suites before the first 'suite' event is emitted. This can break the + // logic used to determine if the div is already scrolled to the bottom. If this is false, + // auto-scrolling unconditionally scrolls to the bottom no matter how far up the div is + // currently scrolled. If true, auto-scrolling only happens if the div is scrolled close to the + // bottom. + let manuallyScrolled = false; + // The 'scroll' event is fired for manual scrolling as well as JavaScript-initiated scrolling. + // This is incremented while auto-scrolling and decremented when done auto-scrolling. This is + // used to ensure that auto-scrolling never sets manuallyScrolled to true. + let autoScrolling = 0; + + // Auto-scroll the #mocha-report div to show the newly added test entry if it was previously + // scrolled to the bottom. + const autoscroll = (newElement) => { + const mr = $('#mocha-report')[0]; + const scroll = !manuallyScrolled || (() => { + const offsetTopAbs = newElement.getBoundingClientRect().top; + const mrOffsetTopAbs = mr.getBoundingClientRect().top - mr.scrollTop; + const offsetTop = offsetTopAbs - mrOffsetTopAbs; + // Add some margin to cover rounding error and to make it easier to engage the auto-scroll. + return offsetTop <= mr.clientHeight + mr.scrollTop + 5; + })(); + if (!scroll) return; + ++autoScrolling; + mr.scrollTop = mr.scrollHeight; + manuallyScrolled = false; + }; + + $('#mocha-report').on('scroll', () => { + if (!autoScrolling) manuallyScrolled = true; + else --autoScrolling; + }); + runner.on('start', () => { stats.start = new Date(); }); runner.on('suite', (suite) => { - suite.root || stats.suites++; if (suite.root) return; + autoscroll($('#mocha-report .suite').last()[0]); + stats.suites++; append(suite.title); level++; }); @@ -49,16 +85,11 @@ $(() => (async () => { } }); - // Scroll down test display after each test - const mochaEl = $('#mocha-report')[0]; - runner.on('test', () => { - mochaEl.scrollTop = mochaEl.scrollHeight; - }); - // max time a test is allowed to run // TODO this should be lowered once timeslider_revision.js is faster let killTimeout; runner.on('test end', () => { + autoscroll($('#mocha-report .test').last()[0]); stats.tests++; }); From 36d2af531882a5ad330b718aed5d4e9a487ac3fe Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 9 Jun 2021 00:34:33 -0400 Subject: [PATCH 187/218] tests: Display frontend spec loading progress --- src/tests/frontend/runner.js | 57 ++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index e3394269363..4a0fb651ae2 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -188,12 +188,59 @@ $(() => (async () => { // asynchronous form of require()). In addition, the performance gains would be minimal because // require-kernel only loads 2 at a time by default. (Increasing the default could cause problems // because browsers like to limit the number of concurrent fetches.) - for (const spec of await $.getJSON('frontendTestSpecs.json')) { - const desc = spec - .replace(/^ep_etherpad-lite\/tests\/frontend\/specs\//, ' ') - .replace(/^([^/ ]*)\/static\/tests\/frontend\/specs\//, '<$1> '); - describe(`${desc}.js`, function () { require(spec); }); + + const $log = $('
'); + const appendToLog = (msg) => { + if (typeof msg === 'string') msg = document.createTextNode(msg); + // Add some margin to cover rounding error and to make it easier to engage the auto-scroll. + const scrolledToBottom = $log[0].scrollHeight <= $log[0].scrollTop + $log[0].clientHeight + 5; + const $msg = $('
').css('white-space', 'nowrap').append(msg).appendTo($log); + if (scrolledToBottom) $log[0].scrollTop = $log[0].scrollHeight; + return $msg; + }; + const $bar = $(''); + let barLastUpdate = Date.now(); + const incrementBar = async (amount = 1) => { + $bar.attr('value', Number.parseInt($bar.attr('value')) + 1); + // Give the browser an opportunity to draw the progress bar's new length. `await + // Promise.resolve()` isn't enough, so a timeout is used. Sleeping every increment (even 0ms) + // unnecessarily slows down spec loading so the sleep is occasional. + if (Date.now() - barLastUpdate > 100) { + await new Promise((resolve) => setTimeout(resolve, 0)); + barLastUpdate = Date.now(); + } + }; + const $progressArea = $('
') + .css({'display': 'flex', 'flex-direction': 'column', 'height': '100%'}) + .append($('
').css({flex: '1 0 0'})) + .append($('
') + .css({'flex': '0 1 auto', 'font-weight': 'bold'}) + .text('Loading frontend test specs...')) + .append($log.css({flex: '0 1 auto', overflow: 'auto'})) + .append($bar.css({flex: '0 0 auto', width: '100%'})) + .appendTo('#mocha'); + const specs = await $.getJSON('frontendTestSpecs.json'); + if (specs.length > 0) { + $bar.attr({value: 0, max: specs.length}); + await incrementBar(0); + } + const makeDesc = (spec) => `${spec + .replace(/^ep_etherpad-lite\/tests\/frontend\/specs\//, ' ') + .replace(/^([^/ ]*)\/static\/tests\/frontend\/specs\//, '<$1> ')}.js`; + for (const spec of specs) { + const desc = makeDesc(spec); + const $msg = appendToLog(`Loading ${desc}...`); + try { + describe(desc, function () { require(spec); }); + } catch (err) { + $msg.append($('').css('color', 'red').text(' FAILED')); + appendToLog($('
').text(`${err.stack || err}`));
+      throw err;
+    }
+    $msg.append(' done');
+    await incrementBar();
   }
+  $progressArea.remove();
 
   await helper.init();
   const grep = getURLParameter('grep');

From acd11c3948f14dfffb6ec0386fb94b1418487bc0 Mon Sep 17 00:00:00 2001
From: Richard Hansen 
Date: Wed, 9 Jun 2021 19:54:36 -0400
Subject: [PATCH 188/218] tests: Fetch frontend test specs in parallel

---
 src/tests/frontend/runner.js | 76 +++++++++++++++++++++++++++++++-----
 1 file changed, 67 insertions(+), 9 deletions(-)

diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js
index 4a0fb651ae2..9cfd65d19a6 100644
--- a/src/tests/frontend/runner.js
+++ b/src/tests/frontend/runner.js
@@ -182,12 +182,51 @@ $(() => (async () => {
     }],
   });
 
-  // This loads the test specs serially. While it is technically possible to load them in parallel,
-  // the code would be very complex (it involves wrapping require.define(), configuring
-  // require-kernel to use the wrapped .define() via require.setGlobalKeyPath(), and using the
-  // asynchronous form of require()). In addition, the performance gains would be minimal because
-  // require-kernel only loads 2 at a time by default. (Increasing the default could cause problems
-  // because browsers like to limit the number of concurrent fetches.)
+  // Speed up tests by loading test definitions in parallel. Approach: Define a new global object
+  // that has a define() method, which is a wrapper around window.require.define(). The wrapper
+  // mutates the module definition function to temporarily replace Mocha's functions with
+  // placeholders. The placeholders make it possible to defer the actual Mocha function calls until
+  // after the modules are all loaded in parallel. require.setGlobalKeyPath() is used to coax
+  // require-kernel into using the wrapper define() method instead of require.define().
+
+  // Per-module log of attempted Mocha function calls. Key is module path, value is an array of
+  // [functionName, argsArray] arrays.
+  const mochaCalls = new Map();
+  const mochaFns =
+      ['describe', 'context', 'it', 'specify', 'before', 'after', 'beforeEach', 'afterEach'];
+  window.testRunnerRequire = {
+    define(...args) {
+      if (args.length === 2) args = [{[args[0]]: args[1]}];
+      if (args.length !== 1) throw new Error('unexpected args passed to testRunnerRequire.define');
+      const [origDefs] = args;
+      const defs = {};
+      for (const [path, origDef] of Object.entries(origDefs)) {
+        defs[path] = function (require, exports, module) {
+          const calls = [];
+          mochaCalls.set(module.id.replace(/\.js$/, ''), calls);
+          // Backup Mocha functions. Note that because modules can require other modules, these
+          // backups might be placeholders, not the actual Mocha functions.
+          const backups = {};
+          for (const fn of mochaFns) {
+            // Note: Test specs can require other modules, so window[fn] might be a placeholder
+            // function, not the actual Mocha function.
+            backups[fn] = window[fn];
+            window[fn] = (...args) => calls.push([fn, args]);
+          }
+          try {
+            return origDef.call(this, require, exports, module);
+          } finally {
+            Object.assign(window, backups);
+          }
+        };
+      }
+      return require.define(defs);
+    },
+  };
+  require.setGlobalKeyPath('testRunnerRequire');
+  // Increase fetch parallelism to speed up test spec loading. (Note: The browser might limit to a
+  // lower value -- this is just an upper limit.)
+  require.setRequestMaximum(20);
 
   const $log = $('
'); const appendToLog = (msg) => { @@ -221,17 +260,36 @@ $(() => (async () => { .appendTo('#mocha'); const specs = await $.getJSON('frontendTestSpecs.json'); if (specs.length > 0) { - $bar.attr({value: 0, max: specs.length}); + $bar.attr({value: 0, max: specs.length * 2}); await incrementBar(0); } const makeDesc = (spec) => `${spec .replace(/^ep_etherpad-lite\/tests\/frontend\/specs\//, ' ') .replace(/^([^/ ]*)\/static\/tests\/frontend\/specs\//, '<$1> ')}.js`; + await Promise.all(specs.map(async (spec) => { + const $msg = appendToLog(`Fetching ${makeDesc(spec)}...`); + try { + await new Promise((resolve, reject) => require(spec, (module) => { + if (module == null) return reject(new Error(`failed to load module ${spec}`)); + resolve(); + })); + } catch (err) { + $msg.append($('').css('color', 'red').text(' FAILED')); + appendToLog($('
').text(`${err.stack || err}`));
+      throw err;
+    }
+    $msg.append(' done');
+    await incrementBar();
+  }));
+  require.setGlobalKeyPath('require');
+  delete window.testRunnerRequire;
   for (const spec of specs) {
     const desc = makeDesc(spec);
-    const $msg = appendToLog(`Loading ${desc}...`);
+    const $msg = appendToLog(`Executing ${desc}...`);
     try {
-      describe(desc, function () { require(spec); });
+      describe(desc, function () {
+        for (const [fn, args] of mochaCalls.get(spec)) window[fn](...args);
+      });
     } catch (err) {
       $msg.append($('').css('color', 'red').text(' FAILED'));
       appendToLog($('
').text(`${err.stack || err}`));

From 565b1c5271072cd3c137c81153e7a492885945b0 Mon Sep 17 00:00:00 2001
From: Richard Hansen 
Date: Thu, 10 Jun 2021 13:28:55 -0400
Subject: [PATCH 189/218] tests: Fix flexbox grow/shrink factors

  * Make sure the `#mocha-report` div grows to fill the available
    vertical space.
  * Prevent the "Loading frontend test specs..." div from shrinking.
---
 src/tests/frontend/runner.css | 2 +-
 src/tests/frontend/runner.js  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/tests/frontend/runner.css b/src/tests/frontend/runner.css
index aac044c1354..6331b81839e 100644
--- a/src/tests/frontend/runner.css
+++ b/src/tests/frontend/runner.css
@@ -171,7 +171,7 @@ body {
 }
 
 #mocha-report {
-  flex: 0 1 auto;
+  flex: 1 1 auto;
   overflow: auto;
   margin: 0;
 }
diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js
index 9cfd65d19a6..2784b508eae 100644
--- a/src/tests/frontend/runner.js
+++ b/src/tests/frontend/runner.js
@@ -253,7 +253,7 @@ $(() => (async () => {
       .css({'display': 'flex', 'flex-direction': 'column', 'height': '100%'})
       .append($('
').css({flex: '1 0 0'})) .append($('
') - .css({'flex': '0 1 auto', 'font-weight': 'bold'}) + .css({'flex': '0 0 auto', 'font-weight': 'bold'}) .text('Loading frontend test specs...')) .append($log.css({flex: '0 1 auto', overflow: 'auto'})) .append($bar.css({flex: '0 0 auto', width: '100%'})) From 3e4df68510ba55efc4f81afb031de3c2cf6f1df8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 10 Jun 2021 15:26:56 -0400 Subject: [PATCH 190/218] tests: Enable fake webcam on Microsoft Edge --- src/tests/frontend/travis/remote_runner.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 958f280b514..264d8032c8b 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -7,6 +7,7 @@ process.on('unhandledRejection', (err) => { throw err; }); const async = require('async'); const swd = require('selenium-webdriver'); const swdChrome = require('selenium-webdriver/chrome'); +const swdEdge = require('selenium-webdriver/edge'); const swdFirefox = require('selenium-webdriver/firefox'); const isAdminRunner = process.argv[2] === 'admin'; @@ -28,6 +29,8 @@ const finishedRegex = /FINISHED.*[0-9]+ tests passed, ([0-9]+) tests failed/; const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { const chromeOptions = new swdChrome.Options() .addArguments('use-fake-device-for-media-stream', 'use-fake-ui-for-media-stream'); + const edgeOptions = new swdEdge.Options() + .addArguments('use-fake-device-for-media-stream', 'use-fake-ui-for-media-stream'); const firefoxOptions = new swdFirefox.Options() .setPreference('media.navigator.permission.disabled', true) .setPreference('media.navigator.streams.fake', true); @@ -47,6 +50,7 @@ const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { }, }, testSettings)) .setChromeOptions(chromeOptions) + .setEdgeOptions(edgeOptions) .setFirefoxOptions(firefoxOptions) .build(); const url = `https://saucelabs.com/jobs/${(await driver.getSession()).getId()}`; From 081b97c41dfa71f12a18390bd2fb5489253c84ff Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 14 Jun 2021 04:22:32 -0400 Subject: [PATCH 191/218] tests: Wrap more Mocha functions --- src/tests/frontend/runner.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index 2784b508eae..cc9d0315ff7 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -192,8 +192,21 @@ $(() => (async () => { // Per-module log of attempted Mocha function calls. Key is module path, value is an array of // [functionName, argsArray] arrays. const mochaCalls = new Map(); - const mochaFns = - ['describe', 'context', 'it', 'specify', 'before', 'after', 'beforeEach', 'afterEach']; + const mochaFns = [ + 'after', + 'afterEach', + 'before', + 'beforeEach', + 'context', + 'describe', + 'it', + 'run', + 'specify', + 'xcontext', // Undocumented as of Mocha 7.1.2. + 'xdescribe', // Undocumented as of Mocha 7.1.2. + 'xit', // Undocumented as of Mocha 7.1.2. + 'xspecify', // Undocumented as of Mocha 7.1.2. + ]; window.testRunnerRequire = { define(...args) { if (args.length === 2) args = [{[args[0]]: args[1]}]; From 5dcb7a75498a7326bff312987a65ce02ec880594 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 14 Jun 2021 04:22:46 -0400 Subject: [PATCH 192/218] tests: Don't attempt to wrap non-functions --- src/tests/frontend/runner.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index cc9d0315ff7..449c126c53c 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -221,6 +221,7 @@ $(() => (async () => { // backups might be placeholders, not the actual Mocha functions. const backups = {}; for (const fn of mochaFns) { + if (typeof window[fn] !== 'function') continue; // Note: Test specs can require other modules, so window[fn] might be a placeholder // function, not the actual Mocha function. backups[fn] = window[fn]; From 66ee9c5ef9e03a869462e164fe5a09caf549adb7 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Sun, 13 Jun 2021 17:14:01 +0200 Subject: [PATCH 193/218] update package-lock --- src/package-lock.json | 1047 ++++++++++++++++++++++++++++++++--------- 1 file changed, 814 insertions(+), 233 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index eabd53b76e2..10680fa9416 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -5,13 +5,14 @@ "requires": true, "dependencies": { "@apidevtools/json-schema-ref-parser": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.7.tgz", - "integrity": "sha512-QdwOGF1+eeyFh+17v2Tz626WX0nucd1iKOm6JUTUvCZdbolblCOOQCxGrQPY0f7jEhn36PiAWqZnsC2r5vmUWg==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz", + "integrity": "sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w==", "requires": { "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", "call-me-maybe": "^1.0.1", - "js-yaml": "^3.13.1" + "js-yaml": "^4.1.0" } }, "@azure/abort-controller": { @@ -20,15 +21,13 @@ "integrity": "sha512-lNUmDRVGpanCsiUN3NWxFTdwmdFI53xwhkTFfHDGTYk46ca7Ind3nanJc+U6Zj9Tv+9nTCWRBscWEW1DyKOpTw==", "requires": { "tslib": "^2.0.0" - }, - "dependencies": { - "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" - } } }, + "@azure/core-asynciterator-polyfill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.0.tgz", + "integrity": "sha512-kmv8CGrPfN9SwMwrkiBK9VTQYxdFQEGe0BmQk+M8io56P9KNzpAxcWE/1fxJj7uouwN4kXF0BHW8DNlgx+wtCg==" + }, "@azure/core-auth": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.3.0.tgz", @@ -36,24 +35,167 @@ "requires": { "@azure/abort-controller": "^1.0.0", "tslib": "^2.0.0" + } + }, + "@azure/core-http": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-1.2.5.tgz", + "integrity": "sha512-SjjjqaO9emyn+XM0Qyzt5RsgddOIpGAfhWH6+d8X6/HbhFrtvXLJIz85EMoIO+T4rX3ISStik9MD5LMW9IZg4A==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-asynciterator-polyfill": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-tracing": "1.0.0-preview.11", + "@azure/logger": "^1.0.0", + "@types/node-fetch": "^2.5.0", + "@types/tunnel": "^0.0.1", + "form-data": "^3.0.0", + "node-fetch": "^2.6.0", + "process": "^0.11.10", + "tough-cookie": "^4.0.0", + "tslib": "^2.0.0", + "tunnel": "^0.0.6", + "uuid": "^8.3.0", + "xml2js": "^0.4.19" }, "dependencies": { - "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" } } }, + "@azure/core-lro": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-1.0.5.tgz", + "integrity": "sha512-0EFCFZxARrIoLWMIRt4vuqconRVIO2Iin7nFBfJiYCCbKp5eEmxutNk8uqudPmG0XFl5YqlVh68/al/vbE5OOg==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-http": "^1.2.0", + "@azure/core-tracing": "1.0.0-preview.11", + "events": "^3.0.0", + "tslib": "^2.0.0" + } + }, + "@azure/core-paging": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.1.3.tgz", + "integrity": "sha512-his7Ah40ThEYORSpIAwuh6B8wkGwO/zG7gqVtmSE4WAJ46e36zUDXTKReUCLBDc6HmjjApQQxxcRFy5FruG79A==", + "requires": { + "@azure/core-asynciterator-polyfill": "^1.0.0" + } + }, + "@azure/core-tracing": { + "version": "1.0.0-preview.11", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.11.tgz", + "integrity": "sha512-frF0pJc9HTmKncVokhBxCqipjbql02DThQ1ZJ9wLi7SDMLdPAFyDI5xZNzX5guLz+/DtPkY+SGK2li9FIXqshQ==", + "requires": { + "@opencensus/web-types": "0.0.7", + "@opentelemetry/api": "1.0.0-rc.0", + "tslib": "^2.0.0" + } + }, + "@azure/identity": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.3.0.tgz", + "integrity": "sha512-qYTaWA+5ir4+/iEry7n3l1TyeNhTHP8IRpjsbNv8ur8W/QjqZmCz1H2naebRp5tQmehXfo1pUrp2ew+qGhTh0g==", + "requires": { + "@azure/core-http": "^1.2.4", + "@azure/core-tracing": "1.0.0-preview.11", + "@azure/logger": "^1.0.0", + "@azure/msal-node": "1.0.0-beta.6", + "@types/stoppable": "^1.1.0", + "axios": "^0.21.1", + "events": "^3.0.0", + "jws": "^4.0.0", + "keytar": "^7.3.0", + "msal": "^1.0.2", + "open": "^7.0.0", + "qs": "^6.7.0", + "stoppable": "^1.1.0", + "tslib": "^2.0.0", + "uuid": "^8.3.0" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } + } + }, + "@azure/keyvault-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.1.0.tgz", + "integrity": "sha512-xdz7nkYulWSo2+CsBRxaOe2JrGWQP3WxEjPLdIzZNHyaYBuLlnKHL3XWYH5j0UUVoDcmQClZUlZ2P/kdOiy/aQ==", + "requires": { + "@azure/core-http": "^1.1.6", + "@azure/core-lro": "^1.0.2", + "@azure/core-paging": "^1.1.1", + "@azure/core-tracing": "1.0.0-preview.9", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^0.10.2", + "tslib": "^2.0.0" + }, + "dependencies": { + "@azure/core-tracing": { + "version": "1.0.0-preview.9", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.9.tgz", + "integrity": "sha512-zczolCLJ5QG42AEPQ+Qg9SRYNUyB+yZ5dzof4YEc+dyWczO9G2sBqbAjLB7IqrsdHN2apkiB2oXeDKCsq48jug==", + "requires": { + "@opencensus/web-types": "0.0.7", + "@opentelemetry/api": "^0.10.2", + "tslib": "^2.0.0" + } + }, + "@opentelemetry/api": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.10.2.tgz", + "integrity": "sha512-GtpMGd6vkzDMYcpu2t9LlhEgMy/SzBwRnz48EejlRArYqZzqSzAsKmegUK7zHgl+EOIaK9mKHhnRaQu3qw20cA==", + "requires": { + "@opentelemetry/context-base": "^0.10.2" + } + } + } + }, + "@azure/logger": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.2.tgz", + "integrity": "sha512-YZNjNV0vL3nN2nedmcjQBcpCTo3oqceXmgiQtEm6fLpucjRZyQKAQruhCmCpRlB1iykqKJJ/Y8CDmT5rIE6IJw==", + "requires": { + "tslib": "^2.0.0" + } + }, "@azure/ms-rest-azure-env": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@azure/ms-rest-azure-env/-/ms-rest-azure-env-2.0.0.tgz", "integrity": "sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw==" }, "@azure/ms-rest-js": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.4.1.tgz", - "integrity": "sha512-76Tc+RRqXj1bqwg/1TX2KHaAGyFLzypqBQ65+FaVxw+oSeZhZcePRFM4r0lqRVwweC9RTct95aNPnMZSzGxDwA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.5.1.tgz", + "integrity": "sha512-2McxhuZy1PRcAsX8PFRD6ELJg1Hr2a1vGvUjcWqVGcfej6MeXplC0P9GzJfNh739fNtzyq0F6ps657o+nVHJpg==", "requires": { "@azure/core-auth": "^1.1.4", "abort-controller": "^3.0.0", @@ -85,39 +227,62 @@ "psl": "^1.1.28", "punycode": "^2.1.1" } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" } } }, "@azure/ms-rest-nodeauth": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.0.9.tgz", - "integrity": "sha512-+GdDHUJlWtIDanRZemFooLy68NsBDhN/Oni9DSFeoXIFNGlSe1IOes8/IRkQdrNXyUvBanuzzR7I5WYYzYQsmA==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.0.10.tgz", + "integrity": "sha512-oel7ibYlredh2wo7XwNYMx4jWlbMkIzCC8t8VpdhsAWDJVNSSce+DYj5jjZn1oED+QsCytVM2B7/QTuLN1/yDw==", "requires": { "@azure/ms-rest-azure-env": "^2.0.0", "@azure/ms-rest-js": "^2.0.4", - "adal-node": "^0.1.28" + "adal-node": "^0.2.2" + } + }, + "@azure/msal-common": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-4.3.0.tgz", + "integrity": "sha512-jFqUWe83wVb6O8cNGGBFg2QlKvqM1ezUgJTEV7kIsAPX0RXhGFE4B1DLNt6hCnkTXDbw+KGW0zgxOEr4MJQwLw==", + "requires": { + "debug": "^4.1.1" }, "dependencies": { - "@types/node": { - "version": "8.10.66", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", - "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==" - }, - "adal-node": { - "version": "0.1.28", - "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz", - "integrity": "sha1-RoxLs+u9lrEnBmn0ucuk4AZepIU=", - "requires": { - "@types/node": "^8.0.47", - "async": ">=0.6.0", - "date-utils": "*", - "jws": "3.x.x", - "request": ">= 2.52.0", - "underscore": ">= 1.3.1", - "uuid": "^3.1.0", - "xmldom": ">= 0.1.x", - "xpath.js": "~1.1.0" + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "@azure/msal-node": { + "version": "1.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.6.tgz", + "integrity": "sha512-ZQI11Uz1j0HJohb9JZLRD8z0moVcPks1AFW4Q/Gcl67+QvH4aKEJti7fjCcipEEZYb/qzLSO8U6IZgPYytsiJQ==", + "requires": { + "@azure/msal-common": "^4.0.0", + "axios": "^0.21.1", + "jsonwebtoken": "^8.5.1", + "uuid": "^8.3.0" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" } } }, @@ -131,32 +296,32 @@ } }, "@babel/helper-validator-identifier": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", - "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", + "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", "dev": true }, "@babel/highlight": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", - "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.14.0", + "@babel/helper-validator-identifier": "^7.14.5", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@eslint/eslintrc": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.1.tgz", - "integrity": "sha512-5v7TDE9plVhvxQeWLXDTvFvJBdH6pEsdnl2g/dAptmuFEPedQ4Erq5rsDsX+mvAM610IhNaO2W5V1dOOnDKxkQ==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.2.tgz", + "integrity": "sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.1.1", "espree": "^7.3.0", - "globals": "^12.1.0", + "globals": "^13.9.0", "ignore": "^4.0.6", "import-fresh": "^3.2.1", "js-yaml": "^3.13.1", @@ -164,6 +329,15 @@ "strip-json-comments": "^3.1.1" }, "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, "debug": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", @@ -173,13 +347,14 @@ "ms": "2.1.2" } }, - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "argparse": "^1.0.7", + "esprima": "^4.0.0" } }, "ms": { @@ -188,6 +363,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -234,6 +415,21 @@ "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" }, + "@opencensus/web-types": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@opencensus/web-types/-/web-types-0.0.7.tgz", + "integrity": "sha512-xB+w7ZDAu3YBzqH44rCmG9/RlrOmFuDPt/bpf17eJr8eZSrLt7nc7LnWdxM9Mmoj/YKMHpxRg28txu3TcpiL+g==" + }, + "@opentelemetry/api": { + "version": "1.0.0-rc.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.0-rc.0.tgz", + "integrity": "sha512-iXKByCMfrlO5S6Oh97BuM56tM2cIBB0XsL/vWF/AtJrJEKx4MC/Xdu0xDsGXMGcNWpqF7ujMsjjnp0+UHBwnDQ==" + }, + "@opentelemetry/context-base": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.10.2.tgz", + "integrity": "sha512-hZNKjKOYsckoOEgBziGMnBcX0M7EtstnCmwz5jZUOUYwlZ+/xxX6z3jPu1XVO2Jivk0eLfuP9GP+vFD49CMetw==" + }, "@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -279,15 +475,41 @@ "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" }, + "@types/json-schema": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", + "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" + }, "@types/long": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/node": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz", - "integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==" + "version": "15.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", + "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==" + }, + "@types/node-fetch": { + "version": "2.5.10", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.10.tgz", + "integrity": "sha512-IpkX0AasN44hgEad0gEF/V6EgR5n69VEqPEgnmoM8GsIGro3PowbWs4tR6IhxUTyPLpOn+fiGG6nrQhcmoCuIQ==", + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + }, + "dependencies": { + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } }, "@types/request": { "version": "2.48.5", @@ -312,11 +534,27 @@ } } }, + "@types/stoppable": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.1.tgz", + "integrity": "sha512-b8N+fCADRIYYrGZOcmOR8ZNBOqhktWTB/bMUl5LvGtT201QKJZOOH5UsFyI3qtteM6ZAJbJqZoBcLqqxKIwjhw==", + "requires": { + "@types/node": "*" + } + }, "@types/tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==" }, + "@types/tunnel": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.1.tgz", + "integrity": "sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A==", + "requires": { + "@types/node": "*" + } + }, "@types/unist": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", @@ -385,6 +623,15 @@ "requires": { "lodash": "^4.17.14" } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } } } }, @@ -501,12 +748,9 @@ } }, "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "array-flatten": { "version": "1.1.1", @@ -835,9 +1079,9 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "cassandra-driver": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.6.2.tgz", - "integrity": "sha512-XfDMlpyEl+mI1DSaw8YdLBUyc5wXwuVYWN0WEZOC50BrHSPUmCXA7fyyexwk3j5XiYd/7BXWvO9nnZ50asJPoQ==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.6.3.tgz", + "integrity": "sha512-npW670TXjTHrdb15LUFN01wssb9vvz6SuNYcppesoKcUXx3Q29nXVhRtnvsnkG0BaSnDGvCCR4udrzYLsbh+sg==", "requires": { "@types/long": "^4.0.0", "@types/node": ">=8", @@ -1169,6 +1413,15 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "requires": { + "mimic-response": "^2.0.0" + } + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -1348,6 +1601,15 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "optional": true, + "requires": { + "once": "^1.4.0" + } + }, "engine.io": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz", @@ -1441,9 +1703,9 @@ "integrity": "sha1-eYCZstvTfKK8dJ5TinwTB9C1BJk=" }, "es-abstract": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz", - "integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==", + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", + "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", "dev": true, "requires": { "call-bind": "^1.0.2", @@ -1454,14 +1716,14 @@ "has-symbols": "^1.0.2", "is-callable": "^1.2.3", "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.2", - "is-string": "^1.0.5", - "object-inspect": "^1.9.0", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.10.3", "object-keys": "^1.1.1", "object.assign": "^4.1.2", "string.prototype.trimend": "^1.0.4", "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.0" + "unbox-primitive": "^1.0.1" }, "dependencies": { "object.assign": { @@ -1500,28 +1762,30 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.26.0.tgz", - "integrity": "sha512-4R1ieRf52/izcZE7AlLy56uIHHDLT74Yzz2Iv2l6kDaYvEu9x+wMB5dZArVL8SYGXSYV2YAg70FcW5Y5nGGNIg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.28.0.tgz", + "integrity": "sha512-UMfH0VSjP0G4p3EWirscJEQ/cHqnT/iuH6oNZOB94nBjWbMnhGEPxsZm1eyIW0C/9jLI0Fow4W5DXLjEI7mn1g==", "dev": true, "requires": { "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.1", + "@eslint/eslintrc": "^0.4.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.0.1", "doctrine": "^3.0.0", "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", "eslint-scope": "^5.1.1", "eslint-utils": "^2.1.0", "eslint-visitor-keys": "^2.0.0", "espree": "^7.3.1", "esquery": "^1.4.0", "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", + "glob-parent": "^5.1.2", "globals": "^13.6.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", @@ -1530,7 +1794,7 @@ "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", - "lodash": "^4.17.21", + "lodash.merge": "^4.6.2", "minimatch": "^3.0.4", "natural-compare": "^1.4.0", "optionator": "^0.9.1", @@ -1539,7 +1803,7 @@ "semver": "^7.2.1", "strip-ansi": "^6.0.0", "strip-json-comments": "^3.1.0", - "table": "^6.0.4", + "table": "^6.0.9", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, @@ -1559,6 +1823,15 @@ "color-convert": "^2.0.1" } }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, "chalk": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", @@ -1593,12 +1866,28 @@ "ms": "2.1.2" } }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -1614,6 +1903,12 @@ "lru-cache": "^6.0.0" } }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, "strip-ansi": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", @@ -1692,9 +1987,9 @@ } }, "eslint-plugin-mocha": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-8.1.0.tgz", - "integrity": "sha512-1EgHvXKRl7W3mq3sntZAi5T24agRMyiTPL4bSXe+B4GksYOjAPEWYx+J3eJg4It1l2NMNZJtk0gQyQ6mfiPhQg==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-8.2.0.tgz", + "integrity": "sha512-8oOR47Ejt+YJPNQzedbiklDqS1zurEaNrxXpRs+Uk4DMDPVmKNagShFeUaYsfvWP55AhI+P1non5QZAHV6K78A==", "dev": true, "requires": { "eslint-utils": "^2.1.0", @@ -1742,9 +2037,9 @@ "dev": true }, "eslint-plugin-you-dont-need-lodash-underscore": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-you-dont-need-lodash-underscore/-/eslint-plugin-you-dont-need-lodash-underscore-6.11.0.tgz", - "integrity": "sha512-cIprUmcACzxBg5rUrrCMbyAI3O0jYsB80+9PGq8XsvRTrxDSIzLitNhBetu9erb3TDxyW6OPseyOZzfQdR8oow==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-you-dont-need-lodash-underscore/-/eslint-plugin-you-dont-need-lodash-underscore-6.12.0.tgz", + "integrity": "sha512-WF4mNp+k2532iswT6iUd1BX6qjd3AV4cFy/09VC82GY9SsRtvkxhUIx7JNGSe0/bLyd57oTr4inPFiIaENXhGw==", "dev": true, "requires": { "kebab-case": "^1.0.0" @@ -1810,7 +2105,8 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true }, "esquery": { "version": "1.4.0", @@ -1888,6 +2184,17 @@ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "optional": true + }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -2081,9 +2388,9 @@ "dev": true }, "follow-redirects": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.0.tgz", - "integrity": "sha512-0vRwd7RKQBTt+mgu87mtYeofLFZpTas2S9zY+jIeuLJMNvudIgF52nr19q40HOwH5RrhWIPuj9puybzSJiRrVg==" + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" }, "forever-agent": { "version": "0.6.1", @@ -2106,15 +2413,21 @@ "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==" }, "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "optional": true + }, "fs-minipass": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", @@ -2199,6 +2512,12 @@ "assert-plus": "^1.0.0" } }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", + "optional": true + }, "glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -2222,20 +2541,12 @@ } }, "globals": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.8.0.tgz", - "integrity": "sha512-rHtdA6+PDBIjeEvA91rpqzEvk/k3/i7EeNQiryiWuJH0Hw9cpyJMAt2jtbAwUaRdhD+573X4vWw6IcjKPasi9Q==", + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.9.0.tgz", + "integrity": "sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA==", "dev": true, "requires": { "type-fest": "^0.20.2" - }, - "dependencies": { - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - } } }, "graceful-fs": { @@ -2561,12 +2872,12 @@ } }, "is-boolean-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz", - "integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", + "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", "dev": true, "requires": { - "call-bind": "^1.0.0" + "call-bind": "^1.0.2" } }, "is-buffer": { @@ -2581,17 +2892,17 @@ "dev": true }, "is-core-module": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.3.0.tgz", - "integrity": "sha512-xSphU2KG9867tsYdLD4RWQ1VqdFl4HTO9Thf3I/3dLEfr0dbPTWKsuCKrgqMljg4nPE+Gq0VCnzT3gr0CyBmsw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", "requires": { "has": "^1.0.3" } }, "is-date-object": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.3.tgz", - "integrity": "sha512-tDpEUInNcy2Yw3lNSepK3Wdw1RnXLcIVienz6Ou631Acl15cJyRWK4dgA1vCmOEgIbtOV0W7MHg+AR2Gdg1NXQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", + "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", "dev": true }, "is-decimal": { @@ -2599,6 +2910,11 @@ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==" }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==" + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2640,9 +2956,9 @@ "dev": true }, "is-number-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", - "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", + "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", "dev": true }, "is-observable": { @@ -2661,28 +2977,28 @@ "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=" }, "is-regex": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", - "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", + "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", "dev": true, "requires": { "call-bind": "^1.0.2", - "has-symbols": "^1.0.1" + "has-symbols": "^1.0.2" } }, "is-string": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", - "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", + "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", "dev": true }, "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", "dev": true, "requires": { - "has-symbols": "^1.0.1" + "has-symbols": "^1.0.2" } }, "is-typedarray": { @@ -2690,6 +3006,14 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "requires": { + "is-docker": "^2.0.0" + } + }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -2735,12 +3059,11 @@ "dev": true }, "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" } }, "jsbi": { @@ -2791,6 +3114,39 @@ "integrity": "sha1-8K8gBQVPDwrefqIRhhS2ncUS2GU=", "dev": true }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -2869,12 +3225,24 @@ } }, "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", "requires": { - "jwa": "^1.4.1", + "jwa": "^2.0.0", "safe-buffer": "^5.0.1" + }, + "dependencies": { + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + } } }, "kebab-case": { @@ -2883,6 +3251,16 @@ "integrity": "sha512-txPHx6nVLhv8PHGXIlAk0nYoh894SpAqGPXNvbg2hh8spvHXIah3+vT87DLoa59nKgC6scD3u3xAuRIgiMqbfQ==", "dev": true }, + "keytar": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.7.0.tgz", + "integrity": "sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==", + "optional": true, + "requires": { + "node-addon-api": "^3.0.0", + "prebuild-install": "^6.0.0" + } + }, "languages4translatewiki": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/languages4translatewiki/-/languages4translatewiki-0.1.3.tgz", @@ -2963,6 +3341,36 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, "lodash.map": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", @@ -2973,6 +3381,11 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lodash.pick": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", @@ -3098,18 +3511,24 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, "mime-db": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", - "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==" + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", + "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==" }, "mime-types": { - "version": "2.1.30", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", - "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", + "version": "2.1.31", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", + "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", "requires": { - "mime-db": "1.47.0" + "mime-db": "1.48.0" } }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -3150,6 +3569,12 @@ "minimist": "^1.2.5" } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "optional": true + }, "mocha": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.2.tgz", @@ -3188,6 +3613,15 @@ "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", "dev": true }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -3227,6 +3661,12 @@ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, "supports-color": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", @@ -3254,30 +3694,22 @@ "dev": true }, "mock-json-schema": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/mock-json-schema/-/mock-json-schema-1.0.8.tgz", - "integrity": "sha512-22yL+WggSo8HXqw0HkXgXXJjJMSBCfv54htfwN4BabaFdJ3808jL0CzE+VaBRlj8Nr0+pnSVE9YvsDG5Quu6hQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/mock-json-schema/-/mock-json-schema-1.1.1.tgz", + "integrity": "sha512-YV23vlsLP1EEOy0EviUvZTluXjLR+rhMzeayP2rcDiezj3RW01MhOSQkbQskdtg0K2fnGas5LKbSXgNjAOSX4A==", "requires": { - "lodash": "^4.17.11", - "openapi-types": "^1.3.2" - }, - "dependencies": { - "openapi-types": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-1.3.5.tgz", - "integrity": "sha512-11oi4zYorsgvg5yBarZplAqbpev5HkuVNPlZaPTknPDzAynq+lnJdXAmruGWP0s+dNYZS7bjM+xrTpJw7184Fg==" - } + "lodash": "^4.17.21" } }, "mongodb": { - "version": "3.6.6", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.6.tgz", - "integrity": "sha512-WlirMiuV1UPbej5JeCMqE93JRfZ/ZzqE7nJTwP85XzjAF4rRSeq2bGCb1cjfoHLOF06+HxADaPGqT0g3SbVT1w==", + "version": "3.6.9", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.9.tgz", + "integrity": "sha512-1nSCKgSunzn/CXwgOWgbPHUWOO5OfERcuOWISmqd610jn0s8BU9K4879iJVabqgpPPbA6hO7rG48eq+fGED3Mg==", "requires": { "bl": "^2.2.1", "bson": "^1.1.4", "denque": "^1.4.1", - "optional-require": "^1.0.2", + "optional-require": "^1.0.3", "safe-buffer": "^5.1.2", "saslprep": "^1.0.0" } @@ -3287,10 +3719,25 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "msal": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/msal/-/msal-1.4.11.tgz", + "integrity": "sha512-8vW5/+irlcQQk87r8Qp3/kQEc552hr7FQLJ6GF5LLkqnwJDDxrswz6RYPiQhmiampymIs0PbHVZrNf8m+6DmgQ==", + "requires": { + "tslib": "^1.9.3" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, "mssql": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/mssql/-/mssql-7.0.0.tgz", - "integrity": "sha512-/6IXQUucUw4wpWXq5eO56P17NQkKbY6sak8NAUrofY5whWkGg0VCxpPQjq8qiuV2hGPUt6+Yo/5La0o4SpatZA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-7.1.3.tgz", + "integrity": "sha512-VCtGfJhb9ik5RV3PZQS9jG9I261cghwyWG4YZWn4+13k377sclkCx7/loctCnMNk1EYJFIIAWYCsk1GYwF1Yag==", "requires": { "@tediousjs/connection-string": "^0.3.0", "debug": "^4", @@ -3386,6 +3833,12 @@ } } }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "optional": true + }, "native-duplexpair": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz", @@ -3454,15 +3907,24 @@ } } }, + "node-abi": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.0.tgz", + "integrity": "sha512-g6bZh3YCKQRdwuO/tSZZYJAw622SjsRfJ2X0Iy4sSOHZ34/sPPdVBn8fev2tj7njzLwuqPw9uMtGsGkO5kIQvg==", + "optional": true, + "requires": { + "semver": "^5.4.1" + } + }, "node-abort-controller": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-1.2.1.tgz", "integrity": "sha512-79PYeJuj6S9+yOHirR0JBLFOgjB6sQCir10uN6xRx25iD+ZD4ULqgRn3MwWBRaQGB0vEgReJzWwJo42T1R6YbQ==" }, "node-addon-api": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz", - "integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "optional": true }, "node-environment-flags": { @@ -6743,9 +7205,9 @@ "optional": true }, "object-inspect": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.2.tgz", - "integrity": "sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA==" + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", + "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==" }, "object-keys": { "version": "1.1.1", @@ -6777,9 +7239,9 @@ } }, "observable-fns": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.5.1.tgz", - "integrity": "sha512-wf7g4Jpo1Wt2KIqZKLGeiuLOEMqpaOZ5gJn7DmSdqXgTdxRwSdBhWegQQpPteQ2gZvzCKqNNpwb853wcpA0j7A==" + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.6.1.tgz", + "integrity": "sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==" }, "on-finished": { "version": "2.3.0", @@ -6802,6 +7264,15 @@ "wrappy": "1" } }, + "open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, "openapi-backend": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-3.9.2.tgz", @@ -6982,9 +7453,9 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-to-regexp": { "version": "0.1.7", @@ -7051,9 +7522,9 @@ } }, "picomatch": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", - "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", "dev": true }, "postgres-array": { @@ -7079,12 +7550,38 @@ "xtend": "^4.0.0" } }, + "prebuild-install": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.3.tgz", + "integrity": "sha512-iqqSR84tNYQUQHRXalSKdIaM8Ov1QxOVuBNWI7+BzZWv6Ih9k75wOnH1rGQ9WWTaaLkTpxWKIciOF0KyfM74+Q==", + "optional": true, + "requires": { + "detect-libc": "^1.0.3", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^2.21.0", + "npmlog": "^4.0.1", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^3.0.3", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + } + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -7113,11 +7610,11 @@ } }, "proxy-addr": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", - "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "requires": { - "forwarded": "~0.1.2", + "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, @@ -7126,6 +7623,16 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -7384,6 +7891,7 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "optional": true, "requires": { "glob": "^7.1.3" } @@ -7418,15 +7926,26 @@ "integrity": "sha1-gRwwAxNoYTPvAAcSXjsO1wCXiBU=" }, "selenium-webdriver": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-beta.3.tgz", - "integrity": "sha512-R0mGHpQkSKgIWiPgcKDcckh4A6aaK0KTyWxs5ieuiI7zsXQ+Kb6neph+dNoeqq3jSBGyv3ONo2w3oohoL4D/Rg==", + "version": "4.0.0-beta.4", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-beta.4.tgz", + "integrity": "sha512-+s/CIYkWzmnC9WASBxxVj7Lm0dcyl6OaFxwIJaFCT5WCuACiimEEr4lUnOOFP/QlKfkDQ56m+aRczaq2EvJEJg==", "dev": true, "requires": { - "jszip": "^3.5.0", - "rimraf": "^2.7.1", + "jszip": "^3.6.0", + "rimraf": "^3.0.2", "tmp": "^0.2.1", - "ws": "^7.3.1" + "ws": ">=7.4.6" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "semver": { @@ -7535,10 +8054,27 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "optional": true }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "optional": true + }, + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "optional": true, + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "simple-git": { - "version": "2.38.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.38.0.tgz", - "integrity": "sha512-CORjrfirWMEGbJAxaXDH/PjZVOeATeG2bkafM9DsLVcFkbF9sXQGIIpEI6FeyXpvUsFK69T/pa4+4FKY9TUJMQ==", + "version": "2.39.1", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.39.1.tgz", + "integrity": "sha512-+kEAkyQHsWejYxQNCzTrjvCxJOcijpB49RSs7HV+TK9B9prUq7YBNpFstQvjfGBn4Hecywp4tm+breGGGHlHwA==", "requires": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", @@ -7794,9 +8330,9 @@ } }, "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" }, "sqlite3": { "version": "5.0.2", @@ -7835,6 +8371,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==" + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -7993,9 +8534,9 @@ "integrity": "sha1-cAcEaNbSl3ylI3suUZyn0Gouo/0=" }, "table": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.0.tgz", - "integrity": "sha512-SAM+5p6V99gYiiy2gT5ArdzgM1dLDed0nkrWmG6Fry/bUS/m9x83BwpJUOf1Qj/x2qJd+thL6IkIx7qPGRxqBw==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", + "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", "dev": true, "requires": { "ajv": "^8.0.1", @@ -8007,9 +8548,9 @@ }, "dependencies": { "ajv": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.3.0.tgz", - "integrity": "sha512-RYE7B5An83d7eWnDR8kbdaIFqmKCNsP16ay1hDbJEU+sa0e3H9SebskCt0Uufem6cfAVu7Col6ubcn/W+Sm8/Q==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.0.tgz", + "integrity": "sha512-cnUG4NSBiM4YFBxgZIj/In3/6KX+rQ2l2YPRVcvAMQGWEPKuXoPIhxzwqh31jA3IPbI4qEOp/5ILI4ynioXsGQ==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -8069,16 +8610,56 @@ "inherits": "2" } }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "optional": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "optional": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "optional": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + } + } + }, "tarn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz", "integrity": "sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw==" }, "tedious": { - "version": "11.0.8", - "resolved": "https://registry.npmjs.org/tedious/-/tedious-11.0.8.tgz", - "integrity": "sha512-Qrl0Vo6nazO7KhFgjG0jqUkX6lVauj+0QS5dBEeVx5VkHFa3g4fDTd0vwSTuy7o1aeleiezMPERU8e4svWxPSQ==", + "version": "11.0.9", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-11.0.9.tgz", + "integrity": "sha512-VEIDlPYQNp9Mct0LDFV5O4cihyq/7D+UU0WH6973+NnQZessYe3CFggHeyfKRw2Dx8AQtWB6tOg4misKiG2mpg==", "requires": { + "@azure/identity": "^1.3.0", + "@azure/keyvault-keys": "^4.1.0", "@azure/ms-rest-nodeauth": "^3.0.6", "@js-joda/core": "^3.2.0", "adal-node": "^0.2.1", @@ -8109,17 +8690,12 @@ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, "iconv-lite": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", - "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" } - }, - "sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" } } }, @@ -8140,14 +8716,14 @@ "dev": true }, "threads": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/threads/-/threads-1.6.4.tgz", - "integrity": "sha512-A+9MQFAUha9W8MjIPmrvETy98qVmZFr5Unox9D95y7kvz3fBpGiFS7JOZs07B2KvTHoRNI5MrGudRVeCmv4Alw==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/threads/-/threads-1.6.5.tgz", + "integrity": "sha512-yL1NN4qZ25crW8wDoGn7TqbENJ69w3zCEjIGXpbqmQ4I+QHrG8+DLaZVKoX74OQUXWCI2lbbrUxDxAbr1xjDGQ==", "requires": { "callsites": "^3.1.0", "debug": "^4.2.0", "is-observable": "^2.1.0", - "observable-fns": "^0.5.1", + "observable-fns": "^0.6.1", "tiny-worker": ">= 2" }, "dependencies": { @@ -8233,9 +8809,9 @@ "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==" }, "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" }, "tunnel": { "version": "0.0.6", @@ -8271,9 +8847,9 @@ "dev": true }, "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, "type-is": { @@ -8286,9 +8862,9 @@ } }, "ueberdb2": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.8.tgz", - "integrity": "sha512-11l+4kwC6XhMaj8sbLroVBM3utCACTEQWlhCEzmyy5Bm3l5uKqtlyUNOMnd/awZUEiazcKQH9PDYdFuhkqxDhw==", + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.10.tgz", + "integrity": "sha512-Odg0Mdj17oK6biHuHMKN2DLny+WDG07qb2JoRx6SoluRIXb6eqceK7TilkydyDSiY6MrEeqWcjh9NKBLS2KtSQ==", "requires": { "async": "^3.2.0", "cassandra-driver": "^4.5.1", @@ -8356,6 +8932,11 @@ "@types/unist": "^2.0.2" } }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, "unorm": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", @@ -8533,9 +9114,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", - "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==" + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" }, "wtfnode": { "version": "0.8.4", @@ -8562,9 +9143,9 @@ "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==" }, "xmlhttprequest-ssl": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.2.tgz", - "integrity": "sha512-tYOaldF/0BLfKuoA39QMwD4j2m8lq4DIncqj1yuNELX4vz9+z/ieG/vwmctjJce+boFHXstqhWnHSxc4W8f4qg==" + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz", + "integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==" }, "xpath.js": { "version": "1.1.0", From ef1ba21104d9e366d7694466d78824ea9a018f69 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 14 Jun 2021 13:43:42 -0400 Subject: [PATCH 194/218] deps: Drop support for Node.js < 12.13.0 --- .github/workflows/backend-tests.yml | 4 ++-- .github/workflows/frontend-admin-tests.yml | 2 +- .github/workflows/upgrade-from-latest-release.yml | 2 +- CHANGELOG.md | 1 + README.md | 5 +++-- doc/plugins.md | 2 +- src/bin/cleanRun.sh | 2 +- src/bin/debugRun.sh | 2 +- src/bin/doc/package.json | 2 +- src/bin/fastRun.sh | 2 +- src/bin/functions.sh | 12 +----------- src/bin/plugins/checkPlugin.js | 2 +- src/bin/run.sh | 2 +- src/node/server.js | 4 ++-- src/package.json | 2 +- 15 files changed, 19 insertions(+), 27 deletions(-) diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index a7c254042ee..0b12eda686b 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - node: [10, 12, 14, 15] + node: [12, 14, 15] steps: - name: Checkout repository @@ -50,7 +50,7 @@ jobs: strategy: fail-fast: false matrix: - node: [10, 12, 14, 15] + node: [12, 14, 15] steps: - name: Checkout repository diff --git a/.github/workflows/frontend-admin-tests.yml b/.github/workflows/frontend-admin-tests.yml index bff9228a732..98385e9f8c1 100644 --- a/.github/workflows/frontend-admin-tests.yml +++ b/.github/workflows/frontend-admin-tests.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - node: [10, 12, 14, 15] + node: [12, 14, 15] steps: - name: Generate Sauce Labs strings diff --git a/.github/workflows/upgrade-from-latest-release.yml b/.github/workflows/upgrade-from-latest-release.yml index d32c51f9731..171dfc4eab7 100644 --- a/.github/workflows/upgrade-from-latest-release.yml +++ b/.github/workflows/upgrade-from-latest-release.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - node: [10, 12, 14, 15] + node: [12, 14, 15] steps: - name: Check out latest release diff --git a/CHANGELOG.md b/CHANGELOG.md index a2e3184df73..2b251d6af6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Compatibility changes +* Node.js v12.13.0 or later is now required. * The `favicon` setting is now interpreted as a pathname to a favicon file, not a URL. Please see the documentation comment in `settings.json.template`. * The undocumented `faviconPad` and `faviconTimeslider` settings have been diff --git a/README.md b/README.md index d3543186baf..e0c110b3f4f 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Etherpad is extremely flexible providing you the means to modify it to solve wha # Installation ## Requirements -- `nodejs` >= **10.17.0**. +- [Node.js](https://nodejs.org/) >= **12.13.0**. ## GNU/Linux and other UNIX-like systems @@ -46,7 +46,8 @@ src/bin/run.sh ``` ### Manual install -You'll need git and [node.js](https://nodejs.org) installed (minimum required Node version: **10.17.0**). + +You'll need Git and [Node.js](https://nodejs.org/) installed. **As any user (we recommend creating a separate user called etherpad):** diff --git a/doc/plugins.md b/doc/plugins.md index 1ed0c4f1287..cc7c867d548 100644 --- a/doc/plugins.md +++ b/doc/plugins.md @@ -225,7 +225,7 @@ publish your plugin. "author": "USERNAME (REAL NAME) ", "contributors": [], "dependencies": {"MODULE": "0.3.20"}, - "engines": { "node": "^10.17.0 || >=11.14.0"} + "engines": {"node": ">=12.13.0"} } ``` diff --git a/src/bin/cleanRun.sh b/src/bin/cleanRun.sh index d8407d92e4b..556180eb425 100755 --- a/src/bin/cleanRun.sh +++ b/src/bin/cleanRun.sh @@ -36,4 +36,4 @@ src/bin/installDeps.sh "$@" || exit 1 #Move to the node folder and start echo "Starting Etherpad..." -exec node $(compute_node_args) src/node/server.js "$@" +exec node src/node/server.js "$@" diff --git a/src/bin/debugRun.sh b/src/bin/debugRun.sh index fd5c34b2cc8..2fae42eeeec 100755 --- a/src/bin/debugRun.sh +++ b/src/bin/debugRun.sh @@ -16,4 +16,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) -exec node $(compute_node_args) --inspect=0.0.0.0:9229 src/node/server.js "$@" +exec node --inspect=0.0.0.0:9229 src/node/server.js "$@" diff --git a/src/bin/doc/package.json b/src/bin/doc/package.json index a1a83423cdd..c17020ea601 100644 --- a/src/bin/doc/package.json +++ b/src/bin/doc/package.json @@ -4,7 +4,7 @@ "description": "Internal tool for generating Node.js API docs", "version": "0.0.0", "engines": { - "node": ">=10.17.0" + "node": ">=12.13.0" }, "dependencies": { "marked": "^2.0.0" diff --git a/src/bin/fastRun.sh b/src/bin/fastRun.sh index ec53f20fe23..b2703316b8b 100755 --- a/src/bin/fastRun.sh +++ b/src/bin/fastRun.sh @@ -19,4 +19,4 @@ cd "${MY_DIR}/../.." || exit 1 echo "Running directly, without checking/installing dependencies" # run Etherpad main class -exec node $(compute_node_args) src/node/server.js "$@" +exec node src/node/server.js "$@" diff --git a/src/bin/functions.sh b/src/bin/functions.sh index c7f3c85561f..e41fa8f390f 100644 --- a/src/bin/functions.sh +++ b/src/bin/functions.sh @@ -1,5 +1,5 @@ # minimum required node version -REQUIRED_NODE_MAJOR=10 +REQUIRED_NODE_MAJOR=12 REQUIRED_NODE_MINOR=13 # minimum required npm version @@ -50,16 +50,6 @@ get_program_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" diff --git a/src/bin/plugins/checkPlugin.js b/src/bin/plugins/checkPlugin.js index 0bf3d25b95b..3c36712caf9 100755 --- a/src/bin/plugins/checkPlugin.js +++ b/src/bin/plugins/checkPlugin.js @@ -263,7 +263,7 @@ fs.readdir(pluginPath, (err, rootFiles) => { console.warn('No engines or node engine in package.json'); if (autoFix) { const engines = { - node: '^10.17.0 || >=11.14.0', + node: '>=12.13.0', }; parsedPackageJSON.engines = engines; writePackageJson(parsedPackageJSON); diff --git a/src/bin/run.sh b/src/bin/run.sh index 4f6993ff5cc..b655e96354a 100755 --- a/src/bin/run.sh +++ b/src/bin/run.sh @@ -32,4 +32,4 @@ src/bin/installDeps.sh "$@" || exit 1 # Move to the node folder and start log "Starting Etherpad..." -exec node $(compute_node_args) src/node/server.js "$@" +exec node src/node/server.js "$@" diff --git a/src/node/server.js b/src/node/server.js index f895df2af90..5fef8f33079 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -41,8 +41,8 @@ if (settings.dumpOnUncleanExit) { * any modules that require newer versions of NodeJS */ const NodeVersion = require('./utils/NodeVersion'); -NodeVersion.enforceMinNodeVersion('10.17.0'); -NodeVersion.checkDeprecationStatus('10.17.0', '1.8.8'); +NodeVersion.enforceMinNodeVersion('12.13.0'); +NodeVersion.checkDeprecationStatus('12.13.0', '1.8.14'); const UpdateCheck = require('./utils/UpdateCheck'); const db = require('./db/DB'); diff --git a/src/package.json b/src/package.json index 7752ced484d..9b981cf5d0b 100644 --- a/src/package.json +++ b/src/package.json @@ -235,7 +235,7 @@ "root": true }, "engines": { - "node": "^10.17.0 || >=11.14.0", + "node": ">=12.13.0", "npm": ">=5.5.1" }, "repository": { From 44343e5c5ec04f228d794ae5e69ab7c7f1eaa3ff Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 14 Jun 2021 13:50:16 -0400 Subject: [PATCH 195/218] tests: Replace Node.js v15 with v16 --- .github/workflows/backend-tests.yml | 4 ++-- .github/workflows/frontend-admin-tests.yml | 2 +- .github/workflows/upgrade-from-latest-release.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index 0b12eda686b..e88b3273144 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - node: [12, 14, 15] + node: [12, 14, 16] steps: - name: Checkout repository @@ -50,7 +50,7 @@ jobs: strategy: fail-fast: false matrix: - node: [12, 14, 15] + node: [12, 14, 16] steps: - name: Checkout repository diff --git a/.github/workflows/frontend-admin-tests.yml b/.github/workflows/frontend-admin-tests.yml index 98385e9f8c1..44cb697f2dc 100644 --- a/.github/workflows/frontend-admin-tests.yml +++ b/.github/workflows/frontend-admin-tests.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - node: [12, 14, 15] + node: [12, 14, 16] steps: - name: Generate Sauce Labs strings diff --git a/.github/workflows/upgrade-from-latest-release.yml b/.github/workflows/upgrade-from-latest-release.yml index 171dfc4eab7..e57e63ebb66 100644 --- a/.github/workflows/upgrade-from-latest-release.yml +++ b/.github/workflows/upgrade-from-latest-release.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - node: [12, 14, 15] + node: [12, 14, 16] steps: - name: Check out latest release From 7ca336c28e19632632f2c825b11114372bd0364d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 14 Jun 2021 14:36:17 -0400 Subject: [PATCH 196/218] lint: Update eslint-config-etherpad and friends --- src/bin/plugins/checkPlugin.js | 11 ++++++----- src/package-lock.json | 31 +++++++++++++++++++++---------- src/package.json | 8 ++++---- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/bin/plugins/checkPlugin.js b/src/bin/plugins/checkPlugin.js index 3c36712caf9..4876c13629f 100755 --- a/src/bin/plugins/checkPlugin.js +++ b/src/bin/plugins/checkPlugin.js @@ -220,14 +220,15 @@ fs.readdir(pluginPath, (err, rootFiles) => { } updateDeps(parsedPackageJSON, 'devDependencies', { - 'eslint': '^7.20.0', - 'eslint-config-etherpad': '^1.0.25', + 'eslint': '^7.28.0', + 'eslint-config-etherpad': '^2.0.0', + 'eslint-plugin-cypress': '^2.11.3', 'eslint-plugin-eslint-comments': '^3.2.0', - 'eslint-plugin-mocha': '^8.0.0', + 'eslint-plugin-mocha': '^9.0.0', 'eslint-plugin-node': '^11.1.0', 'eslint-plugin-prefer-arrow': '^1.2.3', - 'eslint-plugin-promise': '^4.3.1', - 'eslint-plugin-you-dont-need-lodash-underscore': '^6.11.0', + 'eslint-plugin-promise': '^5.1.0', + 'eslint-plugin-you-dont-need-lodash-underscore': '^6.12.0', }); updateDeps(parsedPackageJSON, 'peerDependencies', { diff --git a/src/package-lock.json b/src/package-lock.json index 10680fa9416..28b182bd9c8 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1936,9 +1936,9 @@ } }, "eslint-config-etherpad": { - "version": "1.0.27", - "resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-1.0.27.tgz", - "integrity": "sha512-WLo7WOazNiPirTpJkJcUK9LTv0/EdwrzJuP7NljaAsA1h9f5/VhMHKnrcmXyOv2255mjpG52ouidvZLcD356Hw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-2.0.0.tgz", + "integrity": "sha512-ejBTLZiXkreSHNsdHWk/vCRkieYb6CpVZb/DH2QKbYktqRN/EFgaSISLb/8n8HZA5XvLVLbRDvDyBc/h3tIEcA==", "dev": true }, "eslint-plugin-cypress": { @@ -1987,13 +1987,24 @@ } }, "eslint-plugin-mocha": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-8.2.0.tgz", - "integrity": "sha512-8oOR47Ejt+YJPNQzedbiklDqS1zurEaNrxXpRs+Uk4DMDPVmKNagShFeUaYsfvWP55AhI+P1non5QZAHV6K78A==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-9.0.0.tgz", + "integrity": "sha512-d7knAcQj1jPCzZf3caeBIn3BnW6ikcvfz0kSqQpwPYcVGLoJV5sz0l0OJB2LR8I7dvTDbqq1oV6ylhSgzA10zg==", "dev": true, "requires": { - "eslint-utils": "^2.1.0", + "eslint-utils": "^3.0.0", "ramda": "^0.27.1" + }, + "dependencies": { + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + } + } } }, "eslint-plugin-node": { @@ -7755,9 +7766,9 @@ } }, "regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, "rehype": { diff --git a/src/package.json b/src/package.json index 9b981cf5d0b..3ad2858d42d 100644 --- a/src/package.json +++ b/src/package.json @@ -78,15 +78,15 @@ "etherpad-lite": "node/server.js" }, "devDependencies": { - "eslint": "^7.26.0", - "eslint-config-etherpad": "^1.0.27", + "eslint": "^7.28.0", + "eslint-config-etherpad": "^2.0.0", "eslint-plugin-cypress": "^2.11.3", "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-mocha": "^8.1.0", + "eslint-plugin-mocha": "^9.0.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-promise": "^5.1.0", - "eslint-plugin-you-dont-need-lodash-underscore": "^6.11.0", + "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0", "etherpad-cli-client": "0.0.9", "mocha": "7.1.2", "mocha-froth": "^0.2.10", From 53cca5a743b79ca4374d60185983a5f0f9b04b63 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 16 Jun 2021 01:28:36 -0400 Subject: [PATCH 197/218] PadMessageHandler: Also send `USER_NEWINFO` messages on reconnect Now the user list is correct after a reconnect. This also allows ep_webrtc to automatically recover after a temporary network glitch. --- src/node/handler/PadMessageHandler.js | 103 ++++++++++++-------------- 1 file changed, 46 insertions(+), 57 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 899991a1122..a1194ea9192 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -1121,72 +1121,61 @@ const handleClientReady = async (socket, message, authorID) => { // Save the current revision in sessioninfos, should be the same as in clientVars sessionInfo.rev = pad.getHeadRevisionNumber(); + } - // prepare the notification for the other users on the pad, that this user joined - const messageToTheOtherUsers = { + // Notify other users about this new user. + socket.broadcast.to(padIds.padId).json.send({ + type: 'COLLABROOM', + data: { + type: 'USER_NEWINFO', + userInfo: { + colorId: authorColorId, + name: authorName, + userId: authorID, + }, + }, + }); + + // Notify this new user about other users. + await Promise.all(_getRoomSockets(pad.id).map(async (roomSocket) => { + if (roomSocket.id === socket.id) return; + + // sessioninfos might change while enumerating, so check if the sessionID is still assigned to a + // valid session. + const sessionInfo = sessioninfos[roomSocket.id]; + if (sessionInfo == null) return; + + // get the authorname & colorId + const authorId = sessionInfo.author; + // The authorId of this other user might be unknown if the other user just connected and has + // not yet sent a CLIENT_READY message. + if (authorId == null) return; + + // reuse previously created cache of author's data + const authorInfo = historicalAuthorData[authorId] || await authorManager.getAuthor(authorId); + if (authorInfo == null) { + messageLogger.error( + `Author ${authorId} connected via socket.io session ${roomSocket.id} is missing from ` + + 'the global author database. This should never happen because the author ID is ' + + 'generated by the same code that adds the author to the database.'); + // Don't bother telling the new user about this mystery author. + return; + } + + const msg = { type: 'COLLABROOM', data: { type: 'USER_NEWINFO', userInfo: { - colorId: authorColorId, - userId: authorID, + colorId: authorInfo.colorId, + name: authorInfo.name, + userId: authorId, }, }, }; - // Add the authorname of this new User, if avaiable - if (authorName != null) { - messageToTheOtherUsers.data.userInfo.name = authorName; - } - - // notify all existing users about new user - socket.broadcast.to(padIds.padId).json.send(messageToTheOtherUsers); - - // 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; - } - - // Since sessioninfos might change while being enumerated, check if the - // sessionID is still assigned to a valid session - const sessionInfo = sessioninfos[roomSocket.id]; - if (sessionInfo == null) return; - - // get the authorname & colorId - const authorId = sessionInfo.author; - // The authorId of this other user might be unknown if the other user just connected and has - // not yet sent a CLIENT_READY message. - if (authorId == null) return; - - // reuse previously created cache of author's data - const authorInfo = historicalAuthorData[authorId] || await authorManager.getAuthor(authorId); - if (authorInfo == null) { - messageLogger.error( - `Author ${authorId} connected via socket.io session ${roomSocket.id} is missing from ` + - 'the global author database. This should never happen because the author ID is ' + - 'generated by the same code that adds the author to the database.'); - // Don't bother telling the new user about this mystery author. - return; - } - - // Send the new User a Notification about this other user - const msg = { - type: 'COLLABROOM', - data: { - type: 'USER_NEWINFO', - userInfo: { - colorId: authorInfo.colorId, - name: authorInfo.name, - userId: authorId, - }, - }, - }; - - socket.json.send(msg); - })); - } + socket.json.send(msg); + })); }; /** From 251cc7ab324d138b3f05d03ee3ef70707321678e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 16 Jun 2021 02:05:14 -0400 Subject: [PATCH 198/218] CSS: Fix button icon centering --- src/static/css/pad/icons.css | 6 +++--- src/static/skins/colibris/src/components/buttons.css | 6 ------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/static/css/pad/icons.css b/src/static/css/pad/icons.css index c89d359e07e..0ae7ed2db6a 100644 --- a/src/static/css/pad/icons.css +++ b/src/static/css/pad/icons.css @@ -24,6 +24,9 @@ font-style: normal; font-variant: normal; text-rendering: auto; + display: flex; + justify-content: center; + align-items: center; } .buttonicon:before, [class^="buttonicon-"]:before, [class*=" buttonicon-"]:before { @@ -34,9 +37,6 @@ font-size: 15px; display: inline-block; text-decoration: inherit; - width: 1em; - margin: 0; - text-align: center; /* For safety - reset parent styles, that can break glyph codes*/ font-variant: normal; diff --git a/src/static/skins/colibris/src/components/buttons.css b/src/static/skins/colibris/src/components/buttons.css index ccaea9d4d9a..9a3445478fb 100644 --- a/src/static/skins/colibris/src/components/buttons.css +++ b/src/static/skins/colibris/src/components/buttons.css @@ -23,9 +23,3 @@ button, .btn color: #485365; color: var(--text-color); } - -.buttonicon:before, [class^="buttonicon-"]:before, [class*=" buttonicon-"]:before { - line-height: 18px; -} - - From 485538bd79620f254e1088d84abd8658435fe563 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Thu, 17 Jun 2021 06:14:45 +0200 Subject: [PATCH 199/218] bump wtfnode to fix #5078 --- src/package-lock.json | 74 +++++++++++++++++-------------------------- src/package.json | 2 +- 2 files changed, 30 insertions(+), 46 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 28b182bd9c8..f1cdda7641f 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -38,9 +38,9 @@ } }, "@azure/core-http": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-1.2.5.tgz", - "integrity": "sha512-SjjjqaO9emyn+XM0Qyzt5RsgddOIpGAfhWH6+d8X6/HbhFrtvXLJIz85EMoIO+T4rX3ISStik9MD5LMW9IZg4A==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-1.2.6.tgz", + "integrity": "sha512-odtH7UMKtekc5YQ86xg9GlVHNXR6pq2JgJ5FBo7/jbOjNGdBqcrIVrZx2bevXVJz/uUTSx6vUf62gzTXTfqYSQ==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-asynciterator-polyfill": "^1.0.0", @@ -53,7 +53,7 @@ "node-fetch": "^2.6.0", "process": "^0.11.10", "tough-cookie": "^4.0.0", - "tslib": "^2.0.0", + "tslib": "^2.2.0", "tunnel": "^0.0.6", "uuid": "^8.3.0", "xml2js": "^0.4.19" @@ -146,37 +146,17 @@ } }, "@azure/keyvault-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.1.0.tgz", - "integrity": "sha512-xdz7nkYulWSo2+CsBRxaOe2JrGWQP3WxEjPLdIzZNHyaYBuLlnKHL3XWYH5j0UUVoDcmQClZUlZ2P/kdOiy/aQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.2.1.tgz", + "integrity": "sha512-bO3Dl4cJgOkYSLudmzkSFg4os4gsDvaUozcJ9ZKdqZjIp/RHIZRFytbRcNe40rpKH2iLXcavNGVpMvEzAfERyQ==", "requires": { - "@azure/core-http": "^1.1.6", + "@azure/abort-controller": "^1.0.0", + "@azure/core-http": "^1.2.0", "@azure/core-lro": "^1.0.2", "@azure/core-paging": "^1.1.1", - "@azure/core-tracing": "1.0.0-preview.9", + "@azure/core-tracing": "1.0.0-preview.11", "@azure/logger": "^1.0.0", - "@opentelemetry/api": "^0.10.2", - "tslib": "^2.0.0" - }, - "dependencies": { - "@azure/core-tracing": { - "version": "1.0.0-preview.9", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.9.tgz", - "integrity": "sha512-zczolCLJ5QG42AEPQ+Qg9SRYNUyB+yZ5dzof4YEc+dyWczO9G2sBqbAjLB7IqrsdHN2apkiB2oXeDKCsq48jug==", - "requires": { - "@opencensus/web-types": "0.0.7", - "@opentelemetry/api": "^0.10.2", - "tslib": "^2.0.0" - } - }, - "@opentelemetry/api": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.10.2.tgz", - "integrity": "sha512-GtpMGd6vkzDMYcpu2t9LlhEgMy/SzBwRnz48EejlRArYqZzqSzAsKmegUK7zHgl+EOIaK9mKHhnRaQu3qw20cA==", - "requires": { - "@opentelemetry/context-base": "^0.10.2" - } - } + "tslib": "^2.2.0" } }, "@azure/logger": { @@ -193,9 +173,9 @@ "integrity": "sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw==" }, "@azure/ms-rest-js": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.5.1.tgz", - "integrity": "sha512-2McxhuZy1PRcAsX8PFRD6ELJg1Hr2a1vGvUjcWqVGcfej6MeXplC0P9GzJfNh739fNtzyq0F6ps657o+nVHJpg==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.5.2.tgz", + "integrity": "sha512-9nCuuoYwHZEZw1t0MVtENH+c1k2R4maYAlBBDSZhZu6bEucyfYUUigNXXKjt2cFBt4sO+sTzi0uI0f/fiPFr+Q==", "requires": { "@azure/core-auth": "^1.1.4", "abort-controller": "^3.0.0", @@ -425,11 +405,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.0-rc.0.tgz", "integrity": "sha512-iXKByCMfrlO5S6Oh97BuM56tM2cIBB0XsL/vWF/AtJrJEKx4MC/Xdu0xDsGXMGcNWpqF7ujMsjjnp0+UHBwnDQ==" }, - "@opentelemetry/context-base": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.10.2.tgz", - "integrity": "sha512-hZNKjKOYsckoOEgBziGMnBcX0M7EtstnCmwz5jZUOUYwlZ+/xxX6z3jPu1XVO2Jivk0eLfuP9GP+vFD49CMetw==" - }, "@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -1253,6 +1228,11 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "coffeescript": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz", + "integrity": "sha512-J2jRPX0eeFh5VKyVnoLrfVFgLZtnnmp96WQSLAS8OrLm2wtQLcnikYKe1gViJKDH7vucjuhHvBKKBP3rKcD1tQ==" + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -8083,9 +8063,9 @@ } }, "simple-git": { - "version": "2.39.1", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.39.1.tgz", - "integrity": "sha512-+kEAkyQHsWejYxQNCzTrjvCxJOcijpB49RSs7HV+TK9B9prUq7YBNpFstQvjfGBn4Hecywp4tm+breGGGHlHwA==", + "version": "2.40.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.40.0.tgz", + "integrity": "sha512-7IO/eQwrN5kvS38TTu9ljhG9tx2nn0BTqZOmqpPpp51TvE44YIvLA6fETqEVA8w/SeEfPaVv6mk7Tsk9Jns+ag==", "requires": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", @@ -9130,9 +9110,13 @@ "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" }, "wtfnode": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.8.4.tgz", - "integrity": "sha512-64GEKtMt/MUBuAm+8kHqP74ojjafzu00aT0JKsmkIwYmjRQ/odO0yhbzKLm+Z9v1gMla+8dwITRKzTAlHsB+Og==" + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.9.0.tgz", + "integrity": "sha512-IKHfNAFZwfm0uCt/zuFADN3mHyoB+ZrmwFpRGOxKPIXV0tifqpIaTH3NvImA7yy7GimsAayZGTaNvOmavKzE+A==", + "requires": { + "coffeescript": "^2.5.1", + "source-map-support": "^0.5.19" + } }, "xml2js": { "version": "0.4.23", diff --git a/src/package.json b/src/package.json index 3ad2858d42d..a1bdc4fda9d 100644 --- a/src/package.json +++ b/src/package.json @@ -72,7 +72,7 @@ "ueberdb2": "^1.4.7", "underscore": "1.13.1", "unorm": "1.6.0", - "wtfnode": "^0.8.4" + "wtfnode": "^0.9.0" }, "bin": { "etherpad-lite": "node/server.js" From 4b3e47bd23673e9a0a5428fa89afbfa0ff8257a0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 17 Jun 2021 19:41:44 -0400 Subject: [PATCH 200/218] bin/importSqlFile.js: Read the file one line at a time This avoids running out of memory if the file is large. --- src/bin/importSqlFile.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/bin/importSqlFile.js b/src/bin/importSqlFile.js index b7e3a16c0f2..148503e8de7 100644 --- a/src/bin/importSqlFile.js +++ b/src/bin/importSqlFile.js @@ -48,6 +48,7 @@ const unescape = (val) => { (async () => { const fs = require('fs'); const log4js = require('log4js'); + const readline = require('readline'); const settings = require('../node/utils/Settings'); const ueberDB = require('ueberdb2'); @@ -71,14 +72,12 @@ const unescape = (val) => { await util.promisify(db.init.bind(db))(); log('done'); - log('open output file...'); - const lines = fs.readFileSync(sqlFile, 'utf8').split('\n'); + log(`Opening ${sqlFile}...`); + const stream = fs.createReadStream(sqlFile, {encoding: 'utf8'}); - const count = lines.length; + log(`Reading ${sqlFile}...`); let keyNo = 0; - - process.stdout.write(`Start importing ${count} keys...\n`); - lines.forEach((l) => { + for await (const l of readline.createInterface({input: stream, crlfDelay: Infinity})) { if (l.substr(0, 27) === 'REPLACE INTO store VALUES (') { const pos = l.indexOf("', '"); const key = l.substr(28, pos - 28); @@ -88,11 +87,9 @@ const unescape = (val) => { console.log(`unval: ${unescape(value)}`); db.set(key, unescape(value), null); keyNo++; - if (keyNo % 1000 === 0) { - process.stdout.write(` ${keyNo}/${count}\n`); - } + if (keyNo % 1000 === 0) log(` ${keyNo}`); } - }); + } process.stdout.write('\n'); process.stdout.write('done. waiting for db to finish transaction. ' + 'depended on dbms this may take some time..\n'); From 7bdd0f2f09610b4f3e3f71d7a4432e65df84cbfa Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 18 Jun 2021 04:28:41 -0400 Subject: [PATCH 201/218] bin/updatePlugins.sh: Many refinements * cd to top-level Etherpad directory is now more robust. * Only attempt to update packages whose names begin with `ep_`. * Don't create `package-lock.json`. * Improve logging. * Improve error handling. --- src/bin/updatePlugins.sh | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/bin/updatePlugins.sh b/src/bin/updatePlugins.sh index ce7a46f6bbd..586ba16234b 100755 --- a/src/bin/updatePlugins.sh +++ b/src/bin/updatePlugins.sh @@ -1,20 +1,11 @@ #!/bin/sh - -#Move to the folder where ep-lite is installed -cd $(dirname $0) - -#Was this script started in the bin folder? if yes move out -if [ -d "../bin" ]; then - cd "../" -fi - -# npm outdated --depth=0 | grep -v "^Package" | awk '{print $1}' | xargs npm install $1 --save-dev -OUTDATED=$(npm outdated --depth=0 | grep -v "^Package" | awk '{print $1}') -# echo $OUTDATED -if test -n "$OUTDATED"; then - echo "Plugins require update, doing this now..." - echo "Updating $OUTDATED" - npm install $OUTDATED --save-dev -else - echo "Plugins are all up to date" -fi +set -e +mydir=$(cd "${0%/*}" && pwd -P) || exit 1 +cd "${mydir}"/../.. +OUTDATED=$(npm outdated --depth=0 | awk '{print $1}' | grep '^ep_') || { + echo "All plugins are up-to-date" + exit 0 +} +set -- ${OUTDATED} +echo "Updating plugins: $*" +exec npm install --no-save "$@" From 9fcd86b3cd06761d5dee0d4db6ac99cc68280837 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 18 Jun 2021 00:52:34 -0400 Subject: [PATCH 202/218] Pad: Fix `