From 3c31988e9ab808f36e8ae6105266c5effcb11ad0 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 10 Jun 2016 16:06:02 +0100 Subject: [PATCH 01/70] enable on-demand viewing of pdfs for per-user compiles --- .../coffee/ide/pdfng/directives/pdfRenderer.coffee | 3 ++- .../coffee/ide/pdfng/directives/pdfViewer.coffee | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee b/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee index 5639d670f0..e634afea3a 100644 --- a/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee +++ b/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee @@ -12,7 +12,8 @@ define [ constructor: (@url, @options) -> # PDFJS.disableFontFace = true # avoids repaints, uses worker more - # PDFJS.disableAutoFetch = true # enable this to prevent loading whole file + if @options.disableAutoFetch + PDFJS.disableAutoFetch = true # prevent loading whole file # PDFJS.disableStream # PDFJS.disableRange @scale = @options.scale || 1 diff --git a/services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee b/services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee index f0ec8d61b1..4b6209717c 100644 --- a/services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee +++ b/services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee @@ -27,13 +27,21 @@ define [ $scope.document.destroy() if $scope.document? $scope.loadCount = if $scope.loadCount? then $scope.loadCount + 1 else 1 # TODO need a proper url manipulation library to add to query string - $scope.document = new PDFRenderer($scope.pdfSrc + '&pdfng=true' , { + url = $scope.pdfSrc + # add 'pdfng=true' to show that we are using the angular pdfjs viewer + queryStringExists = url.match(/\?/) + url = url + (if not queryStringExists then '?' else '&') + 'pdfng=true' + # for isolated compiles, load the pdf on-demand because nobody will overwrite it + onDemandLoading = window.location?.search?.match(/isolated=true/)? + $scope.document = new PDFRenderer(url, { scale: 1, + disableAutoFetch: if onDemandLoading then true else undefined navigateFn: (ref) -> # this function captures clicks on the annotation links $scope.navigateTo = ref $scope.$apply() progressCallback: (progress) -> + return if onDemandLoading is true # don't show progress for on-demand page loading $scope.$emit 'progress', progress loadedCallback: () -> $scope.$emit 'loaded' From 88564ea126692d97ded10063aff8dcf098534c27 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 28 Jun 2016 15:48:11 +0100 Subject: [PATCH 02/70] Dynamically determine if the files dropdown should drop up or drop down. --- services/web/app/views/project/editor/pdf.jade | 8 ++++++-- .../ide/pdf/controllers/PdfController.coffee | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index 091831a22d..66cea0f09e 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -151,12 +151,16 @@ div.full-size.pdf(ng-controller="PdfController") ) i.fa.fa-trash-o |   - div.dropdown(style="display: inline-block;", dropdown) + div.files-dropdown( + style="display: inline-block;", + ng-class="shouldDropUp ? 'dropup' : 'dropdown'" + dropdown + ) a.btn.btn-default.btn-sm( href dropdown-toggle ) - | !{translate("other_logs_and_files")} + | !{translate("other_logs_and_files")} span.caret ul.dropdown-menu.dropdown-menu-right li(ng-repeat="file in pdf.outputFiles") diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 610915ec3a..50b1b40aa0 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -15,6 +15,22 @@ define [ $scope.shouldShowLogs = false $scope.wikiEnabled = window.wikiEnabled; + # view logic to check whether the files dropdown should "drop up" or "drop down" + $scope.shouldDropUp = false + + logsContainerEl = document.querySelector ".pdf-logs" + filesDropdownEl = logsContainerEl?.querySelector ".files-dropdown" + + # get the top coordinate of the files dropdown as a ratio (to the logs container height) + # logs container supports scrollable content, so it's possible that ratio > 1. + getFilesDropdownTopCoordAsRatio = () -> + filesDropdownEl?.getBoundingClientRect().top / logsContainerEl?.getBoundingClientRect().height + + $scope.$watch "shouldShowLogs", (shouldShow) -> + if shouldShow + $scope.$applyAsync () -> + $scope.shouldDropUp = getFilesDropdownTopCoordAsRatio() > 0.65 + if ace.require("ace/lib/useragent").isMac $scope.modifierKey = "Cmd" else From a6d184358f026adefb180eb0f3e62fd3953bdde1 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 28 Jun 2016 15:51:50 +0100 Subject: [PATCH 03/70] Avoid inline-styles. --- services/web/app/views/project/editor/pdf.jade | 3 +-- services/web/public/stylesheets/app/editor/pdf.less | 9 +++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index 66cea0f09e..77bc7cc379 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -141,7 +141,7 @@ div.full-size.pdf(ng-controller="PdfController") p.entry-content(ng-show="entry.content") {{ entry.content.trim() }} p - .pull-right + .files-dropdown-container a.btn.btn-default.btn-sm( href, tooltip="#{translate('clear_cached_files')}", @@ -152,7 +152,6 @@ div.full-size.pdf(ng-controller="PdfController") i.fa.fa-trash-o |   div.files-dropdown( - style="display: inline-block;", ng-class="shouldDropUp ? 'dropup' : 'dropdown'" dropdown ) diff --git a/services/web/public/stylesheets/app/editor/pdf.less b/services/web/public/stylesheets/app/editor/pdf.less index 2d147c2f69..7e9b1ff05c 100644 --- a/services/web/public/stylesheets/app/editor/pdf.less +++ b/services/web/public/stylesheets/app/editor/pdf.less @@ -284,3 +284,12 @@ margin-top: 20px; } } + +.files-dropdown-container { + .pull-right(); + position: relative; +} + + .files-dropdown { + display: inline-block; + } From 5ad0c06770803c99f8d9e7e8db1eed16fe8f06f6 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 28 Jun 2016 17:19:02 +0100 Subject: [PATCH 04/70] Regex now matches new folder nomenclature. --- .../web/public/coffee/ide/pdf/controllers/PdfController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 610915ec3a..ab85755c36 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -237,7 +237,7 @@ define [ return null normalizeFilePath = (path) -> - path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}\/(\.\/)?/, "") + path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}-[0-9a-f]{24}\/(\.\/)?/, "") path = path.replace(/^\/compile\//, "") rootDocDirname = ide.fileTreeManager.getRootDocDirname() From db213c0621526a5a33c7fd5a55a0f1e85739a926 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 29 Jun 2016 11:35:25 +0100 Subject: [PATCH 05/70] Begin keeping record of user sessions in reds. --- .../AuthenticationController.coffee | 3 ++ .../Features/User/UserController.coffee | 4 +++ .../Features/User/UserSessionsManager.coffee | 31 +++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 services/web/app/coffee/Features/User/UserSessionsManager.coffee diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index e7db5d9f65..8851363b5f 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -9,6 +9,7 @@ Url = require("url") Settings = require "settings-sharelatex" basicAuth = require('basic-auth-connect') UserHandler = require("../User/UserHandler") +UserSessionsManager = require("../User/UserSessionsManager") module.exports = AuthenticationController = login: (req, res, next = (error) ->) -> @@ -141,4 +142,6 @@ module.exports = AuthenticationController = req.session[key] = value req.session.user = lightUser + + UserSessionsManager.onLogin(user, req.sessionID, () ->) callback() diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index 5dd32d1978..3b7bbb5d29 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -8,6 +8,7 @@ logger = require("logger-sharelatex") metrics = require("../../infrastructure/Metrics") Url = require("url") AuthenticationManager = require("../Authentication/AuthenticationManager") +UserSessionsManager = require("./UserSessionsManager") UserUpdater = require("./UserUpdater") settings = require "settings-sharelatex" @@ -81,9 +82,12 @@ module.exports = UserController = logout : (req, res)-> metrics.inc "user.logout" logger.log user: req?.session?.user, "logging out" + sessionId = "#{req.sessionID}" + user = req?.session?.user req.session.destroy (err)-> if err logger.err err: err, 'error destorying session' + UserSessionsManager.onLogout(user, sessionId) res.redirect '/login' register : (req, res, next = (error) ->)-> diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee new file mode 100644 index 0000000000..2cc5d73821 --- /dev/null +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -0,0 +1,31 @@ +Settings = require('settings-sharelatex') +redis = require('redis-sharelatex') +rclient = redis.createClient(Settings.redis.web) +logger = require("logger-sharelatex") + +module.exports = UserSessionsManager = + + _sessionSetKey: (user) -> + console.log ">>", user + return "UserSessions:#{user._id}" + + onLogin: (user, sessionId, callback=(err)-> ) -> + logger.log {user_id: user._id, sessionId}, "onLogin handler" + sessionSetKey = UserSessionsManager._sessionSetKey(user) + rclient.sadd sessionSetKey, sessionId, (err, response) -> + if err + logger.err {err, user_id: user._id, sessionId}, "error while adding session key to UserSessions set" + return callback(err) + callback() + + onLogout: (user, sessionId, callback=(err)-> ) -> + logger.log {user_id: user._id, sessionId}, "onLogout handler" + if !user + logger.log {sessionId}, "no user, for some reason" + return callback() + sessionSetKey = UserSessionsManager._sessionSetKey(user) + rclient.srem sessionSetKey, sessionId, (err, response) -> + if err + logger.err {err, user_id: user._id, sessionId}, "error while removing session key from UserSessions set" + return callback(err) + callback() From 36e09e3b0c6324505ad56f255b8926b182b645fc Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 29 Jun 2016 14:19:16 +0100 Subject: [PATCH 06/70] Make the user id portion of the path optional. --- .../web/public/coffee/ide/pdf/controllers/PdfController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index ab85755c36..b9b8d234dd 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -237,7 +237,7 @@ define [ return null normalizeFilePath = (path) -> - path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}-[0-9a-f]{24}\/(\.\/)?/, "") + path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}(-[0-9a-f]{24})?\/(\.\/)?/, "") path = path.replace(/^\/compile\//, "") rootDocDirname = ide.fileTreeManager.getRootDocDirname() From 03fedafaf538668cd48c20ff0ac15c8c481799e1 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 29 Jun 2016 15:12:12 +0100 Subject: [PATCH 07/70] save the session key string, rather than just the id --- .../coffee/Features/User/UserSessionsManager.coffee | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index 2cc5d73821..edd35f2f97 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -6,13 +6,17 @@ logger = require("logger-sharelatex") module.exports = UserSessionsManager = _sessionSetKey: (user) -> - console.log ">>", user return "UserSessions:#{user._id}" + # mimic the key used by the express sessions + _sessionKey: (sessionId) -> + return "sess:#{sessionId}" + onLogin: (user, sessionId, callback=(err)-> ) -> logger.log {user_id: user._id, sessionId}, "onLogin handler" sessionSetKey = UserSessionsManager._sessionSetKey(user) - rclient.sadd sessionSetKey, sessionId, (err, response) -> + value = UserSessionsManager._sessionKey sessionId + rclient.sadd sessionSetKey, value, (err, response) -> if err logger.err {err, user_id: user._id, sessionId}, "error while adding session key to UserSessions set" return callback(err) @@ -24,7 +28,8 @@ module.exports = UserSessionsManager = logger.log {sessionId}, "no user, for some reason" return callback() sessionSetKey = UserSessionsManager._sessionSetKey(user) - rclient.srem sessionSetKey, sessionId, (err, response) -> + value = UserSessionsManager._sessionKey sessionId + rclient.srem sessionSetKey, value, (err, response) -> if err logger.err {err, user_id: user._id, sessionId}, "error while removing session key from UserSessions set" return callback(err) From 000c862ae1882c974714a435fc8ca013b5493a3b Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 29 Jun 2016 15:36:33 +0100 Subject: [PATCH 08/70] return 404 if blog returns 403, caused by file not existing --- services/web/app/coffee/Features/Blog/BlogController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Blog/BlogController.coffee b/services/web/app/coffee/Features/Blog/BlogController.coffee index 8f726602c3..11af432624 100644 --- a/services/web/app/coffee/Features/Blog/BlogController.coffee +++ b/services/web/app/coffee/Features/Blog/BlogController.coffee @@ -20,7 +20,7 @@ module.exports = BlogController = logger.log url:url, "proxying request to blog api" request.get blogUrl, (err, r, data)-> - if r?.statusCode == 404 + if r?.statusCode == 404 or r?.statusCode == 403 return ErrorController.notFound(req, res, next) if err? return res.send 500 From c5f53ef4317794423c2eaa736e65e739d3411885 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 30 Jun 2016 11:30:30 +0100 Subject: [PATCH 09/70] Track log hints feedback. --- services/web/app/views/project/editor/pdf.jade | 4 ++-- .../public/coffee/ide/pdf/controllers/PdfController.coffee | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index 091831a22d..3bc459f290 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -128,12 +128,12 @@ div.full-size.pdf(ng-controller="PdfController") ) label.card-hint-feedback-label #{translate("log_hint_feedback_label")} a.card-hint-feedback-positive( - ng-click="feedbackSent = true;" + ng-click="trackLogHintsPositiveFeedback(entry.ruleId); feedbackSent = true;" href ) #{translate("answer_yes")} span  /  a.card-hint-feedback-negative( - ng-click="feedbackSent = true;" + ng-click="trackLogHintsNegativeFeedback(entry.ruleId); feedbackSent = true;" href ) #{translate("answer_no")} .card-hint-feedback(ng-show="feedbackSent") diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index b9b8d234dd..261730e1ba 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -15,6 +15,13 @@ define [ $scope.shouldShowLogs = false $scope.wikiEnabled = window.wikiEnabled; + # log hints tracking + trackLogHintsFeedback = (isPositive, hintId) -> + event_tracking.send 'log-hints', (if isPositive then 'feedback-positive' else 'feedback-negative'), hintId + + $scope.trackLogHintsPositiveFeedback = (hintId) -> trackLogHintsFeedback true, hintId + $scope.trackLogHintsNegativeFeedback = (hintId) -> trackLogHintsFeedback false, hintId + if ace.require("ace/lib/useragent").isMac $scope.modifierKey = "Cmd" else From d93c96d43c7470ea42dc882ef8e98b3a92b13dad Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 30 Jun 2016 11:30:43 +0100 Subject: [PATCH 10/70] Improve log hints ids. --- .../coffee/ide/human-readable-logs/HumanReadableLogs.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee index 31fc11b6a3..747ca1ad08 100644 --- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee +++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee @@ -12,7 +12,8 @@ define [ ruleDetails = _getRule entry.message if (ruleDetails?) - entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/[^a-zA-Z0-9]/g, '_').toLowerCase() if ruleDetails.regexToMatch? + entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1) if ruleDetails.regexToMatch? + entry.humanReadableHint = ruleDetails.humanReadableHint if ruleDetails.humanReadableHint? entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL? From a0fcc7e3ed7cdd0b21224eb1fccc5cbf0c64c70d Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 30 Jun 2016 13:56:10 +0100 Subject: [PATCH 11/70] Skip null user objects when getting collaborators --- .../Features/Collaborators/CollaboratorsHandler.coffee | 9 +++++++-- .../Collaborators/CollaboratorsHandlerTests.coffee | 4 +++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 71737eecff..b8e0f3e606 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -30,12 +30,17 @@ module.exports = CollaboratorsHandler = getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> return callback(error) if error? + result = [] async.mapLimit members, 3, (member, cb) -> UserGetter.getUser member.id, (error, user) -> return cb(error) if error? - return cb(null, { user: user, privilegeLevel: member.privilegeLevel }) - callback + if user? + result.push { user: user, privilegeLevel: member.privilegeLevel } + cb() + (error) -> + return callback(error) if error? + callback null, result getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) -> # In future if the schema changes and getting all member ids is more expensive (multiple documents) diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index 632b43e7f2..bb7e370044 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -77,17 +77,19 @@ describe "CollaboratorsHandler", -> { id: "read-only-ref-2", privilegeLevel: "readOnly" } { id: "read-write-ref-1", privilegeLevel: "readAndWrite" } { id: "read-write-ref-2", privilegeLevel: "readAndWrite" } + { id: "doesnt-exist", privilegeLevel: "readAndWrite" } ]) @UserGetter.getUser = sinon.stub() @UserGetter.getUser.withArgs("read-only-ref-1").yields(null, { _id: "read-only-ref-1" }) @UserGetter.getUser.withArgs("read-only-ref-2").yields(null, { _id: "read-only-ref-2" }) @UserGetter.getUser.withArgs("read-write-ref-1").yields(null, { _id: "read-write-ref-1" }) @UserGetter.getUser.withArgs("read-write-ref-2").yields(null, { _id: "read-write-ref-2" }) + @UserGetter.getUser.withArgs("doesnt-exist").yields(null, null) @CollaboratorHandler.getMembersWithPrivilegeLevels @project_id, @callback it "should return an array of members with their privilege levels", -> @callback - .calledWith(undefined, [ + .calledWith(null, [ { user: { _id: "read-only-ref-1" }, privilegeLevel: "readOnly" } { user: { _id: "read-only-ref-2" }, privilegeLevel: "readOnly" } { user: { _id: "read-write-ref-1" }, privilegeLevel: "readAndWrite" } From b76cc7e31462999f8c3071beb4136eb9fee7d7b2 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 30 Jun 2016 14:21:44 +0100 Subject: [PATCH 12/70] Remove a user from being a collaborator when their account is deleted --- .../Collaborators/CollaboratorsHandler.coffee | 11 +++++++++++ .../Features/Project/ProjectDeleter.coffee | 8 +++++--- .../CollaboratorsHandlerTests.coffee | 19 ++++++++++++++++--- .../coffee/Project/ProjectDeleterTests.coffee | 10 +++++++++- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index b8e0f3e606..5f2580497d 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -87,6 +87,17 @@ module.exports = CollaboratorsHandler = logger.error err: err, "problem removing user from project collaberators" callback(err) + removeUserFromAllProjets: (user_id, callback = (error) ->) -> + CollaboratorsHandler.getProjectsUserIsCollaboratorOf user_id, { _id: 1 }, (error, readAndWriteProjects = [], readOnlyProjects = []) -> + return callback(error) if error? + allProjects = readAndWriteProjects.concat(readOnlyProjects) + jobs = [] + for project in allProjects + do (project) -> + jobs.push (cb) -> + CollaboratorsHandler.removeUserFromProject project._id, user_id, cb + async.series jobs, callback + addEmailToProject: (project_id, adding_user_id, unparsed_email, privilegeLevel, callback = (error, user) ->) -> emails = mimelib.parseAddresses(unparsed_email) email = emails[0]?.address?.toLowerCase() diff --git a/services/web/app/coffee/Features/Project/ProjectDeleter.coffee b/services/web/app/coffee/Features/Project/ProjectDeleter.coffee index 8ba8a65845..f398ea75b5 100644 --- a/services/web/app/coffee/Features/Project/ProjectDeleter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectDeleter.coffee @@ -24,9 +24,11 @@ module.exports = ProjectDeleter = update = {deletedByExternalDataSource: false} Project.update conditions, update, {}, callback - deleteUsersProjects: (owner_id, callback)-> - logger.log owner_id:owner_id, "deleting users projects" - Project.remove owner_ref:owner_id, callback + deleteUsersProjects: (user_id, callback)-> + logger.log {user_id}, "deleting users projects" + Project.remove owner_ref:user_id, (error) -> + return callback(error) if error? + CollaboratorsHandler.removeUserFromAllProjets user_id, callback deleteProject: (project_id, callback = (error) ->) -> # archiveProject takes care of the clean-up diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index bb7e370044..645c39076c 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -276,6 +276,19 @@ describe "CollaboratorsHandler", -> it "should not add any users to the proejct", -> @CollaboratorHandler.addUserIdToProject.called.should.equal false - - - + describe "removeUserFromAllProjets", -> + beforeEach (done) -> + @CollaboratorHandler.getProjectsUserIsCollaboratorOf = sinon.stub() + @CollaboratorHandler.getProjectsUserIsCollaboratorOf.withArgs(@user_id, { _id: 1 }).yields( + null, + [ { _id: "read-and-write-0" }, { _id: "read-and-write-1" } ], + [ { _id: "read-only-0" }, { _id: "read-only-1" } ] + ) + @CollaboratorHandler.removeUserFromProject = sinon.stub().yields() + @CollaboratorHandler.removeUserFromAllProjets @user_id, done + + it "should remove the user from each project", -> + for project_id in ["read-and-write-0", "read-and-write-1", "read-only-0", "read-only-1"] + @CollaboratorHandler.removeUserFromProject + .calledWith(project_id, @user_id) + .should.equal true \ No newline at end of file diff --git a/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee index 601f7867b0..6a99ed8150 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee @@ -27,13 +27,15 @@ describe 'ProjectDeleter', -> removeProjectFromAllTags: sinon.stub().callsArgWith(2) @ProjectGetter = getProject:sinon.stub() + @CollaboratorsHandler = + removeUserFromAllProjets: sinon.stub().yields() @deleter = SandboxedModule.require modulePath, requires: "../Editor/EditorController": @editorController '../../models/Project':{Project:@Project} '../DocumentUpdater/DocumentUpdaterHandler': @documentUpdaterHandler "../Tags/TagsHandler":@TagsHandler "../FileStore/FileStoreHandler": @FileStoreHandler = {} - "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} + "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler "./ProjectGetter": @ProjectGetter 'logger-sharelatex': log:-> @@ -74,6 +76,12 @@ describe 'ProjectDeleter', -> @Project.remove.calledWith(owner_ref:user_id).should.equal true done() + it "should remove all the projects the user is a collaborator of", (done)-> + user_id = 1234 + @deleter.deleteUsersProjects user_id, => + @CollaboratorsHandler.removeUserFromAllProjets.calledWith(user_id).should.equal true + done() + describe "deleteProject", -> beforeEach (done) -> @project_id = "mock-project-id-123" From 882e28a8110509c1b6a70ac14ef5f1ed6bb5d42f Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 30 Jun 2016 14:30:04 +0100 Subject: [PATCH 13/70] Don't choke on null projects --- .../Features/Collaborators/CollaboratorsHandler.coffee | 1 + .../coffee/Collaborators/CollaboratorsHandlerTests.coffee | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 5f2580497d..81557fea42 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -95,6 +95,7 @@ module.exports = CollaboratorsHandler = for project in allProjects do (project) -> jobs.push (cb) -> + return cb() if !project? CollaboratorsHandler.removeUserFromProject project._id, user_id, cb async.series jobs, callback diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index 645c39076c..37e39aaacb 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -276,13 +276,13 @@ describe "CollaboratorsHandler", -> it "should not add any users to the proejct", -> @CollaboratorHandler.addUserIdToProject.called.should.equal false - describe "removeUserFromAllProjets", -> + describe "removeUserFromAllProjects", -> beforeEach (done) -> @CollaboratorHandler.getProjectsUserIsCollaboratorOf = sinon.stub() @CollaboratorHandler.getProjectsUserIsCollaboratorOf.withArgs(@user_id, { _id: 1 }).yields( null, - [ { _id: "read-and-write-0" }, { _id: "read-and-write-1" } ], - [ { _id: "read-only-0" }, { _id: "read-only-1" } ] + [ { _id: "read-and-write-0" }, { _id: "read-and-write-1" }, null ], + [ { _id: "read-only-0" }, { _id: "read-only-1" }, null ] ) @CollaboratorHandler.removeUserFromProject = sinon.stub().yields() @CollaboratorHandler.removeUserFromAllProjets @user_id, done From c04e00026ba4b87ffcff847a9aafb84f36c8f0ef Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 30 Jun 2016 09:43:09 +0100 Subject: [PATCH 14/70] remove scaffolding for per-user compiles so that it is always by default --- .../coffee/Features/Compile/CompileController.coffee | 12 ++++-------- .../coffee/Features/Compile/CompileManager.coffee | 2 +- services/web/config/settings.defaults.coffee | 4 ++++ .../coffee/ide/pdf/controllers/PdfController.coffee | 10 ---------- .../controllers/WordCountModalController.coffee | 6 +----- .../coffee/Compile/CompileControllerTests.coffee | 6 +----- .../coffee/Compile/CompileManagerTests.coffee | 2 +- 7 files changed, 12 insertions(+), 30 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index 5f3edd4de5..a790948db5 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -29,8 +29,6 @@ module.exports = CompileController = options.compiler = req.body.compiler if req.body?.draft options.draft = req.body.draft - if req.query?.isolated is "true" - options.isolated = true logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request" CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) -> return next(error) if error? @@ -44,17 +42,15 @@ module.exports = CompileController = } _compileAsUser: (req, callback) -> - # callback with user_id if isolated flag is set on request, undefined otherwise - isolated = req.query?.isolated is "true" - if isolated + # callback with user_id if per-user, undefined otherwise + if not Settings.disablePerUserCompiles AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id) else callback() # do a per-project compile, not per-user _downloadAsUser: (req, callback) -> - # callback with user_id if isolated flag or user_id param is set on request, undefined otherwise - isolated = req.query?.isolated is "true" or req.params.user_id? - if isolated + # callback with user_id if per-user, undefined otherwise + if not Settings.disablePerUserCompiles AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id) else callback() # do a per-project compile, not per-user diff --git a/services/web/app/coffee/Features/Compile/CompileManager.coffee b/services/web/app/coffee/Features/Compile/CompileManager.coffee index 8350ca1196..c561576525 100755 --- a/services/web/app/coffee/Features/Compile/CompileManager.coffee +++ b/services/web/app/coffee/Features/Compile/CompileManager.coffee @@ -38,7 +38,7 @@ module.exports = CompileManager = for key, value of limits options[key] = value # only pass user_id down to clsi if this is a per-user compile - compileAsUser = if options.isolated then user_id else undefined + compileAsUser = if Settings.disablePerUserCompiles then undefined else user_id ClsiManager.sendRequest project_id, compileAsUser, options, (error, status, outputFiles, clsiServerId, validationProblems) -> return callback(error) if error? logger.log files: outputFiles, "output files" diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 25b240c08d..e6fb6a505d 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -262,6 +262,10 @@ module.exports = settings = # Should we allow access to any page without logging in? This includes # public projects, /learn, /templates, about pages, etc. allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false + + # Use a single compile directory for all users in a project + # (otherwise each user has their own directory) + disablePerUserCompiles: true # Maximum size of text documents in the real-time editing system. max_doc_length: 2 * 1024 * 1024 # 2mb diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index b9b8d234dd..e095df5695 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -50,8 +50,6 @@ define [ params = {} if options.isAutoCompile params["auto_compile"]=true - if perUserCompile # send ?isolated=true for per-user compiles - params["isolated"] = true return $http.post url, { rootDoc_id: options.rootDocOverride_id or null draft: $scope.draft @@ -125,9 +123,6 @@ define [ # convert the qs hash into a query string and append it $scope.pdf.qs = createQueryString qs $scope.pdf.url += $scope.pdf.qs - # special case for the download url - if perUserCompile - qs.isolated = true # Save all downloads as files qs.popupDownload = true $scope.pdf.downloadUrl = "/project/#{$scope.project_id}/output/output.pdf" + createQueryString(qs) @@ -147,8 +142,6 @@ define [ else file.name = file.path qs = {} - if perUserCompile - qs.isolated = true if response.clsiServerId? qs.clsiserverid = response.clsiServerId file.url = "/project/#{project_id}/output/#{file.path}" + createQueryString qs @@ -274,7 +267,6 @@ define [ method: "DELETE" params: clsiserverid:ide.clsiServerId - isolated: perUserCompile headers: "X-Csrf-Token": window.csrfToken } @@ -361,7 +353,6 @@ define [ line: row + 1 column: column clsiserverid:ide.clsiServerId - isolated: perUserCompile } }) .success (data) -> @@ -407,7 +398,6 @@ define [ h: h.toFixed(2) v: v.toFixed(2) clsiserverid:ide.clsiServerId - isolated: perUserCompile } }) .success (data) -> diff --git a/services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee b/services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee index 5166a32ae1..2aa4efb505 100644 --- a/services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee +++ b/services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee @@ -5,15 +5,11 @@ define [ $scope.status = loading:true - # enable per-user containers by default - perUserCompile = true - opts = url:"/project/#{ide.project_id}/wordcount" method:"GET" params: clsiserverid:ide.clsiServerId - isolated: perUserCompile $http opts .success (data) -> $scope.status.loading = false @@ -22,4 +18,4 @@ define [ $scope.status.error = true $scope.cancel = () -> - $modalInstance.dismiss('cancel') \ No newline at end of file + $modalInstance.dismiss('cancel') diff --git a/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee b/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee index 97b41fb33b..d7a13578e2 100644 --- a/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee @@ -139,7 +139,7 @@ describe "CompileController", -> .should.equal true it "should proxy the PDF from the CLSI", -> - @CompileController.proxyToClsi.calledWith(@project_id, "/project/#{@project_id}/output/output.pdf", @req, @res, @next).should.equal true + @CompileController.proxyToClsi.calledWith(@project_id, "/project/#{@project_id}/user/#{@user_id}/output/output.pdf", @req, @res, @next).should.equal true describe "when the pdf is not going to be used in pdfjs viewer", -> @@ -338,8 +338,6 @@ describe "CompileController", -> @req = params: project_id:@project_id - query: - isolated: "true" @CompileManager.compile.callsArgWith(3) @CompileController.proxyToClsi = sinon.stub() @res = @@ -362,8 +360,6 @@ describe "CompileController", -> @CompileManager.wordCount = sinon.stub().callsArgWith(3, null, {content:"body"}) @req.params = Project_id: @project_id - @req.query = - isolated: "true" @res.send = sinon.stub() @res.contentType = sinon.stub() @CompileController.wordCount @req, @res, @next diff --git a/services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee index 82a4ab734b..849e9e8ccc 100644 --- a/services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee @@ -71,7 +71,7 @@ describe "CompileManager", -> it "should run the compile with the compile limits", -> @ClsiManager.sendRequest - .calledWith(@project_id, undefined, { + .calledWith(@project_id, @user_id, { timeout: @limits.timeout }) .should.equal true From 7cb266d904f29270e539ccc4875aed495def1539 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 30 Jun 2016 14:58:54 +0100 Subject: [PATCH 15/70] removed imagemin --- services/web/Gruntfile.coffee | 25 +++++++++++++------------ services/web/package.json | 1 - 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index 1fae80dd90..a52c1b8a55 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -17,7 +17,7 @@ module.exports = (grunt) -> grunt.loadNpmTasks 'grunt-contrib-watch' grunt.loadNpmTasks 'grunt-parallel' grunt.loadNpmTasks 'grunt-exec' - grunt.loadNpmTasks 'grunt-contrib-imagemin' + # grunt.loadNpmTasks 'grunt-contrib-imagemin' grunt.loadNpmTasks 'grunt-contrib-cssmin' config = @@ -47,17 +47,17 @@ module.exports = (grunt) -> stream:true - imagemin: - dynamic: - files: [{ - expand: true - cwd: 'public/img/' - src: ['**/*.{png,jpg,gif}'] - dest: 'public/img/' - }] - options: - interlaced:false - optimizationLevel: 7 + # imagemin: + # dynamic: + # files: [{ + # expand: true + # cwd: 'public/img/' + # src: ['**/*.{png,jpg,gif}'] + # dest: 'public/img/' + # }] + # options: + # interlaced:false + # optimizationLevel: 7 @@ -218,6 +218,7 @@ module.exports = (grunt) -> pattern: "@@RELEASE@@" replacement: process.env.BUILD_NUMBER || "(unknown build)" + availabletasks: diff --git a/services/web/package.json b/services/web/package.json index 8d14a350dc..86c7dee403 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -70,7 +70,6 @@ "grunt-contrib-clean": "0.5.0", "grunt-contrib-coffee": "0.10.0", "grunt-contrib-cssmin": "^1.0.1", - "grunt-contrib-imagemin": "^1.0.1", "grunt-contrib-less": "0.9.0", "grunt-contrib-requirejs": "0.4.1", "grunt-contrib-watch": "^1.0.0", From 73ad1fde5ca0e2f362c9b9d70ea4527c28201ec7 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 30 Jun 2016 15:15:37 +0100 Subject: [PATCH 16/70] comment out disabling of per user compiles --- services/web/config/settings.defaults.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index e6fb6a505d..47902d5826 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -265,7 +265,7 @@ module.exports = settings = # Use a single compile directory for all users in a project # (otherwise each user has their own directory) - disablePerUserCompiles: true + # disablePerUserCompiles: true # Maximum size of text documents in the real-time editing system. max_doc_length: 2 * 1024 * 1024 # 2mb From 88bef5c5ea5eb663147e603aaff1d0d3e82bb8b5 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 1 Jul 2016 09:51:22 +0100 Subject: [PATCH 17/70] Add `revokeAllSessions` handler, when password is reset --- .../app/coffee/Features/User/UserController.coffee | 13 ++++++------- .../coffee/Features/User/UserSessionsManager.coffee | 4 ++++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index 3b7bbb5d29..1362da6823 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -121,16 +121,15 @@ module.exports = UserController = logger.log user: user, "password changed" AuthenticationManager.setUserPassword user._id, newPassword1, (error) -> return next(error) if error? - res.send - message: - type:'success' - text:'Your password has been changed' + UserSessionsManager.revokeAllSessions user, (err) -> + return next(err) if err + res.send + message: + type:'success' + text:'Your password has been changed' else logger.log user: user, "current password wrong" res.send message: type:'error' text:'Your old password is wrong' - - - diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index edd35f2f97..4804df004c 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -34,3 +34,7 @@ module.exports = UserSessionsManager = logger.err {err, user_id: user._id, sessionId}, "error while removing session key from UserSessions set" return callback(err) callback() + + revokeAllSessions: (user, callback=(err)->) -> + logger.log {user_id: user._id}, "revoking all existing sessions for user" + callback(null) From ab26edb44bc5f0016d215470d1e75e8e167ac0c7 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 1 Jul 2016 10:20:22 +0100 Subject: [PATCH 18/70] Adjust UserController unit tests --- .../test/UnitTests/coffee/User/UserControllerTests.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee b/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee index d92b8f39d7..098b60fe5d 100644 --- a/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee @@ -44,6 +44,10 @@ describe "UserController", -> siteUrl: "sharelatex.example.com" @UserHandler = populateGroupLicenceInvite:sinon.stub().callsArgWith(1) + @UserSessionsManager = + onLogin: sinon.stub() + onLogout: sinon.stub() + revokeAllSessions: sinon.stub().callsArgWith(1, null) @UserController = SandboxedModule.require modulePath, requires: "./UserLocator": @UserLocator "./UserDeleter": @UserDeleter @@ -56,6 +60,7 @@ describe "UserController", -> "../Referal/ReferalAllocator":@ReferalAllocator "../Subscription/SubscriptionDomainHandler":@SubscriptionDomainHandler "./UserHandler":@UserHandler + "./UserSessionsManager": @UserSessionsManager "settings-sharelatex": @settings "logger-sharelatex": log:-> From d8ffa5b4b14dbbf01adee849d8aceaad3aa453e7 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 1 Jul 2016 11:24:46 +0100 Subject: [PATCH 19/70] set expiry on the user sessions set. --- .../Features/User/UserSessionsManager.coffee | 35 +++++++++++++------ .../app/coffee/infrastructure/Server.coffee | 5 ++- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index 4804df004c..f808f7bceb 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -16,11 +16,14 @@ module.exports = UserSessionsManager = logger.log {user_id: user._id, sessionId}, "onLogin handler" sessionSetKey = UserSessionsManager._sessionSetKey(user) value = UserSessionsManager._sessionKey sessionId - rclient.sadd sessionSetKey, value, (err, response) -> - if err - logger.err {err, user_id: user._id, sessionId}, "error while adding session key to UserSessions set" - return callback(err) - callback() + rclient.multi() + .sadd(sessionSetKey, value) + .expire(sessionSetKey, "#{Settings.cookieSessionLength}") + .exec (err, response) -> + if err + logger.err {err, user_id: user._id, sessionId}, "error while adding session key to UserSessions set" + return callback(err) + callback() onLogout: (user, sessionId, callback=(err)-> ) -> logger.log {user_id: user._id, sessionId}, "onLogout handler" @@ -29,12 +32,24 @@ module.exports = UserSessionsManager = return callback() sessionSetKey = UserSessionsManager._sessionSetKey(user) value = UserSessionsManager._sessionKey sessionId - rclient.srem sessionSetKey, value, (err, response) -> - if err - logger.err {err, user_id: user._id, sessionId}, "error while removing session key from UserSessions set" - return callback(err) - callback() + rclient.multi() + .srem(sessionSetKey, value) + .expire(sessionSetKey, "#{Settings.cookieSessionLength}") + .exec (err, response) -> + if err + logger.err {err, user_id: user._id, sessionId}, "error while removing session key from UserSessions set" + return callback(err) + callback() revokeAllSessions: (user, callback=(err)->) -> logger.log {user_id: user._id}, "revoking all existing sessions for user" callback(null) + + touch: (user, callback=(err)->) -> + if !user + return callback(null) + sessionSetKey = UserSessionsManager._sessionSetKey(user) + rclient.expire sessionSetKey, "#{Settings.cookieSessionLength}", (err, response) -> + if err + logger.err {err, user_id: user._id}, "error while updating ttl on UserSessions set" + return callback(err) diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index 11a3b5237e..b5b49d1329 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -31,6 +31,7 @@ translations = require("translations-sharelatex").setup(Settings.i18n) Modules = require "./Modules" ErrorController = require "../Features/Errors/ErrorController" +UserSessionsManager = require "../Features/User/UserSessionsManager" metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongojs/node_modules/mongodb"), logger) metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongoose/node_modules/mongodb"), logger) @@ -89,6 +90,8 @@ webRouter.use translations.setLangBasedOnDomainMiddlewear # Measure expiry from last request, not last login webRouter.use (req, res, next) -> req.session.touch() + if req?.session?.user + UserSessionsManager.touch(req.session.user, (err)->) next() webRouter.use ReferalConnect.use @@ -114,7 +117,7 @@ app.use (req, res, next) -> apiRouter.get "/status", (req, res)-> res.send("web sharelatex is alive") - + profiler = require "v8-profiler" apiRouter.get "/profile", (req, res) -> time = parseInt(req.query.time || "1000") From 5ae0aeaf94cb67eb3c9848b36c96d2c4efeff8a8 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 1 Jul 2016 13:37:56 +0100 Subject: [PATCH 20/70] Fix logging. --- .../web/app/coffee/Features/User/UserSessionsManager.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index f808f7bceb..62e2ddc22d 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -21,7 +21,7 @@ module.exports = UserSessionsManager = .expire(sessionSetKey, "#{Settings.cookieSessionLength}") .exec (err, response) -> if err - logger.err {err, user_id: user._id, sessionId}, "error while adding session key to UserSessions set" + logger.err {err, user_id: user._id, sessionSetKey}, "error while adding session key to UserSessions set" return callback(err) callback() @@ -37,7 +37,7 @@ module.exports = UserSessionsManager = .expire(sessionSetKey, "#{Settings.cookieSessionLength}") .exec (err, response) -> if err - logger.err {err, user_id: user._id, sessionId}, "error while removing session key from UserSessions set" + logger.err {err, user_id: user._id, sessionSetKey}, "error while removing session key from UserSessions set" return callback(err) callback() From 8855d0fa0fa7bf90e8804850b21ea462952c7dd9 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 1 Jul 2016 13:38:13 +0100 Subject: [PATCH 21/70] Clear out sessions --- .../Features/User/UserSessionsManager.coffee | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index 62e2ddc22d..bad1a1cb37 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -43,7 +43,20 @@ module.exports = UserSessionsManager = revokeAllSessions: (user, callback=(err)->) -> logger.log {user_id: user._id}, "revoking all existing sessions for user" - callback(null) + sessionSetKey = UserSessionsManager._sessionSetKey(user) + rclient.smembers sessionSetKey, (err, sessionKeys) -> + if err + logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set" + return callback(err) + logger.log {user_id: user._id, count: sessionKeys.length}, "deleting sessions for user" + rclient.multi() + .del(sessionKeys) + .srem(sessionSetKey, sessionKeys) + .exec (err, result) -> + if err + logger.err {err, user_id: user._id, sessionSetKey}, "error revoking all sessions for user" + return callback(err) + callback(null) touch: (user, callback=(err)->) -> if !user From 6938f1d055ba2fd14d07328a59b37517a979320a Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 1 Jul 2016 15:04:13 +0100 Subject: [PATCH 22/70] No need to build extra string --- services/web/app/coffee/Features/User/UserController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index 1362da6823..f40bf59037 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -82,7 +82,7 @@ module.exports = UserController = logout : (req, res)-> metrics.inc "user.logout" logger.log user: req?.session?.user, "logging out" - sessionId = "#{req.sessionID}" + sessionId = req.sessionID user = req?.session?.user req.session.destroy (err)-> if err From 6f4a7f4779a22e1668737a21f39afe19c956a8aa Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 1 Jul 2016 15:04:39 +0100 Subject: [PATCH 23/70] mvp for using sprites for flags, needs border removing --- services/web/Gruntfile.coffee | 11 +- services/web/app/views/layout/footer.jade | 6 +- services/web/public/img/sprite.png | Bin 0 -> 14176 bytes .../web/public/stylesheets/app/sprites.less | 105 ++++++++++++++++++ services/web/public/stylesheets/style.less | 1 + 5 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 services/web/public/img/sprite.png create mode 100644 services/web/public/stylesheets/app/sprites.less diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index a52c1b8a55..99f29ed2a6 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -17,8 +17,9 @@ module.exports = (grunt) -> grunt.loadNpmTasks 'grunt-contrib-watch' grunt.loadNpmTasks 'grunt-parallel' grunt.loadNpmTasks 'grunt-exec' - # grunt.loadNpmTasks 'grunt-contrib-imagemin' grunt.loadNpmTasks 'grunt-contrib-cssmin' + # grunt.loadNpmTasks 'grunt-contrib-imagemin' + grunt.loadNpmTasks 'grunt-sprity' config = @@ -59,6 +60,14 @@ module.exports = (grunt) -> # interlaced:false # optimizationLevel: 7 + sprity: + + sprite: + options: + cssPath:"/img/" + 'style': '../../public/stylesheets/app/sprites.less' + src: ['./public/img/flags/24/*.png'] + dest: './public/img/sprite', coffee: diff --git a/services/web/app/views/layout/footer.jade b/services/web/app/views/layout/footer.jade index 7ef26b3493..bc57352086 100644 --- a/services/web/app/views/layout/footer.jade +++ b/services/web/app/views/layout/footer.jade @@ -15,7 +15,7 @@ footer.site-footer aria-expanded="false", tooltip="#{translate('language')}" ) - img(src="/img/flags/24/#{currentLngCode}.png") + img(class="icon icon-#{currentLngCode}") ul.dropdown-menu(role="menu") li.dropdown-header #{translate("language")} @@ -23,9 +23,9 @@ footer.site-footer if !subdomainDetails.hide li.lngOption a.menu-indent(href=subdomainDetails.url+currentUrl) - img(src="/img/flags/24/#{subdomainDetails.lngCode}.png") + img(class="icon icon-#{subdomainDetails.lngCode}") | #{translate(subdomainDetails.lngCode)} - + //- img(src="/img/flags/24/.png") each item in nav.left_footer li if item.url diff --git a/services/web/public/img/sprite.png b/services/web/public/img/sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..4de03050d5b139f21a4b86cfe5763fb74c8f266e GIT binary patch literal 14176 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
  • 7G+Z@k{rk6)92`buTB}s&hrX%FYDmH^`y6>mvM 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 Date: Fri, 1 Jul 2016 15:33:59 +0100 Subject: [PATCH 24/70] Refactor method names in `UserSessionsManager` --- .../AuthenticationController.coffee | 10 ++-- .../Features/User/UserController.coffee | 4 +- .../Features/User/UserSessionsManager.coffee | 6 +-- .../AuthenticationControllerTests.coffee | 53 ++++++++++--------- .../coffee/User/UserControllerTests.coffee | 28 +++++----- 5 files changed, 51 insertions(+), 50 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 8851363b5f..58ff246794 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -14,7 +14,7 @@ UserSessionsManager = require("../User/UserSessionsManager") module.exports = AuthenticationController = login: (req, res, next = (error) ->) -> AuthenticationController.doLogin req.body, req, res, next - + doLogin: (options, req, res, next) -> email = options.email?.toLowerCase() password = options.password @@ -62,7 +62,7 @@ module.exports = AuthenticationController = requireLogin: () -> doRequest = (req, res, next = (error) ->) -> if !req.session.user? - AuthenticationController._redirectToLoginOrRegisterPage(req, res) + AuthenticationController._redirectToLoginOrRegisterPage(req, res) else req.user = req.session.user return next() @@ -93,9 +93,9 @@ module.exports = AuthenticationController = _redirectToLoginOrRegisterPage: (req, res)-> if req.query.zipUrl? or req.query.project_name? - return AuthenticationController._redirectToRegisterPage(req, res) + return AuthenticationController._redirectToRegisterPage(req, res) else - AuthenticationController._redirectToLoginPage(req, res) + AuthenticationController._redirectToLoginPage(req, res) _redirectToLoginPage: (req, res) -> @@ -143,5 +143,5 @@ module.exports = AuthenticationController = req.session.user = lightUser - UserSessionsManager.onLogin(user, req.sessionID, () ->) + UserSessionsManager.trackSession(user, req.sessionID, () ->) callback() diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index f40bf59037..c85ed98e1e 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -87,7 +87,7 @@ module.exports = UserController = req.session.destroy (err)-> if err logger.err err: err, 'error destorying session' - UserSessionsManager.onLogout(user, sessionId) + UserSessionsManager.untrackSession(user, sessionId) res.redirect '/login' register : (req, res, next = (error) ->)-> @@ -121,7 +121,7 @@ module.exports = UserController = logger.log user: user, "password changed" AuthenticationManager.setUserPassword user._id, newPassword1, (error) -> return next(error) if error? - UserSessionsManager.revokeAllSessions user, (err) -> + UserSessionsManager.revokeAllUserSessions user, (err) -> return next(err) if err res.send message: diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index bad1a1cb37..5d9b19d68f 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -12,7 +12,7 @@ module.exports = UserSessionsManager = _sessionKey: (sessionId) -> return "sess:#{sessionId}" - onLogin: (user, sessionId, callback=(err)-> ) -> + trackSession: (user, sessionId, callback=(err)-> ) -> logger.log {user_id: user._id, sessionId}, "onLogin handler" sessionSetKey = UserSessionsManager._sessionSetKey(user) value = UserSessionsManager._sessionKey sessionId @@ -25,7 +25,7 @@ module.exports = UserSessionsManager = return callback(err) callback() - onLogout: (user, sessionId, callback=(err)-> ) -> + untrackSession: (user, sessionId, callback=(err)-> ) -> logger.log {user_id: user._id, sessionId}, "onLogout handler" if !user logger.log {sessionId}, "no user, for some reason" @@ -41,7 +41,7 @@ module.exports = UserSessionsManager = return callback(err) callback() - revokeAllSessions: (user, callback=(err)->) -> + revokeAllUserSessions: (user, callback=(err)->) -> logger.log {user_id: user._id}, "revoking all existing sessions for user" sessionSetKey = UserSessionsManager._sessionSetKey(user) rclient.smembers sessionSetKey, (err, sessionKeys) -> diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee index f83b38617b..d981824246 100644 --- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee @@ -21,6 +21,10 @@ describe "AuthenticationController", -> "../User/UserHandler": @UserHandler = {setupLoginData:sinon.stub()} "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } "settings-sharelatex": {} + "../User/UserSessionsManager": @UserSessionsManager = + trackSession: sinon.stub() + untrackSession: sinon.stub() + revokeAllUserSessions: sinon.stub().callsArgWith(1, null) @user = _id: ObjectId() email: @email = "USER@example.com" @@ -57,7 +61,7 @@ describe "AuthenticationController", -> @res.statusCode.should.equal 429 done() @AuthenticationController.login(@req, @res) - + describe 'when the user is authenticated', -> beforeEach -> @LoginRateLimiter.processLoginRequest.callsArgWith(1, null, true) @@ -76,7 +80,7 @@ describe "AuthenticationController", -> @AuthenticationController.establishUserSession .calledWith(@req, @user) .should.equal true - + it "should set res.session.justLoggedIn", -> @req.session.justLoggedIn.should.equal true @@ -95,7 +99,7 @@ describe "AuthenticationController", -> @logger.log .calledWith(email: @email.toLowerCase(), user_id: @user._id.toString(), "successful log in") .should.equal true - + describe 'when the user is not authenticated', -> beforeEach -> @@ -112,7 +116,7 @@ describe "AuthenticationController", -> it "should not establish a session", -> @AuthenticationController.establishUserSession.called.should.equal false - + it "should not setup the user data in the background", -> @UserHandler.setupLoginData.called.should.equal false @@ -137,12 +141,12 @@ describe "AuthenticationController", -> describe "getLoggedInUserId", -> beforeEach -> - @req = + @req = session :{} it "should return the user id from the session", (done)-> @user_id = "2134" - @req.session.user = + @req.session.user = _id:@user_id @AuthenticationController.getLoggedInUserId @req, (err, user_id)=> expect(user_id).to.equal @user_id @@ -168,7 +172,7 @@ describe "AuthenticationController", -> describe "getLoggedInUser", -> beforeEach -> @UserGetter.getUser = sinon.stub().callsArgWith(1, null, @user) - + describe "with an established session", -> beforeEach -> @req.session = @@ -189,7 +193,7 @@ describe "AuthenticationController", -> _id: "user-id-123" email: "user@sharelatex.com" @middleware = @AuthenticationController.requireLogin() - + describe "when the user is logged in", -> beforeEach -> @req.session = @@ -219,13 +223,13 @@ describe "AuthenticationController", -> beforeEach -> @req.headers = {} @AuthenticationController.httpAuth = sinon.stub() - + describe "with white listed url", -> beforeEach -> @AuthenticationController.addEndpointToLoginWhitelist "/login" @req._parsedUrl.pathname = "/login" @AuthenticationController.requireGlobalLogin @req, @res, @next - + it "should call next() directly", -> @next.called.should.equal true @@ -235,9 +239,9 @@ describe "AuthenticationController", -> @req._parsedUrl.pathname = "/login" @req.url = "/login?query=something" @AuthenticationController.requireGlobalLogin @req, @res, @next - + it "should call next() directly", -> - @next.called.should.equal true + @next.called.should.equal true describe "with http auth", -> beforeEach -> @@ -248,20 +252,20 @@ describe "AuthenticationController", -> @AuthenticationController.httpAuth .calledWith(@req, @res, @next) .should.equal true - + describe "with a user session", -> beforeEach -> @req.session = user: {"mock": "user"} @AuthenticationController.requireGlobalLogin @req, @res, @next - + it "should call next() directly", -> @next.called.should.equal true - + describe "with no login credentials", -> beforeEach -> @AuthenticationController.requireGlobalLogin @req, @res, @next - + it "should redirect to the /login page", -> @res.redirectedTo.should.equal "/login" @@ -274,7 +278,7 @@ describe "AuthenticationController", -> @req.query = {} describe "they have come directly to the url", -> - beforeEach -> + beforeEach -> @req.query = {} @middleware(@req, @res, @next) @@ -284,7 +288,7 @@ describe "AuthenticationController", -> describe "they have come via a templates link", -> - beforeEach -> + beforeEach -> @req.query.zipUrl = "something" @middleware(@req, @res, @next) @@ -293,8 +297,8 @@ describe "AuthenticationController", -> @AuthenticationController._redirectToLoginPage.calledWith(@req, @res).should.equal false describe "they have been invited to a project", -> - - beforeEach -> + + beforeEach -> @req.query.project_name = "something" @middleware(@req, @res, @next) @@ -333,7 +337,7 @@ describe "AuthenticationController", -> beforeEach -> @UserUpdater.updateUser = sinon.stub().callsArg(2) @AuthenticationController._recordSuccessfulLogin(@user._id, @callback) - + it "should increment the user.login.success metric", -> @Metrics.inc .calledWith("user.login.success") @@ -349,7 +353,7 @@ describe "AuthenticationController", -> describe "_recordFailedLogin", -> beforeEach -> @AuthenticationController._recordFailedLogin(@callback) - + it "should increment the user.login.failed metric", -> @Metrics.inc .calledWith("user.login.failed") @@ -374,13 +378,12 @@ describe "AuthenticationController", -> @req.session.user.last_name.should.equal @user.last_name @req.session.user.referal_id.should.equal @user.referal_id @req.session.user.isAdmin.should.equal @user.isAdmin - + it "should destroy the session", -> @req.session.destroy.called.should.equal true - + it "should regenerate the session to protect against session fixation", -> @req.sessionStore.generate.calledWith(@req).should.equal true it "should return the callback", -> @callback.called.should.equal true - diff --git a/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee b/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee index 098b60fe5d..bb16d872cd 100644 --- a/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee @@ -19,9 +19,9 @@ describe "UserController", -> save:sinon.stub().callsArgWith(0) ace:{} - @UserDeleter = + @UserDeleter = deleteUser: sinon.stub().callsArgWith(1) - @UserLocator = + @UserLocator = findById: sinon.stub().callsArgWith(1, null, @user) @User = findById: sinon.stub().callsArgWith(1, null, @user) @@ -36,18 +36,18 @@ describe "UserController", -> setUserPassword: sinon.stub() @ReferalAllocator = allocate:sinon.stub() - @SubscriptionDomainHandler = + @SubscriptionDomainHandler = autoAllocate:sinon.stub() @UserUpdater = changeEmailAddress:sinon.stub() @settings = siteUrl: "sharelatex.example.com" - @UserHandler = + @UserHandler = populateGroupLicenceInvite:sinon.stub().callsArgWith(1) @UserSessionsManager = - onLogin: sinon.stub() - onLogout: sinon.stub() - revokeAllSessions: sinon.stub().callsArgWith(1, null) + trackSession: sinon.stub() + untrackSession: sinon.stub() + revokeAllUserSessions: sinon.stub().callsArgWith(1, null) @UserController = SandboxedModule.require modulePath, requires: "./UserLocator": @UserLocator "./UserDeleter": @UserDeleter @@ -62,13 +62,13 @@ describe "UserController", -> "./UserHandler":@UserHandler "./UserSessionsManager": @UserSessionsManager "settings-sharelatex": @settings - "logger-sharelatex": + "logger-sharelatex": log:-> err:-> "../../infrastructure/Metrics": inc:-> - @req = - session: + @req = + session: destroy:-> user : _id : @user_id @@ -203,12 +203,12 @@ describe "UserController", -> @UserRegistrationHandler.registerNewUserAndSendActivationEmail = sinon.stub().callsArgWith(1, null, @user, @url = "mock/url") @req.body.email = @user.email = @email = "email@example.com" @UserController.register @req, @res - + it "should register the user and send them an email", -> @UserRegistrationHandler.registerNewUserAndSendActivationEmail .calledWith(@email) .should.equal true - + it "should return the user and activation url", -> @res.json .calledWith({ @@ -238,7 +238,7 @@ describe "UserController", -> @res.send = => @AuthenticationManager.setUserPassword.called.should.equal false done() - @UserController.changePassword @req, @res + @UserController.changePassword @req, @res it "should set the new password if they do match", (done)-> @AuthenticationManager.authenticate.callsArgWith(2, null, @user) @@ -250,5 +250,3 @@ describe "UserController", -> @AuthenticationManager.setUserPassword.calledWith(@user._id, "newpass").should.equal true done() @UserController.changePassword @req, @res - - From 0ac9b05d024b2dea83ef50e53c6d7f881986623c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 1 Jul 2016 15:49:07 +0100 Subject: [PATCH 25/70] Add `ip_address` and `session_created` to the session user object. --- .../Features/Authentication/AuthenticationController.coffee | 2 ++ .../Authentication/AuthenticationControllerTests.coffee | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 58ff246794..d75bef5207 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -133,6 +133,8 @@ module.exports = AuthenticationController = isAdmin: user.isAdmin email: user.email referal_id: user.referal_id + session_created: (new Date()).toISOString() + ip_address: req.ip # Regenerate the session to get a new sessionID (cookie value) to # protect against session fixation attacks oldSession = req.session diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee index d981824246..4e9b248f25 100644 --- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee @@ -369,6 +369,7 @@ describe "AuthenticationController", -> destroy : sinon.stub() @req.sessionStore = generate: sinon.stub() + @req.ip = "1.2.3.4" @AuthenticationController.establishUserSession @req, @user, @callback it "should set the session user to a basic version of the user", -> @@ -378,6 +379,9 @@ describe "AuthenticationController", -> @req.session.user.last_name.should.equal @user.last_name @req.session.user.referal_id.should.equal @user.referal_id @req.session.user.isAdmin.should.equal @user.isAdmin + @req.session.user.ip_address.should.equal @req.ip + expect(typeof @req.session.user.ip_address).to.equal 'string' + expect(typeof @req.session.user.session_created).to.equal 'string' it "should destroy the session", -> @req.session.destroy.called.should.equal true From 6f20f565fe9b7f5f7c50ceb0ecbab5829ada51f6 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 1 Jul 2016 15:55:50 +0100 Subject: [PATCH 26/70] Add early-return guards --- .../Features/User/UserSessionsManager.coffee | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index 5d9b19d68f..cbaa4a7760 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -13,6 +13,12 @@ module.exports = UserSessionsManager = return "sess:#{sessionId}" trackSession: (user, sessionId, callback=(err)-> ) -> + if !user + logger.log {sessionId}, "no user to track, returning" + return callback(null) + if !sessionId + logger.log {user_id: user._id}, "no sessionId to track, returning" + return callback(null) logger.log {user_id: user._id, sessionId}, "onLogin handler" sessionSetKey = UserSessionsManager._sessionSetKey(user) value = UserSessionsManager._sessionKey sessionId @@ -26,6 +32,12 @@ module.exports = UserSessionsManager = callback() untrackSession: (user, sessionId, callback=(err)-> ) -> + if !user + logger.log {sessionId}, "no user to untrack, returning" + return callback(null) + if !sessionId + logger.log {user_id: user._id}, "no sessionId to untrack, returning" + return callback(null) logger.log {user_id: user._id, sessionId}, "onLogout handler" if !user logger.log {sessionId}, "no user, for some reason" @@ -42,6 +54,9 @@ module.exports = UserSessionsManager = callback() revokeAllUserSessions: (user, callback=(err)->) -> + if !user + logger.log {}, "no user to revoke sessions for, returning" + return callback(null) logger.log {user_id: user._id}, "revoking all existing sessions for user" sessionSetKey = UserSessionsManager._sessionSetKey(user) rclient.smembers sessionSetKey, (err, sessionKeys) -> @@ -60,6 +75,7 @@ module.exports = UserSessionsManager = touch: (user, callback=(err)->) -> if !user + logger.log {}, "no user to touch sessions for, returning" return callback(null) sessionSetKey = UserSessionsManager._sessionSetKey(user) rclient.expire sessionSetKey, "#{Settings.cookieSessionLength}", (err, response) -> From 4f49a25639fb9f1956d50c00d8653bf0d836ab02 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 1 Jul 2016 16:02:01 +0100 Subject: [PATCH 27/70] Use a figure element instead of an img, as there is no src attribute. --- services/web/app/views/layout/footer.jade | 4 ++-- services/web/public/stylesheets/components/footer.less | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/layout/footer.jade b/services/web/app/views/layout/footer.jade index bc57352086..ea7a36e2ae 100644 --- a/services/web/app/views/layout/footer.jade +++ b/services/web/app/views/layout/footer.jade @@ -15,7 +15,7 @@ footer.site-footer aria-expanded="false", tooltip="#{translate('language')}" ) - img(class="icon icon-#{currentLngCode}") + figure(class="icon icon-lang icon-#{currentLngCode}") ul.dropdown-menu(role="menu") li.dropdown-header #{translate("language")} @@ -23,7 +23,7 @@ footer.site-footer if !subdomainDetails.hide li.lngOption a.menu-indent(href=subdomainDetails.url+currentUrl) - img(class="icon icon-#{subdomainDetails.lngCode}") + figure(class="icon icon-lang icon-#{subdomainDetails.lngCode}") | #{translate(subdomainDetails.lngCode)} //- img(src="/img/flags/24/.png") each item in nav.left_footer diff --git a/services/web/public/stylesheets/components/footer.less b/services/web/public/stylesheets/components/footer.less index 7a3bc66723..32adc1ee90 100644 --- a/services/web/public/stylesheets/components/footer.less +++ b/services/web/public/stylesheets/components/footer.less @@ -23,3 +23,8 @@ footer.site-footer { } } } + +.icon-lang { + display: inline-block; + vertical-align: middle; +} \ No newline at end of file From 64a73b0356ff85a40407a40399cd66f1b78ef8d1 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 1 Jul 2016 16:02:40 +0100 Subject: [PATCH 28/70] Disable default 4px margin on grunt-sprity, also add the built files. --- services/web/Gruntfile.coffee | 1 + services/web/public/img/sprite.png | Bin 14176 -> 12032 bytes .../web/public/stylesheets/app/sprites.less | 118 +++++++++--------- 3 files changed, 60 insertions(+), 59 deletions(-) diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index 99f29ed2a6..988172e257 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -66,6 +66,7 @@ module.exports = (grunt) -> options: cssPath:"/img/" 'style': '../../public/stylesheets/app/sprites.less' + margin: 0 src: ['./public/img/flags/24/*.png'] dest: './public/img/sprite', diff --git a/services/web/public/img/sprite.png b/services/web/public/img/sprite.png index 4de03050d5b139f21a4b86cfe5763fb74c8f266e..366d994ce8428da6c637f81a2cfa683705745ed1 100644 GIT binary patch literal 12032 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
  • 7G+Z@k{rk6)92`buTB}s&hrX%FYDmH^`y6>mvM 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 Date: Mon, 4 Jul 2016 09:27:38 +0100 Subject: [PATCH 29/70] check sessions on login and logout. --- .../Features/User/UserSessionsManager.coffee | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index cbaa4a7760..0d4dff5927 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -2,6 +2,7 @@ Settings = require('settings-sharelatex') redis = require('redis-sharelatex') rclient = redis.createClient(Settings.redis.web) logger = require("logger-sharelatex") +Async = require('async') module.exports = UserSessionsManager = @@ -30,6 +31,8 @@ module.exports = UserSessionsManager = logger.err {err, user_id: user._id, sessionSetKey}, "error while adding session key to UserSessions set" return callback(err) callback() + UserSessionsManager._checkSessions(user, () ->) + null untrackSession: (user, sessionId, callback=(err)-> ) -> if !user @@ -52,6 +55,8 @@ module.exports = UserSessionsManager = logger.err {err, user_id: user._id, sessionSetKey}, "error while removing session key from UserSessions set" return callback(err) callback() + UserSessionsManager._checkSessions(user, () ->) + null revokeAllUserSessions: (user, callback=(err)->) -> if !user @@ -82,3 +87,35 @@ module.exports = UserSessionsManager = if err logger.err {err, user_id: user._id}, "error while updating ttl on UserSessions set" return callback(err) + + _checkSessions: (user, callback=(err)->) -> + if !user + logger.log {}, "no user, returning" + return callback(null) + logger.log {user_id: user._id}, "checking sessions for user" + sessionSetKey = UserSessionsManager._sessionSetKey(user) + rclient.smembers sessionSetKey, (err, sessionKeys) -> + if err + logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set" + return callback(err) + logger.log {user_id: user._id, count: sessionKeys.length}, "checking sessions for user" + Async.series( + sessionKeys.map( + (key) -> + (next) -> + rclient.get key, (err, val) -> + if err + return next(err) + if val == null + logger.log {user_id: user._id, key}, ">> removing key from UserSessions set" + rclient.srem sessionSetKey, key, (err, result) -> + if err + return next(err) + return next(null) + else + next() + ) + , (err, results) -> + logger.log {user_id: user._id}, "done checking sessions for user" + return callback(err) + ) From a869c99c60cee02e2b57f0828ba170f80590a506 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 4 Jul 2016 10:05:34 +0100 Subject: [PATCH 30/70] Begin testing `UserSessionsManager`. --- .../Features/User/UserSessionsManager.coffee | 3 +- .../coffee/User/UserSessionsManager.coffee | 47 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index 0d4dff5927..87d5398b8a 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -1,9 +1,10 @@ Settings = require('settings-sharelatex') redis = require('redis-sharelatex') -rclient = redis.createClient(Settings.redis.web) logger = require("logger-sharelatex") Async = require('async') +rclient = redis.createClient(Settings.redis.web) + module.exports = UserSessionsManager = _sessionSetKey: (user) -> diff --git a/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee b/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee new file mode 100644 index 0000000000..9aca469a3b --- /dev/null +++ b/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee @@ -0,0 +1,47 @@ +sinon = require('sinon') +chai = require('chai') +should = chai.should() +expect = chai.expect +modulePath = "../../../../app/js/Features/User/UserSessionsManager.js" +SandboxedModule = require('sandboxed-module') + +describe 'UserSessionsManager', -> + + beforeEach -> + @user = + _id: "abcd" + email: "user@example.com" + @sessionId = 'some_session_id' + @rclient = + multi: () => @rclient + exec: sinon.stub() + get: sinon.stub().returns(@rclient) + sadd: sinon.stub().returns(@rclient) + srem: sinon.stub().returns(@rclient) + smembers: sinon.stub().returns(@rclient) + expire: sinon.stub().returns(@rclient) + @redis = + createClient: () -> @rclient + @logger = + err: sinon.stub() + error: sinon.stub() + log: sinon.stub() + @settings = + redis: + web: {} + @UserSessionsManager = SandboxedModule.require modulePath, requires: + "redis-sharelatex": @redis + "logger-sharelatex": @logger + "settings-sharelatex": @settings + + describe '_sessionSetKey', -> + + it 'should build the correct key', -> + result = @UserSessionsManager._sessionSetKey(@user) + result.should.equal 'UserSessions:abcd' + + describe '_sessionKey', -> + + it 'should build the correct key', -> + result = @UserSessionsManager._sessionKey(@sessionId) + result.should.equal 'sess:some_session_id' From 2ae2e6ed4f4a56830af15fe5a45f08712e70787e Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 4 Jul 2016 10:37:01 +0100 Subject: [PATCH 31/70] Test `trackSession` --- .../Features/User/UserSessionsManager.coffee | 6 +- .../coffee/User/UserSessionsManager.coffee | 117 ++++++++++++++++-- 2 files changed, 112 insertions(+), 11 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index 87d5398b8a..d64fd7d846 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -31,9 +31,8 @@ module.exports = UserSessionsManager = if err logger.err {err, user_id: user._id, sessionSetKey}, "error while adding session key to UserSessions set" return callback(err) + UserSessionsManager._checkSessions(user, () ->) callback() - UserSessionsManager._checkSessions(user, () ->) - null untrackSession: (user, sessionId, callback=(err)-> ) -> if !user @@ -55,9 +54,8 @@ module.exports = UserSessionsManager = if err logger.err {err, user_id: user._id, sessionSetKey}, "error while removing session key from UserSessions set" return callback(err) + UserSessionsManager._checkSessions(user, () ->) callback() - UserSessionsManager._checkSessions(user, () ->) - null revokeAllUserSessions: (user, callback=(err)->) -> if !user diff --git a/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee b/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee index 9aca469a3b..a995ccc1c8 100644 --- a/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee +++ b/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee @@ -12,16 +12,25 @@ describe 'UserSessionsManager', -> _id: "abcd" email: "user@example.com" @sessionId = 'some_session_id' + @rclient = - multi: () => @rclient + multi: sinon.stub() exec: sinon.stub() - get: sinon.stub().returns(@rclient) - sadd: sinon.stub().returns(@rclient) - srem: sinon.stub().returns(@rclient) - smembers: sinon.stub().returns(@rclient) - expire: sinon.stub().returns(@rclient) + get: sinon.stub() + sadd: sinon.stub() + srem: sinon.stub() + smembers: sinon.stub() + expire: sinon.stub() + @rclient.multi.returns(@rclient) + @rclient.get.returns(@rclient) + @rclient.sadd.returns(@rclient) + @rclient.srem.returns(@rclient) + @rclient.smembers.returns(@rclient) + @rclient.expire.returns(@rclient) + @rclient.exec.callsArgWith(0, null) + @redis = - createClient: () -> @rclient + createClient: () => @rclient @logger = err: sinon.stub() error: sinon.stub() @@ -45,3 +54,97 @@ describe 'UserSessionsManager', -> it 'should build the correct key', -> result = @UserSessionsManager._sessionKey(@sessionId) result.should.equal 'sess:some_session_id' + + describe 'trackSession', -> + + beforeEach -> + @call = (callback) => + @UserSessionsManager.trackSession @user, @sessionId, callback + @rclient.exec.callsArgWith(0, null) + @_checkSessions = sinon.stub(@UserSessionsManager, '_checkSessions').returns(null) + + afterEach -> + @_checkSessions.restore() + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + done() + + it 'should call the appropriate redis methods', (done) -> + @call (err) => + @rclient.multi.callCount.should.equal 1 + @rclient.sadd.callCount.should.equal 1 + @rclient.expire.callCount.should.equal 1 + @rclient.exec.callCount.should.equal 1 + done() + + it 'should call _checkSessions', (done) -> + @call (err) => + @_checkSessions.callCount.should.equal 1 + done() + + describe 'when rclient produces an error', -> + + beforeEach -> + @rclient.exec.callsArgWith(0, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + it 'should not call _checkSessions', (done) -> + @call (err) => + @_checkSessions.callCount.should.equal 0 + done() + + describe 'when no user is supplied', -> + + beforeEach -> + @call = (callback) => + @UserSessionsManager.trackSession null, @sessionId, callback + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.equal null + done() + + it 'should not call the appropriate redis methods', (done) -> + @call (err) => + @rclient.multi.callCount.should.equal 0 + @rclient.sadd.callCount.should.equal 0 + @rclient.expire.callCount.should.equal 0 + @rclient.exec.callCount.should.equal 0 + done() + + it 'should not call _checkSessions', (done) -> + @call (err) => + @_checkSessions.callCount.should.equal 0 + done() + + describe 'when no sessionId is supplied', -> + + beforeEach -> + @call = (callback) => + @UserSessionsManager.trackSession @user, null, callback + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.equal null + done() + + it 'should not call the appropriate redis methods', (done) -> + @call (err) => + @rclient.multi.callCount.should.equal 0 + @rclient.sadd.callCount.should.equal 0 + @rclient.expire.callCount.should.equal 0 + @rclient.exec.callCount.should.equal 0 + done() + + it 'should not call _checkSessions', (done) -> + @call (err) => + @_checkSessions.callCount.should.equal 0 + done() From dfcb47fb5c849ef4ee5949cde8f449a9b569c538 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 4 Jul 2016 10:56:13 +0100 Subject: [PATCH 32/70] test `untrackSession` --- .../coffee/User/UserSessionsManager.coffee | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee b/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee index a995ccc1c8..5e6990ac25 100644 --- a/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee +++ b/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee @@ -148,3 +148,97 @@ describe 'UserSessionsManager', -> @call (err) => @_checkSessions.callCount.should.equal 0 done() + + describe 'untrackSession', -> + + beforeEach -> + @call = (callback) => + @UserSessionsManager.untrackSession @user, @sessionId, callback + @rclient.exec.callsArgWith(0, null) + @_checkSessions = sinon.stub(@UserSessionsManager, '_checkSessions').returns(null) + + afterEach -> + @_checkSessions.restore() + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + done() + + it 'should call the appropriate redis methods', (done) -> + @call (err) => + @rclient.multi.callCount.should.equal 1 + @rclient.srem.callCount.should.equal 1 + @rclient.expire.callCount.should.equal 1 + @rclient.exec.callCount.should.equal 1 + done() + + it 'should call _checkSessions', (done) -> + @call (err) => + @_checkSessions.callCount.should.equal 1 + done() + + describe 'when rclient produces an error', -> + + beforeEach -> + @rclient.exec.callsArgWith(0, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + it 'should not call _checkSessions', (done) -> + @call (err) => + @_checkSessions.callCount.should.equal 0 + done() + + describe 'when no user is supplied', -> + + beforeEach -> + @call = (callback) => + @UserSessionsManager.untrackSession null, @sessionId, callback + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.equal null + done() + + it 'should not call the appropriate redis methods', (done) -> + @call (err) => + @rclient.multi.callCount.should.equal 0 + @rclient.srem.callCount.should.equal 0 + @rclient.expire.callCount.should.equal 0 + @rclient.exec.callCount.should.equal 0 + done() + + it 'should not call _checkSessions', (done) -> + @call (err) => + @_checkSessions.callCount.should.equal 0 + done() + + describe 'when no sessionId is supplied', -> + + beforeEach -> + @call = (callback) => + @UserSessionsManager.untrackSession @user, null, callback + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.equal null + done() + + it 'should not call the appropriate redis methods', (done) -> + @call (err) => + @rclient.multi.callCount.should.equal 0 + @rclient.srem.callCount.should.equal 0 + @rclient.expire.callCount.should.equal 0 + @rclient.exec.callCount.should.equal 0 + done() + + it 'should not call _checkSessions', (done) -> + @call (err) => + @_checkSessions.callCount.should.equal 0 + done() From 699ba21032aa95867bce6cb8334500d484d1167f Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 4 Jul 2016 11:15:28 +0100 Subject: [PATCH 33/70] test `revokeAllUserSessions` --- .../coffee/User/UserSessionsManager.coffee | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee b/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee index 5e6990ac25..dcb1271d16 100644 --- a/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee +++ b/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee @@ -17,12 +17,14 @@ describe 'UserSessionsManager', -> multi: sinon.stub() exec: sinon.stub() get: sinon.stub() + del: sinon.stub() sadd: sinon.stub() srem: sinon.stub() smembers: sinon.stub() expire: sinon.stub() @rclient.multi.returns(@rclient) @rclient.get.returns(@rclient) + @rclient.del.returns(@rclient) @rclient.sadd.returns(@rclient) @rclient.srem.returns(@rclient) @rclient.smembers.returns(@rclient) @@ -242,3 +244,60 @@ describe 'UserSessionsManager', -> @call (err) => @_checkSessions.callCount.should.equal 0 done() + + ## + describe 'revokeAllUserSessions', -> + + beforeEach -> + @sessionKeys = ['one', 'two'] + @rclient.smembers.callsArgWith(1, null, @sessionKeys) + @rclient.exec.callsArgWith(0, null) + @call = (callback) => + @UserSessionsManager.revokeAllUserSessions @user, callback + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + done() + + it 'should call the appropriate redis methods', (done) -> + @call (err) => + @rclient.smembers.callCount.should.equal 1 + @rclient.multi.callCount.should.equal 1 + @rclient.del.callCount.should.equal 1 + @rclient.del.firstCall.args[0].should.deep.equal(@sessionKeys) + @rclient.srem.callCount.should.equal 1 + @rclient.srem.firstCall.args[1].should.deep.equal(@sessionKeys) + @rclient.exec.callCount.should.equal 1 + done() + + describe 'when rclient produces an error', -> + + beforeEach -> + @rclient.exec.callsArgWith(0, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + describe 'when no user is supplied', -> + + beforeEach -> + @call = (callback) => + @UserSessionsManager.revokeAllUserSessions null, callback + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.equal null + done() + + it 'should not call the appropriate redis methods', (done) -> + @call (err) => + @rclient.smembers.callCount.should.equal 0 + @rclient.multi.callCount.should.equal 0 + @rclient.del.callCount.should.equal 0 + @rclient.srem.callCount.should.equal 0 + @rclient.exec.callCount.should.equal 0 + done() From df0a7bc54745ef4138416e99c0d09be31a10c050 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 4 Jul 2016 14:04:10 +0100 Subject: [PATCH 34/70] test the `_checkSessions` function. --- .../Features/User/UserSessionsManager.coffee | 1 + .../coffee/User/UserSessionsManager.coffee | 126 ++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index d64fd7d846..d672419d0b 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -86,6 +86,7 @@ module.exports = UserSessionsManager = if err logger.err {err, user_id: user._id}, "error while updating ttl on UserSessions set" return callback(err) + callback(null) _checkSessions: (user, callback=(err)->) -> if !user diff --git a/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee b/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee index dcb1271d16..52699da099 100644 --- a/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee +++ b/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee @@ -165,6 +165,7 @@ describe 'UserSessionsManager', -> it 'should not produce an error', (done) -> @call (err) => expect(err).to.not.be.instanceof Error + expect(err).to.equal undefined done() it 'should call the appropriate redis methods', (done) -> @@ -258,6 +259,7 @@ describe 'UserSessionsManager', -> it 'should not produce an error', (done) -> @call (err) => expect(err).to.not.be.instanceof Error + expect(err).to.equal null done() it 'should call the appropriate redis methods', (done) -> @@ -301,3 +303,127 @@ describe 'UserSessionsManager', -> @rclient.srem.callCount.should.equal 0 @rclient.exec.callCount.should.equal 0 done() + + describe 'touch', -> + + beforeEach -> + @rclient.expire.callsArgWith(2, null) + @call = (callback) => + @UserSessionsManager.touch @user, callback + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.equal null + done() + + it 'should call rclient.expire', (done) -> + @call (err) => + @rclient.expire.callCount.should.equal 1 + done() + + describe 'when rclient produces an error', -> + + beforeEach -> + @rclient.expire.callsArgWith(2, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + describe 'when no user is supplied', -> + + beforeEach -> + @call = (callback) => + @UserSessionsManager.touch null, callback + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.equal null + done() + + it 'should not call expire', (done) -> + @call (err) => + @rclient.expire.callCount.should.equal 0 + done() + + describe '_checkSessions', -> + + beforeEach -> + @call = (callback) => + @UserSessionsManager._checkSessions @user, callback + @sessionKeys = ['one', 'two'] + @rclient.smembers.callsArgWith(1, null, @sessionKeys) + @rclient.get.callsArgWith(1, null, 'some-value') + @rclient.srem.callsArgWith(2, null, {}) + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.equal undefined + done() + + it 'should call the appropriate redis methods', (done) -> + @call (err) => + @rclient.smembers.callCount.should.equal 1 + @rclient.get.callCount.should.equal 2 + @rclient.srem.callCount.should.equal 0 + done() + + describe 'when one of the keys is not present in redis', -> + + beforeEach -> + @rclient.get.onCall(0).callsArgWith(1, null, 'some-val') + @rclient.get.onCall(1).callsArgWith(1, null, null) + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.equal undefined + done() + + it 'should remove that key from the set', (done) -> + @call (err) => + @rclient.smembers.callCount.should.equal 1 + @rclient.get.callCount.should.equal 2 + @rclient.srem.callCount.should.equal 1 + @rclient.srem.firstCall.args[1].should.equal 'two' + done() + + describe 'when no user is supplied', -> + + beforeEach -> + @call = (callback) => + @UserSessionsManager._checkSessions null, callback + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.equal null + done() + + it 'should not call redis methods', (done) -> + @call (err) => + @rclient.smembers.callCount.should.equal 0 + @rclient.get.callCount.should.equal 0 + done() + + describe 'when one of the get operations produces an error', -> + + beforeEach -> + @rclient.get.onCall(0).callsArgWith(1, new Error('woops'), null) + @rclient.get.onCall(1).callsArgWith(1, null, null) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + it 'should call the right redis methods, bailing out early', (done) -> + @call (err) => + @rclient.smembers.callCount.should.equal 1 + @rclient.get.callCount.should.equal 1 + @rclient.srem.callCount.should.equal 0 + done() From 040739ef7f34579c637891050fc687219be1b9f0 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 4 Jul 2016 16:03:29 +0100 Subject: [PATCH 35/70] Comment out sprity and leave a not in README.md. --- services/web/Gruntfile.coffee | 19 +++++++++---------- services/web/README.md | 8 ++++++++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index 988172e257..78ce4190d0 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -19,7 +19,7 @@ module.exports = (grunt) -> grunt.loadNpmTasks 'grunt-exec' grunt.loadNpmTasks 'grunt-contrib-cssmin' # grunt.loadNpmTasks 'grunt-contrib-imagemin' - grunt.loadNpmTasks 'grunt-sprity' + # grunt.loadNpmTasks 'grunt-sprity' config = @@ -60,15 +60,14 @@ module.exports = (grunt) -> # interlaced:false # optimizationLevel: 7 - sprity: - - sprite: - options: - cssPath:"/img/" - 'style': '../../public/stylesheets/app/sprites.less' - margin: 0 - src: ['./public/img/flags/24/*.png'] - dest: './public/img/sprite', + # sprity: + # sprite: + # options: + # cssPath:"/img/" + # 'style': '../../public/stylesheets/app/sprites.less' + # margin: 0 + # src: ['./public/img/flags/24/*.png'] + # dest: './public/img/sprite' coffee: diff --git a/services/web/README.md b/services/web/README.md index 00d94343ab..323b43f028 100644 --- a/services/web/README.md +++ b/services/web/README.md @@ -6,9 +6,17 @@ web-sharelatex is the front-end web service of the open-source web-based collabo It serves all the HTML pages, CSS and javascript to the client. web-sharelatex also contains a lot of logic around creating and editing projects, and account management. + The rest of the ShareLaTeX stack, along with information about contributing can be found in the [sharelatex/sharelatex](https://github.com/sharelatex/sharelatex) repository. +Build process +---------------- + +web-sharelatex uses [Grunt](http://gruntjs.com/) to build its front-end related assets. + +Image processing tasks are commented out in the gruntfile and the needed packages aren't presently in the project's `package.json`. If the images need to be processed again (minified and sprited), start by fetching the packages (`npm install grunt-contrib-imagemin grunt-sprity`), then *decomment* the tasks in `Gruntfile.coffee`. After this, the tasks can be called (explicitly, via `grunt imagemin` and `grunt sprity`). + Unit test status ---------------- From 7dd1906eaf746a68fd478bb59ab84ef0d9589df4 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 4 Jul 2016 16:51:31 +0100 Subject: [PATCH 36/70] Initial Countly integration. --- services/web/app/views/layout.jade | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index 82b230c976..3ab9e23a64 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -59,6 +59,27 @@ html(itemscope, itemtype='http://schema.org/Product') email: "#{session.user.email}", }) // End Heap Analytics + + // Countly Analytics + if (settings.analytics && settings.analytics.countly && settings.analytics.countly.token) + script(type="text/javascript"). + var Countly = Countly || {}; + Countly.q = Countly.q || []; + Countly.app_key = "#{settings.analytics.countly.token}"; + Countly.url = "https://try.count.ly"; + Countly.q.push(['track_sessions']); + Countly.q.push(['track_pageview']); + + (function() { + var cly = document.createElement('script'); cly.type = 'text/javascript'; + cly.async = true; + //enter url of script here + cly.src = 'https://cdnjs.cloudflare.com/ajax/libs/countly-sdk-web/16.6.0/countly.min.js'; + cly.onload = function(){Countly.init()}; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(cly, s); + })(); + + // End countly Analytics script(type="text/javascript"). window.csrfToken = "#{csrfToken}"; From bec3d2ad4260847020726ce729664d6719e34db1 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 5 Jul 2016 10:24:24 +0100 Subject: [PATCH 37/70] start acceptance tests for sessions --- services/web/README.md | 8 +++ .../acceptance/coffee/SessionTests.coffee | 51 +++++++++++++++++++ .../acceptance/coffee/helpers/User.coffee | 21 +++++++- .../acceptance/coffee/helpers/redis.coffee | 27 ++++++++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 services/web/test/acceptance/coffee/SessionTests.coffee create mode 100644 services/web/test/acceptance/coffee/helpers/redis.coffee diff --git a/services/web/README.md b/services/web/README.md index 00d94343ab..9d69f8744d 100644 --- a/services/web/README.md +++ b/services/web/README.md @@ -42,3 +42,11 @@ in the `public/img/iconshock` directory found via [findicons.com](http://findicons.com/icon/498089/height?id=526085#) +## Acceptance Tests + +To run the Acceptance tests: + +- set `allowPublicAccess` to true, either in the configuration file, + or by setting the environment variable `SHARELATEX_ALLOW_PUBLIC_ACCESS` to `true` +- start the server (`grunt`) +- in a separate terminal, run `grunt test:acceptance` diff --git a/services/web/test/acceptance/coffee/SessionTests.coffee b/services/web/test/acceptance/coffee/SessionTests.coffee new file mode 100644 index 0000000000..e0d3471b84 --- /dev/null +++ b/services/web/test/acceptance/coffee/SessionTests.coffee @@ -0,0 +1,51 @@ +expect = require("chai").expect +async = require("async") +User = require "./helpers/User" +request = require "./helpers/request" +settings = require "settings-sharelatex" +redis = require "./helpers/redis" + +describe "Sessions", -> + before (done) -> + @timeout(10000) + @user1 = new User() + @site_admin = new User({email: "admin@example.com"}) + async.series [ + (cb) => @user1.login cb + (cb) => @user1.logout cb + ], done + + describe "one session", -> + + it "should have one session in UserSessions", (done) -> + async.series( + [ + (next) => + redis.clearUserSessions @user1, next + + # login, should add session to set + , (next) => + @user1.login (err) -> + next(err) + + , (next) => + redis.getUserSessions @user1, (err, sessions) => + expect(sessions.length).to.equal 1 + expect(sessions[0].slice(0, 5)).to.equal 'sess:' + next() + + # logout, should remove session from set + , (next) => + @user1.logout (err) -> + next(err) + + , (next) => + redis.getUserSessions @user1, (err, sessions) => + expect(sessions.length).to.equal 0 + next() + + ], (err, result) => + if err + throw err + done() + ) diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index c13a45499d..5198d5a6d2 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -27,11 +27,28 @@ class User db.users.findOne {email: @email}, (error, user) => return callback(error) if error? @id = user?._id?.toString() + @_id = user?._id?.toString() callback() - + + logout: (callback = (error) ->) -> + @getCsrfToken (error) => + return callback(error) if error? + @request.get { + url: "/logout" # Register will log in, but also ensure user exists + json: + email: @email + password: @password + }, (error, response, body) => + return callback(error) if error? + db.users.findOne {email: @email}, (error, user) => + return callback(error) if error? + @id = user?._id?.toString() + @_id = user?._id?.toString() + callback() + ensure_admin: (callback = (error) ->) -> db.users.update {_id: ObjectId(@id)}, { $set: { isAdmin: true }}, callback - + createProject: (name, callback = (error, project_id) ->) -> @request.post { url: "/project/new", diff --git a/services/web/test/acceptance/coffee/helpers/redis.coffee b/services/web/test/acceptance/coffee/helpers/redis.coffee new file mode 100644 index 0000000000..2611e4fc57 --- /dev/null +++ b/services/web/test/acceptance/coffee/helpers/redis.coffee @@ -0,0 +1,27 @@ +Settings = require('settings-sharelatex') +redis = require('redis-sharelatex') +logger = require("logger-sharelatex") +Async = require('async') + +rclient = redis.createClient(Settings.redis.web) + +module.exports = + + getUserSessions: (user, callback=(err, sessionsSet)->) -> + rclient.smembers "UserSessions:#{user._id}", (err, result) -> + return callback(err, result) + + clearUserSessions: (user, callback=(err)->) -> + sessionSetKey = "UserSessions:#{user._id}" + rclient.smembers sessionSetKey, (err, sessionKeys) -> + if err + return callback(err) + if sessionKeys.length == 0 + return callback(null) + rclient.multi() + .del(sessionKeys) + .srem(sessionSetKey, sessionKeys) + .exec (err, result) -> + if err + return callback(err) + callback(null) From fac3698c40a2590e879c400901a3d6332960b86b Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 5 Jul 2016 11:08:58 +0100 Subject: [PATCH 38/70] Better namespacing of sprite CSS classes. --- services/web/app/views/layout/footer.jade | 4 +- .../web/public/stylesheets/app/sprites.less | 42 +++++++++---------- .../public/stylesheets/components/footer.less | 2 +- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/services/web/app/views/layout/footer.jade b/services/web/app/views/layout/footer.jade index ea7a36e2ae..efd64b6f6e 100644 --- a/services/web/app/views/layout/footer.jade +++ b/services/web/app/views/layout/footer.jade @@ -15,7 +15,7 @@ footer.site-footer aria-expanded="false", tooltip="#{translate('language')}" ) - figure(class="icon icon-lang icon-#{currentLngCode}") + figure(class="sprite-icon sprite-icon-lang sprite-icon-#{currentLngCode}") ul.dropdown-menu(role="menu") li.dropdown-header #{translate("language")} @@ -23,7 +23,7 @@ footer.site-footer if !subdomainDetails.hide li.lngOption a.menu-indent(href=subdomainDetails.url+currentUrl) - figure(class="icon icon-lang icon-#{subdomainDetails.lngCode}") + figure(class="sprite-icon sprite-icon-lang sprite-icon-#{subdomainDetails.lngCode}") | #{translate(subdomainDetails.lngCode)} //- img(src="/img/flags/24/.png") each item in nav.left_footer diff --git a/services/web/public/stylesheets/app/sprites.less b/services/web/public/stylesheets/app/sprites.less index 8b658648f0..b64e84ad68 100644 --- a/services/web/public/stylesheets/app/sprites.less +++ b/services/web/public/stylesheets/app/sprites.less @@ -1,104 +1,104 @@ -.icon { +.sprite-icon { background-image: url('/img/sprite.png'); } -.icon-ko { +.sprite-icon-ko { background-position: -0px -0px; width: 24px; height: 24px; } -.icon-cn { +.sprite-icon-cn { background-position: -0px -24px; width: 24px; height: 24px; } -.icon-da { +.sprite-icon-da { background-position: -0px -48px; width: 24px; height: 24px; } -.icon-de { +.sprite-icon-de { background-position: -0px -72px; width: 24px; height: 24px; } -.icon-en { +.sprite-icon-en { background-position: -0px -96px; width: 24px; height: 24px; } -.icon-es { +.sprite-icon-es { background-position: -0px -120px; width: 24px; height: 24px; } -.icon-fi { +.sprite-icon-fi { background-position: -0px -144px; width: 24px; height: 24px; } -.icon-fr { +.sprite-icon-fr { background-position: -0px -168px; width: 24px; height: 24px; } -.icon-it { +.sprite-icon-it { background-position: -0px -192px; width: 24px; height: 24px; } -.icon-ja { +.sprite-icon-ja { background-position: -0px -216px; width: 24px; height: 24px; } -.icon-cs { +.sprite-icon-cs { background-position: -0px -240px; width: 24px; height: 24px; } -.icon-nl { +.sprite-icon-nl { background-position: -0px -264px; width: 24px; height: 24px; } -.icon-no { +.sprite-icon-no { background-position: -0px -288px; width: 24px; height: 24px; } -.icon-pl { +.sprite-icon-pl { background-position: -0px -312px; width: 24px; height: 24px; } -.icon-pt { +.sprite-icon-pt { background-position: -0px -336px; width: 24px; height: 24px; } -.icon-ru { +.sprite-icon-ru { background-position: -0px -360px; width: 24px; height: 24px; } -.icon-sv { +.sprite-icon-sv { background-position: -0px -384px; width: 24px; height: 24px; } -.icon-tr { +.sprite-icon-tr { background-position: -0px -408px; width: 24px; height: 24px; } -.icon-uk { +.sprite-icon-uk { background-position: -0px -432px; width: 24px; height: 24px; } -.icon-zh-CN { +.sprite-icon-zh-CN { background-position: -0px -456px; width: 24px; height: 24px; diff --git a/services/web/public/stylesheets/components/footer.less b/services/web/public/stylesheets/components/footer.less index 32adc1ee90..e4e4f82da1 100644 --- a/services/web/public/stylesheets/components/footer.less +++ b/services/web/public/stylesheets/components/footer.less @@ -24,7 +24,7 @@ footer.site-footer { } } -.icon-lang { +.sprite-icon-lang { display: inline-block; vertical-align: middle; } \ No newline at end of file From 6e282ab30882238332f8aa68d6f23fa44dbdc4c8 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 5 Jul 2016 14:19:59 +0100 Subject: [PATCH 39/70] clear sessions on password reset --- .../PasswordReset/PasswordResetController.coffee | 15 +++++++++------ .../PasswordResetControllerTests.coffee | 12 +++++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee b/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee index fb0f75beee..ec5371f0f2 100644 --- a/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee +++ b/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee @@ -2,6 +2,7 @@ PasswordResetHandler = require("./PasswordResetHandler") RateLimiter = require("../../infrastructure/RateLimiter") AuthenticationController = require("../Authentication/AuthenticationController") UserGetter = require("../User/UserGetter") +UserSessionsManager = require("../User/UserSessionsManager") logger = require "logger-sharelatex" module.exports = @@ -47,11 +48,13 @@ module.exports = PasswordResetHandler.setNewUserPassword passwordResetToken?.trim(), password?.trim(), (err, found, user_id) -> return next(err) if err? if found - if req.body.login_after - UserGetter.getUser user_id, {email: 1}, (err, user) -> - return next(err) if err? - AuthenticationController.doLogin {email:user.email, password: password}, req, res, next - else - res.sendStatus 200 + UserSessionsManager.revokeAllUserSessions {_id: user_id}, [], (err) -> + return next(err) if err? + if req.body.login_after + UserGetter.getUser user_id, {email: 1}, (err, user) -> + return next(err) if err? + AuthenticationController.doLogin {email:user.email, password: password}, req, res, next + else + res.sendStatus 200 else res.sendStatus 404 diff --git a/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee b/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee index 27852e3e07..89c6479734 100644 --- a/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee @@ -17,6 +17,8 @@ describe "PasswordResetController", -> setNewUserPassword:sinon.stub() @RateLimiter = addCount: sinon.stub() + @UserSessionsManager = + revokeAllUserSessions: sinon.stub().callsArgWith(2, null) @PasswordResetController = SandboxedModule.require modulePath, requires: "settings-sharelatex":@settings "./PasswordResetHandler":@PasswordResetHandler @@ -24,6 +26,7 @@ describe "PasswordResetController", -> "../../infrastructure/RateLimiter":@RateLimiter "../Authentication/AuthenticationController": @AuthenticationController = {} "../User/UserGetter": @UserGetter = {} + "../User/UserSessionsManager": @UserSessionsManager @email = "bob@bob.com " @token = "my security token that was emailed to me" @@ -134,7 +137,14 @@ describe "PasswordResetController", -> @req.session.should.not.have.property 'resetToken' done() @PasswordResetController.setNewUserPassword @req, @res - + + it 'should clear sessions', (done) -> + @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true) + @res.sendStatus = (code)=> + @UserSessionsManager.revokeAllUserSessions.callCount.should.equal 1 + done() + @PasswordResetController.setNewUserPassword @req, @res + it "should login user if login_after is set", (done) -> @UserGetter.getUser = sinon.stub().callsArgWith(2, null, { email: "joe@example.com" }) @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true, @user_id = "user-id-123") From 5b9903551aad915410da515e75c2cac323518fc2 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 5 Jul 2016 14:20:47 +0100 Subject: [PATCH 40/70] add an array of session ids to retain. --- .../app/coffee/Features/User/UserController.coffee | 2 +- .../coffee/Features/User/UserSessionsManager.coffee | 12 ++++++++---- .../UnitTests/coffee/User/UserControllerTests.coffee | 2 +- .../UnitTests/coffee/User/UserSessionsManager.coffee | 11 +++++++---- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index c85ed98e1e..ce96dd2a2f 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -121,7 +121,7 @@ module.exports = UserController = logger.log user: user, "password changed" AuthenticationManager.setUserPassword user._id, newPassword1, (error) -> return next(error) if error? - UserSessionsManager.revokeAllUserSessions user, (err) -> + UserSessionsManager.revokeAllUserSessions user, [req.sessionID], (err) -> return next(err) if err res.send message: diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index d672419d0b..547e8c9d96 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -2,6 +2,7 @@ Settings = require('settings-sharelatex') redis = require('redis-sharelatex') logger = require("logger-sharelatex") Async = require('async') +_ = require('underscore') rclient = redis.createClient(Settings.redis.web) @@ -57,7 +58,9 @@ module.exports = UserSessionsManager = UserSessionsManager._checkSessions(user, () ->) callback() - revokeAllUserSessions: (user, callback=(err)->) -> + revokeAllUserSessions: (user, retain, callback=(err)->) -> + if !retain + retain = [] if !user logger.log {}, "no user to revoke sessions for, returning" return callback(null) @@ -67,10 +70,11 @@ module.exports = UserSessionsManager = if err logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set" return callback(err) - logger.log {user_id: user._id, count: sessionKeys.length}, "deleting sessions for user" + keysToDelete = _.filter(sessionKeys, (k) => k not in retain) + logger.log {user_id: user._id, count: keysToDelete.length}, "deleting sessions for user" rclient.multi() - .del(sessionKeys) - .srem(sessionSetKey, sessionKeys) + .del(keysToDelete) + .srem(sessionSetKey, keysToDelete) .exec (err, result) -> if err logger.err {err, user_id: user._id, sessionSetKey}, "error revoking all sessions for user" diff --git a/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee b/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee index bb16d872cd..19d17e7ac3 100644 --- a/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee @@ -47,7 +47,7 @@ describe "UserController", -> @UserSessionsManager = trackSession: sinon.stub() untrackSession: sinon.stub() - revokeAllUserSessions: sinon.stub().callsArgWith(1, null) + revokeAllUserSessions: sinon.stub().callsArgWith(2, null) @UserController = SandboxedModule.require modulePath, requires: "./UserLocator": @UserLocator "./UserDeleter": @UserDeleter diff --git a/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee b/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee index 52699da099..c7e1438718 100644 --- a/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee +++ b/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee @@ -254,7 +254,7 @@ describe 'UserSessionsManager', -> @rclient.smembers.callsArgWith(1, null, @sessionKeys) @rclient.exec.callsArgWith(0, null) @call = (callback) => - @UserSessionsManager.revokeAllUserSessions @user, callback + @UserSessionsManager.revokeAllUserSessions @user, [], callback it 'should not produce an error', (done) -> @call (err) => @@ -266,10 +266,13 @@ describe 'UserSessionsManager', -> @call (err) => @rclient.smembers.callCount.should.equal 1 @rclient.multi.callCount.should.equal 1 + @rclient.del.callCount.should.equal 1 - @rclient.del.firstCall.args[0].should.deep.equal(@sessionKeys) + expect(@rclient.del.firstCall.args[0]).to.deep.equal @sessionKeys + @rclient.srem.callCount.should.equal 1 - @rclient.srem.firstCall.args[1].should.deep.equal(@sessionKeys) + expect(@rclient.srem.firstCall.args[1]).to.deep.equal @sessionKeys + @rclient.exec.callCount.should.equal 1 done() @@ -287,7 +290,7 @@ describe 'UserSessionsManager', -> beforeEach -> @call = (callback) => - @UserSessionsManager.revokeAllUserSessions null, callback + @UserSessionsManager.revokeAllUserSessions null, [], callback it 'should not produce an error', (done) -> @call (err) => From 2caa80bbcb99d06c01bfedb47ad9bf39d179b803 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 5 Jul 2016 14:21:38 +0100 Subject: [PATCH 41/70] acceptance test for logging in two sessions. --- .../acceptance/coffee/SessionTests.coffee | 66 ++++++++++++++++++- .../acceptance/coffee/helpers/User.coffee | 17 ++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/services/web/test/acceptance/coffee/SessionTests.coffee b/services/web/test/acceptance/coffee/SessionTests.coffee index e0d3471b84..679d168f97 100644 --- a/services/web/test/acceptance/coffee/SessionTests.coffee +++ b/services/web/test/acceptance/coffee/SessionTests.coffee @@ -17,7 +17,7 @@ describe "Sessions", -> describe "one session", -> - it "should have one session in UserSessions", (done) -> + it "should have one session in UserSessions set", (done) -> async.series( [ (next) => @@ -49,3 +49,67 @@ describe "Sessions", -> throw err done() ) + + describe "two sessions", -> + + before -> + # set up second session for this user + @user2 = new User() + @user2.email = @user1.email + @user2.password = @user1.password + + it "should have two sessions in UserSessions set", (done) -> + async.series( + [ + (next) => + redis.clearUserSessions @user1, next + + # login, should add session to set + , (next) => + @user1.login (err) -> + next(err) + + , (next) => + redis.getUserSessions @user1, (err, sessions) => + expect(sessions.length).to.equal 1 + expect(sessions[0].slice(0, 5)).to.equal 'sess:' + next() + + # login again, should add the second session to set + , (next) => + @user2.login (err) -> + next(err) + + , (next) => + redis.getUserSessions @user1, (err, sessions) => + expect(sessions.length).to.equal 2 + expect(sessions[0].slice(0, 5)).to.equal 'sess:' + expect(sessions[1].slice(0, 5)).to.equal 'sess:' + next() + + # logout first session, should remove session from set + , (next) => + @user1.logout (err) -> + next(err) + + , (next) => + redis.getUserSessions @user1, (err, sessions) => + expect(sessions.length).to.equal 1 + next() + + # logout second session, should remove last session from set + , (next) => + @user2.logout (err) -> + next(err) + + , (next) => + redis.getUserSessions @user1, (err, sessions) => + expect(sessions.length).to.equal 0 + next() + + ], (err, result) => + if err + throw err + done() + ) + diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index 5198d5a6d2..fa6f3a9ba4 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -91,4 +91,19 @@ class User }) callback() -module.exports = User \ No newline at end of file + resetPassword: (newPassword, callback = (error) ->) -> + @getCsrfToken (error) => + return callback(error) if error? + @request.post { + url: "/user/password/set" # Register will log in, but also ensure user exists + json: + password: @password + }, (error, response, body) => + return callback(error) if error? + db.users.findOne {email: @email}, (error, user) => + return callback(error) if error? + @id = user?._id?.toString() + @_id = user?._id?.toString() + callback() + +module.exports = User From a40cbdd84e68c2ce4bb2e800ba3c4aa1c5c3add3 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 5 Jul 2016 14:32:56 +0100 Subject: [PATCH 42/70] Track share modal opening. --- .../coffee/ide/share/controllers/ShareController.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/share/controllers/ShareController.coffee b/services/web/public/coffee/ide/share/controllers/ShareController.coffee index d76bd07965..b3813b27b2 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareController.coffee @@ -1,8 +1,10 @@ define [ "base" ], (App) -> - App.controller "ShareController", ["$scope", "$modal", ($scope, $modal) -> + App.controller "ShareController", ["$scope", "$modal", "event_tracking", ($scope, $modal, event_tracking) -> $scope.openShareProjectModal = () -> + event_tracking.send('share-modal-opened') + $modal.open( templateUrl: "shareProjectModalTemplate" controller: "ShareProjectModalController" From 3f1a47eccbf5e64b8b511c9c72b82db26e5d25f8 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 5 Jul 2016 14:54:26 +0100 Subject: [PATCH 43/70] map the 'retain' values to session ids --- .../web/app/coffee/Features/User/UserSessionsManager.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index 547e8c9d96..571cfd7b8d 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -61,6 +61,7 @@ module.exports = UserSessionsManager = revokeAllUserSessions: (user, retain, callback=(err)->) -> if !retain retain = [] + retain = retain.map((i) -> UserSessionsManager._sessionKey(i)) if !user logger.log {}, "no user to revoke sessions for, returning" return callback(null) @@ -70,7 +71,7 @@ module.exports = UserSessionsManager = if err logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set" return callback(err) - keysToDelete = _.filter(sessionKeys, (k) => k not in retain) + keysToDelete = _.filter(sessionKeys, (k) -> k not in retain) logger.log {user_id: user._id, count: keysToDelete.length}, "deleting sessions for user" rclient.multi() .del(keysToDelete) From a1c662b9d87788da163879eb9171edc16609281d Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 5 Jul 2016 14:55:08 +0100 Subject: [PATCH 44/70] Test session revocation on password change. --- .../acceptance/coffee/SessionTests.coffee | 77 +++++++++++++++++++ .../acceptance/coffee/helpers/User.coffee | 12 +-- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/services/web/test/acceptance/coffee/SessionTests.coffee b/services/web/test/acceptance/coffee/SessionTests.coffee index 679d168f97..b2b02917ec 100644 --- a/services/web/test/acceptance/coffee/SessionTests.coffee +++ b/services/web/test/acceptance/coffee/SessionTests.coffee @@ -113,3 +113,80 @@ describe "Sessions", -> done() ) + describe 'three sessions, password reset', -> + + before -> + # set up second session for this user + @user2 = new User() + @user2.email = @user1.email + @user2.password = @user1.password + @user3 = new User() + @user3.email = @user1.email + @user3.password = @user1.password + + it "should erase both sessions when password is reset", (done) -> + async.series( + [ + (next) => + redis.clearUserSessions @user1, next + + # login, should add session to set + , (next) => + @user1.login (err) -> + next(err) + + , (next) => + redis.getUserSessions @user1, (err, sessions) => + expect(sessions.length).to.equal 1 + expect(sessions[0].slice(0, 5)).to.equal 'sess:' + next() + + # login again, should add the second session to set + , (next) => + @user2.login (err) -> + next(err) + + , (next) => + redis.getUserSessions @user1, (err, sessions) => + expect(sessions.length).to.equal 2 + expect(sessions[0].slice(0, 5)).to.equal 'sess:' + expect(sessions[1].slice(0, 5)).to.equal 'sess:' + next() + + # login third session, should add the second session to set + , (next) => + @user3.login (err) -> + next(err) + + , (next) => + redis.getUserSessions @user1, (err, sessions) => + expect(sessions.length).to.equal 3 + expect(sessions[0].slice(0, 5)).to.equal 'sess:' + expect(sessions[1].slice(0, 5)).to.equal 'sess:' + next() + + # password reset from second session, should erase two of the three sessions + , (next) => + @user2.changePassword (err) -> + next(err) + + , (next) => + redis.getUserSessions @user2, (err, sessions) => + expect(sessions.length).to.equal 1 + next() + + # logout second session, should remove last session from set + , (next) => + @user2.logout (err) -> + next(err) + + , (next) => + redis.getUserSessions @user1, (err, sessions) => + expect(sessions.length).to.equal 0 + next() + + ], (err, result) => + if err + throw err + done() + ) diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index fa6f3a9ba4..afdd766306 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -34,7 +34,7 @@ class User @getCsrfToken (error) => return callback(error) if error? @request.get { - url: "/logout" # Register will log in, but also ensure user exists + url: "/logout" json: email: @email password: @password @@ -91,19 +91,19 @@ class User }) callback() - resetPassword: (newPassword, callback = (error) ->) -> + changePassword: (callback = (error) ->) -> @getCsrfToken (error) => return callback(error) if error? @request.post { - url: "/user/password/set" # Register will log in, but also ensure user exists + url: "/user/password/update" json: - password: @password + currentPassword: @password + newPassword1: @password + newPassword2: @password }, (error, response, body) => return callback(error) if error? db.users.findOne {email: @email}, (error, user) => return callback(error) if error? - @id = user?._id?.toString() - @_id = user?._id?.toString() callback() module.exports = User From 71b8f86e8972ed5b25492cb41e70d7b05070916f Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 5 Jul 2016 15:07:16 +0100 Subject: [PATCH 45/70] Track editor macro views. --- services/web/public/coffee/ide.coffee | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index ceebadac46..d5bb4a3f51 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -42,7 +42,7 @@ define [ ReferencesManager ) -> - App.controller "IdeController", ($scope, $timeout, ide, localStorage) -> + App.controller "IdeController", ($scope, $timeout, ide, localStorage, event_tracking) -> # Don't freak out if we're already in an apply callback $scope.$originalApply = $scope.$apply $scope.$apply = (fn = () ->) -> @@ -69,6 +69,16 @@ define [ $scope.chat = {} + # Tracking code. + $scope.$watch "ui.view", (newView, oldView) -> + event_tracking.send "ide-open-view-#{ newView }" if newView? + + $scope.$watch "ui.chatOpen", (isOpen) -> + event_tracking.send "ide-open-chat" if isOpen + + $scope.$watch "ui.leftMenuShown", (isOpen) -> + event_tracking.send "ide-open-left-menu" if isOpen + # End of tracking code. window._ide = ide From 464866847feee9f3dc21783a58a00d154a6708d4 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 5 Jul 2016 15:14:29 +0100 Subject: [PATCH 46/70] Fix name of UserSessionsManager test module. --- ...UserSessionsManager.coffee => UserSessionsManagerTests.coffee} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename services/web/test/UnitTests/coffee/User/{UserSessionsManager.coffee => UserSessionsManagerTests.coffee} (100%) diff --git a/services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee b/services/web/test/UnitTests/coffee/User/UserSessionsManagerTests.coffee similarity index 100% rename from services/web/test/UnitTests/coffee/User/UserSessionsManager.coffee rename to services/web/test/UnitTests/coffee/User/UserSessionsManagerTests.coffee From 304529754d185c60f7b5491916ae9208575e4703 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 5 Jul 2016 15:16:55 +0100 Subject: [PATCH 47/70] Track editor modals. --- .../coffee/ide/hotkeys/controllers/HotkeysController.coffee | 4 +++- .../coffee/ide/share/controllers/ShareController.coffee | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee b/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee index 4d0adef4fa..762721a329 100644 --- a/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee +++ b/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee @@ -2,8 +2,10 @@ define [ "base" "ace/ace" ], (App) -> - App.controller "HotkeysController", ($scope, $modal) -> + App.controller "HotkeysController", ($scope, $modal, event_tracking) -> $scope.openHotkeysModal = -> + event_tracking.send 'ide-open-hotkeys-modal' + $modal.open { templateUrl: "hotkeysModalTemplate" controller: "HotkeysModalController" diff --git a/services/web/public/coffee/ide/share/controllers/ShareController.coffee b/services/web/public/coffee/ide/share/controllers/ShareController.coffee index b3813b27b2..5a12cbb245 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareController.coffee @@ -3,7 +3,7 @@ define [ ], (App) -> App.controller "ShareController", ["$scope", "$modal", "event_tracking", ($scope, $modal, event_tracking) -> $scope.openShareProjectModal = () -> - event_tracking.send('share-modal-opened') + event_tracking.send 'ide-open-share-modal' $modal.open( templateUrl: "shareProjectModalTemplate" From 0362fee60fa34017dd30b467ac028b69d4925f40 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 5 Jul 2016 15:32:32 +0100 Subject: [PATCH 48/70] test the `retain` option to `revokeAllUserSessions`. --- .../User/UserSessionsManagerTests.coffee | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/services/web/test/UnitTests/coffee/User/UserSessionsManagerTests.coffee b/services/web/test/UnitTests/coffee/User/UserSessionsManagerTests.coffee index c7e1438718..4eee728288 100644 --- a/services/web/test/UnitTests/coffee/User/UserSessionsManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/User/UserSessionsManagerTests.coffee @@ -250,11 +250,12 @@ describe 'UserSessionsManager', -> describe 'revokeAllUserSessions', -> beforeEach -> - @sessionKeys = ['one', 'two'] + @sessionKeys = ['sess:one', 'sess:two'] + @retain = [] @rclient.smembers.callsArgWith(1, null, @sessionKeys) @rclient.exec.callsArgWith(0, null) @call = (callback) => - @UserSessionsManager.revokeAllUserSessions @user, [], callback + @UserSessionsManager.revokeAllUserSessions @user, @retain, callback it 'should not produce an error', (done) -> @call (err) => @@ -276,6 +277,37 @@ describe 'UserSessionsManager', -> @rclient.exec.callCount.should.equal 1 done() + describe 'when a session is retained', -> + + beforeEach -> + @sessionKeys = ['sess:one', 'sess:two', 'sess:three', 'sess:four'] + @retain = ['two'] + @rclient.smembers.callsArgWith(1, null, @sessionKeys) + @rclient.exec.callsArgWith(0, null) + @call = (callback) => + @UserSessionsManager.revokeAllUserSessions @user, @retain, callback + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.equal null + done() + + it 'should call the appropriate redis methods', (done) -> + @call (err) => + @rclient.smembers.callCount.should.equal 1 + @rclient.multi.callCount.should.equal 1 + @rclient.del.callCount.should.equal 1 + @rclient.srem.callCount.should.equal 1 + @rclient.exec.callCount.should.equal 1 + done() + + it 'should remove all sessions except for the retained one', (done) -> + @call (err) => + expect(@rclient.del.firstCall.args[0]).to.deep.equal(['sess:one', 'sess:three', 'sess:four']) + expect(@rclient.srem.firstCall.args[1]).to.deep.equal(['sess:one', 'sess:three', 'sess:four']) + done() + describe 'when rclient produces an error', -> beforeEach -> @@ -290,7 +322,7 @@ describe 'UserSessionsManager', -> beforeEach -> @call = (callback) => - @UserSessionsManager.revokeAllUserSessions null, [], callback + @UserSessionsManager.revokeAllUserSessions null, @retain, callback it 'should not produce an error', (done) -> @call (err) => From 96f6603bd35e50f9f0728251b3a75db1c058c354 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 5 Jul 2016 15:37:13 +0100 Subject: [PATCH 49/70] Track settings changes. --- .../ide/settings/services/settings.coffee | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/settings/services/settings.coffee b/services/web/public/coffee/ide/settings/services/settings.coffee index 4e6bbcea3d..0c4b52dd89 100644 --- a/services/web/public/coffee/ide/settings/services/settings.coffee +++ b/services/web/public/coffee/ide/settings/services/settings.coffee @@ -1,19 +1,37 @@ define [ "base" ], (App) -> - App.factory "settings", ["ide", (ide) -> + App.factory "settings", ["ide", "event_tracking", (ide, event_tracking) -> return { saveSettings: (data) -> + for key in Object.keys(data) + changedSetting = key + changedSettingVal = data[key] + event_tracking.send "setting-changed", { changedSetting, changedSettingVal } + data._csrf = window.csrfToken ide.$http.post "/user/settings", data + saveProjectSettings: (data) -> + for key in Object.keys(data) + changedSetting = key + changedSettingVal = data[key] + event_tracking.send "project-setting-changed", { changedSetting, changedSettingVal} + data._csrf = window.csrfToken ide.$http.post "/project/#{ide.project_id}/settings", data + saveProjectAdminSettings: (data) -> + for key in Object.keys(data) + changedSetting = key + changedSettingVal = data[key] + event_tracking.send "project-admin-setting-changed", { changedSetting, changedSettingVal } + data._csrf = window.csrfToken ide.$http.post "/project/#{ide.project_id}/settings/admin", data + } ] \ No newline at end of file From d5efe81a464cd766fc3d72b73bcc4e5d4d34cd15 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 5 Jul 2016 16:02:46 +0100 Subject: [PATCH 50/70] Track recompiles. --- services/web/app/views/project/editor/editor.jade | 2 +- .../coffee/ide/pdf/controllers/PdfController.coffee | 6 ++++++ .../coffee/ide/settings/services/settings.coffee | 12 +++++++++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/services/web/app/views/project/editor/editor.jade b/services/web/app/views/project/editor/editor.jade index d6c0876f96..ea5efff304 100644 --- a/services/web/app/views/project/editor/editor.jade +++ b/services/web/app/views/project/editor/editor.jade @@ -35,7 +35,7 @@ div.full-size( resize-on="layout:main:resize,layout:pdf:resize", annotations="pdf.logEntryAnnotations[editor.open_doc_id]", read-only="!permissions.write", - on-ctrl-enter="recompile" + on-ctrl-enter="recompileViaKey" ) .ui-layout-east diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 715dbb9161..5f283145d1 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -247,7 +247,10 @@ define [ return path $scope.recompile = (options = {}) -> + event_tracking.send "editor-recompile", options + return if $scope.pdf.compiling + $scope.pdf.compiling = true ide.$scope.$broadcast("flush-changes") @@ -267,6 +270,9 @@ define [ # This needs to be public. ide.$scope.recompile = $scope.recompile + # This method is a simply wrapper and exists only for tracking purposes. + ide.$scope.recompileViaKey = () -> + $scope.recompile { keyShortcut: true } $scope.clearCache = () -> $http { diff --git a/services/web/public/coffee/ide/settings/services/settings.coffee b/services/web/public/coffee/ide/settings/services/settings.coffee index 0c4b52dd89..409d571d33 100644 --- a/services/web/public/coffee/ide/settings/services/settings.coffee +++ b/services/web/public/coffee/ide/settings/services/settings.coffee @@ -4,31 +4,37 @@ define [ App.factory "settings", ["ide", "event_tracking", (ide, event_tracking) -> return { saveSettings: (data) -> + # Tracking code. for key in Object.keys(data) changedSetting = key changedSettingVal = data[key] event_tracking.send "setting-changed", { changedSetting, changedSettingVal } - + # End of tracking code. + data._csrf = window.csrfToken ide.$http.post "/user/settings", data saveProjectSettings: (data) -> + # Tracking code. for key in Object.keys(data) changedSetting = key changedSettingVal = data[key] event_tracking.send "project-setting-changed", { changedSetting, changedSettingVal} - + # End of tracking code. + data._csrf = window.csrfToken ide.$http.post "/project/#{ide.project_id}/settings", data saveProjectAdminSettings: (data) -> + # Tracking code. for key in Object.keys(data) changedSetting = key changedSettingVal = data[key] event_tracking.send "project-admin-setting-changed", { changedSetting, changedSettingVal } - + # End of tracking code. + data._csrf = window.csrfToken ide.$http.post "/project/#{ide.project_id}/settings/admin", data From 909f7c15b55b0dfa6574af8f39f8bb2a886e2bcc Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 5 Jul 2016 16:27:14 +0100 Subject: [PATCH 51/70] Track log-related actions. --- services/web/app/views/project/editor/pdf.jade | 6 +++++- .../coffee/ide/pdf/controllers/PdfController.coffee | 10 ++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index 3bc459f290..1f135d665f 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -119,7 +119,11 @@ div.full-size.pdf(ng-controller="PdfController") ng-bind-html="wikiEnabled ? entry.humanReadableHint : stripHTMLFromString(entry.humanReadableHint)") .card-hint-actions.clearfix .card-hint-ext-link(ng-if="wikiEnabled") - a(ng-href="{{ entry.extraInfoURL }}", target="_blank") + a( + ng-href="{{ entry.extraInfoURL }}", + ng-click="trackLogHintsLearnMore()" + target="_blank" + ) i.fa.fa-external-link | #{translate("log_hint_extra_info")} .card-hint-feedback( diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 5f283145d1..036e726f40 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -16,8 +16,11 @@ define [ $scope.wikiEnabled = window.wikiEnabled; # log hints tracking + $scope.trackLogHintsLearnMore = () -> + event_tracking.send 'logs-hints-learn-more' + trackLogHintsFeedback = (isPositive, hintId) -> - event_tracking.send 'log-hints', (if isPositive then 'feedback-positive' else 'feedback-negative'), hintId + event_tracking.send 'log-hints-feedback', { isPositive, hintId } $scope.trackLogHintsPositiveFeedback = (hintId) -> trackLogHintsFeedback true, hintId $scope.trackLogHintsNegativeFeedback = (hintId) -> trackLogHintsFeedback false, hintId @@ -286,6 +289,7 @@ define [ $scope.toggleLogs = () -> $scope.shouldShowLogs = !$scope.shouldShowLogs + event_tracking.send "ide-open-logs" if $scope.shouldShowLogs $scope.showPdf = () -> $scope.pdf.view = "pdf" @@ -293,6 +297,7 @@ define [ $scope.toggleRawLog = () -> $scope.pdf.showRawLog = !$scope.pdf.showRawLog + event_tracking.send "logs-view-raw" if $scope.pdf.showRawLog $scope.openClearCacheModal = () -> modalInstance = $modal.open( @@ -445,8 +450,9 @@ define [ ide.editorManager.openDoc(doc, gotoLine: line) ] - App.controller "PdfLogEntryController", ["$scope", "ide", ($scope, ide) -> + App.controller "PdfLogEntryController", ["$scope", "ide", "event_tracking", ($scope, ide, event_tracking) -> $scope.openInEditor = (entry) -> + event_tracking.send 'logs-jump-to-location' entity = ide.fileTreeManager.findEntityByPath(entry.file) return if !entity? or entity.type != "doc" if entry.line? From c9b1faa1a1c3a2a40aa473a694d449bd745fbbc4 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 6 Jul 2016 11:59:33 +0100 Subject: [PATCH 52/70] Add user info to correlate with events. --- services/web/app/views/layout.jade | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index 3ab9e23a64..3a5b227fac 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -67,6 +67,7 @@ html(itemscope, itemtype='http://schema.org/Product') Countly.q = Countly.q || []; Countly.app_key = "#{settings.analytics.countly.token}"; Countly.url = "https://try.count.ly"; + Countly.q.push(['track_sessions']); Countly.q.push(['track_pageview']); @@ -79,6 +80,15 @@ html(itemscope, itemtype='http://schema.org/Product') var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(cly, s); })(); + if (session && session.user) + script(type="text/javascript"). + Countly.q.push(['user_details', { + handle: "#{session.user._id}", + email: "#{session.user.email}", + }]); + + + // End countly Analytics script(type="text/javascript"). From 7e1c1f7414d208d92885928fb23ba518d6c023d9 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 6 Jul 2016 12:12:22 +0100 Subject: [PATCH 53/70] Remove heap integration. --- services/web/app/views/layout.jade | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index 3a5b227fac..76b6df7f78 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -48,31 +48,19 @@ html(itemscope, itemtype='http://schema.org/Product') script(type='text/javascript'). window.ga = function() { console.log("Sending to GA", arguments) }; - // Heap Analytics - if (settings.analytics && settings.analytics.heap && session && session.user) - script(type="text/javascript"). - window.heap=window.heap||[],heap.load=function(e,t){window.heap.appid=e,window.heap.config=t=t||{};var n=t.forceSSL||"https:"===document.location.protocol,a=document.createElement("script");a.type="text/javascript",a.async=!0,a.src=(n?"https:":"http:")+"//cdn.heapanalytics.com/js/heap-"+e+".js";var o=document.getElementsByTagName("script")[0];o.parentNode.insertBefore(a,o);for(var r=function(e){return function(){heap.push([e].concat(Array.prototype.slice.call(arguments,0)))}},p=["clearEventProperties","identify","setEventProperties","track","unsetEventProperty"],c=0;c Date: Wed, 6 Jul 2016 12:12:54 +0100 Subject: [PATCH 54/70] Track user log in and sign up. --- services/web/app/views/layout.jade | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index 76b6df7f78..3995a3c39f 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -74,6 +74,18 @@ html(itemscope, itemtype='http://schema.org/Product') handle: '#{session.user._id}', email: '#{session.user.email}', }]); + + if (justRegistered) + script(type="text/javascript"). + Countly.q.push(['add_event',{ + key: 'user-registered' + }); + + if (justLoggedIn) + script(type="text/javascript"). + Countly.q.push(['add_event',{ + key: 'user-logged-in' + }); // End countly Analytics script(type="text/javascript"). From 9f724d0a04ad9270f1e96f528f2bd7a3f5163655 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 6 Jul 2016 12:14:01 +0100 Subject: [PATCH 55/70] Add tests to check if users can access a restricted page. --- .../acceptance/coffee/SessionTests.coffee | 63 ++++++++++++++++++- .../acceptance/coffee/helpers/User.coffee | 11 ++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/services/web/test/acceptance/coffee/SessionTests.coffee b/services/web/test/acceptance/coffee/SessionTests.coffee index b2b02917ec..cff5b66406 100644 --- a/services/web/test/acceptance/coffee/SessionTests.coffee +++ b/services/web/test/acceptance/coffee/SessionTests.coffee @@ -7,7 +7,7 @@ redis = require "./helpers/redis" describe "Sessions", -> before (done) -> - @timeout(10000) + @timeout(20000) @user1 = new User() @site_admin = new User({email: "admin@example.com"}) async.series [ @@ -34,6 +34,13 @@ describe "Sessions", -> expect(sessions[0].slice(0, 5)).to.equal 'sess:' next() + # should be able to access settings page + , (next) => + @user1.getUserSettingsPage (err, statusCode) => + expect(err).to.equal null + expect(statusCode).to.equal 200 + next() + # logout, should remove session from set , (next) => @user1.logout (err) -> @@ -87,6 +94,19 @@ describe "Sessions", -> expect(sessions[1].slice(0, 5)).to.equal 'sess:' next() + # both should be able to access settings page + , (next) => + @user1.getUserSettingsPage (err, statusCode) => + expect(err).to.equal null + expect(statusCode).to.equal 200 + next() + + , (next) => + @user2.getUserSettingsPage (err, statusCode) => + expect(err).to.equal null + expect(statusCode).to.equal 200 + next() + # logout first session, should remove session from set , (next) => @user1.logout (err) -> @@ -97,6 +117,20 @@ describe "Sessions", -> expect(sessions.length).to.equal 1 next() + # first session should not have access to settings page + , (next) => + @user1.getUserSettingsPage (err, statusCode) => + expect(err).to.equal null + expect(statusCode).to.equal 302 + next() + + # second session should still have access to settings + , (next) => + @user2.getUserSettingsPage (err, statusCode) => + expect(err).to.equal null + expect(statusCode).to.equal 200 + next() + # logout second session, should remove last session from set , (next) => @user2.logout (err) -> @@ -107,6 +141,13 @@ describe "Sessions", -> expect(sessions.length).to.equal 0 next() + # second session should not have access to settings page + , (next) => + @user2.getUserSettingsPage (err, statusCode) => + expect(err).to.equal null + expect(statusCode).to.equal 302 + next() + ], (err, result) => if err throw err @@ -175,6 +216,26 @@ describe "Sessions", -> expect(sessions.length).to.equal 1 next() + # users one and three should not be able to access settings page + , (next) => + @user1.getUserSettingsPage (err, statusCode) => + expect(err).to.equal null + expect(statusCode).to.equal 302 + next() + + , (next) => + @user3.getUserSettingsPage (err, statusCode) => + expect(err).to.equal null + expect(statusCode).to.equal 302 + next() + + # user two should still be logged in, and able to access settings page + , (next) => + @user2.getUserSettingsPage (err, statusCode) => + expect(err).to.equal null + expect(statusCode).to.equal 200 + next() + # logout second session, should remove last session from set , (next) => @user2.logout (err) -> diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index afdd766306..888473578e 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -106,4 +106,15 @@ class User return callback(error) if error? callback() + getUserSettingsPage: (callback = (error, statusCode) ->) -> + @getCsrfToken (error) => + return callback(error) if error? + @request.get { + url: "/user/settings" + }, (error, response, body) => + return callback(error) if error? + callback(null, response.statusCode) + + + module.exports = User From b841d41b685ef1fac6a93d0127face2e5f66973d Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 6 Jul 2016 12:26:21 +0100 Subject: [PATCH 56/70] Actually send the data to Countly. --- services/web/public/coffee/main/event.coffee | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/main/event.coffee b/services/web/public/coffee/main/event.coffee index a0f7a4cedb..2acd79a116 100644 --- a/services/web/public/coffee/main/event.coffee +++ b/services/web/public/coffee/main/event.coffee @@ -6,10 +6,24 @@ define [ return { send: (category, action, label, value)-> ga('send', 'event', category, action, label, value) - event_name = "#{action}-#{category}" - window?.heap?.track?(event_name, {label, value}) + + sendCountly: (key, segmentation) -> + eventData = { key } + eventData.segmentation = segmentation if segmentation? + + Countly.q.push(['add_event', eventData]); } + # App.directive "countlyTrack", () -> + # return { + # restrict: "A" + # scope: false, + # link: (scope, el, attrs) -> + # eventKey = attrs.countlyTrack + # if (eventKey?) + # el.on "click", () -> + # console.log eventKey + # } #header $('.navbar a').on "click", (e)-> From 0dd3a70b4bfa09c370e90bb8800feb2c01b657fe Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 6 Jul 2016 12:32:33 +0100 Subject: [PATCH 57/70] Use Countly tracking method where appropriate. --- services/web/public/coffee/ide.coffee | 6 +++--- .../ide/hotkeys/controllers/HotkeysController.coffee | 2 +- .../coffee/ide/pdf/controllers/PdfController.coffee | 12 ++++++------ .../coffee/ide/settings/services/settings.coffee | 6 +++--- .../ide/share/controllers/ShareController.coffee | 2 +- services/web/public/coffee/main/event.coffee | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index d5bb4a3f51..ed56f04c32 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -71,13 +71,13 @@ define [ # Tracking code. $scope.$watch "ui.view", (newView, oldView) -> - event_tracking.send "ide-open-view-#{ newView }" if newView? + event_tracking.sendCountly "ide-open-view-#{ newView }" if newView? $scope.$watch "ui.chatOpen", (isOpen) -> - event_tracking.send "ide-open-chat" if isOpen + event_tracking.sendCountly "ide-open-chat" if isOpen $scope.$watch "ui.leftMenuShown", (isOpen) -> - event_tracking.send "ide-open-left-menu" if isOpen + event_tracking.sendCountly "ide-open-left-menu" if isOpen # End of tracking code. window._ide = ide diff --git a/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee b/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee index 762721a329..36eaa5d555 100644 --- a/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee +++ b/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee @@ -4,7 +4,7 @@ define [ ], (App) -> App.controller "HotkeysController", ($scope, $modal, event_tracking) -> $scope.openHotkeysModal = -> - event_tracking.send 'ide-open-hotkeys-modal' + event_tracking.sendCountly "ide-open-hotkeys-modal" $modal.open { templateUrl: "hotkeysModalTemplate" diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 036e726f40..afd61983c9 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -17,10 +17,10 @@ define [ # log hints tracking $scope.trackLogHintsLearnMore = () -> - event_tracking.send 'logs-hints-learn-more' + event_tracking.sendCountly "logs-hints-learn-more" trackLogHintsFeedback = (isPositive, hintId) -> - event_tracking.send 'log-hints-feedback', { isPositive, hintId } + event_tracking.sendCountly "log-hints-feedback", { isPositive, hintId } $scope.trackLogHintsPositiveFeedback = (hintId) -> trackLogHintsFeedback true, hintId $scope.trackLogHintsNegativeFeedback = (hintId) -> trackLogHintsFeedback false, hintId @@ -250,7 +250,7 @@ define [ return path $scope.recompile = (options = {}) -> - event_tracking.send "editor-recompile", options + event_tracking.sendCountly "editor-recompile", options return if $scope.pdf.compiling @@ -289,7 +289,7 @@ define [ $scope.toggleLogs = () -> $scope.shouldShowLogs = !$scope.shouldShowLogs - event_tracking.send "ide-open-logs" if $scope.shouldShowLogs + event_tracking.sendCountly "ide-open-logs" if $scope.shouldShowLogs $scope.showPdf = () -> $scope.pdf.view = "pdf" @@ -297,7 +297,7 @@ define [ $scope.toggleRawLog = () -> $scope.pdf.showRawLog = !$scope.pdf.showRawLog - event_tracking.send "logs-view-raw" if $scope.pdf.showRawLog + event_tracking.sendCountly "logs-view-raw" if $scope.pdf.showRawLog $scope.openClearCacheModal = () -> modalInstance = $modal.open( @@ -452,7 +452,7 @@ define [ App.controller "PdfLogEntryController", ["$scope", "ide", "event_tracking", ($scope, ide, event_tracking) -> $scope.openInEditor = (entry) -> - event_tracking.send 'logs-jump-to-location' + event_tracking.sendCountly 'logs-jump-to-location' entity = ide.fileTreeManager.findEntityByPath(entry.file) return if !entity? or entity.type != "doc" if entry.line? diff --git a/services/web/public/coffee/ide/settings/services/settings.coffee b/services/web/public/coffee/ide/settings/services/settings.coffee index 409d571d33..14dfe92f56 100644 --- a/services/web/public/coffee/ide/settings/services/settings.coffee +++ b/services/web/public/coffee/ide/settings/services/settings.coffee @@ -8,7 +8,7 @@ define [ for key in Object.keys(data) changedSetting = key changedSettingVal = data[key] - event_tracking.send "setting-changed", { changedSetting, changedSettingVal } + event_tracking.sendCountly "setting-changed", { changedSetting, changedSettingVal } # End of tracking code. data._csrf = window.csrfToken @@ -20,7 +20,7 @@ define [ for key in Object.keys(data) changedSetting = key changedSettingVal = data[key] - event_tracking.send "project-setting-changed", { changedSetting, changedSettingVal} + event_tracking.sendCountly "project-setting-changed", { changedSetting, changedSettingVal} # End of tracking code. data._csrf = window.csrfToken @@ -32,7 +32,7 @@ define [ for key in Object.keys(data) changedSetting = key changedSettingVal = data[key] - event_tracking.send "project-admin-setting-changed", { changedSetting, changedSettingVal } + event_tracking.sendCountly "project-admin-setting-changed", { changedSetting, changedSettingVal } # End of tracking code. data._csrf = window.csrfToken diff --git a/services/web/public/coffee/ide/share/controllers/ShareController.coffee b/services/web/public/coffee/ide/share/controllers/ShareController.coffee index 5a12cbb245..fc74862528 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareController.coffee @@ -3,7 +3,7 @@ define [ ], (App) -> App.controller "ShareController", ["$scope", "$modal", "event_tracking", ($scope, $modal, event_tracking) -> $scope.openShareProjectModal = () -> - event_tracking.send 'ide-open-share-modal' + event_tracking.sendCountly "ide-open-share-modal" $modal.open( templateUrl: "shareProjectModalTemplate" diff --git a/services/web/public/coffee/main/event.coffee b/services/web/public/coffee/main/event.coffee index 2acd79a116..c8fee3601c 100644 --- a/services/web/public/coffee/main/event.coffee +++ b/services/web/public/coffee/main/event.coffee @@ -11,7 +11,7 @@ define [ eventData = { key } eventData.segmentation = segmentation if segmentation? - Countly.q.push(['add_event', eventData]); + Countly.q.push([ "add_event", eventData ]); } # App.directive "countlyTrack", () -> From 95f14359203d5f5a6b7d580ea6f490393e0dd824 Mon Sep 17 00:00:00 2001 From: MCribbin Date: Wed, 6 Jul 2016 14:16:21 +0100 Subject: [PATCH 58/70] Update HumanReadableLogsRules.coffee --- .../HumanReadableLogsRules.coffee | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee index e7735e89dc..fee0472b2a 100644 --- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee +++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee @@ -12,7 +12,7 @@ define -> [ """ , regexToMatch: /Display math should end with \$\$/ - extraInfoURL: "https://www.sharelatex.com/learn/Errors/Display_math_should_end_with_$$." + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Display_math_should_end_with_$$" humanReadableHint: """ You have forgotten a $ sign at the end of 'display math' mode. When writing in display math mode, you must always math write inside $$ \u2026 $$. Check that the number of $s match around each math expression. """ @@ -24,67 +24,67 @@ define -> [ """ , regexToMatch: /(undefined )?[rR]eference(s)?.+(undefined)?/ - extraInfoURL: "https://www.sharelatex.com/learn/Errors/There_were_undefined_references." + extraInfoURL: "https://www.sharelatex.com/learn/Errors/There_were_undefined_references" humanReadableHint: """ You have referenced something which has not yet been labelled. If you have labelled it already, make sure that what is written inside \\ref{...} is the same as what is written inside \\label{...}. """ , regexToMatch: /Citation .+ on page .+ undefined on input line .+/ - extraInfoURL: "https://www.sharelatex.com/learn/Errors/Citation_XXX_on_page_XXX_undefined_on_input_line_XXX." + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Citation_XXX_on_page_XXX_undefined_on_input_line_XXX" humanReadableHint: """ You have cited something which is not included in your bibliography. Make sure that the citation (\\cite{...}) has a corresponding key in your bibliography, and that both are spelled the same way. """ , regexToMatch: /(Label .+)? multiply[ -]defined( labels)?/ - extraInfoURL: "https://www.sharelatex.com/learn/Errors/There_were_multiply-defined_labels." + extraInfoURL: "https://www.sharelatex.com/learn/Errors/There_were_multiply-defined_labels" humanReadableHint: """ You have used the same label more than once. Check that each \\label{...} labels only one item. """ , regexToMatch: /`!?h' float specifier changed to `!?ht'/ - extraInfoURL: "https://www.sharelatex.com/learn/Errors/%60!h%27_float_specifier_changed_to_%60!ht%27." + extraInfoURL: "https://www.sharelatex.com/learn/Errors/%60!h%27_float_specifier_changed_to_%60!ht%27" humanReadableHint: """ The float specifier 'h' is too strict of a demand for LaTeX to place your float in a nice way here. Try relaxing it by using 'ht', or even 'htbp' if necessary. If you want to try keep the float here anyway, check out the float package. """ , regexToMatch: /No positions in optional float specifier/ - extraInfoURL: "https://www.sharelatex.com/learn/Errors/No_positions_in_optional_float_specifier." + extraInfoURL: "https://www.sharelatex.com/learn/Errors/No_positions_in_optional_float_specifier" humanReadableHint: """ You have forgotten to include a float specifier, which tells LaTeX where to position your figure. Find out more about float specifiers here. """ , regexToMatch: /Undefined control sequence/ - extraInfoURL: "https://www.sharelatex.com/learn/Errors/Undefined_control_sequence." + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Undefined_control_sequence" humanReadableHint: """ The compiler is having trouble understanding a command you have used. Check that the command is spelled correctly. If the command is part of a package, make sure you have included the package in your preamble using \\usepackage{...}. """ , regexToMatch: /File .+ not found/ - extraInfoURL: "https://www.sharelatex.com/learn/Errors/File_XXX_not_found_on_input_line_XXX." + extraInfoURL: "https://www.sharelatex.com/learn/Errors/File_XXX_not_found_on_input_line_XXX" humanReadableHint: """ The compiler cannot find the file you want to include. Make sure that you have uploaded the file and specified the file location correctly. """ , regexToMatch: /LaTeX Error: Unknown graphics extension: \..+/ - extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_graphics_extension:_.gif." + extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_graphics_extension:_.XXX" humanReadableHint: """ The compiler does not recognise the file type of one of your images. Make sure you are using a supported image format for your choice of compiler, and check that there are no periods (.) in the name of your image. """ , regexToMatch: /LaTeX Error: Unknown float option `H'/ - extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60H%27." + extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60H%27" humanReadableHint: """ The compiler isn't recognizing the float option 'H'. Include \\usepackage{float} in your preamble to fix this. """ , regexToMatch: /LaTeX Error: Unknown float option `.+'/ - extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60H%27." + extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60H%27" humanReadableHint: """ You have used a float specifier which the compiler does not understand. You can learn more about the different float options available for placing figures here. """ , regexToMatch: /LaTeX Error: \\math.+ allowed only in math mode/ - extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_%5Cmathrm_allowed_only_in_math_mode." + extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_%5Cmathrm_allowed_only_in_math_mode" humanReadableHint: """ You have used a font command which is only available in math mode. To use this command, you must be in maths mode (E.g. $ \u2026 $ or \\begin{math} \u2026 \\end{math}). If you want to use it outside of math mode, use the text version instead: \\textrm, \\textit, etc. """ From 8e2475afdc07f47408c4082bd203dba4f76d93b6 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 6 Jul 2016 14:24:50 +0100 Subject: [PATCH 59/70] Track hints feedback in both GA and Countly; clean-up. --- services/web/app/views/layout.jade | 8 +++++--- .../coffee/ide/pdf/controllers/PdfController.coffee | 1 + services/web/public/coffee/main/event.coffee | 5 ++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index 3995a3c39f..4b38d67bd8 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -71,21 +71,23 @@ html(itemscope, itemtype='http://schema.org/Product') if (session && session.user) script(type="text/javascript"). Countly.q.push(['user_details', { - handle: '#{session.user._id}', email: '#{session.user.email}', + custom: { + handle: '#{session.user._id}', + } }]); if (justRegistered) script(type="text/javascript"). Countly.q.push(['add_event',{ key: 'user-registered' - }); + }]); if (justLoggedIn) script(type="text/javascript"). Countly.q.push(['add_event',{ key: 'user-logged-in' - }); + }]); // End countly Analytics script(type="text/javascript"). diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index afd61983c9..6ca950671f 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -20,6 +20,7 @@ define [ event_tracking.sendCountly "logs-hints-learn-more" trackLogHintsFeedback = (isPositive, hintId) -> + event_tracking.send 'log-hints', (if isPositive then 'feedback-positive' else 'feedback-negative'), hintId event_tracking.sendCountly "log-hints-feedback", { isPositive, hintId } $scope.trackLogHintsPositiveFeedback = (hintId) -> trackLogHintsFeedback true, hintId diff --git a/services/web/public/coffee/main/event.coffee b/services/web/public/coffee/main/event.coffee index c8fee3601c..58e43f3b9f 100644 --- a/services/web/public/coffee/main/event.coffee +++ b/services/web/public/coffee/main/event.coffee @@ -9,9 +9,8 @@ define [ sendCountly: (key, segmentation) -> eventData = { key } - eventData.segmentation = segmentation if segmentation? - - Countly.q.push([ "add_event", eventData ]); + eventData.segmentation = segmentation if segmentation? + Countly?.q.push([ "add_event", eventData ]); } # App.directive "countlyTrack", () -> From 7a3d3d3ffd8a0b62fa73a669b453b94fd58b74bc Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 6 Jul 2016 14:44:28 +0100 Subject: [PATCH 60/70] Make Countly track by internal ids. --- services/web/app/views/layout.jade | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index 4b38d67bd8..14e4aa6107 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -70,10 +70,12 @@ html(itemscope, itemtype='http://schema.org/Product') if (session && session.user) script(type="text/javascript"). + Countly.q.push(['change_id', '#{session.user._id}', true ]); + Countly.q.push(['user_details', { email: '#{session.user.email}', custom: { - handle: '#{session.user._id}', + userId: '#{session.user._id}', } }]); @@ -85,6 +87,7 @@ html(itemscope, itemtype='http://schema.org/Product') if (justLoggedIn) script(type="text/javascript"). + Countly.q.push(['add_event',{ key: 'user-logged-in' }]); From dda0841af3ee91b8674aa2d4980d1e081fded461 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 6 Jul 2016 15:39:03 +0100 Subject: [PATCH 61/70] Return early if there are no keys to delete --- .../Features/User/UserSessionsManager.coffee | 3 +++ .../User/UserSessionsManagerTests.coffee | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index 571cfd7b8d..cd8407dd40 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -72,6 +72,9 @@ module.exports = UserSessionsManager = logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set" return callback(err) keysToDelete = _.filter(sessionKeys, (k) -> k not in retain) + if keysToDelete.length == 0 + logger.log {user_id: user._id}, "no sessions in UserSessions set to delete, returning" + return callback(null) logger.log {user_id: user._id, count: keysToDelete.length}, "deleting sessions for user" rclient.multi() .del(keysToDelete) diff --git a/services/web/test/UnitTests/coffee/User/UserSessionsManagerTests.coffee b/services/web/test/UnitTests/coffee/User/UserSessionsManagerTests.coffee index 4eee728288..9a09aa88f0 100644 --- a/services/web/test/UnitTests/coffee/User/UserSessionsManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/User/UserSessionsManagerTests.coffee @@ -339,6 +339,26 @@ describe 'UserSessionsManager', -> @rclient.exec.callCount.should.equal 0 done() + describe 'when there are no keys to delete', -> + + beforeEach -> + @rclient.smembers.callsArgWith(1, null, []) + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.equal null + done() + + it 'should not do the delete operation', (done) -> + @call (err) => + @rclient.smembers.callCount.should.equal 1 + @rclient.multi.callCount.should.equal 0 + @rclient.del.callCount.should.equal 0 + @rclient.srem.callCount.should.equal 0 + @rclient.exec.callCount.should.equal 0 + done() + describe 'touch', -> beforeEach -> From 54ec75d36ab38ef2e81e63df94b306b77cf0d16b Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 6 Jul 2016 15:55:36 +0100 Subject: [PATCH 62/70] Avoid line-breaks when showing the log icon. --- services/web/public/stylesheets/app/editor/pdf.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/public/stylesheets/app/editor/pdf.less b/services/web/public/stylesheets/app/editor/pdf.less index 7e9b1ff05c..a4d2eeb214 100644 --- a/services/web/public/stylesheets/app/editor/pdf.less +++ b/services/web/public/stylesheets/app/editor/pdf.less @@ -122,7 +122,7 @@ font-weight: 700; .fa { - display: none; + opacity: 0; } } .entry-message { @@ -138,7 +138,7 @@ &:hover .line-no { color: inherit; .fa { - display: inline-block; + opacity: 1; } } From dadee03ca33ab108173505032d1cd18e26505432 Mon Sep 17 00:00:00 2001 From: MCribbin Date: Wed, 6 Jul 2016 15:57:56 +0100 Subject: [PATCH 63/70] Update HumanReadableLogsRules.coffee Added new linke for LaTeX Error: Unknown float option `q'. Changed hint for "No positions in optional float specifier" to include quick fix (will probably need some clearer way of saying which square brackets need to be removed) --- .../ide/human-readable-logs/HumanReadableLogsRules.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee index fee0472b2a..05a4eec761 100644 --- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee +++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee @@ -50,7 +50,7 @@ define -> [ regexToMatch: /No positions in optional float specifier/ extraInfoURL: "https://www.sharelatex.com/learn/Errors/No_positions_in_optional_float_specifier" humanReadableHint: """ - You have forgotten to include a float specifier, which tells LaTeX where to position your figure. Find out more about float specifiers here. + You have forgotten to include a float specifier, which tells LaTeX where to position your figure. To fix this, either insert a float specifier inside the square brackets (float options), or remove the square brackets. Find out more about float specifiers here. """ , regexToMatch: /Undefined control sequence/ @@ -77,8 +77,8 @@ define -> [ The compiler isn't recognizing the float option 'H'. Include \\usepackage{float} in your preamble to fix this. """ , - regexToMatch: /LaTeX Error: Unknown float option `.+'/ - extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60H%27" + regexToMatch: /LaTeX Error: Unknown float option `q'/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60q%27" humanReadableHint: """ You have used a float specifier which the compiler does not understand. You can learn more about the different float options available for placing figures here. """ From b62fbd4d3f004b2e6dfeb5cfc2e335cf5142a9e8 Mon Sep 17 00:00:00 2001 From: MCribbin Date: Wed, 6 Jul 2016 16:18:27 +0100 Subject: [PATCH 64/70] Update HumanReadableLogsRules.coffee --- .../ide/human-readable-logs/HumanReadableLogsRules.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee index 05a4eec761..f123515deb 100644 --- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee +++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee @@ -50,7 +50,7 @@ define -> [ regexToMatch: /No positions in optional float specifier/ extraInfoURL: "https://www.sharelatex.com/learn/Errors/No_positions_in_optional_float_specifier" humanReadableHint: """ - You have forgotten to include a float specifier, which tells LaTeX where to position your figure. To fix this, either insert a float specifier inside the square brackets (float options), or remove the square brackets. Find out more about float specifiers here. + You have forgotten to include a float specifier, which tells LaTeX where to position your figure. To fix this, either insert a float specifier inside the square brackets (e.g. \begin{figure}[h]), or remove the square brackets (e.g. \begin{figure}). Find out more about float specifiers here. """ , regexToMatch: /Undefined control sequence/ From 9e35bdcaea7c2d459bd3ca91a32674a5a323c9c7 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 7 Jul 2016 09:35:44 +0100 Subject: [PATCH 65/70] Refactor: add `?` suffix to truth tests. --- .../Features/User/UserController.coffee | 2 +- .../Features/User/UserSessionsManager.coffee | 26 +++++++++---------- .../app/coffee/infrastructure/Server.coffee | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index ce96dd2a2f..546cea035e 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -122,7 +122,7 @@ module.exports = UserController = AuthenticationManager.setUserPassword user._id, newPassword1, (error) -> return next(error) if error? UserSessionsManager.revokeAllUserSessions user, [req.sessionID], (err) -> - return next(err) if err + return next(err) if err? res.send message: type:'success' diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index cd8407dd40..30a9d7471a 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -16,10 +16,10 @@ module.exports = UserSessionsManager = return "sess:#{sessionId}" trackSession: (user, sessionId, callback=(err)-> ) -> - if !user + if !user? logger.log {sessionId}, "no user to track, returning" return callback(null) - if !sessionId + if !sessionId? logger.log {user_id: user._id}, "no sessionId to track, returning" return callback(null) logger.log {user_id: user._id, sessionId}, "onLogin handler" @@ -29,17 +29,17 @@ module.exports = UserSessionsManager = .sadd(sessionSetKey, value) .expire(sessionSetKey, "#{Settings.cookieSessionLength}") .exec (err, response) -> - if err + if err? logger.err {err, user_id: user._id, sessionSetKey}, "error while adding session key to UserSessions set" return callback(err) UserSessionsManager._checkSessions(user, () ->) callback() untrackSession: (user, sessionId, callback=(err)-> ) -> - if !user + if !user? logger.log {sessionId}, "no user to untrack, returning" return callback(null) - if !sessionId + if !sessionId? logger.log {user_id: user._id}, "no sessionId to untrack, returning" return callback(null) logger.log {user_id: user._id, sessionId}, "onLogout handler" @@ -52,7 +52,7 @@ module.exports = UserSessionsManager = .srem(sessionSetKey, value) .expire(sessionSetKey, "#{Settings.cookieSessionLength}") .exec (err, response) -> - if err + if err? logger.err {err, user_id: user._id, sessionSetKey}, "error while removing session key from UserSessions set" return callback(err) UserSessionsManager._checkSessions(user, () ->) @@ -68,7 +68,7 @@ module.exports = UserSessionsManager = logger.log {user_id: user._id}, "revoking all existing sessions for user" sessionSetKey = UserSessionsManager._sessionSetKey(user) rclient.smembers sessionSetKey, (err, sessionKeys) -> - if err + if err? logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set" return callback(err) keysToDelete = _.filter(sessionKeys, (k) -> k not in retain) @@ -80,7 +80,7 @@ module.exports = UserSessionsManager = .del(keysToDelete) .srem(sessionSetKey, keysToDelete) .exec (err, result) -> - if err + if err? logger.err {err, user_id: user._id, sessionSetKey}, "error revoking all sessions for user" return callback(err) callback(null) @@ -91,7 +91,7 @@ module.exports = UserSessionsManager = return callback(null) sessionSetKey = UserSessionsManager._sessionSetKey(user) rclient.expire sessionSetKey, "#{Settings.cookieSessionLength}", (err, response) -> - if err + if err? logger.err {err, user_id: user._id}, "error while updating ttl on UserSessions set" return callback(err) callback(null) @@ -103,7 +103,7 @@ module.exports = UserSessionsManager = logger.log {user_id: user._id}, "checking sessions for user" sessionSetKey = UserSessionsManager._sessionSetKey(user) rclient.smembers sessionSetKey, (err, sessionKeys) -> - if err + if err? logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set" return callback(err) logger.log {user_id: user._id, count: sessionKeys.length}, "checking sessions for user" @@ -112,12 +112,12 @@ module.exports = UserSessionsManager = (key) -> (next) -> rclient.get key, (err, val) -> - if err + if err? return next(err) - if val == null + if !val? logger.log {user_id: user._id, key}, ">> removing key from UserSessions set" rclient.srem sessionSetKey, key, (err, result) -> - if err + if err? return next(err) return next(null) else diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index b5b49d1329..3b40ff6885 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -90,7 +90,7 @@ webRouter.use translations.setLangBasedOnDomainMiddlewear # Measure expiry from last request, not last login webRouter.use (req, res, next) -> req.session.touch() - if req?.session?.user + if req?.session?.user? UserSessionsManager.touch(req.session.user, (err)->) next() From 0e9beb5653bcc6a8c06924fb4480f146a42a74ca Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 7 Jul 2016 09:42:01 +0100 Subject: [PATCH 66/70] remove daft redundancy. --- .../web/app/coffee/Features/User/UserSessionsManager.coffee | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index 30a9d7471a..95974ec59a 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -43,9 +43,6 @@ module.exports = UserSessionsManager = logger.log {user_id: user._id}, "no sessionId to untrack, returning" return callback(null) logger.log {user_id: user._id, sessionId}, "onLogout handler" - if !user - logger.log {sessionId}, "no user, for some reason" - return callback() sessionSetKey = UserSessionsManager._sessionSetKey(user) value = UserSessionsManager._sessionKey sessionId rclient.multi() From 07cce99c176184ebe7572d30c4dfadc0f28e3463 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 8 Jul 2016 10:06:46 +0100 Subject: [PATCH 67/70] Add sampled tracking method. --- .../public/coffee/ide/pdf/controllers/PdfController.coffee | 4 +++- services/web/public/coffee/main/event.coffee | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 415d2f6aad..3f0d15e218 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -267,7 +267,9 @@ define [ return path $scope.recompile = (options = {}) -> - event_tracking.sendCountly "editor-recompile", options + # event_tracking.sendCountly "editor-recompile", options + + event_tracking.sendCountlySampled "editor-recompile", options return if $scope.pdf.compiling diff --git a/services/web/public/coffee/main/event.coffee b/services/web/public/coffee/main/event.coffee index 58e43f3b9f..68decfd5ee 100644 --- a/services/web/public/coffee/main/event.coffee +++ b/services/web/public/coffee/main/event.coffee @@ -11,6 +11,9 @@ define [ eventData = { key } eventData.segmentation = segmentation if segmentation? Countly?.q.push([ "add_event", eventData ]); + + sendCountlySampled: (key, segmentation) -> + @sendCountly key, segmentation if Math.random() < .01 } # App.directive "countlyTrack", () -> From dfe9e05abe7d1be9d0230bae9bfa9e58b5d55e6e Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 8 Jul 2016 10:07:45 +0100 Subject: [PATCH 68/70] Use sampled tracking in editor recompiles. --- .../public/coffee/ide/pdf/controllers/PdfController.coffee | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 3f0d15e218..21ec07f7b8 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -267,12 +267,10 @@ define [ return path $scope.recompile = (options = {}) -> - # event_tracking.sendCountly "editor-recompile", options - - event_tracking.sendCountlySampled "editor-recompile", options - return if $scope.pdf.compiling + event_tracking.sendCountlySampled "editor-recompile", options + $scope.pdf.compiling = true ide.$scope.$broadcast("flush-changes") From c67ad84be8fa1cae776ec320c67a2f863a488adf Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 8 Jul 2016 10:26:40 +0100 Subject: [PATCH 69/70] Track positive and negative feedback as different events. --- .../public/coffee/ide/pdf/controllers/PdfController.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 415d2f6aad..ee6039c61f 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -36,8 +36,8 @@ define [ event_tracking.sendCountly "logs-hints-learn-more" trackLogHintsFeedback = (isPositive, hintId) -> - event_tracking.send 'log-hints', (if isPositive then 'feedback-positive' else 'feedback-negative'), hintId - event_tracking.sendCountly "log-hints-feedback", { isPositive, hintId } + event_tracking.send "log-hints", (if isPositive then "feedback-positive" else "feedback-negative"), hintId + event_tracking.sendCountly (if isPositive then "log-hints-feedback-positive" else "log-hints-feedback-negative"), { hintId } $scope.trackLogHintsPositiveFeedback = (hintId) -> trackLogHintsFeedback true, hintId $scope.trackLogHintsNegativeFeedback = (hintId) -> trackLogHintsFeedback false, hintId From e1fb4a51c634489716dc8e2697ffb64486399ffd Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 8 Jul 2016 11:17:18 +0100 Subject: [PATCH 70/70] Identify sampled events. --- .../web/public/coffee/ide/pdf/controllers/PdfController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 5cae91ffd2..635db982c9 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -269,7 +269,7 @@ define [ $scope.recompile = (options = {}) -> return if $scope.pdf.compiling - event_tracking.sendCountlySampled "editor-recompile", options + event_tracking.sendCountlySampled "editor-recompile-sampled", options $scope.pdf.compiling = true