sync with ether/etherpad-lite

This commit is contained in:
ilmar 2018-05-02 11:08:57 +03:00
parent 64a2e5b7a3
commit c9863f81ad
20 changed files with 491 additions and 95 deletions

View File

@ -1,3 +1,24 @@
# 1.6.5
* SECURITY: Escape data when listing available plugins
* FIX: Fix typo in apicalls.js which prevented importing isValidJSONPName
* FIX: fixed plugin dependency issue
* FIX: Update iframe_editor.css
* FIX: unbreak Safari iOS line wrapping
# 1.6.4
* SECURITY: exploitable /admin access - CVE-2018-9845
* SECURITY: DoS with pad exports - CVE-2018-9327
* SECURITY: Remote Code Execution - CVE-2018-9326
* SECURITY: Pad data leak - CVE-2018-9325
* Fix: Admin redirect URL
* Fix: Various script Fixes
* Fix: Various CSS/Style/Layout fixes
* NEW: Improved Pad contents readability
* NEW: Hook: onAccessCheck
* NEW: SESSIONKEY and APIKey customizable path
* NEW: checkPads script
* NEW: Support "cluster mode"
# 1.6.3 # 1.6.3
* SECURITY: Update ejs * SECURITY: Update ejs
* SECURITY: xss vulnerability when reading window.location.href * SECURITY: xss vulnerability when reading window.location.href

View File

