From d7afb4e5131bc2a99c22633bb2a852cb88c5401d Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 5 Feb 2015 16:37:37 +0000 Subject: [PATCH] Clean up unused real-time code in web --- services/web/app.coffee | 4 - .../ConnectedUsersController.coffee | 13 - .../ConnectedUsersManager.coffee | 79 ----- .../Features/Editor/EditorController.coffee | 142 --------- .../Editor/EditorHttpController.coffee | 24 +- .../Editor/EditorRealTimeController.coffee | 12 - .../Editor/EditorUpdatesController.coffee | 61 ---- .../infrastructure/BackgroundTasks.coffee | 7 - .../app/coffee/infrastructure/Server.coffee | 10 +- .../infrastructure/SocketIoConfig.coffee | 20 -- services/web/app/coffee/router.coffee | 68 +--- services/web/app/views/project/editor.jade | 2 +- .../online-users/OnlineUsersManager.coffee | 36 +-- .../ConnectedUsersControllerTests.coffee | 46 --- .../ConnectedUsersManagerTests.coffee | 154 --------- .../Editor/EditorControllerTests.coffee | 298 +----------------- .../Editor/EditorHttpControllerTests.coffee | 64 +++- .../EditorRealTimeControllerTests.coffee | 46 --- .../EditorUpdatesControllerTests.coffee | 164 ---------- 19 files changed, 106 insertions(+), 1144 deletions(-) delete mode 100644 services/web/app/coffee/Features/ConnectedUsers/ConnectedUsersController.coffee delete mode 100644 services/web/app/coffee/Features/ConnectedUsers/ConnectedUsersManager.coffee delete mode 100644 services/web/app/coffee/Features/Editor/EditorUpdatesController.coffee delete mode 100644 services/web/app/coffee/infrastructure/BackgroundTasks.coffee delete mode 100644 services/web/app/coffee/infrastructure/SocketIoConfig.coffee delete mode 100644 services/web/test/UnitTests/coffee/ConnectedUsers/ConnectedUsersControllerTests.coffee delete mode 100644 services/web/test/UnitTests/coffee/ConnectedUsers/ConnectedUsersManagerTests.coffee delete mode 100644 services/web/test/UnitTests/coffee/Editor/EditorUpdatesControllerTests.coffee diff --git a/services/web/app.coffee b/services/web/app.coffee index dbf91d01de..89faaba987 100644 --- a/services/web/app.coffee +++ b/services/web/app.coffee @@ -6,10 +6,8 @@ logger.logger.serializers.project = require("./app/js/infrastructure/LoggerSeria metrics = require("metrics-sharelatex") metrics.initialize("web") Server = require("./app/js/infrastructure/Server") -BackgroundTasks = require("./app/js/infrastructure/BackgroundTasks") Errors = require "./app/js/errors" - argv = require("optimist") .options("user", {alias : "u", description : "Run the server with permissions of the specified user"}) .options("group", {alias : "g", description : "Run the server with permissions of the specified group"}) @@ -32,8 +30,6 @@ if Settings.catchErrors process.on "uncaughtException", (error) -> logger.error err: error, "uncaughtException" -BackgroundTasks.run() - port = Settings.port or Settings.internal?.web?.port or 3000 host = Settings.internal.web.host or "localhost" Server.server.listen port, host, -> diff --git a/services/web/app/coffee/Features/ConnectedUsers/ConnectedUsersController.coffee b/services/web/app/coffee/Features/ConnectedUsers/ConnectedUsersController.coffee deleted file mode 100644 index 4acbbcab96..0000000000 --- a/services/web/app/coffee/Features/ConnectedUsers/ConnectedUsersController.coffee +++ /dev/null @@ -1,13 +0,0 @@ -ConnectedUsersManager = require("./ConnectedUsersManager") -logger = require("logger-sharelatex") - -module.exports = - - getConnectedUsers: (req, res)-> - project_id = req.params.Project_id - ConnectedUsersManager.getConnectedUsers project_id, (err, users)-> - if err? - logger.err err:err, project_id:project_id, "problem getting connected users" - return res.send 500 - res.send(users) - diff --git a/services/web/app/coffee/Features/ConnectedUsers/ConnectedUsersManager.coffee b/services/web/app/coffee/Features/ConnectedUsers/ConnectedUsersManager.coffee deleted file mode 100644 index caf4a45f53..0000000000 --- a/services/web/app/coffee/Features/ConnectedUsers/ConnectedUsersManager.coffee +++ /dev/null @@ -1,79 +0,0 @@ -_ = require("underscore") -async = require("async") -Settings = require('settings-sharelatex') -logger = require("logger-sharelatex") -redis = require("redis-sharelatex") -rclient = redis.createClient(Settings.redis.web) - - -ONE_HOUR_IN_S = 60 * 60 -ONE_DAY_IN_S = ONE_HOUR_IN_S * 24 -FOUR_DAYS_IN_S = ONE_DAY_IN_S * 4 - -USER_TIMEOUT_IN_S = ONE_HOUR_IN_S / 4 - -buildProjectSetKey = (project_id)-> return "clients_in_project:#{project_id}" -buildUserKey = (project_id, client_id)-> return "connected_user:#{project_id}:#{client_id}" - - -module.exports = - - # Use the same method for when a user connects, and when a user sends a cursor - # update. This way we don't care if the connected_user key has expired when - # we receive a cursor update. - updateUserPosition: (project_id, client_id, user, cursorData, callback = (err)->)-> - logger.log project_id:project_id, client_id:client_id, "marking user as connected" - - multi = rclient.multi() - - multi.sadd buildProjectSetKey(project_id), client_id - multi.expire buildProjectSetKey(project_id), FOUR_DAYS_IN_S - - multi.hset buildUserKey(project_id, client_id), "last_updated_at", Date.now() - multi.hset buildUserKey(project_id, client_id), "user_id", user._id - multi.hset buildUserKey(project_id, client_id), "first_name", user.first_name - multi.hset buildUserKey(project_id, client_id), "last_name", user.last_name - multi.hset buildUserKey(project_id, client_id), "email", user.email - - if cursorData? - multi.hset buildUserKey(project_id, client_id), "cursorData", JSON.stringify(cursorData) - multi.expire buildUserKey(project_id, client_id), USER_TIMEOUT_IN_S - - multi.exec (err)-> - if err? - logger.err err:err, project_id:project_id, client_id:client_id, "problem marking user as connected" - callback(err) - - markUserAsDisconnected: (project_id, client_id, callback)-> - logger.log project_id:project_id, client_id:client_id, "marking user as disconnected" - multi = rclient.multi() - multi.srem buildProjectSetKey(project_id), client_id - multi.expire buildProjectSetKey(project_id), FOUR_DAYS_IN_S - multi.del buildUserKey(project_id, client_id) - multi.exec callback - - - _getConnectedUser: (project_id, client_id, callback)-> - rclient.hgetall buildUserKey(project_id, client_id), (err, result)-> - if !result? - result = - connected : false - client_id:client_id - else - result.connected = true - result.client_id = client_id - if result.cursorData? - result.cursorData = JSON.parse(result.cursorData) - callback err, result - - getConnectedUsers: (project_id, callback)-> - self = @ - rclient.smembers buildProjectSetKey(project_id), (err, results)-> - jobs = results.map (client_id)-> - (cb)-> - self._getConnectedUser(project_id, client_id, cb) - async.series jobs, (err, users)-> - users = _.filter users, (user)-> - user.connected - callback err, users - diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee index 39a0bb02f0..51896b4fe4 100644 --- a/services/web/app/coffee/Features/Editor/EditorController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorController.coffee @@ -6,17 +6,13 @@ ProjectEntityHandler = require('../Project/ProjectEntityHandler') ProjectOptionsHandler = require('../Project/ProjectOptionsHandler') ProjectDetailsHandler = require('../Project/ProjectDetailsHandler') ProjectDeleter = require("../Project/ProjectDeleter") -ProjectGetter = require('../Project/ProjectGetter') -UserGetter = require('../User/UserGetter') CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') LimitationsManager = require("../Subscription/LimitationsManager") -AuthorizationManager = require("../Security/AuthorizationManager") EditorRealTimeController = require("./EditorRealTimeController") TrackChangesManager = require("../TrackChanges/TrackChangesManager") Settings = require('settings-sharelatex') async = require('async') -ConnectedUsersManager = require("../ConnectedUsers/ConnectedUsersManager") LockManager = require("../../infrastructure/LockManager") _ = require('underscore') redis = require("redis-sharelatex") @@ -24,144 +20,6 @@ rclientPub = redis.createClient(Settings.redis.web) rclientSub = redis.createClient(Settings.redis.web) module.exports = EditorController = - protocolVersion: 2 - - reportError: (client, clientError, callback = () ->) -> - client.get "project_id", (error, project_id) -> - client.get "user_id", (error, user_id) -> - logger.error err: clientError, project_id: project_id, user_id: user_id, "client error" - callback() - - joinProject: (client, user, project_id, callback) -> - logger.log user_id:user._id, project_id:project_id, "user joining project" - Metrics.inc "editor.join-project" - EditorController.buildJoinProjectView project_id, user._id, (error, project, privilegeLevel, protocolVersion) -> - return callback(error) if error? - if !privilegeLevel - callback new Error("Not authorized") - else - client.join(project_id) - client.set("project_id", project_id) - client.set("owner_id", project.owner._id) - client.set("user_id", user._id) - client.set("first_name", user.first_name) - client.set("last_name", user.last_name) - client.set("email", user.email) - client.set("connected_time", new Date()) - client.set("signup_date", user.signUpDate) - client.set("login_count", user.loginCount) - AuthorizationManager.setPrivilegeLevelOnClient client, privilegeLevel - - callback null, project, privilegeLevel, EditorController.protocolVersion - - # can be done after the connection has happened - ConnectedUsersManager.updateUserPosition project_id, client.id, user, null, -> - - # Only show the 'renamed or deleted' message once - if project.deletedByExternalDataSource - ProjectDeleter.unmarkAsDeletedByExternalSource project_id - - buildJoinProjectView: (project_id, user_id, callback = (error, project, privilegeLevel) ->) -> - ProjectGetter.getProjectWithoutDocLines project_id, (error, project) -> - return callback(error) if error? - return callback(new Error("not found")) if !project? - ProjectGetter.populateProjectWithUsers project, (error, project) -> - return callback(error) if error? - UserGetter.getUser user_id, { isAdmin: true }, (error, user) -> - return callback(error) if error? - AuthorizationManager.getPrivilegeLevelForProject project, user, (error, canAccess, privilegeLevel) -> - return callback(error) if error? - if !canAccess - callback null, null, false - else - callback(null, - ProjectEditorHandler.buildProjectModelView(project), - privilegeLevel - ) - - leaveProject: (client, user) -> - self = @ - client.get "project_id", (error, project_id) -> - return if error? or !project_id? - EditorRealTimeController.emitToRoom(project_id, "clientTracking.clientDisconnected", client.id) - ConnectedUsersManager.markUserAsDisconnected project_id, client.id, -> - logger.log user_id:user._id, project_id:project_id, "user leaving project" - self.flushProjectIfEmpty(project_id) - - joinDoc: (client, project_id, doc_id, fromVersion, callback = (error, docLines, version) ->) -> - # fromVersion is optional - if typeof fromVersion == "function" - callback = fromVersion - fromVersion = -1 - - client.get "user_id", (error, user_id) -> - logger.log user_id: user_id, project_id: project_id, doc_id: doc_id, "user joining doc" - Metrics.inc "editor.join-doc" - client.join doc_id - DocumentUpdaterHandler.getDocument project_id, doc_id, fromVersion, (err, docLines, version, ops)-> - # Encode any binary bits of data so it can go via WebSockets - # See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html - if docLines? - docLines = for line in docLines - if line.text? - try - line.text = unescape(encodeURIComponent(line.text)) - catch err - logger.err err:err, project_id:project_id, doc_id:doc_id, fromVersion:fromVersion, line:line, "error encoding line.text uri component" - else - try - line = unescape(encodeURIComponent(line)) - catch err - logger.err err:err, project_id:project_id, doc_id:doc_id, fromVersion:fromVersion, line:line, "error encoding line uri component" - line - callback(err, docLines, version, ops) - - leaveDoc: (client, project_id, doc_id, callback = (error) ->) -> - client.get "user_id", (error, user_id) -> - logger.log user_id: user_id, project_id: project_id, doc_id: doc_id, "user leaving doc" - Metrics.inc "editor.leave-doc" - client.leave doc_id - callback() - - flushProjectIfEmpty: (project_id, callback = ->)-> - setTimeout (-> - io = require('../../infrastructure/Server').io - peopleStillInProject = io.sockets.clients(project_id).length - logger.log project_id: project_id, connectedCount: peopleStillInProject, "flushing if empty" - if peopleStillInProject == 0 - DocumentUpdaterHandler.flushProjectToMongoAndDelete(project_id) - TrackChangesManager.flushProject(project_id) - callback() - ), 500 - - updateClientPosition: (client, cursorData, callback = (error) ->) -> - async.parallel { - project_id: (cb)-> client.get "project_id", cb - first_name: (cb)-> client.get "first_name", cb - last_name: (cb)-> client.get "last_name", cb - email: (cb)-> client.get "email", cb - user_id: (cb)-> client.get "user_id", cb - }, (err, results)-> - {first_name, last_name, user_id, email, project_id} = results - cursorData.id = client.id - cursorData.user_id = user_id if user_id? - cursorData.email = email if email? - if first_name? and last_name? - cursorData.name = first_name + " " + last_name - ConnectedUsersManager.updateUserPosition(project_id, client.id, { - first_name: first_name, - last_name: last_name, - email: email, - user_id: user_id - }, { - row: cursorData.row, - column: cursorData.column, - doc_id: cursorData.doc_id - }, ->) - else - cursorData.name = "Anonymous" - EditorRealTimeController.emitToRoom(project_id, "clientTracking.clientUpdated", cursorData) - addUserToProject: (project_id, email, privileges, callback = (error, collaborator_added)->)-> email = email.toLowerCase() LimitationsManager.isCollaboratorLimitReached project_id, (error, limit_reached) => diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index b98c5af85f..384d2168c8 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -3,6 +3,10 @@ ProjectDeleter = require "../Project/ProjectDeleter" logger = require "logger-sharelatex" EditorRealTimeController = require "./EditorRealTimeController" EditorController = require "./EditorController" +ProjectGetter = require('../Project/ProjectGetter') +UserGetter = require('../User/UserGetter') +AuthorizationManager = require("../Security/AuthorizationManager") +ProjectEditorHandler = require('../Project/ProjectEditorHandler') Metrics = require('../../infrastructure/Metrics') module.exports = EditorHttpController = @@ -11,7 +15,7 @@ module.exports = EditorHttpController = user_id = req.query.user_id logger.log {user_id, project_id}, "join project request" Metrics.inc "editor.join-project" - EditorController.buildJoinProjectView project_id, user_id, (error, project, privilegeLevel) -> + EditorHttpController._buildJoinProjectView project_id, user_id, (error, project, privilegeLevel) -> return next(error) if error? res.json { project: project @@ -21,6 +25,24 @@ module.exports = EditorHttpController = if project?.deletedByExternalDataSource ProjectDeleter.unmarkAsDeletedByExternalSource project_id + _buildJoinProjectView: (project_id, user_id, callback = (error, project, privilegeLevel) ->) -> + ProjectGetter.getProjectWithoutDocLines project_id, (error, project) -> + return callback(error) if error? + return callback(new Error("not found")) if !project? + ProjectGetter.populateProjectWithUsers project, (error, project) -> + return callback(error) if error? + UserGetter.getUser user_id, { isAdmin: true }, (error, user) -> + return callback(error) if error? + AuthorizationManager.getPrivilegeLevelForProject project, user, (error, canAccess, privilegeLevel) -> + return callback(error) if error? + if !canAccess + callback null, null, false + else + callback(null, + ProjectEditorHandler.buildProjectModelView(project), + privilegeLevel + ) + restoreDoc: (req, res, next) -> project_id = req.params.Project_id doc_id = req.params.doc_id diff --git a/services/web/app/coffee/Features/Editor/EditorRealTimeController.coffee b/services/web/app/coffee/Features/Editor/EditorRealTimeController.coffee index 76f4f909c1..bd4f6af8e8 100644 --- a/services/web/app/coffee/Features/Editor/EditorRealTimeController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorRealTimeController.coffee @@ -16,15 +16,3 @@ module.exports = EditorRealTimeController = emitToAll: (message, payload...) -> @emitToRoom "all", message, payload... - listenForEditorEvents: () -> - @rclientSub.subscribe "editor-events" - @rclientSub.on "message", @_processEditorEvent.bind(@) - - _processEditorEvent: (channel, message) -> - io = require('../../infrastructure/Server').io - message = JSON.parse(message) - if message.room_id == "all" - io.sockets.emit(message.message, message.payload...) - else - io.sockets.in(message.room_id).emit(message.message, message.payload...) - diff --git a/services/web/app/coffee/Features/Editor/EditorUpdatesController.coffee b/services/web/app/coffee/Features/Editor/EditorUpdatesController.coffee deleted file mode 100644 index 0233c67aaa..0000000000 --- a/services/web/app/coffee/Features/Editor/EditorUpdatesController.coffee +++ /dev/null @@ -1,61 +0,0 @@ -logger = require "logger-sharelatex" -metrics = require('../../infrastructure/Metrics') -Settings = require 'settings-sharelatex' -redis = require("redis-sharelatex") -rclient = redis.createClient(Settings.redis.web) -DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') -EditorRealTimeController = require("./EditorRealTimeController") - -module.exports = EditorUpdatesController = - _applyUpdate: (client, project_id, doc_id, update, callback = (error) ->) -> - metrics.inc "editor.doc-update", 0.3 - metrics.set "editor.active-projects", project_id, 0.3 - client.get "user_id", (error, user_id) -> - metrics.set "editor.active-users", user_id, 0.3 - - logger.log doc_id: doc_id, project_id: project_id, client_id: update.meta?.source, version: update.v, "sending update to doc updater" - - DocumentUpdaterHandler.queueChange project_id, doc_id, update, (error) -> - if error? - logger.error err:error, project_id: project_id, doc_id: doc_id, client_id: update.meta?.source, version: update.v, "document was not available for update" - client.disconnect() - callback(error) - - applyOtUpdate: (client, project_id, doc_id, update) -> - update.meta ||= {} - update.meta.source = client.id - client.get "user_id", (error, user_id) -> - update.meta.user_id = user_id - EditorUpdatesController._applyUpdate client, project_id, doc_id, update - - listenForUpdatesFromDocumentUpdater: () -> - rclient.subscribe "applied-ops" - rclient.on "message", @_processMessageFromDocumentUpdater.bind(@) - - _processMessageFromDocumentUpdater: (channel, message) -> - message = JSON.parse message - if message.op? - @_applyUpdateFromDocumentUpdater(message.doc_id, message.op) - else if message.error? - @_processErrorFromDocumentUpdater(message.doc_id, message.error, message) - - _applyUpdateFromDocumentUpdater: (doc_id, update) -> - io = require('../../infrastructure/Server').io - for client in io.sockets.clients(doc_id) - if client.id == update.meta.source - logger.log doc_id: doc_id, version: update.v, source: update.meta?.source, "distributing update to sender" - client.emit "otUpdateApplied", v: update.v, doc: update.doc - else - logger.log doc_id: doc_id, version: update.v, source: update.meta?.source, client_id: client.id, "distributing update to collaborator" - client.emit "otUpdateApplied", update - - _processErrorFromDocumentUpdater: (doc_id, error, message) -> - io = require('../../infrastructure/Server').io - logger.error err: error, doc_id: doc_id, "error from document updater" - for client in io.sockets.clients(doc_id) - client.emit "otUpdateError", error, message - client.disconnect() - - - - diff --git a/services/web/app/coffee/infrastructure/BackgroundTasks.coffee b/services/web/app/coffee/infrastructure/BackgroundTasks.coffee deleted file mode 100644 index 6433efc9e8..0000000000 --- a/services/web/app/coffee/infrastructure/BackgroundTasks.coffee +++ /dev/null @@ -1,7 +0,0 @@ -EditorUpdatesController = require("../Features/Editor/EditorUpdatesController") -EditorRealTimeController = require("../Features/Editor/EditorRealTimeController") - -module.exports = BackgroundTasks = - run: () -> - EditorUpdatesController.listenForUpdatesFromDocumentUpdater() - EditorRealTimeController.listenForEditorEvents() diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index 635dff4c12..3e7a89a0d6 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -5,11 +5,8 @@ logger = require 'logger-sharelatex' metrics = require('./Metrics') crawlerLogger = require('./CrawlerLogger') expressLocals = require('./ExpressLocals') -socketIoConfig = require('./SocketIoConfig') Router = require('../router') metrics.inc("startup") -SessionSockets = require('session.socket.io') - redis = require("redis-sharelatex") rclient = redis.createClient(Settings.redis.web) @@ -119,13 +116,8 @@ app.get "/profile", (req, res) -> logger.info ("creating HTTP server").yellow server = require('http').createServer(app) -io = require('socket.io').listen(server) - -sessionSockets = new SessionSockets(io, sessionStore, cookieParser, cookieKey) -router = new Router(app, io, sessionSockets) -socketIoConfig.configure(io) +router = new Router(app) module.exports = - io: io app: app server: server diff --git a/services/web/app/coffee/infrastructure/SocketIoConfig.coffee b/services/web/app/coffee/infrastructure/SocketIoConfig.coffee deleted file mode 100644 index d21dec90f4..0000000000 --- a/services/web/app/coffee/infrastructure/SocketIoConfig.coffee +++ /dev/null @@ -1,20 +0,0 @@ -SocketIoRedisStore = require('socket.io/lib/stores/redis') - -module.exports = - configure: (io)-> - io.configure -> - io.enable('browser client minification') - io.enable('browser client etag') - - # Fix for Safari 5 error of "Error during WebSocket handshake: location mismatch" - # See http://answers.dotcloud.com/question/578/problem-with-websocket-over-ssl-in-safari-with - io.set('match origin protocol', true) - - # gzip uses a Node 0.8.x method of calling the gzip program which - # doesn't work with 0.6.x - #io.enable('browser client gzip') - io.set('transports', ['websocket', 'flashsocket', 'htmlfile', 'xhr-polling', 'jsonp-polling']) - io.set('log level', 1) - - io.configure 'production', -> - io.set('log level', 1) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index b753fbafea..d1422998d6 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -7,7 +7,6 @@ SecurityManager = require('./managers/SecurityManager') AuthorizationManager = require('./Features/Security/AuthorizationManager') EditorController = require("./Features/Editor/EditorController") EditorRouter = require("./Features/Editor/EditorRouter") -EditorUpdatesController = require("./Features/Editor/EditorUpdatesController") Settings = require('settings-sharelatex') TpdsController = require('./Features/ThirdPartyDataStore/TpdsController') SubscriptionRouter = require './Features/Subscription/SubscriptionRouter' @@ -34,7 +33,6 @@ StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter") ChatController = require("./Features/Chat/ChatController") BlogController = require("./Features/Blog/BlogController") WikiController = require("./Features/Wiki/WikiController") -ConnectedUsersController = require("./Features/ConnectedUsers/ConnectedUsersController") DropboxRouter = require "./Features/Dropbox/DropboxRouter" dropboxHandler = require "./Features/Dropbox/DropboxHandler" Modules = require "./infrastructure/Modules" @@ -50,7 +48,7 @@ httpAuth = require('express').basicAuth (user, pass)-> return isValid module.exports = class Router - constructor: (app, io, socketSessions)-> + constructor: (app)-> app.use(app.router) app.get '/login', UserPagesController.loginPage @@ -126,8 +124,6 @@ module.exports = class Router app.get "/project/:Project_id/doc/:doc_id/diff", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi app.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi - app.get '/project/:Project_id/connected_users', SecurityManager.requestCanAccessProject, ConnectedUsersController.getConnectedUsers - app.get '/Project/:Project_id/download/zip', SecurityManager.requestCanAccessProject, ProjectDownloadsController.downloadProject app.get '/project/download/zip', SecurityManager.requestCanAccessMultipleProjects, ProjectDownloadsController.downloadMultipleProjects @@ -226,65 +222,3 @@ module.exports = class Router res.send(204) app.get '*', ErrorController.notFound - - socketSessions.on 'connection', (err, client, session)-> - metrics.inc('socket-io.connection') - # This is not ideal - we should come up with a better way of handling - # anonymous users, but various logging lines rely on user._id - if !session or !session.user? - user = {_id: "anonymous-user"} - else - user = session.user - - client.on 'joinProject', (data, callback) -> - EditorController.joinProject(client, user, data.project_id, callback) - - client.on 'disconnect', () -> - metrics.inc ('socket-io.disconnect') - EditorController.leaveProject client, user - - client.on 'applyOtUpdate', (doc_id, update) -> - AuthorizationManager.ensureClientCanEditProject client, (error, project_id) => - EditorUpdatesController.applyOtUpdate(client, project_id, doc_id, update) - - client.on 'clientTracking.updatePosition', (cursorData) -> - AuthorizationManager.ensureClientCanViewProject client, (error, project_id) => - EditorController.updateClientPosition(client, cursorData) - - client.on 'leaveDoc', (doc_id, callback)-> - AuthorizationManager.ensureClientCanViewProject client, (error, project_id) => - EditorController.leaveDoc(client, project_id, doc_id, callback) - - client.on 'joinDoc', (args...)-> - AuthorizationManager.ensureClientCanViewProject client, (error, project_id) => - EditorController.joinDoc(client, project_id, args...) - - # The remaining can be done via HTTP - client.on 'addUserToProject', (email, newPrivalageLevel, callback)-> - AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) => - EditorController.addUserToProject project_id, email, newPrivalageLevel, callback - - client.on 'removeUserFromProject', (user_id, callback)-> - AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) => - EditorController.removeUserFromProject(project_id, user_id, callback) - - client.on 'getUserDropboxLinkStatus', (owner_id, callback)-> - AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) => - dropboxHandler.getUserRegistrationStatus owner_id, callback - - # client.on 'publishProjectAsTemplate', (user_id, callback)-> - # AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) => - # TemplatesController.publishProject user_id, project_id, callback - # - # client.on 'unPublishProjectAsTemplate', (user_id, callback)-> - # AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) => - # TemplatesController.unPublishProject user_id, project_id, callback - # - # client.on 'updateProjectDescription', (description, callback)-> - # AuthorizationManager.ensureClientCanEditProject client, (error, project_id) => - # EditorController.updateProjectDescription project_id, description, callback - # - # client.on "getPublishedDetails", (user_id, callback)-> - # AuthorizationManager.ensureClientCanViewProject client, (error, project_id) => - # TemplatesController.getTemplateDetails user_id, project_id, callback - diff --git a/services/web/app/views/project/editor.jade b/services/web/app/views/project/editor.jade index 7e0f8fe92e..dc1403c909 100644 --- a/services/web/app/views/project/editor.jade +++ b/services/web/app/views/project/editor.jade @@ -75,7 +75,7 @@ block content .modal-footer button.btn.btn-info(ng-click="done()") #{translate("ok")} - script(src='/socket.io/socket.io.js') + script(src='#{settings.websocketsUrl}/socket.io/socket.io.js') //- We need to do .replace(/\//g, '\\/') do that '' -> '<\/script>' //- and doesn't prematurely end the script tag. diff --git a/services/web/public/coffee/ide/online-users/OnlineUsersManager.coffee b/services/web/public/coffee/ide/online-users/OnlineUsersManager.coffee index d3cd23937a..dd833913ce 100644 --- a/services/web/public/coffee/ide/online-users/OnlineUsersManager.coffee +++ b/services/web/public/coffee/ide/online-users/OnlineUsersManager.coffee @@ -12,26 +12,24 @@ define [ @sendCursorPositionUpdate(position) @$scope.$on "project:joined", () => - ide.$http - .get "/project/#{@ide.$scope.project._id}/connected_users" - .success (connectedUsers) => - @$scope.onlineUsers = {} - for user in connectedUsers or [] - if user.client_id == @ide.socket.socket.sessionid - # Don't store myself - continue - # Store data in the same format returned by clientTracking.clientUpdated + @ide.socket.emit "clientTracking.getConnectedUsers", (error, connectedUsers) => + @$scope.onlineUsers = {} + for user in connectedUsers or [] + if user.client_id == @ide.socket.socket.sessionid + # Don't store myself + continue + # Store data in the same format returned by clientTracking.clientUpdated - @$scope.onlineUsers[user.client_id] = { - id: user.client_id - user_id: user.user_id - email: user.email - name: "#{user.first_name} #{user.last_name}" - doc_id: user.cursorData?.doc_id - row: user.cursorData?.row - column: user.cursorData?.column - } - @refreshOnlineUsers() + @$scope.onlineUsers[user.client_id] = { + id: user.client_id + user_id: user.user_id + email: user.email + name: "#{user.first_name} #{user.last_name}" + doc_id: user.cursorData?.doc_id + row: user.cursorData?.row + column: user.cursorData?.column + } + @refreshOnlineUsers() @ide.socket.on "clientTracking.clientUpdated", (client) => if client.id != @ide.socket.socket.sessionid # Check it's not me! diff --git a/services/web/test/UnitTests/coffee/ConnectedUsers/ConnectedUsersControllerTests.coffee b/services/web/test/UnitTests/coffee/ConnectedUsers/ConnectedUsersControllerTests.coffee deleted file mode 100644 index fb7fd653ea..0000000000 --- a/services/web/test/UnitTests/coffee/ConnectedUsers/ConnectedUsersControllerTests.coffee +++ /dev/null @@ -1,46 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/ConnectedUsers/ConnectedUsersController" -expect = require("chai").expect - -describe "ConnectedUsersController", -> - - beforeEach -> - - @settings = {} - @ConnectedUsersManager = - getConnectedUsers:sinon.stub() - @ConnectedUsersController = SandboxedModule.require modulePath, requires: - "settings-sharelatex":@settings - "./ConnectedUsersManager":@ConnectedUsersManager - "logger-sharelatex": - log:-> - err:-> - @project_id = "231312390128309" - @req = - params: - project_id:@project_id - @res = {} - - - describe "getConnectedUsers", -> - - beforeEach -> - @connectedUsersData = [{user_id:"312321"}, {user_id:"3213213"}] - - it "should get the connected user data for that project", (done)-> - @ConnectedUsersManager.getConnectedUsers.callsArgWith(1, null, @connectedUsersData) - @res.send = (d)=> - d.should.deep.equal @connectedUsersData - done() - @ConnectedUsersController.getConnectedUsers @req, @res - - it "should send a 500 on an error", (done)-> - @ConnectedUsersManager.getConnectedUsers.callsArgWith(1, "error") - @res.send = (code)=> - code.should.equal 500 - done() - @ConnectedUsersController.getConnectedUsers @req, @res diff --git a/services/web/test/UnitTests/coffee/ConnectedUsers/ConnectedUsersManagerTests.coffee b/services/web/test/UnitTests/coffee/ConnectedUsers/ConnectedUsersManagerTests.coffee deleted file mode 100644 index 365034645d..0000000000 --- a/services/web/test/UnitTests/coffee/ConnectedUsers/ConnectedUsersManagerTests.coffee +++ /dev/null @@ -1,154 +0,0 @@ - -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/ConnectedUsers/ConnectedUsersManager" -expect = require("chai").expect -tk = require("timekeeper") - - -describe "ConnectedUsersManager", -> - - beforeEach -> - - @settings = - redis: - web:{} - @rClient = - auth:-> - setex:sinon.stub() - sadd:sinon.stub() - get: sinon.stub() - srem:sinon.stub() - del:sinon.stub() - smembers:sinon.stub() - expire:sinon.stub() - hset:sinon.stub() - hgetall:sinon.stub() - exec:sinon.stub() - multi: => return @rClient - tk.freeze(new Date()) - - @ConnectedUsersManager = SandboxedModule.require modulePath, requires: - "settings-sharelatex":@settings - "logger-sharelatex": log:-> - "redis-sharelatex": createClient:=> - return @rClient - @client_id = "32132132" - @project_id = "dskjh2u21321" - @user = { - _id: "user-id-123" - first_name: "Joe" - last_name: "Bloggs" - email: "joe@example.com" - } - @cursorData = { row: 12, column: 9, doc_id: '53c3b8c85fee64000023dc6e' } - - afterEach -> - tk.reset() - - describe "updateUserPosition", -> - beforeEach -> - @rClient.exec.callsArgWith(0) - - it "should set a key with the date and give it a ttl", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "last_updated_at", Date.now()).should.equal true - done() - - it "should set a key with the user_id", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "user_id", @user._id).should.equal true - done() - - it "should set a key with the first_name", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "first_name", @user.first_name).should.equal true - done() - - it "should set a key with the last_name", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "last_name", @user.last_name).should.equal true - done() - - it "should set a key with the email", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "email", @user.email).should.equal true - done() - - it "should push the client_id on to the project list", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.sadd.calledWith("clients_in_project:#{@project_id}", @client_id).should.equal true - done() - - it "should add a ttl to the project set so it stays clean", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.expire.calledWith("clients_in_project:#{@project_id}", 24 * 4 * 60 * 60).should.equal true - done() - - it "should add a ttl to the connected user so it stays clean", (done) -> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.expire.calledWith("connected_user:#{@project_id}:#{@client_id}", 60 * 15).should.equal true - done() - - it "should set the cursor position when provided", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, @cursorData, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "cursorData", JSON.stringify(@cursorData)).should.equal true - done() - - describe "markUserAsDisconnected", -> - beforeEach -> - @rClient.exec.callsArgWith(0) - - it "should remove the user from the set", (done)-> - @ConnectedUsersManager.markUserAsDisconnected @project_id, @client_id, (err)=> - @rClient.srem.calledWith("clients_in_project:#{@project_id}", @client_id).should.equal true - done() - - it "should delete the connected_user string", (done)-> - @ConnectedUsersManager.markUserAsDisconnected @project_id, @client_id, (err)=> - @rClient.del.calledWith("connected_user:#{@project_id}:#{@client_id}").should.equal true - done() - - it "should add a ttl to the connected user set so it stays clean", (done)-> - @ConnectedUsersManager.markUserAsDisconnected @project_id, @client_id, (err)=> - @rClient.expire.calledWith("clients_in_project:#{@project_id}", 24 * 4 * 60 * 60).should.equal true - done() - - describe "_getConnectedUser", -> - - it "should get the user returning connected if there is a value", (done)-> - cursorData = JSON.stringify(cursorData:{row:1}) - @rClient.hgetall.callsArgWith(1, null, {connected_at:new Date(), cursorData}) - @ConnectedUsersManager._getConnectedUser @project_id, @client_id, (err, result)=> - result.connected.should.equal true - result.client_id.should.equal @client_id - done() - - it "should get the user returning connected if there is a value", (done)-> - @rClient.hgetall.callsArgWith(1) - @ConnectedUsersManager._getConnectedUser @project_id, @client_id, (err, result)=> - result.connected.should.equal false - result.client_id.should.equal @client_id - done() - - describe "getConnectedUsers", -> - - beforeEach -> - @users = ["1234", "5678", "9123"] - @rClient.smembers.callsArgWith(1, null, @users) - @ConnectedUsersManager._getConnectedUser = sinon.stub() - @ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[0]).callsArgWith(2, null, {connected:true, client_id:@users[0]}) - @ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[1]).callsArgWith(2, null, {connected:false, client_id:@users[1]}) - @ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[2]).callsArgWith(2, null, {connected:true, client_id:@users[2]}) - - - it "should only return the users in the list which are still in redis", (done)-> - @ConnectedUsersManager.getConnectedUsers @project_id, (err, users)=> - users.length.should.equal 2 - users[0].should.deep.equal {client_id:@users[0], connected:true} - users[1].should.deep.equal {client_id:@users[2], connected:true} - done() - diff --git a/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee index d4e05c8bb1..52e76e4374 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee @@ -18,10 +18,6 @@ describe "EditorController", -> @doc_id = "test-doc-id" @source = "dropbox" - @projectModelView = - _id: @project_id - owner:{_id:"something"} - @user = _id: @user_id = "user-id" projects: {} @@ -37,12 +33,10 @@ describe "EditorController", -> setSpellCheckLanguage: sinon.spy() @ProjectEntityHandler = flushProjectToThirdPartyDataStore:sinon.stub() - @ProjectEditorHandler = - buildProjectModelView : sinon.stub().returns(@projectModelView) + @ProjectEditorHandler = {} @Project = findPopulatedById: sinon.stub().callsArgWith(1, null, @project) @LimitationsManager = {} - @AuthorizationManager = {} @client = new MockClient() @settings = @@ -57,9 +51,6 @@ describe "EditorController", -> addUserToProject: sinon.stub().callsArgWith(3) @ProjectDeleter = deleteProject: sinon.stub() - @ConnectedUsersManager = - markUserAsDisconnected:sinon.stub() - updateUserPosition:sinon.stub() @LockManager = getLock : sinon.stub() releaseLock : sinon.stub() @@ -70,308 +61,21 @@ describe "EditorController", -> '../Project/ProjectOptionsHandler' : @ProjectOptionsHandler '../Project/ProjectDetailsHandler': @ProjectDetailsHandler '../Project/ProjectDeleter' : @ProjectDeleter - '../Project/ProjectGetter' : @ProjectGetter = {} - '../User/UserGetter': @UserGetter = {} '../Collaborators/CollaboratorsHandler': @CollaboratorsHandler '../DocumentUpdater/DocumentUpdaterHandler' : @DocumentUpdaterHandler '../Subscription/LimitationsManager' : @LimitationsManager - '../Security/AuthorizationManager' : @AuthorizationManager '../../models/Project' : Project: @Project "settings-sharelatex":@settings '../Dropbox/DropboxProjectLinker':@dropboxProjectLinker './EditorRealTimeController':@EditorRealTimeController = {} "../../infrastructure/Metrics": @Metrics = { inc: sinon.stub() } "../TrackChanges/TrackChangesManager": @TrackChangesManager = {} - "../ConnectedUsers/ConnectedUsersManager":@ConnectedUsersManager "../../infrastructure/LockManager":@LockManager 'redis-sharelatex':createClient:-> auth:-> "logger-sharelatex": @logger = log: sinon.stub() err: sinon.stub() - describe "joinProject", -> - beforeEach -> - sinon.spy(@client, "set") - sinon.spy(@client, "get") - @AuthorizationManager.setPrivilegeLevelOnClient = sinon.stub() - @EditorRealTimeController.emitToRoom = sinon.stub() - @ConnectedUsersManager.updateUserPosition.callsArgWith(4) - @ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() - - describe "when authorized", -> - beforeEach -> - @EditorController.buildJoinProjectView = sinon.stub().callsArgWith(2, null, @projectModelView, "owner") - @EditorController.joinProject(@client, @user, @project_id, @callback) - - it "should set the privilege level on the client", -> - @AuthorizationManager.setPrivilegeLevelOnClient - .calledWith(@client, "owner") - .should.equal.true - - it "should add the client to the project channel", -> - @client.join.calledWith(@project_id).should.equal true - - it "should set the project_id of the client", -> - @client.set.calledWith("project_id", @project_id).should.equal true - - it "should mark the user as connected with the ConnectedUsersManager", -> - @ConnectedUsersManager.updateUserPosition.calledWith(@project_id, @client.id, @user, null).should.equal true - - it "should return the project model view, privilege level and protocol version", -> - @callback.calledWith(null, @projectModelView, "owner", @EditorController.protocolVersion).should.equal true - - describe "when not authorized", -> - beforeEach -> - @EditorController.buildJoinProjectView = sinon.stub().callsArgWith(2, null, null, false) - @EditorController.joinProject(@client, @user, @project_id, @callback) - - it "should not set the privilege level on the client", -> - @AuthorizationManager.setPrivilegeLevelOnClient - .called.should.equal false - - it "should not add the client to the project channel", -> - @client.join.called.should.equal false - - it "should not set the project_id of the client", -> - @client.set.called.should.equal false - - it "should return an error", -> - @callback.calledWith(sinon.match.truthy).should.equal true - - describe "when the project is marked as deleted", -> - beforeEach -> - @projectModelView.deletedByExternalDataSource = true - @EditorController.buildJoinProjectView = sinon.stub().callsArgWith(2, null, @projectModelView, "owner") - @EditorController.joinProject(@client, @user, @project_id, @callback) - - it "should remove the flag to send a user a message about the project being deleted", -> - @ProjectDeleter.unmarkAsDeletedByExternalSource - .calledWith(@project_id) - .should.equal true - - describe "buildJoinProjectView", -> - beforeEach -> - @ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, @project) - @ProjectGetter.populateProjectWithUsers = sinon.stub().callsArgWith(1, null, @project) - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user) - - describe "when authorized", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject = - sinon.stub().callsArgWith(2, null, true, "owner") - @EditorController.buildJoinProjectView(@project_id, @user_id, @callback) - - it "should find the project without doc lines", -> - @ProjectGetter.getProjectWithoutDocLines - .calledWith(@project_id) - .should.equal true - - it "should populate the user references in the project", -> - @ProjectGetter.populateProjectWithUsers - .calledWith(@project) - .should.equal true - - it "should look up the user", -> - @UserGetter.getUser - .calledWith(@user_id, { isAdmin: true }) - .should.equal true - - it "should check the privilege level", -> - @AuthorizationManager.getPrivilegeLevelForProject - .calledWith(@project, @user) - .should.equal true - - it "should return the project model view, privilege level and protocol version", -> - @callback.calledWith(null, @projectModelView, "owner").should.equal true - - describe "when not authorized", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject = - sinon.stub().callsArgWith(2, null, false, null) - @EditorController.buildJoinProjectView(@project_id, @user_id, @callback) - - it "should return false in the callback", -> - @callback.calledWith(null, null, false).should.equal true - - - describe "leaveProject", -> - beforeEach -> - sinon.stub(@client, "set") - sinon.stub(@client, "get").callsArgWith(1, null, @project_id) - @EditorRealTimeController.emitToRoom = sinon.stub() - @EditorController.flushProjectIfEmpty = sinon.stub() - @EditorController.leaveProject @client, @user - @ConnectedUsersManager.markUserAsDisconnected.callsArgWith(2) - - it "should call the flush project if empty function", -> - @EditorController.flushProjectIfEmpty - .calledWith(@project_id) - .should.equal true - - it "should emit a clientDisconnect to the project room", -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "clientTracking.clientDisconnected", @client.id) - .should.equal true - - it "should mark the user as connected with the ConnectedUsersManager", -> - @ConnectedUsersManager.markUserAsDisconnected.calledWith(@project_id, @client.id).should.equal true - - - describe "joinDoc", -> - beforeEach -> - @client.join = sinon.stub() - @client.set("user_id", @user_id) - @fromVersion = 40 - @docLines = ["foo", "bar"] - @ops = ["mock-op-1", "mock-op-2"] - @version = 42 - @DocumentUpdaterHandler.getDocument = sinon.stub().callsArgWith(3, null, @docLines, @version, @ops) - - describe "with a fromVersion", -> - beforeEach -> - @EditorController.joinDoc @client, @project_id, @doc_id, @fromVersion, @callback - - it "should add the client to the socket.io room for the doc", -> - @client.join.calledWith(@doc_id).should.equal true - - it "should get the document", -> - @DocumentUpdaterHandler.getDocument - .calledWith(@project_id, @doc_id, @fromVersion) - .should.equal true - - it "should return the doclines and version and ops", -> - @callback.calledWith(null, @docLines, @version, @ops).should.equal true - - it "should increment the join-doc metric", -> - @Metrics.inc.calledWith("editor.join-doc").should.equal true - - it "should log out the request", -> - @logger.log - .calledWith(user_id: @user_id, project_id: @project_id, doc_id: @doc_id, "user joining doc") - .should.equal true - - describe "without a fromVersion", -> - beforeEach -> - @EditorController.joinDoc @client, @project_id, @doc_id, @callback - - it "should get the document with fromVersion=-1", -> - @DocumentUpdaterHandler.getDocument - .calledWith(@project_id, @doc_id, -1) - .should.equal true - - it "should return the doclines and version and ops", -> - @callback.calledWith(null, @docLines, @version, @ops).should.equal true - - describe "leaveDoc", -> - beforeEach -> - @client.leave = sinon.stub() - @client.set("user_id", @user_id) - @EditorController.leaveDoc @client, @project_id, @doc_id, @callback - - it "should remove the client from the socket.io room for the doc", -> - @client.leave.calledWith(@doc_id).should.equal true - - it "should increment the leave-doc metric", -> - @Metrics.inc.calledWith("editor.leave-doc").should.equal true - - it "should log out the request", -> - @logger.log - .calledWith(user_id: @user_id, project_id: @project_id, doc_id: @doc_id, "user leaving doc") - - .should.equal true - - describe "flushProjectIfEmpty", -> - beforeEach -> - @DocumentUpdaterHandler.flushProjectToMongoAndDelete = sinon.stub() - @TrackChangesManager.flushProject = sinon.stub() - - describe "when a project has no more users", -> - it "should do the flush after the config set timeout to ensure that a reconect didn't just happen", (done)-> - @rooms[@project_id] = [] - @EditorController.flushProjectIfEmpty @project_id, => - @DocumentUpdaterHandler.flushProjectToMongoAndDelete.calledWith(@project_id).should.equal(true) - @TrackChangesManager.flushProject.calledWith(@project_id).should.equal true - done() - - describe "when a project still has connected users", -> - it "should not flush the project", (done)-> - @rooms[@project_id] = ["socket-id-1", "socket-id-2"] - @EditorController.flushProjectIfEmpty @project_id, => - @DocumentUpdaterHandler.flushProjectToMongoAndDelete.calledWith(@project_id).should.equal(false) - @TrackChangesManager.flushProject.calledWith(@project_id).should.equal false - done() - - describe "updateClientPosition", -> - beforeEach -> - @EditorRealTimeController.emitToRoom = sinon.stub() - @ConnectedUsersManager.updateUserPosition.callsArgWith(4) - @update = { - doc_id: @doc_id = "doc-id-123" - row: @row = 42 - column: @column = 37 - } - - - describe "with a logged in user", -> - beforeEach -> - @clientParams = { - project_id: @project_id - first_name: @first_name = "Douglas" - last_name: @last_name = "Adams" - email: @email = "joe@example.com" - user_id: @user_id = "user-id-123" - } - @client.get = (param, callback) => callback null, @clientParams[param] - @EditorController.updateClientPosition @client, @update - - @populatedCursorData = - doc_id: @doc_id, - id: @client.id - name: "#{@first_name} #{@last_name}" - row: @row - column: @column - email: @email - user_id: @user_id - - it "should send the update to the project room with the user's name", -> - @EditorRealTimeController.emitToRoom.calledWith(@project_id, "clientTracking.clientUpdated", @populatedCursorData).should.equal true - - it "should send the cursor data to the connected user manager", (done)-> - @ConnectedUsersManager.updateUserPosition.calledWith(@project_id, @client.id, { - user_id: @user_id, - email: @email, - first_name: @first_name, - last_name: @last_name - }, { - row: @row - column: @column - doc_id: @doc_id - }).should.equal true - done() - - describe "with an anonymous user", -> - beforeEach -> - @clientParams = { - project_id: @project_id - } - @client.get = (param, callback) => callback null, @clientParams[param] - @EditorController.updateClientPosition @client, @update - - it "should send the update to the project room with an anonymous name", -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "clientTracking.clientUpdated", { - doc_id: @doc_id, - id: @client.id - name: "Anonymous" - row: @row - column: @column - }) - .should.equal true - - it "should not send cursor data to the connected user manager", (done)-> - @ConnectedUsersManager.updateUserPosition.called.should.equal false - done() - describe "addUserToProject", -> beforeEach -> @email = "Jane.Doe@example.com" diff --git a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee index fce17bb9e3..966c50dede 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee @@ -8,6 +8,10 @@ describe "EditorHttpController", -> @EditorHttpController = SandboxedModule.require modulePath, requires: '../Project/ProjectEntityHandler' : @ProjectEntityHandler = {} '../Project/ProjectDeleter' : @ProjectDeleter = {} + '../Project/ProjectGetter' : @ProjectGetter = {} + '../User/UserGetter' : @UserGetter = {} + "../Security/AuthorizationManager": @AuthorizationManager = {} + '../Project/ProjectEditorHandler': @ProjectEditorHandler = {} "./EditorRealTimeController": @EditorRealTimeController = {} "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } "./EditorController": @EditorController = {} @@ -21,6 +25,7 @@ describe "EditorHttpController", -> @res = send: sinon.stub() json: sinon.stub() + @callback = sinon.stub() describe "joinProject", -> beforeEach -> @@ -31,7 +36,7 @@ describe "EditorHttpController", -> @projectView = { _id: @project_id } - @EditorController.buildJoinProjectView = sinon.stub().callsArgWith(2, null, @projectView, "owner") + @EditorHttpController._buildJoinProjectView = sinon.stub().callsArgWith(2, null, @projectView, "owner") @ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() describe "successfully", -> @@ -39,7 +44,7 @@ describe "EditorHttpController", -> @EditorHttpController.joinProject @req, @res it "should get the project view", -> - @EditorController.buildJoinProjectView + @EditorHttpController._buildJoinProjectView .calledWith(@project_id, @user_id) .should.equal true @@ -71,6 +76,61 @@ describe "EditorHttpController", -> .calledWith(@project_id) .should.equal true + describe "_buildJoinProjectView", -> + beforeEach -> + @project = + _id: @project_id + owner_ref:{_id:"something"} + @user = + _id: @user_id = "user-id" + projects: {} + @projectModelView = + _id: @project_id + owner:{_id:"something"} + view: true + @ProjectEditorHandler.buildProjectModelView = sinon.stub().returns(@projectModelView) + @ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, @project) + @ProjectGetter.populateProjectWithUsers = sinon.stub().callsArgWith(1, null, @project) + @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user) + + describe "when authorized", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject = + sinon.stub().callsArgWith(2, null, true, "owner") + @EditorHttpController._buildJoinProjectView(@project_id, @user_id, @callback) + + it "should find the project without doc lines", -> + @ProjectGetter.getProjectWithoutDocLines + .calledWith(@project_id) + .should.equal true + + it "should populate the user references in the project", -> + @ProjectGetter.populateProjectWithUsers + .calledWith(@project) + .should.equal true + + it "should look up the user", -> + @UserGetter.getUser + .calledWith(@user_id, { isAdmin: true }) + .should.equal true + + it "should check the privilege level", -> + @AuthorizationManager.getPrivilegeLevelForProject + .calledWith(@project, @user) + .should.equal true + + it "should return the project model view, privilege level and protocol version", -> + @callback.calledWith(null, @projectModelView, "owner").should.equal true + + describe "when not authorized", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject = + sinon.stub().callsArgWith(2, null, false, null) + @EditorHttpController._buildJoinProjectView(@project_id, @user_id, @callback) + + it "should return false in the callback", -> + @callback.calledWith(null, null, false).should.equal true + describe "restoreDoc", -> beforeEach -> @req.params = diff --git a/services/web/test/UnitTests/coffee/Editor/EditorRealTimeControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorRealTimeControllerTests.coffee index 9bb1fb3a04..49c60a6ccf 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorRealTimeControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorRealTimeControllerTests.coffee @@ -41,49 +41,3 @@ describe "EditorRealTimeController", -> @EditorRealTimeController.emitToRoom .calledWith("all", @message, @payload...) .should.equal true - - describe "listenForEditorEvents", -> - beforeEach -> - @EditorRealTimeController._processEditorEvent = sinon.stub() - @EditorRealTimeController.listenForEditorEvents() - - it "should subscribe to the editor-events channel", -> - @EditorRealTimeController.rclientSub.subscribe - .calledWith("editor-events") - .should.equal true - - it "should process the events with _processEditorEvent", -> - @EditorRealTimeController.rclientSub.on - .calledWith("message", sinon.match.func) - .should.equal true - - describe "_processEditorEvent", -> - describe "with a designated room", -> - beforeEach -> - @io.sockets = - in: sinon.stub().returns(emit: @emit = sinon.stub()) - data = JSON.stringify - room_id: @room_id - message: @message - payload: @payload - @EditorRealTimeController._processEditorEvent("editor-events", data) - - it "should send the message to all clients in the room", -> - @io.sockets.in - .calledWith(@room_id) - .should.equal true - @emit.calledWith(@message, @payload...).should.equal true - - describe "when emitting to all", -> - beforeEach -> - @io.sockets = - emit: @emit = sinon.stub() - data = JSON.stringify - room_id: "all" - message: @message - payload: @payload - @EditorRealTimeController._processEditorEvent("editor-events", data) - - it "should send the message to all clients", -> - @emit.calledWith(@message, @payload...).should.equal true - diff --git a/services/web/test/UnitTests/coffee/Editor/EditorUpdatesControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorUpdatesControllerTests.coffee deleted file mode 100644 index 74d6807bb5..0000000000 --- a/services/web/test/UnitTests/coffee/Editor/EditorUpdatesControllerTests.coffee +++ /dev/null @@ -1,164 +0,0 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../../app/js/Features/Editor/EditorUpdatesController' -MockClient = require "../helpers/MockClient" -assert = require('assert') - -describe "EditorUpdatesController", -> - beforeEach -> - @project_id = "project-id-123" - @doc_id = "doc-id-123" - @client = new MockClient() - @callback = sinon.stub() - @EditorUpdatesController = SandboxedModule.require modulePath, requires: - "logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub() } - "./EditorRealTimeController" : @EditorRealTimeController = {} - "../DocumentUpdater/DocumentUpdaterHandler" : @DocumentUpdaterHandler = {} - "../../infrastructure/Metrics" : @metrics = { set: sinon.stub(), inc: sinon.stub() } - "../../infrastructure/Server" : io: @io = {} - "redis-sharelatex" : - createClient: ()=> - @rclient = {auth:->} - - describe "_applyUpdate", -> - beforeEach -> - @update = {op: {p: 12, t: "foo"}} - @client.set("user_id", @user_id = "user-id-123") - @DocumentUpdaterHandler.queueChange = sinon.stub().callsArg(3) - - describe "succesfully", -> - beforeEach -> - @EditorUpdatesController._applyUpdate @client, @project_id, @doc_id, @update, @callback - - it "should queue the update", -> - @DocumentUpdaterHandler.queueChange - .calledWith(@project_id, @doc_id, @update) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - it "should update the active users metric", -> - @metrics.set.calledWith("editor.active-users", @user_id).should.equal true - - it "should update the active projects metric", -> - @metrics.set.calledWith("editor.active-projects", @project_id).should.equal true - - it "should increment the doc updates", -> - @metrics.inc.calledWith("editor.doc-update").should.equal true - - describe "unsuccessfully", -> - beforeEach -> - @client.disconnect = sinon.stub() - @DocumentUpdaterHandler.queueChange = sinon.stub().callsArgWith(3, new Error("Something went wrong")) - @EditorUpdatesController._applyUpdate @client, @project_id, @doc_id, @update, @callback - - it "should disconnect the client", -> - @client.disconnect.called.should.equal true - - it "should log an error", -> - @logger.error.called.should.equal true - - describe "applyOtUpdate", -> - beforeEach -> - @client.id = "client-id" - @client.set("user_id", @user_id = "user-id-123") - @update = {op: {p: 12, t: "foo"}} - @EditorUpdatesController._applyUpdate = sinon.stub() - @EditorUpdatesController.applyOtUpdate @client, @project_id, @doc_id, @update - - it "should set the source of the update to the client id", -> - @update.meta.source.should.equal @client.id - - it "should set the user_id of the update to the user id", -> - @update.meta.user_id.should.equal @user_id - - it "should apply the update", -> - @EditorUpdatesController._applyUpdate - .calledWith(@client, @project_id, @doc_id, @update) - .should.equal true - - describe "listenForUpdatesFromDocumentUpdater", -> - beforeEach -> - @rclient.subscribe = sinon.stub() - @rclient.on = sinon.stub() - @EditorUpdatesController.listenForUpdatesFromDocumentUpdater() - - it "should subscribe to the doc-updater stream", -> - @rclient.subscribe.calledWith("applied-ops").should.equal true - - it "should register a callback to handle updates", -> - @rclient.on.calledWith("message").should.equal true - - describe "_processMessageFromDocumentUpdater", -> - describe "with update", -> - beforeEach -> - @message = - doc_id: @doc_id - op: {t: "foo", p: 12} - @EditorUpdatesController._applyUpdateFromDocumentUpdater = sinon.stub() - @EditorUpdatesController._processMessageFromDocumentUpdater "applied-ops", JSON.stringify(@message) - - it "should apply the update", -> - @EditorUpdatesController._applyUpdateFromDocumentUpdater - .calledWith(@doc_id, @message.op) - .should.equal true - - describe "with error", -> - beforeEach -> - @message = - doc_id: @doc_id - error: "Something went wrong" - @EditorUpdatesController._processErrorFromDocumentUpdater = sinon.stub() - @EditorUpdatesController._processMessageFromDocumentUpdater "applied-ops", JSON.stringify(@message) - - it "should process the error", -> - @EditorUpdatesController._processErrorFromDocumentUpdater - .calledWith(@doc_id, @message.error) - .should.equal true - - describe "_applyUpdateFromDocumentUpdater", -> - beforeEach -> - @sourceClient = new MockClient() - @otherClients = [new MockClient(), new MockClient()] - @update = - op: [ t: "foo", p: 12 ] - meta: source: @sourceClient.id - v: @version = 42 - doc: @doc_id - @io.sockets = - clients: sinon.stub().returns([@sourceClient, @otherClients...]) - @EditorUpdatesController._applyUpdateFromDocumentUpdater @doc_id, @update - - it "should send a version bump to the source client", -> - @sourceClient.emit - .calledWith("otUpdateApplied", v: @version, doc: @doc_id) - .should.equal true - - it "should get the clients connected to the document", -> - @io.sockets.clients - .calledWith(@doc_id) - .should.equal true - - it "should send the full update to the other clients", -> - for client in @otherClients - client.emit - .calledWith("otUpdateApplied", @update) - .should.equal true - - describe "_processErrorFromDocumentUpdater", -> - beforeEach -> - @clients = [new MockClient(), new MockClient()] - @io.sockets = - clients: sinon.stub().returns(@clients) - @EditorUpdatesController._processErrorFromDocumentUpdater @doc_id, "Something went wrong" - - it "should log out an error", -> - @logger.error.called.should.equal true - - it "should disconnect all clients in that document", -> - @io.sockets.clients.calledWith(@doc_id).should.equal true - for client in @clients - client.disconnect.called.should.equal true -