Merge branch 'develop' of github.com:ether/etherpad-lite into testings

This commit is contained in:
John McLear 2020-09-22 11:00:11 +01:00
commit ab43b01174
18 changed files with 232 additions and 196 deletions

View file

@ -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

View file

@ -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!",

View file

@ -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ð <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">settu þá upp AbiWord forritið</a>.",
"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ð <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">settu þá upp AbiWord forritið eða LibreOffice</a>.",
"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?",

View file

@ -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",

View file

@ -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 <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">installera AbiWord eller LibreOffice</a>.",
"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.",

View file

@ -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;

View file

@ -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);
}
};

View file

@ -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);
}

View file

@ -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"
}

View file

@ -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 */
}

View file

@ -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;
}
}
}
/* 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;
}

View file

@ -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;

View file

@ -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;
}
}
}
/* 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;
}

View file

@ -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');
}
}

View file

@ -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; */
}

View file

@ -46,10 +46,3 @@
border-radius: 0;
}
}
@media only screen and (max-width: 800px) {
#editorcontainerbox {
margin-bottom: 39px; /* margin for bottom toolbar */
}
}

View file

@ -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);
});
});

View file

@ -46,6 +46,5 @@ cd src
failed=0
npm run test || failed=1
npm run test-contentcollector || failed=1
exit $failed