@ -1,4 +1,4 @@
# Developer Guidelines # Contributor Guidelines
(Please talk to people on the mailing list before you change this page, see our section on [how to get in touch](https://github.com/ether/etherpad-lite#get-in-touch)) (Please talk to people on the mailing list before you change this page, see our section on [how to get in touch](https://github.com/ether/etherpad-lite#get-in-touch))
## How to write a bug report ## How to write a bug report
@ -35,7 +35,7 @@ The logfile location is defined in startup script or the log is directly shown i
To make sure everybody is going in the same direction: To make sure everybody is going in the same direction:
* easy to install for admins and easy to use for people * easy to install for admins and easy to use for people
* easy to integrate into other apps, but also usable as standalone * easy to integrate into other apps, but also usable as standalone
* using less resources on server side * lightweight and scalable
* extensible, as much functionality should be extendable with plugins so changes don't have to be done in core. * extensible, as much functionality should be extendable with plugins so changes don't have to be done in core.
Also, keep it maintainable. We don't wanna end up as the monster Etherpad was! Also, keep it maintainable. We don't wanna end up as the monster Etherpad was!
@ -92,3 +92,19 @@ You can build the docs e.g. produce html, using `make docs`. At some point in th
## Testing ## Testing
Front-end tests are found in the `tests/frontend/` folder in the repository. Run them by pointing your browser to `<yourdomainhere>/tests/frontend`. Front-end tests are found in the `tests/frontend/` folder in the repository. Run them by pointing your browser to `<yourdomainhere>/tests/frontend`.
## Things you can help with
Etherpad is much more than software. So if you aren't a developer then worry not, there is still a LOT you can do! A big part of what we do is community engagement. You can help in the following ways
* Triage bugs (applying labels) and confirming their existance
* Testing fixes (simply applying them and seeing if it fixes your issue or not) - Some git experience required
* Notifying large site admins of new releases
* Writing Changelogs for releases
* Creating Windows packages
* Creating releases
* Bumping dependencies periodically and checking they don't break anything
* Write proposals for grants
* Co-Author and Publish CVEs
* Work with SFC to maintain legal side of project
* Maintain TODO page - https://github.com/ether/etherpad-lite/wiki/TODO#IMPORTANT_TODOS
* Replying to messages on IRC / The Mailing list / Emails

View File

@ -17,7 +17,7 @@ Etherpad is a really-real time collaborative editor scalable to thousands of sim
``` ```
curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash - curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash -
sudo apt-get install -y nodejs sudo apt-get install -y nodejs
git clone git://github.com/ether/etherpad-lite.git && cd etherpad-lite && bin/run.sh git clone https://github.com/ether/etherpad-lite.git && cd etherpad-lite && bin/run.sh
``` ```
## GNU/Linux and other UNIX-like systems ## GNU/Linux and other UNIX-like systems

View File

@ -44,5 +44,86 @@
"pad.importExport.import": "Carié n'archivi o document ëd test", "pad.importExport.import": "Carié n'archivi o document ëd test",
"pad.importExport.importSuccessful": "Bele fàit!", "pad.importExport.importSuccessful": "Bele fàit!",
"pad.importExport.export": "Esporté ël feuj atual coma:", "pad.importExport.export": "Esporté ël feuj atual coma:",
"pad.importExport.exportetherpad": "Etherpad" "pad.importExport.exportetherpad": "Etherpad",
"pad.importExport.exporthtml": "HTML",
"pad.importExport.exportplain": "Mach test",
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "A peul mach amporté dij formà ëd test sempi o HTML. Për dle fonsionalità d'amportassion pi avansà, ch'a <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">anstala AbiWord</a>.",
"pad.modals.connected": "Colegà.",
"pad.modals.reconnecting": "Neuva conession a sò feuj...",
"pad.modals.forcereconnect": "Forsé la neuva conession",
"pad.modals.reconnecttimer": "Tentativ ëd neuva conession",
"pad.modals.cancel": "Anulé",
"pad.modals.userdup": "Duvertà an n'àutra fnestra",
"pad.modals.userdup.explanation": "Ës feuj a smija esse duvert an vàire fnestre ansima a st'ordinator.",
"pad.modals.userdup.advice": "Coleghesse torna për dovré costa fnestra.",
"pad.modals.unauth": "Nen autorisà",
"pad.modals.unauth.explanation": "Ij sò përmess a son cangià antramentre ch'a vëdìa costa pàgina. Ch'a sërca ëd coleghesse torna.",
"pad.modals.looping.explanation": "A-i é dij problema ëd comunicassion con ël servent ëd sincronisassion.",
"pad.modals.looping.cause": "Peul desse che chiel a l'é colegasse con un para-feu o un mandatari incompatìbil.",
"pad.modals.initsocketfail": "Ël servent a l'é introvàbil.",
"pad.modals.initsocketfail.explanation": "Impossìbil coleghesse al servent ëd sincronisassion.",
"pad.modals.initsocketfail.cause": "A l'é probàbil che sòn a sia dovù a sò navigador o a soa conession an sl'aragnà.",
"pad.modals.slowcommit.explanation": "Ël servent a rëspond nen.",
"pad.modals.slowcommit.cause": "Sòn a podrìa esse dovù a dij problema ëd conession a l'aragnà.",
"pad.modals.badChangeset.explanation": "Na modìfica ch'a l'ha fàit a l'é stàita cassificà tanme ilegal dal servent ëd sincronisassion.",
"pad.modals.badChangeset.cause": "Sòn a podrìa esse dovù a na bruta configurassion dël servent o a chèich àutr comportament nen ëspetà. Për piasì, ch'a contata l'aministrator dël servissi, s'a pensa ch'a sia n'eror. Ch'a preuva a rintré torna ant ël sistema për andé anans a modifiché.",
"pad.modals.corruptPad.explanation": "Ël feuj al qual a sërca d'acede a l'é corompù.",
"pad.modals.corruptPad.cause": "Sòn a podrìa esse dovù a na configurassion ësbalià dël servent o a chèich àutr comportament nen ëspetà. Për piasì, ch'a contata l'aministrator dël servissi.",
"pad.modals.deleted": "Dëscancelà.",
"pad.modals.deleted.explanation": "Ës feuj a l'é stàit eliminà.",
"pad.modals.disconnected": "A l'é stàit dëscolegà",
"pad.modals.disconnected.explanation": "La conession al servent a l'é perdusse",
"pad.modals.disconnected.cause": "Ël servent a podrìa esse indisponìbil. Për piasì, ch'a anforma l'aministrator dël servissi si ël problema a persist.",
"pad.share": "Partagé 's feuj",
"pad.share.readonly": "Mach letura",
"pad.share.link": "Liura",
"pad.share.emebdcode": "Ancorporé na liura",
"pad.chat": "Ciaciarada",
"pad.chat.title": "Duverté la ciaciarada për cost feuj.",
"pad.chat.loadmessages": "Carié pi 'd mëssagi",
"timeslider.pageTitle": "Stòria dinàmica ëd {{appTitle}}",
"timeslider.toolbar.returnbutton": "Torné al feuj",
"timeslider.toolbar.authors": "Autor:",
"timeslider.toolbar.authorsList": "Gnun autor",
"timeslider.toolbar.exportlink.title": "Esporté",
"timeslider.exportCurrent": "Esporté la version corenta tanme:",
"timeslider.version": "Version {{version}}",
"timeslider.saved": "Argistrà ai {{day}} {{month}} {{year}}",
"timeslider.playPause": "Letura / Pàusa dij contnù dël feuj",
"timeslider.backRevision": "Andé andaré ëd na revision ant ës feuj",
"timeslider.forwardRevision": "Andé anans ëd na revision ant ëd feuj",
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
"timeslider.month.january": "Gené",
"timeslider.month.february": "Fërvé",
"timeslider.month.march": "Mars",
"timeslider.month.april": "Avril",
"timeslider.month.may": "Maj",
"timeslider.month.june": "Giugn",
"timeslider.month.july": "Luj",
"timeslider.month.august": "Ost",
"timeslider.month.september": "Stèmber",
"timeslider.month.october": "Otóber",
"timeslider.month.november": "Novèmber",
"timeslider.month.december": "Dzèmber",
"timeslider.unnamedauthors": "{{num}} {[plural(num) one: autor anònim, other: autor anònim ]}",
"pad.savedrevs.marked": "Sa revision a l'é adess marcà tanme revision argistrà",
"pad.savedrevs.timeslider": "A peul vëdde le revision argistrà an visitand la stòria",
"pad.userlist.entername": "Ch'a buta sò nòm",
"pad.userlist.unnamed": "anònim",
"pad.userlist.guest": "Anvità",
"pad.userlist.deny": "Arfudé",
"pad.userlist.approve": "Aprové",
"pad.editbar.clearcolors": "Dëscancelé ij color ëd paternità dj'autor an tut ël document?",
"pad.impexp.importbutton": "Amporté adess",
"pad.impexp.importing": "An camin ch'as ampòrta...",
"pad.impexp.confirmimport": "Amportand n'archivi as dëscancelërà ël test corent dël feuj. É-lo sigur ëd vorèj felo?",
"pad.impexp.convertFailed": "I l'oma nen podù amporté s'archivi. Për piasì, ch'a deuvra n'àutr formà ëd document o ch'a còpia e ancòla a man",
"pad.impexp.padHasData": "I l'oma nen podù amporté s'archivi përché 's feuj a l'ha già avù dle modìfiche; për piasì, ch'a ampòrta un feuj neuv",
"pad.impexp.uploadFailed": "Ël cariament a l'ha falì, për piasì ch'a preuva torna",
"pad.impexp.importfailed": "Amportassion falìa",
"pad.impexp.copypaste": "Për piasì, ch'a còpia e ancòla",
"pad.impexp.exportdisabled": "L'esportassion an formà {{type}} a l'é disativà. Për piasì, ch'a contata sò aministrator ëd sistema për ij detaj."
} }

