From 9497ee734f415daa266e015ea4b5deb45033aa38 Mon Sep 17 00:00:00 2001 From: muxator Date: Fri, 8 Feb 2019 23:20:57 +0100 Subject: [PATCH 001/183] prepare to async: trivial reformatting This change is only cosmetic. Its aim is do make it easier to understand the async changes that are going to be merged later on. It was extracted from the original work from Ray Bellis. To verify that nothing has changed, you can run the following command on each file touched by this commit: npm install uglify-es diff --unified <(uglify-js --beautify bracketize ) <(uglify-js --beautify bracketize ) This is a complete script that does the same automatically (works from a mercurial clone): ```bash #!/usr/bin/env bash set -eu REVISION= PARENT_REV=$(hg identify --rev "${REVISION}" --template '{p1rev}') FILE_LIST=$(hg status --no-status --change ${REVISION}) UGLIFYJS="node_modules/uglify-es/bin/uglifyjs" for FILE_NAME in ${FILE_LIST[@]}; do echo "Checking ${FILE_NAME}" diff --unified \ <("${UGLIFYJS}" --beautify bracketize <(hg cat --rev "${PARENT_REV}" "${FILE_NAME}")) \ <("${UGLIFYJS}" --beautify bracketize <(hg cat --rev "${REVISION}" "${FILE_NAME}")) done ``` --- bin/checkAllPads.js | 207 +++-- bin/checkPad.js | 130 ++- bin/deletePad.js | 50 +- bin/extractPadData.js | 118 ++- src/node/db/API.js | 762 +++++++--------- src/node/db/AuthorManager.js | 163 ++-- src/node/db/DB.js | 27 +- src/node/db/GroupManager.js | 293 +++---- src/node/db/Pad.js | 463 +++++----- src/node/db/PadManager.js | 177 ++-- src/node/db/ReadOnlyManager.js | 49 +- src/node/db/SecurityManager.js | 350 ++++---- src/node/db/SessionManager.js | 248 +++--- src/node/db/SessionStore.js | 42 +- src/node/handler/APIHandler.js | 96 +- src/node/handler/ExportHandler.js | 117 ++- src/node/handler/ImportHandler.js | 152 ++-- src/node/handler/PadMessageHandler.js | 1010 +++++++++++----------- src/node/handler/SocketIORouter.js | 104 ++- src/node/hooks/express/adminplugins.js | 103 ++- src/node/hooks/express/errorhandling.js | 15 +- src/node/hooks/express/importexport.js | 14 +- src/node/hooks/express/padreadonly.js | 39 +- src/node/hooks/express/padurlsanitize.js | 19 +- src/node/hooks/express/tests.js | 51 +- src/node/padaccess.js | 10 +- src/node/server.js | 24 +- src/node/utils/ExportTxt.js | 165 ++-- src/node/utils/ImportEtherpad.js | 43 +- src/node/utils/ImportHtml.js | 26 +- src/node/utils/padDiff.js | 498 ++++++----- src/static/js/pluginfw/installer.js | 77 +- src/static/js/pluginfw/plugins.js | 7 +- 33 files changed, 2706 insertions(+), 2943 deletions(-) diff --git a/bin/checkAllPads.js b/bin/checkAllPads.js index a94c38d23..c4467fa79 100644 --- a/bin/checkAllPads.js +++ b/bin/checkAllPads.js @@ -1,144 +1,133 @@ /* - This is a debug tool. It checks all revisions for data corruption -*/ + * This is a debug tool. It checks all revisions for data corruption + */ -if(process.argv.length != 2) -{ +if (process.argv.length != 2) { console.error("Use: node bin/checkAllPads.js"); process.exit(1); } -//initialize the variables +// initialize the variables var db, settings, padManager; -var npm = require("../src/node_modules/npm"); -var async = require("../src/node_modules/async"); +var npm = require('../src/node_modules/npm'); +var async = require('../src/node_modules/async'); -var Changeset = require("../src/static/js/Changeset"); +var Changeset = require('../src/static/js/Changeset'); async.series([ - //load npm + // load npm function(callback) { npm.load({}, callback); }, - //load modules + + // load modules function(callback) { settings = require('../src/node/utils/Settings'); db = require('../src/node/db/DB'); - //initialize the database + // initialize the database db.init(callback); }, - //load pads - function (callback) - { + + // load pads + function (callback) { padManager = require('../src/node/db/PadManager'); - - padManager.listAllPads(function(err, res) - { + + padManager.listAllPads(function(err, res) { padIds = res.padIDs; callback(err); }); }, - function (callback) - { - async.forEach(padIds, function(padId, callback) - { - padManager.getPad(padId, function(err, pad) { + + function (callback) { + async.forEach(padIds, function(padId, callback) { + padManager.getPad(padId, function(err, pad) { + if (err) { + callback(err); + } + + // check if the pad has a pool + if (pad.pool === undefined ) { + console.error("[" + pad.id + "] Missing attribute pool"); + callback(); + + return; + } + + // create an array with key kevisions + // key revisions always save the full pad atext + var head = pad.getHeadRevisionNumber(); + var keyRevisions = []; + for (var i = 0; i < head; i += 100) { + keyRevisions.push(i); + } + + // run through all key revisions + async.forEachSeries(keyRevisions, function(keyRev, callback) { + // create an array of revisions we need till the next keyRevision or the End + var revisionsNeeded = []; + + for(var i = keyRev; i <= keyRev + 100 && i <= head; i++) { + revisionsNeeded.push(i); + } + + // this array will hold all revision changesets + var revisions = []; + + // run through all needed revisions and get them from the database + async.forEach(revisionsNeeded, function(revNum, callback) { + db.db.get("pad:" + pad.id + ":revs:" + revNum, function(err, revision) { + revisions[revNum] = revision; + callback(err); + }); + }, + + function(err) { if (err) { - callback(err); + callback(err); + return; } - - //check if the pad has a pool - if(pad.pool === undefined ) - { - console.error("[" + pad.id + "] Missing attribute pool"); + + // check if the revision exists + if (revisions[keyRev] == null) { + console.error("[" + pad.id + "] Missing revision " + keyRev); + callback(); + return; + } + + // check if there is a atext in the keyRevisions + if (revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) { + console.error("[" + pad.id + "] Missing atext in revision " + keyRev); + callback(); + return; + } + + var apool = pad.pool; + var atext = revisions[keyRev].meta.atext; + + for(var i = keyRev + 1; i <= keyRev + 100 && i <= head; i++) { + try { + // console.log("[" + pad.id + "] check revision " + i); + var cs = revisions[i].changeset; + atext = Changeset.applyToAText(cs, atext, apool); + } catch(e) { + console.error("[" + pad.id + "] Bad changeset at revision " + i + " - " + e.message); callback(); return; + } } - //create an array with key kevisions - //key revisions always save the full pad atext - var head = pad.getHeadRevisionNumber(); - var keyRevisions = []; - for(var i=0;i pad.getHeadRevisionNumber()) - { - callback(new customError("rev is higher than the head revision of the pad","apierror")); + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; + + // the client asked for a special revision + if (rev !== undefined) { + // check if this is a valid revision + if (rev > pad.getHeadRevisionNumber()) { + callback(new customError("rev is higher than the head revision of the pad", "apierror")); return; } - - //get the changeset for this revision - pad.getRevisionChangeset(rev, function(err, changeset) - { - if(ERR(err, callback)) return; - + + // get the changeset for this revision + pad.getRevisionChangeset(rev, function(err, changeset) { + if (ERR(err, callback)) return; + callback(null, changeset); }) return; } - //the client wants the latest changeset, lets return it to him - pad.getRevisionChangeset(pad.getHeadRevisionNumber(), function(err, changeset) - { - if(ERR(err, callback)) return; + // the client wants the latest changeset, lets return it to him + pad.getRevisionChangeset(pad.getHeadRevisionNumber(), function(err, changeset) { + if (ERR(err, callback)) return; callback(null, changeset); }) @@ -191,7 +182,7 @@ exports.getRevisionChangeset = function(padID, rev, callback) } /** -getText(padID, [rev]) returns the text of a pad +getText(padID, [rev]) returns the text of a pad Example returns: @@ -200,69 +191,61 @@ Example returns: */ exports.getText = function(padID, rev, callback) { - //check if rev is a number - if(rev !== undefined && typeof rev != "number") - { - //try to parse the number - if(isNaN(parseInt(rev))) - { + // check if rev is a number + if (rev !== undefined && typeof rev != "number") { + // try to parse the number + if (isNaN(parseInt(rev))) { callback(new customError("rev is not a number", "apierror")); return; } rev = parseInt(rev); } - - //ensure this is not a negativ number - if(rev !== undefined && rev < 0) - { - callback(new customError("rev is a negativ number","apierror")); + + // ensure this is not a negative number + if (rev !== undefined && rev < 0) { + callback(new customError("rev is a negativ number", "apierror")); return; } - - //ensure this is not a float value - if(rev !== undefined && !is_int(rev)) - { - callback(new customError("rev is a float value","apierror")); + + // ensure this is not a float value + if (rev !== undefined && !is_int(rev)) { + callback(new customError("rev is a float value", "apierror")); return; } - - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; - - //the client asked for a special revision - if(rev !== undefined) - { - //check if this is a valid revision - if(rev > pad.getHeadRevisionNumber()) - { - callback(new customError("rev is higher than the head revision of the pad","apierror")); + + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; + + // the client asked for a special revision + if (rev !== undefined) { + // check if this is a valid revision + if (rev > pad.getHeadRevisionNumber()) { + callback(new customError("rev is higher than the head revision of the pad", "apierror")); return; } - - //get the text of this revision - pad.getInternalRevisionAText(rev, function(err, atext) - { - if(ERR(err, callback)) return; - + + // get the text of this revision + pad.getInternalRevisionAText(rev, function(err, atext) { + if (ERR(err, callback)) return; + var data = {text: atext.text}; - + callback(null, data); }) return; } - //the client wants the latest text, lets return it to him + // the client wants the latest text, lets return it to him var padText = exportTxt.getTXTFromAtext(pad, pad.atext); callback(null, {"text": padText}); }); } /** -setText(padID, text) sets the text of a pad +setText(padID, text) sets the text of a pad Example returns: @@ -271,23 +254,21 @@ Example returns: {code: 1, message:"text too long", data: null} */ exports.setText = function(padID, text, callback) -{ - //text is required - if(typeof text != "string") - { - callback(new customError("text is no string","apierror")); +{ + // text is required + if (typeof text != "string") { + callback(new customError("text is no string", "apierror")); return; } - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; - - //set the text + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; + + // set the text pad.setText(text); - - //update the clients on the pad + + // update the clients on the pad padMessageHandler.updatePadClients(pad, callback); }); } @@ -303,29 +284,25 @@ Example returns: */ exports.appendText = function(padID, text, callback) { - //text is required - if(typeof text != "string") - { - callback(new customError("text is no string","apierror")); + // text is required + if (typeof text != "string") { + callback(new customError("text is no string", "apierror")); return; } - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; pad.appendText(text); - //update the clients on the pad + // update the clients on the pad padMessageHandler.updatePadClients(pad, callback); }); }; - - /** -getHTML(padID, [rev]) returns the html of a pad +getHTML(padID, [rev]) returns the html of a pad Example returns: @@ -334,47 +311,39 @@ Example returns: */ exports.getHTML = function(padID, rev, callback) { - if (rev !== undefined && typeof rev != "number") - { - if (isNaN(parseInt(rev))) - { - callback(new customError("rev is not a number","apierror")); + if (rev !== undefined && typeof rev != "number") { + if (isNaN(parseInt(rev))) { + callback(new customError("rev is not a number", "apierror")); return; } rev = parseInt(rev); } - if(rev !== undefined && rev < 0) - { - callback(new customError("rev is a negative number","apierror")); + if (rev !== undefined && rev < 0) { + callback(new customError("rev is a negative number", "apierror")); return; } - if(rev !== undefined && !is_int(rev)) - { - callback(new customError("rev is a float value","apierror")); + if (rev !== undefined && !is_int(rev)) { + callback(new customError("rev is a float value", "apierror")); return; } - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; - - //the client asked for a special revision - if(rev !== undefined) - { - //check if this is a valid revision - if(rev > pad.getHeadRevisionNumber()) - { - callback(new customError("rev is higher than the head revision of the pad","apierror")); + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; + + // the client asked for a special revision + if (rev !== undefined) { + // check if this is a valid revision + if (rev > pad.getHeadRevisionNumber()) { + callback(new customError("rev is higher than the head revision of the pad", "apierror")); return; } - - //get the html of this revision - exportHtml.getPadHTML(pad, rev, function(err, html) - { - if(ERR(err, callback)) return; + + // get the html of this revision + exportHtml.getPadHTML(pad, rev, function(err, html) { + if (ERR(err, callback)) return; html = "" +html; // adds HTML head html += ""; var data = {html: html}; @@ -384,10 +353,9 @@ exports.getHTML = function(padID, rev, callback) return; } - //the client wants the latest text, lets return it to him - exportHtml.getPadHTML(pad, undefined, function (err, html) - { - if(ERR(err, callback)) return; + // the client wants the latest text, lets return it to him + exportHtml.getPadHTML(pad, undefined, function(err, html) { + if (ERR(err, callback)) return; html = "" +html; // adds HTML head html += ""; var data = {html: html}; @@ -406,26 +374,24 @@ Example returns: */ exports.setHTML = function(padID, html, callback) { - //html is required - if(typeof html != "string") - { - callback(new customError("html is no string","apierror")); + // html is required + if (typeof html != "string") { + callback(new customError("html is no string", "apierror")); return; } - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; // add a new changeset with the new html to the pad - importHtml.setPadHTML(pad, cleanText(html), function(e){ - if(e){ - callback(new customError("HTML is malformed","apierror")); + importHtml.setPadHTML(pad, cleanText(html), function(e) { + if (e) { + callback(new customError("HTML is malformed", "apierror")); return; } - //update the clients on the pad + // update the clients on the pad padMessageHandler.updatePadClients(pad, callback); }); }); @@ -449,54 +415,46 @@ Example returns: */ exports.getChatHistory = function(padID, start, end, callback) { - if(start && end) - { - if(start < 0) - { - callback(new customError("start is below zero","apierror")); + if (start && end) { + if (start < 0) { + callback(new customError("start is below zero", "apierror")); return; } - if(end < 0) - { - callback(new customError("end is below zero","apierror")); + if (end < 0) { + callback(new customError("end is below zero", "apierror")); return; } - if(start > end) - { - callback(new customError("start is higher than end","apierror")); + if (start > end) { + callback(new customError("start is higher than end", "apierror")); return; } } - - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; + + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; + var chatHead = pad.chatHead; - + // fall back to getting the whole chat-history if a parameter is missing - if(!start || !end) - { + if (!start || !end) { start = 0; end = pad.chatHead; } - - if(start > chatHead) - { - callback(new customError("start is higher than the current chatHead","apierror")); + + if (start > chatHead) { + callback(new customError("start is higher than the current chatHead", "apierror")); return; } - if(end > chatHead) - { - callback(new customError("end is higher than the current chatHead","apierror")); + if (end > chatHead) { + callback(new customError("end is higher than the current chatHead", "apierror")); return; } - + // the the whole message-log and return it to the client pad.getChatMessages(start, end, - function(err, msgs) - { - if(ERR(err, callback)) return; + function(err, msgs) { + if (ERR(err, callback)) return; callback(null, {messages: msgs}); }); }); @@ -512,16 +470,14 @@ Example returns: */ exports.appendChatMessage = function(padID, text, authorID, time, callback) { - //text is required - if(typeof text != "string") - { - callback(new customError("text is no string","apierror")); + // text is required + if (typeof text != "string") { + callback(new customError("text is no string", "apierror")); return; } - + // if time is not an integer value - if(time === undefined || !is_int(time)) - { + if (time === undefined || !is_int(time)) { // set time to current timestamp time = Date.now(); } @@ -537,7 +493,7 @@ exports.appendChatMessage = function(padID, text, authorID, time, callback) /*****************/ /** -getRevisionsCount(padID) returns the number of revisions of this pad +getRevisionsCount(padID) returns the number of revisions of this pad Example returns: @@ -546,11 +502,10 @@ Example returns: */ exports.getRevisionsCount = function(padID, callback) { - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; - + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; + callback(null, {revisions: pad.getHeadRevisionNumber()}); }); } @@ -565,10 +520,9 @@ Example returns: */ exports.getSavedRevisionsCount = function(padID, callback) { - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; callback(null, {savedRevisions: pad.getSavedRevisionsNumber()}); }); @@ -584,10 +538,9 @@ Example returns: */ exports.listSavedRevisions = function(padID, callback) { - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; callback(null, {savedRevisions: pad.getSavedRevisionsList()}); }); @@ -603,12 +556,10 @@ Example returns: */ exports.saveRevision = function(padID, rev, callback) { - //check if rev is a number - if(rev !== undefined && typeof rev != "number") - { - //try to parse the number - if(isNaN(parseInt(rev))) - { + // check if rev is a number + if (rev !== undefined && typeof rev != "number") { + // try to parse the number + if (isNaN(parseInt(rev))) { callback(new customError("rev is not a number", "apierror")); return; } @@ -616,32 +567,27 @@ exports.saveRevision = function(padID, rev, callback) rev = parseInt(rev); } - //ensure this is not a negativ number - if(rev !== undefined && rev < 0) - { - callback(new customError("rev is a negativ number","apierror")); + // ensure this is not a negative number + if (rev !== undefined && rev < 0) { + callback(new customError("rev is a negativ number", "apierror")); return; } - //ensure this is not a float value - if(rev !== undefined && !is_int(rev)) - { - callback(new customError("rev is a float value","apierror")); + // ensure this is not a float value + if (rev !== undefined && !is_int(rev)) { + callback(new customError("rev is a float value", "apierror")); return; } - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; - //the client asked for a special revision - if(rev !== undefined) - { - //check if this is a valid revision - if(rev > pad.getHeadRevisionNumber()) - { - callback(new customError("rev is higher than the head revision of the pad","apierror")); + // the client asked for a special revision + if (rev !== undefined) { + // check if this is a valid revision + if (rev > pad.getHeadRevisionNumber()) { + callback(new customError("rev is higher than the head revision of the pad", "apierror")); return; } } else { @@ -649,7 +595,7 @@ exports.saveRevision = function(padID, rev, callback) } authorManager.createAuthor('API', function(err, author) { - if(ERR(err, callback)) return; + if (ERR(err, callback)) return; pad.addSavedRevision(rev, author.authorID, 'Saved through API call'); callback(); @@ -667,19 +613,19 @@ Example returns: */ exports.getLastEdited = function(padID, callback) { - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; + pad.getLastEdit(function(err, value) { - if(ERR(err, callback)) return; + if (ERR(err, callback)) return; callback(null, {lastEdited: value}); }); }); } /** -createPad(padName [, text]) creates a new pad in this group +createPad(padName [, text]) creates a new pad in this group Example returns: @@ -687,34 +633,30 @@ Example returns: {code: 1, message:"pad does already exist", data: null} */ exports.createPad = function(padID, text, callback) -{ - //ensure there is no $ in the padID - if(padID) - { - if(padID.indexOf("$") != -1) - { - callback(new customError("createPad can't create group pads","apierror")); +{ + if (padID) { + // ensure there is no $ in the padID + if (padID.indexOf("$") != -1) { + callback(new customError("createPad can't create group pads", "apierror")); return; } - //check for url special characters - if(padID.match(/(\/|\?|&|#)/)) - { - callback(new customError("malformed padID: Remove special characters","apierror")); + // check for url special characters + if (padID.match(/(\/|\?|&|#)/)) { + callback(new customError("malformed padID: Remove special characters", "apierror")); return; } } - //create pad - getPadSafe(padID, false, text, function(err) - { - if(ERR(err, callback)) return; + // create pad + getPadSafe(padID, false, text, function(err) { + if (ERR(err, callback)) return; callback(); }); } /** -deletePad(padID) deletes a pad +deletePad(padID) deletes a pad Example returns: @@ -723,10 +665,9 @@ Example returns: */ exports.deletePad = function(padID, callback) { - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; - + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; + pad.remove(callback); }); } @@ -738,14 +679,12 @@ exports.deletePad = function(padID, callback) {code:0, message:"ok", data:null} {code: 1, message:"padID does not exist", data: null} */ -exports.restoreRevision = function (padID, rev, callback) +exports.restoreRevision = function(padID, rev, callback) { - //check if rev is a number - if (rev !== undefined && typeof rev != "number") - { - //try to parse the number - if (isNaN(parseInt(rev))) - { + // check if rev is a number + if (rev !== undefined && typeof rev != "number") { + // try to parse the number + if (isNaN(parseInt(rev))) { callback(new customError("rev is not a number", "apierror")); return; } @@ -753,51 +692,43 @@ exports.restoreRevision = function (padID, rev, callback) rev = parseInt(rev); } - //ensure this is not a negativ number - if (rev !== undefined && rev < 0) - { + // ensure this is not a negative number + if (rev !== undefined && rev < 0) { callback(new customError("rev is a negativ number", "apierror")); return; } - //ensure this is not a float value - if (rev !== undefined && !is_int(rev)) - { + // ensure this is not a float value + if (rev !== undefined && !is_int(rev)) { callback(new customError("rev is a float value", "apierror")); return; } - //get the pad - getPadSafe(padID, true, function (err, pad) - { + // get the pad + getPadSafe(padID, true, function(err, pad) { if (ERR(err, callback)) return; - //check if this is a valid revision - if (rev > pad.getHeadRevisionNumber()) - { + // check if this is a valid revision + if (rev > pad.getHeadRevisionNumber()) { callback(new customError("rev is higher than the head revision of the pad", "apierror")); return; } - pad.getInternalRevisionAText(rev, function (err, atext) - { + pad.getInternalRevisionAText(rev, function(err, atext) { if (ERR(err, callback)) return; var oldText = pad.text(); atext.text += "\n"; - function eachAttribRun(attribs, func) - { + function eachAttribRun(attribs, func) { var attribsIter = Changeset.opIterator(attribs); var textIndex = 0; var newTextStart = 0; var newTextEnd = atext.text.length; - while (attribsIter.hasNext()) - { + while (attribsIter.hasNext()) { var op = attribsIter.next(); var nextIndex = textIndex + op.chars; - if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) - { + if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); } textIndex = nextIndex; @@ -808,29 +739,25 @@ exports.restoreRevision = function (padID, rev, callback) var builder = Changeset.builder(oldText.length); // assemble each line into the builder - eachAttribRun(atext.attribs, function (start, end, attribs) - { + eachAttribRun(atext.attribs, function(start, end, attribs) { builder.insert(atext.text.substring(start, end), attribs); }); var lastNewlinePos = oldText.lastIndexOf('\n'); - if (lastNewlinePos < 0) - { + if (lastNewlinePos < 0) { builder.remove(oldText.length - 1, 0); - } else - { + } else { builder.remove(lastNewlinePos, oldText.match(/\n/g).length - 1); builder.remove(oldText.length - lastNewlinePos - 1, 0); } var changeset = builder.toString(); - //append the changeset + // append the changeset pad.appendRevision(changeset); - // - padMessageHandler.updatePadClients(pad, function () - { - }); + + // update the clients on the pad + padMessageHandler.updatePadClients(pad, function() {}); callback(null, null); }); @@ -838,7 +765,7 @@ exports.restoreRevision = function (padID, rev, callback) }; /** -copyPad(sourceID, destinationID[, force=false]) copies a pad. If force is true, +copyPad(sourceID, destinationID[, force=false]) copies a pad. If force is true, the destination will be overwritten if it exists. Example returns: @@ -848,16 +775,15 @@ Example returns: */ exports.copyPad = function(sourceID, destinationID, force, callback) { - getPadSafe(sourceID, true, function(err, pad) - { - if(ERR(err, callback)) return; - + getPadSafe(sourceID, true, function(err, pad) { + if (ERR(err, callback)) return; + pad.copy(destinationID, force, callback); }); } /** -movePad(sourceID, destinationID[, force=false]) moves a pad. If force is true, +movePad(sourceID, destinationID[, force=false]) moves a pad. If force is true, the destination will be overwritten if it exists. Example returns: @@ -867,18 +793,17 @@ Example returns: */ exports.movePad = function(sourceID, destinationID, force, callback) { - getPadSafe(sourceID, true, function(err, pad) - { - if(ERR(err, callback)) return; - + getPadSafe(sourceID, true, function(err, pad) { + if (ERR(err, callback)) return; + pad.copy(destinationID, force, function(err) { - if(ERR(err, callback)) return; + if (ERR(err, callback)) return; pad.remove(callback); }); }); } /** -getReadOnlyLink(padID) returns the read only link of a pad +getReadOnlyLink(padID) returns the read only link of a pad Example returns: @@ -887,15 +812,13 @@ Example returns: */ exports.getReadOnlyID = function(padID, callback) { - //we don't need the pad object, but this function does all the security stuff for us - getPadSafe(padID, true, function(err) - { - if(ERR(err, callback)) return; - - //get the readonlyId - readOnlyManager.getReadOnlyId(padID, function(err, readOnlyId) - { - if(ERR(err, callback)) return; + // we don't need the pad object, but this function does all the security stuff for us + getPadSafe(padID, true, function(err) { + if (ERR(err, callback)) return; + + // get the readonlyId + readOnlyManager.getReadOnlyId(padID, function(err, readOnlyId) { + if (ERR(err, callback)) return; callback(null, {readOnlyID: readOnlyId}); }); }); @@ -911,14 +834,12 @@ Example returns: */ exports.getPadID = function(roID, callback) { - //get the PadId - readOnlyManager.getPadId(roID, function(err, retrievedPadID) - { - if(ERR(err, callback)) return; + // get the PadId + readOnlyManager.getPadId(roID, function(err, retrievedPadID) { + if (ERR(err, callback)) return; - if(retrievedPadID == null) - { - callback(new customError("padID does not exist","apierror")); + if (retrievedPadID == null) { + callback(new customError("padID does not exist", "apierror")); return; } @@ -927,7 +848,7 @@ exports.getPadID = function(roID, callback) } /** -setPublicStatus(padID, publicStatus) sets a boolean for the public status of a pad +setPublicStatus(padID, publicStatus) sets a boolean for the public status of a pad Example returns: @@ -936,31 +857,29 @@ Example returns: */ exports.setPublicStatus = function(padID, publicStatus, callback) { - //ensure this is a group pad - if(padID && padID.indexOf("$") == -1) - { - callback(new customError("You can only get/set the publicStatus of pads that belong to a group","apierror")); + // ensure this is a group pad + if (padID && padID.indexOf("$") == -1) { + callback(new customError("You can only get/set the publicStatus of pads that belong to a group", "apierror")); return; } - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; - - //convert string to boolean - if(typeof publicStatus == "string") + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; + + // convert string to boolean + if (typeof publicStatus == "string") publicStatus = publicStatus == "true" ? true : false; - - //set the password + + // set the password pad.setPublicStatus(publicStatus); - + callback(); }); } /** -getPublicStatus(padID) return true of false +getPublicStatus(padID) return true of false Example returns: @@ -969,24 +888,22 @@ Example returns: */ exports.getPublicStatus = function(padID, callback) { - //ensure this is a group pad - if(padID && padID.indexOf("$") == -1) - { - callback(new customError("You can only get/set the publicStatus of pads that belong to a group","apierror")); + // ensure this is a group pad + if (padID && padID.indexOf("$") == -1) { + callback(new customError("You can only get/set the publicStatus of pads that belong to a group", "apierror")); return; } - - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; - + + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; + callback(null, {publicStatus: pad.getPublicStatus()}); }); } /** -setPassword(padID, password) returns ok or a error message +setPassword(padID, password) returns ok or a error message Example returns: @@ -995,27 +912,25 @@ Example returns: */ exports.setPassword = function(padID, password, callback) { - //ensure this is a group pad - if(padID && padID.indexOf("$") == -1) - { - callback(new customError("You can only get/set the password of pads that belong to a group","apierror")); + // ensure this is a group pad + if (padID && padID.indexOf("$") == -1) { + callback(new customError("You can only get/set the password of pads that belong to a group", "apierror")); return; } - - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; - - //set the password + + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; + + // set the password pad.setPassword(password == "" ? null : password); - + callback(); }); } /** -isPasswordProtected(padID) returns true or false +isPasswordProtected(padID) returns true or false Example returns: @@ -1024,24 +939,22 @@ Example returns: */ exports.isPasswordProtected = function(padID, callback) { - //ensure this is a group pad - if(padID && padID.indexOf("$") == -1) - { - callback(new customError("You can only get/set the password of pads that belong to a group","apierror")); + // ensure this is a group pad + if (padID && padID.indexOf("$") == -1) { + callback(new customError("You can only get/set the password of pads that belong to a group", "apierror")); return; } - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; - + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; + callback(null, {isPasswordProtected: pad.isPasswordProtected()}); }); } /** -listAuthorsOfPad(padID) returns an array of authors who contributed to this pad +listAuthorsOfPad(padID) returns an array of authors who contributed to this pad Example returns: @@ -1050,11 +963,10 @@ Example returns: */ exports.listAuthorsOfPad = function(padID, callback) { - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; - + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; + callback(null, {authorIDs: pad.getAllAuthors()}); }); } @@ -1082,8 +994,8 @@ Example returns: {code: 1, message:"padID does not exist"} */ -exports.sendClientsMessage = function (padID, msg, callback) { - getPadSafe(padID, true, function (err, pad) { +exports.sendClientsMessage = function(padID, msg, callback) { + getPadSafe(padID, true, function(err, pad) { if (ERR(err, callback)) { return; } @@ -1115,10 +1027,10 @@ Example returns: */ exports.getChatHead = function(padID, callback) { - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(ERR(err, callback)) return; + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (ERR(err, callback)) return; + callback(null, {chatHead: pad.chatHead}); }); } @@ -1131,69 +1043,64 @@ Example returns: {"code":0,"message":"ok","data":{"html":"Welcome to Etherpad!

This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!

Get involved with Etherpad at http://etherpad.org
aw

","authors":["a.HKIv23mEbachFYfH",""]}} {"code":4,"message":"no or wrong API Key","data":null} */ -exports.createDiffHTML = function(padID, startRev, endRev, callback){ - //check if rev is a number - if(startRev !== undefined && typeof startRev != "number") - { - //try to parse the number - if(isNaN(parseInt(startRev))) - { +exports.createDiffHTML = function(padID, startRev, endRev, callback) { + // check if startRev is a number + if (startRev !== undefined && typeof startRev != "number") { + // try to parse the number + if (isNaN(parseInt(startRev))) { callback({stop: "startRev is not a number"}); return; } startRev = parseInt(startRev, 10); } - - //check if rev is a number - if(endRev !== undefined && typeof endRev != "number") - { - //try to parse the number - if(isNaN(parseInt(endRev))) - { + + // check if endRev is a number + if (endRev !== undefined && typeof endRev != "number") { + // try to parse the number + if (isNaN(parseInt(endRev))) { callback({stop: "endRev is not a number"}); return; } endRev = parseInt(endRev, 10); } - - //get the pad - getPadSafe(padID, true, function(err, pad) - { - if(err){ + + // get the pad + getPadSafe(padID, true, function(err, pad) { + if (err) { return callback(err); } - + try { var padDiff = new PadDiff(pad, startRev, endRev); } catch(e) { return callback({stop:e.message}); } var html, authors; - + async.series([ - function(callback){ - padDiff.getHtml(function(err, _html){ - if(err){ + function(callback) { + padDiff.getHtml(function(err, _html) { + if (err) { return callback(err); } - + html = _html; callback(); }); }, - function(callback){ - padDiff.getAuthors(function(err, _authors){ - if(err){ + function(callback) { + padDiff.getAuthors(function(err, _authors) { + if (err) { return callback(err); } - + authors = _authors; callback(); }); } - ], function(err){ + ], function(err) { callback(err, {html: html, authors: authors}) }); }); @@ -1203,53 +1110,44 @@ exports.createDiffHTML = function(padID, startRev, endRev, callback){ /** INTERNAL HELPER FUNCTIONS */ /******************************/ -//checks if a number is an int +// checks if a number is an int function is_int(value) -{ +{ return (parseFloat(value) == parseInt(value)) && !isNaN(value) } -//gets a pad safe +// gets a pad safe function getPadSafe(padID, shouldExist, text, callback) { - if(typeof text == "function") - { + if (typeof text == "function") { callback = text; text = null; } - //check if padID is a string - if(typeof padID != "string") - { - callback(new customError("padID is not a string","apierror")); + // check if padID is a string + if (typeof padID != "string") { + callback(new customError("padID is not a string", "apierror")); return; } - - //check if the padID maches the requirements - if(!padManager.isValidPadId(padID)) - { - callback(new customError("padID did not match requirements","apierror")); + + // check if the padID maches the requirements + if (!padManager.isValidPadId(padID)) { + callback(new customError("padID did not match requirements", "apierror")); return; } - - //check if the pad exists - padManager.doesPadExists(padID, function(err, exists) - { - if(ERR(err, callback)) return; - - //does not exist, but should - if(exists == false && shouldExist == true) - { - callback(new customError("padID does not exist","apierror")); - } - //does exists, but shouldn't - else if(exists == true && shouldExist == false) - { - callback(new customError("padID does already exist","apierror")); - } - //pad exists, let's get it - else - { + + // check if the pad exists + padManager.doesPadExists(padID, function(err, exists) { + if (ERR(err, callback)) return; + + // does not exist, but should + if (exists == false && shouldExist == true) { + callback(new customError("padID does not exist", "apierror")); + } else if (exists == true && shouldExist == false) { + // does exist, but shouldn't + callback(new customError("padID does already exist", "apierror")); + } else { + // pad exists, let's get it padManager.getPad(padID, text, callback); } }); diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index bcb6d393d..818eaab28 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -18,25 +18,33 @@ * limitations under the License. */ - var ERR = require("async-stacktrace"); var db = require("./DB").db; var customError = require("../utils/customError"); var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; -exports.getColorPalette = function(){ - return ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ffa8a8", "#ffe699", "#cfff9e", "#99ffb3", "#a3ffff", "#99b3ff", "#cc99ff", "#ff99e5", "#e7b1b1", "#e9dcAf", "#cde9af", "#bfedcc", "#b1e7e7", "#c3cdee", "#d2b8ea", "#eec3e6", "#e9cece", "#e7e0ca", "#d3e5c7", "#bce1c5", "#c1e2e2", "#c1c9e2", "#cfc1e2", "#e0bdd9", "#baded3", "#a0f8eb", "#b1e7e0", "#c3c8e4", "#cec5e2", "#b1d5e7", "#cda8f0", "#f0f0a8", "#f2f2a6", "#f5a8eb", "#c5f9a9", "#ececbb", "#e7c4bc", "#daf0b2", "#b0a0fd", "#bce2e7", "#cce2bb", "#ec9afe", "#edabbd", "#aeaeea", "#c4e7b1", "#d722bb", "#f3a5e7", "#ffa8a8", "#d8c0c5", "#eaaedd", "#adc6eb", "#bedad1", "#dee9af", "#e9afc2", "#f8d2a0", "#b3b3e6"]; +exports.getColorPalette = function() { + return [ + "#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", + "#ffa8a8", "#ffe699", "#cfff9e", "#99ffb3", "#a3ffff", "#99b3ff", "#cc99ff", "#ff99e5", + "#e7b1b1", "#e9dcAf", "#cde9af", "#bfedcc", "#b1e7e7", "#c3cdee", "#d2b8ea", "#eec3e6", + "#e9cece", "#e7e0ca", "#d3e5c7", "#bce1c5", "#c1e2e2", "#c1c9e2", "#cfc1e2", "#e0bdd9", + "#baded3", "#a0f8eb", "#b1e7e0", "#c3c8e4", "#cec5e2", "#b1d5e7", "#cda8f0", "#f0f0a8", + "#f2f2a6", "#f5a8eb", "#c5f9a9", "#ececbb", "#e7c4bc", "#daf0b2", "#b0a0fd", "#bce2e7", + "#cce2bb", "#ec9afe", "#edabbd", "#aeaeea", "#c4e7b1", "#d722bb", "#f3a5e7", "#ffa8a8", + "#d8c0c5", "#eaaedd", "#adc6eb", "#bedad1", "#dee9af", "#e9afc2", "#f8d2a0", "#b3b3e6" + ]; }; /** * Checks if the author exists */ -exports.doesAuthorExists = function (authorID, callback) +exports.doesAuthorExists = function(authorID, callback) { - //check if the database entry of this author exists - db.get("globalAuthor:" + authorID, function (err, author) - { - if(ERR(err, callback)) return; + // check if the database entry of this author exists + db.get("globalAuthor:" + authorID, function(err, author) { + if (ERR(err, callback)) return; + callback(null, author != null); }); } @@ -46,12 +54,12 @@ exports.doesAuthorExists = function (authorID, callback) * @param {String} token The token * @param {Function} callback callback (err, author) */ -exports.getAuthor4Token = function (token, callback) +exports.getAuthor4Token = function(token, callback) { - mapAuthorWithDBKey("token2author", token, function(err, author) - { - if(ERR(err, callback)) return; - //return only the sub value authorID + mapAuthorWithDBKey("token2author", token, function(err, author) { + if (ERR(err, callback)) return; + + // return only the sub value authorID callback(null, author ? author.authorID : author); }); } @@ -62,17 +70,17 @@ exports.getAuthor4Token = function (token, callback) * @param {String} name The name of the author (optional) * @param {Function} callback callback (err, author) */ -exports.createAuthorIfNotExistsFor = function (authorMapper, name, callback) +exports.createAuthorIfNotExistsFor = function(authorMapper, name, callback) { - mapAuthorWithDBKey("mapper2author", authorMapper, function(err, author) - { - if(ERR(err, callback)) return; + mapAuthorWithDBKey("mapper2author", authorMapper, function(err, author) { + if (ERR(err, callback)) return; - //set the name of this author - if(name) + if (name) { + // set the name of this author exports.setAuthorName(author.authorID, name); + } - //return the authorID + // return the authorID callback(null, author); }); } @@ -86,33 +94,30 @@ exports.createAuthorIfNotExistsFor = function (authorMapper, name, callback) */ function mapAuthorWithDBKey (mapperkey, mapper, callback) { - //try to map to an author - db.get(mapperkey + ":" + mapper, function (err, author) - { - if(ERR(err, callback)) return; + // try to map to an author + db.get(mapperkey + ":" + mapper, function(err, author) { + if (ERR(err, callback)) return; - //there is no author with this mapper, so create one - if(author == null) - { - exports.createAuthor(null, function(err, author) - { - if(ERR(err, callback)) return; + if (author == null) { + // there is no author with this mapper, so create one + exports.createAuthor(null, function(err, author) { + if (ERR(err, callback)) return; - //create the token2author relation + // create the token2author relation db.set(mapperkey + ":" + mapper, author.authorID); - //return the author + // return the author callback(null, author); }); return; } - //there is a author with this mapper - //update the timestamp of this author + // there is an author with this mapper + // update the timestamp of this author db.setSub("globalAuthor:" + author, ["timestamp"], Date.now()); - //return the author + // return the author callback(null, {authorID: author}); }); } @@ -123,13 +128,17 @@ function mapAuthorWithDBKey (mapperkey, mapper, callback) */ exports.createAuthor = function(name, callback) { - //create the new author name + // create the new author name var author = "a." + randomString(16); - //create the globalAuthors db entry - var authorObj = {"colorId" : Math.floor(Math.random()*(exports.getColorPalette().length)), "name": name, "timestamp": Date.now()}; + // create the globalAuthors db entry + var authorObj = { + "colorId": Math.floor(Math.random() * (exports.getColorPalette().length)), + "name": name, + "timestamp": Date.now() + }; - //set the global author db entry + // set the global author db entry db.set("globalAuthor:" + author, authorObj); callback(null, {authorID: author}); @@ -140,19 +149,17 @@ exports.createAuthor = function(name, callback) * @param {String} author The id of the author * @param {Function} callback callback(err, authorObj) */ -exports.getAuthor = function (author, callback) +exports.getAuthor = function(author, callback) { db.get("globalAuthor:" + author, callback); } - - /** * Returns the color Id of the author * @param {String} author The id of the author * @param {Function} callback callback(err, colorId) */ -exports.getAuthorColorId = function (author, callback) +exports.getAuthorColorId = function(author, callback) { db.getSub("globalAuthor:" + author, ["colorId"], callback); } @@ -163,7 +170,7 @@ exports.getAuthorColorId = function (author, callback) * @param {String} colorId The color id of the author * @param {Function} callback (optional) */ -exports.setAuthorColorId = function (author, colorId, callback) +exports.setAuthorColorId = function(author, colorId, callback) { db.setSub("globalAuthor:" + author, ["colorId"], colorId, callback); } @@ -173,7 +180,7 @@ exports.setAuthorColorId = function (author, colorId, callback) * @param {String} author The id of the author * @param {Function} callback callback(err, name) */ -exports.getAuthorName = function (author, callback) +exports.getAuthorName = function(author, callback) { db.getSub("globalAuthor:" + author, ["name"], callback); } @@ -184,7 +191,7 @@ exports.getAuthorName = function (author, callback) * @param {String} name The name of the author * @param {Function} callback (optional) */ -exports.setAuthorName = function (author, name, callback) +exports.setAuthorName = function(author, name, callback) { db.setSub("globalAuthor:" + author, ["name"], name, callback); } @@ -194,33 +201,33 @@ exports.setAuthorName = function (author, name, callback) * @param {String} author The id of the author * @param {Function} callback (optional) */ -exports.listPadsOfAuthor = function (authorID, callback) +exports.listPadsOfAuthor = function(authorID, callback) { /* There are two other places where this array is manipulated: * (1) When the author is added to a pad, the author object is also updated * (2) When a pad is deleted, each author of that pad is also updated */ - //get the globalAuthor - db.get("globalAuthor:" + authorID, function(err, author) - { - if(ERR(err, callback)) return; - //author does not exists - if(author == null) - { - callback(new customError("authorID does not exist","apierror")) + // get the globalAuthor + db.get("globalAuthor:" + authorID, function(err, author) { + if (ERR(err, callback)) return; + + if (author == null) { + // author does not exist + callback(new customError("authorID does not exist", "apierror")); + return; } - //everything is fine, return the pad IDs + // everything is fine, return the pad IDs var pads = []; - if(author.padIDs != null) - { - for (var padId in author.padIDs) - { + + if (author.padIDs != null) { + for (var padId in author.padIDs) { pads.push(padId); } } + callback(null, {padIDs: pads}); }); } @@ -230,24 +237,22 @@ exports.listPadsOfAuthor = function (authorID, callback) * @param {String} author The id of the author * @param {String} padID The id of the pad the author contributes to */ -exports.addPad = function (authorID, padID) +exports.addPad = function(authorID, padID) { - //get the entry - db.get("globalAuthor:" + authorID, function(err, author) - { - if(ERR(err)) return; - if(author == null) return; + // get the entry + db.get("globalAuthor:" + authorID, function(err, author) { + if (ERR(err)) return; + if (author == null) return; - //the entry doesn't exist so far, let's create it - if(author.padIDs == null) - { + if (author.padIDs == null) { + // the entry doesn't exist so far, let's create it author.padIDs = {}; } - //add the entry for this pad - author.padIDs[padID] = 1;// anything, because value is not used + // add the entry for this pad + author.padIDs[padID] = 1; // anything, because value is not used - //save the new element back + // save the new element back db.set("globalAuthor:" + authorID, author); }); } @@ -257,16 +262,14 @@ exports.addPad = function (authorID, padID) * @param {String} author The id of the author * @param {String} padID The id of the pad the author contributes to */ -exports.removePad = function (authorID, padID) +exports.removePad = function(authorID, padID) { - db.get("globalAuthor:" + authorID, function (err, author) - { - if(ERR(err)) return; - if(author == null) return; + db.get("globalAuthor:" + authorID, function(err, author) { + if (ERR(err)) return; + if (author == null) return; - if(author.padIDs != null) - { - //remove pad from author + if (author.padIDs != null) { + // remove pad from author delete author.padIDs[padID]; db.set("globalAuthor:" + authorID, author); } diff --git a/src/node/db/DB.js b/src/node/db/DB.js index 3c65d5cdb..93848b1bd 100644 --- a/src/node/db/DB.js +++ b/src/node/db/DB.js @@ -1,5 +1,5 @@ /** - * The DB Module provides a database initalized with the settings + * The DB Module provides a database initalized with the settings * provided by the settings module */ @@ -23,7 +23,7 @@ var ueberDB = require("ueberdb2"); var settings = require("../utils/Settings"); var log4js = require('log4js'); -//set database settings +// set database settings var db = new ueberDB.database(settings.dbType, settings.dbSettings, null, log4js.getLogger("ueberDB")); /** @@ -33,24 +33,19 @@ exports.db = null; /** * Initalizes the database with the settings provided by the settings module - * @param {Function} callback + * @param {Function} callback */ -exports.init = function(callback) -{ - //initalize the database async - db.init(function(err) - { - //there was an error while initializing the database, output it and stop - if(err) - { +exports.init = function(callback) { + // initalize the database async + db.init(function(err) { + if (err) { + // there was an error while initializing the database, output it and stop console.error("ERROR: Problem while initalizing the database"); console.error(err.stack ? err.stack : err); process.exit(1); - } - //everything ok - else - { - exports.db = db; + } else { + // everything ok + exports.db = db; callback(null); } }); diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.js index 0c9be1221..5f8183ea9 100644 --- a/src/node/db/GroupManager.js +++ b/src/node/db/GroupManager.js @@ -17,7 +17,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - var ERR = require("async-stacktrace"); var customError = require("../utils/customError"); @@ -29,117 +28,112 @@ var sessionManager = require("./SessionManager"); exports.listAllGroups = function(callback) { db.get("groups", function (err, groups) { - if(ERR(err, callback)) return; - - // there are no groups - if(groups == null) { + if (ERR(err, callback)) return; + + if (groups == null) { + // there are no groups callback(null, {groupIDs: []}); + return; } - + var groupIDs = []; - for ( var groupID in groups ) { + for (var groupID in groups) { groupIDs.push(groupID); } callback(null, {groupIDs: groupIDs}); }); } - + exports.deleteGroup = function(groupID, callback) { var group; async.series([ - //ensure group exists - function (callback) - { - //try to get the group entry - db.get("group:" + groupID, function (err, _group) - { - if(ERR(err, callback)) return; - - //group does not exist - if(_group == null) - { - callback(new customError("groupID does not exist","apierror")); + // ensure group exists + function (callback) { + // try to get the group entry + db.get("group:" + groupID, function (err, _group) { + if (ERR(err, callback)) return; + + if (_group == null) { + // group does not exist + callback(new customError("groupID does not exist", "apierror")); return; } - //group exists, everything is fine + // group exists, everything is fine group = _group; callback(); }); }, - //iterate trough all pads of this groups and delete them - function(callback) - { - //collect all padIDs in an array, that allows us to use async.forEach + + // iterate through all pads of this group and delete them + function(callback) { + // collect all padIDs in an array, that allows us to use async.forEach var padIDs = []; - for(var i in group.pads) - { + for(var i in group.pads) { padIDs.push(i); } - - //loop trough all pads and delete them - async.forEach(padIDs, function(padID, callback) - { - padManager.getPad(padID, function(err, pad) - { - if(ERR(err, callback)) return; - + + // loop through all pads and delete them + async.forEach(padIDs, function(padID, callback) { + padManager.getPad(padID, function(err, pad) { + if (ERR(err, callback)) return; + pad.remove(callback); }); }, callback); }, - //iterate trough group2sessions and delete all sessions + + // iterate through group2sessions and delete all sessions function(callback) { - //try to get the group entry - db.get("group2sessions:" + groupID, function (err, group2sessions) - { - if(ERR(err, callback)) return; - - //skip if there is no group2sessions entry - if(group2sessions == null) {callback(); return} - - //collect all sessions in an array, that allows us to use async.forEach + // try to get the group entry + db.get("group2sessions:" + groupID, function (err, group2sessions) { + if (ERR(err, callback)) return; + + // skip if there is no group2sessions entry + if (group2sessions == null) { callback(); return } + + // collect all sessions in an array, that allows us to use async.forEach var sessions = []; - for(var i in group2sessions.sessionsIDs) - { + for (var i in group2sessions.sessionsIDs) { sessions.push(i); } - - //loop trough all sessions and delete them - async.forEach(sessions, function(session, callback) - { + + // loop through all sessions and delete them + async.forEach(sessions, function(session, callback) { sessionManager.deleteSession(session, callback); }, callback); }); }, - //remove group and group2sessions entry - function(callback) - { + + // remove group and group2sessions entry + function(callback) { db.remove("group2sessions:" + groupID); db.remove("group:" + groupID); callback(); }, - //unlist the group - function(callback) - { + + // unlist the group + function(callback) { exports.listAllGroups(function(err, groups) { - if(ERR(err, callback)) return; + if (ERR(err, callback)) return; groups = groups? groups.groupIDs : []; - // it's not listed - if(groups.indexOf(groupID) == -1) { + if (groups.indexOf(groupID) == -1) { + // it's not listed callback(); + return; } + // remove from the list groups.splice(groups.indexOf(groupID), 1); - - // store empty groupe list - if(groups.length == 0) { + + // store empty group list + if (groups.length == 0) { db.set("groups", {}); callback(); return; @@ -150,50 +144,51 @@ exports.deleteGroup = function(groupID, callback) async.forEach(groups, function(group, cb) { newGroups[group] = 1; cb(); - },function() { + }, + function() { db.set("groups", newGroups); callback(); }); }); } - ], function(err) - { - if(ERR(err, callback)) return; + ], + function(err) { + if (ERR(err, callback)) return; callback(); }); } - + exports.doesGroupExist = function(groupID, callback) { - //try to get the group entry - db.get("group:" + groupID, function (err, group) - { - if(ERR(err, callback)) return; + // try to get the group entry + db.get("group:" + groupID, function (err, group) { + if (ERR(err, callback)) return; callback(null, group != null); }); } exports.createGroup = function(callback) { - //search for non existing groupID + // search for non existing groupID var groupID = "g." + randomString(16); - - //create the group + + // create the group db.set("group:" + groupID, {pads: {}}); - - //list the group + + // list the group exports.listAllGroups(function(err, groups) { - if(ERR(err, callback)) return; + if (ERR(err, callback)) return; + groups = groups? groups.groupIDs : []; - groups.push(groupID); - + // regenerate group list var newGroups = {}; async.forEach(groups, function(group, cb) { newGroups[group] = 1; cb(); - },function() { + }, + function() { db.set("groups", newGroups); callback(null, {groupID: groupID}); }); @@ -202,129 +197,121 @@ exports.createGroup = function(callback) exports.createGroupIfNotExistsFor = function(groupMapper, callback) { - //ensure mapper is optional - if(typeof groupMapper != "string") - { - callback(new customError("groupMapper is no string","apierror")); + // ensure mapper is optional + if (typeof groupMapper != "string") { + callback(new customError("groupMapper is no string", "apierror")); return; } - - //try to get a group for this mapper - db.get("mapper2group:"+groupMapper, function(err, groupID) - { + + // try to get a group for this mapper + db.get("mapper2group:" + groupMapper, function(err, groupID) { function createGroupForMapper(cb) { - exports.createGroup(function(err, responseObj) - { - if(ERR(err, cb)) return; - - //create the mapper entry for this group - db.set("mapper2group:"+groupMapper, responseObj.groupID); - + exports.createGroup(function(err, responseObj) { + if (ERR(err, cb)) return; + + // create the mapper entry for this group + db.set("mapper2group:" + groupMapper, responseObj.groupID); + cb(null, responseObj); }); } - if(ERR(err, callback)) return; - - // there is a group for this mapper - if(groupID) { + if (ERR(err, callback)) return; + + if (groupID) { + // there is a group for this mapper exports.doesGroupExist(groupID, function(err, exists) { - if(ERR(err, callback)) return; - if(exists) return callback(null, {groupID: groupID}); + if (ERR(err, callback)) return; + + if (exists) return callback(null, {groupID: groupID}); // hah, the returned group doesn't exist, let's create one createGroupForMapper(callback) - }) + }); return; } - //there is no group for this mapper, let's create a group + // there is no group for this mapper, let's create a group createGroupForMapper(callback) }); } exports.createGroupPad = function(groupID, padName, text, callback) { - //create the padID + // create the padID var padID = groupID + "$" + padName; async.series([ - //ensure group exists - function (callback) - { - exports.doesGroupExist(groupID, function(err, exists) - { - if(ERR(err, callback)) return; - - //group does not exist - if(exists == false) - { - callback(new customError("groupID does not exist","apierror")); + // ensure group exists + function (callback) { + exports.doesGroupExist(groupID, function(err, exists) { + if (ERR(err, callback)) return; + + if (exists == false) { + // group does not exist + callback(new customError("groupID does not exist", "apierror")); return; } - //group exists, everything is fine + // group exists, everything is fine callback(); }); }, - //ensure pad does not exists - function (callback) - { - padManager.doesPadExists(padID, function(err, exists) - { - if(ERR(err, callback)) return; - - //pad exists already - if(exists == true) - { - callback(new customError("padName does already exist","apierror")); + + // ensure pad doesn't exist already + function (callback) { + padManager.doesPadExists(padID, function(err, exists) { + if (ERR(err, callback)) return; + + if (exists == true) { + // pad exists already + callback(new customError("padName does already exist", "apierror")); return; } - //pad does not exist, everything is fine + // pad does not exist, everything is fine callback(); }); }, - //create the pad - function (callback) - { - padManager.getPad(padID, text, function(err) - { - if(ERR(err, callback)) return; + + // create the pad + function (callback) { + padManager.getPad(padID, text, function(err) { + if (ERR(err, callback)) return; + callback(); }); }, - //create an entry in the group for this pad - function (callback) - { + + // create an entry in the group for this pad + function (callback) { db.setSub("group:" + groupID, ["pads", padID], 1); callback(); } - ], function(err) - { - if(ERR(err, callback)) return; + ], + function(err) { + if (ERR(err, callback)) return; + callback(null, {padID: padID}); }); } exports.listPads = function(groupID, callback) { - exports.doesGroupExist(groupID, function(err, exists) - { - if(ERR(err, callback)) return; - - //group does not exist - if(exists == false) - { - callback(new customError("groupID does not exist","apierror")); + exports.doesGroupExist(groupID, function(err, exists) { + if (ERR(err, callback)) return; + + // ensure the group exists + if (exists == false) { + callback(new customError("groupID does not exist", "apierror")); return; } - //group exists, let's get the pads - db.getSub("group:" + groupID, ["pads"], function(err, result) - { - if(ERR(err, callback)) return; + // group exists, let's get the pads + db.getSub("group:" + groupID, ["pads"], function(err, result) { + if (ERR(err, callback)) return; + var pads = []; for ( var padId in result ) { pads.push(padId); diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index b74de5228..c5a75a89f 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -19,7 +19,7 @@ var crypto = require("crypto"); var randomString = require("../utils/randomstring"); var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -//serialization/deserialization attributes +// serialization/deserialization attributes var attributeBlackList = ["id"]; var jsonableList = ["pool"]; @@ -33,7 +33,6 @@ exports.cleanText = function (txt) { var Pad = function Pad(id) { - this.atext = Changeset.makeAText("\n"); this.pool = new AttributePool(); this.head = -1; @@ -60,7 +59,7 @@ Pad.prototype.getSavedRevisionsNumber = function getSavedRevisionsNumber() { Pad.prototype.getSavedRevisionsList = function getSavedRevisionsList() { var savedRev = new Array(); - for(var rev in this.savedRevisions){ + for (var rev in this.savedRevisions) { savedRev.push(this.savedRevisions[rev].revNum); } savedRev.sort(function(a, b) { @@ -74,8 +73,9 @@ Pad.prototype.getPublicStatus = function getPublicStatus() { }; Pad.prototype.appendRevision = function appendRevision(aChangeset, author) { - if(!author) + if (!author) { author = ''; + } var newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool); Changeset.copyAText(newAText, this.atext); @@ -88,21 +88,22 @@ Pad.prototype.appendRevision = function appendRevision(aChangeset, author) { newRevData.meta.author = author; newRevData.meta.timestamp = Date.now(); - //ex. getNumForAuthor - if(author != '') + // ex. getNumForAuthor + if (author != '') { this.pool.putAttrib(['author', author || '']); + } - if(newRev % 100 == 0) - { + if (newRev % 100 == 0) { newRevData.meta.atext = this.atext; } - db.set("pad:"+this.id+":revs:"+newRev, newRevData); + db.set("pad:" + this.id + ":revs:" + newRev, newRevData); this.saveToDatabase(); // set the author to pad - if(author) + if (author) { authorManager.addPad(author, this.id); + } if (this.head == 0) { hooks.callAll("padCreate", {'pad':this, 'author': author}); @@ -111,49 +112,47 @@ Pad.prototype.appendRevision = function appendRevision(aChangeset, author) { } }; -//save all attributes to the database -Pad.prototype.saveToDatabase = function saveToDatabase(){ +// save all attributes to the database +Pad.prototype.saveToDatabase = function saveToDatabase() { var dbObject = {}; - for(var attr in this){ - if(typeof this[attr] === "function") continue; - if(attributeBlackList.indexOf(attr) !== -1) continue; + for (var attr in this) { + if (typeof this[attr] === "function") continue; + if (attributeBlackList.indexOf(attr) !== -1) continue; dbObject[attr] = this[attr]; - if(jsonableList.indexOf(attr) !== -1){ + if (jsonableList.indexOf(attr) !== -1) { dbObject[attr] = dbObject[attr].toJsonable(); } } - db.set("pad:"+this.id, dbObject); + db.set("pad:" + this.id, dbObject); } // get time of last edit (changeset application) -Pad.prototype.getLastEdit = function getLastEdit(callback){ +Pad.prototype.getLastEdit = function getLastEdit(callback) { var revNum = this.getHeadRevisionNumber(); - db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "timestamp"], callback); + db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "timestamp"], callback); } Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum, callback) { - db.getSub("pad:"+this.id+":revs:"+revNum, ["changeset"], callback); + db.getSub("pad:" + this.id + ":revs:" + revNum, ["changeset"], callback); }; Pad.prototype.getRevisionAuthor = function getRevisionAuthor(revNum, callback) { - db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "author"], callback); + db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "author"], callback); }; Pad.prototype.getRevisionDate = function getRevisionDate(revNum, callback) { - db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "timestamp"], callback); + db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "timestamp"], callback); }; Pad.prototype.getAllAuthors = function getAllAuthors() { var authors = []; - for(var key in this.pool.numToAttrib) - { - if(this.pool.numToAttrib[key][0] == "author" && this.pool.numToAttrib[key][1] != "") - { + for(var key in this.pool.numToAttrib) { + if (this.pool.numToAttrib[key][0] == "author" && this.pool.numToAttrib[key][1] != "") { authors.push(this.pool.numToAttrib[key][1]); } } @@ -168,26 +167,22 @@ Pad.prototype.getInternalRevisionAText = function getInternalRevisionAText(targe var atext; var changesets = []; - //find out which changesets are needed + // find out which changesets are needed var neededChangesets = []; var curRev = keyRev; - while (curRev < targetRev) - { + while (curRev < targetRev) { curRev++; neededChangesets.push(curRev); } async.series([ - //get all needed data out of the database - function(callback) - { + // get all needed data out of the database + function(callback) { async.parallel([ - //get the atext of the key revision - function (callback) - { - db.getSub("pad:"+_this.id+":revs:"+keyRev, ["meta", "atext"], function(err, _atext) - { - if(ERR(err, callback)) return; + // get the atext of the key revision + function (callback) { + db.getSub("pad:" + _this.id + ":revs:" + keyRev, ["meta", "atext"], function(err, _atext) { + if (ERR(err, callback)) return; try { atext = Changeset.cloneAText(_atext); } catch (e) { @@ -197,14 +192,12 @@ Pad.prototype.getInternalRevisionAText = function getInternalRevisionAText(targe callback(); }); }, - //get all needed changesets - function (callback) - { - async.forEach(neededChangesets, function(item, callback) - { - _this.getRevisionChangeset(item, function(err, changeset) - { - if(ERR(err, callback)) return; + + // get all needed changesets + function (callback) { + async.forEach(neededChangesets, function(item, callback) { + _this.getRevisionChangeset(item, function(err, changeset) { + if (ERR(err, callback)) return; changesets[item] = changeset; callback(); }); @@ -212,52 +205,53 @@ Pad.prototype.getInternalRevisionAText = function getInternalRevisionAText(targe } ], callback); }, - //apply all changesets to the key changeset - function(callback) - { + + // apply all changesets to the key changeset + function(callback) { var apool = _this.apool(); var curRev = keyRev; - while (curRev < targetRev) - { + while (curRev < targetRev) { curRev++; var cs = changesets[curRev]; - try{ + try { atext = Changeset.applyToAText(cs, atext, apool); - }catch(e) { + } catch(e) { return callback(e) } } callback(null); } - ], function(err) - { - if(ERR(err, callback)) return; + ], + function(err) { + if (ERR(err, callback)) return; callback(null, atext); }); }; Pad.prototype.getRevision = function getRevisionChangeset(revNum, callback) { - db.get("pad:"+this.id+":revs:"+revNum, callback); + db.get("pad:" + this.id + ":revs:" + revNum, callback); }; -Pad.prototype.getAllAuthorColors = function getAllAuthorColors(callback){ +Pad.prototype.getAllAuthorColors = function getAllAuthorColors(callback) { var authors = this.getAllAuthors(); var returnTable = {}; var colorPalette = authorManager.getColorPalette(); - async.forEach(authors, function(author, callback){ - authorManager.getAuthorColorId(author, function(err, colorId){ - if(err){ + async.forEach(authors, function(author, callback) { + authorManager.getAuthorColorId(author, function(err, colorId) { + if (err) { return callback(err); } - //colorId might be a hex color or an number out of the palette - returnTable[author]=colorPalette[colorId] || colorId; + + // colorId might be a hex color or an number out of the palette + returnTable[author] = colorPalette[colorId] || colorId; callback(); }); - }, function(err){ + }, + function(err) { callback(err, returnTable); }); }; @@ -266,15 +260,18 @@ Pad.prototype.getValidRevisionRange = function getValidRevisionRange(startRev, e startRev = parseInt(startRev, 10); var head = this.getHeadRevisionNumber(); endRev = endRev ? parseInt(endRev, 10) : head; - if(isNaN(startRev) || startRev < 0 || startRev > head) { + + if (isNaN(startRev) || startRev < 0 || startRev > head) { startRev = null; } - if(isNaN(endRev) || endRev < startRev) { + + if (isNaN(endRev) || endRev < startRev) { endRev = null; - } else if(endRev > head) { + } else if (endRev > head) { endRev = head; } - if(startRev !== null && endRev !== null) { + + if (startRev !== null && endRev !== null) { return { startRev: startRev , endRev: endRev } } return null; @@ -289,12 +286,12 @@ Pad.prototype.text = function text() { }; Pad.prototype.setText = function setText(newText) { - //clean the new text + // clean the new text newText = exports.cleanText(newText); var oldText = this.text(); - //create the changeset + // create the changeset // We want to ensure the pad still ends with a \n, but otherwise keep // getText() and setText() consistent. var changeset; @@ -304,27 +301,27 @@ Pad.prototype.setText = function setText(newText) { changeset = Changeset.makeSplice(oldText, 0, oldText.length-1, newText); } - //append the changeset + // append the changeset this.appendRevision(changeset); }; Pad.prototype.appendText = function appendText(newText) { - //clean the new text + // clean the new text newText = exports.cleanText(newText); var oldText = this.text(); - //create the changeset + // create the changeset var changeset = Changeset.makeSplice(oldText, oldText.length, 0, newText); - //append the changeset + // append the changeset this.appendRevision(changeset); }; Pad.prototype.appendChatMessage = function appendChatMessage(text, userId, time) { this.chatHead++; - //save the chat entry in the database - db.set("pad:"+this.id+":chat:"+this.chatHead, {"text": text, "userId": userId, "time": time}); + // save the chat entry in the database + db.set("pad:" + this.id + ":chat:" + this.chatHead, { "text": text, "userId": userId, "time": time }); this.saveToDatabase(); }; @@ -333,77 +330,70 @@ Pad.prototype.getChatMessage = function getChatMessage(entryNum, callback) { var entry; async.series([ - //get the chat entry - function(callback) - { - db.get("pad:"+_this.id+":chat:"+entryNum, function(err, _entry) - { - if(ERR(err, callback)) return; + // get the chat entry + function(callback) { + db.get("pad:" + _this.id + ":chat:" + entryNum, function(err, _entry) { + if (ERR(err, callback)) return; entry = _entry; callback(); }); }, - //add the authorName - function(callback) - { - //this chat message doesn't exist, return null - if(entry == null) - { + + // add the authorName + function(callback) { + // this chat message doesn't exist, return null + if (entry == null) { callback(); return; } - //get the authorName - authorManager.getAuthorName(entry.userId, function(err, authorName) - { - if(ERR(err, callback)) return; + // get the authorName + authorManager.getAuthorName(entry.userId, function(err, authorName) { + if (ERR(err, callback)) return; entry.userName = authorName; callback(); }); } - ], function(err) - { - if(ERR(err, callback)) return; + ], + function(err) { + if (ERR(err, callback)) return; callback(null, entry); }); }; Pad.prototype.getChatMessages = function getChatMessages(start, end, callback) { - //collect the numbers of chat entries and in which order we need them + // collect the numbers of chat entries and in which order we need them var neededEntries = []; var order = 0; - for(var i=start;i<=end; i++) - { - neededEntries.push({entryNum:i, order: order}); + for (var i = start; i <= end; i++) { + neededEntries.push({ entryNum: i, order: order }); order++; } var _this = this; - //get all entries out of the database + // get all entries out of the database var entries = []; - async.forEach(neededEntries, function(entryObject, callback) - { - _this.getChatMessage(entryObject.entryNum, function(err, entry) - { - if(ERR(err, callback)) return; + async.forEach(neededEntries, function(entryObject, callback) { + _this.getChatMessage(entryObject.entryNum, function(err, entry) { + if (ERR(err, callback)) return; entries[entryObject.order] = entry; callback(); }); - }, function(err) - { - if(ERR(err, callback)) return; + }, + function(err) { + if (ERR(err, callback)) return; - //sort out broken chat entries - //it looks like in happend in the past that the chat head was - //incremented, but the chat message wasn't added + // sort out broken chat entries + // it looks like in happened in the past that the chat head was + // incremented, but the chat message wasn't added var cleanedEntries = []; - for(var i=0;i delete the entry of this pad in the group - function(callback) - { - if(padID.indexOf("$") === -1) - { + // is it a group pad? -> delete the entry of this pad in the group + function(callback) { + if (padID.indexOf("$") === -1) { // it isn't a group pad, nothing to do here callback(); return; } // it is a group pad - var groupID = padID.substring(0,padID.indexOf("$")); + var groupID = padID.substring(0, padID.indexOf("$")); - db.get("group:" + groupID, function (err, group) - { - if(ERR(err, callback)) return; + db.get("group:" + groupID, function (err, group) { + if (ERR(err, callback)) return; - //remove the pad entry + // remove the pad entry delete group.pads[padID]; - //set the new value + // set the new value db.set("group:" + groupID, group); callback(); }); }, - //remove the readonly entries - function(callback) - { - readOnlyManager.getReadOnlyId(padID, function(err, readonlyID) - { - if(ERR(err, callback)) return; + + // remove the readonly entries + function(callback) { + readOnlyManager.getReadOnlyId(padID, function(err, readonlyID) { + if (ERR(err, callback)) return; db.remove("pad2readonly:" + padID); db.remove("readonly2pad:" + readonlyID); @@ -656,37 +629,34 @@ Pad.prototype.remove = function remove(callback) { callback(); }); }, - //delete all chat messages - function(callback) - { + + // delete all chat messages + function(callback) { var chatHead = _this.chatHead; - for(var i=0;i<=chatHead;i++) - { - db.remove("pad:"+padID+":chat:"+i); + for (var i = 0; i <= chatHead; i++) { + db.remove("pad:" + padID + ":chat:" + i); } callback(); }, - //delete all revisions - function(callback) - { + + // delete all revisions + function(callback) { var revHead = _this.head; - for(var i=0;i<=revHead;i++) - { - db.remove("pad:"+padID+":revs:"+i); + for (var i = 0; i <= revHead; i++) { + db.remove("pad:" + padID + ":revs:" + i); } callback(); }, - //remove pad from all authors who contributed - function(callback) - { + + // remove pad from all authors who contributed + function(callback) { var authorIDs = _this.getAllAuthors(); - authorIDs.forEach(function (authorID) - { + authorIDs.forEach(function (authorID) { authorManager.removePad(authorID, padID); }); @@ -694,20 +664,21 @@ Pad.prototype.remove = function remove(callback) { } ], callback); }, - //delete the pad entry and delete pad from padManager - function(callback) - { + + // delete the pad entry and delete pad from padManager + function(callback) { padManager.removePad(padID); - hooks.callAll("padRemove", {'padID':padID}); + hooks.callAll("padRemove", { 'padID': padID }); callback(); } - ], function(err) - { - if(ERR(err, callback)) return; + ], + function(err) { + if (ERR(err, callback)) return; callback(); }); }; - //set in db + +// set in db Pad.prototype.setPublicStatus = function setPublicStatus(publicStatus) { this.publicStatus = publicStatus; this.saveToDatabase(); @@ -727,14 +698,14 @@ Pad.prototype.isPasswordProtected = function isPasswordProtected() { }; Pad.prototype.addSavedRevision = function addSavedRevision(revNum, savedById, label) { - //if this revision is already saved, return silently - for(var i in this.savedRevisions){ - if(this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum){ + // if this revision is already saved, return silently + for (var i in this.savedRevisions) { + if (this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum) { return; } } - //build the saved revision object + // build the saved revision object var savedRevision = {}; savedRevision.revNum = revNum; savedRevision.savedById = savedById; @@ -742,7 +713,7 @@ Pad.prototype.addSavedRevision = function addSavedRevision(revNum, savedById, la savedRevision.timestamp = Date.now(); savedRevision.id = randomString(10); - //save this new saved revision + // save this new saved revision this.savedRevisions.push(savedRevision); this.saveToDatabase(); }; @@ -753,19 +724,17 @@ Pad.prototype.getSavedRevisions = function getSavedRevisions() { /* Crypto helper methods */ -function hash(password, salt) -{ +function hash(password, salt) { var shasum = crypto.createHash('sha512'); shasum.update(password + salt); + return shasum.digest("hex") + "$" + salt; } -function generateSalt() -{ +function generateSalt() { return randomString(86); } -function compare(hashStr, password) -{ +function compare(hashStr, password) { return hash(password, hashStr.split("$")[1]) === hashStr; } diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index 035ef3e5e..b198de81c 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -23,7 +23,7 @@ var customError = require("../utils/customError"); var Pad = require("../db/Pad").Pad; var db = require("./DB").db; -/** +/** * A cache of all loaded Pads. * * Provides "get" and "set" functions, @@ -35,12 +35,11 @@ var db = require("./DB").db; * that's defined somewhere more sensible. */ var globalPads = { - get: function (name) { return this[':'+name]; }, - set: function (name, value) - { + get: function(name) { return this[':'+name]; }, + set: function(name, value) { this[':'+name] = value; }, - remove: function (name) { + remove: function(name) { delete this[':'+name]; } }; @@ -54,56 +53,63 @@ var padList = { list: [], sorted : false, initiated: false, - init: function(cb) - { - db.findKeys("pad:*", "*:*:*", function(err, dbData) - { - if(ERR(err, cb)) return; - if(dbData != null){ + init: function(cb) { + db.findKeys("pad:*", "*:*:*", function(err, dbData) { + if (ERR(err, cb)) return; + + if (dbData != null) { padList.initiated = true - dbData.forEach(function(val){ + dbData.forEach(function(val) { padList.addPad(val.replace(/pad:/,""),false); }); - cb && cb() + + cb && cb(); } }); + return this; }, load: function(cb) { - if(this.initiated) cb && cb() - else this.init(cb) + if (this.initiated) { + cb && cb(); + } else { + this.init(cb); + } }, /** * Returns all pads in alphabetical order as array. */ - getPads: function(cb){ + getPads: function(cb) { this.load(function() { - if(!padList.sorted){ + if (!padList.sorted) { padList.list = padList.list.sort(); padList.sorted = true; } + cb && cb(padList.list); }) }, - addPad: function(name) - { - if(!this.initiated) return; - if(this.list.indexOf(name) == -1){ + addPad: function(name) { + if (!this.initiated) return; + + if (this.list.indexOf(name) == -1) { this.list.push(name); - this.sorted=false; + this.sorted = false; } }, - removePad: function(name) - { - if(!this.initiated) return; + removePad: function(name) { + if (!this.initiated) return; + var index = this.list.indexOf(name); - if(index>-1){ - this.list.splice(index,1); - this.sorted=false; + + if (index > -1) { + this.list.splice(index, 1); + this.sorted = false; } } }; -//initialises the allknowing data structure + +// initialises the all-knowing data structure /** * An array of padId transformations. These represent changes in pad name policy over @@ -117,58 +123,56 @@ var padIdTransforms = [ /** * Returns a Pad Object with the callback * @param id A String with the id of the pad - * @param {Function} callback + * @param {Function} callback */ exports.getPad = function(id, text, callback) -{ - //check if this is a valid padId - if(!exports.isValidPadId(id)) - { - callback(new customError(id + " is not a valid padId","apierror")); +{ + // check if this is a valid padId + if (!exports.isValidPadId(id)) { + callback(new customError(id + " is not a valid padId", "apierror")); + return; } - - //make text an optional parameter - if(typeof text == "function") - { + + // make text an optional parameter + if (typeof text == "function") { callback = text; text = null; } - - //check if this is a valid text - if(text != null) - { - //check if text is a string - if(typeof text != "string") - { - callback(new customError("text is not a string","apierror")); + + // check if this is a valid text + if (text != null) { + // check if text is a string + if (typeof text != "string") { + callback(new customError("text is not a string", "apierror")); + return; } - - //check if text is less than 100k chars - if(text.length > 100000) - { - callback(new customError("text must be less than 100k chars","apierror")); + + // check if text is less than 100k chars + if (text.length > 100000) { + callback(new customError("text must be less than 100k chars", "apierror")); + return; } } - + var pad = globalPads.get(id); - - //return pad if its already loaded - if(pad != null) - { + + // return pad if it's already loaded + if (pad != null) { callback(null, pad); + return; } - //try to load pad + // try to load pad pad = new Pad(id); - //initalize the pad - pad.init(text, function(err) - { - if(ERR(err, callback)) return; + // initalize the pad + pad.init(text, function(err) { + if (ERR(err, callback)) return; + globalPads.set(id, pad); padList.addPad(id); callback(null, pad); @@ -182,49 +186,48 @@ exports.listAllPads = function(cb) }); } -//checks if a pad exists +// checks if a pad exists exports.doesPadExists = function(padId, callback) { - db.get("pad:"+padId, function(err, value) - { - if(ERR(err, callback)) return; - if(value != null && value.atext){ + db.get("pad:" + padId, function(err, value) { + if (ERR(err, callback)) return; + + if (value != null && value.atext) { callback(null, true); - } - else - { - callback(null, false); + } else { + callback(null, false); } }); } -//returns a sanitized padId, respecting legacy pad id formats +// returns a sanitized padId, respecting legacy pad id formats exports.sanitizePadId = function(padId, callback) { var transform_index = arguments[2] || 0; - //we're out of possible transformations, so just return it - if(transform_index >= padIdTransforms.length) - { + + // we're out of possible transformations, so just return it + if (transform_index >= padIdTransforms.length) { callback(padId); + return; } - //check if padId exists - exports.doesPadExists(padId, function(junk, exists) - { - if(exists) - { + // check if padId exists + exports.doesPadExists(padId, function(junk, exists) { + if (exists) { callback(padId); + return; } - //get the next transformation *that's different* + // get the next transformation *that's different* var transformedPadId = padId; - while(transformedPadId == padId && transform_index < padIdTransforms.length) - { + + while(transformedPadId == padId && transform_index < padIdTransforms.length) { transformedPadId = padId.replace(padIdTransforms[transform_index][0], padIdTransforms[transform_index][1]); transform_index += 1; } - //check the next transform + + // check the next transform exports.sanitizePadId(transformedPadId, callback, transform_index); }); } @@ -237,13 +240,13 @@ exports.isValidPadId = function(padId) /** * Removes the pad from database and unloads it. */ -exports.removePad = function(padId){ - db.remove("pad:"+padId); +exports.removePad = function(padId) { + db.remove("pad:" + padId); exports.unloadPad(padId); padList.removePad(padId); } -//removes a pad from the cache +// removes a pad from the cache exports.unloadPad = function(padId) { globalPads.remove(padId); diff --git a/src/node/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.js index f49f71e23..ac7bee045 100644 --- a/src/node/db/ReadOnlyManager.js +++ b/src/node/db/ReadOnlyManager.js @@ -29,43 +29,40 @@ var randomString = require("../utils/randomstring"); * @param {String} padId the id of the pad */ exports.getReadOnlyId = function (padId, callback) -{ +{ var readOnlyId; - + async.waterfall([ - //check if there is a pad2readonly entry - function(callback) - { + // check if there is a pad2readonly entry + function(callback) { db.get("pad2readonly:" + padId, callback); }, - function(dbReadOnlyId, callback) - { - //there is no readOnly Entry in the database, let's create one - if(dbReadOnlyId == null) - { + + function(dbReadOnlyId, callback) { + if (dbReadOnlyId == null) { + // there is no readOnly Entry in the database, let's create one readOnlyId = "r." + randomString(16); - + db.set("pad2readonly:" + padId, readOnlyId); db.set("readonly2pad:" + readOnlyId, padId); - } - //there is a readOnly Entry in the database, let's take this one - else - { + } else { + // there is a readOnly Entry in the database, let's take this one readOnlyId = dbReadOnlyId; } - + callback(); } - ], function(err) - { - if(ERR(err, callback)) return; - //return the results + ], + function(err) { + if (ERR(err, callback)) return; + + // return the results callback(null, readOnlyId); }) } /** - * returns a the padId for a read only id + * returns the padId for a read only id * @param {String} readOnlyId read only id */ exports.getPadId = function(readOnlyId, callback) @@ -74,20 +71,21 @@ exports.getPadId = function(readOnlyId, callback) } /** - * returns a the padId and readonlyPadId in an object for any id + * returns the padId and readonlyPadId in an object for any id * @param {String} padIdOrReadonlyPadId read only id or real pad id */ exports.getIds = function(id, callback) { - if (id.indexOf("r.") == 0) + if (id.indexOf("r.") == 0) { exports.getPadId(id, function (err, value) { - if(ERR(err, callback)) return; + if (ERR(err, callback)) return; + callback(null, { readOnlyPadId: id, padId: value, // Might be null, if this is an unknown read-only id readonly: true }); }); - else + } else { exports.getReadOnlyId(id, function (err, value) { callback(null, { readOnlyPadId: value, @@ -95,4 +93,5 @@ exports.getIds = function(id, callback) { readonly: false }); }); + } } diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 2c46ac508..117f2794f 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -18,7 +18,6 @@ * limitations under the License. */ - var ERR = require("async-stacktrace"); var async = require("async"); var authorManager = require("./AuthorManager"); @@ -34,59 +33,57 @@ var authLogger = log4js.getLogger("auth"); * @param padID the pad the user wants to access * @param sessionCookie the session the user has (set via api) * @param token the token of the author (randomly generated at client side, used for public pads) - * @param password the password the user has given to access this pad, can be null + * @param password the password the user has given to access this pad, can be null * @param callback will be called with (err, {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx}) - */ -exports.checkAccess = function (padID, sessionCookie, token, password, callback) -{ + */ +exports.checkAccess = function(padID, sessionCookie, token, password, callback) +{ var statusObject; - - if(!padID) { + + if (!padID) { callback(null, {accessStatus: "deny"}); return; } // allow plugins to deny access var deniedByHook = hooks.callAll("onAccessCheck", {'padID': padID, 'password': password, 'token': token, 'sessionCookie': sessionCookie}).indexOf(false) > -1; - if(deniedByHook) - { + if (deniedByHook) { callback(null, {accessStatus: "deny"}); return; } - // a valid session is required (api-only mode) - if(settings.requireSession) - { - // without sessionCookie, access is denied - if(!sessionCookie) - { + if (settings.requireSession) { + // a valid session is required (api-only mode) + if (!sessionCookie) { + // without sessionCookie, access is denied callback(null, {accessStatus: "deny"}); + return; } - } - // a session is not required, so we'll check if it's a public pad - else - { - // it's not a group pad, means we can grant access - if(padID.indexOf("$") == -1) - { - //get author for this token - authorManager.getAuthor4Token(token, function(err, author) - { - if(ERR(err, callback)) return; - + } else { + // a session is not required, so we'll check if it's a public pad + if (padID.indexOf("$") == -1) { + // it's not a group pad, means we can grant access + + // get author for this token + authorManager.getAuthor4Token(token, function(err, author) { + if (ERR(err, callback)) return; + // assume user has access - statusObject = {accessStatus: "grant", authorID: author}; - // user can't create pads - if(settings.editOnly) - { + statusObject = { accessStatus: "grant", authorID: author }; + + if (settings.editOnly) { + // user can't create pads + // check if pad exists - padManager.doesPadExists(padID, function(err, exists) - { - if(ERR(err, callback)) return; - - // pad doesn't exist - user can't have access - if(!exists) statusObject.accessStatus = "deny"; + padManager.doesPadExists(padID, function(err, exists) { + if (ERR(err, callback)) return; + + if (!exists) { + // pad doesn't exist - user can't have access + statusObject.accessStatus = "deny"; + } + // grant or deny access, with author of token callback(null, statusObject); }); @@ -98,12 +95,12 @@ exports.checkAccess = function (padID, sessionCookie, token, password, callback) // grant access, with author of token callback(null, statusObject); }); - - //don't continue + + // don't continue return; } } - + var groupID = padID.split("$")[0]; var padExists = false; var validSession = false; @@ -114,216 +111,193 @@ exports.checkAccess = function (padID, sessionCookie, token, password, callback) var passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong async.series([ - //get basic informations from the database - function(callback) - { + // get basic informations from the database + function(callback) { async.parallel([ - //does pad exists - function(callback) - { - padManager.doesPadExists(padID, function(err, exists) - { - if(ERR(err, callback)) return; + // does pad exist + function(callback) { + padManager.doesPadExists(padID, function(err, exists) { + if (ERR(err, callback)) return; + padExists = exists; callback(); }); }, - //get information about all sessions contained in this cookie - function(callback) - { - if (!sessionCookie) - { + + // get information about all sessions contained in this cookie + function(callback) { + if (!sessionCookie) { callback(); return; } - + var sessionIDs = sessionCookie.split(','); - async.forEach(sessionIDs, function(sessionID, callback) - { - sessionManager.getSessionInfo(sessionID, function(err, sessionInfo) - { - //skip session if it doesn't exist - if(err && err.message == "sessionID does not exist") - { + + async.forEach(sessionIDs, function(sessionID, callback) { + sessionManager.getSessionInfo(sessionID, function(err, sessionInfo) { + // skip session if it doesn't exist + if (err && err.message == "sessionID does not exist") { authLogger.debug("Auth failed: unknown session"); callback(); + return; } - - if(ERR(err, callback)) return; - + + if (ERR(err, callback)) return; + var now = Math.floor(Date.now()/1000); - - //is it for this group? - if(sessionInfo.groupID != groupID) - { + + // is it for this group? + if (sessionInfo.groupID != groupID) { authLogger.debug("Auth failed: wrong group"); callback(); + return; } - - //is validUntil still ok? - if(sessionInfo.validUntil <= now) - { + + // is validUntil still ok? + if (sessionInfo.validUntil <= now) { authLogger.debug("Auth failed: validUntil"); callback(); + return; } - + // There is a valid session validSession = true; sessionAuthor = sessionInfo.authorID; - + callback(); }); }, callback); }, - //get author for token - function(callback) - { - //get author for this token - authorManager.getAuthor4Token(token, function(err, author) - { - if(ERR(err, callback)) return; + + // get author for token + function(callback) { + // get author for this token + authorManager.getAuthor4Token(token, function(err, author) { + if (ERR(err, callback)) return; + tokenAuthor = author; callback(); }); } ], callback); }, - //get more informations of this pad, if avaiable - function(callback) - { - //skip this if the pad doesn't exists - if(padExists == false) - { + + // get more informations of this pad, if avaiable + function(callback) { + // skip this if the pad doesn't exist + if (padExists == false) { callback(); + return; } - - padManager.getPad(padID, function(err, pad) - { - if(ERR(err, callback)) return; - - //is it a public pad? + + padManager.getPad(padID, function(err, pad) { + if (ERR(err, callback)) return; + + // is it a public pad? isPublic = pad.getPublicStatus(); - - //is it password protected? + + // is it password protected? isPasswordProtected = pad.isPasswordProtected(); - - //is password correct? - if(isPasswordProtected && password && pad.isCorrectPassword(password)) - { + + // is password correct? + if (isPasswordProtected && password && pad.isCorrectPassword(password)) { passwordStatus = "correct"; } - + callback(); }); }, - function(callback) - { - //- a valid session for this group is avaible AND pad exists - if(validSession && padExists) - { - //- the pad is not password protected - if(!isPasswordProtected) - { - //--> grant access - statusObject = {accessStatus: "grant", authorID: sessionAuthor}; - } - //- the setting to bypass password validation is set - else if(settings.sessionNoPassword) - { - //--> grant access - statusObject = {accessStatus: "grant", authorID: sessionAuthor}; - } - //- the pad is password protected and password is correct - else if(isPasswordProtected && passwordStatus == "correct") - { - //--> grant access - statusObject = {accessStatus: "grant", authorID: sessionAuthor}; - } - //- the pad is password protected but wrong password given - else if(isPasswordProtected && passwordStatus == "wrong") - { - //--> deny access, ask for new password and tell them that the password is wrong - statusObject = {accessStatus: "wrongPassword"}; - } - //- the pad is password protected but no password given - else if(isPasswordProtected && passwordStatus == "notGiven") - { - //--> ask for password - statusObject = {accessStatus: "needPassword"}; - } - else - { + + function(callback) { + if (validSession && padExists) { + // - a valid session for this group is avaible AND pad exists + if (!isPasswordProtected) { + // - the pad is not password protected + + // --> grant access + statusObject = { accessStatus: "grant", authorID: sessionAuthor }; + } else if (settings.sessionNoPassword) { + // - the setting to bypass password validation is set + + // --> grant access + statusObject = { accessStatus: "grant", authorID: sessionAuthor }; + } else if (isPasswordProtected && passwordStatus == "correct") { + // - the pad is password protected and password is correct + + // --> grant access + statusObject = { accessStatus: "grant", authorID: sessionAuthor }; + } else if (isPasswordProtected && passwordStatus == "wrong") { + // - the pad is password protected but wrong password given + + // --> deny access, ask for new password and tell them that the password is wrong + statusObject = { accessStatus: "wrongPassword" }; + } else if (isPasswordProtected && passwordStatus == "notGiven") { + // - the pad is password protected but no password given + + // --> ask for password + statusObject = { accessStatus: "needPassword" }; + } else { throw new Error("Ops, something wrong happend"); } - } - //- a valid session for this group avaible but pad doesn't exists - else if(validSession && !padExists) - { - //--> grant access + } else if (validSession && !padExists) { + // - a valid session for this group avaible but pad doesn't exist + + // --> grant access statusObject = {accessStatus: "grant", authorID: sessionAuthor}; - //--> deny access if user isn't allowed to create the pad - if(settings.editOnly) - { + + if (settings.editOnly) { + // --> deny access if user isn't allowed to create the pad authLogger.debug("Auth failed: valid session & pad does not exist"); statusObject.accessStatus = "deny"; } - } - // there is no valid session avaiable AND pad exists - else if(!validSession && padExists) - { - //-- its public and not password protected - if(isPublic && !isPasswordProtected) - { - //--> grant access, with author of token + } else if (!validSession && padExists) { + // there is no valid session avaiable AND pad exists + + // -- it's public and not password protected + if (isPublic && !isPasswordProtected) { + // --> grant access, with author of token statusObject = {accessStatus: "grant", authorID: tokenAuthor}; - } - //- its public and password protected and password is correct - else if(isPublic && isPasswordProtected && passwordStatus == "correct") - { - //--> grant access, with author of token + } else if (isPublic && isPasswordProtected && passwordStatus == "correct") { + // - it's public and password protected and password is correct + + // --> grant access, with author of token statusObject = {accessStatus: "grant", authorID: tokenAuthor}; - } - //- its public and the pad is password protected but wrong password given - else if(isPublic && isPasswordProtected && passwordStatus == "wrong") - { - //--> deny access, ask for new password and tell them that the password is wrong + } else if (isPublic && isPasswordProtected && passwordStatus == "wrong") { + // - it's public and the pad is password protected but wrong password given + + // --> deny access, ask for new password and tell them that the password is wrong statusObject = {accessStatus: "wrongPassword"}; - } - //- its public and the pad is password protected but no password given - else if(isPublic && isPasswordProtected && passwordStatus == "notGiven") - { - //--> ask for password + } else if (isPublic && isPasswordProtected && passwordStatus == "notGiven") { + // - it's public and the pad is password protected but no password given + + // --> ask for password statusObject = {accessStatus: "needPassword"}; - } - //- its not public - else if(!isPublic) - { + } else if (!isPublic) { + // - it's not public + authLogger.debug("Auth failed: invalid session & pad is not public"); - //--> deny access + // --> deny access statusObject = {accessStatus: "deny"}; - } - else - { + } else { throw new Error("Ops, something wrong happend"); } - } - // there is no valid session avaiable AND pad doesn't exists - else - { - authLogger.debug("Auth failed: invalid session & pad does not exist"); - //--> deny access - statusObject = {accessStatus: "deny"}; + } else { + // there is no valid session avaiable AND pad doesn't exist + authLogger.debug("Auth failed: invalid session & pad does not exist"); + // --> deny access + statusObject = {accessStatus: "deny"}; } - + callback(); } - ], function(err) - { - if(ERR(err, callback)) return; + ], + function(err) { + if (ERR(err, callback)) return; + callback(null, statusObject); }); }; diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index 99803ee71..6b580c033 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -17,7 +17,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - var ERR = require("async-stacktrace"); var customError = require("../utils/customError"); @@ -26,7 +25,7 @@ var db = require("./DB").db; var async = require("async"); var groupMangager = require("./GroupManager"); var authorMangager = require("./AuthorManager"); - + exports.doesSessionExist = function(sessionID, callback) { //check if the database entry of this session exists @@ -36,7 +35,7 @@ exports.doesSessionExist = function(sessionID, callback) callback(null, session != null); }); } - + /** * Creates a new session between an author and a group */ @@ -45,159 +44,148 @@ exports.createSession = function(groupID, authorID, validUntil, callback) var sessionID; async.series([ - //check if group exists + // check if the group exists function(callback) { groupMangager.doesGroupExist(groupID, function(err, exists) { if(ERR(err, callback)) return; - - //group does not exist - if(exists == false) - { - callback(new customError("groupID does not exist","apierror")); - } - //everything is fine, continue - else - { + + // group does not exist + if (exists == false) { + callback(new customError("groupID does not exist", "apierror")); + } else { + // everything is fine, continue callback(); } }); }, - //check if author exists + + // check if the author exists function(callback) { authorMangager.doesAuthorExists(authorID, function(err, exists) { if(ERR(err, callback)) return; - - //author does not exist - if(exists == false) - { - callback(new customError("authorID does not exist","apierror")); - } - //everything is fine, continue - else - { + + if (exists == false) { + // author does not exist + callback(new customError("authorID does not exist", "apierror")); + } else { + // everything is fine, continue callback(); } }); }, - //check validUntil and create the session db entry + + // check validUntil and create the session db entry function(callback) { - //check if rev is a number - if(typeof validUntil != "number") + // check if rev is a number + if (typeof validUntil != "number") { - //try to parse the number - if(isNaN(parseInt(validUntil))) - { - callback(new customError("validUntil is not a number","apierror")); + // try to parse the number + if (isNaN(parseInt(validUntil))) { + callback(new customError("validUntil is not a number", "apierror")); return; } validUntil = parseInt(validUntil); } - - //ensure this is not a negativ number - if(validUntil < 0) - { - callback(new customError("validUntil is a negativ number","apierror")); + + // ensure this is not a negative number + if (validUntil < 0) { + callback(new customError("validUntil is a negativ number", "apierror")); return; } - - //ensure this is not a float value - if(!is_int(validUntil)) - { - callback(new customError("validUntil is a float value","apierror")); + + // ensure this is not a float value + if (!is_int(validUntil)) { + callback(new customError("validUntil is a float value", "apierror")); return; } - - //check if validUntil is in the future - if(Math.floor(Date.now()/1000) > validUntil) - { - callback(new customError("validUntil is in the past","apierror")); + + // check if validUntil is in the future + if (Math.floor(Date.now()/1000) > validUntil) { + callback(new customError("validUntil is in the past", "apierror")); return; } - - //generate sessionID + + // generate sessionID sessionID = "s." + randomString(16); - - //set the session into the database + + // set the session into the database db.set("session:" + sessionID, {"groupID": groupID, "authorID": authorID, "validUntil": validUntil}); - + callback(); }, - //set the group2sessions entry + + // set the group2sessions entry function(callback) { - //get the entry + // get the entry db.get("group2sessions:" + groupID, function(err, group2sessions) { if(ERR(err, callback)) return; - - //the entry doesn't exist so far, let's create it - if(group2sessions == null || group2sessions.sessionIDs == null) - { + + if (group2sessions == null || group2sessions.sessionIDs == null) { + // the entry doesn't exist so far, let's create it group2sessions = {sessionIDs : {}}; } - - //add the entry for this session + + // add the entry for this session group2sessions.sessionIDs[sessionID] = 1; - - //save the new element back + + // save the new element back db.set("group2sessions:" + groupID, group2sessions); - + callback(); }); }, - //set the author2sessions entry + + // set the author2sessions entry function(callback) { - //get the entry + // get the entry db.get("author2sessions:" + authorID, function(err, author2sessions) { if(ERR(err, callback)) return; - - //the entry doesn't exist so far, let's create it - if(author2sessions == null || author2sessions.sessionIDs == null) - { + + if (author2sessions == null || author2sessions.sessionIDs == null) { + // the entry doesn't exist so far, let's create it author2sessions = {sessionIDs : {}}; } - - //add the entry for this session + + // add the entry for this session author2sessions.sessionIDs[sessionID] = 1; - + //save the new element back db.set("author2sessions:" + authorID, author2sessions); - + callback(); }); } ], function(err) { if(ERR(err, callback)) return; - - //return error and sessionID + + // return error and sessionID callback(null, {sessionID: sessionID}); }) } exports.getSessionInfo = function(sessionID, callback) { - //check if the database entry of this session exists + // check if the database entry of this session exists db.get("session:" + sessionID, function (err, session) { if(ERR(err, callback)) return; - - //session does not exists - if(session == null) - { - callback(new customError("sessionID does not exist","apierror")) - } - //everything is fine, return the sessioninfos - else - { + + if (session == null) { + // session does not exist + callback(new customError("sessionID does not exist", "apierror")) + } else { + // everything is fine, return the sessioninfos callback(null, session); } }); @@ -214,27 +202,25 @@ exports.deleteSession = function(sessionID, callback) async.series([ function(callback) { - //get the session entry + // get the session entry db.get("session:" + sessionID, function (err, session) { if(ERR(err, callback)) return; - - //session does not exists - if(session == null) - { - callback(new customError("sessionID does not exist","apierror")) - } - //everything is fine, return the sessioninfos - else - { + + if (session == null) { + // session does not exist + callback(new customError("sessionID does not exist", "apierror")) + } else { + // everything is fine, use the sessioninfos authorID = session.authorID; groupID = session.groupID; - + callback(); } }); }, - //get the group2sessions entry + + // get the group2sessions entry function(callback) { db.get("group2sessions:" + groupID, function (err, _group2sessions) @@ -244,7 +230,8 @@ exports.deleteSession = function(sessionID, callback) callback(); }); }, - //get the author2sessions entry + + // get the author2sessions entry function(callback) { db.get("author2sessions:" + authorID, function (err, _author2sessions) @@ -254,24 +241,25 @@ exports.deleteSession = function(sessionID, callback) callback(); }); }, - //remove the values from the database + + // remove the values from the database function(callback) { //remove the session db.remove("session:" + sessionID); - - //remove session from group2sessions + + // remove session from group2sessions if(group2sessions != null) { // Maybe the group was already deleted delete group2sessions.sessionIDs[sessionID]; db.set("group2sessions:" + groupID, group2sessions); } - //remove session from author2sessions + // remove session from author2sessions if(author2sessions != null) { // Maybe the author was already deleted delete author2sessions.sessionIDs[sessionID]; db.set("author2sessions:" + authorID, author2sessions); } - + callback(); } ], function(err) @@ -286,40 +274,34 @@ exports.listSessionsOfGroup = function(groupID, callback) groupMangager.doesGroupExist(groupID, function(err, exists) { if(ERR(err, callback)) return; - - //group does not exist - if(exists == false) - { - callback(new customError("groupID does not exist","apierror")); - } - //everything is fine, continue - else - { + + if (exists == false) { + // group does not exist + callback(new customError("groupID does not exist", "apierror")); + } else { + // everything is fine, continue listSessionsWithDBKey("group2sessions:" + groupID, callback); } }); } exports.listSessionsOfAuthor = function(authorID, callback) -{ +{ authorMangager.doesAuthorExists(authorID, function(err, exists) { if(ERR(err, callback)) return; - - //group does not exist - if(exists == false) - { - callback(new customError("authorID does not exist","apierror")); - } - //everything is fine, continue - else - { + + if (exists == false) { + // group does not exist + callback(new customError("authorID does not exist", "apierror")); + } else { + // everything is fine, continue listSessionsWithDBKey("author2sessions:" + authorID, callback); } }); } -//this function is basicly the code listSessionsOfAuthor and listSessionsOfGroup has in common +// this function is basically the code listSessionsOfAuthor and listSessionsOfGroup has in common function listSessionsWithDBKey (dbkey, callback) { var sessions; @@ -327,7 +309,7 @@ function listSessionsWithDBKey (dbkey, callback) async.series([ function(callback) { - //get the group2sessions entry + // get the group2sessions entry db.get(dbkey, function(err, sessionObject) { if(ERR(err, callback)) return; @@ -335,26 +317,24 @@ function listSessionsWithDBKey (dbkey, callback) callback(); }); }, + function(callback) - { - //collect all sessionIDs in an arrary + { + // collect all sessionIDs in an arrary var sessionIDs = []; for (var i in sessions) { sessionIDs.push(i); } - - //foreach trough the sessions and get the sessioninfos + + // iterate through the sessions and get the sessioninfos async.forEach(sessionIDs, function(sessionID, callback) { exports.getSessionInfo(sessionID, function(err, sessionInfo) { - if (err == "apierror: sessionID does not exist") - { + if (err == "apierror: sessionID does not exist") { console.warn(`Found bad session ${sessionID} in ${dbkey}`); - } - else if(ERR(err, callback)) - { + } else if(ERR(err, callback)) { return; } @@ -370,8 +350,8 @@ function listSessionsWithDBKey (dbkey, callback) }); } -//checks if a number is an int +// checks if a number is an int function is_int(value) -{ - return (parseFloat(value) == parseInt(value)) && !isNaN(value) +{ + return (parseFloat(value) == parseInt(value)) && !isNaN(value); } diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index 974046908..80d35f290 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -1,4 +1,4 @@ - /* +/* * Stores session data in the database * Source; https://github.com/edy-b/SciFlowWriter/blob/develop/available_plugins/ep_sciflowwriter/db/DirtyStore.js * This is not used for authors that are created via the API at current @@ -13,11 +13,12 @@ var SessionStore = module.exports = function SessionStore() {}; SessionStore.prototype.__proto__ = Store.prototype; -SessionStore.prototype.get = function(sid, fn){ +SessionStore.prototype.get = function(sid, fn) { messageLogger.debug('GET ' + sid); + var self = this; - db.get("sessionstorage:" + sid, function (err, sess) - { + + 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) { @@ -31,26 +32,30 @@ SessionStore.prototype.get = function(sid, fn){ }); }; -SessionStore.prototype.set = function(sid, sess, fn){ +SessionStore.prototype.set = function(sid, sess, fn) { messageLogger.debug('SET ' + sid); + db.set("sessionstorage:" + sid, sess); - process.nextTick(function(){ - if(fn) fn(); + process.nextTick(function() { + if (fn) fn(); }); }; -SessionStore.prototype.destroy = function(sid, fn){ +SessionStore.prototype.destroy = function(sid, fn) { messageLogger.debug('DESTROY ' + sid); + db.remove("sessionstorage:" + sid); - process.nextTick(function(){ - if(fn) fn(); + process.nextTick(function() { + if (fn) fn(); }); }; -SessionStore.prototype.all = function(fn){ +SessionStore.prototype.all = function(fn) { messageLogger.debug('ALL'); + var sessions = []; - db.forEach(function(key, value){ + + db.forEach(function(key, value) { if (key.substr(0,15) === "sessionstorage:") { sessions.push(value); } @@ -58,20 +63,23 @@ SessionStore.prototype.all = function(fn){ fn(null, sessions); }; -SessionStore.prototype.clear = function(fn){ +SessionStore.prototype.clear = function(fn) { messageLogger.debug('CLEAR'); - db.forEach(function(key, value){ + + db.forEach(function(key, value) { if (key.substr(0,15) === "sessionstorage:") { db.db.remove("session:" + key); } }); - if(fn) fn(); + if (fn) fn(); }; -SessionStore.prototype.length = function(fn){ +SessionStore.prototype.length = function(fn) { messageLogger.debug('LENGTH'); + var i = 0; - db.forEach(function(key, value){ + + db.forEach(function(key, value) { if (key.substr(0,15) === "sessionstorage:") { i++; } diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index 6ec5907e2..3ed7aa4e1 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -32,19 +32,17 @@ var apiHandlerLogger = log4js.getLogger('APIHandler'); //ensure we have an apikey var apikey = null; var apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || "./APIKEY.txt"); -try -{ + +try { apikey = fs.readFileSync(apikeyFilename,"utf8"); apiHandlerLogger.info(`Api key file read from: "${apikeyFilename}"`); -} -catch(e) -{ +} catch(e) { apiHandlerLogger.info(`Api key file "${apikeyFilename}" not found. Creating with random contents.`); apikey = randomString(32); fs.writeFileSync(apikeyFilename,apikey,"utf8"); } -//a list of all functions +// a list of all functions var version = {}; version["1"] = Object.assign({}, @@ -156,106 +154,92 @@ exports.handle = function(apiVersion, functionName, fields, req, res) { //check if this is a valid apiversion var isKnownApiVersion = false; - for(var knownApiVersion in version) - { - if(knownApiVersion == apiVersion) - { + + for (var knownApiVersion in version) { + if (knownApiVersion == apiVersion) { isKnownApiVersion = true; break; } } - //say goodbye if this is an unknown API version - if(!isKnownApiVersion) - { + // say goodbye if this is an unknown API version + if (!isKnownApiVersion) { res.statusCode = 404; res.send({code: 3, message: "no such api version", data: null}); return; } - //check if this is a valid function name + // check if this is a valid function name var isKnownFunctionname = false; - for(var knownFunctionname in version[apiVersion]) - { - if(knownFunctionname == functionName) - { + + for (var knownFunctionname in version[apiVersion]) { + if (knownFunctionname == functionName) { isKnownFunctionname = true; break; } } - //say goodbye if this is a unknown function - if(!isKnownFunctionname) - { + // say goodbye if this is an unknown function + if (!isKnownFunctionname) { res.send({code: 3, message: "no such function", data: null}); return; } - //check the api key! + // check the api key! fields["apikey"] = fields["apikey"] || fields["api_key"]; - if(fields["apikey"] != apikey.trim()) - { + if (fields["apikey"] != apikey.trim()) { res.statusCode = 401; res.send({code: 4, message: "no or wrong API Key", data: null}); return; } - //sanitize any pad id's before continuing - if(fields["padID"]) - { - padManager.sanitizePadId(fields["padID"], function(padId) - { + // sanitize any padIDs before continuing + if (fields["padID"]) { + padManager.sanitizePadId(fields["padID"], function(padId) { fields["padID"] = padId; callAPI(apiVersion, functionName, fields, req, res); }); - } - else if(fields["padName"]) - { - padManager.sanitizePadId(fields["padName"], function(padId) - { + } else if (fields["padName"]) { + padManager.sanitizePadId(fields["padName"], function(padId) { fields["padName"] = padId; callAPI(apiVersion, functionName, fields, req, res); }); - } - else - { + } else { callAPI(apiVersion, functionName, fields, req, res); } } -//calls the api function +// calls the api function function callAPI(apiVersion, functionName, fields, req, res) { - //put the function parameters in an array + // put the function parameters in an array var functionParams = version[apiVersion][functionName].map(function (field) { return fields[field] - }) + }); - //add a callback function to handle the response - functionParams.push(function(err, data) - { - // no error happend, everything is fine - if(err == null) - { - if(!data) + // add a callback function to handle the response + functionParams.push(function(err, data) { + if (err == null) { + // no error happened, everything is fine + + if (!data) { data = null; + } res.send({code: 0, message: "ok", data: data}); - } - // parameters were wrong and the api stopped execution, pass the error - else if(err.name == "apierror") - { + } else if (err.name == "apierror") { + // parameters were wrong and the api stopped execution, pass the error + res.send({code: 1, message: err.message, data: null}); - } - //an unknown error happend - else - { + } else { + // an unknown error happened + res.send({code: 2, message: "internal error", data: null}); ERR(err); } }); - //call the api function + // call the api function api[functionName].apply(this, functionParams); } diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js index db3d2d40d..3bcabf233 100644 --- a/src/node/handler/ExportHandler.js +++ b/src/node/handler/ExportHandler.js @@ -32,13 +32,15 @@ var TidyHtml = require('../utils/TidyHtml'); var convertor = null; -//load abiword only if its enabled -if(settings.abiword != null) +// load abiword only if it is enabled +if (settings.abiword != null) { convertor = require("../utils/Abiword"); +} // Use LibreOffice if an executable has been defined in the settings -if(settings.soffice != null) +if (settings.soffice != null) { convertor = require("../utils/LibreOffice"); +} const tempDirectory = os.tmpdir(); @@ -53,62 +55,55 @@ exports.doExport = function(req, res, padId, type) hooks.aCallFirst("exportFileName", padId, function(err, hookFileName){ // if fileName is set then set it to the padId, note that fileName is returned as an array. - if(hookFileName.length) fileName = hookFileName; + if (hookFileName.length) { + fileName = hookFileName; + } - //tell the browser that this is a downloadable file + // tell the browser that this is a downloadable file res.attachment(fileName + "." + type); - //if this is a plain text export, we can do this directly + // if this is a plain text export, we can do this directly // We have to over engineer this because tabs are stored as attributes and not plain text - if(type == "etherpad"){ - exportEtherpad.getPadRaw(padId, function(err, pad){ - if(!err){ + if (type == "etherpad") { + exportEtherpad.getPadRaw(padId, function(err, pad) { + if (!err) { res.send(pad); // return; } }); - } - else if(type == "txt") - { - exporttxt.getPadTXTDocument(padId, req.params.rev, function(err, txt) - { - if(!err) { + } else if (type == "txt") { + exporttxt.getPadTXTDocument(padId, req.params.rev, function(err, txt) { + if (!err) { res.send(txt); } }); - } - else - { + } else { var html; var randNum; var srcFile, destFile; async.series([ - //render the html document - function(callback) - { - exporthtml.getPadHTMLDocument(padId, req.params.rev, function(err, _html) - { - if(ERR(err, callback)) return; + // render the html document + function(callback) { + exporthtml.getPadHTMLDocument(padId, req.params.rev, function(err, _html) { + if (ERR(err, callback)) return; html = _html; callback(); }); }, - //decide what to do with the html export - function(callback) - { - //if this is a html export, we can send this from here directly - if(type == "html") - { + + // decide what to do with the html export + function(callback) { + // if this is a html export, we can send this from here directly + if (type == "html") { // do any final changes the plugin might want to make - hooks.aCallFirst("exportHTMLSend", html, function(err, newHTML){ - if(newHTML.length) html = newHTML; + hooks.aCallFirst("exportHTMLSend", html, function(err, newHTML) { + if (newHTML.length) html = newHTML; res.send(html); callback("stop"); }); - } - else //write the html export to a file - { + } else { + // write the html export to a file randNum = Math.floor(Math.random()*0xFFFFFFFF); srcFile = tempDirectory + "/etherpad_export_" + randNum + ".html"; fs.writeFile(srcFile, html, callback); @@ -116,64 +111,56 @@ exports.doExport = function(req, res, padId, type) }, // Tidy up the exported HTML - function(callback) - { - //ensure html can be collected by the garbage collector + function(callback) { + // ensure html can be collected by the garbage collector html = null; TidyHtml.tidy(srcFile, callback); }, - //send the convert job to the convertor (abiword, libreoffice, ..) - function(callback) - { + // send the convert job to the convertor (abiword, libreoffice, ..) + function(callback) { destFile = tempDirectory + "/etherpad_export_" + randNum + "." + type; // Allow plugins to overwrite the convert in export process - hooks.aCallAll("exportConvert", {srcFile: srcFile, destFile: destFile, req: req, res: res}, function(err, result){ - if(!err && result.length > 0){ + hooks.aCallAll("exportConvert", { srcFile: srcFile, destFile: destFile, req: req, res: res }, function(err, result) { + if (!err && result.length > 0) { // console.log("export handled by plugin", destFile); handledByPlugin = true; callback(); - }else{ + } else { convertor.convertFile(srcFile, destFile, type, callback); } }); }, - //send the file - function(callback) - { + + // send the file + function(callback) { res.sendFile(destFile, null, callback); }, - //clean up temporary files - function(callback) - { + + // clean up temporary files + function(callback) { async.parallel([ - function(callback) - { + function(callback) { fs.unlink(srcFile, callback); }, - function(callback) - { - //100ms delay to accomidate for slow windows fs - if(os.type().indexOf("Windows") > -1) - { - setTimeout(function() - { + function(callback) { + // 100ms delay to accommodate for slow windows fs + if (os.type().indexOf("Windows") > -1) { + setTimeout(function() { fs.unlink(destFile, callback); }, 100); - } - else - { + } else { fs.unlink(destFile, callback); } } ], callback); } - ], function(err) - { - if(err && err != "stop") ERR(err); + ], + function(err) { + if (err && err != "stop") ERR(err); }) } } diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index 30b773972..dae71bf6d 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -37,29 +37,30 @@ var ERR = require("async-stacktrace") var convertor = null; var exportExtension = "htm"; -//load abiword only if its enabled and if soffice is disabled -if(settings.abiword != null && settings.soffice === null) +// load abiword only if it is enabled and if soffice is disabled +if (settings.abiword != null && settings.soffice === null) { convertor = require("../utils/Abiword"); +} -//load soffice only if its enabled -if(settings.soffice != null) { +// load soffice only if it is enabled +if (settings.soffice != null) { convertor = require("../utils/LibreOffice"); exportExtension = "html"; } const tmpDirectory = os.tmpdir(); - + /** * do a requested import - */ + */ exports.doImport = function(req, res, padId) { var apiLogger = log4js.getLogger("ImportHandler"); - //pipe to a file - //convert file to html via abiword or soffice - //set html in the pad - + // pipe to a file + // convert file to html via abiword or soffice + // set html in the pad + var srcFile, destFile , pad , text @@ -68,69 +69,74 @@ exports.doImport = function(req, res, padId) , useConvertor; var randNum = Math.floor(Math.random()*0xFFFFFFFF); - + // setting flag for whether to use convertor or not useConvertor = (convertor != null); async.series([ - //save the uploaded file to /tmp + // save the uploaded file to /tmp function(callback) { var form = new formidable.IncomingForm(); form.keepExtensions = true; form.uploadDir = tmpDirectory; - - form.parse(req, function(err, fields, files) { - //the upload failed, stop at this point - if(err || files.file === undefined) { - if(err) console.warn("Uploading Error: " + err.stack); + + form.parse(req, function(err, fields, files) { + if (err || files.file === undefined) { + // the upload failed, stop at this point + if (err) { + console.warn("Uploading Error: " + err.stack); + } callback("uploadFailed"); return; } - //everything ok, continue - //save the path of the uploaded file + // everything ok, continue + // save the path of the uploaded file srcFile = files.file.path; callback(); }); }, - - //ensure this is a file ending we know, else we change the file ending to .txt - //this allows us to accept source code files like .c or .java + + // ensure this is a file ending we know, else we change the file ending to .txt + // this allows us to accept source code files like .c or .java function(callback) { var fileEnding = path.extname(srcFile).toLowerCase() , knownFileEndings = [".txt", ".doc", ".docx", ".pdf", ".odt", ".html", ".htm", ".etherpad", ".rtf"] , fileEndingKnown = (knownFileEndings.indexOf(fileEnding) > -1); - - //if the file ending is known, continue as normal - if(fileEndingKnown) { + + // if the file ending is known, continue as normal + if (fileEndingKnown) { callback(); - + return; } - //we need to rename this file with a .txt ending - if(settings.allowUnknownFileEnds === true){ + // we need to rename this file with a .txt ending + if (settings.allowUnknownFileEnds === true) { var oldSrcFile = srcFile; - srcFile = path.join(path.dirname(srcFile),path.basename(srcFile, fileEnding)+".txt"); + srcFile = path.join(path.dirname(srcFile), path.basename(srcFile, fileEnding) + ".txt"); fs.rename(oldSrcFile, srcFile, callback); - }else{ + } else { console.warn("Not allowing unknown file type to be imported", fileEnding); callback("uploadFailed"); } }, - function(callback){ + + function(callback) { destFile = path.join(tmpDirectory, "etherpad_import_" + randNum + "." + exportExtension); // Logic for allowing external Import Plugins - hooks.aCallAll("import", {srcFile: srcFile, destFile: destFile}, function(err, result){ - if(ERR(err, callback)) return callback(); - if(result.length > 0){ // This feels hacky and wrong.. + hooks.aCallAll("import", { srcFile: srcFile, destFile: destFile }, function(err, result) { + if (ERR(err, callback)) return callback(); + + if (result.length > 0) { // This feels hacky and wrong.. importHandledByPlugin = true; } callback(); }); }, + function(callback) { var fileEnding = path.extname(srcFile).toLowerCase() var fileIsNotEtherpad = (fileEnding !== ".etherpad"); @@ -141,23 +147,24 @@ exports.doImport = function(req, res, padId) return; } - // we do this here so we can see if the pad has quit ea few edits - padManager.getPad(padId, function(err, _pad){ + // we do this here so we can see if the pad has quite a few edits + padManager.getPad(padId, function(err, _pad) { var headCount = _pad.head; - if(headCount >= 10){ - apiLogger.warn("Direct database Import attempt of a pad that already has content, we wont be doing this") + if (headCount >= 10) { + apiLogger.warn("Direct database Import attempt of a pad that already has content, we wont be doing this"); return callback("padHasData"); } - fs.readFile(srcFile, "utf8", function(err, _text){ + fs.readFile(srcFile, "utf8", function(err, _text) { directDatabaseAccess = true; - importEtherpad.setPadRaw(padId, _text, function(err){ + importEtherpad.setPadRaw(padId, _text, function(err) { callback(); }); }); }); }, - //convert file to html + + // convert file to html if necessary function(callback) { if (importHandledByPlugin || directDatabaseAccess) { callback(); @@ -168,18 +175,20 @@ exports.doImport = function(req, res, padId) var fileEnding = path.extname(srcFile).toLowerCase(); var fileIsHTML = (fileEnding === ".html" || fileEnding === ".htm"); var fileIsTXT = (fileEnding === ".txt"); + if (fileIsTXT) useConvertor = false; // Don't use convertor for text files + // See https://github.com/ether/etherpad-lite/issues/2572 if (fileIsHTML || (useConvertor === false)) { // if no convertor only rename fs.rename(srcFile, destFile, callback); - + return; } convertor.convertFile(srcFile, destFile, exportExtension, function(err) { - //catch convert errors - if(err) { + // catch convert errors + if (err) { console.warn("Converting Error:", err); return callback("convertFailed"); } @@ -187,7 +196,7 @@ exports.doImport = function(req, res, padId) callback(); }); }, - + function(callback) { if (useConvertor || directDatabaseAccess) { callback(); @@ -216,17 +225,17 @@ exports.doImport = function(req, res, padId) callback(); }); }, - - //get the pad object + + // get the pad object function(callback) { - padManager.getPad(padId, function(err, _pad){ - if(ERR(err, callback)) return; + padManager.getPad(padId, function(err, _pad) { + if (ERR(err, callback)) return; pad = _pad; callback(); }); }, - - //read the text + + // read the text function(callback) { if (directDatabaseAccess) { callback(); @@ -234,43 +243,46 @@ exports.doImport = function(req, res, padId) return; } - fs.readFile(destFile, "utf8", function(err, _text){ - if(ERR(err, callback)) return; + fs.readFile(destFile, "utf8", function(err, _text) { + if (ERR(err, callback)) return; text = _text; // Title needs to be stripped out else it appends it to the pad.. text = text.replace("", "<!-- <title>"); text = text.replace("","-->"); - //node on windows has a delay on releasing of the file lock. - //We add a 100ms delay to work around this - if(os.type().indexOf("Windows") > -1){ + // node on windows has a delay on releasing of the file lock. + // We add a 100ms delay to work around this + if (os.type().indexOf("Windows") > -1) { setTimeout(function() {callback();}, 100); } else { callback(); } }); }, - - //change text of the pad and broadcast the changeset + + // change text of the pad and broadcast the changeset function(callback) { - if(!directDatabaseAccess){ + if (!directDatabaseAccess) { var fileEnding = path.extname(srcFile).toLowerCase(); if (importHandledByPlugin || useConvertor || fileEnding == ".htm" || fileEnding == ".html") { importHtml.setPadHTML(pad, text, function(e){ - if(e) apiLogger.warn("Error importing, possibly caused by malformed HTML"); + if (e) { + apiLogger.warn("Error importing, possibly caused by malformed HTML"); + } }); } else { pad.setText(text); } } - // Load the Pad into memory then brodcast updates to all clients + // Load the Pad into memory then broadcast updates to all clients padManager.unloadPad(padId); - padManager.getPad(padId, function(err, _pad){ + padManager.getPad(padId, function(err, _pad) { var pad = _pad; padManager.unloadPad(padId); + // direct Database Access means a pad user should perform a switchToPad - // and not attempt to recieve updated pad data.. + // and not attempt to receive updated pad data if (directDatabaseAccess) { callback(); @@ -283,8 +295,8 @@ exports.doImport = function(req, res, padId) }); }, - - //clean up temporary files + + // clean up temporary files function(callback) { if (directDatabaseAccess) { callback(); @@ -308,17 +320,16 @@ exports.doImport = function(req, res, padId) } ], function(err) { var status = "ok"; - - //check for known errors and replace the status - if(err == "uploadFailed" || err == "convertFailed" || err == "padHasData") - { + + // check for known errors and replace the status + if (err == "uploadFailed" || err == "convertFailed" || err == "padHasData") { status = err; err = null; } ERR(err); - //close the connection + // close the connection res.send( " \ \ @@ -331,4 +342,3 @@ exports.doImport = function(req, res, padId) ); }); } - diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 506b3aae4..5c20ed1b6 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -54,8 +54,8 @@ exports.sessioninfos = sessioninfos; // Measure total amount of users stats.gauge('totalUsers', function() { - return Object.keys(socketio.sockets.sockets).length -}) + return Object.keys(socketio.sockets.sockets).length; +}); /** * A changeset queue per pad that is processed by handleUserChanges() @@ -63,7 +63,7 @@ stats.gauge('totalUsers', function() { var padChannels = new channels.channels(handleUserChanges); /** - * Saves the Socket class we need to send and recieve data from the client + * Saves the Socket class we need to send and receive data from the client */ var socketio; @@ -84,7 +84,7 @@ exports.handleConnect = function(client) { stats.meter('connects').mark(); - //Initalize sessioninfos for this new session + // Initalize sessioninfos for this new session sessioninfos[client.id]={}; } @@ -97,11 +97,11 @@ exports.kickSessionsFromPad = function(padID) if(typeof socketio.sockets['clients'] !== 'function') return; - //skip if there is nobody on this pad + // skip if there is nobody on this pad if(_getRoomClients(padID).length == 0) return; - //disconnect everyone from this pad + // disconnect everyone from this pad socketio.sockets.in(padID).json.send({disconnect:"deleted"}); } @@ -113,29 +113,26 @@ exports.handleDisconnect = function(client) { stats.meter('disconnects').mark(); - //save the padname of this session + // save the padname of this session var session = sessioninfos[client.id]; - //if this connection was already etablished with a handshake, send a disconnect message to the others - if(session && session.author) - { - + // if this connection was already etablished with a handshake, send a disconnect message to the others + if (session && session.author) { // Get the IP address from our persistant object var ip = remoteAddress[client.id]; // Anonymize the IP address if IP logging is disabled - if(settings.disableIPlogging) { + if (settings.disableIPlogging) { ip = 'ANONYMOUS'; } - accessLogger.info('[LEAVE] Pad "'+session.padId+'": Author "'+session.author+'" on client '+client.id+' with IP "'+ip+'" left the pad') + accessLogger.info('[LEAVE] Pad "' + session.padId + '": Author "' + session.author + '" on client ' + client.id + ' with IP "' + ip + '" left the pad'); - //get the author color out of the db - authorManager.getAuthorColorId(session.author, function(err, color) - { + // get the author color out of the db + authorManager.getAuthorColorId(session.author, function(err, color) { ERR(err); - //prepare the notification for the other users on the pad, that this user left + // prepare the notification for the other users on the pad, that this user left var messageToTheOtherUsers = { "type": "COLLABROOM", "data": { @@ -149,7 +146,7 @@ exports.handleDisconnect = function(client) } }; - //Go trough all user that are still on the pad, and send them the USER_LEAVE message + // Go through all user that are still on the pad, and send them the USER_LEAVE message client.broadcast.to(session.padId).json.send(messageToTheOtherUsers); // Allow plugins to hook into users leaving the pad @@ -157,7 +154,7 @@ exports.handleDisconnect = function(client) }); } - //Delete the sessioninfos entrys of this session + // Delete the sessioninfos entrys of this session delete sessioninfos[client.id]; } @@ -168,23 +165,24 @@ exports.handleDisconnect = function(client) */ exports.handleMessage = function(client, message) { - if(message == null) - { + if (message == null) { return; } - if(!message.type) - { + + if (!message.type) { return; } - var thisSession = sessioninfos[client.id] - if(!thisSession) { + + var thisSession = sessioninfos[client.id]; + + if (!thisSession) { messageLogger.warn("Dropped message from an unknown connection.") return; } - var handleMessageHook = function(callback){ + var handleMessageHook = function(callback) { // Allow plugins to bypass the readonly message blocker - hooks.aCallAll("handleMessageSecurity", { client: client, message: message }, function ( err, messages ) { + hooks.aCallAll("handleMessageSecurity", { client: client, message: message }, function( err, messages ) { if(ERR(err, callback)) return; _.each(messages, function(newMessage){ if ( newMessage === true ) { @@ -196,7 +194,7 @@ exports.handleMessage = function(client, message) var dropMessage = false; // Call handleMessage hook. If a plugin returns null, the message will be dropped. Note that for all messages // handleMessage will be called, even if the client is not authorized - hooks.aCallAll("handleMessage", { client: client, message: message }, function ( err, messages ) { + hooks.aCallAll("handleMessage", { client: client, message: message }, function( err, messages ) { if(ERR(err, callback)) return; _.each(messages, function(newMessage){ if ( newMessage === null ) { @@ -210,18 +208,18 @@ exports.handleMessage = function(client, message) } - var finalHandler = function () { - //Check what type of message we get and delegate to the other methodes - if(message.type == "CLIENT_READY") { + var finalHandler = function() { + // Check what type of message we get and delegate to the other methods + if (message.type == "CLIENT_READY") { handleClientReady(client, message); - } else if(message.type == "CHANGESET_REQ") { + } else if (message.type == "CHANGESET_REQ") { handleChangesetRequest(client, message); } else if(message.type == "COLLABROOM") { if (thisSession.readonly) { messageLogger.warn("Dropped message, COLLABROOM for readonly pad"); } else if (message.data.type == "USER_CHANGES") { stats.counter('pendingEdits').inc() - padChannels.emit(message.padId, {client: client, message: message});// add to pad queue + padChannels.emit(message.padId, {client: client, message: message}); // add to pad queue } else if (message.data.type == "USERINFO_UPDATE") { handleUserInfoUpdate(client, message); } else if (message.data.type == "CHAT_MESSAGE") { @@ -242,7 +240,7 @@ exports.handleMessage = function(client, message) } else { messageLogger.warn("Dropped message, unknown Message Type " + message.type); } - }; + } /* * In a previous version of this code, an "if (message)" wrapped the @@ -259,11 +257,11 @@ exports.handleMessage = function(client, message) async.series([ handleMessageHook, - //check permissions - function(callback) - { + + // check permissions + function(callback) { // client tried to auth for the first time (first msg from the client) - if(message.type == "CLIENT_READY") { + if (message.type == "CLIENT_READY") { createSessionInfo(client, message); } @@ -274,31 +272,27 @@ exports.handleMessage = function(client, message) // FIXME: Allow to override readwrite access with readonly // Simulate using the load testing tool - if(!sessioninfos[client.id].auth){ + if (!sessioninfos[client.id].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.") return; } var auth = sessioninfos[client.id].auth; - var checkAccessCallback = function(err, statusObject) - { - if(ERR(err, callback)) return; + var checkAccessCallback = function(err, statusObject) { + if (ERR(err, callback)) return; - //access was granted - if(statusObject.accessStatus == "grant") - { + if (statusObject.accessStatus == "grant") { + // access was granted callback(); - } - //no access, send the client a message that tell him why - else - { + } else { + // no access, send the client a message that tells him why client.json.send({accessStatus: statusObject.accessStatus}) } }; - //check if pad is requested via readOnly + // check if pad is requested via readOnly if (auth.padID.indexOf("r.") === 0) { - //Pad is readOnly, first get the real Pad ID + // Pad is readOnly, first get the real Pad ID readOnlyManager.getPadId(auth.padID, function(err, value) { ERR(err); securityManager.checkAccess(value, auth.sessionID, auth.token, auth.password, checkAccessCallback); @@ -321,40 +315,40 @@ function handleSaveRevisionMessage(client, message){ var padId = sessioninfos[client.id].padId; var userId = sessioninfos[client.id].author; - padManager.getPad(padId, function(err, pad) - { - if(ERR(err)) return; + padManager.getPad(padId, function(err, pad) { + if (ERR(err)) return; pad.addSavedRevision(pad.head, userId); }); } /** - * Handles a custom message, different to the function below as it handles objects not strings and you can - * direct the message to specific sessionID + * Handles a custom message, different to the function below as it handles + * objects not strings and you can direct the message to specific sessionID * * @param msg {Object} the message we're sending * @param sessionID {string} the socketIO session to which we're sending this message */ -exports.handleCustomObjectMessage = function (msg, sessionID, cb) { - if(msg.data.type === "CUSTOM"){ - if(sessionID){ // If a sessionID is targeted then send directly to this sessionID - socketio.sockets.socket(sessionID).json.send(msg); // send a targeted message - }else{ - socketio.sockets.in(msg.data.payload.padId).json.send(msg); // broadcast to all clients on this pad +exports.handleCustomObjectMessage = function(msg, sessionID, cb) { + if (msg.data.type === "CUSTOM") { + if (sessionID){ + // a sessionID is targeted: directly to this sessionID + socketio.sockets.socket(sessionID).json.send(msg); + } else { + // broadcast to all clients on this pad + socketio.sockets.in(msg.data.payload.padId).json.send(msg); } } cb(null, {}); } - /** * Handles a custom message (sent via HTTP API request) * * @param padID {Pad} the pad to which we're sending this message * @param msgString {String} the message we're sending */ -exports.handleCustomMessage = function (padID, msgString, cb) { +exports.handleCustomMessage = function(padID, msgString, cb) { var time = Date.now(); var msg = { type: 'COLLABROOM', @@ -390,34 +384,33 @@ function handleChatMessage(client, message) * @param text the text of the chat message * @param padId the padId to send the chat message to */ -exports.sendChatMessageToPadClients = function (time, userId, text, padId) { +exports.sendChatMessageToPadClients = function(time, userId, text, padId) { var pad; var userName; async.series([ - //get the pad - function(callback) - { - padManager.getPad(padId, function(err, _pad) - { - if(ERR(err, callback)) return; + // get the pad + function(callback) { + padManager.getPad(padId, function(err, _pad) { + if (ERR(err, callback)) return; + pad = _pad; callback(); }); }, - function(callback) - { - authorManager.getAuthorName(userId, function(err, _userName) - { - if(ERR(err, callback)) return; + + function(callback) { + authorManager.getAuthorName(userId, function(err, _userName) { + if (ERR(err, callback)) return; + userName = _userName; callback(); }); }, - //save the chat message and broadcast it - function(callback) - { - //save the chat message + + // save the chat message and broadcast it + function(callback) { + // save the chat message pad.appendChatMessage(text, userId, time); var msg = { @@ -431,13 +424,13 @@ exports.sendChatMessageToPadClients = function (time, userId, text, padId) { } }; - //broadcast the chat message to everyone on the pad + // broadcast the chat message to everyone on the pad socketio.sockets.in(padId).json.send(msg); callback(); } - ], function(err) - { + ], + function(err) { ERR(err); }); } @@ -449,13 +442,12 @@ exports.sendChatMessageToPadClients = function (time, userId, text, padId) { */ function handleGetChatMessages(client, message) { - if(message.data.start == null) - { + if (message.data.start == null) { messageLogger.warn("Dropped message, GetChatMessages Message has no start!"); return; } - if(message.data.end == null) - { + + if (message.data.end == null) { messageLogger.warn("Dropped message, GetChatMessages Message has no start!"); return; } @@ -464,8 +456,7 @@ function handleGetChatMessages(client, message) var end = message.data.end; var count = end - start; - if(count < 0 || count > 100) - { + if (count < 0 || count > 100) { messageLogger.warn("Dropped message, GetChatMessages Message, client requested invalid amout of messages!"); return; } @@ -474,21 +465,19 @@ function handleGetChatMessages(client, message) var pad; async.series([ - //get the pad - function(callback) - { - padManager.getPad(padId, function(err, _pad) - { - if(ERR(err, callback)) return; + // get the pad + function(callback) { + padManager.getPad(padId, function(err, _pad) { + if (ERR(err, callback)) return; + pad = _pad; callback(); }); }, - function(callback) - { - pad.getChatMessages(start, end, function(err, chatMessages) - { - if(ERR(err, callback)) return; + + function(callback) { + pad.getChatMessages(start, end, function(err, chatMessages) { + if (ERR(err, callback)) return; var infoMsg = { type: "COLLABROOM", @@ -511,14 +500,13 @@ function handleGetChatMessages(client, message) */ function handleSuggestUserName(client, message) { - //check if all ok - if(message.data.payload.newName == null) - { + // check if all ok + if (message.data.payload.newName == null) { messageLogger.warn("Dropped message, suggestUserName Message has no newName!"); return; } - if(message.data.payload.unnamedId == null) - { + + if (message.data.payload.unnamedId == null) { messageLogger.warn("Dropped message, suggestUserName Message has no unnamedId!"); return; } @@ -526,10 +514,10 @@ function handleSuggestUserName(client, message) var padId = sessioninfos[client.id].padId; var roomClients = _getRoomClients(padId); - //search the author and send him this message + // search the author and send him this message roomClients.forEach(function(client) { var session = sessioninfos[client.id]; - if(session && session.author == message.data.payload.unnamedId) { + if (session && session.author == message.data.payload.unnamedId) { client.json.send(message); } }); @@ -542,37 +530,35 @@ function handleSuggestUserName(client, message) */ function handleUserInfoUpdate(client, message) { - //check if all ok - if(message.data.userInfo == null) - { + // check if all ok + if (message.data.userInfo == null) { messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no userInfo!"); return; } - if(message.data.userInfo.colorId == null) - { + + if (message.data.userInfo.colorId == null) { messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no colorId!"); return; } // Check that we have a valid session and author to update. var session = sessioninfos[client.id]; - if(!session || !session.author || !session.padId) - { + if (!session || !session.author || !session.padId) { messageLogger.warn("Dropped message, USERINFO_UPDATE Session not ready." + message.data); return; } - //Find out the author name of this session + // Find out the author name of this session var author = session.author; // Check colorId is a Hex color var isColor = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(message.data.userInfo.colorId) // for #f00 (Thanks Smamatti) - if(!isColor){ + if (!isColor) { messageLogger.warn("Dropped message, USERINFO_UPDATE Color is malformed." + message.data); return; } - //Tell the authorManager about the new attributes + // Tell the authorManager about the new attributes authorManager.setAuthorColorId(author, message.data.userInfo.colorId); authorManager.setAuthorName(author, message.data.userInfo.name); @@ -585,7 +571,7 @@ function handleUserInfoUpdate(client, message) type: "USER_NEWINFO", userInfo: { userId: author, - //set a null name, when there is no name set. cause the client wants it null + // set a null name, when there is no name set. cause the client wants it null name: message.data.userInfo.name || null, colorId: message.data.userInfo.colorId, userAgent: "Anonymous", @@ -594,7 +580,7 @@ function handleUserInfoUpdate(client, message) } }; - //Send the other clients on the pad the update message + // Send the other clients on the pad the update message client.broadcast.to(padId).json.send(infoMsg); } @@ -621,34 +607,34 @@ function handleUserChanges(data, cb) stats.counter('pendingEdits').dec() // Make sure all required fields are present - if(message.data.baseRev == null) - { + if (message.data.baseRev == null) { messageLogger.warn("Dropped message, USER_CHANGES Message has no baseRev!"); return cb(); } - if(message.data.apool == null) - { + + if (message.data.apool == null) { messageLogger.warn("Dropped message, USER_CHANGES Message has no apool!"); return cb(); } - if(message.data.changeset == null) - { + + if (message.data.changeset == null) { messageLogger.warn("Dropped message, USER_CHANGES Message has no changeset!"); return cb(); } - //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 - //if the session was valid when the message arrived in the first place - if(!sessioninfos[client.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 + // if the session was valid when the message arrived in the first place + if (!sessioninfos[client.id]) { messageLogger.warn("Dropped message, disconnect happened in the mean time"); return cb(); } - //get all Vars we need + // get all Vars we need var baseRev = message.data.baseRev; var wireApool = (new AttributePool()).fromJsonable(message.data.apool); var changeset = message.data.changeset; + // The client might disconnect between our callbacks. We should still // finish processing the changeset, so keep a reference to the session. var thisSession = sessioninfos[client.id]; @@ -659,31 +645,29 @@ function handleUserChanges(data, cb) var stopWatch = stats.timer('edits').start(); async.series([ - //get the pad - function(callback) - { - padManager.getPad(thisSession.padId, function(err, value) - { - if(ERR(err, callback)) return; + // get the pad + function(callback) { + padManager.getPad(thisSession.padId, function(err, value) { + if (ERR(err, callback)) return; + pad = value; callback(); }); }, - //create the changeset - function(callback) - { - //ex. _checkChangesetAndPool - try - { + // create the changeset + function(callback) { + // ex. _checkChangesetAndPool + + try { // Verify that the changeset has valid syntax and is in canonical form Changeset.checkRep(changeset); // Verify that the attribute indexes used in the changeset are all // defined in the accompanying attribute pool. Changeset.eachAttribNumber(changeset, function(n) { - if (! wireApool.getAttrib(n)) { - throw new Error("Attribute pool is missing attribute "+n+" for changeset "+changeset); + if (!wireApool.getAttrib(n)) { + throw new Error("Attribute pool is missing attribute " + n + " for changeset " + changeset); } }); @@ -693,102 +677,105 @@ function handleUserChanges(data, cb) while(iterator.hasNext()) { op = iterator.next() - //+ can add text with attribs - //= can change or add attribs - //- can have attribs, but they are discarded and don't show up in the attribs - but do show up in the pool + // + can add text with attribs + // = can change or add attribs + // - can have attribs, but they are discarded and don't show up in the attribs - but do show up in the pool op.attribs.split('*').forEach(function(attr) { - if(!attr) return + if (!attr) return; + attr = wireApool.getAttrib(attr) - if(!attr) return - //the empty author is used in the clearAuthorship functionality so this should be the only exception - if('author' == attr[0] && (attr[1] != thisSession.author && attr[1] != '')) throw new Error("Trying to submit changes as another author in changeset "+changeset); - }) + if (!attr) return; + + // the empty author is used in the clearAuthorship functionality so this should be the only exception + if ('author' == attr[0] && (attr[1] != thisSession.author && attr[1] != '')) { + throw new Error("Trying to submit changes as another author in changeset " + changeset); + } + }); } - //ex. adoptChangesetAttribs + // ex. adoptChangesetAttribs - //Afaik, it copies the new attributes from the changeset, to the global Attribute Pool + // Afaik, it copies the new attributes from the changeset, to the global Attribute Pool changeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool); - } - catch(e) - { + } catch(e) { // There is an error in this changeset, so just refuse it client.json.send({disconnect:"badChangeset"}); stats.meter('failedChangesets').mark(); return callback(new Error("Can't apply USER_CHANGES, because "+e.message)); } - //ex. applyUserChanges + // ex. applyUserChanges apool = pad.pool; r = baseRev; // The client's changeset might not be based on the latest revision, // since other clients are sending changes at the same time. // Update the changeset so that it can be applied to the latest revision. - //https://github.com/caolan/async#whilst + // https://github.com/caolan/async#whilst async.whilst( function() { return r < pad.getHeadRevisionNumber(); }, function(callback) { r++; - pad.getRevisionChangeset(r, function(err, c) - { - if(ERR(err, callback)) return; + pad.getRevisionChangeset(r, function(err, c) { + if (ERR(err, callback)) return; // At this point, both "c" (from the pad) and "changeset" (from the // client) are relative to revision r - 1. The follow function // rebases "changeset" so that it is relative to revision r // and can be applied after "c". - try - { + try { // a changeset can be based on an old revision with the same changes in it // prevent eplite from accepting it TODO: better send the client a NEW_CHANGES // of that revision - if(baseRev+1 == r && c == changeset) { + if (baseRev + 1 == r && c == changeset) { client.json.send({disconnect:"badChangeset"}); stats.meter('failedChangesets').mark(); + return callback(new Error("Won't apply USER_CHANGES, because it contains an already accepted changeset")); } + changeset = Changeset.follow(c, changeset, false, apool); - }catch(e){ + } catch(e) { client.json.send({disconnect:"badChangeset"}); stats.meter('failedChangesets').mark(); + return callback(new Error("Can't apply USER_CHANGES, because "+e.message)); } - if ((r - baseRev) % 200 == 0) { // don't let the stack get too deep + if ((r - baseRev) % 200 == 0) { + // don't let the stack get too deep async.nextTick(callback); } else { callback(null); } }); }, - //use the callback of the series function + + // use the callback of the series function callback ); }, - //do correction changesets, and send it to all users - function (callback) - { + + // do correction changesets, and send it to all users + function(callback) { var prevText = pad.text(); - if (Changeset.oldLen(changeset) != prevText.length) - { + if (Changeset.oldLen(changeset) != prevText.length) { client.json.send({disconnect:"badChangeset"}); stats.meter('failedChangesets').mark(); + return callback(new Error("Can't apply USER_CHANGES "+changeset+" with oldLen " + Changeset.oldLen(changeset) + " to document of length " + prevText.length)); } - try - { + try { pad.appendRevision(changeset, thisSession.author); - } - catch(e) - { + } catch(e) { client.json.send({disconnect:"badChangeset"}); stats.meter('failedChangesets').mark(); + return callback(e) } @@ -809,21 +796,25 @@ function handleUserChanges(data, cb) }); callback(); } - ], function(err) - { + ], + function(err) { stopWatch.end() cb(); - if(err) console.warn(err.stack || err) + + if(err) { + console.warn(err.stack || err); + } }); } exports.updatePadClients = function(pad, callback) { - //skip this step if noone is on this pad + // skip this if no-one is on this pad var roomClients = _getRoomClients(pad.id); - if(roomClients.length==0) + if (roomClients.length == 0) { return callback(); + } // since all clients usually get the same set of changesets, store them in local cache // to remove unnecessary roundtrip to the datalayer @@ -833,26 +824,27 @@ exports.updatePadClients = function(pad, callback) // but benefit of reusing cached revision object is HUGE var revCache = {}; - //go trough all sessions on this pad + // go through all sessions on this pad async.forEach(roomClients, function(client, callback){ var sid = client.id; - //https://github.com/caolan/async#whilst - //send them all new changesets + // https://github.com/caolan/async#whilst + // send them all new changesets async.whilst( - function (){ return sessioninfos[sid] && sessioninfos[sid].rev < pad.getHeadRevisionNumber()}, + function() { return sessioninfos[sid] && sessioninfos[sid].rev < pad.getHeadRevisionNumber()}, function(callback) { var r = sessioninfos[sid].rev + 1; async.waterfall([ function(callback) { - if(revCache[r]) + if(revCache[r]) { callback(null, revCache[r]); - else + } else { pad.getRevision(r, callback); + } }, - function(revision, callback) - { + + function(revision, callback) { revCache[r] = revision; var author = revision.meta.author, @@ -860,15 +852,13 @@ exports.updatePadClients = function(pad, callback) currentTime = revision.meta.timestamp; // next if session has not been deleted - if(sessioninfos[sid] == null) + if (sessioninfos[sid] == null) { return callback(null); - - if(author == sessioninfos[sid].author) - { - client.json.send({"type":"COLLABROOM","data":{type:"ACCEPT_COMMIT", newRev:r}}); } - else - { + + if (author == sessioninfos[sid].author) { + client.json.send({"type":"COLLABROOM","data":{type:"ACCEPT_COMMIT", newRev:r}}); + } else { var forWire = Changeset.prepareForWire(revChangeset, pad.pool); var wireMsg = {"type":"COLLABROOM", "data":{type:"NEW_CHANGES", @@ -882,7 +872,8 @@ exports.updatePadClients = function(pad, callback) client.json.send(wireMsg); } - if(sessioninfos[sid]){ + + if (sessioninfos[sid]) { sessioninfos[sid].time = currentTime; sessioninfos[sid].rev = r; } @@ -909,19 +900,18 @@ function _correctMarkersInPad(atext, apool) { while (iter.hasNext()) { var op = iter.next(); - var hasMarker = _.find(AttributeManager.lineAttributes, function(attribute){ + var hasMarker = _.find(AttributeManager.lineAttributes, function(attribute) { return Changeset.opAttributeValue(op, attribute, apool); }) !== undefined; if (hasMarker) { - for(var i=0;i 0 && text.charAt(offset-1) != '\n') { badMarkers.push(offset); } offset++; } - } - else { + } else { offset += op.chars; } } @@ -932,12 +922,15 @@ function _correctMarkersInPad(atext, apool) { // create changeset that removes these bad markers offset = 0; + var builder = Changeset.builder(text.length); + badMarkers.forEach(function(pos) { builder.keepText(text.substring(offset, pos)); builder.remove(1); offset = pos+1; }); + return builder.toString(); } @@ -950,7 +943,7 @@ function handleSwitchToPad(client, message) async.forEach(roomClients, function(client, callback) { var sinfo = sessioninfos[client.id]; - if(sinfo && sinfo.author == currentSession.author) { + if (sinfo && sinfo.author == currentSession.author) { // fix user's counter, works on page refresh or if user closes browser window and then rejoins sessioninfos[client.id] = {}; client.leave(padId); @@ -986,24 +979,23 @@ function createSessionInfo(client, message) */ function handleClientReady(client, message) { - //check if all ok - if(!message.token) - { + // check if all ok + if (!message.token) { messageLogger.warn("Dropped message, CLIENT_READY Message has no token!"); return; } - if(!message.padId) - { + + if (!message.padId) { messageLogger.warn("Dropped message, CLIENT_READY Message has no padId!"); return; } - if(!message.protocolVersion) - { + + if (!message.protocolVersion) { messageLogger.warn("Dropped message, CLIENT_READY Message has no protocolVersion!"); return; } - if(message.protocolVersion != 2) - { + + if (message.protocolVersion != 2) { messageLogger.warn("Dropped message, CLIENT_READY Message has a unknown protocolVersion '" + message.protocolVersion + "'!"); return; } @@ -1019,97 +1011,92 @@ function handleClientReady(client, message) hooks.callAll("clientReady", message); async.series([ - //Get ro/rw id:s - function (callback) - { + // Get ro/rw id:s + function(callback) { readOnlyManager.getIds(message.padId, function(err, value) { - if(ERR(err, callback)) return; + if (ERR(err, callback)) return; + padIds = value; callback(); }); }, - //check permissions - function(callback) - { + + // check permissions + function(callback) { // Note: message.sessionID is an entierly different kind of - // session from the sessions we use here! Beware! FIXME: Call - // our "sessions" "connections". + // session from the sessions we use here! Beware! + // FIXME: Call our "sessions" "connections". // FIXME: Use a hook instead // FIXME: Allow to override readwrite access with readonly - securityManager.checkAccess (padIds.padId, message.sessionID, message.token, message.password, function(err, statusObject) - { - if(ERR(err, callback)) return; + securityManager.checkAccess(padIds.padId, message.sessionID, message.token, message.password, function(err, statusObject) { + if (ERR(err, callback)) return; - //access was granted - if(statusObject.accessStatus == "grant") - { + if (statusObject.accessStatus == "grant") { + // access was granted author = statusObject.authorID; callback(); - } - //no access, send the client a message that tell him why - else - { + } else { + // no access, send the client a message that tells him why client.json.send({accessStatus: statusObject.accessStatus}) } }); }, - //get all authordata of this new user, and load the pad-object from the database + + // get all authordata of this new user, and load the pad-object from the database function(callback) { async.parallel([ - //get colorId and name - function(callback) - { - authorManager.getAuthor(author, function(err, value) - { - if(ERR(err, callback)) return; + // get colorId and name + function(callback) { + authorManager.getAuthor(author, function(err, value) { + if (ERR(err, callback)) return; + authorColorId = value.colorId; authorName = value.name; callback(); }); }, - //get pad - function(callback) - { - padManager.getPad(padIds.padId, function(err, value) - { - if(ERR(err, callback)) return; + + // get pad + function(callback) { + padManager.getPad(padIds.padId, function(err, value) { + if (ERR(err, callback)) return; + pad = value; callback(); }); } ], callback); }, - //these db requests all need the pad object (timestamp of latest revission, author data) - function(callback) - { + + // these db requests all need the pad object (timestamp of latest revission, author data) + function(callback) { var authors = pad.getAllAuthors(); async.parallel([ - //get timestamp of latest revission needed for timeslider - function(callback) - { - pad.getRevisionDate(pad.getHeadRevisionNumber(), function(err, date) - { - if(ERR(err, callback)) return; + // get timestamp of latest revission needed for timeslider + function(callback) { + pad.getRevisionDate(pad.getHeadRevisionNumber(), function(err, date) { + if (ERR(err, callback)) return; + currentTime = date; callback(); }); }, - //get all author data out of the database - function(callback) - { - async.forEach(authors, function(authorId, callback) - { - authorManager.getAuthor(authorId, function(err, author) - { - if(!author && !err) - { + + // get all author data out of the database + function(callback) { + async.forEach(authors, function(authorId, callback) { + authorManager.getAuthor(authorId, function(err, author) { + if (!author && !err) { messageLogger.error("There is no author for authorId:", authorId); + return callback(); } - if(ERR(err, callback)) return; - historicalAuthorData[authorId] = {name: author.name, colorId: author.colorId}; // Filter author attribs (e.g. don't send author's pads to all clients) + + if (ERR(err, callback)) return; + + historicalAuthorData[authorId] = { name: author.name, colorId: author.colorId }; // Filter author attribs (e.g. don't send author's pads to all clients) callback(); }); }, callback); @@ -1117,56 +1104,57 @@ function handleClientReady(client, message) ], callback); }, - //glue the clientVars together, send them and tell the other clients that a new one is there - function(callback) - { - //Check that the client is still here. It might have disconnected between callbacks. - if(sessioninfos[client.id] === undefined) - return callback(); - //Check if this author is already on the pad, if yes, kick the other sessions! + // glue the clientVars together, send them and tell the other clients that a new one is there + function(callback) { + // Check that the client is still here. It might have disconnected between callbacks. + if(sessioninfos[client.id] === undefined) { + return callback(); + } + + // Check if this author is already on the pad, if yes, kick the other sessions! var roomClients = _getRoomClients(pad.id); async.forEach(roomClients, function(client, callback) { var sinfo = sessioninfos[client.id]; - if(sinfo && sinfo.author == author) { + + if (sinfo && sinfo.author == author) { // fix user's counter, works on page refresh or if user closes browser window and then rejoins sessioninfos[client.id] = {}; client.leave(padIds.padId); - client.json.send({disconnect:"userdup"}); + client.json.send({ disconnect:"userdup" }); } }); - //Save in sessioninfos that this session belonges to this pad + // Save in sessioninfos that this session belonges to this pad sessioninfos[client.id].padId = padIds.padId; sessioninfos[client.id].readOnlyPadId = padIds.readOnlyPadId; sessioninfos[client.id].readonly = padIds.readonly; - //Log creation/(re-)entering of a pad + // Log creation/(re-)entering of a pad var ip = remoteAddress[client.id]; - //Anonymize the IP address if IP logging is disabled - if(settings.disableIPlogging) { + // Anonymize the IP address if IP logging is disabled + if (settings.disableIPlogging) { ip = 'ANONYMOUS'; } - if(pad.head > 0) { - accessLogger.info('[ENTER] Pad "'+padIds.padId+'": Client '+client.id+' with IP "'+ip+'" entered the pad'); - } - else if(pad.head == 0) { - accessLogger.info('[CREATE] Pad "'+padIds.padId+'": Client '+client.id+' with IP "'+ip+'" created the pad'); + if (pad.head > 0) { + accessLogger.info('[ENTER] Pad "' + padIds.padId + '": Client ' + client.id + ' with IP "' + ip + '" entered the pad'); + } else if (pad.head == 0) { + accessLogger.info('[CREATE] Pad "' + padIds.padId + '": Client ' + client.id + ' with IP "' + ip + '" created the pad'); } - //If this is a reconnect, we don't have to send the client the ClientVars again - if(message.reconnect == true) - { - //Join the pad and start receiving updates + if (message.reconnect == true) { + // If this is a reconnect, we don't have to send the client the ClientVars again + // Join the pad and start receiving updates 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; - //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 + // 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 = {}; @@ -1174,68 +1162,68 @@ function handleClientReady(client, message) var endNum = pad.getHeadRevisionNumber() + 1; async.series([ - //push all the revision numbers needed into revisionsNeeded array - function(callback) - { + // 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 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; + + // 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; + + // 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; + + // 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) - { + + // 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", @@ -1249,8 +1237,8 @@ function handleClientReady(client, message) client.json.send(wireMsg); callback(); }); - if (startNum == endNum) - { + + if (startNum == endNum) { var Msg = {"type":"COLLABROOM", "data":{type:"CLIENT_RECONNECT", noChanges: true, @@ -1259,19 +1247,17 @@ function handleClientReady(client, message) client.json.send(Msg); } }); - } - //This is a normal first connect - else - { - //prepare all values for the wire, there'S a chance that this throws, if the pad is corrupted + } else { + // This is a normal first connect + // prepare all values for the wire, there's a chance that this throws, if the pad is corrupted try { var atext = Changeset.cloneAText(pad.atext); var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); var apool = attribsForWire.pool.toJsonable(); atext.attribs = attribsForWire.translated; - }catch(e) { + } catch(e) { console.error(e.stack || e) - client.json.send({disconnect:"corruptPad"});// pull the breaks + client.json.send({ disconnect:"corruptPad" });// pull the brakes return callback(); } @@ -1335,35 +1321,36 @@ function handleClientReady(client, message) "initialChangesets": [] // FIXME: REMOVE THIS SHIT } - //Add a username to the clientVars if one avaiable - if(authorName != null) - { + // Add a username to the clientVars if one avaiable + if (authorName != null) { clientVars.userName = authorName; } - //call the clientVars-hook so plugins can modify them before they get sent to the client - hooks.aCallAll("clientVars", { clientVars: clientVars, pad: pad }, function ( err, messages ) { - if(ERR(err, callback)) return; + // call the clientVars-hook so plugins can modify them before they get sent to the client + hooks.aCallAll("clientVars", { clientVars: clientVars, pad: pad }, function( err, messages ) { + if (ERR(err, callback)) return; _.each(messages, function(newVars) { - //combine our old object with the new attributes from the hook + // combine our old object with the new attributes from the hook for(var attr in newVars) { clientVars[attr] = newVars[attr]; } }); - //Join the pad and start receiving updates + // Join the pad and start receiving updates client.join(padIds.padId); - //Send the clientVars to the Client - client.json.send({type: "CLIENT_VARS", data: clientVars}); - //Save the current revision in sessioninfos, should be the same as in clientVars + + // Send the clientVars to the Client + client.json.send({ type: "CLIENT_VARS", data: clientVars }); + + // Save the current revision in sessioninfos, should be the same as in clientVars sessioninfos[client.id].rev = pad.getHeadRevisionNumber(); }); } sessioninfos[client.id].author = author; - //prepare the notification for the other users on the pad, that this user joined + // prepare the notification for the other users on the pad, that this user joined var messageToTheOtherUsers = { "type": "COLLABROOM", "data": { @@ -1377,47 +1364,47 @@ function handleClientReady(client, message) } }; - //Add the authorname of this new User, if avaiable - if(authorName != null) - { + // Add the authorname of this new User, if avaiable + if (authorName != null) { messageToTheOtherUsers.data.userInfo.name = authorName; } // notify all existing users about new user client.broadcast.to(padIds.padId).json.send(messageToTheOtherUsers); - //Get sessions for this pad + // Get sessions for this pad var roomClients = _getRoomClients(pad.id); - async.forEach(roomClients, function(roomClient, callback) - { + async.forEach(roomClients, function(roomClient, callback) { var author; - //Jump over, if this session is the connection session - if(roomClient.id == client.id) + // Jump over, if this session is the connection session + if (roomClient.id == client.id) { return callback(); + } - - //Since sessioninfos might change while being enumerated, check if the - //sessionID is still assigned to a valid session - if(sessioninfos[roomClient.id] !== undefined) + // Since sessioninfos might change while being enumerated, check if the + // sessionID is still assigned to a valid session + if (sessioninfos[roomClient.id] !== undefined) { author = sessioninfos[roomClient.id].author; - else // If the client id is not valid, callback(); + } else { + // If the client id is not valid, callback(); return callback(); + } async.waterfall([ - //get the authorname & colorId - function(callback) - { + // get the authorname & colorId + function(callback) { // reuse previously created cache of author's data - if(historicalAuthorData[author]) + if (historicalAuthorData[author]) { callback(null, historicalAuthorData[author]); - else + } else { authorManager.getAuthor(author, callback); + } }, - function (authorInfo, callback) - { - //Send the new User a Notification about this other user + + function(authorInfo, callback) { + // Send the new User a Notification about this other user var msg = { "type": "COLLABROOM", "data": { @@ -1431,13 +1418,14 @@ function handleClientReady(client, message) } } }; + client.json.send(msg); } ], callback); }, callback); } - ],function(err) - { + ], + function(err) { ERR(err); }); } @@ -1447,35 +1435,34 @@ function handleClientReady(client, message) */ function handleChangesetRequest(client, message) { - //check if all ok - if(message.data == null) - { + // check if all ok + if (message.data == null) { messageLogger.warn("Dropped message, changeset request has no data!"); return; } - if(message.padId == null) - { + + if (message.padId == null) { messageLogger.warn("Dropped message, changeset request has no padId!"); return; } - if(message.data.granularity == null) - { + + if (message.data.granularity == null) { messageLogger.warn("Dropped message, changeset request has no granularity!"); return; } - //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill - if(Math.floor(message.data.granularity) !== message.data.granularity) - { + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill + if (Math.floor(message.data.granularity) !== message.data.granularity) { messageLogger.warn("Dropped message, changeset request granularity is not an integer!"); return; } - if(message.data.start == null) - { + + if (message.data.start == null) { messageLogger.warn("Dropped message, changeset request has no start!"); return; } - if(message.data.requestID == null) - { + + if (message.data.requestID == null) { messageLogger.warn("Dropped message, changeset request has no requestID!"); return; } @@ -1486,23 +1473,24 @@ function handleChangesetRequest(client, message) var padIds; async.series([ - function (callback) { + function(callback) { readOnlyManager.getIds(message.padId, function(err, value) { - if(ERR(err, callback)) return; + if (ERR(err, callback)) return; + padIds = value; callback(); }); }, - function (callback) { - //build the requested rough changesets and send them back - getChangesetInfo(padIds.padId, start, end, granularity, function(err, changesetInfo) - { - if(err) return console.error('Error while handling a changeset request for '+padIds.padId, err, message.data); + + function(callback) { + // build the requested rough changesets and send them back + getChangesetInfo(padIds.padId, start, end, granularity, function(err, changesetInfo) { + if (err) return console.error('Error while handling a changeset request for ' + padIds.padId, err, message.data); var data = changesetInfo; data.requestID = message.data.requestID; - client.json.send({type: "CHANGESET_REQ", data: data}); + client.json.send({ type: "CHANGESET_REQ", data: data }); }); } ]); @@ -1526,96 +1514,91 @@ function getChangesetInfo(padId, startNum, endNum, granularity, callback) var head_revision = 0; async.series([ - //get the pad from the database - function(callback) - { - padManager.getPad(padId, function(err, _pad) - { - if(ERR(err, callback)) return; + // get the pad from the database + function(callback) { + padManager.getPad(padId, function(err, _pad) { + if (ERR(err, callback)) return; + pad = _pad; head_revision = pad.getHeadRevisionNumber(); callback(); }); }, - function(callback) - { - //calculate the last full endnum + + function(callback) { + // calculate the last full endnum var lastRev = pad.getHeadRevisionNumber(); - if (endNum > lastRev+1) { - endNum = lastRev+1; + if (endNum > lastRev + 1) { + endNum = lastRev + 1; } - endNum = Math.floor(endNum / granularity)*granularity; + + endNum = Math.floor(endNum / granularity) * granularity; var compositesChangesetNeeded = []; var revTimesNeeded = []; - //figure out which composite Changeset and revTimes we need, to load them in bulk + // figure out which composite Changeset and revTimes we need, to load them in bulk var compositeStart = startNum; - while (compositeStart < endNum) - { + while (compositeStart < endNum) { var compositeEnd = compositeStart + granularity; - //add the composite Changeset we needed - compositesChangesetNeeded.push({start: compositeStart, end: compositeEnd}); + // add the composite Changeset we needed + compositesChangesetNeeded.push({ start: compositeStart, end: compositeEnd }); - //add the t1 time we need + // add the t1 time we need revTimesNeeded.push(compositeStart == 0 ? 0 : compositeStart - 1); - //add the t2 time we need + + // add the t2 time we need revTimesNeeded.push(compositeEnd - 1); compositeStart += granularity; } - //get all needed db values parallel + // get all needed db values parallel async.parallel([ - function(callback) - { - //get all needed composite Changesets - async.forEach(compositesChangesetNeeded, function(item, callback) - { - composePadChangesets(padId, item.start, item.end, function(err, changeset) - { - if(ERR(err, callback)) return; + function(callback) { + // get all needed composite Changesets + async.forEach(compositesChangesetNeeded, function(item, callback) { + composePadChangesets(padId, item.start, item.end, function(err, changeset) { + if (ERR(err, callback)) return; + composedChangesets[item.start + "/" + item.end] = changeset; callback(); }); }, callback); }, - function(callback) - { - //get all needed revision Dates - async.forEach(revTimesNeeded, function(revNum, callback) - { - pad.getRevisionDate(revNum, function(err, revDate) - { - if(ERR(err, callback)) return; + + function(callback) { + // get all needed revision Dates + async.forEach(revTimesNeeded, function(revNum, callback) { + pad.getRevisionDate(revNum, function(err, revDate) { + if (ERR(err, callback)) return; + revisionDate[revNum] = Math.floor(revDate/1000); callback(); }); }, callback); }, - //get the lines - function(callback) - { - getPadLines(padId, startNum-1, function(err, _lines) - { - if(ERR(err, callback)) return; + + // get the lines + function(callback) { + getPadLines(padId, startNum-1, function(err, _lines) { + if (ERR(err, callback)) return; + lines = _lines; callback(); }); } ], callback); }, - //doesn't know what happens here excatly :/ - function(callback) - { + + // don't know what happens here exactly :/ + function(callback) { var compositeStart = startNum; - while (compositeStart < endNum) - { + while (compositeStart < endNum) { var compositeEnd = compositeStart + granularity; - if (compositeEnd > endNum || compositeEnd > head_revision+1) - { + if (compositeEnd > endNum || compositeEnd > head_revision+1) { break; } @@ -1629,12 +1612,9 @@ function getChangesetInfo(padId, startNum, endNum, granularity, callback) var backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool); var t1, t2; - if (compositeStart == 0) - { + if (compositeStart == 0) { t1 = revisionDate[0]; - } - else - { + } else { t1 = revisionDate[compositeStart - 1]; } @@ -1649,9 +1629,9 @@ function getChangesetInfo(padId, startNum, endNum, granularity, callback) callback(); } - ], function(err) - { - if(ERR(err, callback)) return; + ], + function(err) { + if (ERR(err, callback)) return; callback(null, {forwardsChangesets: forwardsChangesets, backwardsChangesets: backwardsChangesets, @@ -1674,43 +1654,41 @@ function getPadLines(padId, revNum, callback) var pad; async.series([ - //get the pad from the database - function(callback) - { - padManager.getPad(padId, function(err, _pad) - { - if(ERR(err, callback)) return; + // get the pad from the database + function(callback) { + padManager.getPad(padId, function(err, _pad) { + if (ERR(err, callback)) return; + pad = _pad; callback(); }); }, - //get the atext - function(callback) - { - if(revNum >= 0) - { - pad.getInternalRevisionAText(revNum, function(err, _atext) - { - if(ERR(err, callback)) return; + + // get the atext + function(callback) { + if (revNum >= 0) { + pad.getInternalRevisionAText(revNum, function(err, _atext) { + if (ERR(err, callback)) return; + atext = _atext; callback(); }); - } - else - { + } else { atext = Changeset.makeAText("\n"); callback(null); } }, - function(callback) - { + + function(callback) { result.textlines = Changeset.splitTextLines(atext.text); result.alines = Changeset.splitAttributionLines(atext.attribs, atext.text); callback(null); } - ], function(err) - { - if(ERR(err, callback)) return; + ], + + function(err) { + if (ERR(err, callback)) return; + callback(null, result); }); } @@ -1726,74 +1704,77 @@ function composePadChangesets(padId, startNum, endNum, callback) var changeset; async.series([ - //get the pad from the database - function(callback) - { - padManager.getPad(padId, function(err, _pad) - { - if(ERR(err, callback)) return; + // get the pad from the database + function(callback) { + padManager.getPad(padId, function(err, _pad) { + if (ERR(err, callback)) return; + pad = _pad; callback(); }); }, - //fetch all changesets we need - function(callback) - { + + // fetch all changesets we need + function(callback) { var changesetsNeeded=[]; var headNum = pad.getHeadRevisionNumber(); - if (endNum > headNum+1) - endNum = headNum+1; - if (startNum < 0) + if (endNum > headNum + 1) { + endNum = headNum + 1; + } + + if (startNum < 0) { startNum = 0; - //create a array for all changesets, we will - //replace the values with the changeset later - for(var r=startNum;r b[property]) - return dir? 1 : -1; + if (a[property] < b[property]) { + return dir? -1 : 1; + } + + if (a[property] > b[property]) { + return dir? 1 : -1; + } + // a must be equal to b return 0; - }) + }); } diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js index 104a9c1bb..9a07dc669 100644 --- a/src/node/hooks/express/errorhandling.js +++ b/src/node/hooks/express/errorhandling.js @@ -11,20 +11,23 @@ exports.gracefulShutdown = function(err) { console.error(err); } - //ensure there is only one graceful shutdown running - if(exports.onShutdown) return; + // ensure there is only one graceful shutdown running + if (exports.onShutdown) { + return; + } + exports.onShutdown = true; console.log("graceful shutdown..."); - //do the db shutdown + // do the db shutdown db.db.doShutdown(function() { console.log("db sucessfully closed."); process.exit(0); }); - setTimeout(function(){ + setTimeout(function() { process.exit(1); }, 3000); } @@ -35,14 +38,14 @@ exports.expressCreateServer = function (hook_name, args, cb) { exports.app = args.app; // Handle errors - args.app.use(function(err, req, res, next){ + args.app.use(function(err, req, res, next) { // if an error occurs Connect will pass it down // through these "error-handling" middleware // allowing you to respond however you like res.status(500).send({ error: 'Sorry, something bad happened!' }); console.error(err.stack? err.stack : err.toString()); stats.meter('http500').mark() - }) + }); /* * Connect graceful shutdown with sigint and uncaught exception diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js index a62942cc0..fe97aa2f9 100644 --- a/src/node/hooks/express/importexport.js +++ b/src/node/hooks/express/importexport.js @@ -13,7 +13,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { return; } - //if abiword is disabled, and this is a format we only support with abiword, output a message + // if abiword is disabled, and this is a format we only support with abiword, output a message if (settings.exportAvailable() == "no" && ["odt", "pdf", "doc"].indexOf(req.params.type) !== -1) { res.send("This export is not enabled at this Etherpad instance. Set the path to Abiword or SOffice in settings.json to enable this feature"); @@ -24,9 +24,8 @@ exports.expressCreateServer = function (hook_name, args, cb) { hasPadAccess(req, res, function() { console.log('req.params.pad', req.params.pad); - padManager.doesPadExists(req.params.pad, function(err, exists) - { - if(!exists) { + padManager.doesPadExists(req.params.pad, function(err, exists) { + if (!exists) { return next(); } @@ -35,12 +34,11 @@ exports.expressCreateServer = function (hook_name, args, cb) { }); }); - //handle import requests + // handle import requests args.app.post('/p/:pad/import', function(req, res, next) { hasPadAccess(req, res, function() { - padManager.doesPadExists(req.params.pad, function(err, exists) - { - if(!exists) { + padManager.doesPadExists(req.params.pad, function(err, exists) { + if (!exists) { return next(); } diff --git a/src/node/hooks/express/padreadonly.js b/src/node/hooks/express/padreadonly.js index bff8adf7b..77b197c7c 100644 --- a/src/node/hooks/express/padreadonly.js +++ b/src/node/hooks/express/padreadonly.js @@ -5,52 +5,45 @@ var hasPadAccess = require("../../padaccess"); var exporthtml = require("../../utils/ExportHtml"); exports.expressCreateServer = function (hook_name, args, cb) { - //serve read only pad - args.app.get('/ro/:id', function(req, res) - { + // serve read only pad + args.app.get('/ro/:id', function(req, res) { var html; var padId; async.series([ - //translate the read only pad to a padId - function(callback) - { - readOnlyManager.getPadId(req.params.id, function(err, _padId) - { + // translate the read only pad to a padId + function(callback) { + readOnlyManager.getPadId(req.params.id, function(err, _padId) { if(ERR(err, callback)) return; padId = _padId; - //we need that to tell hasPadAcess about the pad + // we need that to tell hasPadAcess about the pad req.params.pad = padId; callback(); }); }, - //render the html document - function(callback) - { - //return if the there is no padId - if(padId == null) - { + // render the html document + function(callback) { + // return if the there is no padId + if(padId == null) { callback("notfound"); return; } - hasPadAccess(req, res, function() - { - //render the html document - exporthtml.getPadHTMLDocument(padId, null, function(err, _html) - { + hasPadAccess(req, res, function() { + // render the html document + exporthtml.getPadHTMLDocument(padId, null, function(err, _html) { if(ERR(err, callback)) return; html = _html; callback(); }); }); } - ], function(err) - { - //throw any unexpected error + ], + function(err) { + // throw any unexpected error if(err && err != "notfound") ERR(err); diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js index be3ffb1b4..a7fb9f33e 100644 --- a/src/node/hooks/express/padurlsanitize.js +++ b/src/node/hooks/express/padurlsanitize.js @@ -2,29 +2,26 @@ var padManager = require('../../db/PadManager'); var url = require('url'); exports.expressCreateServer = function (hook_name, args, cb) { - //redirects browser to the pad's sanitized url if needed. otherwise, renders the html + + // redirects browser to the pad's sanitized url if needed. otherwise, renders the html args.app.param('pad', function (req, res, next, padId) { - //ensure the padname is valid and the url doesn't end with a / - if(!padManager.isValidPadId(padId) || /\/$/.test(req.url)) - { + // ensure the padname is valid and the url doesn't end with a / + if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) { res.status(404).send('Such a padname is forbidden'); return; } padManager.sanitizePadId(padId, function(sanitizedPadId) { - //the pad id was sanitized, so we redirect to the sanitized version - if(sanitizedPadId != padId) - { + if (sanitizedPadId != padId) { + // the pad id was sanitized, so we redirect to the sanitized version var real_url = sanitizedPadId; real_url = encodeURIComponent(real_url); var query = url.parse(req.url).query; if ( query ) real_url += '?' + query; res.header('Location', real_url); res.status(302).send('You should be redirected to ' + real_url + ''); - } - //the pad id was fine, so just render it - else - { + } else { + // the pad id was fine, so just render it next(); } }); diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index d0dcc0cc6..b5c4ca56c 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -4,37 +4,35 @@ var path = require("path") , async = require("async"); exports.expressCreateServer = function (hook_name, args, cb) { - args.app.get('/tests/frontend/specs_list.js', function(req, res){ - + args.app.get('/tests/frontend/specs_list.js', function(req, res) { async.parallel({ - coreSpecs: function(callback){ + coreSpecs: function(callback) { exports.getCoreTests(callback); }, - pluginSpecs: function(callback){ + pluginSpecs: function(callback) { exports.getPluginTests(callback); } }, - function(err, results){ + function(err, results) { var files = results.coreSpecs; // push the core specs to a file object files = files.concat(results.pluginSpecs); // add the plugin Specs to the core specs console.debug("Sent browser the following test specs:", files.sort()); res.send("var specs_list = " + JSON.stringify(files.sort()) + ";\n"); }); - }); - // path.join seems to normalize by default, but we'll just be explicit var rootTestFolder = path.normalize(path.join(npm.root, "../tests/frontend/")); - var url2FilePath = function(url){ + var url2FilePath = function(url) { var subPath = url.substr("/tests/frontend".length); - if (subPath == ""){ + if (subPath == "") { subPath = "index.html" } subPath = subPath.split("?")[0]; var filePath = path.normalize(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,13 +44,13 @@ exports.expressCreateServer = function (hook_name, args, cb) { var specFilePath = url2FilePath(req.url); var specFileName = path.basename(specFilePath); - fs.readFile(specFilePath, function(err, content){ - if(err){ return res.send(500); } - + fs.readFile(specFilePath, function(err, content) { + if (err) { return res.send(500); } + content = "describe(" + JSON.stringify(specFileName) + ", function(){ " + content + " });"; res.send(content); - }); + }); }); args.app.get('/tests/frontend/*', function (req, res) { @@ -62,19 +60,21 @@ exports.expressCreateServer = function (hook_name, args, cb) { args.app.get('/tests/frontend', function (req, res) { res.redirect('/tests/frontend/'); - }); + }); } -exports.getPluginTests = function(callback){ +exports.getPluginTests = function(callback) { var pluginSpecs = []; var plugins = fs.readdirSync('node_modules'); - plugins.forEach(function(plugin){ - if(fs.existsSync("node_modules/"+plugin+"/static/tests/frontend/specs")){ // if plugins exists - var specFiles = fs.readdirSync("node_modules/"+plugin+"/static/tests/frontend/specs/"); - async.forEach(specFiles, function(spec){ // for each specFile push it to pluginSpecs - pluginSpecs.push("/static/plugins/"+plugin+"/static/tests/frontend/specs/" + spec); + plugins.forEach(function(plugin) { + if (fs.existsSync("node_modules/" + plugin + "/static/tests/frontend/specs")) { + // if plugins exists + var specFiles = fs.readdirSync("node_modules/" + plugin + "/static/tests/frontend/specs/"); + async.forEach(specFiles, function(spec) { + // for each specFile push it to pluginSpecs + pluginSpecs.push("/static/plugins/" + plugin + "/static/tests/frontend/specs/" + spec); }, - function(err){ + function(err) { // blow up if something bad happens! }); } @@ -82,10 +82,11 @@ exports.getPluginTests = function(callback){ callback(null, pluginSpecs); } -exports.getCoreTests = function(callback){ - fs.readdir('tests/frontend/specs', function(err, coreSpecs){ // get the core test specs - if(err){ return res.send(500); } +exports.getCoreTests = function(callback) { + // get the core test specs + fs.readdir('tests/frontend/specs', function(err, coreSpecs) { + if (err) { return res.send(500); } + callback(null, coreSpecs); }); } - diff --git a/src/node/padaccess.js b/src/node/padaccess.js index 1f2e8834b..c8c888cd0 100644 --- a/src/node/padaccess.js +++ b/src/node/padaccess.js @@ -1,16 +1,16 @@ var ERR = require("async-stacktrace"); var securityManager = require('./db/SecurityManager'); -//checks for padAccess +// checks for padAccess module.exports = function (req, res, callback) { securityManager.checkAccess(req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password, function(err, accessObj) { - if(ERR(err, callback)) return; + if (ERR(err, callback)) return; - //there is access, continue - if(accessObj.accessStatus == "grant") { + if (accessObj.accessStatus == "grant") { + // there is access, continue callback(); - //no access } else { + // no access res.status(403).send("403 - Can't touch this"); } }); diff --git a/src/node/server.js b/src/node/server.js index 3db54284c..2ba1fa47d 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -1,7 +1,7 @@ #!/usr/bin/env node /** - * This module is started with bin/run.sh. It sets up a Express HTTP and a Socket.IO Server. - * Static file Requests are answered directly from this module, Socket.IO messages are passed + * This module is started with bin/run.sh. It sets up a Express HTTP and a Socket.IO Server. + * Static file Requests are answered directly from this module, Socket.IO messages are passed * to MessageHandler and minfied requests are passed to minified. */ @@ -46,8 +46,8 @@ NodeVersion.enforceMinNodeVersion('8.9.0'); */ var stats = require('./stats'); stats.gauge('memoryUsage', function() { - return process.memoryUsage().rss -}) + return process.memoryUsage().rss; +}); var settings , db @@ -59,8 +59,8 @@ async.waterfall([ // load npm function(callback) { npm.load({}, function(er) { - callback(er) - }) + callback(er); + }); }, // load everything @@ -73,14 +73,13 @@ async.waterfall([ callback(); }, - //initalize the database - function (callback) - { + // initalize the database + function (callback) { db.init(callback); }, function(callback) { - plugins.update(callback) + plugins.update(callback); }, function (callback) { @@ -93,9 +92,8 @@ async.waterfall([ callback(); }, - //initalize the http server - function (callback) - { + // initalize the http server + function (callback) { hooks.callAll("createServer", {}); callback(null); } diff --git a/src/node/utils/ExportTxt.js b/src/node/utils/ExportTxt.js index 8a40e800d..a42878df6 100644 --- a/src/node/utils/ExportTxt.js +++ b/src/node/utils/ExportTxt.js @@ -30,40 +30,32 @@ function getPadTXT(pad, revNum, callback) var atext = pad.atext; var html; async.waterfall([ + // fetch revision atext + function(callback) { + if (revNum != undefined) { + pad.getInternalRevisionAText(revNum, function(err, revisionAtext) { + if (ERR(err, callback)) return; - - function (callback) - { - if (revNum != undefined) - { - pad.getInternalRevisionAText(revNum, function (err, revisionAtext) - { - if(ERR(err, callback)) return; atext = revisionAtext; callback(); }); - } - else - { + } else { callback(null); } }, // convert atext to html - - - function (callback) - { - html = getTXTFromAtext(pad, atext); // only this line is different to the HTML function + function(callback) { + // only this line is different to the HTML function + html = getTXTFromAtext(pad, atext); callback(null); }], + // run final callback + function(err) { + if (ERR(err, callback)) return; - - function (err) - { - if(ERR(err, callback)) return; callback(null, html); }); } @@ -80,17 +72,14 @@ function getTXTFromAtext(pad, atext, authorColors) var anumMap = {}; var css = ""; - props.forEach(function (propName, i) - { + props.forEach(function(propName, i) { var propTrueNum = apool.putAttrib([propName, true], true); - if (propTrueNum >= 0) - { + if (propTrueNum >= 0) { anumMap[propTrueNum] = i; } }); - function getLineTXT(text, attribs) - { + function getLineTXT(text, attribs) { var propVals = [false, false, false]; var ENTER = 1; var STAY = 2; @@ -106,94 +95,77 @@ function getTXTFromAtext(pad, atext, authorColors) var idx = 0; - function processNextChars(numChars) - { - if (numChars <= 0) - { + function processNextChars(numChars) { + if (numChars <= 0) { return; } var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars)); idx += numChars; - while (iter.hasNext()) - { + while (iter.hasNext()) { var o = iter.next(); var propChanged = false; - Changeset.eachAttribNumber(o.attribs, function (a) - { - if (a in anumMap) - { + + Changeset.eachAttribNumber(o.attribs, function(a) { + if (a in anumMap) { var i = anumMap[a]; // i = 0 => bold, etc. - if (!propVals[i]) - { + + if (!propVals[i]) { propVals[i] = ENTER; propChanged = true; - } - else - { + } else { propVals[i] = STAY; } } }); - for (var i = 0; i < propVals.length; i++) - { - if (propVals[i] === true) - { + + for (var i = 0; i < propVals.length; i++) { + if (propVals[i] === true) { propVals[i] = LEAVE; propChanged = true; - } - else if (propVals[i] === STAY) - { - propVals[i] = true; // set it back + } else if (propVals[i] === STAY) { + // set it back + propVals[i] = true; } } + // now each member of propVal is in {false,LEAVE,ENTER,true} // according to what happens at start of span - if (propChanged) - { + if (propChanged) { // leaving bold (e.g.) also leaves italics, etc. var left = false; - for (var i = 0; i < propVals.length; i++) - { + + for (var i = 0; i < propVals.length; i++) { var v = propVals[i]; - if (!left) - { - if (v === LEAVE) - { + + if (!left) { + if (v === LEAVE) { left = true; } - } - else - { - if (v === true) - { - propVals[i] = STAY; // tag will be closed and re-opened + } else { + if (v === true) { + // tag will be closed and re-opened + propVals[i] = STAY; } } } var tags2close = []; - for (var i = propVals.length - 1; i >= 0; i--) - { - if (propVals[i] === LEAVE) - { + for (var i = propVals.length - 1; i >= 0; i--) { + if (propVals[i] === LEAVE) { //emitCloseTag(i); tags2close.push(i); propVals[i] = false; - } - else if (propVals[i] === STAY) - { + } else if (propVals[i] === STAY) { //emitCloseTag(i); tags2close.push(i); } } - for (var i = 0; i < propVals.length; i++) - { - if (propVals[i] === ENTER || propVals[i] === STAY) - { + for (var i = 0; i < propVals.length; i++) { + if (propVals[i] === ENTER || propVals[i] === STAY) { propVals[i] = true; } } @@ -201,9 +173,9 @@ function getTXTFromAtext(pad, atext, authorColors) } // end if (propChanged) var chars = o.chars; - if (o.lines) - { - chars--; // exclude newline at end of line, if present + if (o.lines) { + // exclude newline at end of line, if present + chars--; } var s = taker.take(chars); @@ -220,19 +192,19 @@ function getTXTFromAtext(pad, atext, authorColors) } // end iteration over spans in line var tags2close = []; - for (var i = propVals.length - 1; i >= 0; i--) - { - if (propVals[i]) - { + for (var i = propVals.length - 1; i >= 0; i--) { + if (propVals[i]) { tags2close.push(i); propVals[i] = false; } } } // end processNextChars + processNextChars(text.length - idx); return(assem.toString()); } // end getLineHTML + var pieces = [css]; // Need to deal with constraints imposed on HTML lists; can @@ -242,41 +214,44 @@ function getTXTFromAtext(pad, atext, authorColors) // so we want to do something reasonable there. We also // want to deal gracefully with blank lines. // => keeps track of the parents level of indentation - for (var i = 0; i < textLines.length; i++) - { + for (var i = 0; i < textLines.length; i++) { var line = _analyzeLine(textLines[i], attribLines[i], apool); var lineContent = getLineTXT(line.text, line.aline); - if(line.listTypeName == "bullet"){ + + if (line.listTypeName == "bullet") { lineContent = "* " + lineContent; // add a bullet } - if(line.listLevel > 0){ - for (var j = line.listLevel - 1; j >= 0; j--){ + + if (line.listLevel > 0) { + for (var j = line.listLevel - 1; j >= 0; j--) { pieces.push('\t'); } - if(line.listTypeName == "number"){ + + if (line.listTypeName == "number") { pieces.push(line.listLevel + ". "); // This is bad because it doesn't truly reflect what the user // sees because browsers do magic on nested
  1. s } + pieces.push(lineContent, '\n'); - }else{ + } else { pieces.push(lineContent, '\n'); } } return pieces.join(''); } + exports.getTXTFromAtext = getTXTFromAtext; -exports.getPadTXTDocument = function (padId, revNum, callback) +exports.getPadTXTDocument = function(padId, revNum, callback) { - padManager.getPad(padId, function (err, pad) - { - if(ERR(err, callback)) return; + padManager.getPad(padId, function(err, pad) { + if (ERR(err, callback)) return; + + getPadTXT(pad, revNum, function(err, html) { + if (ERR(err, callback)) return; - getPadTXT(pad, revNum, function (err, html) - { - if(ERR(err, callback)) return; callback(null, html); }); }); diff --git a/src/node/utils/ImportEtherpad.js b/src/node/utils/ImportEtherpad.js index bf1129cb9..1ff8b9b04 100644 --- a/src/node/utils/ImportEtherpad.js +++ b/src/node/utils/ImportEtherpad.js @@ -18,57 +18,56 @@ var log4js = require('log4js'); var async = require("async"); var db = require("../db/DB").db; -exports.setPadRaw = function(padId, records, callback){ +exports.setPadRaw = function(padId, records, callback) +{ records = JSON.parse(records); - async.eachSeries(Object.keys(records), function(key, cb){ - var value = records[key] + async.eachSeries(Object.keys(records), function(key, cb) { + var value = records[key]; - if(!value){ + if (!value) { return setImmediate(cb); } - // Author data - if(value.padIDs){ - // rewrite author pad ids + if (value.padIDs) { + // Author data - rewrite author pad ids value.padIDs[padId] = 1; var newKey = key; // Does this author already exist? - db.get(key, function(err, author){ - if(author){ - // Yes, add the padID to the author.. - if( Object.prototype.toString.call(author) === '[object Array]'){ + db.get(key, function(err, author) { + if (author) { + // Yes, add the padID to the author + if (Object.prototype.toString.call(author) === '[object Array]') { author.padIDs.push(padId); } value = author; - }else{ + } else { // No, create a new array with the author info in value.padIDs = [padId]; } }); - - // Not author data, probably pad data - }else{ - // we can split it to look to see if its pad data + } else { + // Not author data, probably pad data + // we can split it to look to see if it's pad data var oldPadId = key.split(":"); - // we know its pad data.. - if(oldPadId[0] === "pad"){ - + // we know it's pad data + if (oldPadId[0] === "pad") { // so set the new pad id for the author oldPadId[1] = padId; - + // and create the value var newKey = oldPadId.join(":"); // create the new key } - } + // Write the value to the server db.set(newKey, value); setImmediate(cb); - }, function(){ + }, + function() { callback(null, true); }); } diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index d71e27201..04037eab5 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -36,19 +36,22 @@ function setPadHTML(pad, html, callback) // Convert a dom tree into a list of lines and attribute liens // using the content collector object var cc = contentcollector.makeContentCollector(true, null, pad.pool); - try{ // we use a try here because if the HTML is bad it will blow up + try { + // we use a try here because if the HTML is bad it will blow up cc.collectContent(doc); - }catch(e){ + } catch(e) { apiLogger.warn("HTML was not properly formed", e); - return callback(e); // We don't process the HTML because it was bad.. + + // don't process the HTML because it was bad + return callback(e); } var result = cc.finish(); apiLogger.debug('Lines:'); + var i; - for (i = 0; i < result.lines.length; i += 1) - { + for (i = 0; i < result.lines.length; i += 1) { apiLogger.debug('Line ' + (i + 1) + ' text: ' + result.lines[i]); apiLogger.debug('Line ' + (i + 1) + ' attributes: ' + result.lineAttribs[i]); } @@ -59,18 +62,15 @@ function setPadHTML(pad, html, callback) apiLogger.debug(newText); var newAttribs = result.lineAttribs.join('|1+1') + '|1+1'; - function eachAttribRun(attribs, func /*(startInNewText, endInNewText, attribs)*/ ) - { + function eachAttribRun(attribs, func /*(startInNewText, endInNewText, attribs)*/ ) { var attribsIter = Changeset.opIterator(attribs); var textIndex = 0; var newTextStart = 0; var newTextEnd = newText.length; - while (attribsIter.hasNext()) - { + while (attribsIter.hasNext()) { var op = attribsIter.next(); var nextIndex = textIndex + op.chars; - if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) - { + if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); } textIndex = nextIndex; @@ -81,13 +81,13 @@ function setPadHTML(pad, html, callback) var builder = Changeset.builder(1); // assemble each line into the builder - eachAttribRun(newAttribs, function(start, end, attribs) - { + eachAttribRun(newAttribs, function(start, end, attribs) { builder.insert(newText.substring(start, end), attribs); }); // the changeset is ready! var theChangeset = builder.toString(); + apiLogger.debug('The changeset: ' + theChangeset); pad.setText("\n"); pad.appendRevision(theChangeset); diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js index 24d5bb0c2..33938b19c 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.js @@ -1,336 +1,348 @@ var Changeset = require("../../static/js/Changeset"); var async = require("async"); var exportHtml = require('./ExportHtml'); - -function PadDiff (pad, fromRev, toRev){ - //check parameters - if(!pad || !pad.id || !pad.atext || !pad.pool) - { + +function PadDiff (pad, fromRev, toRev) { + // check parameters + if (!pad || !pad.id || !pad.atext || !pad.pool) { throw new Error('Invalid pad'); } - + var range = pad.getValidRevisionRange(fromRev, toRev); - if(!range) { throw new Error('Invalid revision range.' + + if (!range) { + throw new Error('Invalid revision range.' + ' startRev: ' + fromRev + - ' endRev: ' + toRev); } - + ' endRev: ' + toRev); + } + this._pad = pad; this._fromRev = range.startRev; this._toRev = range.endRev; this._html = null; this._authors = []; } - -PadDiff.prototype._isClearAuthorship = function(changeset){ - //unpack + +PadDiff.prototype._isClearAuthorship = function(changeset) { + // unpack var unpacked = Changeset.unpack(changeset); - - //check if there is nothing in the charBank - if(unpacked.charBank !== "") + + // check if there is nothing in the charBank + if (unpacked.charBank !== "") { return false; - - //check if oldLength == newLength - if(unpacked.oldLen !== unpacked.newLen) + } + + // check if oldLength == newLength + if (unpacked.oldLen !== unpacked.newLen) { return false; - - //lets iterator over the operators + } + + // lets iterator over the operators var iterator = Changeset.opIterator(unpacked.ops); - - //get the first operator, this should be a clear operator + + // get the first operator, this should be a clear operator var clearOperator = iterator.next(); - - //check if there is only one operator - if(iterator.hasNext() === true) + + // check if there is only one operator + if (iterator.hasNext() === true) { return false; - - //check if this operator doesn't change text - if(clearOperator.opcode !== "=") + } + + // check if this operator doesn't change text + if (clearOperator.opcode !== "=") { return false; - - //check that this operator applys to the complete text - //if the text ends with a new line, its exactly one character less, else it has the same length - if(clearOperator.chars !== unpacked.oldLen-1 && clearOperator.chars !== unpacked.oldLen) + } + + // check that this operator applys to the complete text + // if the text ends with a new line, its exactly one character less, else it has the same length + if (clearOperator.chars !== unpacked.oldLen-1 && clearOperator.chars !== unpacked.oldLen) { return false; - + } + var attributes = []; - Changeset.eachAttribNumber(changeset, function(attrNum){ + Changeset.eachAttribNumber(changeset, function(attrNum) { attributes.push(attrNum); }); - - //check that this changeset uses only one attribute - if(attributes.length !== 1) + + // check that this changeset uses only one attribute + if (attributes.length !== 1) { return false; - + } + var appliedAttribute = this._pad.pool.getAttrib(attributes[0]); - - //check if the applied attribute is an anonymous author attribute - if(appliedAttribute[0] !== "author" || appliedAttribute[1] !== "") + + // check if the applied attribute is an anonymous author attribute + if (appliedAttribute[0] !== "author" || appliedAttribute[1] !== "") { return false; - + } + return true; }; - -PadDiff.prototype._createClearAuthorship = function(rev, callback){ + +PadDiff.prototype._createClearAuthorship = function(rev, callback) { var self = this; - this._pad.getInternalRevisionAText(rev, function(err, atext){ - if(err){ - return callback(err); - } - - //build clearAuthorship changeset + this._pad.getInternalRevisionAText(rev, function(err, atext) { + if (err) { + return callback(err); + } + + // build clearAuthorship changeset var builder = Changeset.builder(atext.text.length); builder.keepText(atext.text, [['author','']], self._pad.pool); var changeset = builder.toString(); - + callback(null, changeset); }); }; - -PadDiff.prototype._createClearStartAtext = function(rev, callback){ + +PadDiff.prototype._createClearStartAtext = function(rev, callback) { var self = this; - - //get the atext of this revision - this._pad.getInternalRevisionAText(rev, function(err, atext){ - if(err){ - return callback(err); - } - - //create the clearAuthorship changeset - self._createClearAuthorship(rev, function(err, changeset){ - if(err){ - return callback(err); - } - - try { - //apply the clearAuthorship changeset - var newAText = Changeset.applyToAText(changeset, atext, self._pad.pool); - } catch(err) { - return callback(err) - } - - callback(null, newAText); - }); - }); -}; - -PadDiff.prototype._getChangesetsInBulk = function(startRev, count, callback) { - var self = this; - - //find out which revisions we need - var revisions = []; - for(var i=startRev;i<(startRev+count) && i<=this._pad.head;i++){ - revisions.push(i); - } - - var changesets = [], authors = []; - - //get all needed revisions - async.forEach(revisions, function(rev, callback){ - self._pad.getRevision(rev, function(err, revision){ - if(err){ + + // get the atext of this revision + this._pad.getInternalRevisionAText(rev, function(err, atext) { + if (err) { + return callback(err); + } + + // create the clearAuthorship changeset + self._createClearAuthorship(rev, function(err, changeset) { + if (err) { return callback(err); } - + + try { + // apply the clearAuthorship changeset + var newAText = Changeset.applyToAText(changeset, atext, self._pad.pool); + } catch(err) { + return callback(err) + } + + callback(null, newAText); + }); + }); +}; + +PadDiff.prototype._getChangesetsInBulk = function(startRev, count, callback) { + var self = this; + + // find out which revisions we need + var revisions = []; + for (var i = startRev; i < (startRev + count) && i <= this._pad.head; i++) { + revisions.push(i); + } + + var changesets = [], authors = []; + + // get all needed revisions + async.forEach(revisions, function(rev, callback) { + self._pad.getRevision(rev, function(err, revision) { + if (err) { + return callback(err); + } + var arrayNum = rev-startRev; - + changesets[arrayNum] = revision.changeset; authors[arrayNum] = revision.meta.author; - + callback(); }); - }, function(err){ + }, + function(err) { callback(err, changesets, authors); }); }; - + PadDiff.prototype._addAuthors = function(authors) { var self = this; - //add to array if not in the array - authors.forEach(function(author){ - if(self._authors.indexOf(author) == -1){ + + // add to array if not in the array + authors.forEach(function(author) { + if (self._authors.indexOf(author) == -1) { self._authors.push(author); } }); }; - + PadDiff.prototype._createDiffAtext = function(callback) { var self = this; var bulkSize = 100; - - //get the cleaned startAText - self._createClearStartAtext(self._fromRev, function(err, atext){ - if(err) { return callback(err); } - + + // get the cleaned startAText + self._createClearStartAtext(self._fromRev, function(err, atext) { + if (err) { return callback(err); } + var superChangeset = null; - + var rev = self._fromRev + 1; - - //async while loop + + // async while loop async.whilst( - //loop condition + // loop condition function () { return rev <= self._toRev; }, - - //loop body + + // loop body function (callback) { - //get the bulk - self._getChangesetsInBulk(rev,bulkSize,function(err, changesets, authors){ + // get the bulk + self._getChangesetsInBulk(rev,bulkSize,function(err, changesets, authors) { var addedAuthors = []; - - //run trough all changesets - for(var i=0;i 0) { if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) { curLine++; @@ -384,22 +396,25 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { curLineNextOp.chars = 0; curLineOpIter = Changeset.opIterator(alines_get(curLine)); } + if (!curLineNextOp.chars) { curLineOpIter.next(curLineNextOp); } + var charsToUse = Math.min(numChars, curLineNextOp.chars); + func(charsToUse, curLineNextOp.attribs, charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0); numChars -= charsToUse; curLineNextOp.chars -= charsToUse; curChar += charsToUse; } - + if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) { curLine++; curChar = 0; } } - + function skip(N, L) { if (L) { curLine += L; @@ -412,27 +427,29 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { } } } - + function nextText(numChars) { var len = 0; var assem = Changeset.stringAssembler(); var firstString = lines_get(curLine).substring(curChar); len += firstString.length; assem.append(firstString); - + var lineNum = curLine + 1; + while (len < numChars) { var nextString = lines_get(lineNum); len += nextString.length; assem.append(nextString); lineNum++; } - + return assem.toString().substring(0, numChars); } - + function cachedStrFunc(func) { var cache = {}; + return function (s) { if (!cache[s]) { cache[s] = func(s); @@ -440,57 +457,59 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { return cache[s]; }; } - + var attribKeys = []; var attribValues = []; - - //iterate over all operators of this changeset + + // iterate over all operators of this changeset while (csIter.hasNext()) { var csOp = csIter.next(); - - if (csOp.opcode == '=') { + + if (csOp.opcode == '=') { var textBank = nextText(csOp.chars); - + // decide if this equal operator is an attribution change or not. We can see this by checkinf if attribs is set. // If the text this operator applies to is only a star, than this is a false positive and should be ignored if (csOp.attribs && textBank != "*") { var deletedAttrib = apool.putAttrib(["removed", true]); var authorAttrib = apool.putAttrib(["author", ""]); - + attribKeys.length = 0; attribValues.length = 0; Changeset.eachAttribNumber(csOp.attribs, function (n) { attribKeys.push(apool.getAttribKey(n)); attribValues.push(apool.getAttribValue(n)); - - if(apool.getAttribKey(n) === "author"){ + + if (apool.getAttribKey(n) === "author") { authorAttrib = n; } }); - + var undoBackToAttribs = cachedStrFunc(function (attribs) { var backAttribs = []; for (var i = 0; i < attribKeys.length; i++) { var appliedKey = attribKeys[i]; var appliedValue = attribValues[i]; var oldValue = Changeset.attribsAttributeValue(attribs, appliedKey, apool); + if (appliedValue != oldValue) { backAttribs.push([appliedKey, oldValue]); } } + return Changeset.makeAttribsString('=', backAttribs, apool); }); - + var oldAttribsAddition = "*" + Changeset.numToString(deletedAttrib) + "*" + Changeset.numToString(authorAttrib); - + var textLeftToProcess = textBank; - - while(textLeftToProcess.length > 0){ - //process till the next line break or process only one line break + + while(textLeftToProcess.length > 0) { + // process till the next line break or process only one line break var lengthToProcess = textLeftToProcess.indexOf("\n"); var lineBreak = false; - switch(lengthToProcess){ - case -1: + switch(lengthToProcess) { + case -1: lengthToProcess=textLeftToProcess.length; break; case 0: @@ -498,27 +517,28 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { lengthToProcess=1; break; } - - //get the text we want to procceed in this step + + // get the text we want to procceed in this step var processText = textLeftToProcess.substr(0, lengthToProcess); + textLeftToProcess = textLeftToProcess.substr(lengthToProcess); - - if(lineBreak){ - builder.keep(1, 1); //just skip linebreaks, don't do a insert + keep for a linebreak - - //consume the attributes of this linebreak - consumeAttribRuns(1, function(){}); + + if (lineBreak) { + builder.keep(1, 1); // just skip linebreaks, don't do a insert + keep for a linebreak + + // consume the attributes of this linebreak + consumeAttribRuns(1, function() {}); } else { - //add the old text via an insert, but add a deletion attribute + the author attribute of the author who deleted it + // add the old text via an insert, but add a deletion attribute + the author attribute of the author who deleted it var textBankIndex = 0; consumeAttribRuns(lengthToProcess, function (len, attribs, endsLine) { - //get the old attributes back + // get the old attributes back var attribs = (undoBackToAttribs(attribs) || "") + oldAttribsAddition; - + builder.insert(processText.substr(textBankIndex, len), attribs); textBankIndex += len; }); - + builder.keep(lengthToProcess, 0); } } @@ -531,16 +551,16 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { } else if (csOp.opcode == '-') { var textBank = nextText(csOp.chars); var textBankIndex = 0; - + consumeAttribRuns(csOp.chars, function (len, attribs, endsLine) { builder.insert(textBank.substr(textBankIndex, len), attribs + csOp.attribs); textBankIndex += len; }); } } - + return Changeset.checkRep(builder.toString()); }; - -//export the constructor + +// export the constructor module.exports = PadDiff; diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js index cd2ed3305..fbd0e1d97 100644 --- a/src/static/js/pluginfw/installer.js +++ b/src/static/js/pluginfw/installer.js @@ -4,12 +4,14 @@ var npm = require("npm"); var request = require("request"); var npmIsLoaded = false; -var withNpm = function (npmfn) { - if(npmIsLoaded) return npmfn(); - npm.load({}, function (er) { +var withNpm = function(npmfn) { + if (npmIsLoaded) return npmfn(); + + npm.load({}, function(er) { if (er) return npmfn(er); + npmIsLoaded = true; - npm.on("log", function (message) { + npm.on("log", function(message) { console.log('npm: ',message) }); npmfn(); @@ -17,26 +19,33 @@ var withNpm = function (npmfn) { } var tasks = 0 + function wrapTaskCb(cb) { - tasks++ + tasks++; + return function() { cb && cb.apply(this, arguments); tasks--; - if(tasks == 0) onAllTasksFinished(); + if (tasks == 0) onAllTasksFinished(); } } + function onAllTasksFinished() { - hooks.aCallAll("restartServer", {}, function () {}); + hooks.aCallAll("restartServer", {}, function() {}); } exports.uninstall = function(plugin_name, cb) { cb = wrapTaskCb(cb); - withNpm(function (er) { + + withNpm(function(er) { if (er) return cb && cb(er); - npm.commands.uninstall([plugin_name], function (er) { + + npm.commands.uninstall([plugin_name], function(er) { if (er) return cb && cb(er); - hooks.aCallAll("pluginUninstall", {plugin_name: plugin_name}, function (er, data) { + + hooks.aCallAll("pluginUninstall", {plugin_name: plugin_name}, function(er, data) { if (er) return cb(er); + plugins.update(cb); }); }); @@ -44,13 +53,17 @@ exports.uninstall = function(plugin_name, cb) { }; exports.install = function(plugin_name, cb) { - cb = wrapTaskCb(cb) - withNpm(function (er) { + cb = wrapTaskCb(cb); + + withNpm(function(er) { if (er) return cb && cb(er); - npm.commands.install([plugin_name], function (er) { + + npm.commands.install([plugin_name], function(er) { if (er) return cb && cb(er); - hooks.aCallAll("pluginInstall", {plugin_name: plugin_name}, function (er, data) { + + hooks.aCallAll("pluginInstall", {plugin_name: plugin_name}, function(er, data) { if (er) return cb(er); + plugins.update(cb); }); }); @@ -63,41 +76,53 @@ var cacheTimestamp = 0; exports.getAvailablePlugins = function(maxCacheAge, cb) { request("https://static.etherpad.org/plugins.json", function(er, response, plugins){ if (er) return cb && cb(er); - if(exports.availablePlugins && maxCacheAge && Math.round(+new Date/1000)-cacheTimestamp <= maxCacheAge) { - return cb && cb(null, exports.availablePlugins) + + if (exports.availablePlugins && maxCacheAge && Math.round(+ new Date / 1000) - cacheTimestamp <= maxCacheAge) { + return cb && cb(null, exports.availablePlugins); } + try { plugins = JSON.parse(plugins); } catch (err) { console.error('error parsing plugins.json:', err); plugins = []; } + exports.availablePlugins = plugins; - cacheTimestamp = Math.round(+new Date/1000); - cb && cb(null, plugins) + cacheTimestamp = Math.round(+ new Date / 1000); + + cb && cb(null, plugins); }); }; exports.search = function(searchTerm, maxCacheAge, cb) { exports.getAvailablePlugins(maxCacheAge, function(er, results) { - if(er) return cb && cb(er); + if (er) return cb && cb(er); + var res = {}; - if (searchTerm) + + if (searchTerm) { searchTerm = searchTerm.toLowerCase(); - for (var pluginName in results) { // for every available plugin + } + + for (var pluginName in results) { + // for every available plugin if (pluginName.indexOf(plugins.prefix) != 0) continue; // TODO: Also search in keywords here! - if(searchTerm && !~results[pluginName].name.toLowerCase().indexOf(searchTerm) + if (searchTerm && !~results[pluginName].name.toLowerCase().indexOf(searchTerm) && (typeof results[pluginName].description != "undefined" && !~results[pluginName].description.toLowerCase().indexOf(searchTerm) ) - ){ - if(typeof results[pluginName].description === "undefined"){ + ) { + if (typeof results[pluginName].description === "undefined") { console.debug('plugin without Description: %s', results[pluginName].name); } + continue; } + res[pluginName] = results[pluginName]; } - cb && cb(null, res) - }) + + cb && cb(null, res); + }); }; diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js index 7cb4b1bdc..38d6b68c0 100644 --- a/src/static/js/pluginfw/plugins.js +++ b/src/static/js/pluginfw/plugins.js @@ -55,6 +55,7 @@ exports.formatHooks = function (hook_set_name) { exports.callInit = function (cb) { var hooks = require("./hooks"); + async.map( Object.keys(exports.plugins), function (plugin_name, cb) { @@ -83,6 +84,7 @@ exports.update = function (cb) { exports.getPackages(function (er, packages) { var parts = []; var plugins = {}; + // Load plugin metadata ep.json async.forEach( Object.keys(packages), @@ -106,6 +108,7 @@ exports.getPackages = function (cb) { var dir = path.resolve(npm.dir, '..'); readInstalled(dir, function (er, data) { if (er) cb(er, null); + var packages = {}; function flatten(deps) { _.chain(deps).keys().each(function (name) { @@ -116,12 +119,12 @@ exports.getPackages = function (cb) { delete packages[name].dependencies; delete packages[name].parent; } - + // I don't think we need recursion //if (deps[name].dependencies !== undefined) flatten(deps[name].dependencies); }); } - + var tmp = {}; tmp[data.name] = data; flatten(tmp[data.name].dependencies); From e84179831413cd0feee6e1ba0ae80e9826754a5a Mon Sep 17 00:00:00 2001 From: muxator Date: Fri, 15 Feb 2019 22:52:53 +0100 Subject: [PATCH 002/183] prepare to async: typos in error messages This change extracts the grammar correction performed on the async branch, anticipating them in a single commit. It cannot be folded with the previous one, as it is not purely cosmetic. --- src/node/db/API.js | 14 +++++++------- src/node/db/GroupManager.js | 2 +- src/node/db/SessionManager.js | 2 +- src/node/handler/ImportHandler.js | 2 +- src/node/handler/PadMessageHandler.js | 2 +- src/node/handler/SocketIORouter.js | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/node/db/API.js b/src/node/db/API.js index edae1ba32..a2c0d60b7 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -204,7 +204,7 @@ exports.getText = function(padID, rev, callback) // ensure this is not a negative number if (rev !== undefined && rev < 0) { - callback(new customError("rev is a negativ number", "apierror")); + callback(new customError("rev is a negative number", "apierror")); return; } @@ -257,7 +257,7 @@ exports.setText = function(padID, text, callback) { // text is required if (typeof text != "string") { - callback(new customError("text is no string", "apierror")); + callback(new customError("text is not a string", "apierror")); return; } @@ -286,7 +286,7 @@ exports.appendText = function(padID, text, callback) { // text is required if (typeof text != "string") { - callback(new customError("text is no string", "apierror")); + callback(new customError("text is not a string", "apierror")); return; } @@ -376,7 +376,7 @@ exports.setHTML = function(padID, html, callback) { // html is required if (typeof html != "string") { - callback(new customError("html is no string", "apierror")); + callback(new customError("html is not a string", "apierror")); return; } @@ -472,7 +472,7 @@ exports.appendChatMessage = function(padID, text, authorID, time, callback) { // text is required if (typeof text != "string") { - callback(new customError("text is no string", "apierror")); + callback(new customError("text is not a string", "apierror")); return; } @@ -569,7 +569,7 @@ exports.saveRevision = function(padID, rev, callback) // ensure this is not a negative number if (rev !== undefined && rev < 0) { - callback(new customError("rev is a negativ number", "apierror")); + callback(new customError("rev is a negative number", "apierror")); return; } @@ -694,7 +694,7 @@ exports.restoreRevision = function(padID, rev, callback) // ensure this is not a negative number if (rev !== undefined && rev < 0) { - callback(new customError("rev is a negativ number", "apierror")); + callback(new customError("rev is a negative number", "apierror")); return; } diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.js index 5f8183ea9..f384f70aa 100644 --- a/src/node/db/GroupManager.js +++ b/src/node/db/GroupManager.js @@ -199,7 +199,7 @@ exports.createGroupIfNotExistsFor = function(groupMapper, callback) { // ensure mapper is optional if (typeof groupMapper != "string") { - callback(new customError("groupMapper is no string", "apierror")); + callback(new customError("groupMapper is not a string", "apierror")); return; } diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index 6b580c033..e039ee459 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -95,7 +95,7 @@ exports.createSession = function(groupID, authorID, validUntil, callback) // ensure this is not a negative number if (validUntil < 0) { - callback(new customError("validUntil is a negativ number", "apierror")); + callback(new customError("validUntil is a negative number", "apierror")); return; } diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index dae71bf6d..ec07bf6e5 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -151,7 +151,7 @@ exports.doImport = function(req, res, padId) padManager.getPad(padId, function(err, _pad) { var headCount = _pad.head; if (headCount >= 10) { - apiLogger.warn("Direct database Import attempt of a pad that already has content, we wont be doing this"); + apiLogger.warn("Direct database Import attempt of a pad that already has content, we won't be doing this"); return callback("padHasData"); } diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 5c20ed1b6..e32d0e61e 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -457,7 +457,7 @@ function handleGetChatMessages(client, message) var count = end - start; if (count < 0 || count > 100) { - messageLogger.warn("Dropped message, GetChatMessages Message, client requested invalid amout of messages!"); + messageLogger.warn("Dropped message, GetChatMessages Message, client requested invalid amount of messages!"); return; } diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index c1e78a910..a94080b9d 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -116,7 +116,7 @@ exports.setSocketIO = function(_socket) { } } else { // drop message - messageLogger.warn("Dropped message cause of bad permissions:" + stringifyWithoutPassword(message)); + messageLogger.warn("Dropped message because of bad permissions:" + stringifyWithoutPassword(message)); } } }); From 11453d544c2a47b562004e383106599226c51e87 Mon Sep 17 00:00:00 2001 From: muxator Date: Fri, 1 Mar 2019 09:43:41 +0100 Subject: [PATCH 003/183] prepare to async: stricter checks This change is in preparation of the future async refactoring by Ray. It tries to extract as many changes in boolean conditions as possible, in order to make more evident identifying eventual logic bugs in the future work. This proved already useful in at least one case. BEWARE: this commit exposes an incoherency in the DB API, in which, depending on the driver used, some functions can return null or undefined. This condition will be externally fixed by the final commit in this series ("db/DB.js: prevent DB layer from returning undefined"). Until that commit, the code base may have some bugs. --- src/node/db/API.js | 32 +++++++++++++++--------------- src/node/db/AuthorManager.js | 12 +++++------ src/node/db/GroupManager.js | 8 ++++---- src/node/db/SecurityManager.js | 14 ++++++------- src/node/handler/APIHandler.js | 2 +- src/node/handler/SocketIORouter.js | 2 +- src/node/padaccess.js | 2 +- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/node/db/API.js b/src/node/db/API.js index a2c0d60b7..b6acefcd4 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -192,7 +192,7 @@ Example returns: exports.getText = function(padID, rev, callback) { // check if rev is a number - if (rev !== undefined && typeof rev != "number") { + if (rev !== undefined && typeof rev !== "number") { // try to parse the number if (isNaN(parseInt(rev))) { callback(new customError("rev is not a number", "apierror")); @@ -256,7 +256,7 @@ Example returns: exports.setText = function(padID, text, callback) { // text is required - if (typeof text != "string") { + if (typeof text !== "string") { callback(new customError("text is not a string", "apierror")); return; } @@ -285,7 +285,7 @@ Example returns: exports.appendText = function(padID, text, callback) { // text is required - if (typeof text != "string") { + if (typeof text !== "string") { callback(new customError("text is not a string", "apierror")); return; } @@ -311,7 +311,7 @@ Example returns: */ exports.getHTML = function(padID, rev, callback) { - if (rev !== undefined && typeof rev != "number") { + if (rev !== undefined && typeof rev !== "number") { if (isNaN(parseInt(rev))) { callback(new customError("rev is not a number", "apierror")); return; @@ -375,7 +375,7 @@ Example returns: exports.setHTML = function(padID, html, callback) { // html is required - if (typeof html != "string") { + if (typeof html !== "string") { callback(new customError("html is not a string", "apierror")); return; } @@ -471,7 +471,7 @@ Example returns: exports.appendChatMessage = function(padID, text, authorID, time, callback) { // text is required - if (typeof text != "string") { + if (typeof text !== "string") { callback(new customError("text is not a string", "apierror")); return; } @@ -557,7 +557,7 @@ Example returns: exports.saveRevision = function(padID, rev, callback) { // check if rev is a number - if (rev !== undefined && typeof rev != "number") { + if (rev !== undefined && typeof rev !== "number") { // try to parse the number if (isNaN(parseInt(rev))) { callback(new customError("rev is not a number", "apierror")); @@ -636,7 +636,7 @@ exports.createPad = function(padID, text, callback) { if (padID) { // ensure there is no $ in the padID - if (padID.indexOf("$") != -1) { + if (padID.indexOf("$") !== -1) { callback(new customError("createPad can't create group pads", "apierror")); return; } @@ -682,7 +682,7 @@ exports.deletePad = function(padID, callback) exports.restoreRevision = function(padID, rev, callback) { // check if rev is a number - if (rev !== undefined && typeof rev != "number") { + if (rev !== undefined && typeof rev !== "number") { // try to parse the number if (isNaN(parseInt(rev))) { callback(new customError("rev is not a number", "apierror")); @@ -838,7 +838,7 @@ exports.getPadID = function(roID, callback) readOnlyManager.getPadId(roID, function(err, retrievedPadID) { if (ERR(err, callback)) return; - if (retrievedPadID == null) { + if (retrievedPadID === null) { callback(new customError("padID does not exist", "apierror")); return; } @@ -858,7 +858,7 @@ Example returns: exports.setPublicStatus = function(padID, publicStatus, callback) { // ensure this is a group pad - if (padID && padID.indexOf("$") == -1) { + if (padID && padID.indexOf("$") === -1) { callback(new customError("You can only get/set the publicStatus of pads that belong to a group", "apierror")); return; } @@ -868,7 +868,7 @@ exports.setPublicStatus = function(padID, publicStatus, callback) if (ERR(err, callback)) return; // convert string to boolean - if (typeof publicStatus == "string") + if (typeof publicStatus === "string") publicStatus = publicStatus == "true" ? true : false; // set the password @@ -1045,7 +1045,7 @@ Example returns: */ exports.createDiffHTML = function(padID, startRev, endRev, callback) { // check if startRev is a number - if (startRev !== undefined && typeof startRev != "number") { + if (startRev !== undefined && typeof startRev !== "number") { // try to parse the number if (isNaN(parseInt(startRev))) { callback({stop: "startRev is not a number"}); @@ -1056,7 +1056,7 @@ exports.createDiffHTML = function(padID, startRev, endRev, callback) { } // check if endRev is a number - if (endRev !== undefined && typeof endRev != "number") { + if (endRev !== undefined && typeof endRev !== "number") { // try to parse the number if (isNaN(parseInt(endRev))) { callback({stop: "endRev is not a number"}); @@ -1119,13 +1119,13 @@ function is_int(value) // gets a pad safe function getPadSafe(padID, shouldExist, text, callback) { - if (typeof text == "function") { + if (typeof text === "function") { callback = text; text = null; } // check if padID is a string - if (typeof padID != "string") { + if (typeof padID !== "string") { callback(new customError("padID is not a string", "apierror")); return; } diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index 818eaab28..d04c4ca32 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -45,7 +45,7 @@ exports.doesAuthorExists = function(authorID, callback) db.get("globalAuthor:" + authorID, function(err, author) { if (ERR(err, callback)) return; - callback(null, author != null); + callback(null, author !== null); }); } @@ -98,7 +98,7 @@ function mapAuthorWithDBKey (mapperkey, mapper, callback) db.get(mapperkey + ":" + mapper, function(err, author) { if (ERR(err, callback)) return; - if (author == null) { + if (author === null) { // there is no author with this mapper, so create one exports.createAuthor(null, function(err, author) { if (ERR(err, callback)) return; @@ -212,7 +212,7 @@ exports.listPadsOfAuthor = function(authorID, callback) db.get("globalAuthor:" + authorID, function(err, author) { if (ERR(err, callback)) return; - if (author == null) { + if (author === null) { // author does not exist callback(new customError("authorID does not exist", "apierror")); @@ -242,7 +242,7 @@ exports.addPad = function(authorID, padID) // get the entry db.get("globalAuthor:" + authorID, function(err, author) { if (ERR(err)) return; - if (author == null) return; + if (author === null) return; if (author.padIDs == null) { // the entry doesn't exist so far, let's create it @@ -266,9 +266,9 @@ exports.removePad = function(authorID, padID) { db.get("globalAuthor:" + authorID, function(err, author) { if (ERR(err)) return; - if (author == null) return; + if (author === null) return; - if (author.padIDs != null) { + if (author.padIDs !== null) { // remove pad from author delete author.padIDs[padID]; db.set("globalAuthor:" + authorID, author); diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.js index f384f70aa..985883203 100644 --- a/src/node/db/GroupManager.js +++ b/src/node/db/GroupManager.js @@ -122,7 +122,7 @@ exports.deleteGroup = function(groupID, callback) if (ERR(err, callback)) return; groups = groups? groups.groupIDs : []; - if (groups.indexOf(groupID) == -1) { + if (groups.indexOf(groupID) === -1) { // it's not listed callback(); @@ -198,7 +198,7 @@ exports.createGroup = function(callback) exports.createGroupIfNotExistsFor = function(groupMapper, callback) { // ensure mapper is optional - if (typeof groupMapper != "string") { + if (typeof groupMapper !== "string") { callback(new customError("groupMapper is not a string", "apierror")); return; } @@ -248,7 +248,7 @@ exports.createGroupPad = function(groupID, padName, text, callback) exports.doesGroupExist(groupID, function(err, exists) { if (ERR(err, callback)) return; - if (exists == false) { + if (!exists) { // group does not exist callback(new customError("groupID does not exist", "apierror")); return; @@ -303,7 +303,7 @@ exports.listPads = function(groupID, callback) if (ERR(err, callback)) return; // ensure the group exists - if (exists == false) { + if (!exists) { callback(new customError("groupID does not exist", "apierror")); return; } diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 117f2794f..bc8f59426 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -62,7 +62,7 @@ exports.checkAccess = function(padID, sessionCookie, token, password, callback) } } else { // a session is not required, so we'll check if it's a public pad - if (padID.indexOf("$") == -1) { + if (padID.indexOf("$") === -1) { // it's not a group pad, means we can grant access // get author for this token @@ -225,17 +225,17 @@ exports.checkAccess = function(padID, sessionCookie, token, password, callback) // --> grant access statusObject = { accessStatus: "grant", authorID: sessionAuthor }; - } else if (isPasswordProtected && passwordStatus == "correct") { + } else if (isPasswordProtected && passwordStatus === "correct") { // - the pad is password protected and password is correct // --> grant access statusObject = { accessStatus: "grant", authorID: sessionAuthor }; - } else if (isPasswordProtected && passwordStatus == "wrong") { + } else if (isPasswordProtected && passwordStatus === "wrong") { // - the pad is password protected but wrong password given // --> deny access, ask for new password and tell them that the password is wrong statusObject = { accessStatus: "wrongPassword" }; - } else if (isPasswordProtected && passwordStatus == "notGiven") { + } else if (isPasswordProtected && passwordStatus === "notGiven") { // - the pad is password protected but no password given // --> ask for password @@ -261,17 +261,17 @@ exports.checkAccess = function(padID, sessionCookie, token, password, callback) if (isPublic && !isPasswordProtected) { // --> grant access, with author of token statusObject = {accessStatus: "grant", authorID: tokenAuthor}; - } else if (isPublic && isPasswordProtected && passwordStatus == "correct") { + } else if (isPublic && isPasswordProtected && passwordStatus === "correct") { // - it's public and password protected and password is correct // --> grant access, with author of token statusObject = {accessStatus: "grant", authorID: tokenAuthor}; - } else if (isPublic && isPasswordProtected && passwordStatus == "wrong") { + } else if (isPublic && isPasswordProtected && passwordStatus === "wrong") { // - it's public and the pad is password protected but wrong password given // --> deny access, ask for new password and tell them that the password is wrong statusObject = {accessStatus: "wrongPassword"}; - } else if (isPublic && isPasswordProtected && passwordStatus == "notGiven") { + } else if (isPublic && isPasswordProtected && passwordStatus === "notGiven") { // - it's public and the pad is password protected but no password given // --> ask for password diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index 3ed7aa4e1..5d731ddc7 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -188,7 +188,7 @@ exports.handle = function(apiVersion, functionName, fields, req, res) // check the api key! fields["apikey"] = fields["apikey"] || fields["api_key"]; - if (fields["apikey"] != apikey.trim()) { + if (fields["apikey"] !== apikey.trim()) { res.statusCode = 401; res.send({code: 4, message: "no or wrong API Key", data: null}); return; diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index a94080b9d..08f9f47e8 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -95,7 +95,7 @@ exports.setSocketIO = function(_socket) { var checkAccessCallback = function(err, statusObject) { ERR(err); - if (statusObject.accessStatus == "grant") { + if (statusObject.accessStatus === "grant") { // access was granted, mark the client as authorized and handle the message clientAuthorized = true; handleMessage(client, message); diff --git a/src/node/padaccess.js b/src/node/padaccess.js index c8c888cd0..a25ad642c 100644 --- a/src/node/padaccess.js +++ b/src/node/padaccess.js @@ -6,7 +6,7 @@ module.exports = function (req, res, callback) { securityManager.checkAccess(req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password, function(err, accessObj) { if (ERR(err, callback)) return; - if (accessObj.accessStatus == "grant") { + if (accessObj.accessStatus === "grant") { // there is access, continue callback(); } else { From 98993fe15620f50d454c85597116dbd1c90fd044 Mon Sep 17 00:00:00 2001 From: muxator Date: Sat, 9 Feb 2019 02:18:36 +0100 Subject: [PATCH 004/183] db/SessionManager.js: "groupMangager" -> "groupManager" Extracted from Ray's work. --- src/node/db/SessionManager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index e039ee459..5016cf242 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -23,7 +23,7 @@ var customError = require("../utils/customError"); var randomString = require("../utils/randomstring"); var db = require("./DB").db; var async = require("async"); -var groupMangager = require("./GroupManager"); +var groupManager = require("./GroupManager"); var authorMangager = require("./AuthorManager"); exports.doesSessionExist = function(sessionID, callback) @@ -47,7 +47,7 @@ exports.createSession = function(groupID, authorID, validUntil, callback) // check if the group exists function(callback) { - groupMangager.doesGroupExist(groupID, function(err, exists) + groupManager.doesGroupExist(groupID, function(err, exists) { if(ERR(err, callback)) return; @@ -271,7 +271,7 @@ exports.deleteSession = function(sessionID, callback) exports.listSessionsOfGroup = function(groupID, callback) { - groupMangager.doesGroupExist(groupID, function(err, exists) + groupManager.doesGroupExist(groupID, function(err, exists) { if(ERR(err, callback)) return; From b0846ded149a708ddfb5d1d45c4b8c649f9fe626 Mon Sep 17 00:00:00 2001 From: muxator Date: Sat, 9 Feb 2019 02:19:14 +0100 Subject: [PATCH 005/183] db/SessionManager.js: "authorMangager" -> "authorManager" Extracted from Ray's work. --- src/node/db/SessionManager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index 5016cf242..28ad4e036 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -24,7 +24,7 @@ var randomString = require("../utils/randomstring"); var db = require("./DB").db; var async = require("async"); var groupManager = require("./GroupManager"); -var authorMangager = require("./AuthorManager"); +var authorManager = require("./AuthorManager"); exports.doesSessionExist = function(sessionID, callback) { @@ -64,7 +64,7 @@ exports.createSession = function(groupID, authorID, validUntil, callback) // check if the author exists function(callback) { - authorMangager.doesAuthorExists(authorID, function(err, exists) + authorManager.doesAuthorExists(authorID, function(err, exists) { if(ERR(err, callback)) return; @@ -287,7 +287,7 @@ exports.listSessionsOfGroup = function(groupID, callback) exports.listSessionsOfAuthor = function(authorID, callback) { - authorMangager.doesAuthorExists(authorID, function(err, exists) + authorManager.doesAuthorExists(authorID, function(err, exists) { if(ERR(err, callback)) return; From 3802073695166bc0892e0956e69aa5ab93a21752 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Fri, 18 Jan 2019 13:48:46 +0000 Subject: [PATCH 006/183] db/DB.js: allow a Promise return instead of callbacks in init() --- src/node/db/DB.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/node/db/DB.js b/src/node/db/DB.js index 93848b1bd..fa94259b5 100644 --- a/src/node/db/DB.js +++ b/src/node/db/DB.js @@ -35,7 +35,7 @@ exports.db = null; * Initalizes the database with the settings provided by the settings module * @param {Function} callback */ -exports.init = function(callback) { +function init(callback) { // initalize the database async db.init(function(err) { if (err) { @@ -50,3 +50,19 @@ exports.init = function(callback) { } }); } + +/** + * Initalizes the database with the settings provided by the settings module + * If the callback is not supplied a Promise is returned instead. + * @param {Function} callback + */ +exports.init = function(callback) +{ + if (callback === undefined) { + return new Promise(resolve => init(resolve)); + } else if (typeof callback === "function") { + init(callback); + } else { + throw new TypeError("DB.init callback parameter"); + } +} From 8d85ae582e83405f36e077ca2deadd7623549043 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Fri, 18 Jan 2019 13:49:17 +0000 Subject: [PATCH 007/183] pluginfw/hooks.js: allow returning a Promise in aCallFirst(), aCallAll() Since this code can end up loaded in browsers when using client side plugins, avoid use of ES6 syntax features such as arrow functions until MSIE support is finally dropped. --- src/static/js/pluginfw/hooks.js | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 7f26c3bfb..489709e33 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -78,7 +78,7 @@ exports.callAll = function (hook_name, args) { } } -exports.aCallAll = function (hook_name, args, cb) { +function aCallAll(hook_name, args, cb) { if (!args) args = {}; if (!cb) cb = function () {}; if (exports.plugins.hooks[hook_name] === undefined) return cb(null, []); @@ -93,6 +93,19 @@ exports.aCallAll = function (hook_name, args, cb) { ); } +/* return a Promise if cb is not supplied */ +exports.aCallAll = function (hook_name, args, cb) { + if (cb === undefined) { + return new Promise(function(resolve, reject) { + aCallAll(hook_name, args, function(err, res) { + return err ? reject(err) : resolve(res); + }); + }); + } else { + return aCallAll(hook_name, args, cb); + } +} + exports.callFirst = function (hook_name, args) { if (!args) args = {}; if (exports.plugins.hooks[hook_name] === undefined) return []; @@ -101,7 +114,7 @@ exports.callFirst = function (hook_name, args) { }); } -exports.aCallFirst = function (hook_name, args, cb) { +function aCallFirst(hook_name, args, cb) { if (!args) args = {}; if (!cb) cb = function () {}; if (exports.plugins.hooks[hook_name] === undefined) return cb(null, []); @@ -114,6 +127,19 @@ exports.aCallFirst = function (hook_name, args, cb) { ); } +/* return a Promise if cb is not supplied */ +exports.aCallFirst = function (hook_name, args, cb) { + if (cb === undefined) { + return new Promise(function(resolve, reject) { + aCallFirst(hook_name, args, function(err, res) { + return err ? reject(err) : resolve(res); + }); + }); + } else { + return aCallFirst(hook_name, args, cb); + } +} + exports.callAllStr = function(hook_name, args, sep, pre, post) { if (sep == undefined) sep = ''; if (pre == undefined) pre = ''; From 80b301915480da8281bfa1a388fffef8672e977d Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Fri, 18 Jan 2019 13:52:37 +0000 Subject: [PATCH 008/183] pluginfw/plugins.js: converted to Promise API --- src/static/js/pluginfw/plugins.js | 153 ++++++++++++++---------------- 1 file changed, 69 insertions(+), 84 deletions(-) diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js index 38d6b68c0..ed9c66a37 100644 --- a/src/static/js/pluginfw/plugins.js +++ b/src/static/js/pluginfw/plugins.js @@ -1,7 +1,6 @@ var npm = require("npm/lib/npm.js"); var readInstalled = require("./read-installed.js"); var path = require("path"); -var async = require("async"); var fs = require("fs"); var tsort = require("./tsort"); var util = require("util"); @@ -15,6 +14,7 @@ exports.plugins = {}; exports.parts = []; exports.hooks = {}; +// @TODO RPB this appears to be unused exports.ensure = function (cb) { if (!exports.loaded) exports.update(cb); @@ -53,109 +53,94 @@ exports.formatHooks = function (hook_set_name) { return "
    " + res.join("\n") + "
    "; }; -exports.callInit = function (cb) { +exports.callInit = function () { + const fsp_stat = util.promisify(fs.stat); + const fsp_writeFile = util.promisify(fs.writeFile); + var hooks = require("./hooks"); - async.map( - Object.keys(exports.plugins), - function (plugin_name, cb) { - var plugin = exports.plugins[plugin_name]; - fs.stat(path.normalize(path.join(plugin.package.path, ".ep_initialized")), function (err, stats) { - if (err) { - async.waterfall([ - function (cb) { fs.writeFile(path.normalize(path.join(plugin.package.path, ".ep_initialized")), 'done', cb); }, - function (cb) { hooks.aCallAll("init_" + plugin_name, {}, cb); }, - cb, - ]); - } else { - cb(); - } - }); - }, - function () { cb(); } - ); + let p = Object.keys(exports.plugins).map(function (plugin_name) { + let plugin = exports.plugins[plugin_name]; + let ep_init = path.normalize(path.join(plugin.package.path, ".ep_initialized")); + return fsp_stat(ep_init).catch(async function() { + await fsp_writeFile(ep_init, "done"); + await hooks.aCallAll("init_" + plugin_name, {}); + }); + }); + + return Promise.all(p); } exports.pathNormalization = function (part, hook_fn_name) { return path.normalize(path.join(path.dirname(exports.plugins[part.plugin].package.path), hook_fn_name)); } -exports.update = function (cb) { - exports.getPackages(function (er, packages) { - var parts = []; - var plugins = {}; +exports.update = async function () { + let packages = await exports.getPackages(); + var parts = []; + var plugins = {}; - // Load plugin metadata ep.json - async.forEach( - Object.keys(packages), - function (plugin_name, cb) { - loadPlugin(packages, plugin_name, plugins, parts, cb); - }, - function (err) { - if (err) cb(err); - exports.plugins = plugins; - exports.parts = sortParts(parts); - exports.hooks = pluginUtils.extractHooks(exports.parts, "hooks", exports.pathNormalization); - exports.loaded = true; - exports.callInit(cb); - } - ); + // Load plugin metadata ep.json + let p = Object.keys(packages).map(function (plugin_name) { + return loadPlugin(packages, plugin_name, plugins, parts); }); - }; -exports.getPackages = function (cb) { + return Promise.all(p).then(function() { + exports.plugins = plugins; + exports.parts = sortParts(parts); + exports.hooks = pluginUtils.extractHooks(exports.parts, "hooks", exports.pathNormalization); + exports.loaded = true; + }).then(exports.callInit); +} + +exports.getPackages = async function () { // Load list of installed NPM packages, flatten it to a list, and filter out only packages with names that var dir = path.resolve(npm.dir, '..'); - readInstalled(dir, function (er, data) { - if (er) cb(er, null); + let data = await util.promisify(readInstalled)(dir); - var packages = {}; - function flatten(deps) { - _.chain(deps).keys().each(function (name) { - if (name.indexOf(exports.prefix) === 0) { - packages[name] = _.clone(deps[name]); - // Delete anything that creates loops so that the plugin - // list can be sent as JSON to the web client - delete packages[name].dependencies; - delete packages[name].parent; - } + var packages = {}; + function flatten(deps) { + _.chain(deps).keys().each(function (name) { + if (name.indexOf(exports.prefix) === 0) { + packages[name] = _.clone(deps[name]); + // Delete anything that creates loops so that the plugin + // list can be sent as JSON to the web client + delete packages[name].dependencies; + delete packages[name].parent; + } - // I don't think we need recursion - //if (deps[name].dependencies !== undefined) flatten(deps[name].dependencies); - }); - } + // I don't think we need recursion + //if (deps[name].dependencies !== undefined) flatten(deps[name].dependencies); + }); + } - var tmp = {}; - tmp[data.name] = data; - flatten(tmp[data.name].dependencies); - cb(null, packages); - }); + var tmp = {}; + tmp[data.name] = data; + flatten(tmp[data.name].dependencies); + return packages; }; -function loadPlugin(packages, plugin_name, plugins, parts, cb) { +async function loadPlugin(packages, plugin_name, plugins, parts) { + let fsp_readFile = util.promisify(fs.readFile); + var plugin_path = path.resolve(packages[plugin_name].path, "ep.json"); - fs.readFile( - plugin_path, - function (er, data) { - if (er) { - console.error("Unable to load plugin definition file " + plugin_path); - return cb(); - } - try { - var plugin = JSON.parse(data); - plugin['package'] = packages[plugin_name]; - plugins[plugin_name] = plugin; - _.each(plugin.parts, function (part) { - part.plugin = plugin_name; - part.full_name = plugin_name + "/" + part.name; - parts[part.full_name] = part; - }); - } catch (ex) { - console.error("Unable to parse plugin definition file " + plugin_path + ": " + ex.toString()); - } - cb(); + try { + let data = await fsp_readFile(plugin_path); + try { + var plugin = JSON.parse(data); + plugin['package'] = packages[plugin_name]; + plugins[plugin_name] = plugin; + _.each(plugin.parts, function (part) { + part.plugin = plugin_name; + part.full_name = plugin_name + "/" + part.name; + parts[part.full_name] = part; + }); + } catch (ex) { + console.error("Unable to parse plugin definition file " + plugin_path + ": " + ex.toString()); } - ); + } catch (er) { + console.error("Unable to load plugin definition file " + plugin_path); + } } function partsToParentChildList(parts) { From a579dfc2852ca5bfb57b3aa411398b7b7a477489 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Fri, 18 Jan 2019 16:10:48 +0000 Subject: [PATCH 009/183] pluginfw/installer.js: use Promise version of hooks.aCallAll() in install(), uninstall() We cannot use arrow functions in this file, because code in /src/static can end up being loaded in browsers, and we still support IE11. --- src/static/js/pluginfw/installer.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js index fbd0e1d97..dbf8696ed 100644 --- a/src/static/js/pluginfw/installer.js +++ b/src/static/js/pluginfw/installer.js @@ -34,6 +34,10 @@ function onAllTasksFinished() { hooks.aCallAll("restartServer", {}, function() {}); } +/* + * We cannot use arrow functions in this file, because code in /src/static + * can end up being loaded in browsers, and we still support IE11. + */ exports.uninstall = function(plugin_name, cb) { cb = wrapTaskCb(cb); @@ -42,16 +46,18 @@ exports.uninstall = function(plugin_name, cb) { npm.commands.uninstall([plugin_name], function(er) { if (er) return cb && cb(er); - - hooks.aCallAll("pluginUninstall", {plugin_name: plugin_name}, function(er, data) { - if (er) return cb(er); - - plugins.update(cb); - }); + hooks.aCallAll("pluginUninstall", {plugin_name: plugin_name}) + .then(plugins.update) + .then(function() { cb(null) }) + .catch(function(er) { cb(er) }); }); }); }; +/* + * We cannot use arrow functions in this file, because code in /src/static + * can end up being loaded in browsers, and we still support IE11. + */ exports.install = function(plugin_name, cb) { cb = wrapTaskCb(cb); @@ -60,12 +66,10 @@ exports.install = function(plugin_name, cb) { npm.commands.install([plugin_name], function(er) { if (er) return cb && cb(er); - - hooks.aCallAll("pluginInstall", {plugin_name: plugin_name}, function(er, data) { - if (er) return cb(er); - - plugins.update(cb); - }); + hooks.aCallAll("pluginInstall", {plugin_name: plugin_name}) + .then(plugins.update) + .then(function() { cb(null) }) + .catch(function(er) { cb(er) }); }); }); }; From 4877ec319a2a094271ad760246e7936145eea258 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Fri, 18 Jan 2019 16:10:25 +0000 Subject: [PATCH 010/183] server.js: rewritten to use Promises --- src/node/server.js | 78 ++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 47 deletions(-) diff --git a/src/node/server.js b/src/node/server.js index 2ba1fa47d..f683de82c 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -22,7 +22,6 @@ */ var log4js = require('log4js') - , async = require('async') , NodeVersion = require('./utils/NodeVersion') ; @@ -49,52 +48,37 @@ stats.gauge('memoryUsage', function() { return process.memoryUsage().rss; }); -var settings - , db - , plugins - , hooks; +/* + * no use of let or await here because it would cause startup + * to fail completely on very early versions of NodeJS + */ var npm = require("npm/lib/npm.js"); -async.waterfall([ - // load npm - function(callback) { - npm.load({}, function(er) { - callback(er); +npm.load({}, function() { + var settings = require('./utils/Settings'); + var db = require('./db/DB'); + var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); + var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); + hooks.plugins = plugins; + + db.init() + .then(plugins.update) + .then(function() { + console.info("Installed plugins: " + plugins.formatPluginsWithVersion()); + console.debug("Installed parts:\n" + plugins.formatParts()); + console.debug("Installed hooks:\n" + plugins.formatHooks()); + + // Call loadSettings hook + hooks.aCallAll("loadSettings", { settings: settings }); + + // initalize the http server + hooks.callAll("createServer", {}); + }) + .catch(function(e) { + console.error("exception thrown: " + e.message); + if (e.stack) { + console.log(e.stack); + } + process.exit(1); }); - }, - - // load everything - function(callback) { - settings = require('./utils/Settings'); - db = require('./db/DB'); - plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); - hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); - hooks.plugins = plugins; - callback(); - }, - - // initalize the database - function (callback) { - db.init(callback); - }, - - function(callback) { - plugins.update(callback); - }, - - function (callback) { - console.info("Installed plugins: " + plugins.formatPluginsWithVersion()); - console.debug("Installed parts:\n" + plugins.formatParts()); - console.debug("Installed hooks:\n" + plugins.formatHooks()); - - // Call loadSettings hook - hooks.aCallAll("loadSettings", { settings: settings }); - callback(); - }, - - // initalize the http server - function (callback) { - hooks.callAll("createServer", {}); - callback(null); - } -]); +}); From 40c45077ef3a0bfdd3c49eedf47fd18dcb38d0f6 Mon Sep 17 00:00:00 2001 From: muxator Date: Sat, 9 Feb 2019 01:15:50 +0100 Subject: [PATCH 011/183] db/GroupManager.js: factored out a variable Extracted from Ray's work. --- src/node/db/GroupManager.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.js index 985883203..88aa8aa04 100644 --- a/src/node/db/GroupManager.js +++ b/src/node/db/GroupManager.js @@ -122,7 +122,9 @@ exports.deleteGroup = function(groupID, callback) if (ERR(err, callback)) return; groups = groups? groups.groupIDs : []; - if (groups.indexOf(groupID) === -1) { + let index = groups.indexOf(groupID); + + if (index === -1) { // it's not listed callback(); @@ -130,7 +132,7 @@ exports.deleteGroup = function(groupID, callback) } // remove from the list - groups.splice(groups.indexOf(groupID), 1); + groups.splice(index, 1); // store empty group list if (groups.length == 0) { From 17fe32ec0c1078983cb4a35686ea1d5be3b9a8e8 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Mon, 21 Jan 2019 16:28:05 +0000 Subject: [PATCH 012/183] start using "thenify" to support callback and promises PadManager.sanitizePadId() can't use thenify: single arg callback --- src/node/db/API.js | 121 +++++++++++++++++---------------- src/node/db/AuthorManager.js | 41 +++++------ src/node/db/DB.js | 21 +----- src/node/db/GroupManager.js | 29 ++++---- src/node/db/Pad.js | 49 ++++++------- src/node/db/PadManager.js | 38 +++++++---- src/node/db/ReadOnlyManager.js | 13 ++-- src/node/db/SessionManager.js | 25 +++---- src/package.json | 1 + 9 files changed, 170 insertions(+), 168 deletions(-) diff --git a/src/node/db/API.js b/src/node/db/API.js index b6acefcd4..df2e25f10 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -33,6 +33,7 @@ var exportTxt = require("../utils/ExportTxt"); var importHtml = require("../utils/ImportHtml"); var cleanText = require("./Pad").cleanText; var PadDiff = require("../utils/padDiff"); +const thenify = require("thenify").withCallback; /**********************/ /**GROUP FUNCTIONS*****/ @@ -103,14 +104,14 @@ Example returns: } */ -exports.getAttributePool = function(padID, callback) +exports.getAttributePool = thenify(function(padID, callback) { getPadSafe(padID, true, function(err, pad) { if (ERR(err, callback)) return; callback(null, {pool: pad.pool}); }); -} +}); /** getRevisionChangeset (padID, [rev]) @@ -125,7 +126,7 @@ Example returns: } */ -exports.getRevisionChangeset = function(padID, rev, callback) +exports.getRevisionChangeset = thenify(function(padID, rev, callback) { // check if rev is a number if (rev !== undefined && typeof rev !== "number") { @@ -179,7 +180,7 @@ exports.getRevisionChangeset = function(padID, rev, callback) callback(null, changeset); }) }); -} +}); /** getText(padID, [rev]) returns the text of a pad @@ -189,7 +190,7 @@ Example returns: {code: 0, message:"ok", data: {text:"Welcome Text"}} {code: 1, message:"padID does not exist", data: null} */ -exports.getText = function(padID, rev, callback) +exports.getText = thenify(function(padID, rev, callback) { // check if rev is a number if (rev !== undefined && typeof rev !== "number") { @@ -242,7 +243,7 @@ exports.getText = function(padID, rev, callback) var padText = exportTxt.getTXTFromAtext(pad, pad.atext); callback(null, {"text": padText}); }); -} +}); /** setText(padID, text) sets the text of a pad @@ -253,7 +254,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} {code: 1, message:"text too long", data: null} */ -exports.setText = function(padID, text, callback) +exports.setText = thenify(function(padID, text, callback) { // text is required if (typeof text !== "string") { @@ -271,7 +272,7 @@ exports.setText = function(padID, text, callback) // update the clients on the pad padMessageHandler.updatePadClients(pad, callback); }); -} +}); /** appendText(padID, text) appends text to a pad @@ -282,7 +283,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} {code: 1, message:"text too long", data: null} */ -exports.appendText = function(padID, text, callback) +exports.appendText = thenify(function(padID, text, callback) { // text is required if (typeof text !== "string") { @@ -299,7 +300,7 @@ exports.appendText = function(padID, text, callback) // update the clients on the pad padMessageHandler.updatePadClients(pad, callback); }); -}; +}); /** getHTML(padID, [rev]) returns the html of a pad @@ -309,7 +310,7 @@ Example returns: {code: 0, message:"ok", data: {text:"Welcome Text"}} {code: 1, message:"padID does not exist", data: null} */ -exports.getHTML = function(padID, rev, callback) +exports.getHTML = thenify(function(padID, rev, callback) { if (rev !== undefined && typeof rev !== "number") { if (isNaN(parseInt(rev))) { @@ -362,7 +363,7 @@ exports.getHTML = function(padID, rev, callback) callback(null, data); }); }); -} +}); /** setHTML(padID, html) sets the text of a pad based on HTML @@ -372,7 +373,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.setHTML = function(padID, html, callback) +exports.setHTML = thenify(function(padID, html, callback) { // html is required if (typeof html !== "string") { @@ -395,7 +396,7 @@ exports.setHTML = function(padID, html, callback) padMessageHandler.updatePadClients(pad, callback); }); }); -} +}); /******************/ /**CHAT FUNCTIONS */ @@ -413,7 +414,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} */ -exports.getChatHistory = function(padID, start, end, callback) +exports.getChatHistory = thenify(function(padID, start, end, callback) { if (start && end) { if (start < 0) { @@ -458,7 +459,7 @@ exports.getChatHistory = function(padID, start, end, callback) callback(null, {messages: msgs}); }); }); -} +}); /** appendChatMessage(padID, text, authorID, time), creates a chat message for the pad id, time is a timestamp @@ -468,7 +469,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.appendChatMessage = function(padID, text, authorID, time, callback) +exports.appendChatMessage = thenify(function(padID, text, authorID, time, callback) { // text is required if (typeof text !== "string") { @@ -486,7 +487,7 @@ exports.appendChatMessage = function(padID, text, authorID, time, callback) padMessageHandler.sendChatMessageToPadClients(time, authorID, text, padID); callback(); -} +}); /*****************/ /**PAD FUNCTIONS */ @@ -500,7 +501,7 @@ Example returns: {code: 0, message:"ok", data: {revisions: 56}} {code: 1, message:"padID does not exist", data: null} */ -exports.getRevisionsCount = function(padID, callback) +exports.getRevisionsCount = thenify(function(padID, callback) { // get the pad getPadSafe(padID, true, function(err, pad) { @@ -508,7 +509,7 @@ exports.getRevisionsCount = function(padID, callback) callback(null, {revisions: pad.getHeadRevisionNumber()}); }); -} +}); /** getSavedRevisionsCount(padID) returns the number of saved revisions of this pad @@ -518,7 +519,7 @@ Example returns: {code: 0, message:"ok", data: {savedRevisions: 42}} {code: 1, message:"padID does not exist", data: null} */ -exports.getSavedRevisionsCount = function(padID, callback) +exports.getSavedRevisionsCount = thenify(function(padID, callback) { // get the pad getPadSafe(padID, true, function(err, pad) { @@ -526,7 +527,7 @@ exports.getSavedRevisionsCount = function(padID, callback) callback(null, {savedRevisions: pad.getSavedRevisionsNumber()}); }); -} +}); /** listSavedRevisions(padID) returns the list of saved revisions of this pad @@ -536,7 +537,7 @@ Example returns: {code: 0, message:"ok", data: {savedRevisions: [2, 42, 1337]}} {code: 1, message:"padID does not exist", data: null} */ -exports.listSavedRevisions = function(padID, callback) +exports.listSavedRevisions = thenify(function(padID, callback) { // get the pad getPadSafe(padID, true, function(err, pad) { @@ -544,7 +545,7 @@ exports.listSavedRevisions = function(padID, callback) callback(null, {savedRevisions: pad.getSavedRevisionsList()}); }); -} +}); /** saveRevision(padID) returns the list of saved revisions of this pad @@ -554,7 +555,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.saveRevision = function(padID, rev, callback) +exports.saveRevision = thenify(function(padID, rev, callback) { // check if rev is a number if (rev !== undefined && typeof rev !== "number") { @@ -601,7 +602,7 @@ exports.saveRevision = function(padID, rev, callback) callback(); }); }); -} +}); /** getLastEdited(padID) returns the timestamp of the last revision of the pad @@ -611,7 +612,7 @@ Example returns: {code: 0, message:"ok", data: {lastEdited: 1340815946602}} {code: 1, message:"padID does not exist", data: null} */ -exports.getLastEdited = function(padID, callback) +exports.getLastEdited = thenify(function(padID, callback) { // get the pad getPadSafe(padID, true, function(err, pad) { @@ -622,7 +623,7 @@ exports.getLastEdited = function(padID, callback) callback(null, {lastEdited: value}); }); }); -} +}); /** createPad(padName [, text]) creates a new pad in this group @@ -632,7 +633,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"pad does already exist", data: null} */ -exports.createPad = function(padID, text, callback) +exports.createPad = thenify(function(padID, text, callback) { if (padID) { // ensure there is no $ in the padID @@ -653,7 +654,7 @@ exports.createPad = function(padID, text, callback) if (ERR(err, callback)) return; callback(); }); -} +}); /** deletePad(padID) deletes a pad @@ -663,14 +664,14 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.deletePad = function(padID, callback) +exports.deletePad = thenify(function(padID, callback) { getPadSafe(padID, true, function(err, pad) { if (ERR(err, callback)) return; pad.remove(callback); }); -} +}); /** restoreRevision(padID, [rev]) Restores revision from past as new changeset @@ -679,7 +680,7 @@ exports.deletePad = function(padID, callback) {code:0, message:"ok", data:null} {code: 1, message:"padID does not exist", data: null} */ -exports.restoreRevision = function(padID, rev, callback) +exports.restoreRevision = thenify(function(padID, rev, callback) { // check if rev is a number if (rev !== undefined && typeof rev !== "number") { @@ -762,7 +763,7 @@ exports.restoreRevision = function(padID, rev, callback) }); }); -}; +}); /** copyPad(sourceID, destinationID[, force=false]) copies a pad. If force is true, @@ -773,14 +774,14 @@ Example returns: {code: 0, message:"ok", data: {padID: destinationID}} {code: 1, message:"padID does not exist", data: null} */ -exports.copyPad = function(sourceID, destinationID, force, callback) +exports.copyPad = thenify(function(sourceID, destinationID, force, callback) { getPadSafe(sourceID, true, function(err, pad) { if (ERR(err, callback)) return; pad.copy(destinationID, force, callback); }); -} +}); /** movePad(sourceID, destinationID[, force=false]) moves a pad. If force is true, @@ -791,7 +792,7 @@ Example returns: {code: 0, message:"ok", data: {padID: destinationID}} {code: 1, message:"padID does not exist", data: null} */ -exports.movePad = function(sourceID, destinationID, force, callback) +exports.movePad = thenify(function(sourceID, destinationID, force, callback) { getPadSafe(sourceID, true, function(err, pad) { if (ERR(err, callback)) return; @@ -801,7 +802,7 @@ exports.movePad = function(sourceID, destinationID, force, callback) pad.remove(callback); }); }); -} +}); /** getReadOnlyLink(padID) returns the read only link of a pad @@ -810,7 +811,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.getReadOnlyID = function(padID, callback) +exports.getReadOnlyID = thenify(function(padID, callback) { // we don't need the pad object, but this function does all the security stuff for us getPadSafe(padID, true, function(err) { @@ -822,7 +823,7 @@ exports.getReadOnlyID = function(padID, callback) callback(null, {readOnlyID: readOnlyId}); }); }); -} +}); /** getPadID(roID) returns the padID of a pad based on the readonlyID(roID) @@ -832,7 +833,7 @@ Example returns: {code: 0, message:"ok", data: {padID: padID}} {code: 1, message:"padID does not exist", data: null} */ -exports.getPadID = function(roID, callback) +exports.getPadID = thenify(function(roID, callback) { // get the PadId readOnlyManager.getPadId(roID, function(err, retrievedPadID) { @@ -845,7 +846,7 @@ exports.getPadID = function(roID, callback) callback(null, {padID: retrievedPadID}); }); -} +}); /** setPublicStatus(padID, publicStatus) sets a boolean for the public status of a pad @@ -855,7 +856,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.setPublicStatus = function(padID, publicStatus, callback) +exports.setPublicStatus = thenify(function(padID, publicStatus, callback) { // ensure this is a group pad if (padID && padID.indexOf("$") === -1) { @@ -876,7 +877,7 @@ exports.setPublicStatus = function(padID, publicStatus, callback) callback(); }); -} +}); /** getPublicStatus(padID) return true of false @@ -886,7 +887,7 @@ Example returns: {code: 0, message:"ok", data: {publicStatus: true}} {code: 1, message:"padID does not exist", data: null} */ -exports.getPublicStatus = function(padID, callback) +exports.getPublicStatus = thenify(function(padID, callback) { // ensure this is a group pad if (padID && padID.indexOf("$") == -1) { @@ -900,7 +901,7 @@ exports.getPublicStatus = function(padID, callback) callback(null, {publicStatus: pad.getPublicStatus()}); }); -} +}); /** setPassword(padID, password) returns ok or a error message @@ -910,7 +911,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.setPassword = function(padID, password, callback) +exports.setPassword = thenify(function(padID, password, callback) { // ensure this is a group pad if (padID && padID.indexOf("$") == -1) { @@ -927,7 +928,7 @@ exports.setPassword = function(padID, password, callback) callback(); }); -} +}); /** isPasswordProtected(padID) returns true or false @@ -937,7 +938,7 @@ Example returns: {code: 0, message:"ok", data: {passwordProtection: true}} {code: 1, message:"padID does not exist", data: null} */ -exports.isPasswordProtected = function(padID, callback) +exports.isPasswordProtected = thenify(function(padID, callback) { // ensure this is a group pad if (padID && padID.indexOf("$") == -1) { @@ -951,7 +952,7 @@ exports.isPasswordProtected = function(padID, callback) callback(null, {isPasswordProtected: pad.isPasswordProtected()}); }); -} +}); /** listAuthorsOfPad(padID) returns an array of authors who contributed to this pad @@ -961,7 +962,7 @@ Example returns: {code: 0, message:"ok", data: {authorIDs : ["a.s8oes9dhwrvt0zif", "a.akf8finncvomlqva"]} {code: 1, message:"padID does not exist", data: null} */ -exports.listAuthorsOfPad = function(padID, callback) +exports.listAuthorsOfPad = thenify(function(padID, callback) { // get the pad getPadSafe(padID, true, function(err, pad) { @@ -969,7 +970,7 @@ exports.listAuthorsOfPad = function(padID, callback) callback(null, {authorIDs: pad.getAllAuthors()}); }); -} +}); /** sendClientsMessage(padID, msg) sends a message to all clients connected to the @@ -994,7 +995,7 @@ Example returns: {code: 1, message:"padID does not exist"} */ -exports.sendClientsMessage = function(padID, msg, callback) { +exports.sendClientsMessage = thenify(function(padID, msg, callback) { getPadSafe(padID, true, function(err, pad) { if (ERR(err, callback)) { return; @@ -1002,7 +1003,7 @@ exports.sendClientsMessage = function(padID, msg, callback) { padMessageHandler.handleCustomMessage(padID, msg, callback); } ); -} +}); /** checkToken() returns ok when the current api token is valid @@ -1012,10 +1013,10 @@ Example returns: {"code":0,"message":"ok","data":null} {"code":4,"message":"no or wrong API Key","data":null} */ -exports.checkToken = function(callback) +exports.checkToken = thenify(function(callback) { callback(); -} +}); /** getChatHead(padID) returns the chatHead (last number of the last chat-message) of the pad @@ -1025,7 +1026,7 @@ Example returns: {code: 0, message:"ok", data: {chatHead: 42}} {code: 1, message:"padID does not exist", data: null} */ -exports.getChatHead = function(padID, callback) +exports.getChatHead = thenify(function(padID, callback) { // get the pad getPadSafe(padID, true, function(err, pad) { @@ -1033,7 +1034,7 @@ exports.getChatHead = function(padID, callback) callback(null, {chatHead: pad.chatHead}); }); -} +}); /** createDiffHTML(padID, startRev, endRev) returns an object of diffs from 2 points in a pad @@ -1043,7 +1044,7 @@ Example returns: {"code":0,"message":"ok","data":{"html":"Welcome to Etherpad!

    This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!

    Get involved with Etherpad at http://etherpad.org
    aw

    ","authors":["a.HKIv23mEbachFYfH",""]}} {"code":4,"message":"no or wrong API Key","data":null} */ -exports.createDiffHTML = function(padID, startRev, endRev, callback) { +exports.createDiffHTML = thenify(function(padID, startRev, endRev, callback) { // check if startRev is a number if (startRev !== undefined && typeof startRev !== "number") { // try to parse the number @@ -1104,7 +1105,7 @@ exports.createDiffHTML = function(padID, startRev, endRev, callback) { callback(err, {html: html, authors: authors}) }); }); -} +}); /******************************/ /** INTERNAL HELPER FUNCTIONS */ diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index d04c4ca32..0db187706 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -22,6 +22,7 @@ var ERR = require("async-stacktrace"); var db = require("./DB").db; var customError = require("../utils/customError"); var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +const thenify = require("thenify").withCallback; exports.getColorPalette = function() { return [ @@ -39,7 +40,7 @@ exports.getColorPalette = function() { /** * Checks if the author exists */ -exports.doesAuthorExists = function(authorID, callback) +exports.doesAuthorExists = thenify(function(authorID, callback) { // check if the database entry of this author exists db.get("globalAuthor:" + authorID, function(err, author) { @@ -47,14 +48,14 @@ exports.doesAuthorExists = function(authorID, callback) callback(null, author !== null); }); -} +}); /** * Returns the AuthorID for a token. * @param {String} token The token * @param {Function} callback callback (err, author) */ -exports.getAuthor4Token = function(token, callback) +exports.getAuthor4Token = thenify(function(token, callback) { mapAuthorWithDBKey("token2author", token, function(err, author) { if (ERR(err, callback)) return; @@ -62,7 +63,7 @@ exports.getAuthor4Token = function(token, callback) // return only the sub value authorID callback(null, author ? author.authorID : author); }); -} +}); /** * Returns the AuthorID for a mapper. @@ -70,7 +71,7 @@ exports.getAuthor4Token = function(token, callback) * @param {String} name The name of the author (optional) * @param {Function} callback callback (err, author) */ -exports.createAuthorIfNotExistsFor = function(authorMapper, name, callback) +exports.createAuthorIfNotExistsFor = thenify(function(authorMapper, name, callback) { mapAuthorWithDBKey("mapper2author", authorMapper, function(err, author) { if (ERR(err, callback)) return; @@ -83,7 +84,7 @@ exports.createAuthorIfNotExistsFor = function(authorMapper, name, callback) // return the authorID callback(null, author); }); -} +}); /** * Returns the AuthorID for a mapper. We can map using a mapperkey, @@ -126,7 +127,7 @@ function mapAuthorWithDBKey (mapperkey, mapper, callback) * Internal function that creates the database entry for an author * @param {String} name The name of the author */ -exports.createAuthor = function(name, callback) +exports.createAuthor = thenify(function(name, callback) { // create the new author name var author = "a." + randomString(16); @@ -142,27 +143,27 @@ exports.createAuthor = function(name, callback) db.set("globalAuthor:" + author, authorObj); callback(null, {authorID: author}); -} +}); /** * Returns the Author Obj of the author * @param {String} author The id of the author * @param {Function} callback callback(err, authorObj) */ -exports.getAuthor = function(author, callback) +exports.getAuthor = thenify(function(author, callback) { db.get("globalAuthor:" + author, callback); -} +}); /** * Returns the color Id of the author * @param {String} author The id of the author * @param {Function} callback callback(err, colorId) */ -exports.getAuthorColorId = function(author, callback) +exports.getAuthorColorId = thenify(function(author, callback) { db.getSub("globalAuthor:" + author, ["colorId"], callback); -} +}); /** * Sets the color Id of the author @@ -170,20 +171,20 @@ exports.getAuthorColorId = function(author, callback) * @param {String} colorId The color id of the author * @param {Function} callback (optional) */ -exports.setAuthorColorId = function(author, colorId, callback) +exports.setAuthorColorId = thenify(function(author, colorId, callback) { db.setSub("globalAuthor:" + author, ["colorId"], colorId, callback); -} +}); /** * Returns the name of the author * @param {String} author The id of the author * @param {Function} callback callback(err, name) */ -exports.getAuthorName = function(author, callback) +exports.getAuthorName = thenify(function(author, callback) { db.getSub("globalAuthor:" + author, ["name"], callback); -} +}); /** * Sets the name of the author @@ -191,17 +192,17 @@ exports.getAuthorName = function(author, callback) * @param {String} name The name of the author * @param {Function} callback (optional) */ -exports.setAuthorName = function(author, name, callback) +exports.setAuthorName = thenify(function(author, name, callback) { db.setSub("globalAuthor:" + author, ["name"], name, callback); -} +}); /** * Returns an array of all pads this author contributed to * @param {String} author The id of the author * @param {Function} callback (optional) */ -exports.listPadsOfAuthor = function(authorID, callback) +exports.listPadsOfAuthor = thenify(function(authorID, callback) { /* There are two other places where this array is manipulated: * (1) When the author is added to a pad, the author object is also updated @@ -230,7 +231,7 @@ exports.listPadsOfAuthor = function(authorID, callback) callback(null, {padIDs: pads}); }); -} +}); /** * Adds a new pad to the list of contributions diff --git a/src/node/db/DB.js b/src/node/db/DB.js index fa94259b5..e6ddd5fb3 100644 --- a/src/node/db/DB.js +++ b/src/node/db/DB.js @@ -22,6 +22,7 @@ var ueberDB = require("ueberdb2"); var settings = require("../utils/Settings"); var log4js = require('log4js'); +const thenify = require("thenify").withCallback; // set database settings var db = new ueberDB.database(settings.dbType, settings.dbSettings, null, log4js.getLogger("ueberDB")); @@ -35,7 +36,7 @@ exports.db = null; * Initalizes the database with the settings provided by the settings module * @param {Function} callback */ -function init(callback) { +exports.init = thenify(function (callback) { // initalize the database async db.init(function(err) { if (err) { @@ -49,20 +50,4 @@ function init(callback) { callback(null); } }); -} - -/** - * Initalizes the database with the settings provided by the settings module - * If the callback is not supplied a Promise is returned instead. - * @param {Function} callback - */ -exports.init = function(callback) -{ - if (callback === undefined) { - return new Promise(resolve => init(resolve)); - } else if (typeof callback === "function") { - init(callback); - } else { - throw new TypeError("DB.init callback parameter"); - } -} +}); diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.js index 88aa8aa04..d72efdff1 100644 --- a/src/node/db/GroupManager.js +++ b/src/node/db/GroupManager.js @@ -25,8 +25,9 @@ var db = require("./DB").db; var async = require("async"); var padManager = require("./PadManager"); var sessionManager = require("./SessionManager"); +const thenify = require("thenify").withCallback; -exports.listAllGroups = function(callback) { +exports.listAllGroups = thenify(function(callback) { db.get("groups", function (err, groups) { if (ERR(err, callback)) return; @@ -43,9 +44,9 @@ exports.listAllGroups = function(callback) { } callback(null, {groupIDs: groupIDs}); }); -} +}); -exports.deleteGroup = function(groupID, callback) +exports.deleteGroup = thenify(function(groupID, callback) { var group; @@ -158,18 +159,18 @@ exports.deleteGroup = function(groupID, callback) if (ERR(err, callback)) return; callback(); }); -} +}); -exports.doesGroupExist = function(groupID, callback) +exports.doesGroupExist = thenify(function(groupID, callback) { // try to get the group entry db.get("group:" + groupID, function (err, group) { if (ERR(err, callback)) return; callback(null, group != null); }); -} +}); -exports.createGroup = function(callback) +exports.createGroup = thenify(function(callback) { // search for non existing groupID var groupID = "g." + randomString(16); @@ -195,9 +196,9 @@ exports.createGroup = function(callback) callback(null, {groupID: groupID}); }); }); -} +}); -exports.createGroupIfNotExistsFor = function(groupMapper, callback) +exports.createGroupIfNotExistsFor = thenify(function(groupMapper, callback) { // ensure mapper is optional if (typeof groupMapper !== "string") { @@ -237,9 +238,9 @@ exports.createGroupIfNotExistsFor = function(groupMapper, callback) // there is no group for this mapper, let's create a group createGroupForMapper(callback) }); -} +}); -exports.createGroupPad = function(groupID, padName, text, callback) +exports.createGroupPad = thenify(function(groupID, padName, text, callback) { // create the padID var padID = groupID + "$" + padName; @@ -297,9 +298,9 @@ exports.createGroupPad = function(groupID, padName, text, callback) callback(null, {padID: padID}); }); -} +}); -exports.listPads = function(groupID, callback) +exports.listPads = thenify(function(groupID, callback) { exports.doesGroupExist(groupID, function(err, exists) { if (ERR(err, callback)) return; @@ -321,4 +322,4 @@ exports.listPads = function(groupID, callback) callback(null, {padIDs: pads}); }); }); -} +}); diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index c5a75a89f..34d000631 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -18,6 +18,7 @@ var readOnlyManager = require("./ReadOnlyManager"); var crypto = require("crypto"); var randomString = require("../utils/randomstring"); var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); +const thenify = require("thenify").withCallback; // serialization/deserialization attributes var attributeBlackList = ["id"]; @@ -131,22 +132,22 @@ Pad.prototype.saveToDatabase = function saveToDatabase() { } // get time of last edit (changeset application) -Pad.prototype.getLastEdit = function getLastEdit(callback) { +Pad.prototype.getLastEdit = thenify(function getLastEdit(callback) { var revNum = this.getHeadRevisionNumber(); db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "timestamp"], callback); -} +}); -Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum, callback) { +Pad.prototype.getRevisionChangeset = thenify(function getRevisionChangeset(revNum, callback) { db.getSub("pad:" + this.id + ":revs:" + revNum, ["changeset"], callback); -}; +}); -Pad.prototype.getRevisionAuthor = function getRevisionAuthor(revNum, callback) { +Pad.prototype.getRevisionAuthor = thenify(function getRevisionAuthor(revNum, callback) { db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "author"], callback); -}; +}); -Pad.prototype.getRevisionDate = function getRevisionDate(revNum, callback) { +Pad.prototype.getRevisionDate = thenify(function getRevisionDate(revNum, callback) { db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "timestamp"], callback); -}; +}); Pad.prototype.getAllAuthors = function getAllAuthors() { var authors = []; @@ -160,7 +161,7 @@ Pad.prototype.getAllAuthors = function getAllAuthors() { return authors; }; -Pad.prototype.getInternalRevisionAText = function getInternalRevisionAText(targetRev, callback) { +Pad.prototype.getInternalRevisionAText = thenify(function getInternalRevisionAText(targetRev, callback) { var _this = this; var keyRev = this.getKeyRevisionNumber(targetRev); @@ -228,13 +229,13 @@ Pad.prototype.getInternalRevisionAText = function getInternalRevisionAText(targe if (ERR(err, callback)) return; callback(null, atext); }); -}; +}); -Pad.prototype.getRevision = function getRevisionChangeset(revNum, callback) { +Pad.prototype.getRevision = thenify(function getRevisionChangeset(revNum, callback) { db.get("pad:" + this.id + ":revs:" + revNum, callback); -}; +}); -Pad.prototype.getAllAuthorColors = function getAllAuthorColors(callback) { +Pad.prototype.getAllAuthorColors = thenify(function getAllAuthorColors(callback) { var authors = this.getAllAuthors(); var returnTable = {}; var colorPalette = authorManager.getColorPalette(); @@ -254,7 +255,7 @@ Pad.prototype.getAllAuthorColors = function getAllAuthorColors(callback) { function(err) { callback(err, returnTable); }); -}; +}); Pad.prototype.getValidRevisionRange = function getValidRevisionRange(startRev, endRev) { startRev = parseInt(startRev, 10); @@ -325,7 +326,7 @@ Pad.prototype.appendChatMessage = function appendChatMessage(text, userId, time) this.saveToDatabase(); }; -Pad.prototype.getChatMessage = function getChatMessage(entryNum, callback) { +Pad.prototype.getChatMessage = thenify(function getChatMessage(entryNum, callback) { var _this = this; var entry; @@ -359,9 +360,9 @@ Pad.prototype.getChatMessage = function getChatMessage(entryNum, callback) { if (ERR(err, callback)) return; callback(null, entry); }); -}; +}); -Pad.prototype.getChatMessages = function getChatMessages(start, end, callback) { +Pad.prototype.getChatMessages = thenify(function getChatMessages(start, end, callback) { // collect the numbers of chat entries and in which order we need them var neededEntries = []; var order = 0; @@ -398,9 +399,9 @@ Pad.prototype.getChatMessages = function getChatMessages(start, end, callback) { callback(null, cleanedEntries); }); -}; +}); -Pad.prototype.init = function init(text, callback) { +Pad.prototype.init = thenify(function init(text, callback) { var _this = this; // replace text with default text if text isn't set @@ -432,9 +433,9 @@ Pad.prototype.init = function init(text, callback) { hooks.callAll("padLoad", { 'pad': _this }); callback(null); }); -}; +}); -Pad.prototype.copy = function copy(destinationID, force, callback) { +Pad.prototype.copy = thenify(function copy(destinationID, force, callback) { var sourceID = this.id; var _this = this; var destGroupID; @@ -581,9 +582,9 @@ Pad.prototype.copy = function copy(destinationID, force, callback) { if (ERR(err, callback)) return; callback(null, { padID: destinationID }); }); -}; +}); -Pad.prototype.remove = function remove(callback) { +Pad.prototype.remove = thenify(function remove(callback) { var padID = this.id; var _this = this; @@ -676,7 +677,7 @@ Pad.prototype.remove = function remove(callback) { if (ERR(err, callback)) return; callback(); }); -}; +}); // set in db Pad.prototype.setPublicStatus = function setPublicStatus(publicStatus) { diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index b198de81c..469a7f496 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -22,6 +22,7 @@ var ERR = require("async-stacktrace"); var customError = require("../utils/customError"); var Pad = require("../db/Pad").Pad; var db = require("./DB").db; +const thenify = require("thenify").withCallback; /** * A cache of all loaded Pads. @@ -53,7 +54,7 @@ var padList = { list: [], sorted : false, initiated: false, - init: function(cb) { + init: thenify(function(cb) { db.findKeys("pad:*", "*:*:*", function(err, dbData) { if (ERR(err, cb)) return; @@ -68,18 +69,18 @@ var padList = { }); return this; - }, - load: function(cb) { + }), + load: thenify(function(cb) { if (this.initiated) { cb && cb(); } else { this.init(cb); } - }, + }), /** * Returns all pads in alphabetical order as array. */ - getPads: function(cb) { + getPads: thenify(function(cb) { this.load(function() { if (!padList.sorted) { padList.list = padList.list.sort(); @@ -88,7 +89,7 @@ var padList = { cb && cb(padList.list); }) - }, + }), addPad: function(name) { if (!this.initiated) return; @@ -125,7 +126,7 @@ var padIdTransforms = [ * @param id A String with the id of the pad * @param {Function} callback */ -exports.getPad = function(id, text, callback) +exports.getPad = thenify(function(id, text, callback) { // check if this is a valid padId if (!exports.isValidPadId(id)) { @@ -177,17 +178,17 @@ exports.getPad = function(id, text, callback) padList.addPad(id); callback(null, pad); }); -} +}); -exports.listAllPads = function(cb) +exports.listAllPads = thenify(function(cb) { padList.getPads(function(list) { cb && cb(null, {padIDs: list}); }); -} +}); // checks if a pad exists -exports.doesPadExists = function(padId, callback) +exports.doesPadExists = thenify(function(padId, callback) { db.get("pad:" + padId, function(err, value) { if (ERR(err, callback)) return; @@ -198,10 +199,10 @@ exports.doesPadExists = function(padId, callback) callback(null, false); } }); -} +}); // returns a sanitized padId, respecting legacy pad id formats -exports.sanitizePadId = function(padId, callback) { +function sanitizePadId(padId, callback) { var transform_index = arguments[2] || 0; // we're out of possible transformations, so just return it @@ -228,10 +229,19 @@ exports.sanitizePadId = function(padId, callback) { } // check the next transform - exports.sanitizePadId(transformedPadId, callback, transform_index); + sanitizePadId(transformedPadId, callback, transform_index); }); } +// sanitizePadId can't use thenify: single arg callback +exports.sanitizePadId = function(padId, callback) { + if (callback) { + return sanitizePadId(padId, callback); + } else { + return new Promise(resolve => sanitizePadId(padId, resolve)); + } +} + exports.isValidPadId = function(padId) { return /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId); diff --git a/src/node/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.js index ac7bee045..14c6f75c7 100644 --- a/src/node/db/ReadOnlyManager.js +++ b/src/node/db/ReadOnlyManager.js @@ -23,12 +23,13 @@ var ERR = require("async-stacktrace"); var db = require("./DB").db; var async = require("async"); var randomString = require("../utils/randomstring"); +const thenify = require("thenify").withCallback; /** * returns a read only id for a pad * @param {String} padId the id of the pad */ -exports.getReadOnlyId = function (padId, callback) +exports.getReadOnlyId = thenify(function (padId, callback) { var readOnlyId; @@ -59,22 +60,22 @@ exports.getReadOnlyId = function (padId, callback) // return the results callback(null, readOnlyId); }) -} +}); /** * returns the padId for a read only id * @param {String} readOnlyId read only id */ -exports.getPadId = function(readOnlyId, callback) +exports.getPadId = thenify(function(readOnlyId, callback) { db.get("readonly2pad:" + readOnlyId, callback); -} +}); /** * returns the padId and readonlyPadId in an object for any id * @param {String} padIdOrReadonlyPadId read only id or real pad id */ -exports.getIds = function(id, callback) { +exports.getIds = thenify(function(id, callback) { if (id.indexOf("r.") == 0) { exports.getPadId(id, function (err, value) { if (ERR(err, callback)) return; @@ -94,4 +95,4 @@ exports.getIds = function(id, callback) { }); }); } -} +}); diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index 28ad4e036..f1f759da6 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -25,8 +25,9 @@ var db = require("./DB").db; var async = require("async"); var groupManager = require("./GroupManager"); var authorManager = require("./AuthorManager"); +const thenify = require("thenify").withCallback; -exports.doesSessionExist = function(sessionID, callback) +exports.doesSessionExist = thenify(function(sessionID, callback) { //check if the database entry of this session exists db.get("session:" + sessionID, function (err, session) @@ -34,12 +35,12 @@ exports.doesSessionExist = function(sessionID, callback) if(ERR(err, callback)) return; callback(null, session != null); }); -} +}); /** * Creates a new session between an author and a group */ -exports.createSession = function(groupID, authorID, validUntil, callback) +exports.createSession = thenify(function(groupID, authorID, validUntil, callback) { var sessionID; @@ -172,9 +173,9 @@ exports.createSession = function(groupID, authorID, validUntil, callback) // return error and sessionID callback(null, {sessionID: sessionID}); }) -} +}); -exports.getSessionInfo = function(sessionID, callback) +exports.getSessionInfo = thenify(function(sessionID, callback) { // check if the database entry of this session exists db.get("session:" + sessionID, function (err, session) @@ -189,12 +190,12 @@ exports.getSessionInfo = function(sessionID, callback) callback(null, session); } }); -} +}); /** * Deletes a session */ -exports.deleteSession = function(sessionID, callback) +exports.deleteSession = thenify(function(sessionID, callback) { var authorID, groupID; var group2sessions, author2sessions; @@ -267,9 +268,9 @@ exports.deleteSession = function(sessionID, callback) if(ERR(err, callback)) return; callback(); }) -} +}); -exports.listSessionsOfGroup = function(groupID, callback) +exports.listSessionsOfGroup = thenify(function(groupID, callback) { groupManager.doesGroupExist(groupID, function(err, exists) { @@ -283,9 +284,9 @@ exports.listSessionsOfGroup = function(groupID, callback) listSessionsWithDBKey("group2sessions:" + groupID, callback); } }); -} +}); -exports.listSessionsOfAuthor = function(authorID, callback) +exports.listSessionsOfAuthor = thenify(function(authorID, callback) { authorManager.doesAuthorExists(authorID, function(err, exists) { @@ -299,7 +300,7 @@ exports.listSessionsOfAuthor = function(authorID, callback) listSessionsWithDBKey("author2sessions:" + authorID, callback); } }); -} +}); // this function is basically the code listSessionsOfAuthor and listSessionsOfGroup has in common function listSessionsWithDBKey (dbkey, callback) diff --git a/src/package.json b/src/package.json index e3cb756a6..fa63b0710 100644 --- a/src/package.json +++ b/src/package.json @@ -57,6 +57,7 @@ "slide": "1.1.6", "socket.io": "2.1.1", "swagger-node-express": "2.1.3", + "thenify": "^3.3.0", "tinycon": "0.0.1", "ueberdb2": "0.4.0", "uglify-js": "2.6.2", From ec5baa2ab337b2bd46375514e114cde141d7e2f3 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Tue, 22 Jan 2019 12:58:26 +0000 Subject: [PATCH 013/183] PadMessageHandler.js: convert two remaining API calls to thenify --- src/node/handler/PadMessageHandler.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index e32d0e61e..2edd9390e 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -38,6 +38,7 @@ var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); var channels = require("channels"); var stats = require('../stats'); var remoteAddress = require("../utils/RemoteAddress").remoteAddress; +const thenify = require("thenify").withCallback; /** * A associative array that saves informations about a session @@ -1788,16 +1789,16 @@ function _getRoomClients(padID) { /** * Get the number of users in a pad */ -exports.padUsersCount = function(padID, callback) { +exports.padUsersCount = thenify(function(padID, callback) { callback(null, { padUsersCount: _getRoomClients(padID).length }); -} +}); /** * Get the list of users in a pad */ -exports.padUsers = function(padID, callback) { +exports.padUsers = thenify(function(padID, callback) { var result = []; var roomClients = _getRoomClients(padID); @@ -1821,6 +1822,6 @@ exports.padUsers = function(padID, callback) { callback(null, {padUsers: result}); }); -} +}); exports.sessioninfos = sessioninfos; From c4f1f837478ab72899c5ead035d0d43069f064ba Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Tue, 22 Jan 2019 13:30:28 +0000 Subject: [PATCH 014/183] APIHandler.js: use promises --- src/node/handler/APIHandler.js | 50 +++++++++++++++------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index 5d731ddc7..07b9b55c5 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -150,7 +150,7 @@ exports.version = version; * @req express request object * @res express response object */ -exports.handle = function(apiVersion, functionName, fields, req, res) +exports.handle = async function(apiVersion, functionName, fields, req, res) { //check if this is a valid apiversion var isKnownApiVersion = false; @@ -194,41 +194,38 @@ exports.handle = function(apiVersion, functionName, fields, req, res) return; } - // sanitize any padIDs before continuing - if (fields["padID"]) { - padManager.sanitizePadId(fields["padID"], function(padId) { - fields["padID"] = padId; - callAPI(apiVersion, functionName, fields, req, res); - }); - } else if (fields["padName"]) { - padManager.sanitizePadId(fields["padName"], function(padId) { - fields["padName"] = padId; - callAPI(apiVersion, functionName, fields, req, res); - }); - } else { - callAPI(apiVersion, functionName, fields, req, res); + try { + // sanitize any padIDs before continuing + if (fields["padID"]) { + fields["padID"] = await padManager.sanitizePadId(fields["padID"]); + } else if (fields["padName"]) { + fields["padName"] = await padManager.sanitizePadId(fields["padName"]); + } + await callAPI(apiVersion, functionName, fields, req, res); + } catch (e) { + ERR(e); } } // calls the api function -function callAPI(apiVersion, functionName, fields, req, res) +async function callAPI(apiVersion, functionName, fields, req, res) { // put the function parameters in an array var functionParams = version[apiVersion][functionName].map(function (field) { return fields[field] }); - // add a callback function to handle the response - functionParams.push(function(err, data) { - if (err == null) { - // no error happened, everything is fine + try { + // call the api function + let data = await api[functionName].apply(this, functionParams); - if (!data) { + if (!data) { data = null; - } + } - res.send({code: 0, message: "ok", data: data}); - } else if (err.name == "apierror") { + res.send({code: 0, message: "ok", data: data}); + } catch (err) { + if (err.name == "apierror") { // parameters were wrong and the api stopped execution, pass the error res.send({code: 1, message: err.message, data: null}); @@ -236,10 +233,7 @@ function callAPI(apiVersion, functionName, fields, req, res) // an unknown error happened res.send({code: 2, message: "internal error", data: null}); - ERR(err); + throw err; } - }); - - // call the api function - api[functionName].apply(this, functionParams); + } } From 5d7162ac9ae5f97ab4822c45557dc1226699ab25 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Tue, 22 Jan 2019 14:58:25 +0000 Subject: [PATCH 015/183] utils/ImportHtml.js: migrate to thenify --- src/node/utils/ImportHtml.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index 04037eab5..d46b715d3 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -18,6 +18,7 @@ var log4js = require('log4js'); var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var contentcollector = require("ep_etherpad-lite/static/js/contentcollector"); var cheerio = require("cheerio"); +const thenify = require("thenify").withCallback; function setPadHTML(pad, html, callback) { @@ -94,4 +95,4 @@ function setPadHTML(pad, html, callback) callback(null); } -exports.setPadHTML = setPadHTML; +exports.setPadHTML = thenify(setPadHTML); From 584e48143014faaa6ef38ed715a5763000c0c2a6 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Tue, 22 Jan 2019 15:48:29 +0000 Subject: [PATCH 016/183] PadMessageHandler.js: migrate to thenify --- src/node/handler/PadMessageHandler.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 2edd9390e..8fc43b156 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -61,7 +61,7 @@ stats.gauge('totalUsers', function() { /** * A changeset queue per pad that is processed by handleUserChanges() */ -var padChannels = new channels.channels(handleUserChanges); +var padChannels = new channels.channels(thenify(handleUserChanges)); /** * Saves the Socket class we need to send and receive data from the client @@ -330,7 +330,7 @@ function handleSaveRevisionMessage(client, message){ * @param msg {Object} the message we're sending * @param sessionID {string} the socketIO session to which we're sending this message */ -exports.handleCustomObjectMessage = function(msg, sessionID, cb) { +exports.handleCustomObjectMessage = thenify(function(msg, sessionID, cb) { if (msg.data.type === "CUSTOM") { if (sessionID){ // a sessionID is targeted: directly to this sessionID @@ -341,7 +341,8 @@ exports.handleCustomObjectMessage = function(msg, sessionID, cb) { } } cb(null, {}); -} +}); + /** * Handles a custom message (sent via HTTP API request) @@ -349,7 +350,7 @@ exports.handleCustomObjectMessage = function(msg, sessionID, cb) { * @param padID {Pad} the pad to which we're sending this message * @param msgString {String} the message we're sending */ -exports.handleCustomMessage = function(padID, msgString, cb) { +exports.handleCustomMessage = thenify(function(padID, msgString, cb) { var time = Date.now(); var msg = { type: 'COLLABROOM', @@ -361,7 +362,7 @@ exports.handleCustomMessage = function(padID, msgString, cb) { socketio.sockets.in(padID).json.send(msg); cb(null, {}); -} +}); /** * Handles a Chat Message From 5ef4a2d1d5eb66186b2c1766d3058ed8d537b624 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Tue, 22 Jan 2019 17:30:33 +0000 Subject: [PATCH 017/183] more thenify in node/utils/* --- src/node/utils/ExportEtherpad.js | 5 +++-- src/node/utils/ExportHtml.js | 7 ++++--- src/node/utils/ImportEtherpad.js | 5 +++-- src/node/utils/TidyHtml.js | 5 +++-- src/node/utils/padDiff.js | 25 +++++++++++++------------ 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/node/utils/ExportEtherpad.js b/src/node/utils/ExportEtherpad.js index a68ab0b2a..84fe80333 100644 --- a/src/node/utils/ExportEtherpad.js +++ b/src/node/utils/ExportEtherpad.js @@ -18,8 +18,9 @@ var async = require("async"); var db = require("../db/DB").db; var ERR = require("async-stacktrace"); +const thenify = require("thenify").withCallback; -exports.getPadRaw = function(padId, callback){ +exports.getPadRaw = thenify(function(padId, callback){ async.waterfall([ function(cb){ db.get("pad:"+padId, cb); @@ -69,4 +70,4 @@ exports.getPadRaw = function(padId, callback){ ], function(err, data){ callback(null, data); }); -} +}); diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js index f001fe452..fb15d867e 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.js @@ -25,6 +25,7 @@ var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); var eejs = require('ep_etherpad-lite/node/eejs'); var _analyzeLine = require('./ExportHelper')._analyzeLine; var _encodeWhitespace = require('./ExportHelper')._encodeWhitespace; +const thenify = require("thenify").withCallback; function getPadHTML(pad, revNum, callback) { @@ -67,7 +68,7 @@ function getPadHTML(pad, revNum, callback) }); } -exports.getPadHTML = getPadHTML; +exports.getPadHTML = thenify(getPadHTML); exports.getHTMLFromAtext = getHTMLFromAtext; function getHTMLFromAtext(pad, atext, authorColors) @@ -459,7 +460,7 @@ function getHTMLFromAtext(pad, atext, authorColors) return pieces.join(''); } -exports.getPadHTMLDocument = function (padId, revNum, callback) +exports.getPadHTMLDocument = thenify(function (padId, revNum, callback) { padManager.getPad(padId, function (err, pad) { @@ -484,7 +485,7 @@ exports.getPadHTMLDocument = function (padId, revNum, callback) }); }); }); -}; +}); // copied from ACE var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; diff --git a/src/node/utils/ImportEtherpad.js b/src/node/utils/ImportEtherpad.js index 1ff8b9b04..138c42936 100644 --- a/src/node/utils/ImportEtherpad.js +++ b/src/node/utils/ImportEtherpad.js @@ -17,8 +17,9 @@ var log4js = require('log4js'); var async = require("async"); var db = require("../db/DB").db; +const thenify = require("thenify").withCallback; -exports.setPadRaw = function(padId, records, callback) +exports.setPadRaw = thenify(function(padId, records, callback) { records = JSON.parse(records); @@ -70,4 +71,4 @@ exports.setPadRaw = function(padId, records, callback) function() { callback(null, true); }); -} +}); diff --git a/src/node/utils/TidyHtml.js b/src/node/utils/TidyHtml.js index 5d4e6ed75..0f7119894 100644 --- a/src/node/utils/TidyHtml.js +++ b/src/node/utils/TidyHtml.js @@ -5,8 +5,9 @@ var log4js = require('log4js'); var settings = require('./Settings'); var spawn = require('child_process').spawn; +const thenify = require("thenify").withCallback; -exports.tidy = function(srcFile, callback) { +exports.tidy = thenify(function(srcFile, callback) { var logger = log4js.getLogger('TidyHtml'); // Don't do anything if Tidy hasn't been enabled @@ -38,4 +39,4 @@ exports.tidy = function(srcFile, callback) { return callback('Tidy died with exit code ' + code); } }); -}; +}); diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js index 33938b19c..a801fdc84 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.js @@ -1,6 +1,7 @@ var Changeset = require("../../static/js/Changeset"); var async = require("async"); var exportHtml = require('./ExportHtml'); +const thenify = require("thenify").withCallback; function PadDiff (pad, fromRev, toRev) { // check parameters @@ -78,7 +79,7 @@ PadDiff.prototype._isClearAuthorship = function(changeset) { return true; }; -PadDiff.prototype._createClearAuthorship = function(rev, callback) { +PadDiff.prototype._createClearAuthorship = thenify(function(rev, callback) { var self = this; this._pad.getInternalRevisionAText(rev, function(err, atext) { if (err) { @@ -92,9 +93,9 @@ PadDiff.prototype._createClearAuthorship = function(rev, callback) { callback(null, changeset); }); -}; +}); -PadDiff.prototype._createClearStartAtext = function(rev, callback) { +PadDiff.prototype._createClearStartAtext = thenify(function(rev, callback) { var self = this; // get the atext of this revision @@ -119,9 +120,9 @@ PadDiff.prototype._createClearStartAtext = function(rev, callback) { callback(null, newAText); }); }); -}; +}); -PadDiff.prototype._getChangesetsInBulk = function(startRev, count, callback) { +PadDiff.prototype._getChangesetsInBulk = thenify(function(startRev, count, callback) { var self = this; // find out which revisions we need @@ -150,7 +151,7 @@ PadDiff.prototype._getChangesetsInBulk = function(startRev, count, callback) { function(err) { callback(err, changesets, authors); }); -}; +}); PadDiff.prototype._addAuthors = function(authors) { var self = this; @@ -163,7 +164,7 @@ PadDiff.prototype._addAuthors = function(authors) { }); }; -PadDiff.prototype._createDiffAtext = function(callback) { +PadDiff.prototype._createDiffAtext = thenify(function(callback) { var self = this; var bulkSize = 100; @@ -237,9 +238,9 @@ PadDiff.prototype._createDiffAtext = function(callback) { } ); }); -}; +}); -PadDiff.prototype.getHtml = function(callback) { +PadDiff.prototype.getHtml = thenify(function(callback) { // cache the html if (this._html != null) { return callback(null, this._html); @@ -283,9 +284,9 @@ PadDiff.prototype.getHtml = function(callback) { function(err) { callback(err, html); }); -}; +}); -PadDiff.prototype.getAuthors = function(callback) { +PadDiff.prototype.getAuthors = thenify(function(callback) { var self = this; // check if html was already produced, if not produce it, this generates the author array at the same time @@ -300,7 +301,7 @@ PadDiff.prototype.getAuthors = function(callback) { } else { callback(null, self._authors); } -}; +}); PadDiff.prototype._extendChangesetWithAuthor = function(changeset, author, apool) { // unpack From 0c2d6625410b81dbd299ed92c5e97735580166de Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Wed, 23 Jan 2019 12:24:53 +0000 Subject: [PATCH 018/183] plugins download and search: converted to Promises Also fixed a bug where the system would make a request to the central server for the plugin list for every search even if the list was already cached. --- src/node/hooks/express/adminplugins.js | 49 +++++++++++++------------- src/static/js/pluginfw/installer.js | 40 +++++++++++---------- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js index 4c94c5780..7cfb160b9 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.js @@ -42,14 +42,10 @@ exports.socketio = function(hook_name, args, cb) { socket.emit("results:installed", {installed: installed}); }); - socket.on("checkUpdates", function() { + socket.on("checkUpdates", async function() { // Check plugins for updates - installer.getAvailablePlugins(/*maxCacheAge:*/ 60 * 10, function(er, results) { - if (er) { - console.warn(er); - socket.emit("results:updatable", {updatable: {}}); - return; - } + try { + let results = await installer.getAvailablePlugins(/*maxCacheAge:*/ 60 * 10); var updatable = _(plugins.plugins).keys().filter(function(plugin) { if (!results[plugin]) return false; @@ -61,27 +57,26 @@ exports.socketio = function(hook_name, args, cb) { }); socket.emit("results:updatable", {updatable: updatable}); - }); + } catch (er) { + console.warn(er); + + socket.emit("results:updatable", {updatable: {}}); + } }); - socket.on("getAvailable", function(query) { - installer.getAvailablePlugins(/*maxCacheAge:*/ false, function(er, results) { - if (er) { - console.error(er); - results = {}; - } - - socket.emit("results:available", results); - }); + socket.on("getAvailable", async function(query) { + try { + let results = await installer.getAvailablePlugins(/*maxCacheAge:*/ false); + socket.emit("results:available", results); + } catch (er) { + console.error(er); + socket.emit("results:available", {}); + } }); - socket.on("search", function(query) { - installer.search(query.searchTerm, /*maxCacheAge:*/ 60 * 10, function(er, results) { - if (er) { - console.error(er); - results = {}; - } - + socket.on("search", async function(query) { + try { + let results = await installer.search(query.searchTerm, /*maxCacheAge:*/ 60 * 10); var res = Object.keys(results) .map(function(pluginName) { return results[pluginName]; @@ -92,7 +87,11 @@ exports.socketio = function(hook_name, args, cb) { res = sortPluginList(res, query.sortBy, query.sortDir) .slice(query.offset, query.offset+query.limit); socket.emit("results:search", {results: res, query: query}); - }); + } catch (er) { + console.error(er); + + socket.emit("results:search", {results: {}, query: query}); + } }); socket.on("install", function(plugin_name) { diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js index dbf8696ed..934d5f036 100644 --- a/src/static/js/pluginfw/installer.js +++ b/src/static/js/pluginfw/installer.js @@ -77,33 +77,35 @@ exports.install = function(plugin_name, cb) { exports.availablePlugins = null; var cacheTimestamp = 0; -exports.getAvailablePlugins = function(maxCacheAge, cb) { - request("https://static.etherpad.org/plugins.json", function(er, response, plugins){ - if (er) return cb && cb(er); +exports.getAvailablePlugins = function(maxCacheAge) { + var nowTimestamp = Math.round(Date.now() / 1000); - if (exports.availablePlugins && maxCacheAge && Math.round(+ new Date / 1000) - cacheTimestamp <= maxCacheAge) { - return cb && cb(null, exports.availablePlugins); + return new Promise(function(resolve, reject) { + // check cache age before making any request + if (exports.availablePlugins && maxCacheAge && (nowTimestamp - cacheTimestamp) <= maxCacheAge) { + return resolve(exports.availablePlugins); } - try { - plugins = JSON.parse(plugins); - } catch (err) { - console.error('error parsing plugins.json:', err); - plugins = []; - } + request("https://static.etherpad.org/plugins.json", function(er, response, plugins) { + if (er) return reject(er); - exports.availablePlugins = plugins; - cacheTimestamp = Math.round(+ new Date / 1000); + try { + plugins = JSON.parse(plugins); + } catch (err) { + console.error('error parsing plugins.json:', err); + plugins = []; + } - cb && cb(null, plugins); + exports.availablePlugins = plugins; + cacheTimestamp = nowTimestamp; + resolve(plugins); + }); }); }; -exports.search = function(searchTerm, maxCacheAge, cb) { - exports.getAvailablePlugins(maxCacheAge, function(er, results) { - if (er) return cb && cb(er); - +exports.search = function(searchTerm, maxCacheAge) { + return exports.getAvailablePlugins(maxCacheAge).then(function(results) { var res = {}; if (searchTerm) { @@ -127,6 +129,6 @@ exports.search = function(searchTerm, maxCacheAge, cb) { res[pluginName] = results[pluginName]; } - cb && cb(null, res); + return res; }); }; From 23a3a079a601ba8d8565420d5268ade11661bc7c Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Wed, 23 Jan 2019 16:21:40 +0000 Subject: [PATCH 019/183] tests.js: remove use of async.js Use real `async` instead of async.js where applicable. The `getPluginTests()` function was never truly async anyway because it only contains calls to synchronous `fs` modules. --- src/node/hooks/express/tests.js | 73 +++++++++++++++------------------ 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index b5c4ca56c..443e9f685 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -1,24 +1,19 @@ var path = require("path") , npm = require("npm") , fs = require("fs") - , async = require("async"); + , util = require("util"); exports.expressCreateServer = function (hook_name, args, cb) { - args.app.get('/tests/frontend/specs_list.js', function(req, res) { - async.parallel({ - coreSpecs: function(callback) { - exports.getCoreTests(callback); - }, - pluginSpecs: function(callback) { - exports.getPluginTests(callback); - } - }, - function(err, results) { - var files = results.coreSpecs; // push the core specs to a file object - files = files.concat(results.pluginSpecs); // add the plugin Specs to the core specs - console.debug("Sent browser the following test specs:", files.sort()); - res.send("var specs_list = " + JSON.stringify(files.sort()) + ";\n"); - }); + args.app.get('/tests/frontend/specs_list.js', async function(req, res) { + let [coreTests, pluginTests] = await Promise.all([ + exports.getCoreTests(), + exports.getPluginTests() + ]); + + // merge the two sets of results + let files = [].concat(coreTests, pluginTests).sort(); + console.debug("Sent browser the following test specs:", files); + res.send("var specs_list = " + JSON.stringify(files) + ";\n"); }); // path.join seems to normalize by default, but we'll just be explicit @@ -63,30 +58,30 @@ exports.expressCreateServer = function (hook_name, args, cb) { }); } -exports.getPluginTests = function(callback) { - var pluginSpecs = []; - var plugins = fs.readdirSync('node_modules'); - plugins.forEach(function(plugin) { - if (fs.existsSync("node_modules/" + plugin + "/static/tests/frontend/specs")) { - // if plugins exists - var specFiles = fs.readdirSync("node_modules/" + plugin + "/static/tests/frontend/specs/"); - async.forEach(specFiles, function(spec) { - // for each specFile push it to pluginSpecs - pluginSpecs.push("/static/plugins/" + plugin + "/static/tests/frontend/specs/" + spec); - }, - function(err) { - // blow up if something bad happens! - }); - } - }); - callback(null, pluginSpecs); +const readdir = util.promisify(fs.readdir); + +exports.getPluginTests = async function(callback) { + const moduleDir = "node_modules/"; + const specPath = "/static/tests/frontend/specs/"; + const staticDir = "/static/plugins/"; + + let pluginSpecs = []; + + let plugins = await readdir(moduleDir); + let promises = plugins + .map(plugin => [ plugin, moduleDir + plugin + specPath] ) + .filter(([plugin, specDir]) => fs.existsSync(specDir)) // check plugin exists + .map(([plugin, specDir]) => { + return readdir(specDir) + .then(specFiles => specFiles.map(spec => { + pluginSpecs.push(staticDir + plugin + specPath + spec); + })); + }); + + return Promise.all(promises).then(() => pluginSpecs); } -exports.getCoreTests = function(callback) { +exports.getCoreTests = function() { // get the core test specs - fs.readdir('tests/frontend/specs', function(err, coreSpecs) { - if (err) { return res.send(500); } - - callback(null, coreSpecs); - }); + return readdir('tests/frontend/specs'); } From 34fdaa4e8c656d13bd2cc33d5396cbe4aa7e8367 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Wed, 23 Jan 2019 16:25:29 +0000 Subject: [PATCH 020/183] db/SecurityManager.js: convert checkAccess() to thenify --- src/node/db/SecurityManager.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index bc8f59426..e9aefcd61 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -27,6 +27,7 @@ var sessionManager = require("./SessionManager"); var settings = require("../utils/Settings"); var log4js = require('log4js'); var authLogger = log4js.getLogger("auth"); +const thenify = require("thenify").withCallback; /** * This function controlls the access to a pad, it checks if the user can access a pad. @@ -36,7 +37,7 @@ var authLogger = log4js.getLogger("auth"); * @param password the password the user has given to access this pad, can be null * @param callback will be called with (err, {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx}) */ -exports.checkAccess = function(padID, sessionCookie, token, password, callback) +exports.checkAccess = thenify(function(padID, sessionCookie, token, password, callback) { var statusObject; @@ -300,4 +301,4 @@ exports.checkAccess = function(padID, sessionCookie, token, password, callback) callback(null, statusObject); }); -}; +}); From d5d28717c47c40a4e91e74d3d8363fa543f08f63 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Wed, 23 Jan 2019 16:29:36 +0000 Subject: [PATCH 021/183] access controls: promisification `getPadAccess()` (src/node/padaccess.js) is now "promise only", resolving to `true` or `false` as appropriate, and throwing an exception if there's an error. The two call sites (padreadonly.js and importexport.js) updated to match. --- src/node/hooks/express/importexport.js | 37 ++++++++--------- src/node/hooks/express/padreadonly.js | 57 ++++++-------------------- src/node/padaccess.js | 15 ++++--- 3 files changed, 39 insertions(+), 70 deletions(-) diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js index fe97aa2f9..ef10e0145 100644 --- a/src/node/hooks/express/importexport.js +++ b/src/node/hooks/express/importexport.js @@ -5,12 +5,11 @@ var importHandler = require('../../handler/ImportHandler'); var padManager = require("../../db/PadManager"); 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', async function(req, res, next) { var types = ["pdf", "doc", "txt", "html", "odt", "etherpad"]; //send a 404 if we don't support this filetype if (types.indexOf(req.params.type) == -1) { - next(); - return; + return next(); } // if abiword is disabled, and this is a format we only support with abiword, output a message @@ -22,28 +21,26 @@ exports.expressCreateServer = function (hook_name, args, cb) { res.header("Access-Control-Allow-Origin", "*"); - hasPadAccess(req, res, function() { + if (await hasPadAccess(req, res)) { console.log('req.params.pad', req.params.pad); - padManager.doesPadExists(req.params.pad, function(err, exists) { - if (!exists) { - return next(); - } + let exists = await padManager.doesPadExists(req.params.pad); + 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 - args.app.post('/p/:pad/import', function(req, res, next) { - hasPadAccess(req, res, function() { - padManager.doesPadExists(req.params.pad, function(err, exists) { - if (!exists) { - return next(); - } + args.app.post('/p/:pad/import', async function(req, res, next) { + if (await hasPadAccess(req, res)) { + let exists = await padManager.doesPadExists(req.params.pad); + if (!exists) { + return next(); + } - importHandler.doImport(req, res, req.params.pad); - }); - }); + importHandler.doImport(req, res, req.params.pad); + } }); } diff --git a/src/node/hooks/express/padreadonly.js b/src/node/hooks/express/padreadonly.js index 77b197c7c..f699e27e9 100644 --- a/src/node/hooks/express/padreadonly.js +++ b/src/node/hooks/express/padreadonly.js @@ -1,57 +1,26 @@ -var async = require('async'); -var ERR = require("async-stacktrace"); var readOnlyManager = require("../../db/ReadOnlyManager"); var hasPadAccess = require("../../padaccess"); var exporthtml = require("../../utils/ExportHtml"); exports.expressCreateServer = function (hook_name, args, cb) { // serve read only pad - args.app.get('/ro/:id', function(req, res) { - var html; - var padId; + args.app.get('/ro/:id', async function(req, res) { - async.series([ - // translate the read only pad to a padId - function(callback) { - readOnlyManager.getPadId(req.params.id, function(err, _padId) { - if(ERR(err, callback)) return; + // translate the read only pad to a padId + let padId = await readOnlyManager.getPadId(req.params.id); + if (padId == null) { + res.status(404).send('404 - Not Found'); + return; + } - padId = _padId; + // we need that to tell hasPadAcess about the pad + req.params.pad = padId; - // we need that to tell hasPadAcess about the pad - req.params.pad = padId; - - callback(); - }); - }, + if (await hasPadAccess(req, res)) { // render the html document - function(callback) { - // return if the there is no padId - if(padId == null) { - callback("notfound"); - return; - } - - hasPadAccess(req, res, function() { - // render the html document - exporthtml.getPadHTMLDocument(padId, null, function(err, _html) { - if(ERR(err, callback)) return; - html = _html; - callback(); - }); - }); - } - ], - function(err) { - // throw any unexpected error - if(err && err != "notfound") - ERR(err); - - if(err == "notfound") - res.status(404).send('404 - Not Found'); - else - res.send(html); - }); + html = await exporthtml.getPadHTMLDocument(padId, null); + res.send(html); + } }); } diff --git a/src/node/padaccess.js b/src/node/padaccess.js index a25ad642c..3449f7d16 100644 --- a/src/node/padaccess.js +++ b/src/node/padaccess.js @@ -1,17 +1,20 @@ -var ERR = require("async-stacktrace"); var securityManager = require('./db/SecurityManager'); // checks for padAccess -module.exports = function (req, res, callback) { - securityManager.checkAccess(req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password, function(err, accessObj) { - if (ERR(err, callback)) return; +module.exports = async function (req, res) { + try { + let accessObj = await securityManager.checkAccess(req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password); if (accessObj.accessStatus === "grant") { // there is access, continue - callback(); + 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; + } } From b699621e5a239292dc576fb2f928075daf76f243 Mon Sep 17 00:00:00 2001 From: muxator Date: Sat, 9 Feb 2019 00:05:21 +0100 Subject: [PATCH 022/183] padurlsanitize.js: invert a condition prior to refactoring Extracted from Ray's work. --- src/node/hooks/express/padurlsanitize.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js index a7fb9f33e..b5ce926ae 100644 --- a/src/node/hooks/express/padurlsanitize.js +++ b/src/node/hooks/express/padurlsanitize.js @@ -12,7 +12,10 @@ exports.expressCreateServer = function (hook_name, args, cb) { } padManager.sanitizePadId(padId, function(sanitizedPadId) { - if (sanitizedPadId != padId) { + if (sanitizedPadId === padId) { + // the pad id was fine, so just render it + next(); + } else { // the pad id was sanitized, so we redirect to the sanitized version var real_url = sanitizedPadId; real_url = encodeURIComponent(real_url); @@ -20,9 +23,6 @@ exports.expressCreateServer = function (hook_name, args, cb) { if ( query ) real_url += '?' + query; res.header('Location', real_url); res.status(302).send('You should be redirected to ' + real_url + ''); - } else { - // the pad id was fine, so just render it - next(); } }); }); From 96d875b4d15b01e1a48c2de8bae08ffacb802c0e Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Wed, 23 Jan 2019 16:36:28 +0000 Subject: [PATCH 023/183] padurlsanitize.js: rewritten to consume promises --- src/node/hooks/express/padurlsanitize.js | 30 ++++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js index b5ce926ae..ad8d3c431 100644 --- a/src/node/hooks/express/padurlsanitize.js +++ b/src/node/hooks/express/padurlsanitize.js @@ -4,26 +4,26 @@ var url = require('url'); exports.expressCreateServer = function (hook_name, args, cb) { // redirects browser to the pad's sanitized url if needed. otherwise, renders the html - args.app.param('pad', function (req, res, next, padId) { + args.app.param('pad', async function (req, res, next, padId) { // ensure the padname is valid and the url doesn't end with a / if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) { res.status(404).send('Such a padname is forbidden'); return; } - padManager.sanitizePadId(padId, function(sanitizedPadId) { - if (sanitizedPadId === padId) { - // the pad id was fine, so just render it - next(); - } else { - // the pad id was sanitized, so we redirect to the sanitized version - var real_url = sanitizedPadId; - real_url = encodeURIComponent(real_url); - var query = url.parse(req.url).query; - if ( query ) real_url += '?' + query; - res.header('Location', real_url); - res.status(302).send('You should be redirected to ' + real_url + ''); - } - }); + let sanitizedPadId = await padManager.sanitizePadId(padId); + + if (sanitizedPadId === padId) { + // the pad id was fine, so just render it + next(); + } else { + // the pad id was sanitized, so we redirect to the sanitized version + var real_url = sanitizedPadId; + real_url = encodeURIComponent(real_url); + var query = url.parse(req.url).query; + if ( query ) real_url += '?' + query; + res.header('Location', real_url); + res.status(302).send('You should be redirected to ' + real_url + ''); + } }); } From 630af9af7d673b8547e72bc8e3296dc7207b6c28 Mon Sep 17 00:00:00 2001 From: muxator Date: Sat, 9 Feb 2019 00:14:53 +0100 Subject: [PATCH 024/183] db/SessionStore.js: call nextTick() only if there is something to do Changed two occurrences of: process.nextTick(function() { if (fn) fn(); }); with if (fn) { process.nextTick(fn); } i.e. such that no function even gets added to the `nextTick` queue unless there's actually a function to be called. Extracted from Ray's work. --- src/node/db/SessionStore.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index 80d35f290..b9686b19b 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -36,18 +36,18 @@ SessionStore.prototype.set = function(sid, sess, fn) { messageLogger.debug('SET ' + sid); db.set("sessionstorage:" + sid, sess); - process.nextTick(function() { - if (fn) fn(); - }); + if (fn) { + process.nextTick(fn); + } }; SessionStore.prototype.destroy = function(sid, fn) { messageLogger.debug('DESTROY ' + sid); db.remove("sessionstorage:" + sid); - process.nextTick(function() { - if (fn) fn(); - }); + if (fn) { + process.nextTick(fn); + } }; SessionStore.prototype.all = function(fn) { From 583ea92aaf8bd9b450572f4feb306f6f411b10da Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Wed, 23 Jan 2019 16:58:43 +0000 Subject: [PATCH 025/183] db/SessionStore.js: do not migrate to Promises. Make optional all(), clear() and length() 1. This module was not migrated to Promises, because it is only used via express-session, which can't actually use promises anyway. 2. all(), clear() and length() depend on the presence of the `db.forEach()` function, which in ueberdb2 doesn't even exist. Fortunately those three methods are optional, so I made their existence conditional on the presence of `db.forEach`. 3. in SessionStore.clear(), replaced a call to db.db.remove() with db.remove() --- src/node/db/SessionStore.js | 76 +++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index b9686b19b..647cbbc8d 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -2,6 +2,9 @@ * Stores session data in the database * Source; https://github.com/edy-b/SciFlowWriter/blob/develop/available_plugins/ep_sciflowwriter/db/DirtyStore.js * This is not used for authors that are created via the API at current + * + * RPB: this module was not migrated to Promises, because it is only used via + * express-session, which can't actually use promises anyway. */ var Store = require('ep_etherpad-lite/node_modules/express-session').Store, @@ -50,39 +53,46 @@ SessionStore.prototype.destroy = function(sid, fn) { } }; -SessionStore.prototype.all = function(fn) { - messageLogger.debug('ALL'); +/* + * 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 = []; + var sessions = []; - db.forEach(function(key, value) { - if (key.substr(0,15) === "sessionstorage:") { - sessions.push(value); - } - }); - fn(null, sessions); -}; - -SessionStore.prototype.clear = function(fn) { - messageLogger.debug('CLEAR'); - - db.forEach(function(key, value) { - if (key.substr(0,15) === "sessionstorage:") { - db.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); + db.forEach(function(key, value) { + if (key.substr(0,15) === "sessionstorage:") { + sessions.push(value); + } + }); + fn(null, sessions); + }; + + SessionStore.prototype.clear = function(fn) { + messageLogger.debug('CLEAR'); + + db.forEach(function(key, value) { + if (key.substr(0,15) === "sessionstorage:") { + db.remove("session:" + key); + } + }); + if (fn) fn(); + }; + + SessionStore.prototype.length = function(fn) { + messageLogger.debug('LENGTH'); + + var i = 0; + + db.forEach(function(key, value) { + if (key.substr(0,15) === "sessionstorage:") { + i++; + } + }); + fn(null, i); + } }; From 29e9f86cad2763b215f2111290b4e7da4ecdf1e8 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Wed, 23 Jan 2019 18:08:47 +0000 Subject: [PATCH 026/183] db/DB.js: add Promise-only API methods Promisified methods: - get() - set() - findKeys() - getSub() - setSub() - remove() - doShutdown() --- src/node/db/DB.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/node/db/DB.js b/src/node/db/DB.js index e6ddd5fb3..c462ac303 100644 --- a/src/node/db/DB.js +++ b/src/node/db/DB.js @@ -23,6 +23,7 @@ var ueberDB = require("ueberdb2"); var settings = require("../utils/Settings"); var log4js = require('log4js'); const thenify = require("thenify").withCallback; +const util = require("util"); // set database settings var db = new ueberDB.database(settings.dbType, settings.dbSettings, null, log4js.getLogger("ueberDB")); @@ -47,6 +48,12 @@ exports.init = thenify(function (callback) { } else { // everything ok exports.db = db; + + // set up Promise-based methods + ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove', 'doShutdown'].forEach(fn => { + exports[fn] = util.promisify(db[fn].bind(db)); + }); + callback(null); } }); From 70a045ad3cad5acd7617ffd46089d1cc9f75e18a Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Fri, 25 Jan 2019 12:56:57 +0000 Subject: [PATCH 027/183] db/GroupManager.js: mostly converted to Promises / async --- src/node/db/GroupManager.js | 341 +++++++++++------------------------- 1 file changed, 102 insertions(+), 239 deletions(-) diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.js index d72efdff1..29a7ff063 100644 --- a/src/node/db/GroupManager.js +++ b/src/node/db/GroupManager.js @@ -21,305 +21,168 @@ var ERR = require("async-stacktrace"); var customError = require("../utils/customError"); var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; -var db = require("./DB").db; -var async = require("async"); +var db = require("./DB"); var padManager = require("./PadManager"); var sessionManager = require("./SessionManager"); const thenify = require("thenify").withCallback; -exports.listAllGroups = thenify(function(callback) { - db.get("groups", function (err, groups) { - if (ERR(err, callback)) return; - - if (groups == null) { - // there are no groups - callback(null, {groupIDs: []}); - - return; - } - - var groupIDs = []; - for (var groupID in groups) { - groupIDs.push(groupID); - } - callback(null, {groupIDs: groupIDs}); - }); -}); - -exports.deleteGroup = thenify(function(groupID, callback) +exports.listAllGroups = async function() { - var group; + let groups = await db.get("groups"); + groups = groups || {}; - async.series([ - // ensure group exists - function (callback) { - // try to get the group entry - db.get("group:" + groupID, function (err, _group) { - if (ERR(err, callback)) return; + let groupIDs = Object.keys(groups); + return { groupIDs }; +} - if (_group == null) { - // group does not exist - callback(new customError("groupID does not exist", "apierror")); - return; - } +exports.deleteGroup = async function(groupID) +{ + let group = await db.get("group:" + groupID); - // group exists, everything is fine - group = _group; - callback(); - }); - }, + // ensure group exists + if (group == null) { + // group does not exist + throw new customError("groupID does not exist", "apierror"); + } - // iterate through all pads of this group and delete them - function(callback) { - // collect all padIDs in an array, that allows us to use async.forEach - var padIDs = []; - for(var i in group.pads) { - padIDs.push(i); - } + // iterate through all pads of this group and delete them + for (let padID in group.pads) { + let pad = await padManager.getPad(padID); + await pad.remove(); + } - // loop through all pads and delete them - async.forEach(padIDs, function(padID, callback) { - padManager.getPad(padID, function(err, pad) { - if (ERR(err, callback)) return; + // iterate through group2sessions and delete all sessions + let group2sessions = await db.get("group2sessions:" + groupID); + let sessions = group2sessions ? group2sessions.sessionsIDs : []; - pad.remove(callback); - }); - }, callback); - }, + // loop through all sessions and delete them + for (let session in sessions) { + await sessionManager.deleteSession(session); + } - // iterate through group2sessions and delete all sessions - function(callback) - { - // try to get the group entry - db.get("group2sessions:" + groupID, function (err, group2sessions) { - if (ERR(err, callback)) return; + // remove group and group2sessions entry + await db.remove("group2sessions:" + groupID); + await db.remove("group:" + groupID); - // skip if there is no group2sessions entry - if (group2sessions == null) { callback(); return } + // unlist the group + let groups = await exports.listAllGroups(); + groups = groups ? groups.groupIDs : []; - // collect all sessions in an array, that allows us to use async.forEach - var sessions = []; - for (var i in group2sessions.sessionsIDs) { - sessions.push(i); - } + let index = groups.indexOf(groupID); - // loop through all sessions and delete them - async.forEach(sessions, function(session, callback) { - sessionManager.deleteSession(session, callback); - }, callback); - }); - }, + if (index === -1) { + // it's not listed - // remove group and group2sessions entry - function(callback) { - db.remove("group2sessions:" + groupID); - db.remove("group:" + groupID); - callback(); - }, + return; + } - // unlist the group - function(callback) { - exports.listAllGroups(function(err, groups) { - if (ERR(err, callback)) return; - groups = groups? groups.groupIDs : []; + // remove from the list + groups.splice(index, 1); - let index = groups.indexOf(groupID); - - if (index === -1) { - // it's not listed - callback(); - - return; - } - - // remove from the list - groups.splice(index, 1); - - // store empty group list - if (groups.length == 0) { - db.set("groups", {}); - callback(); - return; - } - - // regenerate group list - var newGroups = {}; - async.forEach(groups, function(group, cb) { - newGroups[group] = 1; - cb(); - }, - function() { - db.set("groups", newGroups); - callback(); - }); - }); - } - ], - function(err) { - if (ERR(err, callback)) return; - callback(); - }); -}); + // regenerate group list + var newGroups = {}; + groups.forEach(group => newGroups[group] = 1); + await db.set("groups", newGroups); +} +// @TODO: this is the only function still called with a callback exports.doesGroupExist = thenify(function(groupID, callback) { // try to get the group entry - db.get("group:" + groupID, function (err, group) { + db.db.get("group:" + groupID, function (err, group) { if (ERR(err, callback)) return; callback(null, group != null); }); }); -exports.createGroup = thenify(function(callback) +exports.createGroup = async function() { // search for non existing groupID var groupID = "g." + randomString(16); // create the group - db.set("group:" + groupID, {pads: {}}); + await db.set("group:" + groupID, {pads: {}}); // list the group - exports.listAllGroups(function(err, groups) { - if (ERR(err, callback)) return; + let groups = await exports.listAllGroups(); + groups = groups? groups.groupIDs : []; + groups.push(groupID); - groups = groups? groups.groupIDs : []; - groups.push(groupID); + // regenerate group list + var newGroups = {}; + groups.forEach(group => newGroups[group] = 1); + await db.set("groups", newGroups); - // regenerate group list - var newGroups = {}; - async.forEach(groups, function(group, cb) { - newGroups[group] = 1; - cb(); - }, - function() { - db.set("groups", newGroups); - callback(null, {groupID: groupID}); - }); - }); -}); + return { groupID }; +} -exports.createGroupIfNotExistsFor = thenify(function(groupMapper, callback) +exports.createGroupIfNotExistsFor = async function(groupMapper) { // ensure mapper is optional if (typeof groupMapper !== "string") { - callback(new customError("groupMapper is not a string", "apierror")); - return; + throw new customError("groupMapper is not a string", "apierror"); } // try to get a group for this mapper - db.get("mapper2group:" + groupMapper, function(err, groupID) { - function createGroupForMapper(cb) { - exports.createGroup(function(err, responseObj) { - if (ERR(err, cb)) return; + let groupID = await db.get("mapper2group:" + groupMapper); - // create the mapper entry for this group - db.set("mapper2group:" + groupMapper, responseObj.groupID); + if (groupID) { + // there is a group for this mapper + let exists = await exports.doesGroupExist(groupID); - cb(null, responseObj); - }); - } + if (exists) return { groupID }; + } - if (ERR(err, callback)) return; + // hah, the returned group doesn't exist, let's create one + let result = await exports.createGroup(); - if (groupID) { - // there is a group for this mapper - exports.doesGroupExist(groupID, function(err, exists) { - if (ERR(err, callback)) return; + // create the mapper entry for this group + await db.set("mapper2group:" + groupMapper, result.groupID); - if (exists) return callback(null, {groupID: groupID}); + return result; +} - // hah, the returned group doesn't exist, let's create one - createGroupForMapper(callback) - }); - - return; - } - - // there is no group for this mapper, let's create a group - createGroupForMapper(callback) - }); -}); - -exports.createGroupPad = thenify(function(groupID, padName, text, callback) +exports.createGroupPad = async function(groupID, padName, text) { // create the padID - var padID = groupID + "$" + padName; + let padID = groupID + "$" + padName; - async.series([ - // ensure group exists - function (callback) { - exports.doesGroupExist(groupID, function(err, exists) { - if (ERR(err, callback)) return; + // ensure group exists + let groupExists = await exports.doesGroupExist(groupID); - if (!exists) { - // group does not exist - callback(new customError("groupID does not exist", "apierror")); - return; - } + if (!groupExists) { + throw new customError("groupID does not exist", "apierror"); + } - // group exists, everything is fine - callback(); - }); - }, + // ensure pad doesn't exist already + let padExists = await padManager.doesPadExists(padID); - // ensure pad doesn't exist already - function (callback) { - padManager.doesPadExists(padID, function(err, exists) { - if (ERR(err, callback)) return; + if (padExists) { + // pad exists already + throw new customError("padName does already exist", "apierror"); + } - if (exists == true) { - // pad exists already - callback(new customError("padName does already exist", "apierror")); - return; - } + // create the pad + await padManager.getPad(padID, text); - // pad does not exist, everything is fine - callback(); - }); - }, + //create an entry in the group for this pad + await db.setSub("group:" + groupID, ["pads", padID], 1); - // create the pad - function (callback) { - padManager.getPad(padID, text, function(err) { - if (ERR(err, callback)) return; + return { padID }; +} - callback(); - }); - }, - - // create an entry in the group for this pad - function (callback) { - db.setSub("group:" + groupID, ["pads", padID], 1); - callback(); - } - ], - function(err) { - if (ERR(err, callback)) return; - - callback(null, {padID: padID}); - }); -}); - -exports.listPads = thenify(function(groupID, callback) +exports.listPads = async function(groupID) { - exports.doesGroupExist(groupID, function(err, exists) { - if (ERR(err, callback)) return; + let exists = await exports.doesGroupExist(groupID); - // ensure the group exists - if (!exists) { - callback(new customError("groupID does not exist", "apierror")); - return; - } + // ensure the group exists + if (!exists) { + throw new customError("groupID does not exist", "apierror"); + } - // group exists, let's get the pads - db.getSub("group:" + groupID, ["pads"], function(err, result) { - if (ERR(err, callback)) return; + // group exists, let's get the pads + let result = await db.getSub("group:" + groupID, ["pads"]); + let padIDs = Object.keys(result); - var pads = []; - for ( var padId in result ) { - pads.push(padId); - } - callback(null, {padIDs: pads}); - }); - }); -}); + return { padIDs }; +} From 16c4c33f49d5fb3e4fa0124e4d59c0ccce42dfde Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Fri, 25 Jan 2019 13:37:24 +0000 Subject: [PATCH 028/183] db/AuthorManager.js: renamed doesAuthorExists() -> doesAuthorExist() Removed the 's' for consistency with the other `doesFooExist()` manager calls. Retained an alias for plugins that might be using it. --- src/node/db/AuthorManager.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index 0db187706..0676b1b8a 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -40,7 +40,7 @@ exports.getColorPalette = function() { /** * Checks if the author exists */ -exports.doesAuthorExists = thenify(function(authorID, callback) +exports.doesAuthorExist = thenify(function(authorID, callback) { // check if the database entry of this author exists db.get("globalAuthor:" + authorID, function(err, author) { @@ -50,6 +50,9 @@ exports.doesAuthorExists = thenify(function(authorID, callback) }); }); +/* exported for backwards compatibility */ +exports.doesAuthorExists = exports.doesAuthorExist; + /** * Returns the AuthorID for a token. * @param {String} token The token From a875ca6c3089a6a3f02f3fa0403aba302f42daa5 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Fri, 25 Jan 2019 14:53:24 +0000 Subject: [PATCH 029/183] db/SessionManager.js: mostly converted to Promises --- src/node/db/SessionManager.js | 394 ++++++++++++---------------------- 1 file changed, 133 insertions(+), 261 deletions(-) diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index f1f759da6..67d3e3816 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -21,160 +21,106 @@ var ERR = require("async-stacktrace"); var customError = require("../utils/customError"); var randomString = require("../utils/randomstring"); -var db = require("./DB").db; -var async = require("async"); +var db = require("./DB"); var groupManager = require("./GroupManager"); var authorManager = require("./AuthorManager"); const thenify = require("thenify").withCallback; -exports.doesSessionExist = thenify(function(sessionID, callback) +exports.doesSessionExist = async function(sessionID) { //check if the database entry of this session exists - db.get("session:" + sessionID, function (err, session) - { - if(ERR(err, callback)) return; - callback(null, session != null); - }); -}); + let session = await db.get("session:" + sessionID); + return (session !== null); +} /** * Creates a new session between an author and a group */ -exports.createSession = thenify(function(groupID, authorID, validUntil, callback) +exports.createSession = async function(groupID, authorID, validUntil) { - var sessionID; + // check if the group exists + let groupExists = await groupManager.doesGroupExist(groupID); + if (!groupExists) { + throw new customError("groupID does not exist", "apierror"); + } - async.series([ - // check if the group exists - function(callback) - { - groupManager.doesGroupExist(groupID, function(err, exists) - { - if(ERR(err, callback)) return; + // check if the author exists + let authorExists = await authorManager.doesAuthorExist(authorID); + if (!authorExists) { + throw new customError("authorID does not exist", "apierror"); + } - // group does not exist - if (exists == false) { - callback(new customError("groupID does not exist", "apierror")); - } else { - // everything is fine, continue - callback(); - } - }); - }, + // try to parse validUntil if it's not a number + if (typeof validUntil !== "number") { + validUntil = parseInt(validUntil); + } - // check if the author exists - function(callback) - { - authorManager.doesAuthorExists(authorID, function(err, exists) - { - if(ERR(err, callback)) return; + // check it's a valid number + if (isNaN(validUntil)) { + throw new customError("validUntil is not a number", "apierror"); + } - if (exists == false) { - // author does not exist - callback(new customError("authorID does not exist", "apierror")); - } else { - // everything is fine, continue - callback(); - } - }); - }, + // ensure this is not a negative number + if (validUntil < 0) { + throw new customError("validUntil is a negative number", "apierror"); + } - // check validUntil and create the session db entry - function(callback) - { - // check if rev is a number - if (typeof validUntil != "number") - { - // try to parse the number - if (isNaN(parseInt(validUntil))) { - callback(new customError("validUntil is not a number", "apierror")); - return; - } + // ensure this is not a float value + if (!is_int(validUntil)) { + throw new customError("validUntil is a float value", "apierror"); + } - validUntil = parseInt(validUntil); - } + // check if validUntil is in the future + if (validUntil < Math.floor(Date.now() / 1000)) { + throw new customError("validUntil is in the past", "apierror"); + } - // ensure this is not a negative number - if (validUntil < 0) { - callback(new customError("validUntil is a negative number", "apierror")); - return; - } + // generate sessionID + let sessionID = "s." + randomString(16); - // ensure this is not a float value - if (!is_int(validUntil)) { - callback(new customError("validUntil is a float value", "apierror")); - return; - } + // set the session into the database + await db.set("session:" + sessionID, {"groupID": groupID, "authorID": authorID, "validUntil": validUntil}); - // check if validUntil is in the future - if (Math.floor(Date.now()/1000) > validUntil) { - callback(new customError("validUntil is in the past", "apierror")); - return; - } + // get the entry + let group2sessions = await db.get("group2sessions:" + groupID); - // generate sessionID - sessionID = "s." + randomString(16); + /* + * In some cases, the db layer could return "undefined" as well as "null". + * Thus, it is not possible to perform strict null checks on group2sessions. + * In a previous version of this code, a strict check broke session + * management. + * + * See: https://github.com/ether/etherpad-lite/issues/3567#issuecomment-468613960 + */ + if (!group2sessions || !group2sessions.sessionIDs) { + // the entry doesn't exist so far, let's create it + group2sessions = {sessionIDs : {}}; + } - // set the session into the database - db.set("session:" + sessionID, {"groupID": groupID, "authorID": authorID, "validUntil": validUntil}); + // add the entry for this session + group2sessions.sessionIDs[sessionID] = 1; - callback(); - }, + // save the new element back + await db.set("group2sessions:" + groupID, group2sessions); - // set the group2sessions entry - function(callback) - { - // get the entry - db.get("group2sessions:" + groupID, function(err, group2sessions) - { - if(ERR(err, callback)) return; + // get the author2sessions entry + let author2sessions = await db.get("author2sessions:" + authorID); - if (group2sessions == null || group2sessions.sessionIDs == null) { - // the entry doesn't exist so far, let's create it - group2sessions = {sessionIDs : {}}; - } + if (author2sessions == null || author2sessions.sessionIDs == null) { + // the entry doesn't exist so far, let's create it + author2sessions = {sessionIDs : {}}; + } - // add the entry for this session - group2sessions.sessionIDs[sessionID] = 1; + // add the entry for this session + author2sessions.sessionIDs[sessionID] = 1; - // save the new element back - db.set("group2sessions:" + groupID, group2sessions); + //save the new element back + await db.set("author2sessions:" + authorID, author2sessions); - callback(); - }); - }, - - // set the author2sessions entry - function(callback) - { - // get the entry - db.get("author2sessions:" + authorID, function(err, author2sessions) - { - if(ERR(err, callback)) return; - - if (author2sessions == null || author2sessions.sessionIDs == null) { - // the entry doesn't exist so far, let's create it - author2sessions = {sessionIDs : {}}; - } - - // add the entry for this session - author2sessions.sessionIDs[sessionID] = 1; - - //save the new element back - db.set("author2sessions:" + authorID, author2sessions); - - callback(); - }); - } - ], function(err) - { - if(ERR(err, callback)) return; - - // return error and sessionID - callback(null, {sessionID: sessionID}); - }) -}); + return { sessionID }; +} +// @TODO once external dependencies are using Promises exports.getSessionInfo = thenify(function(sessionID, callback) { // check if the database entry of this session exists @@ -195,160 +141,86 @@ exports.getSessionInfo = thenify(function(sessionID, callback) /** * Deletes a session */ -exports.deleteSession = thenify(function(sessionID, callback) +exports.deleteSession = async function(sessionID) { - var authorID, groupID; - var group2sessions, author2sessions; + // ensure that the session exists + let session = await db.get("session:" + sessionID); + if (session == null) { + throw new customError("sessionID does not exist", "apierror"); + } - async.series([ - function(callback) - { - // get the session entry - db.get("session:" + sessionID, function (err, session) - { - if(ERR(err, callback)) return; + // everything is fine, use the sessioninfos + let groupID = session.groupID; + let authorID = session.authorID; - if (session == null) { - // session does not exist - callback(new customError("sessionID does not exist", "apierror")) - } else { - // everything is fine, use the sessioninfos - authorID = session.authorID; - groupID = session.groupID; + // get the group2sessions and author2sessions entries + let group2sessions = await db.get("group2sessions:" + groupID); + let author2sessions = await db.get("author2sessions:" + authorID); - callback(); - } - }); - }, + // remove the session + await db.remove("session:" + sessionID); - // get the group2sessions entry - function(callback) - { - db.get("group2sessions:" + groupID, function (err, _group2sessions) - { - if(ERR(err, callback)) return; - group2sessions = _group2sessions; - callback(); - }); - }, + // remove session from group2sessions + if (group2sessions != null) { // Maybe the group was already deleted + delete group2sessions.sessionIDs[sessionID]; + await db.set("group2sessions:" + groupID, group2sessions); + } - // get the author2sessions entry - function(callback) - { - db.get("author2sessions:" + authorID, function (err, _author2sessions) - { - if(ERR(err, callback)) return; - author2sessions = _author2sessions; - callback(); - }); - }, + // remove session from author2sessions + if (author2sessions != null) { // Maybe the author was already deleted + delete author2sessions.sessionIDs[sessionID]; + await db.set("author2sessions:" + authorID, author2sessions); + } +} - // remove the values from the database - function(callback) - { - //remove the session - db.remove("session:" + sessionID); - - // remove session from group2sessions - if(group2sessions != null) { // Maybe the group was already deleted - delete group2sessions.sessionIDs[sessionID]; - db.set("group2sessions:" + groupID, group2sessions); - } - - // remove session from author2sessions - if(author2sessions != null) { // Maybe the author was already deleted - delete author2sessions.sessionIDs[sessionID]; - db.set("author2sessions:" + authorID, author2sessions); - } - - callback(); - } - ], function(err) - { - if(ERR(err, callback)) return; - callback(); - }) -}); - -exports.listSessionsOfGroup = thenify(function(groupID, callback) +exports.listSessionsOfGroup = async function(groupID) { - groupManager.doesGroupExist(groupID, function(err, exists) - { - if(ERR(err, callback)) return; + // check that the group exists + let exists = await groupManager.doesGroupExist(groupID); + if (!exists) { + throw new customError("groupID does not exist", "apierror"); + } - if (exists == false) { - // group does not exist - callback(new customError("groupID does not exist", "apierror")); - } else { - // everything is fine, continue - listSessionsWithDBKey("group2sessions:" + groupID, callback); - } - }); -}); + let sessions = await listSessionsWithDBKey("group2sessions:" + groupID); + return sessions; +} -exports.listSessionsOfAuthor = thenify(function(authorID, callback) +exports.listSessionsOfAuthor = async function(authorID) { - authorManager.doesAuthorExists(authorID, function(err, exists) - { - if(ERR(err, callback)) return; + // check that the author exists + let exists = await authorManager.doesAuthorExist(authorID) + if (!exists) { + throw new customError("authorID does not exist", "apierror"); + } - if (exists == false) { - // group does not exist - callback(new customError("authorID does not exist", "apierror")); - } else { - // everything is fine, continue - listSessionsWithDBKey("author2sessions:" + authorID, callback); - } - }); -}); + let sessions = await listSessionsWithDBKey("author2sessions:" + authorID); + return sessions; +} // this function is basically the code listSessionsOfAuthor and listSessionsOfGroup has in common -function listSessionsWithDBKey (dbkey, callback) +// required to return null rather than an empty object if there are none +async function listSessionsWithDBKey(dbkey, callback) { - var sessions; + // get the group2sessions entry + let sessionObject = await db.get(dbkey); + let sessions = sessionObject ? sessionObject.sessionIDs : null; - async.series([ - function(callback) - { - // get the group2sessions entry - db.get(dbkey, function(err, sessionObject) - { - if(ERR(err, callback)) return; - sessions = sessionObject ? sessionObject.sessionIDs : null; - callback(); - }); - }, - - function(callback) - { - // collect all sessionIDs in an arrary - var sessionIDs = []; - for (var i in sessions) - { - sessionIDs.push(i); + // iterate through the sessions and get the sessioninfos + for (let sessionID in sessions) { + try { + let sessionInfo = await exports.getSessionInfo(sessionID); + sessions[sessionID] = sessionInfo; + } catch (err) { + if (err == "apierror: sessionID does not exist") { + console.warn(`Found bad session ${sessionID} in ${dbkey}`); + sessions[sessionID] = null; + } else { + throw err; } - - // iterate through the sessions and get the sessioninfos - async.forEach(sessionIDs, function(sessionID, callback) - { - exports.getSessionInfo(sessionID, function(err, sessionInfo) - { - if (err == "apierror: sessionID does not exist") { - console.warn(`Found bad session ${sessionID} in ${dbkey}`); - } else if(ERR(err, callback)) { - return; - } - - sessions[sessionID] = sessionInfo; - callback(); - }); - }, callback); } - ], function(err) - { - if(ERR(err, callback)) return; - callback(null, sessions); - }); + } + + return sessions; } // checks if a number is an int From bf9e3f92b5d3597758cba0f7461b5dec4bfbbc56 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Fri, 25 Jan 2019 15:05:12 +0000 Subject: [PATCH 030/183] db/PadManager.js: renamed doesPadExists() -> doesPadExist() Removed the 's' for consistency with the other `doesFooExist()` manager calls. Retained an alias for plugins that might be using it. --- src/node/db/PadManager.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index 469a7f496..d2747d313 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -188,7 +188,7 @@ exports.listAllPads = thenify(function(cb) }); // checks if a pad exists -exports.doesPadExists = thenify(function(padId, callback) +exports.doesPadExist = thenify(function(padId, callback) { db.get("pad:" + padId, function(err, value) { if (ERR(err, callback)) return; @@ -201,6 +201,9 @@ exports.doesPadExists = thenify(function(padId, callback) }); }); +// alias for backwards compatibility +exports.doesPadExists = exports.doesPadExist; + // returns a sanitized padId, respecting legacy pad id formats function sanitizePadId(padId, callback) { var transform_index = arguments[2] || 0; From eedae98e2fe133adf783c0d297c84c9787375649 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Fri, 25 Jan 2019 15:15:16 +0000 Subject: [PATCH 031/183] db/PadManager.js: convert sanitizePadId() to Promises The function is now iterative rather than recursive. --- src/node/db/PadManager.js | 67 +++++++++++++-------------------------- 1 file changed, 22 insertions(+), 45 deletions(-) diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index d2747d313..e478636b0 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -112,15 +112,6 @@ var padList = { // initialises the all-knowing data structure -/** - * An array of padId transformations. These represent changes in pad name policy over - * time, and allow us to "play back" these changes so legacy padIds can be found. - */ -var padIdTransforms = [ - [/\s+/g, '_'], - [/:+/g, '_'] -]; - /** * Returns a Pad Object with the callback * @param id A String with the id of the pad @@ -204,45 +195,31 @@ exports.doesPadExist = thenify(function(padId, callback) // alias for backwards compatibility exports.doesPadExists = exports.doesPadExist; +/** + * An array of padId transformations. These represent changes in pad name policy over + * time, and allow us to "play back" these changes so legacy padIds can be found. + */ +const padIdTransforms = [ + [/\s+/g, '_'], + [/:+/g, '_'] +]; + // returns a sanitized padId, respecting legacy pad id formats -function sanitizePadId(padId, callback) { - var transform_index = arguments[2] || 0; +exports.sanitizePadId = async function sanitizePadId(padId) { + for (let i = 0, n = padIdTransforms.length; i < n; ++i) { + let exists = await exports.doesPadExist(padId); + + if (exists) { + return padId; + } + + let [from, to] = padIdTransforms[i]; + + padId = padId.replace(from, to); + } // we're out of possible transformations, so just return it - if (transform_index >= padIdTransforms.length) { - callback(padId); - - return; - } - - // check if padId exists - exports.doesPadExists(padId, function(junk, exists) { - if (exists) { - callback(padId); - - return; - } - - // get the next transformation *that's different* - var transformedPadId = padId; - - while(transformedPadId == padId && transform_index < padIdTransforms.length) { - transformedPadId = padId.replace(padIdTransforms[transform_index][0], padIdTransforms[transform_index][1]); - transform_index += 1; - } - - // check the next transform - sanitizePadId(transformedPadId, callback, transform_index); - }); -} - -// sanitizePadId can't use thenify: single arg callback -exports.sanitizePadId = function(padId, callback) { - if (callback) { - return sanitizePadId(padId, callback); - } else { - return new Promise(resolve => sanitizePadId(padId, resolve)); - } + return padId; } exports.isValidPadId = function(padId) From 8f53e4407ed326ed96154b547e786c16a5c31d3f Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Fri, 25 Jan 2019 15:47:25 +0000 Subject: [PATCH 032/183] db/AuthorManager.js: partial conversion to Promises --- src/node/db/AuthorManager.js | 134 +++++++++++++++-------------------- 1 file changed, 59 insertions(+), 75 deletions(-) diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index 0676b1b8a..d204b64ab 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -19,7 +19,7 @@ */ var ERR = require("async-stacktrace"); -var db = require("./DB").db; +var db = require("./DB"); var customError = require("../utils/customError"); var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; const thenify = require("thenify").withCallback; @@ -40,15 +40,12 @@ exports.getColorPalette = function() { /** * Checks if the author exists */ -exports.doesAuthorExist = thenify(function(authorID, callback) +exports.doesAuthorExist = async function(authorID) { - // check if the database entry of this author exists - db.get("globalAuthor:" + authorID, function(err, author) { - if (ERR(err, callback)) return; + let author = await db.get("globalAuthor:" + authorID); - callback(null, author !== null); - }); -}); + return author !== null; +} /* exported for backwards compatibility */ exports.doesAuthorExists = exports.doesAuthorExist; @@ -72,22 +69,18 @@ exports.getAuthor4Token = thenify(function(token, callback) * Returns the AuthorID for a mapper. * @param {String} token The mapper * @param {String} name The name of the author (optional) - * @param {Function} callback callback (err, author) */ -exports.createAuthorIfNotExistsFor = thenify(function(authorMapper, name, callback) +exports.createAuthorIfNotExistsFor = async function(authorMapper, name) { - mapAuthorWithDBKey("mapper2author", authorMapper, function(err, author) { - if (ERR(err, callback)) return; + let author = await mapAuthorWithDBKey("mapper2author", authorMapper); - if (name) { - // set the name of this author - exports.setAuthorName(author.authorID, name); - } + if (name) { + // set the name of this author + await exports.setAuthorName(author.authorID, name); + } - // return the authorID - callback(null, author); - }); -}); + return author; +}; /** * Returns the AuthorID for a mapper. We can map using a mapperkey, @@ -96,10 +89,10 @@ exports.createAuthorIfNotExistsFor = thenify(function(authorMapper, name, callba * @param {String} mapper The mapper * @param {Function} callback callback (err, author) */ -function mapAuthorWithDBKey (mapperkey, mapper, callback) +let mapAuthorWithDBKey = thenify(function mapAuthorWithDBKey (mapperkey, mapper, callback) { // try to map to an author - db.get(mapperkey + ":" + mapper, function(err, author) { + db.db.get(mapperkey + ":" + mapper, function(err, author) { if (ERR(err, callback)) return; if (author === null) { @@ -108,7 +101,7 @@ function mapAuthorWithDBKey (mapperkey, mapper, callback) if (ERR(err, callback)) return; // create the token2author relation - db.set(mapperkey + ":" + mapper, author.authorID); + db.db.set(mapperkey + ":" + mapper, author.authorID); // return the author callback(null, author); @@ -119,12 +112,12 @@ function mapAuthorWithDBKey (mapperkey, mapper, callback) // there is an author with this mapper // update the timestamp of this author - db.setSub("globalAuthor:" + author, ["timestamp"], Date.now()); + db.db.setSub("globalAuthor:" + author, ["timestamp"], Date.now()); // return the author callback(null, {authorID: author}); }); -} +}); /** * Internal function that creates the database entry for an author @@ -143,7 +136,7 @@ exports.createAuthor = thenify(function(name, callback) }; // set the global author db entry - db.set("globalAuthor:" + author, authorObj); + db.db.set("globalAuthor:" + author, authorObj); callback(null, {authorID: author}); }); @@ -155,7 +148,7 @@ exports.createAuthor = thenify(function(name, callback) */ exports.getAuthor = thenify(function(author, callback) { - db.get("globalAuthor:" + author, callback); + db.db.get("globalAuthor:" + author, callback); }); /** @@ -165,7 +158,7 @@ exports.getAuthor = thenify(function(author, callback) */ exports.getAuthorColorId = thenify(function(author, callback) { - db.getSub("globalAuthor:" + author, ["colorId"], callback); + db.db.getSub("globalAuthor:" + author, ["colorId"], callback); }); /** @@ -176,7 +169,7 @@ exports.getAuthorColorId = thenify(function(author, callback) */ exports.setAuthorColorId = thenify(function(author, colorId, callback) { - db.setSub("globalAuthor:" + author, ["colorId"], colorId, callback); + db.db.setSub("globalAuthor:" + author, ["colorId"], colorId, callback); }); /** @@ -186,7 +179,7 @@ exports.setAuthorColorId = thenify(function(author, colorId, callback) */ exports.getAuthorName = thenify(function(author, callback) { - db.getSub("globalAuthor:" + author, ["name"], callback); + db.db.getSub("globalAuthor:" + author, ["name"], callback); }); /** @@ -197,15 +190,14 @@ exports.getAuthorName = thenify(function(author, callback) */ exports.setAuthorName = thenify(function(author, name, callback) { - db.setSub("globalAuthor:" + author, ["name"], name, callback); + db.db.setSub("globalAuthor:" + author, ["name"], name, callback); }); /** * Returns an array of all pads this author contributed to * @param {String} author The id of the author - * @param {Function} callback (optional) */ -exports.listPadsOfAuthor = thenify(function(authorID, callback) +exports.listPadsOfAuthor = async function(authorID) { /* There are two other places where this array is manipulated: * (1) When the author is added to a pad, the author object is also updated @@ -213,52 +205,45 @@ exports.listPadsOfAuthor = thenify(function(authorID, callback) */ // get the globalAuthor - db.get("globalAuthor:" + authorID, function(err, author) { - if (ERR(err, callback)) return; + let author = await db.get("globalAuthor:" + authorID); - if (author === null) { - // author does not exist - callback(new customError("authorID does not exist", "apierror")); + if (author === null) { + // author does not exist + throw new customError("authorID does not exist", "apierror"); + } - return; - } + // everything is fine, return the pad IDs + let padIDs = Object.keys(author.padIDs || {}); - // everything is fine, return the pad IDs - var pads = []; - - if (author.padIDs != null) { - for (var padId in author.padIDs) { - pads.push(padId); - } - } - - callback(null, {padIDs: pads}); - }); -}); + return { padIDs }; +} /** * Adds a new pad to the list of contributions * @param {String} author The id of the author * @param {String} padID The id of the pad the author contributes to */ -exports.addPad = function(authorID, padID) +exports.addPad = async function(authorID, padID) { // get the entry - db.get("globalAuthor:" + authorID, function(err, author) { - if (ERR(err)) return; - if (author === null) return; + let author = await db.get("globalAuthor:" + authorID); - if (author.padIDs == null) { - // the entry doesn't exist so far, let's create it - author.padIDs = {}; - } + if (author === null) return; - // add the entry for this pad - author.padIDs[padID] = 1; // anything, because value is not used + /* + * ACHTUNG: padIDs can also be undefined, not just null, so it is not possible + * to perform a strict check here + */ + if (!author.padIDs) { + // the entry doesn't exist so far, let's create it + author.padIDs = {}; + } - // save the new element back - db.set("globalAuthor:" + authorID, author); - }); + // add the entry for this pad + author.padIDs[padID] = 1; // anything, because value is not used + + // save the new element back + db.set("globalAuthor:" + authorID, author); } /** @@ -266,16 +251,15 @@ exports.addPad = function(authorID, padID) * @param {String} author The id of the author * @param {String} padID The id of the pad the author contributes to */ -exports.removePad = function(authorID, padID) +exports.removePad = async function(authorID, padID) { - db.get("globalAuthor:" + authorID, function(err, author) { - if (ERR(err)) return; - if (author === null) return; + let author = await db.get("globalAuthor:" + authorID); - if (author.padIDs !== null) { - // remove pad from author - delete author.padIDs[padID]; - db.set("globalAuthor:" + authorID, author); - } - }); + if (author === null) return; + + if (author.padIDs !== null) { + // remove pad from author + delete author.padIDs[padID]; + db.set("globalAuthor:" + authorID, author); + } } From 1b6430ae9fa62a6d6bdc090bb94947d98dac5e07 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Fri, 25 Jan 2019 18:07:01 +0000 Subject: [PATCH 033/183] db/PadMessageHandler.js: partial conversion to Promises Converted those functions that API.js still depends on, and others that at this point are never called via the nodeback mechanism. --- src/node/handler/PadMessageHandler.js | 63 ++++++++++++--------------- 1 file changed, 27 insertions(+), 36 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 8fc43b156..77d7dc6c5 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -330,7 +330,7 @@ function handleSaveRevisionMessage(client, message){ * @param msg {Object} the message we're sending * @param sessionID {string} the socketIO session to which we're sending this message */ -exports.handleCustomObjectMessage = thenify(function(msg, sessionID, cb) { +exports.handleCustomObjectMessage = async function(msg, sessionID) { if (msg.data.type === "CUSTOM") { if (sessionID){ // a sessionID is targeted: directly to this sessionID @@ -340,9 +340,7 @@ exports.handleCustomObjectMessage = thenify(function(msg, sessionID, cb) { socketio.sockets.in(msg.data.payload.padId).json.send(msg); } } - cb(null, {}); -}); - +} /** * Handles a custom message (sent via HTTP API request) @@ -809,7 +807,7 @@ function handleUserChanges(data, cb) }); } -exports.updatePadClients = function(pad, callback) +exports.updatePadClients = thenify(function(pad, callback) { // skip this if no-one is on this pad var roomClients = _getRoomClients(pad.id); @@ -886,7 +884,7 @@ exports.updatePadClients = function(pad, callback) callback ); },callback); -} +}); /** * Copied from the Etherpad Source Code. Don't know what this method does excatly... @@ -1503,7 +1501,7 @@ function handleChangesetRequest(client, message) * Tries to rebuild the getChangestInfo function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144 */ -function getChangesetInfo(padId, startNum, endNum, granularity, callback) +let getChangesetInfo = thenify(function getChangesetInfo(padId, startNum, endNum, granularity, callback) { var forwardsChangesets = []; var backwardsChangesets = []; @@ -1643,13 +1641,13 @@ function getChangesetInfo(padId, startNum, endNum, granularity, callback) start: startNum, granularity: granularity }); }); -} +}); /** * Tries to rebuild the getPadLines function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263 */ -function getPadLines(padId, revNum, callback) +let getPadLines = thenify(function getPadLines(padId, revNum, callback) { var atext; var result = {}; @@ -1693,13 +1691,13 @@ function getPadLines(padId, revNum, callback) callback(null, result); }); -} +}); /** * Tries to rebuild the composePadChangeset function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241 */ -function composePadChangesets(padId, startNum, endNum, callback) +let composePadChangesets = thenify(function(padId, startNum, endNum, callback) { var pad; var changesets = {}; @@ -1772,7 +1770,7 @@ function composePadChangesets(padId, startNum, endNum, callback) callback(null, changeset); }); -} +}); function _getRoomClients(padID) { var roomClients = []; @@ -1790,39 +1788,32 @@ function _getRoomClients(padID) { /** * Get the number of users in a pad */ -exports.padUsersCount = thenify(function(padID, callback) { - callback(null, { +exports.padUsersCount = function(padID) { + return { padUsersCount: _getRoomClients(padID).length - }); -}); + } +} /** * Get the list of users in a pad */ -exports.padUsers = thenify(function(padID, callback) { - var result = []; +exports.padUsers = async function(padID) { - var roomClients = _getRoomClients(padID); + let padUsers = []; + let roomClients = _getRoomClients(padID); - async.forEach(roomClients, function(roomClient, callback) { - var s = sessioninfos[roomClient.id]; + for (let i = 0, n = roomClients.length; i < n; ++i) { + let roomClient = roomClients[i]; + + let s = sessioninfos[roomClient.id]; if (s) { - authorManager.getAuthor(s.author, function(err, author) { - if (ERR(err, callback)) return; - - author.id = s.author; - result.push(author); - callback(); - }); - } else { - callback(); + let author = await authorManager.getAuthor(s.author); + author.id = s.author; + padUsers.push(author); } - }, - function(err) { - if (ERR(err, callback)) return; + } - callback(null, {padUsers: result}); - }); -}); + return { padUsers }; +} exports.sessioninfos = sessioninfos; From e7dc0766fdcc3c8477e97c83393fcc2683fab72e Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Fri, 25 Jan 2019 18:08:34 +0000 Subject: [PATCH 034/183] db/API.js: complete conversion to Promises This patch also contains significant refactoring relating to error checking of arguments supplied to the functions (e.g. rev) facilitated by use of `throw` instead of nodeback errors. --- src/node/db/API.js | 914 +++++++++++++++++---------------------------- 1 file changed, 336 insertions(+), 578 deletions(-) diff --git a/src/node/db/API.js b/src/node/db/API.js index df2e25f10..f3274a22a 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -18,7 +18,6 @@ * limitations under the License. */ -var ERR = require("async-stacktrace"); var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var customError = require("../utils/customError"); var padManager = require("./PadManager"); @@ -27,13 +26,11 @@ var readOnlyManager = require("./ReadOnlyManager"); var groupManager = require("./GroupManager"); var authorManager = require("./AuthorManager"); var sessionManager = require("./SessionManager"); -var async = require("async"); var exportHtml = require("../utils/ExportHtml"); var exportTxt = require("../utils/ExportTxt"); var importHtml = require("../utils/ImportHtml"); var cleanText = require("./Pad").cleanText; var PadDiff = require("../utils/padDiff"); -const thenify = require("thenify").withCallback; /**********************/ /**GROUP FUNCTIONS*****/ @@ -104,14 +101,11 @@ Example returns: } */ -exports.getAttributePool = thenify(function(padID, callback) +exports.getAttributePool = async function(padID) { - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; - - callback(null, {pool: pad.pool}); - }); -}); + let pad = await getPadSafe(padID, true); + return { pool: pad.pool }; +} /** getRevisionChangeset (padID, [rev]) @@ -126,61 +120,32 @@ Example returns: } */ -exports.getRevisionChangeset = thenify(function(padID, rev, callback) +exports.getRevisionChangeset = async function(padID, rev) { - // check if rev is a number - if (rev !== undefined && typeof rev !== "number") { - // try to parse the number - if (isNaN(parseInt(rev))) { - callback(new customError("rev is not a number", "apierror")); - return; - } - - rev = parseInt(rev); - } - - // ensure this is not a negative number - if (rev !== undefined && rev < 0) { - callback(new customError("rev is not a negative number", "apierror")); - return; - } - - // ensure this is not a float value - if (rev !== undefined && !is_int(rev)) { - callback(new customError("rev is a float value", "apierror")); - return; + // try to parse the revision number + if (rev !== undefined) { + rev = checkValidRev(rev); } // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; + let pad = await getPadSafe(padID, true); + let head = pad.getHeadRevisionNumber(); - // the client asked for a special revision - if (rev !== undefined) { - // check if this is a valid revision - if (rev > pad.getHeadRevisionNumber()) { - callback(new customError("rev is higher than the head revision of the pad", "apierror")); - return; - } + // the client asked for a special revision + if (rev !== undefined) { - // get the changeset for this revision - pad.getRevisionChangeset(rev, function(err, changeset) { - if (ERR(err, callback)) return; - - callback(null, changeset); - }) - - return; + // check if this is a valid revision + if (rev > head) { + throw new customError("rev is higher than the head revision of the pad", "apierror"); } - // the client wants the latest changeset, lets return it to him - pad.getRevisionChangeset(pad.getHeadRevisionNumber(), function(err, changeset) { - if (ERR(err, callback)) return; + // get the changeset for this revision + return pad.getRevisionChangeset(rev); + } - callback(null, changeset); - }) - }); -}); + // the client wants the latest changeset, lets return it to him + return pad.getRevisionChangeset(head); +} /** getText(padID, [rev]) returns the text of a pad @@ -190,60 +155,34 @@ Example returns: {code: 0, message:"ok", data: {text:"Welcome Text"}} {code: 1, message:"padID does not exist", data: null} */ -exports.getText = thenify(function(padID, rev, callback) +exports.getText = async function(padID, rev) { - // check if rev is a number - if (rev !== undefined && typeof rev !== "number") { - // try to parse the number - if (isNaN(parseInt(rev))) { - callback(new customError("rev is not a number", "apierror")); - return; - } - - rev = parseInt(rev); - } - - // ensure this is not a negative number - if (rev !== undefined && rev < 0) { - callback(new customError("rev is a negative number", "apierror")); - return; - } - - // ensure this is not a float value - if (rev !== undefined && !is_int(rev)) { - callback(new customError("rev is a float value", "apierror")); - return; + // try to parse the revision number + if (rev !== undefined) { + rev = checkValidRev(rev); } // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; + let pad = await getPadSafe(padID, true); + let head = pad.getHeadRevisionNumber(); - // the client asked for a special revision - if (rev !== undefined) { - // check if this is a valid revision - if (rev > pad.getHeadRevisionNumber()) { - callback(new customError("rev is higher than the head revision of the pad", "apierror")); - return; - } + // the client asked for a special revision + if (rev !== undefined) { - // get the text of this revision - pad.getInternalRevisionAText(rev, function(err, atext) { - if (ERR(err, callback)) return; - - var data = {text: atext.text}; - - callback(null, data); - }) - - return; + // check if this is a valid revision + if (rev > head) { + throw new customError("rev is higher than the head revision of the pad", "apierror"); } - // the client wants the latest text, lets return it to him - var padText = exportTxt.getTXTFromAtext(pad, pad.atext); - callback(null, {"text": padText}); - }); -}); + // get the text of this revision + let text = await pad.getInternalRevisionAText(rev); + return { text }; + } + + // the client wants the latest text, lets return it to him + let text = exportTxt.getTXTFromAtext(pad, pad.atext); + return { text }; +} /** setText(padID, text) sets the text of a pad @@ -254,25 +193,22 @@ Example returns: {code: 1, message:"padID does not exist", data: null} {code: 1, message:"text too long", data: null} */ -exports.setText = thenify(function(padID, text, callback) +exports.setText = async function(padID, text) { // text is required if (typeof text !== "string") { - callback(new customError("text is not a string", "apierror")); - return; + throw new customError("text is not a string", "apierror"); } // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; + let pad = await getPadSafe(padID, true); - // set the text - pad.setText(text); + // set the text + pad.setText(text); - // update the clients on the pad - padMessageHandler.updatePadClients(pad, callback); - }); -}); + // update the clients on the pad + padMessageHandler.updatePadClients(pad); +} /** appendText(padID, text) appends text to a pad @@ -283,24 +219,20 @@ Example returns: {code: 1, message:"padID does not exist", data: null} {code: 1, message:"text too long", data: null} */ -exports.appendText = thenify(function(padID, text, callback) +exports.appendText = async function(padID, text) { // text is required if (typeof text !== "string") { - callback(new customError("text is not a string", "apierror")); - return; + throw new customError("text is not a string", "apierror"); } - // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; + // get and update the pad + let pad = await getPadSafe(padID, true); + pad.appendText(text); - pad.appendText(text); - - // update the clients on the pad - padMessageHandler.updatePadClients(pad, callback); - }); -}); + // update the clients on the pad + padMessageHandler.updatePadClients(pad); +} /** getHTML(padID, [rev]) returns the html of a pad @@ -310,60 +242,30 @@ Example returns: {code: 0, message:"ok", data: {text:"Welcome Text"}} {code: 1, message:"padID does not exist", data: null} */ -exports.getHTML = thenify(function(padID, rev, callback) +exports.getHTML = async function(padID, rev) { - if (rev !== undefined && typeof rev !== "number") { - if (isNaN(parseInt(rev))) { - callback(new customError("rev is not a number", "apierror")); - return; + if (rev !== undefined) { + rev = checkValidRev(rev); + } + + let pad = await getPadSafe(padID, true); + + // the client asked for a special revision + if (rev !== undefined) { + // check if this is a valid revision + let head = pad.getHeadRevisionNumber(); + if (rev > head) { + throw new customError("rev is higher than the head revision of the pad", "apierror"); } - - rev = parseInt(rev); } - if (rev !== undefined && rev < 0) { - callback(new customError("rev is a negative number", "apierror")); - return; - } + // get the html of this revision + html = await exportHtml.getPadHTML(pad, rev); - if (rev !== undefined && !is_int(rev)) { - callback(new customError("rev is a float value", "apierror")); - return; - } - - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; - - // the client asked for a special revision - if (rev !== undefined) { - // check if this is a valid revision - if (rev > pad.getHeadRevisionNumber()) { - callback(new customError("rev is higher than the head revision of the pad", "apierror")); - return; - } - - // get the html of this revision - exportHtml.getPadHTML(pad, rev, function(err, html) { - if (ERR(err, callback)) return; - html = "" +html; // adds HTML head - html += ""; - var data = {html: html}; - callback(null, data); - }); - - return; - } - - // the client wants the latest text, lets return it to him - exportHtml.getPadHTML(pad, undefined, function(err, html) { - if (ERR(err, callback)) return; - html = "" +html; // adds HTML head - html += ""; - var data = {html: html}; - callback(null, data); - }); - }); -}); + // wrap the HTML + html = "" + html + ""; + return { html }; +} /** setHTML(padID, html) sets the text of a pad based on HTML @@ -373,30 +275,26 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.setHTML = thenify(function(padID, html, callback) +exports.setHTML = async function(padID, html) { - // html is required + // html string is required if (typeof html !== "string") { - callback(new customError("html is not a string", "apierror")); - return; + throw new customError("html is not a string", "apierror"); } // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; + let pad = await getPadSafe(padID, true); - // add a new changeset with the new html to the pad - importHtml.setPadHTML(pad, cleanText(html), function(e) { - if (e) { - callback(new customError("HTML is malformed", "apierror")); - return; - } + // add a new changeset with the new html to the pad + try { + await importHtml.setPadHTML(pad, cleanText(html)); + } catch (e) { + throw new customError("HTML is malformed", "apierror"); + } - // update the clients on the pad - padMessageHandler.updatePadClients(pad, callback); - }); - }); -}); + // update the clients on the pad + padMessageHandler.updatePadClients(pad); +}; /******************/ /**CHAT FUNCTIONS */ @@ -414,52 +312,43 @@ Example returns: {code: 1, message:"padID does not exist", data: null} */ -exports.getChatHistory = thenify(function(padID, start, end, callback) +exports.getChatHistory = async function(padID, start, end) { if (start && end) { if (start < 0) { - callback(new customError("start is below zero", "apierror")); - return; + throw new customError("start is below zero", "apierror"); } if (end < 0) { - callback(new customError("end is below zero", "apierror")); - return; + throw new customError("end is below zero", "apierror"); } if (start > end) { - callback(new customError("start is higher than end", "apierror")); - return; + throw new customError("start is higher than end", "apierror"); } } // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; + let pad = await getPadSafe(padID, true); - var chatHead = pad.chatHead; + var chatHead = pad.chatHead; - // fall back to getting the whole chat-history if a parameter is missing - if (!start || !end) { + // fall back to getting the whole chat-history if a parameter is missing + if (!start || !end) { start = 0; end = pad.chatHead; - } + } - if (start > chatHead) { - callback(new customError("start is higher than the current chatHead", "apierror")); - return; - } - if (end > chatHead) { - callback(new customError("end is higher than the current chatHead", "apierror")); - return; - } + if (start > chatHead) { + throw new customError("start is higher than the current chatHead", "apierror"); + } + if (end > chatHead) { + throw new customError("end is higher than the current chatHead", "apierror"); + } - // the the whole message-log and return it to the client - pad.getChatMessages(start, end, - function(err, msgs) { - if (ERR(err, callback)) return; - callback(null, {messages: msgs}); - }); - }); -}); + // the the whole message-log and return it to the client + let messages = await pad.getChatMessages(start, end); + + return { messages }; +} /** appendChatMessage(padID, text, authorID, time), creates a chat message for the pad id, time is a timestamp @@ -469,25 +358,23 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.appendChatMessage = thenify(function(padID, text, authorID, time, callback) +exports.appendChatMessage = async function(padID, text, authorID, time) { // text is required if (typeof text !== "string") { - callback(new customError("text is not a string", "apierror")); - return; + throw new customError("text is not a string", "apierror"); } - // if time is not an integer value + // if time is not an integer value set time to current timestamp if (time === undefined || !is_int(time)) { - // set time to current timestamp time = Date.now(); } + // @TODO - missing getPadSafe() call ? + // save chat message to database and send message to all connected clients padMessageHandler.sendChatMessageToPadClients(time, authorID, text, padID); - - callback(); -}); +} /*****************/ /**PAD FUNCTIONS */ @@ -501,15 +388,12 @@ Example returns: {code: 0, message:"ok", data: {revisions: 56}} {code: 1, message:"padID does not exist", data: null} */ -exports.getRevisionsCount = thenify(function(padID, callback) +exports.getRevisionsCount = async function(padID) { // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; - - callback(null, {revisions: pad.getHeadRevisionNumber()}); - }); -}); + let pad = await getPadSafe(padID, true); + return { revisions: pad.getHeadRevisionNumber() }; +} /** getSavedRevisionsCount(padID) returns the number of saved revisions of this pad @@ -519,15 +403,12 @@ Example returns: {code: 0, message:"ok", data: {savedRevisions: 42}} {code: 1, message:"padID does not exist", data: null} */ -exports.getSavedRevisionsCount = thenify(function(padID, callback) +exports.getSavedRevisionsCount = async function(padID) { // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; - - callback(null, {savedRevisions: pad.getSavedRevisionsNumber()}); - }); -}); + let pad = await getPadSafe(padID, true); + return { savedRevisions: pad.getSavedRevisionsNumber() }; +} /** listSavedRevisions(padID) returns the list of saved revisions of this pad @@ -537,15 +418,12 @@ Example returns: {code: 0, message:"ok", data: {savedRevisions: [2, 42, 1337]}} {code: 1, message:"padID does not exist", data: null} */ -exports.listSavedRevisions = thenify(function(padID, callback) +exports.listSavedRevisions = async function(padID) { // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; - - callback(null, {savedRevisions: pad.getSavedRevisionsList()}); - }); -}); + let pad = await getPadSafe(padID, true); + return { savedRevisions: pad.getSavedRevisionsList() }; +} /** saveRevision(padID) returns the list of saved revisions of this pad @@ -555,54 +433,29 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.saveRevision = thenify(function(padID, rev, callback) +exports.saveRevision = async function(padID, rev) { // check if rev is a number - if (rev !== undefined && typeof rev !== "number") { - // try to parse the number - if (isNaN(parseInt(rev))) { - callback(new customError("rev is not a number", "apierror")); - return; - } - - rev = parseInt(rev); - } - - // ensure this is not a negative number - if (rev !== undefined && rev < 0) { - callback(new customError("rev is a negative number", "apierror")); - return; - } - - // ensure this is not a float value - if (rev !== undefined && !is_int(rev)) { - callback(new customError("rev is a float value", "apierror")); - return; + if (rev !== undefined) { + rev = checkValidRev(rev); } // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; + let pad = await getPadSafe(padID, true); + let head = pad.getHeadRevisionNumber(); - // the client asked for a special revision - if (rev !== undefined) { - // check if this is a valid revision - if (rev > pad.getHeadRevisionNumber()) { - callback(new customError("rev is higher than the head revision of the pad", "apierror")); - return; - } - } else { - rev = pad.getHeadRevisionNumber(); + // the client asked for a special revision + if (rev !== undefined) { + if (rev > head) { + throw new customError("rev is higher than the head revision of the pad", "apierror"); } + } else { + rev = pad.getHeadRevisionNumber(); + } - authorManager.createAuthor('API', function(err, author) { - if (ERR(err, callback)) return; - - pad.addSavedRevision(rev, author.authorID, 'Saved through API call'); - callback(); - }); - }); -}); + let author = authorManager.createAuthor('API'); + pad.addSavedRevision(rev, author.authorID, 'Saved through API call'); +} /** getLastEdited(padID) returns the timestamp of the last revision of the pad @@ -612,18 +465,13 @@ Example returns: {code: 0, message:"ok", data: {lastEdited: 1340815946602}} {code: 1, message:"padID does not exist", data: null} */ -exports.getLastEdited = thenify(function(padID, callback) +exports.getLastEdited = async function(padID) { // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; - - pad.getLastEdit(function(err, value) { - if (ERR(err, callback)) return; - callback(null, {lastEdited: value}); - }); - }); -}); + let pad = await getPadSafe(padID, true); + let lastEdited = await pad.getLastEdit(); + return { lastEdited }; +} /** createPad(padName [, text]) creates a new pad in this group @@ -633,28 +481,23 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"pad does already exist", data: null} */ -exports.createPad = thenify(function(padID, text, callback) +exports.createPad = async function(padID, text) { if (padID) { // ensure there is no $ in the padID if (padID.indexOf("$") !== -1) { - callback(new customError("createPad can't create group pads", "apierror")); - return; + throw new customError("createPad can't create group pads", "apierror"); } // check for url special characters if (padID.match(/(\/|\?|&|#)/)) { - callback(new customError("malformed padID: Remove special characters", "apierror")); - return; + throw new customError("malformed padID: Remove special characters", "apierror"); } } // create pad - getPadSafe(padID, false, text, function(err) { - if (ERR(err, callback)) return; - callback(); - }); -}); + await getPadSafe(padID, false, text); +} /** deletePad(padID) deletes a pad @@ -664,14 +507,12 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.deletePad = thenify(function(padID, callback) +exports.deletePad = async function(padID) { - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; + let pad = await getPadSafe(padID, true); + await pad.remove(); +} - pad.remove(callback); - }); -}); /** restoreRevision(padID, [rev]) Restores revision from past as new changeset @@ -680,90 +521,66 @@ exports.deletePad = thenify(function(padID, callback) {code:0, message:"ok", data:null} {code: 1, message:"padID does not exist", data: null} */ -exports.restoreRevision = thenify(function(padID, rev, callback) +exports.restoreRevision = async function(padID, rev) { // check if rev is a number - if (rev !== undefined && typeof rev !== "number") { - // try to parse the number - if (isNaN(parseInt(rev))) { - callback(new customError("rev is not a number", "apierror")); - return; - } - - rev = parseInt(rev); - } - - // ensure this is not a negative number - if (rev !== undefined && rev < 0) { - callback(new customError("rev is a negative number", "apierror")); - return; - } - - // ensure this is not a float value - if (rev !== undefined && !is_int(rev)) { - callback(new customError("rev is a float value", "apierror")); - return; + if (rev === undefined) { + throw new customeError("rev is not defined", "apierror"); } + rev = checkValidRev(rev); // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; + let pad = await getPadSafe(padID, true); + // check if this is a valid revision + if (rev > pad.getHeadRevisionNumber()) { + throw new customError("rev is higher than the head revision of the pad", "apierror"); + } - // check if this is a valid revision - if (rev > pad.getHeadRevisionNumber()) { - callback(new customError("rev is higher than the head revision of the pad", "apierror")); - return; + let atext = await pad.getInternalRevisionAText(rev); + + var oldText = pad.text(); + atext.text += "\n"; + + function eachAttribRun(attribs, func) { + var attribsIter = Changeset.opIterator(attribs); + var textIndex = 0; + var newTextStart = 0; + var newTextEnd = atext.text.length; + while (attribsIter.hasNext()) { + var op = attribsIter.next(); + var nextIndex = textIndex + op.chars; + if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { + func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); + } + textIndex = nextIndex; } + } - pad.getInternalRevisionAText(rev, function(err, atext) { - if (ERR(err, callback)) return; - - var oldText = pad.text(); - atext.text += "\n"; - function eachAttribRun(attribs, func) { - var attribsIter = Changeset.opIterator(attribs); - var textIndex = 0; - var newTextStart = 0; - var newTextEnd = atext.text.length; - while (attribsIter.hasNext()) { - var op = attribsIter.next(); - var nextIndex = textIndex + op.chars; - if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { - func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); - } - textIndex = nextIndex; - } - } - - // create a new changeset with a helper builder object - var builder = Changeset.builder(oldText.length); - - // assemble each line into the builder - eachAttribRun(atext.attribs, function(start, end, attribs) { - builder.insert(atext.text.substring(start, end), attribs); - }); - - var lastNewlinePos = oldText.lastIndexOf('\n'); - if (lastNewlinePos < 0) { - builder.remove(oldText.length - 1, 0); - } else { - builder.remove(lastNewlinePos, oldText.match(/\n/g).length - 1); - builder.remove(oldText.length - lastNewlinePos - 1, 0); - } - - var changeset = builder.toString(); - - // append the changeset - pad.appendRevision(changeset); - - // update the clients on the pad - padMessageHandler.updatePadClients(pad, function() {}); - callback(null, null); - }); + // create a new changeset with a helper builder object + var builder = Changeset.builder(oldText.length); + // assemble each line into the builder + eachAttribRun(atext.attribs, function(start, end, attribs) { + builder.insert(atext.text.substring(start, end), attribs); }); -}); + + var lastNewlinePos = oldText.lastIndexOf('\n'); + if (lastNewlinePos < 0) { + builder.remove(oldText.length - 1, 0); + } else { + builder.remove(lastNewlinePos, oldText.match(/\n/g).length - 1); + builder.remove(oldText.length - lastNewlinePos - 1, 0); + } + + var changeset = builder.toString(); + + // append the changeset + pad.appendRevision(changeset); + + // update the clients on the pad + padMessageHandler.updatePadClients(pad); +} /** copyPad(sourceID, destinationID[, force=false]) copies a pad. If force is true, @@ -774,14 +591,11 @@ Example returns: {code: 0, message:"ok", data: {padID: destinationID}} {code: 1, message:"padID does not exist", data: null} */ -exports.copyPad = thenify(function(sourceID, destinationID, force, callback) +exports.copyPad = async function(sourceID, destinationID, force) { - getPadSafe(sourceID, true, function(err, pad) { - if (ERR(err, callback)) return; - - pad.copy(destinationID, force, callback); - }); -}); + let pad = await getPadSafe(sourceID, true); + await pad.copy(destinationID, force); +} /** movePad(sourceID, destinationID[, force=false]) moves a pad. If force is true, @@ -792,17 +606,13 @@ Example returns: {code: 0, message:"ok", data: {padID: destinationID}} {code: 1, message:"padID does not exist", data: null} */ -exports.movePad = thenify(function(sourceID, destinationID, force, callback) +exports.movePad = async function(sourceID, destinationID, force) { - getPadSafe(sourceID, true, function(err, pad) { - if (ERR(err, callback)) return; + let pad = await getPadSafe(sourceID, true); + await pad.copy(destinationID, force); + await pad.remove(); +} - pad.copy(destinationID, force, function(err) { - if (ERR(err, callback)) return; - pad.remove(callback); - }); - }); -}); /** getReadOnlyLink(padID) returns the read only link of a pad @@ -811,19 +621,16 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.getReadOnlyID = thenify(function(padID, callback) +exports.getReadOnlyID = async function(padID) { // we don't need the pad object, but this function does all the security stuff for us - getPadSafe(padID, true, function(err) { - if (ERR(err, callback)) return; + await getPadSafe(padID, true); - // get the readonlyId - readOnlyManager.getReadOnlyId(padID, function(err, readOnlyId) { - if (ERR(err, callback)) return; - callback(null, {readOnlyID: readOnlyId}); - }); - }); -}); + // get the readonlyId + let readOnlyID = await readOnlyManager.getReadOnlyId(padID); + + return { readOnlyID }; +} /** getPadID(roID) returns the padID of a pad based on the readonlyID(roID) @@ -833,20 +640,16 @@ Example returns: {code: 0, message:"ok", data: {padID: padID}} {code: 1, message:"padID does not exist", data: null} */ -exports.getPadID = thenify(function(roID, callback) +exports.getPadID = async function(roID) { // get the PadId - readOnlyManager.getPadId(roID, function(err, retrievedPadID) { - if (ERR(err, callback)) return; + let padID = await readOnlyManager.getPadId(roID); + if (padID === null) { + throw new customError("padID does not exist", "apierror"); + } - if (retrievedPadID === null) { - callback(new customError("padID does not exist", "apierror")); - return; - } - - callback(null, {padID: retrievedPadID}); - }); -}); + return { padID }; +} /** setPublicStatus(padID, publicStatus) sets a boolean for the public status of a pad @@ -856,28 +659,22 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.setPublicStatus = thenify(function(padID, publicStatus, callback) +exports.setPublicStatus = async function(padID, publicStatus) { // ensure this is a group pad - if (padID && padID.indexOf("$") === -1) { - callback(new customError("You can only get/set the publicStatus of pads that belong to a group", "apierror")); - return; - } + checkGroupPad(padID, "publicStatus"); // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; + let pad = await getPadSafe(padID, true); - // convert string to boolean - if (typeof publicStatus === "string") - publicStatus = publicStatus == "true" ? true : false; + // convert string to boolean + if (typeof publicStatus === "string") { + publicStatus = (publicStatus.toLowerCase() === "true"); + } - // set the password - pad.setPublicStatus(publicStatus); - - callback(); - }); -}); + // set the password + pad.setPublicStatus(publicStatus); +} /** getPublicStatus(padID) return true of false @@ -887,21 +684,15 @@ Example returns: {code: 0, message:"ok", data: {publicStatus: true}} {code: 1, message:"padID does not exist", data: null} */ -exports.getPublicStatus = thenify(function(padID, callback) +exports.getPublicStatus = async function(padID) { // ensure this is a group pad - if (padID && padID.indexOf("$") == -1) { - callback(new customError("You can only get/set the publicStatus of pads that belong to a group", "apierror")); - return; - } + checkGroupPad(padID, "publicStatus"); // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; - - callback(null, {publicStatus: pad.getPublicStatus()}); - }); -}); + let pad = await getPadSafe(padID, true); + return { publicStatus: pad.getPublicStatus() }; +} /** setPassword(padID, password) returns ok or a error message @@ -911,24 +702,17 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.setPassword = thenify(function(padID, password, callback) +exports.setPassword = async function(padID, password) { // ensure this is a group pad - if (padID && padID.indexOf("$") == -1) { - callback(new customError("You can only get/set the password of pads that belong to a group", "apierror")); - return; - } + checkGroupPad(padID, "password"); // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; + let pad = await getPadSafe(padID, true); - // set the password - pad.setPassword(password == "" ? null : password); - - callback(); - }); -}); + // set the password + pad.setPassword(password == "" ? null : password); +} /** isPasswordProtected(padID) returns true or false @@ -938,21 +722,15 @@ Example returns: {code: 0, message:"ok", data: {passwordProtection: true}} {code: 1, message:"padID does not exist", data: null} */ -exports.isPasswordProtected = thenify(function(padID, callback) +exports.isPasswordProtected = async function(padID) { // ensure this is a group pad - if (padID && padID.indexOf("$") == -1) { - callback(new customError("You can only get/set the password of pads that belong to a group", "apierror")); - return; - } + checkGroupPad(padID, "password"); // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; - - callback(null, {isPasswordProtected: pad.isPasswordProtected()}); - }); -}); + let pad = await getPadSafe(padID, true); + return { isPasswordProtected: pad.isPasswordProtected() }; +} /** listAuthorsOfPad(padID) returns an array of authors who contributed to this pad @@ -962,15 +740,13 @@ Example returns: {code: 0, message:"ok", data: {authorIDs : ["a.s8oes9dhwrvt0zif", "a.akf8finncvomlqva"]} {code: 1, message:"padID does not exist", data: null} */ -exports.listAuthorsOfPad = thenify(function(padID, callback) +exports.listAuthorsOfPad = async function(padID) { // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; - - callback(null, {authorIDs: pad.getAllAuthors()}); - }); -}); + let pad = await getPadSafe(padID, true); + let authorIDs = pad.getAllAuthors(); + return { authorIDs }; +} /** sendClientsMessage(padID, msg) sends a message to all clients connected to the @@ -995,15 +771,10 @@ Example returns: {code: 1, message:"padID does not exist"} */ -exports.sendClientsMessage = thenify(function(padID, msg, callback) { - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) { - return; - } - - padMessageHandler.handleCustomMessage(padID, msg, callback); - } ); -}); +exports.sendClientsMessage = async function(padID, msg) { + let pad = await getPadSafe(padID, true); + padMessageHandler.handleCustomMessage(padID, msg); +} /** checkToken() returns ok when the current api token is valid @@ -1013,10 +784,9 @@ Example returns: {"code":0,"message":"ok","data":null} {"code":4,"message":"no or wrong API Key","data":null} */ -exports.checkToken = thenify(function(callback) +exports.checkToken = async function() { - callback(); -}); +} /** getChatHead(padID) returns the chatHead (last number of the last chat-message) of the pad @@ -1026,15 +796,12 @@ Example returns: {code: 0, message:"ok", data: {chatHead: 42}} {code: 1, message:"padID does not exist", data: null} */ -exports.getChatHead = thenify(function(padID, callback) +exports.getChatHead = async function(padID) { // get the pad - getPadSafe(padID, true, function(err, pad) { - if (ERR(err, callback)) return; - - callback(null, {chatHead: pad.chatHead}); - }); -}); + let pad = await getPadSafe(padID, true); + return { chatHead: pad.chatHead }; +} /** createDiffHTML(padID, startRev, endRev) returns an object of diffs from 2 points in a pad @@ -1044,68 +811,31 @@ Example returns: {"code":0,"message":"ok","data":{"html":"Welcome to Etherpad!

    This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!

    Get involved with Etherpad at http://etherpad.org
    aw

    ","authors":["a.HKIv23mEbachFYfH",""]}} {"code":4,"message":"no or wrong API Key","data":null} */ -exports.createDiffHTML = thenify(function(padID, startRev, endRev, callback) { - // check if startRev is a number - if (startRev !== undefined && typeof startRev !== "number") { - // try to parse the number - if (isNaN(parseInt(startRev))) { - callback({stop: "startRev is not a number"}); - return; - } +exports.createDiffHTML = async function(padID, startRev, endRev) { - startRev = parseInt(startRev, 10); + // check if startRev is a number + if (startRev !== undefined) { + startRev = checkValidRev(startRev); } // check if endRev is a number - if (endRev !== undefined && typeof endRev !== "number") { - // try to parse the number - if (isNaN(parseInt(endRev))) { - callback({stop: "endRev is not a number"}); - return; - } - - endRev = parseInt(endRev, 10); + if (endRev !== undefined) { + endRev = checkValidRev(endRev); } // get the pad - getPadSafe(padID, true, function(err, pad) { - if (err) { - return callback(err); - } + let pad = await getPadSafe(padID, true); + try { + var padDiff = new PadDiff(pad, startRev, endRev); + } catch (e) { + throw { stop: e.message }; + } - try { - var padDiff = new PadDiff(pad, startRev, endRev); - } catch(e) { - return callback({stop:e.message}); - } - var html, authors; + let html = await padDiff.getHtml(); + let authors = await padDiff.getAuthors(); - async.series([ - function(callback) { - padDiff.getHtml(function(err, _html) { - if (err) { - return callback(err); - } - - html = _html; - callback(); - }); - }, - function(callback) { - padDiff.getAuthors(function(err, _authors) { - if (err) { - return callback(err); - } - - authors = _authors; - callback(); - }); - } - ], function(err) { - callback(err, {html: html, authors: authors}) - }); - }); -}); + return { html, authors }; +} /******************************/ /** INTERNAL HELPER FUNCTIONS */ @@ -1114,42 +844,70 @@ exports.createDiffHTML = thenify(function(padID, startRev, endRev, callback) { // checks if a number is an int function is_int(value) { - return (parseFloat(value) == parseInt(value)) && !isNaN(value) + return (parseFloat(value) == parseInt(value, 10)) && !isNaN(value) } // gets a pad safe -function getPadSafe(padID, shouldExist, text, callback) +async function getPadSafe(padID, shouldExist, text) { - if (typeof text === "function") { - callback = text; - text = null; - } - // check if padID is a string if (typeof padID !== "string") { - callback(new customError("padID is not a string", "apierror")); - return; + throw new customError("padID is not a string", "apierror"); } // check if the padID maches the requirements if (!padManager.isValidPadId(padID)) { - callback(new customError("padID did not match requirements", "apierror")); - return; + throw new customError("padID did not match requirements", "apierror"); } // check if the pad exists - padManager.doesPadExists(padID, function(err, exists) { - if (ERR(err, callback)) return; + let exists = await padManager.doesPadExists(padID); + if (!exists && shouldExist) { // does not exist, but should - if (exists == false && shouldExist == true) { - callback(new customError("padID does not exist", "apierror")); - } else if (exists == true && shouldExist == false) { - // does exist, but shouldn't - callback(new customError("padID does already exist", "apierror")); - } else { - // pad exists, let's get it - padManager.getPad(padID, text, callback); - } - }); + throw new customError("padID does not exist", "apierror"); + } + + if (exists && !shouldExist) { + // does exist, but shouldn't + throw new customError("padID does already exist", "apierror"); + } + + // 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 +function checkValidRev(rev) +{ + if (typeof rev !== "number") { + rev = parseInt(rev, 10); + } + + // check if rev is a number + if (isNaN(rev)) { + throw new customError("rev is not a number", "apierror"); + } + + // ensure this is not a negative number + if (rev < 0) { + throw new customError("rev is not a negative number", "apierror"); + } + + // ensure this is not a float value + if (!is_int(rev)) { + throw new customError("rev is a float value", "apierror"); + } + + return rev; +} + +// checks if a padID is part of a group +function checkGroupPad(padID, field) +{ + // ensure this is a group pad + if (padID && padID.indexOf("$") === -1) { + throw new customError(`You can only get/set the ${field} of pads that belong to a group`, "apierror"); + } } From 7709fd46e53141fc6fdfd534dab48f8c88e4f75e Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Sat, 26 Jan 2019 23:52:02 +0000 Subject: [PATCH 035/183] utility scripts: converted to use the Promise interface --- bin/checkAllPads.js | 178 ++++++++++++++++-------------------------- bin/checkPad.js | 160 +++++++++++++++---------------------- bin/deletePad.js | 70 ++++++----------- bin/extractPadData.js | 112 +++++++++++--------------- 4 files changed, 202 insertions(+), 318 deletions(-) diff --git a/bin/checkAllPads.js b/bin/checkAllPads.js index c4467fa79..0d4e8bb8d 100644 --- a/bin/checkAllPads.js +++ b/bin/checkAllPads.js @@ -7,128 +7,88 @@ if (process.argv.length != 2) { process.exit(1); } -// initialize the variables -var db, settings, padManager; -var npm = require('../src/node_modules/npm'); -var async = require('../src/node_modules/async'); - -var Changeset = require('../src/static/js/Changeset'); - -async.series([ - // load npm - function(callback) { - npm.load({}, callback); - }, - - // load modules - function(callback) { - settings = require('../src/node/utils/Settings'); - db = require('../src/node/db/DB'); +// load and initialize NPM +let npm = require('../src/node_modules/npm'); +npm.load({}, async function() { + try { // initialize the database - db.init(callback); - }, + let settings = require('../src/node/utils/Settings'); + let db = require('../src/node/db/DB'); + await db.init(); - // load pads - function (callback) { - padManager = require('../src/node/db/PadManager'); + // load modules + let Changeset = require('../src/static/js/Changeset'); + let padManager = require('../src/node/db/PadManager'); - padManager.listAllPads(function(err, res) { - padIds = res.padIDs; - callback(err); - }); - }, + // get all pads + let res = await padManager.listAllPads(); - function (callback) { - async.forEach(padIds, function(padId, callback) { - padManager.getPad(padId, function(err, pad) { - if (err) { - callback(err); + for (let padId of res.padIDs) { + + let pad = await padManager.getPad(padId); + + // check if the pad has a pool + if (pad.pool === undefined) { + console.error("[" + pad.id + "] Missing attribute pool"); + continue; + } + + // create an array with key kevisions + // key revisions always save the full pad atext + let head = pad.getHeadRevisionNumber(); + let keyRevisions = []; + for (let rev = 0; rev < head; rev += 100) { + keyRevisions.push(rev); + } + + // run through all key revisions + for (let keyRev of keyRevisions) { + + // create an array of revisions we need till the next keyRevision or the End + var revisionsNeeded = []; + for (let rev = keyRev ; rev <= keyRev + 100 && rev <= head; rev++) { + revisionsNeeded.push(rev); } - // check if the pad has a pool - if (pad.pool === undefined ) { - console.error("[" + pad.id + "] Missing attribute pool"); - callback(); + // this array will hold all revision changesets + var revisions = []; - return; + // run through all needed revisions and get them from the database + for (let revNum of revisionsNeeded) { + let revision = await db.get("pad:" + pad.id + ":revs:" + revNum); + revisions[revNum] = revision; } - // create an array with key kevisions - // key revisions always save the full pad atext - var head = pad.getHeadRevisionNumber(); - var keyRevisions = []; - for (var i = 0; i < head; i += 100) { - keyRevisions.push(i); + // check if the revision exists + if (revisions[keyRev] == null) { + console.error("[" + pad.id + "] Missing revision " + keyRev); + continue; } - // run through all key revisions - async.forEachSeries(keyRevisions, function(keyRev, callback) { - // create an array of revisions we need till the next keyRevision or the End - var revisionsNeeded = []; + // check if there is a atext in the keyRevisions + if (revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) { + console.error("[" + pad.id + "] Missing atext in revision " + keyRev); + continue; + } - for(var i = keyRev; i <= keyRev + 100 && i <= head; i++) { - revisionsNeeded.push(i); + let apool = pad.pool; + let atext = revisions[keyRev].meta.atext; + + for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) { + try { + let cs = revisions[rev].changeset; + atext = Changeset.applyToAText(cs, atext, apool); + } catch (e) { + console.error("[" + pad.id + "] Bad changeset at revision " + i + " - " + e.message); } - - // this array will hold all revision changesets - var revisions = []; - - // run through all needed revisions and get them from the database - async.forEach(revisionsNeeded, function(revNum, callback) { - db.db.get("pad:" + pad.id + ":revs:" + revNum, function(err, revision) { - revisions[revNum] = revision; - callback(err); - }); - }, - - function(err) { - if (err) { - callback(err); - return; - } - - // check if the revision exists - if (revisions[keyRev] == null) { - console.error("[" + pad.id + "] Missing revision " + keyRev); - callback(); - return; - } - - // check if there is a atext in the keyRevisions - if (revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) { - console.error("[" + pad.id + "] Missing atext in revision " + keyRev); - callback(); - return; - } - - var apool = pad.pool; - var atext = revisions[keyRev].meta.atext; - - for(var i = keyRev + 1; i <= keyRev + 100 && i <= head; i++) { - try { - // console.log("[" + pad.id + "] check revision " + i); - var cs = revisions[i].changeset; - atext = Changeset.applyToAText(cs, atext, apool); - } catch(e) { - console.error("[" + pad.id + "] Bad changeset at revision " + i + " - " + e.message); - callback(); - return; - } - } - - callback(); - }); - }, callback); - }); - }, callback); - } -], -function (err) { - if (err) { - throw err; - } else { - console.log("finished"); - process.exit(0); + } + } + console.log("finished"); + process.exit(0); + } + } catch (err) { + console.trace(err); + process.exit(1); } }); diff --git a/bin/checkPad.js b/bin/checkPad.js index 4d864d106..c6a3a1971 100644 --- a/bin/checkPad.js +++ b/bin/checkPad.js @@ -7,121 +7,89 @@ if (process.argv.length != 3) { process.exit(1); } -//get the padID -var padId = process.argv[2]; +// get the padID +const padId = process.argv[2]; -// initialize the variables -var db, settings, padManager; -var npm = require('../src/node_modules/npm'); -var async = require('../src/node_modules/async'); +// load and initialize NPM; +let npm = require('../src/node_modules/npm'); +npm.load({}, async function() { -var Changeset = require('ep_etherpad-lite/static/js/Changeset'); + try { + // initialize database + let settings = require('../src/node/utils/Settings'); + let db = require('../src/node/db/DB'); + await db.init(); -async.series([ - // load npm - function(callback) { - npm.load({}, function(er) { - callback(er); - }); - }, + // load modules + let Changeset = require('ep_etherpad-lite/static/js/Changeset'); + let padManager = require('../src/node/db/PadManager'); - // load modules - function(callback) { - settings = require('../src/node/utils/Settings'); - db = require('../src/node/db/DB'); + let exists = await padManager.doesPadExists(padId); + if (!exists) { + console.error("Pad does not exist"); + process.exit(1); + } - // initialize the database - db.init(callback); - }, + // get the pad + let pad = await padManager.getPad(padId); - // get the pad - function (callback) { - padManager = require('../src/node/db/PadManager'); - - padManager.doesPadExists(padId, function(err, exists) { - if (!exists) { - console.error("Pad does not exist"); - process.exit(1); - } - - padManager.getPad(padId, function(err, _pad) { - pad = _pad; - callback(err); - }); - }); - }, - - function (callback) { // create an array with key revisions // key revisions always save the full pad atext - var head = pad.getHeadRevisionNumber(); - var keyRevisions = []; - for (var i = 0; i < head; i += 100) { - keyRevisions.push(i); + let head = pad.getHeadRevisionNumber(); + let keyRevisions = []; + for (let rev = 0; rev < head; rev += 100) { + keyRevisions.push(rev); } // run through all key revisions - async.forEachSeries(keyRevisions, function(keyRev, callback) { + for (let keyRev of keyRevisions) { + // create an array of revisions we need till the next keyRevision or the End - var revisionsNeeded = []; - for(var i = keyRev; i <= keyRev + 100 && i <= head; i++) { - revisionsNeeded.push(i); + let revisionsNeeded = []; + for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) { + revisionsNeeded.push(rev); } // this array will hold all revision changesets var revisions = []; // run through all needed revisions and get them from the database - async.forEach(revisionsNeeded, function(revNum, callback) { - db.db.get("pad:" + padId + ":revs:" + revNum, function(err, revision) { - revisions[revNum] = revision; - callback(err); - }); - }, - function(err) { - if (err) { - callback(err); - return; + for (let revNum of revisionsNeeded) { + let revision = await db.get("pad:" + padId + ":revs:" + revNum); + revisions[revNum] = revision; + } + + // check if the pad has a pool + if (pad.pool === undefined ) { + console.error("Attribute pool is missing"); + process.exit(1); + } + + // check if there is an atext in the keyRevisions + if (revisions[keyRev] === undefined || revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) { + console.error("No atext in key revision " + keyRev); + continue; + } + + let apool = pad.pool; + let atext = revisions[keyRev].meta.atext; + + for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) { + try { + // console.log("check revision " + rev); + let cs = revisions[rev].changeset; + atext = Changeset.applyToAText(cs, atext, apool); + } catch(e) { + console.error("Bad changeset at revision " + rev + " - " + e.message); + continue; } + } + console.log("finished"); + process.exit(0); + } - // check if the pad has a pool - if (pad.pool === undefined) { - console.error("Attribute pool is missing"); - process.exit(1); - } - - // check if there is an atext in the keyRevisions - if (revisions[keyRev] === undefined || revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) { - console.error("No atext in key revision " + keyRev); - callback(); - return; - } - - var apool = pad.pool; - var atext = revisions[keyRev].meta.atext; - - for (var i = keyRev + 1; i <= keyRev + 100 && i <= head; i++) { - try { - // console.log("check revision " + i); - var cs = revisions[i].changeset; - atext = Changeset.applyToAText(cs, atext, apool); - } catch(e) { - console.error("Bad changeset at revision " + i + " - " + e.message); - callback(); - return; - } - } - - callback(); - }); - }, callback); - } -], -function (err) { - if(err) { - throw err; - } else { - console.log("finished"); - process.exit(0); + } catch (e) { + console.trace(e); + process.exit(1); } }); diff --git a/bin/deletePad.js b/bin/deletePad.js index 31856d382..e9e273e3e 100644 --- a/bin/deletePad.js +++ b/bin/deletePad.js @@ -9,55 +9,33 @@ if (process.argv.length != 3) { } // get the padID -var padId = process.argv[2]; +let padId = process.argv[2]; -var db, padManager, pad, settings; -var neededDBValues = ["pad:"+padId]; +let npm = require('../src/node_modules/npm'); -var npm = require('../src/node_modules/npm'); -var async = require('../src/node_modules/async'); - -async.series([ - // load npm - function(callback) { - npm.load({}, function(er) { - if (er) { - console.error("Could not load NPM: " + er) - process.exit(1); - } else { - callback(); - } - }); - }, - - // load modules - function(callback) { - settings = require('../src/node/utils/Settings'); - db = require('../src/node/db/DB'); - callback(); - }, - - // initialize the database - function (callback) { - db.init(callback); - }, - - // delete the pad and its links - function (callback) { - padManager = require('../src/node/db/PadManager'); - - padManager.removePad(padId, function(err){ - callback(err); - }); - - callback(); +npm.load({}, async function(er) { + if (er) { + console.error("Could not load NPM: " + er) + process.exit(1); } -], -function (err) { - if(err) { - throw err; - } else { + + try { + let settings = require('../src/node/utils/Settings'); + let db = require('../src/node/db/DB'); + await db.init(); + + padManager = require('../src/node/db/PadManager'); + await padManager.removePad(padId); + console.log("Finished deleting padId: " + padId); - process.exit(); + process.exit(0); + + } catch (e) { + if (err.name === "apierror") { + console.error(e); + } else { + console.trace(e); + } + process.exit(1); } }); diff --git a/bin/extractPadData.js b/bin/extractPadData.js index ac22e11b7..cce297f71 100644 --- a/bin/extractPadData.js +++ b/bin/extractPadData.js @@ -10,88 +10,66 @@ if (process.argv.length != 3) { } // get the padID -var padId = process.argv[2]; +let padId = process.argv[2]; -var db, dirty, padManager, pad, settings; -var neededDBValues = ["pad:"+padId]; +let npm = require('../src/node_modules/npm'); -var npm = require('../node_modules/ep_etherpad-lite/node_modules/npm'); -var async = require('../node_modules/ep_etherpad-lite/node_modules/async'); +npm.load({}, async function(er) { + if (er) { + console.error("Could not load NPM: " + er) + process.exit(1); + } -async.series([ - // load npm - function(callback) { - npm.load({}, function(er) { - if (er) { - console.error("Could not load NPM: " + er) - process.exit(1); - } else { - callback(); - } - }) - }, + try { + // initialize database + let settings = require('../src/node/utils/Settings'); + let db = require('../src/node/db/DB'); + await db.init(); - // load modules - function(callback) { - settings = require('../node_modules/ep_etherpad-lite/node/utils/Settings'); - db = require('../node_modules/ep_etherpad-lite/node/db/DB'); - dirty = require('../node_modules/ep_etherpad-lite/node_modules/ueberDB/node_modules/dirty')(padId + ".db"); - callback(); - }, + // load extra modules + let dirtyDB = require('../src/node_modules/dirty'); + let padManager = require('../src/node/db/PadManager'); + let util = require('util'); - // initialize the database - function (callback) { - db.init(callback); - }, + // initialize output database + let dirty = dirtyDB(padId + '.db'); - // get the pad - function (callback) { - padManager = require('../node_modules/ep_etherpad-lite/node/db/PadManager'); + // Promise wrapped get and set function + let wrapped = db.db.db.wrappedDB; + let get = util.promisify(wrapped.get.bind(wrapped)); + let set = util.promisify(dirty.set.bind(dirty)); - padManager.getPad(padId, function(err, _pad) { - pad = _pad; - callback(err); - }); - }, + // array in which required key values will be accumulated + let neededDBValues = ['pad:' + padId]; + + // get the actual pad object + let pad = await padManager.getPad(padId); - function (callback) { // add all authors - var authors = pad.getAllAuthors(); - for (var i = 0; i < authors.length; i++) { - neededDBValues.push('globalAuthor:' + authors[i]); - } + neededDBValues.push(...pad.getAllAuthors().map(author => 'globalAuthor:' + author)); // add all revisions - var revHead = pad.head; - for (var i = 0; i <= revHead; i++) { - neededDBValues.push('pad:' + padId + ':revs:' + i); + for (let rev = 0; rev <= pad.head; ++rev) { + neededDBValues.push('pad:' + padId + ':revs:' + rev); } - // get all chat values - var chatHead = pad.chatHead; - for (var i = 0; i <= chatHead; i++) { - neededDBValues.push('pad:' + padId + ':chat:' + i); + // add all chat values + for (let chat = 0; chat <= pad.chatHead; ++chat) { + neededDBValues.push('pad:' + padId + ':chat:' + chat); } - // get and set all values - async.forEach(neededDBValues, function(dbkey, callback) { - db.db.db.wrappedDB.get(dbkey, function(err, dbvalue) { - if (err) { callback(err); return} + for (let dbkey of neededDBValues) { + let dbvalue = await get(dbkey); + if (dbvalue && typeof dbvalue !== 'object') { + dbvalue = JSON.parse(dbvalue); + } + await set(dbkey, dbvalue); + } - if (dbvalue && typeof dbvalue != 'object') { - dbvalue = JSON.parse(dbvalue); // if it's not json then parse it as json - } - - dirty.set(dbkey, dbvalue, callback); - }); - }, callback); - } -], -function (err) { - if (err) { - throw err; - } else { - console.log("finished"); - process.exit(); + console.log('finished'); + process.exit(0); + } catch (er) { + console.error(er); + process.exit(1); } }); From acc0b05702e6a3a7038c51ab01a5657abe6a35e6 Mon Sep 17 00:00:00 2001 From: muxator Date: Sun, 27 Jan 2019 13:48:24 +0100 Subject: [PATCH 036/183] windows: 1.7.5 was released with node 8.15.0. Document it here. --- bin/buildForWindows.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/buildForWindows.sh b/bin/buildForWindows.sh index a9fbd70b1..3257bf187 100755 --- a/bin/buildForWindows.sh +++ b/bin/buildForWindows.sh @@ -1,6 +1,6 @@ #!/bin/sh -NODE_VERSION="8.9.0" +NODE_VERSION="8.15.0" #Move to the folder where ep-lite is installed cd `dirname $0` From e58da69cfbb9af955e0e6424ec9496a34182fe7c Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Mon, 28 Jan 2019 13:13:24 +0000 Subject: [PATCH 037/183] db/SecurityManager.js: converted checkAccess() to pure Promises Also converted the handler functions that depend on checkAccess() into async functions too. NB: this commit needs specific attention to it because it touches a lot of security related code! --- src/node/db/SecurityManager.js | 410 +++++++------- src/node/handler/PadMessageHandler.js | 749 +++++++++++--------------- src/node/handler/SocketIORouter.js | 36 +- 3 files changed, 508 insertions(+), 687 deletions(-) diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index e9aefcd61..dcdbe6a62 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -18,8 +18,6 @@ * limitations under the License. */ -var ERR = require("async-stacktrace"); -var async = require("async"); var authorManager = require("./AuthorManager"); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); var padManager = require("./PadManager"); @@ -35,270 +33,222 @@ const thenify = require("thenify").withCallback; * @param sessionCookie the session the user has (set via api) * @param token the token of the author (randomly generated at client side, used for public pads) * @param password the password the user has given to access this pad, can be null - * @param callback will be called with (err, {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx}) + * @return {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx}) */ -exports.checkAccess = thenify(function(padID, sessionCookie, token, password, callback) +exports.checkAccess = async function(padID, sessionCookie, token, password) { - var statusObject; + // immutable object + let deny = Object.freeze({ accessStatus: "deny" }); if (!padID) { - callback(null, {accessStatus: "deny"}); - return; + return deny; } // allow plugins to deny access var deniedByHook = hooks.callAll("onAccessCheck", {'padID': padID, 'password': password, 'token': token, 'sessionCookie': sessionCookie}).indexOf(false) > -1; if (deniedByHook) { - callback(null, {accessStatus: "deny"}); - return; + return deny; } + // get author for this token + let tokenAuthor = await authorManager.getAuthor4Token(token); + + // check if pad exists + let padExists = await padManager.doesPadExist(padID); + if (settings.requireSession) { // a valid session is required (api-only mode) if (!sessionCookie) { // without sessionCookie, access is denied - callback(null, {accessStatus: "deny"}); - - return; + return deny; } } else { // a session is not required, so we'll check if it's a public pad if (padID.indexOf("$") === -1) { // it's not a group pad, means we can grant access - // get author for this token - authorManager.getAuthor4Token(token, function(err, author) { - if (ERR(err, callback)) return; + // assume user has access + let statusObject = { accessStatus: "grant", authorID: tokenAuthor }; - // assume user has access - statusObject = { accessStatus: "grant", authorID: author }; + if (settings.editOnly) { + // user can't create pads - if (settings.editOnly) { - // user can't create pads - - // check if pad exists - padManager.doesPadExists(padID, function(err, exists) { - if (ERR(err, callback)) return; - - if (!exists) { - // pad doesn't exist - user can't have access - statusObject.accessStatus = "deny"; - } - - // grant or deny access, with author of token - callback(null, statusObject); - }); - - return; + if (!padExists) { + // pad doesn't exist - user can't have access + statusObject.accessStatus = "deny"; } + } - // user may create new pads - no need to check anything - // grant access, with author of token - callback(null, statusObject); - }); - - // don't continue - return; + // user may create new pads - no need to check anything + // grant access, with author of token + return statusObject; } } - var groupID = padID.split("$")[0]; - var padExists = false; - var validSession = false; - var sessionAuthor; - var tokenAuthor; - var isPublic; - var isPasswordProtected; - var passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong + let validSession = false; + let sessionAuthor; + let isPublic; + let isPasswordProtected; + let passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong - async.series([ - // get basic informations from the database - function(callback) { - async.parallel([ - // does pad exist - function(callback) { - padManager.doesPadExists(padID, function(err, exists) { - if (ERR(err, callback)) return; + // get information about all sessions contained in this cookie + if (sessionCookie) { + let groupID = padID.split("$")[0]; + let sessionIDs = sessionCookie.split(','); - padExists = exists; - callback(); - }); - }, + // was previously iterated in parallel using async.forEach + for (let sessionID of sessionIDs) { + try { + let sessionInfo = await sessionManager.getSessionInfo(sessionID); - // get information about all sessions contained in this cookie - function(callback) { - if (!sessionCookie) { - callback(); - return; - } - - var sessionIDs = sessionCookie.split(','); - - async.forEach(sessionIDs, function(sessionID, callback) { - sessionManager.getSessionInfo(sessionID, function(err, sessionInfo) { - // skip session if it doesn't exist - if (err && err.message == "sessionID does not exist") { - authLogger.debug("Auth failed: unknown session"); - callback(); - - return; - } - - if (ERR(err, callback)) return; - - var now = Math.floor(Date.now()/1000); - - // is it for this group? - if (sessionInfo.groupID != groupID) { - authLogger.debug("Auth failed: wrong group"); - callback(); - - return; - } - - // is validUntil still ok? - if (sessionInfo.validUntil <= now) { - authLogger.debug("Auth failed: validUntil"); - callback(); - - return; - } - - // There is a valid session - validSession = true; - sessionAuthor = sessionInfo.authorID; - - callback(); - }); - }, callback); - }, - - // get author for token - function(callback) { - // get author for this token - authorManager.getAuthor4Token(token, function(err, author) { - if (ERR(err, callback)) return; - - tokenAuthor = author; - callback(); - }); - } - ], callback); - }, - - // get more informations of this pad, if avaiable - function(callback) { - // skip this if the pad doesn't exist - if (padExists == false) { - callback(); - - return; - } - - padManager.getPad(padID, function(err, pad) { - if (ERR(err, callback)) return; - - // is it a public pad? - isPublic = pad.getPublicStatus(); - - // is it password protected? - isPasswordProtected = pad.isPasswordProtected(); - - // is password correct? - if (isPasswordProtected && password && pad.isCorrectPassword(password)) { - passwordStatus = "correct"; + // is it for this group? + if (sessionInfo.groupID != groupID) { + authLogger.debug("Auth failed: wrong group"); + continue; } - callback(); - }); - }, + // is validUntil still ok? + let now = Math.floor(Date.now() / 1000); + if (sessionInfo.validUntil <= now) { + authLogger.debug("Auth failed: validUntil"); + continue; + } - function(callback) { - if (validSession && padExists) { - // - a valid session for this group is avaible AND pad exists - if (!isPasswordProtected) { - // - the pad is not password protected - - // --> grant access - statusObject = { accessStatus: "grant", authorID: sessionAuthor }; - } else if (settings.sessionNoPassword) { - // - the setting to bypass password validation is set - - // --> grant access - statusObject = { accessStatus: "grant", authorID: sessionAuthor }; - } else if (isPasswordProtected && passwordStatus === "correct") { - // - the pad is password protected and password is correct - - // --> grant access - statusObject = { accessStatus: "grant", authorID: sessionAuthor }; - } else if (isPasswordProtected && passwordStatus === "wrong") { - // - the pad is password protected but wrong password given - - // --> deny access, ask for new password and tell them that the password is wrong - statusObject = { accessStatus: "wrongPassword" }; - } else if (isPasswordProtected && passwordStatus === "notGiven") { - // - the pad is password protected but no password given - - // --> ask for password - statusObject = { accessStatus: "needPassword" }; + // fall-through - there is a valid session + validSession = true; + sessionAuthor = sessionInfo.authorID; + break; + } catch (err) { + // skip session if it doesn't exist + if (err.message == "sessionID does not exist") { + authLogger.debug("Auth failed: unknown session"); } else { - throw new Error("Ops, something wrong happend"); + throw err; } - } else if (validSession && !padExists) { - // - a valid session for this group avaible but pad doesn't exist - - // --> grant access - statusObject = {accessStatus: "grant", authorID: sessionAuthor}; - - if (settings.editOnly) { - // --> deny access if user isn't allowed to create the pad - authLogger.debug("Auth failed: valid session & pad does not exist"); - statusObject.accessStatus = "deny"; - } - } else if (!validSession && padExists) { - // there is no valid session avaiable AND pad exists - - // -- it's public and not password protected - if (isPublic && !isPasswordProtected) { - // --> grant access, with author of token - statusObject = {accessStatus: "grant", authorID: tokenAuthor}; - } else if (isPublic && isPasswordProtected && passwordStatus === "correct") { - // - it's public and password protected and password is correct - - // --> grant access, with author of token - statusObject = {accessStatus: "grant", authorID: tokenAuthor}; - } else if (isPublic && isPasswordProtected && passwordStatus === "wrong") { - // - it's public and the pad is password protected but wrong password given - - // --> deny access, ask for new password and tell them that the password is wrong - statusObject = {accessStatus: "wrongPassword"}; - } else if (isPublic && isPasswordProtected && passwordStatus === "notGiven") { - // - it's public and the pad is password protected but no password given - - // --> ask for password - statusObject = {accessStatus: "needPassword"}; - } else if (!isPublic) { - // - it's not public - - authLogger.debug("Auth failed: invalid session & pad is not public"); - // --> deny access - statusObject = {accessStatus: "deny"}; - } else { - throw new Error("Ops, something wrong happend"); - } - } else { - // there is no valid session avaiable AND pad doesn't exist - authLogger.debug("Auth failed: invalid session & pad does not exist"); - // --> deny access - statusObject = {accessStatus: "deny"}; } - - callback(); } - ], - function(err) { - if (ERR(err, callback)) return; + } - callback(null, statusObject); - }); -}); + if (padExists) { + let pad = await padManager.getPad(padID); + + // is it a public pad? + isPublic = pad.getPublicStatus(); + + // is it password protected? + isPasswordProtected = pad.isPasswordProtected(); + + // is password correct? + if (isPasswordProtected && password && pad.isCorrectPassword(password)) { + passwordStatus = "correct"; + } + } + + // - a valid session for this group is avaible AND pad exists + if (validSession && padExists) { + let authorID = sessionAuthor; + let grant = Object.freeze({ accessStatus: "grant", authorID }); + + if (!isPasswordProtected) { + // - the pad is not password protected + + // --> grant access + return grant; + } + + if (settings.sessionNoPassword) { + // - the setting to bypass password validation is set + + // --> grant access + return grant; + } + + if (isPasswordProtected && passwordStatus === "correct") { + // - the pad is password protected and password is correct + + // --> grant access + return grant; + } + + if (isPasswordProtected && passwordStatus === "wrong") { + // - the pad is password protected but wrong password given + + // --> deny access, ask for new password and tell them that the password is wrong + return { accessStatus: "wrongPassword" }; + } + + if (isPasswordProtected && passwordStatus === "notGiven") { + // - the pad is password protected but no password given + + // --> ask for password + return { accessStatus: "needPassword" }; + } + + throw new Error("Oops, something wrong happend"); + } + + if (validSession && !padExists) { + // - a valid session for this group avaible but pad doesn't exist + + // --> grant access by default + let accessStatus = "grant"; + let authorID = sessionAuthor; + + // --> deny access if user isn't allowed to create the pad + if (settings.editOnly) { + authLogger.debug("Auth failed: valid session & pad does not exist"); + accessStatus = "deny"; + } + + return { accessStatus, authorID }; + } + + if (!validSession && padExists) { + // there is no valid session avaiable AND pad exists + + let authorID = tokenAuthor; + let grant = Object.freeze({ accessStatus: "grant", authorID }); + + if (isPublic && !isPasswordProtected) { + // -- it's public and not password protected + + // --> grant access, with author of token + return grant; + } + + if (isPublic && isPasswordProtected && passwordStatus === "correct") { + // - it's public and password protected and password is correct + + // --> grant access, with author of token + return grant; + } + + if (isPublic && isPasswordProtected && passwordStatus === "wrong") { + // - it's public and the pad is password protected but wrong password given + + // --> deny access, ask for new password and tell them that the password is wrong + return { accessStatus: "wrongPassword" }; + } + + if (isPublic && isPasswordProtected && passwordStatus === "notGiven") { + // - it's public and the pad is password protected but no password given + + // --> ask for password + return { accessStatus: "needPassword" }; + } + + if (!isPublic) { + // - it's not public + + authLogger.debug("Auth failed: invalid session & pad is not public"); + // --> deny access + return { accessStatus: "deny" }; + } + + throw new Error("Oops, something wrong happend"); + } + + // there is no valid session avaiable AND pad doesn't exist + authLogger.debug("Auth failed: invalid session & pad does not exist"); + return { accessStatus: "deny" }; +} diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 77d7dc6c5..3004ae49c 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -164,7 +164,7 @@ exports.handleDisconnect = function(client) * @param client the client that send this message * @param message the message from the client */ -exports.handleMessage = function(client, message) +exports.handleMessage = async function(client, message) { if (message == null) { return; @@ -181,35 +181,33 @@ exports.handleMessage = function(client, message) return; } - var handleMessageHook = function(callback) { + async function handleMessageHook() { // Allow plugins to bypass the readonly message blocker - hooks.aCallAll("handleMessageSecurity", { client: client, message: message }, function( err, messages ) { - if(ERR(err, callback)) return; - _.each(messages, function(newMessage){ - if ( newMessage === true ) { - thisSession.readonly = false; - } - }); - }); + let messages = await hooks.aCallAll("handleMessageSecurity", { client: client, message: message }); + + for (let message of messages) { + if (message === true) { + thisSession.readonly = false; + break; + } + } + + let dropMessage = false; - var dropMessage = false; // Call handleMessage hook. If a plugin returns null, the message will be dropped. Note that for all messages // handleMessage will be called, even if the client is not authorized - hooks.aCallAll("handleMessage", { client: client, message: message }, function( err, messages ) { - if(ERR(err, callback)) return; - _.each(messages, function(newMessage){ - if ( newMessage === null ) { - dropMessage = true; - } - }); - - // If no plugins explicitly told us to drop the message, its ok to proceed - if(!dropMessage){ callback() }; - }); + messages = await hooks.aCallAll("handleMessage", { client: client, message: message }); + for (let message of messages) { + if (message === null ) { + dropMessage = true; + break; + } + } + return dropMessage; } - var finalHandler = function() { + function finalHandler() { // Check what type of message we get and delegate to the other methods if (message.type == "CLIENT_READY") { handleClientReady(client, message); @@ -256,54 +254,49 @@ exports.handleMessage = function(client, message) return; } - async.series([ - handleMessageHook, + let dropMessage = await handleMessageHook(); + if (!dropMessage) { // check permissions - function(callback) { - // client tried to auth for the first time (first msg from the client) - if (message.type == "CLIENT_READY") { + + // client tried to auth for the first time (first msg from the client) + if (message.type == "CLIENT_READY") { createSessionInfo(client, message); - } + } - // Note: message.sessionID is an entirely different kind of - // session from the sessions we use here! Beware! - // FIXME: Call our "sessions" "connections". - // FIXME: Use a hook instead - // FIXME: Allow to override readwrite access with readonly + // Note: message.sessionID is an entirely different kind of + // session from the sessions we use here! Beware! + // FIXME: Call our "sessions" "connections". + // FIXME: Use a hook instead + // FIXME: Allow to override readwrite access with readonly - // Simulate using the load testing tool - if (!sessioninfos[client.id].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.") - return; - } + // Simulate using the load testing tool + if (!sessioninfos[client.id].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.") + return; + } - var auth = sessioninfos[client.id].auth; - var checkAccessCallback = function(err, statusObject) { - if (ERR(err, callback)) return; + let auth = sessioninfos[client.id].auth; - if (statusObject.accessStatus == "grant") { - // access was granted - callback(); - } else { - // no access, send the client a message that tells him why - client.json.send({accessStatus: statusObject.accessStatus}) - } - }; + // check if pad is requested via readOnly + let padId = auth.padID; - // check if pad is requested via readOnly - if (auth.padID.indexOf("r.") === 0) { - // Pad is readOnly, first get the real Pad ID - readOnlyManager.getPadId(auth.padID, function(err, value) { - ERR(err); - securityManager.checkAccess(value, auth.sessionID, auth.token, auth.password, checkAccessCallback); - }); - } else { - securityManager.checkAccess(auth.padID, auth.sessionID, auth.token, auth.password, checkAccessCallback); - } - }, - finalHandler - ]); + // Pad is readOnly, first get the real Pad ID + if (padId.indexOf("r.") === 0) { + padId = await readOnlyManager.getPadId(padID); + } + + let { accessStatus } = await securityManager.checkAccess(padId, auth.sessionID, auth.token, auth.password); + + // no access, send the client a message that tells him why + if (accessStatus !== "grant") { + client.json.send({ accessStatus }); + return; + } + + // access was granted + finalHandler(); + } } @@ -977,7 +970,7 @@ function createSessionInfo(client, message) * @param client the client that send this message * @param message the message from the client */ -function handleClientReady(client, message) +async function handleClientReady(client, message) { // check if all ok if (!message.token) { @@ -1000,434 +993,319 @@ function handleClientReady(client, message) return; } - var author; - var authorName; - var authorColorId; - var pad; var historicalAuthorData = {}; - var currentTime; - var padIds; hooks.callAll("clientReady", message); - async.series([ - // Get ro/rw id:s - function(callback) { - readOnlyManager.getIds(message.padId, function(err, value) { - if (ERR(err, callback)) return; + // Get ro/rw id:s + let padIds = await readOnlyManager.getIds(message.padId); - padIds = value; - callback(); - }); - }, + // check permissions - // check permissions - function(callback) { - // Note: message.sessionID is an entierly different kind of - // session from the sessions we use here! Beware! - // FIXME: Call our "sessions" "connections". - // FIXME: Use a hook instead - // FIXME: Allow to override readwrite access with readonly - securityManager.checkAccess(padIds.padId, message.sessionID, message.token, message.password, function(err, statusObject) { - if (ERR(err, callback)) return; + // Note: message.sessionID is an entierly different kind of + // session from the sessions we use here! Beware! + // FIXME: Call our "sessions" "connections". + // FIXME: Use a hook instead + // FIXME: Allow to override readwrite access with readonly + let statusObject = await securityManager.checkAccess(padIds.padId, message.sessionID, message.token, message.password); + let accessStatus = statusObject.accessStatus; - if (statusObject.accessStatus == "grant") { - // access was granted - author = statusObject.authorID; - callback(); - } else { - // no access, send the client a message that tells him why - client.json.send({accessStatus: statusObject.accessStatus}) - } - }); - }, + // no access, send the client a message that tells him why + if (accessStatus !== "grant") { + client.json.send({ accessStatus }); + return; + } - // get all authordata of this new user, and load the pad-object from the database - function(callback) - { - async.parallel([ - // get colorId and name - function(callback) { - authorManager.getAuthor(author, function(err, value) { - if (ERR(err, callback)) return; + let author = statusObject.authorID; - authorColorId = value.colorId; - authorName = value.name; - callback(); - }); - }, + // get all authordata of this new user, and load the pad-object from the database + let value = await authorManager.getAuthor(author); + let authorColorId = value.colorId; + let authorName = value.name; - // get pad - function(callback) { - padManager.getPad(padIds.padId, function(err, value) { - if (ERR(err, callback)) return; + // get pad + let pad = await padManager.getPad(padIds.padId); - pad = value; - callback(); - }); - } - ], callback); - }, + // these db requests all need the pad object (timestamp of latest revision, author data) + let authors = pad.getAllAuthors(); - // these db requests all need the pad object (timestamp of latest revission, author data) - function(callback) { - var authors = pad.getAllAuthors(); + // get timestamp of latest revision needed for timeslider + let currentTime = await pad.getRevisionDate(pad.getHeadRevisionNumber()); - async.parallel([ - // get timestamp of latest revission needed for timeslider - function(callback) { - pad.getRevisionDate(pad.getHeadRevisionNumber(), function(err, date) { - if (ERR(err, callback)) return; + // get all author data out of the database + for (let authorId of authors) { + try { + let author = await authorManager.getAuthor(authorId); + historicalAuthorData[authorId] = { name: author.name, colorId: author.colorId }; // Filter author attribs (e.g. don't send author's pads to all clients) + } catch (err) { + messageLogger.error("There is no author for authorId:", authorId); + } + } - currentTime = date; - callback(); - }); - }, + // glue the clientVars together, send them and tell the other clients that a new one is there - // get all author data out of the database - function(callback) { - async.forEach(authors, function(authorId, callback) { - authorManager.getAuthor(authorId, function(err, author) { - if (!author && !err) { - messageLogger.error("There is no author for authorId:", authorId); + // Check that the client is still here. It might have disconnected between callbacks. + if (sessioninfos[client.id] === undefined) { + return; + } - return callback(); - } + // Check if this author is already on the pad, if yes, kick the other sessions! + let roomClients = _getRoomClients(pad.id); - if (ERR(err, callback)) return; + for (let client of roomClients) { + let sinfo = sessioninfos[client.id]; + if (sinfo && sinfo.author == author) { + // fix user's counter, works on page refresh or if user closes browser window and then rejoins + sessioninfos[client.id] = {}; + client.leave(padIds.padId); + client.json.send({disconnect:"userdup"}); + } + } - historicalAuthorData[authorId] = { name: author.name, colorId: author.colorId }; // Filter author attribs (e.g. don't send author's pads to all clients) - callback(); - }); - }, callback); - } - ], callback); + // Save in sessioninfos that this session belonges to this pad + sessioninfos[client.id].padId = padIds.padId; + sessioninfos[client.id].readOnlyPadId = padIds.readOnlyPadId; + sessioninfos[client.id].readonly = padIds.readonly; - }, + // Log creation/(re-)entering of a pad + let ip = remoteAddress[client.id]; - // glue the clientVars together, send them and tell the other clients that a new one is there - function(callback) { - // Check that the client is still here. It might have disconnected between callbacks. - if(sessioninfos[client.id] === undefined) { - return callback(); - } + // Anonymize the IP address if IP logging is disabled + if (settings.disableIPlogging) { + ip = 'ANONYMOUS'; + } - // Check if this author is already on the pad, if yes, kick the other sessions! - var roomClients = _getRoomClients(pad.id); + if (pad.head > 0) { + accessLogger.info('[ENTER] Pad "' + padIds.padId + '": Client ' + client.id + ' with IP "' + ip + '" entered the pad'); + } else if (pad.head == 0) { + accessLogger.info('[CREATE] Pad "' + padIds.padId + '": Client ' + client.id + ' with IP "' + ip + '" created the pad'); + } - async.forEach(roomClients, function(client, callback) { - var sinfo = sessioninfos[client.id]; + if (message.reconnect) { + // If this is a reconnect, we don't have to send the client the ClientVars again + // Join the pad and start receiving updates + client.join(padIds.padId); - if (sinfo && sinfo.author == author) { - // fix user's counter, works on page refresh or if user closes browser window and then rejoins - sessioninfos[client.id] = {}; - client.leave(padIds.padId); - client.json.send({ disconnect:"userdup" }); - } - }); + // Save the revision in sessioninfos, we take the revision from the info the client send to us + sessioninfos[client.id].rev = message.client_rev; - // Save in sessioninfos that this session belonges to this pad - sessioninfos[client.id].padId = padIds.padId; - sessioninfos[client.id].readOnlyPadId = padIds.readOnlyPadId; - sessioninfos[client.id].readonly = padIds.readonly; + // 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 = {}; - // Log creation/(re-)entering of a pad - var ip = remoteAddress[client.id]; + var startNum = message.client_rev + 1; + var endNum = pad.getHeadRevisionNumber() + 1; - // Anonymize the IP address if IP logging is disabled - if (settings.disableIPlogging) { - ip = 'ANONYMOUS'; - } + var headNum = pad.getHeadRevisionNumber(); - if (pad.head > 0) { - accessLogger.info('[ENTER] Pad "' + padIds.padId + '": Client ' + client.id + ' with IP "' + ip + '" entered the pad'); - } else if (pad.head == 0) { - accessLogger.info('[CREATE] Pad "' + padIds.padId + '": Client ' + client.id + ' with IP "' + ip + '" created the pad'); - } + if (endNum > headNum + 1) { + endNum = headNum + 1; + } - if (message.reconnect == true) { - // If this is a reconnect, we don't have to send the client the ClientVars again - // Join the pad and start receiving updates - client.join(padIds.padId); + if (startNum < 0) { + startNum = 0; + } - // Save the revision in sessioninfos, we take the revision from the info the client send to us - sessioninfos[client.id].rev = message.client_rev; + for (let r = startNum; r < endNum; r++) { + revisionsNeeded.push(r); + changesets[r] = {}; + } - // 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 = {}; + // get changesets, author and timestamp needed for pending revisions + for (let revNum of revisionsNeeded) { + changesets[revNum]['changeset'] = await pad.getRevisionChangeset(revNum); + changesets[revNum]['author'] = await pad.getRevisionAuthor(revNum); + changesets[revNum]['timestamp'] = await pad.getRevisionDate(revNum); + } - var startNum = message.client_rev + 1; - var endNum = pad.getHeadRevisionNumber() + 1; + // return pending changesets + for (let r of revisionsNeeded) { - 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'] + let forWire = Changeset.prepareForWire(changesets[r]['changeset'], pad.pool); + let 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(); - }); + client.json.send(wireMsg); + } - if (startNum == endNum) { - var Msg = {"type":"COLLABROOM", - "data":{type:"CLIENT_RECONNECT", - noChanges: true, - newRev: pad.getHeadRevisionNumber() - }}; - client.json.send(Msg); - } - }); - } else { - // This is a normal first connect - // prepare all values for the wire, there's a chance that this throws, if the pad is corrupted - try { - var atext = Changeset.cloneAText(pad.atext); - var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); - var apool = attribsForWire.pool.toJsonable(); - atext.attribs = attribsForWire.translated; - } catch(e) { - console.error(e.stack || e) - client.json.send({ disconnect:"corruptPad" });// pull the brakes - return callback(); - } + if (startNum == endNum) { + var Msg = {"type":"COLLABROOM", + "data":{type:"CLIENT_RECONNECT", + noChanges: true, + newRev: pad.getHeadRevisionNumber() + }}; + client.json.send(Msg); + } - // Warning: never ever send padIds.padId to the client. If the - // client is read only you would open a security hole 1 swedish - // mile wide... - var clientVars = { - "skinName": settings.skinName, - "accountPrivs": { - "maxRevisions": 100 - }, - "automaticReconnectionTimeout": settings.automaticReconnectionTimeout, - "initialRevisionList": [], - "initialOptions": { - "guestPolicy": "deny" - }, - "savedRevisions": pad.getSavedRevisions(), - "collab_client_vars": { - "initialAttributedText": atext, - "clientIp": "127.0.0.1", - "padId": message.padId, - "historicalAuthorData": historicalAuthorData, - "apool": apool, - "rev": pad.getHeadRevisionNumber(), - "time": currentTime, - }, - "colorPalette": authorManager.getColorPalette(), + } else { + // This is a normal first connect + + // prepare all values for the wire, there's a chance that this throws, if the pad is corrupted + try { + var atext = Changeset.cloneAText(pad.atext); + var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); + var apool = attribsForWire.pool.toJsonable(); + atext.attribs = attribsForWire.translated; + } catch(e) { + console.error(e.stack || e) + client.json.send({ disconnect:"corruptPad" }); // pull the brakes + + return; + } + + // Warning: never ever send padIds.padId to the client. If the + // client is read only you would open a security hole 1 swedish + // mile wide... + var clientVars = { + "skinName": settings.skinName, + "accountPrivs": { + "maxRevisions": 100 + }, + "automaticReconnectionTimeout": settings.automaticReconnectionTimeout, + "initialRevisionList": [], + "initialOptions": { + "guestPolicy": "deny" + }, + "savedRevisions": pad.getSavedRevisions(), + "collab_client_vars": { + "initialAttributedText": atext, "clientIp": "127.0.0.1", - "userIsGuest": true, - "userColor": authorColorId, "padId": message.padId, - "padOptions": settings.padOptions, - "padShortcutEnabled": settings.padShortcutEnabled, - "initialTitle": "Pad: " + message.padId, - "opts": {}, - // tell the client the number of the latest chat-message, which will be - // used to request the latest 100 chat-messages later (GET_CHAT_MESSAGES) - "chatHead": pad.chatHead, - "numConnectedUsers": roomClients.length, - "readOnlyId": padIds.readOnlyPadId, - "readonly": padIds.readonly, - "serverTimestamp": Date.now(), - "userId": author, - "abiwordAvailable": settings.abiwordAvailable(), - "sofficeAvailable": settings.sofficeAvailable(), - "exportAvailable": settings.exportAvailable(), - "plugins": { - "plugins": plugins.plugins, - "parts": plugins.parts, - }, - "indentationOnNewLine": settings.indentationOnNewLine, - "scrollWhenFocusLineIsOutOfViewport": { - "percentage" : { - "editionAboveViewport": settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport, - "editionBelowViewport": settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport, - }, - "duration": settings.scrollWhenFocusLineIsOutOfViewport.duration, - "scrollWhenCaretIsInTheLastLineOfViewport": settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport, - "percentageToScrollWhenUserPressesArrowUp": settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp, - }, - "initialChangesets": [] // FIXME: REMOVE THIS SHIT + "historicalAuthorData": historicalAuthorData, + "apool": apool, + "rev": pad.getHeadRevisionNumber(), + "time": currentTime, + }, + "colorPalette": authorManager.getColorPalette(), + "clientIp": "127.0.0.1", + "userIsGuest": true, + "userColor": authorColorId, + "padId": message.padId, + "padOptions": settings.padOptions, + "padShortcutEnabled": settings.padShortcutEnabled, + "initialTitle": "Pad: " + message.padId, + "opts": {}, + // tell the client the number of the latest chat-message, which will be + // used to request the latest 100 chat-messages later (GET_CHAT_MESSAGES) + "chatHead": pad.chatHead, + "numConnectedUsers": roomClients.length, + "readOnlyId": padIds.readOnlyPadId, + "readonly": padIds.readonly, + "serverTimestamp": Date.now(), + "userId": author, + "abiwordAvailable": settings.abiwordAvailable(), + "sofficeAvailable": settings.sofficeAvailable(), + "exportAvailable": settings.exportAvailable(), + "plugins": { + "plugins": plugins.plugins, + "parts": plugins.parts, + }, + "indentationOnNewLine": settings.indentationOnNewLine, + "scrollWhenFocusLineIsOutOfViewport": { + "percentage" : { + "editionAboveViewport": settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport, + "editionBelowViewport": settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport, + }, + "duration": settings.scrollWhenFocusLineIsOutOfViewport.duration, + "scrollWhenCaretIsInTheLastLineOfViewport": settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport, + "percentageToScrollWhenUserPressesArrowUp": settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp, + }, + "initialChangesets": [] // FIXME: REMOVE THIS SHIT + } + + // Add a username to the clientVars if one avaiable + if (authorName != null) { + clientVars.userName = authorName; + } + + // call the clientVars-hook so plugins can modify them before they get sent to the client + let messages = await hooks.aCallAll("clientVars", { clientVars: clientVars, pad: pad }); + + // combine our old object with the new attributes from the hook + for (let msg of messages) { + Object.assign(clientVars, msg); + } + + // Join the pad and start receiving updates + client.join(padIds.padId); + + // Send the clientVars to the Client + client.json.send({type: "CLIENT_VARS", data: clientVars}); + + // Save the current revision in sessioninfos, should be the same as in clientVars + sessioninfos[client.id].rev = pad.getHeadRevisionNumber(); + + sessioninfos[client.id].author = author; + + // prepare the notification for the other users on the pad, that this user joined + let messageToTheOtherUsers = { + "type": "COLLABROOM", + "data": { + type: "USER_NEWINFO", + userInfo: { + "ip": "127.0.0.1", + "colorId": authorColorId, + "userAgent": "Anonymous", + "userId": author } + } + }; - // Add a username to the clientVars if one avaiable - if (authorName != null) { - clientVars.userName = authorName; - } + // Add the authorname of this new User, if avaiable + if (authorName != null) { + messageToTheOtherUsers.data.userInfo.name = authorName; + } - // call the clientVars-hook so plugins can modify them before they get sent to the client - hooks.aCallAll("clientVars", { clientVars: clientVars, pad: pad }, function( err, messages ) { - if (ERR(err, callback)) return; + // notify all existing users about new user + client.broadcast.to(padIds.padId).json.send(messageToTheOtherUsers); - _.each(messages, function(newVars) { - // combine our old object with the new attributes from the hook - for(var attr in newVars) { - clientVars[attr] = newVars[attr]; - } - }); + // Get sessions for this pad + roomClients = _getRoomClients(pad.id); + for (let roomClient of roomClients) { - // Join the pad and start receiving updates - client.join(padIds.padId); - - // Send the clientVars to the Client - client.json.send({ type: "CLIENT_VARS", data: clientVars }); - - // Save the current revision in sessioninfos, should be the same as in clientVars - sessioninfos[client.id].rev = pad.getHeadRevisionNumber(); - }); + // Jump over, if this session is the connection session + if (roomClient.id == client.id) { + continue; } - sessioninfos[client.id].author = author; + // Since sessioninfos might change while being enumerated, check if the + // sessionID is still assigned to a valid session + if (sessioninfos[roomClient.id] === undefined) { + continue; + } - // prepare the notification for the other users on the pad, that this user joined - var messageToTheOtherUsers = { + let author = sessioninfos[roomClient.id].author; + + // get the authorname & colorId + + // reuse previously created cache of author's data + let authorInfo = historicalAuthorData[author] || await authorManager.getAuthor(author); + + // Send the new User a Notification about this other user + let msg = { "type": "COLLABROOM", "data": { type: "USER_NEWINFO", userInfo: { "ip": "127.0.0.1", - "colorId": authorColorId, + "colorId": authorInfo.colorId, + "name": authorInfo.name, "userAgent": "Anonymous", "userId": author } } }; - // Add the authorname of this new User, if avaiable - if (authorName != null) { - messageToTheOtherUsers.data.userInfo.name = authorName; - } - - // notify all existing users about new user - client.broadcast.to(padIds.padId).json.send(messageToTheOtherUsers); - - // Get sessions for this pad - var roomClients = _getRoomClients(pad.id); - - async.forEach(roomClients, function(roomClient, callback) { - var author; - - // Jump over, if this session is the connection session - if (roomClient.id == client.id) { - return callback(); - } - - // Since sessioninfos might change while being enumerated, check if the - // sessionID is still assigned to a valid session - if (sessioninfos[roomClient.id] !== undefined) { - author = sessioninfos[roomClient.id].author; - } else { - // If the client id is not valid, callback(); - return callback(); - } - - async.waterfall([ - // get the authorname & colorId - function(callback) { - // reuse previously created cache of author's data - if (historicalAuthorData[author]) { - callback(null, historicalAuthorData[author]); - } else { - authorManager.getAuthor(author, callback); - } - }, - - function(authorInfo, callback) { - // Send the new User a Notification about this other user - var msg = { - "type": "COLLABROOM", - "data": { - type: "USER_NEWINFO", - userInfo: { - "ip": "127.0.0.1", - "colorId": authorInfo.colorId, - "name": authorInfo.name, - "userAgent": "Anonymous", - "userId": author - } - } - }; - - client.json.send(msg); - } - ], callback); - }, callback); + client.json.send(msg); } - ], - function(err) { - ERR(err); - }); + } } /** @@ -1496,7 +1374,6 @@ function handleChangesetRequest(client, message) ]); } - /** * Tries to rebuild the getChangestInfo function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144 diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index 08f9f47e8..27c1e9a22 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -19,7 +19,6 @@ * limitations under the License. */ -var ERR = require("async-stacktrace"); var log4js = require('log4js'); var messageLogger = log4js.getLogger("message"); var securityManager = require("../db/SecurityManager"); @@ -80,7 +79,7 @@ exports.setSocketIO = function(_socket) { components[i].handleConnect(client); } - client.on('message', function(message) { + client.on('message', async function(message) { if (message.protocolVersion && message.protocolVersion != 2) { messageLogger.warn("Protocolversion header is not correct:" + stringifyWithoutPassword(message)); return; @@ -92,27 +91,22 @@ exports.setSocketIO = function(_socket) { } else { // try to authorize the client if (message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) { - var checkAccessCallback = function(err, statusObject) { - ERR(err); + // check for read-only pads + let padId = message.padId; + if (padId.indexOf("r.") === 0) { + padId = await readOnlyManager.getPadId(message.padId); + } - if (statusObject.accessStatus === "grant") { - // access was granted, mark the client as authorized and handle the message - clientAuthorized = true; - handleMessage(client, message); - } else { - // no access, send the client a message that tells him why - messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message)); - client.json.send({accessStatus: statusObject.accessStatus}); - } - }; - if (message.padId.indexOf("r.") === 0) { - readOnlyManager.getPadId(message.padId, function(err, value) { - ERR(err); - securityManager.checkAccess(value, message.sessionID, message.token, message.password, checkAccessCallback); - }); + let { accessStatus } = await securityManager.checkAccess(padId, message.sessionID, message.token, message.password); + + if (accessStatus === "grant") { + // access was granted, mark the client as authorized and handle the message + clientAuthorized = true; + handleMessage(client, message); } else { - // this message has everything to try an authorization - securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, checkAccessCallback); + // no access, send the client a message that tells him why + messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message)); + client.json.send({ accessStatus }); } } else { // drop message From 005c0afa97e348e95a9ff8ee8ff0e480c6daf4f1 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Mon, 28 Jan 2019 14:44:36 +0000 Subject: [PATCH 038/183] db/SessionManager.js: completely converted to Promises/async --- src/node/db/SessionManager.js | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index 67d3e3816..9161205d7 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -18,13 +18,11 @@ * limitations under the License. */ -var ERR = require("async-stacktrace"); var customError = require("../utils/customError"); var randomString = require("../utils/randomstring"); var db = require("./DB"); var groupManager = require("./GroupManager"); var authorManager = require("./AuthorManager"); -const thenify = require("thenify").withCallback; exports.doesSessionExist = async function(sessionID) { @@ -120,23 +118,19 @@ exports.createSession = async function(groupID, authorID, validUntil) return { sessionID }; } -// @TODO once external dependencies are using Promises -exports.getSessionInfo = thenify(function(sessionID, callback) +exports.getSessionInfo = async function(sessionID) { // check if the database entry of this session exists - db.get("session:" + sessionID, function (err, session) - { - if(ERR(err, callback)) return; + let session = await db.get("session:" + sessionID); - if (session == null) { - // session does not exist - callback(new customError("sessionID does not exist", "apierror")) - } else { - // everything is fine, return the sessioninfos - callback(null, session); - } - }); -}); + if (session == null) { + // session does not exist + throw new customError("sessionID does not exist", "apierror"); + } + + // everything is fine, return the sessioninfos + return session; +} /** * Deletes a session @@ -199,7 +193,7 @@ exports.listSessionsOfAuthor = async function(authorID) // this function is basically the code listSessionsOfAuthor and listSessionsOfGroup has in common // required to return null rather than an empty object if there are none -async function listSessionsWithDBKey(dbkey, callback) +async function listSessionsWithDBKey(dbkey) { // get the group2sessions entry let sessionObject = await db.get(dbkey); From 81089644725e9510503af6c017c2887799895b49 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Mon, 28 Jan 2019 15:36:36 +0000 Subject: [PATCH 039/183] db/AuthorManager.js: further conversion also fixes a missing await calling `.createAuthor` in db/Pad.js --- src/node/db/API.js | 2 +- src/node/db/AuthorManager.js | 90 ++++++++++++++++-------------------- 2 files changed, 40 insertions(+), 52 deletions(-) diff --git a/src/node/db/API.js b/src/node/db/API.js index f3274a22a..3fcf187cf 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -453,7 +453,7 @@ exports.saveRevision = async function(padID, rev) rev = pad.getHeadRevisionNumber(); } - let author = authorManager.createAuthor('API'); + let author = await authorManager.createAuthor('API'); pad.addSavedRevision(rev, author.authorID, 'Saved through API call'); } diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index d204b64ab..99584da19 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -18,7 +18,6 @@ * limitations under the License. */ -var ERR = require("async-stacktrace"); var db = require("./DB"); var customError = require("../utils/customError"); var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; @@ -53,17 +52,14 @@ exports.doesAuthorExists = exports.doesAuthorExist; /** * Returns the AuthorID for a token. * @param {String} token The token - * @param {Function} callback callback (err, author) */ -exports.getAuthor4Token = thenify(function(token, callback) +exports.getAuthor4Token = async function(token) { - mapAuthorWithDBKey("token2author", token, function(err, author) { - if (ERR(err, callback)) return; + let author = await mapAuthorWithDBKey("token2author", token); - // return only the sub value authorID - callback(null, author ? author.authorID : author); - }); -}); + // return only the sub value authorID + return author ? author.authorID : author; +} /** * Returns the AuthorID for a mapper. @@ -87,69 +83,63 @@ exports.createAuthorIfNotExistsFor = async function(authorMapper, name) * so far this is token2author and mapper2author * @param {String} mapperkey The database key name for this mapper * @param {String} mapper The mapper - * @param {Function} callback callback (err, author) */ -let mapAuthorWithDBKey = thenify(function mapAuthorWithDBKey (mapperkey, mapper, callback) +async function mapAuthorWithDBKey (mapperkey, mapper) { // try to map to an author - db.db.get(mapperkey + ":" + mapper, function(err, author) { - if (ERR(err, callback)) return; + let author = await db.get(mapperkey + ":" + mapper); - if (author === null) { - // there is no author with this mapper, so create one - exports.createAuthor(null, function(err, author) { - if (ERR(err, callback)) return; + if (author === null) { + // there is no author with this mapper, so create one + let author = await exports.createAuthor(null); - // create the token2author relation - db.db.set(mapperkey + ":" + mapper, author.authorID); - - // return the author - callback(null, author); - }); - - return; - } - - // there is an author with this mapper - // update the timestamp of this author - db.db.setSub("globalAuthor:" + author, ["timestamp"], Date.now()); + // create the token2author relation + await db.set(mapperkey + ":" + mapper, author.authorID); // return the author - callback(null, {authorID: author}); - }); -}); + return author; + } + + // there is an author with this mapper + // update the timestamp of this author + await db.setSub("globalAuthor:" + author, ["timestamp"], Date.now()); + + // 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 = thenify(function(name, callback) +exports.createAuthor = function(name) { // create the new author name - var author = "a." + randomString(16); + let author = "a." + randomString(16); // create the globalAuthors db entry - var authorObj = { + let authorObj = { "colorId": Math.floor(Math.random() * (exports.getColorPalette().length)), "name": name, "timestamp": Date.now() }; // set the global author db entry - db.db.set("globalAuthor:" + author, authorObj); + // NB: no await, since we're not waiting for the DB set to finish + db.set("globalAuthor:" + author, authorObj); - callback(null, {authorID: author}); -}); + return { authorID: author }; +} /** * Returns the Author Obj of the author * @param {String} author The id of the author - * @param {Function} callback callback(err, authorObj) */ -exports.getAuthor = thenify(function(author, callback) +exports.getAuthor = function(author) { - db.db.get("globalAuthor:" + author, callback); -}); + // NB: result is already a Promise + return db.get("globalAuthor:" + author); +} /** * Returns the color Id of the author @@ -165,12 +155,11 @@ exports.getAuthorColorId = thenify(function(author, callback) * Sets the color Id of the author * @param {String} author The id of the author * @param {String} colorId The color id of the author - * @param {Function} callback (optional) */ -exports.setAuthorColorId = thenify(function(author, colorId, callback) +exports.setAuthorColorId = function(author, colorId) { - db.db.setSub("globalAuthor:" + author, ["colorId"], colorId, callback); -}); + return db.setSub("globalAuthor:" + author, ["colorId"], colorId); +} /** * Returns the name of the author @@ -186,12 +175,11 @@ exports.getAuthorName = thenify(function(author, callback) * Sets the name of the author * @param {String} author The id of the author * @param {String} name The name of the author - * @param {Function} callback (optional) */ -exports.setAuthorName = thenify(function(author, name, callback) +exports.setAuthorName = function(author, name) { - db.db.setSub("globalAuthor:" + author, ["name"], name, callback); -}); + return db.setSub("globalAuthor:" + author, ["name"], name); +} /** * Returns an array of all pads this author contributed to From bbe4a5f756ed0150064cad098d36ce964d0b54b1 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Mon, 28 Jan 2019 16:20:30 +0000 Subject: [PATCH 040/183] db/PadManager.js: more conversion to Promises/async --- src/node/db/PadManager.js | 61 +++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index e478636b0..858ce5b45 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -50,46 +50,43 @@ var globalPads = { * * Updated without db access as new pads are created/old ones removed. */ -var padList = { +let padList = { list: [], sorted : false, initiated: false, - init: thenify(function(cb) { - db.findKeys("pad:*", "*:*:*", function(err, dbData) { - if (ERR(err, cb)) return; + init: async function() { + let dbData = await db.findKeys("pad:*", "*:*:*"); - if (dbData != null) { - padList.initiated = true - dbData.forEach(function(val) { - padList.addPad(val.replace(/pad:/,""),false); - }); + if (dbData != null) { + this.initiated = true; - cb && cb(); + for (let val of dbData) { + this.addPad(val.replace(/pad:/,""), false); } - }); + } return this; - }), - load: thenify(function(cb) { - if (this.initiated) { - cb && cb(); - } else { - this.init(cb); + }, + load: async function() { + if (!this.initiated) { + return this.init(); } - }), + + return this; + }, /** * Returns all pads in alphabetical order as array. */ - getPads: thenify(function(cb) { - this.load(function() { - if (!padList.sorted) { - padList.list = padList.list.sort(); - padList.sorted = true; - } + getPads: async function() { + await this.load(); - cb && cb(padList.list); - }) - }), + if (!this.sorted) { + this.list.sort(); + this.sorted = true; + } + + return this.list; + }, addPad: function(name) { if (!this.initiated) return; @@ -171,12 +168,12 @@ exports.getPad = thenify(function(id, text, callback) }); }); -exports.listAllPads = thenify(function(cb) +exports.listAllPads = async function() { - padList.getPads(function(list) { - cb && cb(null, {padIDs: list}); - }); -}); + let padIDs = await padList.getPads(); + + return { padIDs }; +} // checks if a pad exists exports.doesPadExist = thenify(function(padId, callback) From 7f19033cc06e2cc0dbd0482dd1bad87494f3c87f Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Wed, 30 Jan 2019 10:25:01 +0000 Subject: [PATCH 041/183] SocketIORouter: code formatting cleanups --- src/node/handler/SocketIORouter.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index 27c1e9a22..077a62beb 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -75,7 +75,7 @@ exports.setSocketIO = function(_socket) { } // tell all components about this connect - for (var i in components) { + for (let i in components) { components[i].handleConnect(client); } @@ -117,7 +117,7 @@ exports.setSocketIO = function(_socket) { client.on('disconnect', function() { // tell all components about this disconnect - for (var i in components) { + for (let i in components) { components[i].handleDisconnect(client); } }); @@ -142,14 +142,10 @@ function handleMessage(client, message) // this ensures there are no passwords in the log function stringifyWithoutPassword(message) { - var newMessage = {}; + let newMessage = Object.assign({}, message); - for (var i in message) { - if (i == "password" && message[i] != null) { - newMessage["password"] = "xxx"; - } else { - newMessage[i] = message[i]; - } + if (newMessage.password != null) { + newMessage.password = "xxx"; } return JSON.stringify(newMessage); From 982d4f380a284a6d64e6314bf232c8e4ea542b83 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Wed, 30 Jan 2019 10:25:46 +0000 Subject: [PATCH 042/183] db/Pad.js: start use of promise DB methods --- src/node/db/Pad.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index 34d000631..4abf2cab2 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -6,7 +6,7 @@ var ERR = require("async-stacktrace"); var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var AttributePool = require("ep_etherpad-lite/static/js/AttributePool"); -var db = require("./DB").db; +var db = require("./DB"); var async = require("async"); var settings = require('../utils/Settings'); var authorManager = require("./AuthorManager"); @@ -128,25 +128,25 @@ Pad.prototype.saveToDatabase = function saveToDatabase() { } } - db.set("pad:" + this.id, dbObject); + db.db.set("pad:" + this.id, dbObject); } // get time of last edit (changeset application) -Pad.prototype.getLastEdit = thenify(function getLastEdit(callback) { +Pad.prototype.getLastEdit = function getLastEdit() { var revNum = this.getHeadRevisionNumber(); - db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "timestamp"], callback); -}); + return db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "timestamp"]); +} Pad.prototype.getRevisionChangeset = thenify(function getRevisionChangeset(revNum, callback) { - db.getSub("pad:" + this.id + ":revs:" + revNum, ["changeset"], callback); + db.db.getSub("pad:" + this.id + ":revs:" + revNum, ["changeset"], callback); }); Pad.prototype.getRevisionAuthor = thenify(function getRevisionAuthor(revNum, callback) { - db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "author"], callback); + db.db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "author"], callback); }); Pad.prototype.getRevisionDate = thenify(function getRevisionDate(revNum, callback) { - db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "timestamp"], callback); + db.db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "timestamp"], callback); }); Pad.prototype.getAllAuthors = function getAllAuthors() { @@ -182,7 +182,7 @@ Pad.prototype.getInternalRevisionAText = thenify(function getInternalRevisionATe async.parallel([ // get the atext of the key revision function (callback) { - db.getSub("pad:" + _this.id + ":revs:" + keyRev, ["meta", "atext"], function(err, _atext) { + db.db.getSub("pad:" + _this.id + ":revs:" + keyRev, ["meta", "atext"], function(err, _atext) { if (ERR(err, callback)) return; try { atext = Changeset.cloneAText(_atext); @@ -232,7 +232,7 @@ Pad.prototype.getInternalRevisionAText = thenify(function getInternalRevisionATe }); Pad.prototype.getRevision = thenify(function getRevisionChangeset(revNum, callback) { - db.get("pad:" + this.id + ":revs:" + revNum, callback); + db.db.get("pad:" + this.id + ":revs:" + revNum, callback); }); Pad.prototype.getAllAuthorColors = thenify(function getAllAuthorColors(callback) { @@ -333,7 +333,7 @@ Pad.prototype.getChatMessage = thenify(function getChatMessage(entryNum, callbac async.series([ // get the chat entry function(callback) { - db.get("pad:" + _this.id + ":chat:" + entryNum, function(err, _entry) { + db.db.get("pad:" + _this.id + ":chat:" + entryNum, function(err, _entry) { if (ERR(err, callback)) return; entry = _entry; callback(); @@ -410,7 +410,7 @@ Pad.prototype.init = thenify(function init(text, callback) { } // try to load the pad - db.get("pad:" + this.id, function(err, value) { + db.db.get("pad:" + this.id, function(err, value) { if (ERR(err, callback)) return; // if this pad exists, load it @@ -509,7 +509,7 @@ Pad.prototype.copy = thenify(function copy(destinationID, force, callback) { // copy the 'pad' entry function(callback) { - db.get("pad:" + sourceID, function(err, pad) { + db.db.get("pad:" + sourceID, function(err, pad) { db.set("pad:" + destinationID, pad); }); @@ -524,7 +524,7 @@ Pad.prototype.copy = thenify(function copy(destinationID, force, callback) { var chatHead = _this.chatHead; for (var i=0; i <= chatHead; i++) { - db.get("pad:" + sourceID + ":chat:" + i, function (err, chat) { + db.db.get("pad:" + sourceID + ":chat:" + i, function (err, chat) { if (ERR(err, callback)) return; db.set("pad:" + destinationID + ":chat:" + i, chat); }); @@ -537,7 +537,7 @@ Pad.prototype.copy = thenify(function copy(destinationID, force, callback) { function(callback) { var revHead = _this.head; for (var i=0; i <= revHead; i++) { - db.get("pad:" + sourceID + ":revs:" + i, function (err, rev) { + db.db.get("pad:" + sourceID + ":revs:" + i, function (err, rev) { if (ERR(err, callback)) return; db.set("pad:" + destinationID + ":revs:" + i, rev); }); @@ -606,7 +606,7 @@ Pad.prototype.remove = thenify(function remove(callback) { // it is a group pad var groupID = padID.substring(0, padID.indexOf("$")); - db.get("group:" + groupID, function (err, group) { + db.db.get("group:" + groupID, function (err, group) { if (ERR(err, callback)) return; // remove the pad entry From 58d0e6cea462cac9571d2f7b03f2b6a07a7ed0df Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Wed, 30 Jan 2019 10:41:10 +0000 Subject: [PATCH 043/183] APIHandler.js: further cleanup - removed possible issue with failing to sanitize `padName` if `padId` was also supplied - removed unnecessary `try` block - simplified API and function name matching tests --- src/node/handler/APIHandler.js | 48 +++++++++++----------------------- 1 file changed, 15 insertions(+), 33 deletions(-) diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index 07b9b55c5..3898daaf5 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -19,7 +19,6 @@ */ var absolutePaths = require('../utils/AbsolutePaths'); -var ERR = require("async-stacktrace"); var fs = require("fs"); var api = require("../db/API"); var log4js = require('log4js'); @@ -152,35 +151,16 @@ exports.version = version; */ exports.handle = async function(apiVersion, functionName, fields, req, res) { - //check if this is a valid apiversion - var isKnownApiVersion = false; - - for (var knownApiVersion in version) { - if (knownApiVersion == apiVersion) { - isKnownApiVersion = true; - break; - } - } - // say goodbye if this is an unknown API version - if (!isKnownApiVersion) { + if (!(apiVersion in version)) { res.statusCode = 404; res.send({code: 3, message: "no such api version", data: null}); return; } - // check if this is a valid function name - var isKnownFunctionname = false; - - for (var knownFunctionname in version[apiVersion]) { - if (knownFunctionname == functionName) { - isKnownFunctionname = true; - break; - } - } - // say goodbye if this is an unknown function - if (!isKnownFunctionname) { + if (!(functionName in version[apiVersion])) { + // no status code?! res.send({code: 3, message: "no such function", data: null}); return; } @@ -194,17 +174,19 @@ exports.handle = async function(apiVersion, functionName, fields, req, res) return; } - try { - // sanitize any padIDs before continuing - if (fields["padID"]) { - fields["padID"] = await padManager.sanitizePadId(fields["padID"]); - } else if (fields["padName"]) { - fields["padName"] = await padManager.sanitizePadId(fields["padName"]); - } - await callAPI(apiVersion, functionName, fields, req, res); - } catch (e) { - ERR(e); + // sanitize any padIDs before continuing + if (fields["padID"]) { + fields["padID"] = await padManager.sanitizePadId(fields["padID"]); } + // there was an 'else' here before - removed it to ensure + // that this sanitize step can't be circumvented by forcing + // the first branch to be taken + if (fields["padName"]) { + fields["padName"] = await padManager.sanitizePadId(fields["padName"]); + } + + // no need to await - callAPI returns a promise + return callAPI(apiVersion, functionName, fields, req, res); } // calls the api function From c499a08030b76fc0a5e5835d9b1c986117e2a903 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Wed, 30 Jan 2019 10:47:50 +0000 Subject: [PATCH 044/183] bin/repairPad.js: conversion to promise/async - but see also github issue #3545 --- bin/repairPad.js | 144 +++++++++++++++++++---------------------------- 1 file changed, 58 insertions(+), 86 deletions(-) diff --git a/bin/repairPad.js b/bin/repairPad.js index 28f28cb6e..d495baef5 100644 --- a/bin/repairPad.js +++ b/bin/repairPad.js @@ -1,106 +1,78 @@ /* - This is a repair tool. It extracts all datas of a pad, removes and inserts them again. -*/ + * This is a repair tool. It extracts all datas of a pad, removes and inserts them again. + */ console.warn("WARNING: This script must not be used while etherpad is running!"); -if(process.argv.length != 3) -{ +if (process.argv.length != 3) { console.error("Use: node bin/repairPad.js $PADID"); process.exit(1); } -//get the padID + +// get the padID var padId = process.argv[2]; -var db, padManager, pad, settings; -var neededDBValues = ["pad:"+padId]; +let npm = require("../src/node_modules/npm"); +npm.load({}, async function(er) { + if (er) { + console.error("Could not load NPM: " + er) + process.exit(1); + } -var npm = require("../src/node_modules/npm"); -var async = require("../src/node_modules/async"); + try { + // intialize database + let settings = require('../src/node/utils/Settings'); + let db = require('../src/node/db/DB'); + await db.init(); -async.series([ - // load npm - function(callback) { - npm.load({}, function(er) { - if(er) - { - console.error("Could not load NPM: " + er) - process.exit(1); - } - else - { - callback(); - } - }) - }, - // load modules - function(callback) { - settings = require('../src/node/utils/Settings'); - db = require('../src/node/db/DB'); - callback(); - }, - //initialize the database - function (callback) - { - db.init(callback); - }, - //get the pad - function (callback) - { - padManager = require('../src/node/db/PadManager'); - - padManager.getPad(padId, function(err, _pad) - { - pad = _pad; - callback(err); - }); - }, - function (callback) - { - //add all authors - var authors = pad.getAllAuthors(); - for(var i=0;i "globalAuthor:")); + + // add all revisions + for (let rev = 0; rev <= pad.head; ++rev) { + neededDBValues.push("pad:" + padId + ":revs:" + rev); } - - //add all revisions - var revHead = pad.head; - for(var i=0;i<=revHead;i++) - { - neededDBValues.push("pad:"+padId+":revs:" + i); + + // add all chat values + for (let chat = 0; chat <= pad.chatHead; ++chat) { + neededDBValues.push("pad:" + padId + ":chat:" + chat); } - - //get all chat values - var chatHead = pad.chatHead; - for(var i=0;i<=chatHead;i++) - { - neededDBValues.push("pad:"+padId+":chat:" + i); - } - callback(); - }, - function (callback) { - db = db.db; + + // + // NB: this script doesn't actually does what's documented + // since the `value` fields in the following `.forEach` + // block are just the array index numbers + // + // the script therefore craps out now before it can do + // any damage. + // + // See gitlab issue #3545 + // + console.info("aborting [gitlab #3545]"); + process.exit(1); + + // now fetch and reinsert every key neededDBValues.forEach(function(key, value) { - console.debug("Key: "+key+", value: "+value); + console.log("Key: " + key+ ", value: " + value); db.remove(key); db.set(key, value); }); - callback(); - } -], function (err) -{ - if(err) throw err; - else - { + console.info("finished"); - process.exit(); + process.exit(0); + + } catch (er) { + if (er.name === "apierror") { + console.error(er); + } else { + console.trace(er); + } } }); - -//get the pad object -//get all revisions of this pad -//get all authors related to this pad -//get the readonly link related to this pad -//get the chat entries related to this pad -//remove all keys from database and insert them again From d543d5ae6af0cbad75e942e1c7e3a9f892d46e5a Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Wed, 30 Jan 2019 10:43:01 +0000 Subject: [PATCH 045/183] PadMessageHandler.js: convert handleUserChanges() to Promises - the call site still expects a nodeback function, so also introduced the `nodeify` module to allow that function to work as expected. --- src/node/handler/PadMessageHandler.js | 253 +++++++++++--------------- src/package.json | 2 +- 2 files changed, 109 insertions(+), 146 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 3004ae49c..1e9496cff 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -39,6 +39,7 @@ var channels = require("channels"); var stats = require('../stats'); var remoteAddress = require("../utils/RemoteAddress").remoteAddress; const thenify = require("thenify").withCallback; +const nodeify = require("nodeify"); /** * A associative array that saves informations about a session @@ -61,7 +62,11 @@ stats.gauge('totalUsers', function() { /** * A changeset queue per pad that is processed by handleUserChanges() */ -var padChannels = new channels.channels(thenify(handleUserChanges)); +var padChannels = new channels.channels(handleUserChangesCB); + +function handleUserChangesCB(data, callback) { + return nodeify(handleUserChanges(data), callback); +} /** * Saves the Socket class we need to send and receive data from the client @@ -591,7 +596,7 @@ function handleUserInfoUpdate(client, message) * @param client the client that send this message * @param message the message from the client */ -function handleUserChanges(data, cb) +async function handleUserChanges(data) { var client = data.client , message = data.message @@ -602,17 +607,17 @@ function handleUserChanges(data, cb) // Make sure all required fields are present if (message.data.baseRev == null) { messageLogger.warn("Dropped message, USER_CHANGES Message has no baseRev!"); - return cb(); + return; } if (message.data.apool == null) { messageLogger.warn("Dropped message, USER_CHANGES Message has no apool!"); - return cb(); + return; } if (message.data.changeset == null) { messageLogger.warn("Dropped message, USER_CHANGES Message has no changeset!"); - return cb(); + return; } // TODO: this might happen with other messages too => find one place to copy the session @@ -620,7 +625,7 @@ function handleUserChanges(data, cb) // if the session was valid when the message arrived in the first place if (!sessioninfos[client.id]) { messageLogger.warn("Dropped message, disconnect happened in the mean time"); - return cb(); + return; } // get all Vars we need @@ -632,172 +637,130 @@ function handleUserChanges(data, cb) // finish processing the changeset, so keep a reference to the session. var thisSession = sessioninfos[client.id]; - var r, apool, pad; - // Measure time to process edit var stopWatch = stats.timer('edits').start(); - async.series([ - // get the pad - function(callback) { - padManager.getPad(thisSession.padId, function(err, value) { - if (ERR(err, callback)) return; + // get the pad + let pad = await padManager.getPad(thisSession.padId); - pad = value; - callback(); + // create the changeset + try { + try { + // Verify that the changeset has valid syntax and is in canonical form + Changeset.checkRep(changeset); + + // Verify that the attribute indexes used in the changeset are all + // defined in the accompanying attribute pool. + Changeset.eachAttribNumber(changeset, function(n) { + if (!wireApool.getAttrib(n)) { + throw new Error("Attribute pool is missing attribute " + n + " for changeset " + changeset); + } }); - }, - // create the changeset - function(callback) { - // ex. _checkChangesetAndPool + // Validate all added 'author' attribs to be the same value as the current user + var iterator = Changeset.opIterator(Changeset.unpack(changeset).ops) + , op; - try { - // Verify that the changeset has valid syntax and is in canonical form - Changeset.checkRep(changeset); + while (iterator.hasNext()) { + op = iterator.next() - // Verify that the attribute indexes used in the changeset are all - // defined in the accompanying attribute pool. - Changeset.eachAttribNumber(changeset, function(n) { - if (!wireApool.getAttrib(n)) { - throw new Error("Attribute pool is missing attribute " + n + " for changeset " + changeset); + // + can add text with attribs + // = can change or add attribs + // - can have attribs, but they are discarded and don't show up in the attribs - but do show up in the pool + + op.attribs.split('*').forEach(function(attr) { + if (!attr) return; + + attr = wireApool.getAttrib(attr); + if (!attr) return; + + // the empty author is used in the clearAuthorship functionality so this should be the only exception + if ('author' == attr[0] && (attr[1] != thisSession.author && attr[1] != '')) { + throw new Error("Trying to submit changes as another author in changeset " + changeset); } }); - - // Validate all added 'author' attribs to be the same value as the current user - var iterator = Changeset.opIterator(Changeset.unpack(changeset).ops) - , op - while(iterator.hasNext()) { - op = iterator.next() - - // + can add text with attribs - // = can change or add attribs - // - can have attribs, but they are discarded and don't show up in the attribs - but do show up in the pool - - op.attribs.split('*').forEach(function(attr) { - if (!attr) return; - - attr = wireApool.getAttrib(attr) - if (!attr) return; - - // the empty author is used in the clearAuthorship functionality so this should be the only exception - if ('author' == attr[0] && (attr[1] != thisSession.author && attr[1] != '')) { - throw new Error("Trying to submit changes as another author in changeset " + changeset); - } - }); - } - - // ex. adoptChangesetAttribs - - // Afaik, it copies the new attributes from the changeset, to the global Attribute Pool - changeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool); - } catch(e) { - // There is an error in this changeset, so just refuse it - client.json.send({disconnect:"badChangeset"}); - stats.meter('failedChangesets').mark(); - return callback(new Error("Can't apply USER_CHANGES, because "+e.message)); } - // ex. applyUserChanges - apool = pad.pool; - r = baseRev; + // ex. adoptChangesetAttribs - // The client's changeset might not be based on the latest revision, - // since other clients are sending changes at the same time. - // Update the changeset so that it can be applied to the latest revision. - // https://github.com/caolan/async#whilst - async.whilst( - function() { return r < pad.getHeadRevisionNumber(); }, - function(callback) - { - r++; + // Afaik, it copies the new attributes from the changeset, to the global Attribute Pool + changeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool); - pad.getRevisionChangeset(r, function(err, c) { - if (ERR(err, callback)) return; + } catch(e) { + // There is an error in this changeset, so just refuse it + client.json.send({ disconnect: "badChangeset" }); + stats.meter('failedChangesets').mark(); + throw new Error("Can't apply USER_CHANGES, because " + e.message); + } - // At this point, both "c" (from the pad) and "changeset" (from the - // client) are relative to revision r - 1. The follow function - // rebases "changeset" so that it is relative to revision r - // and can be applied after "c". - try { - // a changeset can be based on an old revision with the same changes in it - // prevent eplite from accepting it TODO: better send the client a NEW_CHANGES - // of that revision - if (baseRev + 1 == r && c == changeset) { - client.json.send({disconnect:"badChangeset"}); - stats.meter('failedChangesets').mark(); + // ex. applyUserChanges + let apool = pad.pool; + let r = baseRev; - return callback(new Error("Won't apply USER_CHANGES, because it contains an already accepted changeset")); - } + // The client's changeset might not be based on the latest revision, + // since other clients are sending changes at the same time. + // Update the changeset so that it can be applied to the latest revision. + while (r < pad.getHeadRevisionNumber()) { + r++; - changeset = Changeset.follow(c, changeset, false, apool); - } catch(e) { - client.json.send({disconnect:"badChangeset"}); - stats.meter('failedChangesets').mark(); + let c = await pad.getRevisionChangeset(r); - return callback(new Error("Can't apply USER_CHANGES, because "+e.message)); - } - - if ((r - baseRev) % 200 == 0) { - // don't let the stack get too deep - async.nextTick(callback); - } else { - callback(null); - } - }); - }, - - // use the callback of the series function - callback - ); - }, - - // do correction changesets, and send it to all users - function(callback) { - var prevText = pad.text(); - - if (Changeset.oldLen(changeset) != prevText.length) { - client.json.send({disconnect:"badChangeset"}); - stats.meter('failedChangesets').mark(); - - return callback(new Error("Can't apply USER_CHANGES "+changeset+" with oldLen " + Changeset.oldLen(changeset) + " to document of length " + prevText.length)); - } + // At this point, both "c" (from the pad) and "changeset" (from the + // client) are relative to revision r - 1. The follow function + // rebases "changeset" so that it is relative to revision r + // and can be applied after "c". try { - pad.appendRevision(changeset, thisSession.author); + // a changeset can be based on an old revision with the same changes in it + // prevent eplite from accepting it TODO: better send the client a NEW_CHANGES + // of that revision + if (baseRev + 1 == r && c == changeset) { + client.json.send({disconnect:"badChangeset"}); + stats.meter('failedChangesets').mark(); + throw new Error("Won't apply USER_CHANGES, because it contains an already accepted changeset"); + } + + changeset = Changeset.follow(c, changeset, false, apool); } catch(e) { client.json.send({disconnect:"badChangeset"}); stats.meter('failedChangesets').mark(); - - return callback(e) + throw new Error("Can't apply USER_CHANGES, because " + e.message); } - - var correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool); - if (correctionChangeset) { - pad.appendRevision(correctionChangeset); - } - - // Make sure the pad always ends with an empty line. - if (pad.text().lastIndexOf("\n") != pad.text().length-1) { - var nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length-1, - 0, "\n"); - pad.appendRevision(nlChangeset); - } - - exports.updatePadClients(pad, function(er) { - ERR(er) - }); - callback(); } - ], - function(err) { - stopWatch.end() - cb(); - if(err) { - console.warn(err.stack || err); + let prevText = pad.text(); + + if (Changeset.oldLen(changeset) != prevText.length) { + client.json.send({disconnect:"badChangeset"}); + stats.meter('failedChangesets').mark(); + throw new Error("Can't apply USER_CHANGES "+changeset+" with oldLen " + Changeset.oldLen(changeset) + " to document of length " + prevText.length); } - }); + + try { + pad.appendRevision(changeset, thisSession.author); + } catch(e) { + client.json.send({ disconnect: "badChangeset" }); + stats.meter('failedChangesets').mark(); + throw e; + } + + let correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool); + if (correctionChangeset) { + pad.appendRevision(correctionChangeset); + } + + // Make sure the pad always ends with an empty line. + if (pad.text().lastIndexOf("\n") != pad.text().length-1) { + var nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length - 1, 0, "\n"); + pad.appendRevision(nlChangeset); + } + + await exports.updatePadClients(pad); + } catch (err) { + console.warn(err.stack || err); + } + + stopWatch.end(); } exports.updatePadClients = thenify(function(pad, callback) diff --git a/src/package.json b/src/package.json index fa63b0710..11259e76d 100644 --- a/src/package.json +++ b/src/package.json @@ -48,6 +48,7 @@ "languages4translatewiki": "0.1.3", "log4js": "0.6.35", "measured-core": "1.11.2", + "nodeify": "^1.0.1", "npm": "6.4.1", "object.values": "^1.0.4", "request": "2.88.0", @@ -87,4 +88,3 @@ "version": "1.7.5", "license": "Apache-2.0" } - From 9246a1de26cdf94238dd9296be060b64e3d9f346 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Wed, 30 Jan 2019 13:55:49 +0000 Subject: [PATCH 046/183] PadMessageHandler.js: further conversion --- src/node/handler/PadMessageHandler.js | 397 +++++++++----------------- 1 file changed, 135 insertions(+), 262 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 1e9496cff..25f9879bc 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -1274,7 +1274,7 @@ async function handleClientReady(client, message) /** * Handles a request for a rough changeset, the timeslider client needs it */ -function handleChangesetRequest(client, message) +async function handleChangesetRequest(client, message) { // check if all ok if (message.data == null) { @@ -1308,309 +1308,182 @@ function handleChangesetRequest(client, message) return; } - var granularity = message.data.granularity; - var start = message.data.start; - var end = start + (100 * granularity); - var padIds; + let granularity = message.data.granularity; + let start = message.data.start; + let end = start + (100 * granularity); - async.series([ - function(callback) { - readOnlyManager.getIds(message.padId, function(err, value) { - if (ERR(err, callback)) return; + let padIds = await readOnlyManager.getIds(message.padId); - padIds = value; - callback(); - }); - }, - - function(callback) { - // build the requested rough changesets and send them back - getChangesetInfo(padIds.padId, start, end, granularity, function(err, changesetInfo) { - if (err) return console.error('Error while handling a changeset request for ' + padIds.padId, err, message.data); - - var data = changesetInfo; - data.requestID = message.data.requestID; - - client.json.send({ type: "CHANGESET_REQ", data: data }); - }); - } - ]); + // build the requested rough changesets and send them back + try { + let data = await getChangesetInfo(padIds.padId, start, end, granularity); + data.requestID = message.data.requestID; + client.json.send({ type: "CHANGESET_REQ", data }); + } catch (err) { + console.error('Error while handling a changeset request for ' + padIds.padId, err, message.data); + } } /** * Tries to rebuild the getChangestInfo function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144 */ -let getChangesetInfo = thenify(function getChangesetInfo(padId, startNum, endNum, granularity, callback) +async function getChangesetInfo(padId, startNum, endNum, granularity) { - var forwardsChangesets = []; - var backwardsChangesets = []; - var timeDeltas = []; - var apool = new AttributePool(); - var pad; - var composedChangesets = {}; - var revisionDate = []; - var lines; - var head_revision = 0; + let pad = await padManager.getPad(padId); + let head_revision = pad.getHeadRevisionNumber(); - async.series([ - // get the pad from the database - function(callback) { - padManager.getPad(padId, function(err, _pad) { - if (ERR(err, callback)) return; + // calculate the last full endnum + if (endNum > head_revision + 1) { + endNum = head_revision + 1; + } + endNum = Math.floor(endNum / granularity) * granularity; - pad = _pad; - head_revision = pad.getHeadRevisionNumber(); - callback(); - }); - }, + let compositesChangesetNeeded = []; + let revTimesNeeded = []; - function(callback) { - // calculate the last full endnum - var lastRev = pad.getHeadRevisionNumber(); - if (endNum > lastRev + 1) { - endNum = lastRev + 1; - } + // figure out which composite Changeset and revTimes we need, to load them in bulk + for (let start = startNum; start < endNum; start += granularity) { + let end = start + granularity; - endNum = Math.floor(endNum / granularity) * granularity; + // add the composite Changeset we needed + compositesChangesetNeeded.push({ start, end }); - var compositesChangesetNeeded = []; - var revTimesNeeded = []; + // add the t1 time we need + revTimesNeeded.push(start == 0 ? 0 : start - 1); - // figure out which composite Changeset and revTimes we need, to load them in bulk - var compositeStart = startNum; - while (compositeStart < endNum) { - var compositeEnd = compositeStart + granularity; + // add the t2 time we need + revTimesNeeded.push(end - 1); + } - // add the composite Changeset we needed - compositesChangesetNeeded.push({ start: compositeStart, end: compositeEnd }); + // get all needed db values parallel - no await here since + // it would make all the lookups run in series - // add the t1 time we need - revTimesNeeded.push(compositeStart == 0 ? 0 : compositeStart - 1); + // get all needed composite Changesets + let composedChangesets = {}; + let p1 = Promise.all(compositesChangesetNeeded.map(item => { + return composePadChangesets(padId, item.start, item.end).then(changeset => { + composedChangesets[item.start + "/" + item.end] = changeset; + }); + })); - // add the t2 time we need - revTimesNeeded.push(compositeEnd - 1); + // get all needed revision Dates + let revisionDate = []; + let p2 = Promise.all(revTimesNeeded.map(revNum => { + return pad.getRevisionDate(revNum).then(revDate => { + revisionDate[revNum] = Math.floor(revDate / 1000); + }); + })); - compositeStart += granularity; - } - - // get all needed db values parallel - async.parallel([ - function(callback) { - // get all needed composite Changesets - async.forEach(compositesChangesetNeeded, function(item, callback) { - composePadChangesets(padId, item.start, item.end, function(err, changeset) { - if (ERR(err, callback)) return; - - composedChangesets[item.start + "/" + item.end] = changeset; - callback(); - }); - }, callback); - }, - - function(callback) { - // get all needed revision Dates - async.forEach(revTimesNeeded, function(revNum, callback) { - pad.getRevisionDate(revNum, function(err, revDate) { - if (ERR(err, callback)) return; - - revisionDate[revNum] = Math.floor(revDate/1000); - callback(); - }); - }, callback); - }, - - // get the lines - function(callback) { - getPadLines(padId, startNum-1, function(err, _lines) { - if (ERR(err, callback)) return; - - lines = _lines; - callback(); - }); - } - ], callback); - }, - - // don't know what happens here exactly :/ - function(callback) { - var compositeStart = startNum; - - while (compositeStart < endNum) { - var compositeEnd = compositeStart + granularity; - if (compositeEnd > endNum || compositeEnd > head_revision+1) { - break; - } - - var forwards = composedChangesets[compositeStart + "/" + compositeEnd]; - var backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool()); - - Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool()); - Changeset.mutateTextLines(forwards, lines.textlines); - - var forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool); - var backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool); - - var t1, t2; - if (compositeStart == 0) { - t1 = revisionDate[0]; - } else { - t1 = revisionDate[compositeStart - 1]; - } - - t2 = revisionDate[compositeEnd - 1]; - - timeDeltas.push(t2 - t1); - forwardsChangesets.push(forwards2); - backwardsChangesets.push(backwards2); - - compositeStart += granularity; - } - - callback(); - } - ], - function(err) { - if (ERR(err, callback)) return; - - callback(null, {forwardsChangesets: forwardsChangesets, - backwardsChangesets: backwardsChangesets, - apool: apool.toJsonable(), - actualEndNum: endNum, - timeDeltas: timeDeltas, - start: startNum, - granularity: granularity }); + // get the lines + let lines; + let p3 = getPadLines(padId, startNum - 1).then(_lines => { + lines = _lines; }); -}); + + // wait for all of the above to complete + await Promise.all([p1, p2, p3]); + + // doesn't know what happens here exactly :/ + let timeDeltas = []; + let forwardsChangesets = []; + let backwardsChangesets = []; + let apool = new AttributePool(); + + for (let compositeStart = startNum; compositeStart < endNum; compositeStart += granularity) { + let compositeEnd = compositeStart + granularity; + if (compositeEnd > endNum || compositeEnd > head_revision + 1) { + break; + } + + let forwards = composedChangesets[compositeStart + "/" + compositeEnd]; + let backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool()); + + Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool()); + Changeset.mutateTextLines(forwards, lines.textlines); + + let forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool); + let backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool); + + let t1 = (compositeStart == 0) ? revisionDate[0] : revisionDate[compositeStart - 1]; + let t2 = revisionDate[compositeEnd - 1]; + + timeDeltas.push(t2 - t1); + forwardsChangesets.push(forwards2); + backwardsChangesets.push(backwards2); + } + + return { forwardsChangesets, backwardsChangesets, + apool: apool.toJsonable(), actualEndNum: endNum, + timeDeltas, start: startNum, granularity }; +} /** * Tries to rebuild the getPadLines function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263 */ -let getPadLines = thenify(function getPadLines(padId, revNum, callback) +async function getPadLines(padId, revNum) { - var atext; - var result = {}; - var pad; + let pad = padManager.getPad(padId); - async.series([ - // get the pad from the database - function(callback) { - padManager.getPad(padId, function(err, _pad) { - if (ERR(err, callback)) return; + // get the atext + let atext; - pad = _pad; - callback(); - }); - }, + if (revNum >= 0) { + atext = await pad.getInternalRevisionAText(revNum); + } else { + atext = Changeset.makeAText("\n"); + } - // get the atext - function(callback) { - if (revNum >= 0) { - pad.getInternalRevisionAText(revNum, function(err, _atext) { - if (ERR(err, callback)) return; - - atext = _atext; - callback(); - }); - } else { - atext = Changeset.makeAText("\n"); - callback(null); - } - }, - - function(callback) { - result.textlines = Changeset.splitTextLines(atext.text); - result.alines = Changeset.splitAttributionLines(atext.attribs, atext.text); - callback(null); - } - ], - - function(err) { - if (ERR(err, callback)) return; - - callback(null, result); - }); -}); + return { + textlines: Changeset.splitTextLines(atext.text), + alines: Changeset.splitAttributionLines(atext.attribs, atext.text) + }; +} /** * Tries to rebuild the composePadChangeset function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241 */ -let composePadChangesets = thenify(function(padId, startNum, endNum, callback) +async function composePadChangesets (padId, startNum, endNum) { - var pad; - var changesets = {}; - var changeset; + let pad = await padManager.getPad(padId); - async.series([ - // get the pad from the database - function(callback) { - padManager.getPad(padId, function(err, _pad) { - if (ERR(err, callback)) return; + // fetch all changesets we need + let headNum = pad.getHeadRevisionNumber(); + endNum = Math.min(endNum, headNum + 1); + startNum = Math.max(startNum, 0); - pad = _pad; - callback(); - }); - }, + // create an array for all changesets, we will + // replace the values with the changeset later + let changesetsNeeded = []; + for (let r = startNum ; r < endNum; r++) { + changesetsNeeded.push(r); + } - // fetch all changesets we need - function(callback) { - var changesetsNeeded=[]; + // get all changesets + let changesets = {}; + await Promise.all(changesetsNeeded.map(revNum => { + return pad.getRevisionChangeset(revNum).then(changeset => changesets[revNum] = changeset); + })); - var headNum = pad.getHeadRevisionNumber(); - if (endNum > headNum + 1) { - endNum = headNum + 1; - } + // compose Changesets + try { + let changeset = changesets[startNum]; + let pool = pad.apool(); - if (startNum < 0) { - startNum = 0; - } - - // create an array for all changesets, we will - // replace the values with the changeset later - for (var r = startNum; r < endNum; r++) { - changesetsNeeded.push(r); - } - - // get all changesets - async.forEach(changesetsNeeded, function(revNum,callback) { - pad.getRevisionChangeset(revNum, function(err, value) { - if (ERR(err, callback)) return; - - changesets[revNum] = value; - callback(); - }); - },callback); - }, - - // compose Changesets - function(callback) { - changeset = changesets[startNum]; - var pool = pad.apool(); - - try { - for (var r = startNum + 1; r < endNum; r++) { - var cs = changesets[r]; - changeset = Changeset.compose(changeset, cs, pool); - } - } catch(e) { - // r-1 indicates the rev that was build starting with startNum, applying startNum+1, +2, +3 - console.warn("failed to compose cs in pad:", padId, " startrev:", startNum, " current rev:", r); - return callback(e); - } - - callback(null); + for (let r = startNum + 1; r < endNum; r++) { + let cs = changesets[r]; + changeset = Changeset.compose(changeset, cs, pool); } - ], + return changeset; - // return err and changeset - function(err) { - if (ERR(err, callback)) return; - - callback(null, changeset); - }); -}); + } catch (e) { + // r-1 indicates the rev that was build starting with startNum, applying startNum+1, +2, +3 + console.warn("failed to compose cs in pad:", padId, " startrev:", startNum," current rev:", r); + throw e; + } +} function _getRoomClients(padID) { var roomClients = []; From bb80325d2cf8e653cb8fc7f8b40e0a64c6b3c071 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Wed, 30 Jan 2019 15:27:42 +0000 Subject: [PATCH 047/183] PadMessageHandler.js: completed conversion --- src/node/handler/ImportHandler.js | 6 +- src/node/handler/PadMessageHandler.js | 297 ++++++++++---------------- 2 files changed, 115 insertions(+), 188 deletions(-) diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index ec07bf6e5..e92df4434 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -289,9 +289,9 @@ exports.doImport = function(req, res, padId) return; } - padMessageHandler.updatePadClients(pad, function(){ - callback(); - }); + // @TODO: not waiting for updatePadClients to finish + padMessageHandler.updatePadClients(pad); + callback(); }); }, diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 25f9879bc..b41af86b9 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -19,8 +19,6 @@ */ -var ERR = require("async-stacktrace"); -var async = require("async"); var padManager = require("../db/PadManager"); var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var AttributePool = require("ep_etherpad-lite/static/js/AttributePool"); @@ -38,7 +36,6 @@ var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); var channels = require("channels"); var stats = require('../stats'); var remoteAddress = require("../utils/RemoteAddress").remoteAddress; -const thenify = require("thenify").withCallback; const nodeify = require("nodeify"); /** @@ -62,16 +59,14 @@ stats.gauge('totalUsers', function() { /** * A changeset queue per pad that is processed by handleUserChanges() */ -var padChannels = new channels.channels(handleUserChangesCB); - -function handleUserChangesCB(data, callback) { +var padChannels = new channels.channels(function(data, callback) { return nodeify(handleUserChanges(data), callback); -} +}); /** * Saves the Socket class we need to send and receive data from the client */ -var socketio; +let socketio; /** * This Method is called by server.js to tell the message handler on which socket it should send @@ -115,17 +110,17 @@ exports.kickSessionsFromPad = function(padID) * Handles the disconnection of a user * @param client the client that leaves */ -exports.handleDisconnect = function(client) +exports.handleDisconnect = async function(client) { stats.meter('disconnects').mark(); // save the padname of this session - var session = sessioninfos[client.id]; + let session = sessioninfos[client.id]; // if this connection was already etablished with a handshake, send a disconnect message to the others if (session && session.author) { // Get the IP address from our persistant object - var ip = remoteAddress[client.id]; + let ip = remoteAddress[client.id]; // Anonymize the IP address if IP logging is disabled if (settings.disableIPlogging) { @@ -135,29 +130,27 @@ exports.handleDisconnect = function(client) accessLogger.info('[LEAVE] Pad "' + session.padId + '": Author "' + session.author + '" on client ' + client.id + ' with IP "' + ip + '" left the pad'); // get the author color out of the db - authorManager.getAuthorColorId(session.author, function(err, color) { - ERR(err); + let color = await authorManager.getAuthorColorId(session.author); - // prepare the notification for the other users on the pad, that this user left - var messageToTheOtherUsers = { - "type": "COLLABROOM", - "data": { - type: "USER_LEAVE", - userInfo: { - "ip": "127.0.0.1", - "colorId": color, - "userAgent": "Anonymous", - "userId": session.author - } + // prepare the notification for the other users on the pad, that this user left + let messageToTheOtherUsers = { + "type": "COLLABROOM", + "data": { + type: "USER_LEAVE", + userInfo: { + "ip": "127.0.0.1", + "colorId": color, + "userAgent": "Anonymous", + "userId": session.author } - }; + } + }; - // Go through all user that are still on the pad, and send them the USER_LEAVE message - client.broadcast.to(session.padId).json.send(messageToTheOtherUsers); + // Go through all user that are still on the pad, and send them the USER_LEAVE message + client.broadcast.to(session.padId).json.send(messageToTheOtherUsers); - // Allow plugins to hook into users leaving the pad - hooks.callAll("userLeave", session); - }); + // Allow plugins to hook into users leaving the pad + hooks.callAll("userLeave", session); } // Delete the sessioninfos entrys of this session @@ -179,7 +172,7 @@ exports.handleMessage = async function(client, message) return; } - var thisSession = sessioninfos[client.id]; + let thisSession = sessioninfos[client.id]; if (!thisSession) { messageLogger.warn("Dropped message from an unknown connection.") @@ -248,7 +241,7 @@ exports.handleMessage = async function(client, message) /* * In a previous version of this code, an "if (message)" wrapped the - * following async.series(). + * following series of async calls [now replaced with await calls] * This ugly "!Boolean(message)" is a lame way to exactly negate the truthy * condition and replace it with an early return, while being sure to leave * the original behaviour unchanged. @@ -310,15 +303,13 @@ exports.handleMessage = async function(client, message) * @param client the client that send this message * @param message the message from the client */ -function handleSaveRevisionMessage(client, message){ +async function handleSaveRevisionMessage(client, message) +{ var padId = sessioninfos[client.id].padId; var userId = sessioninfos[client.id].author; - padManager.getPad(padId, function(err, pad) { - if (ERR(err)) return; - - pad.addSavedRevision(pad.head, userId); - }); + let pad = await padManager.getPad(padId); + pad.addSavedRevision(pad.head, userId); } /** @@ -328,7 +319,7 @@ function handleSaveRevisionMessage(client, message){ * @param msg {Object} the message we're sending * @param sessionID {string} the socketIO session to which we're sending this message */ -exports.handleCustomObjectMessage = async function(msg, sessionID) { +exports.handleCustomObjectMessage = function(msg, sessionID) { if (msg.data.type === "CUSTOM") { if (sessionID){ // a sessionID is targeted: directly to this sessionID @@ -346,9 +337,9 @@ exports.handleCustomObjectMessage = async function(msg, sessionID) { * @param padID {Pad} the pad to which we're sending this message * @param msgString {String} the message we're sending */ -exports.handleCustomMessage = thenify(function(padID, msgString, cb) { - var time = Date.now(); - var msg = { +exports.handleCustomMessage = function(padID, msgString) { + let time = Date.now(); + let msg = { type: 'COLLABROOM', data: { type: msgString, @@ -356,9 +347,7 @@ exports.handleCustomMessage = thenify(function(padID, msgString, cb) { } }; socketio.sockets.in(padID).json.send(msg); - - cb(null, {}); -}); +} /** * Handles a Chat Message @@ -382,55 +371,24 @@ function handleChatMessage(client, message) * @param text the text of the chat message * @param padId the padId to send the chat message to */ -exports.sendChatMessageToPadClients = function(time, userId, text, padId) { - var pad; - var userName; +exports.sendChatMessageToPadClients = async function(time, userId, text, padId) +{ + // get the pad + let pad = await padManager.getPad(padId); - async.series([ - // get the pad - function(callback) { - padManager.getPad(padId, function(err, _pad) { - if (ERR(err, callback)) return; + // get the author + let userName = await authorManager.getAuthorName(userId); - pad = _pad; - callback(); - }); - }, + // save the chat message + pad.appendChatMessage(text, userId, time); - function(callback) { - authorManager.getAuthorName(userId, function(err, _userName) { - if (ERR(err, callback)) return; + let msg = { + type: "COLLABROOM", + data: { type: "CHAT_MESSAGE", userId, userName, time, text } + }; - userName = _userName; - callback(); - }); - }, - - // save the chat message and broadcast it - function(callback) { - // save the chat message - pad.appendChatMessage(text, userId, time); - - var msg = { - type: "COLLABROOM", - data: { - type: "CHAT_MESSAGE", - userId: userId, - userName: userName, - time: time, - text: text - } - }; - - // broadcast the chat message to everyone on the pad - socketio.sockets.in(padId).json.send(msg); - - callback(); - } - ], - function(err) { - ERR(err); - }); + // broadcast the chat message to everyone on the pad + socketio.sockets.in(padId).json.send(msg); } /** @@ -438,7 +396,7 @@ exports.sendChatMessageToPadClients = function(time, userId, text, padId) { * @param client the client that send this message * @param message the message from the client */ -function handleGetChatMessages(client, message) +async function handleGetChatMessages(client, message) { if (message.data.start == null) { messageLogger.warn("Dropped message, GetChatMessages Message has no start!"); @@ -450,45 +408,29 @@ function handleGetChatMessages(client, message) return; } - var start = message.data.start; - var end = message.data.end; - var count = end - start; + let start = message.data.start; + let end = message.data.end; + let count = end - start; if (count < 0 || count > 100) { messageLogger.warn("Dropped message, GetChatMessages Message, client requested invalid amount of messages!"); return; } - var padId = sessioninfos[client.id].padId; - var pad; + let padId = sessioninfos[client.id].padId; + let pad = await padManager.getPad(padId); - async.series([ - // get the pad - function(callback) { - padManager.getPad(padId, function(err, _pad) { - if (ERR(err, callback)) return; + let chatMessages = await pad.getChatMessages(start, end); + let infoMsg = { + type: "COLLABROOM", + data: { + type: "CHAT_MESSAGES", + messages: chatMessages + } + }; - pad = _pad; - callback(); - }); - }, - - function(callback) { - pad.getChatMessages(start, end, function(err, chatMessages) { - if (ERR(err, callback)) return; - - var infoMsg = { - type: "COLLABROOM", - data: { - type: "CHAT_MESSAGES", - messages: chatMessages - } - }; - - // send the messages back to the client - client.json.send(infoMsg); - }); - }]); + // send the messages back to the client + client.json.send(infoMsg); } /** @@ -763,13 +705,13 @@ async function handleUserChanges(data) stopWatch.end(); } -exports.updatePadClients = thenify(function(pad, callback) +exports.updatePadClients = async function(pad) { // skip this if no-one is on this pad - var roomClients = _getRoomClients(pad.id); + let roomClients = _getRoomClients(pad.id); if (roomClients.length == 0) { - return callback(); + return; } // since all clients usually get the same set of changesets, store them in local cache @@ -778,69 +720,54 @@ exports.updatePadClients = thenify(function(pad, callback) // BEFORE first result will be landed to our cache object. The solution is to replace parallel processing // via async.forEach with sequential for() loop. There is no real benefits of running this in parallel, // but benefit of reusing cached revision object is HUGE - var revCache = {}; + let revCache = {}; // go through all sessions on this pad - async.forEach(roomClients, function(client, callback){ - var sid = client.id; - // https://github.com/caolan/async#whilst + for (let client of roomClients) { + let sid = client.id; + // send them all new changesets - async.whilst( - function() { return sessioninfos[sid] && sessioninfos[sid].rev < pad.getHeadRevisionNumber()}, - function(callback) - { - var r = sessioninfos[sid].rev + 1; + while (sessioninfos[sid] && sessioninfos[sid].rev < pad.getHeadRevisionNumber()) { + let r = sessioninfos[sid].rev + 1; + let revision = revCache[r]; + if (!revision) { + revision = await pad.getRevision(r); + revCache[r] = revision; + } - async.waterfall([ - function(callback) { - if(revCache[r]) { - callback(null, revCache[r]); - } else { - pad.getRevision(r, callback); - } - }, + let author = revision.meta.author, + revChangeset = revision.changeset, + currentTime = revision.meta.timestamp; - function(revision, callback) { - revCache[r] = revision; + // next if session has not been deleted + if (sessioninfos[sid] == null) { + continue; + } - var author = revision.meta.author, - revChangeset = revision.changeset, - currentTime = revision.meta.timestamp; + if (author == sessioninfos[sid].author) { + client.json.send({ "type": "COLLABROOM", "data":{ type: "ACCEPT_COMMIT", newRev: r }}); + } else { + let forWire = Changeset.prepareForWire(revChangeset, pad.pool); + let wireMsg = {"type": "COLLABROOM", + "data": { type:"NEW_CHANGES", + newRev:r, + changeset: forWire.translated, + apool: forWire.pool, + author: author, + currentTime: currentTime, + timeDelta: currentTime - sessioninfos[sid].time + }}; - // next if session has not been deleted - if (sessioninfos[sid] == null) { - return callback(null); - } + client.json.send(wireMsg); + } - if (author == sessioninfos[sid].author) { - client.json.send({"type":"COLLABROOM","data":{type:"ACCEPT_COMMIT", newRev:r}}); - } else { - var forWire = Changeset.prepareForWire(revChangeset, pad.pool); - var wireMsg = {"type":"COLLABROOM", - "data":{type:"NEW_CHANGES", - newRev:r, - changeset: forWire.translated, - apool: forWire.pool, - author: author, - currentTime: currentTime, - timeDelta: currentTime - sessioninfos[sid].time - }}; - - client.json.send(wireMsg); - } - - if (sessioninfos[sid]) { - sessioninfos[sid].time = currentTime; - sessioninfos[sid].rev = r; - } - callback(null); - } - ], callback); - }, - callback - ); - },callback); -}); + if (sessioninfos[sid]) { + sessioninfos[sid].time = currentTime; + sessioninfos[sid].rev = r; + } + } + } +} /** * Copied from the Etherpad Source Code. Don't know what this method does excatly... @@ -893,12 +820,12 @@ function _correctMarkersInPad(atext, apool) { function handleSwitchToPad(client, message) { // clear the session and leave the room - var currentSession = sessioninfos[client.id]; - var padId = currentSession.padId; - var roomClients = _getRoomClients(padId); + let currentSession = sessioninfos[client.id]; + let padId = currentSession.padId; + let roomClients = _getRoomClients(padId); - async.forEach(roomClients, function(client, callback) { - var sinfo = sessioninfos[client.id]; + roomClients.forEach(client => { + let sinfo = sessioninfos[client.id]; if (sinfo && sinfo.author == currentSession.author) { // fix user's counter, works on page refresh or if user closes browser window and then rejoins sessioninfos[client.id] = {}; From 5192a0c498bd99a14bdf9729089b789ef64bfc86 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Wed, 30 Jan 2019 16:19:51 +0000 Subject: [PATCH 048/183] db/ReadOnlyManager.js: completed conversion Requires temporary hack within `Pad.remove()` to allow for the lack of callback on the rewritten version. --- src/node/db/Pad.js | 8 +++- src/node/db/ReadOnlyManager.js | 79 ++++++++++------------------------ 2 files changed, 30 insertions(+), 57 deletions(-) diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index 4abf2cab2..db35bd367 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -19,6 +19,7 @@ var crypto = require("crypto"); var randomString = require("../utils/randomstring"); var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); const thenify = require("thenify").withCallback; +const nodeify = require("nodeify"); // serialization/deserialization attributes var attributeBlackList = ["id"]; @@ -621,7 +622,12 @@ Pad.prototype.remove = thenify(function remove(callback) { // remove the readonly entries function(callback) { - readOnlyManager.getReadOnlyId(padID, function(err, readonlyID) { + // @TODO - temporary until surrounding code is Promisified + function getReadOnlyId(padID, callback) { + return nodeify(readOnlyManager.getReadOnlyId(padID), callback); + } + + getReadOnlyId(padID, function(err, readonlyID) { if (ERR(err, callback)) return; db.remove("pad2readonly:" + padID); diff --git a/src/node/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.js index 14c6f75c7..96a52d479 100644 --- a/src/node/db/ReadOnlyManager.js +++ b/src/node/db/ReadOnlyManager.js @@ -19,80 +19,47 @@ */ -var ERR = require("async-stacktrace"); -var db = require("./DB").db; -var async = require("async"); +var db = require("./DB"); var randomString = require("../utils/randomstring"); -const thenify = require("thenify").withCallback; /** * returns a read only id for a pad * @param {String} padId the id of the pad */ -exports.getReadOnlyId = thenify(function (padId, callback) +exports.getReadOnlyId = async function (padId) { - var readOnlyId; + // check if there is a pad2readonly entry + let readOnlyId = await db.get("pad2readonly:" + padId); - async.waterfall([ - // check if there is a pad2readonly entry - function(callback) { - db.get("pad2readonly:" + padId, callback); - }, + // there is no readOnly Entry in the database, let's create one + if (readOnlyId == null) { + readOnlyId = "r." + randomString(16); + db.set("pad2readonly:" + padId, readOnlyId); + db.set("readonly2pad:" + readOnlyId, padId); + } - function(dbReadOnlyId, callback) { - if (dbReadOnlyId == null) { - // there is no readOnly Entry in the database, let's create one - readOnlyId = "r." + randomString(16); - - db.set("pad2readonly:" + padId, readOnlyId); - db.set("readonly2pad:" + readOnlyId, padId); - } else { - // there is a readOnly Entry in the database, let's take this one - readOnlyId = dbReadOnlyId; - } - - callback(); - } - ], - function(err) { - if (ERR(err, callback)) return; - - // return the results - callback(null, readOnlyId); - }) -}); + return readOnlyId; +} /** * returns the padId for a read only id * @param {String} readOnlyId read only id */ -exports.getPadId = thenify(function(readOnlyId, callback) +exports.getPadId = function(readOnlyId) { - db.get("readonly2pad:" + readOnlyId, callback); -}); + return db.get("readonly2pad:" + readOnlyId); +} /** * returns the padId and readonlyPadId in an object for any id * @param {String} padIdOrReadonlyPadId read only id or real pad id */ -exports.getIds = thenify(function(id, callback) { - if (id.indexOf("r.") == 0) { - exports.getPadId(id, function (err, value) { - if (ERR(err, callback)) return; +exports.getIds = async function(id) { + let readonly = (id.indexOf("r.") === 0); - callback(null, { - readOnlyPadId: id, - padId: value, // Might be null, if this is an unknown read-only id - readonly: true - }); - }); - } else { - exports.getReadOnlyId(id, function (err, value) { - callback(null, { - readOnlyPadId: value, - padId: id, - readonly: false - }); - }); - } -}); + // Might be null, if this is an unknown read-only id + let readOnlyPadId = readonly ? id : await exports.getReadOnlyId(id); + let padId = readonly ? await exports.getPadId(id) : id; + + return { readOnlyPadId, padId, readonly }; +} From 62345ac8f7c733454fc051c0905cdf2d8dc0cb82 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Thu, 31 Jan 2019 08:55:36 +0000 Subject: [PATCH 049/183] import/export: conversion to Promises/async NB1: needs additional review and testing - no abiword available on my test bed NB2: in ImportHandler.js, directly delete the file, and handle the eventual error later: checking before for existence is prone to race conditions, and does not handle any errors anyway. --- src/node/db/API.js | 2 +- src/node/handler/ExportHandler.js | 196 ++++++------- src/node/handler/ImportHandler.js | 449 +++++++++++++----------------- src/node/utils/ExportEtherpad.js | 93 +++---- src/node/utils/ExportHtml.js | 94 ++----- src/node/utils/ExportTxt.js | 53 +--- src/node/utils/ImportEtherpad.js | 52 ++-- src/node/utils/ImportHtml.js | 10 +- 8 files changed, 379 insertions(+), 570 deletions(-) diff --git a/src/node/db/API.js b/src/node/db/API.js index 3fcf187cf..f0b44d92e 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -287,7 +287,7 @@ exports.setHTML = async function(padID, html) // add a new changeset with the new html to the pad try { - await importHtml.setPadHTML(pad, cleanText(html)); + importHtml.setPadHTML(pad, cleanText(html)); } catch (e) { throw new customError("HTML is malformed", "apierror"); } diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js index 3bcabf233..39638c222 100644 --- a/src/node/handler/ExportHandler.js +++ b/src/node/handler/ExportHandler.js @@ -19,18 +19,20 @@ * limitations under the License. */ -var ERR = require("async-stacktrace"); var exporthtml = require("../utils/ExportHtml"); var exporttxt = require("../utils/ExportTxt"); var exportEtherpad = require("../utils/ExportEtherpad"); -var async = require("async"); var fs = require("fs"); var settings = require('../utils/Settings'); var os = require('os'); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); var TidyHtml = require('../utils/TidyHtml'); +const util = require("util"); -var convertor = null; +const fsp_writeFile = util.promisify(fs.writeFile); +const fsp_unlink = util.promisify(fs.unlink); + +let convertor = null; // load abiword only if it is enabled if (settings.abiword != null) { @@ -47,122 +49,92 @@ const tempDirectory = os.tmpdir(); /** * do a requested export */ -exports.doExport = function(req, res, padId, type) +async function doExport(req, res, padId, type) { var fileName = padId; // allow fileName to be overwritten by a hook, the type type is kept static for security reasons - hooks.aCallFirst("exportFileName", padId, - function(err, hookFileName){ - // if fileName is set then set it to the padId, note that fileName is returned as an array. - if (hookFileName.length) { - fileName = hookFileName; - } + let hookFileName = await hooks.aCallFirst("exportFileName", padId); - // tell the browser that this is a downloadable file - res.attachment(fileName + "." + type); + // if fileName is set then set it to the padId, note that fileName is returned as an array. + if (hookFileName.length) { + fileName = hookFileName; + } - // if this is a plain text export, we can do this directly - // We have to over engineer this because tabs are stored as attributes and not plain text - if (type == "etherpad") { - exportEtherpad.getPadRaw(padId, function(err, pad) { - if (!err) { - res.send(pad); - // return; - } - }); - } else if (type == "txt") { - exporttxt.getPadTXTDocument(padId, req.params.rev, function(err, txt) { - if (!err) { - res.send(txt); - } - }); - } else { - var html; - var randNum; - var srcFile, destFile; + // tell the browser that this is a downloadable file + res.attachment(fileName + "." + type); - async.series([ - // render the html document - function(callback) { - exporthtml.getPadHTMLDocument(padId, req.params.rev, function(err, _html) { - if (ERR(err, callback)) return; - html = _html; - callback(); - }); - }, + // if this is a plain text export, we can do this directly + // We have to over engineer this because tabs are stored as attributes and not plain text + if (type === "etherpad") { + let pad = await exportEtherpad.getPadRaw(padId); + res.send(pad); + } else if (type === "txt") { + let txt = await exporttxt.getPadTXTDocument(padId, req.params.rev); + res.send(txt); + } else { + // render the html document + let html = await exporthtml.getPadHTMLDocument(padId, req.params.rev); - // decide what to do with the html export - function(callback) { - // if this is a html export, we can send this from here directly - if (type == "html") { - // do any final changes the plugin might want to make - hooks.aCallFirst("exportHTMLSend", html, function(err, newHTML) { - if (newHTML.length) html = newHTML; - res.send(html); - callback("stop"); - }); - } else { - // write the html export to a file - randNum = Math.floor(Math.random()*0xFFFFFFFF); - srcFile = tempDirectory + "/etherpad_export_" + randNum + ".html"; - fs.writeFile(srcFile, html, callback); - } - }, + // decide what to do with the html export - // Tidy up the exported HTML - function(callback) { - // ensure html can be collected by the garbage collector - html = null; - - TidyHtml.tidy(srcFile, callback); - }, - - // send the convert job to the convertor (abiword, libreoffice, ..) - function(callback) { - destFile = tempDirectory + "/etherpad_export_" + randNum + "." + type; - - // Allow plugins to overwrite the convert in export process - hooks.aCallAll("exportConvert", { srcFile: srcFile, destFile: destFile, req: req, res: res }, function(err, result) { - if (!err && result.length > 0) { - // console.log("export handled by plugin", destFile); - handledByPlugin = true; - callback(); - } else { - convertor.convertFile(srcFile, destFile, type, callback); - } - }); - - }, - - // send the file - function(callback) { - res.sendFile(destFile, null, callback); - }, - - // clean up temporary files - function(callback) { - async.parallel([ - function(callback) { - fs.unlink(srcFile, callback); - }, - function(callback) { - // 100ms delay to accommodate for slow windows fs - if (os.type().indexOf("Windows") > -1) { - setTimeout(function() { - fs.unlink(destFile, callback); - }, 100); - } else { - fs.unlink(destFile, callback); - } - } - ], callback); - } - ], - function(err) { - if (err && err != "stop") ERR(err); - }) - } + // if this is a html export, we can send this from here directly + if (type === "html") { + // do any final changes the plugin might want to make + let newHTML = await hooks.aCallFirst("exportHTMLSend", html); + if (newHTML.length) html = newHTML; + res.send(html); + throw "stop"; } - ); -}; + + // else write the html export to a file + let randNum = Math.floor(Math.random()*0xFFFFFFFF); + let srcFile = tempDirectory + "/etherpad_export_" + randNum + ".html"; + await fsp_writeFile(srcFile, html); + + // Tidy up the exported HTML + // ensure html can be collected by the garbage collector + html = null; + await TidyHtml.tidy(srcFile); + + // send the convert job to the convertor (abiword, libreoffice, ..) + let destFile = tempDirectory + "/etherpad_export_" + randNum + "." + type; + + // Allow plugins to overwrite the convert in export process + let result = await hooks.aCallAll("exportConvert", { srcFile, destFile, req, res }); + if (result.length > 0) { + // console.log("export handled by plugin", destFile); + handledByPlugin = true; + } else { + // @TODO no Promise interface for convertors (yet) + await new Promise((resolve, reject) => { + convertor.convertFile(srcFile, destFile, type, function(err) { + err ? reject("convertFailed") : resolve(); + }); + }); + } + + // send the file + let sendFile = util.promisify(res.sendFile); + await res.sendFile(destFile, null); + + // clean up temporary files + await fsp_unlink(srcFile); + + // 100ms delay to accommodate for slow windows fs + if (os.type().indexOf("Windows") > -1) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + + await fsp_unlink(destFile); + } +} + +exports.doExport = function(req, res, padId, type) +{ + doExport(req, res, padId, type).catch(err => { + if (err !== "stop") { + throw err; + } + }); +} diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index e92df4434..fb90c5059 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -20,10 +20,8 @@ * limitations under the License. */ -var ERR = require("async-stacktrace") - , padManager = require("../db/PadManager") +var padManager = require("../db/PadManager") , padMessageHandler = require("./PadMessageHandler") - , async = require("async") , fs = require("fs") , path = require("path") , settings = require('../utils/Settings') @@ -32,10 +30,16 @@ var ERR = require("async-stacktrace") , importHtml = require("../utils/ImportHtml") , importEtherpad = require("../utils/ImportEtherpad") , log4js = require("log4js") - , hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); + , hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js") + , util = require("util"); -var convertor = null; -var exportExtension = "htm"; +let fsp_exists = util.promisify(fs.exists); +let fsp_rename = util.promisify(fs.rename); +let fsp_readFile = util.promisify(fs.readFile); +let fsp_unlink = util.promisify(fs.unlink) + +let convertor = null; +let exportExtension = "htm"; // load abiword only if it is enabled and if soffice is disabled if (settings.abiword != null && settings.soffice === null) { @@ -53,292 +57,213 @@ const tmpDirectory = os.tmpdir(); /** * do a requested import */ -exports.doImport = function(req, res, padId) +async function doImport(req, res, padId) { var apiLogger = log4js.getLogger("ImportHandler"); // pipe to a file // convert file to html via abiword or soffice // set html in the pad - - var srcFile, destFile - , pad - , text - , importHandledByPlugin - , directDatabaseAccess - , useConvertor; - var randNum = Math.floor(Math.random()*0xFFFFFFFF); // setting flag for whether to use convertor or not - useConvertor = (convertor != null); + let useConvertor = (convertor != null); - async.series([ - // save the uploaded file to /tmp - function(callback) { - var form = new formidable.IncomingForm(); - form.keepExtensions = true; - form.uploadDir = tmpDirectory; + let form = new formidable.IncomingForm(); + form.keepExtensions = true; + form.uploadDir = tmpDirectory; - form.parse(req, function(err, fields, files) { - if (err || files.file === undefined) { - // the upload failed, stop at this point - if (err) { - console.warn("Uploading Error: " + err.stack); - } - callback("uploadFailed"); - - return; + // locally wrapped Promise, since form.parse requires a callback + let srcFile = await new Promise((resolve, reject) => { + form.parse(req, function(err, fields, files) { + if (err || files.file === undefined) { + // the upload failed, stop at this point + if (err) { + console.warn("Uploading Error: " + err.stack); } - - // everything ok, continue - // save the path of the uploaded file - srcFile = files.file.path; - callback(); - }); - }, - - // ensure this is a file ending we know, else we change the file ending to .txt - // this allows us to accept source code files like .c or .java - function(callback) { - var fileEnding = path.extname(srcFile).toLowerCase() - , knownFileEndings = [".txt", ".doc", ".docx", ".pdf", ".odt", ".html", ".htm", ".etherpad", ".rtf"] - , fileEndingKnown = (knownFileEndings.indexOf(fileEnding) > -1); - - // if the file ending is known, continue as normal - if (fileEndingKnown) { - callback(); - - return; + reject("uploadFailed"); } + resolve(files.file.path); + }); + }); + // ensure this is a file ending we know, else we change the file ending to .txt + // this allows us to accept source code files like .c or .java + let fileEnding = path.extname(srcFile).toLowerCase() + , knownFileEndings = [".txt", ".doc", ".docx", ".pdf", ".odt", ".html", ".htm", ".etherpad", ".rtf"] + , fileEndingUnknown = (knownFileEndings.indexOf(fileEnding) < 0); + + if (fileEndingUnknown) { + // the file ending is not known + + if (settings.allowUnknownFileEnds === true) { // we need to rename this file with a .txt ending - if (settings.allowUnknownFileEnds === true) { - var oldSrcFile = srcFile; - srcFile = path.join(path.dirname(srcFile), path.basename(srcFile, fileEnding) + ".txt"); - fs.rename(oldSrcFile, srcFile, callback); - } else { - console.warn("Not allowing unknown file type to be imported", fileEnding); - callback("uploadFailed"); - } - }, + let oldSrcFile = srcFile; - function(callback) { - destFile = path.join(tmpDirectory, "etherpad_import_" + randNum + "." + exportExtension); + srcFile = path.join(path.dirname(srcFile), path.basename(srcFile, fileEnding) + ".txt"); + await fs.rename(oldSrcFile, srcFile); + } else { + console.warn("Not allowing unknown file type to be imported", fileEnding); + throw "uploadFailed"; + } + } - // Logic for allowing external Import Plugins - hooks.aCallAll("import", { srcFile: srcFile, destFile: destFile }, function(err, result) { - if (ERR(err, callback)) return callback(); + let destFile = path.join(tmpDirectory, "etherpad_import_" + randNum + "." + exportExtension); - if (result.length > 0) { // This feels hacky and wrong.. - importHandledByPlugin = true; - } - callback(); - }); - }, + // Logic for allowing external Import Plugins + let result = await hooks.aCallAll("import", { srcFile, destFile }); + let importHandledByPlugin = (result.length > 0); // This feels hacky and wrong.. - function(callback) { - var fileEnding = path.extname(srcFile).toLowerCase() - var fileIsNotEtherpad = (fileEnding !== ".etherpad"); + let fileIsEtherpad = (fileEnding === ".etherpad"); + let fileIsHTML = (fileEnding === ".html" || fileEnding === ".htm"); + let fileIsTXT = (fileEnding === ".txt"); - if (fileIsNotEtherpad) { - callback(); + let directDatabaseAccess = false; - return; - } + if (fileIsEtherpad) { + // we do this here so we can see if the pad has quite a few edits + let _pad = await padManager.getPad(padId); + let headCount = _pad.head; - // we do this here so we can see if the pad has quite a few edits - padManager.getPad(padId, function(err, _pad) { - var headCount = _pad.head; - if (headCount >= 10) { - apiLogger.warn("Direct database Import attempt of a pad that already has content, we won't be doing this"); - return callback("padHasData"); - } + if (headCount >= 10) { + apiLogger.warn("Direct database Import attempt of a pad that already has content, we won't be doing this"); + throw "padHasData"; + } - fs.readFile(srcFile, "utf8", function(err, _text) { - directDatabaseAccess = true; - importEtherpad.setPadRaw(padId, _text, function(err) { - callback(); - }); + const fsp_readFile = util.promisify(fs.readFile); + let _text = await fsp_readFile(srcFile, "utf8"); + directDatabaseAccess = true; + await importEtherpad.setPadRaw(padId, _text); + } + + // convert file to html if necessary + if (!importHandledByPlugin && !directDatabaseAccess) { + if (fileIsTXT) { + // Don't use convertor for text files + useConvertor = false; + } + + // See https://github.com/ether/etherpad-lite/issues/2572 + if (fileIsHTML || !useConvertor) { + // if no convertor only rename + fs.renameSync(srcFile, destFile); + } else { + // @TODO - no Promise interface for convertors (yet) + await new Promise((resolve, reject) => { + convertor.convertFile(srcFile, destFile, exportExtension, function(err) { + // catch convert errors + if (err) { + console.warn("Converting Error:", err); + reject("convertFailed"); + } + resolve(); }); }); - }, - - // convert file to html if necessary - function(callback) { - if (importHandledByPlugin || directDatabaseAccess) { - callback(); - - return; - } - - var fileEnding = path.extname(srcFile).toLowerCase(); - var fileIsHTML = (fileEnding === ".html" || fileEnding === ".htm"); - var fileIsTXT = (fileEnding === ".txt"); - - if (fileIsTXT) useConvertor = false; // Don't use convertor for text files - - // See https://github.com/ether/etherpad-lite/issues/2572 - if (fileIsHTML || (useConvertor === false)) { - // if no convertor only rename - fs.rename(srcFile, destFile, callback); - - return; - } - - convertor.convertFile(srcFile, destFile, exportExtension, function(err) { - // catch convert errors - if (err) { - console.warn("Converting Error:", err); - return callback("convertFailed"); - } - - callback(); - }); - }, - - function(callback) { - if (useConvertor || directDatabaseAccess) { - callback(); - - return; - } - - // Read the file with no encoding for raw buffer access. - fs.readFile(destFile, function(err, buf) { - if (err) throw err; - var isAscii = true; - // Check if there are only ascii chars in the uploaded file - for (var i=0, len=buf.length; i 240) { - isAscii=false; - break; - } - } - - if (!isAscii) { - callback("uploadFailed"); - - return; - } - - callback(); - }); - }, - - // get the pad object - function(callback) { - padManager.getPad(padId, function(err, _pad) { - if (ERR(err, callback)) return; - pad = _pad; - callback(); - }); - }, - - // read the text - function(callback) { - if (directDatabaseAccess) { - callback(); - - return; - } - - fs.readFile(destFile, "utf8", function(err, _text) { - if (ERR(err, callback)) return; - text = _text; - // Title needs to be stripped out else it appends it to the pad.. - text = text.replace("", "<!-- <title>"); - text = text.replace("","-->"); - - // node on windows has a delay on releasing of the file lock. - // We add a 100ms delay to work around this - if (os.type().indexOf("Windows") > -1) { - setTimeout(function() {callback();}, 100); - } else { - callback(); - } - }); - }, - - // change text of the pad and broadcast the changeset - function(callback) { - if (!directDatabaseAccess) { - var fileEnding = path.extname(srcFile).toLowerCase(); - if (importHandledByPlugin || useConvertor || fileEnding == ".htm" || fileEnding == ".html") { - importHtml.setPadHTML(pad, text, function(e){ - if (e) { - apiLogger.warn("Error importing, possibly caused by malformed HTML"); - } - }); - } else { - pad.setText(text); - } - } - - // Load the Pad into memory then broadcast updates to all clients - padManager.unloadPad(padId); - padManager.getPad(padId, function(err, _pad) { - var pad = _pad; - padManager.unloadPad(padId); - - // direct Database Access means a pad user should perform a switchToPad - // and not attempt to receive updated pad data - if (directDatabaseAccess) { - callback(); - - return; - } - - // @TODO: not waiting for updatePadClients to finish - padMessageHandler.updatePadClients(pad); - callback(); - }); - - }, - - // clean up temporary files - function(callback) { - if (directDatabaseAccess) { - callback(); - - return; - } - - try { - fs.unlinkSync(srcFile); - } catch (e) { - console.log(e); - } - - try { - fs.unlinkSync(destFile); - } catch (e) { - console.log(e); - } - - callback(); } - ], function(err) { - var status = "ok"; + } + if (!useConvertor && !directDatabaseAccess) { + // Read the file with no encoding for raw buffer access. + let buf = await fsp_readFile(destFile); + + // Check if there are only ascii chars in the uploaded file + let isAscii = ! Array.prototype.some.call(buf, c => (c > 240)); + + if (!isAscii) { + throw "uploadFailed"; + } + } + + // get the pad object + let pad = await padManager.getPad(padId); + + // read the text + let text; + + if (!directDatabaseAccess) { + text = await fsp_readFile(destFile, "utf8"); + + // Title needs to be stripped out else it appends it to the pad.. + text = text.replace("", "<!-- <title>"); + text = text.replace("","-->"); + + // node on windows has a delay on releasing of the file lock. + // We add a 100ms delay to work around this + if (os.type().indexOf("Windows") > -1){ + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + + // change text of the pad and broadcast the changeset + if (!directDatabaseAccess) { + if (importHandledByPlugin || useConvertor || fileIsHTML) { + try { + importHtml.setPadHTML(pad, text); + } catch (e) { + apiLogger.warn("Error importing, possibly caused by malformed HTML"); + } + } else { + pad.setText(text); + } + } + + // Load the Pad into memory then broadcast updates to all clients + padManager.unloadPad(padId); + pad = await padManager.getPad(padId); + padManager.unloadPad(padId); + + // direct Database Access means a pad user should perform a switchToPad + // and not attempt to receive updated pad data + if (!directDatabaseAccess) { + // tell clients to update + await padMessageHandler.updatePadClients(pad); + } + + if (!directDatabaseAccess) { + // clean up temporary files + + /* + * TODO: directly delete the file and handle the eventual error. Checking + * before for existence is prone to race conditions, and does not handle any + * errors anyway. + */ + if (await fsp_exists(srcFile)) { + fsp_unlink(srcFile); + } + + if (await fsp_exists(destFile)) { + fsp_unlink(destFile); + } + } + + return directDatabaseAccess; +} + +exports.doImport = function (req, res, padId) +{ + let status = "ok"; + let directDatabaseAccess; + + doImport(req, res, padId).then(result => { + directDatabaseAccess = result; + }).catch(err => { // check for known errors and replace the status if (err == "uploadFailed" || err == "convertFailed" || err == "padHasData") { status = err; - err = null; + } else { + throw err; } - - ERR(err); - - // close the connection - res.send( - " \ - \ - \ - " - ); }); + + // close the connection + res.send( + " \ + \ + \ + " + ); } diff --git a/src/node/utils/ExportEtherpad.js b/src/node/utils/ExportEtherpad.js index 84fe80333..0e8ef3bf1 100644 --- a/src/node/utils/ExportEtherpad.js +++ b/src/node/utils/ExportEtherpad.js @@ -15,59 +15,48 @@ */ -var async = require("async"); -var db = require("../db/DB").db; -var ERR = require("async-stacktrace"); -const thenify = require("thenify").withCallback; +let db = require("../db/DB"); -exports.getPadRaw = thenify(function(padId, callback){ - async.waterfall([ - function(cb){ - db.get("pad:"+padId, cb); - }, - function(padcontent,cb){ - var records = ["pad:"+padId]; - for (var i = 0; i <= padcontent.head; i++) { - records.push("pad:"+padId+":revs:" + i); - } +exports.getPadRaw = async function(padId) { - for (var i = 0; i <= padcontent.chatHead; i++) { - records.push("pad:"+padId+":chat:" + i); - } + let padKey = "pad:" + padId; + let padcontent = await db.get(padKey); - var data = {}; - - async.forEachSeries(Object.keys(records), function(key, r){ - - // For each piece of info about a pad. - db.get(records[key], function(err, entry){ - data[records[key]] = entry; - - // Get the Pad Authors - if(entry.pool && entry.pool.numToAttrib){ - var authors = entry.pool.numToAttrib; - async.forEachSeries(Object.keys(authors), function(k, c){ - if(authors[k][0] === "author"){ - var authorId = authors[k][1]; - - // Get the author info - db.get("globalAuthor:"+authorId, function(e, authorEntry){ - if(authorEntry && authorEntry.padIDs) authorEntry.padIDs = padId; - if(!e) data["globalAuthor:"+authorId] = authorEntry; - }); - - } - // console.log("authorsK", authors[k]); - c(null); - }); - } - r(null); // callback; - }); - }, function(err){ - cb(err, data); - }) + let records = [ padKey ]; + for (let i = 0; i <= padcontent.head; i++) { + records.push(padKey + ":revs:" + i); } - ], function(err, data){ - callback(null, data); - }); -}); + + for (let i = 0; i <= padcontent.chatHead; i++) { + records.push(padKey + ":chat:" + i); + } + + let data = {}; + for (let key of records) { + + // For each piece of info about a pad. + let entry = data[key] = await db.get(key); + + // Get the Pad Authors + if (entry.pool && entry.pool.numToAttrib) { + let authors = entry.pool.numToAttrib; + + for (let k of Object.keys(authors)) { + if (authors[k][0] === "author") { + let authorId = authors[k][1]; + + // Get the author info + let authorEntry = await db.get("globalAuthor:" + authorId); + if (authorEntry) { + data["globalAuthor:" + authorId] = authorEntry; + if (authorEntry.padIDs) { + authorEntry.padIDs = padId; + } + } + } + } + } + } + + return data; +} diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js index fb15d867e..9cbcd2aa0 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.js @@ -14,61 +14,29 @@ * limitations under the License. */ - -var async = require("async"); var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var padManager = require("../db/PadManager"); -var ERR = require("async-stacktrace"); var _ = require('underscore'); var Security = require('ep_etherpad-lite/static/js/security'); var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); var eejs = require('ep_etherpad-lite/node/eejs'); var _analyzeLine = require('./ExportHelper')._analyzeLine; var _encodeWhitespace = require('./ExportHelper')._encodeWhitespace; -const thenify = require("thenify").withCallback; -function getPadHTML(pad, revNum, callback) +async function getPadHTML(pad, revNum) { - var atext = pad.atext; - var html; - async.waterfall([ + let atext = pad.atext; + // fetch revision atext - function (callback) - { - if (revNum != undefined) - { - pad.getInternalRevisionAText(revNum, function (err, revisionAtext) - { - if(ERR(err, callback)) return; - atext = revisionAtext; - callback(); - }); - } - else - { - callback(null); - } - }, + if (revNum != undefined) { + atext = await pad.getInternalRevisionAText(revNum); + } // convert atext to html - - - function (callback) - { - html = getHTMLFromAtext(pad, atext); - callback(null); - }], - // run final callback - - - function (err) - { - if(ERR(err, callback)) return; - callback(null, html); - }); + return getHTMLFromAtext(pad, atext); } -exports.getPadHTML = thenify(getPadHTML); +exports.getPadHTML = getPadHTML; exports.getHTMLFromAtext = getHTMLFromAtext; function getHTMLFromAtext(pad, atext, authorColors) @@ -82,15 +50,16 @@ function getHTMLFromAtext(pad, atext, authorColors) // prepare tags stored as ['tag', true] to be exported hooks.aCallAll("exportHtmlAdditionalTags", pad, function(err, newProps){ - newProps.forEach(function (propName, i){ + newProps.forEach(function (propName, i) { tags.push(propName); props.push(propName); }); }); + // prepare tags stored as ['tag', 'value'] to be exported. This will generate HTML // with tags like hooks.aCallAll("exportHtmlAdditionalTagsWithData", pad, function(err, newProps){ - newProps.forEach(function (propName, i){ + newProps.forEach(function (propName, i) { tags.push('span data-' + propName[0] + '="' + propName[1] + '"'); props.push(propName); }); @@ -454,38 +423,31 @@ function getHTMLFromAtext(pad, atext, authorColors) hooks.aCallAll("getLineHTMLForExport", context); pieces.push(context.lineContent, "
    "); - } } + } return pieces.join(''); } -exports.getPadHTMLDocument = thenify(function (padId, revNum, callback) +exports.getPadHTMLDocument = async function (padId, revNum) { - padManager.getPad(padId, function (err, pad) - { - if(ERR(err, callback)) return; + let pad = await padManager.getPad(padId); - var stylesForExportCSS = ""; - // Include some Styles into the Head for Export - hooks.aCallAll("stylesForExport", padId, function(err, stylesForExport){ - stylesForExport.forEach(function(css){ - stylesForExportCSS += css; - }); - - getPadHTML(pad, revNum, function (err, html) - { - if(ERR(err, callback)) return; - var exportedDoc = eejs.require("ep_etherpad-lite/templates/export_html.html", { - body: html, - padId: Security.escapeHTML(padId), - extraCSS: stylesForExportCSS - }); - callback(null, exportedDoc); - }); - }); + // Include some Styles into the Head for Export + let stylesForExportCSS = ""; + let stylesForExport = await hooks.aCallAll("stylesForExport", padId); + stylesForExport.forEach(function(css){ + stylesForExportCSS += css; }); -}); + + let html = await getPadHTML(pad, revNum); + + return eejs.require("ep_etherpad-lite/templates/export_html.html", { + body: html, + padId: Security.escapeHTML(padId), + extraCSS: stylesForExportCSS + }); +} // copied from ACE var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; diff --git a/src/node/utils/ExportTxt.js b/src/node/utils/ExportTxt.js index a42878df6..304f77b8a 100644 --- a/src/node/utils/ExportTxt.js +++ b/src/node/utils/ExportTxt.js @@ -18,46 +18,22 @@ * limitations under the License. */ -var async = require("async"); var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var padManager = require("../db/PadManager"); -var ERR = require("async-stacktrace"); var _analyzeLine = require('./ExportHelper')._analyzeLine; // This is slightly different than the HTML method as it passes the output to getTXTFromAText -function getPadTXT(pad, revNum, callback) +var getPadTXT = async function(pad, revNum) { - var atext = pad.atext; - var html; - async.waterfall([ + let atext = pad.atext; - // fetch revision atext - function(callback) { - if (revNum != undefined) { - pad.getInternalRevisionAText(revNum, function(err, revisionAtext) { - if (ERR(err, callback)) return; - - atext = revisionAtext; - callback(); - }); - } else { - callback(null); - } - }, + if (revNum != undefined) { + // fetch revision atext + atext = await pad.getInternalRevisionAText(revNum); + } // convert atext to html - function(callback) { - // only this line is different to the HTML function - html = getTXTFromAtext(pad, atext); - callback(null); - }], - - // run final callback - function(err) { - if (ERR(err, callback)) return; - - callback(null, html); - }); + return getTXTFromAtext(pad, atext); } // This is different than the functionality provided in ExportHtml as it provides formatting @@ -244,15 +220,8 @@ function getTXTFromAtext(pad, atext, authorColors) exports.getTXTFromAtext = getTXTFromAtext; -exports.getPadTXTDocument = function(padId, revNum, callback) +exports.getPadTXTDocument = async function(padId, revNum) { - padManager.getPad(padId, function(err, pad) { - if (ERR(err, callback)) return; - - getPadTXT(pad, revNum, function(err, html) { - if (ERR(err, callback)) return; - - callback(null, html); - }); - }); -}; + let pad = await padManager.getPad(padId); + return getPadTXT(pad, revNum); +} diff --git a/src/node/utils/ImportEtherpad.js b/src/node/utils/ImportEtherpad.js index 138c42936..a5b1074e6 100644 --- a/src/node/utils/ImportEtherpad.js +++ b/src/node/utils/ImportEtherpad.js @@ -15,43 +15,44 @@ */ var log4js = require('log4js'); -var async = require("async"); -var db = require("../db/DB").db; -const thenify = require("thenify").withCallback; +const db = require("../db/DB"); -exports.setPadRaw = thenify(function(padId, records, callback) +exports.setPadRaw = function(padId, records) { records = JSON.parse(records); - async.eachSeries(Object.keys(records), function(key, cb) { - var value = records[key]; + Object.keys(records).forEach(async function(key) { + let value = records[key]; if (!value) { - return setImmediate(cb); + return; } + let newKey; + if (value.padIDs) { // Author data - rewrite author pad ids value.padIDs[padId] = 1; - var newKey = key; + newKey = key; // Does this author already exist? - db.get(key, function(err, author) { - if (author) { - // Yes, add the padID to the author - if (Object.prototype.toString.call(author) === '[object Array]') { - author.padIDs.push(padId); - } - value = author; - } else { - // No, create a new array with the author info in - value.padIDs = [padId]; + let author = await db.get(key); + + if (author) { + // Yes, add the padID to the author + if (Object.prototype.toString.call(author) === '[object Array]') { + author.padIDs.push(padId); } - }); + + value = author; + } else { + // No, create a new array with the author info in + value.padIDs = [ padId ]; + } } else { // Not author data, probably pad data // we can split it to look to see if it's pad data - var oldPadId = key.split(":"); + let oldPadId = key.split(":"); // we know it's pad data if (oldPadId[0] === "pad") { @@ -59,16 +60,11 @@ exports.setPadRaw = thenify(function(padId, records, callback) oldPadId[1] = padId; // and create the value - var newKey = oldPadId.join(":"); // create the new key + newKey = oldPadId.join(":"); // create the new key } } // Write the value to the server - db.set(newKey, value); - - setImmediate(cb); - }, - function() { - callback(null, true); + await db.set(newKey, value); }); -}); +} diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index d46b715d3..63b35fa75 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -18,9 +18,8 @@ var log4js = require('log4js'); var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var contentcollector = require("ep_etherpad-lite/static/js/contentcollector"); var cheerio = require("cheerio"); -const thenify = require("thenify").withCallback; -function setPadHTML(pad, html, callback) +exports.setPadHTML = function(pad, html) { var apiLogger = log4js.getLogger("ImportHtml"); @@ -44,7 +43,7 @@ function setPadHTML(pad, html, callback) apiLogger.warn("HTML was not properly formed", e); // don't process the HTML because it was bad - return callback(e); + throw e; } var result = cc.finish(); @@ -52,7 +51,7 @@ function setPadHTML(pad, html, callback) apiLogger.debug('Lines:'); var i; - for (i = 0; i < result.lines.length; i += 1) { + for (i = 0; i < result.lines.length; i++) { apiLogger.debug('Line ' + (i + 1) + ' text: ' + result.lines[i]); apiLogger.debug('Line ' + (i + 1) + ' attributes: ' + result.lineAttribs[i]); } @@ -92,7 +91,4 @@ function setPadHTML(pad, html, callback) apiLogger.debug('The changeset: ' + theChangeset); pad.setText("\n"); pad.appendRevision(theChangeset); - callback(null); } - -exports.setPadHTML = thenify(setPadHTML); From b664eb488cc59271947d2adb1d0162c5ca7962c9 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Fri, 8 Feb 2019 14:46:05 +0000 Subject: [PATCH 050/183] ImportHandler.js: ensure import connection closing happens at the right point --- src/node/handler/ImportHandler.js | 71 ++++++++++++++++--------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index fb90c5059..d2bd05289 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -118,8 +118,6 @@ async function doImport(req, res, padId) let fileIsHTML = (fileEnding === ".html" || fileEnding === ".htm"); let fileIsTXT = (fileEnding === ".txt"); - let directDatabaseAccess = false; - if (fileIsEtherpad) { // we do this here so we can see if the pad has quite a few edits let _pad = await padManager.getPad(padId); @@ -132,12 +130,12 @@ async function doImport(req, res, padId) const fsp_readFile = util.promisify(fs.readFile); let _text = await fsp_readFile(srcFile, "utf8"); - directDatabaseAccess = true; + req.directDatabaseAccess = true; await importEtherpad.setPadRaw(padId, _text); } // convert file to html if necessary - if (!importHandledByPlugin && !directDatabaseAccess) { + if (!importHandledByPlugin && !req.directDatabaseAccess) { if (fileIsTXT) { // Don't use convertor for text files useConvertor = false; @@ -162,7 +160,7 @@ async function doImport(req, res, padId) } } - if (!useConvertor && !directDatabaseAccess) { + if (!useConvertor && !req.directDatabaseAccess) { // Read the file with no encoding for raw buffer access. let buf = await fsp_readFile(destFile); @@ -180,7 +178,7 @@ async function doImport(req, res, padId) // read the text let text; - if (!directDatabaseAccess) { + if (!req.directDatabaseAccess) { text = await fsp_readFile(destFile, "utf8"); // Title needs to be stripped out else it appends it to the pad.. @@ -195,7 +193,7 @@ async function doImport(req, res, padId) } // change text of the pad and broadcast the changeset - if (!directDatabaseAccess) { + if (!req.directDatabaseAccess) { if (importHandledByPlugin || useConvertor || fileIsHTML) { try { importHtml.setPadHTML(pad, text); @@ -214,56 +212,59 @@ async function doImport(req, res, padId) // direct Database Access means a pad user should perform a switchToPad // and not attempt to receive updated pad data - if (!directDatabaseAccess) { - // tell clients to update - await padMessageHandler.updatePadClients(pad); + if (req.directDatabaseAccess) { + return; } - if (!directDatabaseAccess) { - // clean up temporary files + // tell clients to update + await padMessageHandler.updatePadClients(pad); - /* - * TODO: directly delete the file and handle the eventual error. Checking - * before for existence is prone to race conditions, and does not handle any - * errors anyway. - */ - if (await fsp_exists(srcFile)) { - fsp_unlink(srcFile); - } + // clean up temporary files - if (await fsp_exists(destFile)) { - fsp_unlink(destFile); - } + /* + * TODO: directly delete the file and handle the eventual error. Checking + * before for existence is prone to race conditions, and does not handle any + * errors anyway. + */ + if (await fsp_exists(srcFile)) { + fsp_unlink(srcFile); } - return directDatabaseAccess; + if (await fsp_exists(destFile)) { + fsp_unlink(destFile); + } } exports.doImport = function (req, res, padId) { + /** + * NB: abuse the 'req' object by storing an additional + * 'directDatabaseAccess' property on it so that it can + * be passed back in the HTML below. + * + * this is necessary because in the 'throw' paths of + * the function above there's no other way to return + * a value to the caller. + */ let status = "ok"; - let directDatabaseAccess; - - doImport(req, res, padId).then(result => { - directDatabaseAccess = result; - }).catch(err => { + doImport(req, res, padId).catch(err => { // check for known errors and replace the status if (err == "uploadFailed" || err == "convertFailed" || err == "padHasData") { status = err; } else { throw err; } - }); - - // close the connection - res.send( + }).then(() => { + // close the connection + res.send( " \ \ \ " - ); + ); + }); } From ebb8a64e3c9468fa7eeb4b0b44b742ad0374fb79 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Thu, 31 Jan 2019 11:14:27 +0000 Subject: [PATCH 051/183] errorhandling.js: use promise db.doShutdown interface --- src/node/hooks/express/errorhandling.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js index 9a07dc669..66553621c 100644 --- a/src/node/hooks/express/errorhandling.js +++ b/src/node/hooks/express/errorhandling.js @@ -21,7 +21,7 @@ exports.gracefulShutdown = function(err) { console.log("graceful shutdown..."); // do the db shutdown - db.db.doShutdown(function() { + db.doShutdown().then(function() { console.log("db sucessfully closed."); process.exit(0); From 6d1b6b2796a90911a3ed3ec28a70bbb2cd91b814 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Thu, 31 Jan 2019 11:14:38 +0000 Subject: [PATCH 052/183] db/Pad.js: convert to promises/async Also updated some small chunks of dependent code that couldn't be converted until this one had been done. --- src/node/db/AuthorManager.js | 14 +- src/node/db/GroupManager.js | 14 +- src/node/db/Pad.js | 606 ++++++++++++----------------------- src/node/db/PadManager.js | 56 +--- 4 files changed, 236 insertions(+), 454 deletions(-) diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index 99584da19..0c6e3f750 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -144,12 +144,11 @@ exports.getAuthor = function(author) /** * Returns the color Id of the author * @param {String} author The id of the author - * @param {Function} callback callback(err, colorId) */ -exports.getAuthorColorId = thenify(function(author, callback) +exports.getAuthorColorId = function(author) { - db.db.getSub("globalAuthor:" + author, ["colorId"], callback); -}); + return db.getSub("globalAuthor:" + author, ["colorId"]); +} /** * Sets the color Id of the author @@ -164,12 +163,11 @@ exports.setAuthorColorId = function(author, colorId) /** * Returns the name of the author * @param {String} author The id of the author - * @param {Function} callback callback(err, name) */ -exports.getAuthorName = thenify(function(author, callback) +exports.getAuthorName = function(author) { - db.db.getSub("globalAuthor:" + author, ["name"], callback); -}); + return db.getSub("globalAuthor:" + author, ["name"]); +} /** * Sets the name of the author diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.js index 29a7ff063..238c423b1 100644 --- a/src/node/db/GroupManager.js +++ b/src/node/db/GroupManager.js @@ -18,13 +18,11 @@ * limitations under the License. */ -var ERR = require("async-stacktrace"); var customError = require("../utils/customError"); var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; var db = require("./DB"); var padManager = require("./PadManager"); var sessionManager = require("./SessionManager"); -const thenify = require("thenify").withCallback; exports.listAllGroups = async function() { @@ -85,15 +83,13 @@ exports.deleteGroup = async function(groupID) await db.set("groups", newGroups); } -// @TODO: this is the only function still called with a callback -exports.doesGroupExist = thenify(function(groupID, callback) +exports.doesGroupExist = async function(groupID) { // try to get the group entry - db.db.get("group:" + groupID, function (err, group) { - if (ERR(err, callback)) return; - callback(null, group != null); - }); -}); + let group = await db.get("group:" + groupID); + + return (group != null); +} exports.createGroup = async function() { diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index db35bd367..fb36bdf58 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -3,11 +3,9 @@ */ -var ERR = require("async-stacktrace"); var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var AttributePool = require("ep_etherpad-lite/static/js/AttributePool"); var db = require("./DB"); -var async = require("async"); var settings = require('../utils/Settings'); var authorManager = require("./AuthorManager"); var padManager = require("./PadManager"); @@ -18,8 +16,6 @@ var readOnlyManager = require("./ReadOnlyManager"); var crypto = require("crypto"); var randomString = require("../utils/randomstring"); var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -const thenify = require("thenify").withCallback; -const nodeify = require("nodeify"); // serialization/deserialization attributes var attributeBlackList = ["id"]; @@ -34,7 +30,7 @@ exports.cleanText = function (txt) { }; -var Pad = function Pad(id) { +let Pad = function Pad(id) { this.atext = Changeset.makeAText("\n"); this.pool = new AttributePool(); this.head = -1; @@ -129,7 +125,7 @@ Pad.prototype.saveToDatabase = function saveToDatabase() { } } - db.db.set("pad:" + this.id, dbObject); + db.set("pad:" + this.id, dbObject); } // get time of last edit (changeset application) @@ -138,17 +134,17 @@ Pad.prototype.getLastEdit = function getLastEdit() { return db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "timestamp"]); } -Pad.prototype.getRevisionChangeset = thenify(function getRevisionChangeset(revNum, callback) { - db.db.getSub("pad:" + this.id + ":revs:" + revNum, ["changeset"], callback); -}); +Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum) { + return db.getSub("pad:" + this.id + ":revs:" + revNum, ["changeset"]); +} -Pad.prototype.getRevisionAuthor = thenify(function getRevisionAuthor(revNum, callback) { - db.db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "author"], callback); -}); +Pad.prototype.getRevisionAuthor = function getRevisionAuthor(revNum) { + return db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "author"]); +} -Pad.prototype.getRevisionDate = thenify(function getRevisionDate(revNum, callback) { - db.db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "timestamp"], callback); -}); +Pad.prototype.getRevisionDate = function getRevisionDate(revNum) { + return db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "timestamp"]); +} Pad.prototype.getAllAuthors = function getAllAuthors() { var authors = []; @@ -162,101 +158,57 @@ Pad.prototype.getAllAuthors = function getAllAuthors() { return authors; }; -Pad.prototype.getInternalRevisionAText = thenify(function getInternalRevisionAText(targetRev, callback) { - var _this = this; - - var keyRev = this.getKeyRevisionNumber(targetRev); - var atext; - var changesets = []; +Pad.prototype.getInternalRevisionAText = async function getInternalRevisionAText(targetRev) { + let keyRev = this.getKeyRevisionNumber(targetRev); // find out which changesets are needed - var neededChangesets = []; - var curRev = keyRev; - while (curRev < targetRev) { - curRev++; - neededChangesets.push(curRev); + let neededChangesets = []; + for (let curRev = keyRev; curRev < targetRev; ) { + neededChangesets.push(++curRev); } - async.series([ - // get all needed data out of the database - function(callback) { - async.parallel([ - // get the atext of the key revision - function (callback) { - db.db.getSub("pad:" + _this.id + ":revs:" + keyRev, ["meta", "atext"], function(err, _atext) { - if (ERR(err, callback)) return; - try { - atext = Changeset.cloneAText(_atext); - } catch (e) { - return callback(e); - } + // get all needed data out of the database - callback(); - }); - }, + // get the atext of the key revision + let _atext = await db.getSub("pad:" + this.id + ":revs:" + keyRev, ["meta", "atext"]); + let atext = Changeset.cloneAText(_atext); - // get all needed changesets - function (callback) { - async.forEach(neededChangesets, function(item, callback) { - _this.getRevisionChangeset(item, function(err, changeset) { - if (ERR(err, callback)) return; - changesets[item] = changeset; - callback(); - }); - }, callback); - } - ], callback); - }, + // get all needed changesets + let changesets = []; + await Promise.all(neededChangesets.map(item => { + return this.getRevisionChangeset(item).then(changeset => { + changesets[item] = changeset; + }); + })); - // apply all changesets to the key changeset - function(callback) { - var apool = _this.apool(); - var curRev = keyRev; + // apply all changesets to the key changeset + let apool = this.apool(); + for (let curRev = keyRev; curRev < targetRev; ) { + let cs = changesets[++curRev]; + atext = Changeset.applyToAText(cs, atext, apool); + } - while (curRev < targetRev) { - curRev++; - var cs = changesets[curRev]; - try { - atext = Changeset.applyToAText(cs, atext, apool); - } catch(e) { - return callback(e) - } - } + return atext; +} - callback(null); - } - ], - function(err) { - if (ERR(err, callback)) return; - callback(null, atext); - }); -}); +Pad.prototype.getRevision = function getRevisionChangeset(revNum) { + return db.get("pad:" + this.id + ":revs:" + revNum); +} -Pad.prototype.getRevision = thenify(function getRevisionChangeset(revNum, callback) { - db.db.get("pad:" + this.id + ":revs:" + revNum, callback); -}); - -Pad.prototype.getAllAuthorColors = thenify(function getAllAuthorColors(callback) { - var authors = this.getAllAuthors(); - var returnTable = {}; - var colorPalette = authorManager.getColorPalette(); - - async.forEach(authors, function(author, callback) { - authorManager.getAuthorColorId(author, function(err, colorId) { - if (err) { - return callback(err); - } +Pad.prototype.getAllAuthorColors = async function getAllAuthorColors() { + let authors = this.getAllAuthors(); + let returnTable = {}; + let colorPalette = authorManager.getColorPalette(); + await Promise.all(authors.map(author => { + return authorManager.getAuthorColorId(author).then(colorId => { // colorId might be a hex color or an number out of the palette returnTable[author] = colorPalette[colorId] || colorId; - - callback(); }); - }, - function(err) { - callback(err, returnTable); - }); -}); + })); + + return returnTable; +} Pad.prototype.getValidRevisionRange = function getValidRevisionRange(startRev, endRev) { startRev = parseInt(startRev, 10); @@ -327,83 +279,49 @@ Pad.prototype.appendChatMessage = function appendChatMessage(text, userId, time) this.saveToDatabase(); }; -Pad.prototype.getChatMessage = thenify(function getChatMessage(entryNum, callback) { - var _this = this; - var entry; +Pad.prototype.getChatMessage = async function getChatMessage(entryNum) { + // get the chat entry + let entry = await db.get("pad:" + this.id + ":chat:" + entryNum); - async.series([ - // get the chat entry - function(callback) { - db.db.get("pad:" + _this.id + ":chat:" + entryNum, function(err, _entry) { - if (ERR(err, callback)) return; - entry = _entry; - callback(); - }); - }, - - // add the authorName - function(callback) { - // this chat message doesn't exist, return null - if (entry == null) { - callback(); - return; - } - - // get the authorName - authorManager.getAuthorName(entry.userId, function(err, authorName) { - if (ERR(err, callback)) return; - entry.userName = authorName; - callback(); - }); - } - ], - function(err) { - if (ERR(err, callback)) return; - callback(null, entry); - }); -}); - -Pad.prototype.getChatMessages = thenify(function getChatMessages(start, end, callback) { - // collect the numbers of chat entries and in which order we need them - var neededEntries = []; - var order = 0; - for (var i = start; i <= end; i++) { - neededEntries.push({ entryNum: i, order: order }); - order++; + // get the authorName if the entry exists + if (entry != null) { + entry.userName = await authorManager.getAuthorName(entry.userId); } - var _this = this; + return entry; +}; + +Pad.prototype.getChatMessages = async function getChatMessages(start, end) { + + // collect the numbers of chat entries and in which order we need them + let neededEntries = []; + for (let order = 0, entryNum = start; entryNum <= end; ++order, ++entryNum) { + neededEntries.push({ entryNum, order }); + } // get all entries out of the database - var entries = []; - async.forEach(neededEntries, function(entryObject, callback) { - _this.getChatMessage(entryObject.entryNum, function(err, entry) { - if (ERR(err, callback)) return; + let entries = []; + await Promise.all(neededEntries.map(entryObject => { + return this.getChatMessage(entryObject.entryNum).then(entry => { entries[entryObject.order] = entry; - callback(); }); - }, - function(err) { - if (ERR(err, callback)) return; + })); - // sort out broken chat entries - // it looks like in happened in the past that the chat head was - // incremented, but the chat message wasn't added - var cleanedEntries = []; - for (var i=0; i < entries.length; i++) { - if (entries[i] != null) { - cleanedEntries.push(entries[i]); - } else { - console.warn("WARNING: Found broken chat entry in pad " + _this.id); - } + // sort out broken chat entries + // it looks like in happened in the past that the chat head was + // incremented, but the chat message wasn't added + let cleanedEntries = entries.filter(entry => { + let pass = (entry != null); + if (!pass) { + console.warn("WARNING: Found broken chat entry in pad " + this.id); } - - callback(null, cleanedEntries); + return pass; }); -}); -Pad.prototype.init = thenify(function init(text, callback) { - var _this = this; + return cleanedEntries; +} + +Pad.prototype.init = async function init(text) { // replace text with default text if text isn't set if (text == null) { @@ -411,35 +329,31 @@ Pad.prototype.init = thenify(function init(text, callback) { } // try to load the pad - db.db.get("pad:" + this.id, function(err, value) { - if (ERR(err, callback)) return; + let value = await db.get("pad:" + this.id); - // if this pad exists, load it - if (value != null) { - // copy all attr. To a transfrom via fromJsonable if necessary - for (var attr in value) { - if (jsonableList.indexOf(attr) !== -1) { - _this[attr] = _this[attr].fromJsonable(value[attr]); - } else { - _this[attr] = value[attr]; - } + // if this pad exists, load it + if (value != null) { + // copy all attr. To a transfrom via fromJsonable if necassary + for (var attr in value) { + if (jsonableList.indexOf(attr) !== -1) { + this[attr] = this[attr].fromJsonable(value[attr]); + } else { + this[attr] = value[attr]; } - } else { - // this pad doesn't exist, so create it - var firstChangeset = Changeset.makeSplice("\n", 0, 0, exports.cleanText(text)); - - _this.appendRevision(firstChangeset, ''); } + } else { + // this pad doesn't exist, so create it + let firstChangeset = Changeset.makeSplice("\n", 0, 0, exports.cleanText(text)); - hooks.callAll("padLoad", { 'pad': _this }); - callback(null); - }); -}); + this.appendRevision(firstChangeset, ''); + } -Pad.prototype.copy = thenify(function copy(destinationID, force, callback) { - var sourceID = this.id; - var _this = this; - var destGroupID; + hooks.callAll("padLoad", { 'pad': this }); +} + +Pad.prototype.copy = async function copy(destinationID, force) { + + let sourceID = this.id; // allow force to be a string if (typeof force === "string") { @@ -454,236 +368,134 @@ Pad.prototype.copy = thenify(function copy(destinationID, force, callback) { // padMessageHandler.kickSessionsFromPad(sourceID); // flush the source pad: - _this.saveToDatabase(); + this.saveToDatabase(); - async.series([ - // if it's a group pad, let's make sure the group exists. - function(callback) { - if (destinationID.indexOf("$") === -1) { - callback(); - return; - } + // if it's a group pad, let's make sure the group exists. + let destGroupID; + if (destinationID.indexOf("$") >= 0) { - destGroupID = destinationID.split("$")[0]; - groupManager.doesGroupExist(destGroupID, function (err, exists) { - if (ERR(err, callback)) return; + destGroupID = destinationID.split("$")[0] + let groupExists = await groupManager.doesGroupExist(destGroupID); - // group does not exist - if (exists == false) { - callback(new customError("groupID does not exist for destinationID", "apierror")); - return; - } - - // everything is fine, continue - callback(); - }); - }, - - // if the pad exists, we should abort, unless forced. - function(callback) { - padManager.doesPadExists(destinationID, function (err, exists) { - if (ERR(err, callback)) return; - - /* - * this is the negation of a truthy comparison. Has been left in this - * wonky state to keep the old (possibly buggy) behaviour - */ - if (!(exists == true)) { - callback(); - return; - } - - if (!force) { - console.error("erroring out without force"); - callback(new customError("destinationID already exists", "apierror")); - console.error("erroring out without force - after"); - return; - } - - // exists and forcing - padManager.getPad(destinationID, function(err, pad) { - if (ERR(err, callback)) return; - pad.remove(callback); - }); - }); - }, - - // copy the 'pad' entry - function(callback) { - db.db.get("pad:" + sourceID, function(err, pad) { - db.set("pad:" + destinationID, pad); - }); - - callback(); - }, - - // copy all relations - function(callback) { - async.parallel([ - // copy all chat messages - function(callback) { - var chatHead = _this.chatHead; - - for (var i=0; i <= chatHead; i++) { - db.db.get("pad:" + sourceID + ":chat:" + i, function (err, chat) { - if (ERR(err, callback)) return; - db.set("pad:" + destinationID + ":chat:" + i, chat); - }); - } - - callback(); - }, - - // copy all revisions - function(callback) { - var revHead = _this.head; - for (var i=0; i <= revHead; i++) { - db.db.get("pad:" + sourceID + ":revs:" + i, function (err, rev) { - if (ERR(err, callback)) return; - db.set("pad:" + destinationID + ":revs:" + i, rev); - }); - } - - callback(); - }, - - // add the new pad to all authors who contributed to the old one - function(callback) { - var authorIDs = _this.getAllAuthors(); - authorIDs.forEach(function (authorID) { - authorManager.addPad(authorID, destinationID); - }); - - callback(); - }, - // parallel - ], callback); - }, - - function(callback) { - if (destGroupID) { - // Group pad? Add it to the group's list - db.setSub("group:" + destGroupID, ["pads", destinationID], 1); - } - - // Initialize the new pad (will update the listAllPads cache) - setTimeout(function() { - padManager.getPad(destinationID, null, callback) // this runs too early. - }, 10); - }, - - // let the plugins know the pad was copied - function(callback) { - hooks.callAll('padCopy', { 'originalPad': _this, 'destinationID': destinationID }); - callback(); + // group does not exist + if (!groupExists) { + throw new customError("groupID does not exist for destinationID", "apierror"); } - // series - ], - function(err) { - if (ERR(err, callback)) return; - callback(null, { padID: destinationID }); - }); -}); + } -Pad.prototype.remove = thenify(function remove(callback) { + // if the pad exists, we should abort, unless forced. + let exists = await padManager.doesPadExist(destinationID); + + if (exists) { + if (!force) { + console.error("erroring out without force"); + throw new customError("destinationID already exists", "apierror"); + + return; + } + + // exists and forcing + let pad = await padManager.getPad(destinationID); + await pad.remove(callback); + } + + // copy the 'pad' entry + let pad = await db.get("pad:" + sourceID); + db.set("pad:" + destinationID, pad); + + // copy all relations in parallel + let promises = []; + + // copy all chat messages + let chatHead = this.chatHead; + for (let i = 0; i <= chatHead; ++i) { + let p = db.get("pad:" + sourceID + ":chat:" + i).then(chat => { + return db.set("pad:" + destinationID + ":chat:" + i, chat); + }); + promises.push(p); + } + + // copy all revisions + let revHead = this.head; + for (let i = 0; i <= revHead; ++i) { + let p = db.get("pad:" + sourceID + ":revs:" + i).then(rev => { + return db.set("pad:" + destinationID + ":revs:" + i, rev); + }); + promises.push(p); + } + + // add the new pad to all authors who contributed to the old one + this.getAllAuthors().forEach(authorID => { + authorManager.addPad(authorID, destinationID); + }); + + // wait for the above to complete + await Promise.all(promises); + + // Group pad? Add it to the group's list + if (destGroupID) { + await db.setSub("group:" + destGroupID, ["pads", destinationID], 1); + } + + // delay still necessary? + await new Promise(resolve => setTimeout(resolve, 10)); + + // Initialize the new pad (will update the listAllPads cache) + await padManager.getPad(destinationID, null); // this runs too early. + + // let the plugins know the pad was copied + hooks.callAll('padCopy', { 'originalPad': this, 'destinationID': destinationID }); + + return { padID: destinationID }; +} + +Pad.prototype.remove = async function remove() { var padID = this.id; - var _this = this; // kick everyone from this pad padMessageHandler.kickSessionsFromPad(padID); - async.series([ - // delete all relations - function(callback) { - async.parallel([ - // is it a group pad? -> delete the entry of this pad in the group - function(callback) { - if (padID.indexOf("$") === -1) { - // it isn't a group pad, nothing to do here - callback(); - return; - } + // delete all relations + + // is it a group pad? -> delete the entry of this pad in the group + if (padID.indexOf("$") >= 0) { - // it is a group pad - var groupID = padID.substring(0, padID.indexOf("$")); + // it is a group pad + let groupID = padID.substring(0, padID.indexOf("$")); + let group = await db.get("group:" + groupID); - db.db.get("group:" + groupID, function (err, group) { - if (ERR(err, callback)) return; + // remove the pad entry + delete group.pads[padID]; - // remove the pad entry - delete group.pads[padID]; + // set the new value + db.set("group:" + groupID, group); + } - // set the new value - db.set("group:" + groupID, group); + // remove the readonly entries + let readonlyID = readOnlyManager.getReadOnlyId(padID); - callback(); - }); - }, + db.remove("pad2readonly:" + padID); + db.remove("readonly2pad:" + readonlyID); - // remove the readonly entries - function(callback) { - // @TODO - temporary until surrounding code is Promisified - function getReadOnlyId(padID, callback) { - return nodeify(readOnlyManager.getReadOnlyId(padID), callback); - } + // delete all chat messages + for (let i = 0, n = this.chatHead; i <= n; ++i) { + db.remove("pad:" + padID + ":chat:" + i); + } - getReadOnlyId(padID, function(err, readonlyID) { - if (ERR(err, callback)) return; + // delete all revisions + for (let i = 0, n = this.head; i <= n; ++i) { + db.remove("pad:" + padID + ":revs:" + i); + } - db.remove("pad2readonly:" + padID); - db.remove("readonly2pad:" + readonlyID); - - callback(); - }); - }, - - // delete all chat messages - function(callback) { - var chatHead = _this.chatHead; - - for (var i = 0; i <= chatHead; i++) { - db.remove("pad:" + padID + ":chat:" + i); - } - - callback(); - }, - - // delete all revisions - function(callback) { - var revHead = _this.head; - - for (var i = 0; i <= revHead; i++) { - db.remove("pad:" + padID + ":revs:" + i); - } - - callback(); - }, - - // remove pad from all authors who contributed - function(callback) { - var authorIDs = _this.getAllAuthors(); - - authorIDs.forEach(function (authorID) { - authorManager.removePad(authorID, padID); - }); - - callback(); - } - ], callback); - }, - - // delete the pad entry and delete pad from padManager - function(callback) { - padManager.removePad(padID); - hooks.callAll("padRemove", { 'padID': padID }); - callback(); - } - ], - function(err) { - if (ERR(err, callback)) return; - callback(); + // remove pad from all authors who contributed + this.getAllAuthors().forEach(authorID => { + authorManager.removePad(authorID, padID); }); -}); + + // delete the pad entry and delete pad from padManager + padManager.removePad(padID); + hooks.callAll("padRemove", { padID }); +} // set in db Pad.prototype.setPublicStatus = function setPublicStatus(publicStatus) { diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index 858ce5b45..23164a7a9 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -18,11 +18,9 @@ * limitations under the License. */ -var ERR = require("async-stacktrace"); var customError = require("../utils/customError"); var Pad = require("../db/Pad").Pad; -var db = require("./DB").db; -const thenify = require("thenify").withCallback; +var db = require("./DB"); /** * A cache of all loaded Pads. @@ -114,59 +112,43 @@ let padList = { * @param id A String with the id of the pad * @param {Function} callback */ -exports.getPad = thenify(function(id, text, callback) +exports.getPad = async function(id, text) { // check if this is a valid padId if (!exports.isValidPadId(id)) { - callback(new customError(id + " is not a valid padId", "apierror")); - - return; - } - - // make text an optional parameter - if (typeof text == "function") { - callback = text; - text = null; + throw new customError(id + " is not a valid padId", "apierror"); } // check if this is a valid text if (text != null) { // check if text is a string if (typeof text != "string") { - callback(new customError("text is not a string", "apierror")); - - return; + throw new customError("text is not a string", "apierror"); } // check if text is less than 100k chars if (text.length > 100000) { - callback(new customError("text must be less than 100k chars", "apierror")); - - return; + throw new customError("text must be less than 100k chars", "apierror"); } } - var pad = globalPads.get(id); + let pad = globalPads.get(id); // return pad if it's already loaded if (pad != null) { - callback(null, pad); - - return; + return pad; } // try to load pad pad = new Pad(id); // initalize the pad - pad.init(text, function(err) { - if (ERR(err, callback)) return; + await pad.init(text); + globalPads.set(id, pad); + padList.addPad(id); - globalPads.set(id, pad); - padList.addPad(id); - callback(null, pad); - }); -}); + return pad; +} exports.listAllPads = async function() { @@ -176,18 +158,12 @@ exports.listAllPads = async function() } // checks if a pad exists -exports.doesPadExist = thenify(function(padId, callback) +exports.doesPadExist = async function(padId) { - db.get("pad:" + padId, function(err, value) { - if (ERR(err, callback)) return; + let value = await db.get("pad:" + padId); - if (value != null && value.atext) { - callback(null, true); - } else { - callback(null, false); - } - }); -}); + return (value != null && value.atext); +} // alias for backwards compatibility exports.doesPadExists = exports.doesPadExist; From 4622309dc247ef7fcefc8dcca6d73ab07c44b3b6 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Thu, 31 Jan 2019 13:42:41 +0000 Subject: [PATCH 053/183] TidyHtml.js: convert to promises test case uses "nodeify" to convert the calls to TidyHtml back into nodeback because it integrates better with the test framework --- src/node/utils/TidyHtml.js | 59 +++++++++++++++++---------------- tests/backend/specs/api/tidy.js | 10 ++++-- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/src/node/utils/TidyHtml.js b/src/node/utils/TidyHtml.js index 0f7119894..26d48a62f 100644 --- a/src/node/utils/TidyHtml.js +++ b/src/node/utils/TidyHtml.js @@ -5,38 +5,39 @@ var log4js = require('log4js'); var settings = require('./Settings'); var spawn = require('child_process').spawn; -const thenify = require("thenify").withCallback; -exports.tidy = thenify(function(srcFile, callback) { +exports.tidy = function(srcFile) { var logger = log4js.getLogger('TidyHtml'); - // Don't do anything if Tidy hasn't been enabled - if (!settings.tidyHtml) { - logger.debug('tidyHtml has not been configured yet, ignoring tidy request'); - return callback(null); - } + return new Promise((resolve, reject) => { - var errMessage = ''; - - // Spawn a new tidy instance that cleans up the file inline - logger.debug('Tidying ' + srcFile); - var tidy = spawn(settings.tidyHtml, ['-modify', srcFile]); - - // Keep track of any error messages - tidy.stderr.on('data', function (data) { - errMessage += data.toString(); - }); - - // Wait until Tidy is done - tidy.on('close', function(code) { - // Tidy returns a 0 when no errors occur and a 1 exit code when - // the file could be tidied but a few warnings were generated - if (code === 0 || code === 1) { - logger.debug('Tidied ' + srcFile + ' successfully'); - return callback(null); - } else { - logger.error('Failed to tidy ' + srcFile + '\n' + errMessage); - return callback('Tidy died with exit code ' + code); + // Don't do anything if Tidy hasn't been enabled + if (!settings.tidyHtml) { + logger.debug('tidyHtml has not been configured yet, ignoring tidy request'); + return resolve(null); } + + var errMessage = ''; + + // Spawn a new tidy instance that cleans up the file inline + logger.debug('Tidying ' + srcFile); + var tidy = spawn(settings.tidyHtml, ['-modify', srcFile]); + + // Keep track of any error messages + tidy.stderr.on('data', function (data) { + errMessage += data.toString(); + }); + + tidy.on('close', function(code) { + // Tidy returns a 0 when no errors occur and a 1 exit code when + // the file could be tidied but a few warnings were generated + if (code === 0 || code === 1) { + logger.debug('Tidied ' + srcFile + ' successfully'); + resolve(null); + } else { + logger.error('Failed to tidy ' + srcFile + '\n' + errMessage); + reject('Tidy died with exit code ' + code); + } + }); }); -}); +} diff --git a/tests/backend/specs/api/tidy.js b/tests/backend/specs/api/tidy.js index 6f38ac7b0..3ef61931b 100644 --- a/tests/backend/specs/api/tidy.js +++ b/tests/backend/specs/api/tidy.js @@ -1,10 +1,12 @@ var assert = require('assert') + os = require('os'), fs = require('fs'), path = require('path'), TidyHtml = null, Settings = null; var npm = require("../../../../src/node_modules/npm/lib/npm.js"); +var nodeify = require('../../../../src/node_modules/nodeify'); describe('tidyHtml', function() { before(function(done) { @@ -16,6 +18,10 @@ describe('tidyHtml', function() { }); }); + function tidy(file, callback) { + return nodeify(TidyHtml.tidy(file), callback); + } + it('Tidies HTML', function(done) { // If the user hasn't configured Tidy, we skip this tests as it's required for this test if (!Settings.tidyHtml) { @@ -27,7 +33,7 @@ describe('tidyHtml', function() { var tmpFile = path.join(tmpDir, 'tmp_' + (Math.floor(Math.random() * 1000000)) + '.html') fs.writeFileSync(tmpFile, '

    a paragraph

  2. List without outer UL
  3. trailing closing p

    '); - TidyHtml.tidy(tmpFile, function(err){ + tidy(tmpFile, function(err){ assert.ok(!err); // Read the file again @@ -56,7 +62,7 @@ describe('tidyHtml', function() { this.skip(); } - TidyHtml.tidy('/some/none/existing/file.html', function(err) { + tidy('/some/none/existing/file.html', function(err) { assert.ok(err); return done(); }); From ccb49dcdc1e5fea2bbd61f110de85af3c6213600 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Thu, 31 Jan 2019 14:38:56 +0000 Subject: [PATCH 054/183] padDiff.js: convert to Promises/async --- src/node/utils/padDiff.js | 268 +++++++++++++------------------------- 1 file changed, 93 insertions(+), 175 deletions(-) diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js index a801fdc84..7cf29aba4 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.js @@ -1,7 +1,5 @@ var Changeset = require("../../static/js/Changeset"); -var async = require("async"); var exportHtml = require('./ExportHtml'); -const thenify = require("thenify").withCallback; function PadDiff (pad, fromRev, toRev) { // check parameters @@ -79,79 +77,52 @@ PadDiff.prototype._isClearAuthorship = function(changeset) { return true; }; -PadDiff.prototype._createClearAuthorship = thenify(function(rev, callback) { - var self = this; - this._pad.getInternalRevisionAText(rev, function(err, atext) { - if (err) { - return callback(err); - } +PadDiff.prototype._createClearAuthorship = async function(rev) { - // build clearAuthorship changeset - var builder = Changeset.builder(atext.text.length); - builder.keepText(atext.text, [['author','']], self._pad.pool); - var changeset = builder.toString(); + let atext = await this._pad.getInternalRevisionAText(rev); - callback(null, changeset); - }); -}); + // build clearAuthorship changeset + var builder = Changeset.builder(atext.text.length); + builder.keepText(atext.text, [['author','']], this._pad.pool); + var changeset = builder.toString(); -PadDiff.prototype._createClearStartAtext = thenify(function(rev, callback) { - var self = this; + return changeset; +} + +PadDiff.prototype._createClearStartAtext = async function(rev) { // get the atext of this revision - this._pad.getInternalRevisionAText(rev, function(err, atext) { - if (err) { - return callback(err); - } + let atext = this._pad.getInternalRevisionAText(rev); - // create the clearAuthorship changeset - self._createClearAuthorship(rev, function(err, changeset) { - if (err) { - return callback(err); - } + // create the clearAuthorship changeset + let changeset = await this._createClearAuthorship(rev); - try { - // apply the clearAuthorship changeset - var newAText = Changeset.applyToAText(changeset, atext, self._pad.pool); - } catch(err) { - return callback(err) - } + // apply the clearAuthorship changeset + let newAText = Changeset.applyToAText(changeset, atext, this._pad.pool); - callback(null, newAText); - }); - }); -}); + return newAText; +} -PadDiff.prototype._getChangesetsInBulk = thenify(function(startRev, count, callback) { - var self = this; +PadDiff.prototype._getChangesetsInBulk = async function(startRev, count) { // find out which revisions we need - var revisions = []; - for (var i = startRev; i < (startRev + count) && i <= this._pad.head; i++) { + let revisions = []; + for (let i = startRev; i < (startRev + count) && i <= this._pad.head; i++) { revisions.push(i); } - var changesets = [], authors = []; - - // get all needed revisions - async.forEach(revisions, function(rev, callback) { - self._pad.getRevision(rev, function(err, revision) { - if (err) { - return callback(err); - } - - var arrayNum = rev-startRev; - + // get all needed revisions (in parallel) + let changesets = [], authors = []; + await Promise.all(revisions.map(rev => { + return this._pad.getRevision(rev).then(revision => { + let arrayNum = rev - startRev; changesets[arrayNum] = revision.changeset; authors[arrayNum] = revision.meta.author; - - callback(); }); - }, - function(err) { - callback(err, changesets, authors); - }); -}); + })); + + return { changesets, authors }; +} PadDiff.prototype._addAuthors = function(authors) { var self = this; @@ -164,144 +135,91 @@ PadDiff.prototype._addAuthors = function(authors) { }); }; -PadDiff.prototype._createDiffAtext = thenify(function(callback) { - var self = this; - var bulkSize = 100; +PadDiff.prototype._createDiffAtext = async function() { + + let bulkSize = 100; // get the cleaned startAText - self._createClearStartAtext(self._fromRev, function(err, atext) { - if (err) { return callback(err); } + let atext = await this._createClearStartAtext(this._fromRev); - var superChangeset = null; + let superChangeset = null; + let rev = this._fromRev + 1; - var rev = self._fromRev + 1; + for (let rev = this._fromRev + 1; rev <= this._toRev; rev += bulkSize) { - // async while loop - async.whilst( - // loop condition - function () { return rev <= self._toRev; }, + // get the bulk + let { changesets, authors } = await this._getChangesetsInBulk(rev, bulkSize); - // loop body - function (callback) { - // get the bulk - self._getChangesetsInBulk(rev,bulkSize,function(err, changesets, authors) { - var addedAuthors = []; + let addedAuthors = []; - // run trough all changesets - for (var i = 0; i < changesets.length && (rev + i) <= self._toRev; i++) { - var changeset = changesets[i]; + // run through all changesets + for (let i = 0; i < changesets.length && (rev + i) <= this._toRev; ++i) { + let changeset = changesets[i]; - // skip clearAuthorship Changesets - if (self._isClearAuthorship(changeset)) { - continue; - } - - changeset = self._extendChangesetWithAuthor(changeset, authors[i], self._pad.pool); - - // add this author to the authorarray - addedAuthors.push(authors[i]); - - // compose it with the superChangset - if (superChangeset === null) { - superChangeset = changeset; - } else { - superChangeset = Changeset.composeWithDeletions(superChangeset, changeset, self._pad.pool); - } - } - - // add the authors to the PadDiff authorArray - self._addAuthors(addedAuthors); - - // lets continue with the next bulk - rev += bulkSize; - callback(); - }); - }, - - // after the loop has ended - function (err) { - // if there are only clearAuthorship changesets, we don't get a superChangeset, so we can skip this step - if (superChangeset) { - var deletionChangeset = self._createDeletionChangeset(superChangeset,atext,self._pad.pool); - - try { - // apply the superChangeset, which includes all addings - atext = Changeset.applyToAText(superChangeset, atext, self._pad.pool); - // apply the deletionChangeset, which adds a deletions - atext = Changeset.applyToAText(deletionChangeset, atext, self._pad.pool); - } catch(err) { - return callback(err) - } - } - - callback(err, atext); + // skip clearAuthorship Changesets + if (this._isClearAuthorship(changeset)) { + continue; } - ); - }); -}); -PadDiff.prototype.getHtml = thenify(function(callback) { + changeset = this._extendChangesetWithAuthor(changeset, authors[i], this._pad.pool); + + // add this author to the authorarray + addedAuthors.push(authors[i]); + + // compose it with the superChangset + if (superChangeset === null) { + superChangeset = changeset; + } else { + superChangeset = Changeset.composeWithDeletions(superChangeset, changeset, this._pad.pool); + } + } + + // add the authors to the PadDiff authorArray + this._addAuthors(addedAuthors); + } + + // if there are only clearAuthorship changesets, we don't get a superChangeset, so we can skip this step + if (superChangeset) { + let deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool); + + // apply the superChangeset, which includes all addings + atext = Changeset.applyToAText(superChangeset, atext, this._pad.pool); + + // apply the deletionChangeset, which adds a deletions + atext = Changeset.applyToAText(deletionChangeset, atext, this._pad.pool); + } + + return atext; +} + +PadDiff.prototype.getHtml = async function() { + // cache the html if (this._html != null) { - return callback(null, this._html); + return this._html; } - var self = this; - var atext, html, authorColors; + // get the diff atext + let atext = await this._createDiffAtext(); - async.series([ - // get the diff atext - function(callback) { - self._createDiffAtext(function(err, _atext) { - if (err) { - return callback(err); - } + // get the authorColor table + let authorColors = await this._pad.getAllAuthorColors(); - atext = _atext; - callback(); - }); - }, + // convert the atext to html + this._html = exportHtml.getHTMLFromAtext(this._pad, atext, authorColors); - // get the authorColor table - function(callback) { - self._pad.getAllAuthorColors(function(err, _authorColors) { - if (err) { - return callback(err); - } + return this._html; +} - authorColors = _authorColors; - callback(); - }); - }, - - // convert the atext to html - function(callback) { - html = exportHtml.getHTMLFromAtext(self._pad, atext, authorColors); - self._html = html; - callback(); - } - ], - function(err) { - callback(err, html); - }); -}); - -PadDiff.prototype.getAuthors = thenify(function(callback) { - var self = this; +PadDiff.prototype.getAuthors = async function() { // check if html was already produced, if not produce it, this generates the author array at the same time - if (self._html == null) { - self.getHtml(function(err) { - if (err) { - return callback(err); - } - - callback(null, self._authors); - }); - } else { - callback(null, self._authors); + if (this._html == null) { + await this.getHtml(); } -}); + + return self._authors; +} PadDiff.prototype._extendChangesetWithAuthor = function(changeset, author, apool) { // unpack From b1c5024bcf008b816b7b80ac5bdb8a857567726a Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Thu, 31 Jan 2019 14:48:22 +0000 Subject: [PATCH 055/183] remove thenify use - no longer required --- src/node/db/AuthorManager.js | 1 - src/node/db/DB.js | 40 +++++++++++++++++----------------- src/node/db/SecurityManager.js | 1 - src/package.json | 1 - 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index 0c6e3f750..a17952248 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -21,7 +21,6 @@ var db = require("./DB"); var customError = require("../utils/customError"); var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; -const thenify = require("thenify").withCallback; exports.getColorPalette = function() { return [ diff --git a/src/node/db/DB.js b/src/node/db/DB.js index c462ac303..17cf3080a 100644 --- a/src/node/db/DB.js +++ b/src/node/db/DB.js @@ -22,11 +22,10 @@ var ueberDB = require("ueberdb2"); var settings = require("../utils/Settings"); var log4js = require('log4js'); -const thenify = require("thenify").withCallback; const util = require("util"); // set database settings -var db = new ueberDB.database(settings.dbType, settings.dbSettings, null, log4js.getLogger("ueberDB")); +let db = new ueberDB.database(settings.dbType, settings.dbSettings, null, log4js.getLogger("ueberDB")); /** * The UeberDB Object that provides the database functions @@ -37,24 +36,25 @@ exports.db = null; * Initalizes the database with the settings provided by the settings module * @param {Function} callback */ -exports.init = thenify(function (callback) { +exports.init = function() { // initalize the database async - db.init(function(err) { - if (err) { - // there was an error while initializing the database, output it and stop - console.error("ERROR: Problem while initalizing the database"); - console.error(err.stack ? err.stack : err); - process.exit(1); - } else { - // everything ok - exports.db = db; + return new Promise((resolve, reject) => { + db.init(function(err) { + if (err) { + // there was an error while initializing the database, output it and stop + console.error("ERROR: Problem while initalizing the database"); + console.error(err.stack ? err.stack : err); + process.exit(1); + } else { + // everything ok, set up Promise-based methods + ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove', 'doShutdown'].forEach(fn => { + exports[fn] = util.promisify(db[fn].bind(db)); + }); - // set up Promise-based methods - ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove', 'doShutdown'].forEach(fn => { - exports[fn] = util.promisify(db[fn].bind(db)); - }); - - callback(null); - } + // exposed for those callers that need the underlying raw API + exports.db = db; + resolve(); + } + }); }); -}); +} diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index dcdbe6a62..e50a0b9c2 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -25,7 +25,6 @@ var sessionManager = require("./SessionManager"); var settings = require("../utils/Settings"); var log4js = require('log4js'); var authLogger = log4js.getLogger("auth"); -const thenify = require("thenify").withCallback; /** * This function controlls the access to a pad, it checks if the user can access a pad. diff --git a/src/package.json b/src/package.json index 11259e76d..2b5eb5c18 100644 --- a/src/package.json +++ b/src/package.json @@ -58,7 +58,6 @@ "slide": "1.1.6", "socket.io": "2.1.1", "swagger-node-express": "2.1.3", - "thenify": "^3.3.0", "tinycon": "0.0.1", "ueberdb2": "0.4.0", "uglify-js": "2.6.2", From 07ae44ddf4f2fd77ce04ab9a9e3cb405fe032806 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Thu, 31 Jan 2019 15:46:25 +0000 Subject: [PATCH 056/183] PadMessageHandler.js: cope better with session disconnects --- src/node/handler/PadMessageHandler.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index b41af86b9..cccb9e8a2 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -268,6 +268,12 @@ exports.handleMessage = async function(client, message) // FIXME: Use a hook instead // FIXME: Allow to override readwrite access with readonly + // the session may have been dropped during earlier processing + if (!sessioninfos[client.id]) { + messageLogger.warn("Dropping message from a connection that has gone away.") + return; + } + // Simulate using the load testing tool if (!sessioninfos[client.id].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.") From e7c2fad7b0418eb891506e6890f5df380edda7ea Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Fri, 1 Feb 2019 00:07:06 +0000 Subject: [PATCH 057/183] convert some async loops into parallel loops If you use `await` inside a loop it makes the loop inherently serial. If you omit the `await` however, the tasks will all start but the loop will finish while the tasks are still being scheduled. So, to make a set of tasks run in parallel but then have the code block after the loop once all the tasks have been completed you have to get an array of Promises (one for each iteration) and then use `Promise.all()` to wait for those promises to be resolved. Using `Array#map` is a convenient way to go from an array of inputs to the require array of Promises. --- src/node/db/GroupManager.js | 19 +++---- src/node/handler/PadMessageHandler.js | 80 ++++++++++++++------------- 2 files changed, 51 insertions(+), 48 deletions(-) diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.js index 238c423b1..5df034ef6 100644 --- a/src/node/db/GroupManager.js +++ b/src/node/db/GroupManager.js @@ -43,20 +43,19 @@ exports.deleteGroup = async function(groupID) throw new customError("groupID does not exist", "apierror"); } - // iterate through all pads of this group and delete them - for (let padID in group.pads) { - let pad = await padManager.getPad(padID); - await pad.remove(); - } + // iterate through all pads of this group and delete them (in parallel) + await Promise.all(Object.keys(group.pads).map(padID => { + return padManager.getPad(padID).then(pad => pad.remove()); + })); // iterate through group2sessions and delete all sessions let group2sessions = await db.get("group2sessions:" + groupID); - let sessions = group2sessions ? group2sessions.sessionsIDs : []; + let sessions = group2sessions ? group2sessions.sessionsIDs : {}; - // loop through all sessions and delete them - for (let session in sessions) { - await sessionManager.deleteSession(session); - } + // loop through all sessions and delete them (in parallel) + await Promise.all(Object.keys(sessions).map(session => { + return sessionManager.deleteSession(session); + })); // remove group and group2sessions entry await db.remove("group2sessions:" + groupID); diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index cccb9e8a2..1c7b44cfc 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -722,6 +722,7 @@ exports.updatePadClients = async function(pad) // since all clients usually get the same set of changesets, store them in local cache // to remove unnecessary roundtrip to the datalayer + // NB: note below possibly now accommodated via the change to promises/async // TODO: in REAL world, if we're working without datalayer cache, all requests to revisions will be fired // BEFORE first result will be landed to our cache object. The solution is to replace parallel processing // via async.forEach with sequential for() loop. There is no real benefits of running this in parallel, @@ -928,15 +929,16 @@ async function handleClientReady(client, message) // get timestamp of latest revision needed for timeslider let currentTime = await pad.getRevisionDate(pad.getHeadRevisionNumber()); - // get all author data out of the database - for (let authorId of authors) { - try { - let author = await authorManager.getAuthor(authorId); - historicalAuthorData[authorId] = { name: author.name, colorId: author.colorId }; // Filter author attribs (e.g. don't send author's pads to all clients) - } catch (err) { - messageLogger.error("There is no author for authorId:", authorId); - } - } + // get all author data out of the database (in parallel) + await Promise.all(authors.map(authorId => { + return authorManager.getAuthor(authorId).then(author => { + if (!author) { + messageLogger.error("There is no author for authorId:", authorId); + } else { + historicalAuthorData[authorId] = { name: author.name, colorId: author.colorId }; // Filter author attribs (e.g. don't send author's pads to all clients) + } + }); + })); // glue the clientVars together, send them and tell the other clients that a new one is there @@ -1162,45 +1164,47 @@ async function handleClientReady(client, message) // notify all existing users about new user client.broadcast.to(padIds.padId).json.send(messageToTheOtherUsers); - // Get sessions for this pad + // Get sessions for this pad and update them (in parallel) roomClients = _getRoomClients(pad.id); - for (let roomClient of roomClients) { + await Promise.all(_getRoomClients(pad.id).map(async roomClient => { // Jump over, if this session is the connection session if (roomClient.id == client.id) { - continue; + return; } // Since sessioninfos might change while being enumerated, check if the // sessionID is still assigned to a valid session if (sessioninfos[roomClient.id] === undefined) { - continue; + return; } - let author = sessioninfos[roomClient.id].author; - // get the authorname & colorId + let author = sessioninfos[roomClient.id].author; + let cached = historicalAuthorData[author]; // reuse previously created cache of author's data - let authorInfo = historicalAuthorData[author] || await authorManager.getAuthor(author); + let p = cached ? Promise.resolve(cached) : authorManager.getAuthor(author); - // Send the new User a Notification about this other user - let msg = { - "type": "COLLABROOM", - "data": { - type: "USER_NEWINFO", - userInfo: { - "ip": "127.0.0.1", - "colorId": authorInfo.colorId, - "name": authorInfo.name, - "userAgent": "Anonymous", - "userId": author + return p.then(authorInfo => { + // Send the new User a Notification about this other user + let msg = { + "type": "COLLABROOM", + "data": { + type: "USER_NEWINFO", + userInfo: { + "ip": "127.0.0.1", + "colorId": authorInfo.colorId, + "name": authorInfo.name, + "userAgent": "Anonymous", + "userId": author + } } - } - }; + }; - client.json.send(msg); - } + client.json.send(msg); + }); + })); } } @@ -1448,16 +1452,16 @@ exports.padUsers = async function(padID) { let padUsers = []; let roomClients = _getRoomClients(padID); - for (let i = 0, n = roomClients.length; i < n; ++i) { - let roomClient = roomClients[i]; - + // iterate over all clients (in parallel) + await Promise.all(roomClients.map(async roomClient => { let s = sessioninfos[roomClient.id]; if (s) { - let author = await authorManager.getAuthor(s.author); - author.id = s.author; - padUsers.push(author); + return authorManager.getAuthor(s.author).then(author => { + author.id = s.author; + padUsers.push(author); + }); } - } + })); return { padUsers }; } From 769933786ceabf6aac87f788fef5560270e4db93 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Fri, 1 Feb 2019 09:57:50 +0000 Subject: [PATCH 058/183] allow some operations to proceed in parallel some code chunks previously used `async.parallel` but if you use `await` that forces them to be run serially. Instead, you can initiate the operation (getting a Promise) and then _later_ `await` the result of that Promise. --- src/node/db/Pad.js | 14 ++++++++++---- src/node/db/SecurityManager.js | 26 +++++++++++++++++--------- src/node/handler/PadMessageHandler.js | 18 ++++++++++-------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index fb36bdf58..6c97fee8d 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -169,9 +169,8 @@ Pad.prototype.getInternalRevisionAText = async function getInternalRevisionAText // get all needed data out of the database - // get the atext of the key revision - let _atext = await db.getSub("pad:" + this.id + ":revs:" + keyRev, ["meta", "atext"]); - let atext = Changeset.cloneAText(_atext); + // start to get the atext of the key revision + let p_atext = db.getSub("pad:" + this.id + ":revs:" + keyRev, ["meta", "atext"]); // get all needed changesets let changesets = []; @@ -181,6 +180,10 @@ Pad.prototype.getInternalRevisionAText = async function getInternalRevisionAText }); })); + // we should have the atext by now + let atext = await p_atext; + atext = Changeset.cloneAText(atext); + // apply all changesets to the key changeset let apool = this.apool(); for (let curRev = keyRev; curRev < targetRev; ) { @@ -455,7 +458,10 @@ Pad.prototype.remove = async function remove() { // kick everyone from this pad padMessageHandler.kickSessionsFromPad(padID); - // delete all relations + // delete all relations - the original code used async.parallel but + // none of the operations except getting the group depended on callbacks + // so the database operations here are just started and then left to + // run to completion // is it a group pad? -> delete the entry of this pad in the group if (padID.indexOf("$") >= 0) { diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index e50a0b9c2..23af82836 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -49,11 +49,11 @@ exports.checkAccess = async function(padID, sessionCookie, token, password) return deny; } - // get author for this token - let tokenAuthor = await authorManager.getAuthor4Token(token); + // start to get author for this token + let p_tokenAuthor = authorManager.getAuthor4Token(token); - // check if pad exists - let padExists = await padManager.doesPadExist(padID); + // start to check if pad exists + let p_padExists = padManager.doesPadExist(padID); if (settings.requireSession) { // a valid session is required (api-only mode) @@ -67,11 +67,14 @@ exports.checkAccess = async function(padID, sessionCookie, token, password) // it's not a group pad, means we can grant access // assume user has access - let statusObject = { accessStatus: "grant", authorID: tokenAuthor }; + let authorID = await p_tokenAuthor; + let statusObject = { accessStatus: "grant", authorID }; if (settings.editOnly) { // user can't create pads + let padExists = await p_padExists; + if (!padExists) { // pad doesn't exist - user can't have access statusObject.accessStatus = "deny"; @@ -96,10 +99,13 @@ exports.checkAccess = async function(padID, sessionCookie, token, password) let sessionIDs = sessionCookie.split(','); // was previously iterated in parallel using async.forEach - for (let sessionID of sessionIDs) { - try { - let sessionInfo = await sessionManager.getSessionInfo(sessionID); + let sessionInfos = await Promise.all(sessionIDs.map(sessionID => { + return sessionManager.getSessionInfo(sessionID); + })); + // seperated out the iteration of sessioninfos from the (parallel) fetches from the DB + for (let sessionInfo of sessionInfos) { + try { // is it for this group? if (sessionInfo.groupID != groupID) { authLogger.debug("Auth failed: wrong group"); @@ -128,6 +134,8 @@ exports.checkAccess = async function(padID, sessionCookie, token, password) } } + let padExists = await p_padExists; + if (padExists) { let pad = await padManager.getPad(padID); @@ -205,7 +213,7 @@ exports.checkAccess = async function(padID, sessionCookie, token, password) if (!validSession && padExists) { // there is no valid session avaiable AND pad exists - let authorID = tokenAuthor; + let authorID = await p_tokenAuthor; let grant = Object.freeze({ accessStatus: "grant", authorID }); if (isPublic && !isPasswordProtected) { diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 1c7b44cfc..18b08af5b 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -890,8 +890,6 @@ async function handleClientReady(client, message) return; } - var historicalAuthorData = {}; - hooks.callAll("clientReady", message); // Get ro/rw id:s @@ -915,12 +913,12 @@ async function handleClientReady(client, message) let author = statusObject.authorID; - // get all authordata of this new user, and load the pad-object from the database + // get all authordata of this new user let value = await authorManager.getAuthor(author); let authorColorId = value.colorId; let authorName = value.name; - // get pad + // load the pad-object from the database let pad = await padManager.getPad(padIds.padId); // these db requests all need the pad object (timestamp of latest revision, author data) @@ -930,6 +928,7 @@ async function handleClientReady(client, message) let currentTime = await pad.getRevisionDate(pad.getHeadRevisionNumber()); // get all author data out of the database (in parallel) + let historicalAuthorData = {}; await Promise.all(authors.map(authorId => { return authorManager.getAuthor(authorId).then(author => { if (!author) { @@ -1010,12 +1009,15 @@ async function handleClientReady(client, message) changesets[r] = {}; } - // get changesets, author and timestamp needed for pending revisions + // get changesets, author and timestamp needed for pending revisions (in parallel) + let promises = []; for (let revNum of revisionsNeeded) { - changesets[revNum]['changeset'] = await pad.getRevisionChangeset(revNum); - changesets[revNum]['author'] = await pad.getRevisionAuthor(revNum); - changesets[revNum]['timestamp'] = await pad.getRevisionDate(revNum); + let cs = changesets[revNum]; + promises.push( pad.getRevisionChangeset(revNum).then(result => cs.changeset = result )); + promises.push( pad.getRevisionAuthor(revNum).then(result => cs.author = result )); + promises.push( pad.getRevisionDate(revNum).then(result => cs.timestamp = result )); } + await Promise.all(promises); // return pending changesets for (let r of revisionsNeeded) { From 2e4ee39cc36ff889af3a10f07d8ec2df9e2c717b Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 7 Feb 2019 15:55:56 +0100 Subject: [PATCH 059/183] Localisation updates from https://translatewiki.net. --- src/locales/diq.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/locales/diq.json b/src/locales/diq.json index 453867566..4c16f941f 100644 --- a/src/locales/diq.json +++ b/src/locales/diq.json @@ -9,9 +9,9 @@ "Gırd" ] }, - "index.newPad": "Pedo newe", - "index.createOpenPad": "Yana eno bamaeya bloknot vıraz/ak:", - "pad.toolbar.bold.title": "Qalın (Ctrl-B)", + "index.newPad": "Bloknoto newe", + "index.createOpenPad": "ya zi be nê nameyi ra yew bloknot vıraze/ake:", + "pad.toolbar.bold.title": "Qalınd (Ctrl-B)", "pad.toolbar.italic.title": "Namıte (Ctrl-I)", "pad.toolbar.underline.title": "Bınxetın (Ctrl-U)", "pad.toolbar.strikethrough.title": "Serxetın (Ctrl+5)", @@ -20,7 +20,7 @@ "pad.toolbar.indent.title": "Serrêze (TAB)", "pad.toolbar.unindent.title": "Teberdayış (Shift+TAB)", "pad.toolbar.undo.title": "Meke (Ctrl-Z)", - "pad.toolbar.redo.title": "Fına bıke (Ctrl-Y)", + "pad.toolbar.redo.title": "Newe ke (Ctrl-Y)", "pad.toolbar.clearAuthorship.title": "Rengê Nuştoğiê Arıstey (Ctrl+Shift+C)", "pad.toolbar.import_export.title": "Babaetna tewranê dosyaya azere/ateber ke", "pad.toolbar.timeslider.title": "Ğızagê zemani", From 78c057af3117045e517b32f87e86bacd61a9e937 Mon Sep 17 00:00:00 2001 From: muxator Date: Fri, 8 Feb 2019 19:10:49 +0100 Subject: [PATCH 060/183] NodeVersion.js: factor out require('semver') --- src/node/utils/NodeVersion.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/utils/NodeVersion.js b/src/node/utils/NodeVersion.js index 2909d5bc9..dac492b1f 100644 --- a/src/node/utils/NodeVersion.js +++ b/src/node/utils/NodeVersion.js @@ -18,6 +18,8 @@ * limitations under the License. */ +const semver = require('semver'); + /** * Quits if Etherpad is not running on a given minimum Node version * @@ -25,7 +27,6 @@ * @param {Function} callback Standard callback function */ exports.enforceMinNodeVersion = function(minNodeVersion, callback) { - const semver = require('semver'); const currentNodeVersion = process.version; // we cannot use template literals, since we still do not know if we are @@ -45,7 +46,6 @@ exports.enforceMinNodeVersion = function(minNodeVersion, callback) { * @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated Node releases */ exports.checkDeprecationStatus = function(lowestNonDeprecatedNodeVersion, epRemovalVersion, callback) { - const semver = require('semver'); const currentNodeVersion = process.version; if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) { From 631b23f7a252c25f46bbb1e2e640eadd6843ffad Mon Sep 17 00:00:00 2001 From: muxator Date: Mon, 11 Feb 2019 00:02:55 +0100 Subject: [PATCH 061/183] utils/AbsolutePaths.js: do not break when running as a Windows manual install A Windows manual install has the same directory layout of a normal Unix one (e.g. the nice symlink node_modules/ep_etherpad-lite -> ../src). Only when running from the pre-built Windows package the directory layout is different (e.g. src is physically copied into node_modules/ep_etherpad-lite). The previous version of the code wrongly assumed that all Windows installs would be run from the pre-built pakage. In this version the path search is the same on all platform. If it fails, and we are on Windows, there is a fallback for the specific case of the pre-built package. Fixes #3550 --- src/node/utils/AbsolutePaths.js | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/node/utils/AbsolutePaths.js b/src/node/utils/AbsolutePaths.js index 55dfc98e3..9d864c474 100644 --- a/src/node/utils/AbsolutePaths.js +++ b/src/node/utils/AbsolutePaths.js @@ -61,8 +61,8 @@ var popIfEndsWith = function(stringArray, lastDesiredElements) { * Heuristically computes the directory in which Etherpad is installed. * * All the relative paths have to be interpreted against this absolute base - * path. Since the Unix and Windows install have a different layout on disk, - * they are treated as two special cases. + * path. Since the Windows package install has a different layout on disk, it is + * dealt with as a special case. * * The path is computed only on first invocation. Subsequent invocations return * a cached value. @@ -79,26 +79,27 @@ exports.findEtherpadRoot = function() { const findRoot = require('find-root'); const foundRoot = findRoot(__dirname); + const splitFoundRoot = foundRoot.split(path.sep); - var directoriesToStrip; - if (process.platform === 'win32') { + /* + * On Unix platforms and on Windows manual installs, foundRoot's value will + * be: + * + * \src + */ + var maybeEtherpadRoot = popIfEndsWith(splitFoundRoot, ['src']); + + if ((maybeEtherpadRoot === false) && (process.platform === 'win32')) { /* - * Given the structure of our Windows package, foundRoot's value - * will be the following on win32: + * If we did not find the path we are expecting, and we are running under + * Windows, we may still be running from a prebuilt package, whose directory + * structure is different: * * \node_modules\ep_etherpad-lite */ - directoriesToStrip = ['node_modules', 'ep_etherpad-lite']; - } else { - /* - * On Unix platforms, foundRoot's value will be: - * - * \src - */ - directoriesToStrip = ['src']; + maybeEtherpadRoot = popIfEndsWith(splitFoundRoot, ['node_modules', 'ep_etherpad-lite']); } - const maybeEtherpadRoot = popIfEndsWith(foundRoot.split(path.sep), directoriesToStrip); if (maybeEtherpadRoot === false) { absPathLogger.error(`Could not identity Etherpad base path in this ${process.platform} installation in "${foundRoot}"`); process.exit(1); From c333984cd8b87fb485bd9bcbea0ccacf21cfd0f8 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 14 Feb 2019 09:09:18 +0100 Subject: [PATCH 062/183] Localisation updates from https://translatewiki.net. --- src/locales/ar.json | 2 +- src/locales/diq.json | 5 +++-- src/locales/it.json | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/locales/ar.json b/src/locales/ar.json index 0237830f1..c3330a6d7 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -38,7 +38,7 @@ "pad.noCookie": "الكوكيز غير متاحة. الرجاء السماح بتحميل الكوكيز على متصفحك!", "pad.passwordRequired": "تحتاج إلى كلمة مرور للوصول إلى هذا الباد", "pad.permissionDenied": "ليس لديك إذن لدخول هذا الباد", - "pad.wrongPassword": "كانت كلمة المرور خاطئة", + "pad.wrongPassword": "كانت كلمة السر خاطئة", "pad.settings.padSettings": "إعدادات الباد", "pad.settings.myView": "رؤيتي", "pad.settings.stickychat": "الدردشة دائما على الشاشة", diff --git a/src/locales/diq.json b/src/locales/diq.json index 4c16f941f..8bf66e9c5 100644 --- a/src/locales/diq.json +++ b/src/locales/diq.json @@ -6,7 +6,8 @@ "Mirzali", "Kumkumuk", "1917 Ekim Devrimi", - "Gırd" + "Gırd", + "Orbot707" ] }, "index.newPad": "Bloknoto newe", @@ -104,7 +105,7 @@ "timeslider.forwardRevision": "Ena bloknot de şo revizyonê bini", "timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", "timeslider.month.january": "Çele", - "timeslider.month.february": "Zemherı", + "timeslider.month.february": "Gucige", "timeslider.month.march": "Adar", "timeslider.month.april": "Nisane", "timeslider.month.may": "Gulane", diff --git a/src/locales/it.json b/src/locales/it.json index d7d7c095a..a797dbebb 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -17,8 +17,8 @@ "pad.toolbar.strikethrough.title": "Barrato (Ctrl+5)", "pad.toolbar.ol.title": "Elenco numerato (Ctrl+Shift+N)", "pad.toolbar.ul.title": "Elenco puntato (Ctrl+Shift+L)", - "pad.toolbar.indent.title": "Rientro (TAB)", - "pad.toolbar.unindent.title": "Riduci rientro (Shift+TAB)", + "pad.toolbar.indent.title": "Indentazione (TAB)", + "pad.toolbar.unindent.title": "Riduci indentazione (Shift+TAB)", "pad.toolbar.undo.title": "Annulla (Ctrl-Z)", "pad.toolbar.redo.title": "Ripeti (Ctrl-Y)", "pad.toolbar.clearAuthorship.title": "Elimina i colori che indicano gli autori (Ctrl+Shift+C)", From d5d428c4eed58de4cb087a7765187d224d0f188a Mon Sep 17 00:00:00 2001 From: muxator Date: Sat, 16 Feb 2019 00:14:39 +0100 Subject: [PATCH 063/183] windows: allow graceful shutdown on Windows, too Until Etherpad 1.7.5, process.on('SIGTERM') and process.on('SIGINT') were not hooked up under Windows, because old nodejs versions did not support them. This excluded the possibility of doing a graceful shutdown of the database connection under that platform. According to nodejs 6.x documentation, it is now safe to do so. This allows to gracefully close the DB connection when hitting CTRL+C under Windows, for example. Source: https://nodejs.org/docs/latest-v6.x/api/process.html#process_signal_events - SIGTERM is not supported on Windows, it can be listened on. - SIGINT from the terminal is supported on all platforms, and can usually be generated with +C (though this may be configurable). It is not generated when terminal raw mode is enabled. --- src/node/hooks/express/errorhandling.js | 32 ++++++++++++++++++------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js index c36595bdd..104a9c1bb 100644 --- a/src/node/hooks/express/errorhandling.js +++ b/src/node/hooks/express/errorhandling.js @@ -44,13 +44,27 @@ exports.expressCreateServer = function (hook_name, args, cb) { stats.meter('http500').mark() }) - //connect graceful shutdown with sigint and uncaughtexception - if(os.type().indexOf("Windows") == -1) { - //sigint is so far not working on windows - //https://github.com/joyent/node/issues/1553 - process.on('SIGINT', exports.gracefulShutdown); - // when running as PID1 (e.g. in docker container) - // allow graceful shutdown on SIGTERM c.f. #3265 - process.on('SIGTERM', exports.gracefulShutdown); - } + /* + * Connect graceful shutdown with sigint and uncaught exception + * + * Until Etherpad 1.7.5, process.on('SIGTERM') and process.on('SIGINT') were + * not hooked up under Windows, because old nodejs versions did not support + * them. + * + * According to nodejs 6.x documentation, it is now safe to do so. This + * allows to gracefully close the DB connection when hitting CTRL+C under + * Windows, for example. + * + * Source: https://nodejs.org/docs/latest-v6.x/api/process.html#process_signal_events + * + * - SIGTERM is not supported on Windows, it can be listened on. + * - SIGINT from the terminal is supported on all platforms, and can usually + * be generated with +C (though this may be configurable). It is not + * generated when terminal raw mode is enabled. + */ + process.on('SIGINT', exports.gracefulShutdown); + + // when running as PID1 (e.g. in docker container) + // allow graceful shutdown on SIGTERM c.f. #3265 + process.on('SIGTERM', exports.gracefulShutdown); } From b16b98f8ca2f9818b2094ed030a8bfb1597e415b Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 18 Feb 2019 08:00:31 +0100 Subject: [PATCH 064/183] Localisation updates from https://translatewiki.net. --- src/locales/ar.json | 2 +- src/locales/mk.json | 5 +++-- src/locales/pa.json | 11 ++++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/locales/ar.json b/src/locales/ar.json index c3330a6d7..b337cab99 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -36,7 +36,7 @@ "pad.colorpicker.cancel": "إلغاء", "pad.loading": "جارٍ التحميل...", "pad.noCookie": "الكوكيز غير متاحة. الرجاء السماح بتحميل الكوكيز على متصفحك!", - "pad.passwordRequired": "تحتاج إلى كلمة مرور للوصول إلى هذا الباد", + "pad.passwordRequired": "تحتاج إلى كلمة سر للوصول إلى هذا الباد", "pad.permissionDenied": "ليس لديك إذن لدخول هذا الباد", "pad.wrongPassword": "كانت كلمة السر خاطئة", "pad.settings.padSettings": "إعدادات الباد", diff --git a/src/locales/mk.json b/src/locales/mk.json index fda3bf4ff..5c2ce32cf 100644 --- a/src/locales/mk.json +++ b/src/locales/mk.json @@ -2,7 +2,8 @@ "@metadata": { "authors": [ "Bjankuloski06", - "Brest" + "Brest", + "Vlad5250" ] }, "index.newPad": "Нова тетратка", @@ -23,7 +24,7 @@ "pad.toolbar.savedRevision.title": "Зачувај преработка", "pad.toolbar.settings.title": "Поставки", "pad.toolbar.embed.title": "Споделете и вметнете ја тетраткава", - "pad.toolbar.showusers.title": "Прикаж. корисниците на тетраткава", + "pad.toolbar.showusers.title": "Прикажи корисниците на тетраткава", "pad.colorpicker.save": "Зачувај", "pad.colorpicker.cancel": "Откажи", "pad.loading": "Вчитувам...", diff --git a/src/locales/pa.json b/src/locales/pa.json index f1e8ffc92..2b3a085af 100644 --- a/src/locales/pa.json +++ b/src/locales/pa.json @@ -4,7 +4,8 @@ "Aalam", "Babanwalia", "ਪ੍ਰਚਾਰਕ", - "Tow" + "Tow", + "ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ" ] }, "index.newPad": "ਨਵਾਂ ਪੈਡ", @@ -22,7 +23,7 @@ "pad.toolbar.clearAuthorship.title": "ਪਰਮਾਣਕਿਤਾ ਰੰਗ ਸਾਫ਼ ਕਰੋ (Ctrl+Shift+C)", "pad.toolbar.import_export.title": "ਵੱਖ-ਵੱਖ ਫਾਇਲ ਫਾਰਮੈਟ ਤੋਂ/ਵਿੱਚ ਇੰਪੋਰਟ/ਐਕਸਪੋਰਟ ਕਰੋ", "pad.toolbar.timeslider.title": "ਸਮਾਂ-ਲਕੀਰ", - "pad.toolbar.savedRevision.title": "ਰੀਵਿਜ਼ਨ ਸੰਭਾਲੋ", + "pad.toolbar.savedRevision.title": "ਦੁਹਰਾਅ ਸਾਂਭੋ", "pad.toolbar.settings.title": "ਸੈਟਿੰਗ", "pad.toolbar.embed.title": "ਇਹ ਪੈਡ ਸਾਂਝਾ ਤੇ ਇੰਬੈੱਡ ਕਰੋ", "pad.toolbar.showusers.title": "ਇਹ ਪੈਡ ਉੱਤੇ ਯੂਜ਼ਰ ਵੇਖਾਓ", @@ -30,9 +31,9 @@ "pad.colorpicker.cancel": "ਰੱਦ ਕਰੋ", "pad.loading": "…ਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ", "pad.noCookie": "ਕੂਕੀਜ਼ ਨਹੀਂ ਲੱਭੀਅਾਂ। ਕਿਰਪਾ ਕਰਕੇ ਬ੍ਰਾੳੂਜ਼ਰ ਵਿੱਚ ਕੂਕੀਜ਼ ਲਾਗੂ ਕਰੋ।", - "pad.passwordRequired": "ਇਹ ਪੈਡ ਦੀ ਵਰਤੋਂ ਕਰਨ ਲਈ ਤੁਹਾਨੂੰ ਪਾਸਵਰਡ ਚਾਹੀਦਾ ਹੈ", + "pad.passwordRequired": "ਇਸ ਪੈਡ ਤੱਕ ਅਪੜਨ ਲਈ ਤੁਹਾਨੂੰ ਇੱਕ ਲੰਘ-ਸ਼ਬਦ ਦੀ ਲੋੜ ਹੈ", "pad.permissionDenied": "ਇਹ ਪੈਡ ਵਰਤਨ ਲਈ ਤੁਹਾਨੂੰ ਅਧਿਕਾਰ ਨਹੀਂ ਹਨ", - "pad.wrongPassword": "ਤੁਹਾਡਾ ਪਾਸਵਰਡ ਗਲਤੀ ਸੀ", + "pad.wrongPassword": "ਤੁਹਾਡਾ ਲੰਘ-ਸ਼ਬਦ ਗਲਤ ਸੀ", "pad.settings.padSettings": "ਪੈਡ ਸੈਟਿੰਗ", "pad.settings.myView": "ਮੇਰੀ ਝਲਕ", "pad.settings.stickychat": "ਹਮੇਸ਼ਾ ਸਕਰੀਨ ਉੱਤੇ ਗੱਲ ਕਰੋ", @@ -97,7 +98,7 @@ "timeslider.saved": "{{day}} {{month}} {{year}} ਨੂੰ ਸੰਭਾਲਿਆ", "timeslider.playPause": "ਪੈਡ ਸਮੱਗਰੀ ਚਲਾਓ / ਵਿਰਾਮ ਕਰੋ", "timeslider.backRevision": "ਇਸ ਪੈਡ ਵਿੱਚ ਪਿਛਲੇ ਰੀਵਿਜ਼ਨ ਤੇ ਜਾਓ", - "timeslider.forwardRevision": "ਇਸ ਪੈਡ ਵਿੱਚ ਅਗਲੇ ਰੀਵਿਜ਼ਨ ਤੇ ਜਾਓ", + "timeslider.forwardRevision": "ਇਸ ਪੈਡ ਵਿੱਚ ਅਗਲੇ ਦੁਹਰਾਅ ਤੇ ਜਾਓ", "timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", "timeslider.month.january": "ਜਨਵਰੀ", "timeslider.month.february": "ਫ਼ਰਵਰੀ", From 36addd22057fcae747b5604029ad9fd57c6c7ac2 Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 19 Feb 2019 00:41:51 +0100 Subject: [PATCH 065/183] server.js: group together the loading of the stats system No functional changes, this is intended to simplify subsequent patches. --- src/node/server.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/node/server.js b/src/node/server.js index 9ccedfa60..1207dff96 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -23,12 +23,15 @@ var log4js = require('log4js') , async = require('async') - , stats = require('./stats') , NodeVersion = require('./utils/NodeVersion') ; log4js.replaceConsole(); +/* + * start up stats counting system + */ +var stats = require('./stats'); stats.gauge('memoryUsage', function() { return process.memoryUsage().rss }) From 9d9b7c9fafb77836f37c558b6cf173f9df62d013 Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 19 Feb 2019 00:46:37 +0100 Subject: [PATCH 066/183] NodeVersion.js: do not use callbacks, simplify calling style in server.js --- src/node/server.js | 21 +++++++++++---------- src/node/utils/NodeVersion.js | 13 +++++-------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/node/server.js b/src/node/server.js index 1207dff96..56cd580d3 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -28,6 +28,17 @@ var log4js = require('log4js') log4js.replaceConsole(); +/* + * early check for version compatibility before calling + * any modules that require newer versions of NodeJS + */ +NodeVersion.enforceMinNodeVersion('6.9.0'); + +/* + * Since Etherpad 1.8.0, at least NodeJS 8.9.0 will be required + */ +NodeVersion.checkDeprecationStatus('8.9.0', '1.8.0'); + /* * start up stats counting system */ @@ -43,16 +54,6 @@ var settings var npm = require("npm/lib/npm.js"); async.waterfall([ - function(callback) - { - NodeVersion.enforceMinNodeVersion('6.9.0', callback); - }, - - function(callback) - { - NodeVersion.checkDeprecationStatus('8.9.0', '1.8.0', callback); - }, - // load npm function(callback) { npm.load({}, function(er) { diff --git a/src/node/utils/NodeVersion.js b/src/node/utils/NodeVersion.js index dac492b1f..1ebbcbca0 100644 --- a/src/node/utils/NodeVersion.js +++ b/src/node/utils/NodeVersion.js @@ -24,19 +24,18 @@ const semver = require('semver'); * Quits if Etherpad is not running on a given minimum Node version * * @param {String} minNodeVersion Minimum required Node version - * @param {Function} callback Standard callback function */ -exports.enforceMinNodeVersion = function(minNodeVersion, callback) { +exports.enforceMinNodeVersion = function(minNodeVersion) { const currentNodeVersion = process.version; // we cannot use template literals, since we still do not know if we are // running under Node >= 4.0 if (semver.lt(currentNodeVersion, minNodeVersion)) { console.error('Running Etherpad on Node ' + currentNodeVersion + ' is not supported. Please upgrade at least to Node ' + minNodeVersion); - } else { - console.debug('Running on Node ' + currentNodeVersion + ' (minimum required Node version: ' + minNodeVersion + ')'); - callback(); + process.exit(1); } + + console.debug('Running on Node ' + currentNodeVersion + ' (minimum required Node version: ' + minNodeVersion + ')'); }; /** @@ -45,12 +44,10 @@ exports.enforceMinNodeVersion = function(minNodeVersion, callback) { * @param {String} lowestNonDeprecatedNodeVersion all Node version less than this one are deprecated * @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated Node releases */ -exports.checkDeprecationStatus = function(lowestNonDeprecatedNodeVersion, epRemovalVersion, callback) { +exports.checkDeprecationStatus = function(lowestNonDeprecatedNodeVersion, epRemovalVersion) { const currentNodeVersion = process.version; if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) { console.warn(`Support for Node ${currentNodeVersion} will be removed in Etherpad ${epRemovalVersion}. Please consider updating at least to Node ${lowestNonDeprecatedNodeVersion}`); } - - callback(); }; From 9d35d15ae357471b322cc40c397f4d4bef0763a0 Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 19 Feb 2019 00:48:50 +0100 Subject: [PATCH 067/183] node8: require nodejs >= 8.9.0, npm >= 6.4 Next version will be Etherpad 1.8. As planned in #3424, we are going to require NodeJS >=8.9.0 and npm >= 6.4. This commit implements that change and updates documentation and scripts. Subsequent changes will get rid of old idioms, dating back to node < 0.7, that still survive in the code. Once migrated to NodeJS 8, we will be able to start working on migrating the code base from callbacks to async/await, greatly simplifying legibility (see #3540). Closes #3557 --- README.md | 4 ++-- bin/installDeps.sh | 6 +++--- doc/plugins.md | 2 +- src/node/server.js | 8 +++++--- src/package.json | 4 ++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 41ac95848..c7ca0d4d6 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Etherpad is a really-real time collaborative editor scalable to thousands of sim # Installation ## Requirements -- `nodejs` >= **6.9.0** (preferred: `nodejs` >= **8.9**) +- `nodejs` >= **8.9.0** ## Uber-Quick Ubuntu ``` @@ -19,7 +19,7 @@ git clone --branch master https://github.com/ether/etherpad-lite.git && cd ether ``` ## GNU/Linux and other UNIX-like systems -You'll need git and [node.js](https://nodejs.org) installed (minimum required Node version: **6.9.0**, preferred: >= **8.9**). +You'll need git and [node.js](https://nodejs.org) installed (minimum required Node version: **8.9.0**). **As any user (we recommend creating a separate user called etherpad):** diff --git a/bin/installDeps.sh b/bin/installDeps.sh index a56031217..f083974c0 100755 --- a/bin/installDeps.sh +++ b/bin/installDeps.sh @@ -1,12 +1,12 @@ #!/bin/sh # minimum required node version -REQUIRED_NODE_MAJOR=6 +REQUIRED_NODE_MAJOR=8 REQUIRED_NODE_MINOR=9 # minimum required npm version -REQUIRED_NPM_MAJOR=3 -REQUIRED_NPM_MINOR=10 +REQUIRED_NPM_MAJOR=6 +REQUIRED_NPM_MINOR=4 require_minimal_version() { PROGRAM_LABEL="$1" diff --git a/doc/plugins.md b/doc/plugins.md index 5cb8d0ebf..4429e1bc3 100644 --- a/doc/plugins.md +++ b/doc/plugins.md @@ -115,7 +115,7 @@ Your plugin must also contain a [package definition file](https://docs.npmjs.com "author": "USERNAME (REAL NAME) ", "contributors": [], "dependencies": {"MODULE": "0.3.20"}, - "engines": { "node": ">= 6.9.0"} + "engines": { "node": ">= 8.9.0"} } ``` diff --git a/src/node/server.js b/src/node/server.js index 56cd580d3..3db54284c 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -32,12 +32,14 @@ log4js.replaceConsole(); * early check for version compatibility before calling * any modules that require newer versions of NodeJS */ -NodeVersion.enforceMinNodeVersion('6.9.0'); +NodeVersion.enforceMinNodeVersion('8.9.0'); /* - * Since Etherpad 1.8.0, at least NodeJS 8.9.0 will be required + * As of Etherpad 1.8.0, we do not have any further Node version deprecation in + * place. + * + * NodeVersion.checkDeprecationStatus('10.13.0', '1.9.0'); */ -NodeVersion.checkDeprecationStatus('8.9.0', '1.8.0'); /* * start up stats counting system diff --git a/src/package.json b/src/package.json index 1b9060942..e3cb756a6 100644 --- a/src/package.json +++ b/src/package.json @@ -73,8 +73,8 @@ "wd": "1.11.1" }, "engines": { - "node": ">=6.9.0", - "npm": ">=3.10.8" + "node": ">=8.9.0", + "npm": ">=6.4.1" }, "repository": { "type": "git", From 09949c242a672f258b07f2dbfb9c8662e7853a8c Mon Sep 17 00:00:00 2001 From: muxator Date: Fri, 15 Feb 2019 22:52:53 +0100 Subject: [PATCH 068/183] node8: we no longer need to use a shim for Object.values in stats.js --- src/node/stats.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/node/stats.js b/src/node/stats.js index 5ffa8a07b..ff1752fe9 100644 --- a/src/node/stats.js +++ b/src/node/stats.js @@ -1,19 +1,3 @@ -/* - * TODO: this polyfill is needed for Node 6.9 support. - * - * Once minimum supported Node version is raised to 8.9.0, it will be removed. - */ -if (!Object.values) { - var log4js = require('log4js'); - var statsLogger = log4js.getLogger("stats"); - - statsLogger.warn(`Enabling a polyfill to run on this Node version (${process.version}). Next Etherpad version will remove support for Node version < 8.9.0. Please update your runtime.`); - - var values = require('object.values'); - - values.shim(); -} - var measured = require('measured-core') module.exports = measured.createCollection(); From 6d36bb2c53b19455d6bafde48bb3ac22e0ccd320 Mon Sep 17 00:00:00 2001 From: muxator Date: Fri, 15 Feb 2019 22:30:17 +0100 Subject: [PATCH 069/183] node8: we can safely use os.tmpdir() Since we are requiring node >= 8, we can safely use native functionalities. --- src/node/handler/ExportHandler.js | 8 +------- src/node/handler/ImportHandler.js | 3 +-- tests/backend/specs/api/tidy.js | 3 ++- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js index 678519fbc..db3d2d40d 100644 --- a/src/node/handler/ExportHandler.js +++ b/src/node/handler/ExportHandler.js @@ -40,13 +40,7 @@ if(settings.abiword != null) if(settings.soffice != null) convertor = require("../utils/LibreOffice"); -var tempDirectory = "/tmp"; - -//tempDirectory changes if the operating system is windows -if(os.type().indexOf("Windows") > -1) -{ - tempDirectory = process.env.TEMP; -} +const tempDirectory = os.tmpdir(); /** * do a requested export diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index 45c4c6ea8..b08bdcf60 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -47,8 +47,7 @@ if(settings.soffice != null) { exportExtension = "html"; } -//for node 0.6 compatibily, os.tmpDir() only works from 0.8 -var tmpDirectory = process.env.TEMP || process.env.TMPDIR || process.env.TMP || '/tmp'; +const tmpDirectory = os.tmpdir(); /** * do a requested import diff --git a/tests/backend/specs/api/tidy.js b/tests/backend/specs/api/tidy.js index 47cb49f62..6f38ac7b0 100644 --- a/tests/backend/specs/api/tidy.js +++ b/tests/backend/specs/api/tidy.js @@ -23,7 +23,8 @@ describe('tidyHtml', function() { } // Try to tidy up a bad HTML file - var tmpDir = process.env.TEMP || "/tmp"; + const tmpDir = os.tmpdir(); + var tmpFile = path.join(tmpDir, 'tmp_' + (Math.floor(Math.random() * 1000000)) + '.html') fs.writeFileSync(tmpFile, '

    a paragraph

  4. List without outer UL
  5. trailing closing p

    '); TidyHtml.tidy(tmpFile, function(err){ From 59a6f2e9b8adedf1892e0ec8b763cac57b4af53b Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 19 Feb 2019 00:48:50 +0100 Subject: [PATCH 070/183] node8: get rid of node < 0.7 compatibility when deleting files. - path.exists() is no longer part of nodejs - fs.exists() is deprecated (as of nodejs >= 8) - checking a file for existence before using it is open to raca condition. It is preferable to go ahead and use the file, and eventually handle the error - we can afford two simple synchronous fs operations here --- src/node/handler/ImportHandler.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index b08bdcf60..30b773972 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -292,16 +292,19 @@ exports.doImport = function(req, res, padId) return; } - //for node < 0.7 compatible - var fileExists = fs.exists || path.exists; - async.parallel([ - function(callback){ - fileExists (srcFile, function(exist) { (exist)? fs.unlink(srcFile, callback): callback(); }); - }, - function(callback){ - fileExists (destFile, function(exist) { (exist)? fs.unlink(destFile, callback): callback(); }); - } - ], callback); + try { + fs.unlinkSync(srcFile); + } catch (e) { + console.log(e); + } + + try { + fs.unlinkSync(destFile); + } catch (e) { + console.log(e); + } + + callback(); } ], function(err) { var status = "ok"; From 401db8fce31aa2026c5603f00d80e258d14977cf Mon Sep 17 00:00:00 2001 From: Sebastian Castro <90scastro@gmail.com> Date: Fri, 15 Feb 2019 18:14:02 +0100 Subject: [PATCH 071/183] chat: Adds placeholder to input. Translate stick button --- src/locales/en.json | 2 ++ src/locales/es.json | 2 ++ src/locales/fr.json | 2 ++ src/locales/qqq.json | 2 ++ src/static/js/html10n.js | 3 ++- src/templates/pad.html | 4 ++-- 6 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index e438fa1b4..c8ef1a7ae 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -116,6 +116,8 @@ "pad.chat": "Chat", "pad.chat.title": "Open the chat for this pad.", "pad.chat.loadmessages": "Load more messages", + "pad.chat.stick.title": "Stick chat to screen", + "pad.chat.writeMessage.placeholder": "Write your message here", "timeslider.pageTitle": "{{appTitle}} Timeslider", "timeslider.toolbar.returnbutton": "Return to pad", diff --git a/src/locales/es.json b/src/locales/es.json index 747ec7bf5..c22803f3e 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -99,6 +99,8 @@ "pad.chat": "Chat", "pad.chat.title": "Abrir el chat para este pad.", "pad.chat.loadmessages": "Cargar más mensajes", + "pad.chat.stick.title": "Ampliar", + "pad.chat.writeMessage.placeholder": "Enviar un mensaje", "timeslider.pageTitle": "{{appTitle}} Línea de tiempo", "timeslider.toolbar.returnbutton": "Volver al pad", "timeslider.toolbar.authors": "Autores:", diff --git a/src/locales/fr.json b/src/locales/fr.json index b7f59885b..574858dd1 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -109,6 +109,8 @@ "pad.chat": "Clavardage", "pad.chat.title": "Ouvrir le clavardoir de ce pad.", "pad.chat.loadmessages": "Charger davantage de messages", + "pad.chat.stick.title": "Agrandir le chat", + "pad.chat.writeMessage.placeholder": "Entrez votre message ici", "timeslider.pageTitle": "Historique dynamique de {{appTitle}}", "timeslider.toolbar.returnbutton": "Retourner au pad", "timeslider.toolbar.authors": "Auteurs :", diff --git a/src/locales/qqq.json b/src/locales/qqq.json index 60d62b196..512ef6a31 100644 --- a/src/locales/qqq.json +++ b/src/locales/qqq.json @@ -83,6 +83,8 @@ "pad.chat": "Used as button text and as title of Chat window.\n{{Identical|Chat}}", "pad.chat.title": "Used as tooltip for the Chat button", "pad.chat.loadmessages": "chat messages", + "pad.chat.stick.title": "Tooltip for the stick chat button", + "pad.chat.writeMessage.placeholder": "Placeholder for the chat input", "timeslider.pageTitle": "{{doc-important|Please leave {{appTitle}} parameter untouched. It will be replaced by app title.}}\nInserted into HTML title tag.", "timeslider.toolbar.returnbutton": "Used as link title", "timeslider.toolbar.authors": "A list of Authors follows after the colon.\n{{Identical|Author}}", diff --git a/src/static/js/html10n.js b/src/static/js/html10n.js index 8dea84c28..8fbc6f643 100644 --- a/src/static/js/html10n.js +++ b/src/static/js/html10n.js @@ -854,7 +854,8 @@ window.html10n = (function(window, document, undefined) { , "value": 1 , "placeholder": 1 } - if (index > 0 && str.id.substr(index + 1) in attrList) { // an attribute has been specified + if (index > 0 && str.id.substr(index + 1) in attrList) { + // an attribute has been specified (example: "my_translation_key.placeholder") prop = str.id.substr(index + 1) } else { // no attribute: assuming text content by default prop = document.body.textContent ? 'textContent' : 'innerText' diff --git a/src/templates/pad.html b/src/templates/pad.html index c40116d3a..0e067113e 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -334,7 +334,7 @@
    @@ -342,7 +342,7 @@
    - +
    From 378dbe848510a7fbc1ffed792ccb548ed5168665 Mon Sep 17 00:00:00 2001 From: Sebastian Castro <90scastro@gmail.com> Date: Fri, 15 Feb 2019 17:59:06 +0100 Subject: [PATCH 072/183] skins: Improve clientPluginNames class helper Moving classes to html tag so it can be used to style other part of template depending on plugins like #users, #chat etc... Rename plugin class with "plugin-" prefix, because there were conflicts with some plugins using the same .ep_font_color class to apply css rules --- src/static/js/pluginfw/hooks.js | 2 +- src/static/skins/colibris/src/layout.css | 38 +++++++++---------- .../skins/colibris/src/plugins/font_color.css | 2 +- src/templates/pad.html | 4 +- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 3d19107d6..7f26c3bfb 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -146,7 +146,7 @@ exports.clientPluginNames = function() { var client_plugin_names = _.uniq( exports.plugins.parts .filter(function(part) { return part.hasOwnProperty('client_hooks'); }) - .map(function(part) { return part['plugin']; }) + .map(function(part) { return 'plugin-' + part['plugin']; }) ); return client_plugin_names; diff --git a/src/static/skins/colibris/src/layout.css b/src/static/skins/colibris/src/layout.css index 0e8b57744..d0f7357bf 100644 --- a/src/static/skins/colibris/src/layout.css +++ b/src/static/skins/colibris/src/layout.css @@ -25,11 +25,11 @@ padding-left: 40px; /* space for side div */ } -#outerdocbody.ep_author_neat { +#outerdocbody.plugin-ep_author_neat { padding-left: 120px; /* more space for sidediv */ } @media (max-width:600px) { - #outerdocbody.ep_author_neat { padding-left: 0; } + #outerdocbody.plugin-ep_author_neat { padding-left: 0; } #options-linenoscheck { display:none; } #options-linenoscheck ~ label { display:none; } } @@ -62,7 +62,7 @@ padding: 0; } -#outerdocbody.ep_author_neat #sidediv { +#outerdocbody.plugin-ep_author_neat #sidediv { right: calc(100% - 113px); } @@ -71,24 +71,24 @@ .authortooltip { margin-top: 65px !important; margin-left: 60px; } .caretindicator { margin-top: 61px!important; margin-left: 52px; } -#outerdocbody.ep_author_neat .authortooltip{ margin-left: 145px; } -#outerdocbody.ep_author_neat .caretindicator{ margin-left: 52px; margin-top: 65px!important;} +#outerdocbody.plugin-ep_author_neat .authortooltip{ margin-left: 145px; } +#outerdocbody.plugin-ep_author_neat .caretindicator{ margin-left: 52px; margin-top: 65px!important;} @media (max-width:1000px) { - #outerdocbody.ep_author_neat .authortooltip{ margin-left: 115px; } + #outerdocbody.plugin-ep_author_neat .authortooltip{ margin-left: 115px; } .caretindicator{ margin-left: 13px; } - #outerdocbody.ep_author_neat .caretindicator{ margin-left: 17px; } + #outerdocbody.plugin-ep_author_neat .caretindicator{ margin-left: 17px; } } @media (min-width: 1381px) { - #outerdocbody.ep_comments_page { padding-right: 150px; } } - #outerdocbody.ep_comments_page #comments { left: calc(100% - 150px) } + #outerdocbody.plugin-ep_comments_page { padding-right: 150px; } } + #outerdocbody.plugin-ep_comments_page #comments { left: calc(100% - 150px) } @media (max-width: 1380px) { - #outerdocbody.ep_comments_page #comments { left: calc(100% - 220px) } - #outerdocbody.ep_comments_page { padding-right: 220px; } + #outerdocbody.plugin-ep_comments_page #comments { left: calc(100% - 220px) } + #outerdocbody.plugin-ep_comments_page { padding-right: 220px; } } @media (max-width: 1278px) { - #outerdocbody.ep_comments_page #comments { display: none; } - #outerdocbody.ep_comments_page { padding-right: 0px; } + #outerdocbody.plugin-ep_comments_page #comments { display: none; } + #outerdocbody.plugin-ep_comments_page { padding-right: 0px; } } @media (max-width:1000px) { @@ -107,13 +107,13 @@ .comment-modal, .authortooltip { margin-top: 20px !important; } .caretindicator { margin-top: 0px !important; } - #outerdocbody.ep_author_neat .caretindicator { margin-top: 10px !important; } + #outerdocbody.plugin-ep_author_neat .caretindicator { margin-top: 10px !important; } - #outerdocbody.ep_author_neat #sidedivinner>div:before { padding-right: 10px !important; } - #outerdocbody.ep_author_neat #sidedivinner.authorColors>div, - #outerdocbody.ep_author_neat #sidedivinner.authorColors>div.primary-none, - #outerdocbody.ep_author_neat #sidedivinner>div { padding-right: 6px!important; } - #outerdocbody.ep_author_neat #sidediv { padding-right: 0 !important; } + #outerdocbody.plugin-ep_author_neat #sidedivinner>div:before { padding-right: 10px !important; } + #outerdocbody.plugin-ep_author_neat #sidedivinner.authorColors>div, + #outerdocbody.plugin-ep_author_neat #sidedivinner.authorColors>div.primary-none, + #outerdocbody.plugin-ep_author_neat #sidedivinner>div { padding-right: 6px!important; } + #outerdocbody.plugin-ep_author_neat #sidediv { padding-right: 0 !important; } } @media (max-width:600px) { diff --git a/src/static/skins/colibris/src/plugins/font_color.css b/src/static/skins/colibris/src/plugins/font_color.css index da719cb00..4ecd5c1cd 100644 --- a/src/static/skins/colibris/src/plugins/font_color.css +++ b/src/static/skins/colibris/src/plugins/font_color.css @@ -1,4 +1,4 @@ -li.acl-write.font-color-icon.ep_font_color { +li.acl-write.font-color-icon.plugin-ep_font_color { display: none; } diff --git a/src/templates/pad.html b/src/templates/pad.html index 0e067113e..bee248f57 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -6,7 +6,7 @@ %> <% e.begin_block("htmlHead"); %> - + <% e.end_block(); %> <%=settings.title%> @@ -100,7 +100,7 @@ <% e.end_block(); %> -
    +
    From 9848a600e333e5bb471ae459119476b3406bb6b8 Mon Sep 17 00:00:00 2001 From: Sebastian Castro <90scastro@gmail.com> Date: Fri, 15 Feb 2019 18:01:23 +0100 Subject: [PATCH 073/183] colibris: Fixes #3548 #3549 chat improvements --- .../skins/colibris/src/components/chat.css | 50 ++++++++++++------- .../skins/colibris/src/components/form.css | 1 + .../skins/colibris/src/components/users.css | 12 +++++ 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/static/skins/colibris/src/components/chat.css b/src/static/skins/colibris/src/components/chat.css index eb1dce445..c9a668241 100644 --- a/src/static/skins/colibris/src/components/chat.css +++ b/src/static/skins/colibris/src/components/chat.css @@ -6,6 +6,19 @@ box-shadow: 0 0 0 1px rgba(99, 114, 130, 0.16), 0 8px 16px rgba(27, 39, 51, 0.08); width: 400px; height: 300px; + background-color: #f9f9f9 !important; +} + +#chatbox.stickyChat { + width: 193px !important; + border: none !important; +} + +#chatbox.stickyChat.chatAndUsersChat{ + margin-top: 181px; + box-shadow: none; + border-top: 1px solid #d2d2d2 !important; + padding: 0!important; } #titlebar { @@ -27,16 +40,23 @@ #chattext { top: 45px; - font-size: 13px; - padding: 10px; - bottom: 45px; + font-size: 13px; + bottom: 52px; overflow-y: auto; + padding: 0; + border: none; + border-bottom: 1px solid #cccccc; } -#chattext.authorColors p, #chattext.authorColors span { - background-color: transparent !important; +.plugin-ep_author_neat #chattext { + padding: 10px; } +.plugin-ep_author_neat #chattext.authorColors p, +.plugin-ep_author_neat #chattext.authorColors span { + background-color: transparent !important; +} + #chattext p b { color: #4c4c4c; } @@ -55,8 +75,12 @@ background-color: #C5C5C5; } +#chatbox.stickyChat #chattext { + padding: 0px; +} + #chatinputbox { - padding: 0 5px 5px 10px; + padding: 8px; } #chatinput { @@ -64,18 +88,8 @@ float: right; } -#chatbox.stickyChat { - width: 193px !important; - background-color: #f9f9f9 !important; - border: none !important; -} - -#chatbox.stickyChat #chattext { - padding: 6px; -} - -#chatbox.stickyChat #chatinputbox { - padding: 5px 5px 3px 5px; +.plugin-ep_author_neat #chatbox.stickyChat #chattext { + padding: 5px 3px; } #chaticon { diff --git a/src/static/skins/colibris/src/components/form.css b/src/static/skins/colibris/src/components/form.css index 74a6fb589..5d34fc865 100644 --- a/src/static/skins/colibris/src/components/form.css +++ b/src/static/skins/colibris/src/components/form.css @@ -4,5 +4,6 @@ border-radius: 3px; padding: 8px 10px; background: none !important; + background-color: white !important; box-shadow: none !important; } \ No newline at end of file diff --git a/src/static/skins/colibris/src/components/users.css b/src/static/skins/colibris/src/components/users.css index 7e05b87ae..4d99ba281 100644 --- a/src/static/skins/colibris/src/components/users.css +++ b/src/static/skins/colibris/src/components/users.css @@ -80,4 +80,16 @@ table#otheruserstable { left: initial; top: initial !important; } +} + +#users.chatAndUsers { + width: 172px!important; + box-shadow: none; + border: none !important; + padding: 10px; + padding-top: 15px; +} + +#users.chatAndUsers #myusernameedit { + width: calc(100% - 10px); } \ No newline at end of file From 1900b00ec2c0d9182c6c9a745096552449de2d71 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 25 Feb 2019 10:41:33 +0100 Subject: [PATCH 074/183] Localisation updates from https://translatewiki.net. --- src/locales/ar.json | 2 + src/locales/be-tarask.json | 2 + src/locales/bg.json | 5 ++- src/locales/de.json | 2 + src/locales/diq.json | 4 +- src/locales/fr.json | 2 +- src/locales/it.json | 1 + src/locales/ko.json | 2 + src/locales/lb.json | 1 + src/locales/mk.json | 2 + src/locales/pt-br.json | 2 + src/locales/pt.json | 80 +++++++++++++++++++------------------- src/locales/sl.json | 2 + src/locales/sv.json | 2 + src/locales/zh-hans.json | 4 +- src/locales/zh-hant.json | 2 + 16 files changed, 72 insertions(+), 43 deletions(-) diff --git a/src/locales/ar.json b/src/locales/ar.json index b337cab99..9c7a4d9c5 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -95,6 +95,8 @@ "pad.chat": "دردشة", "pad.chat.title": "فتح الدردشة لهذا الباد", "pad.chat.loadmessages": "تحميل المزيد من الرسائل", + "pad.chat.stick.title": "ألصق الدردشة بالشاشة", + "pad.chat.writeMessage.placeholder": "اكتب رسالتك هنا", "timeslider.pageTitle": "{{appTitle}} متصفح التاريخ", "timeslider.toolbar.returnbutton": "العودة إلى الباد", "timeslider.toolbar.authors": "المؤلفون:", diff --git a/src/locales/be-tarask.json b/src/locales/be-tarask.json index 84af73156..d29f9cb20 100644 --- a/src/locales/be-tarask.json +++ b/src/locales/be-tarask.json @@ -88,6 +88,8 @@ "pad.chat": "Чат", "pad.chat.title": "Адкрыць чат для гэтага дакумэнту.", "pad.chat.loadmessages": "Загрузіць болей паведамленьняў", + "pad.chat.stick.title": "Замацаваць чат на экране", + "pad.chat.writeMessage.placeholder": "Напішыце вашае паведамленьне тут", "timeslider.pageTitle": "Часавая шкала {{appTitle}}", "timeslider.toolbar.returnbutton": "Вярнуцца да дакумэнту", "timeslider.toolbar.authors": "Аўтары:", diff --git a/src/locales/bg.json b/src/locales/bg.json index 0b29b0e77..2a22b5230 100644 --- a/src/locales/bg.json +++ b/src/locales/bg.json @@ -2,7 +2,8 @@ "@metadata": { "authors": [ "Vodnokon4e", - "StanProg" + "StanProg", + "Vlad5250" ] }, "index.newPad": "Нов пад", @@ -46,6 +47,8 @@ "pad.chat": "Чат", "pad.chat.title": "Отваряне на чат за този пад.", "pad.chat.loadmessages": "Зареждане на повече съобщения", + "pad.chat.stick.title": "Залепяне на разговора на екрана", + "pad.chat.writeMessage.placeholder": "Тук напишете съобщение", "timeslider.toolbar.returnbutton": "Връщане към пада", "timeslider.toolbar.authors": "Автори:", "timeslider.toolbar.authorsList": "Няма автори", diff --git a/src/locales/de.json b/src/locales/de.json index 85bd3bfff..5bbbb9b1b 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -92,6 +92,8 @@ "pad.chat": "Unterhaltung", "pad.chat.title": "Den Chat für dieses Pad öffnen.", "pad.chat.loadmessages": "Weitere Nachrichten laden", + "pad.chat.stick.title": "Chat an den Bildschirm anheften", + "pad.chat.writeMessage.placeholder": "Schreibe hier deine Nachricht", "timeslider.pageTitle": "{{appTitle}} Bearbeitungsverlauf", "timeslider.toolbar.returnbutton": "Zurück zum Pad", "timeslider.toolbar.authors": "Autoren:", diff --git a/src/locales/diq.json b/src/locales/diq.json index 8bf66e9c5..93e2fc04d 100644 --- a/src/locales/diq.json +++ b/src/locales/diq.json @@ -26,7 +26,7 @@ "pad.toolbar.import_export.title": "Babaetna tewranê dosyaya azere/ateber ke", "pad.toolbar.timeslider.title": "Ğızagê zemani", "pad.toolbar.savedRevision.title": "Çımraviyarnayışi qeyd ke", - "pad.toolbar.settings.title": "Sazkerdışi", + "pad.toolbar.settings.title": "Eyari", "pad.toolbar.embed.title": "Na bloknot degusn u bıhesrne", "pad.toolbar.showusers.title": "Karbera ena bloknot dı bımotné", "pad.colorpicker.save": "Qeyd ke", @@ -92,6 +92,8 @@ "pad.chat": "Mıhebet", "pad.chat.title": "Qandê ena ped mıhebet ake.", "pad.chat.loadmessages": "Dehana zaf mesaci bar keri", + "pad.chat.stick.title": "Mobet ekran de bıvındarne", + "pad.chat.writeMessage.placeholder": "Mesacê xo tiya bınusne", "timeslider.pageTitle": "Ğızagê zemani {{appTitle}}", "timeslider.toolbar.returnbutton": "Peyser şo ped", "timeslider.toolbar.authors": "Nuştoği:", diff --git a/src/locales/fr.json b/src/locales/fr.json index 574858dd1..51f576a45 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -109,7 +109,7 @@ "pad.chat": "Clavardage", "pad.chat.title": "Ouvrir le clavardoir de ce pad.", "pad.chat.loadmessages": "Charger davantage de messages", - "pad.chat.stick.title": "Agrandir le chat", + "pad.chat.stick.title": "Ancrer la discussion sur l'écran", "pad.chat.writeMessage.placeholder": "Entrez votre message ici", "timeslider.pageTitle": "Historique dynamique de {{appTitle}}", "timeslider.toolbar.returnbutton": "Retourner au pad", diff --git a/src/locales/it.json b/src/locales/it.json index a797dbebb..fbf5706bc 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -91,6 +91,7 @@ "pad.chat": "Chat", "pad.chat.title": "Apri la chat per questo Pad.", "pad.chat.loadmessages": "Carica altri messaggi", + "pad.chat.writeMessage.placeholder": "Scrivi il tuo messaggio qui", "timeslider.pageTitle": "Cronologia {{appTitle}}", "timeslider.toolbar.returnbutton": "Ritorna al Pad", "timeslider.toolbar.authors": "Autori:", diff --git a/src/locales/ko.json b/src/locales/ko.json index 19625eaea..dc325cdd4 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -92,6 +92,8 @@ "pad.chat": "대화", "pad.chat.title": "이 패드에 대화를 엽니다.", "pad.chat.loadmessages": "더 많은 메시지 불러오기", + "pad.chat.stick.title": "채팅을 화면에 고정", + "pad.chat.writeMessage.placeholder": "여기에 메시지를 적으십시오", "timeslider.pageTitle": "{{appTitle}} 시간슬라이더", "timeslider.toolbar.returnbutton": "패드로 돌아가기", "timeslider.toolbar.authors": "저자:", diff --git a/src/locales/lb.json b/src/locales/lb.json index 2f1432a61..99fc69789 100644 --- a/src/locales/lb.json +++ b/src/locales/lb.json @@ -59,6 +59,7 @@ "pad.share.link": "Link", "pad.chat": "Chat", "pad.chat.loadmessages": "Méi Message lueden", + "pad.chat.writeMessage.placeholder": "Schreift Äre Message hei", "timeslider.toolbar.authors": "Auteuren:", "timeslider.toolbar.authorsList": "Keng Auteuren", "timeslider.toolbar.exportlink.title": "Exportéieren", diff --git a/src/locales/mk.json b/src/locales/mk.json index 5c2ce32cf..78d1e65b9 100644 --- a/src/locales/mk.json +++ b/src/locales/mk.json @@ -88,6 +88,8 @@ "pad.chat": "Разговор", "pad.chat.title": "Отвори го разговорот за оваа тетратка.", "pad.chat.loadmessages": "Вчитај уште пораки", + "pad.chat.stick.title": "Залепи го разговорот на екранот", + "pad.chat.writeMessage.placeholder": "Тука напишете порака", "timeslider.pageTitle": "{{appTitle}} Историски преглед", "timeslider.toolbar.returnbutton": "Назад на тетратката", "timeslider.toolbar.authors": "Автори:", diff --git a/src/locales/pt-br.json b/src/locales/pt-br.json index 967af6a17..5f27927f8 100644 --- a/src/locales/pt-br.json +++ b/src/locales/pt-br.json @@ -101,6 +101,8 @@ "pad.chat": "Bate-papo", "pad.chat.title": "Abrir o bate-papo desta nota.", "pad.chat.loadmessages": "Carregar mais mensagens", + "pad.chat.stick.title": "Cole o bate-papo na tela", + "pad.chat.writeMessage.placeholder": "Escreva sua mensagem aqui", "timeslider.pageTitle": "Linha do tempo de {{appTitle}}", "timeslider.toolbar.returnbutton": "Retornar para a nota", "timeslider.toolbar.authors": "Autores:", diff --git a/src/locales/pt.json b/src/locales/pt.json index a65730a08..3f38cca56 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -13,34 +13,34 @@ ] }, "index.newPad": "Nova Nota", - "index.createOpenPad": "ou crie/abra uma Nota com o nome:", - "pad.toolbar.bold.title": "Negrito (Ctrl-B)", - "pad.toolbar.italic.title": "Itálico (Ctrl-I)", - "pad.toolbar.underline.title": "Sublinhado (Ctrl-U)", + "index.createOpenPad": "ou crie/abra uma nota com o nome:", + "pad.toolbar.bold.title": "Negrito (Ctrl+B)", + "pad.toolbar.italic.title": "Itálico (Ctrl+I)", + "pad.toolbar.underline.title": "Sublinhado (Ctrl+U)", "pad.toolbar.strikethrough.title": "Riscar (Ctrl+5)", "pad.toolbar.ol.title": "Lista ordenada (Ctrl+Shift+N)", "pad.toolbar.ul.title": "Lista desordenada (Ctrl+Shift+L)", - "pad.toolbar.indent.title": "Avançar (TAB)", - "pad.toolbar.unindent.title": "Recuar (Shift+TAB)", - "pad.toolbar.undo.title": "Desfazer (Ctrl-Z)", - "pad.toolbar.redo.title": "Refazer (Ctrl-Y)", + "pad.toolbar.indent.title": "Indentar (TAB)", + "pad.toolbar.unindent.title": "Remover indentação (Shift+TAB)", + "pad.toolbar.undo.title": "Desfazer (Ctrl+Z)", + "pad.toolbar.redo.title": "Refazer (Ctrl+Y)", "pad.toolbar.clearAuthorship.title": "Limpar cores de autoria (Ctrl+Shift+C)", "pad.toolbar.import_export.title": "Importar/exportar de/para diferentes formatos de ficheiro", "pad.toolbar.timeslider.title": "Linha de tempo", - "pad.toolbar.savedRevision.title": "Salvar revisão", + "pad.toolbar.savedRevision.title": "Gravar revisão", "pad.toolbar.settings.title": "Configurações", - "pad.toolbar.embed.title": "Compartilhar e incorporar este pad", - "pad.toolbar.showusers.title": "Mostrar os utilizadores nesta Nota", + "pad.toolbar.embed.title": "Partilhar e incorporar esta nota", + "pad.toolbar.showusers.title": "Mostrar os utilizadores nesta nota", "pad.colorpicker.save": "Gravar", "pad.colorpicker.cancel": "Cancelar", "pad.loading": "A carregar…", "pad.noCookie": "O cookie não foi encontrado. Por favor, ative os cookies no seu navegador!", - "pad.passwordRequired": "Precisa de uma senha para aceder a este pad", - "pad.permissionDenied": "Não tem permissão para aceder a este pad.", - "pad.wrongPassword": "A palavra-chave está errada", - "pad.settings.padSettings": "Configurações da Nota", - "pad.settings.myView": "Minha vista", - "pad.settings.stickychat": "Bate-papo sempre no ecrã", + "pad.passwordRequired": "Precisa de uma palavra-passe para aceder a esta nota", + "pad.permissionDenied": "Não tem permissão para aceder a esta nota", + "pad.wrongPassword": "A sua palavra-passe estava errada", + "pad.settings.padSettings": "Configurações da nota", + "pad.settings.myView": "A minha vista", + "pad.settings.stickychat": "Conversação sempre no ecrã", "pad.settings.chatandusers": "Mostrar a conversação e os utilizadores", "pad.settings.colorcheck": "Cores de autoria", "pad.settings.linenocheck": "Números de linha", @@ -52,8 +52,8 @@ "pad.settings.language": "Língua:", "pad.importExport.import_export": "Importar/Exportar", "pad.importExport.import": "Carregar qualquer ficheiro de texto ou documento", - "pad.importExport.importSuccessful": "Bem sucedido!", - "pad.importExport.export": "Exportar a Nota atual como:", + "pad.importExport.importSuccessful": "Completo!", + "pad.importExport.export": "Exportar a nota atual como:", "pad.importExport.exportetherpad": "Etherpad", "pad.importExport.exporthtml": "HTML", "pad.importExport.exportplain": "Texto simples", @@ -62,19 +62,19 @@ "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.abiword.innerHTML": "Só é possível importar texto sem formatação ou HTML. Para obter funcionalidades de importação mais avançadas, por favor instale o AbiWord.", "pad.modals.connected": "Ligado.", - "pad.modals.reconnecting": "Reconectando-se ao seu bloco…", - "pad.modals.forcereconnect": "Forçar reconexão", - "pad.modals.reconnecttimer": "A tentar religar", + "pad.modals.reconnecting": "A restabelecer ligação ao seu bloco…", + "pad.modals.forcereconnect": "Forçar restabelecimento de ligação", + "pad.modals.reconnecttimer": "A tentar restabelecer ligação", "pad.modals.cancel": "Cancelar", "pad.modals.userdup": "Aberto noutra janela", - "pad.modals.userdup.explanation": "Este pad parece estar aberto em mais do que uma janela do navegador neste computador.", + "pad.modals.userdup.explanation": "Esta nota parece estar aberta em mais do que uma janela do navegador neste computador.", "pad.modals.userdup.advice": "Religar para utilizar esta janela.", "pad.modals.unauth": "Não autorizado", "pad.modals.unauth.explanation": "As suas permissões foram alteradas enquanto revia esta página. Tente religar.", "pad.modals.looping.explanation": "Existem problemas de comunicação com o servidor de sincronização.", "pad.modals.looping.cause": "Talvez tenha ligado por um firewall ou proxy incompatível.", "pad.modals.initsocketfail": "O servidor está inacessível.", - "pad.modals.initsocketfail.explanation": "Não foi possível a conexão ao servidor de sincronização.", + "pad.modals.initsocketfail.explanation": "Não foi possível ligar ao servidor de sincronização.", "pad.modals.initsocketfail.cause": "Isto provavelmente ocorreu por um problema no seu navegador ou na sua ligação de Internet.", "pad.modals.slowcommit.explanation": "O servidor não está a responder.", "pad.modals.slowcommit.cause": "Isto pode ser por problemas com a ligação de rede.", @@ -83,28 +83,30 @@ "pad.modals.corruptPad.explanation": "A nota que está a tentar aceder está corrompida.", "pad.modals.corruptPad.cause": "Isto pode ocorrer devido a uma configuração errada do servidor ou algum outro comportamento inesperado. Por favor contacte o administrador.", "pad.modals.deleted": "Eliminado.", - "pad.modals.deleted.explanation": "Este pad foi removido.", - "pad.modals.disconnected": "Você foi desconectado.", - "pad.modals.disconnected.explanation": "A conexão com o servidor foi perdida", + "pad.modals.deleted.explanation": "Esta nota foi removida.", + "pad.modals.disconnected": "Você foi desligado.", + "pad.modals.disconnected.explanation": "A ligação ao servidor foi perdida", "pad.modals.disconnected.cause": "O servidor pode estar indisponível. Por favor, notifique o administrador de serviço se isto continuar a acontecer.", - "pad.share": "Compartilhar este pad", + "pad.share": "Partilhar esta nota", "pad.share.readonly": "Somente para leitura", - "pad.share.link": "Ligação", + "pad.share.link": "Hiperligação", "pad.share.emebdcode": "Incorporar o URL", - "pad.chat": "Bate-papo", - "pad.chat.title": "Abrir o bate-papo para este pad.", + "pad.chat": "Conversação", + "pad.chat.title": "Abrir a conversação para esta nota.", "pad.chat.loadmessages": "Carregar mais mensagens", + "pad.chat.stick.title": "Colar conversação no ecrã", + "pad.chat.writeMessage.placeholder": "Escreva a sua mensagem aqui", "timeslider.pageTitle": "Linha do tempo de {{appTitle}}", - "timeslider.toolbar.returnbutton": "Voltar ao pad", + "timeslider.toolbar.returnbutton": "Voltar à nota", "timeslider.toolbar.authors": "Autores:", "timeslider.toolbar.authorsList": "Sem Autores", "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.playPause": "Reproduzir / Pausar conteúdo do Pad", - "timeslider.backRevision": "Voltar a uma revisão anterior neste Pad", - "timeslider.forwardRevision": "Ir a uma revisão posterior neste Pad", + "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", "timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", "timeslider.month.january": "Janeiro", "timeslider.month.february": "Fevereiro", @@ -129,11 +131,11 @@ "pad.editbar.clearcolors": "Deseja limpar as cores de autoria em todo o documento?", "pad.impexp.importbutton": "Importar agora", "pad.impexp.importing": "Importando...", - "pad.impexp.confirmimport": "A importação de um ficheiro irá substituir o texto atual do pad. Tem certeza que deseja continuar?", + "pad.impexp.confirmimport": "A importação de um ficheiro irá substituir o texto atual da nota. Tem certeza que deseja continuar?", "pad.impexp.convertFailed": "Não foi possível importar este ficheiro. Utilize outro formato ou copie e insira manualmente", - "pad.impexp.padHasData": "Não fomos capazes de importar este ficheiro porque este Pad já tinha alterações, por favor importe para um novo pad", - "pad.impexp.uploadFailed": "O upload falhou. Por favor, tente novamente", + "pad.impexp.padHasData": "Não fomos capazes de importar este ficheiro porque esta nota já tinha alterações; importe para uma nota nova, por favor", + "pad.impexp.uploadFailed": "O carregamento falhou; tente novamente, por favor", "pad.impexp.importfailed": "A importação falhou", - "pad.impexp.copypaste": "Por favor, copie e cole", + "pad.impexp.copypaste": "Copie e insira, por favor", "pad.impexp.exportdisabled": "A exportação no formato {{type}} está desativada. Por favor, contacte o administrador do sistema para mais informações." } diff --git a/src/locales/sl.json b/src/locales/sl.json index f4168f2c1..37f716cd2 100644 --- a/src/locales/sl.json +++ b/src/locales/sl.json @@ -90,6 +90,8 @@ "pad.chat": "Klepet", "pad.chat.title": "Odpri klepetalno okno dokumenta.", "pad.chat.loadmessages": "Naloži več sporočil", + "pad.chat.stick.title": "Prilepi klepet na zaslon", + "pad.chat.writeMessage.placeholder": "Napišite sporočilo", "timeslider.pageTitle": "Časovni trak {{appTitle}}", "timeslider.toolbar.returnbutton": "Vrni se na dokument", "timeslider.toolbar.authors": "Avtorji:", diff --git a/src/locales/sv.json b/src/locales/sv.json index e73c3e7f3..7b03c12b2 100644 --- a/src/locales/sv.json +++ b/src/locales/sv.json @@ -88,6 +88,8 @@ "pad.chat": "Chatt", "pad.chat.title": "Öppna chatten för detta block.", "pad.chat.loadmessages": "Läs in fler meddelanden", + "pad.chat.stick.title": "Fäst chatten på skärmen", + "pad.chat.writeMessage.placeholder": "Skriv ditt meddelande här", "timeslider.pageTitle": "{{appTitle}} tidsreglage", "timeslider.toolbar.returnbutton": "Återvänd till blocket", "timeslider.toolbar.authors": "Författare:", diff --git a/src/locales/zh-hans.json b/src/locales/zh-hans.json index a598d9aad..4cc7c838c 100644 --- a/src/locales/zh-hans.json +++ b/src/locales/zh-hans.json @@ -11,7 +11,8 @@ "Yfdyh000", "乌拉跨氪", "燃玉", - "JuneAugust" + "JuneAugust", + "94rain" ] }, "index.newPad": "新记事本", @@ -96,6 +97,7 @@ "pad.chat": "聊天", "pad.chat.title": "打开此记事本的聊天窗口。", "pad.chat.loadmessages": "加载更多信息", + "pad.chat.writeMessage.placeholder": "在此写下您的消息", "timeslider.pageTitle": "{{appTitle}} 时间轴", "timeslider.toolbar.returnbutton": "返回记事本", "timeslider.toolbar.authors": "作者:", diff --git a/src/locales/zh-hant.json b/src/locales/zh-hant.json index 2a7d6d1ef..6d43f67d7 100644 --- a/src/locales/zh-hant.json +++ b/src/locales/zh-hant.json @@ -93,6 +93,8 @@ "pad.chat": "聊天功能", "pad.chat.title": "打開記事本聊天功能", "pad.chat.loadmessages": "載入更多訊息", + "pad.chat.stick.title": "釘住聊天在螢幕上", + "pad.chat.writeMessage.placeholder": "在此編寫您的訊息", "timeslider.pageTitle": "{{appTitle}}時間軸", "timeslider.toolbar.returnbutton": "返回到記事本", "timeslider.toolbar.authors": "協作者:", From 26f3f1bcd098b926497e840ff8c4ff1b7adcaa4a Mon Sep 17 00:00:00 2001 From: muxator Date: Wed, 13 Feb 2019 14:01:24 +0100 Subject: [PATCH 075/183] db/Pad.js: make "force" parameter non optional in Pad.prototype.copy() This function was simulating two overloads: 1. copy(destinationID, force, callback) 2. copy(destinationID, callback), in this case "force" would be assumed false But all the call sites always used the version with arity 3. Thus, we can remove that optionality and always assume that the funcion will be called with three parameters. This will simplify future work. --- src/node/db/Pad.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index 91ab7f792..4e7d16987 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -454,15 +454,12 @@ Pad.prototype.copy = function copy(destinationID, force, callback) { var _this = this; var destGroupID; - // make force optional - if (typeof force == "function") { - callback = force; - force = false; + // allow force to be a string + if (typeof force === "string") { + force = (force.toLowerCase() === "true"); + } else { + force = !!force; } - else if (force == undefined || force.toLowerCase() != "true") { - force = false; - } - else force = true; // Kick everyone from this pad. // This was commented due to https://github.com/ether/etherpad-lite/issues/3183. From 169a06793db8d12b7eeb084d2ebf3f4b108c0d87 Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 19 Feb 2019 00:15:54 +0100 Subject: [PATCH 076/183] db/API.js: almost removed optional argument handling The HTTP API doesn't ever omit arguments, it always passes `undefined` for a parameter that wasn't supplied in the request. The functions that were simplified are: - getRevisionChangeset() - getText() - getHTML() - saveRevision() The only function still supporting optional arguments is getPadSafe(), which is only called from this module. --- src/node/db/API.js | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/node/db/API.js b/src/node/db/API.js index 21c958809..6d6c8ccff 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -126,13 +126,6 @@ Example returns: */ exports.getRevisionChangeset = function(padID, rev, callback) { - // check if rev is set - if (typeof rev === "function") - { - callback = rev; - rev = undefined; - } - // check if rev is a number if (rev !== undefined && typeof rev !== "number") { @@ -206,13 +199,6 @@ Example returns: */ exports.getText = function(padID, rev, callback) { - //check if rev is set - if(typeof rev == "function") - { - callback = rev; - rev = undefined; - } - //check if rev is a number if(rev !== undefined && typeof rev != "number") { @@ -347,12 +333,6 @@ Example returns: */ exports.getHTML = function(padID, rev, callback) { - if(typeof rev == "function") - { - callback = rev; - rev = undefined; - } - if (rev !== undefined && typeof rev != "number") { if (isNaN(parseInt(rev))) @@ -623,13 +603,6 @@ Example returns: */ exports.saveRevision = function(padID, rev, callback) { - //check if rev is set - if(typeof rev == "function") - { - callback = rev; - rev = undefined; - } - //check if rev is a number if(rev !== undefined && typeof rev != "number") { From 46fdeb8dc4495b5a5b1e9e7a67c66465bf9d3b68 Mon Sep 17 00:00:00 2001 From: muxator Date: Fri, 22 Feb 2019 23:32:10 +0100 Subject: [PATCH 077/183] ExportTxt.js: getPadTXT() does not need to be exported This function is used only inside this module, and does not belong to its external interface. --- src/node/utils/ExportTxt.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/node/utils/ExportTxt.js b/src/node/utils/ExportTxt.js index e3ce01520..8a40e800d 100644 --- a/src/node/utils/ExportTxt.js +++ b/src/node/utils/ExportTxt.js @@ -68,9 +68,6 @@ function getPadTXT(pad, revNum, callback) }); } -exports.getPadTXT = getPadTXT; - - // This is different than the functionality provided in ExportHtml as it provides formatting // functionality that is designed specifically for TXT exports function getTXTFromAtext(pad, atext, authorColors) From 791012bb9b32de3ef938369e7af3703c5e70ad7b Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 26 Feb 2019 19:15:22 +0100 Subject: [PATCH 078/183] PadMessageHandler.js: removed redundant return statement --- src/node/handler/PadMessageHandler.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index c52565de3..fc5df9825 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -531,7 +531,6 @@ function handleSuggestUserName(client, message) var session = sessioninfos[client.id]; if(session && session.author == message.data.payload.unnamedId) { client.json.send(message); - return; } }); } From 4b913172fe72457a2c7598197907188aef186364 Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 26 Feb 2019 22:19:49 +0100 Subject: [PATCH 079/183] PadMessageHandler.js: renamed parameter in handleCustomMessage() to avoid name clash --- src/node/handler/PadMessageHandler.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index fc5df9825..1455660c1 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -352,14 +352,14 @@ exports.handleCustomObjectMessage = function (msg, sessionID, cb) { * Handles a custom message (sent via HTTP API request) * * @param padID {Pad} the pad to which we're sending this message - * @param msg {String} the message we're sending + * @param msgString {String} the message we're sending */ -exports.handleCustomMessage = function (padID, msg, cb) { +exports.handleCustomMessage = function (padID, msgString, cb) { var time = new Date().getTime(); var msg = { type: 'COLLABROOM', data: { - type: msg, + type: msgString, time: time } }; From 90bfbeb38dba23edf83e1a70a2aa6e9a38e26397 Mon Sep 17 00:00:00 2001 From: muxator Date: Wed, 27 Feb 2019 00:56:41 +0100 Subject: [PATCH 080/183] PadMessageHandler.js: fixed logic error in a guard condition The guard condition on count being non negative and < 100 used the wrong boolean operator. In its form it was impossible. This error was introduced in 2013, in 5592c4b0feb42082ab50e1c90753c28e5cd79334. Fixes #3499 --- src/node/handler/PadMessageHandler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 1455660c1..5581dbeb9 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -462,9 +462,9 @@ function handleGetChatMessages(client, message) var start = message.data.start; var end = message.data.end; - var count = start - count; + var count = end - start; - if(count < 0 && count > 100) + if(count < 0 || count > 100) { messageLogger.warn("Dropped message, GetChatMessages Message, client requested invalid amout of messages!"); return; From b34fc2de2b40138275275596a790358fe1e130d9 Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 26 Feb 2019 23:25:15 +0100 Subject: [PATCH 081/183] use Date.now() instead of new Date().getTime() This is documented to be more performant. The substitution was made on frontend code, too (i.e., the one in /static), because Date.now() is supported since IE 9, and we are life supporting only IE 11. Commands: find . -name *.js | xargs sed --in-place "s/new Date().getTime()/Date.now()/g" find . -name *.js | xargs sed --in-place "s/(new Date()).getTime()/Date.now()/g" Not done on jQuery. --- bin/convert.js | 8 ++++---- bin/importSqlFile.js | 4 ++-- src/node/db/API.js | 2 +- src/node/db/AuthorManager.js | 4 ++-- src/node/db/Pad.js | 4 ++-- src/node/db/SecurityManager.js | 2 +- src/node/db/SessionManager.js | 2 +- src/node/handler/PadMessageHandler.js | 6 +++--- src/node/utils/Minify.js | 2 +- src/static/js/ace2_inner.js | 2 +- src/static/js/pad.js | 2 +- tests/frontend/specs/helper.js | 4 ++-- tests/frontend/specs/responsiveness.js | 4 ++-- 13 files changed, 23 insertions(+), 23 deletions(-) diff --git a/bin/convert.js b/bin/convert.js index 757602c29..5b04b2db0 100644 --- a/bin/convert.js +++ b/bin/convert.js @@ -1,4 +1,4 @@ -var startTime = new Date().getTime(); +var startTime = Date.now(); var fs = require("fs"); var ueberDB = require("../src/node_modules/ueberDB"); var mysql = require("../src/node_modules/ueberDB/node_modules/mysql"); @@ -43,7 +43,7 @@ var etherpadDB = mysql.createConnection({ }); //get the timestamp once -var timestamp = new Date().getTime(); +var timestamp = Date.now(); var padIDs; @@ -110,7 +110,7 @@ async.series([ function log(str) { - console.log((new Date().getTime() - startTime)/1000 + "\t" + str); + console.log((Date.now() - startTime)/1000 + "\t" + str); } var padsDone = 0; @@ -121,7 +121,7 @@ function incrementPadStats() if(padsDone%100 == 0) { - var averageTime = Math.round(padsDone/((new Date().getTime() - startTime)/1000)); + var averageTime = Math.round(padsDone/((Date.now() - startTime)/1000)); log(padsDone + "/" + padIDs.length + "\t" + averageTime + " pad/s") } } diff --git a/bin/importSqlFile.js b/bin/importSqlFile.js index 5cdf46e5d..25592438f 100644 --- a/bin/importSqlFile.js +++ b/bin/importSqlFile.js @@ -1,4 +1,4 @@ -var startTime = new Date().getTime(); +var startTime = Date.now(); require("ep_etherpad-lite/node_modules/npm").load({}, function(er,npm) { @@ -73,7 +73,7 @@ require("ep_etherpad-lite/node_modules/npm").load({}, function(er,npm) { function log(str) { - console.log((new Date().getTime() - startTime)/1000 + "\t" + str); + console.log((Date.now() - startTime)/1000 + "\t" + str); } unescape = function(val) { diff --git a/src/node/db/API.js b/src/node/db/API.js index 6d6c8ccff..a95723451 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -522,7 +522,7 @@ exports.appendChatMessage = function(padID, text, authorID, time, callback) if(time === undefined || !is_int(time)) { // set time to current timestamp - time = new Date().getTime(); + time = Date.now(); } var padMessage = require("ep_etherpad-lite/node/handler/PadMessageHandler.js"); diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index c7ebf47f4..bcb6d393d 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -110,7 +110,7 @@ function mapAuthorWithDBKey (mapperkey, mapper, callback) //there is a author with this mapper //update the timestamp of this author - db.setSub("globalAuthor:" + author, ["timestamp"], new Date().getTime()); + db.setSub("globalAuthor:" + author, ["timestamp"], Date.now()); //return the author callback(null, {authorID: author}); @@ -127,7 +127,7 @@ exports.createAuthor = function(name, callback) var author = "a." + randomString(16); //create the globalAuthors db entry - var authorObj = {"colorId" : Math.floor(Math.random()*(exports.getColorPalette().length)), "name": name, "timestamp": new Date().getTime()}; + var authorObj = {"colorId" : Math.floor(Math.random()*(exports.getColorPalette().length)), "name": name, "timestamp": Date.now()}; //set the global author db entry db.set("globalAuthor:" + author, authorObj); diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index 4e7d16987..b74de5228 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -86,7 +86,7 @@ Pad.prototype.appendRevision = function appendRevision(aChangeset, author) { newRevData.changeset = aChangeset; newRevData.meta = {}; newRevData.meta.author = author; - newRevData.meta.timestamp = new Date().getTime(); + newRevData.meta.timestamp = Date.now(); //ex. getNumForAuthor if(author != '') @@ -739,7 +739,7 @@ Pad.prototype.addSavedRevision = function addSavedRevision(revNum, savedById, la savedRevision.revNum = revNum; savedRevision.savedById = savedById; savedRevision.label = label || "Revision " + revNum; - savedRevision.timestamp = new Date().getTime(); + savedRevision.timestamp = Date.now(); savedRevision.id = randomString(10); //save this new saved revision diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index f930b9618..2c46ac508 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -152,7 +152,7 @@ exports.checkAccess = function (padID, sessionCookie, token, password, callback) if(ERR(err, callback)) return; - var now = Math.floor(new Date().getTime()/1000); + var now = Math.floor(Date.now()/1000); //is it for this group? if(sessionInfo.groupID != groupID) diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index 954203758..99803ee71 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -114,7 +114,7 @@ exports.createSession = function(groupID, authorID, validUntil, callback) } //check if validUntil is in the future - if(Math.floor(new Date().getTime()/1000) > validUntil) + if(Math.floor(Date.now()/1000) > validUntil) { callback(new customError("validUntil is in the past","apierror")); return; diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 5581dbeb9..506b3aae4 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -355,7 +355,7 @@ exports.handleCustomObjectMessage = function (msg, sessionID, cb) { * @param msgString {String} the message we're sending */ exports.handleCustomMessage = function (padID, msgString, cb) { - var time = new Date().getTime(); + var time = Date.now(); var msg = { type: 'COLLABROOM', data: { @@ -375,7 +375,7 @@ exports.handleCustomMessage = function (padID, msgString, cb) { */ function handleChatMessage(client, message) { - var time = new Date().getTime(); + var time = Date.now(); var userId = sessioninfos[client.id].author; var text = message.data.text; var padId = sessioninfos[client.id].padId; @@ -1313,7 +1313,7 @@ function handleClientReady(client, message) "numConnectedUsers": roomClients.length, "readOnlyId": padIds.readOnlyPadId, "readonly": padIds.readonly, - "serverTimestamp": new Date().getTime(), + "serverTimestamp": Date.now(), "userId": author, "abiwordAvailable": settings.abiwordAvailable(), "sofficeAvailable": settings.sofficeAvailable(), diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index 4596f404c..6f3b75186 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -204,7 +204,7 @@ function minify(req, res, next) res.setHeader('last-modified', date.toUTCString()); res.setHeader('date', (new Date()).toUTCString()); if (settings.maxAge !== undefined) { - var expiresDate = new Date((new Date()).getTime()+settings.maxAge*1000); + var expiresDate = new Date(Date.now()+settings.maxAge*1000); res.setHeader('expires', expiresDate.toUTCString()); res.setHeader('cache-control', 'max-age=' + settings.maxAge); } diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index a34b94e59..597e8451a 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -1071,7 +1071,7 @@ function Ace2Inner(){ function now() { - return (new Date()).getTime(); + return Date.now(); } function newTimeLimit(ms) diff --git a/src/static/js/pad.js b/src/static/js/pad.js index a72225979..b12058237 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -475,7 +475,7 @@ var pad = { }, _afterHandshake: function() { - pad.clientTimeOffset = new Date().getTime() - clientVars.serverTimestamp; + pad.clientTimeOffset = Date.now() - clientVars.serverTimestamp; //initialize the chat chat.init(this); getParams(); diff --git a/tests/frontend/specs/helper.js b/tests/frontend/specs/helper.js index 8520769aa..727e47960 100644 --- a/tests/frontend/specs/helper.js +++ b/tests/frontend/specs/helper.js @@ -41,12 +41,12 @@ describe("the test helper", function(){ describe("the waitFor method", function(){ it("takes a timeout and waits long enough", function(done){ this.timeout(2000); - var startTime = new Date().getTime(); + var startTime = Date.now(); helper.waitFor(function(){ return false; }, 1500).fail(function(){ - var duration = new Date().getTime() - startTime; + var duration = Date.now() - startTime; expect(duration).to.be.greaterThan(1400); done(); }); diff --git a/tests/frontend/specs/responsiveness.js b/tests/frontend/specs/responsiveness.js index ff7dace17..dec7b8dce 100644 --- a/tests/frontend/specs/responsiveness.js +++ b/tests/frontend/specs/responsiveness.js @@ -49,7 +49,7 @@ describe('Responsiveness of Editor', function() { }).done(function(){ expect( inner$('div').text().length ).to.be.greaterThan( length ); // has the text changed? - var start = new Date().getTime(); // get the start time + var start = Date.now(); // get the start time // send some new text to the screen (ensure all 3 key events are sent) var el = inner$('div').first(); @@ -65,7 +65,7 @@ describe('Responsiveness of Editor', function() { helper.waitFor(function(){ // Wait for the ability to process return true; // Ghetto but works for now }).done(function(){ - var end = new Date().getTime(); // get the current time + var end = Date.now(); // get the current time var delay = end - start; // get the delay as the current time minus the start time console.log('delay:', delay); From ac7663c3372853f1c79d0bf783b3d929ad6a4828 Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Tue, 5 Mar 2019 10:46:57 +0000 Subject: [PATCH 082/183] db/DB.js: prevent DB layer from returning undefined ueberDB2 can return either undefined or null for a missing key, depending on which DB driver is used. This patch changes the promise version of the API so that it will always return null. --- src/node/db/DB.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/node/db/DB.js b/src/node/db/DB.js index 17cf3080a..573563665 100644 --- a/src/node/db/DB.js +++ b/src/node/db/DB.js @@ -51,6 +51,19 @@ exports.init = function() { exports[fn] = util.promisify(db[fn].bind(db)); }); + // set up wrappers for get and getSub that can't return "undefined" + let get = exports.get; + exports.get = async function(key) { + let result = await get(key); + return (result === undefined) ? null : result; + }; + + let getSub = exports.getSub; + exports.getSub = async function(key, sub) { + let result = await getSub(key, sub); + return (result === undefined) ? null : result; + }; + // exposed for those callers that need the underlying raw API exports.db = db; resolve(); From 10172af1998bde046dc2f944f7acedc0b9df4f08 Mon Sep 17 00:00:00 2001 From: muxator Date: Thu, 7 Mar 2019 00:02:24 +0100 Subject: [PATCH 083/183] db/API.js: no need to parseInt(time) here We are already sure that time is an int --- src/node/db/API.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/db/API.js b/src/node/db/API.js index a95723451..b71b5dda8 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -527,7 +527,7 @@ exports.appendChatMessage = function(padID, text, authorID, time, callback) var padMessage = require("ep_etherpad-lite/node/handler/PadMessageHandler.js"); // save chat message to database and send message to all connected clients - padMessage.sendChatMessageToPadClients(parseInt(time), authorID, text, padID); + padMessage.sendChatMessageToPadClients(time, authorID, text, padID); callback(); } From 72260b86de5a4f625d4be3e55dd2ec14da6da37e Mon Sep 17 00:00:00 2001 From: muxator Date: Thu, 7 Mar 2019 00:04:08 +0100 Subject: [PATCH 084/183] db/API.js: reuse the already required padMessageHandler (1 of 2) Commit 94cb743ca8a7 ("Fix API call appendChatMessage to send new message to all connected clients") fixed a bug, but introduced a redundant require(). --- src/node/db/API.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/node/db/API.js b/src/node/db/API.js index b71b5dda8..3a5460524 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -525,9 +525,8 @@ exports.appendChatMessage = function(padID, text, authorID, time, callback) time = Date.now(); } - var padMessage = require("ep_etherpad-lite/node/handler/PadMessageHandler.js"); // save chat message to database and send message to all connected clients - padMessage.sendChatMessageToPadClients(time, authorID, text, padID); + padMessageHandler.sendChatMessageToPadClients(time, authorID, text, padID); callback(); } From 06756e49eebb4bf086011a3ddc20ff561aee9327 Mon Sep 17 00:00:00 2001 From: muxator Date: Thu, 7 Mar 2019 00:14:49 +0100 Subject: [PATCH 085/183] db/API.js: reuse the already required padMessageHandler (2 of 2) It was introduced on 2014-11-12 by commit 9d39c9591ada ("update pad clients"). --- src/node/db/API.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/node/db/API.js b/src/node/db/API.js index 3a5460524..a36f043b0 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -740,7 +740,6 @@ exports.deletePad = function(padID, callback) exports.restoreRevision = function (padID, rev, callback) { var Changeset = require("ep_etherpad-lite/static/js/Changeset"); - var padMessage = require("ep_etherpad-lite/node/handler/PadMessageHandler.js"); //check if rev is a number if (rev !== undefined && typeof rev != "number") @@ -830,7 +829,7 @@ exports.restoreRevision = function (padID, rev, callback) //append the changeset pad.appendRevision(changeset); // - padMessage.updatePadClients(pad, function () + padMessageHandler.updatePadClients(pad, function () { }); callback(null, null); From cc23bd18a45f92f9197844fa50ca91602bbae950 Mon Sep 17 00:00:00 2001 From: muxator Date: Thu, 7 Mar 2019 00:39:41 +0100 Subject: [PATCH 086/183] db/API.js: require() Changeset library at top of file Introduced with d246a191c6cc ("Added option to restore revisions #1791") on 2014-11-08. --- src/node/db/API.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/node/db/API.js b/src/node/db/API.js index a36f043b0..aaf29e67c 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -19,6 +19,7 @@ */ var ERR = require("async-stacktrace"); +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var customError = require("../utils/customError"); var padManager = require("./PadManager"); var padMessageHandler = require("../handler/PadMessageHandler"); @@ -739,8 +740,6 @@ exports.deletePad = function(padID, callback) */ exports.restoreRevision = function (padID, rev, callback) { - var Changeset = require("ep_etherpad-lite/static/js/Changeset"); - //check if rev is a number if (rev !== undefined && typeof rev != "number") { From e9be94e3cf44d4b6956e41fd8facef678a90883e Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 7 Mar 2019 16:09:56 +0100 Subject: [PATCH 087/183] Localisation updates from https://translatewiki.net. --- src/locales/bn.json | 1 + src/locales/br.json | 10 ++++++---- src/locales/he.json | 2 ++ src/locales/pms.json | 2 ++ src/locales/ru.json | 2 ++ src/locales/uk.json | 2 ++ 6 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/locales/bn.json b/src/locales/bn.json index d8ee8bb1e..42dae70b7 100644 --- a/src/locales/bn.json +++ b/src/locales/bn.json @@ -75,6 +75,7 @@ "pad.chat": "চ্যাট", "pad.chat.title": "এই প্যাডের জন্য চ্যাট চালু করুন।", "pad.chat.loadmessages": "আরও বার্তা লোড করুন", + "pad.chat.writeMessage.placeholder": "আপনার বার্তাটি এখানে লিখুন", "timeslider.toolbar.returnbutton": "প্যাডে ফিরে যাও", "timeslider.toolbar.authors": "লেখকগণ:", "timeslider.toolbar.authorsList": "কোনো লেখক নেই", diff --git a/src/locales/br.json b/src/locales/br.json index ea3da7687..b020f9630 100644 --- a/src/locales/br.json +++ b/src/locales/br.json @@ -17,8 +17,8 @@ "pad.toolbar.ul.title": "Listenn en dizurzh (Ktrl+Pennlizherenn+L)", "pad.toolbar.indent.title": "Endantañ (TAB)", "pad.toolbar.unindent.title": "Diendantañ (Shift+TAB)", - "pad.toolbar.undo.title": "Dizober (Ktrl-Z)", - "pad.toolbar.redo.title": "Adober (Ktrl-Y)", + "pad.toolbar.undo.title": "Dizober (Ktrl+Z)", + "pad.toolbar.redo.title": "Adober (Ktrl+Y)", "pad.toolbar.clearAuthorship.title": "Diverkañ al livioù oc'h anaout an aozerien (Ktrl+Pennlizherenn+C)", "pad.toolbar.import_export.title": "Enporzhiañ/Ezporzhiañ eus/war-zu ur furmad restr disheñvel", "pad.toolbar.timeslider.title": "Istor dinamek", @@ -63,7 +63,7 @@ "pad.modals.cancel": "Nullañ", "pad.modals.userdup": "Digor en ur prenestr all", "pad.modals.userdup.explanation": "Digor eo ho pad, war a seblant, e meur a brenestr eus ho merdeer en urzhiataer-mañ.", - "pad.modals.userdup.advice": "Kevreañ en ur implijout ar prenestr-mañ.", + "pad.modals.userdup.advice": "Kevreañ en-dro en ur implijout ar prenestr-mañ.", "pad.modals.unauth": "N'eo ket aotreet", "pad.modals.unauth.explanation": "Kemmet e vo hoc'h aotreoù pa vo diskwelet ar bajenn.-mañ Klaskit kevreañ en-dro.", "pad.modals.looping.explanation": "Kudennoù kehentiñ zo gant ar servijer sinkronelekaat.", @@ -89,6 +89,8 @@ "pad.chat": "Flap", "pad.chat.title": "Digeriñ ar flap kevelet gant ar pad-mañ.", "pad.chat.loadmessages": "Kargañ muioc'h a gemennadennoù", + "pad.chat.stick.title": "Gwriziennañ an diviz war ar skramm", + "pad.chat.writeMessage.placeholder": "Skrivañ ho kemenadenn amañ", "timeslider.pageTitle": "Istor dinamek eus {{appTitle}}", "timeslider.toolbar.returnbutton": "Distreiñ d'ar pad-mañ.", "timeslider.toolbar.authors": "Aozerien :", @@ -126,7 +128,7 @@ "pad.impexp.importing": "Oc'h enporzhiañ...", "pad.impexp.confirmimport": "Ma vez enporzhiet ur restr e vo diverket ar pezh zo en teul a-vremañ. Ha sur oc'h e fell deoc'h mont betek penn ?", "pad.impexp.convertFailed": "N'eus ket bet gallet enporzhiañ ar restr. Ober gant ur furmad teul all pe eilañ/pegañ gant an dorn.", - "pad.impexp.padHasData": "N'hon eus ket gallet enporzhiañ ar restr-mañdre ma'z eus bet degaset kemmoù er bloc'h-se ; enporzhiit anezhi war-zu ur bloc'h nevez, mar plij.", + "pad.impexp.padHasData": "N'hon eus ket gallet enporzhiañ ar restr-mañ dre ma'z eus bet degaset kemmoù er bloc'h-se ; enporzhiit anezhi war-zu ur bloc'h nevez, mar plij.", "pad.impexp.uploadFailed": "C'hwitet eo bet an enporzhiañ. Klaskit en-dro.", "pad.impexp.importfailed": "C'hwitet eo an enporzhiadenn", "pad.impexp.copypaste": "Eilit/pegit, mar plij", diff --git a/src/locales/he.json b/src/locales/he.json index a857671da..01cad9352 100644 --- a/src/locales/he.json +++ b/src/locales/he.json @@ -89,6 +89,8 @@ "pad.chat": "שיחה", "pad.chat.title": "פתיחת השיחה של הפנקס הזה.", "pad.chat.loadmessages": "טעינת הודעות נוספות", + "pad.chat.stick.title": "הצמדת צ׳אט למסך", + "pad.chat.writeMessage.placeholder": "מקום לכתיבת ההודעה שלך", "timeslider.pageTitle": "גולל זמן של {{appTitle}}", "timeslider.toolbar.returnbutton": "חזרה אל הפנקס", "timeslider.toolbar.authors": "כותבים:", diff --git a/src/locales/pms.json b/src/locales/pms.json index cdc80bea3..0ae18bf1e 100644 --- a/src/locales/pms.json +++ b/src/locales/pms.json @@ -84,6 +84,8 @@ "pad.chat": "Ciaciarada", "pad.chat.title": "Duverté la ciaciarada për cost feuj.", "pad.chat.loadmessages": "Carié pi 'd mëssagi", + "pad.chat.stick.title": "Taché la ciaciarada an slë scren", + "pad.chat.writeMessage.placeholder": "Ch'a scriva sò mëssage ambelessì", "timeslider.pageTitle": "Stòria dinàmica ëd {{appTitle}}", "timeslider.toolbar.returnbutton": "Torné al feuj", "timeslider.toolbar.authors": "Autor:", diff --git a/src/locales/ru.json b/src/locales/ru.json index 5d9911466..6b7681ffd 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -94,6 +94,8 @@ "pad.chat": "Чат", "pad.chat.title": "Открыть чат для этого документа.", "pad.chat.loadmessages": "Еще сообщения", + "pad.chat.stick.title": "Закрепить чат на экране", + "pad.chat.writeMessage.placeholder": "Напишите своё сообщение сюда", "timeslider.pageTitle": "Временная шкала {{appTitle}}", "timeslider.toolbar.returnbutton": "К документу", "timeslider.toolbar.authors": "Авторы:", diff --git a/src/locales/uk.json b/src/locales/uk.json index e6e6489bd..9a6ab8e71 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -95,6 +95,8 @@ "pad.chat": "Чат", "pad.chat.title": "Відкрити чат для цього документа.", "pad.chat.loadmessages": "Завантажити більше повідомлень", + "pad.chat.stick.title": "Закріпити чат на екрані", + "pad.chat.writeMessage.placeholder": "Напишіть своє повідомлення сюди", "timeslider.pageTitle": "Часова шкала {{appTitle}}", "timeslider.toolbar.returnbutton": "Повернутись до документа", "timeslider.toolbar.authors": "Автори:", From bf68666ae14cca526b9e30f83217affc5d020b8c Mon Sep 17 00:00:00 2001 From: muxator Date: Fri, 8 Mar 2019 01:38:36 +0100 Subject: [PATCH 088/183] docker: move the docker image creation inside the main repository This is a super simple start. At minimum, configuration via environment variables (see #3543) needs to be integrated in Etherpad to make this user-friendly. Resolves #3524. --- docker/Dockerfile | 52 ++++++++++++++++++++++++++++++++++++++++++ docker/README.md | 49 +++++++++++++++++++++++++++++++++++++++ docker/nop | 4 ++++ docker/supervisor.conf | 20 ++++++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 docker/Dockerfile create mode 100644 docker/README.md create mode 100644 docker/nop create mode 100644 docker/supervisor.conf diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..e705fb42a --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,52 @@ +# Etherpad Lite Dockerfile +# +# https://github.com/ether/etherpad-docker +# +# Author: muxator +# +# Version 0.1 + +FROM node:latest +LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite" + +# install supervisor +RUN apt-get update && apt-get install -y \ + supervisor \ + && rm -rf /var/lib/apt/lists/* + +# install supervisor.conf in a low layer of the container +ADD supervisor.conf /etc/supervisor/supervisor.conf + +# git hash of the version to be built. +# If not given, build the latest development version. +ARG ETHERPAD_VERSION=develop + +# grab the ETHERPAD_VERSION tarball from github (no need to clone the whole +# repository) +RUN echo "Getting version: ${ETHERPAD_VERSION}" && \ + curl \ + --location \ + --fail \ + --silent \ + --show-error \ + --output /opt/etherpad-lite.tar.gz \ + https://github.com/ether/etherpad-lite/archive/"${ETHERPAD_VERSION}".tar.gz && \ + mkdir /opt/etherpad-lite && \ + tar xf /opt/etherpad-lite.tar.gz \ + --directory /opt/etherpad-lite \ + --strip-components=1 && \ + rm /opt/etherpad-lite.tar.gz + +# install node dependencies for Etherpad +RUN /opt/etherpad-lite/bin/installDeps.sh + +# Copy the custom configuration file, if present. The configuration file has to +# be manually put inside the same directory containing the Dockerfile (we cannot +# directly point to "../settings.json" for Docker's security restrictions). +# +# For the conditional COPY trick, see: +# https://stackoverflow.com/questions/31528384/conditional-copy-add-in-dockerfile#46801962 +COPY nop setting[s].json /opt/etherpad-lite/ + +EXPOSE 9001 +CMD ["supervisord", "-c", "/etc/supervisor/supervisor.conf", "-n"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..2c5ad028c --- /dev/null +++ b/docker/README.md @@ -0,0 +1,49 @@ +# Docker image + +This directory contains the files that are used to build the official Docker image on https://hub.docker.com/r/etherpad/etherpad. + +# Rebuilding with custom settings +In order to use a personalized settings file, **you will have to rebuild your image**. + +All of these instructions are as a member of the `docker` group. + +Prepare your custom `settings.json` file: +```bash +cd /docker +cp ../settings.json.template settings.json +[ further edit your settings.json as needed] +``` + +Build the version you prefer: +```bash +# builds latest development version +docker build --tag /etherpad . + +# builds latest stable version +docker build --build-arg ETHERPAD_VERSION=master --tag /etherpad . + +# builds a specific version +docker build --build-arg ETHERPAD_VERSION=1.7.5 --tag /etherpad . + +# builds a specific git hash +docker build --build-arg ETHERPAD_VERSION=4c45ac3cb1ae --tag /etherpad . +``` + +# Downloading from Docker Hub +If you are ok downloading a [prebuilt image from Docker Hub](https://hub.docker.com/r/etherpad/etherpad), these are the commands: +```bash +# gets the latest published version +docker pull etherpad/etherpad + +# gets a specific version +docker pull etherpad/etherpad:1.7.5 +``` + +# Running your instance: + +To run your instance: +```bash +docker run --detach --publish :9001 /etherpad +``` + +And point your browser to `http://:` diff --git a/docker/nop b/docker/nop new file mode 100644 index 000000000..725bfd457 --- /dev/null +++ b/docker/nop @@ -0,0 +1,4 @@ +THIS IS A DUMMY FILE + +It is used to trick Docker in doing a conditional COPY +see: https://stackoverflow.com/questions/31528384/conditional-copy-add-in-dockerfile#46801962 diff --git a/docker/supervisor.conf b/docker/supervisor.conf new file mode 100644 index 000000000..017143b74 --- /dev/null +++ b/docker/supervisor.conf @@ -0,0 +1,20 @@ +[supervisord] +nodaemon=true + +[unix_http_server] +file=/var/run//supervisor.sock +chmod=0700 + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///var/run//supervisor.sock + +[program:etherpad] +directory=/opt/etherpad-lite +command=node node_modules/ep_etherpad-lite/node/server.js +user=root +autostart=true +autorestart=true + From ee4a27d10ec7ac552442ea7529ef2fda8e779a0d Mon Sep 17 00:00:00 2001 From: muxator Date: Sun, 10 Mar 2019 02:11:45 +0100 Subject: [PATCH 089/183] settings.json.template: minor rewording of a comment --- settings.json.template | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/settings.json.template b/settings.json.template index a89cc4247..336d0ebc8 100644 --- a/settings.json.template +++ b/settings.json.template @@ -3,8 +3,8 @@ * * Please edit settings.json, not settings.json.template * - * Please note that since Etherpad 1.6.0 you can store DB credentials in a - * separate file (credentials.json). + * Please note that starting from Etherpad 1.6.0 you can store DB credentials in + * a separate file (credentials.json). */ { /* From d526c5cccaf2eec1e071f66cc59da228da33b5d4 Mon Sep 17 00:00:00 2001 From: muxator Date: Sun, 10 Mar 2019 01:46:55 +0100 Subject: [PATCH 090/183] Settings.js: trivial reformatting --- src/node/utils/Settings.js | 87 +++++++++++++++----------------------- 1 file changed, 35 insertions(+), 52 deletions(-) diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 508e6148d..91f4c6780 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -287,29 +287,26 @@ exports.scrollWhenFocusLineIsOutOfViewport = { //checks if abiword is avaiable exports.abiwordAvailable = function() { - if(exports.abiword != null) - { + if (exports.abiword != null) { return os.type().indexOf("Windows") != -1 ? "withoutPDF" : "yes"; - } - else - { + } else { return "no"; } }; -exports.sofficeAvailable = function () { - if(exports.soffice != null) { +exports.sofficeAvailable = function() { + if (exports.soffice != null) { return os.type().indexOf("Windows") != -1 ? "withoutPDF": "yes"; } else { return "no"; } }; -exports.exportAvailable = function () { +exports.exportAvailable = function() { var abiword = exports.abiwordAvailable(); var soffice = exports.sofficeAvailable(); - if(abiword == "no" && soffice == "no") { + if (abiword == "no" && soffice == "no") { return "no"; } else if ((abiword == "withoutPDF" && soffice == "no") || (abiword == "no" && soffice == "withoutPDF")) { return "withoutPDF"; @@ -321,8 +318,7 @@ exports.exportAvailable = function () { // Provide git version if available exports.getGitCommit = function() { var version = ""; - try - { + try { var rootPath = path.resolve(npm.dir, '..'); if (fs.lstatSync(rootPath + '/.git').isFile()) { rootPath = fs.readFileSync(rootPath + '/.git', "utf8"); @@ -334,9 +330,7 @@ exports.getGitCommit = function() { var refPath = rootPath + "/" + ref.substring(5, ref.indexOf("\n")); version = fs.readFileSync(refPath, "utf-8"); version = version.substring(0, 7); - } - catch(e) - { + } catch(e) { console.warn("Can't get git version for server header\n" + e.message) } return version; @@ -350,24 +344,24 @@ exports.getEpVersion = function() { exports.reloadSettings = function reloadSettings() { // Discover where the settings file lives var settingsFilename = absolutePaths.makeAbsolute(argv.settings || "settings.json"); - + // Discover if a credential file exists var credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || "credentials.json"); var settingsStr, credentialsStr; - try{ + try { //read the settings sync settingsStr = fs.readFileSync(settingsFilename).toString(); console.info(`Settings loaded from: ${settingsFilename}`); - } catch(e){ + } catch(e) { console.warn(`No settings file found in ${settingsFilename}. Continuing using defaults!`); } - try{ + try { //read the credentials sync credentialsStr = fs.readFileSync(credentialsFilename).toString(); console.info(`Credentials file read from: ${credentialsFilename}`); - } catch(e){ + } catch(e) { // Doesn't matter if no credentials file found.. console.info(`No credentials file found in ${credentialsFilename}. Ignoring.`); } @@ -376,68 +370,58 @@ exports.reloadSettings = function reloadSettings() { var settings; var credentials; try { - if(settingsStr) { + if (settingsStr) { settingsStr = jsonminify(settingsStr).replace(",]","]").replace(",}","}"); settings = JSON.parse(settingsStr); } - }catch(e){ + } catch(e) { console.error(`There was an error processing your settings file from ${settingsFilename}:` + e.message); process.exit(1); } - if(credentialsStr) { + if (credentialsStr) { credentialsStr = jsonminify(credentialsStr).replace(",]","]").replace(",}","}"); credentials = JSON.parse(credentialsStr); } //loop trough the settings - for(var i in settings) - { + for (var i in settings) { //test if the setting start with a lowercase character - if(i.charAt(0).search("[a-z]") !== 0) - { + if (i.charAt(0).search("[a-z]") !== 0) { console.warn(`Settings should start with a lowercase character: '${i}'`); } //we know this setting, so we overwrite it //or it's a settings hash, specific to a plugin - if(exports[i] !== undefined || i.indexOf('ep_')==0) - { + if (exports[i] !== undefined || i.indexOf('ep_') == 0) { if (_.isObject(settings[i]) && !_.isArray(settings[i])) { exports[i] = _.defaults(settings[i], exports[i]); } else { exports[i] = settings[i]; } - } - //this setting is unkown, output a warning and throw it away - else - { + } else { + // this setting is unknown, output a warning and throw it away console.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); } } //loop trough the settings - for(var i in credentials) - { + for (var i in credentials) { //test if the setting start with a lowercase character - if(i.charAt(0).search("[a-z]") !== 0) - { + if (i.charAt(0).search("[a-z]") !== 0) { console.warn(`Settings should start with a lowercase character: '${i}'`); } //we know this setting, so we overwrite it //or it's a settings hash, specific to a plugin - if(exports[i] !== undefined || i.indexOf('ep_')==0) - { + if (exports[i] !== undefined || i.indexOf('ep_') == 0) { if (_.isObject(credentials[i]) && !_.isArray(credentials[i])) { exports[i] = _.defaults(credentials[i], exports[i]); } else { exports[i] = credentials[i]; } - } - //this setting is unkown, output a warning and throw it away - else - { + } else { + // this setting is unknown, output a warning and throw it away console.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); } } @@ -483,14 +467,13 @@ exports.reloadSettings = function reloadSettings() { console.info(`Using skin "${exports.skinName}" in dir: ${skinPath}`); } - if(exports.abiword){ + if (exports.abiword) { // Check abiword actually exists - if(exports.abiword != null) - { + if (exports.abiword != null) { fs.exists(exports.abiword, function(exists) { if (!exists) { var abiwordError = "Abiword does not exist at this path, check your settings file"; - if(!exports.suppressErrorsInPadText){ + if (!exports.suppressErrorsInPadText) { exports.defaultPadText = exports.defaultPadText + "\nError: " + abiwordError + suppressDisableMsg; } console.error(abiwordError); @@ -500,12 +483,12 @@ exports.reloadSettings = function reloadSettings() { } } - if(exports.soffice) { - fs.exists(exports.soffice, function (exists) { - if(!exists) { + if (exports.soffice) { + fs.exists(exports.soffice, function(exists) { + if (!exists) { var sofficeError = "SOffice does not exist at this path, check your settings file"; - if(!exports.suppressErrorsInPadText) { + if (!exports.suppressErrorsInPadText) { exports.defaultPadText = exports.defaultPadText + "\nError: " + sofficeError + suppressDisableMsg; } console.error(sofficeError); @@ -528,9 +511,9 @@ exports.reloadSettings = function reloadSettings() { console.warn("Declaring the sessionKey in the settings.json is deprecated. This value is auto-generated now. Please remove the setting from the file."); } - if(exports.dbType === "dirty"){ + if (exports.dbType === "dirty") { var dirtyWarning = "DirtyDB is used. This is fine for testing but not recommended for production."; - if(!exports.suppressErrorsInPadText){ + if (!exports.suppressErrorsInPadText) { exports.defaultPadText = exports.defaultPadText + "\nWarning: " + dirtyWarning + suppressDisableMsg; } From 8fa52659f5431b7d0ba4eb7de3ae05b0c73a473d Mon Sep 17 00:00:00 2001 From: muxator Date: Sat, 9 Mar 2019 08:59:39 +0100 Subject: [PATCH 091/183] Settings.js: trivial rewording of abiword and soffice (libreoffice) error messages --- src/node/utils/Settings.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 91f4c6780..206413c84 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -472,11 +472,11 @@ exports.reloadSettings = function reloadSettings() { if (exports.abiword != null) { fs.exists(exports.abiword, function(exists) { if (!exists) { - var abiwordError = "Abiword does not exist at this path, check your settings file"; + var abiwordError = "Abiword does not exist at this path, check your settings file."; if (!exports.suppressErrorsInPadText) { exports.defaultPadText = exports.defaultPadText + "\nError: " + abiwordError + suppressDisableMsg; } - console.error(abiwordError); + console.error(abiwordError + ` File location: ${exports.abiword}`); exports.abiword = null; } }); @@ -486,12 +486,12 @@ exports.reloadSettings = function reloadSettings() { if (exports.soffice) { fs.exists(exports.soffice, function(exists) { if (!exists) { - var sofficeError = "SOffice does not exist at this path, check your settings file"; + var sofficeError = "soffice (libreoffice) does not exist at this path, check your settings file."; if (!exports.suppressErrorsInPadText) { exports.defaultPadText = exports.defaultPadText + "\nError: " + sofficeError + suppressDisableMsg; } - console.error(sofficeError); + console.error(sofficeError + ` File location: ${exports.soffice}`); exports.soffice = null; } }); From ab57edef331ca3d88e48f60070f254a77d8074b6 Mon Sep 17 00:00:00 2001 From: muxator Date: Sun, 10 Mar 2019 00:26:36 +0100 Subject: [PATCH 092/183] Settings.js: exit gracefully if an invalid credentials.json is passed. Before this commit, when passed a malformed credentials.json the application crashed with a stack dump. Now we catch the error and fail in a controlled way (like already done for settings.json). Example of exception we no longer throw: MALFORMEDJSON ^ SyntaxError: Unexpected token M in JSON at position 0 at JSON.parse () at Object.reloadSettings (/src/node/utils/Settings.js:390:24) at Object. (/src/node/utils/Settings.js:543:9) at Module._compile (module.js:635:30) at Object.Module._extensions..js (module.js:646:10) at Module.load (module.js:554:32) at tryModuleLoad (module.js:497:12) at Function.Module._load (module.js:489:3) at Module.require (module.js:579:17) at require (internal/module.js:11:18) --- src/node/utils/Settings.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 206413c84..98756c27f 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -379,9 +379,14 @@ exports.reloadSettings = function reloadSettings() { process.exit(1); } - if (credentialsStr) { - credentialsStr = jsonminify(credentialsStr).replace(",]","]").replace(",}","}"); - credentials = JSON.parse(credentialsStr); + try { + if (credentialsStr) { + credentialsStr = jsonminify(credentialsStr).replace(",]","]").replace(",}","}"); + credentials = JSON.parse(credentialsStr); + } + } catch(e) { + console.error(`There was an error processing your credentials file from ${credentialsFilename}:` + e.message); + process.exit(1); } //loop trough the settings From 63537682561af3f82248bc899a9d022c80436dbb Mon Sep 17 00:00:00 2001 From: muxator Date: Sat, 9 Mar 2019 10:06:51 +0100 Subject: [PATCH 093/183] Settings.js: factored out storeSettings() Grouped copied & pasted code into a single function. --- src/node/utils/Settings.js | 74 ++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 43 deletions(-) diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 98756c27f..4311bf3da 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -341,6 +341,35 @@ exports.getEpVersion = function() { return require('ep_etherpad-lite/package.json').version; } +/** + * Receives a settingsObj and, if the property name is a valid configuration + * item, stores it in the module's exported properties via a side effect. + * + * This code refactors a previous version that copied & pasted the same code for + * both "settings.json" and "credentials.json". + */ +function storeSettings(settingsObj) { + for (var i in 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}'`); + } + + // we know this setting, so we overwrite it + // or it's a settings hash, specific to a plugin + if (exports[i] !== undefined || i.indexOf('ep_') == 0) { + if (_.isObject(settingsObj[i]) && !_.isArray(settingsObj[i])) { + exports[i] = _.defaults(settingsObj[i], exports[i]); + } else { + exports[i] = settingsObj[i]; + } + } else { + // this setting is unknown, output a warning and throw it away + console.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); + } + } +} + exports.reloadSettings = function reloadSettings() { // Discover where the settings file lives var settingsFilename = absolutePaths.makeAbsolute(argv.settings || "settings.json"); @@ -389,47 +418,8 @@ exports.reloadSettings = function reloadSettings() { process.exit(1); } - //loop trough the settings - for (var i in settings) { - //test if the setting start with a lowercase character - if (i.charAt(0).search("[a-z]") !== 0) { - console.warn(`Settings should start with a lowercase character: '${i}'`); - } - - //we know this setting, so we overwrite it - //or it's a settings hash, specific to a plugin - if (exports[i] !== undefined || i.indexOf('ep_') == 0) { - if (_.isObject(settings[i]) && !_.isArray(settings[i])) { - exports[i] = _.defaults(settings[i], exports[i]); - } else { - exports[i] = settings[i]; - } - } else { - // this setting is unknown, output a warning and throw it away - console.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); - } - } - - //loop trough the settings - for (var i in credentials) { - //test if the setting start with a lowercase character - if (i.charAt(0).search("[a-z]") !== 0) { - console.warn(`Settings should start with a lowercase character: '${i}'`); - } - - //we know this setting, so we overwrite it - //or it's a settings hash, specific to a plugin - if (exports[i] !== undefined || i.indexOf('ep_') == 0) { - if (_.isObject(credentials[i]) && !_.isArray(credentials[i])) { - exports[i] = _.defaults(credentials[i], exports[i]); - } else { - exports[i] = credentials[i]; - } - } else { - // this setting is unknown, output a warning and throw it away - console.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); - } - } + storeSettings(settings); + storeSettings(credentials); log4js.configure(exports.logconfig);//Configure the logging appenders log4js.setGlobalLogLevel(exports.loglevel);//set loglevel @@ -529,5 +519,3 @@ exports.reloadSettings = function reloadSettings() { // initially load settings exports.reloadSettings(); - - From f96e139b17a7f78c9771de57fd2734f92bcd9732 Mon Sep 17 00:00:00 2001 From: muxator Date: Sun, 10 Mar 2019 00:36:53 +0100 Subject: [PATCH 094/183] Settings.js: factored out parseSettings() No functional changes. --- src/node/utils/Settings.js | 88 ++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 4311bf3da..412eb7cd8 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -370,6 +370,53 @@ function storeSettings(settingsObj) { } } +/** + * - reads the JSON configuration file settingsFilename from disk + * - strips the comments + * - returns a parsed Javascript object + * + * The isSettings variable only controls the error logging. + */ +function parseSettings(settingsFilename, isSettings) { + let settingsStr = ""; + + let settingsType, notFoundMessage, notFoundFunction; + + if (isSettings) { + settingsType = "settings"; + notFoundMessage = "Continuing using defaults!"; + notFoundFunction = console.warn; + } else { + settingsType = "credentials"; + notFoundMessage = "Ignoring."; + notFoundFunction = console.info; + } + + try { + //read the settings file + settingsStr = fs.readFileSync(settingsFilename).toString(); + } catch(e) { + notFoundFunction(`No ${settingsType} file found in ${settingsFilename}. ${notFoundMessage}`); + + // or maybe undefined! + return null; + } + + try { + settingsStr = jsonminify(settingsStr).replace(",]","]").replace(",}","}"); + + const settings = JSON.parse(settingsStr); + + console.info(`${settingsType} loaded from: ${settingsFilename}`); + + return settings; + } catch(e) { + console.error(`There was an error processing your ${settingsType} file from ${settingsFilename}: ${e.message}`); + + process.exit(1); + } +} + exports.reloadSettings = function reloadSettings() { // Discover where the settings file lives var settingsFilename = absolutePaths.makeAbsolute(argv.settings || "settings.json"); @@ -377,46 +424,11 @@ exports.reloadSettings = function reloadSettings() { // Discover if a credential file exists var credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || "credentials.json"); - var settingsStr, credentialsStr; - try { - //read the settings sync - settingsStr = fs.readFileSync(settingsFilename).toString(); - console.info(`Settings loaded from: ${settingsFilename}`); - } catch(e) { - console.warn(`No settings file found in ${settingsFilename}. Continuing using defaults!`); - } - - try { - //read the credentials sync - credentialsStr = fs.readFileSync(credentialsFilename).toString(); - console.info(`Credentials file read from: ${credentialsFilename}`); - } catch(e) { - // Doesn't matter if no credentials file found.. - console.info(`No credentials file found in ${credentialsFilename}. Ignoring.`); - } - // try to parse the settings - var settings; - var credentials; - try { - if (settingsStr) { - settingsStr = jsonminify(settingsStr).replace(",]","]").replace(",}","}"); - settings = JSON.parse(settingsStr); - } - } catch(e) { - console.error(`There was an error processing your settings file from ${settingsFilename}:` + e.message); - process.exit(1); - } + var settings = parseSettings(settingsFilename, true); - try { - if (credentialsStr) { - credentialsStr = jsonminify(credentialsStr).replace(",]","]").replace(",}","}"); - credentials = JSON.parse(credentialsStr); - } - } catch(e) { - console.error(`There was an error processing your credentials file from ${credentialsFilename}:` + e.message); - process.exit(1); - } + // try to parse the credentials + var credentials = parseSettings(credentialsFilename, false); storeSettings(settings); storeSettings(credentials); From 6d400050a337551e38072c135bbb86800dba92be Mon Sep 17 00:00:00 2001 From: muxator Date: Sat, 9 Mar 2019 23:01:21 +0100 Subject: [PATCH 095/183] Settings.js: support configuration via environment variables. All the configuration values can be read from environment variables using the syntax "${ENV_VAR_NAME}". This is useful, for example, when running in a Docker container. EXAMPLE: "port": "${PORT}" "minify": "${MINIFY}" "skinName": "${SKIN_NAME}" Would read the configuration values for those items from the environment variables PORT, MINIFY and SKIN_NAME. REMARKS: Please note that a variable substitution always needs to be quoted. "port": 9001, <-- Literal values. When not using substitution, "minify": false only strings must be quoted: booleans and "skin": "colibris" numbers must not. "port": ${PORT} <-- ERROR: this is not valid json "minify": ${MINIFY} "skin": ${SKIN_NAME} "port": "${PORT}" <-- CORRECT: if you want to use a variable "minify": "${MINIFY}" substitution, put quotes around its name, "skin": "${SKIN_NAME}" even if the required value is a number or a boolean. Etherpad will take care of rewriting it to the proper type if necessary. Resolves #3543 --- docker/README.md | 2 + settings.json.template | 33 +++++++++++ src/node/utils/Settings.js | 113 ++++++++++++++++++++++++++++++++++++- 3 files changed, 147 insertions(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 2c5ad028c..87d6e81ea 100644 --- a/docker/README.md +++ b/docker/README.md @@ -14,6 +14,8 @@ cp ../settings.json.template settings.json [ further edit your settings.json as needed] ``` +**Each configuration parameter can also be set via an environment variable**, using the syntax `"${ENV_VAR_NAME}"`. For details, refer to `settings.json.template`. + Build the version you prefer: ```bash # builds latest development version diff --git a/settings.json.template b/settings.json.template index 336d0ebc8..50a095bac 100644 --- a/settings.json.template +++ b/settings.json.template @@ -5,6 +5,39 @@ * * Please note that starting from Etherpad 1.6.0 you can store DB credentials in * a separate file (credentials.json). + * + * + * ENVIRONMENT VARIABLE SUBSTITUTION + * ================================= + * + * All the configuration values can be read from environment variables using the + * syntax "${ENV_VAR_NAME}". + * This is useful, for example, when running in a Docker container. + * + * EXAMPLE: + * "port": "${PORT}" + * "minify": "${MINIFY}" + * "skinName": "${SKIN_NAME}" + * + * Would read the configuration values for those items from the environment + * variables PORT, MINIFY and SKIN_NAME. + * + * REMARKS: + * Please note that a variable substitution always needs to be quoted. + * "port": 9001, <-- Literal values. When not using substitution, + * "minify": false only strings must be quoted: booleans and + * "skin": "colibris" numbers must not. + * + * "port": ${PORT} <-- ERROR: this is not valid json + * "minify": ${MINIFY} + * "skin": ${SKIN_NAME} + * + * "port": "${PORT}" <-- CORRECT: if you want to use a variable + * "minify": "${MINIFY}" substitution, put quotes around its name, + * "skin": "${SKIN_NAME}" even if the required value is a number or a + * boolean. + * Etherpad will take care of rewriting it to + * the proper type if necessary. */ { /* diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 412eb7cd8..d013d4222 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -370,9 +370,118 @@ function storeSettings(settingsObj) { } } +/** + * Takes a javascript object containing Etherpad's configuration, and returns + * another object, in which all the string properties whose name is of the form + * "${ENV_VAR}", got their value replaced with the value of the given + * environment variable. + * + * An environment variable's value is always a string. However, the code base + * makes use of the various json types. To maintain compatiblity, some + * heuristics is applied: + * + * - if ENV_VAR does not exist in the environment, null is returned; + * - if ENV_VAR's value is "true" or "false", it is converted to the js boolean + * values true or false; + * - if ENV_VAR's value looks like a number, it is converted to a js number + * (details in the code). + * + * Variable substitution is performed doing a round trip conversion to/from + * json, using a custom replacer parameter in JSON.stringify(), and parsing the + * JSON back again. This ensures that environment variable replacement is + * performed even on nested objects. + * + * see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter + */ +function lookupEnvironmentVariables(obj) { + const stringifiedAndReplaced = JSON.stringify(obj, (key, value) => { + /* + * the first invocation of replacer() is with an empty key. Just go on, or + * we would zap the entire object. + */ + if (key === '') { + return value; + } + + /* + * If we received from the configuration file a number, a boolean or + * something that is not a string, we can be sure that it was a literal + * value. No need to perform any variable substitution. + * + * The environment variable expansion syntax "${ENV_VAR}" is just a string + * of specific form, after all. + */ + if (typeof value !== 'string') { + return value; + } + + /* + * Let's check if the string value looks like a variable expansion (e.g.: + * "${ENV_VAR}") + */ + const match = value.match(/^\$\{(.*)\}$/); + + if (match === null) { + // no match: use the value literally, without any substitution + + return value; + } + + // we found the name of an environment variable. Let's read its value. + const envVarName = match[1]; + const envVarValue = process.env[envVarName]; + + if (envVarValue === undefined) { + console.warn(`Configuration key ${key} tried to read its value from environment variable ${envVarName}, but no value was found. Returning null. Please check your configuration and environment settings.`); + + /* + * We have to return null, because if we just returned undefined, the + * configuration item "key" would be stripped from the returned object. + */ + return null; + } + + // envVarName contained some value. + + /* + * For numeric and boolean strings let's convert it to proper types before + * returning it, in order to maintain backward compatibility. + */ + + // cooked from https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number + const isNumeric = !isNaN(envVarValue) && !isNaN(parseFloat(envVarValue) && isFinite(envVarValue)); + + if (isNumeric) { + console.debug(`Configuration key "${key}" will be read from environment variable ${envVarName}. Detected numeric string, that will be coerced to a number`); + + return +envVarValue; + } + + // the boolean literal case is easy. + if (envVarValue === "true" || envVarValue === "false") { + console.debug(`Configuration key "${key}" will be read from environment variable ${envVarName}. Detected boolean string, that will be coerced to a boolean`); + + return (envVarValue === "true"); + } + + /* + * The only remaining case is that envVarValue is a string with no special + * meaning, and we just return it as-is. + */ + console.debug(`Configuration key "${key}" will be read from environment variable ${envVarName}`); + + return envVarValue; + }); + + const newSettings = JSON.parse(stringifiedAndReplaced); + + return newSettings; +} + /** * - reads the JSON configuration file settingsFilename from disk * - strips the comments + * - replaces environment variables calling lookupEnvironmentVariables() * - returns a parsed Javascript object * * The isSettings variable only controls the error logging. @@ -409,7 +518,9 @@ function parseSettings(settingsFilename, isSettings) { console.info(`${settingsType} loaded from: ${settingsFilename}`); - return settings; + const replacedSettings = lookupEnvironmentVariables(settings); + + return replacedSettings; } catch(e) { console.error(`There was an error processing your ${settingsType} file from ${settingsFilename}: ${e.message}`); From 43c4fa9c2ed43586277aa7adf0e474b1b88f11bb Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 12 Mar 2019 18:10:24 +0100 Subject: [PATCH 096/183] Await padManager.getPad in getPadLines --- src/node/handler/PadMessageHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 18b08af5b..a4c672431 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -1363,7 +1363,7 @@ async function getChangesetInfo(padId, startNum, endNum, granularity) */ async function getPadLines(padId, revNum) { - let pad = padManager.getPad(padId); + let pad = await padManager.getPad(padId); // get the atext let atext; From e4db905f3c022c8028a72353de080f32a120ddc2 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 18 Mar 2019 08:46:50 +0100 Subject: [PATCH 097/183] Localisation updates from https://translatewiki.net. --- src/locales/lrc.json | 8 +++++--- src/locales/tr.json | 5 ++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/locales/lrc.json b/src/locales/lrc.json index 910f8f1ef..8f04f4837 100644 --- a/src/locales/lrc.json +++ b/src/locales/lrc.json @@ -1,19 +1,21 @@ { "@metadata": { "authors": [ - "Mogoeilor" + "Mogoeilor", + "Lorestani" ] }, "index.newPad": "دشته تازه", "pad.toolbar.bold.title": "توپر", "pad.toolbar.italic.title": "کج کوله(ctrl-l)", "pad.toolbar.underline.title": "زیر خط دار بین (Ctrl-U)", - "pad.toolbar.ol.title": "نوم گه منظم", - "pad.toolbar.ul.title": "نوم گه بی نظم", + "pad.toolbar.ol.title": "نومگٱ مورٱتٱب بیٱ", + "pad.toolbar.ul.title": "نومگٱ مورٱتٱب ناٛیٱ", "pad.toolbar.indent.title": "مئن رئته(TAB)", "pad.toolbar.unindent.title": "وه در رئته (Shift+TAB)", "pad.toolbar.undo.title": "رد انجوم دئین (Ctrl-Z)", "pad.toolbar.redo.title": "د نو انجوم دئین(Ctrl-Y)", + "pad.toolbar.savedRevision.title": "ڤانری بٱلگٱ", "pad.toolbar.settings.title": "میزوکاری", "pad.colorpicker.save": "ذخيره كردن", "pad.colorpicker.cancel": "انجوم شیو كردن", diff --git a/src/locales/tr.json b/src/locales/tr.json index 0be2588f1..4826d5d2e 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -7,7 +7,8 @@ "Meelo", "Trockya", "McAang", - "Vito Genovese" + "Vito Genovese", + "Hedda" ] }, "index.newPad": "Yeni Bloknot", @@ -92,6 +93,8 @@ "pad.chat": "Sohbet", "pad.chat.title": "Bu bloknot için sohbeti açın.", "pad.chat.loadmessages": "Daha fazla mesaj yükle", + "pad.chat.stick.title": "Sohbeti ekrana yapıştır", + "pad.chat.writeMessage.placeholder": "Mesajını buraya yaz", "timeslider.pageTitle": "{{appTitle}} Zaman Çizelgesi", "timeslider.toolbar.returnbutton": "Bloknota geri dön", "timeslider.toolbar.authors": "Yazarlar:", From 346d8232794381805857af184945af87ab947b53 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 21 Mar 2019 10:57:28 +0100 Subject: [PATCH 098/183] Localisation updates from https://translatewiki.net. --- src/locales/lrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/lrc.json b/src/locales/lrc.json index 8f04f4837..98428cb6d 100644 --- a/src/locales/lrc.json +++ b/src/locales/lrc.json @@ -29,7 +29,7 @@ "pad.settings.fontType.normal": "عادی", "pad.settings.fontType.monospaced": "تک جاگه", "pad.settings.globalView": "دیئن جهونی", - "pad.settings.language": "زون:", + "pad.settings.language": "زڤون:", "pad.importExport.import_export": "وامین آوردن/د در دئن", "pad.importExport.importSuccessful": "موفق بی!", "pad.importExport.export": "دشته تازه چی وه در بیه:", From 21ac37170e55dcbf3fa060d7635bf7775bb07b2e Mon Sep 17 00:00:00 2001 From: muxator Date: Thu, 21 Mar 2019 22:18:59 +0100 Subject: [PATCH 099/183] doc: rephrase settings.json.template and Settings.js Better document current behaviour. In this revision, ENV_VAR are supported, default values are not. --- settings.json.template | 30 ++++++++++++++++-------------- src/node/utils/Settings.js | 29 ++++++++++++++++++++--------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/settings.json.template b/settings.json.template index 50a095bac..74f5872dd 100644 --- a/settings.json.template +++ b/settings.json.template @@ -11,7 +11,7 @@ * ================================= * * All the configuration values can be read from environment variables using the - * syntax "${ENV_VAR_NAME}". + * syntax "${ENV_VAR}". * This is useful, for example, when running in a Docker container. * * EXAMPLE: @@ -23,21 +23,23 @@ * variables PORT, MINIFY and SKIN_NAME. * * REMARKS: - * Please note that a variable substitution always needs to be quoted. - * "port": 9001, <-- Literal values. When not using substitution, - * "minify": false only strings must be quoted: booleans and - * "skin": "colibris" numbers must not. + * Please note that variable substitution always needs to be quoted. * - * "port": ${PORT} <-- ERROR: this is not valid json - * "minify": ${MINIFY} - * "skin": ${SKIN_NAME} + * "port": 9001, <-- Literal values. When not using + * "minify": false substitution, only strings must be quoted. + * "skinName": "colibris" Booleans and numbers must not. + * + * "port": "${PORT}" <-- CORRECT: if you want to use a variable + * "minify": "${MINIFY}" substitution, put quotes around its name, + * "skinName": "${SKIN_NAME}" even if the required value is a number or a + * boolean. + * Etherpad will take care of rewriting it to + * the proper type if necessary. + * + * "port": ${PORT} <-- ERROR: this is not valid json. Quotes + * "minify": ${MINIFY} around variable names are missing. + * "skinName": ${SKIN_NAME} * - * "port": "${PORT}" <-- CORRECT: if you want to use a variable - * "minify": "${MINIFY}" substitution, put quotes around its name, - * "skin": "${SKIN_NAME}" even if the required value is a number or a - * boolean. - * Etherpad will take care of rewriting it to - * the proper type if necessary. */ { /* diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index d013d4222..c3b9d21e4 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -372,13 +372,13 @@ function storeSettings(settingsObj) { /** * Takes a javascript object containing Etherpad's configuration, and returns - * another object, in which all the string properties whose name is of the form - * "${ENV_VAR}", got their value replaced with the value of the given + * another object, in which all the string properties whose value is of the form + * "${ENV_VAR}" got their value replaced with the contents of the given * environment variable. * - * An environment variable's value is always a string. However, the code base - * makes use of the various json types. To maintain compatiblity, some - * heuristics is applied: + * By definition, an environment variable's value is always a string. However, + * the code base makes use of the various json types. To maintain compatiblity, + * some heuristics is applied: * * - if ENV_VAR does not exist in the environment, null is returned; * - if ENV_VAR's value is "true" or "false", it is converted to the js boolean @@ -386,10 +386,21 @@ function storeSettings(settingsObj) { * - if ENV_VAR's value looks like a number, it is converted to a js number * (details in the code). * - * Variable substitution is performed doing a round trip conversion to/from - * json, using a custom replacer parameter in JSON.stringify(), and parsing the - * JSON back again. This ensures that environment variable replacement is - * performed even on nested objects. + * The following is a scheme of the behaviour of this function: + * + * +---------------------------+---------------+------------------+ + * | Configuration string in | Value of | Resulting confi- | + * | settings.json | ENV_VAR | guration value | + * |---------------------------|---------------|------------------| + * | "${ENV_VAR}" | "some_string" | "some_string" | + * | "${ENV_VAR}" | "9001" | 9001 | + * | "${ENV_VAR}" | undefined | null | + * +---------------------------+---------------+------------------+ + * + * IMPLEMENTATION NOTE: variable substitution is performed doing a round trip + * conversion to/from json, using a custom replacer parameter in + * JSON.stringify(), and parsing the JSON back again. This ensures that + * environment variable replacement is performed even on nested objects. * * see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter */ From 59b1eed4a81eff23fc890cdf16c3ccfc19a3a809 Mon Sep 17 00:00:00 2001 From: muxator Date: Thu, 21 Mar 2019 21:30:21 +0100 Subject: [PATCH 100/183] Settings.js: rephrased a log message --- src/node/utils/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index c3b9d21e4..b74091854 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -443,7 +443,7 @@ function lookupEnvironmentVariables(obj) { const envVarValue = process.env[envVarName]; if (envVarValue === undefined) { - console.warn(`Configuration key ${key} tried to read its value from environment variable ${envVarName}, but no value was found. Returning null. Please check your configuration and environment settings.`); + console.warn(`Environment variable "${envVarName}" does not contain any value for configuration key "${key}". Returning null. Please check your configuration and environment settings.`); /* * We have to return null, because if we just returned undefined, the From c3bca6506ef64ebc653f49ab217b52e6e871540b Mon Sep 17 00:00:00 2001 From: muxator Date: Thu, 21 Mar 2019 21:32:39 +0100 Subject: [PATCH 101/183] Settings.js: extracted into coerceValue() the logic for string -> number|bool conversion This will be user in a later commit for implementing support for default values --- src/node/utils/Settings.js | 52 +++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index b74091854..4ba07f3fb 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -370,6 +370,33 @@ function storeSettings(settingsObj) { } } +/* + * If stringValue is a numeric string, or its value is "true" or "false", coerce + * them to appropriate JS types. Otherwise return stringValue as-is. + */ +function coerceValue(stringValue) { + // cooked from https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number + const isNumeric = !isNaN(stringValue) && !isNaN(parseFloat(stringValue) && isFinite(stringValue)); + + if (isNumeric) { + // detected numeric string. Coerce to a number + + return +stringValue; + } + + // the boolean literal case is easy. + if (stringValue === "true" ) { + return true; + } + + if (stringValue === "false") { + return false; + } + + // otherwise, return this value as-is + return stringValue; +} + /** * Takes a javascript object containing Etherpad's configuration, and returns * another object, in which all the string properties whose value is of the form @@ -458,30 +485,9 @@ function lookupEnvironmentVariables(obj) { * For numeric and boolean strings let's convert it to proper types before * returning it, in order to maintain backward compatibility. */ + console.debug(`Configuration key "${key}" will be read from environment variable "${envVarName}"`); - // cooked from https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number - const isNumeric = !isNaN(envVarValue) && !isNaN(parseFloat(envVarValue) && isFinite(envVarValue)); - - if (isNumeric) { - console.debug(`Configuration key "${key}" will be read from environment variable ${envVarName}. Detected numeric string, that will be coerced to a number`); - - return +envVarValue; - } - - // the boolean literal case is easy. - if (envVarValue === "true" || envVarValue === "false") { - console.debug(`Configuration key "${key}" will be read from environment variable ${envVarName}. Detected boolean string, that will be coerced to a boolean`); - - return (envVarValue === "true"); - } - - /* - * The only remaining case is that envVarValue is a string with no special - * meaning, and we just return it as-is. - */ - console.debug(`Configuration key "${key}" will be read from environment variable ${envVarName}`); - - return envVarValue; + return coerceValue(envVarValue); }); const newSettings = JSON.parse(stringifiedAndReplaced); From 2955740a6e1009a165b844cd4b06ab4561145f5f Mon Sep 17 00:00:00 2001 From: muxator Date: Thu, 21 Mar 2019 01:37:19 +0100 Subject: [PATCH 102/183] Settings.js: support syntax for default values +---------------------------+---------------+------------------+ | Configuration string in | Value of | Resulting confi- | | settings.json | ENV_VAR | guration value | |---------------------------|---------------|------------------| | "${ENV_VAR}" | "some_string" | "some_string" | | "${ENV_VAR}" | "9001" | 9001 | | "${ENV_VAR}" | undefined | null | | "${ENV_VAR:some_default}" | "some_string" | "some_string" | | "${ENV_VAR:some_default}" | undefined | "some_default" | +---------------------------+---------------+------------------+ Mention this briefly in the main README.md, also. Closes #3578. --- README.md | 1 + docker/README.md | 2 +- settings.json.template | 33 +++++++++++++++++++-------------- src/node/utils/Settings.js | 27 ++++++++++++++++++++------- 4 files changed, 41 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index c7ca0d4d6..3b8ff6184 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ If cloning to a subdirectory within another project, you may need to do the foll You can modify the settings in `settings.json`. If you need to handle multiple settings files, you can pass the path to a settings file to `bin/run.sh` using the `-s|--settings` option: this allows you to run multiple Etherpad instances from the same installation. Similarly, `--credentials` can be used to give a settings override file, `--apikey` to give a different APIKEY.txt file and `--sessionkey` to give a non-default SESSIONKEY.txt. +**Each configuration parameter can also be set via an environment variable**, using the syntax `"${ENV_VAR}"` or `"${ENV_VAR:default_value}"`. For details, refer to `settings.json.template`. Once you have access to your /admin section settings can be modified through the web browser. You should use a dedicated database such as "mysql", if you are planning on using etherpad-in a production environment, since the "dirtyDB" database driver is only for testing and/or development purposes. diff --git a/docker/README.md b/docker/README.md index 87d6e81ea..ece7b7951 100644 --- a/docker/README.md +++ b/docker/README.md @@ -14,7 +14,7 @@ cp ../settings.json.template settings.json [ further edit your settings.json as needed] ``` -**Each configuration parameter can also be set via an environment variable**, using the syntax `"${ENV_VAR_NAME}"`. For details, refer to `settings.json.template`. +**Each configuration parameter can also be set via an environment variable**, using the syntax `"${ENV_VAR}"` or `"${ENV_VAR:default_value}"`. For details, refer to `settings.json.template`. Build the version you prefer: ```bash diff --git a/settings.json.template b/settings.json.template index 74f5872dd..535e502bf 100644 --- a/settings.json.template +++ b/settings.json.template @@ -11,33 +11,38 @@ * ================================= * * All the configuration values can be read from environment variables using the - * syntax "${ENV_VAR}". + * syntax "${ENV_VAR}" or "${ENV_VAR:default_value}". + * * This is useful, for example, when running in a Docker container. * * EXAMPLE: - * "port": "${PORT}" + * "port": "${PORT:9001}" * "minify": "${MINIFY}" - * "skinName": "${SKIN_NAME}" + * "skinName": "${SKIN_NAME:colibris}" * * Would read the configuration values for those items from the environment * variables PORT, MINIFY and SKIN_NAME. + * If PORT and SKIN_NAME variables were not defined, the default values 9001 and + * "colibris" would be used. The configuration value "minify", on the other + * hand, does not have a default indicated. Thus, if the environment variable + * MINIFY were undefined, "minify" would be null (do not do this). * * REMARKS: * Please note that variable substitution always needs to be quoted. * - * "port": 9001, <-- Literal values. When not using - * "minify": false substitution, only strings must be quoted. - * "skinName": "colibris" Booleans and numbers must not. + * "port": 9001, <-- Literal values. When not using + * "minify": false substitution, only strings must be + * "skinName": "colibris" quoted. Booleans and numbers must not. * - * "port": "${PORT}" <-- CORRECT: if you want to use a variable - * "minify": "${MINIFY}" substitution, put quotes around its name, - * "skinName": "${SKIN_NAME}" even if the required value is a number or a - * boolean. - * Etherpad will take care of rewriting it to - * the proper type if necessary. + * "port": "${PORT:9001}" <-- CORRECT: if you want to use a variable + * "minify": "${MINIFY:true}" substitution, put quotes around its name, + * "skinName": "${SKIN_NAME}" even if the required value is a number or + * a boolean. + * Etherpad will take care of rewriting it + * to the proper type if necessary. * - * "port": ${PORT} <-- ERROR: this is not valid json. Quotes - * "minify": ${MINIFY} around variable names are missing. + * "port": ${PORT:9001} <-- ERROR: this is not valid json. Quotes + * "minify": ${MINIFY} around variable names are missing. * "skinName": ${SKIN_NAME} * */ diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 4ba07f3fb..8de69fcf7 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -400,8 +400,8 @@ function coerceValue(stringValue) { /** * Takes a javascript object containing Etherpad's configuration, and returns * another object, in which all the string properties whose value is of the form - * "${ENV_VAR}" got their value replaced with the contents of the given - * environment variable. + * "${ENV_VAR}" or "${ENV_VAR:default_value}" got their value replaced with the + * contents of the given environment variable, or with a default value. * * By definition, an environment variable's value is always a string. However, * the code base makes use of the various json types. To maintain compatiblity, @@ -422,6 +422,8 @@ function coerceValue(stringValue) { * | "${ENV_VAR}" | "some_string" | "some_string" | * | "${ENV_VAR}" | "9001" | 9001 | * | "${ENV_VAR}" | undefined | null | + * | "${ENV_VAR:some_default}" | "some_string" | "some_string" | + * | "${ENV_VAR:some_default}" | undefined | "some_default" | * +---------------------------+---------------+------------------+ * * IMPLEMENTATION NOTE: variable substitution is performed doing a round trip @@ -455,9 +457,10 @@ function lookupEnvironmentVariables(obj) { /* * Let's check if the string value looks like a variable expansion (e.g.: - * "${ENV_VAR}") + * "${ENV_VAR}" or "${ENV_VAR:default_value}") */ - const match = value.match(/^\$\{(.*)\}$/); + // MUXATOR 2019-03-21: we could use named capture groups here once we migrate to nodejs v10 + const match = value.match(/^\$\{([^:]*)(:(.*))?\}$/); if (match === null) { // no match: use the value literally, without any substitution @@ -465,12 +468,16 @@ function lookupEnvironmentVariables(obj) { return value; } - // we found the name of an environment variable. Let's read its value. + /* + * We found the name of an environment variable. Let's read its actual value + * and its default value, if given + */ const envVarName = match[1]; const envVarValue = process.env[envVarName]; + const defaultValue = match[3]; - if (envVarValue === undefined) { - console.warn(`Environment variable "${envVarName}" does not contain any value for configuration key "${key}". Returning null. Please check your configuration and environment settings.`); + 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. Please check your configuration and environment settings.`); /* * We have to return null, because if we just returned undefined, the @@ -479,6 +486,12 @@ function lookupEnvironmentVariables(obj) { return null; } + if ((envVarValue === undefined) && (defaultValue !== undefined)) { + console.debug(`Environment variable "${envVarName}" not found for configuration key "${key}". Falling back to default value.`); + + return coerceValue(defaultValue); + } + // envVarName contained some value. /* From 7433d749f00cd389b6e4f8f1f9b1aa0c0a82dd23 Mon Sep 17 00:00:00 2001 From: Eddie Barraco Date: Wed, 20 Mar 2019 21:34:38 +0100 Subject: [PATCH 103/183] docker: remove supervisord from the Docker image Supervision/management should not be done from inside the container, but externally, by container managers. The container now simply runs node on server.js. The logs are now readable from docker logs . --- docker/Dockerfile | 11 ++--------- docker/supervisor.conf | 20 -------------------- 2 files changed, 2 insertions(+), 29 deletions(-) delete mode 100644 docker/supervisor.conf diff --git a/docker/Dockerfile b/docker/Dockerfile index e705fb42a..26c2d5738 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -9,14 +9,6 @@ FROM node:latest LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite" -# install supervisor -RUN apt-get update && apt-get install -y \ - supervisor \ - && rm -rf /var/lib/apt/lists/* - -# install supervisor.conf in a low layer of the container -ADD supervisor.conf /etc/supervisor/supervisor.conf - # git hash of the version to be built. # If not given, build the latest development version. ARG ETHERPAD_VERSION=develop @@ -49,4 +41,5 @@ RUN /opt/etherpad-lite/bin/installDeps.sh COPY nop setting[s].json /opt/etherpad-lite/ EXPOSE 9001 -CMD ["supervisord", "-c", "/etc/supervisor/supervisor.conf", "-n"] +WORKDIR /opt/etherpad-lite +CMD ["node", "node_modules/ep_etherpad-lite/node/server.js"] diff --git a/docker/supervisor.conf b/docker/supervisor.conf deleted file mode 100644 index 017143b74..000000000 --- a/docker/supervisor.conf +++ /dev/null @@ -1,20 +0,0 @@ -[supervisord] -nodaemon=true - -[unix_http_server] -file=/var/run//supervisor.sock -chmod=0700 - -[rpcinterface:supervisor] -supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface - -[supervisorctl] -serverurl=unix:///var/run//supervisor.sock - -[program:etherpad] -directory=/opt/etherpad-lite -command=node node_modules/ep_etherpad-lite/node/server.js -user=root -autostart=true -autorestart=true - From 7a5470c7bd6a9e9e30bb0af6eea15028790034b2 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 25 Mar 2019 18:58:35 +0100 Subject: [PATCH 104/183] Localisation updates from https://translatewiki.net. --- src/locales/fa.json | 5 ++++- src/locales/ru.json | 11 ++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/locales/fa.json b/src/locales/fa.json index 4b29669e4..099d67594 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -7,7 +7,8 @@ "Reza1615", "ZxxZxxZ", "الناز", - "Omid.koli" + "Omid.koli", + "FarsiNevis" ] }, "index.newPad": "دفترچه یادداشت تازه", @@ -92,6 +93,8 @@ "pad.chat": "گفتگو", "pad.chat.title": "بازکردن گفتگو برای این دفترچه یادداشت", "pad.chat.loadmessages": "بارگیری پیام‌های بیشتر", + "pad.chat.stick.title": "چسباندن چت به صفحه", + "pad.chat.writeMessage.placeholder": "پیام خود را این‌جا بنویسید", "timeslider.pageTitle": "لغزندهٔ زمان {{appTitle}}", "timeslider.toolbar.returnbutton": "بازگشت به دفترچه یادداشت", "timeslider.toolbar.authors": "نویسندگان:", diff --git a/src/locales/ru.json b/src/locales/ru.json index 6b7681ffd..c7b425cbb 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -9,7 +9,8 @@ "Nzeemin", "Facenapalm", "Patrick Star", - "Movses" + "Movses", + "Diralik" ] }, "index.newPad": "Создать", @@ -82,8 +83,8 @@ "pad.modals.badChangeset.cause": "Это может быть из-за неправильной конфигурации сервера или некоторых других неожиданных действий. Пожалуйста, свяжитесь с администратором службы, если вы считаете, что это ошибка. Попробуйте переподключиться для того, чтобы продолжить редактирование.", "pad.modals.corruptPad.explanation": "Документ, к которому вы пытаетесь получить доступ, повреждён.", "pad.modals.corruptPad.cause": "Это может быть из-за неправильной конфигурации сервера или некоторых других неожиданных действий. Пожалуйста, свяжитесь с администратором службы.", - "pad.modals.deleted": "Удален.", - "pad.modals.deleted.explanation": "Этот документ был удален.", + "pad.modals.deleted": "Удалён.", + "pad.modals.deleted.explanation": "Этот документ был удалён.", "pad.modals.disconnected": "Соединение разорвано.", "pad.modals.disconnected.explanation": "Подключение к серверу потеряно", "pad.modals.disconnected.cause": "Сервер, возможно, недоступен. Пожалуйста, сообщите администратору службы, если проблема будет повторятся.", @@ -93,7 +94,7 @@ "pad.share.emebdcode": "Вставить URL", "pad.chat": "Чат", "pad.chat.title": "Открыть чат для этого документа.", - "pad.chat.loadmessages": "Еще сообщения", + "pad.chat.loadmessages": "Ещё сообщения", "pad.chat.stick.title": "Закрепить чат на экране", "pad.chat.writeMessage.placeholder": "Напишите своё сообщение сюда", "timeslider.pageTitle": "Временная шкала {{appTitle}}", @@ -137,5 +138,5 @@ "pad.impexp.uploadFailed": "Загрузка не удалась, пожалуйста, попробуйте ещё раз", "pad.impexp.importfailed": "Ошибка при импорте", "pad.impexp.copypaste": "Пожалуйста, скопируйте", - "pad.impexp.exportdisabled": "Экспорт в формате {{type}} отключен. Для подробной информации обратитесь к системному администратору." + "pad.impexp.exportdisabled": "Экспорт в формате {{type}} отключён. Для подробной информации обратитесь к системному администратору." } From b8df6ca60c717fd6275ea990c4ebafdff0d2540f Mon Sep 17 00:00:00 2001 From: muxator Date: Wed, 27 Mar 2019 00:05:54 +0100 Subject: [PATCH 105/183] handler/PadMessageHandler.js: shuffle around some comments No functional changes --- 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 a4c672431..199c9528b 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -257,9 +257,9 @@ exports.handleMessage = async function(client, message) // check permissions - // client tried to auth for the first time (first msg from the client) if (message.type == "CLIENT_READY") { - createSessionInfo(client, message); + // client tried to auth for the first time (first msg from the client) + createSessionInfo(client, message); } // Note: message.sessionID is an entirely different kind of @@ -285,15 +285,15 @@ exports.handleMessage = async function(client, message) // check if pad is requested via readOnly let padId = auth.padID; - // Pad is readOnly, first get the real Pad ID if (padId.indexOf("r.") === 0) { + // Pad is readOnly, first get the real Pad ID padId = await readOnlyManager.getPadId(padID); } let { accessStatus } = await securityManager.checkAccess(padId, auth.sessionID, auth.token, auth.password); - // no access, send the client a message that tells him why if (accessStatus !== "grant") { + // no access, send the client a message that tells him why client.json.send({ accessStatus }); return; } From 53b3328b5fee54aba3e133392cbee9f070c63b10 Mon Sep 17 00:00:00 2001 From: muxator Date: Wed, 27 Mar 2019 00:09:13 +0100 Subject: [PATCH 106/183] express/padreadonly.js: missing "let" Found by the Typescript compiler when doing an experimental conversion. --- src/node/hooks/express/padreadonly.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/hooks/express/padreadonly.js b/src/node/hooks/express/padreadonly.js index f699e27e9..5264c17cd 100644 --- a/src/node/hooks/express/padreadonly.js +++ b/src/node/hooks/express/padreadonly.js @@ -18,7 +18,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { if (await hasPadAccess(req, res)) { // render the html document - html = await exporthtml.getPadHTMLDocument(padId, null); + let html = await exporthtml.getPadHTMLDocument(padId, null); res.send(html); } }); From 40408134470bdfca3175147b64b7ad4348dd9c9a Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 26 Mar 2019 23:44:04 +0100 Subject: [PATCH 107/183] db/Pad.js: prototype.copy(), removed redundant callback argument This would cause a crash when calling pad.remove(). Found by the Typescript compiler when doing an experimental conversion. --- src/node/db/Pad.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index 6c97fee8d..8aa742850 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -399,7 +399,7 @@ Pad.prototype.copy = async function copy(destinationID, force) { // exists and forcing let pad = await padManager.getPad(destinationID); - await pad.remove(callback); + await pad.remove(); } // copy the 'pad' entry From b9e537ca4f3c614a2fb8afaf4aca6387de02d3b0 Mon Sep 17 00:00:00 2001 From: muxator Date: Wed, 27 Mar 2019 00:11:36 +0100 Subject: [PATCH 108/183] db/Pad.js: removed unreachable return statement Found by the Typescript compiler when doing an experimental conversion. --- src/node/db/Pad.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index 8aa742850..0692215d7 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -393,8 +393,6 @@ Pad.prototype.copy = async function copy(destinationID, force) { if (!force) { console.error("erroring out without force"); throw new customError("destinationID already exists", "apierror"); - - return; } // exists and forcing From aa5e302d99098fed67d8876a5491a10462d7fd6a Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 26 Mar 2019 23:42:35 +0100 Subject: [PATCH 109/183] db/API.js: missing "let" Found by the Typescript compiler when doing an experimental conversion. --- src/node/db/API.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/db/API.js b/src/node/db/API.js index f0b44d92e..8d4d74398 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -260,7 +260,7 @@ exports.getHTML = async function(padID, rev) } // get the html of this revision - html = await exportHtml.getPadHTML(pad, rev); + let html = await exportHtml.getPadHTML(pad, rev); // wrap the HTML html = "" + html + ""; From b2d00ae0713107db134f0bf6671c8a559b973fc1 Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 26 Mar 2019 23:44:51 +0100 Subject: [PATCH 110/183] db/API.js: customeError -> customError Found by the Typescript compiler when doing an experimental conversion. --- src/node/db/API.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/db/API.js b/src/node/db/API.js index 8d4d74398..998d9acd1 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -525,7 +525,7 @@ exports.restoreRevision = async function(padID, rev) { // check if rev is a number if (rev === undefined) { - throw new customeError("rev is not defined", "apierror"); + throw new customError("rev is not defined", "apierror"); } rev = checkValidRev(rev); From 5d067406b14fa008c7f7cdf0c2c6f23dc685ba45 Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 26 Mar 2019 23:47:52 +0100 Subject: [PATCH 111/183] utils/Minify.js: removed unused parameter "redirectCount" in requestURI() Found by the Typescript compiler when doing an experimental conversion. --- 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 6f3b75186..11bfaa82c 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -70,7 +70,7 @@ for (var key in tar) { // 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. -function requestURI(url, method, headers, callback, redirectCount) { +function requestURI(url, method, headers, callback) { var parsedURL = urlutil.parse(url); var status = 500, headers = {}, content = []; From cdd49789735935e4b8db40e0005b4eb0a6b52f95 Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 26 Mar 2019 23:51:04 +0100 Subject: [PATCH 112/183] utils/Minify.js: removed unused parameter "next" in minify() Found by the Typescript compiler when doing an experimental conversion. --- 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 11bfaa82c..b958ab7cc 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -137,7 +137,7 @@ function requestURIs(locations, method, headers, callback) { * @param req the Express request * @param res the Express response */ -function minify(req, res, next) +function minify(req, res) { var filename = req.params['filename']; From c2d8ca212b8be8e57e9a234426720e1d66167d79 Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 26 Mar 2019 23:58:14 +0100 Subject: [PATCH 113/183] utils/Minify.js: always call statFile() with an explicit value for "dirStatLimit" In this way the only external call to statFile() provides an explicit value for "dirStatLimit", and thus the initial check on "undefined" at the start of the function could be removed (just added a comment for now). --- src/node/utils/Minify.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index b958ab7cc..d70c835c6 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -240,7 +240,7 @@ function minify(req, res) res.end(); } } - }); + }, 3); } // find all includes in ace.js and embed them. @@ -287,6 +287,10 @@ function getAceFile(callback) { // Check for the existance of the file and get the last modification date. function statFile(filename, callback, dirStatLimit) { + /* + * The only external call to this function provides an explicit value for + * dirStatLimit: this check could be removed. + */ if (typeof dirStatLimit === 'undefined') { dirStatLimit = 3; } From cbd393d56b16a30e475b3c01e2b351494c193eee Mon Sep 17 00:00:00 2001 From: muxator Date: Wed, 27 Mar 2019 00:08:11 +0100 Subject: [PATCH 114/183] handler/PadMessageHandler.js: handleMessage() got the wrong padId for read only pads This was almost guaranteed to be broken. Found by the Typescript compiler when doing an experimental conversion. --- src/node/handler/PadMessageHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 199c9528b..ffbb74ea8 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -287,7 +287,7 @@ exports.handleMessage = async function(client, message) if (padId.indexOf("r.") === 0) { // Pad is readOnly, first get the real Pad ID - padId = await readOnlyManager.getPadId(padID); + padId = await readOnlyManager.getPadId(padId); } let { accessStatus } = await securityManager.checkAccess(padId, auth.sessionID, auth.token, auth.password); From c1321f5b14fa4db07b722eef266518361541752e Mon Sep 17 00:00:00 2001 From: Adrien le Maire Date: Fri, 29 Mar 2019 10:28:56 +0100 Subject: [PATCH 115/183] remove devdeps from prod builds --- docker/Dockerfile | 4 ++++ docker/README.md | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 26c2d5738..eb4340094 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -13,6 +13,10 @@ LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite" # If not given, build the latest development version. ARG ETHERPAD_VERSION=develop +# Set the following to production to avoid installing devDeps +# this can be one with build args (and is mandatory to build ARM version +ARG NODE_ENV=development + # grab the ETHERPAD_VERSION tarball from github (no need to clone the whole # repository) RUN echo "Getting version: ${ETHERPAD_VERSION}" && \ diff --git a/docker/README.md b/docker/README.md index ece7b7951..c8f3de1da 100644 --- a/docker/README.md +++ b/docker/README.md @@ -22,10 +22,10 @@ Build the version you prefer: docker build --tag /etherpad . # builds latest stable version -docker build --build-arg ETHERPAD_VERSION=master --tag /etherpad . +docker build --build-arg ETHERPAD_VERSION=master --build-arg NODE_ENV=production --tag /etherpad . # builds a specific version -docker build --build-arg ETHERPAD_VERSION=1.7.5 --tag /etherpad . +docker build --build-arg ETHERPAD_VERSION=1.7.5 --build-arg NODE_ENV=production --tag /etherpad . # builds a specific git hash docker build --build-arg ETHERPAD_VERSION=4c45ac3cb1ae --tag /etherpad . From aca1640fdfb4d8f35db1699e468de79351289946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20Leli=C3=A8vre?= Date: Sat, 30 Mar 2019 01:11:29 +0100 Subject: [PATCH 116/183] Fix typos in docker/Dockerfile Fix "one" -> "done", and add missing closing parenthesis. --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index eb4340094..0bf6a7f28 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -14,7 +14,7 @@ LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite" ARG ETHERPAD_VERSION=develop # Set the following to production to avoid installing devDeps -# this can be one with build args (and is mandatory to build ARM version +# this can be done with build args (and is mandatory to build ARM version) ARG NODE_ENV=development # grab the ETHERPAD_VERSION tarball from github (no need to clone the whole From dc338c4e483ad4b3d27d0afac0a12cf46d9e7455 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 1 Apr 2019 20:26:39 +0200 Subject: [PATCH 117/183] Localisation updates from https://translatewiki.net. --- src/locales/hu.json | 2 ++ src/locales/it.json | 1 + src/locales/lb.json | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/locales/hu.json b/src/locales/hu.json index af273f695..fb00c6df2 100644 --- a/src/locales/hu.json +++ b/src/locales/hu.json @@ -93,6 +93,8 @@ "pad.chat": "Csevegés", "pad.chat.title": "A noteszhez tartozó csevegés megnyitása.", "pad.chat.loadmessages": "További üzenetek betöltése", + "pad.chat.stick.title": "Csevegés a képernyőre", + "pad.chat.writeMessage.placeholder": "Írja az üzenetét ide", "timeslider.pageTitle": "{{appTitle}} időcsúszka", "timeslider.toolbar.returnbutton": "Vissza a noteszhez", "timeslider.toolbar.authors": "Szerzők:", diff --git a/src/locales/it.json b/src/locales/it.json index fbf5706bc..30881889d 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -91,6 +91,7 @@ "pad.chat": "Chat", "pad.chat.title": "Apri la chat per questo Pad.", "pad.chat.loadmessages": "Carica altri messaggi", + "pad.chat.stick.title": "Ancora chat nello schermo", "pad.chat.writeMessage.placeholder": "Scrivi il tuo messaggio qui", "timeslider.pageTitle": "Cronologia {{appTitle}}", "timeslider.toolbar.returnbutton": "Ritorna al Pad", diff --git a/src/locales/lb.json b/src/locales/lb.json index 99fc69789..c81ee018d 100644 --- a/src/locales/lb.json +++ b/src/locales/lb.json @@ -12,8 +12,8 @@ "pad.toolbar.italic.title": "Schréi (Ctrl+I)", "pad.toolbar.underline.title": "Ënnerstrach (Ctrl+U)", "pad.toolbar.strikethrough.title": "Duerchgestrach (Ctrl+5)", - "pad.toolbar.ol.title": "Numeréiert Lëscht (Ctrl+Shift+N)", - "pad.toolbar.ul.title": "Net-numeréiert Lëscht (Ctrl+Shift+L)", + "pad.toolbar.ol.title": "Nummeréiert Lëscht (Ctrl+Shift+N)", + "pad.toolbar.ul.title": "Net-nummeréiert Lëscht (Ctrl+Shift+L)", "pad.toolbar.indent.title": "Aréckelen (TAB)", "pad.toolbar.unindent.title": "Erausréckelen (Shift+TAB)", "pad.toolbar.undo.title": "Réckgängeg (Ctrl-Z)", From ae3ecf54d521880907bff1aa1d7604d98a14a99f Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 4 Apr 2019 19:59:52 +0200 Subject: [PATCH 118/183] Localisation updates from https://translatewiki.net. --- src/locales/is.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/locales/is.json b/src/locales/is.json index 65226445d..87f52ac0d 100644 --- a/src/locales/is.json +++ b/src/locales/is.json @@ -87,6 +87,8 @@ "pad.chat": "Spjall", "pad.chat.title": "Opna spjallið fyrir þessa skrifblokk.", "pad.chat.loadmessages": "Hlaða inn fleiri skeytum", + "pad.chat.stick.title": "Festa spjallið á skjáinn", + "pad.chat.writeMessage.placeholder": "Skrifaðu skilaboðin þín hér", "timeslider.pageTitle": "Tímalína {{appTitle}}", "timeslider.toolbar.returnbutton": "Fara til baka í skrifblokk", "timeslider.toolbar.authors": "Höfundar:", From e3cc21e4775b1ae7a87da30a2bc42859316e06e7 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 8 Apr 2019 16:43:29 +0200 Subject: [PATCH 119/183] Localisation updates from https://translatewiki.net. --- src/locales/lrc.json | 120 +++++++++++++++++++++---------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/src/locales/lrc.json b/src/locales/lrc.json index 98428cb6d..654297810 100644 --- a/src/locales/lrc.json +++ b/src/locales/lrc.json @@ -5,76 +5,76 @@ "Lorestani" ] }, - "index.newPad": "دشته تازه", - "pad.toolbar.bold.title": "توپر", - "pad.toolbar.italic.title": "کج کوله(ctrl-l)", - "pad.toolbar.underline.title": "زیر خط دار بین (Ctrl-U)", + "index.newPad": "دٱفتٱرچٱ تازٱ", + "pad.toolbar.bold.title": "تۊپور", + "pad.toolbar.italic.title": "هٱلٛ هار(ctrl-l)", + "pad.toolbar.underline.title": "زؽر خٱت (Ctrl-U)", "pad.toolbar.ol.title": "نومگٱ مورٱتٱب بیٱ", "pad.toolbar.ul.title": "نومگٱ مورٱتٱب ناٛیٱ", - "pad.toolbar.indent.title": "مئن رئته(TAB)", - "pad.toolbar.unindent.title": "وه در رئته (Shift+TAB)", - "pad.toolbar.undo.title": "رد انجوم دئین (Ctrl-Z)", - "pad.toolbar.redo.title": "د نو انجوم دئین(Ctrl-Y)", + "pad.toolbar.indent.title": "قوپساٛیی(TAB)", + "pad.toolbar.unindent.title": "ڤ دٱر رٱتاٛیی (Shift+TAB)", + "pad.toolbar.undo.title": "رٱد ٱنجوم داٛئن (Ctrl-Z)", + "pad.toolbar.redo.title": "د نۊ ٱنجوم داٛئن(Ctrl-Y)", "pad.toolbar.savedRevision.title": "ڤانری بٱلگٱ", "pad.toolbar.settings.title": "میزوکاری", - "pad.colorpicker.save": "ذخيره كردن", - "pad.colorpicker.cancel": "انجوم شیو كردن", - "pad.loading": "د حالت سوار كرد", - "pad.wrongPassword": "پاسوردتو اشتوائه", - "pad.settings.padSettings": "میزوکاری دشته", - "pad.settings.myView": "نظرگه مه", - "pad.settings.stickychat": "همیشه د بلگه چک چنه بکید", - "pad.settings.linenocheck": "شماره خطیا", + "pad.colorpicker.save": "زٱخیرٱ كردن", + "pad.colorpicker.cancel": "ٱنجوم شؽڤ كردن", + "pad.loading": "د هالٱت سڤار كرد...", + "pad.wrongPassword": "پٱسڤردتو اْشتبائٱ", + "pad.settings.padSettings": "میزوکاری دٱفتٱرچٱ", + "pad.settings.myView": "نٱزٱرگٱ ماْ", + "pad.settings.stickychat": "همیشٱ د بٱلگٱ چٱک چنٱ بٱکؽت", + "pad.settings.linenocheck": "شمارٱ خٱتؽا", "pad.settings.fontType": "نوع فونت:", "pad.settings.fontType.normal": "عادی", "pad.settings.fontType.monospaced": "تک جاگه", "pad.settings.globalView": "دیئن جهونی", "pad.settings.language": "زڤون:", - "pad.importExport.import_export": "وامین آوردن/د در دئن", - "pad.importExport.importSuccessful": "موفق بی!", - "pad.importExport.export": "دشته تازه چی وه در بیه:", - "pad.importExport.exporthtml": "اچ تی ام ال", - "pad.importExport.exportplain": "نیسسه ساده", - "pad.importExport.exportword": "واجه پالایشتگر مایکروسافت", - "pad.importExport.exportpdf": "پی دی اف", - "pad.importExport.exportopen": "او دی اف(قالو سند وا بیه)", - "pad.modals.connected": "وصل بیه", - "pad.modals.forcereconnect": "سی وصل بین مژبور کو", - "pad.modals.userdup": "د نیمدری هنی واز بیه", - "pad.modals.initsocketfail": "سرور د دسرسی نئ.", - "pad.modals.deleted": "پاک بیه", - "pad.modals.deleted.explanation": "ای دشته جا وه جا بیه", - "pad.modals.disconnected": "ارتواطتو قطع بیه", - "pad.share": "ای دشته نه بهر کو", - "pad.share.readonly": "فقط بحون", - "pad.share.link": "هوم پیوند", - "pad.chat": "گپ زئن", - "pad.chat.title": "گپ چنه نه سی دشته وا کو.", - "pad.chat.loadmessages": "پیغومیا بیشتر نه سوار کو", - "timeslider.toolbar.returnbutton": "ورگرد د دشته", - "timeslider.toolbar.authors": "نیسنه یا:", - "timeslider.toolbar.authorsList": "بی نیسنه", - "timeslider.toolbar.exportlink.title": "وه در ديئن", - "timeslider.version": "نسقه{{نسقه}}", - "timeslider.month.january": "جانويه", - "timeslider.month.february": "فوريه", + "pad.importExport.import_export": "ڤامین آوئردن/ڤ دٱر داٛئن", + "pad.importExport.importSuccessful": "موئٱفٱق بی!", + "pad.importExport.export": "دٱفتٱرچٱ تازٱ چی ڤ دٱر بیٱ:", + "pad.importExport.exporthtml": "اْچ تی اْم اْل", + "pad.importExport.exportplain": "نیسسٱ سادٱ", + "pad.importExport.exportword": "ڤاژٱ پالایشگٱر مایکروسافت", + "pad.importExport.exportpdf": "پی دی اْف", + "pad.importExport.exportopen": "او دی اْف(قالب سٱنٱد ڤاز)", + "pad.modals.connected": "ڤٱسل بیٱ", + "pad.modals.forcereconnect": "سی ڤٱسل بیئن دوئارٱ مٱجبۊر کو", + "pad.modals.userdup": "د نیمدری هنی ڤاز بیٱ", + "pad.modals.initsocketfail": "سرور د دٱسرسی نؽ.", + "pad.modals.deleted": "پاک بیٱ", + "pad.modals.deleted.explanation": "اؽ دٱفتٱرچٱ جا ڤ جا بیٱ", + "pad.modals.disconnected": "اْرتبات تو قٱت بیٱ.", + "pad.share": "اؽ دٱفتٱرچٱ ناْ بٱئر کو", + "pad.share.readonly": "فقٱت ڤٱننی", + "pad.share.link": "هوم پاٛڤٱن", + "pad.chat": "سالفٱ", + "pad.chat.title": "سالفٱ ناْ سی دٱفتٱرچٱ ڤاز کو.", + "pad.chat.loadmessages": "پاٛغومؽا ؽشتر ناْ سڤار کو", + "timeslider.toolbar.returnbutton": "ڤرگٱشتن ڤ دٱفتٱرچٱ", + "timeslider.toolbar.authors": "نیسٱنٱ یا:", + "timeslider.toolbar.authorsList": "بؽ نیسٱنٱ", + "timeslider.toolbar.exportlink.title": "ڤ دٱر داٛئن", + "timeslider.version": "نۏسخٱ{{نۏسخٱ}}", + "timeslider.month.january": "ژانڤیٱ", + "timeslider.month.february": "فڤریٱ", "timeslider.month.march": "مارس", - "timeslider.month.april": "آوريل", - "timeslider.month.may": "ما", - "timeslider.month.june": "جوئن", - "timeslider.month.july": "جولای", - "timeslider.month.august": "اگوست", + "timeslider.month.april": "آڤريل", + "timeslider.month.may": "ماٛی", + "timeslider.month.june": "ژوئٱن", + "timeslider.month.july": "جۊلای", + "timeslider.month.august": "آگوست", "timeslider.month.september": "سپتامر", - "timeslider.month.october": "اكتور", - "timeslider.month.november": "نوامر", + "timeslider.month.october": "اوكتوبر", + "timeslider.month.november": "نوڤامر", "timeslider.month.december": "دسامر", - "pad.userlist.entername": "نوم تونه وارد بکید", - "pad.userlist.unnamed": "نوم نهشته", - "pad.userlist.guest": "میزوان", - "pad.userlist.deny": "پرو کردن", - "pad.userlist.approve": "اصلا کردن", - "pad.impexp.importbutton": "ایسه وارد کو", - "pad.impexp.importing": "د حالت وارد کردن", - "pad.impexp.importfailed": "وامین آوردن شکست حرد", - "pad.impexp.copypaste": "خواهشن وردار بدیسن" + "pad.userlist.entername": "نوم توناْ ڤارد بٱکؽت", + "pad.userlist.unnamed": "بؽ نوم", + "pad.userlist.guest": "ماٛموݩ", + "pad.userlist.deny": "رٱد کردن", + "pad.userlist.approve": "قبۊل کردن", + "pad.impexp.importbutton": "ایساْ ڤارد کو", + "pad.impexp.importing": "د هالٱت ڤارد کردن...", + "pad.impexp.importfailed": "ڤامؽن آوئردن شکٱست هٱرد", + "pad.impexp.copypaste": "خاهشٱن ڤردار بٱدیسن" } From 1cb9c3e1ce14f9c63c7ea0c538356256727faba9 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 15 Apr 2019 17:36:10 +0200 Subject: [PATCH 120/183] Localisation updates from https://translatewiki.net. --- src/locales/ar.json | 30 +++++----- src/locales/mk.json | 2 +- src/locales/sh.json | 132 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 16 deletions(-) create mode 100644 src/locales/sh.json diff --git a/src/locales/ar.json b/src/locales/ar.json index 9c7a4d9c5..b332508b2 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -15,16 +15,16 @@ }, "index.newPad": "باد جديد", "index.createOpenPad": "أو صنع/فتح باد بوضع اسمه:", - "pad.toolbar.bold.title": "سميك (Ctrl-B)", - "pad.toolbar.italic.title": "مائل (Ctrl-I)", - "pad.toolbar.underline.title": "تسطير (Ctrl-U)", + "pad.toolbar.bold.title": "سميك (Ctrl+B)", + "pad.toolbar.italic.title": "مائل (Ctrl+I)", + "pad.toolbar.underline.title": "تسطير (Ctrl+U)", "pad.toolbar.strikethrough.title": "شطب (Ctrl+5)", "pad.toolbar.ol.title": "قائمة مرتبة (Ctrl+Shift+N)", "pad.toolbar.ul.title": "قائمة غير مرتبة (Ctrl+Shift+L)", - "pad.toolbar.indent.title": "إزاحة", - "pad.toolbar.unindent.title": "حذف الإزاحة", - "pad.toolbar.undo.title": "فك (Ctrl-Z)", - "pad.toolbar.redo.title": "تكرار (Ctrl-Y)", + "pad.toolbar.indent.title": "إزاحة (TAB)", + "pad.toolbar.unindent.title": "حذف الإزاحة (Shift+TAB)", + "pad.toolbar.undo.title": "فك (Ctrl+Z)", + "pad.toolbar.redo.title": "تكرار (Ctrl+Y)", "pad.toolbar.clearAuthorship.title": "مسح ألوان التأليف (Ctrl+Shift+C)", "pad.toolbar.import_export.title": "استيراد/تصدير من/إلى تنسيقات ملفات مختلفة", "pad.toolbar.timeslider.title": "متصفح التاريخ", @@ -32,7 +32,7 @@ "pad.toolbar.settings.title": "الإعدادات", "pad.toolbar.embed.title": "تبادل و تضمين هذا الباد", "pad.toolbar.showusers.title": "عرض المستخدمين على هذا الباد", - "pad.colorpicker.save": "تسجيل", + "pad.colorpicker.save": "حفظ", "pad.colorpicker.cancel": "إلغاء", "pad.loading": "جارٍ التحميل...", "pad.noCookie": "الكوكيز غير متاحة. الرجاء السماح بتحميل الكوكيز على متصفحك!", @@ -63,9 +63,9 @@ "pad.importExport.exportopen": "ODF (نسق المستند المفتوح)", "pad.importExport.abiword.innerHTML": "لا يمكنك الاستيراد إلا من نص عادي أو من تنسيقات HTML. للحصول على المزيد من ميزات الاستيراد المتقدمة، يرجى تثبيت AbiWord.", "pad.modals.connected": "متصل.", - "pad.modals.reconnecting": "إعادة الاتصال ببادك", + "pad.modals.reconnecting": "إعادة الاتصال ببادك..", "pad.modals.forcereconnect": "فرض إعادة الاتصال", - "pad.modals.reconnecttimer": "حاول إعادة الاتصال", + "pad.modals.reconnecttimer": "جاري محاولة إعادة الاتصال", "pad.modals.cancel": "إلغاء", "pad.modals.userdup": "مفتوح في نافذة أخرى", "pad.modals.userdup.explanation": "يبدو أن هذا الباد تم فتحه في أكثر من نافذة متصفح في هذا الحاسوب.", @@ -74,9 +74,9 @@ "pad.modals.unauth.explanation": "لقد تغيرت الأذونات الخاصة بك أثناء عرض هذه الصفحة. أعد محاولة الاتصال.", "pad.modals.looping.explanation": "هناك مشاكل في الاتصال مع ملقم التزامن.", "pad.modals.looping.cause": "ربما كنت متصلاً من خلال وكيل أو جدار حماية غير متوافق.", - "pad.modals.initsocketfail": "لا يمكن الوصول إلى الخادم", + "pad.modals.initsocketfail": "لا يمكن الوصول إلى الخادم.", "pad.modals.initsocketfail.explanation": "تعذر الاتصال بخادم المزامنة.", - "pad.modals.initsocketfail.cause": "هذا على الأرجح بسبب مشكلة في المستعرض الخاص بك أو الاتصال بإنترنت.", + "pad.modals.initsocketfail.cause": "هذا على الأرجح بسبب مشكلة في المستعرض الخاص بك أو الاتصال بالإنترنت.", "pad.modals.slowcommit.explanation": "الخادم لا يستجيب.", "pad.modals.slowcommit.cause": "يمكن أن يكون هذا بسبب مشاكل في الاتصال بالشبكة.", "pad.modals.badChangeset.explanation": "لقد صُنفَت إحدى عمليات التحرير التي قمت بها كعملية غير مسموح بها من قبل ملقم التزامن.", @@ -84,16 +84,16 @@ "pad.modals.corruptPad.explanation": "الباد الذي تحاول الوصول إليه تالف.", "pad.modals.corruptPad.cause": "قد يكون هذا بسبب تكوين ملقم خاطئ أو بسبب سلوك آخر غير متوقع. يرجى الاتصال بمسؤول الخدمة.", "pad.modals.deleted": "محذوف.", - "pad.modals.deleted.explanation": "تمت إزالة هذا الباد", + "pad.modals.deleted.explanation": "تمت إزالة هذا الباد.", "pad.modals.disconnected": "لم تعد متصلا.", "pad.modals.disconnected.explanation": "تم فقدان الاتصال بالخادم", "pad.modals.disconnected.cause": "قد يكون الخادم غير متوفر. يرجى إعلام مسؤول الخدمة إذا كان هذا لا يزال يحدث.", "pad.share": "شارك هذه الباد", "pad.share.readonly": "للقراءة فقط", - "pad.share.link": "رابط", + "pad.share.link": "وصلة", "pad.share.emebdcode": "URL للتضمين", "pad.chat": "دردشة", - "pad.chat.title": "فتح الدردشة لهذا الباد", + "pad.chat.title": "فتح الدردشة لهذا الباد.", "pad.chat.loadmessages": "تحميل المزيد من الرسائل", "pad.chat.stick.title": "ألصق الدردشة بالشاشة", "pad.chat.writeMessage.placeholder": "اكتب رسالتك هنا", diff --git a/src/locales/mk.json b/src/locales/mk.json index 78d1e65b9..41db11976 100644 --- a/src/locales/mk.json +++ b/src/locales/mk.json @@ -125,7 +125,7 @@ "pad.editbar.clearcolors": "Да ги отстранам авторските бои од целиот документ?", "pad.impexp.importbutton": "Увези сега", "pad.impexp.importing": "Увезувам...", - "pad.impexp.confirmimport": "Увезувајќи ја податотеката ќе го замените целиот досегашен текст во тетратката. Дали сте сигурни дека сакате да продолжите?", + "pad.impexp.confirmimport": "Увезувајќи податотека ќе го замените целиот досегашен текст во тетратката. Дали сте сигурни дека сакате да продолжите?", "pad.impexp.convertFailed": "Не можев да ја увезам податотеката. Послужете се со поинаков формат или прекопирајте го текстот рачно.", "pad.impexp.padHasData": "Не можевме да ја увеземе оваа податотека бидејќи оваа тетратка веќе има промени. Увезете ја во нова тетратка.", "pad.impexp.uploadFailed": "Подигањето не успеа. Обидете се повторно.", diff --git a/src/locales/sh.json b/src/locales/sh.json new file mode 100644 index 000000000..f3dd2bbac --- /dev/null +++ b/src/locales/sh.json @@ -0,0 +1,132 @@ +{ + "@metadata": { + "authors": [ + "Conquistador", + "Vlad5250" + ] + }, + "index.newPad": "Novi blokić", + "index.createOpenPad": "ili napravite/otvorite blokić s imenom:", + "pad.toolbar.bold.title": "Podebljano (Ctrl+B)", + "pad.toolbar.italic.title": "Ukošeno (Ctrl+I)", + "pad.toolbar.underline.title": "Podcrtano (Ctrl+U)", + "pad.toolbar.strikethrough.title": "Precrtano (Ctrl+5)", + "pad.toolbar.ol.title": "Poredani spisak (Ctrl+Shift+N)", + "pad.toolbar.ul.title": "Neporedani spisak (Ctrl+Shift+L)", + "pad.toolbar.indent.title": "Uvlaka (TAB)", + "pad.toolbar.unindent.title": "Izvlaka (Shift+TAB)", + "pad.toolbar.undo.title": "Vrati (Ctrl+Z)", + "pad.toolbar.redo.title": "Ponovi (Ctrl+Y)", + "pad.toolbar.clearAuthorship.title": "Ukloni boje autorstva (Ctrl+Shift+C)", + "pad.toolbar.import_export.title": "Uvoz/Izvoz iz/na različite datotečne formate", + "pad.toolbar.timeslider.title": "Historijski pregled", + "pad.toolbar.savedRevision.title": "Snimi inačicu", + "pad.toolbar.settings.title": "Postavke", + "pad.toolbar.embed.title": "Dijelite i umetnite ovaj blokić", + "pad.toolbar.showusers.title": "Pokaži korisnike ovoga blokića", + "pad.colorpicker.save": "Snimi", + "pad.colorpicker.cancel": "Otkaži", + "pad.loading": "Učitavam...", + "pad.noCookie": "Nisam mogao pronaći kolačić. Omogućite kolačiće u vašem pregledniku!", + "pad.passwordRequired": "Potrebna je lozinka za pristup", + "pad.permissionDenied": "Za ovdje nije potrebna dozvola za pristup", + "pad.wrongPassword": "Pogrešna lozinka", + "pad.settings.padSettings": "Postavke blokića", + "pad.settings.myView": "Moj prikaz", + "pad.settings.stickychat": "Ćaskanje uvijek na ekranu", + "pad.settings.chatandusers": "Prikaži ćaskanje i korisnike", + "pad.settings.colorcheck": "Boje autorstva", + "pad.settings.linenocheck": "Brojevi redova", + "pad.settings.rtlcheck": "Da prikažem sadržaj zdesna ulijevo?", + "pad.settings.fontType": "Tip fonta:", + "pad.settings.globalView": "Globalni prikaz", + "pad.settings.language": "Jezik:", + "pad.importExport.import_export": "Uvoz/Izvoz", + "pad.importExport.import": "Otpremanje bilo koje tekstualne datoteke ili dokumenta", + "pad.importExport.importSuccessful": "Uspješno!", + "pad.importExport.export": "Izvezi trenutni blokić kao:", + "pad.importExport.exportetherpad": "Etherpad", + "pad.importExport.exporthtml": "HTML", + "pad.importExport.exportplain": "Obični tekst", + "pad.importExport.exportword": "Microsoft Word", + "pad.importExport.exportpdf": "PDF", + "pad.importExport.exportopen": "ODF (Open Document Format)", + "pad.importExport.abiword.innerHTML": "Možete uvoziti samo iz običnog teksta te HTML-formata. Naprednije mogućnosti uvoza dobit ćete ako uspostavite AbiWord.", + "pad.modals.connected": "Povezano.", + "pad.modals.reconnecting": "Prepovezujemo Vas s blokićem...", + "pad.modals.forcereconnect": "Nametni prepovezivanje", + "pad.modals.reconnecttimer": "Se prepovezivam za", + "pad.modals.cancel": "Otkaži", + "pad.modals.userdup": "Otvoreno u drugom prozoru", + "pad.modals.userdup.explanation": "Ovaj je blokić otvoren u više od jednoga prozora (u pregledniku) na računalu.", + "pad.modals.userdup.advice": "Prepovežite se da biste koristili ovaj prozor.", + "pad.modals.unauth": "Neovlašteno", + "pad.modals.unauth.explanation": "Vaše su dozvole izmijenjene za vrijeme dok ste pregledavali ovu stranicu. Pokušajte se prepovezati.", + "pad.modals.looping.explanation": "Postoje problemi s vezom sa usklađivnim poslužiteljem.", + "pad.modals.looping.cause": "Možda ste se spojili preko neskladne sigurnosne stijene ili proxyja.", + "pad.modals.initsocketfail": "Server je nedostupan.", + "pad.modals.initsocketfail.explanation": "Nisam mogao se povezati sa usklađivnim serverom.", + "pad.modals.initsocketfail.cause": "Ovo je vjerojatno zbog problema s vašim preglednikom ili svemrežnom vezom.", + "pad.modals.slowcommit.explanation": "Server se ne odaziva.", + "pad.modals.slowcommit.cause": "Ovo je vjerojatno zbog problema s mrežnim povezivanjem.", + "pad.modals.badChangeset.explanation": "Poslužitelj za usklađivanje smatra da je izmjena koju ste napravili nedopuštena.", + "pad.modals.badChangeset.cause": "Ovo može biti zbog pogrešne postavljenosti poslužitelja ili nekog drugog neočekivanog ponašanja. Obratite se administratoru ukoliko držite da je to greška. Pokušajte se preuključiti kako biste nastavili s uređivanjem.", + "pad.modals.corruptPad.explanation": "Blokić što pokušavate otvoriti je oštećen.", + "pad.modals.corruptPad.cause": "Ovo može biti zbog pogrešne postavljenosti poslužitelja ili nekog drugog neočekivanog ponašanja. Obratite se administratoru.", + "pad.modals.deleted": "Obrisano.", + "pad.modals.deleted.explanation": "Ovaj blokić je uklonjen.", + "pad.modals.disconnected": "Veza je prekinuta.", + "pad.modals.disconnected.explanation": "Veza s poslužiteljem je prekinuta", + "pad.modals.disconnected.cause": "Moguće je da server nije dostupan. Obavijestite administratora ako se ovo nastavi događati.", + "pad.share": "Dijeli ovaj blokić", + "pad.share.readonly": "Samo čitanje", + "pad.share.link": "Link", + "pad.share.emebdcode": "Umetni URL", + "pad.chat": "Ćaskanje", + "pad.chat.title": "Otvori ćaskanje uz ovaj blokić.", + "pad.chat.loadmessages": "Učitaj više poruka", + "pad.chat.stick.title": "Zalijepi ćaskanje na ekranu", + "pad.chat.writeMessage.placeholder": "Ovdje napišite poruku", + "timeslider.pageTitle": "{{appTitle}} Historijski pregled", + "timeslider.toolbar.returnbutton": "Natrag na blokić", + "timeslider.toolbar.authors": "Autori:", + "timeslider.toolbar.authorsList": "Nema autora", + "timeslider.toolbar.exportlink.title": "Izvoz", + "timeslider.exportCurrent": "Izvezi trenutnu verziju kao:", + "timeslider.version": "Verzija {{version}}", + "timeslider.saved": "Spremljeno {{day}}. {{month}} {{year}}.", + "timeslider.playPause": "Izvrti/pauziraj sadržaj blokića", + "timeslider.backRevision": "Nazad na jednu inačicu ovog blokića", + "timeslider.forwardRevision": "Naprijed na jednu inačicu ovog blokića", + "timeslider.dateformat": "{{day}}. {{month}}. {{year}}. {{hours}}:{{minutes}}:{{seconds}}", + "timeslider.month.january": "januara", + "timeslider.month.february": "februara", + "timeslider.month.march": "marta", + "timeslider.month.april": "aprila", + "timeslider.month.may": "maja", + "timeslider.month.june": "juna", + "timeslider.month.july": "jula", + "timeslider.month.august": "augusta", + "timeslider.month.september": "septembra", + "timeslider.month.october": "oktobra", + "timeslider.month.november": "novembra", + "timeslider.month.december": "decembra", + "timeslider.unnamedauthors": "{{num}} {[plural(num) one: neimenovani autor, plural(num) two: neimenovana autora, plural(num) other: neimenovanih autora ]}", + "pad.savedrevs.marked": "Ova inačica označena je sada kao spremljena", + "pad.savedrevs.timeslider": "Možete pogledati spremljene inačice rabeći vremesledni klizač", + "pad.userlist.entername": "Upišite svoje ime", + "pad.userlist.unnamed": "bez imena", + "pad.userlist.guest": "Gost", + "pad.userlist.deny": "Odbij", + "pad.userlist.approve": "Odobri", + "pad.editbar.clearcolors": "Ukloniti boje autorstva sa cijelog dokumenta?", + "pad.impexp.importbutton": "Uvezi odmah", + "pad.impexp.importing": "Uvozim...", + "pad.impexp.confirmimport": "Uvoženje datoteke presnimit će trenutni sadržaj blokića.\nJeste li sigurni da želite nastaviti?", + "pad.impexp.convertFailed": "Nisam mogao uvesti datoteku. Poslužite se uz neki drugi format ili prekopirajte tekst ručno.", + "pad.impexp.padHasData": "Nismo mogli uvesti ovu datoteku jer je ovaj blokić već ima promjene. Uvezite je u novi blokić.", + "pad.impexp.uploadFailed": "Postavljanje nije uspjelo. Pokušajte ponovo.", + "pad.impexp.importfailed": "Uvoz nije uspio", + "pad.impexp.copypaste": "Prekopirajte", + "pad.impexp.exportdisabled": "Izvoz u formatu {{type}} je onemogućen. Ako želite saznati više o ovome, obratite se administratoru sustava." +} From dc7e49f89dcc9c4b53f51038144cc83e2e8f5919 Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 16 Apr 2019 00:34:29 +0200 Subject: [PATCH 121/183] Remove trailing whitespaces Hoping to minimize future diffs. Not touching vendorized libraries. --- src/node/hooks/express.js | 6 +- src/node/hooks/express/socketio.js | 4 +- src/node/hooks/express/static.js | 6 +- src/node/hooks/express/swagger.js | 8 +-- src/node/utils/Abiword.js | 22 +++---- src/node/utils/ExportHtml.js | 10 ++-- src/node/utils/customError.js | 4 +- src/static/js/AttributePool.js | 4 +- src/static/js/Changeset.js | 84 +++++++++++++-------------- src/static/js/admin/plugins.js | 16 ++--- src/static/js/admin/settings.js | 2 +- src/static/js/broadcast.js | 30 +++++----- src/static/js/broadcast_revisions.js | 2 +- src/static/js/broadcast_slider.js | 32 +++++----- src/static/js/changesettracker.js | 6 +- src/static/js/chat.js | 18 +++--- src/static/js/colorutils.js | 2 +- src/static/js/contentcollector.js | 14 ++--- src/static/js/domline.js | 6 +- src/static/js/l10n.js | 4 +- src/static/js/linestylefilter.js | 12 ++-- src/static/js/pad_connectionstatus.js | 8 +-- src/static/js/pad_cookie.js | 4 +- src/static/js/pad_editor.js | 4 +- src/static/js/pad_utils.js | 8 +-- src/static/js/skiplist.js | 4 +- src/static/js/timeslider.js | 8 +-- src/static/js/undomodule.js | 2 +- 28 files changed, 165 insertions(+), 165 deletions(-) diff --git a/src/node/hooks/express.js b/src/node/hooks/express.js index e7b373805..ccdc3d668 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.js @@ -13,7 +13,7 @@ exports.createServer = function () { console.log("Report bugs at https://github.com/ether/etherpad-lite/issues") serverName = `Etherpad ${settings.getGitCommit()} (http://etherpad.org)`; - + console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`); exports.restartServer(); @@ -45,7 +45,7 @@ exports.restartServer = function () { console.log("SSL -- enabled"); console.log(`SSL -- server key file: ${settings.ssl.key}`); console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`); - + var options = { key: fs.readFileSync( settings.ssl.key ), cert: fs.readFileSync( settings.ssl.cert ) @@ -57,7 +57,7 @@ exports.restartServer = function () { options.ca.push(fs.readFileSync(caFileName)); } } - + var https = require('https'); server = https.createServer(options, app); diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index 23622f3af..de94e9fbb 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -8,7 +8,7 @@ var padMessageHandler = require("../../handler/PadMessageHandler"); var cookieParser = require('cookie-parser'); var sessionModule = require('express-session'); - + exports.expressCreateServer = function (hook_name, args, cb) { //init socket.io and redirect all requests to the MessageHandler // there shouldn't be a browser that isn't compatible to all @@ -57,7 +57,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { // no longer available, details available at: // http://stackoverflow.com/questions/23981741/minify-socket-io-socket-io-js-with-1-0 // if(settings.minify) io.enable('browser client minification'); - + //Initalize the Socket.IO Router socketIORouter.setSocketIO(io); socketIORouter.addComponent("pad", padMessageHandler); diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.js index ef41865e3..4c17fbe3b 100644 --- a/src/node/hooks/express/static.js +++ b/src/node/hooks/express/static.js @@ -40,9 +40,9 @@ exports.expressCreateServer = function (hook_name, args, cb) { var clientParts = _(plugins.parts) .filter(function(part){ return _(part).has('client_hooks') }); - + var clientPlugins = {}; - + _(clientParts).chain() .map(function(part){ return part.plugin }) .uniq() @@ -50,7 +50,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { clientPlugins[name] = _(plugins.plugins[name]).clone(); delete clientPlugins[name]['package']; }); - + res.header("Content-Type","application/json; charset=utf-8"); res.write(JSON.stringify({"plugins": clientPlugins, "parts": clientParts})); res.end(); diff --git a/src/node/hooks/express/swagger.js b/src/node/hooks/express/swagger.js index f606eb882..f3f07cd01 100644 --- a/src/node/hooks/express/swagger.js +++ b/src/node/hooks/express/swagger.js @@ -113,7 +113,7 @@ var API = { "response": {"groupIDs":{"type":"List", "items":{"type":"string"}}} }, }, - + // Author "author": { "create" : { @@ -298,7 +298,7 @@ function capitalise(string){ for (var resource in API) { for (var func in API[resource]) { - + // The base response model var responseModel = { "properties": { @@ -350,7 +350,7 @@ function newSwagger() { exports.expressCreateServer = function (hook_name, args, cb) { for (var version in apiHandler.version) { - + var swagger = newSwagger(); var basePath = "/rest/" + version; @@ -437,7 +437,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { }; swagger.configureSwaggerPaths("", "/api" , ""); - + swagger.configure("http://" + settings.ip + ":" + settings.port + basePath, version); } }; diff --git a/src/node/utils/Abiword.js b/src/node/utils/Abiword.js index 2aae5a8ac..eed844e73 100644 --- a/src/node/utils/Abiword.js +++ b/src/node/utils/Abiword.js @@ -17,7 +17,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - + var spawn = require('child_process').spawn; var async = require("async"); var settings = require("./Settings"); @@ -34,7 +34,7 @@ if(os.type().indexOf("Windows") > -1) { //span an abiword process to perform the conversion var abiword = spawn(settings.abiword, ["--to=" + task.destFile, task.srcFile]); - + //delegate the processing of stdout to another function abiword.stdout.on('data', function (data) { @@ -43,7 +43,7 @@ if(os.type().indexOf("Windows") > -1) }); //append error messages to the buffer - abiword.stderr.on('data', function (data) + abiword.stderr.on('data', function (data) { stdoutBuffer += data.toString(); }); @@ -63,7 +63,7 @@ if(os.type().indexOf("Windows") > -1) callback(); }); }; - + exports.convertFile = function(srcFile, destFile, type, callback) { doConvertTask({"srcFile": srcFile, "destFile": destFile, "type": type}, callback); @@ -79,16 +79,16 @@ else var spawnAbiword = function (){ abiword = spawn(settings.abiword, ["--plugin", "AbiCommand"]); var stdoutBuffer = ""; - var firstPrompt = true; + var firstPrompt = true; //append error messages to the buffer - abiword.stderr.on('data', function (data) + abiword.stderr.on('data', function (data) { stdoutBuffer += data.toString(); }); //abiword died, let's restart abiword and return an error with the callback - abiword.on('exit', function (code) + abiword.on('exit', function (code) { spawnAbiword(); stdoutCallback(`Abiword died with exit code ${code}`); @@ -105,10 +105,10 @@ else { //filter the feedback message var err = stdoutBuffer.search("OK") != -1 ? null : stdoutBuffer; - + //reset the buffer stdoutBuffer = ""; - + //call the callback with the error message //skip the first prompt if(stdoutCallback != null && !firstPrompt) @@ -116,7 +116,7 @@ else stdoutCallback(err); stdoutCallback = null; } - + firstPrompt = false; } }); @@ -138,7 +138,7 @@ else } }; }; - + //Queue with the converts we have to do var queue = async.queue(doConvertTask, 1); exports.convertFile = function(srcFile, destFile, type, callback) diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js index 9cbcd2aa0..18b32d247 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.js @@ -331,12 +331,12 @@ function getHTMLFromAtext(pad, atext, authorColors) nextLine = _analyzeLine(textLines[i + 1], attribLines[i + 1], apool); } hooks.aCallAll('getLineHTMLForExport', context); - //To create list parent elements + //To create list parent elements if ((!prevLine || prevLine.listLevel !== line.listLevel) || (prevLine && line.listTypeName !== prevLine.listTypeName)) { var exists = _.find(openLists, function (item) { - return (item.level === line.listLevel && item.type === line.listTypeName); + return (item.level === line.listLevel && item.type === line.listTypeName); }); if (!exists) { var prevLevel = 0; @@ -365,7 +365,7 @@ function getHTMLFromAtext(pad, atext, authorColors) { pieces.push("
      "); } - } + } } } @@ -398,7 +398,7 @@ function getHTMLFromAtext(pad, atext, authorColors) { pieces.push(""); } - + if (line.listTypeName === "number") { pieces.push("
"); @@ -407,7 +407,7 @@ function getHTMLFromAtext(pad, atext, authorColors) { pieces.push(""); } - } + } } } else//outside any list, need to close line.listLevel of lists diff --git a/src/node/utils/customError.js b/src/node/utils/customError.js index 5ca7a7a41..c18743485 100644 --- a/src/node/utils/customError.js +++ b/src/node/utils/customError.js @@ -5,11 +5,11 @@ function customError(message, errorName) { this.name = errorName || "Error"; this.message = message; - + var stackParts = new Error().stack.split("\n"); stackParts.splice(0,2); stackParts.unshift(this.name + ": " + message); - + this.stack = stackParts.join("\n"); } customError.prototype = Error.prototype; diff --git a/src/static/js/AttributePool.js b/src/static/js/AttributePool.js index 96ea9b0da..7e7634e42 100644 --- a/src/static/js/AttributePool.js +++ b/src/static/js/AttributePool.js @@ -91,6 +91,6 @@ AttributePool.prototype.fromJsonable = function (obj) { } return this; }; - -module.exports = AttributePool; \ No newline at end of file + +module.exports = AttributePool; diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 6f6e7d099..2458ae65e 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -42,7 +42,7 @@ exports.error = function error(msg) { }; /** - * This method is used for assertions with Messages + * This method is used for assertions with Messages * if assert fails, the error function is called. * @param b {boolean} assertion condition * @param msgParts {string} error to be passed if it fails @@ -76,7 +76,7 @@ exports.numToString = function (num) { * Converts stuff before $ to base 10 * @obsolete not really used anywhere?? * @param cs {string} the string - * @return integer + * @return integer */ exports.toBaseTen = function (cs) { var dollarIndex = cs.indexOf('$'); @@ -93,10 +93,10 @@ exports.toBaseTen = function (cs) { */ /** - * returns the required length of the text before changeset + * returns the required length of the text before changeset * can be applied * @param cs {string} String representation of the Changeset - */ + */ exports.oldLen = function (cs) { return exports.unpack(cs).oldLen; }; @@ -104,16 +104,16 @@ exports.oldLen = function (cs) { /** * returns the length of the text after changeset is applied * @param cs {string} String representation of the Changeset - */ + */ exports.newLen = function (cs) { return exports.unpack(cs).newLen; }; /** * this function creates an iterator which decodes string changeset operations - * @param opsStr {string} String encoding of the change operations to be performed - * @param optStartIndex {int} from where in the string should the iterator start - * @return {Op} type object iterator + * @param opsStr {string} String encoding of the change operations to be performed + * @param optStartIndex {int} from where in the string should the iterator start + * @return {Op} type object iterator */ exports.opIterator = function (opsStr, optStartIndex) { //print(opsStr); @@ -131,7 +131,7 @@ exports.opIterator = function (opsStr, optStartIndex) { if (result[0] == '?') { exports.error("Hit error opcode in op stream"); } - + return result; } var regexResult = nextRegexMatch(); @@ -504,7 +504,7 @@ exports.opAssembler = function () { /** * A custom made String Iterator * @param str {string} String to be iterated over - */ + */ exports.stringIterator = function (str) { var curIndex = 0; // newLines is the number of \n between curIndex and str.length @@ -549,7 +549,7 @@ exports.stringIterator = function (str) { }; /** - * A custom made StringBuffer + * A custom made StringBuffer */ exports.stringAssembler = function () { var pieces = []; @@ -827,12 +827,12 @@ exports.textLinesMutator = function (lines) { }; /** - * Function allowing iterating over two Op strings. + * Function allowing iterating over two Op strings. * @params in1 {string} first Op string * @params idx1 {int} integer where 1st iterator should start * @params in2 {string} second Op string * @params idx2 {int} integer where 2nd iterator should start - * @params func {function} which decides how 1st or 2nd iterator + * @params func {function} which decides how 1st or 2nd iterator * advances. When opX.opcode = 0, iterator X advances to * next element * func has signature f(op1, op2, opOut) @@ -889,7 +889,7 @@ exports.unpack = function (cs) { }; /** - * Packs Changeset object into a string + * Packs Changeset object into a string * @params oldLen {int} Old length of the Changeset * @params newLen {int] New length of the Changeset * @params opsStr {string} String encoding of the changes to be made @@ -980,8 +980,8 @@ exports.mutateTextLines = function (cs, lines) { * Composes two attribute strings (see below) into one. * @param att1 {string} first attribute string * @param att2 {string} second attribue string - * @param resultIsMutaton {boolean} - * @param pool {AttribPool} attribute pool + * @param resultIsMutaton {boolean} + * @param pool {AttribPool} attribute pool */ exports.composeAttributes = function (att1, att2, resultIsMutation, pool) { // att1 and att2 are strings like "*3*f*1c", asMutation is a boolean. @@ -1041,8 +1041,8 @@ exports.composeAttributes = function (att1, att2, resultIsMutation, pool) { }; /** - * Function used as parameter for applyZip to apply a Changeset to an - * attribute + * Function used as parameter for applyZip to apply a Changeset to an + * attribute */ exports._slicerZipperFunc = function (attOp, csOp, opOut, pool) { // attOp is the op from the sequence that is being operated on, either an @@ -1359,7 +1359,7 @@ exports.compose = function (cs1, cs2, pool) { * returns a function that tests if a string of attributes * (e.g. *3*4) contains a given attribute key,value that * is already present in the pool. - * @param attribPair array [key,value] of the attribute + * @param attribPair array [key,value] of the attribute * @param pool {AttribPool} Attribute pool */ exports.attributeTester = function (attribPair, pool) { @@ -1391,9 +1391,9 @@ exports.identity = function (N) { /** - * creates a Changeset which works on oldFullText and removes text - * from spliceStart to spliceStart+numRemoved and inserts newText - * instead. Also gives possibility to add attributes optNewTextAPairs + * creates a Changeset which works on oldFullText and removes text + * from spliceStart to spliceStart+numRemoved and inserts newText + * instead. Also gives possibility to add attributes optNewTextAPairs * for the new text. * @param oldFullText {string} old text * @param spliecStart {int} where splicing starts @@ -1429,7 +1429,7 @@ exports.makeSplice = function (oldFullText, spliceStart, numRemoved, newText, op * @param cs Changeset */ exports.toSplices = function (cs) { - // + // var unpacked = exports.unpack(cs); var splices = []; @@ -1460,7 +1460,7 @@ exports.toSplices = function (cs) { }; /** - * + * */ exports.characterRangeFollow = function (cs, startChar, endChar, insertionsAfter) { var newStartChar = startChar; @@ -1547,7 +1547,7 @@ exports.makeAttribution = function (text) { * and runs function func on them * @param cs {Changeset} changeset * @param func {function} function to be called - */ + */ exports.eachAttribNumber = function (cs, func) { var dollarPos = cs.indexOf('$'); if (dollarPos < 0) { @@ -1566,16 +1566,16 @@ exports.eachAttribNumber = function (cs, func) { * callable on a exports, attribution string, or attribs property of an op, * though it may easily create adjacent ops that can be merged. * @param cs {Changeset} changeset to be filtered - * @param filter {function} fnc which returns true if an + * @param filter {function} fnc which returns true if an * attribute X (int) should be kept in the Changeset - */ + */ exports.filterAttribNumbers = function (cs, filter) { return exports.mapAttribNumbers(cs, filter); }; /** - * does exactly the same as exports.filterAttribNumbers - */ + * does exactly the same as exports.filterAttribNumbers + */ exports.mapAttribNumbers = function (cs, func) { var dollarPos = cs.indexOf('$'); if (dollarPos < 0) { @@ -1600,7 +1600,7 @@ exports.mapAttribNumbers = function (cs, func) { /** * Create a Changeset going from Identity to a certain state * @params text {string} text of the final change - * @attribs attribs {string} optional, operations which insert + * @attribs attribs {string} optional, operations which insert * the text and also puts the right attributes */ exports.makeAText = function (text, attribs) { @@ -1611,9 +1611,9 @@ exports.makeAText = function (text, attribs) { }; /** - * Apply a Changeset to a AText + * Apply a Changeset to a AText * @param cs {Changeset} Changeset to be applied - * @param atext {AText} + * @param atext {AText} * @param pool {AttribPool} Attribute Pool to add to */ exports.applyToAText = function (cs, atext, pool) { @@ -1625,7 +1625,7 @@ exports.applyToAText = function (cs, atext, pool) { /** * Clones a AText structure - * @param atext {AText} + * @param atext {AText} */ exports.cloneAText = function (atext) { if (atext) { @@ -1638,7 +1638,7 @@ exports.cloneAText = function (atext) { /** * Copies a AText structure from atext1 to atext2 - * @param atext {AText} + * @param atext {AText} */ exports.copyAText = function (atext1, atext2) { atext2.text = atext1.text; @@ -1647,7 +1647,7 @@ exports.copyAText = function (atext1, atext2) { /** * Append the set of operations from atext to an assembler - * @param atext {AText} + * @param atext {AText} * @param assem Assembler like smartOpAssembler */ exports.appendATextToAssembler = function (atext, assem) { @@ -1685,7 +1685,7 @@ exports.appendATextToAssembler = function (atext, assem) { /** * Creates a clone of a Changeset and it's APool - * @param cs {Changeset} + * @param cs {Changeset} * @param pool {AtributePool} */ exports.prepareForWire = function (cs, pool) { @@ -1706,8 +1706,8 @@ exports.isIdentity = function (cs) { }; /** - * returns all the values of attributes with a certain key - * in an Op attribs string + * returns all the values of attributes with a certain key + * in an Op attribs string * @param attribs {string} Attribute string of a Op * @param key {string} string to be seached for * @param pool {AttribPool} attribute pool @@ -1717,8 +1717,8 @@ exports.opAttributeValue = function (op, key, pool) { }; /** - * returns all the values of attributes with a certain key - * in an attribs string + * returns all the values of attributes with a certain key + * in an attribs string * @param attribs {string} Attribute string * @param key {string} string to be seached for * @param pool {AttribPool} attribute pool @@ -1736,7 +1736,7 @@ exports.attribsAttributeValue = function (attribs, key, pool) { }; /** - * Creates a Changeset builder for a string with initial + * Creates a Changeset builder for a string with initial * length oldLen. Allows to add/remove parts of it * @param oldLen {int} Old length */ @@ -2224,7 +2224,7 @@ exports.composeWithDeletions = function (cs1, cs2, pool) { return exports.pack(len1, len3, newOps, bankAssem.toString()); }; -// This function is 95% like _slicerZipperFunc, we just changed two lines to ensure it merges the attribs of deletions properly. +// This function is 95% like _slicerZipperFunc, we just changed two lines to ensure it merges the attribs of deletions properly. // This is necassary for correct paddiff. But to ensure these changes doesn't affect anything else, we've created a seperate function only used for paddiffs exports._slicerZipperFuncWithDeletions= function (attOp, csOp, opOut, pool) { // attOp is the op from the sequence that is being operated on, either an diff --git a/src/static/js/admin/plugins.js b/src/static/js/admin/plugins.js index c9a244871..3bc0daddc 100644 --- a/src/static/js/admin/plugins.js +++ b/src/static/js/admin/plugins.js @@ -1,5 +1,5 @@ $(document).ready(function () { - + var socket, loc = document.location, port = loc.port == "" ? (loc.protocol == "https:" ? 443 : 80) : loc.port, @@ -23,7 +23,7 @@ $(document).ready(function () { search.searchTerm = searchTerm; socket.emit("search", {searchTerm: searchTerm, offset:search.offset, limit: limit, sortBy: search.sortBy, sortDir: search.sortDir}); search.offset += limit; - + $('#search-progress').show() search.messages.show('fetching') search.searching = true @@ -76,7 +76,7 @@ $(document).ready(function () { function displayPluginList(plugins, container, template) { plugins.forEach(function(plugin) { var row = template.clone(); - + for (attr in plugin) { if(attr == "name"){ // Hack to rewrite URLS into name var link = $(''); @@ -96,7 +96,7 @@ $(document).ready(function () { }) updateHandlers(); } - + function sortPluginList(plugins, property, /*ASC?*/dir) { return plugins.sort(function(a, b) { if (a[property] < b[property]) @@ -113,7 +113,7 @@ $(document).ready(function () { $("#search-query").unbind('keyup').keyup(function () { search($("#search-query").val()); }); - + // Prevent form submit $('#search-query').parent().bind('submit', function() { return false; @@ -167,7 +167,7 @@ $(document).ready(function () { search.messages.hide('nothing-found') search.messages.hide('fetching') $("#search-query").removeAttr('disabled') - + console.log('got search results', data) // add to results @@ -218,7 +218,7 @@ $(document).ready(function () { installed.messages.show("nothing-installed") } }); - + socket.on('results:updatable', function(data) { data.updatable.forEach(function(pluginName) { var $row = $('#installed-plugins > tr.'+pluginName) @@ -250,7 +250,7 @@ $(document).ready(function () { // remove plugin from installed list $('#installed-plugins .'+data.plugin).remove() - + socket.emit("getInstalled"); // update search results diff --git a/src/static/js/admin/settings.js b/src/static/js/admin/settings.js index 6c1f5e236..d95a424e5 100644 --- a/src/static/js/admin/settings.js +++ b/src/static/js/admin/settings.js @@ -31,7 +31,7 @@ $(document).ready(function () { } else{ alert("YOUR JSON IS BAD AND YOU SHOULD FEEL BAD"); - } + } }); /* When the admin clicks save Settings check the JSON then send the JSON back to the server */ diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js index 817155b55..1bd547f52 100644 --- a/src/static/js/broadcast.js +++ b/src/static/js/broadcast.js @@ -1,5 +1,5 @@ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. + * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ @@ -239,7 +239,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro */ function applyChangeset(changeset, revision, preventSliderMovement, timeDelta) - { + { // disable the next 'gotorevision' call handled by a timeslider update if (!preventSliderMovement) { @@ -263,12 +263,12 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro debugLog('Time Delta: ', timeDelta) updateTimer(); - + var authors = _.map(padContents.getActiveAuthors(), function(name) { return authorData[name]; }); - + BroadcastSlider.setAuthors(authors); } @@ -281,7 +281,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro str = '0' + str; return str; } - + var date = new Date(padContents.currentTime); var dateFormat = function() { @@ -296,15 +296,15 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro "month": month, "year": year, "hours": hours, - "minutes": minutes, + "minutes": minutes, "seconds": seconds })); } - - - - - + + + + + $('#timer').html(dateFormat()); var revisionDate = html10n.get("timeslider.saved", { "day": date.getDate(), @@ -327,7 +327,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro $('#revision_date').html(revisionDate) } - + updateTimer(); function goToRevision(newRevision) @@ -378,13 +378,13 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro // Loading changeset history for old revision (to make diff between old and new revision) loadChangesetsForRevision(padContents.currentRevision - 1); } - + var authors = _.map(padContents.getActiveAuthors(), function(name){ return authorData[name]; }); BroadcastSlider.setAuthors(authors); } - + function loadChangesetsForRevision(revision, callback) { if (BroadcastSlider.getSliderLength() > 10000) { @@ -566,7 +566,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro goToRevision.apply(goToRevision, arguments); } } - + BroadcastSlider.onSlider(goToRevisionIfEnabled); var dynamicCSS = makeCSSManager('dynamicsyntax'); diff --git a/src/static/js/broadcast_revisions.js b/src/static/js/broadcast_revisions.js index 1980bdf30..abe3292dc 100644 --- a/src/static/js/broadcast_revisions.js +++ b/src/static/js/broadcast_revisions.js @@ -1,5 +1,5 @@ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. + * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ diff --git a/src/static/js/broadcast_slider.js b/src/static/js/broadcast_slider.js index 2299bba32..1893994ef 100644 --- a/src/static/js/broadcast_slider.js +++ b/src/static/js/broadcast_slider.js @@ -1,5 +1,5 @@ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. + * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ @@ -59,7 +59,7 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) slidercallbacks[i](newval); } } - + var updateSliderElements = function() { for (var i = 0; i < savedRevisions.length; i++) @@ -68,7 +68,7 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) savedRevisions[i].css('left', (position * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)) - 1); } $("#ui-slider-handle").css('left', sliderPos * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)); - } + } var addSavedRevision = function(position, info) { @@ -171,7 +171,7 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) var height = $('#timeslider-top').height(); $('#editorcontainerbox').css({marginTop: height}); }, 600); - + function setAuthors(authors) { var authorsList = $("#authorsList"); @@ -187,7 +187,7 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) if (author.name) { if (numNamed !== 0) authorsList.append(', '); - + $('') .text(author.name || "unnamed") .css('background-color', authorColor) @@ -206,17 +206,17 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) if (numAnonymous > 0) { var anonymousAuthorString = html10n.get("timeslider.unnamedauthors", { num: numAnonymous }); - + if (numNamed !== 0){ authorsList.append(' + ' + anonymousAuthorString); } else { authorsList.append(anonymousAuthorString); } - + if(colorsAnonymous.length > 0){ authorsList.append(' ('); _.each(colorsAnonymous, function(color, i){ - if( i > 0 ) authorsList.append(' '); + if( i > 0 ) authorsList.append(' '); $(' ') .css('background-color', color) .addClass('author author-anonymous') @@ -224,13 +224,13 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) }); authorsList.append(')'); } - + } if (authors.length == 0) { authorsList.append(html10n.get("timeslider.toolbar.authorsList")); } - + fixPadHeight(); } @@ -288,7 +288,7 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) { disableSelection($("#playpause_button")[0]); disableSelection($("#timeslider")[0]); - + $(document).keyup(function(e) { // If focus is on editbar, don't do anything @@ -337,7 +337,7 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) } else if (code == 32) playpause(); }); - + $(window).resize(function() { updateSliderElements(); @@ -467,7 +467,7 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) if (clientVars) { $("#timeslider").show(); - + var startPos = clientVars.collab_client_vars.rev; if(window.location.hash.length > 1) { @@ -478,15 +478,15 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) setTimeout(function() { setSliderPosition(hashRev); }, 1); } } - + setSliderLength(clientVars.collab_client_vars.rev); setSliderPosition(clientVars.collab_client_vars.rev); - + _.each(clientVars.savedRevisions, function(revision) { addSavedRevision(revision.revNum, revision); }) - + } }); })(); diff --git a/src/static/js/changesettracker.js b/src/static/js/changesettracker.js index fe362c4b7..4e7cd3ed6 100644 --- a/src/static/js/changesettracker.js +++ b/src/static/js/changesettracker.js @@ -1,5 +1,5 @@ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. + * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ @@ -163,7 +163,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) else { - // add forEach function to Array.prototype for IE8 + // add forEach function to Array.prototype for IE8 if (!('forEach' in Array.prototype)) { Array.prototype.forEach= function(action, that /*opt*/) { for (var i= 0, n= this.length; i p').eq(-1); } } - }, + }, send: function() { var text = $("#chatinput").val(); @@ -121,7 +121,7 @@ var chat = (function() { //correct the time msg.time += this._pad.clientTimeOffset; - + //create the time string var minutes = "" + new Date(msg.time).getMinutes(); var hours = "" + new Date(msg.time).getHours(); @@ -130,7 +130,7 @@ var chat = (function() if(hours.length == 1) hours = "0" + hours ; var timeStr = hours + ":" + minutes; - + //create the authorclass var authorClass = "author-" + msg.userId.replace(/[^a-y0-9]/g, function(c) { diff --git a/src/static/js/colorutils.js b/src/static/js/colorutils.js index 74a2e4635..af471c453 100644 --- a/src/static/js/colorutils.js +++ b/src/static/js/colorutils.js @@ -1,5 +1,5 @@ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. + * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index 6820da07c..d3bd73383 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -1,5 +1,5 @@ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. + * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ @@ -252,14 +252,14 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas { state.listNesting = (state.listNesting || 0) + 1; } - + if(listType === 'none' || !listType ){ - delete state.lineAttributes['list']; + delete state.lineAttributes['list']; } else{ state.lineAttributes['list'] = listType; } - + _recalcAttribString(state); return oldListType; } @@ -303,7 +303,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas // see https://github.com/ether/etherpad-lite/issues/2567 for more information // in long term the contentcollector should be refactored to get rid of this workaround var ATTRIBUTE_SPLIT_STRING = "::"; - + // see if attributeString is splittable var attributeSplits = a.split(ATTRIBUTE_SPLIT_STRING); if (attributeSplits.length > 1) { @@ -410,7 +410,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas text:txt, styl: null, cls: null - }); + }); var txt = (typeof(txtFromHook)=='object'&&txtFromHook.length==0)?dom.nodeValue(node):txtFromHook[0]; var rest = ''; @@ -504,7 +504,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas tvalue:tvalue, styl: null, cls: null - }); + }); var startNewLine= (typeof(induceLineBreak)=='object'&&induceLineBreak.length==0)?true:induceLineBreak[0]; if(startNewLine){ cc.startNewLine(state); diff --git a/src/static/js/domline.js b/src/static/js/domline.js index a7501fcc6..100ce0919 100644 --- a/src/static/js/domline.js +++ b/src/static/js/domline.js @@ -1,5 +1,5 @@ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. + * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ @@ -135,7 +135,7 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) } postHtml += ''; } - } + } processedMarker = true; } _.map(hooks.callAll("aceDomLineProcessLineAttributes", { @@ -150,7 +150,7 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) if( processedMarker ){ result.lineMarker += txt.length; return; // don't append any text - } + } } var href = null; var simpleTags = null; diff --git a/src/static/js/l10n.js b/src/static/js/l10n.js index c79ea706d..473850bd9 100644 --- a/src/static/js/l10n.js +++ b/src/static/js/l10n.js @@ -6,9 +6,9 @@ html10n.bind('indexed', function() { html10n.localize([language, navigator.language, navigator.userLanguage, 'en']) }) - + html10n.bind('localized', function() { document.documentElement.lang = html10n.getLanguage() document.documentElement.dir = html10n.getDirection() }) -})(document) \ No newline at end of file +})(document) diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index 3cf7a510a..0c7cff812 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -1,5 +1,5 @@ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. + * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ @@ -80,10 +80,10 @@ linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFun { var classes = ''; var isLineAttribMarker = false; - + Changeset.eachAttribNumber(attribs, function(n) { - var key = apool.getAttribKey(n); + var key = apool.getAttribKey(n); if (key) { var value = apool.getAttribValue(n); @@ -115,11 +115,11 @@ linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFun key: key, value: value }, " ", " ", ""); - } + } } } }); - + if(isLineAttribMarker) classes += ' ' + lineAttributeMarker; return classes.substring(1); } @@ -157,7 +157,7 @@ linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFun linestylefilter: linestylefilter, text: txt, "class": cls - }, " ", " ", ""); + }, " ", " ", ""); var disableAuthors = (disableAuthColorForThisLine==null||disableAuthColorForThisLine.length==0)?false:disableAuthColorForThisLine[0]; while (txt.length > 0) { diff --git a/src/static/js/pad_connectionstatus.js b/src/static/js/pad_connectionstatus.js index 76eedbc4d..4e5f41be0 100644 --- a/src/static/js/pad_connectionstatus.js +++ b/src/static/js/pad_connectionstatus.js @@ -1,5 +1,5 @@ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. + * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ @@ -50,7 +50,7 @@ var padconnectionstatus = (function() status = { what: 'reconnecting' }; - + padmodals.showModal('reconnecting'); padmodals.showOverlay(); }, @@ -58,12 +58,12 @@ var padconnectionstatus = (function() { if(status.what == "disconnected") return; - + status = { what: 'disconnected', why: msg }; - + var k = String(msg); // known reason why if (!(k == 'userdup' || k == 'deleted' || k == 'looping' || k == 'slowcommit' || k == 'initsocketfail' || k == 'unauth' || k == 'badChangeset' || k == 'corruptPad')) { diff --git a/src/static/js/pad_cookie.js b/src/static/js/pad_cookie.js index 62c88cffe..2fa20452e 100644 --- a/src/static/js/pad_cookie.js +++ b/src/static/js/pad_cookie.js @@ -1,5 +1,5 @@ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. + * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ @@ -82,7 +82,7 @@ var padcookie = (function() alreadyWarnedAboutNoCookies = true; } } - + function isHttpsScheme() { return window.location.protocol == "https:"; } diff --git a/src/static/js/pad_editor.js b/src/static/js/pad_editor.js index ec2339562..c59a2fc61 100644 --- a/src/static/js/pad_editor.js +++ b/src/static/js/pad_editor.js @@ -142,7 +142,7 @@ var padeditor = (function() } var fontFamily = newOptions['padFontFamily']; - switch (fontFamily) { + switch (fontFamily) { case "monospace": self.ace.setProperty("textface", "monospace"); break; case "montserrat": self.ace.setProperty("textface", "Montserrat"); break; case "opendyslexic": self.ace.setProperty("textface", "OpenDyslexic"); break; @@ -162,7 +162,7 @@ var padeditor = (function() case "wingdings": self.ace.setProperty("textface", "Wingdings"); break; case "sansserif": self.ace.setProperty("textface", "sans-serif"); break; case "serif": self.ace.setProperty("textface", "serif"); break; - default: self.ace.setProperty("textface", ""); break; + default: self.ace.setProperty("textface", ""); break; } }, dispose: function() diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index 9c1277a00..ef675dc75 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -1,5 +1,5 @@ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. + * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ @@ -53,7 +53,7 @@ function createCookie(name, value, days, path){ /* Used by IE */ if(!path){ // IF the Path of the cookie isn't set then just create it on root path = "/"; } - + //Check if we accessed the pad over https var secure = window.location.protocol == "https:" ? ";secure" : ""; @@ -531,9 +531,9 @@ function setupGlobalExceptionHandler() { var errObj = {errorInfo: JSON.stringify({errorId: errorId, msg: msg, url: window.location.href, linenumber: linenumber, userAgent: navigator.userAgent})}; var loc = document.location; var url = loc.protocol + "//" + loc.hostname + ":" + loc.port + "/" + loc.pathname.substr(1, loc.pathname.indexOf("/p/")) + "jserror"; - + $.post(url, errObj); - + return false; }; window.onerror = globalExceptionHandler; diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index a02a2ad52..04155e320 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -1,5 +1,5 @@ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. + * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ @@ -155,7 +155,7 @@ function SkipList() var widthLoc = point.widthSkips[0] + point.nodes[0].downSkipWidths[0]; var 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 while (newNode.levels == 0 || Math.random() < 0.01) diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js index 7e744118f..3cdff91a9 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -60,10 +60,10 @@ function init() { var url = loc.protocol + "//" + loc.hostname + ":" + port + "/"; //find out in which subfolder we are var resource = exports.baseURL.substring(1) + 'socket.io'; - + //build up the socket io connection socket = io.connect(url, {path: exports.baseURL + 'socket.io', resource: resource}); - + //send the ready message once we're connected socket.on('connect', function() { @@ -126,13 +126,13 @@ function sendSocketMsg(type, data) } var fireWhenAllScriptsAreLoaded = []; - + var changesetLoader; function handleClientVars(message) { //save the client Vars clientVars = message.data; - + //load all script that doesn't work without the clientVars BroadcastSlider = require('./broadcast_slider').loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded); require('./broadcast_revisions').loadBroadcastRevisionsJS(); diff --git a/src/static/js/undomodule.js b/src/static/js/undomodule.js index 4b55311bf..6610224fe 100644 --- a/src/static/js/undomodule.js +++ b/src/static/js/undomodule.js @@ -1,5 +1,5 @@ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. + * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ From 75a0f339e1f804df047fe7dc21353a97fda9ef85 Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 16 Apr 2019 00:17:56 +0200 Subject: [PATCH 122/183] Settings.js, express.js: trivial reformatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Future commits by Tristram Gräbener will modify them. --- src/node/hooks/express.js | 23 ++++++++--------- src/node/utils/Settings.js | 53 +++++++++++++++++++++----------------- 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/node/hooks/express.js b/src/node/hooks/express.js index ccdc3d668..ed5cafb74 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.js @@ -19,20 +19,20 @@ exports.createServer = function () { exports.restartServer(); console.log(`You can access your Etherpad instance at http://${settings.ip}:${settings.port}/`); - if(!_.isEmpty(settings.users)){ + if (!_.isEmpty(settings.users)) { console.log(`The plugin admin page is at http://${settings.ip}:${settings.port}/admin/plugins`); - } - else{ + } else { 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'){ + + 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 () { - if (server) { console.log("Restarting express server"); server.close(); @@ -41,7 +41,6 @@ exports.restartServer = function () { var app = express(); // New syntax for express v3 if (settings.ssl) { - console.log("SSL -- enabled"); console.log(`SSL -- server key file: ${settings.ssl.key}`); console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`); @@ -50,9 +49,10 @@ exports.restartServer = function () { key: fs.readFileSync( settings.ssl.key ), cert: fs.readFileSync( settings.ssl.cert ) }; + if (settings.ssl.ca) { options.ca = []; - for(var i = 0; i < settings.ssl.ca.length; i++) { + for (var i = 0; i < settings.ssl.ca.length; i++) { var caFileName = settings.ssl.ca[i]; options.ca.push(fs.readFileSync(caFileName)); } @@ -60,16 +60,15 @@ exports.restartServer = function () { var https = require('https'); server = https.createServer(options, app); - } else { - var http = require('http'); server = http.createServer(app); } - app.use(function (req, res, next) { + app.use(function(req, res, next) { // res.header("X-Frame-Options", "deny"); // breaks embedded pads - if(settings.ssl){ // if we use SSL + if (settings.ssl) { + // we use SSL res.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); } @@ -80,7 +79,7 @@ exports.restartServer = function () { next(); }); - if(settings.trustProxy){ + if (settings.trustProxy) { app.enable('trust proxy'); } diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 8de69fcf7..db1b294b0 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -231,60 +231,67 @@ exports.loadTest = false; exports.indentationOnNewLine = true; /* -* log4js appender configuration -*/ + * log4js appender configuration + */ exports.logconfig = { appenders: [{ type: "console" }]}; /* -* Session Key, do not sure this. -*/ + * Session Key, do not sure this. + */ exports.sessionKey = false; /* -* Trust Proxy, whether or not trust the x-forwarded-for header. -*/ + * Trust Proxy, whether or not trust the x-forwarded-for header. + */ exports.trustProxy = false; -/* This setting is used if you need authentication and/or +/* + * This setting is used if you need authentication and/or * authorization. Note: /admin always requires authentication, and - * either authorization by a module, or a user with is_admin set */ + * either authorization by a module, or a user with is_admin set + */ exports.requireAuthentication = false; exports.requireAuthorization = false; exports.users = {}; /* -* Show settings in admin page, by default it is true -*/ + * Show settings in admin page, by default it is true + */ exports.showSettingsInAdminPage = true; /* -* By default, when caret is moved out of viewport, it scrolls the minimum height needed to make this -* line visible. -*/ + * By default, when caret is moved out of viewport, it scrolls the minimum + * height needed to make this line visible. + */ exports.scrollWhenFocusLineIsOutOfViewport = { /* - * Percentage of viewport height to be additionally scrolled. - */ + * Percentage of viewport height to be additionally scrolled. + */ "percentage": { "editionAboveViewport": 0, "editionBelowViewport": 0 }, + /* - * Time (in milliseconds) used to animate the scroll transition. Set to 0 to disable animation - */ + * Time (in milliseconds) used to animate the scroll transition. Set to 0 to + * disable animation + */ "duration": 0, + /* - * Flag to control if it should scroll when user places the caret in the last line of the viewport - */ - /* - * Percentage of viewport height to be additionally scrolled when user presses arrow up - * in the line of the top of the viewport. + * Percentage of viewport height to be additionally scrolled when user presses arrow up + * in the line of the top of the viewport. */ "percentageToScrollWhenUserPressesArrowUp": 0, + + /* + * Flag to control if it should scroll when user places the caret in the last + * line of the viewport + */ "scrollWhenCaretIsInTheLastLineOfViewport": false }; -//checks if abiword is avaiable +// checks if abiword is avaiable exports.abiwordAvailable = function() { if (exports.abiword != null) { From a6656102d86d1febfaa0a1503008e143c57a0987 Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 16 Apr 2019 00:53:00 +0200 Subject: [PATCH 123/183] CHANGELOG.md: link to https://translatewiki.net instead of plain http --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f95accf8..2c767223d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -297,7 +297,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. # 1.3 * NEW: We now follow the semantic versioning scheme! * NEW: Option to disable IP logging - * NEW: Localisation updates from http://translatewiki.net. + * NEW: Localisation updates from https://translatewiki.net. * Fix: Fix readOnly group pads * Fix: don't fetch padList on every request From 705cc6f5e42254195fbf37a3f2b06cc02276da0a Mon Sep 17 00:00:00 2001 From: muxator Date: Tue, 16 Apr 2019 00:54:54 +0200 Subject: [PATCH 124/183] Change everywhere the link to https://etherpad.org (it was plain http) --- CHANGELOG.md | 2 +- README.md | 4 ++-- doc/api/http_api.md | 2 +- src/node/hooks/express.js | 2 +- src/package.json | 2 +- tests/frontend/specs/urls_become_clickable.js | 14 +++++++------- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c767223d..709c4f5e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # 1.7.5 -* FEATURE: introduced support for multiple skins. See http://etherpad.org/doc/v1.7.5/#index_skins +* FEATURE: introduced support for multiple skins. See https://etherpad.org/doc/v1.7.5/#index_skins * FEATURE: added a new, optional skin. It can be activated choosing `skinName: "colibris"` in `settings.json` * FEATURE: allow file import using LibreOffice * SECURITY: updated many dependencies. No known high or moderate risk dependencies remain. diff --git a/README.md b/README.md index 3b8ff6184..6205ae3c5 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ To update to the latest released version, execute `git pull origin`. The next st ### Prebuilt Windows package This package works out of the box on any windows machine, but it's not very useful for developing purposes... -1. [Download the latest Windows package](http://etherpad.org/#download) +1. [Download the latest Windows package](https://etherpad.org/#download) 2. Extract the folder Now, run `start.bat` and open in your browser. You like it? [Next steps](#next-steps). @@ -124,7 +124,7 @@ Visit the **[FAQ](https://github.com/ether/etherpad-lite/wiki/FAQ)**. # Donate! * [Flattr](https://flattr.com/thing/71378/Etherpad-Foundation) -* Paypal - Press the donate button on [etherpad.org](http://etherpad.org) +* Paypal - Press the donate button on [etherpad.org](https://etherpad.org) * [Bitcoin](https://coinbase.com/checkouts/1e572bf8a82e4663499f7f1f66c2d15a) All donations go to the Etherpad foundation which is part of Software Freedom Conservency diff --git a/doc/api/http_api.md b/doc/api/http_api.md index 59008743a..7599fae00 100644 --- a/doc/api/http_api.md +++ b/doc/api/http_api.md @@ -349,7 +349,7 @@ get the changeset at a given revision, or last revision if 'rev' is not defined. *Example returns:* * `{ "code" : 0, "message" : "ok", - "data" : "Z:1>6b|5+6b$Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at http://etherpad.org\n" + "data" : "Z:1>6b|5+6b$Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at https://etherpad.org\n" }` * `{"code":1,"message":"padID does not exist","data":null}` * `{"code":1,"message":"rev is higher than the head revision of the pad","data":null}` diff --git a/src/node/hooks/express.js b/src/node/hooks/express.js index ed5cafb74..7b45ccf6f 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.js @@ -12,7 +12,7 @@ var serverName; exports.createServer = function () { console.log("Report bugs at https://github.com/ether/etherpad-lite/issues") - serverName = `Etherpad ${settings.getGitCommit()} (http://etherpad.org)`; + serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`; console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`); diff --git a/src/package.json b/src/package.json index 2b5eb5c18..d8d792b22 100644 --- a/src/package.json +++ b/src/package.json @@ -1,7 +1,7 @@ { "name": "ep_etherpad-lite", "description": "A Etherpad based on node.js", - "homepage": "http://etherpad.org", + "homepage": "https://etherpad.org", "keywords": [ "etherpad", "realtime", diff --git a/tests/frontend/specs/urls_become_clickable.js b/tests/frontend/specs/urls_become_clickable.js index b989717e2..7f5e30679 100644 --- a/tests/frontend/specs/urls_become_clickable.js +++ b/tests/frontend/specs/urls_become_clickable.js @@ -6,16 +6,16 @@ describe("urls", function(){ }); it("when you enter an url, it becomes clickable", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + //get the first text element out of the inner iframe var firstTextElement = inner$("div").first(); - + // simulate key presses to delete content firstTextElement.sendkeys('{selectall}'); // select all firstTextElement.sendkeys('{del}'); // clear the first line - firstTextElement.sendkeys('http://etherpad.org'); // insert a URL + firstTextElement.sendkeys('https://etherpad.org'); // insert a URL helper.waitFor(function(){ return inner$("div").first().find("a").length === 1; @@ -28,7 +28,7 @@ describe("urls", function(){ //get the first text element out of the inner iframe var firstTextElement = inner$("div").first(); - var url = "http://etherpad.org/!foo"; + var url = "https://etherpad.org/!foo"; // simulate key presses to delete content firstTextElement.sendkeys('{selectall}'); // select all @@ -50,7 +50,7 @@ describe("urls", function(){ //get the first text element out of the inner iframe var firstTextElement = inner$("div").first(); - var url = "http://etherpad.org/"; + var url = "https://etherpad.org/"; // simulate key presses to delete content firstTextElement.sendkeys('{selectall}'); // select all From 8453f072057b7deedf297c55a5993a2dee38a20c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristram=20Gr=C3=A4bener?= Date: Mon, 15 Apr 2019 12:15:12 +0200 Subject: [PATCH 125/183] Chat bubble: by default hide in CSS The current behaviour is to show the chat bubble and hide if chat is disabled. Because of this, the bubble appears wrongfully for a short time. With this PR, by default it is hidden and displayed only if chat is enabled. Fixes: #3088 --- src/static/css/pad.css | 1 + src/static/js/pad.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/static/css/pad.css b/src/static/css/pad.css index 18b936478..be8b1f0be 100644 --- a/src/static/css/pad.css +++ b/src/static/css/pad.css @@ -643,6 +643,7 @@ table#otheruserstable { border-top-right-radius: 5px; background-color: #fff; cursor: pointer; + display: none; } #chaticon a { text-decoration: none diff --git a/src/static/js/pad.js b/src/static/js/pad.js index b12058237..69fa65902 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -74,7 +74,7 @@ function randomString() var getParameters = [ { name: "noColors", checkVal: "true", callback: function(val) { settings.noColors = true; $('#clearAuthorship').hide(); } }, { name: "showControls", checkVal: "false", callback: function(val) { $('#editbar').addClass('hideControlsEditbar'); $('#editorcontainer').addClass('hideControlsEditor'); } }, - { name: "showChat", checkVal: "false", callback: function(val) { $('#chaticon').hide(); } }, + { name: "showChat", checkVal: "true", callback: function(val) { $('#chaticon').show(); } }, { name: "showLineNumbers", checkVal: "false", callback: function(val) { settings.LineNumbersDisabled = true; } }, { name: "useMonospaceFont", checkVal: "true", callback: function(val) { settings.useMonospaceFontGlobal = true; } }, // If the username is set as a parameter we should set a global value that we can call once we have initiated the pad. From 28a6f505c5084e2de44ba06081ba8913fa97cf7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristram=20Gr=C3=A4bener?= Date: Mon, 15 Apr 2019 16:02:46 +0200 Subject: [PATCH 126/183] Parameters: the version is exposed in http header only when configured Currently the version is exposed in a 'Server' http headers. This commit allows to parameterize it in the settings. By defaults it is not exposed. Fixes #3423 --- settings.json.template | 7 +++++++ src/node/hooks/express.js | 7 ++++++- src/node/utils/Settings.js | 7 +++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/settings.json.template b/settings.json.template index 535e502bf..efdb257db 100644 --- a/settings.json.template +++ b/settings.json.template @@ -409,6 +409,13 @@ */ /* + * Expose Etherpad version in the Server http header. + * + * Do not enable on production machines. + */ + "exposeVersion": false, + + /* * The log level we are using. * * Valid values: DEBUG, INFO, WARN, ERROR diff --git a/src/node/hooks/express.js b/src/node/hooks/express.js index 7b45ccf6f..702214ec8 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.js @@ -75,7 +75,12 @@ exports.restartServer = function () { // Stop IE going into compatability mode // https://github.com/ether/etherpad-lite/issues/2547 res.header("X-UA-Compatible", "IE=Edge,chrome=1"); - res.header("Server", serverName); + + // send git version in the Server response header if exposeVersion is true. + if (settings.exposeVersion) { + res.header("Server", serverName); + } + next(); }); diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index db1b294b0..cffb20cf0 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -291,6 +291,13 @@ exports.scrollWhenFocusLineIsOutOfViewport = { "scrollWhenCaretIsInTheLastLineOfViewport": false }; +/* + * Expose Etherpad version in the Server http header. + * + * Do not enable on production machines. + */ +exports.exposeVersion = false; + // checks if abiword is avaiable exports.abiwordAvailable = function() { From 357780d573a055979ec3917d8b3a95ea9d55fa6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristram=20Gr=C3=A4bener?= Date: Mon, 15 Apr 2019 17:03:06 +0200 Subject: [PATCH 127/183] Display the version in the web interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the settings drop-down this adds an “About” section that also shows the commit if "exposeVersion" is set to true. Fixes #2968 --- settings.json.template | 2 +- src/node/utils/Settings.js | 2 +- src/static/css/pad.css | 7 ++ src/templates/pad.html | 154 +++++++++++++++++++------------------ 4 files changed, 90 insertions(+), 75 deletions(-) diff --git a/settings.json.template b/settings.json.template index efdb257db..fa1baa4b4 100644 --- a/settings.json.template +++ b/settings.json.template @@ -409,7 +409,7 @@ */ /* - * Expose Etherpad version in the Server http header. + * Expose Etherpad version in the web interface and in the Server http header. * * Do not enable on production machines. */ diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index cffb20cf0..23a792ffd 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -292,7 +292,7 @@ exports.scrollWhenFocusLineIsOutOfViewport = { }; /* - * Expose Etherpad version in the Server http header. + * Expose Etherpad version in the web interface and in the Server http header. * * Do not enable on production machines. */ diff --git a/src/static/css/pad.css b/src/static/css/pad.css index be8b1f0be..7d5985946 100644 --- a/src/static/css/pad.css +++ b/src/static/css/pad.css @@ -938,6 +938,13 @@ input[type=checkbox] { outline: none; width: 120px; } +.row { + float: left; + width: 100%; +} +.row + .row { + margin-top: 15px; +} .column { float: left; width:50%; diff --git a/src/templates/pad.html b/src/templates/pad.html index bee248f57..61b9f2268 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -127,80 +127,88 @@