diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bb356c9..906212a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# 1.8.11 + +### Notable fixes + +* Fix server crash issue within PadMessageHandler due to SocketIO handling +* Fix editor issue with drop downs not being visible +* Ensure correct version is passed when loading front end resources +* Ensure underscore and jquery are available in original location for plugin comptability + +### Notable enhancements + +* Improved page load speeds + # 1.8.10 ### Security Patches diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index e32fcf1d..174a6bc5 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -1409,16 +1409,14 @@ const composePadChangesets = async (padId, startNum, endNum) => { }; const _getRoomSockets = (padID) => { - const roomSockets = []; - const room = socketio.sockets.adapter.rooms[padID]; - - if (room) { - for (const id of Object.keys(room.sockets)) { - roomSockets.push(socketio.sockets.sockets[id]); - } - } - - return roomSockets; + const ns = socketio.sockets; // Default namespace. + const adapter = ns.adapter; + // We could call adapter.clients(), but that method is unnecessarily asynchronous. Replicate what + // it does here, but synchronously to avoid a race condition. This code will have to change when + // we update to socket.io v3. + const room = adapter.rooms[padID]; + if (!room) return []; + return Object.keys(room.sockets).map((id) => ns.connected[id]).filter((s) => s); }; /** @@ -1438,14 +1436,13 @@ exports.padUsers = async (padID) => { await Promise.all(_getRoomSockets(padID).map(async (roomSocket) => { const s = sessioninfos[roomSocket.id]; if (s) { - return authorManager.getAuthor(s.author).then((author) => { - // Fixes: https://github.com/ether/etherpad-lite/issues/4120 - // On restart author might not be populated? - if (author) { - author.id = s.author; - padUsers.push(author); - } - }); + const author = await authorManager.getAuthor(s.author); + // Fixes: https://github.com/ether/etherpad-lite/issues/4120 + // On restart author might not be populated? + if (author) { + author.id = s.author; + padUsers.push(author); + } } })); diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.js index d1dec871..2b01f84c 100644 --- a/src/node/hooks/express/static.js +++ b/src/node/hooks/express/static.js @@ -31,7 +31,7 @@ const getTar = async () => { exports.expressCreateServer = async (hookName, args) => { // Cache both minified and static. const assetCache = new CachingMiddleware(); - args.app.all(/\/javascripts\/(.*)/, assetCache.handle); + args.app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache)); // Minify will serve static files compressed (minify enabled). It also has // file-specific hacks for ace/require-kernel/etc. diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.js index 23122342..3cc4daf2 100644 --- a/src/node/utils/caching_middleware.js +++ b/src/node/utils/caching_middleware.js @@ -16,13 +16,14 @@ * limitations under the License. */ -const async = require('async'); const Buffer = require('buffer').Buffer; const fs = require('fs'); +const fsp = fs.promises; const path = require('path'); const zlib = require('zlib'); const settings = require('./Settings'); const existsSync = require('./path_exists'); +const util = require('util'); /* * The crypto module can be absent on reduced node installations. @@ -77,146 +78,126 @@ if (_crypto) { should replace this. */ -function CachingMiddleware() { -} -CachingMiddleware.prototype = new function () { - const handle = (req, res, next) => { +module.exports = class CachingMiddleware { + handle(req, res, next) { + this._handle(req, res, next).catch((err) => next(err || new Error(err))); + } + + async _handle(req, res, next) { if (!(req.method === 'GET' || req.method === 'HEAD') || !CACHE_DIR) { return next(undefined, req, res); } - const old_req = {}; - const old_res = {}; + const oldReq = {}; + const oldRes = {}; const supportsGzip = (req.get('Accept-Encoding') || '').indexOf('gzip') !== -1; - const path = require('url').parse(req.url).path; - const cacheKey = generateCacheKey(path); + const url = new URL(req.url, 'http://localhost'); + const cacheKey = generateCacheKey(url.pathname + url.search); - fs.stat(`${CACHE_DIR}minified_${cacheKey}`, (error, stats) => { - const modifiedSince = (req.headers['if-modified-since'] && - new Date(req.headers['if-modified-since'])); - const lastModifiedCache = !error && stats.mtime; - if (lastModifiedCache && responseCache[cacheKey]) { - req.headers['if-modified-since'] = lastModifiedCache.toUTCString(); + const stats = await fsp.stat(`${CACHE_DIR}minified_${cacheKey}`).catch(() => {}); + const modifiedSince = + req.headers['if-modified-since'] && new Date(req.headers['if-modified-since']); + if (stats != null && stats.mtime && responseCache[cacheKey]) { + req.headers['if-modified-since'] = stats.mtime.toUTCString(); + } else { + delete req.headers['if-modified-since']; + } + + // Always issue get to downstream. + oldReq.method = req.method; + req.method = 'GET'; + + // This handles read/write synchronization as well as its predecessor, + // which is to say, not at all. + // TODO: Implement locking on write or ditch caching of gzip and use + // existing middlewares. + const respond = () => { + req.method = oldReq.method || req.method; + res.write = oldRes.write || res.write; + res.end = oldRes.end || res.end; + + const headers = {}; + Object.assign(headers, (responseCache[cacheKey].headers || {})); + const statusCode = responseCache[cacheKey].statusCode; + + let pathStr = `${CACHE_DIR}minified_${cacheKey}`; + if (supportsGzip && /application\/javascript/.test(headers['content-type'])) { + pathStr += '.gz'; + headers['content-encoding'] = 'gzip'; + } + + const lastModified = headers['last-modified'] && new Date(headers['last-modified']); + + if (statusCode === 200 && lastModified <= modifiedSince) { + res.writeHead(304, headers); + res.end(); + } else if (req.method === 'GET') { + const readStream = fs.createReadStream(pathStr); + res.writeHead(statusCode, headers); + readStream.pipe(res); } else { - delete req.headers['if-modified-since']; + res.writeHead(statusCode, headers); + res.end(); } + }; - // Always issue get to downstream. - old_req.method = req.method; - req.method = 'GET'; + const expirationDate = new Date(((responseCache[cacheKey] || {}).headers || {}).expires); + if (expirationDate > new Date()) { + // Our cached version is still valid. + return respond(); + } - const expirationDate = new Date(((responseCache[cacheKey] || {}).headers || {}).expires); - if (expirationDate > new Date()) { - // Our cached version is still valid. - return respond(); + const _headers = {}; + oldRes.setHeader = res.setHeader; + res.setHeader = (key, value) => { + // Don't set cookies, see issue #707 + if (key.toLowerCase() === 'set-cookie') return; + + _headers[key.toLowerCase()] = value; + oldRes.setHeader.call(res, key, value); + }; + + oldRes.writeHead = res.writeHead; + res.writeHead = (status, headers) => { + res.writeHead = oldRes.writeHead; + if (status === 200) { + // Update cache + let buffer = ''; + + Object.keys(headers || {}).forEach((key) => { + res.setHeader(key, headers[key]); + }); + headers = _headers; + + oldRes.write = res.write; + oldRes.end = res.end; + res.write = (data, encoding) => { + buffer += data.toString(encoding); + }; + res.end = async (data, encoding) => { + await Promise.all([ + fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}`, buffer).catch(() => {}), + util.promisify(zlib.gzip)(buffer) + .then((content) => fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}.gz`, content)) + .catch(() => {}), + ]); + responseCache[cacheKey] = {statusCode: status, headers}; + respond(); + }; + } else if (status === 304) { + // Nothing new changed from the cached version. + oldRes.write = res.write; + oldRes.end = res.end; + res.write = (data, encoding) => {}; + res.end = (data, encoding) => { respond(); }; + } else { + res.writeHead(status, headers); } + }; - const _headers = {}; - old_res.setHeader = res.setHeader; - res.setHeader = (key, value) => { - // Don't set cookies, see issue #707 - if (key.toLowerCase() === 'set-cookie') return; - - _headers[key.toLowerCase()] = value; - old_res.setHeader.call(res, key, value); - }; - - old_res.writeHead = res.writeHead; - res.writeHead = function (status, headers) { - res.writeHead = old_res.writeHead; - if (status === 200) { - // Update cache - let buffer = ''; - - Object.keys(headers || {}).forEach((key) => { - res.setHeader(key, headers[key]); - }); - headers = _headers; - - old_res.write = res.write; - old_res.end = res.end; - res.write = function (data, encoding) { - buffer += data.toString(encoding); - }; - res.end = function (data, encoding) { - async.parallel([ - function (callback) { - const path = `${CACHE_DIR}minified_${cacheKey}`; - fs.writeFile(path, buffer, (error, stats) => { - callback(); - }); - }, - function (callback) { - const path = `${CACHE_DIR}minified_${cacheKey}.gz`; - zlib.gzip(buffer, (error, content) => { - if (error) { - callback(); - } else { - fs.writeFile(path, content, (error, stats) => { - callback(); - }); - } - }); - }, - ], () => { - responseCache[cacheKey] = {statusCode: status, headers}; - respond(); - }); - }; - } else if (status === 304) { - // Nothing new changed from the cached version. - old_res.write = res.write; - old_res.end = res.end; - res.write = function (data, encoding) {}; - res.end = function (data, encoding) { respond(); }; - } else { - res.writeHead(status, headers); - } - }; - - next(undefined, req, res); - - // This handles read/write synchronization as well as its predecessor, - // which is to say, not at all. - // TODO: Implement locking on write or ditch caching of gzip and use - // existing middlewares. - function respond() { - req.method = old_req.method || req.method; - res.write = old_res.write || res.write; - res.end = old_res.end || res.end; - - const headers = {}; - Object.assign(headers, (responseCache[cacheKey].headers || {})); - const statusCode = responseCache[cacheKey].statusCode; - - let pathStr = `${CACHE_DIR}minified_${cacheKey}`; - if (supportsGzip && /application\/javascript/.test(headers['content-type'])) { - pathStr += '.gz'; - headers['content-encoding'] = 'gzip'; - } - - const lastModified = (headers['last-modified'] && - new Date(headers['last-modified'])); - - if (statusCode === 200 && lastModified <= modifiedSince) { - res.writeHead(304, headers); - res.end(); - } else if (req.method === 'GET') { - const readStream = fs.createReadStream(pathStr); - res.writeHead(statusCode, headers); - readStream.pipe(res); - } else { - res.writeHead(statusCode, headers); - res.end(); - } - } - }); - }; - - this.handle = handle; -}(); - -module.exports = CachingMiddleware; + next(undefined, req, res); + } +}; diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json index 04ac8604..8fcc45aa 100644 --- a/src/node/utils/tar.json +++ b/src/node/utils/tar.json @@ -2,6 +2,9 @@ "pad.js": [ "pad.js" , "pad_utils.js" + , "$js-cookie/src/js.cookie.js" + , "security.js" + , "$security.js" , "vendors/browser.js" , "pad_cookie.js" , "pad_editor.js" @@ -22,12 +25,14 @@ , "vendors/farbtastic.js" , "skin_variants.js" , "socketio.js" + , "colorutils.js" ] , "timeslider.js": [ "timeslider.js" , "colorutils.js" , "draggable.js" , "pad_utils.js" + , "$js-cookie/src/js.cookie.js" , "vendors/browser.js" , "pad_cookie.js" , "pad_editor.js" @@ -46,6 +51,8 @@ , "broadcast_slider.js" , "broadcast_revisions.js" , "socketio.js" + , "AttributeManager.js" + , "ChangesetUtils.js" ] , "ace2_inner.js": [ "ace2_inner.js" @@ -65,6 +72,10 @@ , "AttributeManager.js" , "scroll.js" , "caretPosition.js" + , "pad_utils.js" + , "$js-cookie/src/js.cookie.js" + , "security.js" + , "$security.js" ] , "ace2_common.js": [ "ace2_common.js" @@ -72,7 +83,7 @@ , "vendors/jquery.js" , "rjquery.js" , "$async.js" - , "vendors/underscore.js" + , "underscore.js" , "$underscore.js" , "$underscore/underscore.js" , "security.js" @@ -82,5 +93,4 @@ , "pluginfw/shared.js" , "pluginfw/hooks.js" ] -, "jquery.js": ["jquery.js"] } diff --git a/src/package-lock.json b/src/package-lock.json index 90481d15..f22457e1 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1,6 +1,6 @@ { "name": "ep_etherpad-lite", - "version": "1.8.10", + "version": "1.8.11", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/package.json b/src/package.json index dee03f1f..6f3e5161 100644 --- a/src/package.json +++ b/src/package.json @@ -237,6 +237,6 @@ "test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", "test-container": "mocha --timeout 5000 tests/container/specs/api" }, - "version": "1.8.10", + "version": "1.8.11", "license": "Apache-2.0" } diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js index 6cda86c4..c89fc410 100644 --- a/src/static/js/AttributeManager.js +++ b/src/static/js/AttributeManager.js @@ -2,7 +2,7 @@ const Changeset = require('./Changeset'); const ChangesetUtils = require('./ChangesetUtils'); -const _ = require('./vendors/underscore'); +const _ = require('./underscore'); const lineMarkerAttribute = 'lmkr'; diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 40b8625c..4038096e 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -195,6 +195,9 @@ const Ace2Editor = function () { pushStyleTagsFor(iframeHTML, includedCSS); iframeHTML.push(``); + // fill the cache + iframeHTML.push(``); + iframeHTML.push(``); iframeHTML.push(scriptTag( `\n\ @@ -202,11 +205,12 @@ require.setRootURI("../javascripts/src");\n\ require.setLibraryURI("../javascripts/lib");\n\ require.setGlobalKeyPath("require");\n\ \n\ +// intentially moved before requiring client_plugins to save a 307 +var Ace2Inner = require("ep_etherpad-lite/static/js/ace2_inner");\n\ var plugins = require("ep_etherpad-lite/static/js/pluginfw/client_plugins");\n\ plugins.adoptPluginsFromAncestorsOf(window);\n\ \n\ $ = jQuery = require("ep_etherpad-lite/static/js/rjquery").jQuery; // Expose jQuery #HACK\n\ -var Ace2Inner = require("ep_etherpad-lite/static/js/ace2_inner");\n\ \n\ plugins.ensure(function () {\n\ Ace2Inner.init();\n\ diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js index 871e8c9b..26370f6b 100644 --- a/src/static/js/broadcast.js +++ b/src/static/js/broadcast.js @@ -28,7 +28,7 @@ const AttribPool = require('./AttributePool'); const Changeset = require('./Changeset'); const linestylefilter = require('./linestylefilter').linestylefilter; const colorutils = require('./colorutils').colorutils; -const _ = require('./vendors/underscore'); +const _ = require('./underscore'); const hooks = require('./pluginfw/hooks'); // These parameters were global, now they are injected. A reference to the diff --git a/src/static/js/broadcast_slider.js b/src/static/js/broadcast_slider.js index 816a67a2..f03a2283 100644 --- a/src/static/js/broadcast_slider.js +++ b/src/static/js/broadcast_slider.js @@ -23,7 +23,7 @@ // These parameters were global, now they are injected. A reference to the // Timeslider controller would probably be more appropriate. -const _ = require('./vendors/underscore'); +const _ = require('./underscore'); const padmodals = require('./pad_modals').padmodals; const colorutils = require('./colorutils').colorutils; diff --git a/src/static/js/domline.js b/src/static/js/domline.js index 727d81ed..324e1353 100644 --- a/src/static/js/domline.js +++ b/src/static/js/domline.js @@ -24,7 +24,7 @@ const Security = require('./security'); const hooks = require('./pluginfw/hooks'); -const _ = require('./vendors/underscore'); +const _ = require('./underscore'); const lineAttributeMarker = require('./linestylefilter').lineAttributeMarker; const noop = () => {}; diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index 006cf008..4ea74010 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -23,7 +23,7 @@ */ const Ace2Common = require('./ace2_common'); -const _ = require('./vendors/underscore'); +const _ = require('./underscore'); const noop = Ace2Common.noop; diff --git a/src/static/js/vendors/underscore.js b/src/static/js/underscore.js similarity index 100% rename from src/static/js/vendors/underscore.js rename to src/static/js/underscore.js diff --git a/src/static/js/undomodule.js b/src/static/js/undomodule.js index 294b23bb..b8270b80 100644 --- a/src/static/js/undomodule.js +++ b/src/static/js/undomodule.js @@ -23,7 +23,7 @@ */ const Changeset = require('./Changeset'); -const _ = require('./vendors/underscore'); +const _ = require('./underscore'); const undoModule = (() => { const stack = (() => { diff --git a/src/static/js/vendors/nice-select.js b/src/static/js/vendors/nice-select.js index 8975f03e..ee1fc611 100644 --- a/src/static/js/vendors/nice-select.js +++ b/src/static/js/vendors/nice-select.js @@ -63,7 +63,7 @@ .addClass($select.attr('class') || '') .addClass($select.attr('disabled') ? 'disabled' : '') .attr('tabindex', $select.attr('disabled') ? null : '0') - .html('