View File

@ -117,7 +117,7 @@
"timeslider.month.october": "10月", "timeslider.month.october": "10月",
"timeslider.month.november": "11月", "timeslider.month.november": "11月",
"timeslider.month.december": "12月", "timeslider.month.december": "12月",
"timeslider.unnamedauthors": "{{num}}匿名{[plural(num) 作者]}", "timeslider.unnamedauthors": "{{num}}匿名{[plural(num) one:作者, other:作者]}",
"pad.savedrevs.marked": "標記此修訂版本為已儲存修訂版本。", "pad.savedrevs.marked": "標記此修訂版本為已儲存修訂版本。",
"pad.savedrevs.timeslider": "您可使用時段滑標來查看先前保存的版本內容", "pad.savedrevs.timeslider": "您可使用時段滑標來查看先前保存的版本內容",
"pad.userlist.entername": "輸入您的姓名", "pad.userlist.entername": "輸入您的姓名",

View File

@ -1153,6 +1153,101 @@ function handleClientReady(client, message)
client.join(padIds.padId); client.join(padIds.padId);
//Save the revision in sessioninfos, we take the revision from the info the client send to us //Save the revision in sessioninfos, we take the revision from the info the client send to us
sessioninfos[client.id].rev = message.client_rev; sessioninfos[client.id].rev = message.client_rev;
//During the client reconnect, client might miss some revisions from other clients. By using client revision,
//this below code sends all the revisions missed during the client reconnect
var revisionsNeeded = [];
var changesets = {};
var startNum = message.client_rev + 1;
var endNum = pad.getHeadRevisionNumber() + 1;
async.series([
//push all the revision numbers needed into revisionsNeeded array
function(callback)
{
var headNum = pad.getHeadRevisionNumber();
if (endNum > headNum+1)
endNum = headNum+1;
if (startNum < 0)
startNum = 0;
for(var r=startNum;r<endNum;r++)
{
revisionsNeeded.push(r);
changesets[r] = {};
}
callback();
},
//get changesets needed for pending revisions
function(callback)
{
async.eachSeries(revisionsNeeded, function(revNum, callback)
{
pad.getRevisionChangeset(revNum, function(err, value)
{
if(ERR(err)) return;
changesets[revNum]['changeset'] = value;
callback();
});
}, callback);
},
//get author for each changeset
function(callback)
{
async.eachSeries(revisionsNeeded, function(revNum, callback)
{
pad.getRevisionAuthor(revNum, function(err, value)
{
if(ERR(err)) return;
changesets[revNum]['author'] = value;
callback();
});
}, callback);
},
//get timestamp for each changeset
function(callback)
{
async.eachSeries(revisionsNeeded, function(revNum, callback)
{
pad.getRevisionDate(revNum, function(err, value)
{
if(ERR(err)) return;
changesets[revNum]['timestamp'] = value;
callback();
});
}, callback);
}
],
//return error and pending changesets
function(err)
{
if(ERR(err, callback)) return;
async.eachSeries(revisionsNeeded, function(r, callback)
{
var forWire = Changeset.prepareForWire(changesets[r]['changeset'], pad.pool);
var wireMsg = {"type":"COLLABROOM",
"data":{type:"CLIENT_RECONNECT",
headRev:pad.getHeadRevisionNumber(),
newRev:r,
changeset:forWire.translated,
apool: forWire.pool,
author: changesets[r]['author'],
currentTime: changesets[r]['timestamp']
}};
client.json.send(wireMsg);
callback();
});
if (startNum == endNum)
{
var Msg = {"type":"COLLABROOM",
"data":{type:"CLIENT_RECONNECT",
noChanges: true,
newRev: pad.getHeadRevisionNumber()
}};
client.json.send(Msg);
}
});
} }
//This is a normal first connect //This is a normal first connect
else else

