PadMessageHandler: Rename client to socket

The `client` variable is actually a socket.io Socket object. Rename it
to reduce confusion.
This commit is contained in:
Richard Hansen 2020-10-20 18:38:56 -04:00 committed by John McLear
parent cfc7e47db0
commit 91268e14b7
2 changed files with 137 additions and 159 deletions

View file

@ -513,7 +513,8 @@ Called from: src/node/handler/PadMessageHandler.js
Things in context: Things in context:
1. message - the message being handled 1. message - the message being handled
2. client - the socket.io Socket object 2. socket - the socket.io Socket object
3. client - **deprecated** synonym of socket
This hook allows plugins to drop or modify incoming socket.io messages from This hook allows plugins to drop or modify incoming socket.io messages from
clients, before Etherpad processes them. clients, before Etherpad processes them.
@ -526,19 +527,19 @@ Examples:
``` ```
// Using an async function: // Using an async function:
exports.handleMessage = async (hookName, {message, client}) => { exports.handleMessage = async (hookName, {message, socket}) => {
if (message.type === 'USERINFO_UPDATE') { if (message.type === 'USERINFO_UPDATE') {
// Force the display name to the name associated with the account. // Force the display name to the name associated with the account.
const user = client.client.request.session.user || {}; const user = socket.client.request.session.user || {};
if (user.name) message.data.userInfo.name = user.name; if (user.name) message.data.userInfo.name = user.name;
} }
}; };
// Using a regular function: // Using a regular function:
exports.handleMessage = (hookName, {message, client}, callback) => { exports.handleMessage = (hookName, {message, socket}, callback) => {
if (message.type === 'USERINFO_UPDATE') { if (message.type === 'USERINFO_UPDATE') {
// Force the display name to the name associated with the account. // Force the display name to the name associated with the account.
const user = client.client.request.session.user || {}; const user = socket.client.request.session.user || {};
if (user.name) message.data.userInfo.name = user.name; if (user.name) message.data.userInfo.name = user.name;
} }
return cb(); return cb();
@ -551,7 +552,8 @@ Called from: src/node/handler/PadMessageHandler.js
Things in context: Things in context:
1. message - the message being handled 1. message - the message being handled
2. client - the socket.io Socket object 2. socket - the socket.io Socket object
3. client - **deprecated** synonym of socket
This hook allows plugins to grant temporary write access to a pad. It is called This hook allows plugins to grant temporary write access to a pad. It is called
for each incoming message from a client. If write access is granted, it applies for each incoming message from a client. If write access is granted, it applies
@ -568,14 +570,14 @@ Examples:
``` ```
// Using an async function: // Using an async function:
exports.handleMessageSecurity = async (hookName, {message, client}) => { exports.handleMessageSecurity = async (hookName, {message, socket}) => {
if (shouldGrantWriteAccess(message, client)) return true; if (shouldGrantWriteAccess(message, socket)) return true;
return; return;
}; };
// Using a regular function: // Using a regular function:
exports.handleMessageSecurity = (hookName, {message, client}, callback) => { exports.handleMessageSecurity = (hookName, {message, socket}, callback) => {
if (shouldGrantWriteAccess(message, client)) return callback(true); if (shouldGrantWriteAccess(message, socket)) return callback(true);
return callback(); return callback();
}; };
``` ```

View file

@ -18,6 +18,7 @@
* limitations under the License. * limitations under the License.
*/ */
/* global exports, process, require */
var padManager = require("../db/PadManager"); var padManager = require("../db/PadManager");
var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var Changeset = require("ep_etherpad-lite/static/js/Changeset");
@ -66,8 +67,8 @@ stats.gauge('totalUsers', function() {
/** /**
* A changeset queue per pad that is processed by handleUserChanges() * A changeset queue per pad that is processed by handleUserChanges()
*/ */
var padChannels = new channels.channels(function(data, callback) { const padChannels = new channels.channels(({socket, message}, callback) => {
return nodeify(handleUserChanges(data), callback); return nodeify(handleUserChanges(socket, message), callback);
}); });
/** /**
@ -86,19 +87,17 @@ exports.setSocketIO = function(socket_io)
/** /**
* Handles the connection of a new user * Handles the connection of a new user
* @param client the new client * @param socket the socket.io Socket object for the new connection from the client
*/ */
exports.handleConnect = function(client) exports.handleConnect = (socket) => {
{
stats.meter('connects').mark(); stats.meter('connects').mark();
// Initalize sessioninfos for this new session // Initalize sessioninfos for this new session
sessioninfos[client.id]={}; sessioninfos[socket.id] = {};
} };
/** /**
* Kicks all sessions from a pad * Kicks all sessions from a pad
* @param client the new client
*/ */
exports.kickSessionsFromPad = function(padID) exports.kickSessionsFromPad = function(padID)
{ {
@ -106,7 +105,7 @@ exports.kickSessionsFromPad = function(padID)
return; return;
// skip if there is nobody on this pad // skip if there is nobody on this pad
if(_getRoomClients(padID).length === 0) if(_getRoomSockets(padID).length === 0)
return; return;
// disconnect everyone from this pad // disconnect everyone from this pad
@ -115,22 +114,21 @@ exports.kickSessionsFromPad = function(padID)
/** /**
* Handles the disconnection of a user * Handles the disconnection of a user
* @param client the client that leaves * @param socket the socket.io Socket object for the client
*/ */
exports.handleDisconnect = async function(client) exports.handleDisconnect = async (socket) => {
{
stats.meter('disconnects').mark(); stats.meter('disconnects').mark();
// save the padname of this session // save the padname of this session
let session = sessioninfos[client.id]; const session = sessioninfos[socket.id];
// if this connection was already etablished with a handshake, send a disconnect message to the others // if this connection was already etablished with a handshake, send a disconnect message to the others
if (session && session.author) { if (session && session.author) {
const {session: {user} = {}} = client.client.request; const {session: {user} = {}} = socket.client.request;
accessLogger.info('[LEAVE]' + accessLogger.info('[LEAVE]' +
` pad:${session.padId}` + ` pad:${session.padId}` +
` socket:${client.id}` + ` socket:${socket.id}` +
` IP:${settings.disableIPlogging ? 'ANONYMOUS' : client.request.ip}` + ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` +
` authorID:${session.author}` + ` authorID:${session.author}` +
((user && user.username) ? ` username:${user.username}` : '')); ((user && user.username) ? ` username:${user.username}` : ''));
@ -152,33 +150,32 @@ exports.handleDisconnect = async function(client)
}; };
// Go through 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); socket.broadcast.to(session.padId).json.send(messageToTheOtherUsers);
// Allow plugins to hook into users leaving the pad // Allow plugins to hook into users leaving the pad
hooks.callAll("userLeave", session); hooks.callAll("userLeave", session);
} }
// Delete the sessioninfos entrys of this session // Delete the sessioninfos entrys of this session
delete sessioninfos[client.id]; delete sessioninfos[socket.id];
} };
/** /**
* Handles a message from a user * Handles a message from a user
* @param client the client that send this message * @param socket the socket.io Socket object for the client
* @param message the message from the client * @param message the message from the client
*/ */
exports.handleMessage = async function(client, message) exports.handleMessage = async (socket, message) => {
{
var env = process.env.NODE_ENV || 'development'; var env = process.env.NODE_ENV || 'development';
if (env === 'production') { if (env === 'production') {
try { try {
await rateLimiter.consume(client.request.ip); // consume 1 point per event from IP await rateLimiter.consume(socket.request.ip); // consume 1 point per event from IP
} catch (e) { } catch (e) {
console.warn(`Rate limited: ${client.request.ip} to reduce the amount of rate limiting ` + console.warn(`Rate limited: ${socket.request.ip} to reduce the amount of rate limiting ` +
'that happens edit the rateLimit values in settings.json'); 'that happens edit the rateLimit values in settings.json');
stats.meter('rateLimited').mark(); stats.meter('rateLimited').mark();
client.json.send({disconnect:"rateLimited"}); socket.json.send({disconnect: 'rateLimited'});
return; return;
} }
} }
@ -191,7 +188,7 @@ exports.handleMessage = async function(client, message)
return; return;
} }
const thisSession = sessioninfos[client.id]; const thisSession = sessioninfos[socket.id];
if (!thisSession) { if (!thisSession) {
messageLogger.warn("Dropped message from an unknown connection.") messageLogger.warn("Dropped message from an unknown connection.")
@ -217,73 +214,74 @@ exports.handleMessage = async function(client, message)
padId = await readOnlyManager.getPadId(padId); padId = await readOnlyManager.getPadId(padId);
} }
const {session: {user} = {}} = client.client.request; const {session: {user} = {}} = socket.client.request;
const {accessStatus, authorID} = const {accessStatus, authorID} =
await securityManager.checkAccess(padId, auth.sessionID, auth.token, user); await securityManager.checkAccess(padId, auth.sessionID, auth.token, user);
if (accessStatus !== 'grant') { if (accessStatus !== 'grant') {
// Access denied. Send the reason to the user. // Access denied. Send the reason to the user.
client.json.send({accessStatus}); socket.json.send({accessStatus});
return; return;
} }
if (thisSession.author != null && thisSession.author !== authorID) { if (thisSession.author != null && thisSession.author !== authorID) {
messageLogger.warn( messageLogger.warn(
'Rejecting message from client because the author ID changed mid-session.' + 'Rejecting message from client because the author ID changed mid-session.' +
' Bad or missing token or sessionID?' + ' Bad or missing token or sessionID?' +
` socket:${client.id}` + ` socket:${socket.id}` +
` IP:${settings.disableIPlogging ? 'ANONYMOUS' : client.request.ip}` + ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` +
` originalAuthorID:${thisSession.author}` + ` originalAuthorID:${thisSession.author}` +
` newAuthorID:${authorID}` + ` newAuthorID:${authorID}` +
((user && user.username) ? ` username:${user.username}` : '') + ((user && user.username) ? ` username:${user.username}` : '') +
` message:${message}`); ` message:${message}`);
client.json.send({disconnect: 'rejected'}); socket.json.send({disconnect: 'rejected'});
return; return;
} }
thisSession.author = authorID; thisSession.author = authorID;
// Allow plugins to bypass the readonly message blocker // Allow plugins to bypass the readonly message blocker
if ((await hooks.aCallAll('handleMessageSecurity', {client, message})).some((w) => w === true)) { const context = {message, socket, client: socket}; // `client` for backwards compatibility.
if ((await hooks.aCallAll('handleMessageSecurity', context)).some((w) => w === true)) {
thisSession.readonly = false; thisSession.readonly = false;
} }
// Call handleMessage hook. If a plugin returns null, the message will be dropped. // Call handleMessage hook. If a plugin returns null, the message will be dropped.
if ((await hooks.aCallAll('handleMessage', {client, message})).some((m) => m === null)) { if ((await hooks.aCallAll('handleMessage', context)).some((m) => m === null)) {
return; return;
} }
// Drop the message if the client disconnected during the above processing. // Drop the message if the client disconnected during the above processing.
if (sessioninfos[client.id] !== thisSession) { if (sessioninfos[socket.id] !== thisSession) {
messageLogger.warn('Dropping message from a connection that has gone away.') messageLogger.warn('Dropping message from a connection that has gone away.')
return; return;
} }
// Check what type of message we get and delegate to the other methods // Check what type of message we get and delegate to the other methods
if (message.type === "CLIENT_READY") { if (message.type === "CLIENT_READY") {
await handleClientReady(client, message, authorID); await handleClientReady(socket, message, authorID);
} else if (message.type === "CHANGESET_REQ") { } else if (message.type === "CHANGESET_REQ") {
await handleChangesetRequest(client, message); await handleChangesetRequest(socket, message);
} else if(message.type === "COLLABROOM") { } else if(message.type === "COLLABROOM") {
if (thisSession.readonly) { if (thisSession.readonly) {
messageLogger.warn("Dropped message, COLLABROOM for readonly pad"); messageLogger.warn("Dropped message, COLLABROOM for readonly pad");
} else if (message.data.type === "USER_CHANGES") { } else if (message.data.type === "USER_CHANGES") {
stats.counter('pendingEdits').inc() stats.counter('pendingEdits').inc()
padChannels.emit(message.padId, {client: client, message: message}); // add to pad queue padChannels.emit(message.padId, {socket, message}); // add to pad queue
} else if (message.data.type === "USERINFO_UPDATE") { } else if (message.data.type === "USERINFO_UPDATE") {
await handleUserInfoUpdate(client, message); await handleUserInfoUpdate(socket, message);
} else if (message.data.type === "CHAT_MESSAGE") { } else if (message.data.type === "CHAT_MESSAGE") {
await handleChatMessage(client, message); await handleChatMessage(socket, message);
} else if (message.data.type === "GET_CHAT_MESSAGES") { } else if (message.data.type === "GET_CHAT_MESSAGES") {
await handleGetChatMessages(client, message); await handleGetChatMessages(socket, message);
} else if (message.data.type === "SAVE_REVISION") { } else if (message.data.type === "SAVE_REVISION") {
await handleSaveRevisionMessage(client, message); await handleSaveRevisionMessage(socket, message);
} else if (message.data.type === "CLIENT_MESSAGE" && } else if (message.data.type === "CLIENT_MESSAGE" &&
message.data.payload != null && message.data.payload != null &&
message.data.payload.type === "suggestUserName") { message.data.payload.type === "suggestUserName") {
handleSuggestUserName(client, message); handleSuggestUserName(socket, message);
} else { } else {
messageLogger.warn("Dropped message, unknown COLLABROOM Data Type " + message.data.type); messageLogger.warn("Dropped message, unknown COLLABROOM Data Type " + message.data.type);
} }
} else if(message.type === "SWITCH_TO_PAD") { } else if(message.type === "SWITCH_TO_PAD") {
await handleSwitchToPad(client, message, authorID); await handleSwitchToPad(socket, message, authorID);
} else { } else {
messageLogger.warn("Dropped message, unknown Message Type " + message.type); messageLogger.warn("Dropped message, unknown Message Type " + message.type);
} }
@ -292,16 +290,13 @@ exports.handleMessage = async function(client, message)
/** /**
* Handles a save revision message * Handles a save revision message
* @param client the client that send this message * @param socket the socket.io Socket object for the client
* @param message the message from the client * @param message the message from the client
*/ */
async function handleSaveRevisionMessage(client, message) async function handleSaveRevisionMessage(socket, message) {
{ const {padId, author: authorId} = sessioninfos[socket.id];
var padId = sessioninfos[client.id].padId; const pad = await padManager.getPad(padId);
var userId = sessioninfos[client.id].author; await pad.addSavedRevision(pad.head, authorId);
let pad = await padManager.getPad(padId);
await pad.addSavedRevision(pad.head, userId);
} }
/** /**
@ -343,17 +338,14 @@ exports.handleCustomMessage = function(padID, msgString) {
/** /**
* Handles a Chat Message * Handles a Chat Message
* @param client the client that send this message * @param socket the socket.io Socket object for the client
* @param message the message from the client * @param message the message from the client
*/ */
async function handleChatMessage(client, message) async function handleChatMessage(socket, message) {
{
var time = Date.now(); var time = Date.now();
var userId = sessioninfos[client.id].author;
var text = message.data.text; var text = message.data.text;
var padId = sessioninfos[client.id].padId; const {padId, author: authorId} = sessioninfos[socket.id];
await exports.sendChatMessageToPadClients(time, authorId, text, padId);
await exports.sendChatMessageToPadClients(time, userId, text, padId);
} }
/** /**
@ -387,11 +379,10 @@ exports.sendChatMessageToPadClients = async function(time, userId, text, padId)
/** /**
* Handles the clients request for more chat-messages * Handles the clients request for more chat-messages
* @param client the client that send this message * @param socket the socket.io Socket object for the client
* @param message the message from the client * @param message the message from the client
*/ */
async function handleGetChatMessages(client, message) async function handleGetChatMessages(socket, message) {
{
if (message.data.start == null) { if (message.data.start == null) {
messageLogger.warn("Dropped message, GetChatMessages Message has no start!"); messageLogger.warn("Dropped message, GetChatMessages Message has no start!");
return; return;
@ -411,7 +402,7 @@ async function handleGetChatMessages(client, message)
return; return;
} }
let padId = sessioninfos[client.id].padId; const padId = sessioninfos[socket.id].padId;
let pad = await padManager.getPad(padId); let pad = await padManager.getPad(padId);
let chatMessages = await pad.getChatMessages(start, end); let chatMessages = await pad.getChatMessages(start, end);
@ -424,16 +415,15 @@ async function handleGetChatMessages(client, message)
}; };
// send the messages back to the client // send the messages back to the client
client.json.send(infoMsg); socket.json.send(infoMsg);
} }
/** /**
* Handles a handleSuggestUserName, that means a user have suggest a userName for a other user * Handles a handleSuggestUserName, that means a user have suggest a userName for a other user
* @param client the client that send this message * @param socket the socket.io Socket object for the client
* @param message the message from the client * @param message the message from the client
*/ */
function handleSuggestUserName(client, message) function handleSuggestUserName(socket, message) {
{
// check if all ok // check if all ok
if (message.data.payload.newName == null) { if (message.data.payload.newName == null) {
messageLogger.warn("Dropped message, suggestUserName Message has no newName!"); messageLogger.warn("Dropped message, suggestUserName Message has no newName!");
@ -445,25 +435,23 @@ function handleSuggestUserName(client, message)
return; return;
} }
var padId = sessioninfos[client.id].padId; const padId = sessioninfos[socket.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) { _getRoomSockets(padId).forEach((socket) => {
var session = sessioninfos[client.id]; const session = sessioninfos[socket.id];
if (session && session.author === message.data.payload.unnamedId) { if (session && session.author === message.data.payload.unnamedId) {
client.json.send(message); socket.json.send(message);
} }
}); });
} }
/** /**
* Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations * Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations
* @param client the client that send this message * @param socket the socket.io Socket object for the client
* @param message the message from the client * @param message the message from the client
*/ */
async function handleUserInfoUpdate(client, message) async function handleUserInfoUpdate(socket, message) {
{
// check if all ok // check if all ok
if (message.data.userInfo == null) { if (message.data.userInfo == null) {
messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no userInfo!"); messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no userInfo!");
@ -476,7 +464,7 @@ async function handleUserInfoUpdate(client, message)
} }
// Check that we have a valid session and author to update. // Check that we have a valid session and author to update.
var session = sessioninfos[client.id]; const session = sessioninfos[socket.id];
if (!session || !session.author || !session.padId) { if (!session || !session.author || !session.padId) {
messageLogger.warn("Dropped message, USERINFO_UPDATE Session not ready." + message.data); messageLogger.warn("Dropped message, USERINFO_UPDATE Session not ready." + message.data);
return; return;
@ -517,7 +505,7 @@ async 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); socket.broadcast.to(padId).json.send(infoMsg);
// Block until the authorManager has stored the new attributes. // Block until the authorManager has stored the new attributes.
await p; await p;
@ -534,14 +522,10 @@ async function handleUserInfoUpdate(client, message)
* This function is based on a similar one in the original Etherpad. * This function is based on a similar one in the original Etherpad.
* See https://github.com/ether/pad/blob/master/etherpad/src/etherpad/collab/collab_server.js in the function applyUserChanges() * See https://github.com/ether/pad/blob/master/etherpad/src/etherpad/collab/collab_server.js in the function applyUserChanges()
* *
* @param client the client that send this message * @param socket the socket.io Socket object for the client
* @param message the message from the client * @param message the message from the client
*/ */
async function handleUserChanges(data) async function handleUserChanges(socket, message) {
{
var client = data.client
, message = data.message
// This one's no longer pending, as we're gonna process it now // This one's no longer pending, as we're gonna process it now
stats.counter('pendingEdits').dec() stats.counter('pendingEdits').dec()
@ -563,7 +547,7 @@ async function handleUserChanges(data)
// The client might disconnect between our callbacks. We should still // The client might disconnect between our callbacks. We should still
// finish processing the changeset, so keep a reference to the session. // finish processing the changeset, so keep a reference to the session.
const thisSession = sessioninfos[client.id]; const thisSession = sessioninfos[socket.id];
// TODO: this might happen with other messages too => find one place to copy the session // 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 // and always use the copy. atm a message will be ignored if the session is gone even
@ -629,9 +613,9 @@ async function handleUserChanges(data)
} catch(e) { } catch(e) {
// There is an error in this changeset, so just refuse it // There is an error in this changeset, so just refuse it
client.json.send({ disconnect: "badChangeset" }); socket.json.send({disconnect: 'badChangeset'});
stats.meter('failedChangesets').mark(); stats.meter('failedChangesets').mark();
throw new Error(`Can't apply USER_CHANGES from Socket ${client.id} because: ${e.message}`); throw new Error(`Can't apply USER_CHANGES from Socket ${socket.id} because: ${e.message}`);
} }
// ex. applyUserChanges // ex. applyUserChanges
@ -656,14 +640,14 @@ async function handleUserChanges(data)
// prevent eplite from accepting it TODO: better send the client a NEW_CHANGES // prevent eplite from accepting it TODO: better send the client a NEW_CHANGES
// of that revision // of that revision
if (baseRev + 1 === r && c === changeset) { if (baseRev + 1 === r && c === changeset) {
client.json.send({disconnect:"badChangeset"}); socket.json.send({disconnect: 'badChangeset'});
stats.meter('failedChangesets').mark(); stats.meter('failedChangesets').mark();
throw new Error("Won't apply USER_CHANGES, because it contains an already accepted changeset"); throw new Error("Won't apply USER_CHANGES, because it contains an already accepted changeset");
} }
changeset = Changeset.follow(c, changeset, false, apool); changeset = Changeset.follow(c, changeset, false, apool);
} catch(e) { } catch(e) {
client.json.send({disconnect:"badChangeset"}); socket.json.send({disconnect: 'badChangeset'});
stats.meter('failedChangesets').mark(); stats.meter('failedChangesets').mark();
throw new Error("Can't apply USER_CHANGES, because " + e.message); throw new Error("Can't apply USER_CHANGES, because " + e.message);
} }
@ -672,7 +656,7 @@ async function handleUserChanges(data)
let prevText = pad.text(); let prevText = pad.text();
if (Changeset.oldLen(changeset) !== prevText.length) { if (Changeset.oldLen(changeset) !== prevText.length) {
client.json.send({disconnect:"badChangeset"}); socket.json.send({disconnect: 'badChangeset'});
stats.meter('failedChangesets').mark(); stats.meter('failedChangesets').mark();
throw new Error("Can't apply USER_CHANGES "+changeset+" with oldLen " + Changeset.oldLen(changeset) + " to document of length " + prevText.length); throw new Error("Can't apply USER_CHANGES "+changeset+" with oldLen " + Changeset.oldLen(changeset) + " to document of length " + prevText.length);
} }
@ -680,7 +664,7 @@ async function handleUserChanges(data)
try { try {
await pad.appendRevision(changeset, thisSession.author); await pad.appendRevision(changeset, thisSession.author);
} catch(e) { } catch(e) {
client.json.send({ disconnect: "badChangeset" }); socket.json.send({disconnect: 'badChangeset'});
stats.meter('failedChangesets').mark(); stats.meter('failedChangesets').mark();
throw e; throw e;
} }
@ -707,11 +691,8 @@ async function handleUserChanges(data)
exports.updatePadClients = async function(pad) exports.updatePadClients = async function(pad)
{ {
// skip this if no-one is on this pad // skip this if no-one is on this pad
let roomClients = _getRoomClients(pad.id); const roomSockets = _getRoomSockets(pad.id);
if (roomSockets.length === 0) return;
if (roomClients.length === 0) {
return;
}
// since all clients usually get the same set of changesets, store them in local cache // since all clients usually get the same set of changesets, store them in local cache
// to remove unnecessary roundtrip to the datalayer // to remove unnecessary roundtrip to the datalayer
@ -723,8 +704,8 @@ exports.updatePadClients = async function(pad)
let revCache = {}; let revCache = {};
// go through all sessions on this pad // go through all sessions on this pad
for (let client of roomClients) { for (const socket of roomSockets) {
let sid = client.id; const sid = socket.id;
// send them all new changesets // send them all new changesets
while (sessioninfos[sid] && sessioninfos[sid].rev < pad.getHeadRevisionNumber()) { while (sessioninfos[sid] && sessioninfos[sid].rev < pad.getHeadRevisionNumber()) {
@ -745,7 +726,7 @@ exports.updatePadClients = async function(pad)
} }
if (author === sessioninfos[sid].author) { if (author === sessioninfos[sid].author) {
client.json.send({ "type": "COLLABROOM", "data":{ type: "ACCEPT_COMMIT", newRev: r }}); socket.json.send({type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev: r}});
} else { } else {
let forWire = Changeset.prepareForWire(revChangeset, pad.pool); let forWire = Changeset.prepareForWire(revChangeset, pad.pool);
let wireMsg = {"type": "COLLABROOM", let wireMsg = {"type": "COLLABROOM",
@ -758,7 +739,7 @@ exports.updatePadClients = async function(pad)
timeDelta: currentTime - sessioninfos[sid].time timeDelta: currentTime - sessioninfos[sid].time
}}; }};
client.json.send(wireMsg); socket.json.send(wireMsg);
} }
if (sessioninfos[sid]) { if (sessioninfos[sid]) {
@ -817,19 +798,18 @@ function _correctMarkersInPad(atext, apool) {
return builder.toString(); return builder.toString();
} }
async function handleSwitchToPad(client, message, _authorID) async function handleSwitchToPad(socket, message, _authorID) {
{ const currentSessionInfo = sessioninfos[socket.id];
const currentSessionInfo = sessioninfos[client.id];
const padId = currentSessionInfo.padId; const padId = currentSessionInfo.padId;
// Check permissions for the new pad. // Check permissions for the new pad.
const newPadIds = await readOnlyManager.getIds(message.padId); const newPadIds = await readOnlyManager.getIds(message.padId);
const {session: {user} = {}} = client.client.request; const {session: {user} = {}} = socket.client.request;
const {accessStatus, authorID} = await securityManager.checkAccess( const {accessStatus, authorID} = await securityManager.checkAccess(
newPadIds.padId, message.sessionID, message.token, user); newPadIds.padId, message.sessionID, message.token, user);
if (accessStatus !== 'grant') { if (accessStatus !== 'grant') {
// Access denied. Send the reason to the user. // Access denied. Send the reason to the user.
client.json.send({accessStatus}); socket.json.send({accessStatus});
return; return;
} }
// The same token and session ID were passed to checkAccess in handleMessage, so this second call // The same token and session ID were passed to checkAccess in handleMessage, so this second call
@ -838,22 +818,22 @@ async function handleSwitchToPad(client, message, _authorID)
assert(authorID === currentSessionInfo.author); assert(authorID === currentSessionInfo.author);
// Check if the connection dropped during the access check. // Check if the connection dropped during the access check.
if (sessioninfos[client.id] !== currentSessionInfo) return; if (sessioninfos[socket.id] !== currentSessionInfo) return;
// clear the session and leave the room // clear the session and leave the room
_getRoomClients(padId).forEach(client => { _getRoomSockets(padId).forEach((socket) => {
let sinfo = sessioninfos[client.id]; const sinfo = sessioninfos[socket.id];
if (sinfo && sinfo.author === currentSessionInfo.author) { if (sinfo && sinfo.author === currentSessionInfo.author) {
// fix user's counter, works on page refresh or if user closes browser window and then rejoins // fix user's counter, works on page refresh or if user closes browser window and then rejoins
sessioninfos[client.id] = {}; sessioninfos[socket.id] = {};
client.leave(padId); socket.leave(padId);
} }
}); });
// start up the new pad // start up the new pad
const newSessionInfo = sessioninfos[client.id]; const newSessionInfo = sessioninfos[socket.id];
createSessionInfoAuth(newSessionInfo, message); createSessionInfoAuth(newSessionInfo, message);
await handleClientReady(client, message, authorID); await handleClientReady(socket, message, authorID);
} }
// Creates/replaces the auth object in the given session info. // Creates/replaces the auth object in the given session info.
@ -874,11 +854,10 @@ function createSessionInfoAuth(sessionInfo, message)
/** /**
* Handles a CLIENT_READY. A CLIENT_READY is the first message from the client to the server. The Client sends his token * Handles a CLIENT_READY. A CLIENT_READY is the first message from the client to the server. The Client sends his token
* and the pad it wants to enter. The Server answers with the inital values (clientVars) of the pad * and the pad it wants to enter. The Server answers with the inital values (clientVars) of the pad
* @param client the client that send this message * @param socket the socket.io Socket object for the client
* @param message the message from the client * @param message the message from the client
*/ */
async function handleClientReady(client, message, authorID) async function handleClientReady(socket, message, authorID) {
{
// check if all ok // check if all ok
if (!message.token) { if (!message.token) {
messageLogger.warn("Dropped message, CLIENT_READY Message has no token!"); messageLogger.warn("Dropped message, CLIENT_READY Message has no token!");
@ -935,19 +914,19 @@ async function handleClientReady(client, message, authorID)
// glue the clientVars together, send them and tell the other clients that a new one is there // glue the clientVars together, send them and tell the other clients that a new one is there
// Check that the client is still here. It might have disconnected between callbacks. // Check that the client is still here. It might have disconnected between callbacks.
const sessionInfo = sessioninfos[client.id]; const sessionInfo = sessioninfos[socket.id];
if (sessionInfo == null) return; if (sessionInfo == null) return;
// Check if this author is already on the pad, if yes, kick the other sessions! // Check if this author is already on the pad, if yes, kick the other sessions!
let roomClients = _getRoomClients(pad.id); const roomSockets = _getRoomSockets(pad.id);
for (let client of roomClients) { for (const socket of roomSockets) {
let sinfo = sessioninfos[client.id]; const sinfo = sessioninfos[socket.id];
if (sinfo && sinfo.author === authorID) { if (sinfo && sinfo.author === authorID) {
// fix user's counter, works on page refresh or if user closes browser window and then rejoins // fix user's counter, works on page refresh or if user closes browser window and then rejoins
sessioninfos[client.id] = {}; sessioninfos[socket.id] = {};
client.leave(padIds.padId); socket.leave(padIds.padId);
client.json.send({disconnect:"userdup"}); socket.json.send({disconnect: 'userdup'});
} }
} }
@ -955,20 +934,20 @@ async function handleClientReady(client, message, authorID)
sessionInfo.padId = padIds.padId; sessionInfo.padId = padIds.padId;
sessionInfo.readOnlyPadId = padIds.readOnlyPadId; sessionInfo.readOnlyPadId = padIds.readOnlyPadId;
sessionInfo.readonly = sessionInfo.readonly =
padIds.readonly || !webaccess.userCanModify(message.padId, client.client.request); padIds.readonly || !webaccess.userCanModify(message.padId, socket.client.request);
const {session: {user} = {}} = client.client.request; const {session: {user} = {}} = socket.client.request;
accessLogger.info(`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` + accessLogger.info(`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` +
` pad:${padIds.padId}` + ` pad:${padIds.padId}` +
` socket:${client.id}` + ` socket:${socket.id}` +
` IP:${settings.disableIPlogging ? 'ANONYMOUS' : client.request.ip}` + ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` +
` authorID:${authorID}` + ` authorID:${authorID}` +
((user && user.username) ? ` username:${user.username}` : '')); ((user && user.username) ? ` username:${user.username}` : ''));
if (message.reconnect) { if (message.reconnect) {
// If this is a reconnect, we don't have to send the client the ClientVars again // If this is a reconnect, we don't have to send the client the ClientVars again
// Join the pad and start receiving updates // Join the pad and start receiving updates
client.join(padIds.padId); socket.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
sessionInfo.rev = message.client_rev; sessionInfo.rev = message.client_rev;
@ -1019,7 +998,7 @@ async function handleClientReady(client, message, authorID)
author: changesets[r]['author'], author: changesets[r]['author'],
currentTime: changesets[r]['timestamp'] currentTime: changesets[r]['timestamp']
}}; }};
client.json.send(wireMsg); socket.json.send(wireMsg);
} }
if (startNum === endNum) { if (startNum === endNum) {
@ -1028,7 +1007,7 @@ async function handleClientReady(client, message, authorID)
noChanges: true, noChanges: true,
newRev: pad.getHeadRevisionNumber() newRev: pad.getHeadRevisionNumber()
}}; }};
client.json.send(Msg); socket.json.send(Msg);
} }
} else { } else {
@ -1042,7 +1021,7 @@ async function handleClientReady(client, message, authorID)
atext.attribs = attribsForWire.translated; atext.attribs = attribsForWire.translated;
} catch(e) { } catch(e) {
console.error(e.stack || e) console.error(e.stack || e)
client.json.send({ disconnect:"corruptPad" }); // pull the brakes socket.json.send({disconnect: 'corruptPad'}); // pull the brakes
return; return;
} }
@ -1084,7 +1063,7 @@ async function handleClientReady(client, message, authorID)
// tell the client the number of the latest chat-message, which will be // 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) // used to request the latest 100 chat-messages later (GET_CHAT_MESSAGES)
"chatHead": pad.chatHead, "chatHead": pad.chatHead,
"numConnectedUsers": roomClients.length, "numConnectedUsers": roomSockets.length,
"readOnlyId": padIds.readOnlyPadId, "readOnlyId": padIds.readOnlyPadId,
"readonly": sessionInfo.readonly, "readonly": sessionInfo.readonly,
"serverTimestamp": Date.now(), "serverTimestamp": Date.now(),
@ -1115,7 +1094,7 @@ async function handleClientReady(client, message, authorID)
} }
// call the clientVars-hook so plugins can modify them before they get sent to the client // call the clientVars-hook so plugins can modify them before they get sent to the client
let messages = await hooks.aCallAll('clientVars', {clientVars, pad, socket: client}); const messages = await hooks.aCallAll('clientVars', {clientVars, pad, socket});
// combine our old object with the new attributes from the hook // combine our old object with the new attributes from the hook
for (let msg of messages) { for (let msg of messages) {
@ -1123,10 +1102,10 @@ async function handleClientReady(client, message, authorID)
} }
// Join the pad and start receiving updates // Join the pad and start receiving updates
client.join(padIds.padId); socket.join(padIds.padId);
// Send the clientVars to the Client // Send the clientVars to the Client
client.json.send({type: "CLIENT_VARS", data: clientVars}); socket.json.send({type: 'CLIENT_VARS', data: clientVars});
// Save the current revision in sessioninfos, should be the same as in clientVars // Save the current revision in sessioninfos, should be the same as in clientVars
sessionInfo.rev = pad.getHeadRevisionNumber(); sessionInfo.rev = pad.getHeadRevisionNumber();
@ -1151,20 +1130,19 @@ async function handleClientReady(client, message, authorID)
} }
// notify all existing users about new user // notify all existing users about new user
client.broadcast.to(padIds.padId).json.send(messageToTheOtherUsers); socket.broadcast.to(padIds.padId).json.send(messageToTheOtherUsers);
// Get sessions for this pad and update them (in parallel) // Get sessions for this pad and update them (in parallel)
roomClients = _getRoomClients(pad.id); await Promise.all(_getRoomSockets(pad.id).map(async (roomSocket) => {
await Promise.all(_getRoomClients(pad.id).map(async roomClient => {
// Jump over, if this session is the connection session // Jump over, if this session is the connection session
if (roomClient.id === client.id) { if (roomSocket.id === socket.id) {
return; return;
} }
// Since sessioninfos might change while being enumerated, check if the // Since sessioninfos might change while being enumerated, check if the
// sessionID is still assigned to a valid session // sessionID is still assigned to a valid session
const sessionInfo = sessioninfos[roomClient.id]; const sessionInfo = sessioninfos[roomSocket.id];
if (sessionInfo == null) return; if (sessionInfo == null) return;
// get the authorname & colorId // get the authorname & colorId
@ -1211,7 +1189,7 @@ async function handleClientReady(client, message, authorID)
} }
}; };
client.json.send(msg); socket.json.send(msg);
})); }));
} }
} }
@ -1219,8 +1197,7 @@ async function handleClientReady(client, message, authorID)
/** /**
* Handles a request for a rough changeset, the timeslider client needs it * Handles a request for a rough changeset, the timeslider client needs it
*/ */
async function handleChangesetRequest(client, message) async function handleChangesetRequest(socket, message) {
{
// check if all ok // check if all ok
if (message.data == null) { if (message.data == null) {
messageLogger.warn("Dropped message, changeset request has no data!"); messageLogger.warn("Dropped message, changeset request has no data!");
@ -1263,7 +1240,7 @@ async function handleChangesetRequest(client, message)
try { try {
let data = await getChangesetInfo(padIds.padId, start, end, granularity); let data = await getChangesetInfo(padIds.padId, start, end, granularity);
data.requestID = message.data.requestID; data.requestID = message.data.requestID;
client.json.send({ type: "CHANGESET_REQ", data }); socket.json.send({type: 'CHANGESET_REQ', data});
} catch (err) { } catch (err) {
console.error('Error while handling a changeset request for ' + padIds.padId, err.toString(), message.data); console.error('Error while handling a changeset request for ' + padIds.padId, err.toString(), message.data);
} }
@ -1431,17 +1408,17 @@ async function composePadChangesets (padId, startNum, endNum)
} }
} }
function _getRoomClients(padID) { function _getRoomSockets(padID) {
var roomClients = []; const roomSockets = [];
var room = socketio.sockets.adapter.rooms[padID]; var room = socketio.sockets.adapter.rooms[padID];
if (room) { if (room) {
for (var id in room.sockets) { for (var id in room.sockets) {
roomClients.push(socketio.sockets.sockets[id]); roomSockets.push(socketio.sockets.sockets[id]);
} }
} }
return roomClients; return roomSockets;
} }
/** /**
@ -1449,7 +1426,7 @@ function _getRoomClients(padID) {
*/ */
exports.padUsersCount = function(padID) { exports.padUsersCount = function(padID) {
return { return {
padUsersCount: _getRoomClients(padID).length padUsersCount: _getRoomSockets(padID).length
} }
} }
@ -1459,11 +1436,10 @@ exports.padUsersCount = function(padID) {
exports.padUsers = async function(padID) { exports.padUsers = async function(padID) {
let padUsers = []; let padUsers = [];
let roomClients = _getRoomClients(padID);
// iterate over all clients (in parallel) // iterate over all clients (in parallel)
await Promise.all(roomClients.map(async roomClient => { await Promise.all(_getRoomSockets(padID).map(async (roomSocket) => {
let s = sessioninfos[roomClient.id]; const s = sessioninfos[roomSocket.id];
if (s) { if (s) {
return authorManager.getAuthor(s.author).then(author => { return authorManager.getAuthor(s.author).then(author => {
// Fixes: https://github.com/ether/etherpad-lite/issues/4120 // Fixes: https://github.com/ether/etherpad-lite/issues/4120