diff --git a/CHANGELOG.md b/CHANGELOG.md index 28bc0d2d8..093150284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Develop -- TODO Change to 1.8.x. * ... +# 1.8.6 +* IMPORTANT: This fixes a severe problem with postgresql in 1.8.5 +* SECURITY: Fix authentication and authorization bypass vulnerabilities +* API: Update version to 1.2.15 +* FEATURE: Add copyPadWithoutHistory API (#4295) +* FEATURE: Package more asset files to save http requests (#4286) +* MINOR: Improve UI when reconnecting +* TESTS: Improve tests + # 1.8.5 * IMPORTANT DROP OF SUPPORT: Drop support for IE. Browsers now need async/await. * IMPORTANT SECURITY: Rate limit Commits when env=production diff --git a/src/locales/de.json b/src/locales/de.json index bcee159f2..7be360596 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -51,7 +51,7 @@ "pad.settings.fontType.normal": "Normal", "pad.settings.language": "Sprache:", "pad.settings.about": "Über", - "pad.settings.poweredBy": "Powered by $1", + "pad.settings.poweredBy": "Powered by", "pad.importExport.import_export": "Import/Export", "pad.importExport.import": "Textdatei oder Dokument hochladen", "pad.importExport.importSuccessful": "Erfolgreich!", diff --git a/src/locales/is.json b/src/locales/is.json index 0d7fd5afa..394503f1a 100644 --- a/src/locales/is.json +++ b/src/locales/is.json @@ -41,6 +41,8 @@ "pad.settings.fontType": "Leturgerð:", "pad.settings.fontType.normal": "Venjulegt", "pad.settings.language": "Tungumál:", + "pad.settings.about": "Um hugbúnaðinn", + "pad.settings.poweredBy": "Keyrt með", "pad.importExport.import_export": "Flytja inn/út", "pad.importExport.import": "Settu inn hverskyns texta eða skjal", "pad.importExport.importSuccessful": "Heppnaðist!", @@ -51,7 +53,7 @@ "pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Open Document Format)", - "pad.importExport.abiword.innerHTML": "Þú getur aðeins flutt inn úr hreinum texta eða HTML sniðum. Til að geta nýtt \nfleiri þróaðri innflutningssnið settu þá upp AbiWord forritið.", + "pad.importExport.abiword.innerHTML": "Þú getur aðeins flutt inn úr hreinum texta eða HTML sniðum. Til að geta nýtt \nfleiri þróaðri innflutningssnið settu þá upp AbiWord forritið eða LibreOffice.", "pad.modals.connected": "Tengt.", "pad.modals.reconnecting": "Endurtengist skrifblokkinni þinni...", "pad.modals.forcereconnect": "Þvinga endurtengingu", @@ -119,7 +121,7 @@ "pad.userlist.guest": "Gestur", "pad.userlist.deny": "Hafna", "pad.userlist.approve": "Samþykkja", - "pad.editbar.clearcolors": "Hreinsa liti höfunda á öllu skjalinu?", + "pad.editbar.clearcolors": "Hreinsa liti höfunda á öllu skjalinu? Þetta er ekki hægt að afturkalla", "pad.impexp.importbutton": "Flytja inn núna", "pad.impexp.importing": "Flyt inn...", "pad.impexp.confirmimport": "Innflutningur á skrá mun skrifa yfir þann texta sem er á skrifblokkinni núna. \nErtu viss um að þú viljir halda áfram?", diff --git a/src/locales/pt.json b/src/locales/pt.json index 3e0345ce2..b770bd15d 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -9,6 +9,7 @@ "Luckas", "Macofe", "Mansil alfalb", + "MuratTheTurkish", "Ti4goc", "Tuliouel", "Waldir", @@ -110,7 +111,7 @@ "timeslider.toolbar.exportlink.title": "Exportar", "timeslider.exportCurrent": "Exportar versão atual como:", "timeslider.version": "Versão {{version}}", - "timeslider.saved": "Gravado a {{day}} de {{month}} de {{ano}}", + "timeslider.saved": "Gravado a {{day}} de {{month}} de {{year}}", "timeslider.playPause": "Reproduzir / pausar conteúdo da nota", "timeslider.backRevision": "Voltar a uma revisão anterior desta nota", "timeslider.forwardRevision": "Avançar para uma revisão posterior desta nota", diff --git a/src/locales/sv.json b/src/locales/sv.json index 07f4460b4..fcef0a10c 100644 --- a/src/locales/sv.json +++ b/src/locales/sv.json @@ -1,6 +1,7 @@ { "@metadata": { "authors": [ + "Bengtsson96", "Jopparn", "Lokal Profil", "Sabelöga", @@ -15,7 +16,7 @@ "pad.toolbar.underline.title": "Understruken (Ctrl+U)", "pad.toolbar.strikethrough.title": "Genomstruken (Ctrl+5)", "pad.toolbar.ol.title": "Numrerad lista (Ctrl+Shift+N)", - "pad.toolbar.ul.title": "Onumrerad lista (Ctrl+Shift+L)", + "pad.toolbar.ul.title": "Punktlista (Ctrl+Shift+L)", "pad.toolbar.indent.title": "Öka indrag (TABB)", "pad.toolbar.unindent.title": "Minska indrag (Shift+TABB)", "pad.toolbar.undo.title": "Ångra (Ctrl+Z)", @@ -29,7 +30,7 @@ "pad.toolbar.showusers.title": "Visa användarna på detta block", "pad.colorpicker.save": "Spara", "pad.colorpicker.cancel": "Avbryt", - "pad.loading": "Läser in...", + "pad.loading": "Läser in …", "pad.noCookie": "Kunde inte hitta några kakor. Var god tillåt kakor i din webbläsare! Din session och inställningar kommer inte sparas mellan dina besök. Detta kan bero på att Etherpad inte ligger inuti en iFrame i vissa webbläsare. Se till att Etherpad är i samma underdomän/domän som det överordnade iFrame-elementet.", "pad.passwordRequired": "Du behöver ett lösenord för att få tillgång till detta block", "pad.permissionDenied": "Du har inte åtkomstbehörighet för detta block", @@ -58,7 +59,7 @@ "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.abiword.innerHTML": "Du kan endast importera från oformaterad text eller HTML-format. För mer avancerade importfunktioner, var god installera AbiWord eller LibreOffice.", "pad.modals.connected": "Ansluten.", - "pad.modals.reconnecting": "Återansluter till ditt block...", + "pad.modals.reconnecting": "Återansluter till ditt block …", "pad.modals.forcereconnect": "Tvinga återanslutning", "pad.modals.reconnecttimer": "Försöker ansluta igen", "pad.modals.cancel": "Avbryt", @@ -129,7 +130,7 @@ "pad.userlist.approve": "Godkänn", "pad.editbar.clearcolors": "Rensa författarfärger för hela dokumentet? Detta kan inte ångras", "pad.impexp.importbutton": "Importera nu", - "pad.impexp.importing": "Importerar...", + "pad.impexp.importing": "Importerar …", "pad.impexp.confirmimport": "Att importera en fil kommer att skriva över den aktuella texten i blocket. Är du säker på att du vill fortsätta?", "pad.impexp.convertFailed": "Vi kunde inte importera denna fil. Var god använd ett annat dokumentformat eller kopiera och klistra in den manuellt", "pad.impexp.padHasData": "Vi kunde inte importera denna fil eftersom detta block redan har redigerats. Importera den till ett nytt block.", diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index 5f7df1e24..5ba43c462 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -72,7 +72,7 @@ exports.findAuthorID = async (groupID, sessionCookie) => { return undefined; }); const now = Math.floor(Date.now() / 1000); - const isMatch = (si) => (si != null && si.groupID === groupID && si.validUntil <= now); + const isMatch = (si) => (si != null && si.groupID === groupID && now < si.validUntil); const sessionInfo = await promises.firstSatisfies(sessionInfoPromises, isMatch); if (sessionInfo == null) return undefined; return sessionInfo.authorID; diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index 647cbbc8d..e265ee68e 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -7,92 +7,37 @@ * express-session, which can't actually use promises anyway. */ -var Store = require('ep_etherpad-lite/node_modules/express-session').Store, - db = require('ep_etherpad-lite/node/db/DB').db, - log4js = require('ep_etherpad-lite/node_modules/log4js'), - messageLogger = log4js.getLogger("SessionStore"); +const DB = require('ep_etherpad-lite/node/db/DB'); +const Store = require('ep_etherpad-lite/node_modules/express-session').Store; +const log4js = require('ep_etherpad-lite/node_modules/log4js'); -var SessionStore = module.exports = function SessionStore() {}; +const logger = log4js.getLogger('SessionStore'); -SessionStore.prototype.__proto__ = Store.prototype; - -SessionStore.prototype.get = function(sid, fn) { - messageLogger.debug('GET ' + sid); - - var self = this; - - db.get("sessionstorage:" + sid, function(err, sess) { - if (sess) { - sess.cookie.expires = 'string' == typeof sess.cookie.expires ? new Date(sess.cookie.expires) : sess.cookie.expires; - if (!sess.cookie.expires || new Date() < sess.cookie.expires) { - fn(null, sess); +module.exports = class SessionStore extends Store { + get(sid, fn) { + logger.debug('GET ' + sid); + DB.db.get('sessionstorage:' + sid, (err, sess) => { + if (sess) { + sess.cookie.expires = ('string' == typeof sess.cookie.expires + ? new Date(sess.cookie.expires) : sess.cookie.expires); + if (!sess.cookie.expires || new Date() < sess.cookie.expires) { + fn(null, sess); + } else { + this.destroy(sid, fn); + } } else { - self.destroy(sid, fn); - } - } else { - fn(); - } - }); -}; - -SessionStore.prototype.set = function(sid, sess, fn) { - messageLogger.debug('SET ' + sid); - - db.set("sessionstorage:" + sid, sess); - if (fn) { - process.nextTick(fn); - } -}; - -SessionStore.prototype.destroy = function(sid, fn) { - messageLogger.debug('DESTROY ' + sid); - - db.remove("sessionstorage:" + sid); - if (fn) { - process.nextTick(fn); - } -}; - -/* - * RPB: the following methods are optional requirements for a compatible session - * store for express-session, but in any case appear to depend on a - * non-existent feature of ueberdb2 - */ -if (db.forEach) { - SessionStore.prototype.all = function(fn) { - messageLogger.debug('ALL'); - - var sessions = []; - - db.forEach(function(key, value) { - if (key.substr(0,15) === "sessionstorage:") { - sessions.push(value); + fn(); } }); - fn(null, sessions); - }; + } - SessionStore.prototype.clear = function(fn) { - messageLogger.debug('CLEAR'); + set(sid, sess, fn) { + logger.debug('SET ' + sid); + DB.db.set('sessionstorage:' + sid, sess, fn); + } - db.forEach(function(key, value) { - if (key.substr(0,15) === "sessionstorage:") { - db.remove("session:" + key); - } - }); - if (fn) fn(); - }; - - SessionStore.prototype.length = function(fn) { - messageLogger.debug('LENGTH'); - - var i = 0; - - db.forEach(function(key, value) { - if (key.substr(0,15) === "sessionstorage:") { - i++; - } - }); - fn(null, i); + destroy(sid, fn) { + logger.debug('DESTROY ' + sid); + DB.db.remove('sessionstorage:' + sid, fn); } }; diff --git a/src/node/utils/promises.js b/src/node/utils/promises.js index a754823a0..bb973befa 100644 --- a/src/node/utils/promises.js +++ b/src/node/utils/promises.js @@ -35,27 +35,21 @@ exports.firstSatisfies = (promises, predicate) => { return Promise.race(newPromises); }; -exports.timesLimit = function(ltMax, concurrency, promiseCreator) { - var done = 0 - var current = 0 - - function addAnother () { - function _internalRun () { - done++ - - if (done < ltMax) { - addAnother() - } - } - - promiseCreator(current) - .then(_internalRun) - .catch(_internalRun) - - current++ - } - - for (var i = 0; i < concurrency && i < ltMax; i++) { - addAnother() +// Calls `promiseCreator(i)` a total number of `total` times, where `i` is 0 through `total - 1` (in +// order). The `concurrency` argument specifies the maximum number of Promises returned by +// `promiseCreator` that are allowed to be active (unresolved) simultaneously. (In other words: If +// `total` is greater than `concurrency`, then `concurrency` Promises will be created right away, +// and each remaining Promise will be created once one of the earlier Promises resolves.) This async +// function resolves once all `total` Promises have resolved. +exports.timesLimit = async (total, concurrency, promiseCreator) => { + if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive'); + let next = 0; + const addAnother = () => promiseCreator(next++).finally(() => { + if (next < total) return addAnother(); + }); + const promises = []; + for (var i = 0; i < concurrency && i < total; i++) { + promises.push(addAnother()); } + await Promise.all(promises); } diff --git a/src/package.json b/src/package.json index dfb350583..b01e07a02 100644 --- a/src/package.json +++ b/src/package.json @@ -92,10 +92,9 @@ "url": "https://github.com/ether/etherpad-lite.git" }, "scripts": { - "test": "nyc wtfnode node_modules/.bin/_mocha --timeout 5000 --recursive ../tests/backend/specs", - "test-contentcollector": "nyc mocha --timeout 5000 ../tests/backend/specs", + "test": "nyc wtfnode node_modules/.bin/_mocha --timeout 5000 --recursive ../tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", "test-container": "nyc mocha --timeout 5000 ../tests/container/specs/api" }, - "version": "1.8.5", + "version": "1.8.6", "license": "Apache-2.0" } diff --git a/src/static/css/pad/layout.css b/src/static/css/pad/layout.css index 7b64936d7..e5b79c268 100644 --- a/src/static/css/pad/layout.css +++ b/src/static/css/pad/layout.css @@ -47,8 +47,6 @@ body { width: 0; /* hide when the container is empty */ } -@media only screen and (max-width: 800px) { - #editorcontainerbox { - margin-bottom: 39px; /* Leave space for the bottom toolbar on mobile */ - } +.mobile-layout #editorcontainerbox { + margin-bottom: 39px; /* Leave space for the bottom toolbar on mobile */ } diff --git a/src/static/css/pad/popup.css b/src/static/css/pad/popup.css index 00fc8ca51..0eb000996 100644 --- a/src/static/css/pad/popup.css +++ b/src/static/css/pad/popup.css @@ -78,9 +78,11 @@ .popup#users .popup-content { overflow: visible; } - /* Move popup to the bottom, except popup linked to left toolbar, like hyperklink popup */ - .popup:not(.toolbar-popup) { - top: auto; - bottom: 1rem; - } -} \ No newline at end of file +} +/* Move popup to the bottom, except popup linked to left toolbar, like hyperklink popup */ +.mobile-layout .popup:not(.toolbar-popup) { + top: auto; + left: 1rem; + right: auto; + bottom: 1rem; +} diff --git a/src/static/css/pad/popup_users.css b/src/static/css/pad/popup_users.css index 8b6ba82bc..ce4d14365 100644 --- a/src/static/css/pad/popup_users.css +++ b/src/static/css/pad/popup_users.css @@ -98,13 +98,15 @@ input#myusernameedit:not(.editable) { right: calc(100% + 15px); z-index: 101; } -@media (max-width: 800px) { - #mycolorpicker.popup { - top: auto; - bottom: 0; - left: auto !important; - right: 0 !important; - } +.mobile-layout #users.popup { + right: 1rem; + left: auto; +} +.mobile-layout #mycolorpicker.popup { + top: auto; + bottom: 0; + left: auto !important; + right: 0 !important; } #mycolorpicker.popup .btn-container { margin-top: 10px; diff --git a/src/static/css/pad/toolbar.css b/src/static/css/pad/toolbar.css index bc258510a..1a398b199 100644 --- a/src/static/css/pad/toolbar.css +++ b/src/static/css/pad/toolbar.css @@ -139,37 +139,40 @@ .toolbar ul li.separator { width: 5px; } - /* menu_right act like a new toolbar on the bottom of the screen */ - .toolbar .menu_right { - position: fixed; - bottom: 0; - right: 0; - left: 0; - border-top: 1px solid #ccc; - background-color: #f4f4f4; - padding: 0 5px 5px 5px; - } - .toolbar ul.menu_right > li { - margin-right: 8px; - } - .toolbar ul.menu_right > li.separator { - display: none; - } - .toolbar ul.menu_right > li a { - border: none; - background-color: transparent; - margin-left: 5px; - } - .toolbar ul.menu_right > li[data-key="showusers"] { - position: absolute; - right: 0; - top: 0; - bottom: 0; - margin: 0; - } - .toolbar ul.menu_right > li[data-key="showusers"] a { - height: 100%; - width: 40px; - border-radius: 0; - } -} \ No newline at end of file +} + +/* menu_right act like a new toolbar on the bottom of the screen */ +.mobile-layout .toolbar .menu_right { + position: fixed; + bottom: 0; + right: 0; + left: 0; + border-top: 1px solid #ccc; + background-color: #f4f4f4; + padding: 0 5px 5px 5px; +} +.mobile-layout .toolbar ul.menu_right > li { + margin-right: 8px; +} +.mobile-layout .toolbar ul.menu_right > li[data-key="showusers"] { + position: absolute; + right: 0; + top: 0; + bottom: 0; + margin: 0; +} +.mobile-layout .toolbar ul.menu_right > li[data-key="showusers"] a { + height: 100%; + width: 40px; + border-radius: 0; +} +.mobile-layout .toolbar ul.menu_right > li.separator { + display: none; +} +.mobile-layout .toolbar ul.menu_right > li a { + border: none; + margin-left: 5px; +} +.mobile-layout .toolbar ul.menu_right > li a:not(.selected) { + background-color: transparent; +} diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 30d223059..0e40bb990 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -317,12 +317,14 @@ var padeditbar = (function() { // reset style $('.toolbar').removeClass('cropped') + $('body').removeClass('mobile-layout'); var menu_left = $('.toolbar .menu_left')[0]; - // on mobile the menu_right get displayed at the bottom of the screen - var isMobileLayout = $('.toolbar .menu_right').css('position') === 'fixed'; - - if (menu_left && menu_left.scrollWidth > $('.toolbar').width() && isMobileLayout) { + var menuRightWidth = 280; // this is approximate, we cannot measure it because on mobileLayour it takes the full width on the bottom of the page + if (menu_left && menu_left.scrollWidth > $('.toolbar').width() - menuRightWidth || $('.toolbar').width() < 1000) { + $('body').addClass('mobile-layout'); + } + if (menu_left && menu_left.scrollWidth > $('.toolbar').width()) { $('.toolbar').addClass('cropped'); } } diff --git a/src/static/skins/colibris/src/components/toolbar.css b/src/static/skins/colibris/src/components/toolbar.css index 91e9991ed..7f3e71403 100644 --- a/src/static/skins/colibris/src/components/toolbar.css +++ b/src/static/skins/colibris/src/components/toolbar.css @@ -131,23 +131,24 @@ } } -@media (max-width: 800px) { - - .toolbar ul li { - margin: 5px 2px; - } - - .toolbar .menu_right { - border-top: 1px solid #d2d2d2; - border-top: var(--toolbar-border); - background-color: #ffffff; - background-color: var(--bg-color); - padding: 0; - } - - .toolbar ul li a:hover { background-color: transparent; } - - .toolbar ul li.separator { margin: 0; display: none; } +.mobile-layout .toolbar ul li { + margin: 5px 2px; +} +.mobile-layout .toolbar ul li.separator { + margin: 0 5px; +} +@media (max-width: 800px) { + .mobile-layout .toolbar ul li.separator { + display: none; + } +} +.mobile-layout .toolbar .menu_right { + border-top: 1px solid #d2d2d2; + border-top: var(--toolbar-border); + background-color: #ffffff; + background-color: var(--bg-color); + padding: 0; +} +.mobile-layout .toolbar ul li a:hover { + /* background-color: transparent; */ } - - diff --git a/src/static/skins/colibris/src/layout.css b/src/static/skins/colibris/src/layout.css index 6385cf140..1ec3886c8 100644 --- a/src/static/skins/colibris/src/layout.css +++ b/src/static/skins/colibris/src/layout.css @@ -46,10 +46,3 @@ border-radius: 0; } } - -@media only screen and (max-width: 800px) { - #editorcontainerbox { - margin-bottom: 39px; /* margin for bottom toolbar */ - } -} - diff --git a/tests/backend/specs/promises.js b/tests/backend/specs/promises.js new file mode 100644 index 000000000..13a8c532a --- /dev/null +++ b/tests/backend/specs/promises.js @@ -0,0 +1,85 @@ +function m(mod) { return __dirname + '/../../../src/' + mod; } + +const assert = require('assert').strict; +const promises = require(m('node/utils/promises')); + +describe('promises.timesLimit', async () => { + let wantIndex = 0; + const testPromises = []; + const makePromise = (index) => { + // Make sure index increases by one each time. + assert.equal(index, wantIndex++); + // Save the resolve callback (so the test can trigger resolution) + // and the promise itself (to wait for resolve to take effect). + const p = {}; + const promise = new Promise((resolve) => { + p.resolve = resolve; + }); + p.promise = promise; + testPromises.push(p); + return p.promise; + }; + + const total = 11; + const concurrency = 7; + const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise); + + it('honors concurrency', async () => { + assert.equal(wantIndex, concurrency); + }); + + it('creates another when one completes', async () => { + const {promise, resolve} = testPromises.shift(); + resolve(); + await promise; + assert.equal(wantIndex, concurrency + 1); + }); + + it('creates the expected total number of promises', async () => { + while (testPromises.length > 0) { + // Resolve them in random order to ensure that the resolution order doesn't matter. + const i = Math.floor(Math.random() * Math.floor(testPromises.length)); + const {promise, resolve} = testPromises.splice(i, 1)[0]; + resolve(); + await promise; + } + assert.equal(wantIndex, total); + }); + + it('resolves', async () => { + await timesLimitPromise; + }); + + it('does not create too many promises if total < concurrency', async () => { + wantIndex = 0; + assert.equal(testPromises.length, 0); + const total = 7; + const concurrency = 11; + const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise); + while (testPromises.length > 0) { + const {promise, resolve} = testPromises.pop(); + resolve(); + await promise; + } + await timesLimitPromise; + assert.equal(wantIndex, total); + }); + + it('accepts total === 0, concurrency > 0', async () => { + wantIndex = 0; + assert.equal(testPromises.length, 0); + await promises.timesLimit(0, concurrency, makePromise); + assert.equal(wantIndex, 0); + }); + + it('accepts total === 0, concurrency === 0', async () => { + wantIndex = 0; + assert.equal(testPromises.length, 0); + await promises.timesLimit(0, 0, makePromise); + assert.equal(wantIndex, 0); + }); + + it('rejects total > 0, concurrency === 0', async () => { + await assert.rejects(promises.timesLimit(total, 0, makePromise), RangeError); + }); +}); diff --git a/tests/frontend/travis/runnerBackend.sh b/tests/frontend/travis/runnerBackend.sh index 680e96f9e..c595dce02 100755 --- a/tests/frontend/travis/runnerBackend.sh +++ b/tests/frontend/travis/runnerBackend.sh @@ -46,6 +46,5 @@ cd src failed=0 npm run test || failed=1 -npm run test-contentcollector || failed=1 exit $failed