View File

@ -25,6 +25,10 @@ exports.createServer = function () {
else{ else{
console.warn("Admin username and password not set in settings.json. To access admin please uncomment and edit 'users' in settings.json"); console.warn("Admin username and password not set in settings.json. To access admin please uncomment and edit 'users' in settings.json");
} }
var env = process.env.NODE_ENV || 'development';
if(env !== 'production'){
console.warn("Etherpad is running in Development mode. This mode is slower for users and less secure than production mode. You should set the NODE_ENV environment variable to production by using: export NODE_ENV=production");
}
} }
exports.restartServer = function () { exports.restartServer = function () {

View File

@ -3,7 +3,7 @@ var apiLogger = log4js.getLogger("API");
var clientLogger = log4js.getLogger("client"); var clientLogger = log4js.getLogger("client");
var formidable = require('formidable'); var formidable = require('formidable');
var apiHandler = require('../../handler/APIHandler'); var apiHandler = require('../../handler/APIHandler');
var isVarName = require('is-var-name'); var isValidJSONPName = require('./isValidJSONPName');
//This is for making an api call, collecting all post information and passing it to the apiHandler //This is for making an api call, collecting all post information and passing it to the apiHandler
var apiCaller = function(req, res, fields) { var apiCaller = function(req, res, fields) {
@ -19,7 +19,7 @@ var apiCaller = function(req, res, fields) {
apiLogger.info("RESPONSE, " + req.params.func + ", " + response); apiLogger.info("RESPONSE, " + req.params.func + ", " + response);
//is this a jsonp call, if yes, add the function call //is this a jsonp call, if yes, add the function call
if(req.query.jsonp && isVarName(req.query.jsonp)) if(req.query.jsonp && isValidJSONPName.check(req.query.jsonp))
response = req.query.jsonp + "(" + response + ")"; response = req.query.jsonp + "(" + response + ")";
res._____send(response); res._____send(response);

View File

@ -2,6 +2,7 @@ var hasPadAccess = require("../../padaccess");
var settings = require('../../utils/Settings'); var settings = require('../../utils/Settings');
var exportHandler = require('../../handler/ExportHandler'); var exportHandler = require('../../handler/ExportHandler');
var importHandler = require('../../handler/ImportHandler'); var importHandler = require('../../handler/ImportHandler');
var padManager = require("../../db/PadManager");
exports.expressCreateServer = function (hook_name, args, cb) { exports.expressCreateServer = function (hook_name, args, cb) {
args.app.get('/p/:pad/:rev?/export/:type', function(req, res, next) { args.app.get('/p/:pad/:rev?/export/:type', function(req, res, next) {
@ -22,14 +23,29 @@ exports.expressCreateServer = function (hook_name, args, cb) {
res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Origin", "*");
hasPadAccess(req, res, function() { hasPadAccess(req, res, function() {
console.log('req.params.pad', req.params.pad);
padManager.doesPadExists(req.params.pad, function(err, exists)
{
if(!exists) {
return next();
}
exportHandler.doExport(req, res, req.params.pad, req.params.type); exportHandler.doExport(req, res, req.params.pad, req.params.type);
}); });
}); });
});
//handle import requests //handle import requests
args.app.post('/p/:pad/import', function(req, res, next) { args.app.post('/p/:pad/import', function(req, res, next) {
hasPadAccess(req, res, function() { hasPadAccess(req, res, function() {
padManager.doesPadExists(req.params.pad, function(err, exists)
{
if(!exists) {
return next();
}
importHandler.doImport(req, res, req.params.pad); importHandler.doImport(req, res, req.params.pad);
}); });
}); });
});
} }

View File

@ -0,0 +1,83 @@
const RESERVED_WORDS = [
'abstract',
'arguments',
'await',
'boolean',
'break',
'byte',
'case',
'catch',
'char',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'double',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'final',
'finally',
'float',
'for',
'function',
'goto',
'if',
'implements',
'import',
'in',
'instanceof',
'int',
'interface',
'let',
'long',
'native',
'new',
'null',
'package',
'private',
'protected',
'public',
'return',
'short',
'static',
'super',
'switch',
'synchronized',
'this',
'throw',
'throws',
'transient',
'true',
'try',
'typeof',
'var',
'void',
'volatile',
'while',
'with',
'yield'
];
const regex = /^[a-zA-Z_$][0-9a-zA-Z_$]*(?:\[(?:".+"|\'.+\'|\d+)\])*?$/;
module.exports.check = function(inputStr) {
var isValid = true;
inputStr.split(".").forEach(function(part) {
if (!regex.test(part)) {
isValid = false;
}
if (RESERVED_WORDS.indexOf(part) !== -1) {
isValid = false;
}
});
return isValid;
}

View File

@ -20,7 +20,7 @@ exports.basicAuth = function (req, res, next) {
// Do not require auth for static paths and the API...this could be a bit brittle // Do not require auth for static paths and the API...this could be a bit brittle
if (req.path.match(/^\/(static|javascripts|pluginfw|api)/)) return cb(true); if (req.path.match(/^\/(static|javascripts|pluginfw|api)/)) return cb(true);
if (req.path.indexOf('/admin') != 0) { if (req.path.toLowerCase().indexOf('/admin') != 0) {
if (!settings.requireAuthentication) return cb(true); if (!settings.requireAuthentication) return cb(true);
if (!settings.requireAuthorization && req.session && req.session.user) return cb(true); if (!settings.requireAuthorization && req.session && req.session.user) return cb(true);
} }
@ -38,7 +38,7 @@ exports.basicAuth = function (req, res, next) {
var password = userpass.join(':'); var password = userpass.join(':');
var fallback = function(success) { var fallback = function(success) {
if (success) return cb(true); if (success) return cb(true);
if (settings.users[username] != undefined && settings.users[username].password == password) { if (settings.users[username] != undefined && settings.users[username].password === password) {
settings.users[username].username = username; settings.users[username].username = username;
req.session.user = settings.users[username]; req.session.user = settings.users[username];
return cb(true); return cb(true);
@ -129,4 +129,3 @@ exports.expressConfigure = function (hook_name, args, cb) {
args.app.use(exports.basicAuth); args.app.use(exports.basicAuth);
} }

View File

@ -22,25 +22,18 @@ var ERR = require("async-stacktrace");
exports.getPadRaw = function(padId, callback){ exports.getPadRaw = function(padId, callback){
async.waterfall([ async.waterfall([
function(cb){ function(cb){
db.get("pad:"+padId, cb);
// Get the Pad
db.findKeys("pad:"+padId, null, function(err,padcontent){
if(!err){
cb(err, padcontent);
}
})
}, },
function(padcontent,cb){ function(padcontent,cb){
var records = ["pad:"+padId];
// Get the Pad available content keys for (var i = 0; i <= padcontent.head; i++) {
db.findKeys("pad:"+padId+":*", null, function(err,records){ records.push("pad:"+padId+":revs:" + i);
if(!err){
for (var key in padcontent) { records.push(padcontent[key]);}
cb(err, records);
} }
})
}, for (var i = 0; i <= padcontent.chatHead; i++) {
function(records, cb){ records.push("pad:"+padId+":chat:" + i);
}
var data = {}; var data = {};
async.forEachSeries(Object.keys(records), function(key, r){ async.forEachSeries(Object.keys(records), function(key, r){

View File

@ -356,15 +356,7 @@ function getHTMLFromAtext(pad, atext, authorColors)
} }
} }
} }
var context = {
line: line,
lineContent: lineContent,
apool: apool,
attribLine: attribLines[i],
text: textLines[i],
padId: pad.id
}
var lineContentFromHook = hooks.callAll("getLineHTMLForExport", context);
if (whichList >= lists.length)//means we are on a deeper level of indentation than the previous line if (whichList >= lists.length)//means we are on a deeper level of indentation than the previous line
{ {
if(lists.length > 0){ if(lists.length > 0){
@ -381,14 +373,14 @@ function getHTMLFromAtext(pad, atext, authorColors)
if(toOpen > 0){ if(toOpen > 0){
pieces.push(new Array(toOpen + 1).join('<ol>')) pieces.push(new Array(toOpen + 1).join('<ol>'))
} }
pieces.push('<ol class="'+line.listTypeName+'"><li>', context.lineContent || '<br>'); pieces.push('<ol class="'+line.listTypeName+'"><li>', lineContent || '<br>');
} }
else else
{ {
if(toOpen > 0){ if(toOpen > 0){
pieces.push(new Array(toOpen + 1).join('<ul>')) pieces.push(new Array(toOpen + 1).join('<ul>'))
} }
pieces.push('<ul class="'+line.listTypeName+'"><li>', context.lineContent || '<br>'); pieces.push('<ul class="'+line.listTypeName+'"><li>', lineContent || '<br>');
} }
} }
//the following code *seems* dead after my patch. //the following code *seems* dead after my patch.
@ -424,16 +416,16 @@ function getHTMLFromAtext(pad, atext, authorColors)
if(lists[lists.length - 1][1] == "number") if(lists[lists.length - 1][1] == "number")
{ {
pieces.push(new Array(toClose+1).join('</ol>')) pieces.push(new Array(toClose+1).join('</ol>'))
pieces.push('<li>', context.lineContent || '<br>'); pieces.push('<li>', lineContent || '<br>');
} }
else else
{ {
pieces.push(new Array(toClose+1).join('</ul>')) pieces.push(new Array(toClose+1).join('</ul>'))
pieces.push('<li>', context.lineContent || '<br>'); pieces.push('<li>', lineContent || '<br>');
} }
lists = lists.slice(0,whichList+1) lists = lists.slice(0,whichList+1)
} else { } else {
pieces.push('</li><li>', context.lineContent || '<br>'); pieces.push('</li><li>', lineContent || '<br>');
} }
} }
} }
@ -459,9 +451,16 @@ function getHTMLFromAtext(pad, atext, authorColors)
padId: pad.id padId: pad.id
} }
hooks.callAll("getLineHTMLForExport", context); var lineContentFromHook = hooks.callAllStr("getLineHTMLForExport", context, " ", " ", "");
pieces.push(context.lineContent, '<br>'); if (lineContentFromHook)
{
pieces.push(lineContentFromHook, '');
}
else
{
pieces.push(lineContent, '<br>');
}
} }
} }

View File

@ -17,14 +17,14 @@
"etherpad-require-kernel" : "1.0.9", "etherpad-require-kernel" : "1.0.9",
"resolve" : "1.1.7", "resolve" : "1.1.7",
"socket.io" : "1.7.3", "socket.io" : "1.7.3",
"ueberdb2" : "0.3.7", "ueberdb2" : "0.3.8",
"express" : "4.13.4", "express" : "4.13.4",
"express-session" : "1.13.0", "express-session" : "1.13.0",
"cookie-parser" : "1.3.4", "cookie-parser" : "1.3.4",
"async" : "0.9.0", "async" : "0.9.0",
"clean-css" : "3.4.19", "clean-css" : "3.4.19",
"uglify-js" : "2.6.2", "uglify-js" : "2.6.2",
"formidable" : "1.0.17", "formidable" : "1.2.1",
"log4js" : "0.6.35", "log4js" : "0.6.35",
"cheerio" : "0.20.0", "cheerio" : "0.20.0",
"async-stacktrace" : "0.0.2", "async-stacktrace" : "0.0.2",
@ -42,13 +42,12 @@
"channels" : "0.0.4", "channels" : "0.0.4",
"jsonminify" : "0.4.1", "jsonminify" : "0.4.1",
"measured" : "1.1.0", "measured" : "1.1.0",
"mocha" : "2.4.5", "mocha" : "5.0.5",
"supertest" : "1.2.0", "supertest" : "3.0.0"
"is-var-name" : "1.0.0"
}, },
"bin": { "etherpad-lite": "./node/server.js" }, "bin": { "etherpad-lite": "./node/server.js" },
"devDependencies": { "devDependencies": {
"wd" : "0.3.11" "wd" : "1.6.1"
}, },
"engines" : { "node" : ">=0.10.0", "engines" : { "node" : ">=0.10.0",
"npm" : ">=1.0" "npm" : ">=1.0"
@ -56,6 +55,6 @@
"repository" : { "type" : "git", "repository" : { "type" : "git",
"url" : "http://github.com/ether/etherpad-lite.git" "url" : "http://github.com/ether/etherpad-lite.git"
}, },
"version" : "1.6.3", "version" : "1.6.5",
"license" : "Apache-2.0" "license" : "Apache-2.0"
} }

View File

@ -31,17 +31,13 @@ body {
body.grayedout { background-color: #eee !important } body.grayedout { background-color: #eee !important }
#innerdocbody { #innerdocbody {
font-size: 16px; /* overridden by body.style */ font-size: 12px; /* overridden by body.style */
font-family:Arial, sans-serif; /* overridden by body.style */ font-family:Arial, sans-serif; /* overridden by body.style */
line-height: 16px; /* overridden by body.style */ line-height: 16px; /* overridden by body.style */
background-color: white; background-color: white;
color: black; color: black;
} }
.innerdocbody>div{
padding: 1px;
}
body.doesWrap { body.doesWrap {
/* white-space: pre-wrap; */ /* white-space: pre-wrap; */
@ -62,11 +58,9 @@ body.doesWrap {
white-space: normal; white-space: normal;
} }
@-moz-document url-prefix() { body.doesWrap:not(.noprewrap) > div{
body.doesWrap:not(.noprewrap) > div{
/* Related to #1766 */ /* Related to #1766 */
white-space: pre-wrap; white-space: pre-wrap;
}
} }
#innerdocbody { #innerdocbody {

View File

@ -1072,9 +1072,9 @@ input[type=checkbox] {
overflow: auto; overflow: auto;
} }
#mycolorpicker { #mycolorpicker {
left: -73px; left: 0px;
top:auto !important; top:37px !important;
bottom:33px !important; position:fixed;
/* #mycolorpicker: width -#users: width */; /* #mycolorpicker: width -#users: width */;
} }
#editorcontainer { #editorcontainer {

View File

@ -79,12 +79,17 @@ $(document).ready(function () {
for (attr in plugin) { for (attr in plugin) {
if(attr == "name"){ // Hack to rewrite URLS into name if(attr == "name"){ // Hack to rewrite URLS into name
row.find(".name").html("<a target='_blank' title='Plugin details' href='https://npmjs.org/package/"+plugin['name']+"'>"+plugin['name'].substr(3)+"</a>"); // remove 'ep_' var link = $('<a>');
}else{ link.attr('href', 'https://npmjs.org/package/'+plugin['name']);
link.attr('plugin', 'Plugin details');
link.attr('target', '_blank');
link.text(plugin['name'].substr(3));
row.find('.name').append(link);
} else {
row.find("." + attr).text(plugin[attr]); row.find("." + attr).text(plugin[attr]);
} }
} }
row.find(".version").html( plugin.version ); row.find(".version").text(plugin.version);
row.addClass(plugin.name) row.addClass(plugin.name)
row.data('plugin', plugin.name) row.data('plugin', plugin.name)
container.append(row); container.append(row);

View File

@ -60,6 +60,8 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad)
var debugMessages = []; var debugMessages = [];
var msgQueue = []; var msgQueue = [];
var isPendingRevision = false;
tellAceAboutHistoricalAuthors(serverVars.historicalAuthorData); tellAceAboutHistoricalAuthors(serverVars.historicalAuthorData);
tellAceActiveAuthorInfo(initialUserInfo); tellAceActiveAuthorInfo(initialUserInfo);
@ -178,9 +180,16 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad)
editor.applyChangesToBase(changeset, author, apool); editor.applyChangesToBase(changeset, author, apool);
} }
} }
if (isPendingRevision) {
setIsPendingRevision(false);
}
} }
var sentMessage = false; var 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
if (!isPendingRevision)
{
var userChangesData = editor.prepareUserChangeset(); var userChangesData = editor.prepareUserChangeset();
if (userChangesData.changeset) if (userChangesData.changeset)
{ {
@ -196,6 +205,12 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad)
sentMessage = true; sentMessage = true;
callbacks.onInternalAction("commitPerformed"); callbacks.onInternalAction("commitPerformed");
} }
}
else
{
// run again in a few seconds, to check if there was a reconnection attempt
setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges), 3000);
}
if (sentMessage) if (sentMessage)
{ {
@ -330,6 +345,69 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad)
}); });
handleUserChanges(); handleUserChanges();
} }
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;
}
var headRev = msg.headRev;
var newRev = msg.newRev;
var changeset = msg.changeset;
var author = (msg.author || '');
var 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));
// 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())
{
editor.applyPreparedChangesetToBase();
setStateIdle();
callCatchingErrors("onInternalAction", function()
{
callbacks.onInternalAction("commitAcceptedByServer");
});
callCatchingErrors("onConnectionTrouble", function()
{
callbacks.onConnectionTrouble("OK");
});
handleUserChanges();
}
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") else if (msg.type == "NO_COMMIT_PENDING")
{ {
if (state == "COMMITTING") if (state == "COMMITTING")
@ -591,6 +669,11 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad)
schedulePerhapsCallIdleFuncs(); schedulePerhapsCallIdleFuncs();
} }
function setIsPendingRevision(value)
{
isPendingRevision = value;
}
function callWhenNotCommitting(func) function callWhenNotCommitting(func)
{ {
idleFuncs.push(func); idleFuncs.push(func);
@ -656,7 +739,9 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad)
getMissedChanges: getMissedChanges, getMissedChanges: getMissedChanges,
callWhenNotCommitting: callWhenNotCommitting, callWhenNotCommitting: callWhenNotCommitting,
addHistoricalAuthors: tellAceAboutHistoricalAuthors, addHistoricalAuthors: tellAceAboutHistoricalAuthors,
setChannelState: setChannelState setChannelState: setChannelState,
setStateIdle: setStateIdle,
setIsPendingRevision: setIsPendingRevision
}; };
$(document).ready(setUpSocket); $(document).ready(setUpSocket);

View File

@ -206,10 +206,12 @@ function handshake()
socket.on('reconnect', function () { socket.on('reconnect', function () {
pad.collabClient.setChannelState("CONNECTED"); pad.collabClient.setChannelState("CONNECTED");
pad.sendClientReady(true); pad.sendClientReady(receivedClientVars);
}); });
socket.on('reconnecting', function() { socket.on('reconnecting', function() {
pad.collabClient.setStateIdle();
pad.collabClient.setIsPendingRevision(true);
pad.collabClient.setChannelState("RECONNECTING"); pad.collabClient.setChannelState("RECONNECTING");
}); });
@ -217,6 +219,11 @@ function handshake()
pad.collabClient.setChannelState("DISCONNECTED", "reconnect_timeout"); pad.collabClient.setChannelState("DISCONNECTED", "reconnect_timeout");
}); });
socket.on('error', function(error) {
pad.collabClient.setStateIdle();
pad.collabClient.setIsPendingRevision(true);
});
var initalized = false; var initalized = false;
socket.on('message', function(obj) socket.on('message', function(obj)
@ -831,7 +838,7 @@ var pad = {
$.ajax( $.ajax(
{ {
type: 'post', type: 'post',
url: '/ep/pad/connection-diagnostic-info', url: 'ep/pad/connection-diagnostic-info',
data: { data: {
diagnosticInfo: JSON.stringify(pad.diagnosticInfo) diagnosticInfo: JSON.stringify(pad.diagnosticInfo)
}, },

View File

@ -139,6 +139,5 @@ ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol {
</head> </head>
<body> <body>
<%- body %> <%- body %>
<div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div>
</body> </body>
</html> </html>